1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.bluetooth.btservice; 17 18 import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE; 19 import static android.bluetooth.BluetoothProfile.STATE_CONNECTING; 20 21 import static com.android.bluetooth.BluetoothStatsLog.BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__EVENT_TYPE__BOND; 22 import static com.android.bluetooth.BluetoothStatsLog.BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__EVENT_TYPE__PROFILE_CONNECTION; 23 import static com.android.bluetooth.BluetoothStatsLog.BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__EVENT_TYPE__PROFILE_CONNECTION_A2DP; 24 import static com.android.bluetooth.BluetoothStatsLog.BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__EVENT_TYPE__PROFILE_CONNECTION_A2DP_SINK; 25 import static com.android.bluetooth.BluetoothStatsLog.BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__EVENT_TYPE__PROFILE_CONNECTION_BATTERY; 26 import static com.android.bluetooth.BluetoothStatsLog.BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__EVENT_TYPE__PROFILE_CONNECTION_CSIP_SET_COORDINATOR; 27 import static com.android.bluetooth.BluetoothStatsLog.BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__EVENT_TYPE__PROFILE_CONNECTION_HAP_CLIENT; 28 import static com.android.bluetooth.BluetoothStatsLog.BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__EVENT_TYPE__PROFILE_CONNECTION_HEADSET; 29 import static com.android.bluetooth.BluetoothStatsLog.BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__EVENT_TYPE__PROFILE_CONNECTION_HEADSET_CLIENT; 30 import static com.android.bluetooth.BluetoothStatsLog.BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__EVENT_TYPE__PROFILE_CONNECTION_HEARING_AID; 31 import static com.android.bluetooth.BluetoothStatsLog.BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__EVENT_TYPE__PROFILE_CONNECTION_HID_HOST; 32 import static com.android.bluetooth.BluetoothStatsLog.BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__EVENT_TYPE__PROFILE_CONNECTION_LE_AUDIO; 33 import static com.android.bluetooth.BluetoothStatsLog.BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__EVENT_TYPE__PROFILE_CONNECTION_LE_AUDIO_BROADCAST_ASSISTANT; 34 import static com.android.bluetooth.BluetoothStatsLog.BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__EVENT_TYPE__PROFILE_CONNECTION_MAP_CLIENT; 35 import static com.android.bluetooth.BluetoothStatsLog.BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__EVENT_TYPE__PROFILE_CONNECTION_PAN; 36 import static com.android.bluetooth.BluetoothStatsLog.BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__EVENT_TYPE__PROFILE_CONNECTION_PBAP_CLIENT; 37 import static com.android.bluetooth.BluetoothStatsLog.BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__EVENT_TYPE__PROFILE_CONNECTION_VOLUME_CONTROL; 38 import static com.android.bluetooth.BluetoothStatsLog.BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__STATE__STATE_BONDED; 39 import static com.android.bluetooth.BluetoothStatsLog.BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__STATE__STATE_NONE; 40 import static com.android.bluetooth.BluetoothStatsLog.HEARING_DEVICE_ACTIVE_EVENT_REPORTED__DEVICE_TYPE__ASHA; 41 import static com.android.bluetooth.BluetoothStatsLog.HEARING_DEVICE_ACTIVE_EVENT_REPORTED__DEVICE_TYPE__CLASSIC; 42 import static com.android.bluetooth.BluetoothStatsLog.HEARING_DEVICE_ACTIVE_EVENT_REPORTED__DEVICE_TYPE__LE_AUDIO; 43 import static com.android.bluetooth.BluetoothStatsLog.HEARING_DEVICE_ACTIVE_EVENT_REPORTED__TIME_PERIOD__DAY; 44 import static com.android.bluetooth.BluetoothStatsLog.HEARING_DEVICE_ACTIVE_EVENT_REPORTED__TIME_PERIOD__MONTH; 45 import static com.android.bluetooth.BluetoothStatsLog.HEARING_DEVICE_ACTIVE_EVENT_REPORTED__TIME_PERIOD__WEEK; 46 import static com.android.bluetooth.BtRestrictedStatsLog.RESTRICTED_BLUETOOTH_DEVICE_NAME_REPORTED; 47 48 import android.app.AlarmManager; 49 import android.bluetooth.BluetoothA2dp; 50 import android.bluetooth.BluetoothA2dpSink; 51 import android.bluetooth.BluetoothAdapter; 52 import android.bluetooth.BluetoothAvrcpController; 53 import android.bluetooth.BluetoothDevice; 54 import android.bluetooth.BluetoothHapClient; 55 import android.bluetooth.BluetoothHeadset; 56 import android.bluetooth.BluetoothHeadsetClient; 57 import android.bluetooth.BluetoothHearingAid; 58 import android.bluetooth.BluetoothHidDevice; 59 import android.bluetooth.BluetoothHidHost; 60 import android.bluetooth.BluetoothLeAudio; 61 import android.bluetooth.BluetoothMap; 62 import android.bluetooth.BluetoothMapClient; 63 import android.bluetooth.BluetoothPan; 64 import android.bluetooth.BluetoothPbap; 65 import android.bluetooth.BluetoothPbapClient; 66 import android.bluetooth.BluetoothProfile; 67 import android.bluetooth.BluetoothProtoEnums; 68 import android.bluetooth.BluetoothSap; 69 import android.content.BroadcastReceiver; 70 import android.content.ContentResolver; 71 import android.content.Context; 72 import android.content.Intent; 73 import android.content.IntentFilter; 74 import android.os.Build; 75 import android.os.SystemClock; 76 import android.provider.Settings; 77 import android.util.Log; 78 import android.util.proto.ProtoOutputStream; 79 80 import androidx.annotation.RequiresApi; 81 82 import com.android.bluetooth.BluetoothMetricsProto.BluetoothRemoteDeviceInformation; 83 import com.android.bluetooth.BluetoothStatsLog; 84 import com.android.bluetooth.BtRestrictedStatsLog; 85 import com.android.bluetooth.Utils; 86 import com.android.bluetooth.bass_client.BassConstants; 87 import com.android.internal.annotations.VisibleForTesting; 88 89 import com.google.common.hash.BloomFilter; 90 import com.google.common.hash.Funnels; 91 92 import java.io.ByteArrayInputStream; 93 import java.io.File; 94 import java.io.FileInputStream; 95 import java.io.IOException; 96 import java.nio.charset.StandardCharsets; 97 import java.security.MessageDigest; 98 import java.security.NoSuchAlgorithmException; 99 import java.time.LocalDateTime; 100 import java.time.ZoneId; 101 import java.util.ArrayList; 102 import java.util.Collections; 103 import java.util.HashMap; 104 import java.util.List; 105 import java.util.Locale; 106 import java.util.function.BiPredicate; 107 108 /** Class of Bluetooth Metrics */ 109 public class MetricsLogger { 110 private static final String TAG = 111 Utils.TAG_PREFIX_BLUETOOTH + MetricsLogger.class.getSimpleName(); 112 113 private static final String BLOOMFILTER_PATH = "/data/misc/bluetooth"; 114 private static final String BLOOMFILTER_FILE = "/devices_for_metrics_v3"; 115 private static final String MEDICAL_DEVICE_BLOOMFILTER_FILE = "/medical_devices_for_metrics_v1"; 116 public static final String BLOOMFILTER_FULL_PATH = BLOOMFILTER_PATH + BLOOMFILTER_FILE; 117 public static final String MEDICAL_DEVICE_BLOOMFILTER_FULL_PATH = 118 BLOOMFILTER_PATH + MEDICAL_DEVICE_BLOOMFILTER_FILE; 119 120 // 6 hours timeout for counter metrics 121 private static final long BLUETOOTH_COUNTER_METRICS_ACTION_DURATION_MILLIS = 6L * 3600L * 1000L; 122 private static final int MAX_WORDS_ALLOWED_IN_DEVICE_NAME = 7; 123 124 HashMap<Integer, Long> mCounters = new HashMap<>(); 125 private static volatile MetricsLogger sInstance = null; 126 private AdapterService mAdapterService = null; 127 private RemoteDevices mRemoteDevices = null; 128 private AlarmManager mAlarmManager = null; 129 private boolean mInitialized = false; 130 private static final Object sLock = new Object(); 131 private BloomFilter<byte[]> mBloomFilter = null; 132 protected boolean mBloomFilterInitialized = false; 133 134 private BloomFilter<byte[]> mMedicalDeviceBloomFilter = null; 135 136 protected boolean mMedicalDeviceBloomFilterInitialized = false; 137 138 private final AlarmManager.OnAlarmListener mOnAlarmListener = 139 new AlarmManager.OnAlarmListener() { 140 @Override 141 public void onAlarm() { 142 drainBufferedCounters(); 143 scheduleDrains(); 144 } 145 }; 146 getInstance()147 public static MetricsLogger getInstance() { 148 if (sInstance == null) { 149 synchronized (sLock) { 150 if (sInstance == null) { 151 sInstance = new MetricsLogger(); 152 } 153 } 154 } 155 return sInstance; 156 } 157 158 /** 159 * Allow unit tests to substitute MetricsLogger with a test instance 160 * 161 * @param instance a test instance of the MetricsLogger 162 */ 163 @VisibleForTesting setInstanceForTesting(MetricsLogger instance)164 public static void setInstanceForTesting(MetricsLogger instance) { 165 Utils.enforceInstrumentationTestMode(); 166 synchronized (sLock) { 167 Log.d(TAG, "setInstanceForTesting(), set to " + instance); 168 sInstance = instance; 169 } 170 } 171 172 @VisibleForTesting isInitialized()173 boolean isInitialized() { 174 return mInitialized; 175 } 176 initBloomFilter(String path)177 public boolean initBloomFilter(String path) { 178 try { 179 File file = new File(path); 180 if (!file.exists()) { 181 Log.w(TAG, "MetricsLogger is creating a new Bloomfilter file"); 182 DeviceBloomfilterGenerator.generateDefaultBloomfilter(path); 183 } 184 185 FileInputStream in = new FileInputStream(new File(path)); 186 mBloomFilter = BloomFilter.readFrom(in, Funnels.byteArrayFunnel()); 187 mBloomFilterInitialized = true; 188 } catch (IOException e1) { 189 Log.w(TAG, "MetricsLogger can't read the BloomFilter file."); 190 byte[] bloomfilterData = 191 DeviceBloomfilterGenerator.hexStringToByteArray( 192 DeviceBloomfilterGenerator.BLOOM_FILTER_DEFAULT); 193 try { 194 mBloomFilter = 195 BloomFilter.readFrom( 196 new ByteArrayInputStream(bloomfilterData), 197 Funnels.byteArrayFunnel()); 198 mBloomFilterInitialized = true; 199 Log.i(TAG, "The default bloomfilter is used"); 200 return true; 201 } catch (IOException e2) { 202 Log.w(TAG, "The default bloomfilter can't be used."); 203 } 204 return false; 205 } 206 return true; 207 } 208 209 /** Initialize medical device bloom filter */ initMedicalDeviceBloomFilter(String path)210 public boolean initMedicalDeviceBloomFilter(String path) { 211 try { 212 File medicalDeviceFile = new File(path); 213 if (!medicalDeviceFile.exists()) { 214 Log.w(TAG, "MetricsLogger is creating a new medical device Bloomfilter file"); 215 MedicalDeviceBloomfilterGenerator.generateDefaultBloomfilter(path); 216 } 217 218 FileInputStream inputStream = new FileInputStream(new File(path)); 219 mMedicalDeviceBloomFilter = 220 BloomFilter.readFrom(inputStream, Funnels.byteArrayFunnel()); 221 mMedicalDeviceBloomFilterInitialized = true; 222 } catch (IOException e1) { 223 Log.w(TAG, "MetricsLogger can't read the medical device BloomFilter file."); 224 byte[] bloomfilterData = 225 MedicalDeviceBloomfilterGenerator.hexStringToByteArray( 226 MedicalDeviceBloomfilterGenerator.BLOOM_FILTER_DEFAULT); 227 try { 228 mMedicalDeviceBloomFilter = 229 BloomFilter.readFrom( 230 new ByteArrayInputStream(bloomfilterData), 231 Funnels.byteArrayFunnel()); 232 mMedicalDeviceBloomFilterInitialized = true; 233 Log.i(TAG, "The medical device bloomfilter is used"); 234 return true; 235 } catch (IOException e2) { 236 Log.w(TAG, "The medical device bloomfilter can't be used."); 237 } 238 return false; 239 } 240 return true; 241 } 242 setBloomfilter(BloomFilter bloomfilter)243 protected void setBloomfilter(BloomFilter bloomfilter) { 244 mBloomFilter = bloomfilter; 245 } 246 setMedicalDeviceBloomfilter(BloomFilter bloomfilter)247 protected void setMedicalDeviceBloomfilter(BloomFilter bloomfilter) { 248 mMedicalDeviceBloomFilter = bloomfilter; 249 } 250 init(AdapterService adapterService, RemoteDevices remoteDevices)251 void init(AdapterService adapterService, RemoteDevices remoteDevices) { 252 if (mInitialized) { 253 return; 254 } 255 mInitialized = true; 256 mAdapterService = adapterService; 257 mRemoteDevices = remoteDevices; 258 scheduleDrains(); 259 if (!initBloomFilter(BLOOMFILTER_FULL_PATH)) { 260 Log.w(TAG, "MetricsLogger can't initialize the bloomfilter"); 261 // The class is for multiple metrics tasks. 262 // We still want to use this class even if the bloomfilter isn't initialized 263 // so still return true here. 264 } 265 if (!initMedicalDeviceBloomFilter(MEDICAL_DEVICE_BLOOMFILTER_FULL_PATH)) { 266 Log.w(TAG, "MetricsLogger can't initialize the medical device bloomfilter"); 267 // The class is for multiple metrics tasks. 268 // We still want to use this class even if the bloomfilter isn't initialized 269 // so still return true here. 270 } 271 IntentFilter filter = new IntentFilter(); 272 filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); 273 filter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED); 274 filter.addAction(BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED); 275 filter.addAction(BluetoothAvrcpController.ACTION_CONNECTION_STATE_CHANGED); 276 filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED); 277 filter.addAction(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED); 278 filter.addAction(BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED); 279 filter.addAction(BluetoothHidDevice.ACTION_CONNECTION_STATE_CHANGED); 280 filter.addAction(BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED); 281 filter.addAction(BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED); 282 filter.addAction(BluetoothMap.ACTION_CONNECTION_STATE_CHANGED); 283 filter.addAction(BluetoothMapClient.ACTION_CONNECTION_STATE_CHANGED); 284 filter.addAction(BluetoothPan.ACTION_CONNECTION_STATE_CHANGED); 285 filter.addAction(BluetoothPbap.ACTION_CONNECTION_STATE_CHANGED); 286 filter.addAction(BluetoothPbapClient.ACTION_CONNECTION_STATE_CHANGED); 287 filter.addAction(BluetoothSap.ACTION_CONNECTION_STATE_CHANGED); 288 filter.addAction(BluetoothHapClient.ACTION_HAP_CONNECTION_STATE_CHANGED); 289 mAdapterService.registerReceiver(mReceiver, filter); 290 } 291 292 private final BroadcastReceiver mReceiver = 293 new BroadcastReceiver() { 294 @Override 295 public void onReceive(Context context, Intent intent) { 296 String action = intent.getAction(); 297 if (action == null) { 298 Log.w(TAG, "Received intent with null action"); 299 return; 300 } 301 BluetoothDevice device = 302 intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 303 int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1); 304 switch (action) { 305 case BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED: 306 logConnectionStateChanges(BluetoothProfile.A2DP, intent); 307 if (state == BluetoothProfile.STATE_CONNECTED 308 && isMedicalDevice(device)) { 309 updateHearingDeviceActiveTime( 310 device, 311 HEARING_DEVICE_ACTIVE_EVENT_REPORTED__DEVICE_TYPE__CLASSIC); 312 } 313 break; 314 case BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED: 315 logConnectionStateChanges(BluetoothProfile.A2DP_SINK, intent); 316 break; 317 case BluetoothAvrcpController.ACTION_CONNECTION_STATE_CHANGED: 318 logConnectionStateChanges(BluetoothProfile.AVRCP_CONTROLLER, intent); 319 break; 320 case BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED: 321 logConnectionStateChanges(BluetoothProfile.HEADSET, intent); 322 if (state == BluetoothProfile.STATE_CONNECTED 323 && isMedicalDevice(device)) { 324 updateHearingDeviceActiveTime( 325 device, 326 HEARING_DEVICE_ACTIVE_EVENT_REPORTED__DEVICE_TYPE__CLASSIC); 327 } 328 break; 329 case BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED: 330 logConnectionStateChanges(BluetoothProfile.HEADSET_CLIENT, intent); 331 break; 332 case BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED: 333 logConnectionStateChanges(BluetoothProfile.HEARING_AID, intent); 334 if (state == BluetoothProfile.STATE_CONNECTED) { 335 updateHearingDeviceActiveTime( 336 device, 337 HEARING_DEVICE_ACTIVE_EVENT_REPORTED__DEVICE_TYPE__ASHA); 338 } 339 break; 340 case BluetoothHidDevice.ACTION_CONNECTION_STATE_CHANGED: 341 logConnectionStateChanges(BluetoothProfile.HID_DEVICE, intent); 342 break; 343 case BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED: 344 logConnectionStateChanges(BluetoothProfile.HID_HOST, intent); 345 break; 346 case BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED: 347 logConnectionStateChanges(BluetoothProfile.LE_AUDIO, intent); 348 break; 349 case BluetoothMap.ACTION_CONNECTION_STATE_CHANGED: 350 logConnectionStateChanges(BluetoothProfile.MAP, intent); 351 break; 352 case BluetoothMapClient.ACTION_CONNECTION_STATE_CHANGED: 353 logConnectionStateChanges(BluetoothProfile.MAP_CLIENT, intent); 354 break; 355 case BluetoothPan.ACTION_CONNECTION_STATE_CHANGED: 356 logConnectionStateChanges(BluetoothProfile.PAN, intent); 357 break; 358 case BluetoothPbap.ACTION_CONNECTION_STATE_CHANGED: 359 logConnectionStateChanges(BluetoothProfile.PBAP, intent); 360 break; 361 case BluetoothPbapClient.ACTION_CONNECTION_STATE_CHANGED: 362 logConnectionStateChanges(BluetoothProfile.PBAP_CLIENT, intent); 363 break; 364 case BluetoothSap.ACTION_CONNECTION_STATE_CHANGED: 365 logConnectionStateChanges(BluetoothProfile.SAP, intent); 366 break; 367 case BluetoothHapClient.ACTION_HAP_CONNECTION_STATE_CHANGED: 368 if (state == BluetoothProfile.STATE_CONNECTED) { 369 updateHearingDeviceActiveTime( 370 device, 371 HEARING_DEVICE_ACTIVE_EVENT_REPORTED__DEVICE_TYPE__LE_AUDIO 372 ); 373 } 374 break; 375 default: 376 Log.w(TAG, "Received unknown intent " + intent); 377 break; 378 } 379 } 380 }; 381 logConnectionStateChanges(int profile, Intent connIntent)382 private void logConnectionStateChanges(int profile, Intent connIntent) { 383 BluetoothDevice device = connIntent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 384 int state = connIntent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1); 385 int metricId = mAdapterService.getMetricId(device); 386 if (state == STATE_CONNECTING) { 387 String deviceName = mRemoteDevices.getName(device); 388 BluetoothStatsLog.write( 389 BluetoothStatsLog.BLUETOOTH_DEVICE_NAME_REPORTED, metricId, deviceName); 390 logAllowlistedDeviceNameHash(metricId, deviceName); 391 } 392 BluetoothStatsLog.write( 393 BluetoothStatsLog.BLUETOOTH_CONNECTION_STATE_CHANGED, 394 state, 395 0 /* deprecated */, 396 profile, 397 mAdapterService.obfuscateAddress(device), 398 metricId, 399 0, 400 -1); 401 } 402 cacheCount(int key, long count)403 public boolean cacheCount(int key, long count) { 404 if (!mInitialized) { 405 Log.w(TAG, "MetricsLogger isn't initialized"); 406 return false; 407 } 408 if (count <= 0) { 409 Log.w(TAG, "count is not larger than 0. count: " + count + " key: " + key); 410 return false; 411 } 412 long total = 0; 413 414 synchronized (sLock) { 415 if (mCounters.containsKey(key)) { 416 total = mCounters.get(key); 417 } 418 if (Long.MAX_VALUE - total < count) { 419 Log.w(TAG, "count overflows. count: " + count + " current total: " + total); 420 mCounters.put(key, Long.MAX_VALUE); 421 return false; 422 } 423 mCounters.put(key, total + count); 424 } 425 return true; 426 } 427 scheduleDrains()428 protected void scheduleDrains() { 429 Log.i(TAG, "setCounterMetricsAlarm()"); 430 if (mAlarmManager == null) { 431 mAlarmManager = ((Context) mAdapterService).getSystemService(AlarmManager.class); 432 } 433 mAlarmManager.set( 434 AlarmManager.ELAPSED_REALTIME_WAKEUP, 435 SystemClock.elapsedRealtime() + BLUETOOTH_COUNTER_METRICS_ACTION_DURATION_MILLIS, 436 TAG, 437 mOnAlarmListener, 438 null); 439 } 440 count(int key, long count)441 public boolean count(int key, long count) { 442 if (!mInitialized) { 443 Log.w(TAG, "MetricsLogger isn't initialized"); 444 return false; 445 } 446 if (count <= 0) { 447 Log.w(TAG, "count is not larger than 0. count: " + count + " key: " + key); 448 return false; 449 } 450 BluetoothStatsLog.write(BluetoothStatsLog.BLUETOOTH_CODE_PATH_COUNTER, key, count); 451 return true; 452 } 453 drainBufferedCounters()454 protected void drainBufferedCounters() { 455 Log.i(TAG, "drainBufferedCounters()."); 456 synchronized (sLock) { 457 // send mCounters to statsd 458 for (int key : mCounters.keySet()) { 459 count(key, mCounters.get(key)); 460 } 461 mCounters.clear(); 462 } 463 } 464 close()465 void close() { 466 if (!mInitialized) { 467 return; 468 } 469 Log.d(TAG, "close()"); 470 mAdapterService.unregisterReceiver(mReceiver); 471 cancelPendingDrain(); 472 drainBufferedCounters(); 473 mAlarmManager = null; 474 mInitialized = false; 475 mBloomFilterInitialized = false; 476 mMedicalDeviceBloomFilterInitialized = false; 477 } 478 cancelPendingDrain()479 protected void cancelPendingDrain() { 480 mAlarmManager.cancel(mOnAlarmListener); 481 } 482 writeFieldIfNotNull( ProtoOutputStream proto, long fieldType, long fieldCount, long fieldNumber, Object value)483 private static void writeFieldIfNotNull( 484 ProtoOutputStream proto, 485 long fieldType, 486 long fieldCount, 487 long fieldNumber, 488 Object value) { 489 if (value != null) { 490 try { 491 if (fieldType == ProtoOutputStream.FIELD_TYPE_STRING) { 492 proto.write(fieldType | fieldCount | fieldNumber, value.toString()); 493 } 494 495 if (fieldType == ProtoOutputStream.FIELD_TYPE_INT32) { 496 proto.write(fieldType | fieldCount | fieldNumber, (Integer) value); 497 } 498 } catch (Exception e) { 499 Log.e(TAG, "Error writing field " + fieldNumber + ": " + e.getMessage()); 500 } 501 } 502 } 503 504 /** 505 * Retrieves a byte array containing serialized remote device information for the specified 506 * BluetoothDevice. This data can be used for remote device identification and logging. Does not 507 * include medical remote devices. 508 * 509 * @param device The BluetoothDevice for which to retrieve device information. 510 * @return A byte array containing the serialized remote device information. 511 */ getRemoteDeviceInfoProto(BluetoothDevice device)512 public byte[] getRemoteDeviceInfoProto(BluetoothDevice device) { 513 return mInitialized ? buildRemoteDeviceInfoProto(device, false) : null; 514 } 515 516 /** 517 * Retrieves a byte array containing serialized remote device information for the specified 518 * BluetoothDevice. This data can be used for remote device identification and logging. 519 * 520 * @param device The BluetoothDevice for which to retrieve device information. 521 * @param includeMedicalDevices Should be true only if logging as de-identified metric, 522 * otherwise false. 523 * @return A byte array containing the serialized remote device information. 524 */ getRemoteDeviceInfoProto(BluetoothDevice device, boolean includeMedicalDevices)525 public byte[] getRemoteDeviceInfoProto(BluetoothDevice device, boolean includeMedicalDevices) { 526 return mInitialized ? buildRemoteDeviceInfoProto(device, includeMedicalDevices) : null; 527 } 528 buildRemoteDeviceInfoProto( BluetoothDevice device, boolean includeMedicalDevices)529 private byte[] buildRemoteDeviceInfoProto( 530 BluetoothDevice device, boolean includeMedicalDevices) { 531 ProtoOutputStream proto = new ProtoOutputStream(); 532 533 // write Allowlisted Device Name Hash 534 writeFieldIfNotNull( 535 proto, 536 ProtoOutputStream.FIELD_TYPE_STRING, 537 ProtoOutputStream.FIELD_COUNT_SINGLE, 538 BluetoothRemoteDeviceInformation.ALLOWLISTED_DEVICE_NAME_HASH_FIELD_NUMBER, 539 getAllowlistedDeviceNameHash( 540 mAdapterService.getRemoteName(device), includeMedicalDevices)); 541 542 // write COD 543 writeFieldIfNotNull( 544 proto, 545 ProtoOutputStream.FIELD_TYPE_INT32, 546 ProtoOutputStream.FIELD_COUNT_SINGLE, 547 BluetoothRemoteDeviceInformation.CLASS_OF_DEVICE_FIELD_NUMBER, 548 mAdapterService.getRemoteClass(device)); 549 550 // write OUI 551 writeFieldIfNotNull( 552 proto, 553 ProtoOutputStream.FIELD_TYPE_INT32, 554 ProtoOutputStream.FIELD_COUNT_SINGLE, 555 BluetoothRemoteDeviceInformation.OUI_FIELD_NUMBER, 556 getOui(device)); 557 558 // write deviceTypeMetaData 559 writeFieldIfNotNull( 560 proto, 561 ProtoOutputStream.FIELD_TYPE_INT32, 562 ProtoOutputStream.FIELD_COUNT_SINGLE, 563 BluetoothRemoteDeviceInformation.DEVICE_TYPE_METADATA_FIELD_NUMBER, 564 getDeviceTypeMetaData(device)); 565 566 return proto.getBytes(); 567 } 568 getDeviceTypeMetaData(BluetoothDevice device)569 private int getDeviceTypeMetaData(BluetoothDevice device) { 570 byte[] deviceTypeMetaDataBytes = 571 mAdapterService.getMetadata(device, BluetoothDevice.METADATA_DEVICE_TYPE); 572 573 if (deviceTypeMetaDataBytes == null) { 574 return BluetoothProtoEnums.NOT_AVAILABLE; 575 } 576 String deviceTypeMetaData = new String(deviceTypeMetaDataBytes, StandardCharsets.UTF_8); 577 578 return switch (deviceTypeMetaData) { 579 case "Watch" -> BluetoothProtoEnums.WATCH; 580 case "Untethered Headset" -> BluetoothProtoEnums.UNTETHERED_HEADSET; 581 case "Stylus" -> BluetoothProtoEnums.STYLUS; 582 case "Speaker" -> BluetoothProtoEnums.SPEAKER; 583 case "Headset" -> BluetoothProtoEnums.HEADSET; 584 case "Carkit" -> BluetoothProtoEnums.CARKIT; 585 case "Default" -> BluetoothProtoEnums.DEFAULT; 586 default -> BluetoothProtoEnums.NOT_AVAILABLE; 587 }; 588 } 589 getOui(BluetoothDevice device)590 private static int getOui(BluetoothDevice device) { 591 return Integer.parseInt(device.getAddress().replace(":", "").substring(0, 6), 16); 592 } 593 getWordBreakdownList(String deviceName)594 protected List<String> getWordBreakdownList(String deviceName) { 595 if (deviceName == null) { 596 return Collections.emptyList(); 597 } 598 // remove more than one spaces in a row 599 deviceName = deviceName.trim().replaceAll(" +", " "); 600 // remove non alphanumeric characters and spaces, and transform to lower cases. 601 String[] words = 602 deviceName 603 .replaceAll("[^a-zA-Z0-9 ]", "") 604 .toLowerCase(Locale.ROOT) 605 .split(" ", MAX_WORDS_ALLOWED_IN_DEVICE_NAME + 1); 606 607 if (words.length > MAX_WORDS_ALLOWED_IN_DEVICE_NAME) { 608 // Validity checking here to avoid excessively long sequences 609 return Collections.emptyList(); 610 } 611 // collect the word breakdown in an arraylist 612 ArrayList<String> wordBreakdownList = new ArrayList<String>(); 613 for (int start = 0; start < words.length; start++) { 614 615 StringBuilder deviceNameCombination = new StringBuilder(); 616 for (int end = start; end < words.length; end++) { 617 deviceNameCombination.append(words[end]); 618 wordBreakdownList.add(deviceNameCombination.toString()); 619 } 620 } 621 622 // Prevent returning a mutable list 623 return Collections.unmodifiableList(wordBreakdownList); 624 } 625 626 @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) uploadRestrictedBluetoothDeviceName(List<String> wordBreakdownList)627 protected void uploadRestrictedBluetoothDeviceName(List<String> wordBreakdownList) { 628 for (String word : wordBreakdownList) { 629 BtRestrictedStatsLog.write(RESTRICTED_BLUETOOTH_DEVICE_NAME_REPORTED, word); 630 } 631 } 632 getMatchedString(List<String> wordBreakdownList, boolean includeMedicalDevices)633 private String getMatchedString(List<String> wordBreakdownList, boolean includeMedicalDevices) { 634 if (!mBloomFilterInitialized || wordBreakdownList.isEmpty()) { 635 return ""; 636 } 637 638 String matchedString = ""; 639 for (String word : wordBreakdownList) { 640 byte[] sha256 = getSha256(word); 641 if (mBloomFilter.mightContain(sha256) && word.length() > matchedString.length()) { 642 matchedString = word; 643 } 644 } 645 646 return (matchedString.equals("") && includeMedicalDevices) 647 ? getMatchedStringForMedicalDevice(wordBreakdownList) 648 : matchedString; 649 } 650 getMatchedStringForMedicalDevice(List<String> wordBreakdownList)651 private String getMatchedStringForMedicalDevice(List<String> wordBreakdownList) { 652 String matchedString = ""; 653 for (String word : wordBreakdownList) { 654 byte[] sha256 = getSha256(word); 655 if (mMedicalDeviceBloomFilter.mightContain(sha256) 656 && word.length() > matchedString.length()) { 657 matchedString = word; 658 } 659 } 660 return matchedString; 661 } 662 convertAppImportance(int importance)663 private static int convertAppImportance(int importance) { 664 if (importance < IMPORTANCE_FOREGROUND_SERVICE) { 665 return BluetoothStatsLog 666 .LE_APP_SCAN_STATE_CHANGED__APP_IMPORTANCE__IMPORTANCE_HIGHER_THAN_FGS; 667 } 668 if (importance > IMPORTANCE_FOREGROUND_SERVICE) { 669 return BluetoothStatsLog 670 .LE_APP_SCAN_STATE_CHANGED__APP_IMPORTANCE__IMPORTANCE_LOWER_THAN_FGS; 671 } 672 return BluetoothStatsLog.LE_APP_SCAN_STATE_CHANGED__APP_IMPORTANCE__IMPORTANCE_EQUAL_TO_FGS; 673 } 674 675 /** Logs the app scan stats with app attribution when the app scan state changed. */ logAppScanStateChanged( int[] uids, String[] tags, boolean enabled, boolean isFilterScan, boolean isCallbackScan, int scanCallBackType, int scanType, int scanMode, long reportDelayMillis, long scanDurationMillis, int numOngoingScan, boolean isScreenOn, boolean isAppDead, int appImportance, String attributionTag)676 public void logAppScanStateChanged( 677 int[] uids, 678 String[] tags, 679 boolean enabled, 680 boolean isFilterScan, 681 boolean isCallbackScan, 682 int scanCallBackType, 683 int scanType, 684 int scanMode, 685 long reportDelayMillis, 686 long scanDurationMillis, 687 int numOngoingScan, 688 boolean isScreenOn, 689 boolean isAppDead, 690 int appImportance, 691 String attributionTag) { 692 BluetoothStatsLog.write( 693 BluetoothStatsLog.LE_APP_SCAN_STATE_CHANGED, 694 uids, 695 tags, 696 enabled, 697 isFilterScan, 698 isCallbackScan, 699 scanCallBackType, 700 scanType, 701 scanMode, 702 reportDelayMillis, 703 scanDurationMillis, 704 numOngoingScan, 705 isScreenOn, 706 isAppDead, 707 convertAppImportance(appImportance), 708 attributionTag); 709 } 710 711 /** Logs the radio scan stats with app attribution when the radio scan stopped. */ logRadioScanStopped( int[] uids, String[] tags, int scanType, int scanMode, long scanIntervalMillis, long scanWindowMillis, boolean isScreenOn, long scanDurationMillis, int appImportance, String attributionTag)712 public void logRadioScanStopped( 713 int[] uids, 714 String[] tags, 715 int scanType, 716 int scanMode, 717 long scanIntervalMillis, 718 long scanWindowMillis, 719 boolean isScreenOn, 720 long scanDurationMillis, 721 int appImportance, 722 String attributionTag) { 723 BluetoothStatsLog.write( 724 BluetoothStatsLog.LE_RADIO_SCAN_STOPPED, 725 uids, 726 tags, 727 scanType, 728 scanMode, 729 scanIntervalMillis, 730 scanWindowMillis, 731 isScreenOn, 732 scanDurationMillis, 733 convertAppImportance(appImportance), 734 attributionTag); 735 } 736 737 /** Logs the advertise stats with app attribution when the advertise state changed. */ logAdvStateChanged( int[] uids, String[] tags, boolean enabled, int interval, int txPowerLevel, boolean isConnectable, boolean isPeriodicAdvertisingEnabled, boolean hasScanResponse, boolean isExtendedAdv, int instanceCount, long advDurationMs, int appImportance, String attributionTag)738 public void logAdvStateChanged( 739 int[] uids, 740 String[] tags, 741 boolean enabled, 742 int interval, 743 int txPowerLevel, 744 boolean isConnectable, 745 boolean isPeriodicAdvertisingEnabled, 746 boolean hasScanResponse, 747 boolean isExtendedAdv, 748 int instanceCount, 749 long advDurationMs, 750 int appImportance, 751 String attributionTag) { 752 BluetoothStatsLog.write( 753 BluetoothStatsLog.LE_ADV_STATE_CHANGED, 754 uids, 755 tags, 756 enabled, 757 interval, 758 txPowerLevel, 759 isConnectable, 760 isPeriodicAdvertisingEnabled, 761 hasScanResponse, 762 isExtendedAdv, 763 instanceCount, 764 advDurationMs, 765 convertAppImportance(appImportance), 766 attributionTag); 767 } 768 getAllowlistedDeviceNameHash( String deviceName, boolean includeMedicalDevices)769 protected String getAllowlistedDeviceNameHash( 770 String deviceName, boolean includeMedicalDevices) { 771 List<String> wordBreakdownList = getWordBreakdownList(deviceName); 772 String matchedString = getMatchedString(wordBreakdownList, includeMedicalDevices); 773 return getSha256String(matchedString); 774 } 775 logAllowlistedDeviceNameHash(int metricId, String deviceName)776 protected String logAllowlistedDeviceNameHash(int metricId, String deviceName) { 777 List<String> wordBreakdownList = getWordBreakdownList(deviceName); 778 boolean includeMedicalDevices = false; 779 String matchedString = getMatchedString(wordBreakdownList, includeMedicalDevices); 780 if (!matchedString.isEmpty()) { 781 statslogBluetoothDeviceNames(metricId, matchedString); 782 } 783 return getSha256String(matchedString); 784 } 785 statslogBluetoothDeviceNames(int metricId, String matchedString)786 protected void statslogBluetoothDeviceNames(int metricId, String matchedString) { 787 String sha256 = getSha256String(matchedString); 788 Log.d(TAG, "Uploading sha256 hash of matched bluetooth device name: " + sha256); 789 BluetoothStatsLog.write( 790 BluetoothStatsLog.BLUETOOTH_HASHED_DEVICE_NAME_REPORTED, metricId, sha256); 791 } 792 logBluetoothEvent(BluetoothDevice device, int eventType, int state, int uid)793 public void logBluetoothEvent(BluetoothDevice device, int eventType, int state, int uid) { 794 795 if (!mInitialized || mAdapterService.getMetricId(device) == 0) { 796 return; 797 } 798 799 BluetoothStatsLog.write( 800 BluetoothStatsLog.BLUETOOTH_CROSS_LAYER_EVENT_REPORTED, 801 eventType, 802 state, 803 uid, 804 mAdapterService.getMetricId(device), 805 getRemoteDeviceInfoProto(device, false)); 806 } 807 getSha256String(String name)808 protected static String getSha256String(String name) { 809 if (name.isEmpty()) { 810 return ""; 811 } 812 StringBuilder hexString = new StringBuilder(); 813 byte[] hashBytes = getSha256(name); 814 for (byte b : hashBytes) { 815 hexString.append(Utils.formatSimple("%02x", b)); 816 } 817 return hexString.toString(); 818 } 819 getSha256(String name)820 protected static byte[] getSha256(String name) { 821 MessageDigest digest = null; 822 try { 823 digest = MessageDigest.getInstance("SHA-256"); 824 } catch (NoSuchAlgorithmException e) { 825 Log.w(TAG, "No SHA-256 in MessageDigest"); 826 return null; 827 } 828 return digest.digest(name.getBytes(StandardCharsets.UTF_8)); 829 } 830 getProfileEnumFromProfileId(int profile)831 private static int getProfileEnumFromProfileId(int profile) { 832 return switch (profile) { 833 case BluetoothProfile.A2DP -> 834 BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__EVENT_TYPE__PROFILE_CONNECTION_A2DP; 835 case BluetoothProfile.A2DP_SINK -> 836 BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__EVENT_TYPE__PROFILE_CONNECTION_A2DP_SINK; 837 case BluetoothProfile.HEADSET -> 838 BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__EVENT_TYPE__PROFILE_CONNECTION_HEADSET; 839 case BluetoothProfile.HEADSET_CLIENT -> 840 BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__EVENT_TYPE__PROFILE_CONNECTION_HEADSET_CLIENT; 841 case BluetoothProfile.MAP_CLIENT -> 842 BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__EVENT_TYPE__PROFILE_CONNECTION_MAP_CLIENT; 843 case BluetoothProfile.HID_HOST -> 844 BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__EVENT_TYPE__PROFILE_CONNECTION_HID_HOST; 845 case BluetoothProfile.PAN -> 846 BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__EVENT_TYPE__PROFILE_CONNECTION_PAN; 847 case BluetoothProfile.PBAP_CLIENT -> 848 BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__EVENT_TYPE__PROFILE_CONNECTION_PBAP_CLIENT; 849 case BluetoothProfile.HEARING_AID -> 850 BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__EVENT_TYPE__PROFILE_CONNECTION_HEARING_AID; 851 case BluetoothProfile.HAP_CLIENT -> 852 BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__EVENT_TYPE__PROFILE_CONNECTION_HAP_CLIENT; 853 case BluetoothProfile.VOLUME_CONTROL -> 854 BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__EVENT_TYPE__PROFILE_CONNECTION_VOLUME_CONTROL; 855 case BluetoothProfile.CSIP_SET_COORDINATOR -> 856 BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__EVENT_TYPE__PROFILE_CONNECTION_CSIP_SET_COORDINATOR; 857 case BluetoothProfile.LE_AUDIO -> 858 BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__EVENT_TYPE__PROFILE_CONNECTION_LE_AUDIO; 859 case BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT -> 860 BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__EVENT_TYPE__PROFILE_CONNECTION_LE_AUDIO_BROADCAST_ASSISTANT; 861 case BluetoothProfile.BATTERY -> 862 BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__EVENT_TYPE__PROFILE_CONNECTION_BATTERY; 863 default -> BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__EVENT_TYPE__PROFILE_CONNECTION; 864 }; 865 } 866 867 public void logProfileConnectionStateChange( 868 BluetoothDevice device, int profileId, int state, int prevState) { 869 870 switch (state) { 871 case BluetoothAdapter.STATE_CONNECTED: 872 logBluetoothEvent( 873 device, 874 getProfileEnumFromProfileId(profileId), 875 BluetoothStatsLog.BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__STATE__SUCCESS, 876 0); 877 break; 878 case BluetoothAdapter.STATE_DISCONNECTED: 879 if (prevState == BluetoothAdapter.STATE_CONNECTING) { 880 logBluetoothEvent( 881 device, 882 getProfileEnumFromProfileId(profileId), 883 BluetoothStatsLog.BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__STATE__FAIL, 884 0); 885 } 886 break; 887 } 888 } 889 890 /** Logs LE Audio Broadcast audio session. */ 891 public void logLeAudioBroadcastAudioSession( 892 int broadcastId, 893 int[] audioQuality, 894 int groupSize, 895 long sessionDurationMs, 896 long latencySessionConfiguredMs, 897 long latencySessionStreamingMs, 898 int sessionStatus) { 899 if (!mInitialized) { 900 return; 901 } 902 903 BluetoothStatsLog.write( 904 BluetoothStatsLog.BROADCAST_AUDIO_SESSION_REPORTED, 905 broadcastId, 906 audioQuality.length, 907 audioQuality, 908 groupSize, 909 sessionDurationMs, 910 latencySessionConfiguredMs, 911 latencySessionStreamingMs, 912 sessionStatus); 913 } 914 915 /** Logs Bond State Machine event */ 916 public void logBondStateMachineEvent(BluetoothDevice device, int bondState) { 917 switch (bondState) { 918 case BluetoothDevice.BOND_NONE: 919 logBluetoothEvent( 920 device, 921 BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__EVENT_TYPE__BOND, 922 BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__STATE__STATE_NONE, 923 0); 924 break; 925 case BluetoothDevice.BOND_BONDED: 926 logBluetoothEvent( 927 device, 928 BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__EVENT_TYPE__BOND, 929 BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__STATE__STATE_BONDED, 930 0); 931 break; 932 default: 933 } 934 } 935 936 /** Logs LE Audio Broadcast audio sync. */ 937 public void logLeAudioBroadcastAudioSync( 938 BluetoothDevice device, 939 int broadcastId, 940 boolean isLocalBroadcast, 941 long syncDurationMs, 942 long latencyPaSyncMs, 943 long latencyBisSyncMs, 944 int syncStatus) { 945 if (!mInitialized) { 946 return; 947 } 948 949 BluetoothStatsLog.write( 950 BluetoothStatsLog.BROADCAST_AUDIO_SYNC_REPORTED, 951 isLocalBroadcast ? broadcastId : BassConstants.INVALID_BROADCAST_ID, 952 isLocalBroadcast, 953 syncDurationMs, 954 latencyPaSyncMs, 955 latencyBisSyncMs, 956 syncStatus, 957 getRemoteDeviceInfoProto(device, false)); 958 } 959 960 void logHearingDeviceActiveEvent(BluetoothDevice device, int type, int timePeriod) { 961 BluetoothStatsLog.write( 962 BluetoothStatsLog.HEARING_DEVICE_ACTIVE_EVENT_REPORTED, 963 type, 964 timePeriod, 965 getRemoteDeviceInfoProto(device, true)); 966 } 967 968 void updateHearingDeviceActiveTime(BluetoothDevice device, int deviceTypeProto) { 969 // Time comparison includes a +/- 1 hour tolerance to prevent data loss 970 updateLastActiveTime( 971 device, 972 deviceTypeProto, 973 HEARING_DEVICE_ACTIVE_EVENT_REPORTED__TIME_PERIOD__DAY, 974 "last_active_day", 975 (now, lastActive) -> now.isAfter(lastActive.plusDays(1).minusHours(1))); 976 updateLastActiveTime( 977 device, 978 deviceTypeProto, 979 HEARING_DEVICE_ACTIVE_EVENT_REPORTED__TIME_PERIOD__WEEK, 980 "last_active_week", 981 (now, lastActive) -> now.isAfter(lastActive.plusWeeks(1).minusHours(1))); 982 updateLastActiveTime( 983 device, 984 deviceTypeProto, 985 HEARING_DEVICE_ACTIVE_EVENT_REPORTED__TIME_PERIOD__MONTH, 986 "last_active_month", 987 (now, lastActive) -> now.isAfter(lastActive.plusMonths(1).minusHours(1))); 988 } 989 990 private void updateLastActiveTime( 991 BluetoothDevice device, 992 int deviceTypeProto, 993 int timePeriodProto, 994 String timePeriodSettingsKey, 995 BiPredicate<LocalDateTime, LocalDateTime> timeComparison) { 996 final ContentResolver contentResolver = mAdapterService.getContentResolver(); 997 final String lastActive = Settings.Secure.getString(contentResolver, timePeriodSettingsKey); 998 final LocalDateTime now = LocalDateTime.now(ZoneId.systemDefault()); 999 if (lastActive == null || timeComparison.test(now, LocalDateTime.parse(lastActive))) { 1000 Settings.Secure.putString(contentResolver, timePeriodSettingsKey, now.toString()); 1001 logHearingDeviceActiveEvent(device, deviceTypeProto, timePeriodProto); 1002 } 1003 } 1004 1005 private boolean isMedicalDevice(BluetoothDevice device) { 1006 final String deviceName = mAdapterService.getRemoteName(device); 1007 final List<String> wordBreakdownList = getWordBreakdownList(deviceName); 1008 boolean isMedicalDevice = !getMatchedStringForMedicalDevice(wordBreakdownList).isEmpty(); 1009 return isMedicalDevice; 1010 } 1011 } 1012