Centralize nsd associated files
Move all nsd associated files include framework, service and test
to package/Nsd first. After clear all hidden API dependencies, we
can easily migrate these files into connectivity mainline module.
Bug: 206893064
Test: atest FrameworksNetTests CtsNetTestCases
Change-Id: Id26f471a10fe7882e31ccc2dbb73219461f00dbd
diff --git a/packages/Nsd/service/Android.bp b/packages/Nsd/service/Android.bp
new file mode 100644
index 0000000..529f58d
--- /dev/null
+++ b/packages/Nsd/service/Android.bp
@@ -0,0 +1,31 @@
+//
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+ // See: http://go/android-license-faq
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+filegroup {
+ name: "services.connectivity-nsd-sources",
+ srcs: [
+ "src/**/*.java",
+ ],
+ path: "src",
+ visibility: [
+ "//frameworks/base/services/core",
+ ],
+}
diff --git a/packages/Nsd/service/src/com/android/server/INativeDaemonConnectorCallbacks.java b/packages/Nsd/service/src/com/android/server/INativeDaemonConnectorCallbacks.java
new file mode 100644
index 0000000..0cf9dcd
--- /dev/null
+++ b/packages/Nsd/service/src/com/android/server/INativeDaemonConnectorCallbacks.java
@@ -0,0 +1,25 @@
+
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server;
+
+interface INativeDaemonConnectorCallbacks {
+
+ void onDaemonConnected();
+ boolean onCheckHoldWakeLock(int code);
+ boolean onEvent(int code, String raw, String[] cooked);
+}
diff --git a/packages/Nsd/service/src/com/android/server/NativeDaemonConnector.java b/packages/Nsd/service/src/com/android/server/NativeDaemonConnector.java
new file mode 100644
index 0000000..eac767f
--- /dev/null
+++ b/packages/Nsd/service/src/com/android/server/NativeDaemonConnector.java
@@ -0,0 +1,723 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server;
+
+import android.net.LocalSocket;
+import android.net.LocalSocketAddress;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.util.LocalLog;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.Preconditions;
+import com.android.server.power.ShutdownThread;
+import com.google.android.collect.Lists;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.LinkedList;
+import java.util.Objects;
+
+/**
+ * Generic connector class for interfacing with a native daemon which uses the
+ * {@code libsysutils} FrameworkListener protocol.
+ */
+final class NativeDaemonConnector implements Runnable, Handler.Callback, Watchdog.Monitor {
+ private final static boolean VDBG = false;
+
+ private final String TAG;
+
+ private String mSocket;
+ private OutputStream mOutputStream;
+ private LocalLog mLocalLog;
+
+ private volatile boolean mDebug = false;
+ private volatile Object mWarnIfHeld;
+
+ private final ResponseQueue mResponseQueue;
+
+ private final PowerManager.WakeLock mWakeLock;
+
+ private final Looper mLooper;
+
+ private INativeDaemonConnectorCallbacks mCallbacks;
+ private Handler mCallbackHandler;
+
+ private AtomicInteger mSequenceNumber;
+
+ private static final long DEFAULT_TIMEOUT = 1 * 60 * 1000; /* 1 minute */
+ private static final long WARN_EXECUTE_DELAY_MS = 500; /* .5 sec */
+
+ /** Lock held whenever communicating with native daemon. */
+ private final Object mDaemonLock = new Object();
+
+ private final int BUFFER_SIZE = 4096;
+
+ NativeDaemonConnector(INativeDaemonConnectorCallbacks callbacks, String socket,
+ int responseQueueSize, String logTag, int maxLogSize, PowerManager.WakeLock wl) {
+ this(callbacks, socket, responseQueueSize, logTag, maxLogSize, wl,
+ FgThread.get().getLooper());
+ }
+
+ NativeDaemonConnector(INativeDaemonConnectorCallbacks callbacks, String socket,
+ int responseQueueSize, String logTag, int maxLogSize, PowerManager.WakeLock wl,
+ Looper looper) {
+ mCallbacks = callbacks;
+ mSocket = socket;
+ mResponseQueue = new ResponseQueue(responseQueueSize);
+ mWakeLock = wl;
+ if (mWakeLock != null) {
+ mWakeLock.setReferenceCounted(true);
+ }
+ mLooper = looper;
+ mSequenceNumber = new AtomicInteger(0);
+ TAG = logTag != null ? logTag : "NativeDaemonConnector";
+ mLocalLog = new LocalLog(maxLogSize);
+ }
+
+ /**
+ * Enable Set debugging mode, which causes messages to also be written to both
+ * {@link Slog} in addition to internal log.
+ */
+ public void setDebug(boolean debug) {
+ mDebug = debug;
+ }
+
+ /**
+ * Like SystemClock.uptimeMillis, except truncated to an int so it will fit in a message arg.
+ * Inaccurate across 49.7 days of uptime, but only used for debugging.
+ */
+ private int uptimeMillisInt() {
+ return (int) SystemClock.uptimeMillis() & Integer.MAX_VALUE;
+ }
+
+ /**
+ * Yell loudly if someone tries making future {@link #execute(Command)}
+ * calls while holding a lock on the given object.
+ */
+ public void setWarnIfHeld(Object warnIfHeld) {
+ Preconditions.checkState(mWarnIfHeld == null);
+ mWarnIfHeld = Objects.requireNonNull(warnIfHeld);
+ }
+
+ @Override
+ public void run() {
+ mCallbackHandler = new Handler(mLooper, this);
+
+ while (true) {
+ if (isShuttingDown()) break;
+ try {
+ listenToSocket();
+ } catch (Exception e) {
+ loge("Error in NativeDaemonConnector: " + e);
+ if (isShuttingDown()) break;
+ SystemClock.sleep(5000);
+ }
+ }
+ }
+
+ private static boolean isShuttingDown() {
+ String shutdownAct = SystemProperties.get(
+ ShutdownThread.SHUTDOWN_ACTION_PROPERTY, "");
+ return shutdownAct != null && shutdownAct.length() > 0;
+ }
+
+ @Override
+ public boolean handleMessage(Message msg) {
+ final String event = (String) msg.obj;
+ final int start = uptimeMillisInt();
+ final int sent = msg.arg1;
+ try {
+ if (!mCallbacks.onEvent(msg.what, event, NativeDaemonEvent.unescapeArgs(event))) {
+ log(String.format("Unhandled event '%s'", event));
+ }
+ } catch (Exception e) {
+ loge("Error handling '" + event + "': " + e);
+ } finally {
+ if (mCallbacks.onCheckHoldWakeLock(msg.what) && mWakeLock != null) {
+ mWakeLock.release();
+ }
+ final int end = uptimeMillisInt();
+ if (start > sent && start - sent > WARN_EXECUTE_DELAY_MS) {
+ loge(String.format("NDC event {%s} processed too late: %dms", event, start - sent));
+ }
+ if (end > start && end - start > WARN_EXECUTE_DELAY_MS) {
+ loge(String.format("NDC event {%s} took too long: %dms", event, end - start));
+ }
+ }
+ return true;
+ }
+
+ private LocalSocketAddress determineSocketAddress() {
+ // If we're testing, set up a socket in a namespace that's accessible to test code.
+ // In order to ensure that unprivileged apps aren't able to impersonate native daemons on
+ // production devices, even if said native daemons ill-advisedly pick a socket name that
+ // starts with __test__, only allow this on debug builds.
+ if (mSocket.startsWith("__test__") && Build.IS_DEBUGGABLE) {
+ return new LocalSocketAddress(mSocket);
+ } else {
+ return new LocalSocketAddress(mSocket, LocalSocketAddress.Namespace.RESERVED);
+ }
+ }
+
+ private void listenToSocket() throws IOException {
+ LocalSocket socket = null;
+
+ try {
+ socket = new LocalSocket();
+ LocalSocketAddress address = determineSocketAddress();
+
+ socket.connect(address);
+
+ InputStream inputStream = socket.getInputStream();
+ synchronized (mDaemonLock) {
+ mOutputStream = socket.getOutputStream();
+ }
+
+ mCallbacks.onDaemonConnected();
+
+ FileDescriptor[] fdList = null;
+ byte[] buffer = new byte[BUFFER_SIZE];
+ int start = 0;
+
+ while (true) {
+ int count = inputStream.read(buffer, start, BUFFER_SIZE - start);
+ if (count < 0) {
+ loge("got " + count + " reading with start = " + start);
+ break;
+ }
+ fdList = socket.getAncillaryFileDescriptors();
+
+ // Add our starting point to the count and reset the start.
+ count += start;
+ start = 0;
+
+ for (int i = 0; i < count; i++) {
+ if (buffer[i] == 0) {
+ // Note - do not log this raw message since it may contain
+ // sensitive data
+ final String rawEvent = new String(
+ buffer, start, i - start, StandardCharsets.UTF_8);
+
+ boolean releaseWl = false;
+ try {
+ final NativeDaemonEvent event =
+ NativeDaemonEvent.parseRawEvent(rawEvent, fdList);
+
+ log("RCV <- {" + event + "}");
+
+ if (event.isClassUnsolicited()) {
+ // TODO: migrate to sending NativeDaemonEvent instances
+ if (mCallbacks.onCheckHoldWakeLock(event.getCode())
+ && mWakeLock != null) {
+ mWakeLock.acquire();
+ releaseWl = true;
+ }
+ Message msg = mCallbackHandler.obtainMessage(
+ event.getCode(), uptimeMillisInt(), 0, event.getRawEvent());
+ if (mCallbackHandler.sendMessage(msg)) {
+ releaseWl = false;
+ }
+ } else {
+ mResponseQueue.add(event.getCmdNumber(), event);
+ }
+ } catch (IllegalArgumentException e) {
+ log("Problem parsing message " + e);
+ } finally {
+ if (releaseWl) {
+ mWakeLock.release();
+ }
+ }
+
+ start = i + 1;
+ }
+ }
+
+ if (start == 0) {
+ log("RCV incomplete");
+ }
+
+ // We should end at the amount we read. If not, compact then
+ // buffer and read again.
+ if (start != count) {
+ final int remaining = BUFFER_SIZE - start;
+ System.arraycopy(buffer, start, buffer, 0, remaining);
+ start = remaining;
+ } else {
+ start = 0;
+ }
+ }
+ } catch (IOException ex) {
+ loge("Communications error: " + ex);
+ throw ex;
+ } finally {
+ synchronized (mDaemonLock) {
+ if (mOutputStream != null) {
+ try {
+ loge("closing stream for " + mSocket);
+ mOutputStream.close();
+ } catch (IOException e) {
+ loge("Failed closing output stream: " + e);
+ }
+ mOutputStream = null;
+ }
+ }
+
+ try {
+ if (socket != null) {
+ socket.close();
+ }
+ } catch (IOException ex) {
+ loge("Failed closing socket: " + ex);
+ }
+ }
+ }
+
+ /**
+ * Wrapper around argument that indicates it's sensitive and shouldn't be
+ * logged.
+ */
+ public static class SensitiveArg {
+ private final Object mArg;
+
+ public SensitiveArg(Object arg) {
+ mArg = arg;
+ }
+
+ @Override
+ public String toString() {
+ return String.valueOf(mArg);
+ }
+ }
+
+ /**
+ * Make command for daemon, escaping arguments as needed.
+ */
+ @VisibleForTesting
+ static void makeCommand(StringBuilder rawBuilder, StringBuilder logBuilder, int sequenceNumber,
+ String cmd, Object... args) {
+ if (cmd.indexOf('\0') >= 0) {
+ throw new IllegalArgumentException("Unexpected command: " + cmd);
+ }
+ if (cmd.indexOf(' ') >= 0) {
+ throw new IllegalArgumentException("Arguments must be separate from command");
+ }
+
+ rawBuilder.append(sequenceNumber).append(' ').append(cmd);
+ logBuilder.append(sequenceNumber).append(' ').append(cmd);
+ for (Object arg : args) {
+ final String argString = String.valueOf(arg);
+ if (argString.indexOf('\0') >= 0) {
+ throw new IllegalArgumentException("Unexpected argument: " + arg);
+ }
+
+ rawBuilder.append(' ');
+ logBuilder.append(' ');
+
+ appendEscaped(rawBuilder, argString);
+ if (arg instanceof SensitiveArg) {
+ logBuilder.append("[scrubbed]");
+ } else {
+ appendEscaped(logBuilder, argString);
+ }
+ }
+
+ rawBuilder.append('\0');
+ }
+
+ /**
+ * Method that waits until all asychronous notifications sent by the native daemon have
+ * been processed. This method must not be called on the notification thread or an
+ * exception will be thrown.
+ */
+ public void waitForCallbacks() {
+ if (Thread.currentThread() == mLooper.getThread()) {
+ throw new IllegalStateException("Must not call this method on callback thread");
+ }
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ mCallbackHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ latch.countDown();
+ }
+ });
+ try {
+ latch.await();
+ } catch (InterruptedException e) {
+ Slog.wtf(TAG, "Interrupted while waiting for unsolicited response handling", e);
+ }
+ }
+
+ /**
+ * Issue the given command to the native daemon and return a single expected
+ * response.
+ *
+ * @throws NativeDaemonConnectorException when problem communicating with
+ * native daemon, or if the response matches
+ * {@link NativeDaemonEvent#isClassClientError()} or
+ * {@link NativeDaemonEvent#isClassServerError()}.
+ */
+ public NativeDaemonEvent execute(Command cmd) throws NativeDaemonConnectorException {
+ return execute(cmd.mCmd, cmd.mArguments.toArray());
+ }
+
+ /**
+ * Issue the given command to the native daemon and return a single expected
+ * response. Any arguments must be separated from base command so they can
+ * be properly escaped.
+ *
+ * @throws NativeDaemonConnectorException when problem communicating with
+ * native daemon, or if the response matches
+ * {@link NativeDaemonEvent#isClassClientError()} or
+ * {@link NativeDaemonEvent#isClassServerError()}.
+ */
+ public NativeDaemonEvent execute(String cmd, Object... args)
+ throws NativeDaemonConnectorException {
+ return execute(DEFAULT_TIMEOUT, cmd, args);
+ }
+
+ public NativeDaemonEvent execute(long timeoutMs, String cmd, Object... args)
+ throws NativeDaemonConnectorException {
+ final NativeDaemonEvent[] events = executeForList(timeoutMs, cmd, args);
+ if (events.length != 1) {
+ throw new NativeDaemonConnectorException(
+ "Expected exactly one response, but received " + events.length);
+ }
+ return events[0];
+ }
+
+ /**
+ * Issue the given command to the native daemon and return any
+ * {@link NativeDaemonEvent#isClassContinue()} responses, including the
+ * final terminal response.
+ *
+ * @throws NativeDaemonConnectorException when problem communicating with
+ * native daemon, or if the response matches
+ * {@link NativeDaemonEvent#isClassClientError()} or
+ * {@link NativeDaemonEvent#isClassServerError()}.
+ */
+ public NativeDaemonEvent[] executeForList(Command cmd) throws NativeDaemonConnectorException {
+ return executeForList(cmd.mCmd, cmd.mArguments.toArray());
+ }
+
+ /**
+ * Issue the given command to the native daemon and return any
+ * {@link NativeDaemonEvent#isClassContinue()} responses, including the
+ * final terminal response. Any arguments must be separated from base
+ * command so they can be properly escaped.
+ *
+ * @throws NativeDaemonConnectorException when problem communicating with
+ * native daemon, or if the response matches
+ * {@link NativeDaemonEvent#isClassClientError()} or
+ * {@link NativeDaemonEvent#isClassServerError()}.
+ */
+ public NativeDaemonEvent[] executeForList(String cmd, Object... args)
+ throws NativeDaemonConnectorException {
+ return executeForList(DEFAULT_TIMEOUT, cmd, args);
+ }
+
+ /**
+ * Issue the given command to the native daemon and return any {@linke
+ * NativeDaemonEvent@isClassContinue()} responses, including the final
+ * terminal response. Note that the timeout does not count time in deep
+ * sleep. Any arguments must be separated from base command so they can be
+ * properly escaped.
+ *
+ * @throws NativeDaemonConnectorException when problem communicating with
+ * native daemon, or if the response matches
+ * {@link NativeDaemonEvent#isClassClientError()} or
+ * {@link NativeDaemonEvent#isClassServerError()}.
+ */
+ public NativeDaemonEvent[] executeForList(long timeoutMs, String cmd, Object... args)
+ throws NativeDaemonConnectorException {
+ if (mWarnIfHeld != null && Thread.holdsLock(mWarnIfHeld)) {
+ Slog.wtf(TAG, "Calling thread " + Thread.currentThread().getName() + " is holding 0x"
+ + Integer.toHexString(System.identityHashCode(mWarnIfHeld)), new Throwable());
+ }
+
+ final long startTime = SystemClock.elapsedRealtime();
+
+ final ArrayList<NativeDaemonEvent> events = Lists.newArrayList();
+
+ final StringBuilder rawBuilder = new StringBuilder();
+ final StringBuilder logBuilder = new StringBuilder();
+ final int sequenceNumber = mSequenceNumber.incrementAndGet();
+
+ makeCommand(rawBuilder, logBuilder, sequenceNumber, cmd, args);
+
+ final String rawCmd = rawBuilder.toString();
+ final String logCmd = logBuilder.toString();
+
+ log("SND -> {" + logCmd + "}");
+
+ synchronized (mDaemonLock) {
+ if (mOutputStream == null) {
+ throw new NativeDaemonConnectorException("missing output stream");
+ } else {
+ try {
+ mOutputStream.write(rawCmd.getBytes(StandardCharsets.UTF_8));
+ } catch (IOException e) {
+ throw new NativeDaemonConnectorException("problem sending command", e);
+ }
+ }
+ }
+
+ NativeDaemonEvent event = null;
+ do {
+ event = mResponseQueue.remove(sequenceNumber, timeoutMs, logCmd);
+ if (event == null) {
+ loge("timed-out waiting for response to " + logCmd);
+ throw new NativeDaemonTimeoutException(logCmd, event);
+ }
+ if (VDBG) log("RMV <- {" + event + "}");
+ events.add(event);
+ } while (event.isClassContinue());
+
+ final long endTime = SystemClock.elapsedRealtime();
+ if (endTime - startTime > WARN_EXECUTE_DELAY_MS) {
+ loge("NDC Command {" + logCmd + "} took too long (" + (endTime - startTime) + "ms)");
+ }
+
+ if (event.isClassClientError()) {
+ throw new NativeDaemonArgumentException(logCmd, event);
+ }
+ if (event.isClassServerError()) {
+ throw new NativeDaemonFailureException(logCmd, event);
+ }
+
+ return events.toArray(new NativeDaemonEvent[events.size()]);
+ }
+
+ /**
+ * Append the given argument to {@link StringBuilder}, escaping as needed,
+ * and surrounding with quotes when it contains spaces.
+ */
+ @VisibleForTesting
+ static void appendEscaped(StringBuilder builder, String arg) {
+ final boolean hasSpaces = arg.indexOf(' ') >= 0;
+ if (hasSpaces) {
+ builder.append('"');
+ }
+
+ final int length = arg.length();
+ for (int i = 0; i < length; i++) {
+ final char c = arg.charAt(i);
+
+ if (c == '"') {
+ builder.append("\\\"");
+ } else if (c == '\\') {
+ builder.append("\\\\");
+ } else {
+ builder.append(c);
+ }
+ }
+
+ if (hasSpaces) {
+ builder.append('"');
+ }
+ }
+
+ private static class NativeDaemonArgumentException extends NativeDaemonConnectorException {
+ public NativeDaemonArgumentException(String command, NativeDaemonEvent event) {
+ super(command, event);
+ }
+
+ @Override
+ public IllegalArgumentException rethrowAsParcelableException() {
+ throw new IllegalArgumentException(getMessage(), this);
+ }
+ }
+
+ private static class NativeDaemonFailureException extends NativeDaemonConnectorException {
+ public NativeDaemonFailureException(String command, NativeDaemonEvent event) {
+ super(command, event);
+ }
+ }
+
+ /**
+ * Command builder that handles argument list building. Any arguments must
+ * be separated from base command so they can be properly escaped.
+ */
+ public static class Command {
+ private String mCmd;
+ private ArrayList<Object> mArguments = Lists.newArrayList();
+
+ public Command(String cmd, Object... args) {
+ mCmd = cmd;
+ for (Object arg : args) {
+ appendArg(arg);
+ }
+ }
+
+ public Command appendArg(Object arg) {
+ mArguments.add(arg);
+ return this;
+ }
+ }
+
+ /** {@inheritDoc} */
+ public void monitor() {
+ synchronized (mDaemonLock) { }
+ }
+
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ mLocalLog.dump(fd, pw, args);
+ pw.println();
+ mResponseQueue.dump(fd, pw, args);
+ }
+
+ private void log(String logstring) {
+ if (mDebug) Slog.d(TAG, logstring);
+ mLocalLog.log(logstring);
+ }
+
+ private void loge(String logstring) {
+ Slog.e(TAG, logstring);
+ mLocalLog.log(logstring);
+ }
+
+ private static class ResponseQueue {
+
+ private static class PendingCmd {
+ public final int cmdNum;
+ public final String logCmd;
+
+ public BlockingQueue<NativeDaemonEvent> responses =
+ new ArrayBlockingQueue<NativeDaemonEvent>(10);
+
+ // The availableResponseCount member is used to track when we can remove this
+ // instance from the ResponseQueue.
+ // This is used under the protection of a sync of the mPendingCmds object.
+ // A positive value means we've had more writers retreive this object while
+ // a negative value means we've had more readers. When we've had an equal number
+ // (it goes to zero) we can remove this object from the mPendingCmds list.
+ // Note that we may have more responses for this command (and more readers
+ // coming), but that would result in a new PendingCmd instance being created
+ // and added with the same cmdNum.
+ // Also note that when this goes to zero it just means a parity of readers and
+ // writers have retrieved this object - not that they are done using it. The
+ // responses queue may well have more responses yet to be read or may get more
+ // responses added to it. But all those readers/writers have retreived and
+ // hold references to this instance already so it can be removed from
+ // mPendingCmds queue.
+ public int availableResponseCount;
+
+ public PendingCmd(int cmdNum, String logCmd) {
+ this.cmdNum = cmdNum;
+ this.logCmd = logCmd;
+ }
+ }
+
+ private final LinkedList<PendingCmd> mPendingCmds;
+ private int mMaxCount;
+
+ ResponseQueue(int maxCount) {
+ mPendingCmds = new LinkedList<PendingCmd>();
+ mMaxCount = maxCount;
+ }
+
+ public void add(int cmdNum, NativeDaemonEvent response) {
+ PendingCmd found = null;
+ synchronized (mPendingCmds) {
+ for (PendingCmd pendingCmd : mPendingCmds) {
+ if (pendingCmd.cmdNum == cmdNum) {
+ found = pendingCmd;
+ break;
+ }
+ }
+ if (found == null) {
+ // didn't find it - make sure our queue isn't too big before adding
+ while (mPendingCmds.size() >= mMaxCount) {
+ Slog.e("NativeDaemonConnector.ResponseQueue",
+ "more buffered than allowed: " + mPendingCmds.size() +
+ " >= " + mMaxCount);
+ // let any waiter timeout waiting for this
+ PendingCmd pendingCmd = mPendingCmds.remove();
+ Slog.e("NativeDaemonConnector.ResponseQueue",
+ "Removing request: " + pendingCmd.logCmd + " (" +
+ pendingCmd.cmdNum + ")");
+ }
+ found = new PendingCmd(cmdNum, null);
+ mPendingCmds.add(found);
+ }
+ found.availableResponseCount++;
+ // if a matching remove call has already retrieved this we can remove this
+ // instance from our list
+ if (found.availableResponseCount == 0) mPendingCmds.remove(found);
+ }
+ try {
+ found.responses.put(response);
+ } catch (InterruptedException e) { }
+ }
+
+ // note that the timeout does not count time in deep sleep. If you don't want
+ // the device to sleep, hold a wakelock
+ public NativeDaemonEvent remove(int cmdNum, long timeoutMs, String logCmd) {
+ PendingCmd found = null;
+ synchronized (mPendingCmds) {
+ for (PendingCmd pendingCmd : mPendingCmds) {
+ if (pendingCmd.cmdNum == cmdNum) {
+ found = pendingCmd;
+ break;
+ }
+ }
+ if (found == null) {
+ found = new PendingCmd(cmdNum, logCmd);
+ mPendingCmds.add(found);
+ }
+ found.availableResponseCount--;
+ // if a matching add call has already retrieved this we can remove this
+ // instance from our list
+ if (found.availableResponseCount == 0) mPendingCmds.remove(found);
+ }
+ NativeDaemonEvent result = null;
+ try {
+ result = found.responses.poll(timeoutMs, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {}
+ if (result == null) {
+ Slog.e("NativeDaemonConnector.ResponseQueue", "Timeout waiting for response");
+ }
+ return result;
+ }
+
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ pw.println("Pending requests:");
+ synchronized (mPendingCmds) {
+ for (PendingCmd pendingCmd : mPendingCmds) {
+ pw.println(" Cmd " + pendingCmd.cmdNum + " - " + pendingCmd.logCmd);
+ }
+ }
+ }
+ }
+}
diff --git a/packages/Nsd/service/src/com/android/server/NativeDaemonConnectorException.java b/packages/Nsd/service/src/com/android/server/NativeDaemonConnectorException.java
new file mode 100644
index 0000000..4d8881c
--- /dev/null
+++ b/packages/Nsd/service/src/com/android/server/NativeDaemonConnectorException.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server;
+
+import android.os.Parcel;
+
+/**
+ * An exception that indicates there was an error with a
+ * {@link NativeDaemonConnector} operation.
+ */
+public class NativeDaemonConnectorException extends Exception {
+ private String mCmd;
+ private NativeDaemonEvent mEvent;
+
+ public NativeDaemonConnectorException(String detailMessage) {
+ super(detailMessage);
+ }
+
+ public NativeDaemonConnectorException(String detailMessage, Throwable throwable) {
+ super(detailMessage, throwable);
+ }
+
+ public NativeDaemonConnectorException(String cmd, NativeDaemonEvent event) {
+ super("command '" + cmd + "' failed with '" + event + "'");
+ mCmd = cmd;
+ mEvent = event;
+ }
+
+ public int getCode() {
+ return mEvent != null ? mEvent.getCode() : -1;
+ }
+
+ public String getCmd() {
+ return mCmd;
+ }
+
+ /**
+ * Rethrow as a {@link RuntimeException} subclass that is handled by
+ * {@link Parcel#writeException(Exception)}.
+ */
+ public IllegalArgumentException rethrowAsParcelableException() {
+ throw new IllegalStateException(getMessage(), this);
+ }
+}
diff --git a/packages/Nsd/service/src/com/android/server/NativeDaemonEvent.java b/packages/Nsd/service/src/com/android/server/NativeDaemonEvent.java
new file mode 100644
index 0000000..e6feda3
--- /dev/null
+++ b/packages/Nsd/service/src/com/android/server/NativeDaemonEvent.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server;
+
+import android.util.Slog;
+import com.google.android.collect.Lists;
+
+import java.io.FileDescriptor;
+import java.util.ArrayList;
+
+/**
+ * Parsed event from native side of {@link NativeDaemonConnector}.
+ */
+public class NativeDaemonEvent {
+
+ // TODO: keep class ranges in sync with ResponseCode.h
+ // TODO: swap client and server error ranges to roughly mirror HTTP spec
+
+ private final int mCmdNumber;
+ private final int mCode;
+ private final String mMessage;
+ private final String mRawEvent;
+ private final String mLogMessage;
+ private String[] mParsed;
+ private FileDescriptor[] mFdList;
+
+ private NativeDaemonEvent(int cmdNumber, int code, String message,
+ String rawEvent, String logMessage, FileDescriptor[] fdList) {
+ mCmdNumber = cmdNumber;
+ mCode = code;
+ mMessage = message;
+ mRawEvent = rawEvent;
+ mLogMessage = logMessage;
+ mParsed = null;
+ mFdList = fdList;
+ }
+
+ static public final String SENSITIVE_MARKER = "{{sensitive}}";
+
+ public int getCmdNumber() {
+ return mCmdNumber;
+ }
+
+ public int getCode() {
+ return mCode;
+ }
+
+ public String getMessage() {
+ return mMessage;
+ }
+
+ public FileDescriptor[] getFileDescriptors() {
+ return mFdList;
+ }
+
+ @Deprecated
+ public String getRawEvent() {
+ return mRawEvent;
+ }
+
+ @Override
+ public String toString() {
+ return mLogMessage;
+ }
+
+ /**
+ * Test if event represents a partial response which is continued in
+ * additional subsequent events.
+ */
+ public boolean isClassContinue() {
+ return mCode >= 100 && mCode < 200;
+ }
+
+ /**
+ * Test if event represents a command success.
+ */
+ public boolean isClassOk() {
+ return mCode >= 200 && mCode < 300;
+ }
+
+ /**
+ * Test if event represents a remote native daemon error.
+ */
+ public boolean isClassServerError() {
+ return mCode >= 400 && mCode < 500;
+ }
+
+ /**
+ * Test if event represents a command syntax or argument error.
+ */
+ public boolean isClassClientError() {
+ return mCode >= 500 && mCode < 600;
+ }
+
+ /**
+ * Test if event represents an unsolicited event from native daemon.
+ */
+ public boolean isClassUnsolicited() {
+ return isClassUnsolicited(mCode);
+ }
+
+ private static boolean isClassUnsolicited(int code) {
+ return code >= 600 && code < 700;
+ }
+
+ /**
+ * Verify this event matches the given code.
+ *
+ * @throws IllegalStateException if {@link #getCode()} doesn't match.
+ */
+ public void checkCode(int code) {
+ if (mCode != code) {
+ throw new IllegalStateException("Expected " + code + " but was: " + this);
+ }
+ }
+
+ /**
+ * Parse the given raw event into {@link NativeDaemonEvent} instance.
+ *
+ * @throws IllegalArgumentException when line doesn't match format expected
+ * from native side.
+ */
+ public static NativeDaemonEvent parseRawEvent(String rawEvent, FileDescriptor[] fdList) {
+ final String[] parsed = rawEvent.split(" ");
+ if (parsed.length < 2) {
+ throw new IllegalArgumentException("Insufficient arguments");
+ }
+
+ int skiplength = 0;
+
+ final int code;
+ try {
+ code = Integer.parseInt(parsed[0]);
+ skiplength = parsed[0].length() + 1;
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException("problem parsing code", e);
+ }
+
+ int cmdNumber = -1;
+ if (isClassUnsolicited(code) == false) {
+ if (parsed.length < 3) {
+ throw new IllegalArgumentException("Insufficient arguemnts");
+ }
+ try {
+ cmdNumber = Integer.parseInt(parsed[1]);
+ skiplength += parsed[1].length() + 1;
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException("problem parsing cmdNumber", e);
+ }
+ }
+
+ String logMessage = rawEvent;
+ if (parsed.length > 2 && parsed[2].equals(SENSITIVE_MARKER)) {
+ skiplength += parsed[2].length() + 1;
+ logMessage = parsed[0] + " " + parsed[1] + " {}";
+ }
+
+ final String message = rawEvent.substring(skiplength);
+
+ return new NativeDaemonEvent(cmdNumber, code, message, rawEvent, logMessage, fdList);
+ }
+
+ /**
+ * Filter the given {@link NativeDaemonEvent} list, returning
+ * {@link #getMessage()} for any events matching the requested code.
+ */
+ public static String[] filterMessageList(NativeDaemonEvent[] events, int matchCode) {
+ final ArrayList<String> result = Lists.newArrayList();
+ for (NativeDaemonEvent event : events) {
+ if (event.getCode() == matchCode) {
+ result.add(event.getMessage());
+ }
+ }
+ return result.toArray(new String[result.size()]);
+ }
+
+ /**
+ * Find the Nth field of the event.
+ *
+ * This ignores and code or cmdNum, the first return value is given for N=0.
+ * Also understands "\"quoted\" multiword responses" and tries them as a single field
+ */
+ public String getField(int n) {
+ if (mParsed == null) {
+ mParsed = unescapeArgs(mRawEvent);
+ }
+ n += 2; // skip code and command#
+ if (n > mParsed.length) return null;
+ return mParsed[n];
+ }
+
+ public static String[] unescapeArgs(String rawEvent) {
+ final boolean DEBUG_ROUTINE = false;
+ final String LOGTAG = "unescapeArgs";
+ final ArrayList<String> parsed = new ArrayList<String>();
+ final int length = rawEvent.length();
+ int current = 0;
+ int wordEnd = -1;
+ boolean quoted = false;
+
+ if (DEBUG_ROUTINE) Slog.e(LOGTAG, "parsing '" + rawEvent + "'");
+ if (rawEvent.charAt(current) == '\"') {
+ quoted = true;
+ current++;
+ }
+ while (current < length) {
+ // find the end of the word
+ char terminator = quoted ? '\"' : ' ';
+ wordEnd = current;
+ while (wordEnd < length && rawEvent.charAt(wordEnd) != terminator) {
+ if (rawEvent.charAt(wordEnd) == '\\') {
+ // skip the escaped char
+ ++wordEnd;
+ }
+ ++wordEnd;
+ }
+ if (wordEnd > length) wordEnd = length;
+ String word = rawEvent.substring(current, wordEnd);
+ current += word.length();
+ if (!quoted) {
+ word = word.trim();
+ } else {
+ current++; // skip the trailing quote
+ }
+ // unescape stuff within the word
+ word = word.replace("\\\\", "\\");
+ word = word.replace("\\\"", "\"");
+
+ if (DEBUG_ROUTINE) Slog.e(LOGTAG, "found '" + word + "'");
+ parsed.add(word);
+
+ // find the beginning of the next word - either of these options
+ int nextSpace = rawEvent.indexOf(' ', current);
+ int nextQuote = rawEvent.indexOf(" \"", current);
+ if (DEBUG_ROUTINE) {
+ Slog.e(LOGTAG, "nextSpace=" + nextSpace + ", nextQuote=" + nextQuote);
+ }
+ if (nextQuote > -1 && nextQuote <= nextSpace) {
+ quoted = true;
+ current = nextQuote + 2;
+ } else {
+ quoted = false;
+ if (nextSpace > -1) {
+ current = nextSpace + 1;
+ }
+ } // else we just start the next word after the current and read til the end
+ if (DEBUG_ROUTINE) {
+ Slog.e(LOGTAG, "next loop - current=" + current +
+ ", length=" + length + ", quoted=" + quoted);
+ }
+ }
+ return parsed.toArray(new String[parsed.size()]);
+ }
+}
diff --git a/packages/Nsd/service/src/com/android/server/NativeDaemonTimeoutException.java b/packages/Nsd/service/src/com/android/server/NativeDaemonTimeoutException.java
new file mode 100644
index 0000000..658f7d6
--- /dev/null
+++ b/packages/Nsd/service/src/com/android/server/NativeDaemonTimeoutException.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server;
+
+/**
+ * An exception that indicates there was a timeout with a
+ * {@link NativeDaemonConnector} operation.
+ */
+public class NativeDaemonTimeoutException extends NativeDaemonConnectorException {
+ public NativeDaemonTimeoutException(String command, NativeDaemonEvent event) {
+ super(command, event);
+ }
+}
+
diff --git a/packages/Nsd/service/src/com/android/server/NsdService.java b/packages/Nsd/service/src/com/android/server/NsdService.java
new file mode 100644
index 0000000..3e02084
--- /dev/null
+++ b/packages/Nsd/service/src/com/android/server/NsdService.java
@@ -0,0 +1,1103 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.net.nsd.INsdManager;
+import android.net.nsd.INsdManagerCallback;
+import android.net.nsd.INsdServiceConnector;
+import android.net.nsd.NsdManager;
+import android.net.nsd.NsdServiceInfo;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.Base64;
+import android.util.Log;
+import android.util.Pair;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.DumpUtils;
+import com.android.internal.util.State;
+import com.android.internal.util.StateMachine;
+import com.android.net.module.util.DnsSdTxtRecord;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.net.InetAddress;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * Network Service Discovery Service handles remote service discovery operation requests by
+ * implementing the INsdManager interface.
+ *
+ * @hide
+ */
+public class NsdService extends INsdManager.Stub {
+ private static final String TAG = "NsdService";
+ private static final String MDNS_TAG = "mDnsConnector";
+
+ private static final boolean DBG = true;
+ private static final long CLEANUP_DELAY_MS = 10000;
+
+ private final Context mContext;
+ private final NsdSettings mNsdSettings;
+ private final NsdStateMachine mNsdStateMachine;
+ private final DaemonConnection mDaemon;
+ private final NativeCallbackReceiver mDaemonCallback;
+
+ /**
+ * Clients receiving asynchronous messages
+ */
+ private final HashMap<NsdServiceConnector, ClientInfo> mClients = new HashMap<>();
+
+ /* A map from unique id to client info */
+ private final SparseArray<ClientInfo> mIdToClientInfoMap= new SparseArray<>();
+
+ private final long mCleanupDelayMs;
+
+ private static final int INVALID_ID = 0;
+ private int mUniqueId = 1;
+ // The count of the connected legacy clients.
+ private int mLegacyClientCount = 0;
+
+ private class NsdStateMachine extends StateMachine {
+
+ private final DefaultState mDefaultState = new DefaultState();
+ private final DisabledState mDisabledState = new DisabledState();
+ private final EnabledState mEnabledState = new EnabledState();
+
+ @Override
+ protected String getWhatToString(int what) {
+ return NsdManager.nameOf(what);
+ }
+
+ private void maybeStartDaemon() {
+ mDaemon.maybeStart();
+ maybeScheduleStop();
+ }
+
+ private boolean isAnyRequestActive() {
+ return mIdToClientInfoMap.size() != 0;
+ }
+
+ private void scheduleStop() {
+ sendMessageDelayed(NsdManager.DAEMON_CLEANUP, mCleanupDelayMs);
+ }
+ private void maybeScheduleStop() {
+ // The native daemon should stay alive and can't be cleanup
+ // if any legacy client connected.
+ if (!isAnyRequestActive() && mLegacyClientCount == 0) {
+ scheduleStop();
+ }
+ }
+
+ private void cancelStop() {
+ this.removeMessages(NsdManager.DAEMON_CLEANUP);
+ }
+
+ /**
+ * Observes the NSD on/off setting, and takes action when changed.
+ */
+ private void registerForNsdSetting() {
+ final ContentObserver contentObserver = new ContentObserver(this.getHandler()) {
+ @Override
+ public void onChange(boolean selfChange) {
+ notifyEnabled(isNsdEnabled());
+ }
+ };
+
+ final Uri uri = Settings.Global.getUriFor(Settings.Global.NSD_ON);
+ mNsdSettings.registerContentObserver(uri, contentObserver);
+ }
+
+ NsdStateMachine(String name, Handler handler) {
+ super(name, handler);
+ addState(mDefaultState);
+ addState(mDisabledState, mDefaultState);
+ addState(mEnabledState, mDefaultState);
+ State initialState = isNsdEnabled() ? mEnabledState : mDisabledState;
+ setInitialState(initialState);
+ setLogRecSize(25);
+ registerForNsdSetting();
+ }
+
+ class DefaultState extends State {
+ @Override
+ public boolean processMessage(Message msg) {
+ final ClientInfo cInfo;
+ final int clientId = msg.arg2;
+ switch (msg.what) {
+ case NsdManager.REGISTER_CLIENT:
+ final Pair<NsdServiceConnector, INsdManagerCallback> arg =
+ (Pair<NsdServiceConnector, INsdManagerCallback>) msg.obj;
+ final INsdManagerCallback cb = arg.second;
+ try {
+ cb.asBinder().linkToDeath(arg.first, 0);
+ cInfo = new ClientInfo(cb);
+ mClients.put(arg.first, cInfo);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Client " + clientId + " has already died");
+ }
+ break;
+ case NsdManager.UNREGISTER_CLIENT:
+ final NsdServiceConnector connector = (NsdServiceConnector) msg.obj;
+ cInfo = mClients.remove(connector);
+ if (cInfo != null) {
+ cInfo.expungeAllRequests();
+ if (cInfo.isLegacy()) {
+ mLegacyClientCount -= 1;
+ }
+ }
+ maybeScheduleStop();
+ break;
+ case NsdManager.DISCOVER_SERVICES:
+ cInfo = getClientInfoForReply(msg);
+ if (cInfo != null) {
+ cInfo.onDiscoverServicesFailed(
+ clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+ }
+ break;
+ case NsdManager.STOP_DISCOVERY:
+ cInfo = getClientInfoForReply(msg);
+ if (cInfo != null) {
+ cInfo.onStopDiscoveryFailed(
+ clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+ }
+ break;
+ case NsdManager.REGISTER_SERVICE:
+ cInfo = getClientInfoForReply(msg);
+ if (cInfo != null) {
+ cInfo.onRegisterServiceFailed(
+ clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+ }
+ break;
+ case NsdManager.UNREGISTER_SERVICE:
+ cInfo = getClientInfoForReply(msg);
+ if (cInfo != null) {
+ cInfo.onUnregisterServiceFailed(
+ clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+ }
+ break;
+ case NsdManager.RESOLVE_SERVICE:
+ cInfo = getClientInfoForReply(msg);
+ if (cInfo != null) {
+ cInfo.onResolveServiceFailed(
+ clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+ }
+ break;
+ case NsdManager.DAEMON_CLEANUP:
+ mDaemon.maybeStop();
+ break;
+ // This event should be only sent by the legacy (target SDK < S) clients.
+ // Mark the sending client as legacy.
+ case NsdManager.DAEMON_STARTUP:
+ cInfo = getClientInfoForReply(msg);
+ if (cInfo != null) {
+ cancelStop();
+ cInfo.setLegacy();
+ mLegacyClientCount += 1;
+ maybeStartDaemon();
+ }
+ break;
+ case NsdManager.NATIVE_DAEMON_EVENT:
+ default:
+ Slog.e(TAG, "Unhandled " + msg);
+ return NOT_HANDLED;
+ }
+ return HANDLED;
+ }
+
+ private ClientInfo getClientInfoForReply(Message msg) {
+ final ListenerArgs args = (ListenerArgs) msg.obj;
+ return mClients.get(args.connector);
+ }
+ }
+
+ class DisabledState extends State {
+ @Override
+ public void enter() {
+ sendNsdStateChangeBroadcast(false);
+ }
+
+ @Override
+ public boolean processMessage(Message msg) {
+ switch (msg.what) {
+ case NsdManager.ENABLE:
+ transitionTo(mEnabledState);
+ break;
+ default:
+ return NOT_HANDLED;
+ }
+ return HANDLED;
+ }
+ }
+
+ class EnabledState extends State {
+ @Override
+ public void enter() {
+ sendNsdStateChangeBroadcast(true);
+ }
+
+ @Override
+ public void exit() {
+ // TODO: it is incorrect to stop the daemon without expunging all requests
+ // and sending error callbacks to clients.
+ scheduleStop();
+ }
+
+ private boolean requestLimitReached(ClientInfo clientInfo) {
+ if (clientInfo.mClientIds.size() >= ClientInfo.MAX_LIMIT) {
+ if (DBG) Slog.d(TAG, "Exceeded max outstanding requests " + clientInfo);
+ return true;
+ }
+ return false;
+ }
+
+ private void storeRequestMap(int clientId, int globalId, ClientInfo clientInfo, int what) {
+ clientInfo.mClientIds.put(clientId, globalId);
+ clientInfo.mClientRequests.put(clientId, what);
+ mIdToClientInfoMap.put(globalId, clientInfo);
+ // Remove the cleanup event because here comes a new request.
+ cancelStop();
+ }
+
+ private void removeRequestMap(int clientId, int globalId, ClientInfo clientInfo) {
+ clientInfo.mClientIds.delete(clientId);
+ clientInfo.mClientRequests.delete(clientId);
+ mIdToClientInfoMap.remove(globalId);
+ maybeScheduleStop();
+ }
+
+ @Override
+ public boolean processMessage(Message msg) {
+ final ClientInfo clientInfo;
+ final int id;
+ final int clientId = msg.arg2;
+ final ListenerArgs args;
+ switch (msg.what) {
+ case NsdManager.DISABLE:
+ //TODO: cleanup clients
+ transitionTo(mDisabledState);
+ break;
+ case NsdManager.DISCOVER_SERVICES:
+ if (DBG) Slog.d(TAG, "Discover services");
+ args = (ListenerArgs) msg.obj;
+ clientInfo = mClients.get(args.connector);
+
+ if (requestLimitReached(clientInfo)) {
+ clientInfo.onDiscoverServicesFailed(
+ clientId, NsdManager.FAILURE_MAX_LIMIT);
+ break;
+ }
+
+ maybeStartDaemon();
+ id = getUniqueId();
+ if (discoverServices(id, args.serviceInfo.getServiceType())) {
+ if (DBG) {
+ Slog.d(TAG, "Discover " + msg.arg2 + " " + id +
+ args.serviceInfo.getServiceType());
+ }
+ storeRequestMap(clientId, id, clientInfo, msg.what);
+ clientInfo.onDiscoverServicesStarted(clientId, args.serviceInfo);
+ } else {
+ stopServiceDiscovery(id);
+ clientInfo.onDiscoverServicesFailed(clientId,
+ NsdManager.FAILURE_INTERNAL_ERROR);
+ }
+ break;
+ case NsdManager.STOP_DISCOVERY:
+ if (DBG) Slog.d(TAG, "Stop service discovery");
+ args = (ListenerArgs) msg.obj;
+ clientInfo = mClients.get(args.connector);
+
+ try {
+ id = clientInfo.mClientIds.get(clientId);
+ } catch (NullPointerException e) {
+ clientInfo.onStopDiscoveryFailed(
+ clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+ break;
+ }
+ removeRequestMap(clientId, id, clientInfo);
+ if (stopServiceDiscovery(id)) {
+ clientInfo.onStopDiscoverySucceeded(clientId);
+ } else {
+ clientInfo.onStopDiscoveryFailed(
+ clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+ }
+ break;
+ case NsdManager.REGISTER_SERVICE:
+ if (DBG) Slog.d(TAG, "Register service");
+ args = (ListenerArgs) msg.obj;
+ clientInfo = mClients.get(args.connector);
+ if (requestLimitReached(clientInfo)) {
+ clientInfo.onRegisterServiceFailed(
+ clientId, NsdManager.FAILURE_MAX_LIMIT);
+ break;
+ }
+
+ maybeStartDaemon();
+ id = getUniqueId();
+ if (registerService(id, args.serviceInfo)) {
+ if (DBG) Slog.d(TAG, "Register " + clientId + " " + id);
+ storeRequestMap(clientId, id, clientInfo, msg.what);
+ // Return success after mDns reports success
+ } else {
+ unregisterService(id);
+ clientInfo.onRegisterServiceFailed(
+ clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+ }
+ break;
+ case NsdManager.UNREGISTER_SERVICE:
+ if (DBG) Slog.d(TAG, "unregister service");
+ args = (ListenerArgs) msg.obj;
+ clientInfo = mClients.get(args.connector);
+ if (clientInfo == null) {
+ Slog.e(TAG, "Unknown connector in unregistration");
+ break;
+ }
+ id = clientInfo.mClientIds.get(clientId);
+ removeRequestMap(clientId, id, clientInfo);
+ if (unregisterService(id)) {
+ clientInfo.onUnregisterServiceSucceeded(clientId);
+ } else {
+ clientInfo.onUnregisterServiceFailed(
+ clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+ }
+ break;
+ case NsdManager.RESOLVE_SERVICE:
+ if (DBG) Slog.d(TAG, "Resolve service");
+ args = (ListenerArgs) msg.obj;
+ clientInfo = mClients.get(args.connector);
+
+ if (clientInfo.mResolvedService != null) {
+ clientInfo.onResolveServiceFailed(
+ clientId, NsdManager.FAILURE_ALREADY_ACTIVE);
+ break;
+ }
+
+ maybeStartDaemon();
+ id = getUniqueId();
+ if (resolveService(id, args.serviceInfo)) {
+ clientInfo.mResolvedService = new NsdServiceInfo();
+ storeRequestMap(clientId, id, clientInfo, msg.what);
+ } else {
+ clientInfo.onResolveServiceFailed(
+ clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+ }
+ break;
+ case NsdManager.NATIVE_DAEMON_EVENT:
+ NativeEvent event = (NativeEvent) msg.obj;
+ if (!handleNativeEvent(event.code, event.raw, event.cooked)) {
+ return NOT_HANDLED;
+ }
+ break;
+ default:
+ return NOT_HANDLED;
+ }
+ return HANDLED;
+ }
+
+ private boolean handleNativeEvent(int code, String raw, String[] cooked) {
+ NsdServiceInfo servInfo;
+ int id = Integer.parseInt(cooked[1]);
+ ClientInfo clientInfo = mIdToClientInfoMap.get(id);
+ if (clientInfo == null) {
+ String name = NativeResponseCode.nameOf(code);
+ Slog.e(TAG, String.format("id %d for %s has no client mapping", id, name));
+ return false;
+ }
+
+ /* This goes in response as msg.arg2 */
+ int clientId = clientInfo.getClientId(id);
+ if (clientId < 0) {
+ // This can happen because of race conditions. For example,
+ // SERVICE_FOUND may race with STOP_SERVICE_DISCOVERY,
+ // and we may get in this situation.
+ String name = NativeResponseCode.nameOf(code);
+ Slog.d(TAG, String.format(
+ "Notification %s for listener id %d that is no longer active",
+ name, id));
+ return false;
+ }
+ if (DBG) {
+ String name = NativeResponseCode.nameOf(code);
+ Slog.d(TAG, String.format("Native daemon message %s: %s", name, raw));
+ }
+ switch (code) {
+ case NativeResponseCode.SERVICE_FOUND:
+ /* NNN uniqueId serviceName regType domain */
+ servInfo = new NsdServiceInfo(cooked[2], cooked[3]);
+ clientInfo.onServiceFound(clientId, servInfo);
+ break;
+ case NativeResponseCode.SERVICE_LOST:
+ /* NNN uniqueId serviceName regType domain */
+ servInfo = new NsdServiceInfo(cooked[2], cooked[3]);
+ clientInfo.onServiceLost(clientId, servInfo);
+ break;
+ case NativeResponseCode.SERVICE_DISCOVERY_FAILED:
+ /* NNN uniqueId errorCode */
+ clientInfo.onDiscoverServicesFailed(
+ clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+ break;
+ case NativeResponseCode.SERVICE_REGISTERED:
+ /* NNN regId serviceName regType */
+ servInfo = new NsdServiceInfo(cooked[2], null);
+ clientInfo.onRegisterServiceSucceeded(clientId, servInfo);
+ break;
+ case NativeResponseCode.SERVICE_REGISTRATION_FAILED:
+ /* NNN regId errorCode */
+ clientInfo.onRegisterServiceFailed(
+ clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+ break;
+ case NativeResponseCode.SERVICE_UPDATED:
+ /* NNN regId */
+ break;
+ case NativeResponseCode.SERVICE_UPDATE_FAILED:
+ /* NNN regId errorCode */
+ break;
+ case NativeResponseCode.SERVICE_RESOLVED:
+ /* NNN resolveId fullName hostName port txtlen txtdata */
+ int index = 0;
+ while (index < cooked[2].length() && cooked[2].charAt(index) != '.') {
+ if (cooked[2].charAt(index) == '\\') {
+ ++index;
+ }
+ ++index;
+ }
+ if (index >= cooked[2].length()) {
+ Slog.e(TAG, "Invalid service found " + raw);
+ break;
+ }
+ String name = cooked[2].substring(0, index);
+ String rest = cooked[2].substring(index);
+ String type = rest.replace(".local.", "");
+
+ name = unescape(name);
+
+ clientInfo.mResolvedService.setServiceName(name);
+ clientInfo.mResolvedService.setServiceType(type);
+ clientInfo.mResolvedService.setPort(Integer.parseInt(cooked[4]));
+ clientInfo.mResolvedService.setTxtRecords(cooked[6]);
+
+ stopResolveService(id);
+ removeRequestMap(clientId, id, clientInfo);
+
+ int id2 = getUniqueId();
+ if (getAddrInfo(id2, cooked[3])) {
+ storeRequestMap(clientId, id2, clientInfo, NsdManager.RESOLVE_SERVICE);
+ } else {
+ clientInfo.onResolveServiceFailed(
+ clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+ clientInfo.mResolvedService = null;
+ }
+ break;
+ case NativeResponseCode.SERVICE_RESOLUTION_FAILED:
+ /* NNN resolveId errorCode */
+ stopResolveService(id);
+ removeRequestMap(clientId, id, clientInfo);
+ clientInfo.mResolvedService = null;
+ clientInfo.onResolveServiceFailed(
+ clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+ break;
+ case NativeResponseCode.SERVICE_GET_ADDR_FAILED:
+ /* NNN resolveId errorCode */
+ stopGetAddrInfo(id);
+ removeRequestMap(clientId, id, clientInfo);
+ clientInfo.mResolvedService = null;
+ clientInfo.onResolveServiceFailed(
+ clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+ break;
+ case NativeResponseCode.SERVICE_GET_ADDR_SUCCESS:
+ /* NNN resolveId hostname ttl addr */
+ try {
+ clientInfo.mResolvedService.setHost(InetAddress.getByName(cooked[4]));
+ clientInfo.onResolveServiceSucceeded(
+ clientId, clientInfo.mResolvedService);
+ } catch (java.net.UnknownHostException e) {
+ clientInfo.onResolveServiceFailed(
+ clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+ }
+ stopGetAddrInfo(id);
+ removeRequestMap(clientId, id, clientInfo);
+ clientInfo.mResolvedService = null;
+ break;
+ default:
+ return false;
+ }
+ return true;
+ }
+ }
+ }
+
+ private String unescape(String s) {
+ StringBuilder sb = new StringBuilder(s.length());
+ for (int i = 0; i < s.length(); ++i) {
+ char c = s.charAt(i);
+ if (c == '\\') {
+ if (++i >= s.length()) {
+ Slog.e(TAG, "Unexpected end of escape sequence in: " + s);
+ break;
+ }
+ c = s.charAt(i);
+ if (c != '.' && c != '\\') {
+ if (i + 2 >= s.length()) {
+ Slog.e(TAG, "Unexpected end of escape sequence in: " + s);
+ break;
+ }
+ c = (char) ((c-'0') * 100 + (s.charAt(i+1)-'0') * 10 + (s.charAt(i+2)-'0'));
+ i += 2;
+ }
+ }
+ sb.append(c);
+ }
+ return sb.toString();
+ }
+
+ @VisibleForTesting
+ NsdService(Context ctx, NsdSettings settings, Handler handler,
+ DaemonConnectionSupplier fn, long cleanupDelayMs) {
+ mCleanupDelayMs = cleanupDelayMs;
+ mContext = ctx;
+ mNsdSettings = settings;
+ mNsdStateMachine = new NsdStateMachine(TAG, handler);
+ mNsdStateMachine.start();
+ mDaemonCallback = new NativeCallbackReceiver();
+ mDaemon = fn.get(mDaemonCallback);
+ }
+
+ public static NsdService create(Context context) throws InterruptedException {
+ NsdSettings settings = NsdSettings.makeDefault(context);
+ HandlerThread thread = new HandlerThread(TAG);
+ thread.start();
+ Handler handler = new Handler(thread.getLooper());
+ NsdService service = new NsdService(context, settings, handler,
+ DaemonConnection::new, CLEANUP_DELAY_MS);
+ service.mDaemonCallback.awaitConnection();
+ return service;
+ }
+
+ @Override
+ public INsdServiceConnector connect(INsdManagerCallback cb) {
+ mContext.enforceCallingOrSelfPermission(android.Manifest.permission.INTERNET, "NsdService");
+ final INsdServiceConnector connector = new NsdServiceConnector();
+ mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
+ NsdManager.REGISTER_CLIENT, new Pair<>(connector, cb)));
+ return connector;
+ }
+
+ private static class ListenerArgs {
+ public final NsdServiceConnector connector;
+ public final NsdServiceInfo serviceInfo;
+ ListenerArgs(NsdServiceConnector connector, NsdServiceInfo serviceInfo) {
+ this.connector = connector;
+ this.serviceInfo = serviceInfo;
+ }
+ }
+
+ private class NsdServiceConnector extends INsdServiceConnector.Stub
+ implements IBinder.DeathRecipient {
+ @Override
+ public void registerService(int listenerKey, NsdServiceInfo serviceInfo) {
+ mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
+ NsdManager.REGISTER_SERVICE, 0, listenerKey,
+ new ListenerArgs(this, serviceInfo)));
+ }
+
+ @Override
+ public void unregisterService(int listenerKey) {
+ mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
+ NsdManager.UNREGISTER_SERVICE, 0, listenerKey,
+ new ListenerArgs(this, null)));
+ }
+
+ @Override
+ public void discoverServices(int listenerKey, NsdServiceInfo serviceInfo) {
+ mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
+ NsdManager.DISCOVER_SERVICES, 0, listenerKey,
+ new ListenerArgs(this, serviceInfo)));
+ }
+
+ @Override
+ public void stopDiscovery(int listenerKey) {
+ mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
+ NsdManager.STOP_DISCOVERY, 0, listenerKey, new ListenerArgs(this, null)));
+ }
+
+ @Override
+ public void resolveService(int listenerKey, NsdServiceInfo serviceInfo) {
+ mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
+ NsdManager.RESOLVE_SERVICE, 0, listenerKey,
+ new ListenerArgs(this, serviceInfo)));
+ }
+
+ @Override
+ public void startDaemon() {
+ mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
+ NsdManager.DAEMON_STARTUP, new ListenerArgs(this, null)));
+ }
+
+ @Override
+ public void binderDied() {
+ mNsdStateMachine.sendMessage(
+ mNsdStateMachine.obtainMessage(NsdManager.UNREGISTER_CLIENT, this));
+ }
+ }
+
+ private void notifyEnabled(boolean isEnabled) {
+ mNsdStateMachine.sendMessage(isEnabled ? NsdManager.ENABLE : NsdManager.DISABLE);
+ }
+
+ private void sendNsdStateChangeBroadcast(boolean isEnabled) {
+ final Intent intent = new Intent(NsdManager.ACTION_NSD_STATE_CHANGED);
+ intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+ int nsdState = isEnabled ? NsdManager.NSD_STATE_ENABLED : NsdManager.NSD_STATE_DISABLED;
+ intent.putExtra(NsdManager.EXTRA_NSD_STATE, nsdState);
+ mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+ }
+
+ private boolean isNsdEnabled() {
+ boolean ret = mNsdSettings.isEnabled();
+ if (DBG) {
+ Slog.d(TAG, "Network service discovery is " + (ret ? "enabled" : "disabled"));
+ }
+ return ret;
+ }
+
+ private int getUniqueId() {
+ if (++mUniqueId == INVALID_ID) return ++mUniqueId;
+ return mUniqueId;
+ }
+
+ /* These should be in sync with system/netd/server/ResponseCode.h */
+ static final class NativeResponseCode {
+ public static final int SERVICE_DISCOVERY_FAILED = 602;
+ public static final int SERVICE_FOUND = 603;
+ public static final int SERVICE_LOST = 604;
+
+ public static final int SERVICE_REGISTRATION_FAILED = 605;
+ public static final int SERVICE_REGISTERED = 606;
+
+ public static final int SERVICE_RESOLUTION_FAILED = 607;
+ public static final int SERVICE_RESOLVED = 608;
+
+ public static final int SERVICE_UPDATED = 609;
+ public static final int SERVICE_UPDATE_FAILED = 610;
+
+ public static final int SERVICE_GET_ADDR_FAILED = 611;
+ public static final int SERVICE_GET_ADDR_SUCCESS = 612;
+
+ private static final SparseArray<String> CODE_NAMES = new SparseArray<>();
+ static {
+ CODE_NAMES.put(SERVICE_DISCOVERY_FAILED, "SERVICE_DISCOVERY_FAILED");
+ CODE_NAMES.put(SERVICE_FOUND, "SERVICE_FOUND");
+ CODE_NAMES.put(SERVICE_LOST, "SERVICE_LOST");
+ CODE_NAMES.put(SERVICE_REGISTRATION_FAILED, "SERVICE_REGISTRATION_FAILED");
+ CODE_NAMES.put(SERVICE_REGISTERED, "SERVICE_REGISTERED");
+ CODE_NAMES.put(SERVICE_RESOLUTION_FAILED, "SERVICE_RESOLUTION_FAILED");
+ CODE_NAMES.put(SERVICE_RESOLVED, "SERVICE_RESOLVED");
+ CODE_NAMES.put(SERVICE_UPDATED, "SERVICE_UPDATED");
+ CODE_NAMES.put(SERVICE_UPDATE_FAILED, "SERVICE_UPDATE_FAILED");
+ CODE_NAMES.put(SERVICE_GET_ADDR_FAILED, "SERVICE_GET_ADDR_FAILED");
+ CODE_NAMES.put(SERVICE_GET_ADDR_SUCCESS, "SERVICE_GET_ADDR_SUCCESS");
+ }
+
+ static String nameOf(int code) {
+ String name = CODE_NAMES.get(code);
+ if (name == null) {
+ return Integer.toString(code);
+ }
+ return name;
+ }
+ }
+
+ private class NativeEvent {
+ final int code;
+ final String raw;
+ final String[] cooked;
+
+ NativeEvent(int code, String raw, String[] cooked) {
+ this.code = code;
+ this.raw = raw;
+ this.cooked = cooked;
+ }
+ }
+
+ class NativeCallbackReceiver implements INativeDaemonConnectorCallbacks {
+ private final CountDownLatch connected = new CountDownLatch(1);
+
+ public void awaitConnection() throws InterruptedException {
+ connected.await();
+ }
+
+ @Override
+ public void onDaemonConnected() {
+ connected.countDown();
+ }
+
+ @Override
+ public boolean onCheckHoldWakeLock(int code) {
+ return false;
+ }
+
+ @Override
+ public boolean onEvent(int code, String raw, String[] cooked) {
+ // TODO: NDC translates a message to a callback, we could enhance NDC to
+ // directly interact with a state machine through messages
+ NativeEvent event = new NativeEvent(code, raw, cooked);
+ mNsdStateMachine.sendMessage(NsdManager.NATIVE_DAEMON_EVENT, event);
+ return true;
+ }
+ }
+
+ interface DaemonConnectionSupplier {
+ DaemonConnection get(NativeCallbackReceiver callback);
+ }
+
+ @VisibleForTesting
+ public static class DaemonConnection {
+ final NativeDaemonConnector mNativeConnector;
+ boolean mIsStarted = false;
+
+ DaemonConnection(NativeCallbackReceiver callback) {
+ mNativeConnector = new NativeDaemonConnector(callback, "mdns", 10, MDNS_TAG, 25, null);
+ new Thread(mNativeConnector, MDNS_TAG).start();
+ }
+
+ /**
+ * Executes the specified cmd on the daemon.
+ */
+ public boolean execute(Object... args) {
+ if (DBG) {
+ Slog.d(TAG, "mdnssd " + Arrays.toString(args));
+ }
+ try {
+ mNativeConnector.execute("mdnssd", args);
+ } catch (NativeDaemonConnectorException e) {
+ Slog.e(TAG, "Failed to execute mdnssd " + Arrays.toString(args), e);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Starts the daemon if it is not already started.
+ */
+ public void maybeStart() {
+ if (mIsStarted) {
+ return;
+ }
+ execute("start-service");
+ mIsStarted = true;
+ }
+
+ /**
+ * Stops the daemon if it is started.
+ */
+ public void maybeStop() {
+ if (!mIsStarted) {
+ return;
+ }
+ execute("stop-service");
+ mIsStarted = false;
+ }
+ }
+
+ private boolean registerService(int regId, NsdServiceInfo service) {
+ if (DBG) {
+ Slog.d(TAG, "registerService: " + regId + " " + service);
+ }
+ String name = service.getServiceName();
+ String type = service.getServiceType();
+ int port = service.getPort();
+ byte[] textRecord = service.getTxtRecord();
+ String record = Base64.encodeToString(textRecord, Base64.DEFAULT).replace("\n", "");
+ return mDaemon.execute("register", regId, name, type, port, record);
+ }
+
+ private boolean unregisterService(int regId) {
+ return mDaemon.execute("stop-register", regId);
+ }
+
+ private boolean updateService(int regId, DnsSdTxtRecord t) {
+ if (t == null) {
+ return false;
+ }
+ return mDaemon.execute("update", regId, t.size(), t.getRawData());
+ }
+
+ private boolean discoverServices(int discoveryId, String serviceType) {
+ return mDaemon.execute("discover", discoveryId, serviceType);
+ }
+
+ private boolean stopServiceDiscovery(int discoveryId) {
+ return mDaemon.execute("stop-discover", discoveryId);
+ }
+
+ private boolean resolveService(int resolveId, NsdServiceInfo service) {
+ String name = service.getServiceName();
+ String type = service.getServiceType();
+ return mDaemon.execute("resolve", resolveId, name, type, "local.");
+ }
+
+ private boolean stopResolveService(int resolveId) {
+ return mDaemon.execute("stop-resolve", resolveId);
+ }
+
+ private boolean getAddrInfo(int resolveId, String hostname) {
+ return mDaemon.execute("getaddrinfo", resolveId, hostname);
+ }
+
+ private boolean stopGetAddrInfo(int resolveId) {
+ return mDaemon.execute("stop-getaddrinfo", resolveId);
+ }
+
+ @Override
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
+
+ for (ClientInfo client : mClients.values()) {
+ pw.println("Client Info");
+ pw.println(client);
+ }
+
+ mNsdStateMachine.dump(fd, pw, args);
+ }
+
+ /* Information tracked per client */
+ private class ClientInfo {
+
+ private static final int MAX_LIMIT = 10;
+ private final INsdManagerCallback mCb;
+ /* Remembers a resolved service until getaddrinfo completes */
+ private NsdServiceInfo mResolvedService;
+
+ /* A map from client id to unique id sent to mDns */
+ private final SparseIntArray mClientIds = new SparseIntArray();
+
+ /* A map from client id to the type of the request we had received */
+ private final SparseIntArray mClientRequests = new SparseIntArray();
+
+ // The target SDK of this client < Build.VERSION_CODES.S
+ private boolean mIsLegacy = false;
+
+ private ClientInfo(INsdManagerCallback cb) {
+ mCb = cb;
+ if (DBG) Slog.d(TAG, "New client");
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("mResolvedService ").append(mResolvedService).append("\n");
+ sb.append("mIsLegacy ").append(mIsLegacy).append("\n");
+ for(int i = 0; i< mClientIds.size(); i++) {
+ int clientID = mClientIds.keyAt(i);
+ sb.append("clientId ").append(clientID).
+ append(" mDnsId ").append(mClientIds.valueAt(i)).
+ append(" type ").append(mClientRequests.get(clientID)).append("\n");
+ }
+ return sb.toString();
+ }
+
+ private boolean isLegacy() {
+ return mIsLegacy;
+ }
+
+ private void setLegacy() {
+ mIsLegacy = true;
+ }
+
+ // Remove any pending requests from the global map when we get rid of a client,
+ // and send cancellations to the daemon.
+ private void expungeAllRequests() {
+ int globalId, clientId, i;
+ // TODO: to keep handler responsive, do not clean all requests for that client at once.
+ for (i = 0; i < mClientIds.size(); i++) {
+ clientId = mClientIds.keyAt(i);
+ globalId = mClientIds.valueAt(i);
+ mIdToClientInfoMap.remove(globalId);
+ if (DBG) Slog.d(TAG, "Terminating client-ID " + clientId +
+ " global-ID " + globalId + " type " + mClientRequests.get(clientId));
+ switch (mClientRequests.get(clientId)) {
+ case NsdManager.DISCOVER_SERVICES:
+ stopServiceDiscovery(globalId);
+ break;
+ case NsdManager.RESOLVE_SERVICE:
+ stopResolveService(globalId);
+ break;
+ case NsdManager.REGISTER_SERVICE:
+ unregisterService(globalId);
+ break;
+ default:
+ break;
+ }
+ }
+ mClientIds.clear();
+ mClientRequests.clear();
+ }
+
+ // mClientIds is a sparse array of listener id -> mDnsClient id. For a given mDnsClient id,
+ // return the corresponding listener id. mDnsClient id is also called a global id.
+ private int getClientId(final int globalId) {
+ int idx = mClientIds.indexOfValue(globalId);
+ if (idx < 0) {
+ return idx;
+ }
+ return mClientIds.keyAt(idx);
+ }
+
+ void onDiscoverServicesStarted(int listenerKey, NsdServiceInfo info) {
+ try {
+ mCb.onDiscoverServicesStarted(listenerKey, info);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling onDiscoverServicesStarted", e);
+ }
+ }
+
+ void onDiscoverServicesFailed(int listenerKey, int error) {
+ try {
+ mCb.onDiscoverServicesFailed(listenerKey, error);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling onDiscoverServicesFailed", e);
+ }
+ }
+
+ void onServiceFound(int listenerKey, NsdServiceInfo info) {
+ try {
+ mCb.onServiceFound(listenerKey, info);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling onServiceFound(", e);
+ }
+ }
+
+ void onServiceLost(int listenerKey, NsdServiceInfo info) {
+ try {
+ mCb.onServiceLost(listenerKey, info);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling onServiceLost(", e);
+ }
+ }
+
+ void onStopDiscoveryFailed(int listenerKey, int error) {
+ try {
+ mCb.onStopDiscoveryFailed(listenerKey, error);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling onStopDiscoveryFailed", e);
+ }
+ }
+
+ void onStopDiscoverySucceeded(int listenerKey) {
+ try {
+ mCb.onStopDiscoverySucceeded(listenerKey);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling onStopDiscoverySucceeded", e);
+ }
+ }
+
+ void onRegisterServiceFailed(int listenerKey, int error) {
+ try {
+ mCb.onRegisterServiceFailed(listenerKey, error);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling onRegisterServiceFailed", e);
+ }
+ }
+
+ void onRegisterServiceSucceeded(int listenerKey, NsdServiceInfo info) {
+ try {
+ mCb.onRegisterServiceSucceeded(listenerKey, info);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling onRegisterServiceSucceeded", e);
+ }
+ }
+
+ void onUnregisterServiceFailed(int listenerKey, int error) {
+ try {
+ mCb.onUnregisterServiceFailed(listenerKey, error);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling onUnregisterServiceFailed", e);
+ }
+ }
+
+ void onUnregisterServiceSucceeded(int listenerKey) {
+ try {
+ mCb.onUnregisterServiceSucceeded(listenerKey);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling onUnregisterServiceSucceeded", e);
+ }
+ }
+
+ void onResolveServiceFailed(int listenerKey, int error) {
+ try {
+ mCb.onResolveServiceFailed(listenerKey, error);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling onResolveServiceFailed", e);
+ }
+ }
+
+ void onResolveServiceSucceeded(int listenerKey, NsdServiceInfo info) {
+ try {
+ mCb.onResolveServiceSucceeded(listenerKey, info);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling onResolveServiceSucceeded", e);
+ }
+ }
+ }
+
+ /**
+ * Interface which encapsulates dependencies of NsdService that are hard to mock, hard to
+ * override, or have side effects on global state in unit tests.
+ */
+ @VisibleForTesting
+ public interface NsdSettings {
+ boolean isEnabled();
+ void putEnabledStatus(boolean isEnabled);
+ void registerContentObserver(Uri uri, ContentObserver observer);
+
+ static NsdSettings makeDefault(Context context) {
+ final ContentResolver resolver = context.getContentResolver();
+ return new NsdSettings() {
+ @Override
+ public boolean isEnabled() {
+ return Settings.Global.getInt(resolver, Settings.Global.NSD_ON, 1) == 1;
+ }
+
+ @Override
+ public void putEnabledStatus(boolean isEnabled) {
+ Settings.Global.putInt(resolver, Settings.Global.NSD_ON, isEnabled ? 1 : 0);
+ }
+
+ @Override
+ public void registerContentObserver(Uri uri, ContentObserver observer) {
+ resolver.registerContentObserver(uri, false, observer);
+ }
+ };
+ }
+ }
+}