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