AppScanStats.java revision a86ae2ce4305fb0af5522b3f46513b137e98836f
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 java.text.DateFormat;
20import java.text.SimpleDateFormat;
21import java.util.ArrayList;
22import java.util.Date;
23import java.util.Iterator;
24import java.util.List;
25
26import 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 background;
46        int results;
47
48        public LastScan(long timestamp, long duration,
49                        boolean opportunistic, boolean background) {
50            this.duration = duration;
51            this.timestamp = timestamp;
52            this.opportunistic = opportunistic;
53            this.background = background;
54            this.results = 0;
55        }
56    }
57
58    static final int NUM_SCAN_DURATIONS_KEPT = 5;
59
60    String appName;
61    int scansStarted = 0;
62    int scansStopped = 0;
63    boolean isScanning = false;
64    boolean isRegistered = false;
65    long minScanTime = Long.MAX_VALUE;
66    long maxScanTime = 0;
67    long totalScanTime = 0;
68    List<LastScan> lastScans = new ArrayList<LastScan>(NUM_SCAN_DURATIONS_KEPT + 1);
69    long startTime = 0;
70    long stopTime = 0;
71    int results = 0;
72
73    public AppScanStats(String name, ContextMap map, GattService service) {
74        appName = name;
75        contextMap = map;
76        gattService = service;
77    }
78
79    synchronized void addResult() {
80        if (!lastScans.isEmpty())
81            lastScans.get(lastScans.size() - 1).results++;
82
83        results++;
84    }
85
86    synchronized void recordScanStart(ScanSettings settings) {
87        if (isScanning)
88            return;
89
90        this.scansStarted++;
91        isScanning = true;
92        startTime = System.currentTimeMillis();
93
94        LastScan scan = new LastScan(startTime, 0, false, false);
95        if (settings != null) {
96          scan.opportunistic = settings.getScanMode() == ScanSettings.SCAN_MODE_OPPORTUNISTIC;
97          scan.background = (settings.getCallbackType() & ScanSettings.CALLBACK_TYPE_FIRST_MATCH) != 0;
98        }
99        lastScans.add(scan);
100
101        BluetoothProto.ScanEvent scanEvent = new BluetoothProto.ScanEvent();
102        scanEvent.setScanEventType(BluetoothProto.ScanEvent.SCAN_EVENT_START);
103        scanEvent.setScanTechnologyType(BluetoothProto.ScanEvent.SCAN_TECH_TYPE_LE);
104        scanEvent.setEventTimeMillis(System.currentTimeMillis());
105        scanEvent.setInitiator(truncateAppName(appName));
106        gattService.addScanEvent(scanEvent);
107    }
108
109    synchronized void recordScanStop() {
110        if (!isScanning)
111          return;
112
113        this.scansStopped++;
114        isScanning = false;
115        stopTime = System.currentTimeMillis();
116        long scanDuration = stopTime - startTime;
117
118        minScanTime = Math.min(scanDuration, minScanTime);
119        maxScanTime = Math.max(scanDuration, maxScanTime);
120        totalScanTime += scanDuration;
121
122        LastScan curr = lastScans.get(lastScans.size() - 1);
123        curr.duration = scanDuration;
124
125        if (lastScans.size() > NUM_SCAN_DURATIONS_KEPT) {
126            lastScans.remove(0);
127        }
128
129        BluetoothProto.ScanEvent scanEvent = new BluetoothProto.ScanEvent();
130        scanEvent.setScanEventType(BluetoothProto.ScanEvent.SCAN_EVENT_STOP);
131        scanEvent.setScanTechnologyType(BluetoothProto.ScanEvent.SCAN_TECH_TYPE_LE);
132        scanEvent.setEventTimeMillis(System.currentTimeMillis());
133        scanEvent.setInitiator(truncateAppName(appName));
134        gattService.addScanEvent(scanEvent);
135    }
136
137    // This function truncates the app name for privacy reasons. Apps with
138    // four part package names or more get truncated to three parts, and apps
139    // with three part package names names get truncated to two. Apps with two
140    // or less package names names are untouched.
141    // Examples: one.two.three.four => one.two.three
142    //           one.two.three => one.two
143    private String truncateAppName(String name) {
144        String initiator = name;
145        String[] nameSplit = initiator.split("\\.");
146        if (nameSplit.length > 3) {
147            initiator = nameSplit[0] + "." +
148                        nameSplit[1] + "." +
149                        nameSplit[2];
150        } else if (nameSplit.length == 3) {
151            initiator = nameSplit[0] + "." + nameSplit[1];
152        }
153
154        return initiator;
155    }
156
157    synchronized void dumpToString(StringBuilder sb) {
158        long currTime = System.currentTimeMillis();
159        long maxScan = maxScanTime;
160        long minScan = minScanTime;
161        long scanDuration = 0;
162
163        if (lastScans.isEmpty())
164            return;
165
166        if (isScanning) {
167            scanDuration = currTime - startTime;
168            minScan = Math.min(scanDuration, minScan);
169            maxScan = Math.max(scanDuration, maxScan);
170        }
171
172        if (minScan == Long.MAX_VALUE) {
173            minScan = 0;
174        }
175
176        long avgScan = 0;
177        if (scansStarted > 0) {
178            avgScan = (totalScanTime + scanDuration) / scansStarted;
179        }
180
181        LastScan lastScan = lastScans.get(lastScans.size() - 1);
182        sb.append("  " + appName);
183        if (isRegistered) sb.append(" (Registered)");
184        if (lastScan.opportunistic) sb.append(" (Opportunistic)");
185        if (lastScan.background) sb.append(" (Background)");
186        sb.append("\n");
187
188        sb.append("  LE scans (started/stopped)         : " +
189                  scansStarted + " / " +
190                  scansStopped + "\n");
191        sb.append("  Scan time in ms (min/max/avg/total): " +
192                  minScan + " / " +
193                  maxScan + " / " +
194                  avgScan + " / " +
195                  totalScanTime + "\n");
196        sb.append("  Total number of results            : " +
197                  results + "\n");
198
199        if (lastScans.size() != 0) {
200            int lastScansSize = scansStopped < NUM_SCAN_DURATIONS_KEPT ?
201                                scansStopped : NUM_SCAN_DURATIONS_KEPT;
202            sb.append("  Last " + lastScansSize +
203                      " scans                       :\n");
204
205            for (int i = 0; i < lastScansSize; i++) {
206                LastScan scan = lastScans.get(i);
207                Date timestamp = new Date(scan.timestamp);
208                sb.append("    " + dateFormat.format(timestamp) + " - ");
209                sb.append(scan.duration + "ms ");
210                if (scan.opportunistic) sb.append("Opp ");
211                if (scan.background) sb.append("Back ");
212                sb.append(scan.results + " results");
213                sb.append("\n");
214            }
215        }
216
217        ContextMap.App appEntry = contextMap.getByName(appName);
218        if (appEntry != null && isRegistered) {
219            sb.append("  Application ID                     : " +
220                      appEntry.id + "\n");
221            sb.append("  UUID                               : " +
222                      appEntry.uuid + "\n");
223
224            if (isScanning) {
225                sb.append("  Current scan duration in ms        : " +
226                          scanDuration + "\n");
227            }
228
229            List<ContextMap.Connection> connections =
230              contextMap.getConnectionByApp(appEntry.id);
231
232            sb.append("  Connections: " + connections.size() + "\n");
233
234            Iterator<ContextMap.Connection> ii = connections.iterator();
235            while(ii.hasNext()) {
236                ContextMap.Connection connection = ii.next();
237                long connectionTime = System.currentTimeMillis() - connection.startTime;
238                sb.append("    " + connection.connId + ": " +
239                          connection.address + " " + connectionTime + "ms\n");
240            }
241        }
242        sb.append("\n");
243    }
244}
245