| /* |
| * 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 android.Manifest.permission.NETWORK_SETTINGS; |
| import static android.Manifest.permission.NETWORK_STACK; |
| import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK; |
| import static android.net.connectivity.ConnectivityCompatChanges.ENABLE_PLATFORM_MDNS_BACKEND; |
| import static android.net.connectivity.ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER; |
| |
| import android.annotation.FlaggedApi; |
| import android.annotation.IntDef; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.annotation.RequiresPermission; |
| import android.annotation.SdkConstant; |
| import android.annotation.SdkConstant.SdkConstantType; |
| import android.annotation.SystemApi; |
| import android.annotation.SystemService; |
| import android.app.compat.CompatChanges; |
| import android.content.Context; |
| import android.net.ConnectivityManager; |
| import android.net.ConnectivityManager.NetworkCallback; |
| import android.net.ConnectivityThread; |
| import android.net.Network; |
| import android.net.NetworkRequest; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.os.Message; |
| import android.os.RemoteException; |
| import android.text.TextUtils; |
| import android.util.ArrayMap; |
| import android.util.ArraySet; |
| import android.util.Log; |
| import android.util.Pair; |
| import android.util.SparseArray; |
| |
| import com.android.internal.annotations.GuardedBy; |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.modules.utils.build.SdkLevel; |
| import com.android.net.module.util.CollectionUtils; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.util.ArrayList; |
| import java.util.Objects; |
| import java.util.concurrent.Executor; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| /** |
| * 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; |
| |
| // TODO : remove this class when udc-mainline-prod is abandoned and android.net.flags.Flags is |
| // available here |
| /** @hide */ |
| public static class Flags { |
| static final String REGISTER_NSD_OFFLOAD_ENGINE_API = |
| "com.android.net.flags.register_nsd_offload_engine_api"; |
| static final String NSD_SUBTYPES_SUPPORT_ENABLED = |
| "com.android.net.flags.nsd_subtypes_support_enabled"; |
| static final String ADVERTISE_REQUEST_API = |
| "com.android.net.flags.advertise_request_api"; |
| static final String NSD_CUSTOM_HOSTNAME_ENABLED = |
| "com.android.net.flags.nsd_custom_hostname_enabled"; |
| static final String NSD_CUSTOM_TTL_ENABLED = |
| "com.android.net.flags.nsd_custom_ttl_enabled"; |
| } |
| |
| /** |
| * A regex for the acceptable format of a type or subtype label. |
| * @hide |
| */ |
| public static final String TYPE_LABEL_REGEX = "_[a-zA-Z0-9-_]{1,61}[a-zA-Z0-9]"; |
| |
| /** |
| * A regex for the acceptable format of a subtype label. |
| * |
| * As per RFC 6763 7.1, "Subtype strings are not required to begin with an underscore, though |
| * they often do.", and "Subtype strings [...] may be constructed using arbitrary 8-bit data |
| * values. In many cases these data values may be UTF-8 [RFC3629] representations of text, or |
| * even (as in the example above) plain ASCII [RFC20], but they do not have to be.". |
| * |
| * This regex is overly conservative as it mandates the underscore and only allows printable |
| * ASCII characters (codes 0x20 to 0x7e, space to tilde), except for comma (0x2c) and dot |
| * (0x2e); so the NsdManager API does not allow everything the RFC allows. This may be revisited |
| * in the future, but using arbitrary bytes makes logging and testing harder, and using other |
| * characters would probably be a bad idea for interoperability for apps. |
| * @hide |
| */ |
| public static final String SUBTYPE_LABEL_REGEX = "_[" |
| + "\\x20-\\x2b" |
| + "\\x2d" |
| + "\\x2f-\\x7e" |
| + "]{1,62}"; |
| |
| /** |
| * A regex for the acceptable format of a service type specification. |
| * |
| * When it matches, matcher group 1 is an optional leading subtype when using legacy dot syntax |
| * (_subtype._type._tcp). Matcher group 2 is the actual type, and matcher group 3 contains |
| * optional comma-separated subtypes. |
| * @hide |
| */ |
| public static final String TYPE_REGEX = |
| // Optional leading subtype (_subtype._type._tcp) |
| // (?: xxx) is a non-capturing parenthesis, don't capture the dot |
| "^(?:(" + SUBTYPE_LABEL_REGEX + ")\\.)?" |
| // Actual type (_type._tcp.local) |
| + "(" + TYPE_LABEL_REGEX + "\\._(?:tcp|udp))" |
| // Drop '.' at the end of service type that is compatible with old backend. |
| // e.g. allow "_type._tcp.local." |
| + "\\.?" |
| // Optional subtype after comma, for "_type._tcp,_subtype1,_subtype2" format |
| + "((?:," + SUBTYPE_LABEL_REGEX + ")*)" |
| + "$"; |
| |
| /** |
| * 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 |
| */ |
| // TODO: Deprecate this since NSD service is never disabled. |
| 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; |
| |
| /** @hide */ |
| public static final int DISCOVER_SERVICES = 1; |
| /** @hide */ |
| public static final int DISCOVER_SERVICES_STARTED = 2; |
| /** @hide */ |
| public static final int DISCOVER_SERVICES_FAILED = 3; |
| /** @hide */ |
| public static final int SERVICE_FOUND = 4; |
| /** @hide */ |
| public static final int SERVICE_LOST = 5; |
| |
| /** @hide */ |
| public static final int STOP_DISCOVERY = 6; |
| /** @hide */ |
| public static final int STOP_DISCOVERY_FAILED = 7; |
| /** @hide */ |
| public static final int STOP_DISCOVERY_SUCCEEDED = 8; |
| |
| /** @hide */ |
| public static final int REGISTER_SERVICE = 9; |
| /** @hide */ |
| public static final int REGISTER_SERVICE_FAILED = 10; |
| /** @hide */ |
| public static final int REGISTER_SERVICE_SUCCEEDED = 11; |
| |
| /** @hide */ |
| public static final int UNREGISTER_SERVICE = 12; |
| /** @hide */ |
| public static final int UNREGISTER_SERVICE_FAILED = 13; |
| /** @hide */ |
| public static final int UNREGISTER_SERVICE_SUCCEEDED = 14; |
| |
| /** @hide */ |
| public static final int RESOLVE_SERVICE = 15; |
| /** @hide */ |
| public static final int RESOLVE_SERVICE_FAILED = 16; |
| /** @hide */ |
| public static final int RESOLVE_SERVICE_SUCCEEDED = 17; |
| |
| /** @hide */ |
| public static final int DAEMON_CLEANUP = 18; |
| /** @hide */ |
| public static final int DAEMON_STARTUP = 19; |
| |
| /** @hide */ |
| public static final int MDNS_SERVICE_EVENT = 20; |
| |
| /** @hide */ |
| public static final int REGISTER_CLIENT = 21; |
| /** @hide */ |
| public static final int UNREGISTER_CLIENT = 22; |
| |
| /** @hide */ |
| public static final int MDNS_DISCOVERY_MANAGER_EVENT = 23; |
| |
| /** @hide */ |
| public static final int STOP_RESOLUTION = 24; |
| /** @hide */ |
| public static final int STOP_RESOLUTION_FAILED = 25; |
| /** @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; |
| /** @hide */ |
| public static final int REGISTER_OFFLOAD_ENGINE = 33; |
| /** @hide */ |
| public static final int UNREGISTER_OFFLOAD_ENGINE = 34; |
| |
| /** Dns based service discovery protocol */ |
| public static final int PROTOCOL_DNS_SD = 0x0001; |
| |
| /** |
| * The minimum TTL seconds which is allowed for a service registration. |
| * |
| * @hide |
| */ |
| public static final long TTL_SECONDS_MIN = 30L; |
| |
| /** |
| * The maximum TTL seconds which is allowed for a service registration. |
| * |
| * @hide |
| */ |
| public static final long TTL_SECONDS_MAX = 10 * 3600L; |
| |
| 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(MDNS_SERVICE_EVENT, "MDNS_SERVICE_EVENT"); |
| 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"); |
| EVENT_NAMES.put(MDNS_DISCOVERY_MANAGER_EVENT, "MDNS_DISCOVERY_MANAGER_EVENT"); |
| EVENT_NAMES.put(REGISTER_CLIENT, "REGISTER_CLIENT"); |
| EVENT_NAMES.put(UNREGISTER_CLIENT, "UNREGISTER_CLIENT"); |
| } |
| |
| /** @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 static final int DNSSEC_PROTOCOL = 3; |
| |
| private final INsdServiceConnector mService; |
| private final Context mContext; |
| |
| private int mListenerKey = FIRST_LISTENER_KEY; |
| @GuardedBy("mMapLock") |
| private final SparseArray mListenerMap = new SparseArray(); |
| @GuardedBy("mMapLock") |
| private final SparseArray<NsdServiceInfo> mServiceMap = new SparseArray<>(); |
| @GuardedBy("mMapLock") |
| private final SparseArray<DiscoveryRequest> mDiscoveryMap = new SparseArray<>(); |
| @GuardedBy("mMapLock") |
| private final SparseArray<Executor> mExecutorMap = new SparseArray<>(); |
| private final Object mMapLock = new Object(); |
| // Map of listener key sent by client -> per-network discovery tracker |
| @GuardedBy("mPerNetworkDiscoveryMap") |
| private final ArrayMap<Integer, PerNetworkDiscoveryTracker> |
| mPerNetworkDiscoveryMap = new ArrayMap<>(); |
| |
| @GuardedBy("mOffloadEngines") |
| private final ArrayList<OffloadEngineProxy> mOffloadEngines = new ArrayList<>(); |
| private final ServiceHandler mHandler; |
| |
| private static class OffloadEngineProxy extends IOffloadEngine.Stub { |
| private final Executor mExecutor; |
| private final OffloadEngine mEngine; |
| |
| private OffloadEngineProxy(@NonNull Executor executor, @NonNull OffloadEngine appCb) { |
| mExecutor = executor; |
| mEngine = appCb; |
| } |
| |
| @Override |
| public void onOffloadServiceUpdated(OffloadServiceInfo info) { |
| mExecutor.execute(() -> mEngine.onOffloadServiceUpdated(info)); |
| } |
| |
| @Override |
| public void onOffloadServiceRemoved(OffloadServiceInfo info) { |
| mExecutor.execute(() -> mEngine.onOffloadServiceRemoved(info)); |
| } |
| } |
| |
| /** |
| * Registers an OffloadEngine with NsdManager. |
| * |
| * A caller can register itself as an OffloadEngine if it supports mDns hardware offload. |
| * The caller must implement the {@link OffloadEngine} interface and update hardware offload |
| * state property when the {@link OffloadEngine#onOffloadServiceUpdated} and |
| * {@link OffloadEngine#onOffloadServiceRemoved} callback are called. Multiple engines may be |
| * registered for the same interface, and that the same engine cannot be registered twice. |
| * |
| * @param ifaceName indicates which network interface the hardware offload runs on |
| * @param offloadType the type of offload that the offload engine support |
| * @param offloadCapability the capabilities of the offload engine |
| * @param executor the executor on which to receive the offload callbacks |
| * @param engine the OffloadEngine that will receive the offload callbacks |
| * @throws IllegalStateException if the engine is already registered. |
| * |
| * @hide |
| */ |
| @FlaggedApi(NsdManager.Flags.REGISTER_NSD_OFFLOAD_ENGINE_API) |
| @SystemApi |
| @RequiresPermission(anyOf = {NETWORK_SETTINGS, PERMISSION_MAINLINE_NETWORK_STACK, |
| NETWORK_STACK}) |
| public void registerOffloadEngine(@NonNull String ifaceName, |
| @OffloadEngine.OffloadType long offloadType, |
| @OffloadEngine.OffloadCapability long offloadCapability, @NonNull Executor executor, |
| @NonNull OffloadEngine engine) { |
| Objects.requireNonNull(ifaceName); |
| Objects.requireNonNull(executor); |
| Objects.requireNonNull(engine); |
| final OffloadEngineProxy cbImpl = new OffloadEngineProxy(executor, engine); |
| synchronized (mOffloadEngines) { |
| if (CollectionUtils.contains(mOffloadEngines, impl -> impl.mEngine == engine)) { |
| throw new IllegalStateException("This engine is already registered"); |
| } |
| mOffloadEngines.add(cbImpl); |
| } |
| try { |
| mService.registerOffloadEngine(ifaceName, cbImpl, offloadCapability, offloadType); |
| } catch (RemoteException e) { |
| e.rethrowFromSystemServer(); |
| } |
| } |
| |
| |
| /** |
| * Unregisters an OffloadEngine from NsdService. |
| * |
| * A caller can unregister itself as an OffloadEngine when it doesn't want to receive the |
| * callback anymore. The OffloadEngine must have been previously registered with the system |
| * using the {@link NsdManager#registerOffloadEngine} method. |
| * |
| * @param engine OffloadEngine object to be removed from NsdService |
| * @throws IllegalStateException if the engine is not registered. |
| * |
| * @hide |
| */ |
| @FlaggedApi(NsdManager.Flags.REGISTER_NSD_OFFLOAD_ENGINE_API) |
| @SystemApi |
| @RequiresPermission(anyOf = {NETWORK_SETTINGS, PERMISSION_MAINLINE_NETWORK_STACK, |
| NETWORK_STACK}) |
| public void unregisterOffloadEngine(@NonNull OffloadEngine engine) { |
| Objects.requireNonNull(engine); |
| final OffloadEngineProxy cbImpl; |
| synchronized (mOffloadEngines) { |
| final int index = CollectionUtils.indexOf(mOffloadEngines, |
| impl -> impl.mEngine == engine); |
| if (index < 0) { |
| throw new IllegalStateException("This engine is not registered"); |
| } |
| cbImpl = mOffloadEngines.remove(index); |
| } |
| |
| try { |
| mService.unregisterOffloadEngine(cbImpl); |
| } catch (RemoteException e) { |
| e.rethrowFromSystemServer(); |
| } |
| } |
| |
| private class PerNetworkDiscoveryTracker { |
| final String mServiceType; |
| final int mProtocolType; |
| final DiscoveryListener mBaseListener; |
| final Executor mBaseExecutor; |
| final ArrayMap<Network, DelegatingDiscoveryListener> mPerNetworkListeners = |
| new ArrayMap<>(); |
| |
| final NetworkCallback mNetworkCb = new NetworkCallback() { |
| @Override |
| public void onAvailable(@NonNull Network network) { |
| final DelegatingDiscoveryListener wrappedListener = new DelegatingDiscoveryListener( |
| network, mBaseListener, mBaseExecutor); |
| mPerNetworkListeners.put(network, wrappedListener); |
| // Run discovery callbacks inline on the service handler thread, which is the |
| // same thread used by this NetworkCallback, but DelegatingDiscoveryListener will |
| // use the base executor to run the wrapped callbacks. |
| discoverServices(mServiceType, mProtocolType, network, Runnable::run, |
| wrappedListener); |
| } |
| |
| @Override |
| public void onLost(@NonNull Network network) { |
| final DelegatingDiscoveryListener listener = mPerNetworkListeners.get(network); |
| if (listener == null) return; |
| listener.notifyAllServicesLost(); |
| // Listener will be removed from map in discovery stopped callback |
| stopServiceDiscovery(listener); |
| } |
| }; |
| |
| // Accessed from mHandler |
| private boolean mStopRequested; |
| |
| public void start(@NonNull NetworkRequest request) { |
| final ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class); |
| cm.registerNetworkCallback(request, mNetworkCb, mHandler); |
| mHandler.post(() -> mBaseExecutor.execute(() -> |
| mBaseListener.onDiscoveryStarted(mServiceType))); |
| } |
| |
| /** |
| * Stop discovery on all networks tracked by this class. |
| * |
| * This will request all underlying listeners to stop, and the last one to stop will call |
| * onDiscoveryStopped or onStopDiscoveryFailed. |
| * |
| * Must be called on the handler thread. |
| */ |
| public void requestStop() { |
| mHandler.post(() -> { |
| mStopRequested = true; |
| final ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class); |
| cm.unregisterNetworkCallback(mNetworkCb); |
| if (mPerNetworkListeners.size() == 0) { |
| mBaseExecutor.execute(() -> mBaseListener.onDiscoveryStopped(mServiceType)); |
| return; |
| } |
| for (int i = 0; i < mPerNetworkListeners.size(); i++) { |
| final DelegatingDiscoveryListener listener = mPerNetworkListeners.valueAt(i); |
| stopServiceDiscovery(listener); |
| } |
| }); |
| } |
| |
| private PerNetworkDiscoveryTracker(String serviceType, int protocolType, |
| Executor baseExecutor, DiscoveryListener baseListener) { |
| mServiceType = serviceType; |
| mProtocolType = protocolType; |
| mBaseExecutor = baseExecutor; |
| mBaseListener = baseListener; |
| } |
| |
| /** |
| * Subset of NsdServiceInfo that is tracked to generate service lost notifications when a |
| * network is lost. |
| * |
| * Service lost notifications only contain service name, type and network, so only track |
| * that information (Network is known from the listener). This also implements |
| * equals/hashCode for usage in maps. |
| */ |
| private class TrackedNsdInfo { |
| private final String mServiceName; |
| private final String mServiceType; |
| TrackedNsdInfo(NsdServiceInfo info) { |
| mServiceName = info.getServiceName(); |
| mServiceType = info.getServiceType(); |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hash(mServiceName, mServiceType); |
| } |
| |
| @Override |
| public boolean equals(Object obj) { |
| if (!(obj instanceof TrackedNsdInfo)) return false; |
| final TrackedNsdInfo other = (TrackedNsdInfo) obj; |
| return Objects.equals(mServiceName, other.mServiceName) |
| && Objects.equals(mServiceType, other.mServiceType); |
| } |
| } |
| |
| /** |
| * A listener wrapping calls to an app-provided listener, while keeping track of found |
| * services, so they can all be reported lost when the underlying network is lost. |
| * |
| * This should be registered to run on the service handler. |
| */ |
| private class DelegatingDiscoveryListener implements DiscoveryListener { |
| private final Network mNetwork; |
| private final DiscoveryListener mWrapped; |
| private final Executor mWrappedExecutor; |
| private final ArraySet<TrackedNsdInfo> mFoundInfo = new ArraySet<>(); |
| // When this flag is set to true, no further service found or lost callbacks should be |
| // handled. This flag indicates that the network for this DelegatingDiscoveryListener is |
| // lost, and any further callbacks would be redundant. |
| private boolean mAllServicesLost = false; |
| |
| private DelegatingDiscoveryListener(Network network, DiscoveryListener listener, |
| Executor executor) { |
| mNetwork = network; |
| mWrapped = listener; |
| mWrappedExecutor = executor; |
| } |
| |
| void notifyAllServicesLost() { |
| for (int i = 0; i < mFoundInfo.size(); i++) { |
| final TrackedNsdInfo trackedInfo = mFoundInfo.valueAt(i); |
| final NsdServiceInfo serviceInfo = new NsdServiceInfo( |
| trackedInfo.mServiceName, trackedInfo.mServiceType); |
| serviceInfo.setNetwork(mNetwork); |
| mWrappedExecutor.execute(() -> mWrapped.onServiceLost(serviceInfo)); |
| } |
| mAllServicesLost = true; |
| } |
| |
| @Override |
| public void onStartDiscoveryFailed(String serviceType, int errorCode) { |
| // The delegated listener is used when NsdManager takes care of starting/stopping |
| // discovery on multiple networks. Failure to start on one network is not a global |
| // failure to be reported up, as other networks may succeed: just log. |
| Log.e(TAG, "Failed to start discovery for " + serviceType + " on " + mNetwork |
| + " with code " + errorCode); |
| mPerNetworkListeners.remove(mNetwork); |
| } |
| |
| @Override |
| public void onDiscoveryStarted(String serviceType) { |
| // Wrapped listener was called upon registration, it is not called for discovery |
| // on each network |
| } |
| |
| @Override |
| public void onStopDiscoveryFailed(String serviceType, int errorCode) { |
| Log.e(TAG, "Failed to stop discovery for " + serviceType + " on " + mNetwork |
| + " with code " + errorCode); |
| mPerNetworkListeners.remove(mNetwork); |
| if (mStopRequested && mPerNetworkListeners.size() == 0) { |
| // Do not report onStopDiscoveryFailed when some underlying listeners failed: |
| // this does not mean that all listeners did, and onStopDiscoveryFailed is not |
| // actionable anyway. Just report that discovery stopped. |
| mWrappedExecutor.execute(() -> mWrapped.onDiscoveryStopped(serviceType)); |
| } |
| } |
| |
| @Override |
| public void onDiscoveryStopped(String serviceType) { |
| mPerNetworkListeners.remove(mNetwork); |
| if (mStopRequested && mPerNetworkListeners.size() == 0) { |
| mWrappedExecutor.execute(() -> mWrapped.onDiscoveryStopped(serviceType)); |
| } |
| } |
| |
| @Override |
| public void onServiceFound(NsdServiceInfo serviceInfo) { |
| if (mAllServicesLost) { |
| // This DelegatingDiscoveryListener no longer has a network connection. Ignore |
| // the callback. |
| return; |
| } |
| mFoundInfo.add(new TrackedNsdInfo(serviceInfo)); |
| mWrappedExecutor.execute(() -> mWrapped.onServiceFound(serviceInfo)); |
| } |
| |
| @Override |
| public void onServiceLost(NsdServiceInfo serviceInfo) { |
| if (mAllServicesLost) { |
| // This DelegatingDiscoveryListener no longer has a network connection. Ignore |
| // the callback. |
| return; |
| } |
| mFoundInfo.remove(new TrackedNsdInfo(serviceInfo)); |
| mWrappedExecutor.execute(() -> mWrapped.onServiceLost(serviceInfo)); |
| } |
| } |
| } |
| |
| /** |
| * 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; |
| // Use a common singleton thread ConnectivityThread to be shared among all nsd tasks. |
| // Instead of launching separate threads to handle tasks from the various instances. |
| mHandler = new ServiceHandler(ConnectivityThread.getInstanceLooper()); |
| |
| try { |
| mService = service.connect(new NsdCallbackImpl(mHandler), CompatChanges.isChangeEnabled( |
| ENABLE_PLATFORM_MDNS_BACKEND)); |
| } catch (RemoteException e) { |
| throw new RuntimeException("Failed to connect to NsdService"); |
| } |
| |
| // Only proactively start the daemon if the target SDK < S AND platform < V, For target |
| // SDK >= S AND platform < V, the internal service would automatically start/stop the native |
| // daemon as needed. For platform >= V, no action is required because the native daemon is |
| // completely removed. |
| if (!CompatChanges.isChangeEnabled(RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER) |
| && !SdkLevel.isAtLeastV()) { |
| 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 sendDiscoveryRequest( |
| int message, int listenerKey, DiscoveryRequest discoveryRequest) { |
| mServHandler.sendMessage( |
| mServHandler.obtainMessage(message, 0, listenerKey, discoveryRequest)); |
| } |
| |
| 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, DiscoveryRequest discoveryRequest) { |
| sendDiscoveryRequest(DISCOVER_SERVICES_STARTED, listenerKey, discoveryRequest); |
| } |
| |
| @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); |
| } |
| |
| @Override |
| public void onStopResolutionFailed(int listenerKey, int error) { |
| sendError(STOP_RESOLUTION_FAILED, listenerKey, error); |
| } |
| |
| @Override |
| 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); |
| } |
| } |
| |
| /** |
| * 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; |
| |
| /** |
| * Indicates that the stop operation failed because it is not running. |
| * This failure is passed with {@link ResolveListener#onStopResolutionFailed}. |
| */ |
| 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 = { |
| FAILURE_OPERATION_NOT_RUNNING, |
| }) |
| 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 { |
| |
| 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); |
| } |
| |
| /** |
| * Callback for use with {@link NsdManager#resolveService} to resolve the service info and use |
| * with {@link NsdManager#stopServiceResolution} to stop resolution. |
| */ |
| public interface ResolveListener { |
| |
| /** |
| * Called on the internal thread or with an executor passed to |
| * {@link NsdManager#resolveService} to report the resolution was failed with an error. |
| * |
| * A resolution operation would call either onServiceResolved or onResolveFailed once based |
| * on the result. |
| */ |
| void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode); |
| |
| /** |
| * Called on the internal thread or with an executor passed to |
| * {@link NsdManager#resolveService} to report the resolved service info. |
| * |
| * A resolution operation would call either onServiceResolved or onResolveFailed once based |
| * on the result. |
| */ |
| void onServiceResolved(NsdServiceInfo serviceInfo); |
| |
| /** |
| * Called on the internal thread or with an executor passed to |
| * {@link NsdManager#resolveService} to report the resolution was stopped. |
| * |
| * A stop resolution operation would call either onResolutionStopped or |
| * onStopResolutionFailed once based on the result. |
| */ |
| default void onResolutionStopped(@NonNull NsdServiceInfo serviceInfo) { } |
| |
| /** |
| * Called once on the internal thread or with an executor passed to |
| * {@link NsdManager#resolveService} to report that stopping resolution failed with an |
| * error. |
| * |
| * A stop resolution operation would call either onResolutionStopped or |
| * onStopResolutionFailed once based on the result. |
| */ |
| default void onStopResolutionFailed(@NonNull NsdServiceInfo serviceInfo, |
| @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) { |
| super(looper); |
| } |
| |
| @Override |
| public void handleMessage(Message message) { |
| // Do not use message in the executor lambdas, as it will be recycled once this method |
| // returns. Keep references to its content instead. |
| final int what = message.what; |
| final int errorCode = message.arg1; |
| final int key = message.arg2; |
| final Object obj = message.obj; |
| final Object listener; |
| final NsdServiceInfo ns; |
| final DiscoveryRequest discoveryRequest; |
| final Executor executor; |
| synchronized (mMapLock) { |
| listener = mListenerMap.get(key); |
| ns = mServiceMap.get(key); |
| discoveryRequest = mDiscoveryMap.get(key); |
| executor = mExecutorMap.get(key); |
| } |
| if (listener == null) { |
| Log.d(TAG, "Stale key " + key); |
| return; |
| } |
| if (DBG) { |
| if (discoveryRequest != null) { |
| Log.d(TAG, "received " + nameOf(what) + " for key " + key + ", discovery " |
| + discoveryRequest); |
| } else { |
| Log.d(TAG, "received " + nameOf(what) + " for key " + key + ", service " + ns); |
| } |
| } |
| switch (what) { |
| case DISCOVER_SERVICES_STARTED: |
| final String s = getNsdServiceInfoType((DiscoveryRequest) obj); |
| executor.execute(() -> ((DiscoveryListener) listener).onDiscoveryStarted(s)); |
| break; |
| case DISCOVER_SERVICES_FAILED: |
| removeListener(key); |
| executor.execute(() -> ((DiscoveryListener) listener).onStartDiscoveryFailed( |
| getNsdServiceInfoType(discoveryRequest), errorCode)); |
| break; |
| case SERVICE_FOUND: |
| executor.execute(() -> ((DiscoveryListener) listener).onServiceFound( |
| (NsdServiceInfo) obj)); |
| break; |
| case SERVICE_LOST: |
| executor.execute(() -> ((DiscoveryListener) listener).onServiceLost( |
| (NsdServiceInfo) 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); |
| executor.execute(() -> ((DiscoveryListener) listener).onStopDiscoveryFailed( |
| getNsdServiceInfoType(discoveryRequest), errorCode)); |
| break; |
| case STOP_DISCOVERY_SUCCEEDED: |
| removeListener(key); |
| executor.execute(() -> ((DiscoveryListener) listener).onDiscoveryStopped( |
| getNsdServiceInfoType(discoveryRequest))); |
| break; |
| case REGISTER_SERVICE_FAILED: |
| removeListener(key); |
| executor.execute(() -> ((RegistrationListener) listener).onRegistrationFailed( |
| ns, errorCode)); |
| break; |
| case REGISTER_SERVICE_SUCCEEDED: |
| executor.execute(() -> ((RegistrationListener) listener).onServiceRegistered( |
| (NsdServiceInfo) obj)); |
| break; |
| case UNREGISTER_SERVICE_FAILED: |
| removeListener(key); |
| executor.execute(() -> ((RegistrationListener) listener).onUnregistrationFailed( |
| ns, errorCode)); |
| break; |
| case UNREGISTER_SERVICE_SUCCEEDED: |
| // TODO: do not unregister listener until service is unregistered, or provide |
| // alternative way for unregistering ? |
| removeListener(key); |
| executor.execute(() -> ((RegistrationListener) listener).onServiceUnregistered( |
| ns)); |
| break; |
| case RESOLVE_SERVICE_FAILED: |
| removeListener(key); |
| executor.execute(() -> ((ResolveListener) listener).onResolveFailed( |
| ns, errorCode)); |
| break; |
| case RESOLVE_SERVICE_SUCCEEDED: |
| removeListener(key); |
| executor.execute(() -> ((ResolveListener) listener).onServiceResolved( |
| (NsdServiceInfo) obj)); |
| break; |
| case STOP_RESOLUTION_FAILED: |
| removeListener(key); |
| executor.execute(() -> ((ResolveListener) listener).onStopResolutionFailed( |
| ns, errorCode)); |
| break; |
| case STOP_RESOLUTION_SUCCEEDED: |
| removeListener(key); |
| executor.execute(() -> ((ResolveListener) listener).onResolutionStopped( |
| 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; |
| } |
| } |
| } |
| |
| private int nextListenerKey() { |
| // Ensure mListenerKey >= FIRST_LISTENER_KEY; |
| mListenerKey = Math.max(FIRST_LISTENER_KEY, mListenerKey + 1); |
| return mListenerKey; |
| } |
| |
| private int putListener(Object listener, Executor e, NsdServiceInfo serviceInfo) { |
| synchronized (mMapLock) { |
| return putListener(listener, e, mServiceMap, serviceInfo); |
| } |
| } |
| |
| private int putListener(Object listener, Executor e, DiscoveryRequest discoveryRequest) { |
| synchronized (mMapLock) { |
| return putListener(listener, e, mDiscoveryMap, discoveryRequest); |
| } |
| } |
| |
| // Assert that the listener is not in the map, then add it and returns its key |
| private <T> int putListener(Object listener, Executor e, SparseArray<T> map, T value) { |
| synchronized (mMapLock) { |
| checkListener(listener); |
| final int key; |
| final int valueIndex = mListenerMap.indexOfValue(listener); |
| if (valueIndex != -1) { |
| throw new IllegalArgumentException("listener already in use"); |
| } |
| key = nextListenerKey(); |
| mListenerMap.put(key, listener); |
| map.put(key, value); |
| mExecutorMap.put(key, e); |
| return key; |
| } |
| } |
| |
| private int updateRegisteredListener(Object listener, Executor e, NsdServiceInfo s) { |
| final int key; |
| synchronized (mMapLock) { |
| key = getListenerKey(listener); |
| mServiceMap.put(key, s); |
| mExecutorMap.put(key, e); |
| } |
| return key; |
| } |
| |
| private void removeListener(int key) { |
| synchronized (mMapLock) { |
| mListenerMap.remove(key); |
| mServiceMap.remove(key); |
| mDiscoveryMap.remove(key); |
| mExecutorMap.remove(key); |
| } |
| } |
| |
| private int getListenerKey(Object listener) { |
| checkListener(listener); |
| synchronized (mMapLock) { |
| int valueIndex = mListenerMap.indexOfValue(listener); |
| if (valueIndex == -1) { |
| throw new IllegalArgumentException("listener not registered"); |
| } |
| return mListenerMap.keyAt(valueIndex); |
| } |
| } |
| |
| private static String getNsdServiceInfoType(DiscoveryRequest r) { |
| if (r == null) return "?"; |
| return r.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) { |
| registerService(serviceInfo, protocolType, Runnable::run, listener); |
| } |
| |
| /** |
| * 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 executor Executor to run listener callbacks with |
| * @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. |
| */ |
| public void registerService(@NonNull NsdServiceInfo serviceInfo, int protocolType, |
| @NonNull Executor executor, @NonNull RegistrationListener listener) { |
| checkServiceInfoForRegistration(serviceInfo); |
| checkProtocol(protocolType); |
| final AdvertisingRequest.Builder builder = new AdvertisingRequest.Builder(serviceInfo, |
| protocolType); |
| // Optionally assume that the request is an update request if it uses subtypes and the same |
| // listener. This is not documented behavior as support for advertising subtypes via |
| // "_servicename,_sub1,_sub2" has never been documented in the first place, and using |
| // multiple subtypes was broken in T until a later module update. Subtype registration is |
| // documented in the NsdServiceInfo.setSubtypes API instead, but this provides a limited |
| // option for users of the older undocumented behavior, only for subtype changes. |
| if (isSubtypeUpdateRequest(serviceInfo, listener)) { |
| builder.setAdvertisingConfig(AdvertisingRequest.NSD_ADVERTISING_UPDATE_ONLY); |
| } |
| registerService(builder.build(), executor, listener); |
| } |
| |
| private boolean isSubtypeUpdateRequest(@NonNull NsdServiceInfo serviceInfo, @NonNull |
| RegistrationListener listener) { |
| // If the listener is the same object, serviceInfo is for the same service name and |
| // type (outside of subtypes), and either of them use subtypes, treat the request as a |
| // subtype update request. |
| synchronized (mMapLock) { |
| int valueIndex = mListenerMap.indexOfValue(listener); |
| if (valueIndex == -1) { |
| return false; |
| } |
| final int key = mListenerMap.keyAt(valueIndex); |
| NsdServiceInfo existingService = mServiceMap.get(key); |
| if (existingService == null) { |
| return false; |
| } |
| final Pair<String, String> existingTypeSubtype = getTypeAndSubtypes( |
| existingService.getServiceType()); |
| final Pair<String, String> newTypeSubtype = getTypeAndSubtypes( |
| serviceInfo.getServiceType()); |
| if (existingTypeSubtype == null || newTypeSubtype == null) { |
| return false; |
| } |
| final boolean existingHasNoSubtype = TextUtils.isEmpty(existingTypeSubtype.second); |
| final boolean updatedHasNoSubtype = TextUtils.isEmpty(newTypeSubtype.second); |
| if (existingHasNoSubtype && updatedHasNoSubtype) { |
| // Only allow subtype changes when subtypes are used. This ensures that this |
| // behavior does not affect most requests. |
| return false; |
| } |
| |
| return Objects.equals(existingService.getServiceName(), serviceInfo.getServiceName()) |
| && Objects.equals(existingTypeSubtype.first, newTypeSubtype.first); |
| } |
| } |
| |
| /** |
| * Get the base type from a type specification with "_type._tcp,sub1,sub2" syntax. |
| * |
| * <p>This rejects specifications using dot syntax to specify subtypes ("_sub1._type._tcp"). |
| * |
| * @return Type and comma-separated list of subtypes, or null if invalid format. |
| */ |
| @Nullable |
| private static Pair<String, String> getTypeAndSubtypes(@Nullable String typeWithSubtype) { |
| if (typeWithSubtype == null) { |
| return null; |
| } |
| final Matcher matcher = Pattern.compile(TYPE_REGEX).matcher(typeWithSubtype); |
| if (!matcher.matches()) return null; |
| // Reject specifications using leading subtypes with a dot |
| if (!TextUtils.isEmpty(matcher.group(1))) return null; |
| return new Pair<>(matcher.group(2), matcher.group(3)); |
| } |
| |
| /** |
| * 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 advertisingRequest service being registered |
| * @param executor Executor to run listener callbacks with |
| * @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. |
| * |
| * @hide |
| */ |
| // @FlaggedApi(Flags.ADVERTISE_REQUEST_API) |
| public void registerService(@NonNull AdvertisingRequest advertisingRequest, |
| @NonNull Executor executor, |
| @NonNull RegistrationListener listener) { |
| final NsdServiceInfo serviceInfo = advertisingRequest.getServiceInfo(); |
| final int protocolType = advertisingRequest.getProtocolType(); |
| checkServiceInfoForRegistration(serviceInfo); |
| checkProtocol(protocolType); |
| final int key; |
| // For update only request, the old listener has to be reused |
| if ((advertisingRequest.getAdvertisingConfig() |
| & AdvertisingRequest.NSD_ADVERTISING_UPDATE_ONLY) > 0) { |
| key = updateRegisteredListener(listener, executor, serviceInfo); |
| } else { |
| key = putListener(listener, executor, serviceInfo); |
| } |
| try { |
| mService.registerService(key, advertisingRequest); |
| } 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) { |
| discoverServices(serviceType, protocolType, (Network) null, Runnable::run, listener); |
| } |
| |
| /** |
| * 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 network Network to discover services on, or null to discover on all available networks |
| * @param executor Executor to run listener callbacks with |
| * @param listener The listener notifies of a successful discovery and is used |
| * to stop discovery on this serviceType through a call on {@link #stopServiceDiscovery}. |
| */ |
| public void discoverServices(@NonNull String serviceType, int protocolType, |
| @Nullable Network network, @NonNull Executor executor, |
| @NonNull DiscoveryListener listener) { |
| if (TextUtils.isEmpty(serviceType)) { |
| throw new IllegalArgumentException("Service type cannot be empty"); |
| } |
| DiscoveryRequest request = new DiscoveryRequest.Builder(protocolType, serviceType) |
| .setNetwork(network).build(); |
| discoverServices(request, executor, listener); |
| } |
| |
| /** |
| * Initiates 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 discoveryRequest the {@link DiscoveryRequest} object which specifies the discovery |
| * parameters such as service type, subtype and network |
| * @param executor Executor to run listener callbacks with |
| * @param listener The listener notifies of a successful discovery and is used |
| * to stop discovery on this serviceType through a call on {@link #stopServiceDiscovery}. |
| */ |
| @FlaggedApi(Flags.NSD_SUBTYPES_SUPPORT_ENABLED) |
| public void discoverServices(@NonNull DiscoveryRequest discoveryRequest, |
| @NonNull Executor executor, @NonNull DiscoveryListener listener) { |
| int key = putListener(listener, executor, discoveryRequest); |
| try { |
| mService.discoverServices(key, discoveryRequest); |
| } 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. |
| * |
| * <p> During discovery, new networks may connect or existing networks may disconnect - for |
| * example if wifi is reconnected. When a service was found on a network that disconnects, |
| * {@link DiscoveryListener#onServiceLost} will be called. If a new network connects that |
| * matches the {@link NetworkRequest}, {@link DiscoveryListener#onServiceFound} will be called |
| * for services found on that network. Applications that do not want to track networks |
| * themselves are encouraged to use this method instead of other overloads of |
| * {@code discoverServices}, as they will receive proper notifications when a service becomes |
| * available or unavailable due to network changes. |
| * @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 networkRequest Request specifying networks that should be considered when discovering |
| * @param executor Executor to run listener callbacks with |
| * @param listener The listener notifies of a successful discovery and is used |
| * to stop discovery on this serviceType through a call on {@link #stopServiceDiscovery}. |
| */ |
| @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) |
| public void discoverServices(@NonNull String serviceType, int protocolType, |
| @NonNull NetworkRequest networkRequest, @NonNull Executor executor, |
| @NonNull DiscoveryListener listener) { |
| if (TextUtils.isEmpty(serviceType)) { |
| throw new IllegalArgumentException("Service type cannot be empty"); |
| } |
| Objects.requireNonNull(networkRequest, "NetworkRequest cannot be null"); |
| DiscoveryRequest discoveryRequest = |
| new DiscoveryRequest.Builder(protocolType, serviceType).build(); |
| |
| final int baseListenerKey = putListener(listener, executor, discoveryRequest); |
| |
| final PerNetworkDiscoveryTracker discoveryInfo = new PerNetworkDiscoveryTracker( |
| serviceType, protocolType, executor, listener); |
| |
| synchronized (mPerNetworkDiscoveryMap) { |
| mPerNetworkDiscoveryMap.put(baseListenerKey, discoveryInfo); |
| discoveryInfo.start(networkRequest); |
| } |
| } |
| |
| /** |
| * 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); |
| // If this is a PerNetworkDiscovery request, handle it as such |
| synchronized (mPerNetworkDiscoveryMap) { |
| final PerNetworkDiscoveryTracker info = mPerNetworkDiscoveryMap.get(id); |
| if (info != null) { |
| info.requestStop(); |
| return; |
| } |
| } |
| 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. |
| * |
| * @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); |
| } |
| |
| /** |
| * 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 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) { |
| checkServiceInfoForResolution(serviceInfo); |
| int key = putListener(listener, executor, serviceInfo); |
| try { |
| mService.resolveService(key, serviceInfo); |
| } catch (RemoteException e) { |
| e.rethrowFromSystemServer(); |
| } |
| } |
| |
| /** |
| * Stop service resolution initiated with {@link #resolveService}. |
| * |
| * A successful stop is notified with a call to {@link ResolveListener#onResolutionStopped}. |
| * |
| * <p> Upon failure to stop service resolution for example if resolution is done or the |
| * requester stops resolution repeatedly, the application is notified |
| * {@link ResolveListener#onStopResolutionFailed} with {@link #FAILURE_OPERATION_NOT_RUNNING} |
| * |
| * @param listener This should be a listener object that was passed to {@link #resolveService}. |
| * It identifies the resolution that should be stopped and notifies of a |
| * successful or unsuccessful stop. Throws {@code IllegalArgumentException} if |
| * the listener was not passed to resolveService before. |
| */ |
| public void stopServiceResolution(@NonNull ResolveListener listener) { |
| int id = getListenerKey(listener); |
| try { |
| mService.stopResolution(id); |
| } catch (RemoteException e) { |
| e.rethrowFromSystemServer(); |
| } |
| } |
| |
| /** |
| * 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}. |
| * |
| * @param serviceInfo the service to receive updates for |
| * @param executor Executor to run callbacks with |
| * @param listener to receive callback upon service update |
| */ |
| // TODO: use {@link DiscoveryRequest} to specify the service to be subscribed |
| public void registerServiceInfoCallback(@NonNull NsdServiceInfo serviceInfo, |
| @NonNull Executor executor, @NonNull ServiceInfoCallback listener) { |
| checkServiceInfoForResolution(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"); |
| } |
| |
| static void checkProtocol(int protocolType) { |
| if (protocolType != PROTOCOL_DNS_SD) { |
| throw new IllegalArgumentException("Unsupported protocol"); |
| } |
| } |
| |
| private static void checkServiceInfoForResolution(NsdServiceInfo serviceInfo) { |
| Objects.requireNonNull(serviceInfo, "NsdServiceInfo cannot be null"); |
| if (TextUtils.isEmpty(serviceInfo.getServiceName())) { |
| throw new IllegalArgumentException("Service name cannot be empty"); |
| } |
| if (TextUtils.isEmpty(serviceInfo.getServiceType())) { |
| throw new IllegalArgumentException("Service type cannot be empty"); |
| } |
| } |
| |
| private enum ServiceValidationType { |
| NO_SERVICE, |
| HAS_SERVICE, // A service with a positive port |
| HAS_SERVICE_ZERO_PORT, // A service with a zero port |
| } |
| |
| private enum HostValidationType { |
| DEFAULT_HOST, // No host is specified so the default host will be used |
| CUSTOM_HOST, // A custom host with addresses is specified |
| CUSTOM_HOST_NO_ADDRESS, // A custom host without address is specified |
| } |
| |
| private enum PublicKeyValidationType { |
| NO_KEY, |
| HAS_KEY, |
| } |
| |
| /** |
| * Check if the service is valid for registration and classify it as one of {@link |
| * ServiceValidationType}. |
| */ |
| private static ServiceValidationType validateService(NsdServiceInfo serviceInfo) { |
| final boolean hasServiceName = !TextUtils.isEmpty(serviceInfo.getServiceName()); |
| final boolean hasServiceType = !TextUtils.isEmpty(serviceInfo.getServiceType()); |
| if (!hasServiceName && !hasServiceType && serviceInfo.getPort() == 0) { |
| return ServiceValidationType.NO_SERVICE; |
| } |
| if (hasServiceName && hasServiceType) { |
| if (serviceInfo.getPort() < 0) { |
| throw new IllegalArgumentException("Invalid port"); |
| } |
| if (serviceInfo.getPort() == 0) { |
| return ServiceValidationType.HAS_SERVICE_ZERO_PORT; |
| } |
| return ServiceValidationType.HAS_SERVICE; |
| } |
| throw new IllegalArgumentException("The service name or the service type is missing"); |
| } |
| |
| /** |
| * Check if the host is valid for registration and classify it as one of {@link |
| * HostValidationType}. |
| */ |
| private static HostValidationType validateHost(NsdServiceInfo serviceInfo) { |
| final boolean hasHostname = !TextUtils.isEmpty(serviceInfo.getHostname()); |
| final boolean hasHostAddresses = !CollectionUtils.isEmpty(serviceInfo.getHostAddresses()); |
| if (!hasHostname) { |
| // Keep compatible with the legacy behavior: It's allowed to set host |
| // addresses for a service registration although the host addresses |
| // won't be registered. To register the addresses for a host, the |
| // hostname must be specified. |
| return HostValidationType.DEFAULT_HOST; |
| } |
| if (!hasHostAddresses) { |
| return HostValidationType.CUSTOM_HOST_NO_ADDRESS; |
| } |
| return HostValidationType.CUSTOM_HOST; |
| } |
| |
| /** |
| * Check if the public key is valid for registration and classify it as one of {@link |
| * PublicKeyValidationType}. |
| * |
| * <p>For simplicity, it only checks if the protocol is DNSSEC and the RDATA is not fewer than 4 |
| * bytes. See RFC 3445 Section 3. |
| */ |
| private static PublicKeyValidationType validatePublicKey(NsdServiceInfo serviceInfo) { |
| byte[] publicKey = serviceInfo.getPublicKey(); |
| if (publicKey == null) { |
| return PublicKeyValidationType.NO_KEY; |
| } |
| if (publicKey.length < 4) { |
| throw new IllegalArgumentException("The public key should be at least 4 bytes long"); |
| } |
| int protocol = publicKey[2]; |
| if (protocol == DNSSEC_PROTOCOL) { |
| return PublicKeyValidationType.HAS_KEY; |
| } |
| throw new IllegalArgumentException( |
| "The public key's protocol (" |
| + protocol |
| + ") is invalid. It should be DNSSEC_PROTOCOL (3)"); |
| } |
| |
| /** |
| * Check if the {@link NsdServiceInfo} is valid for registration. |
| * |
| * <p>Firstly, check if service, host and public key are all valid respectively. Then check if |
| * the combination of service, host and public key is valid. |
| * |
| * <p>If the {@code serviceInfo} is invalid, throw an {@link IllegalArgumentException} |
| * describing the reason. |
| * |
| * <p>There are the invalid combinations of service, host and public key: |
| * |
| * <ul> |
| * <li>Neither service nor host is specified. |
| * <li>No public key is specified and the service has a zero port. |
| * <li>The registration only contains the hostname but addresses are missing. |
| * </ul> |
| * |
| * <p>Keys are used to reserve hostnames or service names while the service/host is temporarily |
| * inactive, so registrations with a key and just a hostname or a service name are acceptable. |
| * |
| * @hide |
| */ |
| public static void checkServiceInfoForRegistration(NsdServiceInfo serviceInfo) { |
| Objects.requireNonNull(serviceInfo, "NsdServiceInfo cannot be null"); |
| |
| final ServiceValidationType serviceValidation = validateService(serviceInfo); |
| final HostValidationType hostValidation = validateHost(serviceInfo); |
| final PublicKeyValidationType publicKeyValidation = validatePublicKey(serviceInfo); |
| |
| if (serviceValidation == ServiceValidationType.NO_SERVICE |
| && hostValidation == HostValidationType.DEFAULT_HOST) { |
| throw new IllegalArgumentException("Nothing to register"); |
| } |
| if (publicKeyValidation == PublicKeyValidationType.NO_KEY) { |
| if (serviceValidation == ServiceValidationType.HAS_SERVICE_ZERO_PORT) { |
| throw new IllegalArgumentException("The port is missing"); |
| } |
| if (serviceValidation == ServiceValidationType.NO_SERVICE |
| && hostValidation == HostValidationType.CUSTOM_HOST_NO_ADDRESS) { |
| throw new IllegalArgumentException( |
| "The host addresses must be specified unless there is a service"); |
| } |
| } |
| } |
| } |