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

Commit 55578543 authored by Chen Chen's avatar Chen Chen
Browse files

BluetoothMetrics: Add Bluetooth counter metrics in java layer

1) Provide API MetricsLogger.logBluetoothCounterMetrics() to other
bt components
2) Cache the counters in MetricsLogger.mCounters
3) Send the counters to westworld every 6 hours and
before AdapterService is closed

Test: atest BluetoothInstrumentationTests
Bug: 204823249

Change-Id: I5bda4bb32dfe8e71d1774e339d72b82380d27886
parent 1732367e
Loading
Loading
Loading
Loading
+27 −0
Original line number Diff line number Diff line
@@ -98,6 +98,7 @@ import com.android.bluetooth.BluetoothStatsLog;
import com.android.bluetooth.Utils;
import com.android.bluetooth.a2dp.A2dpService;
import com.android.bluetooth.a2dpsink.A2dpSinkService;
import com.android.bluetooth.btservice.MetricsLogger;
import com.android.bluetooth.btservice.RemoteDevices.DeviceProperties;
import com.android.bluetooth.btservice.activityattribution.ActivityAttributionService;
import com.android.bluetooth.btservice.bluetoothkeystore.BluetoothKeystoreService;
@@ -305,6 +306,8 @@ public class AdapterService extends Service {

    private volatile boolean mTestModeEnabled = false;

    private MetricsLogger mMetricsLogger;

    /**
     * Register a {@link ProfileService} with AdapterService.
     *
@@ -445,6 +448,7 @@ public class AdapterService extends Service {
    @Override
    public void onCreate() {
        super.onCreate();
        initMetricsLogger();
        debugLog("onCreate()");
        mDeviceConfigListener.start();
        mRemoteDevices = new RemoteDevices(this, Looper.getMainLooper());
@@ -583,6 +587,27 @@ public class AdapterService extends Service {
        }
    };

    private boolean initMetricsLogger() {
        if (mMetricsLogger != null) {
            return false;
        }
        mMetricsLogger = MetricsLogger.getInstance();
        return mMetricsLogger.init(this);
    }

    private boolean closeMetricsLogger() {
        if (mMetricsLogger == null) {
            return false;
        }
        boolean result = mMetricsLogger.close();
        mMetricsLogger = null;
        return result;
    }

    public void setMetricsLogger(MetricsLogger metricsLogger) {
        mMetricsLogger = metricsLogger;
    }

    void bringUpBle() {
        debugLog("bleOnProcessStart()");

@@ -777,6 +802,8 @@ public class AdapterService extends Service {
            return;
        }

        closeMetricsLogger();

        clearAdapterService(this);

        mCleaningUp = true;
+152 −1
Original line number Diff line number Diff line
@@ -15,18 +15,113 @@
 */
package com.android.bluetooth.btservice;

import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.SystemClock;
import android.util.Log;

import com.android.bluetooth.BluetoothMetricsProto.BluetoothLog;
import com.android.bluetooth.BluetoothMetricsProto.ProfileConnectionStats;
import com.android.bluetooth.BluetoothMetricsProto.ProfileId;
import com.android.bluetooth.BluetoothStatsLog;

import java.util.HashMap;

/**
 * Class with static methods for logging metrics data
 * Class of Bluetooth Metrics
 */
public class MetricsLogger {
    private static final String TAG = "BluetoothMetricsLogger";

    public static final boolean DEBUG = false;

    /**
     * Intent indicating Bluetooth counter metrics should send logs to BluetoothStatsLog
     */
    public static final String BLUETOOTH_COUNTER_METRICS_ACTION =
            "com.android.bluetooth.map.BLUETOOTH_COUNTER_METRICS_ACTION";
    // 6 hours timeout for counter metrics
    private static final long BLUETOOTH_COUNTER_METRICS_ACTION_DURATION_MILLIS = 6L * 3600L * 1000L;

    private static final HashMap<ProfileId, Integer> sProfileConnectionCounts = new HashMap<>();

    HashMap<Integer, Long> mCounters = new HashMap<>();
    private static MetricsLogger sInstance = null;
    private Context mContext = null;
    private AlarmManager mAlarmManager = null;
    private boolean mInitialized = false;
    static final private Object mLock = new Object();

    private BroadcastReceiver mDrainReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (DEBUG) {
                Log.d(TAG, "onReceive: " + action);
            }
            if (action.equals(BLUETOOTH_COUNTER_METRICS_ACTION)) {
                drainBufferedCounters();
            }
        }
    };

    public static MetricsLogger getInstance() {
        if (sInstance == null) {
            synchronized (mLock) {
                if (sInstance == null) {
                    sInstance = new MetricsLogger();
                }
            }
        }
        return sInstance;
    }

    public boolean isInitialized() {
        return mInitialized;
    }

    public boolean init(Context context) {
        if (mInitialized) {
            return false;
        }
        mInitialized = true;
        mContext = context;
        IntentFilter filter = new IntentFilter();
        filter.addAction(BLUETOOTH_COUNTER_METRICS_ACTION);
        mContext.registerReceiver(mDrainReceiver, filter);
        scheduleDrains();
        return true;
    }

    public boolean count(int key, long count) {
        if (!mInitialized) {
            Log.w(TAG, "MetricsLogger isn't initialized");
            return false;
        }
        if (count <= 0) {
            Log.w(TAG, "count is not larger than 0. count: " + count + " key: " + key);
            return false;
        }
        long total = 0;

        synchronized (mLock) {
            if (mCounters.containsKey(key)) {
                total = mCounters.get(key);
            }
            if (Long.MAX_VALUE - total < count) {
                Log.w(TAG, "count overflows. count: " + count + " current total: " + total);
                mCounters.put(key, Long.MAX_VALUE);
                return false;
            }
            mCounters.put(key, total + count);
        }
        return true;
    }

    /**
     * Log profile connection event by incrementing an internal counter for that profile.
     * This log persists over adapter enable/disable and only get cleared when metrics are
@@ -57,4 +152,60 @@ public class MetricsLogger {
            sProfileConnectionCounts.clear();
        }
    }

    protected void scheduleDrains() {
        if (DEBUG) {
            Log.d(TAG, "setCounterMetricsAlarm()");
        }
        if (mAlarmManager == null) {
            mAlarmManager = mContext.getSystemService(AlarmManager.class);
        }
        mAlarmManager.setRepeating(
                AlarmManager.ELAPSED_REALTIME_WAKEUP,
                SystemClock.elapsedRealtime(),
                BLUETOOTH_COUNTER_METRICS_ACTION_DURATION_MILLIS,
                getDrainIntent());
    }

    protected void writeCounter(int key, long count) {
        BluetoothStatsLog.write(
                BluetoothStatsLog.BLUETOOTH_CODE_PATH_COUNTER, key, count);
    }

    protected void drainBufferedCounters() {
        Log.i(TAG, "drainBufferedCounters().");
        synchronized (mLock) {
            // send mCounters to westworld
            for (int key : mCounters.keySet()) {
                writeCounter(key, mCounters.get(key));
            }
            mCounters.clear();
        }
    }

    public boolean close() {
        if (!mInitialized) {
            return false;
        }
        if (DEBUG) {
            Log.d(TAG, "close()");
        }
        cancelPendingDrain();
        drainBufferedCounters();
        mAlarmManager = null;
        mContext = null;
        mInitialized = false;
        return true;
    }
    protected void cancelPendingDrain() {
        PendingIntent pIntent = getDrainIntent();
        pIntent.cancel();
        mAlarmManager.cancel(pIntent);
    }

    private PendingIntent getDrainIntent() {
        Intent counterMetricsIntent = new Intent(BLUETOOTH_COUNTER_METRICS_ACTION);
        return PendingIntent.getBroadcast(
                mContext, 0, counterMetricsIntent, PendingIntent.FLAG_IMMUTABLE);
    }
}
+5 −0
Original line number Diff line number Diff line
@@ -97,6 +97,7 @@ public class AdapterServiceTest {
    private @Mock Binder mBinder;
    private @Mock AudioManager mAudioManager;
    private @Mock android.app.Application mApplication;
    private @Mock MetricsLogger mMockMetricsLogger;

    // BatteryStatsManager is final and cannot be mocked with regular mockito, so just mock the
    // underlying binder calls.
@@ -215,6 +216,10 @@ public class AdapterServiceTest {
        when(mMockService.getName()).thenReturn("Service1");
        when(mMockService2.getName()).thenReturn("Service2");

        when(mMockMetricsLogger.init(any())).thenReturn(true);
        when(mMockMetricsLogger.close()).thenReturn(true);
        mAdapterService.setMetricsLogger(mMockMetricsLogger);

        // Attach a context to the service for permission checks.
        mAdapterService.attach(mMockContext, null, null, null, mApplication, null);
        mAdapterService.onCreate();
+103 −1
Original line number Diff line number Diff line
@@ -15,9 +15,13 @@
 */
