Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 2dd09dcf authored by Jayden Kim's avatar Jayden Kim
Browse files

Adds LE Scan Radio Duration

Bug: 236321914
Test: atest BluetoothInstrumentationTests and device test
Tag: #feature
Change-Id: I9077ee68c63991d573e4879ff700d9a15493af36
parent d158a9e5
Loading
Loading
Loading
Loading
+17 −1
Original line number Diff line number Diff line
@@ -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 java.util.HashMap;

@@ -41,7 +43,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;
@@ -66,6 +68,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;
    }
+98 −1
Original line number Diff line number Diff line
@@ -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;
@@ -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;
@@ -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;

@@ -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;
@@ -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) {
+19 −0
Original line number Diff line number Diff line
@@ -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);
@@ -562,6 +564,7 @@ public class ScanManager {
        }

        void handleScreenOff() {
            AppScanStats.setScreenState(false);
            if (!mScreenOn) {
                return;
            }
@@ -854,6 +857,7 @@ public class ScanManager {
        }

        void handleScreenOn() {
            AppScanStats.setScreenState(true);
            if (mScreenOn) {
                return;
            }
@@ -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
@@ -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 {
@@ -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");
                }
            }
        }

@@ -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);
        }
@@ -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");
                }
            }
        }

+188 −0
Original line number Diff line number Diff line
@@ -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;
@@ -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;
@@ -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;

/**
@@ -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 {
@@ -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();
@@ -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) {
@@ -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);
        }
    }
}