diff options
-rw-r--r-- | android/app/src/com/android/bluetooth/btservice/MetricsLogger.java | 93 | ||||
-rw-r--r-- | android/app/tests/unit/src/com/android/bluetooth/btservice/MetricsLoggerTest.java | 60 |
2 files changed, 153 insertions, 0 deletions
diff --git a/android/app/src/com/android/bluetooth/btservice/MetricsLogger.java b/android/app/src/com/android/bluetooth/btservice/MetricsLogger.java index 87794605dc..f59e69a42d 100644 --- a/android/app/src/com/android/bluetooth/btservice/MetricsLogger.java +++ b/android/app/src/com/android/bluetooth/btservice/MetricsLogger.java @@ -37,6 +37,12 @@ import static com.android.bluetooth.BluetoothStatsLog.BLUETOOTH_CROSS_LAYER_EVEN import static com.android.bluetooth.BluetoothStatsLog.BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__EVENT_TYPE__PROFILE_CONNECTION_VOLUME_CONTROL; import static com.android.bluetooth.BluetoothStatsLog.BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__STATE__STATE_BONDED; import static com.android.bluetooth.BluetoothStatsLog.BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__STATE__STATE_NONE; +import static com.android.bluetooth.BluetoothStatsLog.HEARING_DEVICE_ACTIVE_EVENT_REPORTED__DEVICE_TYPE__ASHA; +import static com.android.bluetooth.BluetoothStatsLog.HEARING_DEVICE_ACTIVE_EVENT_REPORTED__DEVICE_TYPE__CLASSIC; +import static com.android.bluetooth.BluetoothStatsLog.HEARING_DEVICE_ACTIVE_EVENT_REPORTED__DEVICE_TYPE__LE_AUDIO; +import static com.android.bluetooth.BluetoothStatsLog.HEARING_DEVICE_ACTIVE_EVENT_REPORTED__TIME_PERIOD__DAY; +import static com.android.bluetooth.BluetoothStatsLog.HEARING_DEVICE_ACTIVE_EVENT_REPORTED__TIME_PERIOD__MONTH; +import static com.android.bluetooth.BluetoothStatsLog.HEARING_DEVICE_ACTIVE_EVENT_REPORTED__TIME_PERIOD__WEEK; import static com.android.bluetooth.BtRestrictedStatsLog.RESTRICTED_BLUETOOTH_DEVICE_NAME_REPORTED; import android.app.AlarmManager; @@ -45,6 +51,7 @@ import android.bluetooth.BluetoothA2dpSink; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothAvrcpController; import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothHapClient; import android.bluetooth.BluetoothHeadset; import android.bluetooth.BluetoothHeadsetClient; import android.bluetooth.BluetoothHearingAid; @@ -60,11 +67,13 @@ import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothProtoEnums; import android.bluetooth.BluetoothSap; import android.content.BroadcastReceiver; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.Build; import android.os.SystemClock; +import android.provider.Settings; import android.util.Log; import android.util.proto.ProtoOutputStream; @@ -87,11 +96,14 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.time.LocalDateTime; +import java.time.ZoneId; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.function.BiPredicate; /** Class of Bluetooth Metrics */ public class MetricsLogger { @@ -273,6 +285,7 @@ public class MetricsLogger { filter.addAction(BluetoothPbap.ACTION_CONNECTION_STATE_CHANGED); filter.addAction(BluetoothPbapClient.ACTION_CONNECTION_STATE_CHANGED); filter.addAction(BluetoothSap.ACTION_CONNECTION_STATE_CHANGED); + filter.addAction(BluetoothHapClient.ACTION_HAP_CONNECTION_STATE_CHANGED); mAdapterService.registerReceiver(mReceiver, filter); } @@ -285,9 +298,18 @@ public class MetricsLogger { Log.w(TAG, "Received intent with null action"); return; } + BluetoothDevice device = + intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1); switch (action) { case BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED: logConnectionStateChanges(BluetoothProfile.A2DP, intent); + if (state == BluetoothProfile.STATE_CONNECTED + && isMedicalDevice(device)) { + updateHearingDeviceActiveTime( + device, + HEARING_DEVICE_ACTIVE_EVENT_REPORTED__DEVICE_TYPE__CLASSIC); + } break; case BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED: logConnectionStateChanges(BluetoothProfile.A2DP_SINK, intent); @@ -297,12 +319,23 @@ public class MetricsLogger { break; case BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED: logConnectionStateChanges(BluetoothProfile.HEADSET, intent); + if (state == BluetoothProfile.STATE_CONNECTED + && isMedicalDevice(device)) { + updateHearingDeviceActiveTime( + device, + HEARING_DEVICE_ACTIVE_EVENT_REPORTED__DEVICE_TYPE__CLASSIC); + } break; case BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED: logConnectionStateChanges(BluetoothProfile.HEADSET_CLIENT, intent); break; case BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED: logConnectionStateChanges(BluetoothProfile.HEARING_AID, intent); + if (state == BluetoothProfile.STATE_CONNECTED) { + updateHearingDeviceActiveTime( + device, + HEARING_DEVICE_ACTIVE_EVENT_REPORTED__DEVICE_TYPE__ASHA); + } break; case BluetoothHidDevice.ACTION_CONNECTION_STATE_CHANGED: logConnectionStateChanges(BluetoothProfile.HID_DEVICE, intent); @@ -331,6 +364,14 @@ public class MetricsLogger { case BluetoothSap.ACTION_CONNECTION_STATE_CHANGED: logConnectionStateChanges(BluetoothProfile.SAP, intent); break; + case BluetoothHapClient.ACTION_HAP_CONNECTION_STATE_CHANGED: + if (state == BluetoothProfile.STATE_CONNECTED) { + updateHearingDeviceActiveTime( + device, + HEARING_DEVICE_ACTIVE_EVENT_REPORTED__DEVICE_TYPE__LE_AUDIO + ); + } + break; default: Log.w(TAG, "Received unknown intent " + intent); break; @@ -915,4 +956,56 @@ public class MetricsLogger { syncStatus, getRemoteDeviceInfoProto(device, false)); } + + void logHearingDeviceActiveEvent(BluetoothDevice device, int type, int timePeriod) { + BluetoothStatsLog.write( + BluetoothStatsLog.HEARING_DEVICE_ACTIVE_EVENT_REPORTED, + type, + timePeriod, + getRemoteDeviceInfoProto(device, true)); + } + + void updateHearingDeviceActiveTime(BluetoothDevice device, int deviceTypeProto) { + // Time comparison includes a +/- 1 hour tolerance to prevent data loss + updateLastActiveTime( + device, + deviceTypeProto, + HEARING_DEVICE_ACTIVE_EVENT_REPORTED__TIME_PERIOD__DAY, + "last_active_day", + (now, lastActive) -> now.isAfter(lastActive.plusDays(1).minusHours(1))); + updateLastActiveTime( + device, + deviceTypeProto, + HEARING_DEVICE_ACTIVE_EVENT_REPORTED__TIME_PERIOD__WEEK, + "last_active_week", + (now, lastActive) -> now.isAfter(lastActive.plusWeeks(1).minusHours(1))); + updateLastActiveTime( + device, + deviceTypeProto, + HEARING_DEVICE_ACTIVE_EVENT_REPORTED__TIME_PERIOD__MONTH, + "last_active_month", + (now, lastActive) -> now.isAfter(lastActive.plusMonths(1).minusHours(1))); + } + + private void updateLastActiveTime( + BluetoothDevice device, + int deviceTypeProto, + int timePeriodProto, + String timePeriodSettingsKey, + BiPredicate<LocalDateTime, LocalDateTime> timeComparison) { + final ContentResolver contentResolver = mAdapterService.getContentResolver(); + final String lastActive = Settings.Secure.getString(contentResolver, timePeriodSettingsKey); + final LocalDateTime now = LocalDateTime.now(ZoneId.systemDefault()); + if (lastActive == null || timeComparison.test(now, LocalDateTime.parse(lastActive))) { + Settings.Secure.putString(contentResolver, timePeriodSettingsKey, now.toString()); + logHearingDeviceActiveEvent(device, deviceTypeProto, timePeriodProto); + } + } + + private boolean isMedicalDevice(BluetoothDevice device) { + final String deviceName = mAdapterService.getRemoteName(device); + final List<String> wordBreakdownList = getWordBreakdownList(deviceName); + boolean isMedicalDevice = !getMatchedStringForMedicalDevice(wordBreakdownList).isEmpty(); + return isMedicalDevice; + } } diff --git a/android/app/tests/unit/src/com/android/bluetooth/btservice/MetricsLoggerTest.java b/android/app/tests/unit/src/com/android/bluetooth/btservice/MetricsLoggerTest.java index 1b057b3a7e..ae3db21382 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/btservice/MetricsLoggerTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/btservice/MetricsLoggerTest.java @@ -20,12 +20,24 @@ import static com.android.bluetooth.TestUtils.getTestDevice; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + import android.bluetooth.BluetoothDevice; +import android.content.ContentResolver; +import android.provider.Settings; +import androidx.test.core.app.ApplicationProvider; import androidx.test.filters.MediumTest; import androidx.test.runner.AndroidJUnit4; import com.android.bluetooth.BluetoothMetricsProto.BluetoothRemoteDeviceInformation; +import com.android.bluetooth.BluetoothStatsLog; import com.google.common.hash.BloomFilter; import com.google.common.hash.Funnels; @@ -37,9 +49,12 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.Mockito; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.time.LocalDateTime; +import java.time.ZoneId; import java.util.HashMap; import java.util.Map; @@ -236,6 +251,51 @@ public class MetricsLoggerTest { assertThat(mTestableMetricsLogger.logAllowlistedDeviceNameHash(1, "")).isEmpty(); } + @Test + public void testUpdateHearingDeviceActiveTime() { + BluetoothDevice bluetoothDevice = getTestDevice(0); + int day = BluetoothStatsLog.HEARING_DEVICE_ACTIVE_EVENT_REPORTED__TIME_PERIOD__DAY; + int week = BluetoothStatsLog.HEARING_DEVICE_ACTIVE_EVENT_REPORTED__TIME_PERIOD__WEEK; + int month = BluetoothStatsLog.HEARING_DEVICE_ACTIVE_EVENT_REPORTED__TIME_PERIOD__MONTH; + doReturn(ApplicationProvider.getApplicationContext().getContentResolver()) + .when(mAdapterService) + .getContentResolver(); + + // last active time is 2 days ago, should update last active day + TestableMetricsLogger logger = spy(mTestableMetricsLogger); + prepareLastActiveTimeDaysAgo(2); + logger.updateHearingDeviceActiveTime(bluetoothDevice, 1); + verify(logger).logHearingDeviceActiveEvent(any(), anyInt(), eq(day)); + verify(logger, never()).logHearingDeviceActiveEvent(any(), anyInt(), eq(week)); + verify(logger, never()).logHearingDeviceActiveEvent(any(), anyInt(), eq(month)); + + // last active time is 8 days ago, should update last active day and week + Mockito.reset(logger); + prepareLastActiveTimeDaysAgo(8); + logger.updateHearingDeviceActiveTime(bluetoothDevice, 1); + verify(logger).logHearingDeviceActiveEvent(any(), anyInt(), eq(day)); + verify(logger).logHearingDeviceActiveEvent(any(), anyInt(), eq(week)); + verify(logger, never()).logHearingDeviceActiveEvent(any(), anyInt(), eq(month)); + + // last active time is 60 days ago, should update last active day, week and month + Mockito.reset(logger); + prepareLastActiveTimeDaysAgo(60); + logger.updateHearingDeviceActiveTime(bluetoothDevice, 1); + verify(logger).logHearingDeviceActiveEvent(any(), anyInt(), eq(day)); + verify(logger).logHearingDeviceActiveEvent(any(), anyInt(), eq(week)); + verify(logger).logHearingDeviceActiveEvent(any(), anyInt(), eq(month)); + } + + private static void prepareLastActiveTimeDaysAgo(int days) { + final ContentResolver contentResolver = + ApplicationProvider.getApplicationContext().getContentResolver(); + final LocalDateTime now = LocalDateTime.now(ZoneId.systemDefault()); + final String lastActive = now.minusDays(days).toString(); + Settings.Secure.putString(contentResolver, "last_active_day", lastActive); + Settings.Secure.putString(contentResolver, "last_active_week", lastActive); + Settings.Secure.putString(contentResolver, "last_active_month", lastActive); + } + private void initTestingBloomfilter() throws IOException { byte[] bloomfilterData = DeviceBloomfilterGenerator.hexStringToByteArray( |