New API to listen service update
Currently, the resolution is a one shot query, it only notifies
the first finding service information. There is no way to listen
the service update. Thus, add a new API that can register a
callback to listen to the service updates continuously.
Bug: 245369943
Test: atest FrameworksNetTests CtsNetTestCases
Change-Id: I0e9d92b9028375feb3e344ab6c4acb515c5b2be9
diff --git a/framework-t/api/current.txt b/framework-t/api/current.txt
index ed841b8..5532853 100644
--- a/framework-t/api/current.txt
+++ b/framework-t/api/current.txt
@@ -192,14 +192,17 @@
method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void discoverServices(@NonNull String, int, @NonNull android.net.NetworkRequest, @NonNull java.util.concurrent.Executor, @NonNull android.net.nsd.NsdManager.DiscoveryListener);
method public void registerService(android.net.nsd.NsdServiceInfo, int, android.net.nsd.NsdManager.RegistrationListener);
method public void registerService(@NonNull android.net.nsd.NsdServiceInfo, int, @NonNull java.util.concurrent.Executor, @NonNull android.net.nsd.NsdManager.RegistrationListener);
- method public void resolveService(android.net.nsd.NsdServiceInfo, android.net.nsd.NsdManager.ResolveListener);
- method public void resolveService(@NonNull android.net.nsd.NsdServiceInfo, @NonNull java.util.concurrent.Executor, @NonNull android.net.nsd.NsdManager.ResolveListener);
+ method public void registerServiceInfoCallback(@NonNull android.net.nsd.NsdServiceInfo, @NonNull java.util.concurrent.Executor, @NonNull android.net.nsd.NsdManager.ServiceInfoCallback);
+ method @Deprecated public void resolveService(android.net.nsd.NsdServiceInfo, android.net.nsd.NsdManager.ResolveListener);
+ method @Deprecated public void resolveService(@NonNull android.net.nsd.NsdServiceInfo, @NonNull java.util.concurrent.Executor, @NonNull android.net.nsd.NsdManager.ResolveListener);
method public void stopServiceDiscovery(android.net.nsd.NsdManager.DiscoveryListener);
method public void stopServiceResolution(@NonNull android.net.nsd.NsdManager.ResolveListener);
method public void unregisterService(android.net.nsd.NsdManager.RegistrationListener);
+ method public void unregisterServiceInfoCallback(@NonNull android.net.nsd.NsdManager.ServiceInfoCallback);
field public static final String ACTION_NSD_STATE_CHANGED = "android.net.nsd.STATE_CHANGED";
field public static final String EXTRA_NSD_STATE = "nsd_state";
field public static final int FAILURE_ALREADY_ACTIVE = 3; // 0x3
+ field public static final int FAILURE_BAD_PARAMETERS = 6; // 0x6
field public static final int FAILURE_INTERNAL_ERROR = 0; // 0x0
field public static final int FAILURE_MAX_LIMIT = 4; // 0x4
field public static final int FAILURE_OPERATION_NOT_RUNNING = 5; // 0x5
@@ -231,18 +234,27 @@
method public default void onStopResolutionFailed(@NonNull android.net.nsd.NsdServiceInfo, int);
}
+ public static interface NsdManager.ServiceInfoCallback {
+ method public void onServiceInfoCallbackRegistrationFailed(int);
+ method public void onServiceInfoCallbackUnregistered();
+ method public void onServiceLost();
+ method public void onServiceUpdated(@NonNull android.net.nsd.NsdServiceInfo);
+ }
+
public final class NsdServiceInfo implements android.os.Parcelable {
ctor public NsdServiceInfo();
method public int describeContents();
method public java.util.Map<java.lang.String,byte[]> getAttributes();
- method public java.net.InetAddress getHost();
+ method @Deprecated public java.net.InetAddress getHost();
+ method @NonNull public java.util.List<java.net.InetAddress> getHostAddresses();
method @Nullable public android.net.Network getNetwork();
method public int getPort();
method public String getServiceName();
method public String getServiceType();
method public void removeAttribute(String);
method public void setAttribute(String, String);
- method public void setHost(java.net.InetAddress);
+ method @Deprecated public void setHost(java.net.InetAddress);
+ method public void setHostAddresses(@NonNull java.util.List<java.net.InetAddress>);
method public void setNetwork(@Nullable android.net.Network);
method public void setPort(int);
method public void setServiceName(String);
diff --git a/framework-t/src/android/net/nsd/INsdManagerCallback.aidl b/framework-t/src/android/net/nsd/INsdManagerCallback.aidl
index 669efc9..d89bfa9 100644
--- a/framework-t/src/android/net/nsd/INsdManagerCallback.aidl
+++ b/framework-t/src/android/net/nsd/INsdManagerCallback.aidl
@@ -38,4 +38,8 @@
void onResolveServiceSucceeded(int listenerKey, in NsdServiceInfo info);
void onStopResolutionFailed(int listenerKey, int error);
void onStopResolutionSucceeded(int listenerKey);
+ void onServiceInfoCallbackRegistrationFailed(int listenerKey, int error);
+ void onServiceUpdated(int listenerKey, in NsdServiceInfo info);
+ void onServiceUpdatedLost(int listenerKey);
+ void onServiceInfoCallbackUnregistered(int listenerKey);
}
diff --git a/framework-t/src/android/net/nsd/INsdServiceConnector.aidl b/framework-t/src/android/net/nsd/INsdServiceConnector.aidl
index a28fd7d..5533154 100644
--- a/framework-t/src/android/net/nsd/INsdServiceConnector.aidl
+++ b/framework-t/src/android/net/nsd/INsdServiceConnector.aidl
@@ -33,4 +33,6 @@
void resolveService(int listenerKey, in NsdServiceInfo serviceInfo);
void startDaemon();
void stopResolution(int listenerKey);
+ void registerServiceInfoCallback(int listenerKey, in NsdServiceInfo serviceInfo);
+ void unregisterServiceInfoCallback(int listenerKey);
}
\ No newline at end of file
diff --git a/framework-t/src/android/net/nsd/NsdManager.java b/framework-t/src/android/net/nsd/NsdManager.java
index 1a5a667..122e3a0 100644
--- a/framework-t/src/android/net/nsd/NsdManager.java
+++ b/framework-t/src/android/net/nsd/NsdManager.java
@@ -254,6 +254,20 @@
/** @hide */
public static final int STOP_RESOLUTION_SUCCEEDED = 26;
+ /** @hide */
+ public static final int REGISTER_SERVICE_CALLBACK = 27;
+ /** @hide */
+ public static final int REGISTER_SERVICE_CALLBACK_FAILED = 28;
+ /** @hide */
+ public static final int SERVICE_UPDATED = 29;
+ /** @hide */
+ public static final int SERVICE_UPDATED_LOST = 30;
+
+ /** @hide */
+ public static final int UNREGISTER_SERVICE_CALLBACK = 31;
+ /** @hide */
+ public static final int UNREGISTER_SERVICE_CALLBACK_SUCCEEDED = 32;
+
/** Dns based service discovery protocol */
public static final int PROTOCOL_DNS_SD = 0x0001;
@@ -282,6 +296,12 @@
EVENT_NAMES.put(STOP_RESOLUTION, "STOP_RESOLUTION");
EVENT_NAMES.put(STOP_RESOLUTION_FAILED, "STOP_RESOLUTION_FAILED");
EVENT_NAMES.put(STOP_RESOLUTION_SUCCEEDED, "STOP_RESOLUTION_SUCCEEDED");
+ EVENT_NAMES.put(REGISTER_SERVICE_CALLBACK, "REGISTER_SERVICE_CALLBACK");
+ EVENT_NAMES.put(REGISTER_SERVICE_CALLBACK_FAILED, "REGISTER_SERVICE_CALLBACK_FAILED");
+ EVENT_NAMES.put(SERVICE_UPDATED, "SERVICE_UPDATED");
+ EVENT_NAMES.put(UNREGISTER_SERVICE_CALLBACK, "UNREGISTER_SERVICE_CALLBACK");
+ EVENT_NAMES.put(UNREGISTER_SERVICE_CALLBACK_SUCCEEDED,
+ "UNREGISTER_SERVICE_CALLBACK_SUCCEEDED");
}
/** @hide */
@@ -617,6 +637,26 @@
public void onStopResolutionSucceeded(int listenerKey) {
sendNoArg(STOP_RESOLUTION_SUCCEEDED, listenerKey);
}
+
+ @Override
+ public void onServiceInfoCallbackRegistrationFailed(int listenerKey, int error) {
+ sendError(REGISTER_SERVICE_CALLBACK_FAILED, listenerKey, error);
+ }
+
+ @Override
+ public void onServiceUpdated(int listenerKey, NsdServiceInfo info) {
+ sendInfo(SERVICE_UPDATED, listenerKey, info);
+ }
+
+ @Override
+ public void onServiceUpdatedLost(int listenerKey) {
+ sendNoArg(SERVICE_UPDATED_LOST, listenerKey);
+ }
+
+ @Override
+ public void onServiceInfoCallbackUnregistered(int listenerKey) {
+ sendNoArg(UNREGISTER_SERVICE_CALLBACK_SUCCEEDED, listenerKey);
+ }
}
/**
@@ -646,6 +686,14 @@
*/
public static final int FAILURE_OPERATION_NOT_RUNNING = 5;
+ /**
+ * Indicates that the service has failed to resolve because of bad parameters.
+ *
+ * This failure is passed with
+ * {@link ServiceInfoCallback#onServiceInfoCallbackRegistrationFailed}.
+ */
+ public static final int FAILURE_BAD_PARAMETERS = 6;
+
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef(value = {
@@ -654,6 +702,15 @@
public @interface StopOperationFailureCode {
}
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ FAILURE_ALREADY_ACTIVE,
+ FAILURE_BAD_PARAMETERS,
+ })
+ public @interface ResolutionFailureCode {
+ }
+
/** Interface for callback invocation for service discovery */
public interface DiscoveryListener {
@@ -727,6 +784,54 @@
@StopOperationFailureCode int errorCode) { }
}
+ /**
+ * Callback to listen to service info updates.
+ *
+ * For use with {@link NsdManager#registerServiceInfoCallback} to register, and with
+ * {@link NsdManager#unregisterServiceInfoCallback} to stop listening.
+ */
+ public interface ServiceInfoCallback {
+
+ /**
+ * Reports that registering the callback failed with an error.
+ *
+ * Called on the executor passed to {@link NsdManager#registerServiceInfoCallback}.
+ *
+ * onServiceInfoCallbackRegistrationFailed will be called exactly once when the callback
+ * could not be registered. No other callback will be sent in that case.
+ */
+ void onServiceInfoCallbackRegistrationFailed(@ResolutionFailureCode int errorCode);
+
+ /**
+ * Reports updated service info.
+ *
+ * Called on the executor passed to {@link NsdManager#registerServiceInfoCallback}. Any
+ * service updates will be notified via this callback until
+ * {@link NsdManager#unregisterServiceInfoCallback} is called. This will only be called once
+ * the service is found, so may never be called if the service is never present.
+ */
+ void onServiceUpdated(@NonNull NsdServiceInfo serviceInfo);
+
+ /**
+ * Reports when the service that this callback listens to becomes unavailable.
+ *
+ * Called on the executor passed to {@link NsdManager#registerServiceInfoCallback}. The
+ * service may become available again, in which case {@link #onServiceUpdated} will be
+ * called.
+ */
+ void onServiceLost();
+
+ /**
+ * Reports that service info updates have stopped.
+ *
+ * Called on the executor passed to {@link NsdManager#registerServiceInfoCallback}.
+ *
+ * A callback unregistration operation will call onServiceInfoCallbackUnregistered
+ * once. After this, the callback may be reused.
+ */
+ void onServiceInfoCallbackUnregistered();
+ }
+
@VisibleForTesting
class ServiceHandler extends Handler {
ServiceHandler(Looper looper) {
@@ -827,6 +932,23 @@
executor.execute(() -> ((ResolveListener) listener).onResolveStopped(
ns));
break;
+ case REGISTER_SERVICE_CALLBACK_FAILED:
+ removeListener(key);
+ executor.execute(() -> ((ServiceInfoCallback) listener)
+ .onServiceInfoCallbackRegistrationFailed(errorCode));
+ break;
+ case SERVICE_UPDATED:
+ executor.execute(() -> ((ServiceInfoCallback) listener)
+ .onServiceUpdated((NsdServiceInfo) obj));
+ break;
+ case SERVICE_UPDATED_LOST:
+ executor.execute(() -> ((ServiceInfoCallback) listener).onServiceLost());
+ break;
+ case UNREGISTER_SERVICE_CALLBACK_SUCCEEDED:
+ removeListener(key);
+ executor.execute(() -> ((ServiceInfoCallback) listener)
+ .onServiceInfoCallbackUnregistered());
+ break;
default:
Log.d(TAG, "Ignored " + message);
break;
@@ -1138,7 +1260,14 @@
* @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.
+ *
+ * @deprecated the returned ServiceInfo may get stale at any time after resolution, including
+ * immediately after the callback is called, and may not contain some service information that
+ * could be delivered later, like additional host addresses. Prefer using
+ * {@link #registerServiceInfoCallback}, which will keep the application up-to-date with the
+ * state of the service.
*/
+ @Deprecated
public void resolveService(NsdServiceInfo serviceInfo, ResolveListener listener) {
resolveService(serviceInfo, Runnable::run, listener);
}
@@ -1150,7 +1279,14 @@
* @param serviceInfo service to be resolved
* @param executor Executor to run listener callbacks with
* @param listener to receive callback upon success or failure.
+ *
+ * @deprecated the returned ServiceInfo may get stale at any time after resolution, including
+ * immediately after the callback is called, and may not contain some service information that
+ * could be delivered later, like additional host addresses. Prefer using
+ * {@link #registerServiceInfoCallback}, which will keep the application up-to-date with the
+ * state of the service.
*/
+ @Deprecated
public void resolveService(@NonNull NsdServiceInfo serviceInfo,
@NonNull Executor executor, @NonNull ResolveListener listener) {
checkServiceInfo(serviceInfo);
@@ -1185,6 +1321,62 @@
}
}
+ /**
+ * Register a callback to listen for updates to a service.
+ *
+ * An application can listen to a service to continuously monitor availability of given service.
+ * The callback methods will be called on the passed executor. And service updates are sent with
+ * continuous calls to {@link ServiceInfoCallback#onServiceUpdated}.
+ *
+ * This is different from {@link #resolveService} which provides one shot service information.
+ *
+ * <p> An application can listen to a service once a time. It needs to cancel the registration
+ * before registering other callbacks. Upon failure to register a callback for example if
+ * it's a duplicated registration, the application is notified through
+ * {@link ServiceInfoCallback#onServiceInfoCallbackRegistrationFailed} with
+ * {@link #FAILURE_BAD_PARAMETERS} or {@link #FAILURE_ALREADY_ACTIVE}.
+ *
+ * @param serviceInfo the service to receive updates for
+ * @param executor Executor to run callbacks with
+ * @param listener to receive callback upon service update
+ */
+ public void registerServiceInfoCallback(@NonNull NsdServiceInfo serviceInfo,
+ @NonNull Executor executor, @NonNull ServiceInfoCallback listener) {
+ checkServiceInfo(serviceInfo);
+ int key = putListener(listener, executor, serviceInfo);
+ try {
+ mService.registerServiceInfoCallback(key, serviceInfo);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Unregister a callback registered with {@link #registerServiceInfoCallback}.
+ *
+ * A successful unregistration is notified with a call to
+ * {@link ServiceInfoCallback#onServiceInfoCallbackUnregistered}. The same callback can only be
+ * reused after this is called.
+ *
+ * <p>If the callback is not already registered, this will throw with
+ * {@link IllegalArgumentException}.
+ *
+ * @param listener This should be a listener object that was passed to
+ * {@link #registerServiceInfoCallback}. It identifies the registration that
+ * should be unregistered and notifies of a successful or unsuccessful stop.
+ * Throws {@code IllegalArgumentException} if the listener was not passed to
+ * {@link #registerServiceInfoCallback} before.
+ */
+ public void unregisterServiceInfoCallback(@NonNull ServiceInfoCallback listener) {
+ // Will throw IllegalArgumentException if the listener is not known
+ int id = getListenerKey(listener);
+ try {
+ mService.unregisterServiceInfoCallback(id);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
private static void checkListener(Object listener) {
Objects.requireNonNull(listener, "listener cannot be null");
}
diff --git a/framework-t/src/android/net/nsd/NsdServiceInfo.java b/framework-t/src/android/net/nsd/NsdServiceInfo.java
index 6438a60..caeecdd 100644
--- a/framework-t/src/android/net/nsd/NsdServiceInfo.java
+++ b/framework-t/src/android/net/nsd/NsdServiceInfo.java
@@ -26,10 +26,14 @@
import android.util.ArrayMap;
import android.util.Log;
+import com.android.net.module.util.InetAddressUtils;
+
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
import java.util.Collections;
+import java.util.List;
import java.util.Map;
/**
@@ -46,7 +50,7 @@
private final ArrayMap<String, byte[]> mTxtRecord = new ArrayMap<>();
- private InetAddress mHost;
+ private final List<InetAddress> mHostAddresses = new ArrayList<>();
private int mPort;
@@ -84,17 +88,32 @@
mServiceType = s;
}
- /** Get the host address. The host address is valid for a resolved service. */
+ /**
+ * Get the host address. The host address is valid for a resolved service.
+ *
+ * @deprecated Use {@link #getHostAddresses()} to get the entire list of addresses for the host.
+ */
+ @Deprecated
public InetAddress getHost() {
- return mHost;
+ return mHostAddresses.size() == 0 ? null : mHostAddresses.get(0);
}
- /** Set the host address */
+ /**
+ * Set the host address
+ *
+ * @deprecated Use {@link #setHostAddresses(List)} to set multiple addresses for the host.
+ */
+ @Deprecated
public void setHost(InetAddress s) {
- mHost = s;
+ setHostAddresses(Collections.singletonList(s));
}
- /** Get port number. The port number is valid for a resolved service. */
+ /**
+ * Get port number. The port number is valid for a resolved service.
+ *
+ * The port is valid for all addresses.
+ * @see #getHostAddresses()
+ */
public int getPort() {
return mPort;
}
@@ -105,6 +124,24 @@
}
/**
+ * Get the host addresses.
+ *
+ * All host addresses are valid for the resolved service.
+ * All addresses share the same port
+ * @see #getPort()
+ */
+ @NonNull
+ public List<InetAddress> getHostAddresses() {
+ return new ArrayList<>(mHostAddresses);
+ }
+
+ /** Set the host addresses */
+ public void setHostAddresses(@NonNull List<InetAddress> addresses) {
+ mHostAddresses.clear();
+ mHostAddresses.addAll(addresses);
+ }
+
+ /**
* Unpack txt information from a base-64 encoded byte array.
*
* @param txtRecordsRawBytes The raw base64 encoded byte array.
@@ -359,7 +396,7 @@
StringBuilder sb = new StringBuilder();
sb.append("name: ").append(mServiceName)
.append(", type: ").append(mServiceType)
- .append(", host: ").append(mHost)
+ .append(", hostAddresses: ").append(TextUtils.join(", ", mHostAddresses))
.append(", port: ").append(mPort)
.append(", network: ").append(mNetwork);
@@ -377,12 +414,6 @@
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.
@@ -401,6 +432,10 @@
dest.writeParcelable(mNetwork, 0);
dest.writeInt(mInterfaceIndex);
+ dest.writeInt(mHostAddresses.size());
+ for (InetAddress address : mHostAddresses) {
+ InetAddressUtils.parcelInetAddress(dest, address, flags);
+ }
}
/** Implement the Parcelable interface */
@@ -410,13 +445,6 @@
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.
@@ -432,6 +460,10 @@
}
info.mNetwork = in.readParcelable(null, Network.class);
info.mInterfaceIndex = in.readInt();
+ int size = in.readInt();
+ for (int i = 0; i < size; i++) {
+ info.mHostAddresses.add(InetAddressUtils.unparcelInetAddress(in));
+ }
return info;
}
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index ce105ce..b361720 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -83,6 +83,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -412,6 +413,13 @@
clientId, NsdManager.FAILURE_OPERATION_NOT_RUNNING);
}
break;
+ case NsdManager.REGISTER_SERVICE_CALLBACK:
+ cInfo = getClientInfoForReply(msg);
+ if (cInfo != null) {
+ cInfo.onServiceInfoCallbackRegistrationFailed(
+ clientId, NsdManager.FAILURE_BAD_PARAMETERS);
+ }
+ break;
case NsdManager.DAEMON_CLEANUP:
maybeStopDaemon();
break;
@@ -490,6 +498,11 @@
maybeStopMonitoringSocketsIfNoActiveRequest();
}
+ private void clearRegisteredServiceInfo(ClientInfo clientInfo) {
+ clientInfo.mRegisteredService = null;
+ clientInfo.mClientIdForServiceUpdates = 0;
+ }
+
/**
* Check the given service type is valid and construct it to a service type
* which can use for discovery / resolution service.
@@ -793,6 +806,56 @@
clientInfo.mResolvedService = null;
// TODO: Implement the stop resolution with MdnsDiscoveryManager.
break;
+ case NsdManager.REGISTER_SERVICE_CALLBACK:
+ if (DBG) Log.d(TAG, "Register a service callback");
+ args = (ListenerArgs) msg.obj;
+ clientInfo = mClients.get(args.connector);
+ // If the binder death notification for a INsdManagerCallback was received
+ // before any calls are received by NsdService, the clientInfo would be
+ // cleared and cause NPE. Add a null check here to prevent this corner case.
+ if (clientInfo == null) {
+ Log.e(TAG, "Unknown connector in callback registration");
+ break;
+ }
+
+ if (clientInfo.mRegisteredService != null) {
+ clientInfo.onServiceInfoCallbackRegistrationFailed(
+ clientId, NsdManager.FAILURE_ALREADY_ACTIVE);
+ break;
+ }
+
+ maybeStartDaemon();
+ id = getUniqueId();
+ if (resolveService(id, args.serviceInfo)) {
+ clientInfo.mRegisteredService = new NsdServiceInfo();
+ clientInfo.mClientIdForServiceUpdates = clientId;
+ storeRequestMap(clientId, id, clientInfo, msg.what);
+ } else {
+ clientInfo.onServiceInfoCallbackRegistrationFailed(
+ clientId, NsdManager.FAILURE_BAD_PARAMETERS);
+ }
+ break;
+ case NsdManager.UNREGISTER_SERVICE_CALLBACK:
+ if (DBG) Log.d(TAG, "Unregister a service callback");
+ args = (ListenerArgs) msg.obj;
+ clientInfo = mClients.get(args.connector);
+ // If the binder death notification for a INsdManagerCallback was received
+ // before any calls are received by NsdService, the clientInfo would be
+ // cleared and cause NPE. Add a null check here to prevent this corner case.
+ if (clientInfo == null) {
+ Log.e(TAG, "Unknown connector in callback unregistration");
+ break;
+ }
+
+ id = clientInfo.mClientIds.get(clientId);
+ removeRequestMap(clientId, id, clientInfo);
+ if (stopResolveService(id)) {
+ clientInfo.onServiceInfoCallbackUnregistered(clientId);
+ } else {
+ Log.e(TAG, "Failed to unregister service info callback");
+ }
+ clearRegisteredServiceInfo(clientInfo);
+ break;
case MDNS_SERVICE_EVENT:
if (!handleMDnsServiceEvent(msg.arg1, msg.arg2, msg.obj)) {
return NOT_HANDLED;
@@ -809,6 +872,19 @@
return HANDLED;
}
+ private void notifyResolveFailedResult(boolean isListenedToUpdates, int clientId,
+ ClientInfo clientInfo, int error) {
+ if (isListenedToUpdates) {
+ clientInfo.onServiceInfoCallbackRegistrationFailed(clientId, error);
+ clearRegisteredServiceInfo(clientInfo);
+ } else {
+ // The resolve API always returned FAILURE_INTERNAL_ERROR on error; keep it
+ // for backwards compatibility.
+ clientInfo.onResolveServiceFailed(clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+ clientInfo.mResolvedService = null;
+ }
+ }
+
private boolean handleMDnsServiceEvent(int code, int id, Object obj) {
NsdServiceInfo servInfo;
ClientInfo clientInfo = mIdToClientInfoMap.get(id);
@@ -859,6 +935,8 @@
// found services on the same interface index and their network at the time
setServiceNetworkForCallback(servInfo, lostNetId, info.interfaceIdx);
clientInfo.onServiceLost(clientId, servInfo);
+ // TODO: also support registered service lost when not discovering
+ clientInfo.maybeNotifyRegisteredServiceLost(servInfo);
break;
}
case IMDnsEventListener.SERVICE_DISCOVERY_FAILED:
@@ -895,10 +973,15 @@
String rest = fullName.substring(index);
String type = rest.replace(".local.", "");
- clientInfo.mResolvedService.setServiceName(name);
- clientInfo.mResolvedService.setServiceType(type);
- clientInfo.mResolvedService.setPort(info.port);
- clientInfo.mResolvedService.setTxtRecords(info.txtRecord);
+ final boolean isListenedToUpdates =
+ clientId == clientInfo.mClientIdForServiceUpdates;
+ final NsdServiceInfo serviceInfo = isListenedToUpdates
+ ? clientInfo.mRegisteredService : clientInfo.mResolvedService;
+
+ serviceInfo.setServiceName(name);
+ serviceInfo.setServiceType(type);
+ serviceInfo.setPort(info.port);
+ serviceInfo.setTxtRecords(info.txtRecord);
// Network will be added after SERVICE_GET_ADDR_SUCCESS
stopResolveService(id);
@@ -908,9 +991,8 @@
if (getAddrInfo(id2, info.hostname, info.interfaceIdx)) {
storeRequestMap(clientId, id2, clientInfo, NsdManager.RESOLVE_SERVICE);
} else {
- clientInfo.onResolveServiceFailed(
- clientId, NsdManager.FAILURE_INTERNAL_ERROR);
- clientInfo.mResolvedService = null;
+ notifyResolveFailedResult(isListenedToUpdates, clientId, clientInfo,
+ NsdManager.FAILURE_BAD_PARAMETERS);
}
break;
}
@@ -918,17 +1000,17 @@
/* NNN resolveId errorCode */
stopResolveService(id);
removeRequestMap(clientId, id, clientInfo);
- clientInfo.mResolvedService = null;
- clientInfo.onResolveServiceFailed(
- clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+ notifyResolveFailedResult(
+ clientId == clientInfo.mClientIdForServiceUpdates,
+ clientId, clientInfo, NsdManager.FAILURE_BAD_PARAMETERS);
break;
case IMDnsEventListener.SERVICE_GET_ADDR_FAILED:
/* NNN resolveId errorCode */
stopGetAddrInfo(id);
removeRequestMap(clientId, id, clientInfo);
- clientInfo.mResolvedService = null;
- clientInfo.onResolveServiceFailed(
- clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+ notifyResolveFailedResult(
+ clientId == clientInfo.mClientIdForServiceUpdates,
+ clientId, clientInfo, NsdManager.FAILURE_BAD_PARAMETERS);
break;
case IMDnsEventListener.SERVICE_GET_ADDR_SUCCESS: {
/* NNN resolveId hostname ttl addr interfaceIdx netId */
@@ -945,19 +1027,38 @@
// If the resolved service is on an interface without a network, consider it
// as a failure: it would not be usable by apps as they would need
// privileged permissions.
- if (netId != NETID_UNSET && serviceHost != null) {
- clientInfo.mResolvedService.setHost(serviceHost);
- setServiceNetworkForCallback(clientInfo.mResolvedService,
- netId, info.interfaceIdx);
- clientInfo.onResolveServiceSucceeded(
- clientId, clientInfo.mResolvedService);
+ if (clientId == clientInfo.mClientIdForServiceUpdates) {
+ if (netId != NETID_UNSET && serviceHost != null) {
+ setServiceNetworkForCallback(clientInfo.mRegisteredService,
+ netId, info.interfaceIdx);
+ final List<InetAddress> addresses =
+ clientInfo.mRegisteredService.getHostAddresses();
+ addresses.add(serviceHost);
+ clientInfo.mRegisteredService.setHostAddresses(addresses);
+ clientInfo.onServiceUpdated(
+ clientId, clientInfo.mRegisteredService);
+ } else {
+ stopGetAddrInfo(id);
+ removeRequestMap(clientId, id, clientInfo);
+ clearRegisteredServiceInfo(clientInfo);
+ clientInfo.onServiceInfoCallbackRegistrationFailed(
+ clientId, NsdManager.FAILURE_BAD_PARAMETERS);
+ }
} else {
- clientInfo.onResolveServiceFailed(
- clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+ if (netId != NETID_UNSET && serviceHost != null) {
+ clientInfo.mResolvedService.setHost(serviceHost);
+ setServiceNetworkForCallback(clientInfo.mResolvedService,
+ netId, info.interfaceIdx);
+ clientInfo.onResolveServiceSucceeded(
+ clientId, clientInfo.mResolvedService);
+ } else {
+ clientInfo.onResolveServiceFailed(
+ clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+ }
+ stopGetAddrInfo(id);
+ removeRequestMap(clientId, id, clientInfo);
+ clientInfo.mResolvedService = null;
}
- stopGetAddrInfo(id);
- removeRequestMap(clientId, id, clientInfo);
- clientInfo.mResolvedService = null;
break;
}
default:
@@ -1343,6 +1444,20 @@
}
@Override
+ public void registerServiceInfoCallback(int listenerKey, NsdServiceInfo serviceInfo) {
+ mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
+ NsdManager.REGISTER_SERVICE_CALLBACK, 0, listenerKey,
+ new ListenerArgs(this, serviceInfo)));
+ }
+
+ @Override
+ public void unregisterServiceInfoCallback(int listenerKey) {
+ mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
+ NsdManager.UNREGISTER_SERVICE_CALLBACK, 0, listenerKey,
+ new ListenerArgs(this, null)));
+ }
+
+ @Override
public void startDaemon() {
mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
NsdManager.DAEMON_STARTUP, new ListenerArgs(this, null)));
@@ -1503,6 +1618,11 @@
// The target SDK of this client < Build.VERSION_CODES.S
private boolean mIsLegacy = false;
+ /*** The service that is registered to listen to its updates */
+ private NsdServiceInfo mRegisteredService;
+ /*** The client id that listen to updates */
+ private int mClientIdForServiceUpdates;
+
private ClientInfo(INsdManagerCallback cb) {
mCb = cb;
if (DBG) Log.d(TAG, "New client");
@@ -1584,6 +1704,18 @@
return mClientIds.keyAt(idx);
}
+ private void maybeNotifyRegisteredServiceLost(@NonNull NsdServiceInfo info) {
+ if (mRegisteredService == null) return;
+ if (!Objects.equals(mRegisteredService.getServiceName(), info.getServiceName())) return;
+ // Resolved services have a leading dot appended at the beginning of their type, but in
+ // discovered info it's at the end
+ if (!Objects.equals(
+ mRegisteredService.getServiceType() + ".", "." + info.getServiceType())) {
+ return;
+ }
+ onServiceUpdatedLost(mClientIdForServiceUpdates);
+ }
+
void onDiscoverServicesStarted(int listenerKey, NsdServiceInfo info) {
try {
mCb.onDiscoverServicesStarted(listenerKey, info);
@@ -1695,5 +1827,37 @@
Log.e(TAG, "Error calling onStopResolutionSucceeded", e);
}
}
+
+ void onServiceInfoCallbackRegistrationFailed(int listenerKey, int error) {
+ try {
+ mCb.onServiceInfoCallbackRegistrationFailed(listenerKey, error);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling onServiceInfoCallbackRegistrationFailed", e);
+ }
+ }
+
+ void onServiceUpdated(int listenerKey, NsdServiceInfo info) {
+ try {
+ mCb.onServiceUpdated(listenerKey, info);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling onServiceUpdated", e);
+ }
+ }
+
+ void onServiceUpdatedLost(int listenerKey) {
+ try {
+ mCb.onServiceUpdatedLost(listenerKey);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling onServiceUpdatedLost", e);
+ }
+ }
+
+ void onServiceInfoCallbackUnregistered(int listenerKey) {
+ try {
+ mCb.onServiceInfoCallbackUnregistered(listenerKey);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling onServiceInfoCallbackUnregistered", e);
+ }
+ }
}
}
diff --git a/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java b/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java
index 64355ed..9ce0693 100644
--- a/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java
+++ b/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java
@@ -21,6 +21,7 @@
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import android.net.InetAddresses;
import android.net.Network;
import android.os.Build;
import android.os.Bundle;
@@ -38,6 +39,7 @@
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Arrays;
+import java.util.List;
import java.util.Map;
@RunWith(DevSdkIgnoreRunner.class)
@@ -45,6 +47,8 @@
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
public class NsdServiceInfoTest {
+ private static final InetAddress IPV4_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.1");
+ private static final InetAddress IPV6_ADDRESS = InetAddresses.parseNumericAddress("2001:db8::");
public final static InetAddress LOCALHOST;
static {
// Because test.
@@ -124,6 +128,7 @@
fullInfo.setServiceType("_kitten._tcp");
fullInfo.setPort(4242);
fullInfo.setHost(LOCALHOST);
+ fullInfo.setHostAddresses(List.of(IPV4_ADDRESS));
fullInfo.setNetwork(new Network(123));
fullInfo.setInterfaceIndex(456);
checkParcelable(fullInfo);
@@ -139,6 +144,7 @@
attributedInfo.setServiceType("_kitten._tcp");
attributedInfo.setPort(4242);
attributedInfo.setHost(LOCALHOST);
+ fullInfo.setHostAddresses(List.of(IPV6_ADDRESS, IPV4_ADDRESS));
attributedInfo.setAttribute("color", "pink");
attributedInfo.setAttribute("sound", (new String("にゃあ")).getBytes("UTF-8"));
attributedInfo.setAttribute("adorable", (String) null);
diff --git a/tests/unit/java/com/android/server/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java
index a8f8121..c7a6639 100644
--- a/tests/unit/java/com/android/server/NsdServiceTest.java
+++ b/tests/unit/java/com/android/server/NsdServiceTest.java
@@ -17,6 +17,7 @@
package com.android.server;
import static android.net.InetAddresses.parseNumericAddress;
+import static android.net.nsd.NsdManager.FAILURE_BAD_PARAMETERS;
import static android.net.nsd.NsdManager.FAILURE_INTERNAL_ERROR;
import static android.net.nsd.NsdManager.FAILURE_OPERATION_NOT_RUNNING;
@@ -29,6 +30,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
@@ -666,6 +668,133 @@
&& request.getServiceType().equals(ns.getServiceType())));
}
+ private void verifyUpdatedServiceInfo(NsdServiceInfo info, String serviceName,
+ String serviceType, String address, int port, int interfaceIndex, Network network) {
+ assertEquals(serviceName, info.getServiceName());
+ assertEquals(serviceType, info.getServiceType());
+ assertTrue(info.getHostAddresses().contains(parseNumericAddress(address)));
+ assertEquals(port, info.getPort());
+ assertEquals(network, info.getNetwork());
+ assertEquals(interfaceIndex, info.getInterfaceIndex());
+ }
+
+ @Test
+ public void testRegisterAndUnregisterServiceInfoCallback() throws RemoteException {
+ final NsdManager client = connectClient(mService);
+ final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
+ final NsdManager.ServiceInfoCallback serviceInfoCallback = mock(
+ NsdManager.ServiceInfoCallback.class);
+ client.registerServiceInfoCallback(request, Runnable::run, serviceInfoCallback);
+ waitForIdle();
+
+ final IMDnsEventListener eventListener = getEventListener();
+ final ArgumentCaptor<Integer> resolvIdCaptor = ArgumentCaptor.forClass(Integer.class);
+ verify(mMockMDnsM).resolve(resolvIdCaptor.capture(), eq(SERVICE_NAME), eq(SERVICE_TYPE),
+ eq("local.") /* domain */, eq(IFACE_IDX_ANY));
+
+ // Resolve service successfully.
+ final ResolutionInfo resolutionInfo = new ResolutionInfo(
+ resolvIdCaptor.getValue(),
+ IMDnsEventListener.SERVICE_RESOLVED,
+ null /* serviceName */,
+ null /* serviceType */,
+ null /* domain */,
+ SERVICE_FULL_NAME,
+ DOMAIN_NAME,
+ PORT,
+ new byte[0] /* txtRecord */,
+ IFACE_IDX_ANY);
+ doReturn(true).when(mMockMDnsM).getServiceAddress(anyInt(), any(), anyInt());
+ eventListener.onServiceResolutionStatus(resolutionInfo);
+ waitForIdle();
+
+ final ArgumentCaptor<Integer> getAddrIdCaptor = ArgumentCaptor.forClass(Integer.class);
+ verify(mMockMDnsM).getServiceAddress(getAddrIdCaptor.capture(), eq(DOMAIN_NAME),
+ eq(IFACE_IDX_ANY));
+
+ // First address info
+ final String v4Address = "192.0.2.1";
+ final String v6Address = "2001:db8::";
+ final GetAddressInfo addressInfo1 = new GetAddressInfo(
+ getAddrIdCaptor.getValue(),
+ IMDnsEventListener.SERVICE_GET_ADDR_SUCCESS,
+ SERVICE_FULL_NAME,
+ v4Address,
+ IFACE_IDX_ANY,
+ 999 /* netId */);
+ eventListener.onGettingServiceAddressStatus(addressInfo1);
+ waitForIdle();
+
+ final ArgumentCaptor<NsdServiceInfo> updateInfoCaptor =
+ ArgumentCaptor.forClass(NsdServiceInfo.class);
+ verify(serviceInfoCallback, timeout(TIMEOUT_MS).times(1))
+ .onServiceUpdated(updateInfoCaptor.capture());
+ verifyUpdatedServiceInfo(updateInfoCaptor.getAllValues().get(0) /* info */, SERVICE_NAME,
+ "." + SERVICE_TYPE, v4Address, PORT, IFACE_IDX_ANY, new Network(999));
+
+ // Second address info
+ final GetAddressInfo addressInfo2 = new GetAddressInfo(
+ getAddrIdCaptor.getValue(),
+ IMDnsEventListener.SERVICE_GET_ADDR_SUCCESS,
+ SERVICE_FULL_NAME,
+ v6Address,
+ IFACE_IDX_ANY,
+ 999 /* netId */);
+ eventListener.onGettingServiceAddressStatus(addressInfo2);
+ waitForIdle();
+
+ verify(serviceInfoCallback, timeout(TIMEOUT_MS).times(2))
+ .onServiceUpdated(updateInfoCaptor.capture());
+ verifyUpdatedServiceInfo(updateInfoCaptor.getAllValues().get(1) /* info */, SERVICE_NAME,
+ "." + SERVICE_TYPE, v6Address, PORT, IFACE_IDX_ANY, new Network(999));
+
+ client.unregisterServiceInfoCallback(serviceInfoCallback);
+ waitForIdle();
+
+ verify(serviceInfoCallback, timeout(TIMEOUT_MS)).onServiceInfoCallbackUnregistered();
+ }
+
+ @Test
+ public void testRegisterServiceCallbackFailed() throws Exception {
+ final NsdManager client = connectClient(mService);
+ final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
+ final NsdManager.ServiceInfoCallback subscribeListener = mock(
+ NsdManager.ServiceInfoCallback.class);
+ client.registerServiceInfoCallback(request, Runnable::run, subscribeListener);
+ waitForIdle();
+
+ final IMDnsEventListener eventListener = getEventListener();
+ final ArgumentCaptor<Integer> resolvIdCaptor = ArgumentCaptor.forClass(Integer.class);
+ verify(mMockMDnsM).resolve(resolvIdCaptor.capture(), eq(SERVICE_NAME), eq(SERVICE_TYPE),
+ eq("local.") /* domain */, eq(IFACE_IDX_ANY));
+
+ // Fail to resolve service.
+ final ResolutionInfo resolutionFailedInfo = new ResolutionInfo(
+ resolvIdCaptor.getValue(),
+ IMDnsEventListener.SERVICE_RESOLUTION_FAILED,
+ null /* serviceName */,
+ null /* serviceType */,
+ null /* domain */,
+ null /* serviceFullName */,
+ null /* domainName */,
+ 0 /* port */,
+ new byte[0] /* txtRecord */,
+ IFACE_IDX_ANY);
+ eventListener.onServiceResolutionStatus(resolutionFailedInfo);
+ verify(subscribeListener, timeout(TIMEOUT_MS))
+ .onServiceInfoCallbackRegistrationFailed(eq(FAILURE_BAD_PARAMETERS));
+ }
+
+ @Test
+ public void testUnregisterNotRegisteredCallback() {
+ final NsdManager client = connectClient(mService);
+ final NsdManager.ServiceInfoCallback serviceInfoCallback = mock(
+ NsdManager.ServiceInfoCallback.class);
+
+ assertThrows(IllegalArgumentException.class, () ->
+ client.unregisterServiceInfoCallback(serviceInfoCallback));
+ }
+
private void makeServiceWithMdnsDiscoveryManagerEnabled() {
doReturn(true).when(mDeps).isMdnsDiscoveryManagerEnabled(any(Context.class));
doReturn(mDiscoveryManager).when(mDeps).makeMdnsDiscoveryManager(any(), any());