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