• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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