• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 }