diff --git a/framework-t/api/current.txt b/framework-t/api/current.txt index ed841b87ed..55328532ff 100644 --- a/framework-t/api/current.txt +++ b/framework-t/api/current.txt @@ -192,14 +192,17 @@ package android.net.nsd { 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 @@ package android.net.nsd { 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 getAttributes(); - method public java.net.InetAddress getHost(); + method @Deprecated public java.net.InetAddress getHost(); + method @NonNull public java.util.List 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); 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 669efc9730..d89bfa9640 100644 --- a/framework-t/src/android/net/nsd/INsdManagerCallback.aidl +++ b/framework-t/src/android/net/nsd/INsdManagerCallback.aidl @@ -38,4 +38,8 @@ oneway interface INsdManagerCallback { 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 a28fd7d014..55331540c0 100644 --- a/framework-t/src/android/net/nsd/INsdServiceConnector.aidl +++ b/framework-t/src/android/net/nsd/INsdServiceConnector.aidl @@ -33,4 +33,6 @@ interface INsdServiceConnector { 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 1a5a66788a..122e3a02a4 100644 --- a/framework-t/src/android/net/nsd/NsdManager.java +++ b/framework-t/src/android/net/nsd/NsdManager.java @@ -254,6 +254,20 @@ public final class NsdManager { /** @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 @@ public final class NsdManager { 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 final class NsdManager { 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 final class NsdManager { */ 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 final class NsdManager { 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 @@ public final class NsdManager { @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 @@ public final class NsdManager { 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 @@ public final class NsdManager { * @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 @@ public final class NsdManager { * @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 @@ public final class NsdManager { } } + /** + * 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. + * + *

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. + * + *

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 6438a6054f..caeecdda86 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.text.TextUtils; 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 @@ public final class NsdServiceInfo implements Parcelable { private final ArrayMap mTxtRecord = new ArrayMap<>(); - private InetAddress mHost; + private final List mHostAddresses = new ArrayList<>(); private int mPort; @@ -84,17 +88,32 @@ public final class NsdServiceInfo implements Parcelable { 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; } @@ -104,6 +123,24 @@ public final class NsdServiceInfo implements Parcelable { mPort = p; } + /** + * Get the host addresses. + * + * All host addresses are valid for the resolved service. + * All addresses share the same port + * @see #getPort() + */ + @NonNull + public List getHostAddresses() { + return new ArrayList<>(mHostAddresses); + } + + /** Set the host addresses */ + public void setHostAddresses(@NonNull List addresses) { + mHostAddresses.clear(); + mHostAddresses.addAll(addresses); + } + /** * Unpack txt information from a base-64 encoded byte array. * @@ -359,7 +396,7 @@ public final class NsdServiceInfo implements Parcelable { 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 final class NsdServiceInfo implements Parcelable { 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 @@ public final class NsdServiceInfo implements Parcelable { 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 @@ public final class NsdServiceInfo implements Parcelable { 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 @@ public final class NsdServiceInfo implements Parcelable { } 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 ce105cef4a..b3617200fd 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.nio.charset.StandardCharsets; 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 @@ public class NsdService extends INsdManager.Stub { 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 @@ public class NsdService extends INsdManager.Stub { 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 @@ public class NsdService extends INsdManager.Stub { 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 @@ public class NsdService extends INsdManager.Stub { 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 @@ public class NsdService extends INsdManager.Stub { // 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 @@ public class NsdService extends INsdManager.Stub { 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 @@ public class NsdService extends INsdManager.Stub { 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 @@ public class NsdService extends INsdManager.Stub { /* 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 @@ public class NsdService extends INsdManager.Stub { // 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 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: @@ -1342,6 +1443,20 @@ public class NsdService extends INsdManager.Stub { NsdManager.STOP_RESOLUTION, 0, listenerKey, new ListenerArgs(this, null))); } + @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( @@ -1503,6 +1618,11 @@ public class NsdService extends INsdManager.Stub { // 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 @@ public class NsdService extends INsdManager.Stub { 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 @@ public class NsdService extends INsdManager.Stub { 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 64355edaf2..9ce06938cd 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.assertFalse; 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 org.junit.runner.RunWith; 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 @@ import java.util.Map; @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 @@ public class NsdServiceInfoTest { 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 @@ public class NsdServiceInfoTest { 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 a8f8121a7e..c7a66399d8 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.assertArrayEquals; 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 @@ public class NsdServiceTest { && 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 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 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 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 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());