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