Introduce NsdManager API for in-place service updates without re-announcement.
This commit builds upon CL aosp/2817455 by adding a new NsdManager API to
update registered services directly, eliminating the need for exit and
re-announcement messages. This preserves compatibility with existing
apps without requiring an API bump. If an app registers a service with
the same listener and NsdServiceInfo, it's treated as an update request.
Bug: 309372239
Test: TH
Change-Id: Ib84ddf0d92e757eaef225f3e32f5223bc50c8fd6
diff --git a/framework-t/src/android/net/nsd/AdvertisingRequest.java b/framework-t/src/android/net/nsd/AdvertisingRequest.java
new file mode 100644
index 0000000..b1ef98f
--- /dev/null
+++ b/framework-t/src/android/net/nsd/AdvertisingRequest.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net.nsd;
+
+import android.annotation.LongDef;
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Encapsulates parameters for {@link NsdManager#registerService}.
+ * @hide
+ */
+//@FlaggedApi(NsdManager.Flags.ADVERTISE_REQUEST_API)
+public final class AdvertisingRequest implements Parcelable {
+
+ /**
+ * Only update the registration without sending exit and re-announcement.
+ */
+ public static final int NSD_ADVERTISING_UPDATE_ONLY = 1;
+
+
+ @NonNull
+ public static final Creator<AdvertisingRequest> CREATOR =
+ new Creator<>() {
+ @Override
+ public AdvertisingRequest createFromParcel(Parcel in) {
+ final NsdServiceInfo serviceInfo = in.readParcelable(
+ NsdServiceInfo.class.getClassLoader(), NsdServiceInfo.class);
+ final int protocolType = in.readInt();
+ final long advertiseConfig = in.readLong();
+ return new AdvertisingRequest(serviceInfo, protocolType, advertiseConfig);
+ }
+
+ @Override
+ public AdvertisingRequest[] newArray(int size) {
+ return new AdvertisingRequest[size];
+ }
+ };
+ @NonNull
+ private final NsdServiceInfo mServiceInfo;
+ private final int mProtocolType;
+ // Bitmask of @AdvertisingConfig flags. Uses a long to allow 64 possible flags in the future.
+ private final long mAdvertisingConfig;
+
+ /**
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @LongDef(flag = true, prefix = {"NSD_ADVERTISING"}, value = {
+ NSD_ADVERTISING_UPDATE_ONLY,
+ })
+ @interface AdvertisingConfig {}
+
+ /**
+ * The constructor for the advertiseRequest
+ */
+ private AdvertisingRequest(@NonNull NsdServiceInfo serviceInfo, int protocolType,
+ long advertisingConfig) {
+ mServiceInfo = serviceInfo;
+ mProtocolType = protocolType;
+ mAdvertisingConfig = advertisingConfig;
+ }
+
+ /**
+ * Returns the {@link NsdServiceInfo}
+ */
+ @NonNull
+ public NsdServiceInfo getServiceInfo() {
+ return mServiceInfo;
+ }
+
+ /**
+ * Returns the service advertise protocol
+ */
+ public int getProtocolType() {
+ return mProtocolType;
+ }
+
+ /**
+ * Returns the advertising config.
+ */
+ public long getAdvertisingConfig() {
+ return mAdvertisingConfig;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("serviceInfo: ").append(mServiceInfo)
+ .append(", protocolType: ").append(mProtocolType)
+ .append(", advertisingConfig: ").append(mAdvertisingConfig);
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ } else if (!(other instanceof AdvertisingRequest)) {
+ return false;
+ } else {
+ final AdvertisingRequest otherRequest = (AdvertisingRequest) other;
+ return mServiceInfo.equals(otherRequest.mServiceInfo)
+ && mProtocolType == otherRequest.mProtocolType
+ && mAdvertisingConfig == otherRequest.mAdvertisingConfig;
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mServiceInfo, mProtocolType, mAdvertisingConfig);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeParcelable(mServiceInfo, flags);
+ dest.writeInt(mProtocolType);
+ dest.writeLong(mAdvertisingConfig);
+ }
+
+// @FlaggedApi(NsdManager.Flags.ADVERTISE_REQUEST_API)
+ /**
+ * The builder for creating new {@link AdvertisingRequest} objects.
+ * @hide
+ */
+ public static final class Builder {
+ @NonNull
+ private final NsdServiceInfo mServiceInfo;
+ private final int mProtocolType;
+ private long mAdvertisingConfig;
+ /**
+ * Creates a new {@link Builder} object.
+ */
+ public Builder(@NonNull NsdServiceInfo serviceInfo, int protocolType) {
+ mServiceInfo = serviceInfo;
+ mProtocolType = protocolType;
+ }
+
+ /**
+ * Sets advertising configuration flags.
+ *
+ * @param advertisingConfigFlags Bitmask of {@code AdvertisingConfig} flags.
+ */
+ @NonNull
+ public Builder setAdvertisingConfig(long advertisingConfigFlags) {
+ mAdvertisingConfig = advertisingConfigFlags;
+ return this;
+ }
+
+
+ /** Creates a new {@link AdvertisingRequest} object. */
+ @NonNull
+ public AdvertisingRequest build() {
+ return new AdvertisingRequest(mServiceInfo, mProtocolType, mAdvertisingConfig);
+ }
+ }
+}
diff --git a/framework-t/src/android/net/nsd/INsdServiceConnector.aidl b/framework-t/src/android/net/nsd/INsdServiceConnector.aidl
index e671db1..b03eb29 100644
--- a/framework-t/src/android/net/nsd/INsdServiceConnector.aidl
+++ b/framework-t/src/android/net/nsd/INsdServiceConnector.aidl
@@ -16,6 +16,7 @@
package android.net.nsd;
+import android.net.nsd.AdvertisingRequest;
import android.net.nsd.INsdManagerCallback;
import android.net.nsd.IOffloadEngine;
import android.net.nsd.NsdServiceInfo;
@@ -27,7 +28,7 @@
* {@hide}
*/
interface INsdServiceConnector {
- void registerService(int listenerKey, in NsdServiceInfo serviceInfo);
+ void registerService(int listenerKey, in AdvertisingRequest advertisingRequest);
void unregisterService(int listenerKey);
void discoverServices(int listenerKey, in NsdServiceInfo serviceInfo);
void stopDiscovery(int listenerKey);
diff --git a/framework-t/src/android/net/nsd/NsdManager.java b/framework-t/src/android/net/nsd/NsdManager.java
index fcf79eb..053ecf2 100644
--- a/framework-t/src/android/net/nsd/NsdManager.java
+++ b/framework-t/src/android/net/nsd/NsdManager.java
@@ -46,6 +46,7 @@
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;
@@ -57,6 +58,8 @@
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
@@ -152,9 +155,38 @@
"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";
}
/**
+ * A regex for the acceptable format of a type or subtype label.
+ * @hide
+ */
+ public static final String TYPE_SUBTYPE_LABEL_REGEX = "_[a-zA-Z0-9-_]{1,61}[a-zA-Z0-9]";
+
+ /**
+ * 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
+ "^(?:(" + TYPE_SUBTYPE_LABEL_REGEX + ")\\.)?"
+ // Actual type (_type._tcp.local)
+ + "(" + TYPE_SUBTYPE_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
+ + "((?:," + TYPE_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.
@@ -1098,6 +1130,16 @@
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);
@@ -1162,14 +1204,111 @@
*/
public void registerService(@NonNull NsdServiceInfo serviceInfo, int protocolType,
@NonNull Executor executor, @NonNull RegistrationListener listener) {
+ checkServiceInfo(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(@NonNull String typeWithSubtype) {
+ 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();
if (serviceInfo.getPort() <= 0) {
throw new IllegalArgumentException("Invalid port number");
}
checkServiceInfo(serviceInfo);
checkProtocol(protocolType);
- int key = putListener(listener, executor, serviceInfo);
+ 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, serviceInfo);
+ mService.registerService(key, advertisingRequest);
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
diff --git a/framework/aidl-export/android/net/nsd/AdvertisingRequest.aidl b/framework/aidl-export/android/net/nsd/AdvertisingRequest.aidl
new file mode 100644
index 0000000..2848074
--- /dev/null
+++ b/framework/aidl-export/android/net/nsd/AdvertisingRequest.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.nsd;
+
+@JavaOnlyStableParcelable parcelable AdvertisingRequest;
\ No newline at end of file
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index 6086207..1168320 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -26,6 +26,8 @@
import static android.net.nsd.NsdManager.MDNS_DISCOVERY_MANAGER_EVENT;
import static android.net.nsd.NsdManager.MDNS_SERVICE_EVENT;
import static android.net.nsd.NsdManager.RESOLVE_SERVICE_SUCCEEDED;
+import static android.net.nsd.NsdManager.TYPE_REGEX;
+import static android.net.nsd.NsdManager.TYPE_SUBTYPE_LABEL_REGEX;
import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
import static com.android.modules.utils.build.SdkLevel.isAtLeastU;
@@ -51,6 +53,7 @@
import android.net.mdns.aidl.IMDnsEventListener;
import android.net.mdns.aidl.RegistrationInfo;
import android.net.mdns.aidl.ResolutionInfo;
+import android.net.nsd.AdvertisingRequest;
import android.net.nsd.INsdManager;
import android.net.nsd.INsdManagerCallback;
import android.net.nsd.INsdServiceConnector;
@@ -173,7 +176,7 @@
"mdns_advertiser_allowlist_";
private static final String MDNS_ALLOWLIST_FLAG_SUFFIX = "_version";
- private static final String TYPE_SUBTYPE_LABEL_REGEX = "_[a-zA-Z0-9-_]{1,61}[a-zA-Z0-9]";
+
@VisibleForTesting
static final String MDNS_CONFIG_RUNNING_APP_ACTIVE_IMPORTANCE_CUTOFF =
@@ -728,12 +731,11 @@
final ClientInfo clientInfo;
final int transactionId;
final int clientRequestId = msg.arg2;
- final ListenerArgs args;
final OffloadEngineInfo offloadEngineInfo;
switch (msg.what) {
case NsdManager.DISCOVER_SERVICES: {
if (DBG) Log.d(TAG, "Discover services");
- args = (ListenerArgs) msg.obj;
+ final ListenerArgs args = (ListenerArgs) msg.obj;
clientInfo = mClients.get(args.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
@@ -809,7 +811,7 @@
}
case NsdManager.STOP_DISCOVERY: {
if (DBG) Log.d(TAG, "Stop service discovery");
- args = (ListenerArgs) msg.obj;
+ final ListenerArgs args = (ListenerArgs) msg.obj;
clientInfo = mClients.get(args.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
@@ -847,7 +849,7 @@
}
case NsdManager.REGISTER_SERVICE: {
if (DBG) Log.d(TAG, "Register service");
- args = (ListenerArgs) msg.obj;
+ final AdvertisingArgs args = (AdvertisingArgs) msg.obj;
clientInfo = mClients.get(args.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
@@ -862,9 +864,12 @@
NsdManager.FAILURE_MAX_LIMIT, true /* isLegacy */);
break;
}
-
- transactionId = getUniqueId();
- final NsdServiceInfo serviceInfo = args.serviceInfo;
+ final AdvertisingRequest advertisingRequest = args.advertisingRequest;
+ if (advertisingRequest == null) {
+ Log.e(TAG, "Unknown advertisingRequest in registration");
+ break;
+ }
+ final NsdServiceInfo serviceInfo = advertisingRequest.getServiceInfo();
final String serviceType = serviceInfo.getServiceType();
final Pair<String, List<String>> typeSubtype = parseTypeAndSubtype(
serviceType);
@@ -879,6 +884,23 @@
NsdManager.FAILURE_INTERNAL_ERROR, false /* isLegacy */);
break;
}
+ boolean isUpdateOnly = (advertisingRequest.getAdvertisingConfig()
+ & AdvertisingRequest.NSD_ADVERTISING_UPDATE_ONLY) > 0;
+ // If it is an update request, then reuse the old transactionId
+ if (isUpdateOnly) {
+ final ClientRequest existingClientRequest =
+ clientInfo.mClientRequests.get(clientRequestId);
+ if (existingClientRequest == null) {
+ Log.e(TAG, "Invalid update on requestId: " + clientRequestId);
+ clientInfo.onRegisterServiceFailedImmediately(clientRequestId,
+ NsdManager.FAILURE_INTERNAL_ERROR,
+ false /* isLegacy */);
+ break;
+ }
+ transactionId = existingClientRequest.mTransactionId;
+ } else {
+ transactionId = getUniqueId();
+ }
serviceInfo.setServiceType(registerServiceType);
serviceInfo.setServiceName(truncateServiceName(
serviceInfo.getServiceName()));
@@ -899,12 +921,16 @@
serviceInfo.setSubtypes(subtypes);
maybeStartMonitoringSockets();
+ final MdnsAdvertisingOptions mdnsAdvertisingOptions =
+ MdnsAdvertisingOptions.newBuilder().setIsOnlyUpdate(
+ isUpdateOnly).build();
mAdvertiser.addOrUpdateService(transactionId, serviceInfo,
- MdnsAdvertisingOptions.newBuilder().build());
+ mdnsAdvertisingOptions);
storeAdvertiserRequestMap(clientRequestId, transactionId, clientInfo,
serviceInfo.getNetwork());
} else {
maybeStartDaemon();
+ transactionId = getUniqueId();
if (registerService(transactionId, serviceInfo)) {
if (DBG) {
Log.d(TAG, "Register " + clientRequestId
@@ -924,7 +950,7 @@
}
case NsdManager.UNREGISTER_SERVICE: {
if (DBG) Log.d(TAG, "unregister service");
- args = (ListenerArgs) msg.obj;
+ final ListenerArgs args = (ListenerArgs) msg.obj;
clientInfo = mClients.get(args.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
@@ -967,7 +993,7 @@
}
case NsdManager.RESOLVE_SERVICE: {
if (DBG) Log.d(TAG, "Resolve service");
- args = (ListenerArgs) msg.obj;
+ final ListenerArgs args = (ListenerArgs) msg.obj;
clientInfo = mClients.get(args.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
@@ -1029,7 +1055,7 @@
}
case NsdManager.STOP_RESOLUTION: {
if (DBG) Log.d(TAG, "Stop service resolution");
- args = (ListenerArgs) msg.obj;
+ final ListenerArgs args = (ListenerArgs) msg.obj;
clientInfo = mClients.get(args.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
@@ -1068,7 +1094,7 @@
}
case NsdManager.REGISTER_SERVICE_CALLBACK: {
if (DBG) Log.d(TAG, "Register a service callback");
- args = (ListenerArgs) msg.obj;
+ final ListenerArgs args = (ListenerArgs) msg.obj;
clientInfo = mClients.get(args.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
@@ -1111,7 +1137,7 @@
}
case NsdManager.UNREGISTER_SERVICE_CALLBACK: {
if (DBG) Log.d(TAG, "Unregister a service callback");
- args = (ListenerArgs) msg.obj;
+ final ListenerArgs args = (ListenerArgs) msg.obj;
clientInfo = mClients.get(args.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
@@ -1638,20 +1664,7 @@
@Nullable
public static Pair<String, List<String>> parseTypeAndSubtype(String serviceType) {
if (TextUtils.isEmpty(serviceType)) return null;
-
- final String regexString =
- // Optional leading subtype (_subtype._type._tcp)
- // (?: xxx) is a non-capturing parenthesis, don't capture the dot
- "^(?:(" + TYPE_SUBTYPE_LABEL_REGEX + ")\\.)?"
- // Actual type (_type._tcp.local)
- + "(" + TYPE_SUBTYPE_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
- + "((?:," + TYPE_SUBTYPE_LABEL_REGEX + ")*)"
- + "$";
- final Pattern serviceTypePattern = Pattern.compile(regexString);
+ final Pattern serviceTypePattern = Pattern.compile(TYPE_REGEX);
final Matcher matcher = serviceTypePattern.matcher(serviceType);
if (!matcher.matches()) return null;
final String queryType = matcher.group(2);
@@ -2077,20 +2090,33 @@
}
}
+ private static class AdvertisingArgs {
+ public final NsdServiceConnector connector;
+ public final AdvertisingRequest advertisingRequest;
+
+ AdvertisingArgs(NsdServiceConnector connector, AdvertisingRequest advertisingRequest) {
+ this.connector = connector;
+ this.advertisingRequest = advertisingRequest;
+ }
+ }
+
private class NsdServiceConnector extends INsdServiceConnector.Stub
implements IBinder.DeathRecipient {
+
@Override
- public void registerService(int listenerKey, NsdServiceInfo serviceInfo) {
+ public void registerService(int listenerKey, AdvertisingRequest advertisingRequest)
+ throws RemoteException {
mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
NsdManager.REGISTER_SERVICE, 0, listenerKey,
- new ListenerArgs(this, serviceInfo)));
+ new AdvertisingArgs(this, advertisingRequest)
+ ));
}
@Override
public void unregisterService(int listenerKey) {
mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
NsdManager.UNREGISTER_SERVICE, 0, listenerKey,
- new ListenerArgs(this, null)));
+ new ListenerArgs(this, (NsdServiceInfo) null)));
}
@Override
@@ -2102,8 +2128,8 @@
@Override
public void stopDiscovery(int listenerKey) {
- mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
- NsdManager.STOP_DISCOVERY, 0, listenerKey, new ListenerArgs(this, null)));
+ mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(NsdManager.STOP_DISCOVERY,
+ 0, listenerKey, new ListenerArgs(this, (NsdServiceInfo) null)));
}
@Override
@@ -2115,8 +2141,8 @@
@Override
public void stopResolution(int listenerKey) {
- mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
- NsdManager.STOP_RESOLUTION, 0, listenerKey, new ListenerArgs(this, null)));
+ mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(NsdManager.STOP_RESOLUTION,
+ 0, listenerKey, new ListenerArgs(this, (NsdServiceInfo) null)));
}
@Override
@@ -2130,13 +2156,13 @@
public void unregisterServiceInfoCallback(int listenerKey) {
mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
NsdManager.UNREGISTER_SERVICE_CALLBACK, 0, listenerKey,
- new ListenerArgs(this, null)));
+ new ListenerArgs(this, (NsdServiceInfo) null)));
}
@Override
public void startDaemon() {
- mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
- NsdManager.DAEMON_STARTUP, new ListenerArgs(this, null)));
+ mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(NsdManager.DAEMON_STARTUP,
+ new ListenerArgs(this, (NsdServiceInfo) null)));
}
@Override
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
index 32f604e..df0a040 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -541,6 +541,9 @@
}
if (response.isComplete()) {
+ // There is a bug here: the newServiceFound is global right now. The state needs
+ // to be per listener because of the responseMatchesOptions() filter.
+ // Otherwise, it won't handle the subType update properly.
if (newServiceFound || serviceBecomesComplete) {
sharedLog.log("onServiceFound: " + serviceInfo);
listener.onServiceFound(serviceInfo, false /* isServiceFromCache */);
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index e0387c9..a040201 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -1064,6 +1064,32 @@
}
@Test
+ fun testMultipleSubTypeAdvertisingAndDiscovery_withUpdate() {
+ val si1 = makeTestServiceInfo(network = testNetwork1.network).apply {
+ serviceType += ",_subtype1"
+ }
+ val si2 = makeTestServiceInfo(network = testNetwork1.network).apply {
+ serviceType += ",_subtype2"
+ }
+ val registrationRecord = NsdRegistrationRecord()
+ val subtype3DiscoveryRecord = NsdDiscoveryRecord()
+ tryTest {
+ registerService(registrationRecord, si1)
+ updateService(registrationRecord, si2)
+ nsdManager.discoverServices("_subtype2.$serviceType",
+ NsdManager.PROTOCOL_DNS_SD, testNetwork1.network,
+ { it.run() }, subtype3DiscoveryRecord)
+ subtype3DiscoveryRecord.waitForServiceDiscovered(serviceName,
+ serviceType, testNetwork1.network)
+ } cleanupStep {
+ nsdManager.stopServiceDiscovery(subtype3DiscoveryRecord)
+ subtype3DiscoveryRecord.expectCallback<DiscoveryStopped>()
+ } cleanup {
+ nsdManager.unregisterService(registrationRecord)
+ }
+ }
+
+ @Test
fun testSubtypeAdvertising_tooManySubtypes_returnsFailureBadParameters() {
val si = makeTestServiceInfo(network = testNetwork1.network)
// Sets 101 subtypes in total
@@ -1404,6 +1430,18 @@
return cb.serviceInfo
}
+ /**
+ * Update a service.
+ */
+ private fun updateService(
+ record: NsdRegistrationRecord,
+ si: NsdServiceInfo,
+ executor: Executor = Executor { it.run() }
+ ) {
+ nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, executor, record)
+ // TODO: add the callback check for the update.
+ }
+
private fun resolveService(discoveredInfo: NsdServiceInfo): NsdServiceInfo {
val record = NsdResolveRecord()
nsdManager.resolveService(discoveredInfo, Executor { it.run() }, record)
diff --git a/tests/unit/java/android/net/nsd/NsdManagerTest.java b/tests/unit/java/android/net/nsd/NsdManagerTest.java
index 550a9ee..1cfdefe 100644
--- a/tests/unit/java/android/net/nsd/NsdManagerTest.java
+++ b/tests/unit/java/android/net/nsd/NsdManagerTest.java
@@ -196,6 +196,22 @@
verify(listener2, timeout(mTimeoutMs).times(1)).onServiceResolved(reply);
}
+ @Test
+ public void testRegisterServiceWithAdvertisingRequest() throws Exception {
+ final NsdManager manager = mManager;
+ final NsdServiceInfo request = new NsdServiceInfo("another_name2", "another_type2");
+ request.setPort(2203);
+ final AdvertisingRequest advertisingRequest = new AdvertisingRequest.Builder(request,
+ PROTOCOL).build();
+ final NsdManager.RegistrationListener listener = mock(
+ NsdManager.RegistrationListener.class);
+
+ manager.registerService(advertisingRequest, Runnable::run, listener);
+ int key4 = getRequestKey(req -> verify(mServiceConn).registerService(req.capture(), any()));
+ mCallback.onRegisterServiceSucceeded(key4, request);
+ verify(listener, timeout(mTimeoutMs).times(1)).onServiceRegistered(request);
+ }
+
private void doTestRegisterService() throws Exception {
NsdManager manager = mManager;
@@ -346,8 +362,19 @@
NsdManager.ResolveListener listener3 = mock(NsdManager.ResolveListener.class);
NsdServiceInfo invalidService = new NsdServiceInfo(null, null);
- NsdServiceInfo validService = new NsdServiceInfo("a_name", "a_type");
+ NsdServiceInfo validService = new NsdServiceInfo("a_name", "_a_type._tcp");
+ NsdServiceInfo otherServiceWithSubtype = new NsdServiceInfo("b_name", "_a_type._tcp,_sub1");
+ NsdServiceInfo validServiceDuplicate = new NsdServiceInfo("a_name", "_a_type._tcp");
+ NsdServiceInfo validServiceSubtypeUpdate = new NsdServiceInfo("a_name",
+ "_a_type._tcp,_sub1,_s2");
+ NsdServiceInfo otherSubtypeUpdate = new NsdServiceInfo("a_name", "_a_type._tcp,_sub1,_s3");
+ NsdServiceInfo dotSyntaxSubtypeUpdate = new NsdServiceInfo("a_name", "_sub1._a_type._tcp");
validService.setPort(2222);
+ otherServiceWithSubtype.setPort(2222);
+ validServiceDuplicate.setPort(2222);
+ validServiceSubtypeUpdate.setPort(2222);
+ otherSubtypeUpdate.setPort(2222);
+ dotSyntaxSubtypeUpdate.setPort(2222);
// Service registration
// - invalid arguments
@@ -358,7 +385,21 @@
mustFail(() -> { manager.registerService(validService, -1, listener1); });
mustFail(() -> { manager.registerService(validService, PROTOCOL, null); });
manager.registerService(validService, PROTOCOL, listener1);
- // - listener already registered
+ // - update without subtype is not allowed
+ mustFail(() -> { manager.registerService(validServiceDuplicate, PROTOCOL, listener1); });
+ // - update with subtype is allowed
+ manager.registerService(validServiceSubtypeUpdate, PROTOCOL, listener1);
+ // - re-updating to the same subtype is allowed
+ manager.registerService(validServiceSubtypeUpdate, PROTOCOL, listener1);
+ // - updating to other subtypes is allowed
+ manager.registerService(otherSubtypeUpdate, PROTOCOL, listener1);
+ // - update back to the service without subtype is allowed
+ manager.registerService(validService, PROTOCOL, listener1);
+ // - updating to a subtype with _sub._type syntax is not allowed
+ mustFail(() -> { manager.registerService(dotSyntaxSubtypeUpdate, PROTOCOL, listener1); });
+ // - updating to a different service name is not allowed
+ mustFail(() -> { manager.registerService(otherServiceWithSubtype, PROTOCOL, listener1); });
+ // - listener already registered, and not using subtypes
mustFail(() -> { manager.registerService(validService, PROTOCOL, listener1); });
manager.unregisterService(listener1);
// TODO: make listener immediately reusable