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