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