• 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 android.os.Binder;
20 import android.os.RemoteException;
21 import android.os.ServiceManager;
22 import android.os.SystemClock;
23 import android.os.WorkSource;
24 import android.util.StatsLog;
25 
26 import com.android.bluetooth.BluetoothMetricsProto;
27 import com.android.internal.app.IBatteryStats;
28 
29 import java.text.DateFormat;
30 import java.text.SimpleDateFormat;
31 import java.util.ArrayList;
32 import java.util.Date;
33 import java.util.HashMap;
34 import java.util.Iterator;
35 import java.util.List;
36 
37 /**
38  * ScanStats class helps keep track of information about scans
39  * on a per application basis.
40  * @hide
41  */
42 /*package*/ class AppScanStats {
43     private static final String TAG = AppScanStats.class.getSimpleName();
44 
45     static final DateFormat DATE_FORMAT = new SimpleDateFormat("MM-dd HH:mm:ss");
46 
47     /* ContextMap here is needed to grab Apps and Connections */ ContextMap mContextMap;
48 
49     /* GattService is needed to add scan event protos to be dumped later */ GattService
50             mGattService;
51 
52     /* Battery stats is used to keep track of scans and result stats */ IBatteryStats mBatteryStats;
53 
54     class LastScan {
55         public long duration;
56         public long suspendDuration;
57         public long suspendStartTime;
58         public boolean isSuspended;
59         public long timestamp;
60         public boolean opportunistic;
61         public boolean timeout;
62         public boolean background;
63         public boolean filtered;
64         public int results;
65         public int scannerId;
66 
LastScan(long timestamp, long duration, boolean opportunistic, boolean background, boolean filtered, int scannerId)67         LastScan(long timestamp, long duration, boolean opportunistic, boolean background,
68                 boolean filtered, int scannerId) {
69             this.duration = duration;
70             this.timestamp = timestamp;
71             this.opportunistic = opportunistic;
72             this.background = background;
73             this.filtered = filtered;
74             this.results = 0;
75             this.scannerId = scannerId;
76             this.suspendDuration = 0;
77             this.suspendStartTime = 0;
78             this.isSuspended = false;
79         }
80     }
81 
82     static final int NUM_SCAN_DURATIONS_KEPT = 5;
83 
84     // This constant defines the time window an app can scan multiple times.
85     // Any single app can scan up to |NUM_SCAN_DURATIONS_KEPT| times during
86     // this window. Once they reach this limit, they must wait until their
87     // earliest recorded scan exits this window.
88     static final long EXCESSIVE_SCANNING_PERIOD_MS = 30 * 1000;
89 
90     // Maximum msec before scan gets downgraded to opportunistic
91     static final int SCAN_TIMEOUT_MS = 30 * 60 * 1000;
92 
93     public String appName;
94     public WorkSource mWorkSource; // Used for BatteryStats and StatsLog
95     private int mScansStarted = 0;
96     private int mScansStopped = 0;
97     public boolean isRegistered = false;
98     private long mMinScanTime = Long.MAX_VALUE;
99     private long mMaxScanTime = 0;
100     private long mScanStartTime = 0;
101     private long mTotalScanTime = 0;
102     private long mTotalSuspendTime = 0;
103     private List<LastScan> mLastScans = new ArrayList<LastScan>(NUM_SCAN_DURATIONS_KEPT);
104     private HashMap<Integer, LastScan> mOngoingScans = new HashMap<Integer, LastScan>();
105     public long startTime = 0;
106     public long stopTime = 0;
107     public int results = 0;
108 
AppScanStats(String name, WorkSource source, ContextMap map, GattService service)109     AppScanStats(String name, WorkSource source, ContextMap map, GattService service) {
110         appName = name;
111         mContextMap = map;
112         mGattService = service;
113         mBatteryStats = IBatteryStats.Stub.asInterface(ServiceManager.getService("batterystats"));
114 
115         if (source == null) {
116             // Bill the caller if the work source isn't passed through
117             source = new WorkSource(Binder.getCallingUid(), appName);
118         }
119         mWorkSource = source;
120     }
121 
addResult(int scannerId)122     synchronized void addResult(int scannerId) {
123         LastScan scan = getScanFromScannerId(scannerId);
124         if (scan != null) {
125             int batteryStatsResults = ++scan.results;
126 
127             // Only update battery stats after receiving 100 new results in order
128             // to lower the cost of the binder transaction
129             if (batteryStatsResults % 100 == 0) {
130                 try {
131                     mBatteryStats.noteBleScanResults(mWorkSource, 100);
132                 } catch (RemoteException e) {
133                     /* ignore */
134                 }
135             }
136         }
137 
138         results++;
139     }
140 
isScanning()141     boolean isScanning() {
142         return !mOngoingScans.isEmpty();
143     }
144 
getScanFromScannerId(int scannerId)145     LastScan getScanFromScannerId(int scannerId) {
146         return mOngoingScans.get(scannerId);
147     }
148 
recordScanStart(ScanSettings settings, boolean filtered, int scannerId)149     synchronized void recordScanStart(ScanSettings settings, boolean filtered, int scannerId) {
150         LastScan existingScan = getScanFromScannerId(scannerId);
151         if (existingScan != null) {
152             return;
153         }
154         this.mScansStarted++;
155         startTime = SystemClock.elapsedRealtime();
156 
157         LastScan scan = new LastScan(startTime, 0, false, false, filtered, scannerId);
158         if (settings != null) {
159             scan.opportunistic = settings.getScanMode() == ScanSettings.SCAN_MODE_OPPORTUNISTIC;
160             scan.background =
161                     (settings.getCallbackType() & ScanSettings.CALLBACK_TYPE_FIRST_MATCH) != 0;
162         }
163 
164         BluetoothMetricsProto.ScanEvent scanEvent = BluetoothMetricsProto.ScanEvent.newBuilder()
165                 .setScanEventType(BluetoothMetricsProto.ScanEvent.ScanEventType.SCAN_EVENT_START)
166                 .setScanTechnologyType(
167                         BluetoothMetricsProto.ScanEvent.ScanTechnologyType.SCAN_TECH_TYPE_LE)
168                 .setEventTimeMillis(System.currentTimeMillis())
169                 .setInitiator(truncateAppName(appName)).build();
170         mGattService.addScanEvent(scanEvent);
171 
172         if (!isScanning()) {
173             mScanStartTime = startTime;
174         }
175         try {
176             boolean isUnoptimized = !(scan.filtered || scan.background || scan.opportunistic);
177             mBatteryStats.noteBleScanStarted(mWorkSource, isUnoptimized);
178         } catch (RemoteException e) {
179             /* ignore */
180         }
181         writeToStatsLog(scan, StatsLog.BLE_SCAN_STATE_CHANGED__STATE__ON);
182 
183         mOngoingScans.put(scannerId, scan);
184     }
185 
recordScanStop(int scannerId)186     synchronized void recordScanStop(int scannerId) {
187         LastScan scan = getScanFromScannerId(scannerId);
188         if (scan == null) {
189             return;
190         }
191         this.mScansStopped++;
192         stopTime = SystemClock.elapsedRealtime();
193         long scanDuration = stopTime - scan.timestamp;
194         scan.duration = scanDuration;
195         if (scan.isSuspended) {
196             long suspendDuration = stopTime - scan.suspendStartTime;
197             scan.suspendDuration += suspendDuration;
198             mTotalSuspendTime += suspendDuration;
199         }
200         mOngoingScans.remove(scannerId);
201         if (mLastScans.size() >= NUM_SCAN_DURATIONS_KEPT) {
202             mLastScans.remove(0);
203         }
204         mLastScans.add(scan);
205 
206         BluetoothMetricsProto.ScanEvent scanEvent = BluetoothMetricsProto.ScanEvent.newBuilder()
207                 .setScanEventType(BluetoothMetricsProto.ScanEvent.ScanEventType.SCAN_EVENT_STOP)
208                 .setScanTechnologyType(
209                         BluetoothMetricsProto.ScanEvent.ScanTechnologyType.SCAN_TECH_TYPE_LE)
210                 .setEventTimeMillis(System.currentTimeMillis())
211                 .setInitiator(truncateAppName(appName))
212                 .setNumberResults(scan.results)
213                 .build();
214         mGattService.addScanEvent(scanEvent);
215 
216         if (!isScanning()) {
217             long totalDuration = stopTime - mScanStartTime;
218             mTotalScanTime += totalDuration;
219             mMinScanTime = Math.min(totalDuration, mMinScanTime);
220             mMaxScanTime = Math.max(totalDuration, mMaxScanTime);
221         }
222 
223         try {
224             // Inform battery stats of any results it might be missing on
225             // scan stop
226             boolean isUnoptimized = !(scan.filtered || scan.background || scan.opportunistic);
227             mBatteryStats.noteBleScanResults(mWorkSource, scan.results % 100);
228             mBatteryStats.noteBleScanStopped(mWorkSource, isUnoptimized);
229         } catch (RemoteException e) {
230             /* ignore */
231         }
232         writeToStatsLog(scan, StatsLog.BLE_SCAN_STATE_CHANGED__STATE__OFF);
233     }
234 
recordScanSuspend(int scannerId)235     synchronized void recordScanSuspend(int scannerId) {
236         LastScan scan = getScanFromScannerId(scannerId);
237         if (scan == null || scan.isSuspended) {
238             return;
239         }
240         scan.suspendStartTime = SystemClock.elapsedRealtime();
241         scan.isSuspended = true;
242     }
243 
recordScanResume(int scannerId)244     synchronized void recordScanResume(int scannerId) {
245         LastScan scan = getScanFromScannerId(scannerId);
246         long suspendDuration = 0;
247         if (scan == null || !scan.isSuspended) {
248             return;
249         }
250         scan.isSuspended = false;
251         stopTime = SystemClock.elapsedRealtime();
252         suspendDuration = stopTime - scan.suspendStartTime;
253         scan.suspendDuration += suspendDuration;
254         mTotalSuspendTime += suspendDuration;
255     }
256 
writeToStatsLog(LastScan scan, int statsLogState)257     private void writeToStatsLog(LastScan scan, int statsLogState) {
258         for (int i = 0; i < mWorkSource.size(); i++) {
259             StatsLog.write_non_chained(StatsLog.BLE_SCAN_STATE_CHANGED,
260                     mWorkSource.get(i), null,
261                     statsLogState, scan.filtered, scan.background, scan.opportunistic);
262         }
263 
264         final List<WorkSource.WorkChain> workChains = mWorkSource.getWorkChains();
265         if (workChains != null) {
266             for (int i = 0; i < workChains.size(); ++i) {
267                 WorkSource.WorkChain workChain = workChains.get(i);
268                 StatsLog.write(StatsLog.BLE_SCAN_STATE_CHANGED,
269                         workChain.getUids(), workChain.getTags(),
270                         statsLogState, scan.filtered, scan.background, scan.opportunistic);
271             }
272         }
273     }
274 
setScanTimeout(int scannerId)275     synchronized void setScanTimeout(int scannerId) {
276         if (!isScanning()) {
277             return;
278         }
279 
280         LastScan scan = getScanFromScannerId(scannerId);
281         if (scan != null) {
282             scan.timeout = true;
283         }
284     }
285 
isScanningTooFrequently()286     synchronized boolean isScanningTooFrequently() {
287         if (mLastScans.size() < NUM_SCAN_DURATIONS_KEPT) {
288             return false;
289         }
290 
291         return (SystemClock.elapsedRealtime() - mLastScans.get(0).timestamp)
292                 < EXCESSIVE_SCANNING_PERIOD_MS;
293     }
294 
isScanningTooLong()295     synchronized boolean isScanningTooLong() {
296         if (!isScanning()) {
297             return false;
298         }
299         return (SystemClock.elapsedRealtime() - mScanStartTime) > SCAN_TIMEOUT_MS;
300     }
301 
302     // This function truncates the app name for privacy reasons. Apps with
303     // four part package names or more get truncated to three parts, and apps
304     // with three part package names names get truncated to two. Apps with two
305     // or less package names names are untouched.
306     // Examples: one.two.three.four => one.two.three
307     //           one.two.three => one.two
truncateAppName(String name)308     private String truncateAppName(String name) {
309         String initiator = name;
310         String[] nameSplit = initiator.split("\\.");
311         if (nameSplit.length > 3) {
312             initiator = nameSplit[0] + "." + nameSplit[1] + "." + nameSplit[2];
313         } else if (nameSplit.length == 3) {
314             initiator = nameSplit[0] + "." + nameSplit[1];
315         }
316 
317         return initiator;
318     }
319 
dumpToString(StringBuilder sb)320     synchronized void dumpToString(StringBuilder sb) {
321         long currTime = SystemClock.elapsedRealtime();
322         long maxScan = mMaxScanTime;
323         long minScan = mMinScanTime;
324         long scanDuration = 0;
325 
326         if (isScanning()) {
327             scanDuration = currTime - mScanStartTime;
328         }
329         minScan = Math.min(scanDuration, minScan);
330         maxScan = Math.max(scanDuration, maxScan);
331 
332         if (minScan == Long.MAX_VALUE) {
333             minScan = 0;
334         }
335 
336         /*TODO: Average scan time can be skewed for
337          * multiple scan clients. It will show less than
338          * actual value.
339          * */
340         long avgScan = 0;
341         long totalScanTime = mTotalScanTime + scanDuration;
342         if (mScansStarted > 0) {
343             avgScan = totalScanTime / mScansStarted;
344         }
345 
346         sb.append("  " + appName);
347         if (isRegistered) {
348             sb.append(" (Registered)");
349         }
350 
351         if (!mLastScans.isEmpty()) {
352             LastScan lastScan = mLastScans.get(mLastScans.size() - 1);
353             if (lastScan.opportunistic) {
354                 sb.append(" (Opportunistic)");
355             }
356             if (lastScan.background) {
357                 sb.append(" (Background)");
358             }
359             if (lastScan.timeout) {
360                 sb.append(" (Forced-Opportunistic)");
361             }
362             if (lastScan.filtered) {
363                 sb.append(" (Filtered)");
364             }
365         }
366         sb.append("\n");
367 
368         sb.append("  LE scans (started/stopped)         : " + mScansStarted + " / " + mScansStopped
369                 + "\n");
370         sb.append("  Scan time in ms (min/max/avg/total): " + minScan + " / " + maxScan + " / "
371                 + avgScan + " / " + totalScanTime + "\n");
372         if (mTotalSuspendTime != 0) {
373             sb.append("  Total time suspended               : " + mTotalSuspendTime + "ms\n");
374         }
375         sb.append("  Total number of results            : " + results + "\n");
376 
377         long currentTime = System.currentTimeMillis();
378         long elapsedRt = SystemClock.elapsedRealtime();
379         if (!mLastScans.isEmpty()) {
380             sb.append("  Last " + mLastScans.size() + " scans                       :\n");
381 
382             for (int i = 0; i < mLastScans.size(); i++) {
383                 LastScan scan = mLastScans.get(i);
384                 Date timestamp = new Date(currentTime - elapsedRt + scan.timestamp);
385                 sb.append("    " + DATE_FORMAT.format(timestamp) + " - ");
386                 sb.append(scan.duration + "ms ");
387                 if (scan.opportunistic) {
388                     sb.append("Opp ");
389                 }
390                 if (scan.background) {
391                     sb.append("Back ");
392                 }
393                 if (scan.timeout) {
394                     sb.append("Forced ");
395                 }
396                 if (scan.filtered) {
397                     sb.append("Filter ");
398                 }
399                 sb.append(scan.results + " results");
400                 sb.append(" (" + scan.scannerId + ")");
401                 sb.append("\n");
402                 if (scan.suspendDuration != 0) {
403                     sb.append("      └" + " Suspended Time: " + scan.suspendDuration + "ms\n");
404                 }
405             }
406         }
407 
408         if (!mOngoingScans.isEmpty()) {
409             sb.append("  Ongoing scans                      :\n");
410             for (Integer key : mOngoingScans.keySet()) {
411                 LastScan scan = mOngoingScans.get(key);
412                 Date timestamp = new Date(currentTime - elapsedRt + scan.timestamp);
413                 sb.append("    " + DATE_FORMAT.format(timestamp) + " - ");
414                 sb.append((elapsedRt - scan.timestamp) + "ms ");
415                 if (scan.opportunistic) {
416                     sb.append("Opp ");
417                 }
418                 if (scan.background) {
419                     sb.append("Back ");
420                 }
421                 if (scan.timeout) {
422                     sb.append("Forced ");
423                 }
424                 if (scan.filtered) {
425                     sb.append("Filter ");
426                 }
427                 if (scan.isSuspended) {
428                     sb.append("Suspended ");
429                 }
430                 sb.append(scan.results + " results");
431                 sb.append(" (" + scan.scannerId + ")");
432                 sb.append("\n");
433                 if (scan.suspendStartTime != 0) {
434                     long duration = scan.suspendDuration + (scan.isSuspended ? (elapsedRt
435                             - scan.suspendStartTime) : 0);
436                     sb.append("      └" + " Suspended Time: " + duration + "ms\n");
437                 }
438             }
439         }
440 
441         ContextMap.App appEntry = mContextMap.getByName(appName);
442         if (appEntry != null && isRegistered) {
443             sb.append("  Application ID                     : " + appEntry.id + "\n");
444             sb.append("  UUID                               : " + appEntry.uuid + "\n");
445 
446             List<ContextMap.Connection> connections = mContextMap.getConnectionByApp(appEntry.id);
447 
448             sb.append("  Connections: " + connections.size() + "\n");
449 
450             Iterator<ContextMap.Connection> ii = connections.iterator();
451             while (ii.hasNext()) {
452                 ContextMap.Connection connection = ii.next();
453                 long connectionTime = elapsedRt - connection.startTime;
454                 Date timestamp = new Date(currentTime - elapsedRt + connection.startTime);
455                 sb.append("    " + DATE_FORMAT.format(timestamp) + " - ");
456                 sb.append((connectionTime) + "ms ");
457                 sb.append(": " + connection.address + " (" + connection.connId + ")\n");
458             }
459         }
460         sb.append("\n");
461     }
462 }
463