• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
LastScan(long timestamp, long duration, boolean opportunistic, boolean background, boolean filtered)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 
AppScanStats(String name, WorkSource source, ContextMap map, GattService service)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 
addResult()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 
recordScanStart(ScanSettings settings, boolean filtered)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 
recordScanStop()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 
setScanTimeout()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 
isScanningTooFrequently()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 
isScanningTooLong()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
truncateAppName(String name)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 
dumpToString(StringBuilder sb)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