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