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