1 /* 2 * Copyright (C) 2021 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.net.module.util.HandlerUtils.ensureRunningOnHandlerThread; 20 import static com.android.server.connectivity.mdns.MdnsSearchOptions.AGGRESSIVE_QUERY_MODE; 21 import static com.android.server.connectivity.mdns.MdnsServiceCache.ServiceExpiredCallback; 22 import static com.android.server.connectivity.mdns.MdnsServiceCache.findMatchedResponse; 23 import static com.android.server.connectivity.mdns.MdnsQueryScheduler.ScheduledQueryTaskArgs; 24 import static com.android.server.connectivity.mdns.util.MdnsUtils.Clock; 25 import static com.android.server.connectivity.mdns.util.MdnsUtils.buildMdnsServiceInfoFromResponse; 26 27 import android.annotation.NonNull; 28 import android.annotation.Nullable; 29 import android.os.Handler; 30 import android.os.Looper; 31 import android.os.Message; 32 import android.text.TextUtils; 33 import android.util.ArrayMap; 34 import android.util.Pair; 35 36 import androidx.annotation.VisibleForTesting; 37 38 import com.android.net.module.util.CollectionUtils; 39 import com.android.net.module.util.DnsUtils; 40 import com.android.net.module.util.SharedLog; 41 import com.android.server.connectivity.mdns.util.MdnsUtils; 42 43 import java.io.IOException; 44 import java.io.PrintWriter; 45 import java.net.DatagramPacket; 46 import java.net.InetSocketAddress; 47 import java.util.ArrayList; 48 import java.util.Arrays; 49 import java.util.Collection; 50 import java.util.Collections; 51 import java.util.Iterator; 52 import java.util.List; 53 import java.util.Set; 54 import java.util.concurrent.ScheduledExecutorService; 55 56 /** 57 * Instance of this class sends and receives mDNS packets of a given service type and invoke 58 * registered {@link MdnsServiceBrowserListener} instances. 59 */ 60 public class MdnsServiceTypeClient { 61 62 private static final String TAG = MdnsServiceTypeClient.class.getSimpleName(); 63 private static final boolean DBG = MdnsDiscoveryManager.DBG; 64 @VisibleForTesting 65 static final int EVENT_START_QUERYTASK = 1; 66 static final int EVENT_QUERY_RESULT = 2; 67 static final int INVALID_TRANSACTION_ID = -1; 68 69 private final String serviceType; 70 private final String[] serviceTypeLabels; 71 private final MdnsSocketClientBase socketClient; 72 private final MdnsResponseDecoder responseDecoder; 73 private final ScheduledExecutorService executor; 74 @NonNull private final SocketKey socketKey; 75 @NonNull private final SharedLog sharedLog; 76 @NonNull private final Handler handler; 77 @NonNull private final MdnsQueryScheduler mdnsQueryScheduler; 78 @NonNull private final Dependencies dependencies; 79 /** 80 * The service caches for each socket. It should be accessed from looper thread only. 81 */ 82 @NonNull private final MdnsServiceCache serviceCache; 83 @NonNull private final MdnsServiceCache.CacheKey cacheKey; 84 @NonNull private final ServiceExpiredCallback serviceExpiredCallback = 85 new ServiceExpiredCallback() { 86 @Override 87 public void onServiceRecordExpired(@NonNull MdnsResponse previousResponse, 88 @Nullable MdnsResponse newResponse) { 89 notifyRemovedServiceToListeners(previousResponse, "Service record expired"); 90 } 91 }; 92 @NonNull private final MdnsFeatureFlags featureFlags; 93 private final ArrayMap<MdnsServiceBrowserListener, ListenerInfo> listeners = 94 new ArrayMap<>(); 95 private final boolean removeServiceAfterTtlExpires = 96 MdnsConfigs.removeServiceAfterTtlExpires(); 97 private final Clock clock; 98 // Use MdnsRealtimeScheduler for query scheduling, which allows for more accurate sending of 99 // queries. 100 @Nullable private final Scheduler scheduler; 101 102 @Nullable private MdnsSearchOptions searchOptions; 103 104 // The session ID increases when startSendAndReceive() is called where we schedule a 105 // QueryTask for 106 // new subtypes. It stays the same between packets for same subtypes. 107 private long currentSessionId = 0; 108 private long lastSentTime; 109 110 private static class ListenerInfo { 111 @NonNull 112 final MdnsSearchOptions searchOptions; 113 final Set<String> discoveredServiceNames; 114 ListenerInfo(@onNull MdnsSearchOptions searchOptions, @Nullable ListenerInfo previousInfo)115 ListenerInfo(@NonNull MdnsSearchOptions searchOptions, 116 @Nullable ListenerInfo previousInfo) { 117 this.searchOptions = searchOptions; 118 this.discoveredServiceNames = previousInfo == null 119 ? MdnsUtils.newSet() : previousInfo.discoveredServiceNames; 120 } 121 122 /** 123 * Set the given service name as discovered. 124 * 125 * @return true if the service name was not discovered before. 126 */ setServiceDiscovered(@onNull String serviceName)127 boolean setServiceDiscovered(@NonNull String serviceName) { 128 return discoveredServiceNames.add(DnsUtils.toDnsUpperCase(serviceName)); 129 } 130 unsetServiceDiscovered(@onNull String serviceName)131 void unsetServiceDiscovered(@NonNull String serviceName) { 132 discoveredServiceNames.remove(DnsUtils.toDnsUpperCase(serviceName)); 133 } 134 } 135 136 private class QueryTaskHandler extends Handler { QueryTaskHandler(Looper looper)137 QueryTaskHandler(Looper looper) { 138 super(looper); 139 } 140 141 @Override 142 @SuppressWarnings("FutureReturnValueIgnored") handleMessage(Message msg)143 public void handleMessage(Message msg) { 144 switch (msg.what) { 145 case EVENT_START_QUERYTASK: { 146 final ScheduledQueryTaskArgs taskArgs = (ScheduledQueryTaskArgs) msg.obj; 147 // QueryTask should be run immediately after being created (not be scheduled in 148 // advance). Because the result of "makeResponsesForResolve" depends on answers 149 // that were received before it is called, so to take into account all answers 150 // before sending the query, it needs to be called just before sending it. 151 final List<MdnsResponse> servicesToResolve = makeResponsesForResolve(socketKey); 152 final QueryTask queryTask = new QueryTask(taskArgs, servicesToResolve, 153 getAllDiscoverySubtypes(), needSendDiscoveryQueries(listeners), 154 getExistingServices(), searchOptions.onlyUseIpv6OnIpv6OnlyNetworks(), 155 socketKey); 156 executor.submit(queryTask); 157 break; 158 } 159 case EVENT_QUERY_RESULT: { 160 final QuerySentArguments sentResult = (QuerySentArguments) msg.obj; 161 // If a task is cancelled while the Executor is running it, EVENT_QUERY_RESULT 162 // will still be sent when it ends. So use session ID to check if this task 163 // should continue to schedule more. 164 if (sentResult.taskArgs.sessionId != currentSessionId) { 165 break; 166 } 167 168 if ((sentResult.transactionId != INVALID_TRANSACTION_ID)) { 169 for (int i = 0; i < listeners.size(); i++) { 170 listeners.keyAt(i).onDiscoveryQuerySent( 171 sentResult.subTypes, sentResult.transactionId); 172 } 173 } 174 175 tryRemoveServiceAfterTtlExpires(); 176 177 final long now = clock.elapsedRealtime(); 178 lastSentTime = now; 179 final long minRemainingTtl = getMinRemainingTtl(now); 180 final ScheduledQueryTaskArgs args = 181 mdnsQueryScheduler.scheduleNextRun( 182 sentResult.taskArgs.config, 183 minRemainingTtl, 184 now, 185 lastSentTime, 186 sentResult.taskArgs.sessionId, 187 searchOptions.getQueryMode(), 188 searchOptions.numOfQueriesBeforeBackoff(), 189 false /* forceEnableBackoff */ 190 ); 191 final long timeToNextTaskMs = calculateTimeToNextTask(args, now); 192 sharedLog.log(String.format("Query sent with transactionId: %d. " 193 + "Next run: sessionId: %d, in %d ms", 194 sentResult.transactionId, args.sessionId, timeToNextTaskMs)); 195 if (scheduler != null) { 196 setDelayedTask(args, timeToNextTaskMs); 197 } else { 198 dependencies.sendMessageDelayed( 199 handler, 200 handler.obtainMessage(EVENT_START_QUERYTASK, args), 201 timeToNextTaskMs); 202 } 203 break; 204 } 205 default: 206 sharedLog.e("Unrecognized event " + msg.what); 207 break; 208 } 209 } 210 } 211 212 /** 213 * Dependencies of MdnsServiceTypeClient, for injection in tests. 214 */ 215 @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) 216 public static class Dependencies { 217 /** 218 * @see Handler#sendMessageDelayed(Message, long) 219 */ sendMessageDelayed(@onNull Handler handler, @NonNull Message message, long delayMillis)220 public void sendMessageDelayed(@NonNull Handler handler, @NonNull Message message, 221 long delayMillis) { 222 handler.sendMessageDelayed(message, delayMillis); 223 } 224 225 /** 226 * @see Handler#removeMessages(int) 227 */ removeMessages(@onNull Handler handler, int what)228 public void removeMessages(@NonNull Handler handler, int what) { 229 handler.removeMessages(what); 230 } 231 232 /** 233 * @see Handler#hasMessages(int) 234 */ hasMessages(@onNull Handler handler, int what)235 public boolean hasMessages(@NonNull Handler handler, int what) { 236 return handler.hasMessages(what); 237 } 238 239 /** 240 * @see Handler#post(Runnable) 241 */ sendMessage(@onNull Handler handler, @NonNull Message message)242 public void sendMessage(@NonNull Handler handler, @NonNull Message message) { 243 handler.sendMessage(message); 244 } 245 246 /** 247 * Generate the DatagramPackets from given MdnsPacket and InetSocketAddress. 248 * 249 * <p> If the query with known answer feature is enabled and the MdnsPacket is too large for 250 * a single DatagramPacket, it will be split into multiple DatagramPackets. 251 */ getDatagramPacketsFromMdnsPacket( @onNull byte[] packetCreationBuffer, @NonNull MdnsPacket packet, @NonNull InetSocketAddress address, boolean isQueryWithKnownAnswer)252 public List<DatagramPacket> getDatagramPacketsFromMdnsPacket( 253 @NonNull byte[] packetCreationBuffer, @NonNull MdnsPacket packet, 254 @NonNull InetSocketAddress address, boolean isQueryWithKnownAnswer) 255 throws IOException { 256 if (isQueryWithKnownAnswer) { 257 return MdnsUtils.createQueryDatagramPackets(packetCreationBuffer, packet, address); 258 } else { 259 final byte[] queryBuffer = 260 MdnsUtils.createRawDnsPacket(packetCreationBuffer, packet); 261 return List.of(new DatagramPacket(queryBuffer, 0, queryBuffer.length, address)); 262 } 263 } 264 265 /** 266 * @see Scheduler 267 */ 268 @Nullable createScheduler(@onNull Handler handler)269 public Scheduler createScheduler(@NonNull Handler handler) { 270 return SchedulerFactory.createScheduler(handler); 271 } 272 } 273 274 /** 275 * Constructor of {@link MdnsServiceTypeClient}. 276 * 277 * @param socketClient Sends and receives mDNS packet. 278 * @param executor A {@link ScheduledExecutorService} used to schedule query tasks. 279 */ MdnsServiceTypeClient( @onNull String serviceType, @NonNull MdnsSocketClientBase socketClient, @NonNull ScheduledExecutorService executor, @NonNull SocketKey socketKey, @NonNull SharedLog sharedLog, @NonNull Looper looper, @NonNull MdnsServiceCache serviceCache, @NonNull MdnsFeatureFlags featureFlags)280 public MdnsServiceTypeClient( 281 @NonNull String serviceType, 282 @NonNull MdnsSocketClientBase socketClient, 283 @NonNull ScheduledExecutorService executor, 284 @NonNull SocketKey socketKey, 285 @NonNull SharedLog sharedLog, 286 @NonNull Looper looper, 287 @NonNull MdnsServiceCache serviceCache, 288 @NonNull MdnsFeatureFlags featureFlags) { 289 this(serviceType, socketClient, executor, new Clock(), socketKey, sharedLog, looper, 290 new Dependencies(), serviceCache, featureFlags); 291 } 292 293 @VisibleForTesting MdnsServiceTypeClient( @onNull String serviceType, @NonNull MdnsSocketClientBase socketClient, @NonNull ScheduledExecutorService executor, @NonNull Clock clock, @NonNull SocketKey socketKey, @NonNull SharedLog sharedLog, @NonNull Looper looper, @NonNull Dependencies dependencies, @NonNull MdnsServiceCache serviceCache, @NonNull MdnsFeatureFlags featureFlags)294 public MdnsServiceTypeClient( 295 @NonNull String serviceType, 296 @NonNull MdnsSocketClientBase socketClient, 297 @NonNull ScheduledExecutorService executor, 298 @NonNull Clock clock, 299 @NonNull SocketKey socketKey, 300 @NonNull SharedLog sharedLog, 301 @NonNull Looper looper, 302 @NonNull Dependencies dependencies, 303 @NonNull MdnsServiceCache serviceCache, 304 @NonNull MdnsFeatureFlags featureFlags) { 305 this.serviceType = serviceType; 306 this.socketClient = socketClient; 307 this.executor = executor; 308 this.serviceTypeLabels = TextUtils.split(serviceType, "\\."); 309 this.responseDecoder = new MdnsResponseDecoder(clock, serviceTypeLabels); 310 this.clock = clock; 311 this.socketKey = socketKey; 312 this.sharedLog = sharedLog; 313 this.handler = new QueryTaskHandler(looper); 314 this.dependencies = dependencies; 315 this.serviceCache = serviceCache; 316 this.mdnsQueryScheduler = new MdnsQueryScheduler(); 317 this.cacheKey = new MdnsServiceCache.CacheKey(serviceType, socketKey); 318 this.featureFlags = featureFlags; 319 this.scheduler = featureFlags.isAccurateDelayCallbackEnabled() 320 ? dependencies.createScheduler(handler) : null; 321 } 322 323 /** 324 * Do the cleanup of the MdnsServiceTypeClient 325 */ shutDown()326 private void shutDown() { 327 removeScheduledTask(); 328 mdnsQueryScheduler.cancelScheduledRun(); 329 serviceCache.unregisterServiceExpiredCallback(cacheKey); 330 if (scheduler != null) { 331 scheduler.close(); 332 } 333 } 334 getExistingServices()335 private List<MdnsResponse> getExistingServices() { 336 return featureFlags.isQueryWithKnownAnswerEnabled() 337 ? serviceCache.getCachedServices(cacheKey) : Collections.emptyList(); 338 } 339 setDelayedTask(ScheduledQueryTaskArgs args, long timeToNextTaskMs)340 private void setDelayedTask(ScheduledQueryTaskArgs args, long timeToNextTaskMs) { 341 scheduler.removeDelayedMessage(EVENT_START_QUERYTASK); 342 scheduler.sendDelayedMessage( 343 handler.obtainMessage(EVENT_START_QUERYTASK, args), timeToNextTaskMs); 344 } 345 346 /** 347 * Registers {@code listener} for receiving discovery event of mDNS service instances, and 348 * starts 349 * (or continue) to send mDNS queries periodically. 350 * 351 * @param listener The {@link MdnsServiceBrowserListener} to register. 352 * @param searchOptions {@link MdnsSearchOptions} contains the list of subtypes to discover. 353 */ 354 @SuppressWarnings("FutureReturnValueIgnored") startSendAndReceive( @onNull MdnsServiceBrowserListener listener, @NonNull MdnsSearchOptions searchOptions)355 public void startSendAndReceive( 356 @NonNull MdnsServiceBrowserListener listener, 357 @NonNull MdnsSearchOptions searchOptions) { 358 ensureRunningOnHandlerThread(handler); 359 this.searchOptions = searchOptions; 360 boolean hadReply = false; 361 final ListenerInfo existingInfo = listeners.get(listener); 362 final ListenerInfo listenerInfo = new ListenerInfo(searchOptions, existingInfo); 363 listeners.put(listener, listenerInfo); 364 if (existingInfo == null) { 365 for (MdnsResponse existingResponse : serviceCache.getCachedServices(cacheKey)) { 366 if (!responseMatchesOptions(existingResponse, searchOptions)) continue; 367 final MdnsServiceInfo info = buildMdnsServiceInfoFromResponse( 368 existingResponse, serviceTypeLabels, clock.elapsedRealtime()); 369 listener.onServiceNameDiscovered(info, true /* isServiceFromCache */); 370 listenerInfo.setServiceDiscovered(info.getServiceInstanceName()); 371 if (existingResponse.isComplete()) { 372 listener.onServiceFound(info, true /* isServiceFromCache */); 373 hadReply = true; 374 } 375 } 376 } 377 // Remove the next scheduled periodical task. 378 removeScheduledTask(); 379 final boolean forceEnableBackoff = 380 (searchOptions.getQueryMode() == AGGRESSIVE_QUERY_MODE && hadReply); 381 // Keep the latest scheduled run for rescheduling if there is a service in the cache. 382 if (!(forceEnableBackoff)) { 383 mdnsQueryScheduler.cancelScheduledRun(); 384 } 385 final QueryTaskConfig taskConfig = new QueryTaskConfig(searchOptions.getQueryMode()); 386 final long now = clock.elapsedRealtime(); 387 if (lastSentTime == 0) { 388 lastSentTime = now; 389 } 390 final long minRemainingTtl = getMinRemainingTtl(now); 391 if (hadReply) { 392 final ScheduledQueryTaskArgs args = 393 mdnsQueryScheduler.scheduleNextRun( 394 taskConfig, 395 minRemainingTtl, 396 now, 397 lastSentTime, 398 currentSessionId, 399 searchOptions.getQueryMode(), 400 searchOptions.numOfQueriesBeforeBackoff(), 401 forceEnableBackoff 402 ); 403 final long timeToNextTaskMs = calculateTimeToNextTask(args, now); 404 sharedLog.log(String.format("Schedule a query. Next run: sessionId: %d, in %d ms", 405 args.sessionId, timeToNextTaskMs)); 406 if (scheduler != null) { 407 setDelayedTask(args, timeToNextTaskMs); 408 } else { 409 dependencies.sendMessageDelayed( 410 handler, 411 handler.obtainMessage(EVENT_START_QUERYTASK, args), 412 timeToNextTaskMs); 413 } 414 } else { 415 final List<MdnsResponse> servicesToResolve = makeResponsesForResolve(socketKey); 416 final QueryTask queryTask = new QueryTask( 417 mdnsQueryScheduler.scheduleFirstRun(taskConfig, now, 418 minRemainingTtl, currentSessionId), servicesToResolve, 419 getAllDiscoverySubtypes(), needSendDiscoveryQueries(listeners), 420 getExistingServices(), searchOptions.onlyUseIpv6OnIpv6OnlyNetworks(), 421 socketKey); 422 executor.submit(queryTask); 423 } 424 425 serviceCache.registerServiceExpiredCallback(cacheKey, serviceExpiredCallback); 426 } 427 getAllDiscoverySubtypes()428 private Set<String> getAllDiscoverySubtypes() { 429 final Set<String> subtypes = MdnsUtils.newSet(); 430 for (int i = 0; i < listeners.size(); i++) { 431 final MdnsSearchOptions listenerOptions = listeners.valueAt(i).searchOptions; 432 subtypes.addAll(listenerOptions.getSubtypes()); 433 } 434 return subtypes; 435 } 436 437 /** 438 * Get the executor service. 439 */ getExecutor()440 public ScheduledExecutorService getExecutor() { 441 return executor; 442 } 443 444 /** 445 * Get the cache key for this service type client. 446 */ 447 @NonNull getCacheKey()448 public MdnsServiceCache.CacheKey getCacheKey() { 449 return cacheKey; 450 } 451 removeScheduledTask()452 private void removeScheduledTask() { 453 if (scheduler != null) { 454 scheduler.removeDelayedMessage(EVENT_START_QUERYTASK); 455 } else { 456 dependencies.removeMessages(handler, EVENT_START_QUERYTASK); 457 } 458 sharedLog.log("Remove EVENT_START_QUERYTASK" 459 + ", current session: " + currentSessionId); 460 ++currentSessionId; 461 } 462 responseMatchesOptions(@onNull MdnsResponse response, @NonNull MdnsSearchOptions options)463 private boolean responseMatchesOptions(@NonNull MdnsResponse response, 464 @NonNull MdnsSearchOptions options) { 465 final boolean matchesInstanceName = options.getResolveInstanceName() == null 466 // DNS is case-insensitive, so ignore case in the comparison 467 || DnsUtils.equalsIgnoreDnsCase(options.getResolveInstanceName(), 468 response.getServiceInstanceName()); 469 470 // If discovery is requiring some subtypes, the response must have one that matches a 471 // requested one. 472 final List<String> responseSubtypes = response.getSubtypes() == null 473 ? Collections.emptyList() : response.getSubtypes(); 474 final boolean matchesSubtype = options.getSubtypes().size() == 0 475 || CollectionUtils.any(options.getSubtypes(), requiredSub -> 476 CollectionUtils.any(responseSubtypes, actualSub -> 477 DnsUtils.equalsIgnoreDnsCase( 478 MdnsConstants.SUBTYPE_PREFIX + requiredSub, actualSub))); 479 480 return matchesInstanceName && matchesSubtype; 481 } 482 483 /** 484 * Unregisters {@code listener} from receiving discovery event of mDNS service instances. 485 * 486 * @param listener The {@link MdnsServiceBrowserListener} to unregister. 487 * @return {@code true} if no listener is registered with this client after unregistering {@code 488 * listener}. Otherwise returns {@code false}. 489 */ stopSendAndReceive(@onNull MdnsServiceBrowserListener listener)490 public boolean stopSendAndReceive(@NonNull MdnsServiceBrowserListener listener) { 491 ensureRunningOnHandlerThread(handler); 492 if (listeners.remove(listener) == null) { 493 return listeners.isEmpty(); 494 } 495 if (listeners.isEmpty()) { 496 shutDown(); 497 } 498 return listeners.isEmpty(); 499 } 500 501 /** 502 * Process an incoming response packet. 503 */ processResponse(@onNull MdnsPacket packet, @NonNull SocketKey socketKey)504 public synchronized void processResponse(@NonNull MdnsPacket packet, 505 @NonNull SocketKey socketKey) { 506 ensureRunningOnHandlerThread(handler); 507 // Augment the list of current known responses, and generated responses for resolve 508 // requests if there is no known response 509 final List<MdnsResponse> cachedList = serviceCache.getCachedServices(cacheKey); 510 final List<MdnsResponse> currentList = new ArrayList<>(cachedList); 511 List<MdnsResponse> additionalResponses = makeResponsesForResolve(socketKey); 512 for (MdnsResponse additionalResponse : additionalResponses) { 513 if (findMatchedResponse( 514 cachedList, additionalResponse.getServiceInstanceName()) == null) { 515 currentList.add(additionalResponse); 516 } 517 } 518 final Pair<Set<MdnsResponse>, ArrayList<MdnsResponse>> augmentedResult = 519 responseDecoder.augmentResponses(packet, currentList, 520 socketKey.getInterfaceIndex(), socketKey.getNetwork()); 521 522 final Set<MdnsResponse> modifiedResponse = augmentedResult.first; 523 final ArrayList<MdnsResponse> allResponses = augmentedResult.second; 524 525 for (MdnsResponse response : allResponses) { 526 final String serviceInstanceName = response.getServiceInstanceName(); 527 if (modifiedResponse.contains(response)) { 528 if (response.isGoodbye()) { 529 onGoodbyeReceived(serviceInstanceName); 530 } else { 531 onResponseModified(response); 532 } 533 } else if (findMatchedResponse(cachedList, serviceInstanceName) != null) { 534 // If the response is not modified and already in the cache. The cache will 535 // need to be updated to refresh the last receipt time. 536 serviceCache.addOrUpdateService(cacheKey, response); 537 if (DBG) { 538 sharedLog.v("Update the last receipt time for service:" 539 + serviceInstanceName); 540 } 541 } 542 } 543 final boolean hasScheduledTask = scheduler != null 544 ? scheduler.hasDelayedMessage(EVENT_START_QUERYTASK) 545 : dependencies.hasMessages(handler, EVENT_START_QUERYTASK); 546 if (hasScheduledTask) { 547 final long now = clock.elapsedRealtime(); 548 final long minRemainingTtl = getMinRemainingTtl(now); 549 final ScheduledQueryTaskArgs args = 550 mdnsQueryScheduler.maybeRescheduleCurrentRun(now, minRemainingTtl, 551 lastSentTime, currentSessionId + 1, 552 searchOptions.numOfQueriesBeforeBackoff()); 553 if (args != null) { 554 removeScheduledTask(); 555 final long timeToNextTaskMs = calculateTimeToNextTask(args, now); 556 sharedLog.log(String.format("Reschedule a query. Next run: sessionId: %d, in %d ms", 557 args.sessionId, timeToNextTaskMs)); 558 if (scheduler != null) { 559 setDelayedTask(args, timeToNextTaskMs); 560 } else { 561 dependencies.sendMessageDelayed( 562 handler, 563 handler.obtainMessage(EVENT_START_QUERYTASK, args), 564 timeToNextTaskMs); 565 } 566 } 567 } 568 } 569 onFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode)570 public synchronized void onFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode) { 571 ensureRunningOnHandlerThread(handler); 572 for (int i = 0; i < listeners.size(); i++) { 573 listeners.keyAt(i).onFailedToParseMdnsResponse(receivedPacketNumber, errorCode); 574 } 575 } 576 notifyRemovedServiceToListeners(@onNull MdnsResponse response, @NonNull String message)577 private void notifyRemovedServiceToListeners(@NonNull MdnsResponse response, 578 @NonNull String message) { 579 for (int i = 0; i < listeners.size(); i++) { 580 if (!responseMatchesOptions(response, listeners.valueAt(i).searchOptions)) continue; 581 final MdnsServiceBrowserListener listener = listeners.keyAt(i); 582 if (response.getServiceInstanceName() != null) { 583 listeners.valueAt(i).unsetServiceDiscovered(response.getServiceInstanceName()); 584 final MdnsServiceInfo serviceInfo = buildMdnsServiceInfoFromResponse( 585 response, serviceTypeLabels, clock.elapsedRealtime()); 586 if (response.isComplete()) { 587 sharedLog.log(message + ". onServiceRemoved: " + serviceInfo); 588 listener.onServiceRemoved(serviceInfo); 589 } 590 sharedLog.log(message + ". onServiceNameRemoved: " + serviceInfo); 591 listener.onServiceNameRemoved(serviceInfo); 592 } 593 } 594 } 595 596 /** Notify all services are removed because the socket is destroyed. */ notifySocketDestroyed()597 public void notifySocketDestroyed() { 598 ensureRunningOnHandlerThread(handler); 599 for (MdnsResponse response : serviceCache.getCachedServices(cacheKey)) { 600 final String name = response.getServiceInstanceName(); 601 if (name == null) continue; 602 notifyRemovedServiceToListeners(response, "Socket destroyed"); 603 } 604 shutDown(); 605 } 606 onResponseModified(@onNull MdnsResponse response)607 private void onResponseModified(@NonNull MdnsResponse response) { 608 final String serviceInstanceName = response.getServiceInstanceName(); 609 final MdnsResponse currentResponse = 610 serviceCache.getCachedService(serviceInstanceName, cacheKey); 611 612 final boolean newInCache = currentResponse == null; 613 boolean serviceBecomesComplete = false; 614 if (newInCache) { 615 if (serviceInstanceName != null) { 616 serviceCache.addOrUpdateService(cacheKey, response); 617 } 618 } else { 619 boolean before = currentResponse.isComplete(); 620 serviceCache.addOrUpdateService(cacheKey, response); 621 boolean after = response.isComplete(); 622 serviceBecomesComplete = !before && after; 623 } 624 sharedLog.i(String.format( 625 "Handling response from service: %s, newInCache: %b, serviceBecomesComplete:" 626 + " %b, responseIsComplete: %b", 627 serviceInstanceName, newInCache, serviceBecomesComplete, 628 response.isComplete())); 629 final MdnsServiceInfo serviceInfo = buildMdnsServiceInfoFromResponse( 630 response, serviceTypeLabels, clock.elapsedRealtime()); 631 632 for (int i = 0; i < listeners.size(); i++) { 633 // If a service stops matching the options (currently can only happen if it loses a 634 // subtype), service lost callbacks should also be sent; this is not done today as 635 // only expiration of SRV records is used, not PTR records used for subtypes, so 636 // services never lose PTR record subtypes. 637 if (!responseMatchesOptions(response, listeners.valueAt(i).searchOptions)) continue; 638 final MdnsServiceBrowserListener listener = listeners.keyAt(i); 639 final ListenerInfo listenerInfo = listeners.valueAt(i); 640 final boolean newServiceFound = listenerInfo.setServiceDiscovered(serviceInstanceName); 641 if (newServiceFound) { 642 sharedLog.log("onServiceNameDiscovered: " + serviceInfo); 643 listener.onServiceNameDiscovered(serviceInfo, false /* isServiceFromCache */); 644 } 645 646 if (response.isComplete()) { 647 if (newServiceFound || serviceBecomesComplete) { 648 sharedLog.log("onServiceFound: " + serviceInfo); 649 listener.onServiceFound(serviceInfo, false /* isServiceFromCache */); 650 } else { 651 sharedLog.log("onServiceUpdated: " + serviceInfo); 652 listener.onServiceUpdated(serviceInfo); 653 } 654 } 655 } 656 } 657 onGoodbyeReceived(@ullable String serviceInstanceName)658 private void onGoodbyeReceived(@Nullable String serviceInstanceName) { 659 final MdnsResponse response = 660 serviceCache.removeService(serviceInstanceName, cacheKey); 661 if (response == null) { 662 return; 663 } 664 notifyRemovedServiceToListeners(response, "Goodbye received"); 665 } 666 shouldRemoveServiceAfterTtlExpires()667 private boolean shouldRemoveServiceAfterTtlExpires() { 668 if (removeServiceAfterTtlExpires) { 669 return true; 670 } 671 return searchOptions != null && searchOptions.removeExpiredService(); 672 } 673 makeResponsesForResolve(@onNull SocketKey socketKey)674 private List<MdnsResponse> makeResponsesForResolve(@NonNull SocketKey socketKey) { 675 final List<MdnsResponse> resolveResponses = new ArrayList<>(); 676 for (int i = 0; i < listeners.size(); i++) { 677 final String resolveName = listeners.valueAt(i).searchOptions.getResolveInstanceName(); 678 if (resolveName == null) { 679 continue; 680 } 681 if (CollectionUtils.any(resolveResponses, 682 r -> DnsUtils.equalsIgnoreDnsCase(resolveName, r.getServiceInstanceName()))) { 683 continue; 684 } 685 MdnsResponse knownResponse = 686 serviceCache.getCachedService(resolveName, cacheKey); 687 if (knownResponse == null) { 688 final ArrayList<String> instanceFullName = new ArrayList<>( 689 serviceTypeLabels.length + 1); 690 instanceFullName.add(resolveName); 691 instanceFullName.addAll(Arrays.asList(serviceTypeLabels)); 692 knownResponse = new MdnsResponse( 693 0L /* lastUpdateTime */, instanceFullName.toArray(new String[0]), 694 socketKey.getInterfaceIndex(), socketKey.getNetwork()); 695 } 696 resolveResponses.add(knownResponse); 697 } 698 return resolveResponses; 699 } 700 needSendDiscoveryQueries( @onNull ArrayMap<MdnsServiceBrowserListener, ListenerInfo> listeners)701 private static boolean needSendDiscoveryQueries( 702 @NonNull ArrayMap<MdnsServiceBrowserListener, ListenerInfo> listeners) { 703 // Note iterators are discouraged on ArrayMap as per its documentation 704 for (int i = 0; i < listeners.size(); i++) { 705 if (listeners.valueAt(i).searchOptions.getResolveInstanceName() == null) { 706 return true; 707 } 708 } 709 return false; 710 } 711 tryRemoveServiceAfterTtlExpires()712 private void tryRemoveServiceAfterTtlExpires() { 713 if (!shouldRemoveServiceAfterTtlExpires()) return; 714 715 final Iterator<MdnsResponse> iter = serviceCache.getCachedServices(cacheKey).iterator(); 716 while (iter.hasNext()) { 717 MdnsResponse existingResponse = iter.next(); 718 if (existingResponse.hasServiceRecord() 719 && existingResponse.getServiceRecord() 720 .getRemainingTTL(clock.elapsedRealtime()) == 0) { 721 serviceCache.removeService(existingResponse.getServiceInstanceName(), cacheKey); 722 notifyRemovedServiceToListeners(existingResponse, "TTL expired"); 723 } 724 } 725 } 726 727 private static class QuerySentArguments { 728 private final int transactionId; 729 private final List<String> subTypes = new ArrayList<>(); 730 private final ScheduledQueryTaskArgs taskArgs; 731 QuerySentArguments(int transactionId, @NonNull List<String> subTypes, @NonNull ScheduledQueryTaskArgs taskArgs)732 QuerySentArguments(int transactionId, @NonNull List<String> subTypes, 733 @NonNull ScheduledQueryTaskArgs taskArgs) { 734 this.transactionId = transactionId; 735 this.subTypes.addAll(subTypes); 736 this.taskArgs = taskArgs; 737 } 738 } 739 740 // A FutureTask that enqueues a single query, and schedule a new FutureTask for the next task. 741 private class QueryTask implements Runnable { 742 private final ScheduledQueryTaskArgs taskArgs; 743 private final List<MdnsResponse> servicesToResolve = new ArrayList<>(); 744 private final List<String> subtypes = new ArrayList<>(); 745 private final boolean sendDiscoveryQueries; 746 private final List<MdnsResponse> existingServices = new ArrayList<>(); 747 private final boolean onlyUseIpv6OnIpv6OnlyNetworks; 748 private final SocketKey socketKey; QueryTask(@onNull ScheduledQueryTaskArgs taskArgs, @NonNull Collection<MdnsResponse> servicesToResolve, @NonNull Collection<String> subtypes, boolean sendDiscoveryQueries, @NonNull Collection<MdnsResponse> existingServices, boolean onlyUseIpv6OnIpv6OnlyNetworks, @NonNull SocketKey socketKey)749 QueryTask(@NonNull ScheduledQueryTaskArgs taskArgs, 750 @NonNull Collection<MdnsResponse> servicesToResolve, 751 @NonNull Collection<String> subtypes, boolean sendDiscoveryQueries, 752 @NonNull Collection<MdnsResponse> existingServices, 753 boolean onlyUseIpv6OnIpv6OnlyNetworks, 754 @NonNull SocketKey socketKey) { 755 this.taskArgs = taskArgs; 756 this.servicesToResolve.addAll(servicesToResolve); 757 this.subtypes.addAll(subtypes); 758 this.sendDiscoveryQueries = sendDiscoveryQueries; 759 this.existingServices.addAll(existingServices); 760 this.onlyUseIpv6OnIpv6OnlyNetworks = onlyUseIpv6OnIpv6OnlyNetworks; 761 this.socketKey = socketKey; 762 } 763 764 @Override run()765 public void run() { 766 Pair<Integer, List<String>> result; 767 try { 768 result = 769 new EnqueueMdnsQueryCallable( 770 socketClient, 771 serviceType, 772 subtypes, 773 taskArgs.config.expectUnicastResponse, 774 taskArgs.config.getTransactionId(), 775 socketKey, 776 onlyUseIpv6OnIpv6OnlyNetworks, 777 sendDiscoveryQueries, 778 servicesToResolve, 779 clock, 780 sharedLog, 781 dependencies, 782 existingServices, 783 featureFlags.isQueryWithKnownAnswerEnabled()) 784 .call(); 785 } catch (RuntimeException e) { 786 sharedLog.e(String.format("Failed to run EnqueueMdnsQueryCallable for subtype: %s", 787 TextUtils.join(",", subtypes)), e); 788 result = Pair.create(INVALID_TRANSACTION_ID, new ArrayList<>()); 789 } 790 dependencies.sendMessage( 791 handler, handler.obtainMessage(EVENT_QUERY_RESULT, 792 new QuerySentArguments(result.first, result.second, taskArgs))); 793 } 794 } 795 getMinRemainingTtl(long now)796 private long getMinRemainingTtl(long now) { 797 long minRemainingTtl = Long.MAX_VALUE; 798 for (MdnsResponse response : serviceCache.getCachedServices(cacheKey)) { 799 if (!response.isComplete()) { 800 continue; 801 } 802 long remainingTtl = 803 response.getServiceRecord().getRemainingTTL(now); 804 // remainingTtl is <= 0 means the service expired. 805 if (remainingTtl <= 0) { 806 return 0; 807 } 808 if (remainingTtl < minRemainingTtl) { 809 minRemainingTtl = remainingTtl; 810 } 811 } 812 return minRemainingTtl == Long.MAX_VALUE ? 0 : minRemainingTtl; 813 } 814 calculateTimeToNextTask(ScheduledQueryTaskArgs args, long now)815 private static long calculateTimeToNextTask(ScheduledQueryTaskArgs args, 816 long now) { 817 return Math.max(args.timeToRun - now, 0); 818 } 819 820 /** 821 * Dump ServiceTypeClient state. 822 */ dump(PrintWriter pw)823 public void dump(PrintWriter pw) { 824 ensureRunningOnHandlerThread(handler); 825 pw.println("ServiceTypeClient: Type{" + serviceType + "} " + socketKey + " with " 826 + listeners.size() + " listeners."); 827 } 828 }