diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java index 397e5a63d7..ac794a1891 100644 --- a/service-t/src/com/android/server/NsdService.java +++ b/service-t/src/com/android/server/NsdService.java @@ -35,6 +35,7 @@ import static com.android.networkstack.apishim.ConstantsShim.REGISTER_NSD_OFFLOA import static com.android.server.connectivity.mdns.MdnsAdvertiser.AdvertiserMetrics; import static com.android.server.connectivity.mdns.MdnsConstants.NO_PACKET; import static com.android.server.connectivity.mdns.MdnsRecord.MAX_LABEL_LENGTH; +import static com.android.server.connectivity.mdns.MdnsSearchOptions.AGGRESSIVE_QUERY_MODE; import static com.android.server.connectivity.mdns.MdnsSearchOptions.PASSIVE_QUERY_MODE; import static com.android.server.connectivity.mdns.util.MdnsUtils.Clock; @@ -252,6 +253,8 @@ public class NsdService extends INsdManager.Stub { private final RemoteCallbackList mOffloadEngines = new RemoteCallbackList<>(); + @NonNull + private final MdnsFeatureFlags mMdnsFeatureFlags; private static class OffloadEngineInfo { @NonNull final String mInterfaceName; @@ -794,7 +797,10 @@ public class NsdService extends INsdManager.Stub { MdnsSearchOptions.newBuilder() .setNetwork(discoveryRequest.getNetwork()) .setRemoveExpiredService(true) - .setQueryMode(PASSIVE_QUERY_MODE); + .setQueryMode( + mMdnsFeatureFlags.isAggressiveQueryModeEnabled() + ? AGGRESSIVE_QUERY_MODE + : PASSIVE_QUERY_MODE); if (subtype != null) { // checkSubtypeLabels() ensures that subtypes start with '_' but // MdnsSearchOptions expects the underscore to not be present. @@ -1063,7 +1069,9 @@ public class NsdService extends INsdManager.Stub { transactionId, resolveServiceType); final MdnsSearchOptions options = MdnsSearchOptions.newBuilder() .setNetwork(info.getNetwork()) - .setQueryMode(PASSIVE_QUERY_MODE) + .setQueryMode(mMdnsFeatureFlags.isAggressiveQueryModeEnabled() + ? AGGRESSIVE_QUERY_MODE + : PASSIVE_QUERY_MODE) .setResolveInstanceName(info.getServiceName()) .setRemoveExpiredService(true) .build(); @@ -1161,7 +1169,9 @@ public class NsdService extends INsdManager.Stub { transactionId, resolveServiceType); final MdnsSearchOptions options = MdnsSearchOptions.newBuilder() .setNetwork(info.getNetwork()) - .setQueryMode(PASSIVE_QUERY_MODE) + .setQueryMode(mMdnsFeatureFlags.isAggressiveQueryModeEnabled() + ? AGGRESSIVE_QUERY_MODE + : PASSIVE_QUERY_MODE) .setResolveInstanceName(info.getServiceName()) .setRemoveExpiredService(true) .build(); @@ -1787,7 +1797,7 @@ public class NsdService extends INsdManager.Stub { am.addOnUidImportanceListener(new UidImportanceListener(handler), mRunningAppActiveImportanceCutoff); - final MdnsFeatureFlags flags = new MdnsFeatureFlags.Builder() + mMdnsFeatureFlags = new MdnsFeatureFlags.Builder() .setIsMdnsOffloadFeatureEnabled(mDeps.isTetheringFeatureNotChickenedOut( mContext, MdnsFeatureFlags.NSD_FORCE_DISABLE_MDNS_OFFLOAD)) .setIncludeInetAddressRecordsInProbing(mDeps.isFeatureEnabled( @@ -1800,18 +1810,21 @@ public class NsdService extends INsdManager.Stub { mContext, MdnsFeatureFlags.NSD_KNOWN_ANSWER_SUPPRESSION)) .setIsUnicastReplyEnabled(mDeps.isFeatureEnabled( mContext, MdnsFeatureFlags.NSD_UNICAST_REPLY_ENABLED)) + .setIsAggressiveQueryModeEnabled(mDeps.isFeatureEnabled( + mContext, MdnsFeatureFlags.NSD_AGGRESSIVE_QUERY_MODE)) .setOverrideProvider(flag -> mDeps.isFeatureEnabled( mContext, FORCE_ENABLE_FLAG_FOR_TEST_PREFIX + flag)) .build(); mMdnsSocketClient = new MdnsMultinetworkSocketClient(handler.getLooper(), mMdnsSocketProvider, - LOGGER.forSubComponent("MdnsMultinetworkSocketClient"), flags); + LOGGER.forSubComponent("MdnsMultinetworkSocketClient"), mMdnsFeatureFlags); mMdnsDiscoveryManager = deps.makeMdnsDiscoveryManager(new ExecutorProvider(), - mMdnsSocketClient, LOGGER.forSubComponent("MdnsDiscoveryManager"), flags); + mMdnsSocketClient, LOGGER.forSubComponent("MdnsDiscoveryManager"), + mMdnsFeatureFlags); handler.post(() -> mMdnsSocketClient.setCallback(mMdnsDiscoveryManager)); mAdvertiser = deps.makeMdnsAdvertiser(handler.getLooper(), mMdnsSocketProvider, - new AdvertiserCallback(), LOGGER.forSubComponent("MdnsAdvertiser"), flags, - mContext); + new AdvertiserCallback(), LOGGER.forSubComponent("MdnsAdvertiser"), + mMdnsFeatureFlags, mContext); mClock = deps.makeClock(); } diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java index 946616226d..fe9bbba419 100644 --- a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java +++ b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java @@ -57,6 +57,11 @@ public class MdnsFeatureFlags { */ public static final String NSD_UNICAST_REPLY_ENABLED = "nsd_unicast_reply_enabled"; + /** + * A feature flag to control whether the aggressive query mode should be enabled. + */ + public static final String NSD_AGGRESSIVE_QUERY_MODE = "nsd_aggressive_query_mode"; + // Flag for offload feature public final boolean mIsMdnsOffloadFeatureEnabled; @@ -75,6 +80,9 @@ public class MdnsFeatureFlags { // Flag to enable replying unicast to queries requesting unicast replies public final boolean mIsUnicastReplyEnabled; + // Flag for aggressive query mode + public final boolean mIsAggressiveQueryModeEnabled; + @Nullable private final FlagOverrideProvider mOverrideProvider; @@ -102,6 +110,13 @@ public class MdnsFeatureFlags { return mIsUnicastReplyEnabled || isForceEnabledForTest(NSD_UNICAST_REPLY_ENABLED); } + /** + * Indicates whether {@link #NSD_AGGRESSIVE_QUERY_MODE} is enabled, including for testing. + */ + public boolean isAggressiveQueryModeEnabled() { + return mIsAggressiveQueryModeEnabled || isForceEnabledForTest(NSD_AGGRESSIVE_QUERY_MODE); + } + /** * The constructor for {@link MdnsFeatureFlags}. */ @@ -111,6 +126,7 @@ public class MdnsFeatureFlags { boolean isLabelCountLimitEnabled, boolean isKnownAnswerSuppressionEnabled, boolean isUnicastReplyEnabled, + boolean isAggressiveQueryModeEnabled, @Nullable FlagOverrideProvider overrideProvider) { mIsMdnsOffloadFeatureEnabled = isOffloadFeatureEnabled; mIncludeInetAddressRecordsInProbing = includeInetAddressRecordsInProbing; @@ -118,6 +134,7 @@ public class MdnsFeatureFlags { mIsLabelCountLimitEnabled = isLabelCountLimitEnabled; mIsKnownAnswerSuppressionEnabled = isKnownAnswerSuppressionEnabled; mIsUnicastReplyEnabled = isUnicastReplyEnabled; + mIsAggressiveQueryModeEnabled = isAggressiveQueryModeEnabled; mOverrideProvider = overrideProvider; } @@ -136,6 +153,7 @@ public class MdnsFeatureFlags { private boolean mIsLabelCountLimitEnabled; private boolean mIsKnownAnswerSuppressionEnabled; private boolean mIsUnicastReplyEnabled; + private boolean mIsAggressiveQueryModeEnabled; private FlagOverrideProvider mOverrideProvider; /** @@ -148,6 +166,7 @@ public class MdnsFeatureFlags { mIsLabelCountLimitEnabled = true; // Default enabled. mIsKnownAnswerSuppressionEnabled = false; mIsUnicastReplyEnabled = true; + mIsAggressiveQueryModeEnabled = false; mOverrideProvider = null; } @@ -223,6 +242,16 @@ public class MdnsFeatureFlags { return this; } + /** + * Set whether the aggressive query mode is enabled. + * + * @see #NSD_AGGRESSIVE_QUERY_MODE + */ + public Builder setIsAggressiveQueryModeEnabled(boolean isAggressiveQueryModeEnabled) { + mIsAggressiveQueryModeEnabled = isAggressiveQueryModeEnabled; + return this; + } + /** * Builds a {@link MdnsFeatureFlags} with the arguments supplied to this builder. */ @@ -233,6 +262,7 @@ public class MdnsFeatureFlags { mIsLabelCountLimitEnabled, mIsKnownAnswerSuppressionEnabled, mIsUnicastReplyEnabled, + mIsAggressiveQueryModeEnabled, mOverrideProvider); } } diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsSearchOptions.java b/service-t/src/com/android/server/connectivity/mdns/MdnsSearchOptions.java index e52c995979..086094bb2c 100644 --- a/service-t/src/com/android/server/connectivity/mdns/MdnsSearchOptions.java +++ b/service-t/src/com/android/server/connectivity/mdns/MdnsSearchOptions.java @@ -44,6 +44,10 @@ public class MdnsSearchOptions implements Parcelable { public static final int PASSIVE_QUERY_MODE = 0; // Active query mode scans frequently. public static final int ACTIVE_QUERY_MODE = 1; + // Aggressive query mode scans more frequently than the active mode at first, and sends both + // unicast and multicast queries simultaneously, but in long sessions it eventually sends as + // many queries as the PASSIVE mode. + public static final int AGGRESSIVE_QUERY_MODE = 2; /** @hide */ public static final Parcelable.Creator CREATOR = diff --git a/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java b/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java index 6d92bd2de5..10a71a2696 100644 --- a/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java +++ b/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java @@ -16,6 +16,7 @@ package com.android.server.connectivity.mdns; +import static com.android.server.connectivity.mdns.MdnsSearchOptions.AGGRESSIVE_QUERY_MODE; import static com.android.server.connectivity.mdns.MdnsSearchOptions.PASSIVE_QUERY_MODE; import android.annotation.NonNull; @@ -35,13 +36,23 @@ public class QueryTaskConfig { private static final int INITIAL_TIME_BETWEEN_BURSTS_MS = (int) MdnsConfigs.initialTimeBetweenBurstsMs(); - private static final int TIME_BETWEEN_BURSTS_MS = (int) MdnsConfigs.timeBetweenBurstsMs(); + private static final int MAX_TIME_BETWEEN_ACTIVE_PASSIVE_BURSTS_MS = + (int) MdnsConfigs.timeBetweenBurstsMs(); private static final int QUERIES_PER_BURST = (int) MdnsConfigs.queriesPerBurst(); private static final int TIME_BETWEEN_QUERIES_IN_BURST_MS = (int) MdnsConfigs.timeBetweenQueriesInBurstMs(); private static final int QUERIES_PER_BURST_PASSIVE_MODE = (int) MdnsConfigs.queriesPerBurstPassive(); private static final int UNSIGNED_SHORT_MAX_VALUE = 65536; + @VisibleForTesting + // RFC 6762 5.2: The interval between the first two queries MUST be at least one second. + static final int INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS = 1000; + @VisibleForTesting + // Basically this tries to send one query per typical DTIM interval 100ms, to maximize the + // chances that a query will be received if devices are using a DTIM multiplier (in which case + // they only listen once every [multiplier] DTIM intervals). + static final int TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS = 100; + static final int MAX_TIME_BETWEEN_AGGRESSIVE_BURSTS_MS = 60000; // The following fields are used by QueryTask so we need to test them. @VisibleForTesting final List subtypes; @@ -81,6 +92,7 @@ public class QueryTaskConfig { this.queryCount = queryCount; this.socketKey = other.socketKey; } + QueryTaskConfig(@NonNull Collection subtypes, int queryMode, boolean onlyUseIpv6OnIpv6OnlyNetworks, @@ -96,21 +108,59 @@ public class QueryTaskConfig { this.expectUnicastResponse = true; this.isFirstBurst = true; // Config the scan frequency based on the scan mode. - if (this.queryMode == PASSIVE_QUERY_MODE) { + if (this.queryMode == AGGRESSIVE_QUERY_MODE) { + this.timeBetweenBurstsInMs = INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS; + this.delayUntilNextTaskWithoutBackoffMs = + TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS; + } else if (this.queryMode == PASSIVE_QUERY_MODE) { // In passive scan mode, sends a single burst of QUERIES_PER_BURST queries, and then // in each TIME_BETWEEN_BURSTS interval, sends QUERIES_PER_BURST_PASSIVE_MODE // queries. - this.timeBetweenBurstsInMs = TIME_BETWEEN_BURSTS_MS; + this.timeBetweenBurstsInMs = MAX_TIME_BETWEEN_ACTIVE_PASSIVE_BURSTS_MS; + this.delayUntilNextTaskWithoutBackoffMs = TIME_BETWEEN_QUERIES_IN_BURST_MS; } else { // In active scan mode, sends a burst of QUERIES_PER_BURST queries, // TIME_BETWEEN_QUERIES_IN_BURST_MS apart, then waits for the scan interval, and // then repeats. The scan interval starts as INITIAL_TIME_BETWEEN_BURSTS_MS and // doubles until it maxes out at TIME_BETWEEN_BURSTS_MS. this.timeBetweenBurstsInMs = INITIAL_TIME_BETWEEN_BURSTS_MS; + this.delayUntilNextTaskWithoutBackoffMs = TIME_BETWEEN_QUERIES_IN_BURST_MS; } this.socketKey = socketKey; this.queryCount = 0; - this.delayUntilNextTaskWithoutBackoffMs = TIME_BETWEEN_QUERIES_IN_BURST_MS; + } + + long getDelayUntilNextTaskWithoutBackoff(boolean isFirstQueryInBurst, + boolean isLastQueryInBurst) { + if (isFirstQueryInBurst && queryMode == AGGRESSIVE_QUERY_MODE) { + return 0; + } + if (isLastQueryInBurst) { + return timeBetweenBurstsInMs; + } + return queryMode == AGGRESSIVE_QUERY_MODE + ? TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS + : TIME_BETWEEN_QUERIES_IN_BURST_MS; + } + + boolean getNextExpectUnicastResponse(boolean isLastQueryInBurst) { + if (!isLastQueryInBurst) { + return false; + } + if (queryMode == AGGRESSIVE_QUERY_MODE) { + return true; + } + return alwaysAskForUnicastResponse; + } + + int getNextTimeBetweenBurstsMs(boolean isLastQueryInBurst) { + if (!isLastQueryInBurst) { + return timeBetweenBurstsInMs; + } + final int maxTimeBetweenBursts = queryMode == AGGRESSIVE_QUERY_MODE + ? MAX_TIME_BETWEEN_AGGRESSIVE_BURSTS_MS : MAX_TIME_BETWEEN_ACTIVE_PASSIVE_BURSTS_MS; + return timeBetweenBurstsInMs < maxTimeBetweenBursts + ? Math.min(timeBetweenBurstsInMs * 2, maxTimeBetweenBursts) : timeBetweenBurstsInMs; } /** @@ -122,43 +172,28 @@ public class QueryTaskConfig { if (newTransactionId > UNSIGNED_SHORT_MAX_VALUE) { newTransactionId = 1; } - boolean newExpectUnicastResponse = false; boolean newIsFirstBurst = isFirstBurst; int newQueriesPerBurst = queriesPerBurst; int newBurstCounter = burstCounter + 1; - long newDelayUntilNextTaskWithoutBackoffMs = delayUntilNextTaskWithoutBackoffMs; - int newTimeBetweenBurstsInMs = timeBetweenBurstsInMs; - // Only the first query expects uni-cast response. - if (newBurstCounter == queriesPerBurst) { + final boolean isFirstQueryInBurst = newBurstCounter == 1; + final boolean isLastQueryInBurst = newBurstCounter == queriesPerBurst; + if (isLastQueryInBurst) { newBurstCounter = 0; - - if (alwaysAskForUnicastResponse) { - newExpectUnicastResponse = true; - } - // In passive scan mode, sends a single burst of QUERIES_PER_BURST queries, and - // then in each TIME_BETWEEN_BURSTS interval, sends QUERIES_PER_BURST_PASSIVE_MODE - // queries. if (isFirstBurst) { newIsFirstBurst = false; + // In passive scan mode, sends a single burst of QUERIES_PER_BURST queries, and + // then in each TIME_BETWEEN_BURSTS interval, sends QUERIES_PER_BURST_PASSIVE_MODE + // queries. if (queryMode == PASSIVE_QUERY_MODE) { newQueriesPerBurst = QUERIES_PER_BURST_PASSIVE_MODE; } } - // In active scan mode, sends a burst of QUERIES_PER_BURST queries, - // TIME_BETWEEN_QUERIES_IN_BURST_MS apart, then waits for the scan interval, and - // then repeats. The scan interval starts as INITIAL_TIME_BETWEEN_BURSTS_MS and - // doubles until it maxes out at TIME_BETWEEN_BURSTS_MS. - newDelayUntilNextTaskWithoutBackoffMs = timeBetweenBurstsInMs; - if (timeBetweenBurstsInMs < TIME_BETWEEN_BURSTS_MS) { - newTimeBetweenBurstsInMs = Math.min(timeBetweenBurstsInMs * 2, - TIME_BETWEEN_BURSTS_MS); - } - } else { - newDelayUntilNextTaskWithoutBackoffMs = TIME_BETWEEN_QUERIES_IN_BURST_MS; } + return new QueryTaskConfig(this, newQueryCount, newTransactionId, - newExpectUnicastResponse, newIsFirstBurst, newBurstCounter, newQueriesPerBurst, - newTimeBetweenBurstsInMs, newDelayUntilNextTaskWithoutBackoffMs); + getNextExpectUnicastResponse(isLastQueryInBurst), newIsFirstBurst, newBurstCounter, + newQueriesPerBurst, getNextTimeBetweenBurstsMs(isLastQueryInBurst), + getDelayUntilNextTaskWithoutBackoff(isFirstQueryInBurst, isLastQueryInBurst)); } /** diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java index a8cfe63b99..6124c59f19 100644 --- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java +++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java @@ -17,8 +17,12 @@ package com.android.server.connectivity.mdns; import static com.android.server.connectivity.mdns.MdnsSearchOptions.ACTIVE_QUERY_MODE; +import static com.android.server.connectivity.mdns.MdnsSearchOptions.AGGRESSIVE_QUERY_MODE; import static com.android.server.connectivity.mdns.MdnsSearchOptions.PASSIVE_QUERY_MODE; import static com.android.server.connectivity.mdns.MdnsServiceTypeClient.EVENT_START_QUERYTASK; +import static com.android.server.connectivity.mdns.QueryTaskConfig.INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS; +import static com.android.server.connectivity.mdns.QueryTaskConfig.MAX_TIME_BETWEEN_AGGRESSIVE_BURSTS_MS; +import static com.android.server.connectivity.mdns.QueryTaskConfig.TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS; import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2; import static org.junit.Assert.assertArrayEquals; @@ -147,8 +151,8 @@ public class MdnsServiceTypeClientTests { MockitoAnnotations.initMocks(this); doReturn(TEST_ELAPSED_REALTIME).when(mockDecoderClock).elapsedRealtime(); - expectedIPv4Packets = new DatagramPacket[16]; - expectedIPv6Packets = new DatagramPacket[16]; + expectedIPv4Packets = new DatagramPacket[24]; + expectedIPv6Packets = new DatagramPacket[24]; socketKey = new SocketKey(mockNetwork, INTERFACE_INDEX); for (int i = 0; i < expectedIPv4Packets.length; ++i) { @@ -173,7 +177,15 @@ public class MdnsServiceTypeClientTests { .thenReturn(expectedIPv4Packets[12]) .thenReturn(expectedIPv4Packets[13]) .thenReturn(expectedIPv4Packets[14]) - .thenReturn(expectedIPv4Packets[15]); + .thenReturn(expectedIPv4Packets[15]) + .thenReturn(expectedIPv4Packets[16]) + .thenReturn(expectedIPv4Packets[17]) + .thenReturn(expectedIPv4Packets[18]) + .thenReturn(expectedIPv4Packets[19]) + .thenReturn(expectedIPv4Packets[20]) + .thenReturn(expectedIPv4Packets[21]) + .thenReturn(expectedIPv4Packets[22]) + .thenReturn(expectedIPv4Packets[23]); when(mockPacketWriter.getPacket(IPV6_ADDRESS)) .thenReturn(expectedIPv6Packets[0]) @@ -191,7 +203,15 @@ public class MdnsServiceTypeClientTests { .thenReturn(expectedIPv6Packets[12]) .thenReturn(expectedIPv6Packets[13]) .thenReturn(expectedIPv6Packets[14]) - .thenReturn(expectedIPv6Packets[15]); + .thenReturn(expectedIPv6Packets[15]) + .thenReturn(expectedIPv6Packets[16]) + .thenReturn(expectedIPv6Packets[17]) + .thenReturn(expectedIPv6Packets[18]) + .thenReturn(expectedIPv6Packets[19]) + .thenReturn(expectedIPv6Packets[20]) + .thenReturn(expectedIPv6Packets[21]) + .thenReturn(expectedIPv6Packets[22]) + .thenReturn(expectedIPv6Packets[23]); thread = new HandlerThread("MdnsServiceTypeClientTests"); thread.start(); @@ -1672,6 +1692,118 @@ public class MdnsServiceTypeClientTests { socketKey); } + private int getBetweenBurstTime(int burstCounter, int currentBetweenTime, int maxBetweenTime, + int initialBetweenTime) { + return currentBetweenTime < maxBetweenTime + ? Math.min(initialBetweenTime * (int) Math.pow(2, burstCounter), maxBetweenTime) + : currentBetweenTime; + } + + @Test + public void sendQueries_aggressiveScanMode() { + final MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder() + .addSubtype(SUBTYPE).setQueryMode(AGGRESSIVE_QUERY_MODE).build(); + startSendAndReceive(mockListenerOne, searchOptions); + // Always try to remove the task. + verify(mockDeps, times(1)).removeMessages(any(), eq(EVENT_START_QUERYTASK)); + + int burstCounter = 0; + int betweenBurstTime = 0; + for (int i = 0; i < expectedIPv4Packets.length; i += 3) { + verifyAndSendQuery(i, betweenBurstTime, /* expectsUnicastResponse= */ true); + verifyAndSendQuery(i + 1, /* timeInMs= */ 0, /* expectsUnicastResponse= */ false); + verifyAndSendQuery(i + 2, TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS, + /* expectsUnicastResponse= */ false); + betweenBurstTime = getBetweenBurstTime(burstCounter, betweenBurstTime, + MAX_TIME_BETWEEN_AGGRESSIVE_BURSTS_MS, + INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS); + burstCounter++; + } + // Verify that Task is not removed before stopSendAndReceive was called. + verify(mockDeps, times(1)).removeMessages(any(), eq(EVENT_START_QUERYTASK)); + + // Stop sending packets. + stopSendAndReceive(mockListenerOne); + verify(mockDeps, times(2)).removeMessages(any(), eq(EVENT_START_QUERYTASK)); + } + + @Test + public void sendQueries_reentry_aggressiveScanMode() { + final MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder() + .addSubtype(SUBTYPE).setQueryMode(AGGRESSIVE_QUERY_MODE).build(); + startSendAndReceive(mockListenerOne, searchOptions); + // Always try to remove the task. + verify(mockDeps, times(1)).removeMessages(any(), eq(EVENT_START_QUERYTASK)); + + // First burst, first query is sent. + verifyAndSendQuery(0, /* timeInMs= */ 0, /* expectsUnicastResponse= */ true); + + // After the first query is sent, change the subtypes, and restart. + final MdnsSearchOptions searchOptions2 = MdnsSearchOptions.newBuilder().addSubtype(SUBTYPE) + .addSubtype("_subtype2").setQueryMode(AGGRESSIVE_QUERY_MODE).build(); + startSendAndReceive(mockListenerOne, searchOptions2); + // The previous scheduled task should be canceled. + verify(mockDeps, times(2)).removeMessages(any(), eq(EVENT_START_QUERYTASK)); + + // Queries should continue to be sent. + verifyAndSendQuery(1, /* timeInMs= */ 0, /* expectsUnicastResponse= */ true); + verifyAndSendQuery(2, /* timeInMs= */ 0, /* expectsUnicastResponse= */ false); + verifyAndSendQuery(3, TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS, + /* expectsUnicastResponse= */ false); + + // Stop sending packets. + stopSendAndReceive(mockListenerOne); + verify(mockDeps, times(3)).removeMessages(any(), eq(EVENT_START_QUERYTASK)); + } + + @Test + public void sendQueries_blendScanWithQueryBackoff() { + final int numOfQueriesBeforeBackoff = 11; + final MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder() + .addSubtype(SUBTYPE) + .setQueryMode(AGGRESSIVE_QUERY_MODE) + .setNumOfQueriesBeforeBackoff(numOfQueriesBeforeBackoff) + .build(); + startSendAndReceive(mockListenerOne, searchOptions); + // Always try to remove the task. + verify(mockDeps, times(1)).removeMessages(any(), eq(EVENT_START_QUERYTASK)); + + int burstCounter = 0; + int betweenBurstTime = 0; + for (int i = 0; i < numOfQueriesBeforeBackoff; i += 3) { + verifyAndSendQuery(i, betweenBurstTime, /* expectsUnicastResponse= */ true); + verifyAndSendQuery(i + 1, /* timeInMs= */ 0, /* expectsUnicastResponse= */ false); + verifyAndSendQuery(i + 2, TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS, + /* expectsUnicastResponse= */ false); + betweenBurstTime = getBetweenBurstTime(burstCounter, betweenBurstTime, + MAX_TIME_BETWEEN_AGGRESSIVE_BURSTS_MS, + INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS); + burstCounter++; + } + // In backoff mode, the current scheduled task will be canceled and reschedule if the + // 0.8 * smallestRemainingTtl is larger than time to next run. + long currentTime = TEST_TTL / 2 + TEST_ELAPSED_REALTIME; + doReturn(currentTime).when(mockDecoderClock).elapsedRealtime(); + doReturn(true).when(mockDeps).hasMessages(any(), eq(EVENT_START_QUERYTASK)); + processResponse(createResponse( + "service-instance-1", "192.0.2.123", 5353, + SERVICE_TYPE_LABELS, + Collections.emptyMap(), TEST_TTL), socketKey); + verify(mockDeps, times(2)).removeMessages(any(), eq(EVENT_START_QUERYTASK)); + assertNotNull(delayMessage); + verifyAndSendQuery(12 /* index */, (long) (TEST_TTL / 2 * 0.8) /* timeInMs */, + true /* expectsUnicastResponse */, true /* multipleSocketDiscovery */, + 14 /* scheduledCount */); + currentTime += (long) (TEST_TTL / 2 * 0.8); + doReturn(currentTime).when(mockDecoderClock).elapsedRealtime(); + verifyAndSendQuery(13 /* index */, 0 /* timeInMs */, + false /* expectsUnicastResponse */, true /* multipleSocketDiscovery */, + 15 /* scheduledCount */); + verifyAndSendQuery(14 /* index */, TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS, + false /* expectsUnicastResponse */, true /* multipleSocketDiscovery */, + 16 /* scheduledCount */); + } + private static MdnsServiceInfo matchServiceName(String name) { return argThat(info -> info.getServiceInstanceName().equals(name)); }