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 32 import 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 boolean isUnoptimized = !(scan.filtered || scan.background || scan.opportunistic); 150 batteryStats.noteBleScanStarted(workSource, isUnoptimized); 151 } catch (RemoteException e) { 152 /* ignore */ 153 } 154 } 155 156 synchronized void recordScanStop() { 157 if (!isScanning) 158 return; 159 160 this.scansStopped++; 161 isScanning = false; 162 stopTime = SystemClock.elapsedRealtime(); 163 long scanDuration = stopTime - startTime; 164 165 minScanTime = Math.min(scanDuration, minScanTime); 166 maxScanTime = Math.max(scanDuration, maxScanTime); 167 totalScanTime += scanDuration; 168 169 LastScan curr = lastScans.get(lastScans.size() - 1); 170 curr.duration = scanDuration; 171 172 if (lastScans.size() > NUM_SCAN_DURATIONS_KEPT) { 173 lastScans.remove(0); 174 } 175 176 BluetoothProto.ScanEvent scanEvent = new BluetoothProto.ScanEvent(); 177 scanEvent.setScanEventType(BluetoothProto.ScanEvent.SCAN_EVENT_STOP); 178 scanEvent.setScanTechnologyType(BluetoothProto.ScanEvent.SCAN_TECH_TYPE_LE); 179 scanEvent.setEventTimeMillis(System.currentTimeMillis()); 180 scanEvent.setInitiator(truncateAppName(appName)); 181 gattService.addScanEvent(scanEvent); 182 183 try { 184 // Inform battery stats of any results it might be missing on 185 // scan stop 186 batteryStats.noteBleScanResults(workSource, curr.results % 100); 187 batteryStats.noteBleScanStopped(workSource); 188 } catch (RemoteException e) { 189 /* ignore */ 190 } 191 } 192 193 synchronized void setScanTimeout() { 194 if (!isScanning) 195 return; 196 197 if (!lastScans.isEmpty()) { 198 LastScan curr = lastScans.get(lastScans.size() - 1); 199 curr.timeout = true; 200 } 201 } 202 203 synchronized boolean isScanningTooFrequently() { 204 if (lastScans.size() < NUM_SCAN_DURATIONS_KEPT) { 205 return false; 206 } 207 208 return (SystemClock.elapsedRealtime() - lastScans.get(0).timestamp) 209 < EXCESSIVE_SCANNING_PERIOD_MS; 210 } 211 212 synchronized boolean isScanningTooLong() { 213 if (lastScans.isEmpty() || !isScanning) { 214 return false; 215 } 216 217 return (SystemClock.elapsedRealtime() - startTime) > SCAN_TIMEOUT_MS; 218 } 219 220 // This function truncates the app name for privacy reasons. Apps with 221 // four part package names or more get truncated to three parts, and apps 222 // with three part package names names get truncated to two. Apps with two 223 // or less package names names are untouched. 224 // Examples: one.two.three.four => one.two.three 225 // one.two.three => one.two 226 private String truncateAppName(String name) { 227 String initiator = name; 228 String[] nameSplit = initiator.split("\\."); 229 if (nameSplit.length > 3) { 230 initiator = nameSplit[0] + "." + 231 nameSplit[1] + "." + 232 nameSplit[2]; 233 } else if (nameSplit.length == 3) { 234 initiator = nameSplit[0] + "." + nameSplit[1]; 235 } 236 237 return initiator; 238 } 239 240 synchronized void dumpToString(StringBuilder sb) { 241 long currTime = SystemClock.elapsedRealtime(); 242 long maxScan = maxScanTime; 243 long minScan = minScanTime; 244 long scanDuration = 0; 245 246 if (isScanning) { 247 scanDuration = currTime - startTime; 248 minScan = Math.min(scanDuration, minScan); 249 maxScan = Math.max(scanDuration, maxScan); 250 } 251 252 if (minScan == Long.MAX_VALUE) { 253 minScan = 0; 254 } 255 256 long avgScan = 0; 257 if (scansStarted > 0) { 258 avgScan = (totalScanTime + scanDuration) / scansStarted; 259 } 260 261 sb.append(" " + appName); 262 if (isRegistered) sb.append(" (Registered)"); 263 264 if (!lastScans.isEmpty()) { 265 LastScan lastScan = lastScans.get(lastScans.size() - 1); 266 if (lastScan.opportunistic) sb.append(" (Opportunistic)"); 267 if (lastScan.background) sb.append(" (Background)"); 268 if (lastScan.timeout) sb.append(" (Forced-Opportunistic)"); 269 if (lastScan.filtered) sb.append(" (Filtered)"); 270 } 271 sb.append("\n"); 272 273 sb.append(" LE scans (started/stopped) : " + 274 scansStarted + " / " + 275 scansStopped + "\n"); 276 sb.append(" Scan time in ms (min/max/avg/total): " + 277 minScan + " / " + 278 maxScan + " / " + 279 avgScan + " / " + 280 totalScanTime + "\n"); 281 sb.append(" Total number of results : " + 282 results + "\n"); 283 284 if (!lastScans.isEmpty()) { 285 int lastScansSize = scansStopped < NUM_SCAN_DURATIONS_KEPT ? 286 scansStopped : NUM_SCAN_DURATIONS_KEPT; 287 sb.append(" Last " + lastScansSize + 288 " scans :\n"); 289 290 for (int i = 0; i < lastScansSize; i++) { 291 LastScan scan = lastScans.get(i); 292 Date timestamp = new Date(System.currentTimeMillis() - SystemClock.elapsedRealtime() 293 + scan.timestamp); 294 sb.append(" " + dateFormat.format(timestamp) + " - "); 295 sb.append(scan.duration + "ms "); 296 if (scan.opportunistic) sb.append("Opp "); 297 if (scan.background) sb.append("Back "); 298 if (scan.timeout) sb.append("Forced "); 299 if (scan.filtered) sb.append("Filter "); 300 sb.append(scan.results + " results"); 301 sb.append("\n"); 302 } 303 } 304 305 ContextMap.App appEntry = contextMap.getByName(appName); 306 if (appEntry != null && isRegistered) { 307 sb.append(" Application ID : " + 308 appEntry.id + "\n"); 309 sb.append(" UUID : " + 310 appEntry.uuid + "\n"); 311 312 if (isScanning) { 313 sb.append(" Current scan duration in ms : " + 314 scanDuration + "\n"); 315 } 316 317 List<ContextMap.Connection> connections = 318 contextMap.getConnectionByApp(appEntry.id); 319 320 sb.append(" Connections: " + connections.size() + "\n"); 321 322 Iterator<ContextMap.Connection> ii = connections.iterator(); 323 while(ii.hasNext()) { 324 ContextMap.Connection connection = ii.next(); 325 long connectionTime = SystemClock.elapsedRealtime() - connection.startTime; 326 sb.append(" " + connection.connId + ": " + 327 connection.address + " " + connectionTime + "ms\n"); 328 } 329 } 330 sb.append("\n"); 331 } 332 } 333