1887 lines
84 KiB
Java
1887 lines
84 KiB
Java
![]() |
/*
|
||
|
* 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");
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|