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