1 /* 2 * Copyright (C) 2016 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.gatt; 17 18 import android.bluetooth.le.ScanSettings; 19 import android.os.Binder; 20 import android.os.RemoteException; 21 import android.os.ServiceManager; 22 import android.os.SystemClock; 23 import android.os.WorkSource; 24 import android.util.StatsLog; 25 26 import com.android.bluetooth.BluetoothMetricsProto; 27 import com.android.internal.app.IBatteryStats; 28 29 import java.text.DateFormat; 30 import java.text.SimpleDateFormat; 31 import java.util.ArrayList; 32 import java.util.Date; 33 import java.util.HashMap; 34 import java.util.Iterator; 35 import java.util.List; 36 37 /** 38 * ScanStats class helps keep track of information about scans 39 * on a per application basis. 40 * @hide 41 */ 42 /*package*/ class AppScanStats { 43 private static final String TAG = AppScanStats.class.getSimpleName(); 44 45 static final DateFormat DATE_FORMAT = new SimpleDateFormat("MM-dd HH:mm:ss"); 46 47 /* ContextMap here is needed to grab Apps and Connections */ ContextMap mContextMap; 48 49 /* GattService is needed to add scan event protos to be dumped later */ GattService 50 mGattService; 51 52 /* Battery stats is used to keep track of scans and result stats */ IBatteryStats mBatteryStats; 53 54 class LastScan { 55 public long duration; 56 public long suspendDuration; 57 public long suspendStartTime; 58 public boolean isSuspended; 59 public long timestamp; 60 public boolean opportunistic; 61 public boolean timeout; 62 public boolean background; 63 public boolean filtered; 64 public int results; 65 public int scannerId; 66 LastScan(long timestamp, long duration, boolean opportunistic, boolean background, boolean filtered, int scannerId)67 LastScan(long timestamp, long duration, boolean opportunistic, boolean background, 68 boolean filtered, int scannerId) { 69 this.duration = duration; 70 this.timestamp = timestamp; 71 this.opportunistic = opportunistic; 72 this.background = background; 73 this.filtered = filtered; 74 this.results = 0; 75 this.scannerId = scannerId; 76 this.suspendDuration = 0; 77 this.suspendStartTime = 0; 78 this.isSuspended = false; 79 } 80 } 81 82 static final int NUM_SCAN_DURATIONS_KEPT = 5; 83 84 // This constant defines the time window an app can scan multiple times. 85 // Any single app can scan up to |NUM_SCAN_DURATIONS_KEPT| times during 86 // this window. Once they reach this limit, they must wait until their 87 // earliest recorded scan exits this window. 88 static final long EXCESSIVE_SCANNING_PERIOD_MS = 30 * 1000; 89 90 // Maximum msec before scan gets downgraded to opportunistic 91 static final int SCAN_TIMEOUT_MS = 30 * 60 * 1000; 92 93 public String appName; 94 public WorkSource mWorkSource; // Used for BatteryStats and StatsLog 95 private int mScansStarted = 0; 96 private int mScansStopped = 0; 97 public boolean isRegistered = false; 98 private long mMinScanTime = Long.MAX_VALUE; 99 private long mMaxScanTime = 0; 100 private long mScanStartTime = 0; 101 private long mTotalScanTime = 0; 102 private long mTotalSuspendTime = 0; 103 private List<LastScan> mLastScans = new ArrayList<LastScan>(NUM_SCAN_DURATIONS_KEPT); 104 private HashMap<Integer, LastScan> mOngoingScans = new HashMap<Integer, LastScan>(); 105 public long startTime = 0; 106 public long stopTime = 0; 107 public int results = 0; 108 AppScanStats(String name, WorkSource source, ContextMap map, GattService service)109 AppScanStats(String name, WorkSource source, ContextMap map, GattService service) { 110 appName = name; 111 mContextMap = map; 112 mGattService = service; 113 mBatteryStats = IBatteryStats.Stub.asInterface(ServiceManager.getService("batterystats")); 114 115 if (source == null) { 116 // Bill the caller if the work source isn't passed through 117 source = new WorkSource(Binder.getCallingUid(), appName); 118 } 119 mWorkSource = source; 120 } 121 addResult(int scannerId)122 synchronized void addResult(int scannerId) { 123 LastScan scan = getScanFromScannerId(scannerId); 124 if (scan != null) { 125 int batteryStatsResults = ++scan.results; 126 127 // Only update battery stats after receiving 100 new results in order 128 // to lower the cost of the binder transaction 129 if (batteryStatsResults % 100 == 0) { 130 try { 131 mBatteryStats.noteBleScanResults(mWorkSource, 100); 132 } catch (RemoteException e) { 133 /* ignore */ 134 } 135 } 136 } 137 138 results++; 139 } 140 isScanning()141 boolean isScanning() { 142 return !mOngoingScans.isEmpty(); 143 } 144 getScanFromScannerId(int scannerId)145 LastScan getScanFromScannerId(int scannerId) { 146 return mOngoingScans.get(scannerId); 147 } 148 recordScanStart(ScanSettings settings, boolean filtered, int scannerId)149 synchronized void recordScanStart(ScanSettings settings, boolean filtered, int scannerId) { 150 LastScan existingScan = getScanFromScannerId(scannerId); 151 if (existingScan != null) { 152 return; 153 } 154 this.mScansStarted++; 155 startTime = SystemClock.elapsedRealtime(); 156 157 LastScan scan = new LastScan(startTime, 0, false, false, filtered, scannerId); 158 if (settings != null) { 159 scan.opportunistic = settings.getScanMode() == ScanSettings.SCAN_MODE_OPPORTUNISTIC; 160 scan.background = 161 (settings.getCallbackType() & ScanSettings.CALLBACK_TYPE_FIRST_MATCH) != 0; 162 } 163 164 BluetoothMetricsProto.ScanEvent scanEvent = BluetoothMetricsProto.ScanEvent.newBuilder() 165 .setScanEventType(BluetoothMetricsProto.ScanEvent.ScanEventType.SCAN_EVENT_START) 166 .setScanTechnologyType( 167 BluetoothMetricsProto.ScanEvent.ScanTechnologyType.SCAN_TECH_TYPE_LE) 168 .setEventTimeMillis(System.currentTimeMillis()) 169 .setInitiator(truncateAppName(appName)).build(); 170 mGattService.addScanEvent(scanEvent); 171 172 if (!isScanning()) { 173 mScanStartTime = startTime; 174 } 175 try { 176 boolean isUnoptimized = !(scan.filtered || scan.background || scan.opportunistic); 177 mBatteryStats.noteBleScanStarted(mWorkSource, isUnoptimized); 178 } catch (RemoteException e) { 179 /* ignore */ 180 } 181 writeToStatsLog(scan, StatsLog.BLE_SCAN_STATE_CHANGED__STATE__ON); 182 183 mOngoingScans.put(scannerId, scan); 184 } 185 recordScanStop(int scannerId)186 synchronized void recordScanStop(int scannerId) { 187 LastScan scan = getScanFromScannerId(scannerId); 188 if (scan == null) { 189 return; 190 } 191 this.mScansStopped++; 192 stopTime = SystemClock.elapsedRealtime(); 193 long scanDuration = stopTime - scan.timestamp; 194 scan.duration = scanDuration; 195 if (scan.isSuspended) { 196 long suspendDuration = stopTime - scan.suspendStartTime; 197 scan.suspendDuration += suspendDuration; 198 mTotalSuspendTime += suspendDuration; 199 } 200 mOngoingScans.remove(scannerId); 201 if (mLastScans.size() >= NUM_SCAN_DURATIONS_KEPT) { 202 mLastScans.remove(0); 203 } 204 mLastScans.add(scan); 205 206 BluetoothMetricsProto.ScanEvent scanEvent = BluetoothMetricsProto.ScanEvent.newBuilder() 207 .setScanEventType(BluetoothMetricsProto.ScanEvent.ScanEventType.SCAN_EVENT_STOP) 208 .setScanTechnologyType( 209 BluetoothMetricsProto.ScanEvent.ScanTechnologyType.SCAN_TECH_TYPE_LE) 210 .setEventTimeMillis(System.currentTimeMillis()) 211 .setInitiator(truncateAppName(appName)) 212 .setNumberResults(scan.results) 213 .build(); 214 mGattService.addScanEvent(scanEvent); 215 216 if (!isScanning()) { 217 long totalDuration = stopTime - mScanStartTime; 218 mTotalScanTime += totalDuration; 219 mMinScanTime = Math.min(totalDuration, mMinScanTime); 220 mMaxScanTime = Math.max(totalDuration, mMaxScanTime); 221 } 222 223 try { 224 // Inform battery stats of any results it might be missing on 225 // scan stop 226 boolean isUnoptimized = !(scan.filtered || scan.background || scan.opportunistic); 227 mBatteryStats.noteBleScanResults(mWorkSource, scan.results % 100); 228 mBatteryStats.noteBleScanStopped(mWorkSource, isUnoptimized); 229 } catch (RemoteException e) { 230 /* ignore */ 231 } 232 writeToStatsLog(scan, StatsLog.BLE_SCAN_STATE_CHANGED__STATE__OFF); 233 } 234 recordScanSuspend(int scannerId)235 synchronized void recordScanSuspend(int scannerId) { 236 LastScan scan = getScanFromScannerId(scannerId); 237 if (scan == null || scan.isSuspended) { 238 return; 239 } 240 scan.suspendStartTime = SystemClock.elapsedRealtime(); 241 scan.isSuspended = true; 242 } 243 recordScanResume(int scannerId)244 synchronized void recordScanResume(int scannerId) { 245 LastScan scan = getScanFromScannerId(scannerId); 246 long suspendDuration = 0; 247 if (scan == null || !scan.isSuspended) { 248 return; 249 } 250 scan.isSuspended = false; 251 stopTime = SystemClock.elapsedRealtime(); 252 suspendDuration = stopTime - scan.suspendStartTime; 253 scan.suspendDuration += suspendDuration; 254 mTotalSuspendTime += suspendDuration; 255 } 256 writeToStatsLog(LastScan scan, int statsLogState)257 private void writeToStatsLog(LastScan scan, int statsLogState) { 258 for (int i = 0; i < mWorkSource.size(); i++) { 259 StatsLog.write_non_chained(StatsLog.BLE_SCAN_STATE_CHANGED, 260 mWorkSource.get(i), null, 261 statsLogState, scan.filtered, scan.background, scan.opportunistic); 262 } 263 264 final List<WorkSource.WorkChain> workChains = mWorkSource.getWorkChains(); 265 if (workChains != null) { 266 for (int i = 0; i < workChains.size(); ++i) { 267 WorkSource.WorkChain workChain = workChains.get(i); 268 StatsLog.write(StatsLog.BLE_SCAN_STATE_CHANGED, 269 workChain.getUids(), workChain.getTags(), 270 statsLogState, scan.filtered, scan.background, scan.opportunistic); 271 } 272 } 273 } 274 setScanTimeout(int scannerId)275 synchronized void setScanTimeout(int scannerId) { 276 if (!isScanning()) { 277 return; 278 } 279 280 LastScan scan = getScanFromScannerId(scannerId); 281 if (scan != null) { 282 scan.timeout = true; 283 } 284 } 285 isScanningTooFrequently()286 synchronized boolean isScanningTooFrequently() { 287 if (mLastScans.size() < NUM_SCAN_DURATIONS_KEPT) { 288 return false; 289 } 290 291 return (SystemClock.elapsedRealtime() - mLastScans.get(0).timestamp) 292 < EXCESSIVE_SCANNING_PERIOD_MS; 293 } 294 isScanningTooLong()295 synchronized boolean isScanningTooLong() { 296 if (!isScanning()) { 297 return false; 298 } 299 return (SystemClock.elapsedRealtime() - mScanStartTime) > SCAN_TIMEOUT_MS; 300 } 301 302 // This function truncates the app name for privacy reasons. Apps with 303 // four part package names or more get truncated to three parts, and apps 304 // with three part package names names get truncated to two. Apps with two 305 // or less package names names are untouched. 306 // Examples: one.two.three.four => one.two.three 307 // one.two.three => one.two truncateAppName(String name)308 private String truncateAppName(String name) { 309 String initiator = name; 310 String[] nameSplit = initiator.split("\\."); 311 if (nameSplit.length > 3) { 312 initiator = nameSplit[0] + "." + nameSplit[1] + "." + nameSplit[2]; 313 } else if (nameSplit.length == 3) { 314 initiator = nameSplit[0] + "." + nameSplit[1]; 315 } 316 317 return initiator; 318 } 319 dumpToString(StringBuilder sb)320 synchronized void dumpToString(StringBuilder sb) { 321 long currTime = SystemClock.elapsedRealtime(); 322 long maxScan = mMaxScanTime; 323 long minScan = mMinScanTime; 324 long scanDuration = 0; 325 326 if (isScanning()) { 327 scanDuration = currTime - mScanStartTime; 328 } 329 minScan = Math.min(scanDuration, minScan); 330 maxScan = Math.max(scanDuration, maxScan); 331 332 if (minScan == Long.MAX_VALUE) { 333 minScan = 0; 334 } 335 336 /*TODO: Average scan time can be skewed for 337 * multiple scan clients. It will show less than 338 * actual value. 339 * */ 340 long avgScan = 0; 341 long totalScanTime = mTotalScanTime + scanDuration; 342 if (mScansStarted > 0) { 343 avgScan = totalScanTime / mScansStarted; 344 } 345 346 sb.append(" " + appName); 347 if (isRegistered) { 348 sb.append(" (Registered)"); 349 } 350 351 if (!mLastScans.isEmpty()) { 352 LastScan lastScan = mLastScans.get(mLastScans.size() - 1); 353 if (lastScan.opportunistic) { 354 sb.append(" (Opportunistic)"); 355 } 356 if (lastScan.background) { 357 sb.append(" (Background)"); 358 } 359 if (lastScan.timeout) { 360 sb.append(" (Forced-Opportunistic)"); 361 } 362 if (lastScan.filtered) { 363 sb.append(" (Filtered)"); 364 } 365 } 366 sb.append("\n"); 367 368 sb.append(" LE scans (started/stopped) : " + mScansStarted + " / " + mScansStopped 369 + "\n"); 370 sb.append(" Scan time in ms (min/max/avg/total): " + minScan + " / " + maxScan + " / " 371 + avgScan + " / " + totalScanTime + "\n"); 372 if (mTotalSuspendTime != 0) { 373 sb.append(" Total time suspended : " + mTotalSuspendTime + "ms\n"); 374 } 375 sb.append(" Total number of results : " + results + "\n"); 376 377 long currentTime = System.currentTimeMillis(); 378 long elapsedRt = SystemClock.elapsedRealtime(); 379 if (!mLastScans.isEmpty()) { 380 sb.append(" Last " + mLastScans.size() + " scans :\n"); 381 382 for (int i = 0; i < mLastScans.size(); i++) { 383 LastScan scan = mLastScans.get(i); 384 Date timestamp = new Date(currentTime - elapsedRt + scan.timestamp); 385 sb.append(" " + DATE_FORMAT.format(timestamp) + " - "); 386 sb.append(scan.duration + "ms "); 387 if (scan.opportunistic) { 388 sb.append("Opp "); 389 } 390 if (scan.background) { 391 sb.append("Back "); 392 } 393 if (scan.timeout) { 394 sb.append("Forced "); 395 } 396 if (scan.filtered) { 397 sb.append("Filter "); 398 } 399 sb.append(scan.results + " results"); 400 sb.append(" (" + scan.scannerId + ")"); 401 sb.append("\n"); 402 if (scan.suspendDuration != 0) { 403 sb.append(" └" + " Suspended Time: " + scan.suspendDuration + "ms\n"); 404 } 405 } 406 } 407 408 if (!mOngoingScans.isEmpty()) { 409 sb.append(" Ongoing scans :\n"); 410 for (Integer key : mOngoingScans.keySet()) { 411 LastScan scan = mOngoingScans.get(key); 412 Date timestamp = new Date(currentTime - elapsedRt + scan.timestamp); 413 sb.append(" " + DATE_FORMAT.format(timestamp) + " - "); 414 sb.append((elapsedRt - scan.timestamp) + "ms "); 415 if (scan.opportunistic) { 416 sb.append("Opp "); 417 } 418 if (scan.background) { 419 sb.append("Back "); 420 } 421 if (scan.timeout) { 422 sb.append("Forced "); 423 } 424 if (scan.filtered) { 425 sb.append("Filter "); 426 } 427 if (scan.isSuspended) { 428 sb.append("Suspended "); 429 } 430 sb.append(scan.results + " results"); 431 sb.append(" (" + scan.scannerId + ")"); 432 sb.append("\n"); 433 if (scan.suspendStartTime != 0) { 434 long duration = scan.suspendDuration + (scan.isSuspended ? (elapsedRt 435 - scan.suspendStartTime) : 0); 436 sb.append(" └" + " Suspended Time: " + duration + "ms\n"); 437 } 438 } 439 } 440 441 ContextMap.App appEntry = mContextMap.getByName(appName); 442 if (appEntry != null && isRegistered) { 443 sb.append(" Application ID : " + appEntry.id + "\n"); 444 sb.append(" UUID : " + appEntry.uuid + "\n"); 445 446 List<ContextMap.Connection> connections = mContextMap.getConnectionByApp(appEntry.id); 447 448 sb.append(" Connections: " + connections.size() + "\n"); 449 450 Iterator<ContextMap.Connection> ii = connections.iterator(); 451 while (ii.hasNext()) { 452 ContextMap.Connection connection = ii.next(); 453 long connectionTime = elapsedRt - connection.startTime; 454 Date timestamp = new Date(currentTime - elapsedRt + connection.startTime); 455 sb.append(" " + DATE_FORMAT.format(timestamp) + " - "); 456 sb.append((connectionTime) + "ms "); 457 sb.append(": " + connection.address + " (" + connection.connId + ")\n"); 458 } 459 } 460 sb.append("\n"); 461 } 462 } 463