package com.android.bluetooth.btservice;

import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doReturn;

import androidx.test.filters.MediumTest;
import androidx.test.runner.AndroidJUnit4;

import com.android.bluetooth.btservice.AdapterService;
import com.android.bluetooth.BluetoothMetricsProto.BluetoothLog;
import com.android.bluetooth.BluetoothMetricsProto.ProfileConnectionStats;
import com.android.bluetooth.BluetoothMetricsProto.ProfileId;
@@ -27,6 +31,8 @@ import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.util.HashMap;
import java.util.List;
@@ -37,17 +43,42 @@ import java.util.List;
@MediumTest
@RunWith(AndroidJUnit4.class)
public class MetricsLoggerTest {
    private TestableMetricsLogger mTestableMetricsLogger;
    @Mock
    private AdapterService mMockAdapterService;

    public class TestableMetricsLogger extends MetricsLogger {
        public HashMap<Integer, Long> mTestableCounters = new HashMap<>();

        @Override
        protected void writeCounter(int key, long count) {
            mTestableCounters.put(key, count);
        }

        @Override
        protected void scheduleDrains() {
        }

        @Override
        protected void cancelPendingDrain() {
        }
    }

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        // Dump metrics to clean up internal states
        MetricsLogger.dumpProto(BluetoothLog.newBuilder());
        mTestableMetricsLogger = new TestableMetricsLogger();
        doReturn(null)
                .when(mMockAdapterService).registerReceiver(any(), any());
    }

    @After
    public void tearDown() {
        // Dump metrics to clean up internal states
        MetricsLogger.dumpProto(BluetoothLog.newBuilder());
        mTestableMetricsLogger.close();
    }

    /**
@@ -104,4 +135,75 @@ public class MetricsLoggerTest {
        return profileUsageStatsMap;
    }

    /**
     * Test add counters and send them to westworld
     */
    @Test
    public void testAddAndSendCountersNormalCases() {
        mTestableMetricsLogger.init(mMockAdapterService);
        mTestableMetricsLogger.count(1, 10);
        mTestableMetricsLogger.count(1, 10);
        mTestableMetricsLogger.count(2, 5);
        mTestableMetricsLogger.drainBufferedCounters();

        Assert.assertEquals(20L, mTestableMetricsLogger.mTestableCounters.get(1).longValue());
        Assert.assertEquals(5L, mTestableMetricsLogger.mTestableCounters.get(2).longValue());

        mTestableMetricsLogger.count(1, 3);
        mTestableMetricsLogger.count(2, 5);
        mTestableMetricsLogger.count(2, 5);
        mTestableMetricsLogger.count(3, 1);
        mTestableMetricsLogger.drainBufferedCounters();
        Assert.assertEquals(
                3L, mTestableMetricsLogger.mTestableCounters.get(1).longValue());
        Assert.assertEquals(
                10L, mTestableMetricsLogger.mTestableCounters.get(2).longValue());
        Assert.assertEquals(
                1L, mTestableMetricsLogger.mTestableCounters.get(3).longValue());
    }

    @Test
    public void testAddAndSendCountersCornerCases() {
        mTestableMetricsLogger.init(mMockAdapterService);
        Assert.assertTrue(mTestableMetricsLogger.isInitialized());
        mTestableMetricsLogger.count(1, -1);
        mTestableMetricsLogger.count(3, 0);
        mTestableMetricsLogger.count(2, 10);
        mTestableMetricsLogger.count(2, Long.MAX_VALUE - 8L);
        mTestableMetricsLogger.drainBufferedCounters();

        Assert.assertFalse(mTestableMetricsLogger.mTestableCounters.containsKey(1));
        Assert.assertFalse(mTestableMetricsLogger.mTestableCounters.containsKey(3));
        Assert.assertEquals(
                Long.MAX_VALUE, mTestableMetricsLogger.mTestableCounters.get(2).longValue());
    }

    @Test
    public void testMetricsLoggerClose() {
        mTestableMetricsLogger.init(mMockAdapterService);
        mTestableMetricsLogger.count(1, 1);
        mTestableMetricsLogger.count(2, 10);
        mTestableMetricsLogger.count(2, Long.MAX_VALUE);
        mTestableMetricsLogger.close();

        Assert.assertEquals(
                1, mTestableMetricsLogger.mTestableCounters.get(1).longValue());
        Assert.assertEquals(
                Long.MAX_VALUE, mTestableMetricsLogger.mTestableCounters.get(2).longValue());
    }

    @Test
    public void testMetricsLoggerNotInit() {
        Assert.assertFalse(mTestableMetricsLogger.count(1, 1));
        mTestableMetricsLogger.drainBufferedCounters();
        Assert.assertFalse(mTestableMetricsLogger.mTestableCounters.containsKey(1));
        Assert.assertFalse(mTestableMetricsLogger.close());
    }

    @Test
    public void testAddAndSendCountersDoubleInit() {
        Assert.assertTrue(mTestableMetricsLogger.init(mMockAdapterService));
        Assert.assertTrue(mTestableMetricsLogger.isInitialized());
        Assert.assertFalse(mTestableMetricsLogger.init(mMockAdapterService));
    }
}
 No newline at end of file