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