1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.connectivity.mdns; 18 19 import static com.android.server.connectivity.mdns.MdnsConstants.NO_PACKET; 20 import static com.android.server.connectivity.mdns.MdnsInterfaceAdvertiser.CONFLICT_HOST; 21 import static com.android.server.connectivity.mdns.MdnsInterfaceAdvertiser.CONFLICT_SERVICE; 22 import static com.android.server.connectivity.mdns.MdnsRecord.MAX_LABEL_LENGTH; 23 24 import android.annotation.NonNull; 25 import android.annotation.Nullable; 26 import android.annotation.RequiresApi; 27 import android.content.Context; 28 import android.net.LinkAddress; 29 import android.net.Network; 30 import android.net.nsd.NsdManager; 31 import android.net.nsd.NsdServiceInfo; 32 import android.net.nsd.OffloadEngine; 33 import android.net.nsd.OffloadServiceInfo; 34 import android.os.Build; 35 import android.os.Looper; 36 import android.text.TextUtils; 37 import android.util.ArrayMap; 38 import android.util.ArraySet; 39 import android.util.Log; 40 import android.util.SparseArray; 41 42 import com.android.connectivity.resources.R; 43 import com.android.internal.annotations.VisibleForTesting; 44 import com.android.net.module.util.CollectionUtils; 45 import com.android.net.module.util.DnsUtils; 46 import com.android.net.module.util.SharedLog; 47 import com.android.server.connectivity.ConnectivityResources; 48 import com.android.server.connectivity.mdns.util.MdnsUtils; 49 50 import java.security.SecureRandom; 51 import java.util.ArrayList; 52 import java.util.Collections; 53 import java.util.List; 54 import java.util.Map; 55 import java.util.Objects; 56 import java.util.Set; 57 import java.util.UUID; 58 import java.util.function.BiPredicate; 59 import java.util.function.Consumer; 60 61 /** 62 * MdnsAdvertiser manages advertising services per {@link com.android.server.NsdService} requests. 63 * 64 * All methods except the constructor must be called on the looper thread. 65 */ 66 @RequiresApi(Build.VERSION_CODES.TIRAMISU) 67 public class MdnsAdvertiser { 68 private static final String TAG = MdnsAdvertiser.class.getSimpleName(); 69 static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG); 70 71 // Top-level domain for link-local queries, as per RFC6762 3. 72 private static final String LOCAL_TLD = "local"; 73 74 75 private final Looper mLooper; 76 private final AdvertiserCallback mCb; 77 78 // Max-sized buffers to be used as temporary buffer to read/build packets. May be used by 79 // multiple components, but only for self-contained operations in the looper thread, so not 80 // concurrently. 81 // TODO: set according to MTU. 1300 should fit for ethernet MTU 1500 with some overhead. 82 private final byte[] mPacketCreationBuffer = new byte[1300]; 83 84 private final MdnsSocketProvider mSocketProvider; 85 private final ArrayMap<Network, InterfaceAdvertiserRequest> mAdvertiserRequests = 86 new ArrayMap<>(); 87 private final ArrayMap<MdnsInterfaceSocket, MdnsInterfaceAdvertiser> mAllAdvertisers = 88 new ArrayMap<>(); 89 private final SparseArray<Registration> mRegistrations = new SparseArray<>(); 90 private final Dependencies mDeps; 91 private String[] mDeviceHostName; 92 @NonNull private final SharedLog mSharedLog; 93 private final Map<String, List<OffloadServiceInfoWrapper>> mInterfaceOffloadServices = 94 new ArrayMap<>(); 95 private final MdnsFeatureFlags mMdnsFeatureFlags; 96 private final Map<String, Integer> mServiceTypeToOffloadPriority; 97 private final ArraySet<String> mOffloadServiceTypeDenyList; 98 99 /** 100 * Dependencies for {@link MdnsAdvertiser}, useful for testing. 101 */ 102 @VisibleForTesting 103 public static class Dependencies { 104 /** 105 * @see MdnsInterfaceAdvertiser 106 */ makeAdvertiser(@onNull MdnsInterfaceSocket socket, @NonNull List<LinkAddress> initialAddresses, @NonNull Looper looper, @NonNull byte[] packetCreationBuffer, @NonNull MdnsInterfaceAdvertiser.Callback cb, @NonNull String[] deviceHostName, @NonNull SharedLog sharedLog, @NonNull MdnsFeatureFlags mdnsFeatureFlags)107 public MdnsInterfaceAdvertiser makeAdvertiser(@NonNull MdnsInterfaceSocket socket, 108 @NonNull List<LinkAddress> initialAddresses, 109 @NonNull Looper looper, @NonNull byte[] packetCreationBuffer, 110 @NonNull MdnsInterfaceAdvertiser.Callback cb, 111 @NonNull String[] deviceHostName, 112 @NonNull SharedLog sharedLog, 113 @NonNull MdnsFeatureFlags mdnsFeatureFlags) { 114 // Note NetworkInterface is final and not mockable 115 return new MdnsInterfaceAdvertiser(socket, initialAddresses, looper, 116 packetCreationBuffer, cb, deviceHostName, sharedLog, mdnsFeatureFlags); 117 } 118 119 /** 120 * Generates a unique hostname to be used by the device. 121 */ 122 @NonNull generateHostname(boolean useShortFormat)123 public String[] generateHostname(boolean useShortFormat) { 124 // Generate a very-probably-unique hostname. This allows minimizing possible conflicts 125 // to the point that probing for it is no longer necessary (as per RFC6762 8.1 last 126 // paragraph), and does not leak more information than what could already be obtained by 127 // looking at the mDNS packets source address. 128 // This differs from historical behavior that just used "Android.local" for many 129 // devices, creating a lot of conflicts. 130 // Having a different hostname per interface is an acceptable option as per RFC6762 14. 131 // This hostname will change every time the interface is reconnected, so this does not 132 // allow tracking the device. 133 if (useShortFormat) { 134 // A short hostname helps reduce the size of APF mDNS filtering programs, and 135 // is necessary for compatibility with some Matter 1.0 devices which assumed 136 // 16 characters is the maximum length. 137 // Generate a hostname matching Android_[0-9A-Z]{8}, which has 36^8 possibilities. 138 // Even with 100 devices advertising the probability of collision is around 2E-9, 139 // which is negligible. 140 final SecureRandom sr = new SecureRandom(); 141 final String allowedChars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 142 final StringBuilder sb = new StringBuilder(8); 143 for (int i = 0; i < 8; i++) { 144 sb.append(allowedChars.charAt(sr.nextInt(allowedChars.length()))); 145 } 146 return new String[]{ "Android_" + sb.toString(), LOCAL_TLD }; 147 } else { 148 return new String[]{ 149 "Android_" + UUID.randomUUID().toString().replace("-", ""), LOCAL_TLD}; 150 } 151 } 152 } 153 154 /** 155 * Gets the current status of the OffloadServiceInfos per interface. 156 * @param interfaceName the target interfaceName 157 * @return the list of current offloaded services. 158 */ 159 @NonNull getAllInterfaceOffloadServiceInfos( @onNull String interfaceName)160 public List<OffloadServiceInfoWrapper> getAllInterfaceOffloadServiceInfos( 161 @NonNull String interfaceName) { 162 return mInterfaceOffloadServices.getOrDefault(interfaceName, Collections.emptyList()); 163 } 164 isInOffloadDenyList(@onNull String serviceType)165 private boolean isInOffloadDenyList(@NonNull String serviceType) { 166 for (int i = 0; i < mOffloadServiceTypeDenyList.size(); ++i) { 167 final String denyListServiceType = mOffloadServiceTypeDenyList.valueAt(i); 168 if (DnsUtils.equalsIgnoreDnsCase(serviceType, denyListServiceType)) { 169 return true; 170 } 171 } 172 return false; 173 } 174 175 private final MdnsInterfaceAdvertiser.Callback mInterfaceAdvertiserCb = 176 new MdnsInterfaceAdvertiser.Callback() { 177 @Override 178 public void onServiceProbingSucceeded( 179 @NonNull MdnsInterfaceAdvertiser advertiser, int serviceId) { 180 final Registration registration = mRegistrations.get(serviceId); 181 if (registration == null) { 182 mSharedLog.wtf("Register succeeded for unknown registration"); 183 return; 184 } 185 if (mMdnsFeatureFlags.mIsMdnsOffloadFeatureEnabled 186 // TODO: Enable offload when the serviceInfo contains a custom host. 187 && TextUtils.isEmpty(registration.getServiceInfo().getHostname())) { 188 final String serviceType = registration.getServiceInfo().getServiceType(); 189 if (isInOffloadDenyList(serviceType)) { 190 mSharedLog.i("Offload denied for service type: " + serviceType); 191 } else { 192 final String interfaceName = advertiser.getSocketInterfaceName(); 193 final List<OffloadServiceInfoWrapper> existingOffloadServiceInfoWrappers = 194 mInterfaceOffloadServices.computeIfAbsent(interfaceName, 195 k -> new ArrayList<>()); 196 // Remove existing offload services from cache for update. 197 existingOffloadServiceInfoWrappers.removeIf( 198 item -> item.mServiceId == serviceId); 199 200 byte[] rawOffloadPacket = advertiser.getRawOffloadPayload(serviceId); 201 final OffloadServiceInfoWrapper newOffloadServiceInfoWrapper = 202 createOffloadService(serviceId, registration, rawOffloadPacket); 203 existingOffloadServiceInfoWrappers.add(newOffloadServiceInfoWrapper); 204 mCb.onOffloadStartOrUpdate(interfaceName, 205 newOffloadServiceInfoWrapper.mOffloadServiceInfo); 206 } 207 } 208 209 // Wait for all current interfaces to be done probing before notifying of success. 210 if (any(mAllAdvertisers, (k, a) -> a.isProbing(serviceId))) return; 211 // The service may still be unregistered/renamed if a conflict is found on a later added 212 // interface, or if a conflicting announcement/reply is detected (RFC6762 9.) 213 214 if (!registration.mNotifiedRegistrationSuccess) { 215 mCb.onRegisterServiceSucceeded(serviceId, registration.getServiceInfo()); 216 registration.mNotifiedRegistrationSuccess = true; 217 } 218 } 219 220 @Override 221 public void onServiceConflict(@NonNull MdnsInterfaceAdvertiser advertiser, int serviceId, 222 int conflictType) { 223 mSharedLog.i("Found conflict, restarted probing for service " 224 + serviceId + " " 225 + conflictType); 226 227 final Registration registration = mRegistrations.get(serviceId); 228 if (registration == null) return; 229 if (registration.mNotifiedRegistrationSuccess) { 230 // TODO: consider notifying clients that the service is no longer registered with 231 // the old name (back to probing). The legacy implementation did not send any 232 // callback though; it only sent onServiceRegistered after re-probing finishes 233 // (with the old, conflicting, actually not used name as argument... The new 234 // implementation will send callbacks with the new name). 235 registration.mNotifiedRegistrationSuccess = false; 236 registration.mConflictAfterProbingCount++; 237 238 // The service was done probing, just reset it to probing state (RFC6762 9.) 239 forAllAdvertisers(a -> { 240 if (!a.maybeRestartProbingForConflict(serviceId)) { 241 return; 242 } 243 if (mMdnsFeatureFlags.mIsMdnsOffloadFeatureEnabled) { 244 maybeSendOffloadStop(a.getSocketInterfaceName(), serviceId); 245 } 246 }); 247 return; 248 } 249 250 if ((conflictType & CONFLICT_SERVICE) != 0) { 251 // Service conflict was found during probing; rename once to find a name that has no 252 // conflict 253 registration.updateForServiceConflict( 254 registration.makeNewServiceInfoForServiceConflict(1 /* renameCount */), 255 1 /* renameCount */); 256 } 257 258 if ((conflictType & CONFLICT_HOST) != 0) { 259 // Host conflict was found during probing; rename once to find a name that has no 260 // conflict 261 registration.updateForHostConflict( 262 registration.makeNewServiceInfoForHostConflict(1 /* renameCount */), 263 1 /* renameCount */); 264 } 265 266 registration.mConflictDuringProbingCount++; 267 268 // Keep renaming if the new name conflicts in local registrations 269 updateRegistrationUntilNoConflict((net, adv) -> adv.hasRegistration(registration), 270 registration); 271 272 // Update advertisers to use the new name 273 forAllAdvertisers(a -> a.renameServiceForConflict( 274 serviceId, registration.getServiceInfo())); 275 } 276 277 @Override 278 public void onAllServicesRemoved(@NonNull MdnsInterfaceSocket socket) { 279 if (DBG) { mSharedLog.i("onAllServicesRemoved: " + socket); } 280 // Try destroying the advertiser if all services has been removed 281 destroyAdvertiser(socket, false /* interfaceDestroyed */); 282 } 283 }; 284 hasAnyServiceConflict( @onNull BiPredicate<Network, InterfaceAdvertiserRequest> applicableAdvertiserFilter, @NonNull NsdServiceInfo newInfo, @NonNull Registration originalRegistration)285 private boolean hasAnyServiceConflict( 286 @NonNull BiPredicate<Network, InterfaceAdvertiserRequest> applicableAdvertiserFilter, 287 @NonNull NsdServiceInfo newInfo, 288 @NonNull Registration originalRegistration) { 289 return any( 290 mAdvertiserRequests, 291 (network, adv) -> 292 applicableAdvertiserFilter.test(network, adv) 293 && adv.hasServiceConflict(newInfo, originalRegistration)); 294 } 295 hasAnyHostConflict( @onNull BiPredicate<Network, InterfaceAdvertiserRequest> applicableAdvertiserFilter, @NonNull NsdServiceInfo newInfo, int clientUid)296 private boolean hasAnyHostConflict( 297 @NonNull BiPredicate<Network, InterfaceAdvertiserRequest> applicableAdvertiserFilter, 298 @NonNull NsdServiceInfo newInfo, 299 int clientUid) { 300 // Check if it conflicts with custom hosts. 301 if (any( 302 mAdvertiserRequests, 303 (network, adv) -> 304 applicableAdvertiserFilter.test(network, adv) 305 && adv.hasHostConflict(newInfo, clientUid))) { 306 return true; 307 } 308 // Check if it conflicts with the default hostname. 309 return DnsUtils.equalsIgnoreDnsCase(newInfo.getHostname(), mDeviceHostName[0]); 310 } 311 updateRegistrationUntilNoConflict( @onNull BiPredicate<Network, InterfaceAdvertiserRequest> applicableAdvertiserFilter, @NonNull Registration registration)312 private void updateRegistrationUntilNoConflict( 313 @NonNull BiPredicate<Network, InterfaceAdvertiserRequest> applicableAdvertiserFilter, 314 @NonNull Registration registration) { 315 NsdServiceInfo newInfo = registration.getServiceInfo(); 316 317 int renameServiceCount = 0; 318 while (hasAnyServiceConflict(applicableAdvertiserFilter, newInfo, registration)) { 319 renameServiceCount++; 320 newInfo = registration.makeNewServiceInfoForServiceConflict(renameServiceCount); 321 } 322 registration.updateForServiceConflict(newInfo, renameServiceCount); 323 324 if (!TextUtils.isEmpty(registration.getServiceInfo().getHostname())) { 325 int renameHostCount = 0; 326 while (hasAnyHostConflict( 327 applicableAdvertiserFilter, newInfo, registration.mClientUid)) { 328 renameHostCount++; 329 newInfo = registration.makeNewServiceInfoForHostConflict(renameHostCount); 330 } 331 registration.updateForHostConflict(newInfo, renameHostCount); 332 } 333 } 334 maybeSendOffloadStop(final String interfaceName, int serviceId)335 private void maybeSendOffloadStop(final String interfaceName, int serviceId) { 336 final List<OffloadServiceInfoWrapper> existingOffloadServiceInfoWrappers = 337 mInterfaceOffloadServices.get(interfaceName); 338 if (existingOffloadServiceInfoWrappers == null) { 339 return; 340 } 341 // Stop the offloaded service by matching the service id 342 int idx = CollectionUtils.indexOf(existingOffloadServiceInfoWrappers, 343 item -> item.mServiceId == serviceId); 344 if (idx >= 0) { 345 mCb.onOffloadStop(interfaceName, 346 existingOffloadServiceInfoWrappers.get(idx).mOffloadServiceInfo); 347 existingOffloadServiceInfoWrappers.remove(idx); 348 } 349 } 350 351 /** 352 * Destroys the advertiser for the interface indicated by {@code socket}. 353 * 354 * {@code interfaceDestroyed} should be set to {@code true} if this method is called because 355 * the associated interface has been destroyed. 356 */ destroyAdvertiser(MdnsInterfaceSocket socket, boolean interfaceDestroyed)357 private void destroyAdvertiser(MdnsInterfaceSocket socket, boolean interfaceDestroyed) { 358 InterfaceAdvertiserRequest advertiserRequest; 359 360 MdnsInterfaceAdvertiser advertiser = mAllAdvertisers.remove(socket); 361 if (advertiser != null) { 362 advertiser.destroyNow(); 363 if (DBG) { mSharedLog.i("MdnsInterfaceAdvertiser is destroyed: " + advertiser); } 364 } 365 366 for (int i = mAdvertiserRequests.size() - 1; i >= 0; i--) { 367 advertiserRequest = mAdvertiserRequests.valueAt(i); 368 if (advertiserRequest.onAdvertiserDestroyed(socket, interfaceDestroyed)) { 369 if (DBG) { mSharedLog.i("AdvertiserRequest is removed: " + advertiserRequest); } 370 mAdvertiserRequests.removeAt(i); 371 } 372 } 373 } 374 375 /** 376 * A request for a {@link MdnsInterfaceAdvertiser}. 377 * 378 * This class tracks services to be advertised on all sockets provided via a registered 379 * {@link MdnsSocketProvider.SocketCallback}. 380 */ 381 private class InterfaceAdvertiserRequest implements MdnsSocketProvider.SocketCallback { 382 /** Registrations to add to newer MdnsInterfaceAdvertisers when sockets are created. */ 383 @NonNull 384 private final SparseArray<Registration> mPendingRegistrations = new SparseArray<>(); 385 @NonNull 386 private final ArrayMap<MdnsInterfaceSocket, MdnsInterfaceAdvertiser> mAdvertisers = 387 new ArrayMap<>(); 388 InterfaceAdvertiserRequest(@ullable Network requestedNetwork)389 InterfaceAdvertiserRequest(@Nullable Network requestedNetwork) { 390 mSocketProvider.requestSocket(requestedNetwork, this); 391 } 392 393 /** 394 * Called when the interface advertiser associated with {@code socket} has been destroyed. 395 * 396 * {@code interfaceDestroyed} should be set to {@code true} if this method is called because 397 * the associated interface has been destroyed. 398 * 399 * @return true if the {@link InterfaceAdvertiserRequest} should now be deleted 400 */ onAdvertiserDestroyed( @onNull MdnsInterfaceSocket socket, boolean interfaceDestroyed)401 boolean onAdvertiserDestroyed( 402 @NonNull MdnsInterfaceSocket socket, boolean interfaceDestroyed) { 403 final MdnsInterfaceAdvertiser removedAdvertiser = mAdvertisers.remove(socket); 404 if (removedAdvertiser != null 405 && !interfaceDestroyed && mPendingRegistrations.size() > 0) { 406 mSharedLog.wtf( 407 "unexpected onAdvertiserDestroyed() when there are pending registrations"); 408 } 409 410 if (mMdnsFeatureFlags.mIsMdnsOffloadFeatureEnabled && removedAdvertiser != null) { 411 final String interfaceName = removedAdvertiser.getSocketInterfaceName(); 412 // If the interface is destroyed, stop all hardware offloading on that 413 // interface. 414 final List<OffloadServiceInfoWrapper> offloadServiceInfoWrappers = 415 mInterfaceOffloadServices.remove(interfaceName); 416 if (offloadServiceInfoWrappers != null) { 417 for (OffloadServiceInfoWrapper offloadServiceInfoWrapper : 418 offloadServiceInfoWrappers) { 419 mCb.onOffloadStop(interfaceName, 420 offloadServiceInfoWrapper.mOffloadServiceInfo); 421 } 422 } 423 } 424 425 if (mAdvertisers.size() == 0 && mPendingRegistrations.size() == 0) { 426 // No advertiser is using sockets from this request anymore (in particular for exit 427 // announcements), and there is no registration so newer sockets will not be 428 // necessary, so the request can be unregistered. 429 mSocketProvider.unrequestSocket(this); 430 return true; 431 } 432 return false; 433 } 434 435 /** 436 * Return whether this {@link InterfaceAdvertiserRequest} has the given registration. 437 */ hasRegistration(@onNull Registration registration)438 boolean hasRegistration(@NonNull Registration registration) { 439 return mPendingRegistrations.indexOfValue(registration) >= 0; 440 } 441 442 /** 443 * Return whether using the proposed new {@link NsdServiceInfo} to add a registration would 444 * cause a conflict of the service in this {@link InterfaceAdvertiserRequest}. 445 */ hasServiceConflict( @onNull NsdServiceInfo newInfo, @NonNull Registration originalRegistration)446 boolean hasServiceConflict( 447 @NonNull NsdServiceInfo newInfo, @NonNull Registration originalRegistration) { 448 return getConflictingRegistrationDueToService(newInfo, originalRegistration) >= 0; 449 } 450 451 /** 452 * Return whether using the proposed new {@link NsdServiceInfo} to add a registration would 453 * cause a conflict of the host in this {@link InterfaceAdvertiserRequest}. 454 * 455 * @param clientUid UID of the user who wants to advertise the serviceInfo. 456 */ hasHostConflict(@onNull NsdServiceInfo newInfo, int clientUid)457 boolean hasHostConflict(@NonNull NsdServiceInfo newInfo, int clientUid) { 458 return getConflictingRegistrationDueToHost(newInfo, clientUid) >= 0; 459 } 460 461 /** Get the ID of a conflicting registration due to service, or -1 if none. */ getConflictingRegistrationDueToService( @onNull NsdServiceInfo info, @NonNull Registration originalRegistration)462 int getConflictingRegistrationDueToService( 463 @NonNull NsdServiceInfo info, @NonNull Registration originalRegistration) { 464 if (TextUtils.isEmpty(info.getServiceName())) { 465 return -1; 466 } 467 for (int i = 0; i < mPendingRegistrations.size(); i++) { 468 // Never conflict with itself 469 if (mPendingRegistrations.valueAt(i) == originalRegistration) { 470 continue; 471 } 472 final NsdServiceInfo other = mPendingRegistrations.valueAt(i).getServiceInfo(); 473 if (DnsUtils.equalsIgnoreDnsCase(info.getServiceName(), other.getServiceName()) 474 && DnsUtils.equalsIgnoreDnsCase(info.getServiceType(), 475 other.getServiceType())) { 476 return mPendingRegistrations.keyAt(i); 477 } 478 } 479 return -1; 480 } 481 482 /** 483 * Get the ID of a conflicting registration due to host, or -1 if none. 484 * 485 * <p>If there's already another registration with the same hostname requested by another 486 * UID, this is a conflict. 487 * 488 * <p>If there're two registrations both containing address records using the same hostname, 489 * this is a conflict. 490 */ getConflictingRegistrationDueToHost(@onNull NsdServiceInfo info, int clientUid)491 int getConflictingRegistrationDueToHost(@NonNull NsdServiceInfo info, int clientUid) { 492 if (TextUtils.isEmpty(info.getHostname())) { 493 return -1; 494 } 495 for (int i = 0; i < mPendingRegistrations.size(); i++) { 496 final Registration otherRegistration = mPendingRegistrations.valueAt(i); 497 final NsdServiceInfo otherInfo = otherRegistration.getServiceInfo(); 498 final int otherServiceId = mPendingRegistrations.keyAt(i); 499 if (clientUid != otherRegistration.mClientUid 500 && DnsUtils.equalsIgnoreDnsCase( 501 info.getHostname(), otherInfo.getHostname())) { 502 return otherServiceId; 503 } 504 if (!info.getHostAddresses().isEmpty() 505 && !otherInfo.getHostAddresses().isEmpty() 506 && DnsUtils.equalsIgnoreDnsCase( 507 info.getHostname(), otherInfo.getHostname())) { 508 return otherServiceId; 509 } 510 } 511 return -1; 512 } 513 514 /** 515 * Add a service to advertise. 516 * 517 * <p>Conflicts must be checked via {@link #getConflictingRegistrationDueToService} and 518 * {@link #getConflictingRegistrationDueToHost} before attempting to add. 519 */ addService(int id, @NonNull Registration registration)520 void addService(int id, @NonNull Registration registration) { 521 mPendingRegistrations.put(id, registration); 522 for (int i = 0; i < mAdvertisers.size(); i++) { 523 try { 524 mAdvertisers.valueAt(i).addService(id, registration.getServiceInfo(), 525 registration.getAdvertisingOptions()); 526 } catch (NameConflictException e) { 527 mSharedLog.wtf("Name conflict adding services that should have unique names", 528 e); 529 } 530 } 531 } 532 533 /** 534 * Update an already registered service. 535 * The caller is expected to check that the service being updated doesn't change its name 536 */ updateService(int id, @NonNull Registration registration)537 void updateService(int id, @NonNull Registration registration) { 538 mPendingRegistrations.put(id, registration); 539 for (int i = 0; i < mAdvertisers.size(); i++) { 540 mAdvertisers.valueAt(i).updateService( 541 id, registration.getServiceInfo().getSubtypes()); 542 } 543 } 544 removeService(int id)545 void removeService(int id) { 546 mPendingRegistrations.remove(id); 547 for (int i = 0; i < mAdvertisers.size(); i++) { 548 final MdnsInterfaceAdvertiser advertiser = mAdvertisers.valueAt(i); 549 advertiser.removeService(id); 550 551 if (mMdnsFeatureFlags.mIsMdnsOffloadFeatureEnabled) { 552 maybeSendOffloadStop(advertiser.getSocketInterfaceName(), id); 553 } 554 } 555 } 556 getServiceRepliedRequestsCount(int id)557 int getServiceRepliedRequestsCount(int id) { 558 int repliedRequestsCount = NO_PACKET; 559 for (int i = 0; i < mAdvertisers.size(); i++) { 560 repliedRequestsCount += mAdvertisers.valueAt(i).getServiceRepliedRequestsCount(id); 561 } 562 return repliedRequestsCount; 563 } 564 getSentPacketCount(int id)565 int getSentPacketCount(int id) { 566 int sentPacketCount = NO_PACKET; 567 for (int i = 0; i < mAdvertisers.size(); i++) { 568 sentPacketCount += mAdvertisers.valueAt(i).getSentPacketCount(id); 569 } 570 return sentPacketCount; 571 } 572 573 @Override onSocketCreated(@onNull SocketKey socketKey, @NonNull MdnsInterfaceSocket socket, @NonNull List<LinkAddress> addresses)574 public void onSocketCreated(@NonNull SocketKey socketKey, 575 @NonNull MdnsInterfaceSocket socket, 576 @NonNull List<LinkAddress> addresses) { 577 MdnsInterfaceAdvertiser advertiser = mAllAdvertisers.get(socket); 578 if (advertiser == null) { 579 advertiser = mDeps.makeAdvertiser(socket, addresses, mLooper, mPacketCreationBuffer, 580 mInterfaceAdvertiserCb, mDeviceHostName, 581 mSharedLog.forSubComponent(socket.getInterface().getName()), 582 mMdnsFeatureFlags); 583 mAllAdvertisers.put(socket, advertiser); 584 advertiser.start(); 585 } 586 mAdvertisers.put(socket, advertiser); 587 for (int i = 0; i < mPendingRegistrations.size(); i++) { 588 final Registration registration = mPendingRegistrations.valueAt(i); 589 try { 590 advertiser.addService(mPendingRegistrations.keyAt(i), 591 registration.getServiceInfo(), registration.getAdvertisingOptions()); 592 } catch (NameConflictException e) { 593 mSharedLog.wtf("Name conflict adding services that should have unique names", 594 e); 595 } 596 } 597 } 598 599 @Override onInterfaceDestroyed(@onNull SocketKey socketKey, @NonNull MdnsInterfaceSocket socket)600 public void onInterfaceDestroyed(@NonNull SocketKey socketKey, 601 @NonNull MdnsInterfaceSocket socket) { 602 final MdnsInterfaceAdvertiser advertiser = mAdvertisers.get(socket); 603 if (advertiser != null) destroyAdvertiser(socket, true /* interfaceDestroyed */); 604 } 605 606 @Override onAddressesChanged(@onNull SocketKey socketKey, @NonNull MdnsInterfaceSocket socket, @NonNull List<LinkAddress> addresses)607 public void onAddressesChanged(@NonNull SocketKey socketKey, 608 @NonNull MdnsInterfaceSocket socket, @NonNull List<LinkAddress> addresses) { 609 final MdnsInterfaceAdvertiser advertiser = mAdvertisers.get(socket); 610 if (advertiser == null) { 611 return; 612 } 613 advertiser.updateAddresses(addresses); 614 615 if (mMdnsFeatureFlags.mIsMdnsOffloadFeatureEnabled) { 616 // Update address should trigger offload packet update. 617 final String interfaceName = advertiser.getSocketInterfaceName(); 618 final List<OffloadServiceInfoWrapper> existingOffloadServiceInfoWrappers = 619 mInterfaceOffloadServices.get(interfaceName); 620 if (existingOffloadServiceInfoWrappers == null) { 621 return; 622 } 623 final List<OffloadServiceInfoWrapper> updatedOffloadServiceInfoWrappers = 624 new ArrayList<>(existingOffloadServiceInfoWrappers.size()); 625 for (OffloadServiceInfoWrapper oldWrapper : existingOffloadServiceInfoWrappers) { 626 OffloadServiceInfoWrapper newWrapper = new OffloadServiceInfoWrapper( 627 oldWrapper.mServiceId, 628 oldWrapper.mOffloadServiceInfo.withOffloadPayload( 629 advertiser.getRawOffloadPayload(oldWrapper.mServiceId)) 630 ); 631 updatedOffloadServiceInfoWrappers.add(newWrapper); 632 mCb.onOffloadStartOrUpdate(interfaceName, newWrapper.mOffloadServiceInfo); 633 } 634 mInterfaceOffloadServices.put(interfaceName, updatedOffloadServiceInfoWrappers); 635 } 636 } 637 } 638 639 /** 640 * The wrapper class for OffloadServiceInfo including the serviceId. 641 */ 642 public static class OffloadServiceInfoWrapper { 643 public final @NonNull OffloadServiceInfo mOffloadServiceInfo; 644 public final int mServiceId; 645 OffloadServiceInfoWrapper(int serviceId, OffloadServiceInfo offloadServiceInfo)646 OffloadServiceInfoWrapper(int serviceId, OffloadServiceInfo offloadServiceInfo) { 647 mOffloadServiceInfo = offloadServiceInfo; 648 mServiceId = serviceId; 649 } 650 } 651 652 private static class Registration { 653 @Nullable 654 final String mOriginalServiceName; 655 @Nullable 656 final String mOriginalHostname; 657 boolean mNotifiedRegistrationSuccess; 658 private int mServiceNameConflictCount; 659 private int mHostnameConflictCount; 660 @NonNull 661 private NsdServiceInfo mServiceInfo; 662 final int mClientUid; 663 private final MdnsAdvertisingOptions mAdvertisingOptions; 664 int mConflictDuringProbingCount; 665 int mConflictAfterProbingCount; 666 Registration(@onNull NsdServiceInfo serviceInfo, int clientUid, @NonNull MdnsAdvertisingOptions advertisingOptions)667 private Registration(@NonNull NsdServiceInfo serviceInfo, int clientUid, 668 @NonNull MdnsAdvertisingOptions advertisingOptions) { 669 this.mOriginalServiceName = serviceInfo.getServiceName(); 670 this.mOriginalHostname = serviceInfo.getHostname(); 671 this.mServiceInfo = serviceInfo; 672 this.mClientUid = clientUid; 673 this.mAdvertisingOptions = advertisingOptions; 674 } 675 676 /** Check if the new {@link NsdServiceInfo} doesn't update any data other than subtypes. */ isSubtypeOnlyUpdate(@onNull NsdServiceInfo newInfo)677 public boolean isSubtypeOnlyUpdate(@NonNull NsdServiceInfo newInfo) { 678 return Objects.equals(newInfo.getServiceName(), mOriginalServiceName) 679 && Objects.equals(newInfo.getServiceType(), mServiceInfo.getServiceType()) 680 && newInfo.getPort() == mServiceInfo.getPort() 681 && Objects.equals(newInfo.getHostname(), mOriginalHostname) 682 && Objects.equals(newInfo.getHostAddresses(), mServiceInfo.getHostAddresses()) 683 && Objects.equals(newInfo.getNetwork(), mServiceInfo.getNetwork()); 684 } 685 686 /** 687 * Update subTypes for the registration. 688 */ updateSubtypes(@onNull Set<String> subtypes)689 public void updateSubtypes(@NonNull Set<String> subtypes) { 690 mServiceInfo = new NsdServiceInfo(mServiceInfo); 691 mServiceInfo.setSubtypes(subtypes); 692 } 693 694 /** 695 * Update the registration to use a different service name, after a conflict was found. 696 * 697 * @param newInfo New service info to use. 698 * @param renameCount How many renames were done before reaching the current name. 699 */ updateForServiceConflict(@onNull NsdServiceInfo newInfo, int renameCount)700 private void updateForServiceConflict(@NonNull NsdServiceInfo newInfo, int renameCount) { 701 mServiceNameConflictCount += renameCount; 702 mServiceInfo = newInfo; 703 } 704 705 /** 706 * Update the registration to use a different host name, after a conflict was found. 707 * 708 * @param newInfo New service info to use. 709 * @param renameCount How many renames were done before reaching the current name. 710 */ updateForHostConflict(@onNull NsdServiceInfo newInfo, int renameCount)711 private void updateForHostConflict(@NonNull NsdServiceInfo newInfo, int renameCount) { 712 mHostnameConflictCount += renameCount; 713 mServiceInfo = newInfo; 714 } 715 716 /** 717 * Make a new service name for the registration, after a conflict was found. 718 * 719 * If a name conflict was found during probing or because different advertising requests 720 * used the same name, the registration is attempted again with a new name (here using 721 * a number suffix, (1), (2) etc). Registration success is notified once probing succeeds 722 * with a new name. This matches legacy behavior based on mdnsresponder, and appendix D of 723 * RFC6763. 724 * 725 * @param renameCount How much to increase the number suffix for this conflict. 726 */ 727 @NonNull makeNewServiceInfoForServiceConflict(int renameCount)728 public NsdServiceInfo makeNewServiceInfoForServiceConflict(int renameCount) { 729 // In case of conflict choose a different service name. After the first conflict use 730 // "Name (2)", then "Name (3)" etc. 731 // TODO: use a hidden method in NsdServiceInfo once MdnsAdvertiser is moved to service-t 732 final NsdServiceInfo newInfo = new NsdServiceInfo(mServiceInfo); 733 newInfo.setServiceName(getUpdatedServiceName(renameCount)); 734 return newInfo; 735 } 736 737 /** 738 * Make a new hostname for the registration, after a conflict was found. 739 * 740 * <p>If a name conflict was found during probing or because different advertising requests 741 * used the same name, the registration is attempted again with a new name (here using a 742 * number suffix, -1, -2, etc). Registration success is notified once probing succeeds with 743 * a new name. 744 * 745 * @param renameCount How much to increase the number suffix for this conflict. 746 */ 747 @NonNull makeNewServiceInfoForHostConflict(int renameCount)748 public NsdServiceInfo makeNewServiceInfoForHostConflict(int renameCount) { 749 // In case of conflict choose a different hostname. After the first conflict use 750 // "Name-2", then "Name-3" etc. 751 final NsdServiceInfo newInfo = new NsdServiceInfo(mServiceInfo); 752 newInfo.setHostname(getUpdatedHostname(renameCount)); 753 return newInfo; 754 } 755 getUpdatedServiceName(int renameCount)756 private String getUpdatedServiceName(int renameCount) { 757 final String suffix = " (" + (mServiceNameConflictCount + renameCount + 1) + ")"; 758 final String truncatedServiceName = MdnsUtils.truncateServiceName(mOriginalServiceName, 759 MAX_LABEL_LENGTH - suffix.length()); 760 return truncatedServiceName + suffix; 761 } 762 getUpdatedHostname(int renameCount)763 private String getUpdatedHostname(int renameCount) { 764 final String suffix = "-" + (mHostnameConflictCount + renameCount + 1); 765 final String truncatedHostname = 766 MdnsUtils.truncateServiceName( 767 mOriginalHostname, MAX_LABEL_LENGTH - suffix.length()); 768 return truncatedHostname + suffix; 769 } 770 771 @NonNull getServiceInfo()772 public NsdServiceInfo getServiceInfo() { 773 return mServiceInfo; 774 } 775 776 @NonNull getAdvertisingOptions()777 public MdnsAdvertisingOptions getAdvertisingOptions() { 778 return mAdvertisingOptions; 779 } 780 } 781 782 /** 783 * Callbacks for advertising services. 784 * 785 * Every method is called on the MdnsAdvertiser looper thread. 786 */ 787 public interface AdvertiserCallback { 788 /** 789 * Called when a service was successfully registered, after probing. 790 * 791 * @param serviceId ID of the service provided when registering. 792 * @param registeredInfo Registered info, which may be different from the requested info, 793 * after probing and possibly choosing alternative service names. 794 */ onRegisterServiceSucceeded(int serviceId, NsdServiceInfo registeredInfo)795 void onRegisterServiceSucceeded(int serviceId, NsdServiceInfo registeredInfo); 796 797 /** 798 * Called when service registration failed. 799 * 800 * @param serviceId ID of the service provided when registering. 801 * @param errorCode One of {@code NsdManager.FAILURE_*} 802 */ onRegisterServiceFailed(int serviceId, int errorCode)803 void onRegisterServiceFailed(int serviceId, int errorCode); 804 805 // Unregistration is notified immediately as success in NsdService so no callback is needed 806 // here. 807 808 /** 809 * Called when a service is ready to be sent for hardware offloading. 810 * 811 * @param interfaceName the interface for sending the update to. 812 * @param offloadServiceInfo the offloading content. 813 */ onOffloadStartOrUpdate(@onNull String interfaceName, @NonNull OffloadServiceInfo offloadServiceInfo)814 void onOffloadStartOrUpdate(@NonNull String interfaceName, 815 @NonNull OffloadServiceInfo offloadServiceInfo); 816 817 /** 818 * Called when a service is removed or the MdnsInterfaceAdvertiser is destroyed. 819 * 820 * @param interfaceName the interface for sending the update to. 821 * @param offloadServiceInfo the offloading content. 822 */ onOffloadStop(@onNull String interfaceName, @NonNull OffloadServiceInfo offloadServiceInfo)823 void onOffloadStop(@NonNull String interfaceName, 824 @NonNull OffloadServiceInfo offloadServiceInfo); 825 } 826 827 /** 828 * Data class of avdverting metrics. 829 */ 830 public static class AdvertiserMetrics { 831 public final int mRepliedRequestsCount; 832 public final int mSentPacketCount; 833 public final int mConflictDuringProbingCount; 834 public final int mConflictAfterProbingCount; 835 AdvertiserMetrics(int repliedRequestsCount, int sentPacketCount, int conflictDuringProbingCount, int conflictAfterProbingCount)836 public AdvertiserMetrics(int repliedRequestsCount, int sentPacketCount, 837 int conflictDuringProbingCount, int conflictAfterProbingCount) { 838 mRepliedRequestsCount = repliedRequestsCount; 839 mSentPacketCount = sentPacketCount; 840 mConflictDuringProbingCount = conflictDuringProbingCount; 841 mConflictAfterProbingCount = conflictAfterProbingCount; 842 } 843 } 844 MdnsAdvertiser(@onNull Looper looper, @NonNull MdnsSocketProvider socketProvider, @NonNull AdvertiserCallback cb, @NonNull SharedLog sharedLog, @NonNull MdnsFeatureFlags mDnsFeatureFlags, @NonNull Context context)845 public MdnsAdvertiser(@NonNull Looper looper, @NonNull MdnsSocketProvider socketProvider, 846 @NonNull AdvertiserCallback cb, @NonNull SharedLog sharedLog, 847 @NonNull MdnsFeatureFlags mDnsFeatureFlags, @NonNull Context context) { 848 this(looper, socketProvider, cb, new Dependencies(), sharedLog, mDnsFeatureFlags, 849 context); 850 } 851 852 @VisibleForTesting MdnsAdvertiser(@onNull Looper looper, @NonNull MdnsSocketProvider socketProvider, @NonNull AdvertiserCallback cb, @NonNull Dependencies deps, @NonNull SharedLog sharedLog, @NonNull MdnsFeatureFlags mDnsFeatureFlags, @NonNull Context context)853 MdnsAdvertiser(@NonNull Looper looper, @NonNull MdnsSocketProvider socketProvider, 854 @NonNull AdvertiserCallback cb, @NonNull Dependencies deps, 855 @NonNull SharedLog sharedLog, @NonNull MdnsFeatureFlags mDnsFeatureFlags, 856 @NonNull Context context) { 857 mLooper = looper; 858 mCb = cb; 859 mSocketProvider = socketProvider; 860 mDeps = deps; 861 mDeviceHostName = deps.generateHostname(mDnsFeatureFlags.isShortHostnamesEnabled()); 862 mSharedLog = sharedLog; 863 mMdnsFeatureFlags = mDnsFeatureFlags; 864 final ConnectivityResources res = new ConnectivityResources(context); 865 mServiceTypeToOffloadPriority = parseOffloadPriorityList( 866 res.get().getStringArray(R.array.config_nsdOffloadServicesPriority), sharedLog); 867 mOffloadServiceTypeDenyList = new ArraySet<>( 868 res.get().getStringArray(R.array.config_nsdOffloadServicesDenyList)); 869 } 870 parseOffloadPriorityList( @onNull String[] resValues, SharedLog sharedLog)871 private static Map<String, Integer> parseOffloadPriorityList( 872 @NonNull String[] resValues, SharedLog sharedLog) { 873 final Map<String, Integer> priorities = new ArrayMap<>(resValues.length); 874 for (String entry : resValues) { 875 final String[] priorityAndType = entry.split(":", 2); 876 if (priorityAndType.length != 2) { 877 sharedLog.wtf("Invalid config_nsdOffloadServicesPriority ignored: " + entry); 878 continue; 879 } 880 881 final int priority; 882 try { 883 priority = Integer.parseInt(priorityAndType[0]); 884 } catch (NumberFormatException e) { 885 sharedLog.wtf("Invalid priority in config_nsdOffloadServicesPriority: " + entry); 886 continue; 887 } 888 priorities.put(DnsUtils.toDnsUpperCase(priorityAndType[1]), priority); 889 } 890 return priorities; 891 } 892 checkThread()893 private void checkThread() { 894 if (Thread.currentThread() != mLooper.getThread()) { 895 throw new IllegalStateException("This must be called on the looper thread"); 896 } 897 } 898 899 /** 900 * Add or update a service to advertise. 901 * 902 * @param id A unique ID for the service. 903 * @param service The service info to advertise. 904 * @param advertisingOptions The advertising options. 905 * @param clientUid The UID who wants to advertise the service. 906 */ addOrUpdateService(int id, NsdServiceInfo service, MdnsAdvertisingOptions advertisingOptions, int clientUid)907 public void addOrUpdateService(int id, NsdServiceInfo service, 908 MdnsAdvertisingOptions advertisingOptions, int clientUid) { 909 checkThread(); 910 final Registration existingRegistration = mRegistrations.get(id); 911 final Network network = service.getNetwork(); 912 final Set<String> subtypes = service.getSubtypes(); 913 Registration registration; 914 if (advertisingOptions.isOnlyUpdate()) { 915 if (existingRegistration == null) { 916 mSharedLog.e("Update non existing registration for " + service); 917 mCb.onRegisterServiceFailed(id, NsdManager.FAILURE_INTERNAL_ERROR); 918 return; 919 } 920 if (!(existingRegistration.isSubtypeOnlyUpdate(service))) { 921 mSharedLog.e("Update request can only update subType, serviceInfo: " + service 922 + ", existing serviceInfo: " + existingRegistration.getServiceInfo()); 923 mCb.onRegisterServiceFailed(id, NsdManager.FAILURE_INTERNAL_ERROR); 924 return; 925 926 } 927 mSharedLog.i("Update service " + service + " with ID " + id + " and subtypes " 928 + subtypes + " advertisingOptions " + advertisingOptions); 929 registration = existingRegistration; 930 registration.updateSubtypes(subtypes); 931 } else { 932 if (existingRegistration != null) { 933 mSharedLog.e("Adding duplicate registration for " + service); 934 // TODO (b/264986328): add a more specific error code 935 mCb.onRegisterServiceFailed(id, NsdManager.FAILURE_INTERNAL_ERROR); 936 return; 937 } 938 mSharedLog.i("Adding service " + service + " with ID " + id + " and subtypes " 939 + subtypes + " advertisingOptions " + advertisingOptions); 940 registration = new Registration(service, clientUid, advertisingOptions); 941 final BiPredicate<Network, InterfaceAdvertiserRequest> checkConflictFilter; 942 if (network == null) { 943 // If registering on all networks, no advertiser must have conflicts 944 checkConflictFilter = (net, adv) -> true; 945 } else { 946 // If registering on one network, the matching network advertiser and the one 947 // for all networks must not have conflicts 948 checkConflictFilter = (net, adv) -> net == null || network.equals(net); 949 } 950 updateRegistrationUntilNoConflict(checkConflictFilter, registration); 951 } 952 953 InterfaceAdvertiserRequest advertiser = mAdvertiserRequests.get(network); 954 if (advertiser == null) { 955 advertiser = new InterfaceAdvertiserRequest(network); 956 mAdvertiserRequests.put(network, advertiser); 957 } 958 if (advertisingOptions.isOnlyUpdate()) { 959 advertiser.updateService(id, registration); 960 } else { 961 advertiser.addService(id, registration); 962 } 963 mRegistrations.put(id, registration); 964 } 965 966 /** 967 * Remove a previously added service. 968 * @param id ID used when registering. 969 */ removeService(int id)970 public void removeService(int id) { 971 checkThread(); 972 if (!mRegistrations.contains(id)) return; 973 mSharedLog.i("Removing service with ID " + id); 974 for (int i = mAdvertiserRequests.size() - 1; i >= 0; i--) { 975 final InterfaceAdvertiserRequest advertiser = mAdvertiserRequests.valueAt(i); 976 advertiser.removeService(id); 977 } 978 mRegistrations.remove(id); 979 // Regenerates host name when registrations removed. 980 if (mRegistrations.size() == 0) { 981 mDeviceHostName = mDeps.generateHostname(mMdnsFeatureFlags.isShortHostnamesEnabled()); 982 } 983 } 984 985 /** 986 * Get advertising metrics. 987 * 988 * @param id ID used when registering. 989 * @return The advertising metrics includes replied requests count, send packet count, conflict 990 * count during/after probing. 991 */ getAdvertiserMetrics(int id)992 public AdvertiserMetrics getAdvertiserMetrics(int id) { 993 checkThread(); 994 final Registration registration = mRegistrations.get(id); 995 if (registration == null) { 996 return new AdvertiserMetrics( 997 NO_PACKET /* repliedRequestsCount */, 998 NO_PACKET /* sentPacketCount */, 999 0 /* conflictDuringProbingCount */, 1000 0 /* conflictAfterProbingCount */); 1001 } 1002 int repliedRequestsCount = NO_PACKET; 1003 int sentPacketCount = NO_PACKET; 1004 for (int i = 0; i < mAdvertiserRequests.size(); i++) { 1005 repliedRequestsCount += 1006 mAdvertiserRequests.valueAt(i).getServiceRepliedRequestsCount(id); 1007 sentPacketCount += mAdvertiserRequests.valueAt(i).getSentPacketCount(id); 1008 } 1009 return new AdvertiserMetrics(repliedRequestsCount, sentPacketCount, 1010 registration.mConflictDuringProbingCount, registration.mConflictAfterProbingCount); 1011 } 1012 any(@onNull ArrayMap<K, V> map, @NonNull BiPredicate<K, V> predicate)1013 private static <K, V> boolean any(@NonNull ArrayMap<K, V> map, 1014 @NonNull BiPredicate<K, V> predicate) { 1015 for (int i = 0; i < map.size(); i++) { 1016 if (predicate.test(map.keyAt(i), map.valueAt(i))) { 1017 return true; 1018 } 1019 } 1020 return false; 1021 } 1022 forAllAdvertisers(@onNull Consumer<MdnsInterfaceAdvertiser> consumer)1023 private void forAllAdvertisers(@NonNull Consumer<MdnsInterfaceAdvertiser> consumer) { 1024 any(mAllAdvertisers, (socket, advertiser) -> { 1025 consumer.accept(advertiser); 1026 return false; 1027 }); 1028 } 1029 getOffloadSubtype(@onNull NsdServiceInfo nsdServiceInfo)1030 private List<String> getOffloadSubtype(@NonNull NsdServiceInfo nsdServiceInfo) { 1031 // Workaround: Google Cast doesn't announce subtypes per DNS-SD/mDNS spec. 1032 // Thus, subtypes aren't offloaded; only "_googlecast._tcp" is. 1033 // Subtype responses occur when hardware offload is off. 1034 // This solution works because Google Cast doesn't follow the intended usage of subtypes in 1035 // the spec, as it always discovers for both the subtype+base type, and only uses the mDNS 1036 // subtype as an optimization. 1037 if (nsdServiceInfo.getServiceType().equals("_googlecast._tcp")) { 1038 return new ArrayList<>(); 1039 } 1040 return new ArrayList<>(nsdServiceInfo.getSubtypes()); 1041 } 1042 createOffloadService(int serviceId, @NonNull Registration registration, byte[] rawOffloadPacket)1043 private OffloadServiceInfoWrapper createOffloadService(int serviceId, 1044 @NonNull Registration registration, byte[] rawOffloadPacket) { 1045 final NsdServiceInfo nsdServiceInfo = registration.getServiceInfo(); 1046 final Integer mapPriority = mServiceTypeToOffloadPriority.get( 1047 DnsUtils.toDnsUpperCase(nsdServiceInfo.getServiceType())); 1048 // Higher values of priority are less prioritized 1049 final int priority = mapPriority == null ? Integer.MAX_VALUE : mapPriority; 1050 final OffloadServiceInfo offloadServiceInfo = new OffloadServiceInfo( 1051 new OffloadServiceInfo.Key(nsdServiceInfo.getServiceName(), 1052 nsdServiceInfo.getServiceType()), 1053 getOffloadSubtype(nsdServiceInfo), 1054 String.join(".", mDeviceHostName), 1055 rawOffloadPacket, 1056 priority, 1057 // TODO: set the offloadType based on the callback timing. 1058 OffloadEngine.OFFLOAD_TYPE_REPLY); 1059 return new OffloadServiceInfoWrapper(serviceId, offloadServiceInfo); 1060 } 1061 } 1062