1 /*
2  * Copyright 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 com.android.bluetooth.BtRestrictedStatsLog.RESTRICTED_BLUETOOTH_DEVICE_NAME_REPORTED;
19 
20 import android.app.AlarmManager;
21 import android.bluetooth.BluetoothDevice;
22 import android.content.Context;
23 import android.os.Build;
24 import android.os.SystemClock;
25 import android.util.Log;
26 import android.util.proto.ProtoOutputStream;
27 
28 import androidx.annotation.RequiresApi;
29 
30 import com.android.bluetooth.BluetoothMetricsProto.BluetoothLog;
31 import com.android.bluetooth.BluetoothMetricsProto.BluetoothRemoteDeviceInformation;
32 import com.android.bluetooth.BluetoothMetricsProto.ProfileConnectionStats;
33 import com.android.bluetooth.BluetoothMetricsProto.ProfileId;
34 import com.android.bluetooth.BluetoothStatsLog;
35 import com.android.bluetooth.BtRestrictedStatsLog;
36 import com.android.bluetooth.Utils;
37 import com.android.modules.utils.build.SdkLevel;
38 
39 import com.google.common.annotations.VisibleForTesting;
40 import com.google.common.base.Ascii;
41 import com.google.common.hash.BloomFilter;
42 import com.google.common.hash.Funnels;
43 
44 import java.io.ByteArrayInputStream;
45 import java.io.File;
46 import java.io.FileInputStream;
47 import java.io.IOException;
48 import java.nio.charset.StandardCharsets;
49 import java.security.MessageDigest;
50 import java.security.NoSuchAlgorithmException;
51 import java.util.ArrayList;
52 import java.util.Collections;
53 import java.util.HashMap;
54 import java.util.List;
55 
56 /** Class of Bluetooth Metrics */
57 public class MetricsLogger {
58     private static final String TAG = "BluetoothMetricsLogger";
59     private static final String BLOOMFILTER_PATH = "/data/misc/bluetooth";
60     private static final String BLOOMFILTER_FILE = "/devices_for_metrics_v3";
61     public static final String BLOOMFILTER_FULL_PATH = BLOOMFILTER_PATH + BLOOMFILTER_FILE;
62 
63     // 6 hours timeout for counter metrics
64     private static final long BLUETOOTH_COUNTER_METRICS_ACTION_DURATION_MILLIS = 6L * 3600L * 1000L;
65     private static final int MAX_WORDS_ALLOWED_IN_DEVICE_NAME = 7;
66 
67     private static final HashMap<ProfileId, Integer> sProfileConnectionCounts = new HashMap<>();
68 
69     HashMap<Integer, Long> mCounters = new HashMap<>();
70     private static volatile MetricsLogger sInstance = null;
71     private Context mContext = null;
72     private AlarmManager mAlarmManager = null;
73     private boolean mInitialized = false;
74     private static final Object sLock = new Object();
75     private BloomFilter<byte[]> mBloomFilter = null;
76     protected boolean mBloomFilterInitialized = false;
77 
78     private AlarmManager.OnAlarmListener mOnAlarmListener =
79             new AlarmManager.OnAlarmListener() {
80                 @Override
81                 public void onAlarm() {
82                     drainBufferedCounters();
83                     scheduleDrains();
84                 }
85             };
86 
getInstance()87     public static MetricsLogger getInstance() {
88         if (sInstance == null) {
89             synchronized (sLock) {
90                 if (sInstance == null) {
91                     sInstance = new MetricsLogger();
92                 }
93             }
94         }
95         return sInstance;
96     }
97 
98     /**
99      * Allow unit tests to substitute MetricsLogger with a test instance
100      *
101      * @param instance a test instance of the MetricsLogger
102      */
103     @VisibleForTesting
setInstanceForTesting(MetricsLogger instance)104     public static void setInstanceForTesting(MetricsLogger instance) {
105         Utils.enforceInstrumentationTestMode();
106         synchronized (sLock) {
107             Log.d(TAG, "setInstanceForTesting(), set to " + instance);
108             sInstance = instance;
109         }
110     }
111 
isInitialized()112     public boolean isInitialized() {
113         return mInitialized;
114     }
115 
initBloomFilter(String path)116     public boolean initBloomFilter(String path) {
117         try {
118             File file = new File(path);
119             if (!file.exists()) {
120                 Log.w(TAG, "MetricsLogger is creating a new Bloomfilter file");
121                 DeviceBloomfilterGenerator.generateDefaultBloomfilter(path);
122             }
123 
124             FileInputStream in = new FileInputStream(new File(path));
125             mBloomFilter = BloomFilter.readFrom(in, Funnels.byteArrayFunnel());
126             mBloomFilterInitialized = true;
127         } catch (IOException e1) {
128             Log.w(TAG, "MetricsLogger can't read the BloomFilter file.");
129             byte[] bloomfilterData =
130                     DeviceBloomfilterGenerator.hexStringToByteArray(
131                             DeviceBloomfilterGenerator.BLOOM_FILTER_DEFAULT);
132             try {
133                 mBloomFilter =
134                         BloomFilter.readFrom(
135                                 new ByteArrayInputStream(bloomfilterData),
136                                 Funnels.byteArrayFunnel());
137                 mBloomFilterInitialized = true;
138                 Log.i(TAG, "The default bloomfilter is used");
139                 return true;
140             } catch (IOException e2) {
141                 Log.w(TAG, "The default bloomfilter can't be used.");
142             }
143             return false;
144         }
145         return true;
146     }
147 
setBloomfilter(BloomFilter bloomfilter)148     protected void setBloomfilter(BloomFilter bloomfilter) {
149         mBloomFilter = bloomfilter;
150     }
151 
init(Context context)152     public boolean init(Context context) {
153         if (mInitialized) {
154             return false;
155         }
156         mInitialized = true;
157         mContext = context;
158         scheduleDrains();
159         if (!initBloomFilter(BLOOMFILTER_FULL_PATH)) {
160             Log.w(TAG, "MetricsLogger can't initialize the bloomfilter");
161             // The class is for multiple metrics tasks.
162             // We still want to use this class even if the bloomfilter isn't initialized
163             // so still return true here.
164         }
165         return true;
166     }
167 
cacheCount(int key, long count)168     public boolean cacheCount(int key, long count) {
169         if (!mInitialized) {
170             Log.w(TAG, "MetricsLogger isn't initialized");
171             return false;
172         }
173         if (count <= 0) {
174             Log.w(TAG, "count is not larger than 0. count: " + count + " key: " + key);
175             return false;
176         }
177         long total = 0;
178 
179         synchronized (sLock) {
180             if (mCounters.containsKey(key)) {
181                 total = mCounters.get(key);
182             }
183             if (Long.MAX_VALUE - total < count) {
184                 Log.w(TAG, "count overflows. count: " + count + " current total: " + total);
185                 mCounters.put(key, Long.MAX_VALUE);
186                 return false;
187             }
188             mCounters.put(key, total + count);
189         }
190         return true;
191     }
192 
193     /**
194      * Log profile connection event by incrementing an internal counter for that profile. This log
195      * persists over adapter enable/disable and only get cleared when metrics are dumped or when
196      * Bluetooth process is killed.
197      *
198      * @param profileId Bluetooth profile that is connected at this event
199      */
logProfileConnectionEvent(ProfileId profileId)200     public static void logProfileConnectionEvent(ProfileId profileId) {
201         synchronized (sProfileConnectionCounts) {
202             sProfileConnectionCounts.merge(profileId, 1, Integer::sum);
203         }
204     }
205 
206     /**
207      * Dump collected metrics into proto using a builder. Clean up internal data after the dump.
208      *
209      * @param metricsBuilder proto builder for {@link BluetoothLog}
210      */
dumpProto(BluetoothLog.Builder metricsBuilder)211     public static void dumpProto(BluetoothLog.Builder metricsBuilder) {
212         synchronized (sProfileConnectionCounts) {
213             sProfileConnectionCounts.forEach(
214                     (key, value) ->
215                             metricsBuilder.addProfileConnectionStats(
216                                     ProfileConnectionStats.newBuilder()
217                                             .setProfileId(key)
218                                             .setNumTimesConnected(value)
219                                             .build()));
220             sProfileConnectionCounts.clear();
221         }
222     }
223 
scheduleDrains()224     protected void scheduleDrains() {
225         Log.i(TAG, "setCounterMetricsAlarm()");
226         if (mAlarmManager == null) {
227             mAlarmManager = mContext.getSystemService(AlarmManager.class);
228         }
229         mAlarmManager.set(
230                 AlarmManager.ELAPSED_REALTIME_WAKEUP,
231                 SystemClock.elapsedRealtime() + BLUETOOTH_COUNTER_METRICS_ACTION_DURATION_MILLIS,
232                 TAG,
233                 mOnAlarmListener,
234                 null);
235     }
236 
count(int key, long count)237     public boolean count(int key, long count) {
238         if (!mInitialized) {
239             Log.w(TAG, "MetricsLogger isn't initialized");
240             return false;
241         }
242         if (count <= 0) {
243             Log.w(TAG, "count is not larger than 0. count: " + count + " key: " + key);
244             return false;
245         }
246         BluetoothStatsLog.write(BluetoothStatsLog.BLUETOOTH_CODE_PATH_COUNTER, key, count);
247         return true;
248     }
249 
drainBufferedCounters()250     protected void drainBufferedCounters() {
251         Log.i(TAG, "drainBufferedCounters().");
252         synchronized (sLock) {
253             // send mCounters to statsd
254             for (int key : mCounters.keySet()) {
255                 count(key, mCounters.get(key));
256             }
257             mCounters.clear();
258         }
259     }
260 
close()261     public boolean close() {
262         if (!mInitialized) {
263             return false;
264         }
265         Log.d(TAG, "close()");
266         cancelPendingDrain();
267         drainBufferedCounters();
268         mAlarmManager = null;
269         mContext = null;
270         mInitialized = false;
271         mBloomFilterInitialized = false;
272         return true;
273     }
274 
cancelPendingDrain()275     protected void cancelPendingDrain() {
276         mAlarmManager.cancel(mOnAlarmListener);
277     }
278 
writeFieldIfNotNull( ProtoOutputStream proto, long fieldType, long fieldCount, long fieldNumber, Object value)279     private void writeFieldIfNotNull(
280             ProtoOutputStream proto,
281             long fieldType,
282             long fieldCount,
283             long fieldNumber,
284             Object value) {
285         if (value != null) {
286             try {
287                 if (fieldType == ProtoOutputStream.FIELD_TYPE_STRING) {
288                     proto.write(fieldType | fieldCount | fieldNumber, value.toString());
289                 }
290 
291                 if (fieldType == ProtoOutputStream.FIELD_TYPE_INT32) {
292                     proto.write(fieldType | fieldCount | fieldNumber, (Integer) value);
293                 }
294             } catch (Exception e) {
295                 Log.e(TAG, "Error writing field " + fieldNumber + ": " + e.getMessage());
296             }
297         }
298     }
299 
300     /**
301      * Retrieves a byte array containing serialized remote device information for the specified
302      * BluetoothDevice. This data can be used for remote device identification and logging.
303      *
304      * @param device The BluetoothDevice for which to retrieve device information.
305      * @return A byte array containing the serialized remote device information.
306      */
getRemoteDeviceInfoProto(BluetoothDevice device)307     public byte[] getRemoteDeviceInfoProto(BluetoothDevice device) {
308         ProtoOutputStream proto = new ProtoOutputStream();
309 
310         // write Allowlisted Device Name Hash
311         writeFieldIfNotNull(
312                 proto,
313                 ProtoOutputStream.FIELD_TYPE_STRING,
314                 ProtoOutputStream.FIELD_COUNT_SINGLE,
315                 BluetoothRemoteDeviceInformation.ALLOWLISTED_DEVICE_NAME_HASH_FIELD_NUMBER,
316                 getAllowlistedDeviceNameHash(device.getName()));
317 
318         // write COD
319         writeFieldIfNotNull(
320                 proto,
321                 ProtoOutputStream.FIELD_TYPE_INT32,
322                 ProtoOutputStream.FIELD_COUNT_SINGLE,
323                 BluetoothRemoteDeviceInformation.CLASS_OF_DEVICE_FIELD_NUMBER,
324                 device.getBluetoothClass() != null
325                         ? device.getBluetoothClass().getClassOfDevice()
326                         : null);
327 
328         // write OUI
329         writeFieldIfNotNull(
330                 proto,
331                 ProtoOutputStream.FIELD_TYPE_INT32,
332                 ProtoOutputStream.FIELD_COUNT_SINGLE,
333                 BluetoothRemoteDeviceInformation.OUI_FIELD_NUMBER,
334                 getOui(device));
335 
336         return proto.getBytes();
337     }
338 
getOui(BluetoothDevice device)339     private int getOui(BluetoothDevice device) {
340         return Integer.parseInt(device.getAddress().replace(":", "").substring(0, 6), 16);
341     }
342 
getWordBreakdownList(String deviceName)343     private List<String> getWordBreakdownList(String deviceName) {
344         if (deviceName == null) {
345             return Collections.emptyList();
346         }
347         // remove more than one spaces in a row
348         deviceName = deviceName.trim().replaceAll(" +", " ");
349         // remove non alphanumeric characters and spaces, and transform to lower cases.
350         String[] words = Ascii.toLowerCase(deviceName.replaceAll("[^a-zA-Z0-9 ]", "")).split(" ");
351 
352         if (words.length > MAX_WORDS_ALLOWED_IN_DEVICE_NAME) {
353             // Validity checking here to avoid excessively long sequences
354             return Collections.emptyList();
355         }
356         // collect the word breakdown in an arraylist
357         ArrayList<String> wordBreakdownList = new ArrayList<String>();
358         for (int start = 0; start < words.length; start++) {
359 
360             StringBuilder deviceNameCombination = new StringBuilder();
361             for (int end = start; end < words.length; end++) {
362                 deviceNameCombination.append(words[end]);
363                 wordBreakdownList.add(deviceNameCombination.toString());
364             }
365         }
366 
367         return wordBreakdownList;
368     }
369 
370     @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
uploadRestrictedBluetothDeviceName(List<String> wordBreakdownList)371     private void uploadRestrictedBluetothDeviceName(List<String> wordBreakdownList) {
372         for (String word : wordBreakdownList) {
373             BtRestrictedStatsLog.write(RESTRICTED_BLUETOOTH_DEVICE_NAME_REPORTED, word);
374         }
375     }
376 
getMatchedString(List<String> wordBreakdownList)377     private String getMatchedString(List<String> wordBreakdownList) {
378         if (!mBloomFilterInitialized || wordBreakdownList.isEmpty()) {
379             return "";
380         }
381 
382         String matchedString = "";
383         for (String word : wordBreakdownList) {
384             byte[] sha256 = getSha256(word);
385             if (mBloomFilter.mightContain(sha256) && word.length() > matchedString.length()) {
386                 matchedString = word;
387             }
388         }
389         return matchedString;
390     }
391 
getAllowlistedDeviceNameHash(String deviceName)392     protected String getAllowlistedDeviceNameHash(String deviceName) {
393         List<String> wordBreakdownList = getWordBreakdownList(deviceName);
394         String matchedString = getMatchedString(wordBreakdownList);
395         return getSha256String(matchedString);
396     }
397 
logAllowlistedDeviceNameHash( int metricId, String deviceName, boolean logRestrictedNames)398     protected String logAllowlistedDeviceNameHash(
399             int metricId, String deviceName, boolean logRestrictedNames) {
400         List<String> wordBreakdownList = getWordBreakdownList(deviceName);
401         String matchedString = getMatchedString(wordBreakdownList);
402         if (logRestrictedNames) {
403             // Log the restricted bluetooth device name
404             if (SdkLevel.isAtLeastU()) {
405                 uploadRestrictedBluetothDeviceName(wordBreakdownList);
406             }
407         }
408         if (!matchedString.isEmpty()) {
409             statslogBluetoothDeviceNames(metricId, matchedString);
410         }
411         return getSha256String(matchedString);
412     }
413 
statslogBluetoothDeviceNames(int metricId, String matchedString)414     protected void statslogBluetoothDeviceNames(int metricId, String matchedString) {
415         String sha256 = getSha256String(matchedString);
416         Log.d(TAG, "Uploading sha256 hash of matched bluetooth device name: " + sha256);
417         BluetoothStatsLog.write(
418                 BluetoothStatsLog.BLUETOOTH_HASHED_DEVICE_NAME_REPORTED, metricId, sha256);
419     }
420 
getSha256String(String name)421     protected static String getSha256String(String name) {
422         if (name.isEmpty()) {
423             return "";
424         }
425         StringBuilder hexString = new StringBuilder();
426         byte[] hashBytes = getSha256(name);
427         for (byte b : hashBytes) {
428             hexString.append(String.format("%02x", b));
429         }
430         return hexString.toString();
431     }
432 
getSha256(String name)433     protected static byte[] getSha256(String name) {
434         MessageDigest digest = null;
435         try {
436             digest = MessageDigest.getInstance("SHA-256");
437         } catch (NoSuchAlgorithmException e) {
438             Log.w(TAG, "No SHA-256 in MessageDigest");
439             return null;
440         }
441         return digest.digest(name.getBytes(StandardCharsets.UTF_8));
442     }
443 }
444