summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Omair Kamil <okamil@google.com> 2025-01-28 15:52:44 -0800
committer Gerrit Code Review <noreply-gerritcodereview@google.com> 2025-01-28 15:52:44 -0800
commit2e00ab919ab25f60bc02c3e02780634f6c8f6e06 (patch)
treef47673146d178c49cb246ead3be3d509eb29b30a
parentc7a65278f4a470223824a2b18e75fb1f954ec1dc (diff)
parent21de30f151b010b75850a1644de428d66c34cc53 (diff)
Merge "Initial implementation of batch scan throttling." into main
-rw-r--r--android/app/src/com/android/bluetooth/le_scan/ScanController.java4
-rw-r--r--android/app/src/com/android/bluetooth/le_scan/ScanManager.java93
-rw-r--r--android/app/src/com/com/android/bluetooth/le_scan/BatchScanThrottler.java112
-rw-r--r--android/app/tests/unit/src/com/android/bluetooth/le_scan/BatchScanThrottlerTest.java202
-rw-r--r--android/app/tests/unit/src/com/android/bluetooth/le_scan/ScanManagerTest.java10
5 files changed, 374 insertions, 47 deletions
diff --git a/android/app/src/com/android/bluetooth/le_scan/ScanController.java b/android/app/src/com/android/bluetooth/le_scan/ScanController.java
index be2c96f816..7f39fdaaf1 100644
--- a/android/app/src/com/android/bluetooth/le_scan/ScanController.java
+++ b/android/app/src/com/android/bluetooth/le_scan/ScanController.java
@@ -91,7 +91,7 @@ public class ScanController {
private static final int TRUNCATED_RESULT_SIZE = 11;
/** The default floor value for LE batch scan report delays greater than 0 */
- @VisibleForTesting static final long DEFAULT_REPORT_DELAY_FLOOR = 5000;
+ static final long DEFAULT_REPORT_DELAY_FLOOR = 5000L;
private static final int NUM_SCAN_EVENTS_KEPT = 20;
@@ -771,6 +771,7 @@ public class ScanController {
if (app.mCallback != null) {
app.mCallback.onBatchScanResults(permittedResults);
+ mScanManager.batchScanResultDelivered();
} else {
// PendingIntent based
try {
@@ -815,6 +816,7 @@ public class ScanController {
Log.e(TAG, "Exception: " + e);
handleDeadScanClient(client);
}
+ mScanManager.batchScanResultDelivered();
}
// Check and deliver scan results for different scan clients.
diff --git a/android/app/src/com/android/bluetooth/le_scan/ScanManager.java b/android/app/src/com/android/bluetooth/le_scan/ScanManager.java
index 78f759fb13..e946b3afcf 100644
--- a/android/app/src/com/android/bluetooth/le_scan/ScanManager.java
+++ b/android/app/src/com/android/bluetooth/le_scan/ScanManager.java
@@ -146,6 +146,7 @@ public class ScanManager {
@VisibleForTesting boolean mIsConnecting;
@VisibleForTesting int mProfilesConnecting;
private int mProfilesConnected, mProfilesDisconnecting;
+ private final BatchScanThrottler mBatchScanThrottler;
@VisibleForTesting
static class UidImportance {
@@ -158,7 +159,7 @@ public class ScanManager {
}
}
- public ScanManager(
+ ScanManager(
AdapterService adapterService,
ScanController scanController,
BluetoothAdapterProxy bluetoothAdapterProxy,
@@ -202,9 +203,10 @@ public class ScanManager {
IntentFilter locationIntentFilter = new IntentFilter(LocationManager.MODE_CHANGED_ACTION);
locationIntentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
mAdapterService.registerReceiver(mLocationReceiver, locationIntentFilter);
+ mBatchScanThrottler = new BatchScanThrottler(timeProvider, mScreenOn);
}
- public void cleanup() {
+ void cleanup() {
mRegularScanClients.clear();
mBatchClients.clear();
mSuspendedScanClients.clear();
@@ -233,16 +235,16 @@ public class ScanManager {
}
}
- public void registerScanner(UUID uuid) {
+ void registerScanner(UUID uuid) {
mScanNative.registerScanner(uuid.getLeastSignificantBits(), uuid.getMostSignificantBits());
}
- public void unregisterScanner(int scannerId) {
+ void unregisterScanner(int scannerId) {
mScanNative.unregisterScanner(scannerId);
}
/** Returns the regular scan queue. */
- public Set<ScanClient> getRegularScanQueue() {
+ Set<ScanClient> getRegularScanQueue() {
return mRegularScanClients;
}
@@ -252,12 +254,12 @@ public class ScanManager {
}
/** Returns batch scan queue. */
- public Set<ScanClient> getBatchScanQueue() {
+ Set<ScanClient> getBatchScanQueue() {
return mBatchClients;
}
/** Returns a set of full batch scan clients. */
- public Set<ScanClient> getFullBatchScanQueue() {
+ Set<ScanClient> getFullBatchScanQueue() {
// TODO: split full batch scan clients and truncated batch clients so we don't need to
// construct this every time.
Set<ScanClient> fullBatchClients = new HashSet<ScanClient>();
@@ -269,12 +271,12 @@ public class ScanManager {
return fullBatchClients;
}
- public void startScan(ScanClient client) {
+ void startScan(ScanClient client) {
Log.d(TAG, "startScan() " + client);
sendMessage(MSG_START_BLE_SCAN, client);
}
- public void stopScan(int scannerId) {
+ void stopScan(int scannerId) {
ScanClient client = mScanNative.getBatchScanClient(scannerId);
if (client == null) {
client = mScanNative.getRegularScanClient(scannerId);
@@ -285,14 +287,18 @@ public class ScanManager {
sendMessage(MSG_STOP_BLE_SCAN, client);
}
- public void flushBatchScanResults(ScanClient client) {
+ void flushBatchScanResults(ScanClient client) {
sendMessage(MSG_FLUSH_BATCH_RESULTS, client);
}
- public void callbackDone(int scannerId, int status) {
+ void callbackDone(int scannerId, int status) {
mScanNative.callbackDone(scannerId, status);
}
+ void batchScanResultDelivered() {
+ mBatchScanThrottler.resetBackoff();
+ }
+
private void sendMessage(int what, ScanClient client) {
mHandler.obtainMessage(what, client).sendToTarget();
}
@@ -305,10 +311,16 @@ public class ScanManager {
return mBluetoothAdapterProxy.isOffloadedScanFilteringSupported();
}
- public boolean isAutoBatchScanClientEnabled(ScanClient client) {
+ boolean isAutoBatchScanClientEnabled(ScanClient client) {
return mScanNative.isAutoBatchScanClientEnabled(client);
}
+ int getCurrentUsedTrackingAdvertisement() {
+ synchronized (mCurUsedTrackableAdvertisementsLock) {
+ return mCurUsedTrackableAdvertisements;
+ }
+ }
+
// Handler class that handles BLE scan operations.
@VisibleForTesting
class ClientHandler extends Handler {
@@ -534,6 +546,7 @@ public class ScanManager {
}
mScreenOn = false;
Log.d(TAG, "handleScreenOff()");
+ mBatchScanThrottler.onScreenOn(false);
handleSuspendScans();
updateRegularScanClientsScreenOff();
updateRegularScanToBatchScanClients();
@@ -864,6 +877,7 @@ public class ScanManager {
}
mScreenOn = true;
Log.d(TAG, "handleScreenOn()");
+ mBatchScanThrottler.onScreenOn(true);
updateBatchScanToRegularScanClients();
handleResumeScans();
updateRegularScanClientsScreenOn();
@@ -921,14 +935,14 @@ public class ScanManager {
/** Parameters for batch scans. */
static class BatchScanParams {
- public int scanMode;
- public int fullScanscannerId;
- public int truncatedScanscannerId;
+ @VisibleForTesting int mScanMode;
+ private int mFullScanscannerId;
+ private int mTruncatedScanscannerId;
BatchScanParams() {
- scanMode = -1;
- fullScanscannerId = -1;
- truncatedScanscannerId = -1;
+ mScanMode = -1;
+ mFullScanscannerId = -1;
+ mTruncatedScanscannerId = -1;
}
@Override
@@ -939,20 +953,14 @@ public class ScanManager {
if (!(obj instanceof BatchScanParams other)) {
return false;
}
- return scanMode == other.scanMode
- && fullScanscannerId == other.fullScanscannerId
- && truncatedScanscannerId == other.truncatedScanscannerId;
+ return mScanMode == other.mScanMode
+ && mFullScanscannerId == other.mFullScanscannerId
+ && mTruncatedScanscannerId == other.mTruncatedScanscannerId;
}
@Override
public int hashCode() {
- return Objects.hash(scanMode, fullScanscannerId, truncatedScanscannerId);
- }
- }
-
- public int getCurrentUsedTrackingAdvertisement() {
- synchronized (mCurUsedTrackableAdvertisementsLock) {
- return mCurUsedTrackableAdvertisements;
+ return Objects.hash(mScanMode, mFullScanscannerId, mTruncatedScanscannerId);
}
}
@@ -1262,9 +1270,9 @@ public class ScanManager {
waitForCallback();
resetCountDownLatch();
int scanInterval =
- Utils.millsToUnit(getBatchScanIntervalMillis(batchScanParams.scanMode));
+ Utils.millsToUnit(getBatchScanIntervalMillis(batchScanParams.mScanMode));
int scanWindow =
- Utils.millsToUnit(getBatchScanWindowMillis(batchScanParams.scanMode));
+ Utils.millsToUnit(getBatchScanWindowMillis(batchScanParams.mScanMode));
mNativeInterface.gattClientStartBatchScan(
scannerId,
resultType,
@@ -1298,15 +1306,15 @@ public class ScanManager {
BatchScanParams params = new BatchScanParams();
ScanClient winner = getAggressiveClient(mBatchClients);
if (winner != null) {
- params.scanMode = winner.settings.getScanMode();
+ params.mScanMode = winner.settings.getScanMode();
}
// TODO: split full batch scan results and truncated batch scan results to different
// collections.
for (ScanClient client : mBatchClients) {
if (client.settings.getScanResultType() == ScanSettings.SCAN_RESULT_TYPE_FULL) {
- params.fullScanscannerId = client.scannerId;
+ params.mFullScanscannerId = client.scannerId;
} else {
- params.truncatedScanscannerId = client.scannerId;
+ params.mTruncatedScanscannerId = client.scannerId;
}
}
return params;
@@ -1359,7 +1367,10 @@ public class ScanManager {
if (mBatchClients.isEmpty()) {
return;
}
- long batchTriggerIntervalMillis = getBatchTriggerIntervalMillis();
+ long batchTriggerIntervalMillis =
+ Flags.batchScanOptimization()
+ ? mBatchScanThrottler.getBatchTriggerIntervalMillis(mBatchClients)
+ : getBatchTriggerIntervalMillis();
// Allows the alarm to be triggered within
// [batchTriggerIntervalMillis, 1.1 * batchTriggerIntervalMillis]
long windowLengthMillis = batchTriggerIntervalMillis / 10;
@@ -1494,16 +1505,16 @@ public class ScanManager {
void flushBatchResults(int scannerId) {
Log.d(TAG, "flushPendingBatchResults - scannerId = " + scannerId);
- if (mBatchScanParams.fullScanscannerId != -1) {
+ if (mBatchScanParams.mFullScanscannerId != -1) {
resetCountDownLatch();
mNativeInterface.gattClientReadScanReports(
- mBatchScanParams.fullScanscannerId, SCAN_RESULT_TYPE_FULL);
+ mBatchScanParams.mFullScanscannerId, SCAN_RESULT_TYPE_FULL);
waitForCallback();
}
- if (mBatchScanParams.truncatedScanscannerId != -1) {
+ if (mBatchScanParams.mTruncatedScanscannerId != -1) {
resetCountDownLatch();
mNativeInterface.gattClientReadScanReports(
- mBatchScanParams.truncatedScanscannerId, SCAN_RESULT_TYPE_TRUNCATED);
+ mBatchScanParams.mTruncatedScanscannerId, SCAN_RESULT_TYPE_TRUNCATED);
waitForCallback();
}
setBatchAlarm();
@@ -1662,13 +1673,13 @@ public class ScanManager {
/** Return batch scan result type value defined in bt stack. */
private int getResultType(BatchScanParams params) {
- if (params.fullScanscannerId != -1 && params.truncatedScanscannerId != -1) {
+ if (params.mFullScanscannerId != -1 && params.mTruncatedScanscannerId != -1) {
return SCAN_RESULT_TYPE_BOTH;
}
- if (params.truncatedScanscannerId != -1) {
+ if (params.mTruncatedScanscannerId != -1) {
return SCAN_RESULT_TYPE_TRUNCATED;
}
- if (params.fullScanscannerId != -1) {
+ if (params.mFullScanscannerId != -1) {
return SCAN_RESULT_TYPE_FULL;
}
return -1;
diff --git a/android/app/src/com/com/android/bluetooth/le_scan/BatchScanThrottler.java b/android/app/src/com/com/android/bluetooth/le_scan/BatchScanThrottler.java
new file mode 100644
index 0000000000..4fd324dcf9
--- /dev/null
+++ b/android/app/src/com/com/android/bluetooth/le_scan/BatchScanThrottler.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.bluetooth.le_scan;
+
+import static com.android.bluetooth.le_scan.ScanController.DEFAULT_REPORT_DELAY_FLOOR;
+
+import android.provider.DeviceConfig;
+
+import com.android.bluetooth.Utils.TimeProvider;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Set;
+
+/**
+ * Throttler to reduce the number of times the Bluetooth process wakes up to check for pending batch
+ * scan results. The wake-up intervals are increased when no matching results are found and are
+ * longer when the screen is off.
+ */
+class BatchScanThrottler {
+ // Minimum batch trigger interval to check for batched results when the screen is off
+ @VisibleForTesting static final long SCREEN_OFF_MINIMUM_DELAY_FLOOR_MS = 20000L;
+ // Adjusted minimum report delay for unfiltered batch scan clients
+ @VisibleForTesting static final long UNFILTERED_DELAY_FLOOR_MS = 20000L;
+ // Adjusted minimum report delay for unfiltered batch scan clients when the screen is off
+ @VisibleForTesting static final long UNFILTERED_SCREEN_OFF_DELAY_FLOOR_MS = 60000L;
+ // Backoff stages used as multipliers for the minimum delay floor (standard or screen-off)
+ @VisibleForTesting static final int[] BACKOFF_MULTIPLIERS = {1, 1, 2, 2, 4};
+ // Start screen-off trigger interval throttling after the screen has been off for this period
+ // of time. This allows the screen-on intervals to be used for a short period of time after the
+ // screen has gone off, and avoids too much flipping between screen-off and screen-on backoffs
+ // when the screen is off for a short period of time
+ @VisibleForTesting static final long SCREEN_OFF_DELAY_MS = 60000L;
+ private final TimeProvider mTimeProvider;
+ private final long mDelayFloor;
+ private final long mScreenOffDelayFloor;
+ private int mBackoffStage = 0;
+ private long mScreenOffTriggerTime = 0L;
+ private boolean mScreenOffThrottling = false;
+
+ BatchScanThrottler(TimeProvider timeProvider, boolean screenOn) {
+ mTimeProvider = timeProvider;
+ mDelayFloor =
+ DeviceConfig.getLong(
+ DeviceConfig.NAMESPACE_BLUETOOTH,
+ "report_delay",
+ DEFAULT_REPORT_DELAY_FLOOR);
+ mScreenOffDelayFloor = Math.max(mDelayFloor, SCREEN_OFF_MINIMUM_DELAY_FLOOR_MS);
+ onScreenOn(screenOn);
+ }
+
+ void resetBackoff() {
+ mBackoffStage = 0;
+ }
+
+ void onScreenOn(boolean screenOn) {
+ if (screenOn) {
+ mScreenOffTriggerTime = 0L;
+ mScreenOffThrottling = false;
+ resetBackoff();
+ } else {
+ // Screen-off intervals to be used after the trigger time
+ mScreenOffTriggerTime = mTimeProvider.elapsedRealtime() + SCREEN_OFF_DELAY_MS;
+ }
+ }
+
+ long getBatchTriggerIntervalMillis(Set<ScanClient> batchClients) {
+ // Check if we're past the screen-off time and should be using screen-off backoff values
+ if (!mScreenOffThrottling
+ && mScreenOffTriggerTime != 0
+ && mTimeProvider.elapsedRealtime() >= mScreenOffTriggerTime) {
+ mScreenOffThrottling = true;
+ resetBackoff();
+ }
+ long unfilteredFloor =
+ mScreenOffThrottling
+ ? UNFILTERED_SCREEN_OFF_DELAY_FLOOR_MS
+ : UNFILTERED_DELAY_FLOOR_MS;
+ long intervalMillis = Long.MAX_VALUE;
+ for (ScanClient client : batchClients) {
+ if (client.settings.getReportDelayMillis() > 0) {
+ long clientIntervalMillis = client.settings.getReportDelayMillis();
+ if ((client.filters == null || client.filters.isEmpty())
+ && clientIntervalMillis < unfilteredFloor) {
+ clientIntervalMillis = unfilteredFloor;
+ }
+ intervalMillis = Math.min(intervalMillis, clientIntervalMillis);
+ }
+ }
+ int backoffIndex =
+ mBackoffStage >= BACKOFF_MULTIPLIERS.length
+ ? BACKOFF_MULTIPLIERS.length - 1
+ : mBackoffStage++;
+ return Math.max(
+ intervalMillis,
+ (mScreenOffThrottling ? mScreenOffDelayFloor : mDelayFloor)
+ * BACKOFF_MULTIPLIERS[backoffIndex]);
+ }
+}
diff --git a/android/app/tests/unit/src/com/android/bluetooth/le_scan/BatchScanThrottlerTest.java b/android/app/tests/unit/src/com/android/bluetooth/le_scan/BatchScanThrottlerTest.java
new file mode 100644
index 0000000000..67f32c4f0c
--- /dev/null
+++ b/android/app/tests/unit/src/com/android/bluetooth/le_scan/BatchScanThrottlerTest.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.bluetooth.le_scan;
+
+import static android.bluetooth.le.ScanSettings.SCAN_MODE_BALANCED;
+
+import static com.android.bluetooth.le_scan.ScanController.DEFAULT_REPORT_DELAY_FLOOR;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.bluetooth.le.ScanFilter;
+import android.bluetooth.le.ScanSettings;
+import android.platform.test.flag.junit.SetFlagsRule;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.bluetooth.TestUtils.FakeTimeProvider;
+
+import com.google.testing.junit.testparameterinjector.TestParameter;
+import com.google.testing.junit.testparameterinjector.TestParameterInjector;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.time.Duration;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.LongStream;
+
+/** Test cases for {@link BatchScanThrottler}. */
+@SmallTest
+@RunWith(TestParameterInjector.class)
+public class BatchScanThrottlerTest {
+ @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+ @Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
+
+ private FakeTimeProvider mTimeProvider;
+
+ @Before
+ public void setUp() {
+ mTimeProvider = new FakeTimeProvider();
+ }
+
+ private void advanceTime(long amountToAdvanceMillis) {
+ mTimeProvider.advanceTime(Duration.ofMillis(amountToAdvanceMillis));
+ }
+
+ @Test
+ public void basicThrottling(
+ @TestParameter boolean isFiltered, @TestParameter boolean isScreenOn) {
+ BatchScanThrottler throttler = new BatchScanThrottler(mTimeProvider, isScreenOn);
+ if (!isScreenOn) {
+ advanceTime(BatchScanThrottler.SCREEN_OFF_DELAY_MS);
+ }
+ Set<ScanClient> clients =
+ Collections.singleton(
+ createBatchScanClient(DEFAULT_REPORT_DELAY_FLOOR, isFiltered));
+ long[] backoffIntervals =
+ getBackoffIntervals(
+ isScreenOn
+ ? DEFAULT_REPORT_DELAY_FLOOR
+ : BatchScanThrottler.SCREEN_OFF_MINIMUM_DELAY_FLOOR_MS);
+ for (long x : backoffIntervals) {
+ long expected = adjustExpectedInterval(x, isFiltered, isScreenOn);
+ assertThat(throttler.getBatchTriggerIntervalMillis(clients)).isEqualTo(expected);
+ }
+ long expected =
+ adjustExpectedInterval(
+ backoffIntervals[backoffIntervals.length - 1], isFiltered, isScreenOn);
+ // Ensure that subsequent calls continue to return the final throttled interval
+ assertThat(throttler.getBatchTriggerIntervalMillis(clients)).isEqualTo(expected);
+ assertThat(throttler.getBatchTriggerIntervalMillis(clients)).isEqualTo(expected);
+ }
+
+ @Test
+ public void screenOffDelayAndReset(@TestParameter boolean screenOnAtStart) {
+ BatchScanThrottler throttler = new BatchScanThrottler(mTimeProvider, screenOnAtStart);
+ if (screenOnAtStart) {
+ throttler.onScreenOn(false);
+ }
+ Set<ScanClient> clients =
+ Collections.singleton(createBatchScanClient(DEFAULT_REPORT_DELAY_FLOOR, true));
+ long[] backoffIntervals = getBackoffIntervals(DEFAULT_REPORT_DELAY_FLOOR);
+ advanceTime(BatchScanThrottler.SCREEN_OFF_DELAY_MS - 1);
+ for (long x : backoffIntervals) {
+ assertThat(throttler.getBatchTriggerIntervalMillis(clients)).isEqualTo(x);
+ }
+
+ backoffIntervals =
+ getBackoffIntervals(BatchScanThrottler.SCREEN_OFF_MINIMUM_DELAY_FLOOR_MS);
+ advanceTime(1);
+ for (long x : backoffIntervals) {
+ assertThat(throttler.getBatchTriggerIntervalMillis(clients)).isEqualTo(x);
+ }
+ assertThat(throttler.getBatchTriggerIntervalMillis(clients))
+ .isEqualTo(backoffIntervals[backoffIntervals.length - 1]);
+ }
+
+ @Test
+ public void testScreenOnReset() {
+ BatchScanThrottler throttler = new BatchScanThrottler(mTimeProvider, false);
+ advanceTime(BatchScanThrottler.SCREEN_OFF_DELAY_MS);
+ Set<ScanClient> clients =
+ Collections.singleton(createBatchScanClient(DEFAULT_REPORT_DELAY_FLOOR, true));
+ long[] backoffIntervals =
+ getBackoffIntervals(BatchScanThrottler.SCREEN_OFF_MINIMUM_DELAY_FLOOR_MS);
+ for (long x : backoffIntervals) {
+ assertThat(throttler.getBatchTriggerIntervalMillis(clients)).isEqualTo(x);
+ }
+
+ throttler.onScreenOn(true);
+ backoffIntervals = getBackoffIntervals(DEFAULT_REPORT_DELAY_FLOOR);
+ for (long x : backoffIntervals) {
+ assertThat(throttler.getBatchTriggerIntervalMillis(clients)).isEqualTo(x);
+ }
+ assertThat(throttler.getBatchTriggerIntervalMillis(clients))
+ .isEqualTo(backoffIntervals[backoffIntervals.length - 1]);
+ }
+
+ @Test
+ public void resetBackoff_restartsToFirstStage(@TestParameter boolean isScreenOn) {
+ BatchScanThrottler throttler = new BatchScanThrottler(mTimeProvider, isScreenOn);
+ if (!isScreenOn) {
+ // Advance the time before we start the test to when the screen-off intervals should be
+ // used
+ advanceTime(BatchScanThrottler.SCREEN_OFF_DELAY_MS);
+ }
+ Set<ScanClient> clients =
+ Collections.singleton(createBatchScanClient(DEFAULT_REPORT_DELAY_FLOOR, true));
+ long[] backoffIntervals =
+ getBackoffIntervals(
+ isScreenOn
+ ? DEFAULT_REPORT_DELAY_FLOOR
+ : BatchScanThrottler.SCREEN_OFF_MINIMUM_DELAY_FLOOR_MS);
+ for (long x : backoffIntervals) {
+ assertThat(throttler.getBatchTriggerIntervalMillis(clients)).isEqualTo(x);
+ }
+ assertThat(throttler.getBatchTriggerIntervalMillis(clients))
+ .isEqualTo(backoffIntervals[backoffIntervals.length - 1]);
+
+ throttler.resetBackoff();
+ for (long x : backoffIntervals) {
+ assertThat(throttler.getBatchTriggerIntervalMillis(clients)).isEqualTo(x);
+ }
+ assertThat(throttler.getBatchTriggerIntervalMillis(clients))
+ .isEqualTo(backoffIntervals[backoffIntervals.length - 1]);
+ }
+
+ private long adjustExpectedInterval(long interval, boolean isFiltered, boolean isScreenOn) {
+ if (isFiltered) {
+ return interval;
+ }
+ long threshold =
+ isScreenOn
+ ? BatchScanThrottler.UNFILTERED_DELAY_FLOOR_MS
+ : BatchScanThrottler.UNFILTERED_SCREEN_OFF_DELAY_FLOOR_MS;
+ return Math.max(interval, threshold);
+ }
+
+ private long[] getBackoffIntervals(long baseInterval) {
+ return LongStream.range(0, BatchScanThrottler.BACKOFF_MULTIPLIERS.length)
+ .map(x -> BatchScanThrottler.BACKOFF_MULTIPLIERS[(int) x] * baseInterval)
+ .toArray();
+ }
+
+ private ScanClient createBatchScanClient(long reportDelayMillis, boolean isFiltered) {
+ ScanSettings scanSettings =
+ new ScanSettings.Builder()
+ .setScanMode(SCAN_MODE_BALANCED)
+ .setReportDelay(reportDelayMillis)
+ .build();
+
+ return new ScanClient(1, scanSettings, createScanFilterList(isFiltered), 1);
+ }
+
+ private List<ScanFilter> createScanFilterList(boolean isFiltered) {
+ List<ScanFilter> scanFilterList = null;
+ if (isFiltered) {
+ scanFilterList = List.of(new ScanFilter.Builder().setDeviceName("TestName").build());
+ }
+ return scanFilterList;
+ }
+}
diff --git a/android/app/tests/unit/src/com/android/bluetooth/le_scan/ScanManagerTest.java b/android/app/tests/unit/src/com/android/bluetooth/le_scan/ScanManagerTest.java
index 25a27ce876..041a982a9b 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/le_scan/ScanManagerTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/le_scan/ScanManagerTest.java
@@ -1045,7 +1045,7 @@ public class ScanManagerTest {
assertThat(mScanManager.getRegularScanQueue()).doesNotContain(client);
assertThat(mScanManager.getSuspendedScanQueue()).doesNotContain(client);
assertThat(mScanManager.getBatchScanQueue()).contains(client);
- assertThat(mScanManager.getBatchScanParams().scanMode).isEqualTo(expectedScanMode);
+ assertThat(mScanManager.getBatchScanParams().mScanMode).isEqualTo(expectedScanMode);
}
}
@@ -1075,13 +1075,13 @@ public class ScanManagerTest {
sendMessageWaitForProcessed(createStartStopScanMessage(true, client));
assertThat(mScanManager.getRegularScanQueue()).doesNotContain(client);
assertThat(mScanManager.getSuspendedScanQueue()).doesNotContain(client);
- assertThat(mScanManager.getBatchScanParams().scanMode).isEqualTo(expectedScanMode);
+ assertThat(mScanManager.getBatchScanParams().mScanMode).isEqualTo(expectedScanMode);
// Turn on screen
sendMessageWaitForProcessed(createScreenOnOffMessage(true));
assertThat(mScanManager.getRegularScanQueue()).doesNotContain(client);
assertThat(mScanManager.getSuspendedScanQueue()).doesNotContain(client);
assertThat(mScanManager.getBatchScanQueue()).contains(client);
- assertThat(mScanManager.getBatchScanParams().scanMode).isEqualTo(expectedScanMode);
+ assertThat(mScanManager.getBatchScanParams().mScanMode).isEqualTo(expectedScanMode);
}
}
@@ -1152,7 +1152,7 @@ public class ScanManagerTest {
assertThat(mScanManager.getRegularScanQueue()).doesNotContain(client);
assertThat(mScanManager.getSuspendedScanQueue()).doesNotContain(client);
assertThat(mScanManager.getBatchScanQueue()).contains(client);
- assertThat(mScanManager.getBatchScanParams().scanMode)
+ assertThat(mScanManager.getBatchScanParams().mScanMode)
.isEqualTo(expectedScanMode);
// Turn on screen
sendMessageWaitForProcessed(createScreenOnOffMessage(true));
@@ -1166,7 +1166,7 @@ public class ScanManagerTest {
assertThat(mScanManager.getRegularScanQueue()).doesNotContain(client);
assertThat(mScanManager.getSuspendedScanQueue()).doesNotContain(client);
assertThat(mScanManager.getBatchScanQueue()).contains(client);
- assertThat(mScanManager.getBatchScanParams().scanMode)
+ assertThat(mScanManager.getBatchScanParams().mScanMode)
.isEqualTo(expectedScanMode);
});
}