From 11a883dc92a33e47dbef42da5c087de3f3cded56 Mon Sep 17 00:00:00 2001 From: Paul Hu Date: Thu, 14 Dec 2023 07:36:44 +0000 Subject: [PATCH] Add aggressive query mode The current query scan modes, active and passive, have limitations. Unicast queries only originate from the first query session, multicast queries should be sent simultaneously with unicast, and both modes suffer from infrequent scans. To address these shortcomings, a aggressive query mode is proposed. Bug: 271067818 Test: atest FrameworksNetTests NsdManagerTest Change-Id: I87100e03a5b6306dcae79f05c034f000e6dc2889 --- .../src/com/android/server/NsdService.java | 29 +++- .../connectivity/mdns/MdnsFeatureFlags.java | 30 ++++ .../connectivity/mdns/MdnsSearchOptions.java | 4 + .../connectivity/mdns/QueryTaskConfig.java | 93 ++++++++---- .../mdns/MdnsServiceTypeClientTests.java | 140 +++++++++++++++++- 5 files changed, 255 insertions(+), 41 deletions(-) diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java index be0b67353a..77c1ddb1cf 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. @@ -1044,7 +1050,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(); @@ -1142,7 +1150,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(); @@ -1741,7 +1751,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( @@ -1754,18 +1764,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)); }