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