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 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 
     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 
     80     public AppScanStats(String name, ContextMap map, GattService service) {
     81         appName = name;
     82         contextMap = map;
     83         gattService = service;
     84     }
     85 
     86     synchronized void addResult() {
     87         if (!lastScans.isEmpty())
     88             lastScans.get(lastScans.size() - 1).results++;
     89 
     90         results++;
     91     }
     92 
     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 
    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 
    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 
    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
    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 
    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