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