• 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 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