• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.content.Context;
22 import android.os.SystemClock;
23 import android.util.Log;
24 
25 import com.android.bluetooth.BluetoothMetricsProto.BluetoothLog;
26 import com.android.bluetooth.BluetoothMetricsProto.ProfileConnectionStats;
27 import com.android.bluetooth.BluetoothMetricsProto.ProfileId;
28 import com.android.bluetooth.BluetoothStatsLog;
29 import com.android.bluetooth.BtRestrictedStatsLog;
30 import com.android.bluetooth.Utils;
31 import com.android.internal.annotations.VisibleForTesting;
32 import com.android.modules.utils.build.SdkLevel;
33 
34 import com.google.common.hash.BloomFilter;
35 import com.google.common.hash.Funnels;
36 import com.google.common.hash.Hashing;
37 
38 import java.io.ByteArrayInputStream;
39 import java.io.File;
40 import java.io.FileInputStream;
41 import java.io.IOException;
42 import java.nio.charset.StandardCharsets;
43 import java.security.MessageDigest;
44 import java.security.NoSuchAlgorithmException;
45 import java.util.HashMap;
46 
47 /**
48  * Class of Bluetooth Metrics
49  */
50 public class MetricsLogger {
51     private static final String TAG = "BluetoothMetricsLogger";
52     private static final String BLOOMFILTER_PATH = "/data/misc/bluetooth";
53     private static final String BLOOMFILTER_FILE = "/devices_for_metrics";
54     public static final String BLOOMFILTER_FULL_PATH = BLOOMFILTER_PATH + BLOOMFILTER_FILE;
55 
56     public static final boolean DEBUG = false;
57 
58     // 6 hours timeout for counter metrics
59     private static final long BLUETOOTH_COUNTER_METRICS_ACTION_DURATION_MILLIS = 6L * 3600L * 1000L;
60     private static final int MAX_WORDS_ALLOWED_IN_DEVICE_NAME = 7;
61 
62     private static final HashMap<ProfileId, Integer> sProfileConnectionCounts = new HashMap<>();
63 
64     HashMap<Integer, Long> mCounters = new HashMap<>();
65     private static volatile MetricsLogger sInstance = null;
66     private Context mContext = null;
67     private AlarmManager mAlarmManager = null;
68     private boolean mInitialized = false;
69     static final private Object mLock = new Object();
70     private BloomFilter<byte[]> mBloomFilter = null;
71     protected boolean mBloomFilterInitialized = false;
72 
73     private AlarmManager.OnAlarmListener mOnAlarmListener = new AlarmManager.OnAlarmListener () {
74         @Override
75         public void onAlarm() {
76             drainBufferedCounters();
77             scheduleDrains();
78         }
79     };
80 
getInstance()81     public static MetricsLogger getInstance() {
82         if (sInstance == null) {
83             synchronized (mLock) {
84                 if (sInstance == null) {
85                     sInstance = new MetricsLogger();
86                 }
87             }
88         }
89         return sInstance;
90     }
91 
92     /**
93      * Allow unit tests to substitute MetricsLogger with a test instance
94      *
95      * @param instance a test instance of the MetricsLogger
96      */
97     @VisibleForTesting
setInstanceForTesting(MetricsLogger instance)98     public static void setInstanceForTesting(MetricsLogger instance) {
99         Utils.enforceInstrumentationTestMode();
100         synchronized (mLock) {
101             Log.d(TAG, "setInstanceForTesting(), set to " + instance);
102             sInstance = instance;
103         }
104     }
105 
isInitialized()106     public boolean isInitialized() {
107         return mInitialized;
108     }
109 
initBloomFilter(String path)110     public boolean initBloomFilter(String path) {
111         try {
112             File file = new File(path);
113             if (!file.exists()) {
114                 Log.w(TAG, "MetricsLogger is creating a new Bloomfilter file");
115                 DeviceBloomfilterGenerator.generateDefaultBloomfilter(path);
116             }
117 
118             FileInputStream in = new FileInputStream(new File(path));
119             mBloomFilter = BloomFilter.readFrom(in, Funnels.byteArrayFunnel());
120             mBloomFilterInitialized = true;
121         } catch (IOException e1) {
122             Log.w(TAG, "MetricsLogger can't read the BloomFilter file.");
123             byte[] bloomfilterData = DeviceBloomfilterGenerator.hexStringToByteArray(
124                     DeviceBloomfilterGenerator.BLOOM_FILTER_DEFAULT);
125             try {
126                 mBloomFilter = BloomFilter.readFrom(
127                         new ByteArrayInputStream(bloomfilterData), Funnels.byteArrayFunnel());
128                 mBloomFilterInitialized = true;
129                 Log.i(TAG, "The default bloomfilter is used");
130                 return true;
131             } catch (IOException e2) {
132                 Log.w(TAG, "The default bloomfilter can't be used.");
133             }
134             return false;
135         }
136         return true;
137     }
138 
setBloomfilter(BloomFilter bloomfilter)139     protected void setBloomfilter(BloomFilter bloomfilter) {
140         mBloomFilter = bloomfilter;
141     }
142 
init(Context context)143     public boolean init(Context context) {
144         if (mInitialized) {
145             return false;
146         }
147         mInitialized = true;
148         mContext = context;
149         scheduleDrains();
150         if (!initBloomFilter(BLOOMFILTER_FULL_PATH)) {
151             Log.w(TAG, "MetricsLogger can't initialize the bloomfilter");
152             // The class is for multiple metrics tasks.
153             // We still want to use this class even if the bloomfilter isn't initialized
154             // so still return true here.
155         }
156         return true;
157     }
158 
cacheCount(int key, long count)159     public boolean cacheCount(int key, long count) {
160         if (!mInitialized) {
161             Log.w(TAG, "MetricsLogger isn't initialized");
162             return false;
163         }
164         if (count <= 0) {
165             Log.w(TAG, "count is not larger than 0. count: " + count + " key: " + key);
166             return false;
167         }
168         long total = 0;
169 
170         synchronized (mLock) {
171             if (mCounters.containsKey(key)) {
172                 total = mCounters.get(key);
173             }
174             if (Long.MAX_VALUE - total < count) {
175                 Log.w(TAG, "count overflows. count: " + count + " current total: " + total);
176                 mCounters.put(key, Long.MAX_VALUE);
177                 return false;
178             }
179             mCounters.put(key, total + count);
180         }
181         return true;
182     }
183 
184     /**
185      * Log profile connection event by incrementing an internal counter for that profile.
186      * This log persists over adapter enable/disable and only get cleared when metrics are
187      * dumped or when Bluetooth process is killed.
188      *
189      * @param profileId Bluetooth profile that is connected at this event
190      */
logProfileConnectionEvent(ProfileId profileId)191     public static void logProfileConnectionEvent(ProfileId profileId) {
192         synchronized (sProfileConnectionCounts) {
193             sProfileConnectionCounts.merge(profileId, 1, Integer::sum);
194         }
195     }
196 
197     /**
198      * Dump collected metrics into proto using a builder.
199      * Clean up internal data after the dump.
200      *
201      * @param metricsBuilder proto builder for {@link BluetoothLog}
202      */
dumpProto(BluetoothLog.Builder metricsBuilder)203     public static void dumpProto(BluetoothLog.Builder metricsBuilder) {
204         synchronized (sProfileConnectionCounts) {
205             sProfileConnectionCounts.forEach(
206                     (key, value) -> metricsBuilder.addProfileConnectionStats(
207                             ProfileConnectionStats.newBuilder()
208                                     .setProfileId(key)
209                                     .setNumTimesConnected(value)
210                                     .build()));
211             sProfileConnectionCounts.clear();
212         }
213     }
214 
scheduleDrains()215     protected void scheduleDrains() {
216         Log.i(TAG, "setCounterMetricsAlarm()");
217         if (mAlarmManager == null) {
218             mAlarmManager = mContext.getSystemService(AlarmManager.class);
219         }
220         mAlarmManager.set(
221                 AlarmManager.ELAPSED_REALTIME_WAKEUP,
222                 SystemClock.elapsedRealtime() + BLUETOOTH_COUNTER_METRICS_ACTION_DURATION_MILLIS,
223                 TAG,
224                 mOnAlarmListener,
225                 null);
226     }
227 
count(int key, long count)228     public boolean count(int key, long count) {
229         if (!mInitialized) {
230             Log.w(TAG, "MetricsLogger isn't initialized");
231             return false;
232         }
233         if (count <= 0) {
234             Log.w(TAG, "count is not larger than 0. count: " + count + " key: " + key);
235             return false;
236         }
237         BluetoothStatsLog.write(
238                 BluetoothStatsLog.BLUETOOTH_CODE_PATH_COUNTER, key, count);
239         return true;
240     }
241 
drainBufferedCounters()242     protected void drainBufferedCounters() {
243         Log.i(TAG, "drainBufferedCounters().");
244         synchronized (mLock) {
245             // send mCounters to statsd
246             for (int key : mCounters.keySet()) {
247                 count(key, mCounters.get(key));
248             }
249             mCounters.clear();
250         }
251     }
252 
close()253     public boolean close() {
254         if (!mInitialized) {
255             return false;
256         }
257         if (DEBUG) {
258             Log.d(TAG, "close()");
259         }
260         cancelPendingDrain();
261         drainBufferedCounters();
262         mAlarmManager = null;
263         mContext = null;
264         mInitialized = false;
265         mBloomFilterInitialized = false;
266         return true;
267     }
cancelPendingDrain()268     protected void cancelPendingDrain() {
269         mAlarmManager.cancel(mOnAlarmListener);
270     }
271 
logSanitizedBluetoothDeviceName(int metricId, String deviceName)272     protected boolean logSanitizedBluetoothDeviceName(int metricId, String deviceName) {
273         if (!mBloomFilterInitialized || deviceName == null) {
274             return false;
275         }
276 
277         // remove more than one spaces in a row
278         deviceName = deviceName.trim().replaceAll(" +", " ");
279         // remove non alphanumeric characters and spaces, and transform to lower cases.
280         String[] words = deviceName.replaceAll(
281                 "[^a-zA-Z0-9 ]", "").toLowerCase().split(" ");
282 
283         if (words.length > MAX_WORDS_ALLOWED_IN_DEVICE_NAME) {
284             // Validity checking here to avoid excessively long sequences
285             return false;
286         }
287         // find the longest matched substring
288         String matchedString = "";
289         byte[] matchedSha256 = null;
290         for (int start = 0; start < words.length; start++) {
291 
292             String toBeMatched = "";
293             for (int end = start; end < words.length; end++) {
294                 toBeMatched += words[end];
295                 // TODO(b/280868296): Refactor to log even if bloom filter isn't initialized.
296                 if (SdkLevel.isAtLeastU()) {
297                     BtRestrictedStatsLog.write(RESTRICTED_BLUETOOTH_DEVICE_NAME_REPORTED,
298                             toBeMatched);
299                 }
300                 byte[] sha256 = getSha256(toBeMatched);
301                 if (sha256 == null) {
302                     continue;
303                 }
304 
305                 if (mBloomFilter.mightContain(sha256)
306                         && toBeMatched.length() > matchedString.length()) {
307                     matchedString = toBeMatched;
308                     matchedSha256 = sha256;
309                 }
310             }
311         }
312 
313         // upload the sha256 of the longest matched string.
314         if (matchedSha256 == null) {
315             return false;
316         }
317         statslogBluetoothDeviceNames(
318                 metricId,
319                 matchedString,
320                 Hashing.sha256().hashString(matchedString, StandardCharsets.UTF_8).toString());
321         return true;
322     }
323 
statslogBluetoothDeviceNames(int metricId, String matchedString, String sha256)324     protected void statslogBluetoothDeviceNames(int metricId, String matchedString, String sha256) {
325         Log.d(TAG,
326                 "Uploading sha256 hash of matched bluetooth device name: " + sha256);
327         BluetoothStatsLog.write(
328                 BluetoothStatsLog.BLUETOOTH_HASHED_DEVICE_NAME_REPORTED, metricId, sha256);
329     }
330 
getSha256(String name)331     protected static byte[] getSha256(String name) {
332         MessageDigest digest = null;
333         try {
334             digest = MessageDigest.getInstance("SHA-256");
335         } catch (NoSuchAlgorithmException e) {
336             Log.w(TAG, "No SHA-256 in MessageDigest");
337             return null;
338         }
339         return digest.digest(name.getBytes(StandardCharsets.UTF_8));
340     }
341 }
342