Loading android/app/src/com/android/bluetooth/btservice/MetricsLogger.java +17 −1 Original line number Diff line number Diff line Loading @@ -24,6 +24,8 @@ import com.android.bluetooth.BluetoothMetricsProto.BluetoothLog; import com.android.bluetooth.BluetoothMetricsProto.ProfileConnectionStats; import com.android.bluetooth.BluetoothMetricsProto.ProfileId; import com.android.bluetooth.BluetoothStatsLog; import com.android.bluetooth.Utils; import com.android.internal.annotations.VisibleForTesting; import com.google.common.hash.BloomFilter; import com.google.common.hash.Funnels; Loading Loading @@ -54,7 +56,7 @@ public class MetricsLogger { private static final HashMap<ProfileId, Integer> sProfileConnectionCounts = new HashMap<>(); HashMap<Integer, Long> mCounters = new HashMap<>(); private static MetricsLogger sInstance = null; private static volatile MetricsLogger sInstance = null; private Context mContext = null; private AlarmManager mAlarmManager = null; private boolean mInitialized = false; Loading @@ -81,6 +83,20 @@ public class MetricsLogger { return sInstance; } /** * Allow unit tests to substitute MetricsLogger with a test instance * * @param instance a test instance of the MetricsLogger */ @VisibleForTesting public static void setInstanceForTesting(MetricsLogger instance) { Utils.enforceInstrumentationTestMode(); synchronized (mLock) { Log.d(TAG, "setInstanceForTesting(), set to " + instance); sInstance = instance; } } public boolean isInitialized() { return mInitialized; } Loading android/app/src/com/android/bluetooth/gatt/AppScanStats.java +98 −1 Original line number Diff line number Diff line Loading @@ -15,6 +15,7 @@ */ package com.android.bluetooth.gatt; import android.bluetooth.BluetoothProtoEnums; import android.bluetooth.le.ScanFilter; import android.bluetooth.le.ScanSettings; import android.os.BatteryStatsManager; Loading @@ -25,7 +26,9 @@ import android.os.WorkSource; import com.android.bluetooth.BluetoothMetricsProto; import com.android.bluetooth.BluetoothStatsLog; import com.android.bluetooth.btservice.AdapterService; import com.android.bluetooth.btservice.MetricsLogger; import com.android.bluetooth.util.WorkSourceUtil; import com.android.internal.annotations.GuardedBy; import java.text.DateFormat; import java.text.SimpleDateFormat; Loading @@ -49,8 +52,9 @@ import java.util.Objects; // Weight is the duty cycle of the scan mode static final int OPPORTUNISTIC_WEIGHT = 0; static final int SCREEN_OFF_LOW_POWER_WEIGHT = 5; static final int LOW_POWER_WEIGHT = 10; static final int AMBIENT_DISCOVERY_WEIGHT = 20; static final int AMBIENT_DISCOVERY_WEIGHT = 25; static final int BALANCED_WEIGHT = 25; static final int LOW_LATENCY_WEIGHT = 100; Loading @@ -67,6 +71,13 @@ import java.util.Objects; private final AdapterService mAdapterService; private static Object sLock = new Object(); @GuardedBy("sLock") static long sRadioStartTime = 0; static int sRadioScanMode; static boolean sIsRadioStarted = false; static boolean sIsScreenOn = false; class LastScan { public long duration; public long suspendDuration; Loading Loading @@ -329,6 +340,92 @@ import java.util.Objects; scan.isFilterScan, scan.isBackgroundScan, scan.isOpportunisticScan); } static void initScanRadioState() { synchronized (sLock) { sIsRadioStarted = false; } } static boolean recordScanRadioStart(int scanMode) { synchronized (sLock) { if (sIsRadioStarted) { return false; } sRadioStartTime = SystemClock.elapsedRealtime(); sRadioScanMode = scanMode; sIsRadioStarted = true; } return true; } static boolean recordScanRadioStop() { synchronized (sLock) { if (!sIsRadioStarted) { return false; } recordScanRadioDurationMetrics(); sRadioStartTime = 0; sIsRadioStarted = false; } return true; } @GuardedBy("sLock") private static void recordScanRadioDurationMetrics() { if (!sIsRadioStarted) { return; } long currentTime = SystemClock.elapsedRealtime(); long radioScanDuration = currentTime - sRadioStartTime; double scanWeight = getScanWeight(sRadioScanMode) * 0.01; long weightedDuration = (long) (radioScanDuration * scanWeight); if (weightedDuration > 0) { MetricsLogger.getInstance().cacheCount( BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR, weightedDuration); if (sIsScreenOn) { MetricsLogger.getInstance().cacheCount( BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR_SCREEN_ON, weightedDuration); } else { MetricsLogger.getInstance().cacheCount( BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR_SCREEN_OFF, weightedDuration); } } } private static int getScanWeight(int scanMode) { switch (scanMode) { case ScanSettings.SCAN_MODE_OPPORTUNISTIC: return OPPORTUNISTIC_WEIGHT; case ScanSettings.SCAN_MODE_SCREEN_OFF: return SCREEN_OFF_LOW_POWER_WEIGHT; case ScanSettings.SCAN_MODE_LOW_POWER: return LOW_POWER_WEIGHT; case ScanSettings.SCAN_MODE_BALANCED: case ScanSettings.SCAN_MODE_AMBIENT_DISCOVERY: case ScanSettings.SCAN_MODE_SCREEN_OFF_BALANCED: return BALANCED_WEIGHT; case ScanSettings.SCAN_MODE_LOW_LATENCY: return LOW_LATENCY_WEIGHT; default: return LOW_POWER_WEIGHT; } } static void setScreenState(boolean isScreenOn) { synchronized (sLock) { if (sIsScreenOn == isScreenOn) { return; } if (sIsRadioStarted) { recordScanRadioDurationMetrics(); sRadioStartTime = SystemClock.elapsedRealtime(); } sIsScreenOn = isScreenOn; } } synchronized void recordScanSuspend(int scannerId) { LastScan scan = getScanFromScannerId(scannerId); if (scan == null || scan.isSuspended) { Loading android/app/src/com/android/bluetooth/gatt/ScanManager.java +19 −0 Original line number Diff line number Diff line Loading @@ -193,6 +193,8 @@ public class ScanManager { mDm.registerDisplayListener(mDisplayListener, null); } mScreenOn = isScreenOn(); AppScanStats.initScanRadioState(); AppScanStats.setScreenState(mScreenOn); if (mActivityManager != null) { mActivityManager.addOnUidImportanceListener(mUidImportanceListener, FOREGROUND_IMPORTANCE_CUTOFF); Loading Loading @@ -562,6 +564,7 @@ public class ScanManager { } void handleScreenOff() { AppScanStats.setScreenState(false); if (!mScreenOn) { return; } Loading Loading @@ -854,6 +857,7 @@ public class ScanManager { } void handleScreenOn() { AppScanStats.setScreenState(true); if (mScreenOn) { return; } Loading Loading @@ -1042,6 +1046,9 @@ public class ScanManager { int scanWindow = Utils.millsToUnit(scanWindowMs); int scanInterval = Utils.millsToUnit(scanIntervalMs); mNativeInterface.gattClientScan(false); if (!AppScanStats.recordScanRadioStop()) { Log.w(TAG, "There is no scan radio to stop"); } if (DBG) { Log.d(TAG, "Start gattClientScanNative with" + " old scanMode " + mLastConfiguredScanSetting Loading @@ -1053,6 +1060,9 @@ public class ScanManager { mNativeInterface.gattSetScanParameters(client.scannerId, scanInterval, scanWindow); mNativeInterface.gattClientScan(true); if (!AppScanStats.recordScanRadioStart(curScanSetting)) { Log.w(TAG, "Scan radio already started"); } mLastConfiguredScanSetting = curScanSetting; } } else { Loading Loading @@ -1092,6 +1102,9 @@ public class ScanManager { Log.d(TAG, "start gattClientScanNative from startRegularScan()"); } mNativeInterface.gattClientScan(true); if (!AppScanStats.recordScanRadioStart(client.settings.getScanMode())) { Log.w(TAG, "Scan radio already started"); } } } Loading Loading @@ -1315,6 +1328,9 @@ public class ScanManager { Log.d(TAG, "stop gattClientScanNative"); } mNativeInterface.gattClientScan(false); if (!AppScanStats.recordScanRadioStop()) { Log.w(TAG, "There is no scan radio to stop"); } } removeScanFilters(client.scannerId); } Loading Loading @@ -1347,6 +1363,9 @@ public class ScanManager { Log.d(TAG, "stop gattClientScanNative"); } mNativeInterface.gattClientScan(false); if (!AppScanStats.recordScanRadioStop()) { Log.w(TAG, "There is no scan radio to stop"); } } } Loading android/app/tests/unit/src/com/android/bluetooth/gatt/ScanManagerTest.java +188 −0 Original line number Diff line number Diff line Loading @@ -29,11 +29,18 @@ import static android.bluetooth.le.ScanSettings.SCAN_MODE_SCREEN_OFF_BALANCED; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.atMost; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.ActivityManager; import android.bluetooth.BluetoothProtoEnums; import android.bluetooth.le.ScanFilter; import android.bluetooth.le.ScanSettings; import android.content.Context; Loading @@ -53,6 +60,7 @@ import com.android.bluetooth.R; import com.android.bluetooth.TestUtils; import com.android.bluetooth.btservice.AdapterService; import com.android.bluetooth.btservice.BluetoothAdapterProxy; import com.android.bluetooth.btservice.MetricsLogger; import java.util.ArrayList; import java.util.List; Loading @@ -65,7 +73,10 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; /** Loading Loading @@ -98,6 +109,9 @@ public class ScanManagerTest { @Mock private GattObjectsFactory mFactory; @Mock private GattNativeInterface mNativeInterface; @Mock private ScanNativeInterface mScanNativeInterface; @Mock private MetricsLogger mMetricsLogger; @Captor ArgumentCaptor<Long> mScanDurationCaptor; @Before public void setUp() throws Exception { Loading Loading @@ -130,6 +144,8 @@ public class ScanManagerTest { // Mock JNI callback in ScanNativeInterface doReturn(true).when(mScanNativeInterface).waitForCallback(anyInt()); MetricsLogger.setInstanceForTesting(mMetricsLogger); TestUtils.startService(mServiceRule, GattService.class); mService = GattService.getGattService(); assertThat(mService).isNotNull(); Loading Loading @@ -158,6 +174,8 @@ public class ScanManagerTest { TestUtils.clearAdapterService(mAdapterService); BluetoothAdapterProxy.setInstanceForTesting(null); GattObjectsFactory.setInstanceForTesting(null); MetricsLogger.setInstanceForTesting(null); MetricsLogger.getInstance(); } private void testSleep(long millis) { Loading Loading @@ -1028,4 +1046,174 @@ public class ScanManagerTest { assertThat(mScanManager.getSuspendedScanQueue().contains(client)).isFalse(); } } @Test public void testMetricsScanRadioDurationScreenOn() { // Set filtered scan flag final boolean isFiltered = true; // Turn on screen sendMessageWaitForProcessed(createScreenOnOffMessage(true)); verify(mMetricsLogger, never()).cacheCount(anyInt(), anyLong()); Mockito.clearInvocations(mMetricsLogger); // Create scan client ScanClient client = createScanClient(0, isFiltered, SCAN_MODE_LOW_POWER); // Start scan sendMessageWaitForProcessed(createStartStopScanMessage(true, client)); verify(mMetricsLogger, atMost(1)).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR), anyLong()); verify(mMetricsLogger, atMost(1)).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR_SCREEN_ON), anyLong()); verify(mMetricsLogger, never()).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR_SCREEN_OFF), anyLong()); Mockito.clearInvocations(mMetricsLogger); testSleep(50); // Stop scan sendMessageWaitForProcessed(createStartStopScanMessage(false, client)); verify(mMetricsLogger, atLeastOnce()).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR), anyLong()); verify(mMetricsLogger, atLeastOnce()).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR_SCREEN_ON), anyLong()); verify(mMetricsLogger, never()).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR_SCREEN_OFF), anyLong()); Mockito.clearInvocations(mMetricsLogger); } @Test public void testMetricsScanRadioDurationScreenOnOff() { // Set filtered scan flag final boolean isFiltered = true; // Turn on screen sendMessageWaitForProcessed(createScreenOnOffMessage(true)); verify(mMetricsLogger, never()).cacheCount(anyInt(), anyLong()); Mockito.clearInvocations(mMetricsLogger); // Create scan client ScanClient client = createScanClient(0, isFiltered, SCAN_MODE_LOW_POWER); // Start scan sendMessageWaitForProcessed(createStartStopScanMessage(true, client)); verify(mMetricsLogger, atMost(1)).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR), anyLong()); verify(mMetricsLogger, atMost(1)).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR_SCREEN_ON), anyLong()); verify(mMetricsLogger, never()).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR_SCREEN_OFF), anyLong()); Mockito.clearInvocations(mMetricsLogger); testSleep(50); // Turn off screen sendMessageWaitForProcessed(createScreenOnOffMessage(false)); verify(mMetricsLogger, atLeastOnce()).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR), anyLong()); verify(mMetricsLogger, atLeastOnce()).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR_SCREEN_ON), anyLong()); verify(mMetricsLogger, never()).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR_SCREEN_OFF), anyLong()); Mockito.clearInvocations(mMetricsLogger); testSleep(50); // Turn on screen sendMessageWaitForProcessed(createScreenOnOffMessage(true)); verify(mMetricsLogger, atLeastOnce()).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR), anyLong()); verify(mMetricsLogger, never()).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR_SCREEN_ON), anyLong()); verify(mMetricsLogger, atLeastOnce()).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR_SCREEN_OFF), anyLong()); Mockito.clearInvocations(mMetricsLogger); testSleep(50); // Stop scan sendMessageWaitForProcessed(createStartStopScanMessage(false, client)); verify(mMetricsLogger, atLeastOnce()).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR), anyLong()); verify(mMetricsLogger, atLeastOnce()).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR_SCREEN_ON), anyLong()); verify(mMetricsLogger, never()).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR_SCREEN_OFF), anyLong()); Mockito.clearInvocations(mMetricsLogger); } @Test public void testMetricsScanRadioDurationMultiScan() { // Set filtered scan flag final boolean isFiltered = true; // Turn on screen sendMessageWaitForProcessed(createScreenOnOffMessage(true)); verify(mMetricsLogger, never()).cacheCount(anyInt(), anyLong()); Mockito.clearInvocations(mMetricsLogger); // Create scan clients with different duty cycles ScanClient client = createScanClient(0, isFiltered, SCAN_MODE_LOW_POWER); ScanClient client2 = createScanClient(1, isFiltered, SCAN_MODE_BALANCED); // Start scan with lower duty cycle sendMessageWaitForProcessed(createStartStopScanMessage(true, client)); verify(mMetricsLogger, atMost(1)).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR), anyLong()); verify(mMetricsLogger, atMost(1)).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR_SCREEN_ON), anyLong()); verify(mMetricsLogger, never()).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR_SCREEN_OFF), anyLong()); Mockito.clearInvocations(mMetricsLogger); testSleep(50); // Start scan with higher duty cycle sendMessageWaitForProcessed(createStartStopScanMessage(true, client2)); verify(mMetricsLogger, atLeastOnce()).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR), anyLong()); verify(mMetricsLogger, atLeastOnce()).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR_SCREEN_ON), anyLong()); verify(mMetricsLogger, never()).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR_SCREEN_OFF), anyLong()); Mockito.clearInvocations(mMetricsLogger); testSleep(50); // Stop scan with lower duty cycle sendMessageWaitForProcessed(createStartStopScanMessage(false, client)); verify(mMetricsLogger, never()).cacheCount(anyInt(), anyLong()); Mockito.clearInvocations(mMetricsLogger); // Stop scan with higher duty cycle sendMessageWaitForProcessed(createStartStopScanMessage(false, client2)); verify(mMetricsLogger, atLeastOnce()).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR), anyLong()); verify(mMetricsLogger, atLeastOnce()).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR_SCREEN_ON), anyLong()); verify(mMetricsLogger, never()).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR_SCREEN_OFF), anyLong()); Mockito.clearInvocations(mMetricsLogger); } @Test public void testMetricsScanRadioWeightedDuration() { // Set filtered scan flag final boolean isFiltered = true; final long scanTestDuration = 100; // Set scan mode map {scan mode (ScanMode) : scan weight (ScanWeight)} SparseIntArray scanModeMap = new SparseIntArray(); scanModeMap.put(SCAN_MODE_SCREEN_OFF, AppScanStats.SCREEN_OFF_LOW_POWER_WEIGHT); scanModeMap.put(SCAN_MODE_LOW_POWER, AppScanStats.LOW_POWER_WEIGHT); scanModeMap.put(SCAN_MODE_BALANCED, AppScanStats.BALANCED_WEIGHT); scanModeMap.put(SCAN_MODE_LOW_LATENCY, AppScanStats.LOW_LATENCY_WEIGHT); scanModeMap.put(SCAN_MODE_AMBIENT_DISCOVERY, AppScanStats.AMBIENT_DISCOVERY_WEIGHT); // Turn on screen sendMessageWaitForProcessed(createScreenOnOffMessage(true)); Mockito.clearInvocations(mMetricsLogger); for (int i = 0; i < scanModeMap.size(); i++) { int ScanMode = scanModeMap.keyAt(i); long weightedScanDuration = (long)(scanTestDuration * scanModeMap.get(ScanMode) * 0.01); Log.d(TAG, "ScanMode: " + String.valueOf(ScanMode) + " weightedScanDuration: " + String.valueOf(weightedScanDuration)); // Create scan client ScanClient client = createScanClient(i, isFiltered, ScanMode); // Start scan sendMessageWaitForProcessed(createStartStopScanMessage(true, client)); Mockito.clearInvocations(mMetricsLogger); // Wait for scan test duration testSleep(scanTestDuration); // Stop scan sendMessageWaitForProcessed(createStartStopScanMessage(false, client)); verify(mMetricsLogger, atLeastOnce()).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR), mScanDurationCaptor.capture()); long capturedDuration = mScanDurationCaptor.getValue(); Log.d(TAG, "capturedDuration: " + String.valueOf(capturedDuration)); assertThat(weightedScanDuration <= capturedDuration && capturedDuration <= weightedScanDuration + DELAY_ASYNC_MS).isTrue(); Mockito.clearInvocations(mMetricsLogger); } } } Loading
android/app/src/com/android/bluetooth/btservice/MetricsLogger.java +17 −1 Original line number Diff line number Diff line Loading @@ -24,6 +24,8 @@ import com.android.bluetooth.BluetoothMetricsProto.BluetoothLog; import com.android.bluetooth.BluetoothMetricsProto.ProfileConnectionStats; import com.android.bluetooth.BluetoothMetricsProto.ProfileId; import com.android.bluetooth.BluetoothStatsLog; import com.android.bluetooth.Utils; import com.android.internal.annotations.VisibleForTesting; import com.google.common.hash.BloomFilter; import com.google.common.hash.Funnels; Loading Loading @@ -54,7 +56,7 @@ public class MetricsLogger { private static final HashMap<ProfileId, Integer> sProfileConnectionCounts = new HashMap<>(); HashMap<Integer, Long> mCounters = new HashMap<>(); private static MetricsLogger sInstance = null; private static volatile MetricsLogger sInstance = null; private Context mContext = null; private AlarmManager mAlarmManager = null; private boolean mInitialized = false; Loading @@ -81,6 +83,20 @@ public class MetricsLogger { return sInstance; } /** * Allow unit tests to substitute MetricsLogger with a test instance * * @param instance a test instance of the MetricsLogger */ @VisibleForTesting public static void setInstanceForTesting(MetricsLogger instance) { Utils.enforceInstrumentationTestMode(); synchronized (mLock) { Log.d(TAG, "setInstanceForTesting(), set to " + instance); sInstance = instance; } } public boolean isInitialized() { return mInitialized; } Loading
android/app/src/com/android/bluetooth/gatt/AppScanStats.java +98 −1 Original line number Diff line number Diff line Loading @@ -15,6 +15,7 @@ */ package com.android.bluetooth.gatt; import android.bluetooth.BluetoothProtoEnums; import android.bluetooth.le.ScanFilter; import android.bluetooth.le.ScanSettings; import android.os.BatteryStatsManager; Loading @@ -25,7 +26,9 @@ import android.os.WorkSource; import com.android.bluetooth.BluetoothMetricsProto; import com.android.bluetooth.BluetoothStatsLog; import com.android.bluetooth.btservice.AdapterService; import com.android.bluetooth.btservice.MetricsLogger; import com.android.bluetooth.util.WorkSourceUtil; import com.android.internal.annotations.GuardedBy; import java.text.DateFormat; import java.text.SimpleDateFormat; Loading @@ -49,8 +52,9 @@ import java.util.Objects; // Weight is the duty cycle of the scan mode static final int OPPORTUNISTIC_WEIGHT = 0; static final int SCREEN_OFF_LOW_POWER_WEIGHT = 5; static final int LOW_POWER_WEIGHT = 10; static final int AMBIENT_DISCOVERY_WEIGHT = 20; static final int AMBIENT_DISCOVERY_WEIGHT = 25; static final int BALANCED_WEIGHT = 25; static final int LOW_LATENCY_WEIGHT = 100; Loading @@ -67,6 +71,13 @@ import java.util.Objects; private final AdapterService mAdapterService; private static Object sLock = new Object(); @GuardedBy("sLock") static long sRadioStartTime = 0; static int sRadioScanMode; static boolean sIsRadioStarted = false; static boolean sIsScreenOn = false; class LastScan { public long duration; public long suspendDuration; Loading Loading @@ -329,6 +340,92 @@ import java.util.Objects; scan.isFilterScan, scan.isBackgroundScan, scan.isOpportunisticScan); } static void initScanRadioState() { synchronized (sLock) { sIsRadioStarted = false; } } static boolean recordScanRadioStart(int scanMode) { synchronized (sLock) { if (sIsRadioStarted) { return false; } sRadioStartTime = SystemClock.elapsedRealtime(); sRadioScanMode = scanMode; sIsRadioStarted = true; } return true; } static boolean recordScanRadioStop() { synchronized (sLock) { if (!sIsRadioStarted) { return false; } recordScanRadioDurationMetrics(); sRadioStartTime = 0; sIsRadioStarted = false; } return true; } @GuardedBy("sLock") private static void recordScanRadioDurationMetrics() { if (!sIsRadioStarted) { return; } long currentTime = SystemClock.elapsedRealtime(); long radioScanDuration = currentTime - sRadioStartTime; double scanWeight = getScanWeight(sRadioScanMode) * 0.01; long weightedDuration = (long) (radioScanDuration * scanWeight); if (weightedDuration > 0) { MetricsLogger.getInstance().cacheCount( BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR, weightedDuration); if (sIsScreenOn) { MetricsLogger.getInstance().cacheCount( BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR_SCREEN_ON, weightedDuration); } else { MetricsLogger.getInstance().cacheCount( BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR_SCREEN_OFF, weightedDuration); } } } private static int getScanWeight(int scanMode) { switch (scanMode) { case ScanSettings.SCAN_MODE_OPPORTUNISTIC: return OPPORTUNISTIC_WEIGHT; case ScanSettings.SCAN_MODE_SCREEN_OFF: return SCREEN_OFF_LOW_POWER_WEIGHT; case ScanSettings.SCAN_MODE_LOW_POWER: return LOW_POWER_WEIGHT; case ScanSettings.SCAN_MODE_BALANCED: case ScanSettings.SCAN_MODE_AMBIENT_DISCOVERY: case ScanSettings.SCAN_MODE_SCREEN_OFF_BALANCED: return BALANCED_WEIGHT; case ScanSettings.SCAN_MODE_LOW_LATENCY: return LOW_LATENCY_WEIGHT; default: return LOW_POWER_WEIGHT; } } static void setScreenState(boolean isScreenOn) { synchronized (sLock) { if (sIsScreenOn == isScreenOn) { return; } if (sIsRadioStarted) { recordScanRadioDurationMetrics(); sRadioStartTime = SystemClock.elapsedRealtime(); } sIsScreenOn = isScreenOn; } } synchronized void recordScanSuspend(int scannerId) { LastScan scan = getScanFromScannerId(scannerId); if (scan == null || scan.isSuspended) { Loading
android/app/src/com/android/bluetooth/gatt/ScanManager.java +19 −0 Original line number Diff line number Diff line Loading @@ -193,6 +193,8 @@ public class ScanManager { mDm.registerDisplayListener(mDisplayListener, null); } mScreenOn = isScreenOn(); AppScanStats.initScanRadioState(); AppScanStats.setScreenState(mScreenOn); if (mActivityManager != null) { mActivityManager.addOnUidImportanceListener(mUidImportanceListener, FOREGROUND_IMPORTANCE_CUTOFF); Loading Loading @@ -562,6 +564,7 @@ public class ScanManager { } void handleScreenOff() { AppScanStats.setScreenState(false); if (!mScreenOn) { return; } Loading Loading @@ -854,6 +857,7 @@ public class ScanManager { } void handleScreenOn() { AppScanStats.setScreenState(true); if (mScreenOn) { return; } Loading Loading @@ -1042,6 +1046,9 @@ public class ScanManager { int scanWindow = Utils.millsToUnit(scanWindowMs); int scanInterval = Utils.millsToUnit(scanIntervalMs); mNativeInterface.gattClientScan(false); if (!AppScanStats.recordScanRadioStop()) { Log.w(TAG, "There is no scan radio to stop"); } if (DBG) { Log.d(TAG, "Start gattClientScanNative with" + " old scanMode " + mLastConfiguredScanSetting Loading @@ -1053,6 +1060,9 @@ public class ScanManager { mNativeInterface.gattSetScanParameters(client.scannerId, scanInterval, scanWindow); mNativeInterface.gattClientScan(true); if (!AppScanStats.recordScanRadioStart(curScanSetting)) { Log.w(TAG, "Scan radio already started"); } mLastConfiguredScanSetting = curScanSetting; } } else { Loading Loading @@ -1092,6 +1102,9 @@ public class ScanManager { Log.d(TAG, "start gattClientScanNative from startRegularScan()"); } mNativeInterface.gattClientScan(true); if (!AppScanStats.recordScanRadioStart(client.settings.getScanMode())) { Log.w(TAG, "Scan radio already started"); } } } Loading Loading @@ -1315,6 +1328,9 @@ public class ScanManager { Log.d(TAG, "stop gattClientScanNative"); } mNativeInterface.gattClientScan(false); if (!AppScanStats.recordScanRadioStop()) { Log.w(TAG, "There is no scan radio to stop"); } } removeScanFilters(client.scannerId); } Loading Loading @@ -1347,6 +1363,9 @@ public class ScanManager { Log.d(TAG, "stop gattClientScanNative"); } mNativeInterface.gattClientScan(false); if (!AppScanStats.recordScanRadioStop()) { Log.w(TAG, "There is no scan radio to stop"); } } } Loading
android/app/tests/unit/src/com/android/bluetooth/gatt/ScanManagerTest.java +188 −0 Original line number Diff line number Diff line Loading @@ -29,11 +29,18 @@ import static android.bluetooth.le.ScanSettings.SCAN_MODE_SCREEN_OFF_BALANCED; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.atMost; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.ActivityManager; import android.bluetooth.BluetoothProtoEnums; import android.bluetooth.le.ScanFilter; import android.bluetooth.le.ScanSettings; import android.content.Context; Loading @@ -53,6 +60,7 @@ import com.android.bluetooth.R; import com.android.bluetooth.TestUtils; import com.android.bluetooth.btservice.AdapterService; import com.android.bluetooth.btservice.BluetoothAdapterProxy; import com.android.bluetooth.btservice.MetricsLogger; import java.util.ArrayList; import java.util.List; Loading @@ -65,7 +73,10 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; /** Loading Loading @@ -98,6 +109,9 @@ public class ScanManagerTest { @Mock private GattObjectsFactory mFactory; @Mock private GattNativeInterface mNativeInterface; @Mock private ScanNativeInterface mScanNativeInterface; @Mock private MetricsLogger mMetricsLogger; @Captor ArgumentCaptor<Long> mScanDurationCaptor; @Before public void setUp() throws Exception { Loading Loading @@ -130,6 +144,8 @@ public class ScanManagerTest { // Mock JNI callback in ScanNativeInterface doReturn(true).when(mScanNativeInterface).waitForCallback(anyInt()); MetricsLogger.setInstanceForTesting(mMetricsLogger); TestUtils.startService(mServiceRule, GattService.class); mService = GattService.getGattService(); assertThat(mService).isNotNull(); Loading Loading @@ -158,6 +174,8 @@ public class ScanManagerTest { TestUtils.clearAdapterService(mAdapterService); BluetoothAdapterProxy.setInstanceForTesting(null); GattObjectsFactory.setInstanceForTesting(null); MetricsLogger.setInstanceForTesting(null); MetricsLogger.getInstance(); } private void testSleep(long millis) { Loading Loading @@ -1028,4 +1046,174 @@ public class ScanManagerTest { assertThat(mScanManager.getSuspendedScanQueue().contains(client)).isFalse(); } } @Test public void testMetricsScanRadioDurationScreenOn() { // Set filtered scan flag final boolean isFiltered = true; // Turn on screen sendMessageWaitForProcessed(createScreenOnOffMessage(true)); verify(mMetricsLogger, never()).cacheCount(anyInt(), anyLong()); Mockito.clearInvocations(mMetricsLogger); // Create scan client ScanClient client = createScanClient(0, isFiltered, SCAN_MODE_LOW_POWER); // Start scan sendMessageWaitForProcessed(createStartStopScanMessage(true, client)); verify(mMetricsLogger, atMost(1)).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR), anyLong()); verify(mMetricsLogger, atMost(1)).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR_SCREEN_ON), anyLong()); verify(mMetricsLogger, never()).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR_SCREEN_OFF), anyLong()); Mockito.clearInvocations(mMetricsLogger); testSleep(50); // Stop scan sendMessageWaitForProcessed(createStartStopScanMessage(false, client)); verify(mMetricsLogger, atLeastOnce()).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR), anyLong()); verify(mMetricsLogger, atLeastOnce()).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR_SCREEN_ON), anyLong()); verify(mMetricsLogger, never()).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR_SCREEN_OFF), anyLong()); Mockito.clearInvocations(mMetricsLogger); } @Test public void testMetricsScanRadioDurationScreenOnOff() { // Set filtered scan flag final boolean isFiltered = true; // Turn on screen sendMessageWaitForProcessed(createScreenOnOffMessage(true)); verify(mMetricsLogger, never()).cacheCount(anyInt(), anyLong()); Mockito.clearInvocations(mMetricsLogger); // Create scan client ScanClient client = createScanClient(0, isFiltered, SCAN_MODE_LOW_POWER); // Start scan sendMessageWaitForProcessed(createStartStopScanMessage(true, client)); verify(mMetricsLogger, atMost(1)).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR), anyLong()); verify(mMetricsLogger, atMost(1)).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR_SCREEN_ON), anyLong()); verify(mMetricsLogger, never()).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR_SCREEN_OFF), anyLong()); Mockito.clearInvocations(mMetricsLogger); testSleep(50); // Turn off screen sendMessageWaitForProcessed(createScreenOnOffMessage(false)); verify(mMetricsLogger, atLeastOnce()).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR), anyLong()); verify(mMetricsLogger, atLeastOnce()).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR_SCREEN_ON), anyLong()); verify(mMetricsLogger, never()).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR_SCREEN_OFF), anyLong()); Mockito.clearInvocations(mMetricsLogger); testSleep(50); // Turn on screen sendMessageWaitForProcessed(createScreenOnOffMessage(true)); verify(mMetricsLogger, atLeastOnce()).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR), anyLong()); verify(mMetricsLogger, never()).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR_SCREEN_ON), anyLong()); verify(mMetricsLogger, atLeastOnce()).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR_SCREEN_OFF), anyLong()); Mockito.clearInvocations(mMetricsLogger); testSleep(50); // Stop scan sendMessageWaitForProcessed(createStartStopScanMessage(false, client)); verify(mMetricsLogger, atLeastOnce()).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR), anyLong()); verify(mMetricsLogger, atLeastOnce()).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR_SCREEN_ON), anyLong()); verify(mMetricsLogger, never()).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR_SCREEN_OFF), anyLong()); Mockito.clearInvocations(mMetricsLogger); } @Test public void testMetricsScanRadioDurationMultiScan() { // Set filtered scan flag final boolean isFiltered = true; // Turn on screen sendMessageWaitForProcessed(createScreenOnOffMessage(true)); verify(mMetricsLogger, never()).cacheCount(anyInt(), anyLong()); Mockito.clearInvocations(mMetricsLogger); // Create scan clients with different duty cycles ScanClient client = createScanClient(0, isFiltered, SCAN_MODE_LOW_POWER); ScanClient client2 = createScanClient(1, isFiltered, SCAN_MODE_BALANCED); // Start scan with lower duty cycle sendMessageWaitForProcessed(createStartStopScanMessage(true, client)); verify(mMetricsLogger, atMost(1)).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR), anyLong()); verify(mMetricsLogger, atMost(1)).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR_SCREEN_ON), anyLong()); verify(mMetricsLogger, never()).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR_SCREEN_OFF), anyLong()); Mockito.clearInvocations(mMetricsLogger); testSleep(50); // Start scan with higher duty cycle sendMessageWaitForProcessed(createStartStopScanMessage(true, client2)); verify(mMetricsLogger, atLeastOnce()).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR), anyLong()); verify(mMetricsLogger, atLeastOnce()).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR_SCREEN_ON), anyLong()); verify(mMetricsLogger, never()).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR_SCREEN_OFF), anyLong()); Mockito.clearInvocations(mMetricsLogger); testSleep(50); // Stop scan with lower duty cycle sendMessageWaitForProcessed(createStartStopScanMessage(false, client)); verify(mMetricsLogger, never()).cacheCount(anyInt(), anyLong()); Mockito.clearInvocations(mMetricsLogger); // Stop scan with higher duty cycle sendMessageWaitForProcessed(createStartStopScanMessage(false, client2)); verify(mMetricsLogger, atLeastOnce()).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR), anyLong()); verify(mMetricsLogger, atLeastOnce()).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR_SCREEN_ON), anyLong()); verify(mMetricsLogger, never()).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR_SCREEN_OFF), anyLong()); Mockito.clearInvocations(mMetricsLogger); } @Test public void testMetricsScanRadioWeightedDuration() { // Set filtered scan flag final boolean isFiltered = true; final long scanTestDuration = 100; // Set scan mode map {scan mode (ScanMode) : scan weight (ScanWeight)} SparseIntArray scanModeMap = new SparseIntArray(); scanModeMap.put(SCAN_MODE_SCREEN_OFF, AppScanStats.SCREEN_OFF_LOW_POWER_WEIGHT); scanModeMap.put(SCAN_MODE_LOW_POWER, AppScanStats.LOW_POWER_WEIGHT); scanModeMap.put(SCAN_MODE_BALANCED, AppScanStats.BALANCED_WEIGHT); scanModeMap.put(SCAN_MODE_LOW_LATENCY, AppScanStats.LOW_LATENCY_WEIGHT); scanModeMap.put(SCAN_MODE_AMBIENT_DISCOVERY, AppScanStats.AMBIENT_DISCOVERY_WEIGHT); // Turn on screen sendMessageWaitForProcessed(createScreenOnOffMessage(true)); Mockito.clearInvocations(mMetricsLogger); for (int i = 0; i < scanModeMap.size(); i++) { int ScanMode = scanModeMap.keyAt(i); long weightedScanDuration = (long)(scanTestDuration * scanModeMap.get(ScanMode) * 0.01); Log.d(TAG, "ScanMode: " + String.valueOf(ScanMode) + " weightedScanDuration: " + String.valueOf(weightedScanDuration)); // Create scan client ScanClient client = createScanClient(i, isFiltered, ScanMode); // Start scan sendMessageWaitForProcessed(createStartStopScanMessage(true, client)); Mockito.clearInvocations(mMetricsLogger); // Wait for scan test duration testSleep(scanTestDuration); // Stop scan sendMessageWaitForProcessed(createStartStopScanMessage(false, client)); verify(mMetricsLogger, atLeastOnce()).cacheCount( eq(BluetoothProtoEnums.LE_SCAN_RADIO_DURATION_REGULAR), mScanDurationCaptor.capture()); long capturedDuration = mScanDurationCaptor.getValue(); Log.d(TAG, "capturedDuration: " + String.valueOf(capturedDuration)); assertThat(weightedScanDuration <= capturedDuration && capturedDuration <= weightedScanDuration + DELAY_ASYNC_MS).isTrue(); Mockito.clearInvocations(mMetricsLogger); } } }