Merge changes from topic "mdns-subtypes" into main

* changes:
  [mdns] add service-side impl for subtypes advertising
  [mdns] add support for advertising subtypes
This commit is contained in:
Kangping Dong 2023-12-18 08:49:44 +00:00 • committed by Gerrit Code Review
commit 19d055514c
14 changed files with 426 additions and 187 deletions

View file

@ -275,6 +275,7 @@ package android.net.nsd {
method public int getPort();
method public String getServiceName();
method public String getServiceType();
method @FlaggedApi("com.android.net.flags.nsd_subtypes_support_enabled") @NonNull public java.util.Set<java.lang.String> getSubtypes();
method public void removeAttribute(String);
method public void setAttribute(String, String);
method @Deprecated public void setHost(java.net.InetAddress);
@ -283,6 +284,7 @@ package android.net.nsd {
method public void setPort(int);
method public void setServiceName(String);
method public void setServiceType(String);
method @FlaggedApi("com.android.net.flags.nsd_subtypes_support_enabled") public void setSubtypes(@NonNull java.util.Set<java.lang.String>);
method public void writeToParcel(android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.net.nsd.NsdServiceInfo> CREATOR;
}

View file

@ -150,6 +150,8 @@ public final class NsdManager {
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";
}
/**

View file

@ -16,6 +16,9 @@
package android.net.nsd;
import static java.nio.charset.StandardCharsets.UTF_8;
import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
@ -24,6 +27,7 @@ import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import com.android.net.module.util.InetAddressUtils;
@ -35,6 +39,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* A class representing service information for network service discovery
@ -48,9 +53,11 @@ public final class NsdServiceInfo implements Parcelable {
private String mServiceType;
private final ArrayMap<String, byte[]> mTxtRecord = new ArrayMap<>();
private final Set<String> mSubtypes;
private final List<InetAddress> mHostAddresses = new ArrayList<>();
private final ArrayMap<String, byte[]> mTxtRecord;
private final List<InetAddress> mHostAddresses;
private int mPort;
@ -60,14 +67,34 @@ public final class NsdServiceInfo implements Parcelable {
private int mInterfaceIndex;
public NsdServiceInfo() {
mSubtypes = new ArraySet<>();
mTxtRecord = new ArrayMap<>();
mHostAddresses = new ArrayList<>();
}
/** @hide */
public NsdServiceInfo(String sn, String rt) {
this();
mServiceName = sn;
mServiceType = rt;
}
/**
* Creates a copy of {@code other}.
*
* @hide
*/
public NsdServiceInfo(@NonNull NsdServiceInfo other) {
mServiceName = other.getServiceName();
mServiceType = other.getServiceType();
mSubtypes = new ArraySet<>(other.getSubtypes());
mTxtRecord = new ArrayMap<>(other.mTxtRecord);
mHostAddresses = new ArrayList<>(other.getHostAddresses());
mPort = other.getPort();
mNetwork = other.getNetwork();
mInterfaceIndex = other.getInterfaceIndex();
}
/** Get the service name */
public String getServiceName() {
return mServiceName;
@ -391,11 +418,41 @@ public final class NsdServiceInfo implements Parcelable {
mInterfaceIndex = interfaceIndex;
}
/**
* Sets the subtypes to be advertised for this service instance.
*
* The elements in {@code subtypes} should be the subtype identifiers which have the trailing
* "._sub" removed. For example, the subtype should be "_printer" for
* "_printer._sub._http._tcp.local".
*
* Only one subtype will be registered if multiple elements of {@code subtypes} have the same
* case-insensitive value.
*/
@FlaggedApi(NsdManager.Flags.NSD_SUBTYPES_SUPPORT_ENABLED)
public void setSubtypes(@NonNull Set<String> subtypes) {
mSubtypes.clear();
mSubtypes.addAll(subtypes);
}
/**
* Returns subtypes of this service instance.
*
* When this object is returned by the service discovery/browse APIs (etc. {@link
* NsdManager.DiscoveryListener}), the return value may or may not include the subtypes of this
* service.
*/
@FlaggedApi(NsdManager.Flags.NSD_SUBTYPES_SUPPORT_ENABLED)
@NonNull
public Set<String> getSubtypes() {
return Collections.unmodifiableSet(mSubtypes);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("name: ").append(mServiceName)
.append(", type: ").append(mServiceType)
.append(", subtypes: ").append(TextUtils.join(", ", mSubtypes))
.append(", hostAddresses: ").append(TextUtils.join(", ", mHostAddresses))
.append(", port: ").append(mPort)
.append(", network: ").append(mNetwork);
@ -414,6 +471,7 @@ public final class NsdServiceInfo implements Parcelable {
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(mServiceName);
dest.writeString(mServiceType);
dest.writeStringList(new ArrayList<>(mSubtypes));
dest.writeInt(mPort);
// TXT record key/value pairs.
@ -445,6 +503,7 @@ public final class NsdServiceInfo implements Parcelable {
NsdServiceInfo info = new NsdServiceInfo();
info.mServiceName = in.readString();
info.mServiceType = in.readString();
info.setSubtypes(new ArraySet<>(in.createStringArrayList()));
info.mPort = in.readInt();
// TXT record key/value pairs.

View file

@ -111,7 +111,9 @@ import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@ -170,6 +172,8 @@ 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 =
"mdns_config_running_app_active_importance_cutoff";
@ -186,6 +190,7 @@ public class NsdService extends INsdManager.Stub {
static final int NO_TRANSACTION = -1;
private static final int NO_SENT_QUERY_COUNT = 0;
private static final int DISCOVERY_QUERY_SENT_CALLBACK = 1000;
private static final int MAX_SUBTYPE_COUNT = 100;
private static final SharedLog LOGGER = new SharedLog("serviceDiscovery");
private final Context mContext;
@ -688,6 +693,35 @@ public class NsdService extends INsdManager.Stub {
return mClients.get(args.connector);
}
/**
* Returns {@code false} if {@code subtypes} exceeds the maximum number limit or
* contains invalid subtype label.
*/
private boolean checkSubtypeLabels(Set<String> subtypes) {
if (subtypes.size() > MAX_SUBTYPE_COUNT) {
mServiceLogs.e(
"Too many subtypes: " + subtypes.size() + " (max = "
+ MAX_SUBTYPE_COUNT + ")");
return false;
}
for (String subtype : subtypes) {
if (!checkSubtypeLabel(subtype)) {
mServiceLogs.e("Subtype " + subtype + " is invalid");
return false;
}
}
return true;
}
private Set<String> dedupSubtypeLabels(Collection<String> subtypes) {
final Map<String, String> subtypeMap = new LinkedHashMap<>(subtypes.size());
for (String subtype : subtypes) {
subtypeMap.put(MdnsUtils.toDnsLowerCase(subtype), subtype);
}
return new ArraySet<>(subtypeMap.values());
}
@Override
public boolean processMessage(Message msg) {
final ClientInfo clientInfo;
@ -846,13 +880,23 @@ public class NsdService extends INsdManager.Stub {
serviceInfo.setServiceName(truncateServiceName(
serviceInfo.getServiceName()));
Set<String> subtypes = new ArraySet<>(serviceInfo.getSubtypes());
if (!TextUtils.isEmpty(typeSubtype.second)) {
subtypes.add(typeSubtype.second);
}
subtypes = dedupSubtypeLabels(subtypes);
if (!checkSubtypeLabels(subtypes)) {
clientInfo.onRegisterServiceFailedImmediately(clientRequestId,
NsdManager.FAILURE_BAD_PARAMETERS, false /* isLegacy */);
break;
}
serviceInfo.setSubtypes(subtypes);
maybeStartMonitoringSockets();
// TODO: pass in the subtype as well. Including the subtype in the
// service type would generate service instance names like
// Name._subtype._sub._type._tcp, which is incorrect
// (it should be Name._type._tcp).
mAdvertiser.addOrUpdateService(transactionId, serviceInfo,
typeSubtype.second,
MdnsAdvertisingOptions.newBuilder().build());
storeAdvertiserRequestMap(clientRequestId, transactionId, clientInfo,
serviceInfo.getNetwork());
@ -1388,6 +1432,7 @@ public class NsdService extends INsdManager.Stub {
servInfo,
network == null ? INetd.LOCAL_NET_ID : network.netId,
serviceInfo.getInterfaceIndex());
servInfo.setSubtypes(dedupSubtypeLabels(serviceInfo.getSubtypes()));
return servInfo;
}
@ -1591,18 +1636,17 @@ public class NsdService extends INsdManager.Stub {
public static Pair<String, String> parseTypeAndSubtype(String serviceType) {
if (TextUtils.isEmpty(serviceType)) return null;
final String typeOrSubtypePattern = "_[a-zA-Z0-9-_]{1,61}[a-zA-Z0-9]";
final Pattern serviceTypePattern = Pattern.compile(
// Optional leading subtype (_subtype._type._tcp)
// (?: xxx) is a non-capturing parenthesis, don't capture the dot
"^(?:(" + typeOrSubtypePattern + ")\\.)?"
"^(?:(" + TYPE_SUBTYPE_LABEL_REGEX + ")\\.)?"
// Actual type (_type._tcp.local)
+ "(" + typeOrSubtypePattern + "\\._(?:tcp|udp))"
+ "(" + 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,_subtype" format
+ "(?:,(" + typeOrSubtypePattern + "))?"
+ "(?:,(" + TYPE_SUBTYPE_LABEL_REGEX + "))?"
+ "$");
final Matcher matcher = serviceTypePattern.matcher(serviceType);
if (!matcher.matches()) return null;
@ -1611,6 +1655,11 @@ public class NsdService extends INsdManager.Stub {
return new Pair<>(matcher.group(2), subtype);
}
/** Returns {@code true} if {@code subtype} is a valid DNS-SD subtype label. */
private static boolean checkSubtypeLabel(String subtype) {
return Pattern.compile("^" + TYPE_SUBTYPE_LABEL_REGEX + "$").matcher(subtype).matches();
}
@VisibleForTesting
NsdService(Context ctx, Handler handler, long cleanupDelayMs) {
this(ctx, handler, cleanupDelayMs, new Dependencies());

View file

@ -44,6 +44,7 @@ import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
@ -351,8 +352,7 @@ public class MdnsAdvertiser {
mPendingRegistrations.put(id, registration);
for (int i = 0; i < mAdvertisers.size(); i++) {
try {
mAdvertisers.valueAt(i).addService(id, registration.getServiceInfo(),
registration.getSubtype());
mAdvertisers.valueAt(i).addService(id, registration.getServiceInfo());
} catch (NameConflictException e) {
mSharedLog.wtf("Name conflict adding services that should have unique names",
e);
@ -367,7 +367,8 @@ public class MdnsAdvertiser {
void updateService(int id, @NonNull Registration registration) {
mPendingRegistrations.put(id, registration);
for (int i = 0; i < mAdvertisers.size(); i++) {
mAdvertisers.valueAt(i).updateService(id, registration.getSubtype());
mAdvertisers.valueAt(i).updateService(
id, registration.getServiceInfo().getSubtypes());
}
}
@ -417,7 +418,7 @@ public class MdnsAdvertiser {
final Registration registration = mPendingRegistrations.valueAt(i);
try {
advertiser.addService(mPendingRegistrations.keyAt(i),
registration.getServiceInfo(), registration.getSubtype());
registration.getServiceInfo());
} catch (NameConflictException e) {
mSharedLog.wtf("Name conflict adding services that should have unique names",
e);
@ -485,16 +486,12 @@ public class MdnsAdvertiser {
private int mConflictCount;
@NonNull
private NsdServiceInfo mServiceInfo;
@Nullable
private String mSubtype;
int mConflictDuringProbingCount;
int mConflictAfterProbingCount;
private Registration(@NonNull NsdServiceInfo serviceInfo, @Nullable String subtype) {
private Registration(@NonNull NsdServiceInfo serviceInfo) {
this.mOriginalName = serviceInfo.getServiceName();
this.mServiceInfo = serviceInfo;
this.mSubtype = subtype;
}
/**
@ -507,10 +504,11 @@ public class MdnsAdvertiser {
}
/**
* Update subType for the registration.
* Update subTypes for the registration.
*/
public void updateSubtype(@Nullable String subtype) {
this.mSubtype = subtype;
public void updateSubtypes(@NonNull Set<String> subtypes) {
mServiceInfo = new NsdServiceInfo(mServiceInfo);
mServiceInfo.setSubtypes(subtypes);
}
/**
@ -540,17 +538,8 @@ public class MdnsAdvertiser {
// In case of conflict choose a different service name. After the first conflict use
// "Name (2)", then "Name (3)" etc.
// TODO: use a hidden method in NsdServiceInfo once MdnsAdvertiser is moved to service-t
final NsdServiceInfo newInfo = new NsdServiceInfo();
final NsdServiceInfo newInfo = new NsdServiceInfo(mServiceInfo);
newInfo.setServiceName(getUpdatedServiceName(renameCount));
newInfo.setServiceType(mServiceInfo.getServiceType());
for (Map.Entry<String, byte[]> attr : mServiceInfo.getAttributes().entrySet()) {
newInfo.setAttribute(attr.getKey(),
attr.getValue() == null ? null : new String(attr.getValue()));
}
newInfo.setHost(mServiceInfo.getHost());
newInfo.setPort(mServiceInfo.getPort());
newInfo.setNetwork(mServiceInfo.getNetwork());
// interfaceIndex is not set when registering
return newInfo;
}
@ -565,11 +554,6 @@ public class MdnsAdvertiser {
public NsdServiceInfo getServiceInfo() {
return mServiceInfo;
}
@Nullable
public String getSubtype() {
return mSubtype;
}
}
/**
@ -665,14 +649,14 @@ public class MdnsAdvertiser {
*
* @param id A unique ID for the service.
* @param service The service info to advertise.
* @param subtype An optional subtype to advertise the service with.
* @param advertisingOptions The advertising options.
*/
public void addOrUpdateService(int id, NsdServiceInfo service, @Nullable String subtype,
public void addOrUpdateService(int id, NsdServiceInfo service,
MdnsAdvertisingOptions advertisingOptions) {
checkThread();
final Registration existingRegistration = mRegistrations.get(id);
final Network network = service.getNetwork();
final Set<String> subtypes = service.getSubtypes();
Registration registration;
if (advertisingOptions.isOnlyUpdate()) {
if (existingRegistration == null) {
@ -687,10 +671,10 @@ public class MdnsAdvertiser {
return;
}
mSharedLog.i("Update service " + service + " with ID " + id + " and subtype " + subtype
+ " advertisingOptions " + advertisingOptions);
mSharedLog.i("Update service " + service + " with ID " + id + " and subtypes "
+ subtypes + " advertisingOptions " + advertisingOptions);
registration = existingRegistration;
registration.updateSubtype(subtype);
registration.updateSubtypes(subtypes);
} else {
if (existingRegistration != null) {
mSharedLog.e("Adding duplicate registration for " + service);
@ -698,9 +682,9 @@ public class MdnsAdvertiser {
mCb.onRegisterServiceFailed(id, NsdManager.FAILURE_INTERNAL_ERROR);
return;
}
mSharedLog.i("Adding service " + service + " with ID " + id + " and subtype " + subtype
+ " advertisingOptions " + advertisingOptions);
registration = new Registration(service, subtype);
mSharedLog.i("Adding service " + service + " with ID " + id + " and subtypes "
+ subtypes + " advertisingOptions " + advertisingOptions);
registration = new Registration(service);
final BiPredicate<Network, InterfaceAdvertiserRequest> checkConflictFilter;
if (network == null) {
// If registering on all networks, no advertiser must have conflicts
@ -793,15 +777,10 @@ public class MdnsAdvertiser {
private OffloadServiceInfoWrapper createOffloadService(int serviceId,
@NonNull Registration registration, byte[] rawOffloadPacket) {
final NsdServiceInfo nsdServiceInfo = registration.getServiceInfo();
final List<String> subTypes = new ArrayList<>();
String subType = registration.getSubtype();
if (subType != null) {
subTypes.add(subType);
}
final OffloadServiceInfo offloadServiceInfo = new OffloadServiceInfo(
new OffloadServiceInfo.Key(nsdServiceInfo.getServiceName(),
nsdServiceInfo.getServiceType()),
subTypes,
new ArrayList<>(nsdServiceInfo.getSubtypes()),
String.join(".", mDeviceHostName),
rawOffloadPacket,
// TODO: define overlayable resources in

View file

@ -37,6 +37,7 @@ import com.android.server.connectivity.mdns.util.MdnsUtils;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.List;
import java.util.Set;
/**
* A class that handles advertising services on a {@link MdnsInterfaceSocket} tied to an interface.
@ -232,12 +233,12 @@ public class MdnsInterfaceAdvertiser implements MulticastPacketReader.PacketHand
* Update an already registered service without sending exit/re-announcement packet.
*
* @param id An exiting service id
* @param subtype A new subtype
* @param subtypes New subtypes
*/
public void updateService(int id, @Nullable String subtype) {
public void updateService(int id, @NonNull Set<String> subtypes) {
// The current implementation is intended to be used in cases where subtypes don't get
// announced.
mRecordRepository.updateService(id, subtype);
mRecordRepository.updateService(id, subtypes);
}
/**
@ -245,9 +246,8 @@ public class MdnsInterfaceAdvertiser implements MulticastPacketReader.PacketHand
*
* @throws NameConflictException There is already a service being advertised with that name.
*/
public void addService(int id, NsdServiceInfo service, @Nullable String subtype)
throws NameConflictException {
final int replacedExitingService = mRecordRepository.addService(id, service, subtype);
public void addService(int id, NsdServiceInfo service) throws NameConflictException {
final int replacedExitingService = mRecordRepository.addService(id, service);
// Cancel announcements for the existing service. This only happens for exiting services
// (so cancelling exiting announcements), as per RecordRepository.addService.
if (replacedExitingService >= 0) {

View file

@ -74,8 +74,6 @@ public class MdnsRecordRepository {
// Top-level domain for link-local queries, as per RFC6762 3.
private static final String LOCAL_TLD = "local";
// Subtype separator as per RFC6763 7.1 (_printer._sub._http._tcp.local)
private static final String SUBTYPE_SEPARATOR = "_sub";
// Service type for service enumeration (RFC6763 9.)
private static final String[] DNS_SD_SERVICE_TYPE =
@ -161,8 +159,6 @@ public class MdnsRecordRepository {
public final RecordInfo<MdnsTextRecord> txtRecord;
@NonNull
public final NsdServiceInfo serviceInfo;
@Nullable
public final String subtype;
/**
* Whether the service is sending exit announcements and will be destroyed soon.
@ -185,28 +181,28 @@ public class MdnsRecordRepository {
private boolean isProbing;
/**
* Create a ServiceRegistration with only update the subType
* Create a ServiceRegistration with only update the subType.
*/
ServiceRegistration withSubtype(String newSubType) {
return new ServiceRegistration(srvRecord.record.getServiceHost(), serviceInfo,
newSubType, repliedServiceCount, sentPacketCount, exiting, isProbing);
ServiceRegistration withSubtypes(@NonNull Set<String> newSubtypes) {
NsdServiceInfo newServiceInfo = new NsdServiceInfo(serviceInfo);
newServiceInfo.setSubtypes(newSubtypes);
return new ServiceRegistration(srvRecord.record.getServiceHost(), newServiceInfo,
repliedServiceCount, sentPacketCount, exiting, isProbing);
}
/**
* Create a ServiceRegistration for dns-sd service registration (RFC6763).
*/
ServiceRegistration(@NonNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo,
@Nullable String subtype, int repliedServiceCount, int sentPacketCount,
boolean exiting, boolean isProbing) {
int repliedServiceCount, int sentPacketCount, boolean exiting, boolean isProbing) {
this.serviceInfo = serviceInfo;
this.subtype = subtype;
final String[] serviceType = splitServiceType(serviceInfo);
final String[] serviceName = splitFullyQualifiedName(serviceInfo, serviceType);
// Service PTR record
final RecordInfo<MdnsPointerRecord> ptrRecord = new RecordInfo<>(
// Service PTR records
ptrRecords = new ArrayList<>(serviceInfo.getSubtypes().size() + 1);
ptrRecords.add(new RecordInfo<>(
serviceInfo,
new MdnsPointerRecord(
serviceType,
@ -214,26 +210,17 @@ public class MdnsRecordRepository {
false /* cacheFlush */,
NON_NAME_RECORDS_TTL_MILLIS,
serviceName),
true /* sharedName */);
if (subtype == null) {
this.ptrRecords = Collections.singletonList(ptrRecord);
} else {
final String[] subtypeName = new String[serviceType.length + 2];
System.arraycopy(serviceType, 0, subtypeName, 2, serviceType.length);
subtypeName[0] = subtype;
subtypeName[1] = SUBTYPE_SEPARATOR;
final RecordInfo<MdnsPointerRecord> subtypeRecord = new RecordInfo<>(
serviceInfo,
new MdnsPointerRecord(
subtypeName,
0L /* receiptTimeMillis */,
false /* cacheFlush */,
NON_NAME_RECORDS_TTL_MILLIS,
serviceName),
true /* sharedName */);
this.ptrRecords = List.of(ptrRecord, subtypeRecord);
true /* sharedName */));
for (String subtype : serviceInfo.getSubtypes()) {
ptrRecords.add(new RecordInfo<>(
serviceInfo,
new MdnsPointerRecord(
MdnsUtils.constructFullSubtype(serviceType, subtype),
0L /* receiptTimeMillis */,
false /* cacheFlush */,
NON_NAME_RECORDS_TTL_MILLIS,
serviceName),
true /* sharedName */));
}
srvRecord = new RecordInfo<>(
@ -284,8 +271,8 @@ public class MdnsRecordRepository {
* @param serviceInfo Service to advertise
*/
ServiceRegistration(@NonNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo,
@Nullable String subtype, int repliedServiceCount, int sentPacketCount) {
this(deviceHostname, serviceInfo, subtype, repliedServiceCount, sentPacketCount,
int repliedServiceCount, int sentPacketCount) {
this(deviceHostname, serviceInfo,repliedServiceCount, sentPacketCount,
false /* exiting */, true /* isProbing */);
}
@ -328,17 +315,17 @@ public class MdnsRecordRepository {
* Update a service that already registered in the repository.
*
* @param serviceId An existing service ID.
* @param subtype A new subtype
* @param subtypes New subtypes
* @return
*/
public void updateService(int serviceId, @Nullable String subtype) {
public void updateService(int serviceId, @NonNull Set<String> subtypes) {
final ServiceRegistration existingRegistration = mServices.get(serviceId);
if (existingRegistration == null) {
throw new IllegalArgumentException(
"Service ID must already exist for an update request: " + serviceId);
}
final ServiceRegistration updatedRegistration = existingRegistration.withSubtype(
subtype);
final ServiceRegistration updatedRegistration = existingRegistration.withSubtypes(
subtypes);
mServices.put(serviceId, updatedRegistration);
}
@ -352,8 +339,7 @@ public class MdnsRecordRepository {
* ID of the replaced service.
* @throws NameConflictException There is already a (non-exiting) service using the name.
*/
public int addService(int serviceId, NsdServiceInfo serviceInfo, @Nullable String subtype)
throws NameConflictException {
public int addService(int serviceId, NsdServiceInfo serviceInfo) throws NameConflictException {
if (mServices.contains(serviceId)) {
throw new IllegalArgumentException(
"Service ID must not be reused across registrations: " + serviceId);
@ -366,7 +352,7 @@ public class MdnsRecordRepository {
}
final ServiceRegistration registration = new ServiceRegistration(
mDeviceHostname, serviceInfo, subtype, NO_PACKET /* repliedServiceCount */,
mDeviceHostname, serviceInfo, NO_PACKET /* repliedServiceCount */,
NO_PACKET /* sentPacketCount */);
mServices.put(serviceId, registration);
@ -929,7 +915,7 @@ public class MdnsRecordRepository {
if (existing == null) return null;
final ServiceRegistration newService = new ServiceRegistration(mDeviceHostname, newInfo,
existing.subtype, existing.repliedServiceCount, existing.sentPacketCount);
existing.repliedServiceCount, existing.sentPacketCount);
mServices.put(serviceId, newService);
return makeProbingInfo(
serviceId, newService.srvRecord.record, makeProbingInetAddressRecords());

View file

@ -233,6 +233,20 @@ public class MdnsUtils {
&& mdnsRecord.getRemainingTTL(now) <= mdnsRecord.getTtl() / 2;
}
/**
* Creates a new full subtype name with given service type and subtype labels.
*
* For example, given ["_http", "_tcp"] and "_printer", this method returns a new String array
* of ["_printer", "_sub", "_http", "_tcp"].
*/
public static String[] constructFullSubtype(String[] serviceType, String subtype) {
String[] fullSubtype = new String[serviceType.length + 2];
fullSubtype[0] = subtype;
fullSubtype[1] = MdnsConstants.SUBTYPE_LABEL;
System.arraycopy(serviceType, 0, fullSubtype, 2, serviceType.length);
return fullSubtype;
}
/** A wrapper class of {@link SystemClock} to be mocked in unit tests. */
public static class Clock {
/**

View file

@ -18,6 +18,7 @@ package android.net.nsd;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@ -40,6 +41,7 @@ import java.net.InetAddress;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
@RunWith(DevSdkIgnoreRunner.class)
@SmallTest
@ -114,6 +116,7 @@ public class NsdServiceInfoTest {
NsdServiceInfo fullInfo = new NsdServiceInfo();
fullInfo.setServiceName("kitten");
fullInfo.setServiceType("_kitten._tcp");
fullInfo.setSubtypes(Set.of("_thread", "_matter"));
fullInfo.setPort(4242);
fullInfo.setHostAddresses(List.of(IPV4_ADDRESS));
fullInfo.setNetwork(new Network(123));
@ -149,7 +152,7 @@ public class NsdServiceInfoTest {
assertFalse(attributedInfo.getAttributes().keySet().contains("sticky"));
}
public void checkParcelable(NsdServiceInfo original) {
private static void checkParcelable(NsdServiceInfo original) {
// Write to parcel.
Parcel p = Parcel.obtain();
Bundle writer = new Bundle();
@ -179,11 +182,20 @@ public class NsdServiceInfoTest {
}
}
public void assertEmptyServiceInfo(NsdServiceInfo shouldBeEmpty) {
private static void assertEmptyServiceInfo(NsdServiceInfo shouldBeEmpty) {
byte[] txtRecord = shouldBeEmpty.getTxtRecord();
if (txtRecord == null || txtRecord.length == 0) {
return;
}
fail("NsdServiceInfo.getTxtRecord did not return null but " + Arrays.toString(txtRecord));
}
@Test
public void testSubtypesValidSubtypesSuccess() {
NsdServiceInfo info = new NsdServiceInfo();
info.setSubtypes(Set.of("_thread", "_matter"));
assertEquals(Set.of("_thread", "_matter"), info.getSubtypes());
}
}

View file

@ -43,6 +43,7 @@ import android.net.cts.NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStarted
import android.net.cts.NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStopped
import android.net.cts.NsdDiscoveryRecord.DiscoveryEvent.ServiceFound
import android.net.cts.NsdDiscoveryRecord.DiscoveryEvent.ServiceLost
import android.net.cts.NsdRegistrationRecord.RegistrationEvent.RegistrationFailed
import android.net.cts.NsdRegistrationRecord.RegistrationEvent.ServiceRegistered
import android.net.cts.NsdRegistrationRecord.RegistrationEvent.ServiceUnregistered
import android.net.cts.NsdResolveRecord.ResolveEvent.ResolutionStopped
@ -991,6 +992,104 @@ class NsdManagerTest {
}
}
@Test
fun testSubtypeAdvertisingAndDiscovery_withSetSubtypesApi() {
runSubtypeAdvertisingAndDiscoveryTest(useLegacySpecifier = false)
}
@Test
fun testSubtypeAdvertisingAndDiscovery_withSetSubtypesApiAndLegacySpecifier() {
runSubtypeAdvertisingAndDiscoveryTest(useLegacySpecifier = true)
}
private fun runSubtypeAdvertisingAndDiscoveryTest(useLegacySpecifier: Boolean) {
val si = makeTestServiceInfo(network = testNetwork1.network)
if (useLegacySpecifier) {
si.subtypes = setOf("_subtype1")
// Test "_type._tcp.local,_subtype" syntax with the registration
si.serviceType = si.serviceType + ",_subtype2"
} else {
si.subtypes = setOf("_subtype1", "_subtype2")
}
val registrationRecord = NsdRegistrationRecord()
val baseTypeDiscoveryRecord = NsdDiscoveryRecord()
val subtype1DiscoveryRecord = NsdDiscoveryRecord()
val subtype2DiscoveryRecord = NsdDiscoveryRecord()
val otherSubtypeDiscoveryRecord = NsdDiscoveryRecord()
tryTest {
registerService(registrationRecord, si)
nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
testNetwork1.network, Executor { it.run() }, baseTypeDiscoveryRecord)
// Test "<subtype>._type._tcp.local" syntax with discovery. Note this is not
// "<subtype>._sub._type._tcp.local".
nsdManager.discoverServices("_othersubtype.$serviceType",
NsdManager.PROTOCOL_DNS_SD,
testNetwork1.network, Executor { it.run() }, otherSubtypeDiscoveryRecord)
nsdManager.discoverServices("_subtype1.$serviceType",
NsdManager.PROTOCOL_DNS_SD,
testNetwork1.network, Executor { it.run() }, subtype1DiscoveryRecord)
nsdManager.discoverServices("_subtype2.$serviceType",
NsdManager.PROTOCOL_DNS_SD,
testNetwork1.network, Executor { it.run() }, subtype2DiscoveryRecord)
val info1 = subtype1DiscoveryRecord.waitForServiceDiscovered(
serviceName, serviceType, testNetwork1.network)
assertTrue(info1.subtypes.contains("_subtype1"))
val info2 = subtype2DiscoveryRecord.waitForServiceDiscovered(
serviceName, serviceType, testNetwork1.network)
assertTrue(info2.subtypes.contains("_subtype2"))
baseTypeDiscoveryRecord.waitForServiceDiscovered(
serviceName, serviceType, testNetwork1.network)
otherSubtypeDiscoveryRecord.expectCallback<DiscoveryStarted>()
// The subtype callback was registered later but called, no need for an extra delay
otherSubtypeDiscoveryRecord.assertNoCallback(timeoutMs = 0)
} cleanupStep {
nsdManager.stopServiceDiscovery(baseTypeDiscoveryRecord)
nsdManager.stopServiceDiscovery(subtype1DiscoveryRecord)
nsdManager.stopServiceDiscovery(subtype2DiscoveryRecord)
nsdManager.stopServiceDiscovery(otherSubtypeDiscoveryRecord)
baseTypeDiscoveryRecord.expectCallback<DiscoveryStopped>()
subtype1DiscoveryRecord.expectCallback<DiscoveryStopped>()
subtype2DiscoveryRecord.expectCallback<DiscoveryStopped>()
otherSubtypeDiscoveryRecord.expectCallback<DiscoveryStopped>()
} cleanup {
nsdManager.unregisterService(registrationRecord)
}
}
@Test
fun testSubtypeAdvertising_tooManySubtypes_returnsFailureBadParameters() {
val si = makeTestServiceInfo(network = testNetwork1.network)
// Sets 101 subtypes in total
val seq = generateSequence(1) { it + 1}
si.subtypes = seq.take(100).toList().map {it -> "_subtype" + it}.toSet()
si.serviceType = si.serviceType + ",_subtype"
val record = NsdRegistrationRecord()
nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, Executor { it.run() }, record)
val failedCb = record.expectCallback<RegistrationFailed>(REGISTRATION_TIMEOUT_MS)
assertEquals(NsdManager.FAILURE_BAD_PARAMETERS, failedCb.errorCode)
}
@Test
fun testSubtypeAdvertising_emptySubtypeLabel_returnsFailureBadParameters() {
val si = makeTestServiceInfo(network = testNetwork1.network)
si.subtypes = setOf("")
val record = NsdRegistrationRecord()
nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, Executor { it.run() }, record)
val failedCb = record.expectCallback<RegistrationFailed>(REGISTRATION_TIMEOUT_MS)
assertEquals(NsdManager.FAILURE_BAD_PARAMETERS, failedCb.errorCode)
}
@Test
fun testRegisterWithConflictDuringProbing() {
// This test requires shims supporting T+ APIs (NsdServiceInfo.network)

View file

@ -140,6 +140,7 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Queue;
import java.util.Set;
// TODOs:
// - test client can send requests and receive replies
@ -1117,7 +1118,8 @@ public class NsdServiceTest {
waitForIdle();
verify(mAdvertiser).addOrUpdateService(anyInt(), argThat(s ->
"Instance".equals(s.getServiceName())
&& SERVICE_TYPE.equals(s.getServiceType())), eq("_subtype"), any());
&& SERVICE_TYPE.equals(s.getServiceType())
&& s.getSubtypes().equals(Set.of("_subtype"))), any());
final DiscoveryListener discListener = mock(DiscoveryListener.class);
client.discoverServices(typeWithSubtype, PROTOCOL, network, Runnable::run, discListener);
@ -1223,7 +1225,7 @@ public class NsdServiceTest {
final ArgumentCaptor<Integer> serviceIdCaptor = ArgumentCaptor.forClass(Integer.class);
verify(mAdvertiser).addOrUpdateService(serviceIdCaptor.capture(),
argThat(info -> matches(info, regInfo)), eq(null) /* subtype */, any());
argThat(info -> matches(info, regInfo)), any());
client.unregisterService(regListenerWithoutFeature);
waitForIdle();
@ -1283,9 +1285,9 @@ public class NsdServiceTest {
// The advertiser is enabled for _type2 but not _type1
verify(mAdvertiser, never()).addOrUpdateService(anyInt(),
argThat(info -> matches(info, service1)), eq(null) /* subtype */, any());
argThat(info -> matches(info, service1)), any());
verify(mAdvertiser).addOrUpdateService(anyInt(), argThat(info -> matches(info, service2)),
eq(null) /* subtype */, any());
any());
}
@Test
@ -1310,7 +1312,7 @@ public class NsdServiceTest {
verify(mSocketProvider).startMonitoringSockets();
final ArgumentCaptor<Integer> idCaptor = ArgumentCaptor.forClass(Integer.class);
verify(mAdvertiser).addOrUpdateService(idCaptor.capture(), argThat(info ->
matches(info, regInfo)), eq(null) /* subtype */, any());
matches(info, regInfo)), any());
// Verify onServiceRegistered callback
final MdnsAdvertiser.AdvertiserCallback cb = cbCaptor.getValue();
@ -1358,7 +1360,7 @@ public class NsdServiceTest {
client.registerService(regInfo, NsdManager.PROTOCOL_DNS_SD, Runnable::run, regListener);
waitForIdle();
verify(mAdvertiser, never()).addOrUpdateService(anyInt(), any(), any(), any());
verify(mAdvertiser, never()).addOrUpdateService(anyInt(), any(), any());
verify(regListener, timeout(TIMEOUT_MS)).onRegistrationFailed(
argThat(info -> matches(info, regInfo)), eq(FAILURE_INTERNAL_ERROR));
@ -1388,8 +1390,7 @@ public class NsdServiceTest {
final ArgumentCaptor<Integer> idCaptor = ArgumentCaptor.forClass(Integer.class);
// Service name is truncated to 63 characters
verify(mAdvertiser).addOrUpdateService(idCaptor.capture(),
argThat(info -> info.getServiceName().equals("a".repeat(63))),
eq(null) /* subtype */, any());
argThat(info -> info.getServiceName().equals("a".repeat(63))), any());
// Verify onServiceRegistered callback
final MdnsAdvertiser.AdvertiserCallback cb = cbCaptor.getValue();
@ -1479,7 +1480,7 @@ public class NsdServiceTest {
client.registerService(regInfo, NsdManager.PROTOCOL_DNS_SD, Runnable::run, regListener);
waitForIdle();
verify(mSocketProvider).startMonitoringSockets();
verify(mAdvertiser).addOrUpdateService(anyInt(), any(), any(), any());
verify(mAdvertiser).addOrUpdateService(anyInt(), any(), any());
// Verify the discovery uses MdnsDiscoveryManager
final DiscoveryListener discListener = mock(DiscoveryListener.class);
@ -1512,7 +1513,7 @@ public class NsdServiceTest {
client.registerService(regInfo, NsdManager.PROTOCOL_DNS_SD, Runnable::run, regListener);
waitForIdle();
verify(mSocketProvider).startMonitoringSockets();
verify(mAdvertiser).addOrUpdateService(anyInt(), any(), any(), any());
verify(mAdvertiser).addOrUpdateService(anyInt(), any(), any());
final Network wifiNetwork1 = new Network(123);
final Network wifiNetwork2 = new Network(124);

View file

@ -68,6 +68,7 @@ private val TEST_SOCKETKEY_1 = SocketKey(1001 /* interfaceIndex */)
private val TEST_SOCKETKEY_2 = SocketKey(1002 /* interfaceIndex */)
private val TEST_HOSTNAME = arrayOf("Android_test", "local")
private const val TEST_SUBTYPE = "_subtype"
private const val TEST_SUBTYPE2 = "_subtype2"
private val TEST_INTERFACE1 = "test_iface1"
private val TEST_INTERFACE2 = "test_iface2"
private val TEST_OFFLOAD_PACKET1 = byteArrayOf(0x01, 0x02, 0x03)
@ -80,6 +81,13 @@ private val SERVICE_1 = NsdServiceInfo("TestServiceName", "_advertisertest._tcp"
network = TEST_NETWORK_1
}
private val SERVICE_1_SUBTYPE = NsdServiceInfo("TestServiceName", "_advertisertest._tcp").apply {
subtypes = setOf(TEST_SUBTYPE)
port = 12345
hostAddresses = listOf(TEST_ADDR)
network = TEST_NETWORK_1
}
private val LONG_SERVICE_1 =
NsdServiceInfo("a".repeat(48) + "TestServiceName", "_longadvertisertest._tcp").apply {
port = 12345
@ -93,6 +101,14 @@ private val ALL_NETWORKS_SERVICE = NsdServiceInfo("TestServiceName", "_advertise
network = null
}
private val ALL_NETWORKS_SERVICE_SUBTYPE =
NsdServiceInfo("TestServiceName", "_advertisertest._tcp").apply {
subtypes = setOf(TEST_SUBTYPE)
port = 12345
hostAddresses = listOf(TEST_ADDR)
network = null
}
private val ALL_NETWORKS_SERVICE_2 =
NsdServiceInfo("TESTSERVICENAME", "_ADVERTISERTEST._tcp").apply {
port = 12345
@ -189,7 +205,7 @@ class MdnsAdvertiserTest {
val advertiser =
MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags)
postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1,
null /* subtype */, DEFAULT_ADVERTISING_OPTION) }
DEFAULT_ADVERTISING_OPTION) }
val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
verify(socketProvider).requestSocket(eq(TEST_NETWORK_1), socketCbCaptor.capture())
@ -247,14 +263,14 @@ class MdnsAdvertiserTest {
}
@Test
fun testAddService_AllNetworks() {
fun testAddService_AllNetworksWithSubType() {
val advertiser =
MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags)
postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE,
TEST_SUBTYPE, DEFAULT_ADVERTISING_OPTION) }
postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE_SUBTYPE,
DEFAULT_ADVERTISING_OPTION) }
val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
verify(socketProvider).requestSocket(eq(ALL_NETWORKS_SERVICE.network),
verify(socketProvider).requestSocket(eq(ALL_NETWORKS_SERVICE_SUBTYPE.network),
socketCbCaptor.capture())
val socketCb = socketCbCaptor.value
@ -270,9 +286,9 @@ class MdnsAdvertiserTest {
eq(thread.looper), any(), intAdvCbCaptor2.capture(), eq(TEST_HOSTNAME), any(), any()
)
verify(mockInterfaceAdvertiser1).addService(
anyInt(), eq(ALL_NETWORKS_SERVICE), eq(TEST_SUBTYPE))
anyInt(), eq(ALL_NETWORKS_SERVICE_SUBTYPE))
verify(mockInterfaceAdvertiser2).addService(
anyInt(), eq(ALL_NETWORKS_SERVICE), eq(TEST_SUBTYPE))
anyInt(), eq(ALL_NETWORKS_SERVICE_SUBTYPE))
doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
postSync { intAdvCbCaptor1.value.onServiceProbingSucceeded(
@ -286,7 +302,7 @@ class MdnsAdvertiserTest {
mockInterfaceAdvertiser2, SERVICE_ID_1) }
verify(cb).onOffloadStartOrUpdate(eq(TEST_INTERFACE2), eq(OFFLOAD_SERVICEINFO))
verify(cb).onRegisterServiceSucceeded(eq(SERVICE_ID_1),
argThat { it.matches(ALL_NETWORKS_SERVICE) })
argThat { it.matches(ALL_NETWORKS_SERVICE_SUBTYPE) })
// Services are conflicted.
postSync { intAdvCbCaptor1.value.onServiceConflict(mockInterfaceAdvertiser1, SERVICE_ID_1) }
@ -323,7 +339,7 @@ class MdnsAdvertiserTest {
val advertiser =
MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags)
postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1,
null /* subtype */, DEFAULT_ADVERTISING_OPTION) }
DEFAULT_ADVERTISING_OPTION) }
val oneNetSocketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
verify(socketProvider).requestSocket(eq(TEST_NETWORK_1), oneNetSocketCbCaptor.capture())
@ -331,18 +347,18 @@ class MdnsAdvertiserTest {
// Register a service with the same name on all networks (name conflict)
postSync { advertiser.addOrUpdateService(SERVICE_ID_2, ALL_NETWORKS_SERVICE,
null /* subtype */, DEFAULT_ADVERTISING_OPTION) }
DEFAULT_ADVERTISING_OPTION) }
val allNetSocketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
verify(socketProvider).requestSocket(eq(null), allNetSocketCbCaptor.capture())
val allNetSocketCb = allNetSocketCbCaptor.value
postSync { advertiser.addOrUpdateService(LONG_SERVICE_ID_1, LONG_SERVICE_1,
null /* subtype */, DEFAULT_ADVERTISING_OPTION) }
DEFAULT_ADVERTISING_OPTION) }
postSync { advertiser.addOrUpdateService(LONG_SERVICE_ID_2, LONG_ALL_NETWORKS_SERVICE,
null /* subtype */, DEFAULT_ADVERTISING_OPTION) }
DEFAULT_ADVERTISING_OPTION) }
postSync { advertiser.addOrUpdateService(CASE_INSENSITIVE_TEST_SERVICE_ID,
ALL_NETWORKS_SERVICE_2, null /* subtype */, DEFAULT_ADVERTISING_OPTION) }
ALL_NETWORKS_SERVICE_2, DEFAULT_ADVERTISING_OPTION) }
// Callbacks for matching network and all networks both get the socket
postSync {
@ -378,15 +394,15 @@ class MdnsAdvertiserTest {
eq(thread.looper), any(), intAdvCbCaptor.capture(), eq(TEST_HOSTNAME), any(), any()
)
verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_1),
argThat { it.matches(SERVICE_1) }, eq(null))
argThat { it.matches(SERVICE_1) })
verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_2),
argThat { it.matches(expectedRenamed) }, eq(null))
argThat { it.matches(expectedRenamed) })
verify(mockInterfaceAdvertiser1).addService(eq(LONG_SERVICE_ID_1),
argThat { it.matches(LONG_SERVICE_1) }, eq(null))
argThat { it.matches(LONG_SERVICE_1) })
verify(mockInterfaceAdvertiser1).addService(eq(LONG_SERVICE_ID_2),
argThat { it.matches(expectedLongRenamed) }, eq(null))
argThat { it.matches(expectedLongRenamed) })
verify(mockInterfaceAdvertiser1).addService(eq(CASE_INSENSITIVE_TEST_SERVICE_ID),
argThat { it.matches(expectedCaseInsensitiveRenamed) }, eq(null))
argThat { it.matches(expectedCaseInsensitiveRenamed) })
doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
postSync { intAdvCbCaptor.value.onServiceProbingSucceeded(
@ -411,7 +427,7 @@ class MdnsAdvertiserTest {
val advertiser =
MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags)
postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE,
null /* subtype */, DEFAULT_ADVERTISING_OPTION) }
DEFAULT_ADVERTISING_OPTION) }
val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
verify(socketProvider).requestSocket(eq(null), socketCbCaptor.capture())
@ -420,29 +436,28 @@ class MdnsAdvertiserTest {
postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_1, mockSocket1, listOf(TEST_LINKADDR)) }
verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_1),
argThat { it.matches(ALL_NETWORKS_SERVICE) }, eq(null))
argThat { it.matches(ALL_NETWORKS_SERVICE) })
val updateOptions = MdnsAdvertisingOptions.newBuilder().setIsOnlyUpdate(true).build()
// Update with serviceId that is not registered yet should fail
postSync { advertiser.addOrUpdateService(SERVICE_ID_2, ALL_NETWORKS_SERVICE, TEST_SUBTYPE,
postSync { advertiser.addOrUpdateService(SERVICE_ID_2, ALL_NETWORKS_SERVICE_SUBTYPE,
updateOptions) }
verify(cb).onRegisterServiceFailed(SERVICE_ID_2, NsdManager.FAILURE_INTERNAL_ERROR)
// Update service with different NsdServiceInfo should fail
postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1, TEST_SUBTYPE,
updateOptions) }
postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1_SUBTYPE, updateOptions) }
verify(cb).onRegisterServiceFailed(SERVICE_ID_1, NsdManager.FAILURE_INTERNAL_ERROR)
// Update service with same NsdServiceInfo but different subType should succeed
postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE, TEST_SUBTYPE,
postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE_SUBTYPE,
updateOptions) }
verify(mockInterfaceAdvertiser1).updateService(eq(SERVICE_ID_1), eq(TEST_SUBTYPE))
verify(mockInterfaceAdvertiser1).updateService(eq(SERVICE_ID_1), eq(setOf(TEST_SUBTYPE)))
// Newly created MdnsInterfaceAdvertiser will get addService() call.
postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_2, mockSocket2, listOf(TEST_LINKADDR2)) }
verify(mockInterfaceAdvertiser2).addService(eq(SERVICE_ID_1),
argThat { it.matches(ALL_NETWORKS_SERVICE) }, eq(TEST_SUBTYPE))
argThat { it.matches(ALL_NETWORKS_SERVICE_SUBTYPE) })
}
@Test
@ -451,7 +466,7 @@ class MdnsAdvertiserTest {
MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags)
verify(mockDeps, times(1)).generateHostname()
postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1,
null /* subtype */, DEFAULT_ADVERTISING_OPTION) }
DEFAULT_ADVERTISING_OPTION) }
postSync { advertiser.removeService(SERVICE_ID_1) }
verify(mockDeps, times(2)).generateHostname()
}

View file

@ -67,6 +67,13 @@ private val TEST_SERVICE_1 = NsdServiceInfo().apply {
port = 12345
}
private val TEST_SERVICE_1_SUBTYPE = NsdServiceInfo().apply {
subtypes = setOf("_sub")
serviceType = "_testservice._tcp"
serviceName = "MyTestService"
port = 12345
}
@RunWith(DevSdkIgnoreRunner::class)
@IgnoreUpTo(Build.VERSION_CODES.S_V2)
class MdnsInterfaceAdvertiserTest {
@ -119,7 +126,7 @@ class MdnsInterfaceAdvertiserTest {
knownServices.add(inv.getArgument(0))
-1
}.`when`(repository).addService(anyInt(), any(), any())
}.`when`(repository).addService(anyInt(), any())
doAnswer { inv ->
knownServices.remove(inv.getArgument(0))
null
@ -277,10 +284,9 @@ class MdnsInterfaceAdvertiserTest {
@Test
fun testReplaceExitingService() {
doReturn(TEST_SERVICE_ID_DUPLICATE).`when`(repository)
.addService(eq(TEST_SERVICE_ID_DUPLICATE), any(), any())
val subType = "_sub"
advertiser.addService(TEST_SERVICE_ID_DUPLICATE, TEST_SERVICE_1, subType)
verify(repository).addService(eq(TEST_SERVICE_ID_DUPLICATE), any(), any())
.addService(eq(TEST_SERVICE_ID_DUPLICATE), any())
advertiser.addService(TEST_SERVICE_ID_DUPLICATE, TEST_SERVICE_1_SUBTYPE)
verify(repository).addService(eq(TEST_SERVICE_ID_DUPLICATE), any())
verify(announcer).stop(TEST_SERVICE_ID_DUPLICATE)
verify(prober).startProbing(any())
}
@ -288,9 +294,9 @@ class MdnsInterfaceAdvertiserTest {
@Test
fun testUpdateExistingService() {
doReturn(TEST_SERVICE_ID_DUPLICATE).`when`(repository)
.addService(eq(TEST_SERVICE_ID_DUPLICATE), any(), any())
val subType = "_sub"
advertiser.updateService(TEST_SERVICE_ID_DUPLICATE, subType)
.addService(eq(TEST_SERVICE_ID_DUPLICATE), any())
val subTypes = setOf("_sub")
advertiser.updateService(TEST_SERVICE_ID_DUPLICATE, subTypes)
verify(repository).updateService(eq(TEST_SERVICE_ID_DUPLICATE), any())
verify(announcer, never()).stop(TEST_SERVICE_ID_DUPLICATE)
verify(prober, never()).startProbing(any())
@ -302,8 +308,8 @@ class MdnsInterfaceAdvertiserTest {
doReturn(serviceId).`when`(testProbingInfo).serviceId
doReturn(testProbingInfo).`when`(repository).setServiceProbing(serviceId)
advertiser.addService(serviceId, serviceInfo, null /* subtype */)
verify(repository).addService(serviceId, serviceInfo, null /* subtype */)
advertiser.addService(serviceId, serviceInfo)
verify(repository).addService(serviceId, serviceInfo)
verify(prober).startProbing(testProbingInfo)
// Simulate probing success: continues to announcing

View file

@ -27,6 +27,7 @@ import com.android.server.connectivity.mdns.MdnsRecordRepository.getReverseDnsAd
import com.android.server.connectivity.mdns.MdnsServiceInfo.TextEntry
import com.android.testutils.DevSdkIgnoreRule
import com.android.testutils.DevSdkIgnoreRunner
import com.google.common.truth.Truth.assertThat
import java.net.InetSocketAddress
import java.net.NetworkInterface
import java.util.Collections
@ -46,6 +47,7 @@ private const val TEST_SERVICE_ID_2 = 43
private const val TEST_SERVICE_ID_3 = 44
private const val TEST_PORT = 12345
private const val TEST_SUBTYPE = "_subtype"
private const val TEST_SUBTYPE2 = "_subtype2"
private val TEST_HOSTNAME = arrayOf("Android_000102030405060708090A0B0C0D0E0F", "local")
private val TEST_ADDRESSES = listOf(
LinkAddress(parseNumericAddress("192.0.2.111"), 24),
@ -95,8 +97,7 @@ class MdnsRecordRepositoryTest {
fun testAddServiceAndProbe() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
assertEquals(0, repository.servicesCount)
assertEquals(-1, repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1,
null /* subtype */))
assertEquals(-1, repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1))
assertEquals(1, repository.servicesCount)
val probingInfo = repository.setServiceProbing(TEST_SERVICE_ID_1)
@ -131,10 +132,10 @@ class MdnsRecordRepositoryTest {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
assertFailsWith(NameConflictException::class) {
repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1, null /* subtype */)
repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1)
}
assertFailsWith(NameConflictException::class) {
repository.addService(TEST_SERVICE_ID_3, TEST_SERVICE_3, null /* subtype */)
repository.addService(TEST_SERVICE_ID_3, TEST_SERVICE_3)
}
}
@ -144,10 +145,10 @@ class MdnsRecordRepositoryTest {
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
assertFailsWith(IllegalArgumentException::class) {
repository.updateService(TEST_SERVICE_ID_2, null /* subtype */)
repository.updateService(TEST_SERVICE_ID_2, emptySet() /* subtype */)
}
repository.updateService(TEST_SERVICE_ID_1, TEST_SUBTYPE)
repository.updateService(TEST_SERVICE_ID_1, setOf(TEST_SUBTYPE))
val queriedName = arrayOf(TEST_SUBTYPE, "_sub", "_testservice", "_tcp", "local")
val questions = listOf(MdnsPointerRecord(queriedName, false /* isUnicast */))
@ -175,9 +176,9 @@ class MdnsRecordRepositoryTest {
@Test
fun testInvalidReuseOfServiceId() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
assertFailsWith(IllegalArgumentException::class) {
repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_2, null /* subtype */)
repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_2)
}
}
@ -186,7 +187,7 @@ class MdnsRecordRepositoryTest {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
assertFalse(repository.hasActiveService(TEST_SERVICE_ID_1))
repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
assertTrue(repository.hasActiveService(TEST_SERVICE_ID_1))
val probingInfo = repository.setServiceProbing(TEST_SERVICE_ID_1)
@ -229,9 +230,10 @@ class MdnsRecordRepositoryTest {
}
@Test
fun testExitAnnouncements_WithSubtype() {
fun testExitAnnouncements_WithSubtypes() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, TEST_SUBTYPE)
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1,
setOf(TEST_SUBTYPE, TEST_SUBTYPE2))
repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */)
val exitAnnouncement = repository.exitService(TEST_SERVICE_ID_1)
@ -245,7 +247,7 @@ class MdnsRecordRepositoryTest {
assertEquals(0, packet.authorityRecords.size)
assertEquals(0, packet.additionalRecords.size)
assertContentEquals(listOf(
assertThat(packet.answers).containsExactly(
MdnsPointerRecord(
arrayOf("_testservice", "_tcp", "local"),
0L /* receiptTimeMillis */,
@ -258,7 +260,12 @@ class MdnsRecordRepositoryTest {
false /* cacheFlush */,
0L /* ttlMillis */,
arrayOf("MyTestService", "_testservice", "_tcp", "local")),
), packet.answers)
MdnsPointerRecord(
arrayOf("_subtype2", "_sub", "_testservice", "_tcp", "local"),
0L /* receiptTimeMillis */,
false /* cacheFlush */,
0L /* ttlMillis */,
arrayOf("MyTestService", "_testservice", "_tcp", "local")))
repository.removeService(TEST_SERVICE_ID_1)
assertEquals(0, repository.servicesCount)
@ -272,7 +279,7 @@ class MdnsRecordRepositoryTest {
repository.exitService(TEST_SERVICE_ID_1)
assertEquals(TEST_SERVICE_ID_1,
repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1, null /* subtype */))
repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1))
assertEquals(1, repository.servicesCount)
repository.removeService(TEST_SERVICE_ID_2)
@ -283,7 +290,7 @@ class MdnsRecordRepositoryTest {
fun testOnProbingSucceeded() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
val announcementInfo = repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1,
TEST_SUBTYPE)
setOf(TEST_SUBTYPE, TEST_SUBTYPE2))
repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */)
val packet = announcementInfo.getPacket(0)
@ -294,12 +301,13 @@ class MdnsRecordRepositoryTest {
val serviceType = arrayOf("_testservice", "_tcp", "local")
val serviceSubtype = arrayOf(TEST_SUBTYPE, "_sub", "_testservice", "_tcp", "local")
val serviceSubtype2 = arrayOf(TEST_SUBTYPE2, "_sub", "_testservice", "_tcp", "local")
val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
val v4AddrRev = getReverseDnsAddress(TEST_ADDRESSES[0].address)
val v6Addr1Rev = getReverseDnsAddress(TEST_ADDRESSES[1].address)
val v6Addr2Rev = getReverseDnsAddress(TEST_ADDRESSES[2].address)
assertContentEquals(listOf(
assertThat(packet.answers).containsExactly(
// Reverse address and address records for the hostname
MdnsPointerRecord(v4AddrRev,
0L /* receiptTimeMillis */,
@ -346,6 +354,13 @@ class MdnsRecordRepositoryTest {
false /* cacheFlush */,
4500000L /* ttlMillis */,
serviceName),
MdnsPointerRecord(
serviceSubtype2,
0L /* receiptTimeMillis */,
// Not a unique name owned by the announcer, so cacheFlush=false
false /* cacheFlush */,
4500000L /* ttlMillis */,
serviceName),
MdnsServiceRecord(
serviceName,
0L /* receiptTimeMillis */,
@ -367,8 +382,7 @@ class MdnsRecordRepositoryTest {
0L /* receiptTimeMillis */,
false /* cacheFlush */,
4500000L /* ttlMillis */,
serviceType)
), packet.answers)
serviceType))
assertContentEquals(listOf(
MdnsNsecRecord(v4AddrRev,
@ -484,19 +498,20 @@ class MdnsRecordRepositoryTest {
@Test
fun testGetReply() {
doGetReplyTest(subtype = null)
doGetReplyTest(queryWithSubtype = false)
}
@Test
fun testGetReply_WithSubtype() {
doGetReplyTest(TEST_SUBTYPE)
doGetReplyTest(queryWithSubtype = true)
}
private fun doGetReplyTest(subtype: String?) {
private fun doGetReplyTest(queryWithSubtype: Boolean) {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, subtype)
val queriedName = if (subtype == null) arrayOf("_testservice", "_tcp", "local")
else arrayOf(subtype, "_sub", "_testservice", "_tcp", "local")
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1,
setOf(TEST_SUBTYPE, TEST_SUBTYPE2))
val queriedName = if (!queryWithSubtype) arrayOf("_testservice", "_tcp", "local")
else arrayOf(TEST_SUBTYPE, "_sub", "_testservice", "_tcp", "local")
val questions = listOf(MdnsPointerRecord(queriedName, false /* isUnicast */))
val query = MdnsPacket(0 /* flags */, questions, listOf() /* answers */,
@ -577,8 +592,8 @@ class MdnsRecordRepositoryTest {
@Test
fun testGetConflictingServices() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* subtype */)
repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2)
val packet = MdnsPacket(
0 /* flags */,
@ -605,8 +620,8 @@ class MdnsRecordRepositoryTest {
@Test
fun testGetConflictingServicesCaseInsensitive() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* subtype */)
repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2)
val packet = MdnsPacket(
0 /* flags */,
@ -633,8 +648,8 @@ class MdnsRecordRepositoryTest {
@Test
fun testGetConflictingServices_IdenticalService() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* subtype */)
repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2)
val otherTtlMillis = 1234L
val packet = MdnsPacket(
@ -662,8 +677,8 @@ class MdnsRecordRepositoryTest {
@Test
fun testGetConflictingServicesCaseInsensitive_IdenticalService() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* subtype */)
repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2)
val otherTtlMillis = 1234L
val packet = MdnsPacket(
@ -718,8 +733,7 @@ class MdnsRecordRepositoryTest {
MdnsFeatureFlags.newBuilder().setIncludeInetAddressRecordsInProbing(true).build())
repository.updateAddresses(TEST_ADDRESSES)
assertEquals(0, repository.servicesCount)
assertEquals(-1, repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1,
null /* subtype */))
assertEquals(-1, repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1))
assertEquals(1, repository.servicesCount)
val probingInfo = repository.setServiceProbing(TEST_SERVICE_ID_1)
@ -778,10 +792,11 @@ class MdnsRecordRepositoryTest {
private fun MdnsRecordRepository.initWithService(
serviceId: Int,
serviceInfo: NsdServiceInfo,
subtype: String? = null,
subtypes: Set<String> = setOf(),
): AnnouncementInfo {
updateAddresses(TEST_ADDRESSES)
addService(serviceId, serviceInfo, subtype)
serviceInfo.setSubtypes(subtypes)
addService(serviceId, serviceInfo)
val probingInfo = setServiceProbing(serviceId)
assertNotNull(probingInfo)
return onProbingSucceeded(probingInfo)