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