blob: 4cab832fbba6e5479acb2f94aabb82559491e3db [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.bluetooth.gatt;
import android.bluetooth.le.ScanFilter;
import android.bluetooth.le.ScanSettings;
import android.os.Binder;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemClock;
import android.os.WorkSource;
import com.android.bluetooth.BluetoothMetricsProto;
import com.android.bluetooth.BluetoothStatsLog;
import com.android.internal.app.IBatteryStats;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
/**
* ScanStats class helps keep track of information about scans
* on a per application basis.
* @hide
*/
/*package*/ class AppScanStats {
private static final String TAG = AppScanStats.class.getSimpleName();
static final DateFormat DATE_FORMAT = new SimpleDateFormat("MM-dd HH:mm:ss");
static final int OPPORTUNISTIC_WEIGHT = 0;
static final int LOW_POWER_WEIGHT = 10;
static final int BALANCED_WEIGHT = 25;
static final int LOW_LATENCY_WEIGHT = 100;
/* ContextMap here is needed to grab Apps and Connections */ ContextMap mContextMap;
/* GattService is needed to add scan event protos to be dumped later */ GattService
mGattService;
/* Battery stats is used to keep track of scans and result stats */ IBatteryStats
mBatteryStats;
class LastScan {
public long duration;
public long suspendDuration;
public long suspendStartTime;
public boolean isSuspended;
public long timestamp;
public boolean isOpportunisticScan;
public boolean isTimeout;
public boolean isBackgroundScan;
public boolean isFilterScan;
public boolean isCallbackScan;
public boolean isBatchScan;
public int results;
public int scannerId;
public int scanMode;
public int scanCallbackType;
public String filterString;
LastScan(long timestamp, boolean isFilterScan, boolean isCallbackScan, int scannerId,
int scanMode, int scanCallbackType) {
this.duration = 0;
this.timestamp = timestamp;
this.isOpportunisticScan = false;
this.isTimeout = false;
this.isBackgroundScan = false;
this.isFilterScan = isFilterScan;
this.isCallbackScan = isCallbackScan;
this.isBatchScan = false;
this.scanMode = scanMode;
this.scanCallbackType = scanCallbackType;
this.results = 0;
this.scannerId = scannerId;
this.suspendDuration = 0;
this.suspendStartTime = 0;
this.isSuspended = false;
this.filterString = "";
}
}
static final int NUM_SCAN_DURATIONS_KEPT = 5;
// This constant defines the time window an app can scan multiple times.
// Any single app can scan up to |NUM_SCAN_DURATIONS_KEPT| times during
// this window. Once they reach this limit, they must wait until their
// earliest recorded scan exits this window.
static final long EXCESSIVE_SCANNING_PERIOD_MS = 30 * 1000;
// Maximum msec before scan gets downgraded to opportunistic
static final int SCAN_TIMEOUT_MS = 30 * 60 * 1000;
public String appName;
public WorkSource mWorkSource; // Used for BatteryStats and BluetoothStatsLog
private int mScansStarted = 0;
private int mScansStopped = 0;
public boolean isRegistered = false;
private long mScanStartTime = 0;
private long mTotalActiveTime = 0;
private long mTotalSuspendTime = 0;
private long mTotalScanTime = 0;
private long mOppScanTime = 0;
private long mLowPowerScanTime = 0;
private long mBalancedScanTime = 0;
private long mLowLantencyScanTime = 0;
private int mOppScan = 0;
private int mLowPowerScan = 0;
private int mBalancedScan = 0;
private int mLowLantencyScan = 0;
private List<LastScan> mLastScans = new ArrayList<LastScan>(NUM_SCAN_DURATIONS_KEPT);
private HashMap<Integer, LastScan> mOngoingScans = new HashMap<Integer, LastScan>();
public long startTime = 0;
public long stopTime = 0;
public int results = 0;
AppScanStats(String name, WorkSource source, ContextMap map, GattService service) {
appName = name;
mContextMap = map;
mGattService = service;
mBatteryStats = IBatteryStats.Stub.asInterface(ServiceManager.getService("batterystats"));
if (source == null) {
// Bill the caller if the work source isn't passed through
source = new WorkSource(Binder.getCallingUid(), appName);
}
mWorkSource = source;
}
synchronized void addResult(int scannerId) {
LastScan scan = getScanFromScannerId(scannerId);
if (scan != null) {
scan.results++;
// Only update battery stats after receiving 100 new results in order
// to lower the cost of the binder transaction
if (scan.results % 100 == 0) {
try {
mBatteryStats.noteBleScanResults(mWorkSource, 100);
} catch (RemoteException e) {
/* ignore */
}
BluetoothStatsLog.write(
BluetoothStatsLog.BLE_SCAN_RESULT_RECEIVED, mWorkSource, 100);
}
}
results++;
}
boolean isScanning() {
return !mOngoingScans.isEmpty();
}
LastScan getScanFromScannerId(int scannerId) {
return mOngoingScans.get(scannerId);
}
synchronized void recordScanStart(ScanSettings settings, List<ScanFilter> filters,
boolean isFilterScan, boolean isCallbackScan, int scannerId) {
LastScan existingScan = getScanFromScannerId(scannerId);
if (existingScan != null) {
return;
}
this.mScansStarted++;
startTime = SystemClock.elapsedRealtime();
LastScan scan = new LastScan(startTime, isFilterScan, isCallbackScan, scannerId,
settings.getScanMode(), settings.getCallbackType());
if (settings != null) {
scan.isOpportunisticScan = scan.scanMode == ScanSettings.SCAN_MODE_OPPORTUNISTIC;
scan.isBackgroundScan =
(scan.scanCallbackType & ScanSettings.CALLBACK_TYPE_FIRST_MATCH) != 0;
scan.isBatchScan =
settings.getCallbackType() == ScanSettings.CALLBACK_TYPE_ALL_MATCHES
&& settings.getReportDelayMillis() != 0;
switch (scan.scanMode) {
case ScanSettings.SCAN_MODE_OPPORTUNISTIC:
mOppScan++;
break;
case ScanSettings.SCAN_MODE_LOW_POWER:
mLowPowerScan++;
break;
case ScanSettings.SCAN_MODE_BALANCED:
mBalancedScan++;
break;
case ScanSettings.SCAN_MODE_LOW_LATENCY:
mLowLantencyScan++;
break;
}
}
if (isFilterScan) {
for (ScanFilter filter : filters) {
scan.filterString +=
"\n └ " + filterToStringWithoutNullParam(filter);
}
}
BluetoothMetricsProto.ScanEvent scanEvent = BluetoothMetricsProto.ScanEvent.newBuilder()
.setScanEventType(BluetoothMetricsProto.ScanEvent.ScanEventType.SCAN_EVENT_START)
.setScanTechnologyType(
BluetoothMetricsProto.ScanEvent.ScanTechnologyType.SCAN_TECH_TYPE_LE)
.setEventTimeMillis(System.currentTimeMillis())
.setInitiator(truncateAppName(appName)).build();
mGattService.addScanEvent(scanEvent);
if (!isScanning()) {
mScanStartTime = startTime;
}
try {
boolean isUnoptimized =
!(scan.isFilterScan || scan.isBackgroundScan || scan.isOpportunisticScan);
mBatteryStats.noteBleScanStarted(mWorkSource, isUnoptimized);
} catch (RemoteException e) {
/* ignore */
}
BluetoothStatsLog.write(BluetoothStatsLog.BLE_SCAN_STATE_CHANGED, mWorkSource,
BluetoothStatsLog.BLE_SCAN_STATE_CHANGED__STATE__ON,
scan.isFilterScan, scan.isBackgroundScan, scan.isOpportunisticScan);
mOngoingScans.put(scannerId, scan);
}
synchronized void recordScanStop(int scannerId) {
LastScan scan = getScanFromScannerId(scannerId);
if (scan == null) {
return;
}
this.mScansStopped++;
stopTime = SystemClock.elapsedRealtime();
long scanDuration = stopTime - scan.timestamp;
scan.duration = scanDuration;
if (scan.isSuspended) {
long suspendDuration = stopTime - scan.suspendStartTime;
scan.suspendDuration += suspendDuration;
mTotalSuspendTime += suspendDuration;
}
mOngoingScans.remove(scannerId);
if (mLastScans.size() >= NUM_SCAN_DURATIONS_KEPT) {
mLastScans.remove(0);
}
mLastScans.add(scan);
BluetoothMetricsProto.ScanEvent scanEvent = BluetoothMetricsProto.ScanEvent.newBuilder()
.setScanEventType(BluetoothMetricsProto.ScanEvent.ScanEventType.SCAN_EVENT_STOP)
.setScanTechnologyType(
BluetoothMetricsProto.ScanEvent.ScanTechnologyType.SCAN_TECH_TYPE_LE)
.setEventTimeMillis(System.currentTimeMillis())
.setInitiator(truncateAppName(appName))
.setNumberResults(scan.results)
.build();
mGattService.addScanEvent(scanEvent);
mTotalScanTime += scanDuration;
long activeDuration = scanDuration - scan.suspendDuration;
mTotalActiveTime += activeDuration;
switch (scan.scanMode) {
case ScanSettings.SCAN_MODE_OPPORTUNISTIC:
mOppScanTime += activeDuration;
break;
case ScanSettings.SCAN_MODE_LOW_POWER:
mLowPowerScanTime += activeDuration;
break;
case ScanSettings.SCAN_MODE_BALANCED:
mBalancedScanTime += activeDuration;
break;
case ScanSettings.SCAN_MODE_LOW_LATENCY:
mLowLantencyScanTime += activeDuration;
break;
}
try {
// Inform battery stats of any results it might be missing on scan stop
boolean isUnoptimized =
!(scan.isFilterScan || scan.isBackgroundScan || scan.isOpportunisticScan);
mBatteryStats.noteBleScanResults(mWorkSource, scan.results % 100);
mBatteryStats.noteBleScanStopped(mWorkSource, isUnoptimized);
} catch (RemoteException e) {
/* ignore */
}
BluetoothStatsLog.write(
BluetoothStatsLog.BLE_SCAN_RESULT_RECEIVED, mWorkSource, scan.results % 100);
BluetoothStatsLog.write(BluetoothStatsLog.BLE_SCAN_STATE_CHANGED, mWorkSource,
BluetoothStatsLog.BLE_SCAN_STATE_CHANGED__STATE__OFF,
scan.isFilterScan, scan.isBackgroundScan, scan.isOpportunisticScan);
}
synchronized void recordScanSuspend(int scannerId) {
LastScan scan = getScanFromScannerId(scannerId);
if (scan == null || scan.isSuspended) {
return;
}
scan.suspendStartTime = SystemClock.elapsedRealtime();
scan.isSuspended = true;
}
synchronized void recordScanResume(int scannerId) {
LastScan scan = getScanFromScannerId(scannerId);
long suspendDuration = 0;
if (scan == null || !scan.isSuspended) {
return;
}
scan.isSuspended = false;
stopTime = SystemClock.elapsedRealtime();
suspendDuration = stopTime - scan.suspendStartTime;
scan.suspendDuration += suspendDuration;
mTotalSuspendTime += suspendDuration;
}
synchronized void setScanTimeout(int scannerId) {
if (!isScanning()) {
return;
}
LastScan scan = getScanFromScannerId(scannerId);
if (scan != null) {
scan.isTimeout = true;
}
}
synchronized boolean isScanningTooFrequently() {
if (mLastScans.size() < NUM_SCAN_DURATIONS_KEPT) {
return false;
}
return (SystemClock.elapsedRealtime() - mLastScans.get(0).timestamp)
< EXCESSIVE_SCANNING_PERIOD_MS;
}
synchronized boolean isScanningTooLong() {
if (!isScanning()) {
return false;
}
return (SystemClock.elapsedRealtime() - mScanStartTime) > SCAN_TIMEOUT_MS;
}
// This function truncates the app name for privacy reasons. Apps with
// four part package names or more get truncated to three parts, and apps
// with three part package names names get truncated to two. Apps with two
// or less package names names are untouched.
// Examples: one.two.three.four => one.two.three
// one.two.three => one.two
private String truncateAppName(String name) {
String initiator = name;
String[] nameSplit = initiator.split("\\.");
if (nameSplit.length > 3) {
initiator = nameSplit[0] + "." + nameSplit[1] + "." + nameSplit[2];
} else if (nameSplit.length == 3) {
initiator = nameSplit[0] + "." + nameSplit[1];
}
return initiator;
}
private static String filterToStringWithoutNullParam(ScanFilter filter) {
String filterString = "BluetoothLeScanFilter [";
if (filter.getDeviceName() != null) {
filterString += " DeviceName=" + filter.getDeviceName();
}
if (filter.getDeviceAddress() != null) {
filterString += " DeviceAddress=" + filter.getDeviceAddress();
}
if (filter.getServiceUuid() != null) {
filterString += " ServiceUuid=" + filter.getServiceUuid();
}
if (filter.getServiceUuidMask() != null) {
filterString += " ServiceUuidMask=" + filter.getServiceUuidMask();
}
if (filter.getServiceSolicitationUuid() != null) {
filterString += " ServiceSolicitationUuid=" + filter.getServiceSolicitationUuid();
}
if (filter.getServiceSolicitationUuidMask() != null) {
filterString +=
" ServiceSolicitationUuidMask=" + filter.getServiceSolicitationUuidMask();
}
if (filter.getServiceDataUuid() != null) {
filterString += " ServiceDataUuid=" + Objects.toString(filter.getServiceDataUuid());
}
if (filter.getServiceData() != null) {
filterString += " ServiceData=" + Arrays.toString(filter.getServiceData());
}
if (filter.getServiceDataMask() != null) {
filterString += " ServiceDataMask=" + Arrays.toString(filter.getServiceDataMask());
}
if (filter.getManufacturerId() >= 0) {
filterString += " ManufacturerId=" + filter.getManufacturerId();
}
if (filter.getManufacturerData() != null) {
filterString += " ManufacturerData=" + Arrays.toString(filter.getManufacturerData());
}
if (filter.getManufacturerDataMask() != null) {
filterString +=
" ManufacturerDataMask=" + Arrays.toString(filter.getManufacturerDataMask());
}
filterString += " ]";
return filterString;
}
private static String scanModeToString(int scanMode) {
switch (scanMode) {
case ScanSettings.SCAN_MODE_OPPORTUNISTIC:
return "OPPORTUNISTIC";
case ScanSettings.SCAN_MODE_LOW_LATENCY:
return "LOW_LATENCY";
case ScanSettings.SCAN_MODE_BALANCED:
return "BALANCED";
case ScanSettings.SCAN_MODE_LOW_POWER:
return "LOW_POWER";
default:
return "UNKNOWN(" + scanMode + ")";
}
}
private static String callbackTypeToString(int callbackType) {
switch (callbackType) {
case ScanSettings.CALLBACK_TYPE_ALL_MATCHES:
return "ALL_MATCHES";
case ScanSettings.CALLBACK_TYPE_FIRST_MATCH:
return "FIRST_MATCH";
case ScanSettings.CALLBACK_TYPE_MATCH_LOST:
return "LOST";
default:
return callbackType == (ScanSettings.CALLBACK_TYPE_FIRST_MATCH
| ScanSettings.CALLBACK_TYPE_MATCH_LOST) ? "[FIRST_MATCH | LOST]" : "UNKNOWN: "
+ callbackType;
}
}
synchronized void dumpToString(StringBuilder sb) {
long currentTime = System.currentTimeMillis();
long currTime = SystemClock.elapsedRealtime();
long Score = 0;
long scanDuration = 0;
long suspendDuration = 0;
long activeDuration = 0;
long totalActiveTime = mTotalActiveTime;
long totalSuspendTime = mTotalSuspendTime;
long totalScanTime = mTotalScanTime;
long oppScanTime = mOppScanTime;
long lowPowerScanTime = mLowPowerScanTime;
long balancedScanTime = mBalancedScanTime;
long lowLatencyScanTime = mLowLantencyScanTime;
int oppScan = mOppScan;
int lowPowerScan = mLowPowerScan;
int balancedScan = mBalancedScan;
int lowLatencyScan = mLowLantencyScan;
if (!mOngoingScans.isEmpty()) {
for (Integer key : mOngoingScans.keySet()) {
LastScan scan = mOngoingScans.get(key);
scanDuration = currTime - scan.timestamp;
if (scan.isSuspended) {
suspendDuration = currTime - scan.suspendStartTime;
totalSuspendTime += suspendDuration;
}
totalScanTime += scanDuration;
totalSuspendTime += suspendDuration;
activeDuration = scanDuration - scan.suspendDuration - suspendDuration;
totalActiveTime += activeDuration;
switch (scan.scanMode) {
case ScanSettings.SCAN_MODE_OPPORTUNISTIC:
oppScanTime += activeDuration;
break;
case ScanSettings.SCAN_MODE_LOW_POWER:
lowPowerScanTime += activeDuration;
break;
case ScanSettings.SCAN_MODE_BALANCED:
balancedScanTime += activeDuration;
break;
case ScanSettings.SCAN_MODE_LOW_LATENCY:
lowLatencyScanTime += activeDuration;
break;
}
}
}
Score = (oppScanTime * OPPORTUNISTIC_WEIGHT + lowPowerScanTime * LOW_POWER_WEIGHT
+ balancedScanTime * BALANCED_WEIGHT + lowLatencyScanTime * LOW_LATENCY_WEIGHT) / 100;
sb.append(" " + appName);
if (isRegistered) {
sb.append(" (Registered)");
}
sb.append("\n LE scans (started/stopped) : "
+ mScansStarted + " / " + mScansStopped);
sb.append("\n Scan time in ms (active/suspend/total) : "
+ totalActiveTime + " / " + totalSuspendTime + " / " + totalScanTime);
sb.append("\n Scan time with mode in ms (Opp/LowPower/Balanced/LowLatency): "
+ oppScanTime + " / " + lowPowerScanTime + " / " + balancedScanTime + " / "
+ lowLatencyScanTime);
sb.append("\n Scan mode counter (Opp/LowPower/Balanced/LowLatency) : " + oppScan
+ " / " + lowPowerScan + " / " + balancedScan + " / " + lowLatencyScan);
sb.append("\n Score : " + Score);
sb.append("\n Total number of results : " + results);
if (!mLastScans.isEmpty()) {
sb.append("\n Last " + mLastScans.size()
+ " scans :");
for (int i = 0; i < mLastScans.size(); i++) {
LastScan scan = mLastScans.get(i);
Date timestamp = new Date(currentTime - currTime + scan.timestamp);
sb.append("\n " + DATE_FORMAT.format(timestamp) + " - ");
sb.append(scan.duration + "ms ");
if (scan.isOpportunisticScan) {
sb.append("Opp ");
}
if (scan.isBackgroundScan) {
sb.append("Back ");
}
if (scan.isTimeout) {
sb.append("Forced ");
}
if (scan.isFilterScan) {
sb.append("Filter ");
}
sb.append(scan.results + " results");
sb.append(" (" + scan.scannerId + ") ");
if (scan.isCallbackScan) {
sb.append("CB ");
} else {
sb.append("PI ");
}
if (scan.isBatchScan) {
sb.append("Batch Scan");
} else {
sb.append("Regular Scan");
}
if (scan.suspendDuration != 0) {
activeDuration = scan.duration - scan.suspendDuration;
sb.append("\n └ " + "Suspended Time: " + scan.suspendDuration
+ "ms, Active Time: " + activeDuration);
}
sb.append("\n └ " + "Scan Config: [ ScanMode="
+ scanModeToString(scan.scanMode) + ", callbackType="
+ callbackTypeToString(scan.scanCallbackType) + " ]");
if (scan.isFilterScan) {
sb.append(scan.filterString);
}
}
}
if (!mOngoingScans.isEmpty()) {
sb.append("\n Ongoing scans :");
for (Integer key : mOngoingScans.keySet()) {
LastScan scan = mOngoingScans.get(key);
Date timestamp = new Date(currentTime - currTime + scan.timestamp);
sb.append("\n " + DATE_FORMAT.format(timestamp) + " - ");
sb.append((currTime - scan.timestamp) + "ms ");
if (scan.isOpportunisticScan) {
sb.append("Opp ");
}
if (scan.isBackgroundScan) {
sb.append("Back ");
}
if (scan.isTimeout) {
sb.append("Forced ");
}
if (scan.isFilterScan) {
sb.append("Filter ");
}
if (scan.isSuspended) {
sb.append("Suspended ");
}
sb.append(scan.results + " results");
sb.append(" (" + scan.scannerId + ") ");
if (scan.isCallbackScan) {
sb.append("CB ");
} else {
sb.append("PI ");
}
if (scan.isBatchScan) {
sb.append("Batch Scan");
} else {
sb.append("Regular Scan");
}
if (scan.suspendStartTime != 0) {
long duration = scan.suspendDuration + (scan.isSuspended ? (currTime
- scan.suspendStartTime) : 0);
activeDuration = scan.duration - scan.suspendDuration;
sb.append("\n └ " + "Suspended Time:" + scan.suspendDuration
+ "ms, Active Time:" + activeDuration);
}
sb.append("\n └ " + "Scan Config: [ ScanMode="
+ scanModeToString(scan.scanMode) + ", callbackType="
+ callbackTypeToString(scan.scanCallbackType) + " ]");
if (scan.isFilterScan) {
sb.append(scan.filterString);
}
}
}
ContextMap.App appEntry = mContextMap.getByName(appName);
if (appEntry != null && isRegistered) {
sb.append("\n Application ID : " + appEntry.id);
sb.append("\n UUID : " + appEntry.uuid);
List<ContextMap.Connection> connections = mContextMap.getConnectionByApp(appEntry.id);
sb.append("\n Connections: " + connections.size());
Iterator<ContextMap.Connection> ii = connections.iterator();
while (ii.hasNext()) {
ContextMap.Connection connection = ii.next();
long connectionTime = currTime - connection.startTime;
Date timestamp = new Date(currentTime - currTime + connection.startTime);
sb.append("\n " + DATE_FORMAT.format(timestamp) + " - ");
sb.append((connectionTime) + "ms ");
sb.append(": " + connection.address + " (" + connection.connId + ")");
}
}
sb.append("\n\n");
}
}