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/OWNERS b/packages/Nsd/OWNERS
new file mode 100644
index 0000000..4862377
--- /dev/null
+++ b/packages/Nsd/OWNERS
@@ -0,0 +1 @@
+file:platform/packages/modules/Connectivity:master:/OWNERS_core_networking
\ No newline at end of file
diff --git a/packages/Nsd/framework/Android.bp b/packages/Nsd/framework/Android.bp
new file mode 100644
index 0000000..2363a9f
--- /dev/null
+++ b/packages/Nsd/framework/Android.bp
@@ -0,0 +1,54 @@
+//
+// 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: "framework-connectivity-nsd-internal-sources",
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.aidl",
+ ],
+ path: "src",
+ visibility: [
+ "//visibility:private",
+ ],
+}
+
+filegroup {
+ name: "framework-connectivity-nsd-aidl-export-sources",
+ srcs: [
+ "aidl-export/**/*.aidl",
+ ],
+ path: "aidl-export",
+ visibility: [
+ "//visibility:private",
+ ],
+}
+
+filegroup {
+ name: "framework-connectivity-nsd-sources",
+ srcs: [
+ ":framework-connectivity-nsd-internal-sources",
+ ":framework-connectivity-nsd-aidl-export-sources",
+ ],
+ visibility: [
+ "//frameworks/base",
+ ],
+}
diff --git a/packages/Nsd/framework/aidl-export/android/net/nsd/NsdServiceInfo.aidl b/packages/Nsd/framework/aidl-export/android/net/nsd/NsdServiceInfo.aidl
new file mode 100644
index 0000000..657bdd1
--- /dev/null
+++ b/packages/Nsd/framework/aidl-export/android/net/nsd/NsdServiceInfo.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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 android.net.nsd;
+
+@JavaOnlyStableParcelable parcelable NsdServiceInfo;
\ No newline at end of file
diff --git a/packages/Nsd/framework/src/android/net/nsd/INsdManager.aidl b/packages/Nsd/framework/src/android/net/nsd/INsdManager.aidl
new file mode 100644
index 0000000..89e9cdb
--- /dev/null
+++ b/packages/Nsd/framework/src/android/net/nsd/INsdManager.aidl
@@ -0,0 +1,30 @@
+/**
+ * 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 android.net.nsd;
+
+import android.net.nsd.INsdManagerCallback;
+import android.net.nsd.INsdServiceConnector;
+import android.os.Messenger;
+
+/**
+ * Interface that NsdService implements to connect NsdManager clients.
+ *
+ * {@hide}
+ */
+interface INsdManager {
+ INsdServiceConnector connect(INsdManagerCallback cb);
+}
diff --git a/packages/Nsd/framework/src/android/net/nsd/INsdManagerCallback.aidl b/packages/Nsd/framework/src/android/net/nsd/INsdManagerCallback.aidl
new file mode 100644
index 0000000..1a262ec
--- /dev/null
+++ b/packages/Nsd/framework/src/android/net/nsd/INsdManagerCallback.aidl
@@ -0,0 +1,39 @@
+/**
+ * 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 android.net.nsd;
+
+import android.os.Messenger;
+import android.net.nsd.NsdServiceInfo;
+
+/**
+ * Callbacks from NsdService to NsdManager
+ * @hide
+ */
+oneway interface INsdManagerCallback {
+ void onDiscoverServicesStarted(int listenerKey, in NsdServiceInfo info);
+ void onDiscoverServicesFailed(int listenerKey, int error);
+ void onServiceFound(int listenerKey, in NsdServiceInfo info);
+ void onServiceLost(int listenerKey, in NsdServiceInfo info);
+ void onStopDiscoveryFailed(int listenerKey, int error);
+ void onStopDiscoverySucceeded(int listenerKey);
+ void onRegisterServiceFailed(int listenerKey, int error);
+ void onRegisterServiceSucceeded(int listenerKey, in NsdServiceInfo info);
+ void onUnregisterServiceFailed(int listenerKey, int error);
+ void onUnregisterServiceSucceeded(int listenerKey);
+ void onResolveServiceFailed(int listenerKey, int error);
+ void onResolveServiceSucceeded(int listenerKey, in NsdServiceInfo info);
+}
diff --git a/packages/Nsd/framework/src/android/net/nsd/INsdServiceConnector.aidl b/packages/Nsd/framework/src/android/net/nsd/INsdServiceConnector.aidl
new file mode 100644
index 0000000..b06ae55
--- /dev/null
+++ b/packages/Nsd/framework/src/android/net/nsd/INsdServiceConnector.aidl
@@ -0,0 +1,35 @@
+/**
+ * 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 android.net.nsd;
+
+import android.net.nsd.INsdManagerCallback;
+import android.net.nsd.NsdServiceInfo;
+import android.os.Messenger;
+
+/**
+ * Interface that NsdService implements for each NsdManager client.
+ *
+ * {@hide}
+ */
+interface INsdServiceConnector {
+ void registerService(int listenerKey, in NsdServiceInfo serviceInfo);
+ void unregisterService(int listenerKey);
+ void discoverServices(int listenerKey, in NsdServiceInfo serviceInfo);
+ void stopDiscovery(int listenerKey);
+ void resolveService(int listenerKey, in NsdServiceInfo serviceInfo);
+ void startDaemon();
+}
\ No newline at end of file
diff --git a/packages/Nsd/framework/src/android/net/nsd/NsdManager.java b/packages/Nsd/framework/src/android/net/nsd/NsdManager.java
new file mode 100644
index 0000000..6c597e2
--- /dev/null
+++ b/packages/Nsd/framework/src/android/net/nsd/NsdManager.java
@@ -0,0 +1,734 @@
+/*
+ * 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 android.net.nsd;
+
+import static com.android.internal.util.Preconditions.checkArgument;
+import static com.android.internal.util.Preconditions.checkNotNull;
+import static com.android.internal.util.Preconditions.checkStringNotEmpty;
+
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.SystemService;
+import android.app.compat.CompatChanges;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledSince;
+import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.Protocol;
+
+/**
+ * The Network Service Discovery Manager class provides the API to discover services
+ * on a network. As an example, if device A and device B are connected over a Wi-Fi
+ * network, a game registered on device A can be discovered by a game on device
+ * B. Another example use case is an application discovering printers on the network.
+ *
+ * <p> The API currently supports DNS based service discovery and discovery is currently
+ * limited to a local network over Multicast DNS. DNS service discovery is described at
+ * http://files.dns-sd.org/draft-cheshire-dnsext-dns-sd.txt
+ *
+ * <p> The API is asynchronous, and responses to requests from an application are on listener
+ * callbacks on a separate internal thread.
+ *
+ * <p> There are three main operations the API supports - registration, discovery and resolution.
+ * <pre>
+ * Application start
+ * |
+ * |
+ * | onServiceRegistered()
+ * Register any local services /
+ * to be advertised with \
+ * registerService() onRegistrationFailed()
+ * |
+ * |
+ * discoverServices()
+ * |
+ * Maintain a list to track
+ * discovered services
+ * |
+ * |--------->
+ * | |
+ * | onServiceFound()
+ * | |
+ * | add service to list
+ * | |
+ * |<----------
+ * |
+ * |--------->
+ * | |
+ * | onServiceLost()
+ * | |
+ * | remove service from list
+ * | |
+ * |<----------
+ * |
+ * |
+ * | Connect to a service
+ * | from list ?
+ * |
+ * resolveService()
+ * |
+ * onServiceResolved()
+ * |
+ * Establish connection to service
+ * with the host and port information
+ *
+ * </pre>
+ * An application that needs to advertise itself over a network for other applications to
+ * discover it can do so with a call to {@link #registerService}. If Example is a http based
+ * application that can provide HTML data to peer services, it can register a name "Example"
+ * with service type "_http._tcp". A successful registration is notified with a callback to
+ * {@link RegistrationListener#onServiceRegistered} and a failure to register is notified
+ * over {@link RegistrationListener#onRegistrationFailed}
+ *
+ * <p> A peer application looking for http services can initiate a discovery for "_http._tcp"
+ * with a call to {@link #discoverServices}. A service found is notified with a callback
+ * to {@link DiscoveryListener#onServiceFound} and a service lost is notified on
+ * {@link DiscoveryListener#onServiceLost}.
+ *
+ * <p> Once the peer application discovers the "Example" http service, and either needs to read the
+ * attributes of the service or wants to receive data from the "Example" application, it can
+ * initiate a resolve with {@link #resolveService} to resolve the attributes, host, and port
+ * details. A successful resolve is notified on {@link ResolveListener#onServiceResolved} and a
+ * failure is notified on {@link ResolveListener#onResolveFailed}.
+ *
+ * Applications can reserve for a service type at
+ * http://www.iana.org/form/ports-service. Existing services can be found at
+ * http://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xml
+ *
+ * {@see NsdServiceInfo}
+ */
+@SystemService(Context.NSD_SERVICE)
+public final class NsdManager {
+ private static final String TAG = NsdManager.class.getSimpleName();
+ private static final boolean DBG = false;
+
+ /**
+ * When enabled, apps targeting < Android 12 are considered legacy for
+ * the NSD native daemon.
+ * The platform will only keep the daemon running as long as there are
+ * any legacy apps connected.
+ *
+ * After Android 12, directly communicate with native daemon might not
+ * work since the native damon won't always stay alive.
+ * Use the NSD APIs from NsdManager as the replacement is recommended.
+ * An another alternative could be bundling your own mdns solutions instead of
+ * depending on the system mdns native daemon.
+ *
+ * @hide
+ */
+ @ChangeId
+ @EnabledSince(targetSdkVersion = android.os.Build.VERSION_CODES.S)
+ public static final long RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS = 191844585L;
+
+ /**
+ * Broadcast intent action to indicate whether network service discovery is
+ * enabled or disabled. An extra {@link #EXTRA_NSD_STATE} provides the state
+ * information as int.
+ *
+ * @see #EXTRA_NSD_STATE
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_NSD_STATE_CHANGED = "android.net.nsd.STATE_CHANGED";
+
+ /**
+ * The lookup key for an int that indicates whether network service discovery is enabled
+ * or disabled. Retrieve it with {@link android.content.Intent#getIntExtra(String,int)}.
+ *
+ * @see #NSD_STATE_DISABLED
+ * @see #NSD_STATE_ENABLED
+ */
+ public static final String EXTRA_NSD_STATE = "nsd_state";
+
+ /**
+ * Network service discovery is disabled
+ *
+ * @see #ACTION_NSD_STATE_CHANGED
+ */
+ public static final int NSD_STATE_DISABLED = 1;
+
+ /**
+ * Network service discovery is enabled
+ *
+ * @see #ACTION_NSD_STATE_CHANGED
+ */
+ public static final int NSD_STATE_ENABLED = 2;
+
+ private static final int BASE = Protocol.BASE_NSD_MANAGER;
+
+ /** @hide */
+ public static final int DISCOVER_SERVICES = BASE + 1;
+ /** @hide */
+ public static final int DISCOVER_SERVICES_STARTED = BASE + 2;
+ /** @hide */
+ public static final int DISCOVER_SERVICES_FAILED = BASE + 3;
+ /** @hide */
+ public static final int SERVICE_FOUND = BASE + 4;
+ /** @hide */
+ public static final int SERVICE_LOST = BASE + 5;
+
+ /** @hide */
+ public static final int STOP_DISCOVERY = BASE + 6;
+ /** @hide */
+ public static final int STOP_DISCOVERY_FAILED = BASE + 7;
+ /** @hide */
+ public static final int STOP_DISCOVERY_SUCCEEDED = BASE + 8;
+
+ /** @hide */
+ public static final int REGISTER_SERVICE = BASE + 9;
+ /** @hide */
+ public static final int REGISTER_SERVICE_FAILED = BASE + 10;
+ /** @hide */
+ public static final int REGISTER_SERVICE_SUCCEEDED = BASE + 11;
+
+ /** @hide */
+ public static final int UNREGISTER_SERVICE = BASE + 12;
+ /** @hide */
+ public static final int UNREGISTER_SERVICE_FAILED = BASE + 13;
+ /** @hide */
+ public static final int UNREGISTER_SERVICE_SUCCEEDED = BASE + 14;
+
+ /** @hide */
+ public static final int RESOLVE_SERVICE = BASE + 18;
+ /** @hide */
+ public static final int RESOLVE_SERVICE_FAILED = BASE + 19;
+ /** @hide */
+ public static final int RESOLVE_SERVICE_SUCCEEDED = BASE + 20;
+
+ /** @hide */
+ public static final int DAEMON_CLEANUP = BASE + 21;
+
+ /** @hide */
+ public static final int DAEMON_STARTUP = BASE + 22;
+
+ /** @hide */
+ public static final int ENABLE = BASE + 24;
+ /** @hide */
+ public static final int DISABLE = BASE + 25;
+
+ /** @hide */
+ public static final int NATIVE_DAEMON_EVENT = BASE + 26;
+
+ /** @hide */
+ public static final int REGISTER_CLIENT = BASE + 27;
+ /** @hide */
+ public static final int UNREGISTER_CLIENT = BASE + 28;
+
+ /** Dns based service discovery protocol */
+ public static final int PROTOCOL_DNS_SD = 0x0001;
+
+ private static final SparseArray<String> EVENT_NAMES = new SparseArray<>();
+ static {
+ EVENT_NAMES.put(DISCOVER_SERVICES, "DISCOVER_SERVICES");
+ EVENT_NAMES.put(DISCOVER_SERVICES_STARTED, "DISCOVER_SERVICES_STARTED");
+ EVENT_NAMES.put(DISCOVER_SERVICES_FAILED, "DISCOVER_SERVICES_FAILED");
+ EVENT_NAMES.put(SERVICE_FOUND, "SERVICE_FOUND");
+ EVENT_NAMES.put(SERVICE_LOST, "SERVICE_LOST");
+ EVENT_NAMES.put(STOP_DISCOVERY, "STOP_DISCOVERY");
+ EVENT_NAMES.put(STOP_DISCOVERY_FAILED, "STOP_DISCOVERY_FAILED");
+ EVENT_NAMES.put(STOP_DISCOVERY_SUCCEEDED, "STOP_DISCOVERY_SUCCEEDED");
+ EVENT_NAMES.put(REGISTER_SERVICE, "REGISTER_SERVICE");
+ EVENT_NAMES.put(REGISTER_SERVICE_FAILED, "REGISTER_SERVICE_FAILED");
+ EVENT_NAMES.put(REGISTER_SERVICE_SUCCEEDED, "REGISTER_SERVICE_SUCCEEDED");
+ EVENT_NAMES.put(UNREGISTER_SERVICE, "UNREGISTER_SERVICE");
+ EVENT_NAMES.put(UNREGISTER_SERVICE_FAILED, "UNREGISTER_SERVICE_FAILED");
+ EVENT_NAMES.put(UNREGISTER_SERVICE_SUCCEEDED, "UNREGISTER_SERVICE_SUCCEEDED");
+ EVENT_NAMES.put(RESOLVE_SERVICE, "RESOLVE_SERVICE");
+ EVENT_NAMES.put(RESOLVE_SERVICE_FAILED, "RESOLVE_SERVICE_FAILED");
+ EVENT_NAMES.put(RESOLVE_SERVICE_SUCCEEDED, "RESOLVE_SERVICE_SUCCEEDED");
+ EVENT_NAMES.put(DAEMON_CLEANUP, "DAEMON_CLEANUP");
+ EVENT_NAMES.put(DAEMON_STARTUP, "DAEMON_STARTUP");
+ EVENT_NAMES.put(ENABLE, "ENABLE");
+ EVENT_NAMES.put(DISABLE, "DISABLE");
+ EVENT_NAMES.put(NATIVE_DAEMON_EVENT, "NATIVE_DAEMON_EVENT");
+ }
+
+ /** @hide */
+ public static String nameOf(int event) {
+ String name = EVENT_NAMES.get(event);
+ if (name == null) {
+ return Integer.toString(event);
+ }
+ return name;
+ }
+
+ private static final int FIRST_LISTENER_KEY = 1;
+
+ private final INsdServiceConnector mService;
+ private final Context mContext;
+
+ private int mListenerKey = FIRST_LISTENER_KEY;
+ private final SparseArray mListenerMap = new SparseArray();
+ private final SparseArray<NsdServiceInfo> mServiceMap = new SparseArray<>();
+ private final Object mMapLock = new Object();
+
+ private final ServiceHandler mHandler;
+
+ /**
+ * Create a new Nsd instance. Applications use
+ * {@link android.content.Context#getSystemService Context.getSystemService()} to retrieve
+ * {@link android.content.Context#NSD_SERVICE Context.NSD_SERVICE}.
+ * @param service the Binder interface
+ * @hide - hide this because it takes in a parameter of type INsdManager, which
+ * is a system private class.
+ */
+ public NsdManager(Context context, INsdManager service) {
+ mContext = context;
+
+ HandlerThread t = new HandlerThread("NsdManager");
+ t.start();
+ mHandler = new ServiceHandler(t.getLooper());
+
+ try {
+ mService = service.connect(new NsdCallbackImpl(mHandler));
+ } catch (RemoteException e) {
+ throw new RuntimeException("Failed to connect to NsdService");
+ }
+
+ // Only proactively start the daemon if the target SDK < S, otherwise the internal service
+ // would automatically start/stop the native daemon as needed.
+ if (!CompatChanges.isChangeEnabled(RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS)) {
+ try {
+ mService.startDaemon();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to proactively start daemon");
+ // Continue: the daemon can still be started on-demand later
+ }
+ }
+ }
+
+ private static class NsdCallbackImpl extends INsdManagerCallback.Stub {
+ private final Handler mServHandler;
+
+ NsdCallbackImpl(Handler serviceHandler) {
+ mServHandler = serviceHandler;
+ }
+
+ private void sendInfo(int message, int listenerKey, NsdServiceInfo info) {
+ mServHandler.sendMessage(mServHandler.obtainMessage(message, 0, listenerKey, info));
+ }
+
+ private void sendError(int message, int listenerKey, int error) {
+ mServHandler.sendMessage(mServHandler.obtainMessage(message, error, listenerKey));
+ }
+
+ private void sendNoArg(int message, int listenerKey) {
+ mServHandler.sendMessage(mServHandler.obtainMessage(message, 0, listenerKey));
+ }
+
+ @Override
+ public void onDiscoverServicesStarted(int listenerKey, NsdServiceInfo info) {
+ sendInfo(DISCOVER_SERVICES_STARTED, listenerKey, info);
+ }
+
+ @Override
+ public void onDiscoverServicesFailed(int listenerKey, int error) {
+ sendError(DISCOVER_SERVICES_FAILED, listenerKey, error);
+ }
+
+ @Override
+ public void onServiceFound(int listenerKey, NsdServiceInfo info) {
+ sendInfo(SERVICE_FOUND, listenerKey, info);
+ }
+
+ @Override
+ public void onServiceLost(int listenerKey, NsdServiceInfo info) {
+ sendInfo(SERVICE_LOST, listenerKey, info);
+ }
+
+ @Override
+ public void onStopDiscoveryFailed(int listenerKey, int error) {
+ sendError(STOP_DISCOVERY_FAILED, listenerKey, error);
+ }
+
+ @Override
+ public void onStopDiscoverySucceeded(int listenerKey) {
+ sendNoArg(STOP_DISCOVERY_SUCCEEDED, listenerKey);
+ }
+
+ @Override
+ public void onRegisterServiceFailed(int listenerKey, int error) {
+ sendError(REGISTER_SERVICE_FAILED, listenerKey, error);
+ }
+
+ @Override
+ public void onRegisterServiceSucceeded(int listenerKey, NsdServiceInfo info) {
+ sendInfo(REGISTER_SERVICE_SUCCEEDED, listenerKey, info);
+ }
+
+ @Override
+ public void onUnregisterServiceFailed(int listenerKey, int error) {
+ sendError(UNREGISTER_SERVICE_FAILED, listenerKey, error);
+ }
+
+ @Override
+ public void onUnregisterServiceSucceeded(int listenerKey) {
+ sendNoArg(UNREGISTER_SERVICE_SUCCEEDED, listenerKey);
+ }
+
+ @Override
+ public void onResolveServiceFailed(int listenerKey, int error) {
+ sendError(RESOLVE_SERVICE_FAILED, listenerKey, error);
+ }
+
+ @Override
+ public void onResolveServiceSucceeded(int listenerKey, NsdServiceInfo info) {
+ sendInfo(RESOLVE_SERVICE_SUCCEEDED, listenerKey, info);
+ }
+ }
+
+ /**
+ * Failures are passed with {@link RegistrationListener#onRegistrationFailed},
+ * {@link RegistrationListener#onUnregistrationFailed},
+ * {@link DiscoveryListener#onStartDiscoveryFailed},
+ * {@link DiscoveryListener#onStopDiscoveryFailed} or {@link ResolveListener#onResolveFailed}.
+ *
+ * Indicates that the operation failed due to an internal error.
+ */
+ public static final int FAILURE_INTERNAL_ERROR = 0;
+
+ /**
+ * Indicates that the operation failed because it is already active.
+ */
+ public static final int FAILURE_ALREADY_ACTIVE = 3;
+
+ /**
+ * Indicates that the operation failed because the maximum outstanding
+ * requests from the applications have reached.
+ */
+ public static final int FAILURE_MAX_LIMIT = 4;
+
+ /** Interface for callback invocation for service discovery */
+ public interface DiscoveryListener {
+
+ public void onStartDiscoveryFailed(String serviceType, int errorCode);
+
+ public void onStopDiscoveryFailed(String serviceType, int errorCode);
+
+ public void onDiscoveryStarted(String serviceType);
+
+ public void onDiscoveryStopped(String serviceType);
+
+ public void onServiceFound(NsdServiceInfo serviceInfo);
+
+ public void onServiceLost(NsdServiceInfo serviceInfo);
+ }
+
+ /** Interface for callback invocation for service registration */
+ public interface RegistrationListener {
+
+ public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode);
+
+ public void onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode);
+
+ public void onServiceRegistered(NsdServiceInfo serviceInfo);
+
+ public void onServiceUnregistered(NsdServiceInfo serviceInfo);
+ }
+
+ /** Interface for callback invocation for service resolution */
+ public interface ResolveListener {
+
+ public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode);
+
+ public void onServiceResolved(NsdServiceInfo serviceInfo);
+ }
+
+ @VisibleForTesting
+ class ServiceHandler extends Handler {
+ ServiceHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message message) {
+ final int what = message.what;
+ final int key = message.arg2;
+ final Object listener;
+ final NsdServiceInfo ns;
+ synchronized (mMapLock) {
+ listener = mListenerMap.get(key);
+ ns = mServiceMap.get(key);
+ }
+ if (listener == null) {
+ Log.d(TAG, "Stale key " + message.arg2);
+ return;
+ }
+ if (DBG) {
+ Log.d(TAG, "received " + nameOf(what) + " for key " + key + ", service " + ns);
+ }
+ switch (what) {
+ case DISCOVER_SERVICES_STARTED:
+ String s = getNsdServiceInfoType((NsdServiceInfo) message.obj);
+ ((DiscoveryListener) listener).onDiscoveryStarted(s);
+ break;
+ case DISCOVER_SERVICES_FAILED:
+ removeListener(key);
+ ((DiscoveryListener) listener).onStartDiscoveryFailed(getNsdServiceInfoType(ns),
+ message.arg1);
+ break;
+ case SERVICE_FOUND:
+ ((DiscoveryListener) listener).onServiceFound((NsdServiceInfo) message.obj);
+ break;
+ case SERVICE_LOST:
+ ((DiscoveryListener) listener).onServiceLost((NsdServiceInfo) message.obj);
+ break;
+ case STOP_DISCOVERY_FAILED:
+ // TODO: failure to stop discovery should be internal and retried internally, as
+ // the effect for the client is indistinguishable from STOP_DISCOVERY_SUCCEEDED
+ removeListener(key);
+ ((DiscoveryListener) listener).onStopDiscoveryFailed(getNsdServiceInfoType(ns),
+ message.arg1);
+ break;
+ case STOP_DISCOVERY_SUCCEEDED:
+ removeListener(key);
+ ((DiscoveryListener) listener).onDiscoveryStopped(getNsdServiceInfoType(ns));
+ break;
+ case REGISTER_SERVICE_FAILED:
+ removeListener(key);
+ ((RegistrationListener) listener).onRegistrationFailed(ns, message.arg1);
+ break;
+ case REGISTER_SERVICE_SUCCEEDED:
+ ((RegistrationListener) listener).onServiceRegistered(
+ (NsdServiceInfo) message.obj);
+ break;
+ case UNREGISTER_SERVICE_FAILED:
+ removeListener(key);
+ ((RegistrationListener) listener).onUnregistrationFailed(ns, message.arg1);
+ break;
+ case UNREGISTER_SERVICE_SUCCEEDED:
+ // TODO: do not unregister listener until service is unregistered, or provide
+ // alternative way for unregistering ?
+ removeListener(message.arg2);
+ ((RegistrationListener) listener).onServiceUnregistered(ns);
+ break;
+ case RESOLVE_SERVICE_FAILED:
+ removeListener(key);
+ ((ResolveListener) listener).onResolveFailed(ns, message.arg1);
+ break;
+ case RESOLVE_SERVICE_SUCCEEDED:
+ removeListener(key);
+ ((ResolveListener) listener).onServiceResolved((NsdServiceInfo) message.obj);
+ break;
+ default:
+ Log.d(TAG, "Ignored " + message);
+ break;
+ }
+ }
+ }
+
+ private int nextListenerKey() {
+ // Ensure mListenerKey >= FIRST_LISTENER_KEY;
+ mListenerKey = Math.max(FIRST_LISTENER_KEY, mListenerKey + 1);
+ return mListenerKey;
+ }
+
+ // Assert that the listener is not in the map, then add it and returns its key
+ private int putListener(Object listener, NsdServiceInfo s) {
+ checkListener(listener);
+ final int key;
+ synchronized (mMapLock) {
+ int valueIndex = mListenerMap.indexOfValue(listener);
+ checkArgument(valueIndex == -1, "listener already in use");
+ key = nextListenerKey();
+ mListenerMap.put(key, listener);
+ mServiceMap.put(key, s);
+ }
+ return key;
+ }
+
+ private void removeListener(int key) {
+ synchronized (mMapLock) {
+ mListenerMap.remove(key);
+ mServiceMap.remove(key);
+ }
+ }
+
+ private int getListenerKey(Object listener) {
+ checkListener(listener);
+ synchronized (mMapLock) {
+ int valueIndex = mListenerMap.indexOfValue(listener);
+ checkArgument(valueIndex != -1, "listener not registered");
+ return mListenerMap.keyAt(valueIndex);
+ }
+ }
+
+ private static String getNsdServiceInfoType(NsdServiceInfo s) {
+ if (s == null) return "?";
+ return s.getServiceType();
+ }
+
+ /**
+ * Register a service to be discovered by other services.
+ *
+ * <p> The function call immediately returns after sending a request to register service
+ * to the framework. The application is notified of a successful registration
+ * through the callback {@link RegistrationListener#onServiceRegistered} or a failure
+ * through {@link RegistrationListener#onRegistrationFailed}.
+ *
+ * <p> The application should call {@link #unregisterService} when the service
+ * registration is no longer required, and/or whenever the application is stopped.
+ *
+ * @param serviceInfo The service being registered
+ * @param protocolType The service discovery protocol
+ * @param listener The listener notifies of a successful registration and is used to
+ * unregister this service through a call on {@link #unregisterService}. Cannot be null.
+ * Cannot be in use for an active service registration.
+ */
+ public void registerService(NsdServiceInfo serviceInfo, int protocolType,
+ RegistrationListener listener) {
+ checkArgument(serviceInfo.getPort() > 0, "Invalid port number");
+ checkServiceInfo(serviceInfo);
+ checkProtocol(protocolType);
+ int key = putListener(listener, serviceInfo);
+ try {
+ mService.registerService(key, serviceInfo);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Unregister a service registered through {@link #registerService}. A successful
+ * unregister is notified to the application with a call to
+ * {@link RegistrationListener#onServiceUnregistered}.
+ *
+ * @param listener This should be the listener object that was passed to
+ * {@link #registerService}. It identifies the service that should be unregistered
+ * and notifies of a successful or unsuccessful unregistration via the listener
+ * callbacks. In API versions 20 and above, the listener object may be used for
+ * another service registration once the callback has been called. In API versions <= 19,
+ * there is no entirely reliable way to know when a listener may be re-used, and a new
+ * listener should be created for each service registration request.
+ */
+ public void unregisterService(RegistrationListener listener) {
+ int id = getListenerKey(listener);
+ try {
+ mService.unregisterService(id);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Initiate service discovery to browse for instances of a service type. Service discovery
+ * consumes network bandwidth and will continue until the application calls
+ * {@link #stopServiceDiscovery}.
+ *
+ * <p> The function call immediately returns after sending a request to start service
+ * discovery to the framework. The application is notified of a success to initiate
+ * discovery through the callback {@link DiscoveryListener#onDiscoveryStarted} or a failure
+ * through {@link DiscoveryListener#onStartDiscoveryFailed}.
+ *
+ * <p> Upon successful start, application is notified when a service is found with
+ * {@link DiscoveryListener#onServiceFound} or when a service is lost with
+ * {@link DiscoveryListener#onServiceLost}.
+ *
+ * <p> Upon failure to start, service discovery is not active and application does
+ * not need to invoke {@link #stopServiceDiscovery}
+ *
+ * <p> The application should call {@link #stopServiceDiscovery} when discovery of this
+ * service type is no longer required, and/or whenever the application is paused or
+ * stopped.
+ *
+ * @param serviceType The service type being discovered. Examples include "_http._tcp" for
+ * http services or "_ipp._tcp" for printers
+ * @param protocolType The service discovery protocol
+ * @param listener The listener notifies of a successful discovery and is used
+ * to stop discovery on this serviceType through a call on {@link #stopServiceDiscovery}.
+ * Cannot be null. Cannot be in use for an active service discovery.
+ */
+ public void discoverServices(String serviceType, int protocolType, DiscoveryListener listener) {
+ checkStringNotEmpty(serviceType, "Service type cannot be empty");
+ checkProtocol(protocolType);
+
+ NsdServiceInfo s = new NsdServiceInfo();
+ s.setServiceType(serviceType);
+
+ int key = putListener(listener, s);
+ try {
+ mService.discoverServices(key, s);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Stop service discovery initiated with {@link #discoverServices}. An active service
+ * discovery is notified to the application with {@link DiscoveryListener#onDiscoveryStarted}
+ * and it stays active until the application invokes a stop service discovery. A successful
+ * stop is notified to with a call to {@link DiscoveryListener#onDiscoveryStopped}.
+ *
+ * <p> Upon failure to stop service discovery, application is notified through
+ * {@link DiscoveryListener#onStopDiscoveryFailed}.
+ *
+ * @param listener This should be the listener object that was passed to {@link #discoverServices}.
+ * It identifies the discovery that should be stopped and notifies of a successful or
+ * unsuccessful stop. In API versions 20 and above, the listener object may be used for
+ * another service discovery once the callback has been called. In API versions <= 19,
+ * there is no entirely reliable way to know when a listener may be re-used, and a new
+ * listener should be created for each service discovery request.
+ */
+ public void stopServiceDiscovery(DiscoveryListener listener) {
+ int id = getListenerKey(listener);
+ try {
+ mService.stopDiscovery(id);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Resolve a discovered service. An application can resolve a service right before
+ * establishing a connection to fetch the IP and port details on which to setup
+ * the connection.
+ *
+ * @param serviceInfo service to be resolved
+ * @param listener to receive callback upon success or failure. Cannot be null.
+ * Cannot be in use for an active service resolution.
+ */
+ public void resolveService(NsdServiceInfo serviceInfo, ResolveListener listener) {
+ checkServiceInfo(serviceInfo);
+ int key = putListener(listener, serviceInfo);
+ try {
+ mService.resolveService(key, serviceInfo);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ private static void checkListener(Object listener) {
+ checkNotNull(listener, "listener cannot be null");
+ }
+
+ private static void checkProtocol(int protocolType) {
+ checkArgument(protocolType == PROTOCOL_DNS_SD, "Unsupported protocol");
+ }
+
+ private static void checkServiceInfo(NsdServiceInfo serviceInfo) {
+ checkNotNull(serviceInfo, "NsdServiceInfo cannot be null");
+ checkStringNotEmpty(serviceInfo.getServiceName(), "Service name cannot be empty");
+ checkStringNotEmpty(serviceInfo.getServiceType(), "Service type cannot be empty");
+ }
+}
diff --git a/packages/Nsd/framework/src/android/net/nsd/NsdServiceInfo.java b/packages/Nsd/framework/src/android/net/nsd/NsdServiceInfo.java
new file mode 100644
index 0000000..0946499
--- /dev/null
+++ b/packages/Nsd/framework/src/android/net/nsd/NsdServiceInfo.java
@@ -0,0 +1,391 @@
+/*
+ * Copyright (C) 2012 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 android.net.nsd;
+
+import android.annotation.NonNull;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.Base64;
+import android.util.Log;
+
+import java.io.UnsupportedEncodingException;
+import java.net.InetAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * A class representing service information for network service discovery
+ * {@see NsdManager}
+ */
+public final class NsdServiceInfo implements Parcelable {
+
+ private static final String TAG = "NsdServiceInfo";
+
+ private String mServiceName;
+
+ private String mServiceType;
+
+ private final ArrayMap<String, byte[]> mTxtRecord = new ArrayMap<>();
+
+ private InetAddress mHost;
+
+ private int mPort;
+
+ public NsdServiceInfo() {
+ }
+
+ /** @hide */
+ public NsdServiceInfo(String sn, String rt) {
+ mServiceName = sn;
+ mServiceType = rt;
+ }
+
+ /** Get the service name */
+ public String getServiceName() {
+ return mServiceName;
+ }
+
+ /** Set the service name */
+ public void setServiceName(String s) {
+ mServiceName = s;
+ }
+
+ /** Get the service type */
+ public String getServiceType() {
+ return mServiceType;
+ }
+
+ /** Set the service type */
+ public void setServiceType(String s) {
+ mServiceType = s;
+ }
+
+ /** Get the host address. The host address is valid for a resolved service. */
+ public InetAddress getHost() {
+ return mHost;
+ }
+
+ /** Set the host address */
+ public void setHost(InetAddress s) {
+ mHost = s;
+ }
+
+ /** Get port number. The port number is valid for a resolved service. */
+ public int getPort() {
+ return mPort;
+ }
+
+ /** Set port number */
+ public void setPort(int p) {
+ mPort = p;
+ }
+
+ /**
+ * Unpack txt information from a base-64 encoded byte array.
+ *
+ * @param rawRecords The raw base64 encoded records string read from netd.
+ *
+ * @hide
+ */
+ public void setTxtRecords(@NonNull String rawRecords) {
+ byte[] txtRecordsRawBytes = Base64.decode(rawRecords, Base64.DEFAULT);
+
+ // There can be multiple TXT records after each other. Each record has to following format:
+ //
+ // byte type required meaning
+ // ------------------- ------------------- -------- ----------------------------------
+ // 0 unsigned 8 bit yes size of record excluding this byte
+ // 1 - n ASCII but not '=' yes key
+ // n + 1 '=' optional separator of key and value
+ // n + 2 - record size uninterpreted bytes optional value
+ //
+ // Example legal records:
+ // [11, 'm', 'y', 'k', 'e', 'y', '=', 0x0, 0x4, 0x65, 0x7, 0xff]
+ // [17, 'm', 'y', 'K', 'e', 'y', 'W', 'i', 't', 'h', 'N', 'o', 'V', 'a', 'l', 'u', 'e', '=']
+ // [12, 'm', 'y', 'B', 'o', 'o', 'l', 'e', 'a', 'n', 'K', 'e', 'y']
+ //
+ // Example corrupted records
+ // [3, =, 1, 2] <- key is empty
+ // [3, 0, =, 2] <- key contains non-ASCII character. We handle this by replacing the
+ // invalid characters instead of skipping the record.
+ // [30, 'a', =, 2] <- length exceeds total left over bytes in the TXT records array, we
+ // handle this by reducing the length of the record as needed.
+ int pos = 0;
+ while (pos < txtRecordsRawBytes.length) {
+ // recordLen is an unsigned 8 bit value
+ int recordLen = txtRecordsRawBytes[pos] & 0xff;
+ pos += 1;
+
+ try {
+ if (recordLen == 0) {
+ throw new IllegalArgumentException("Zero sized txt record");
+ } else if (pos + recordLen > txtRecordsRawBytes.length) {
+ Log.w(TAG, "Corrupt record length (pos = " + pos + "): " + recordLen);
+ recordLen = txtRecordsRawBytes.length - pos;
+ }
+
+ // Decode key-value records
+ String key = null;
+ byte[] value = null;
+ int valueLen = 0;
+ for (int i = pos; i < pos + recordLen; i++) {
+ if (key == null) {
+ if (txtRecordsRawBytes[i] == '=') {
+ key = new String(txtRecordsRawBytes, pos, i - pos,
+ StandardCharsets.US_ASCII);
+ }
+ } else {
+ if (value == null) {
+ value = new byte[recordLen - key.length() - 1];
+ }
+ value[valueLen] = txtRecordsRawBytes[i];
+ valueLen++;
+ }
+ }
+
+ // If '=' was not found we have a boolean record
+ if (key == null) {
+ key = new String(txtRecordsRawBytes, pos, recordLen, StandardCharsets.US_ASCII);
+ }
+
+ if (TextUtils.isEmpty(key)) {
+ // Empty keys are not allowed (RFC6763 6.4)
+ throw new IllegalArgumentException("Invalid txt record (key is empty)");
+ }
+
+ if (getAttributes().containsKey(key)) {
+ // When we have a duplicate record, the later ones are ignored (RFC6763 6.4)
+ throw new IllegalArgumentException("Invalid txt record (duplicate key \"" + key + "\")");
+ }
+
+ setAttribute(key, value);
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "While parsing txt records (pos = " + pos + "): " + e.getMessage());
+ }
+
+ pos += recordLen;
+ }
+ }
+
+ /** @hide */
+ @UnsupportedAppUsage
+ public void setAttribute(String key, byte[] value) {
+ if (TextUtils.isEmpty(key)) {
+ throw new IllegalArgumentException("Key cannot be empty");
+ }
+
+ // Key must be printable US-ASCII, excluding =.
+ for (int i = 0; i < key.length(); ++i) {
+ char character = key.charAt(i);
+ if (character < 0x20 || character > 0x7E) {
+ throw new IllegalArgumentException("Key strings must be printable US-ASCII");
+ } else if (character == 0x3D) {
+ throw new IllegalArgumentException("Key strings must not include '='");
+ }
+ }
+
+ // Key length + value length must be < 255.
+ if (key.length() + (value == null ? 0 : value.length) >= 255) {
+ throw new IllegalArgumentException("Key length + value length must be < 255 bytes");
+ }
+
+ // Warn if key is > 9 characters, as recommended by RFC 6763 section 6.4.
+ if (key.length() > 9) {
+ Log.w(TAG, "Key lengths > 9 are discouraged: " + key);
+ }
+
+ // Check against total TXT record size limits.
+ // Arbitrary 400 / 1300 byte limits taken from RFC 6763 section 6.2.
+ int txtRecordSize = getTxtRecordSize();
+ int futureSize = txtRecordSize + key.length() + (value == null ? 0 : value.length) + 2;
+ if (futureSize > 1300) {
+ throw new IllegalArgumentException("Total length of attributes must be < 1300 bytes");
+ } else if (futureSize > 400) {
+ Log.w(TAG, "Total length of all attributes exceeds 400 bytes; truncation may occur");
+ }
+
+ mTxtRecord.put(key, value);
+ }
+
+ /**
+ * Add a service attribute as a key/value pair.
+ *
+ * <p> Service attributes are included as DNS-SD TXT record pairs.
+ *
+ * <p> The key must be US-ASCII printable characters, excluding the '=' character. Values may
+ * be UTF-8 strings or null. The total length of key + value must be less than 255 bytes.
+ *
+ * <p> Keys should be short, ideally no more than 9 characters, and unique per instance of
+ * {@link NsdServiceInfo}. Calling {@link #setAttribute} twice with the same key will overwrite
+ * first value.
+ */
+ public void setAttribute(String key, String value) {
+ try {
+ setAttribute(key, value == null ? (byte []) null : value.getBytes("UTF-8"));
+ } catch (UnsupportedEncodingException e) {
+ throw new IllegalArgumentException("Value must be UTF-8");
+ }
+ }
+
+ /** Remove an attribute by key */
+ public void removeAttribute(String key) {
+ mTxtRecord.remove(key);
+ }
+
+ /**
+ * Retrieve attributes as a map of String keys to byte[] values. The attributes map is only
+ * valid for a resolved service.
+ *
+ * <p> The returned map is unmodifiable; changes must be made through {@link #setAttribute} and
+ * {@link #removeAttribute}.
+ */
+ public Map<String, byte[]> getAttributes() {
+ return Collections.unmodifiableMap(mTxtRecord);
+ }
+
+ private int getTxtRecordSize() {
+ int txtRecordSize = 0;
+ for (Map.Entry<String, byte[]> entry : mTxtRecord.entrySet()) {
+ txtRecordSize += 2; // One for the length byte, one for the = between key and value.
+ txtRecordSize += entry.getKey().length();
+ byte[] value = entry.getValue();
+ txtRecordSize += value == null ? 0 : value.length;
+ }
+ return txtRecordSize;
+ }
+
+ /** @hide */
+ public @NonNull byte[] getTxtRecord() {
+ int txtRecordSize = getTxtRecordSize();
+ if (txtRecordSize == 0) {
+ return new byte[]{};
+ }
+
+ byte[] txtRecord = new byte[txtRecordSize];
+ int ptr = 0;
+ for (Map.Entry<String, byte[]> entry : mTxtRecord.entrySet()) {
+ String key = entry.getKey();
+ byte[] value = entry.getValue();
+
+ // One byte to record the length of this key/value pair.
+ txtRecord[ptr++] = (byte) (key.length() + (value == null ? 0 : value.length) + 1);
+
+ // The key, in US-ASCII.
+ // Note: use the StandardCharsets const here because it doesn't raise exceptions and we
+ // already know the key is ASCII at this point.
+ System.arraycopy(key.getBytes(StandardCharsets.US_ASCII), 0, txtRecord, ptr,
+ key.length());
+ ptr += key.length();
+
+ // US-ASCII '=' character.
+ txtRecord[ptr++] = (byte)'=';
+
+ // The value, as any raw bytes.
+ if (value != null) {
+ System.arraycopy(value, 0, txtRecord, ptr, value.length);
+ ptr += value.length;
+ }
+ }
+ return txtRecord;
+ }
+
+ public String toString() {
+ StringBuffer sb = new StringBuffer();
+
+ sb.append("name: ").append(mServiceName)
+ .append(", type: ").append(mServiceType)
+ .append(", host: ").append(mHost)
+ .append(", port: ").append(mPort);
+
+ byte[] txtRecord = getTxtRecord();
+ if (txtRecord != null) {
+ sb.append(", txtRecord: ").append(new String(txtRecord, StandardCharsets.UTF_8));
+ }
+ return sb.toString();
+ }
+
+ /** Implement the Parcelable interface */
+ public int describeContents() {
+ return 0;
+ }
+
+ /** Implement the Parcelable interface */
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mServiceName);
+ dest.writeString(mServiceType);
+ if (mHost != null) {
+ dest.writeInt(1);
+ dest.writeByteArray(mHost.getAddress());
+ } else {
+ dest.writeInt(0);
+ }
+ dest.writeInt(mPort);
+
+ // TXT record key/value pairs.
+ dest.writeInt(mTxtRecord.size());
+ for (String key : mTxtRecord.keySet()) {
+ byte[] value = mTxtRecord.get(key);
+ if (value != null) {
+ dest.writeInt(1);
+ dest.writeInt(value.length);
+ dest.writeByteArray(value);
+ } else {
+ dest.writeInt(0);
+ }
+ dest.writeString(key);
+ }
+ }
+
+ /** Implement the Parcelable interface */
+ public static final @android.annotation.NonNull Creator<NsdServiceInfo> CREATOR =
+ new Creator<NsdServiceInfo>() {
+ public NsdServiceInfo createFromParcel(Parcel in) {
+ NsdServiceInfo info = new NsdServiceInfo();
+ info.mServiceName = in.readString();
+ info.mServiceType = in.readString();
+
+ if (in.readInt() == 1) {
+ try {
+ info.mHost = InetAddress.getByAddress(in.createByteArray());
+ } catch (java.net.UnknownHostException e) {}
+ }
+
+ info.mPort = in.readInt();
+
+ // TXT record key/value pairs.
+ int recordCount = in.readInt();
+ for (int i = 0; i < recordCount; ++i) {
+ byte[] valueArray = null;
+ if (in.readInt() == 1) {
+ int valueLength = in.readInt();
+ valueArray = new byte[valueLength];
+ in.readByteArray(valueArray);
+ }
+ info.mTxtRecord.put(in.readString(), valueArray);
+ }
+ return info;
+ }
+
+ public NsdServiceInfo[] newArray(int size) {
+ return new NsdServiceInfo[size];
+ }
+ };
+}
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);
+ }
+ };
+ }
+ }
+}
diff --git a/packages/Nsd/tests/unit/java/com/android/server/NativeDaemonConnectorTest.java b/packages/Nsd/tests/unit/java/com/android/server/NativeDaemonConnectorTest.java
new file mode 100644
index 0000000..e2253a2
--- /dev/null
+++ b/packages/Nsd/tests/unit/java/com/android/server/NativeDaemonConnectorTest.java
@@ -0,0 +1,97 @@
+/*
+ * 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 static com.android.server.NativeDaemonConnector.appendEscaped;
+import static com.android.server.NativeDaemonConnector.makeCommand;
+
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+
+import com.android.server.NativeDaemonConnector.SensitiveArg;
+
+/**
+ * Tests for {@link NativeDaemonConnector}.
+ */
+@MediumTest
+public class NativeDaemonConnectorTest extends AndroidTestCase {
+ private static final String TAG = "NativeDaemonConnectorTest";
+
+ public void testArgumentNormal() throws Exception {
+ final StringBuilder builder = new StringBuilder();
+
+ builder.setLength(0);
+ appendEscaped(builder, "");
+ assertEquals("", builder.toString());
+
+ builder.setLength(0);
+ appendEscaped(builder, "foo");
+ assertEquals("foo", builder.toString());
+
+ builder.setLength(0);
+ appendEscaped(builder, "foo\"bar");
+ assertEquals("foo\\\"bar", builder.toString());
+
+ builder.setLength(0);
+ appendEscaped(builder, "foo\\bar\\\"baz");
+ assertEquals("foo\\\\bar\\\\\\\"baz", builder.toString());
+ }
+
+ public void testArgumentWithSpaces() throws Exception {
+ final StringBuilder builder = new StringBuilder();
+
+ builder.setLength(0);
+ appendEscaped(builder, "foo bar");
+ assertEquals("\"foo bar\"", builder.toString());
+
+ builder.setLength(0);
+ appendEscaped(builder, "foo\"bar\\baz foo");
+ assertEquals("\"foo\\\"bar\\\\baz foo\"", builder.toString());
+ }
+
+ public void testArgumentWithUtf() throws Exception {
+ final StringBuilder builder = new StringBuilder();
+
+ builder.setLength(0);
+ appendEscaped(builder, "caf\u00E9 c\u00F6ffee");
+ assertEquals("\"caf\u00E9 c\u00F6ffee\"", builder.toString());
+ }
+
+ public void testSensitiveArgs() throws Exception {
+ final StringBuilder rawBuilder = new StringBuilder();
+ final StringBuilder logBuilder = new StringBuilder();
+
+ rawBuilder.setLength(0);
+ logBuilder.setLength(0);
+ makeCommand(rawBuilder, logBuilder, 1, "foo", "bar", "baz");
+ assertEquals("1 foo bar baz\0", rawBuilder.toString());
+ assertEquals("1 foo bar baz", logBuilder.toString());
+
+ rawBuilder.setLength(0);
+ logBuilder.setLength(0);
+ makeCommand(rawBuilder, logBuilder, 1, "foo", new SensitiveArg("bar"), "baz");
+ assertEquals("1 foo bar baz\0", rawBuilder.toString());
+ assertEquals("1 foo [scrubbed] baz", logBuilder.toString());
+
+ rawBuilder.setLength(0);
+ logBuilder.setLength(0);
+ makeCommand(rawBuilder, logBuilder, 1, "foo", new SensitiveArg("foo bar"), "baz baz",
+ new SensitiveArg("wat"));
+ assertEquals("1 foo \"foo bar\" \"baz baz\" wat\0", rawBuilder.toString());
+ assertEquals("1 foo [scrubbed] \"baz baz\" [scrubbed]", logBuilder.toString());
+ }
+}