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