long duration;
long timestamp;
boolean opportunistic;
+ boolean timeout;
boolean background;
int results;
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;
+
String appName;
int scansStarted = 0;
int scansStopped = 0;
gattService.addScanEvent(scanEvent);
}
+ synchronized void setScanTimeout() {
+ if (!isScanning)
+ return;
+
+ if (!lastScans.isEmpty()) {
+ LastScan curr = lastScans.get(lastScans.size() - 1);
+ curr.timeout = true;
+ }
+ }
+
+ synchronized boolean isScanningTooFrequently() {
+ if (lastScans.size() < NUM_SCAN_DURATIONS_KEPT) {
+ return false;
+ }
+
+ return (System.currentTimeMillis() - lastScans.get(0).timestamp) <
+ EXCESSIVE_SCANNING_PERIOD_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
if (isRegistered) sb.append(" (Registered)");
if (lastScan.opportunistic) sb.append(" (Opportunistic)");
if (lastScan.background) sb.append(" (Background)");
+ if (lastScan.timeout) sb.append(" (Forced-Opportunistic)");
sb.append("\n");
sb.append(" LE scans (started/stopped) : " +
sb.append(scan.duration + "ms ");
if (scan.opportunistic) sb.append("Opp ");
if (scan.background) sb.append("Back ");
+ if (scan.timeout) sb.append("Forced ");
sb.append(scan.results + " results");
sb.append("\n");
}
import java.util.UUID;
import java.util.concurrent.TimeUnit;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
/**
* Provides Bluetooth Gatt profile, as a service in
* the Bluetooth application.
} else {
app = mClientMap.getAppScanStatsById(appIf);
}
- if (app != null) app.recordScanStart(settings);
+
+ if (app != null) {
+ if (app.isScanningTooFrequently() &&
+ checkCallingOrSelfPermission(BLUETOOTH_PRIVILEGED) != PERMISSION_GRANTED) {
+ Log.e(TAG, "App '" + app.appName + "' is scanning too frequently");
+ return;
+ }
+ scanClient.stats = app;
+ app.recordScanStart(settings);
+ }
mScanManager.startScan(scanClient);
}
private static final int MSG_START_BLE_SCAN = 0;
private static final int MSG_STOP_BLE_SCAN = 1;
private static final int MSG_FLUSH_BATCH_RESULTS = 2;
+ private static final int MSG_SCAN_TIMEOUT = 3;
+
+ // Maximum msec before scan gets downgraded to opportunistic
+ private static final int SCAN_TIMEOUT_MS = 5 * 60 * 1000;
private static final String ACTION_REFRESH_BATCHED_SCAN =
"com.android.bluetooth.gatt.REFRESH_BATCHED_SCAN";
case MSG_FLUSH_BATCH_RESULTS:
handleFlushBatchResults(client);
break;
+ case MSG_SCAN_TIMEOUT:
+ mScanNative.regularScanTimeout();
+ break;
default:
// Shouldn't happen.
Log.e(TAG, "received an unkown message : " + msg.what);
mScanNative.startRegularScan(client);
if (!mScanNative.isOpportunisticScanClient(client)) {
mScanNative.configureRegularScanParams();
+
+ if (!mScanNative.isFirstMatchScanClient(client)) {
+ Message msg = mHandler.obtainMessage(MSG_SCAN_TIMEOUT);
+ msg.obj = client;
+ // Only one timeout message should exist at any time
+ mHandler.removeMessages(SCAN_TIMEOUT_MS);
+ mHandler.sendMessageDelayed(msg, SCAN_TIMEOUT_MS);
+ }
}
}
}
if (client == null) return;
if (mRegularScanClients.contains(client)) {
mScanNative.stopRegularScan(client);
+
+ if (mScanNative.numRegularScanClients() == 0) {
+ mHandler.removeMessages(MSG_SCAN_TIMEOUT);
+ }
+
if (!mScanNative.isOpportunisticScanClient(client)) {
mScanNative.configureRegularScanParams();
}
return client.settings.getScanMode() == ScanSettings.SCAN_MODE_OPPORTUNISTIC;
}
+ private boolean isFirstMatchScanClient(ScanClient client) {
+ return (client.settings.getCallbackType() & ScanSettings.CALLBACK_TYPE_FIRST_MATCH) != 0;
+ }
+
private void resetBatchScan(ScanClient client) {
int clientIf = client.clientIf;
BatchScanParams batchScanParams = getBatchScanParams();
removeScanFilters(client.clientIf);
}
+ void regularScanTimeout() {
+ for (ScanClient client : mRegularScanClients) {
+ if (!isOpportunisticScanClient(client) && !isFirstMatchScanClient(client)) {
+ logd("clientIf set to scan opportunisticly: " + client.clientIf);
+ setOpportunisticScanClient(client);
+ client.stats.setScanTimeout();
+ }
+ }
+
+ // The scan should continue for background scans
+ configureRegularScanParams();
+ if (numRegularScanClients() == 0) {
+ logd("stop scan");
+ gattClientScanNative(false);
+ }
+ }
+
+ void setOpportunisticScanClient(ScanClient client) {
+ // TODO: Add constructor to ScanSettings.Builder
+ // that can copy values from an existing ScanSettings object
+ ScanSettings.Builder builder = new ScanSettings.Builder();
+ ScanSettings settings = client.settings;
+ builder.setScanMode(ScanSettings.SCAN_MODE_OPPORTUNISTIC);
+ builder.setCallbackType(settings.getCallbackType());
+ builder.setScanResultType(settings.getScanResultType());
+ builder.setReportDelay(settings.getReportDelayMillis());
+ builder.setNumOfMatches(settings.getNumOfMatches());
+ client.settings = builder.build();
+ }
+
// Find the scan client information
ScanClient getClient(int clientIf) {
for (ScanClient client : mRegularScanClients) {