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 0000000000000000000000000000000000000000..b1ef98f73f7b390884ce56c502cc7fe4a1b6e63c --- /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 e671db15e84af343862640920b40fdea32706459..b03eb297a27217f6818caaa7b195f3bf89d32212 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 @@ import android.os.Messenger; * {@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 fcf79eb2b68187718beed0e62d4429c5963cd802..053ecf2637dd00c53953e610e4050043df339790 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.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; @@ -57,6 +58,8 @@ 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 @@ -152,8 +155,37 @@ public final class NsdManager { "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 @@ -1098,6 +1130,16 @@ public final class NsdManager { 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 final class NsdManager { */ 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 0000000000000000000000000000000000000000..2848074634538ea6fc3c355e33b6908122b7cc69 --- /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 6086207f627baf99d99fc2b5f2aa49e459e371b0..11683200124d212e3ecee9ff6c3d40c7481d35d6 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.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK; 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.GetAddressInfo; 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 @@ public class NsdService extends INsdManager.Stub { "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 @@ public class NsdService extends INsdManager.Stub { 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 @@ public class NsdService extends INsdManager.Stub { } 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 @@ public class NsdService extends INsdManager.Stub { } 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 @@ public class NsdService extends INsdManager.Stub { 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 @@ public class NsdService extends INsdManager.Stub { 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 @@ public class NsdService extends INsdManager.Stub { 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 @@ public class NsdService extends INsdManager.Stub { } 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 @@ public class NsdService extends INsdManager.Stub { } 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 @@ public class NsdService extends INsdManager.Stub { } 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 @@ public class NsdService extends INsdManager.Stub { } 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 @@ public class NsdService extends INsdManager.Stub { } 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 @@ public class NsdService extends INsdManager.Stub { @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 @@ public class NsdService extends INsdManager.Stub { } } + 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 @@ public class NsdService extends INsdManager.Stub { @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 @@ public class NsdService extends INsdManager.Stub { @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 class NsdService extends INsdManager.Stub { 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 32f604e8d5d3f010662e020a62e7b24c727f2dd9..df0a04042e1a7e47d38cbf68f22acc011196d747 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 @@ public class MdnsServiceTypeClient { } 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 e0387c96166708b2ab8515664fd7c789cba8f8ea..a04020149bb49bb528bc156e0a7ed1f1af0fe069 100644 --- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt +++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt @@ -1063,6 +1063,32 @@ class NsdManagerTest { } } + @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) @@ -1404,6 +1430,18 @@ class NsdManagerTest { 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 550a9ee26be5438ed28d4019487713614941a599..1cfdefe559a30c82dd00d19f66808d98d5391eb7 100644 --- a/tests/unit/java/android/net/nsd/NsdManagerTest.java +++ b/tests/unit/java/android/net/nsd/NsdManagerTest.java @@ -196,6 +196,22 @@ public class NsdManagerTest { 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 @@ public class NsdManagerTest { 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 @@ public class NsdManagerTest { 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