summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--android/app/jni/com_android_bluetooth_gatt.cpp4
-rw-r--r--android/app/src/com/android/bluetooth/gatt/CallbackInfo.java7
-rw-r--r--android/app/src/com/android/bluetooth/gatt/ContextMap.java99
-rw-r--r--android/app/src/com/android/bluetooth/gatt/GattNativeInterface.java64
-rw-r--r--android/app/src/com/android/bluetooth/gatt/GattObjectsFactory.java10
-rw-r--r--android/app/src/com/android/bluetooth/gatt/GattService.java1445
-rw-r--r--android/app/src/com/android/bluetooth/le_scan/AdvtFilterOnFoundOnLostInfo.java (renamed from android/app/src/com/android/bluetooth/gatt/AdvtFilterOnFoundOnLostInfo.java)2
-rw-r--r--android/app/src/com/android/bluetooth/le_scan/AppScanStats.java21
-rw-r--r--android/app/src/com/android/bluetooth/le_scan/ScanManager.java56
-rw-r--r--android/app/src/com/android/bluetooth/le_scan/TransitionalScanHelper.java1618
-rw-r--r--android/app/tests/unit/src/com/android/bluetooth/gatt/ContextMapTest.java6
-rw-r--r--android/app/tests/unit/src/com/android/bluetooth/gatt/GattServiceBinderTest.java29
-rw-r--r--android/app/tests/unit/src/com/android/bluetooth/gatt/GattServiceTest.java240
-rw-r--r--android/app/tests/unit/src/com/android/bluetooth/le_scan/AdvtFilterOnFoundOnLostInfoTest.java (renamed from android/app/tests/unit/src/com/android/bluetooth/gatt/AdvtFilterOnFoundOnLostInfoTest.java)2
-rw-r--r--android/app/tests/unit/src/com/android/bluetooth/le_scan/AppScanStatsTest.java14
-rw-r--r--android/app/tests/unit/src/com/android/bluetooth/le_scan/ScanManagerTest.java5
-rw-r--r--android/app/tests/unit/src/com/android/bluetooth/le_scan/TransitionalScanHelperTest.java400
17 files changed, 2252 insertions, 1770 deletions
diff --git a/android/app/jni/com_android_bluetooth_gatt.cpp b/android/app/jni/com_android_bluetooth_gatt.cpp
index aaf908a798..95a90f3c58 100644
--- a/android/app/jni/com_android_bluetooth_gatt.cpp
+++ b/android/app/jni/com_android_bluetooth_gatt.cpp
@@ -2827,10 +2827,10 @@ static int register_com_android_bluetooth_gatt_(JNIEnv* env) {
&method_onBatchScanThresholdCrossed},
{"createOnTrackAdvFoundLostObject",
"(II[BI[BIIILjava/lang/String;IIII)"
- "Lcom/android/bluetooth/gatt/AdvtFilterOnFoundOnLostInfo;",
+ "Lcom/android/bluetooth/le_scan/AdvtFilterOnFoundOnLostInfo;",
&method_createOnTrackAdvFoundLostObject},
{"onTrackAdvFoundLost",
- "(Lcom/android/bluetooth/gatt/AdvtFilterOnFoundOnLostInfo;)V",
+ "(Lcom/android/bluetooth/le_scan/AdvtFilterOnFoundOnLostInfo;)V",
&method_onTrackAdvFoundLost},
{"onScanParamSetupCompleted", "(II)V", &method_onScanParamSetupCompleted},
{"getSampleGattDbElement", "()Lcom/android/bluetooth/gatt/GattDbElement;",
diff --git a/android/app/src/com/android/bluetooth/gatt/CallbackInfo.java b/android/app/src/com/android/bluetooth/gatt/CallbackInfo.java
index ab3fe4ea4b..be01605b24 100644
--- a/android/app/src/com/android/bluetooth/gatt/CallbackInfo.java
+++ b/android/app/src/com/android/bluetooth/gatt/CallbackInfo.java
@@ -16,11 +16,12 @@
package com.android.bluetooth.gatt;
/**
- * Helper class that keeps track of callback parameters for app callbacks.
- * These are held during congestion and reported when congestion clears.
+ * Helper class that keeps track of callback parameters for app callbacks. These are held during
+ * congestion and reported when congestion clears.
+ *
* @hide
*/
-/* package */ class CallbackInfo {
+public class CallbackInfo {
public String address;
public int status;
public int handle;
diff --git a/android/app/src/com/android/bluetooth/gatt/ContextMap.java b/android/app/src/com/android/bluetooth/gatt/ContextMap.java
index 3457ed7e59..7492f8a139 100644
--- a/android/app/src/com/android/bluetooth/gatt/ContextMap.java
+++ b/android/app/src/com/android/bluetooth/gatt/ContextMap.java
@@ -18,6 +18,7 @@ package com.android.bluetooth.gatt;
import android.bluetooth.le.AdvertiseData;
import android.bluetooth.le.AdvertisingSetParameters;
import android.bluetooth.le.PeriodicAdvertisingParameters;
+import android.content.Context;
import android.os.Binder;
import android.os.IBinder;
import android.os.IInterface;
@@ -27,10 +28,11 @@ import android.os.UserHandle;
import android.os.WorkSource;
import android.util.Log;
-import androidx.annotation.VisibleForTesting;
import com.android.bluetooth.BluetoothMethodProxy;
import com.android.bluetooth.le_scan.AppScanStats;
+import com.android.bluetooth.le_scan.TransitionalScanHelper;
+import com.android.bluetooth.le_scan.TransitionalScanHelper.PendingIntentInfo;
import com.android.internal.annotations.GuardedBy;
import com.google.common.collect.EvictingQueue;
@@ -98,27 +100,27 @@ public class ContextMap<C, T> {
public Boolean isCongested = false;
/** Whether the calling app has location permission */
- boolean hasLocationPermission;
+ public boolean hasLocationPermission;
/** Whether the calling app has bluetooth privileged permission */
- boolean hasBluetoothPrivilegedPermission;
+ public boolean hasBluetoothPrivilegedPermission;
/** The user handle of the app that started the scan */
- UserHandle mUserHandle;
+ public UserHandle mUserHandle;
/** Whether the calling app has the network settings permission */
- boolean mHasNetworkSettingsPermission;
+ public boolean mHasNetworkSettingsPermission;
/** Whether the calling app has the network setup wizard permission */
- boolean mHasNetworkSetupWizardPermission;
+ public boolean mHasNetworkSetupWizardPermission;
/** Whether the calling app has the network setup wizard permission */
- boolean mHasScanWithoutLocationPermission;
+ public boolean mHasScanWithoutLocationPermission;
/** Whether the calling app has disavowed the use of bluetooth for location */
- boolean mHasDisavowedLocation;
+ public boolean mHasDisavowedLocation;
- boolean mEligibleForSanitizedExposureNotification;
+ public boolean mEligibleForSanitizedExposureNotification;
public List<String> mAssociatedDevices;
@@ -145,10 +147,8 @@ public class ContextMap<C, T> {
this.name = name;
}
- /**
- * Link death recipient
- */
- void linkToDeath(IBinder.DeathRecipient deathRecipient) {
+ /** Link death recipient */
+ public void linkToDeath(IBinder.DeathRecipient deathRecipient) {
// It might not be a binder object
if (callback == null) {
return;
@@ -162,10 +162,8 @@ public class ContextMap<C, T> {
}
}
- /**
- * Unlink death recipient
- */
- void unlinkToDeath() {
+ /** Unlink death recipient */
+ public void unlinkToDeath() {
if (mDeathRecipient != null) {
try {
IBinder binder = ((IInterface) callback).asBinder();
@@ -176,11 +174,11 @@ public class ContextMap<C, T> {
}
}
- void queueCallback(CallbackInfo callbackInfo) {
+ public void queueCallback(CallbackInfo callbackInfo) {
mCongestionQueue.add(callbackInfo);
}
- CallbackInfo popQueuedCallback() {
+ public CallbackInfo popQueuedCallback() {
if (mCongestionQueue.size() == 0) {
return null;
}
@@ -210,12 +208,13 @@ public class ContextMap<C, T> {
private final Object mConnectionsLock = new Object();
/** Add an entry to the application context list. */
- protected App add(
+ public App add(
UUID uuid,
WorkSource workSource,
C callback,
- GattService.PendingIntentInfo piInfo,
- GattService service) {
+ PendingIntentInfo piInfo,
+ Context context,
+ TransitionalScanHelper scanHelper) {
int appUid;
String appName = null;
if (piInfo != null) {
@@ -223,16 +222,18 @@ public class ContextMap<C, T> {
appName = piInfo.callingPackage;
} else {
appUid = Binder.getCallingUid();
- appName = service.getPackageManager().getNameForUid(appUid);
+ appName = context.getPackageManager().getNameForUid(appUid);
}
if (appName == null) {
// Assign an app name if one isn't found
appName = "Unknown App (UID: " + appUid + ")";
}
synchronized (mAppsLock) {
+ // TODO(b/327849650): AppScanStats appears to be only needed for the ScannerMap.
+ // Consider refactoring this.
AppScanStats appScanStats = mAppScanStats.get(appUid);
if (appScanStats == null) {
- appScanStats = new AppScanStats(appName, workSource, this, service);
+ appScanStats = new AppScanStats(appName, workSource, this, context, scanHelper);
mAppScanStats.put(appUid, appScanStats);
}
App app = new App(uuid, callback, (T) piInfo, appName, appScanStats);
@@ -242,10 +243,8 @@ public class ContextMap<C, T> {
}
}
- /**
- * Add an entry to the application context list for advertiser.
- */
- App add(int id, C callback, GattService service) {
+ /** Add an entry to the application context list for advertiser. */
+ public App add(int id, C callback, GattService service) {
int appUid = Binder.getCallingUid();
String appName = service.getPackageManager().getNameForUid(appUid);
if (appName == null) {
@@ -270,10 +269,8 @@ public class ContextMap<C, T> {
}
}
- /**
- * Remove the context for a given UUID
- */
- void remove(UUID uuid) {
+ /** Remove the context for a given UUID */
+ public void remove(UUID uuid) {
synchronized (mAppsLock) {
Iterator<App> i = mApps.iterator();
while (i.hasNext()) {
@@ -288,10 +285,8 @@ public class ContextMap<C, T> {
}
}
- /**
- * Remove the context for a given application ID.
- */
- protected void remove(int id) {
+ /** Remove the context for a given application ID. */
+ public void remove(int id) {
boolean find = false;
synchronized (mAppsLock) {
Iterator<App> i = mApps.iterator();
@@ -311,7 +306,7 @@ public class ContextMap<C, T> {
}
}
- protected List<Integer> getAllAppsIds() {
+ public List<Integer> getAllAppsIds() {
List<Integer> appIds = new ArrayList();
synchronized (mAppsLock) {
Iterator<App> i = mApps.iterator();
@@ -366,10 +361,8 @@ public class ContextMap<C, T> {
}
}
- /**
- * Get an application context by ID.
- */
- protected App getById(int id) {
+ /** Get an application context by ID. */
+ public App getById(int id) {
synchronized (mAppsLock) {
Iterator<App> i = mApps.iterator();
while (i.hasNext()) {
@@ -383,10 +376,8 @@ public class ContextMap<C, T> {
return null;
}
- /**
- * Get an application context by UUID.
- */
- protected App getByUuid(UUID uuid) {
+ /** Get an application context by UUID. */
+ public App getByUuid(UUID uuid) {
synchronized (mAppsLock) {
Iterator<App> i = mApps.iterator();
while (i.hasNext()) {
@@ -417,10 +408,8 @@ public class ContextMap<C, T> {
return null;
}
- /**
- * Get an application context by the context info object.
- */
- protected App getByContextInfo(T contextInfo) {
+ /** Get an application context by the context info object. */
+ public App getByContextInfo(T contextInfo) {
synchronized (mAppsLock) {
Iterator<App> i = mApps.iterator();
while (i.hasNext()) {
@@ -434,10 +423,8 @@ public class ContextMap<C, T> {
return null;
}
- /**
- * Get Logging info by ID
- */
- protected AppScanStats getAppScanStatsById(int id) {
+ /** Get Logging info by ID */
+ public AppScanStats getAppScanStatsById(int id) {
App temp = getById(id);
if (temp != null) {
return temp.appScanStats;
@@ -670,10 +657,8 @@ public class ContextMap<C, T> {
return currentConnections;
}
- /**
- * Erases all application context entries.
- */
- protected void clear() {
+ /** Erases all application context entries. */
+ public void clear() {
synchronized (mAppsLock) {
Iterator<App> i = mApps.iterator();
while (i.hasNext()) {
diff --git a/android/app/src/com/android/bluetooth/gatt/GattNativeInterface.java b/android/app/src/com/android/bluetooth/gatt/GattNativeInterface.java
index 9c8fa539d5..1167bea931 100644
--- a/android/app/src/com/android/bluetooth/gatt/GattNativeInterface.java
+++ b/android/app/src/com/android/bluetooth/gatt/GattNativeInterface.java
@@ -18,6 +18,8 @@ package com.android.bluetooth.gatt;
import android.os.RemoteException;
+import com.android.bluetooth.le_scan.AdvtFilterOnFoundOnLostInfo;
+import com.android.bluetooth.le_scan.TransitionalScanHelper;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
@@ -70,13 +72,24 @@ public class GattNativeInterface {
void onScanResult(int eventType, int addressType, String address, int primaryPhy,
int secondaryPhy, int advertisingSid, int txPower, int rssi, int periodicAdvInt,
byte[] advData, String originalAddress) {
- getGattService().onScanResult(eventType, addressType, address, primaryPhy, secondaryPhy,
- advertisingSid, txPower, rssi, periodicAdvInt, advData, originalAddress);
+ getTransitionalScanHelper()
+ .onScanResult(
+ eventType,
+ addressType,
+ address,
+ primaryPhy,
+ secondaryPhy,
+ advertisingSid,
+ txPower,
+ rssi,
+ periodicAdvInt,
+ advData,
+ originalAddress);
}
void onScannerRegistered(int status, int scannerId, long uuidLsb, long uuidMsb)
throws RemoteException {
- getGattService().onScannerRegistered(status, scannerId, uuidLsb, uuidMsb);
+ getTransitionalScanHelper().onScannerRegistered(status, scannerId, uuidLsb, uuidMsb);
}
void onClientRegistered(int status, int clientIf, long uuidLsb, long uuidMsb)
@@ -187,50 +200,65 @@ public class GattNativeInterface {
}
void onScanFilterEnableDisabled(int action, int status, int clientIf) {
- getGattService().onScanFilterEnableDisabled(action, status, clientIf);
+ getTransitionalScanHelper().onScanFilterEnableDisabled(action, status, clientIf);
}
void onScanFilterParamsConfigured(int action, int status, int clientIf, int availableSpace) {
- getGattService().onScanFilterParamsConfigured(action, status, clientIf, availableSpace);
+ getTransitionalScanHelper()
+ .onScanFilterParamsConfigured(action, status, clientIf, availableSpace);
}
void onScanFilterConfig(int action, int status, int clientIf, int filterType,
int availableSpace) {
- getGattService().onScanFilterConfig(action, status, clientIf, filterType, availableSpace);
+ getTransitionalScanHelper()
+ .onScanFilterConfig(action, status, clientIf, filterType, availableSpace);
}
void onBatchScanStorageConfigured(int status, int clientIf) {
- getGattService().onBatchScanStorageConfigured(status, clientIf);
+ getTransitionalScanHelper().onBatchScanStorageConfigured(status, clientIf);
}
void onBatchScanStartStopped(int startStopAction, int status, int clientIf) {
- getGattService().onBatchScanStartStopped(startStopAction, status, clientIf);
+ getTransitionalScanHelper().onBatchScanStartStopped(startStopAction, status, clientIf);
}
void onBatchScanReports(int status, int scannerId, int reportType, int numRecords,
byte[] recordData) throws RemoteException {
- getGattService().onBatchScanReports(status, scannerId, reportType, numRecords, recordData);
+ getTransitionalScanHelper()
+ .onBatchScanReports(status, scannerId, reportType, numRecords, recordData);
}
void onBatchScanThresholdCrossed(int clientIf) {
- getGattService().onBatchScanThresholdCrossed(clientIf);
+ getTransitionalScanHelper().onBatchScanThresholdCrossed(clientIf);
}
AdvtFilterOnFoundOnLostInfo createOnTrackAdvFoundLostObject(int clientIf, int advPktLen,
byte[] advPkt, int scanRspLen, byte[] scanRsp, int filtIndex, int advState,
int advInfoPresent, String address, int addrType, int txPower, int rssiValue,
int timeStamp) {
- return getGattService().createOnTrackAdvFoundLostObject(clientIf, advPktLen, advPkt,
- scanRspLen, scanRsp, filtIndex, advState, advInfoPresent, address, addrType,
- txPower, rssiValue, timeStamp);
+ return getTransitionalScanHelper()
+ .createOnTrackAdvFoundLostObject(
+ clientIf,
+ advPktLen,
+ advPkt,
+ scanRspLen,
+ scanRsp,
+ filtIndex,
+ advState,
+ advInfoPresent,
+ address,
+ addrType,
+ txPower,
+ rssiValue,
+ timeStamp);
}
void onTrackAdvFoundLost(AdvtFilterOnFoundOnLostInfo trackingInfo) throws RemoteException {
- getGattService().onTrackAdvFoundLost(trackingInfo);
+ getTransitionalScanHelper().onTrackAdvFoundLost(trackingInfo);
}
void onScanParamSetupCompleted(int status, int scannerId) throws RemoteException {
- getGattService().onScanParamSetupCompleted(status, scannerId);
+ getTransitionalScanHelper().onScanParamSetupCompleted(status, scannerId);
}
void onConfigureMTU(int connId, int status, int mtu) throws RemoteException {
@@ -665,5 +693,11 @@ public class GattNativeInterface {
int p1, int p2, int p3, int p4, int p5) {
gattTestNative(command, uuid1Lsb, uuid1Msb, bda1, p1, p2, p3, p4, p5);
}
+
+ // TODO(b/327849650): Callbacks that reference this helper should be moved into the appropriate
+ // native interface (ScanNativeInterface, PeriodicScanNativeInterface, etc.).
+ private TransitionalScanHelper getTransitionalScanHelper() {
+ return mGattService.getTransitionalScanHelper();
+ }
}
diff --git a/android/app/src/com/android/bluetooth/gatt/GattObjectsFactory.java b/android/app/src/com/android/bluetooth/gatt/GattObjectsFactory.java
index 3cc02b851b..0a02c2d142 100644
--- a/android/app/src/com/android/bluetooth/gatt/GattObjectsFactory.java
+++ b/android/app/src/com/android/bluetooth/gatt/GattObjectsFactory.java
@@ -16,6 +16,7 @@
package com.android.bluetooth.gatt;
+import android.content.Context;
import android.os.Looper;
import android.util.Log;
@@ -25,6 +26,7 @@ import com.android.bluetooth.btservice.BluetoothAdapterProxy;
import com.android.bluetooth.le_scan.PeriodicScanManager;
import com.android.bluetooth.le_scan.ScanManager;
import com.android.bluetooth.le_scan.ScanNativeInterface;
+import com.android.bluetooth.le_scan.TransitionalScanHelper;
/**
* Factory class for object initialization to help with unit testing
@@ -75,18 +77,20 @@ public class GattObjectsFactory {
/**
* Create an instance of ScanManager
*
- * @param service a GattService instance
+ * @param context a Context instance
+ * @param scanHelper a TransitionalScanHelper instance
* @param adapterService an AdapterService instance
* @param bluetoothAdapterProxy a bluetoothAdapterProxy instance
* @param looper the looper to be used for processing messages
* @return the created ScanManager instance
*/
public ScanManager createScanManager(
- GattService service,
+ Context context,
+ TransitionalScanHelper scanHelper,
AdapterService adapterService,
BluetoothAdapterProxy bluetoothAdapterProxy,
Looper looper) {
- return new ScanManager(service, adapterService, bluetoothAdapterProxy, looper);
+ return new ScanManager(context, scanHelper, adapterService, bluetoothAdapterProxy, looper);
}
public PeriodicScanManager createPeriodicScanManager(AdapterService adapterService) {
diff --git a/android/app/src/com/android/bluetooth/gatt/GattService.java b/android/app/src/com/android/bluetooth/gatt/GattService.java
index 4f4dab7325..690c841bdd 100644
--- a/android/app/src/com/android/bluetooth/gatt/GattService.java
+++ b/android/app/src/com/android/bluetooth/gatt/GattService.java
@@ -25,7 +25,6 @@ import static com.android.bluetooth.Utils.enforceBluetoothPrivilegedPermission;
import android.annotation.RequiresPermission;
import android.annotation.SuppressLint;
import android.app.ActivityManager;
-import android.app.AppOpsManager;
import android.app.PendingIntent;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
@@ -42,7 +41,6 @@ import android.bluetooth.IBluetoothGattCallback;
import android.bluetooth.IBluetoothGattServerCallback;
import android.bluetooth.le.AdvertiseData;
import android.bluetooth.le.AdvertisingSetParameters;
-import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ChannelSoundingParams;
import android.bluetooth.le.DistanceMeasurementMethod;
import android.bluetooth.le.DistanceMeasurementParams;
@@ -51,22 +49,15 @@ import android.bluetooth.le.IDistanceMeasurementCallback;
import android.bluetooth.le.IPeriodicAdvertisingCallback;
import android.bluetooth.le.IScannerCallback;
import android.bluetooth.le.PeriodicAdvertisingParameters;
-import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanFilter;
-import android.bluetooth.le.ScanRecord;
import android.bluetooth.le.ScanResult;
import android.bluetooth.le.ScanSettings;
-import android.companion.AssociationInfo;
-import android.companion.CompanionDeviceManager;
import android.content.AttributionSource;
import android.content.Context;
-import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.PackageManager.PackageInfoFlags;
import android.content.res.Resources;
-import android.net.MacAddress;
-import android.os.Binder;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
@@ -74,10 +65,7 @@ import android.os.IBinder;
import android.os.Message;
import android.os.ParcelUuid;
import android.os.RemoteException;
-import android.os.SystemClock;
-import android.os.UserHandle;
import android.os.WorkSource;
-import android.provider.DeviceConfig;
import android.provider.Settings;
import android.sysprop.BluetoothProperties;
import android.text.format.DateUtils;
@@ -94,29 +82,20 @@ import com.android.bluetooth.btservice.CompanionManager;
import com.android.bluetooth.btservice.MetricsLogger;
import com.android.bluetooth.btservice.ProfileService;
import com.android.bluetooth.flags.Flags;
-import com.android.bluetooth.le_scan.AppScanStats;
-import com.android.bluetooth.le_scan.PeriodicScanManager;
-import com.android.bluetooth.le_scan.ScanClient;
-import com.android.bluetooth.le_scan.ScanManager;
import com.android.bluetooth.le_scan.TransitionalScanHelper;
-import com.android.bluetooth.util.NumberUtils;
import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.utils.SynchronousResultReceiver;
import libcore.util.HexEncoding;
-import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
-import java.util.concurrent.TimeUnit;
-import java.util.function.Predicate;
/**
* Provides Bluetooth Gatt profile, as a service in
@@ -130,47 +109,7 @@ public class GattService extends ProfileService {
private static final String UUID_SUFFIX = "-0000-1000-8000-00805f9b34fb";
private static final String UUID_ZERO_PAD = "00000000";
- static final int SCAN_FILTER_ENABLED = 1;
- static final int SCAN_FILTER_MODIFIED = 2;
-
private static final int MAC_ADDRESS_LENGTH = 6;
- // Batch scan related constants.
- private static final int TRUNCATED_RESULT_SIZE = 11;
- private static final int TIME_STAMP_LENGTH = 2;
-
- private enum MatchOrigin {
- PSEUDO_ADDRESS,
- ORIGINAL_ADDRESS
- }
-
- private static class MatchResult {
- private final boolean mMatches;
- private final MatchOrigin mOrigin;
- private MatchResult(boolean matches, MatchOrigin origin) {
- this.mMatches = matches;
- this.mOrigin = origin;
- }
-
- public boolean getMatches() {
- return mMatches;
- }
-
- public MatchOrigin getMatchOrigin() {
- return mOrigin;
- }
- }
-
- /**
- * The default floor value for LE batch scan report delays greater than 0
- */
- @VisibleForTesting
- static final long DEFAULT_REPORT_DELAY_FLOOR = 5000;
-
- // onFoundLost related constants
- private static final int ADVT_STATE_ONFOUND = 0;
- private static final int ADVT_STATE_ONLOST = 1;
-
- private static final int ET_LEGACY_MASK = 0x10;
private static final UUID HID_SERVICE_UUID =
UUID.fromString("00001812-0000-1000-8000-00805F9B34FB");
@@ -209,34 +148,8 @@ public class GattService extends ProfileService {
"0201061AFF4C000215426C7565436861726D426561636F6E730EFE1355C509168020691E0EFE13551109426C7565436861726D5F31363936383500000000",
};
- /**
- * Keep the arguments passed in for the PendingIntent.
- */
- public static class PendingIntentInfo {
- public PendingIntent intent;
- public ScanSettings settings;
- public List<ScanFilter> filters;
- public String callingPackage;
- public int callingUid;
-
- @Override
- public boolean equals(Object other) {
- if (!(other instanceof PendingIntentInfo)) {
- return false;
- }
- return intent.equals(((PendingIntentInfo) other).intent);
- }
- }
-
- private final PendingIntent.CancelListener mScanIntentCancelListener =
- new PendingIntent.CancelListener(){
- public void onCanceled(PendingIntent intent) {
- Log.d(TAG, "scanning PendingIntent canceled");
- stopScan(intent, getAttributionSource());
- }
- };
-
- public final TransitionalScanHelper mTransitionalScanHelper = new TransitionalScanHelper();
+ public final TransitionalScanHelper mTransitionalScanHelper =
+ new TransitionalScanHelper(this, this::isTestModeEnabled);
/**
* List of our registered advertisers.
@@ -265,16 +178,6 @@ public class GattService extends ProfileService {
HandleMap mHandleMap = new HandleMap();
private List<UUID> mAdvertisingServiceUuids = new ArrayList<UUID>();
- private int mMaxScanFilters;
-
- private static final int NUM_SCAN_EVENTS_KEPT = 20;
-
- /**
- * Internal list of scan events to use with the proto
- */
- private final ArrayDeque<BluetoothMetricsProto.ScanEvent> mScanEvents =
- new ArrayDeque<>(NUM_SCAN_EVENTS_KEPT);
-
/**
* Set of restricted (which require a BLUETOOTH_PRIVILEGED permission) handles per connectionId.
*/
@@ -289,12 +192,7 @@ public class GattService extends ProfileService {
private AdapterService mAdapterService;
private BluetoothAdapterProxy mBluetoothAdapterProxy;
AdvertiseManager mAdvertiseManager;
- PeriodicScanManager mPeriodicScanManager;
DistanceMeasurementManager mDistanceMeasurementManager;
- ScanManager mScanManager;
- private AppOpsManager mAppOps;
- private CompanionDeviceManager mCompanionManager;
- private String mExposureNotificationPackage;
private Handler mTestModeHandler;
private ActivityManager mActivityManager;
private PackageManager mPackageManager;
@@ -309,23 +207,6 @@ public class GattService extends ProfileService {
}
/**
- */
- private final Predicate<ScanResult> mLocationDenylistPredicate = (scanResult) -> {
- final MacAddress parsedAddress = MacAddress
- .fromString(scanResult.getDevice().getAddress());
- if (mAdapterService.getLocationDenylistMac().test(parsedAddress.toByteArray())) {
- Log.v(TAG, "Skipping device matching denylist: " + scanResult.getDevice());
- return true;
- }
- final ScanRecord scanRecord = scanResult.getScanRecord();
- if (scanRecord.matchesAnyField(mAdapterService.getLocationDenylistAdvertisingData())) {
- Log.v(TAG, "Skipping data matching denylist: " + scanRecord);
- return true;
- }
- return false;
- };
-
- /**
* Reliable write queue
*/
@VisibleForTesting
@@ -343,7 +224,6 @@ public class GattService extends ProfileService {
if (DBG) {
Log.d(TAG, "start()");
}
- mExposureNotificationPackage = getString(R.string.exposure_notification_package);
Settings.Global.putInt(
getContentResolver(), "bluetooth_sanitized_exposure_notification_supported", 1);
@@ -351,8 +231,6 @@ public class GattService extends ProfileService {
mNativeInterface.init(this);
mAdapterService = AdapterService.getAdapterService();
mBluetoothAdapterProxy = BluetoothAdapterProxy.getInstance();
- mCompanionManager = getSystemService(CompanionDeviceManager.class);
- mAppOps = getSystemService(AppOpsManager.class);
mAdvertiseManager =
new AdvertiseManager(
this,
@@ -362,14 +240,7 @@ public class GattService extends ProfileService {
HandlerThread thread = new HandlerThread("BluetoothScanManager");
thread.start();
- mScanManager =
- GattObjectsFactory.getInstance()
- .createScanManager(
- this, mAdapterService, mBluetoothAdapterProxy, thread.getLooper());
-
- mPeriodicScanManager = GattObjectsFactory.getInstance()
- .createPeriodicScanManager(mAdapterService);
-
+ mTransitionalScanHelper.start(thread.getLooper());
mDistanceMeasurementManager = GattObjectsFactory.getInstance()
.createDistanceMeasurementManager(mAdapterService);
@@ -382,7 +253,7 @@ public class GattService extends ProfileService {
if (DBG) {
Log.d(TAG, "stop()");
}
- mTransitionalScanHelper.getScannerMap().clear();
+ mTransitionalScanHelper.stop();
mAdvertiserMap.clear();
mClientMap.clear();
if (Flags.gattCleanupRestrictedHandles()) {
@@ -406,15 +277,14 @@ public class GattService extends ProfileService {
if (mAdvertiseManager != null) {
mAdvertiseManager.cleanup();
}
- if (mScanManager != null) {
- mScanManager.cleanup();
- }
- if (mPeriodicScanManager != null) {
- mPeriodicScanManager.cleanup();
- }
if (mDistanceMeasurementManager != null) {
mDistanceMeasurementManager.cleanup();
}
+ mTransitionalScanHelper.cleanup();
+ }
+
+ TransitionalScanHelper getTransitionalScanHelper() {
+ return mTransitionalScanHelper;
}
// While test mode is enabled, pretend as if the underlying stack
@@ -423,21 +293,31 @@ public class GattService extends ProfileService {
protected void setTestModeEnabled(boolean enableTestMode) {
synchronized (mTestModeLock) {
if (mTestModeHandler == null) {
- mTestModeHandler = new Handler(getMainLooper()) {
- public void handleMessage(Message msg) {
- synchronized (mTestModeLock) {
- if (!GattService.this.isTestModeEnabled()) {
- return;
+ mTestModeHandler =
+ new Handler(getMainLooper()) {
+ public void handleMessage(Message msg) {
+ synchronized (mTestModeLock) {
+ if (!GattService.this.isTestModeEnabled()) {
+ return;
+ }
+ for (String test : TEST_MODE_BEACONS) {
+ mTransitionalScanHelper.onScanResultInternal(
+ 0x1b,
+ 0x1,
+ "DD:34:02:05:5C:4D",
+ 1,
+ 0,
+ 0xff,
+ 127,
+ -54,
+ 0x0,
+ HexEncoding.decode(test),
+ "DD:34:02:05:5C:4E");
+ }
+ sendEmptyMessageDelayed(0, DateUtils.SECOND_IN_MILLIS);
+ }
}
- for (String test : TEST_MODE_BEACONS) {
- onScanResultInternal(0x1b, 0x1, "DD:34:02:05:5C:4D", 1, 0, 0xff,
- 127, -54, 0x0, HexEncoding.decode(test),
- "DD:34:02:05:5C:4E");
- }
- sendEmptyMessageDelayed(0, DateUtils.SECOND_IN_MILLIS);
- }
- }
- };
+ };
}
if (enableTestMode && !isTestModeEnabled()) {
super.setTestModeEnabled(true);
@@ -451,15 +331,6 @@ public class GattService extends ProfileService {
}
}
- @VisibleForTesting
- ScanManager getScanManager() {
- if (mScanManager == null) {
- Log.w(TAG, "getScanManager(): scan manager is null");
- return null;
- }
- return mScanManager;
- }
-
// Suppressed because we are conditionally enforcing
@SuppressLint("AndroidFrameworkRequiresPermission")
private void permissionCheck(UUID characteristicUuid) {
@@ -495,63 +366,7 @@ public class GattService extends ProfileService {
/** Notify Scan manager of bluetooth profile connection state changes */
public void notifyProfileConnectionStateChange(int profile, int fromState, int toState) {
- if (mScanManager == null) {
- Log.w(TAG, "scan manager is null");
- return;
- }
- mScanManager.handleBluetoothProfileConnectionStateChanged(profile, fromState, toState);
- }
-
- /**
- * DeathReceipient handlers used to unregister applications that
- * disconnect ungracefully (ie. crash or forced close).
- */
-
- class ScannerDeathRecipient implements IBinder.DeathRecipient {
- int mScannerId;
- private String mPackageName;
-
- ScannerDeathRecipient(int scannerId, String packageName) {
- mScannerId = scannerId;
- mPackageName = packageName;
- }
-
- @Override
- public void binderDied() {
- if (DBG) {
- Log.d(
- TAG,
- "Binder is dead - unregistering scanner ("
- + mPackageName
- + " "
- + mScannerId
- + ")!");
- }
-
- ScanClient client = getScanClient(mScannerId);
- if (client != null) {
- if (Flags.leScanFixRemoteException()) {
- handleDeadScanClient(client);
- } else {
- client.appDied = true;
- stopScan(client.scannerId, getAttributionSource());
- }
- }
- }
-
- private ScanClient getScanClient(int clientIf) {
- for (ScanClient client : mScanManager.getRegularScanQueue()) {
- if (client.scannerId == clientIf) {
- return client;
- }
- }
- for (ScanClient client : mScanManager.getBatchScanQueue()) {
- if (client.scannerId == clientIf) {
- return client;
- }
- }
- return null;
- }
+ mTransitionalScanHelper.notifyProfileConnectionStateChange(profile, fromState, toState);
}
class ServerDeathRecipient implements IBinder.DeathRecipient {
@@ -700,7 +515,8 @@ public class GattService extends ProfileService {
if (service == null) {
return;
}
- service.registerScanner(callback, workSource, attributionSource);
+ service.getTransitionalScanHelper()
+ .registerScanner(callback, workSource, attributionSource);
}
@Override
@@ -718,7 +534,7 @@ public class GattService extends ProfileService {
if (service == null) {
return;
}
- service.unregisterScanner(scannerId, attributionSource);
+ service.getTransitionalScanHelper().unregisterScanner(scannerId, attributionSource);
}
@Override
@@ -738,7 +554,8 @@ public class GattService extends ProfileService {
if (service == null) {
return;
}
- service.startScan(scannerId, settings, filters, attributionSource);
+ service.getTransitionalScanHelper()
+ .startScan(scannerId, settings, filters, attributionSource);
}
@Override
@@ -761,7 +578,8 @@ public class GattService extends ProfileService {
if (service == null) {
return;
}
- service.registerPiAndStartScan(intent, settings, filters, attributionSource);
+ service.getTransitionalScanHelper()
+ .registerPiAndStartScan(intent, settings, filters, attributionSource);
}
@Override
@@ -780,7 +598,7 @@ public class GattService extends ProfileService {
if (service == null) {
return;
}
- service.stopScan(intent, attributionSource);
+ service.getTransitionalScanHelper().stopScan(intent, attributionSource);
}
@Override
@@ -798,7 +616,7 @@ public class GattService extends ProfileService {
if (service == null) {
return;
}
- service.stopScan(scannerId, attributionSource);
+ service.getTransitionalScanHelper().stopScan(scannerId, attributionSource);
}
@Override
@@ -816,7 +634,8 @@ public class GattService extends ProfileService {
if (service == null) {
return;
}
- service.flushPendingBatchResults(scannerId, attributionSource);
+ service.getTransitionalScanHelper()
+ .flushPendingBatchResults(scannerId, attributionSource);
}
@Override
@@ -1686,7 +1505,8 @@ public class GattService extends ProfileService {
if (service == null) {
return;
}
- service.registerSync(scanResult, skip, timeout, callback, attributionSource);
+ service.getTransitionalScanHelper()
+ .registerSync(scanResult, skip, timeout, callback, attributionSource);
}
@Override
@@ -1705,7 +1525,8 @@ public class GattService extends ProfileService {
if (service == null) {
return;
}
- service.transferSync(bda, serviceData , syncHandle, attributionSource);
+ service.getTransitionalScanHelper()
+ .transferSync(bda, serviceData, syncHandle, attributionSource);
}
@Override
@@ -1725,7 +1546,8 @@ public class GattService extends ProfileService {
if (service == null) {
return;
}
- service.transferSetInfo(bda, serviceData , advHandle, callback, attributionSource);
+ service.getTransitionalScanHelper()
+ .transferSetInfo(bda, serviceData, advHandle, callback, attributionSource);
}
@Override
@@ -1744,7 +1566,7 @@ public class GattService extends ProfileService {
if (service == null) {
return;
}
- service.unregisterSync(callback, attributionSource);
+ service.getTransitionalScanHelper().unregisterSync(callback, attributionSource);
}
@Override
@@ -1922,302 +1744,6 @@ public class GattService extends ProfileService {
* Callback functions - CLIENT
*************************************************************************/
- // EN format defined here:
- // https://blog.google/documents/70/Exposure_Notification_-_Bluetooth_Specification_v1.2.2.pdf
- private static final byte[] EXPOSURE_NOTIFICATION_FLAGS_PREAMBLE = new byte[] {
- // size 2, flag field, flags byte (value is not important)
- (byte) 0x02, (byte) 0x01
- };
- private static final int EXPOSURE_NOTIFICATION_FLAGS_LENGTH = 0x2 + 1;
- private static final byte[] EXPOSURE_NOTIFICATION_PAYLOAD_PREAMBLE = new byte[] {
- // size 3, complete 16 bit UUID, EN UUID
- (byte) 0x03, (byte) 0x03, (byte) 0x6F, (byte) 0xFD,
- // size 23, data for 16 bit UUID, EN UUID
- (byte) 0x17, (byte) 0x16, (byte) 0x6F, (byte) 0xFD,
- // ...payload
- };
- private static final int EXPOSURE_NOTIFICATION_PAYLOAD_LENGTH = 0x03 + 0x17 + 2;
-
- private static boolean arrayStartsWith(byte[] array, byte[] prefix) {
- if (array.length < prefix.length) {
- return false;
- }
- for (int i = 0; i < prefix.length; i++) {
- if (prefix[i] != array[i]) {
- return false;
- }
- }
- return true;
- }
-
- ScanResult getSanitizedExposureNotification(ScanResult result) {
- ScanRecord record = result.getScanRecord();
- // Remove the flags part of the payload, if present
- if (record.getBytes().length > EXPOSURE_NOTIFICATION_FLAGS_LENGTH
- && arrayStartsWith(record.getBytes(), EXPOSURE_NOTIFICATION_FLAGS_PREAMBLE)) {
- record = ScanRecord.parseFromBytes(
- Arrays.copyOfRange(
- record.getBytes(),
- EXPOSURE_NOTIFICATION_FLAGS_LENGTH,
- record.getBytes().length));
- }
-
- if (record.getBytes().length != EXPOSURE_NOTIFICATION_PAYLOAD_LENGTH) {
- return null;
- }
- if (!arrayStartsWith(record.getBytes(), EXPOSURE_NOTIFICATION_PAYLOAD_PREAMBLE)) {
- return null;
- }
-
- return new ScanResult(null, 0, 0, 0, 0, 0, result.getRssi(), 0, record, 0);
- }
-
- void onScanResult(int eventType, int addressType, String address, int primaryPhy,
- int secondaryPhy, int advertisingSid, int txPower, int rssi, int periodicAdvInt,
- byte[] advData, String originalAddress) {
- // When in testing mode, ignore all real-world events
- if (isTestModeEnabled()) return;
-
- AppScanStats.recordScanRadioResultCount();
- onScanResultInternal(eventType, addressType, address, primaryPhy, secondaryPhy,
- advertisingSid, txPower, rssi, periodicAdvInt, advData, originalAddress);
- }
-
- void onScanResultInternal(int eventType, int addressType, String address, int primaryPhy,
- int secondaryPhy, int advertisingSid, int txPower, int rssi, int periodicAdvInt,
- byte[] advData, String originalAddress) {
- if (VDBG) {
- Log.d(TAG, "onScanResult() - eventType=0x" + Integer.toHexString(eventType)
- + ", addressType=" + addressType + ", address=" + address + ", primaryPhy="
- + primaryPhy + ", secondaryPhy=" + secondaryPhy + ", advertisingSid=0x"
- + Integer.toHexString(advertisingSid) + ", txPower=" + txPower + ", rssi="
- + rssi + ", periodicAdvInt=0x" + Integer.toHexString(periodicAdvInt)
- + ", originalAddress=" + originalAddress);
- }
-
- String identityAddress = mAdapterService.getIdentityAddress(address);
- if (!address.equals(identityAddress)) {
- if (VDBG) {
- Log.d(TAG, "found identityAddress of " + address + ", replace originalAddress as "
- + identityAddress);
- }
- originalAddress = identityAddress;
- }
-
-
- byte[] legacyAdvData = Arrays.copyOfRange(advData, 0, 62);
-
- for (ScanClient client : mScanManager.getRegularScanQueue()) {
- TransitionalScanHelper.ScannerMap.App app =
- mTransitionalScanHelper.getScannerMap().getById(client.scannerId);
- if (app == null) {
- if (VDBG) {
- Log.d(TAG, "App is null; skip.");
- }
- continue;
- }
-
- BluetoothDevice device =
- BluetoothAdapter.getDefaultAdapter().getRemoteLeDevice(address, addressType);
-
- ScanSettings settings = client.settings;
- byte[] scanRecordData;
- // This is for compability with applications that assume fixed size scan data.
- if (settings.getLegacy()) {
- if ((eventType & ET_LEGACY_MASK) == 0) {
- // If this is legacy scan, but nonlegacy result - skip.
- if (VDBG) {
- Log.d(TAG, "Legacy scan, non legacy result; skip.");
- }
- continue;
- } else {
- // Some apps are used to fixed-size advertise data.
- scanRecordData = legacyAdvData;
- }
- } else {
- scanRecordData = advData;
- }
-
- ScanRecord scanRecord = ScanRecord.parseFromBytes(scanRecordData);
- ScanResult result =
- new ScanResult(device, eventType, primaryPhy, secondaryPhy, advertisingSid,
- txPower, rssi, periodicAdvInt, scanRecord,
- SystemClock.elapsedRealtimeNanos());
-
- if (client.hasDisavowedLocation) {
- if (mLocationDenylistPredicate.test(result)) {
- Log.i(TAG, "Skipping client for location deny list");
- continue;
- }
- }
-
- boolean hasPermission = hasScanResultPermission(client);
- if (!hasPermission) {
- for (String associatedDevice : client.associatedDevices) {
- if (associatedDevice.equalsIgnoreCase(address)) {
- hasPermission = true;
- break;
- }
- }
- }
- if (!hasPermission && client.eligibleForSanitizedExposureNotification) {
- ScanResult sanitized = getSanitizedExposureNotification(result);
- if (sanitized != null) {
- hasPermission = true;
- result = sanitized;
- }
- }
- MatchResult matchResult = matchesFilters(client, result, originalAddress);
- if (!hasPermission || !matchResult.getMatches()) {
- if (VDBG) {
- Log.d(TAG, "Skipping client: permission="
- + hasPermission + " matches=" + matchResult.getMatches());
- }
- continue;
- }
-
- int callbackType = settings.getCallbackType();
- if (!(callbackType == ScanSettings.CALLBACK_TYPE_ALL_MATCHES
- || callbackType == ScanSettings.CALLBACK_TYPE_ALL_MATCHES_AUTO_BATCH)) {
- if (VDBG) {
- Log.d(TAG, "Skipping client: CALLBACK_TYPE_ALL_MATCHES");
- }
- continue;
- }
-
- try {
- app.appScanStats.addResult(client.scannerId);
- if (app.callback != null) {
- app.callback.onScanResult(result);
- } else {
- // Send the PendingIntent
- ArrayList<ScanResult> results = new ArrayList<>();
- results.add(result);
- sendResultsByPendingIntent(app.info, results,
- ScanSettings.CALLBACK_TYPE_ALL_MATCHES);
- }
- } catch (RemoteException | PendingIntent.CanceledException e) {
- Log.e(TAG, "Exception: " + e);
- if (Flags.leScanFixRemoteException()) {
- handleDeadScanClient(client);
- } else {
- mTransitionalScanHelper.getScannerMap().remove(client.scannerId);
- mScanManager.stopScan(client.scannerId);
- }
- }
- }
- }
-
- private void sendResultByPendingIntent(PendingIntentInfo pii, ScanResult result,
- int callbackType, ScanClient client) {
- ArrayList<ScanResult> results = new ArrayList<>();
- results.add(result);
- try {
- sendResultsByPendingIntent(pii, results, callbackType);
- } catch (PendingIntent.CanceledException e) {
- final long token = Binder.clearCallingIdentity();
- try {
- stopScan(client.scannerId, getAttributionSource());
- unregisterScanner(client.scannerId, getAttributionSource());
- } finally {
- Binder.restoreCallingIdentity(token);
- }
- }
- }
-
- private void sendResultsByPendingIntent(PendingIntentInfo pii, ArrayList<ScanResult> results,
- int callbackType) throws PendingIntent.CanceledException {
- Intent extrasIntent = new Intent();
- extrasIntent.putParcelableArrayListExtra(BluetoothLeScanner.EXTRA_LIST_SCAN_RESULT,
- results);
- extrasIntent.putExtra(BluetoothLeScanner.EXTRA_CALLBACK_TYPE, callbackType);
- pii.intent.send(this, 0, extrasIntent);
- }
-
- private void sendErrorByPendingIntent(PendingIntentInfo pii, int errorCode)
- throws PendingIntent.CanceledException {
- Intent extrasIntent = new Intent();
- extrasIntent.putExtra(BluetoothLeScanner.EXTRA_ERROR_CODE, errorCode);
- pii.intent.send(this, 0, extrasIntent);
- }
-
- void onScannerRegistered(int status, int scannerId, long uuidLsb, long uuidMsb)
- throws RemoteException {
- UUID uuid = new UUID(uuidMsb, uuidLsb);
- if (DBG) {
- Log.d(TAG, "onScannerRegistered() - UUID=" + uuid + ", scannerId=" + scannerId
- + ", status=" + status);
- }
-
- // First check the callback map
- TransitionalScanHelper.ScannerMap.App cbApp =
- mTransitionalScanHelper.getScannerMap().getByUuid(uuid);
- if (cbApp != null) {
- if (status == 0) {
- cbApp.id = scannerId;
- // If app is callback based, setup a death recipient. App will initiate the start.
- // Otherwise, if PendingIntent based, start the scan directly.
- if (cbApp.callback != null) {
- cbApp.linkToDeath(new ScannerDeathRecipient(scannerId, cbApp.name));
- } else {
- continuePiStartScan(scannerId, cbApp);
- }
- } else {
- mTransitionalScanHelper.getScannerMap().remove(scannerId);
- }
- if (cbApp.callback != null) {
- cbApp.callback.onScannerRegistered(status, scannerId);
- }
- }
- }
-
- /** Determines if the given scan client has the appropriate permissions to receive callbacks. */
- private boolean hasScanResultPermission(final ScanClient client) {
- if (client.hasNetworkSettingsPermission
- || client.hasNetworkSetupWizardPermission
- || client.hasScanWithoutLocationPermission) {
- return true;
- }
- if (client.hasDisavowedLocation) {
- return true;
- }
- return client.hasLocationPermission && !Utils.blockedByLocationOff(this, client.userHandle);
- }
-
- // Check if a scan record matches a specific filters.
- private MatchResult matchesFilters(ScanClient client, ScanResult scanResult) {
- return matchesFilters(client, scanResult, null);
- }
-
- // Check if a scan record matches a specific filters or original address
- private MatchResult matchesFilters(ScanClient client, ScanResult scanResult,
- String originalAddress) {
- if (client.filters == null || client.filters.isEmpty()) {
- // TODO: Do we really wanna return true here?
- return new MatchResult(true, MatchOrigin.PSEUDO_ADDRESS);
- }
- for (ScanFilter filter : client.filters) {
- // Need to check the filter matches, and the original address without changing the API
- if (filter.matches(scanResult)) {
- return new MatchResult(true, MatchOrigin.PSEUDO_ADDRESS);
- }
- if (originalAddress != null
- && originalAddress.equalsIgnoreCase(filter.getDeviceAddress())) {
- return new MatchResult(true, MatchOrigin.ORIGINAL_ADDRESS);
- }
- }
- return new MatchResult(false, MatchOrigin.PSEUDO_ADDRESS);
- }
-
- private void handleDeadScanClient(ScanClient client) {
- if (client.appDied) {
- Log.w(TAG, "Already dead client " + client.scannerId);
- return;
- }
- client.appDied = true;
- stopScan(client.scannerId, getAttributionSource());
- }
-
void onClientRegistered(int status, int clientIf, long uuidLsb, long uuidMsb)
throws RemoteException {
UUID uuid = new UUID(uuidMsb, uuidLsb);
@@ -2719,404 +2245,6 @@ public class GattService extends ProfileService {
}
}
- void onScanFilterEnableDisabled(int action, int status, int clientIf) {
- if (DBG) {
- Log.d(TAG, "onScanFilterEnableDisabled() - clientIf=" + clientIf + ", status=" + status
- + ", action=" + action);
- }
- mScanManager.callbackDone(clientIf, status);
- }
-
- void onScanFilterParamsConfigured(int action, int status, int clientIf, int availableSpace) {
- if (DBG) {
- Log.d(TAG,
- "onScanFilterParamsConfigured() - clientIf=" + clientIf + ", status=" + status
- + ", action=" + action + ", availableSpace=" + availableSpace);
- }
- mScanManager.callbackDone(clientIf, status);
- }
-
- void onScanFilterConfig(int action, int status, int clientIf, int filterType,
- int availableSpace) {
- if (DBG) {
- Log.d(TAG, "onScanFilterConfig() - clientIf=" + clientIf + ", action = " + action
- + " status = " + status + ", filterType=" + filterType + ", availableSpace="
- + availableSpace);
- }
-
- mScanManager.callbackDone(clientIf, status);
- }
-
- void onBatchScanStorageConfigured(int status, int clientIf) {
- if (DBG) {
- Log.d(TAG,
- "onBatchScanStorageConfigured() - clientIf=" + clientIf + ", status=" + status);
- }
- mScanManager.callbackDone(clientIf, status);
- }
-
- // TODO: split into two different callbacks : onBatchScanStarted and onBatchScanStopped.
- void onBatchScanStartStopped(int startStopAction, int status, int clientIf) {
- if (DBG) {
- Log.d(TAG, "onBatchScanStartStopped() - clientIf=" + clientIf + ", status=" + status
- + ", startStopAction=" + startStopAction);
- }
- mScanManager.callbackDone(clientIf, status);
- }
-
- ScanClient findBatchScanClientById(int scannerId) {
- for (ScanClient client : mScanManager.getBatchScanQueue()) {
- if (client.scannerId == scannerId) {
- return client;
- }
- }
- return null;
- }
-
- void onBatchScanReports(int status, int scannerId, int reportType, int numRecords,
- byte[] recordData) throws RemoteException {
- // When in testing mode, ignore all real-world events
- if (isTestModeEnabled()) return;
-
- AppScanStats.recordBatchScanRadioResultCount(numRecords);
- onBatchScanReportsInternal(status, scannerId, reportType, numRecords, recordData);
- }
-
- void onBatchScanReportsInternal(int status, int scannerId, int reportType, int numRecords,
- byte[] recordData) throws RemoteException {
- if (DBG) {
- Log.d(TAG, "onBatchScanReports() - scannerId=" + scannerId + ", status=" + status
- + ", reportType=" + reportType + ", numRecords=" + numRecords);
- }
-
- Set<ScanResult> results = parseBatchScanResults(numRecords, reportType, recordData);
- if (reportType == ScanManager.SCAN_RESULT_TYPE_TRUNCATED) {
- // We only support single client for truncated mode.
- TransitionalScanHelper.ScannerMap.App app =
- mTransitionalScanHelper.getScannerMap().getById(scannerId);
- if (app == null) {
- return;
- }
-
- ScanClient client = findBatchScanClientById(scannerId);
- if (client == null) {
- return;
- }
-
- ArrayList<ScanResult> permittedResults;
- if (hasScanResultPermission(client)) {
- permittedResults = new ArrayList<ScanResult>(results);
- } else {
- permittedResults = new ArrayList<ScanResult>();
- for (ScanResult scanResult : results) {
- for (String associatedDevice : client.associatedDevices) {
- if (associatedDevice.equalsIgnoreCase(scanResult.getDevice()
- .getAddress())) {
- permittedResults.add(scanResult);
- }
- }
- }
- if (permittedResults.isEmpty()) {
- return;
- }
- }
-
- if (client.hasDisavowedLocation) {
- permittedResults.removeIf(mLocationDenylistPredicate);
- }
-
- if (app.callback != null) {
- app.callback.onBatchScanResults(permittedResults);
- } else {
- // PendingIntent based
- try {
- sendResultsByPendingIntent(app.info, permittedResults,
- ScanSettings.CALLBACK_TYPE_ALL_MATCHES);
- } catch (PendingIntent.CanceledException e) {
- }
- }
- } else {
- for (ScanClient client : mScanManager.getFullBatchScanQueue()) {
- // Deliver results for each client.
- deliverBatchScan(client, results);
- }
- }
- mScanManager.callbackDone(scannerId, status);
- }
-
- private void sendBatchScanResults(TransitionalScanHelper.ScannerMap.App app, ScanClient client,
- ArrayList<ScanResult> results) {
- try {
- if (app.callback != null) {
- if (mScanManager.isAutoBatchScanClientEnabled(client)) {
- if (DBG) {
- Log.d(TAG, "sendBatchScanResults() to onScanResult()" + client);
- }
- for (ScanResult result : results) {
- app.appScanStats.addResult(client.scannerId);
- app.callback.onScanResult(result);
- }
- } else {
- if (DBG) {
- Log.d(TAG, "sendBatchScanResults() to onBatchScanResults()" + client);
- }
- app.callback.onBatchScanResults(results);
- }
- } else {
- sendResultsByPendingIntent(app.info, results,
- ScanSettings.CALLBACK_TYPE_ALL_MATCHES);
- }
- } catch (RemoteException | PendingIntent.CanceledException e) {
- Log.e(TAG, "Exception: " + e);
- if (Flags.leScanFixRemoteException()) {
- handleDeadScanClient(client);
- } else {
- mTransitionalScanHelper.getScannerMap().remove(client.scannerId);
- mScanManager.stopScan(client.scannerId);
- }
- }
- }
-
- // Check and deliver scan results for different scan clients.
- private void deliverBatchScan(ScanClient client, Set<ScanResult> allResults)
- throws RemoteException {
- ContextMap.App app = mTransitionalScanHelper.getScannerMap().getById(client.scannerId);
- if (app == null) {
- return;
- }
-
- ArrayList<ScanResult> permittedResults;
- if (hasScanResultPermission(client)) {
- permittedResults = new ArrayList<ScanResult>(allResults);
- } else {
- permittedResults = new ArrayList<ScanResult>();
- for (ScanResult scanResult : allResults) {
- for (String associatedDevice : client.associatedDevices) {
- if (associatedDevice.equalsIgnoreCase(scanResult.getDevice().getAddress())) {
- permittedResults.add(scanResult);
- }
- }
- }
- if (permittedResults.isEmpty()) {
- return;
- }
- }
-
- if (client.filters == null || client.filters.isEmpty()) {
- sendBatchScanResults(app, client, permittedResults);
- // TODO: Question to reviewer: Shouldn't there be a return here?
- }
- // Reconstruct the scan results.
- ArrayList<ScanResult> results = new ArrayList<ScanResult>();
- for (ScanResult scanResult : permittedResults) {
- if (matchesFilters(client, scanResult).getMatches()) {
- results.add(scanResult);
- }
- }
- sendBatchScanResults(app, client, results);
- }
-
- private Set<ScanResult> parseBatchScanResults(int numRecords, int reportType,
- byte[] batchRecord) {
- if (numRecords == 0) {
- return Collections.emptySet();
- }
- if (DBG) {
- Log.d(TAG, "current time is " + SystemClock.elapsedRealtimeNanos());
- }
- if (reportType == ScanManager.SCAN_RESULT_TYPE_TRUNCATED) {
- return parseTruncatedResults(numRecords, batchRecord);
- } else {
- return parseFullResults(numRecords, batchRecord);
- }
- }
-
- private Set<ScanResult> parseTruncatedResults(int numRecords, byte[] batchRecord) {
- if (DBG) {
- Log.d(TAG, "batch record " + Arrays.toString(batchRecord));
- }
- Set<ScanResult> results = new HashSet<ScanResult>(numRecords);
- long now = SystemClock.elapsedRealtimeNanos();
- for (int i = 0; i < numRecords; ++i) {
- byte[] record =
- extractBytes(batchRecord, i * TRUNCATED_RESULT_SIZE, TRUNCATED_RESULT_SIZE);
- byte[] address = extractBytes(record, 0, 6);
- reverse(address);
- BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
- int rssi = record[8];
- long timestampNanos = now - parseTimestampNanos(extractBytes(record, 9, 2));
- results.add(new ScanResult(device, ScanRecord.parseFromBytes(new byte[0]), rssi,
- timestampNanos));
- }
- return results;
- }
-
- @VisibleForTesting
- long parseTimestampNanos(byte[] data) {
- long timestampUnit = NumberUtils.littleEndianByteArrayToInt(data);
- // Timestamp is in every 50 ms.
- return TimeUnit.MILLISECONDS.toNanos(timestampUnit * 50);
- }
-
- private Set<ScanResult> parseFullResults(int numRecords, byte[] batchRecord) {
- if (DBG) {
- Log.d(TAG, "Batch record : " + Arrays.toString(batchRecord));
- }
- Set<ScanResult> results = new HashSet<ScanResult>(numRecords);
- int position = 0;
- long now = SystemClock.elapsedRealtimeNanos();
- while (position < batchRecord.length) {
- byte[] address = extractBytes(batchRecord, position, 6);
- // TODO: remove temp hack.
- reverse(address);
- BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
- position += 6;
- // Skip address type.
- position++;
- // Skip tx power level.
- position++;
- int rssi = batchRecord[position++];
- long timestampNanos = now - parseTimestampNanos(extractBytes(batchRecord, position, 2));
- position += 2;
-
- // Combine advertise packet and scan response packet.
- int advertisePacketLen = batchRecord[position++];
- byte[] advertiseBytes = extractBytes(batchRecord, position, advertisePacketLen);
- position += advertisePacketLen;
- int scanResponsePacketLen = batchRecord[position++];
- byte[] scanResponseBytes = extractBytes(batchRecord, position, scanResponsePacketLen);
- position += scanResponsePacketLen;
- byte[] scanRecord = new byte[advertisePacketLen + scanResponsePacketLen];
- System.arraycopy(advertiseBytes, 0, scanRecord, 0, advertisePacketLen);
- System.arraycopy(scanResponseBytes, 0, scanRecord, advertisePacketLen,
- scanResponsePacketLen);
- if (DBG) {
- Log.d(TAG, "ScanRecord : " + Arrays.toString(scanRecord));
- }
- results.add(new ScanResult(device, ScanRecord.parseFromBytes(scanRecord), rssi,
- timestampNanos));
- }
- return results;
- }
-
- // Reverse byte array.
- private void reverse(byte[] address) {
- int len = address.length;
- for (int i = 0; i < len / 2; ++i) {
- byte b = address[i];
- address[i] = address[len - 1 - i];
- address[len - 1 - i] = b;
- }
- }
-
- // Helper method to extract bytes from byte array.
- private static byte[] extractBytes(byte[] scanRecord, int start, int length) {
- byte[] bytes = new byte[length];
- System.arraycopy(scanRecord, start, bytes, 0, length);
- return bytes;
- }
-
- @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
- void onBatchScanThresholdCrossed(int clientIf) {
- if (DBG) {
- Log.d(TAG, "onBatchScanThresholdCrossed() - clientIf=" + clientIf);
- }
- flushPendingBatchResults(clientIf, getAttributionSource());
- }
-
- AdvtFilterOnFoundOnLostInfo createOnTrackAdvFoundLostObject(int clientIf, int advPktLen,
- byte[] advPkt, int scanRspLen, byte[] scanRsp, int filtIndex, int advState,
- int advInfoPresent, String address, int addrType, int txPower, int rssiValue,
- int timeStamp) {
-
- return new AdvtFilterOnFoundOnLostInfo(clientIf, advPktLen, advPkt, scanRspLen, scanRsp,
- filtIndex, advState, advInfoPresent, address, addrType, txPower, rssiValue,
- timeStamp);
- }
-
- void onTrackAdvFoundLost(AdvtFilterOnFoundOnLostInfo trackingInfo) throws RemoteException {
- if (DBG) {
- Log.d(TAG, "onTrackAdvFoundLost() - scannerId= " + trackingInfo.getClientIf()
- + " address = " + trackingInfo.getAddress() + " adv_state = "
- + trackingInfo.getAdvState());
- }
-
- TransitionalScanHelper.ScannerMap.App app =
- mTransitionalScanHelper.getScannerMap().getById(trackingInfo.getClientIf());
- if (app == null || (app.callback == null && app.info == null)) {
- Log.e(TAG, "app or callback is null");
- return;
- }
-
- BluetoothDevice device = BluetoothAdapter.getDefaultAdapter()
- .getRemoteDevice(trackingInfo.getAddress());
- int advertiserState = trackingInfo.getAdvState();
- ScanResult result =
- new ScanResult(device, ScanRecord.parseFromBytes(trackingInfo.getResult()),
- trackingInfo.getRSSIValue(), SystemClock.elapsedRealtimeNanos());
-
- for (ScanClient client : mScanManager.getRegularScanQueue()) {
- if (client.scannerId == trackingInfo.getClientIf()) {
- ScanSettings settings = client.settings;
- if ((advertiserState == ADVT_STATE_ONFOUND) && (
- (settings.getCallbackType() & ScanSettings.CALLBACK_TYPE_FIRST_MATCH)
- != 0)) {
- if (app.callback != null) {
- app.callback.onFoundOrLost(true, result);
- } else {
- sendResultByPendingIntent(app.info, result,
- ScanSettings.CALLBACK_TYPE_FIRST_MATCH, client);
- }
- } else if ((advertiserState == ADVT_STATE_ONLOST) && (
- (settings.getCallbackType() & ScanSettings.CALLBACK_TYPE_MATCH_LOST)
- != 0)) {
- if (app.callback != null) {
- app.callback.onFoundOrLost(false, result);
- } else {
- sendResultByPendingIntent(app.info, result,
- ScanSettings.CALLBACK_TYPE_MATCH_LOST, client);
- }
- } else {
- if (DBG) {
- Log.d(TAG, "Not reporting onlost/onfound : " + advertiserState
- + " scannerId = " + client.scannerId + " callbackType "
- + settings.getCallbackType());
- }
- }
- }
- }
- }
-
- void onScanParamSetupCompleted(int status, int scannerId) throws RemoteException {
- ContextMap.App app = mTransitionalScanHelper.getScannerMap().getById(scannerId);
- if (app == null || app.callback == null) {
- Log.e(TAG, "Advertise app or callback is null");
- return;
- }
- if (DBG) {
- Log.d(TAG, "onScanParamSetupCompleted : " + status);
- }
- }
-
- // callback from ScanManager for dispatch of errors apps.
- public void onScanManagerErrorCallback(int scannerId, int errorCode) throws RemoteException {
- TransitionalScanHelper.ScannerMap.App app =
- mTransitionalScanHelper.getScannerMap().getById(scannerId);
- if (app == null || (app.callback == null && app.info == null)) {
- Log.e(TAG, "App or callback is null");
- return;
- }
- if (app.callback != null) {
- app.callback.onScanManagerErrorCallback(errorCode);
- } else {
- try {
- sendErrorByPendingIntent(app.info, errorCode);
- } catch (PendingIntent.CanceledException e) {
- Log.e(TAG, "Error sending error code via PendingIntent:" + e);
- }
- }
- }
-
void onConfigureMTU(int connId, int status, int mtu) throws RemoteException {
String address = mClientMap.addressByConnId(connId);
@@ -3203,292 +2331,6 @@ public class GattService extends ProfileService {
return deviceList;
}
- @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
- void registerScanner(IScannerCallback callback, WorkSource workSource,
- AttributionSource attributionSource) throws RemoteException {
- if (!Utils.checkScanPermissionForDataDelivery(
- this, attributionSource, "GattService registerScanner")) {
- return;
- }
-
- UUID uuid = UUID.randomUUID();
- if (DBG) {
- Log.d(TAG, "registerScanner() - UUID=" + uuid);
- }
-
- enforceImpersonatationPermissionIfNeeded(workSource);
-
- AppScanStats app = mTransitionalScanHelper.getScannerMap()
- .getAppScanStatsByUid(Binder.getCallingUid());
- if (app != null && app.isScanningTooFrequently()
- && !Utils.checkCallerHasPrivilegedPermission(this)) {
- Log.e(TAG, "App '" + app.appName + "' is scanning too frequently");
- callback.onScannerRegistered(ScanCallback.SCAN_FAILED_SCANNING_TOO_FREQUENTLY, -1);
- return;
- }
-
- mTransitionalScanHelper
- .getScannerMap().add(uuid, workSource, callback, null, this);
- mScanManager.registerScanner(uuid);
- }
-
- @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
- public void unregisterScanner(int scannerId, AttributionSource attributionSource) {
- if (!Utils.checkScanPermissionForDataDelivery(
- this, attributionSource, "GattService unregisterScanner")) {
- return;
- }
-
- if (DBG) {
- Log.d(TAG, "unregisterScanner() - scannerId=" + scannerId);
- }
- mTransitionalScanHelper.getScannerMap().remove(scannerId);
- mScanManager.unregisterScanner(scannerId);
- }
-
- private List<String> getAssociatedDevices(String callingPackage) {
- if (mCompanionManager == null) {
- return Collections.emptyList();
- }
-
- List<String> macAddresses = new ArrayList();
-
- final long identity = Binder.clearCallingIdentity();
- try {
- for (AssociationInfo info : Utils.getCdmAssociations(mCompanionManager)) {
- if (info.getPackageName().equals(callingPackage) && !info.isSelfManaged()
- && info.getDeviceMacAddress() != null) {
- macAddresses.add(info.getDeviceMacAddress().toString());
- }
- }
- } catch (SecurityException se) {
- // Not an app with associated devices
- } catch (Exception e) {
- Log.e(TAG, "Cannot check device associations for " + callingPackage, e);
- } finally {
- Binder.restoreCallingIdentity(identity);
- }
- return macAddresses;
- }
-
- @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
- void startScan(int scannerId, ScanSettings settings, List<ScanFilter> filters,
- AttributionSource attributionSource) {
- if (DBG) {
- Log.d(TAG, "start scan with filters");
- }
-
- if (!Utils.checkScanPermissionForDataDelivery(
- this, attributionSource, "Starting GATT scan.")) {
- return;
- }
-
- enforcePrivilegedPermissionIfNeeded(settings);
- String callingPackage = attributionSource.getPackageName();
- settings = enforceReportDelayFloor(settings);
- enforcePrivilegedPermissionIfNeeded(filters);
- final ScanClient scanClient = new ScanClient(scannerId, settings, filters);
- scanClient.userHandle = Binder.getCallingUserHandle();
- mAppOps.checkPackage(Binder.getCallingUid(), callingPackage);
- scanClient.eligibleForSanitizedExposureNotification =
- callingPackage.equals(mExposureNotificationPackage);
-
- scanClient.hasDisavowedLocation =
- Utils.hasDisavowedLocationForScan(this, attributionSource, isTestModeEnabled());
-
- scanClient.isQApp = checkCallerTargetSdk(this, callingPackage, Build.VERSION_CODES.Q);
- if (!scanClient.hasDisavowedLocation) {
- if (scanClient.isQApp) {
- scanClient.hasLocationPermission = Utils.checkCallerHasFineLocation(
- this, attributionSource, scanClient.userHandle);
- } else {
- scanClient.hasLocationPermission = Utils.checkCallerHasCoarseOrFineLocation(
- this, attributionSource, scanClient.userHandle);
- }
- }
- scanClient.hasNetworkSettingsPermission =
- Utils.checkCallerHasNetworkSettingsPermission(this);
- scanClient.hasNetworkSetupWizardPermission =
- Utils.checkCallerHasNetworkSetupWizardPermission(this);
- scanClient.hasScanWithoutLocationPermission =
- Utils.checkCallerHasScanWithoutLocationPermission(this);
- scanClient.associatedDevices = getAssociatedDevices(callingPackage);
-
- AppScanStats app = mTransitionalScanHelper.getScannerMap().getAppScanStatsById(scannerId);
- ContextMap.App cbApp = mTransitionalScanHelper.getScannerMap().getById(scannerId);
- if (app != null) {
- scanClient.stats = app;
- boolean isFilteredScan = (filters != null) && !filters.isEmpty();
- boolean isCallbackScan = false;
- if (cbApp != null) {
- isCallbackScan = cbApp.callback != null;
- }
- app.recordScanStart(settings, filters, isFilteredScan, isCallbackScan, scannerId);
- }
-
- mScanManager.startScan(scanClient);
- }
-
- @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
- void registerPiAndStartScan(PendingIntent pendingIntent, ScanSettings settings,
- List<ScanFilter> filters, AttributionSource attributionSource) {
- if (DBG) {
- Log.d(TAG, "start scan with filters, for PendingIntent");
- }
-
- if (!Utils.checkScanPermissionForDataDelivery(
- this, attributionSource, "Starting GATT scan.")) {
- return;
- }
- enforcePrivilegedPermissionIfNeeded(settings);
- settings = enforceReportDelayFloor(settings);
- enforcePrivilegedPermissionIfNeeded(filters);
- UUID uuid = UUID.randomUUID();
- String callingPackage = attributionSource.getPackageName();
- int callingUid = attributionSource.getUid();
- PendingIntentInfo piInfo = new PendingIntentInfo();
- piInfo.intent = pendingIntent;
- piInfo.settings = settings;
- piInfo.filters = filters;
- piInfo.callingPackage = callingPackage;
- piInfo.callingUid = callingUid;
- if (DBG) {
- Log.d(
- TAG,
- "startScan(PI) -"
- + (" UUID=" + uuid)
- + (" Package=" + callingPackage)
- + (" UID=" + callingUid));
- }
-
- // Don't start scan if the Pi scan already in mScannerMap.
- if (mTransitionalScanHelper.getScannerMap().getByContextInfo(piInfo) != null) {
- Log.d(TAG, "Don't startScan(PI) since the same Pi scan already in mScannerMap.");
- return;
- }
-
- ContextMap.App app =
- mTransitionalScanHelper.getScannerMap().add(uuid, null, null, piInfo, this);
-
- app.mUserHandle = UserHandle.getUserHandleForUid(Binder.getCallingUid());
- mAppOps.checkPackage(Binder.getCallingUid(), callingPackage);
- app.mEligibleForSanitizedExposureNotification =
- callingPackage.equals(mExposureNotificationPackage);
-
- app.mHasDisavowedLocation =
- Utils.hasDisavowedLocationForScan(this, attributionSource, isTestModeEnabled());
-
- if (!app.mHasDisavowedLocation) {
- try {
- if (checkCallerTargetSdk(this, callingPackage, Build.VERSION_CODES.Q)) {
- app.hasLocationPermission = Utils.checkCallerHasFineLocation(
- this, attributionSource, app.mUserHandle);
- } else {
- app.hasLocationPermission = Utils.checkCallerHasCoarseOrFineLocation(
- this, attributionSource, app.mUserHandle);
- }
- } catch (SecurityException se) {
- // No need to throw here. Just mark as not granted.
- app.hasLocationPermission = false;
- }
- }
- app.mHasNetworkSettingsPermission =
- Utils.checkCallerHasNetworkSettingsPermission(this);
- app.mHasNetworkSetupWizardPermission =
- Utils.checkCallerHasNetworkSetupWizardPermission(this);
- app.mHasScanWithoutLocationPermission =
- Utils.checkCallerHasScanWithoutLocationPermission(this);
- app.mAssociatedDevices = getAssociatedDevices(callingPackage);
- mScanManager.registerScanner(uuid);
-
- // If this fails, we should stop the scan immediately.
- if (!pendingIntent.addCancelListener(Runnable::run, mScanIntentCancelListener)) {
- Log.d(TAG, "scanning PendingIntent is already cancelled, stopping scan.");
- stopScan(pendingIntent, attributionSource);
- }
- }
-
- void continuePiStartScan(int scannerId, TransitionalScanHelper.ScannerMap.App app) {
- final PendingIntentInfo piInfo = app.info;
- final ScanClient scanClient =
- new ScanClient(scannerId, piInfo.settings, piInfo.filters, piInfo.callingUid);
- scanClient.hasLocationPermission = app.hasLocationPermission;
- scanClient.userHandle = app.mUserHandle;
- scanClient.isQApp = checkCallerTargetSdk(this, app.name, Build.VERSION_CODES.Q);
- scanClient.eligibleForSanitizedExposureNotification =
- app.mEligibleForSanitizedExposureNotification;
- scanClient.hasNetworkSettingsPermission = app.mHasNetworkSettingsPermission;
- scanClient.hasNetworkSetupWizardPermission = app.mHasNetworkSetupWizardPermission;
- scanClient.hasScanWithoutLocationPermission = app.mHasScanWithoutLocationPermission;
- scanClient.associatedDevices = app.mAssociatedDevices;
- scanClient.hasDisavowedLocation = app.mHasDisavowedLocation;
-
- AppScanStats scanStats =
- mTransitionalScanHelper.getScannerMap().getAppScanStatsById(scannerId);
- if (scanStats != null) {
- scanClient.stats = scanStats;
- boolean isFilteredScan = (piInfo.filters != null) && !piInfo.filters.isEmpty();
- scanStats.recordScanStart(
- piInfo.settings, piInfo.filters, isFilteredScan, false, scannerId);
- }
-
- mScanManager.startScan(scanClient);
- }
-
- @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
- void flushPendingBatchResults(int scannerId, AttributionSource attributionSource) {
- if (!Utils.checkScanPermissionForDataDelivery(
- this, attributionSource, "GattService flushPendingBatchResults")) {
- return;
- }
- if (DBG) {
- Log.d(TAG, "flushPendingBatchResults - scannerId=" + scannerId);
- }
- mScanManager.flushBatchScanResults(new ScanClient(scannerId));
- }
-
- @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
- void stopScan(int scannerId, AttributionSource attributionSource) {
- if (!Utils.checkScanPermissionForDataDelivery(
- this, attributionSource, "GattService stopScan")) {
- return;
- }
- int scanQueueSize =
- mScanManager.getBatchScanQueue().size() + mScanManager.getRegularScanQueue().size();
- if (DBG) {
- Log.d(TAG, "stopScan() - queue size =" + scanQueueSize);
- }
-
- AppScanStats app = null;
- app = mTransitionalScanHelper.getScannerMap().getAppScanStatsById(scannerId);
- if (app != null) {
- app.recordScanStop(scannerId);
- }
-
- mScanManager.stopScan(scannerId);
- }
-
- @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
- void stopScan(PendingIntent intent, AttributionSource attributionSource) {
- if (!Utils.checkScanPermissionForDataDelivery(
- this, attributionSource, "GattService stopScan")) {
- return;
- }
- PendingIntentInfo pii = new PendingIntentInfo();
- pii.intent = intent;
- ContextMap.App app = mTransitionalScanHelper.getScannerMap().getByContextInfo(pii);
- if (VDBG) {
- Log.d(TAG, "stopScan(PendingIntent): app found = " + app);
- }
- if (app != null) {
- intent.removeCancelListener(mScanIntentCancelListener);
- final int scannerId = app.id;
- stopScan(scannerId, attributionSource);
- // Also unregister the scanner
- unregisterScanner(scannerId, attributionSource);
- }
- }
-
@RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
void disconnectAll(AttributionSource attributionSource) {
if (DBG) {
@@ -3515,48 +2357,6 @@ public class GattService extends ProfileService {
}
/**************************************************************************
- * PERIODIC SCANNING
- *************************************************************************/
- @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
- void registerSync(ScanResult scanResult, int skip, int timeout,
- IPeriodicAdvertisingCallback callback, AttributionSource attributionSource) {
- if (!Utils.checkScanPermissionForDataDelivery(
- this, attributionSource, "GattService registerSync")) {
- return;
- }
- mPeriodicScanManager.startSync(scanResult, skip, timeout, callback);
- }
-
- @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
- void unregisterSync(
- IPeriodicAdvertisingCallback callback, AttributionSource attributionSource) {
- if (!Utils.checkScanPermissionForDataDelivery(
- this, attributionSource, "GattService unregisterSync")) {
- return;
- }
- mPeriodicScanManager.stopSync(callback);
- }
-
- void transferSync(BluetoothDevice bda, int serviceData, int syncHandle,
- AttributionSource attributionSource) {
- if (!Utils.checkScanPermissionForDataDelivery(
- this, attributionSource, "GattService transferSync")) {
- return;
- }
- mPeriodicScanManager.transferSync(bda, serviceData, syncHandle);
- }
-
- void transferSetInfo(BluetoothDevice bda, int serviceData,
- int advHandle, IPeriodicAdvertisingCallback callback,
- AttributionSource attributionSource) {
- if (!Utils.checkScanPermissionForDataDelivery(
- this, attributionSource, "GattService transferSetInfo")) {
- return;
- }
- mPeriodicScanManager.transferSetInfo(bda, serviceData, advHandle, callback);
- }
-
- /**************************************************************************
* ADVERTISING SET
*************************************************************************/
@RequiresPermission(android.Manifest.permission.BLUETOOTH_ADVERTISE)
@@ -3712,7 +2512,7 @@ public class GattService extends ProfileService {
if (DBG) {
Log.d(TAG, "registerClient() - UUID=" + uuid);
}
- mClientMap.add(uuid, null, callback, null, this);
+ mClientMap.add(uuid, null, callback, null, this, mTransitionalScanHelper);
mNativeInterface.gattClientRegisterApp(uuid.getLeastSignificantBits(),
uuid.getMostSignificantBits(), eatt_support);
}
@@ -3822,7 +2622,7 @@ public class GattService extends ProfileService {
return 0;
}
return (AdapterService.getAdapterService().getTotalNumOfTrackableAdvertisements()
- - mScanManager.getCurrentUsedTrackingAdvertisement());
+ - mTransitionalScanHelper.getCurrentUsedTrackingAdvertisement());
}
@RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
@@ -4608,7 +3408,7 @@ public class GattService extends ProfileService {
if (DBG) {
Log.d(TAG, "registerServer() - UUID=" + uuid);
}
- mServerMap.add(uuid, null, callback, null, this);
+ mServerMap.add(uuid, null, callback, null, this, mTransitionalScanHelper);
mNativeInterface.gattServerRegisterApp(uuid.getLeastSignificantBits(),
uuid.getMostSignificantBits(), eatt_support);
}
@@ -4878,87 +3678,6 @@ public class GattService extends ProfileService {
return type;
}
- private boolean needsPrivilegedPermissionForScan(ScanSettings settings) {
- // BLE scan only mode needs special permission.
- if (mAdapterService.getState() != BluetoothAdapter.STATE_ON) {
- return true;
- }
-
- // Regular scan, no special permission.
- if (settings == null) {
- return false;
- }
-
- // Ambient discovery mode, needs privileged permission.
- if (settings.getScanMode() == ScanSettings.SCAN_MODE_AMBIENT_DISCOVERY) {
- return true;
- }
-
- // Regular scan, no special permission.
- if (settings.getReportDelayMillis() == 0) {
- return false;
- }
-
- // Batch scan, truncated mode needs permission.
- return settings.getScanResultType() == ScanSettings.SCAN_RESULT_TYPE_ABBREVIATED;
- }
-
- /*
- * The {@link ScanFilter#setDeviceAddress} API overloads are @SystemApi access methods. This
- * requires that the permissions be BLUETOOTH_PRIVILEGED.
- */
- @SuppressLint("AndroidFrameworkRequiresPermission")
- private void enforcePrivilegedPermissionIfNeeded(List<ScanFilter> filters) {
- if (DBG) {
- Log.d(TAG, "enforcePrivilegedPermissionIfNeeded(" + filters + ")");
- }
- // Some 3p API cases may have null filters, need to allow
- if (filters != null) {
- for (ScanFilter filter : filters) {
- // The only case to enforce here is if there is an address
- // If there is an address, enforce if the correct combination criteria is met.
- if (filter.getDeviceAddress() != null) {
- // At this point we have an address, that means a caller used the
- // setDeviceAddress(address) public API for the ScanFilter
- // We don't want to enforce if the type is PUBLIC and the IRK is null
- // However, if we have a different type that means the caller used a new
- // @SystemApi such as setDeviceAddress(address, type) or
- // setDeviceAddress(address, type, irk) which are both @SystemApi and require
- // permissions to be enforced
- if (filter.getAddressType()
- == BluetoothDevice.ADDRESS_TYPE_PUBLIC && filter.getIrk() == null) {
- // Do not enforce
- } else {
- enforceBluetoothPrivilegedPermission(this);
- }
- }
- }
- }
- }
-
- @SuppressLint("AndroidFrameworkRequiresPermission")
- private void enforcePrivilegedPermissionIfNeeded(ScanSettings settings) {
- if (needsPrivilegedPermissionForScan(settings)) {
- enforceBluetoothPrivilegedPermission(this);
- }
- }
-
- // Enforce caller has UPDATE_DEVICE_STATS permission, which allows the caller to blame other
- // apps for Bluetooth usage. A {@link SecurityException} will be thrown if the caller app does
- // not have UPDATE_DEVICE_STATS permission.
- @RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS)
- private void enforceImpersonatationPermission() {
- enforceCallingOrSelfPermission(android.Manifest.permission.UPDATE_DEVICE_STATS,
- "Need UPDATE_DEVICE_STATS permission");
- }
-
- @SuppressLint("AndroidFrameworkRequiresPermission")
- private void enforceImpersonatationPermissionIfNeeded(WorkSource workSource) {
- if (workSource != null) {
- enforceImpersonatationPermission();
- }
- }
-
private void logClientForegroundInfo(int uid, boolean isDirect) {
if (mPackageManager == null) {
return;
@@ -5015,45 +3734,6 @@ public class GattService extends ProfileService {
}
}
- /**
- * Ensures the report delay is either 0 or at least the floor value (5000ms)
- *
- * @param settings are the scan settings passed into a request to start le scanning
- * @return the passed in ScanSettings object if the report delay is 0 or above the floor value;
- * a new ScanSettings object with the report delay being the floor value if the original
- * report delay was between 0 and the floor value (exclusive of both)
- */
- @VisibleForTesting
- ScanSettings enforceReportDelayFloor(ScanSettings settings) {
- if (settings.getReportDelayMillis() == 0) {
- return settings;
- }
-
- // Need to clear identity to pass device config permission check
- final long callerToken = Binder.clearCallingIdentity();
- try {
- long floor = DeviceConfig.getLong(DeviceConfig.NAMESPACE_BLUETOOTH, "report_delay",
- DEFAULT_REPORT_DELAY_FLOOR);
-
- if (settings.getReportDelayMillis() > floor) {
- return settings;
- } else {
- return new ScanSettings.Builder()
- .setCallbackType(settings.getCallbackType())
- .setLegacy(settings.getLegacy())
- .setMatchMode(settings.getMatchMode())
- .setNumOfMatches(settings.getNumOfMatches())
- .setPhy(settings.getPhy())
- .setReportDelay(floor)
- .setScanMode(settings.getScanMode())
- .setScanResultType(settings.getScanResultType())
- .build();
- }
- } finally {
- Binder.restoreCallingIdentity(callerToken);
- }
- }
-
private void stopNextService(int serverIf, int status) throws RemoteException {
if (DBG) {
Log.d(TAG, "stopNextService() - serverIf=" + serverIf + ", status=" + status);
@@ -5122,8 +3802,6 @@ public class GattService extends ProfileService {
println(sb, " " + uuid);
}
- println(sb, "mMaxScanFilters: " + mMaxScanFilters);
-
sb.append("\nRegistered App\n");
dumpRegisterId(sb);
@@ -5143,15 +3821,6 @@ public class GattService extends ProfileService {
mHandleMap.dump(sb);
}
- public void addScanEvent(BluetoothMetricsProto.ScanEvent event) {
- synchronized (mScanEvents) {
- if (mScanEvents.size() == NUM_SCAN_EVENTS_KEPT) {
- mScanEvents.remove();
- }
- mScanEvents.add(event);
- }
- }
-
private void statsLogAppPackage(String address, int applicationUid, int sessionIndex) {
BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
BluetoothStatsLog.write(
@@ -5181,9 +3850,7 @@ public class GattService extends ProfileService {
@Override
public void dumpProto(BluetoothMetricsProto.BluetoothLog.Builder builder) {
- synchronized (mScanEvents) {
- builder.addAllScanEvent(mScanEvents);
- }
+ mTransitionalScanHelper.dumpProto(builder);
}
/**************************************************************************
diff --git a/android/app/src/com/android/bluetooth/gatt/AdvtFilterOnFoundOnLostInfo.java b/android/app/src/com/android/bluetooth/le_scan/AdvtFilterOnFoundOnLostInfo.java
index c16f8269ba..5db51efb79 100644
--- a/android/app/src/com/android/bluetooth/gatt/AdvtFilterOnFoundOnLostInfo.java
+++ b/android/app/src/com/android/bluetooth/le_scan/AdvtFilterOnFoundOnLostInfo.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.bluetooth.gatt;
+package com.android.bluetooth.le_scan;
import android.annotation.Nullable;
diff --git a/android/app/src/com/android/bluetooth/le_scan/AppScanStats.java b/android/app/src/com/android/bluetooth/le_scan/AppScanStats.java
index e2ce19a0b7..1851ecef9a 100644
--- a/android/app/src/com/android/bluetooth/le_scan/AppScanStats.java
+++ b/android/app/src/com/android/bluetooth/le_scan/AppScanStats.java
@@ -19,6 +19,7 @@ package com.android.bluetooth.le_scan;
import android.bluetooth.BluetoothProtoEnums;
import android.bluetooth.le.ScanFilter;
import android.bluetooth.le.ScanSettings;
+import android.content.Context;
import android.os.BatteryStatsManager;
import android.os.Binder;
import android.os.SystemClock;
@@ -29,7 +30,6 @@ import com.android.bluetooth.BluetoothStatsLog;
import com.android.bluetooth.btservice.AdapterService;
import com.android.bluetooth.btservice.MetricsLogger;
import com.android.bluetooth.gatt.ContextMap;
-import com.android.bluetooth.gatt.GattService;
import com.android.bluetooth.util.WorkSourceUtil;
import com.android.internal.annotations.GuardedBy;
@@ -66,8 +66,8 @@ public class AppScanStats {
// ContextMap here is needed to grab Apps and Connections
ContextMap mContextMap;
- // GattService is needed to add scan event protos to be dumped later
- final GattService mGattService;
+ // TransitionalScanHelper is needed to add scan event protos to be dumped later
+ final TransitionalScanHelper mScanHelper;
// Battery stats is used to keep track of scans and result stats
BatteryStatsManager mBatteryStatsManager;
@@ -149,11 +149,16 @@ public class AppScanStats {
private long stopTime = 0;
private int results = 0;
- public AppScanStats(String name, WorkSource source, ContextMap map, GattService service) {
+ public AppScanStats(
+ String name,
+ WorkSource source,
+ ContextMap map,
+ Context context,
+ TransitionalScanHelper scanHelper) {
appName = name;
mContextMap = map;
- mGattService = service;
- mBatteryStatsManager = service.getSystemService(BatteryStatsManager.class);
+ mScanHelper = scanHelper;
+ mBatteryStatsManager = context.getSystemService(BatteryStatsManager.class);
if (source == null) {
// Bill the caller if the work source isn't passed through
@@ -263,7 +268,7 @@ public class AppScanStats {
BluetoothMetricsProto.ScanEvent.ScanTechnologyType.SCAN_TECH_TYPE_LE)
.setEventTimeMillis(System.currentTimeMillis())
.setInitiator(truncateAppName(appName)).build();
- mGattService.addScanEvent(scanEvent);
+ mScanHelper.addScanEvent(scanEvent);
if (!isScanning()) {
mScanStartTime = startTime;
@@ -308,7 +313,7 @@ public class AppScanStats {
.setInitiator(truncateAppName(appName))
.setNumberResults(scan.results)
.build();
- mGattService.addScanEvent(scanEvent);
+ mScanHelper.addScanEvent(scanEvent);
mTotalScanTime += scanDuration;
long activeDuration = scanDuration - scan.suspendDuration;
diff --git a/android/app/src/com/android/bluetooth/le_scan/ScanManager.java b/android/app/src/com/android/bluetooth/le_scan/ScanManager.java
index f39115a34e..162633f5e6 100644
--- a/android/app/src/com/android/bluetooth/le_scan/ScanManager.java
+++ b/android/app/src/com/android/bluetooth/le_scan/ScanManager.java
@@ -49,7 +49,6 @@ import com.android.bluetooth.btservice.BluetoothAdapterProxy;
import com.android.bluetooth.flags.Flags;
import com.android.bluetooth.gatt.FilterParams;
import com.android.bluetooth.gatt.GattObjectsFactory;
-import com.android.bluetooth.gatt.GattService;
import com.android.bluetooth.gatt.GattServiceConfig;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
@@ -88,9 +87,9 @@ public class ScanManager {
public static final int SCAN_MODE_SCREEN_OFF_BALANCED_WINDOW_MS = 183;
public static final int SCAN_MODE_SCREEN_OFF_BALANCED_INTERVAL_MS = 730;
- // Result type defined in bt stack. Need to be accessed by GattService.
- public static final int SCAN_RESULT_TYPE_TRUNCATED = 1;
- public static final int SCAN_RESULT_TYPE_FULL = 2;
+ // Result type defined in bt stack. Need to be accessed by TransitionalScanHelper.
+ static final int SCAN_RESULT_TYPE_TRUNCATED = 1;
+ static final int SCAN_RESULT_TYPE_FULL = 2;
static final int SCAN_RESULT_TYPE_BOTH = 3;
// Messages for handling BLE scan operations.
@@ -123,7 +122,8 @@ public class ScanManager {
@GuardedBy("mCurUsedTrackableAdvertisementsLock")
private int mCurUsedTrackableAdvertisements = 0;
- private final GattService mService;
+ private final Context mContext;
+ private final TransitionalScanHelper mScanHelper;
private final AdapterService mAdapterService;
private BroadcastReceiver mBatchAlarmReceiver;
private boolean mBatchAlarmReceiverRegistered;
@@ -164,7 +164,8 @@ public class ScanManager {
}
public ScanManager(
- GattService service,
+ Context context,
+ TransitionalScanHelper scanHelper,
AdapterService adapterService,
BluetoothAdapterProxy bluetoothAdapterProxy,
Looper looper) {
@@ -173,11 +174,12 @@ public class ScanManager {
mBatchClients = Collections.newSetFromMap(new ConcurrentHashMap<ScanClient, Boolean>());
mSuspendedScanClients =
Collections.newSetFromMap(new ConcurrentHashMap<ScanClient, Boolean>());
- mService = service;
+ mContext = context;
+ mScanHelper = scanHelper;
mAdapterService = adapterService;
mScanNative = new ScanNative();
- mDm = mService.getSystemService(DisplayManager.class);
- mActivityManager = mService.getSystemService(ActivityManager.class);
+ mDm = mContext.getSystemService(DisplayManager.class);
+ mActivityManager = mContext.getSystemService(ActivityManager.class);
mLocationManager = mAdapterService.getSystemService(LocationManager.class);
mBluetoothAdapterProxy = bluetoothAdapterProxy;
mIsConnecting = false;
@@ -204,7 +206,7 @@ public class ScanManager {
}
IntentFilter locationIntentFilter = new IntentFilter(LocationManager.MODE_CHANGED_ACTION);
locationIntentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
- mService.registerReceiver(mLocationReceiver, locationIntentFilter);
+ mContext.registerReceiver(mLocationReceiver, locationIntentFilter);
}
public void cleanup() {
@@ -236,7 +238,7 @@ public class ScanManager {
}
try {
- mService.unregisterReceiver(mLocationReceiver);
+ mContext.unregisterReceiver(mLocationReceiver);
} catch (IllegalArgumentException e) {
Log.w(TAG, "exception when invoking unregisterReceiver(mLocationReceiver)", e);
}
@@ -523,7 +525,7 @@ public class ScanManager {
if (DBG) {
Log.d(TAG, "app died, unregister scanner - " + client.scannerId);
}
- mService.unregisterScanner(client.scannerId, mService.getAttributionSource());
+ mScanHelper.unregisterScanner(client.scannerId, mContext.getAttributionSource());
}
}
@@ -1023,10 +1025,11 @@ public class ScanManager {
mFilterIndexStack = new ArrayDeque<Integer>();
mClientFilterIndexMap = new HashMap<Integer, Deque<Integer>>();
- mAlarmManager = mService.getSystemService(AlarmManager.class);
+ mAlarmManager = mContext.getSystemService(AlarmManager.class);
Intent batchIntent = new Intent(ACTION_REFRESH_BATCHED_SCAN, null);
- mBatchScanIntervalIntent = PendingIntent.getBroadcast(mService, 0, batchIntent,
- PendingIntent.FLAG_IMMUTABLE);
+ mBatchScanIntervalIntent =
+ PendingIntent.getBroadcast(
+ mContext, 0, batchIntent, PendingIntent.FLAG_IMMUTABLE);
IntentFilter filter = new IntentFilter();
filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
filter.addAction(ACTION_REFRESH_BATCHED_SCAN);
@@ -1047,7 +1050,7 @@ public class ScanManager {
}
}
};
- mService.registerReceiver(mBatchAlarmReceiver, filter);
+ mContext.registerReceiver(mBatchAlarmReceiver, filter);
mBatchAlarmReceiverRegistered = true;
}
@@ -1292,7 +1295,7 @@ public class ScanManager {
// infrequently anyway. To avoid redefining paramete sets, map to the low duty cycle
// parameter set as follows.
private int getBatchScanWindowMillis(int scanMode) {
- ContentResolver resolver = mService.getContentResolver();
+ ContentResolver resolver = mContext.getContentResolver();
switch (scanMode) {
case ScanSettings.SCAN_MODE_LOW_LATENCY:
return Settings.Global.getInt(
@@ -1310,7 +1313,7 @@ public class ScanManager {
}
private int getBatchScanIntervalMillis(int scanMode) {
- ContentResolver resolver = mService.getContentResolver();
+ ContentResolver resolver = mContext.getContentResolver();
switch (scanMode) {
case ScanSettings.SCAN_MODE_LOW_LATENCY:
return Settings.Global.getInt(
@@ -1357,8 +1360,8 @@ public class ScanManager {
Log.e(TAG, "Error freeing for onfound/onlost filter resources "
+ entriesToFree);
try {
- mService.onScanManagerErrorCallback(client.scannerId,
- ScanCallback.SCAN_FAILED_INTERNAL_ERROR);
+ mScanHelper.onScanManagerErrorCallback(
+ client.scannerId, ScanCallback.SCAN_FAILED_INTERNAL_ERROR);
} catch (RemoteException e) {
Log.e(TAG, "failed on onScanManagerCallback at freeing", e);
}
@@ -1480,7 +1483,7 @@ public class ScanManager {
mAlarmManager.cancel(mBatchScanIntervalIntent);
// Protect against multiple calls of cleanup.
if (mBatchAlarmReceiverRegistered) {
- mService.unregisterReceiver(mBatchAlarmReceiver);
+ mContext.unregisterReceiver(mBatchAlarmReceiver);
}
mBatchAlarmReceiverRegistered = false;
}
@@ -1547,8 +1550,8 @@ public class ScanManager {
+ trackEntries);
client.stats.recordTrackingHwFilterNotAvailableCountMetrics();
try {
- mService.onScanManagerErrorCallback(scannerId,
- ScanCallback.SCAN_FAILED_INTERNAL_ERROR);
+ mScanHelper.onScanManagerErrorCallback(
+ scannerId, ScanCallback.SCAN_FAILED_INTERNAL_ERROR);
} catch (RemoteException e) {
Log.e(TAG, "failed on onScanManagerCallback", e);
}
@@ -1706,7 +1709,7 @@ public class ScanManager {
}
private int getScanWindowMillis(ScanSettings settings) {
- ContentResolver resolver = mService.getContentResolver();
+ ContentResolver resolver = mContext.getContentResolver();
if (settings == null) {
return Settings.Global.getInt(
resolver,
@@ -1744,7 +1747,7 @@ public class ScanManager {
}
private int getScanIntervalMillis(ScanSettings settings) {
- ContentResolver resolver = mService.getContentResolver();
+ ContentResolver resolver = mContext.getContentResolver();
if (settings == null) {
return Settings.Global.getInt(
resolver,
@@ -1919,8 +1922,7 @@ public class ScanManager {
new ActivityManager.OnUidImportanceListener() {
@Override
public void onUidImportance(final int uid, final int importance) {
- if (mService.mTransitionalScanHelper.getScannerMap().getAppScanStatsByUid(uid)
- != null) {
+ if (mScanHelper.getScannerMap().getAppScanStatsByUid(uid) != null) {
Message message = new Message();
message.what = MSG_IMPORTANCE_CHANGE;
message.obj = new UidImportance(uid, importance);
diff --git a/android/app/src/com/android/bluetooth/le_scan/TransitionalScanHelper.java b/android/app/src/com/android/bluetooth/le_scan/TransitionalScanHelper.java
index 913cc40478..35cceca4b9 100644
--- a/android/app/src/com/android/bluetooth/le_scan/TransitionalScanHelper.java
+++ b/android/app/src/com/android/bluetooth/le_scan/TransitionalScanHelper.java
@@ -16,12 +16,63 @@
package com.android.bluetooth.le_scan;
+import static com.android.bluetooth.Utils.checkCallerTargetSdk;
+import static com.android.bluetooth.Utils.enforceBluetoothPrivilegedPermission;
+
+import android.annotation.RequiresPermission;
+import android.annotation.SuppressLint;
+import android.app.AppOpsManager;
+import android.app.PendingIntent;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.le.BluetoothLeScanner;
+import android.bluetooth.le.IPeriodicAdvertisingCallback;
import android.bluetooth.le.IScannerCallback;
+import android.bluetooth.le.ScanCallback;
+import android.bluetooth.le.ScanFilter;
+import android.bluetooth.le.ScanRecord;
+import android.bluetooth.le.ScanResult;
+import android.bluetooth.le.ScanSettings;
+import android.companion.AssociationInfo;
+import android.companion.CompanionDeviceManager;
+import android.content.AttributionSource;
+import android.content.Context;
+import android.content.Intent;
+import android.net.MacAddress;
+import android.os.Binder;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.os.WorkSource;
+import android.provider.DeviceConfig;
+import android.util.Log;
+import com.android.bluetooth.BluetoothMetricsProto;
+import com.android.bluetooth.R;
+import com.android.bluetooth.Utils;
+import com.android.bluetooth.btservice.AdapterService;
+import com.android.bluetooth.btservice.BluetoothAdapterProxy;
+import com.android.bluetooth.flags.Flags;
import com.android.bluetooth.gatt.ContextMap;
-import com.android.bluetooth.gatt.GattService;
+import com.android.bluetooth.gatt.GattObjectsFactory;
+import com.android.bluetooth.gatt.GattServiceConfig;
+import com.android.bluetooth.util.NumberUtils;
import com.android.internal.annotations.VisibleForTesting;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Predicate;
+
/**
* A helper class which contains all scan related functions extracted from {@link
* com.android.bluetooth.gatt.GattService}. The purpose of this class is to preserve scan
@@ -31,12 +82,102 @@ import com.android.internal.annotations.VisibleForTesting;
* @hide
*/
public class TransitionalScanHelper {
+ private static final boolean DBG = GattServiceConfig.DBG;
+ private static final boolean VDBG = GattServiceConfig.VDBG;
+ private static final String TAG = GattServiceConfig.TAG_PREFIX + "ScanHelper";
+
+ // Batch scan related constants.
+ private static final int TRUNCATED_RESULT_SIZE = 11;
+ private static final int TIME_STAMP_LENGTH = 2;
+
+ static final int SCAN_FILTER_ENABLED = 1;
+ static final int SCAN_FILTER_MODIFIED = 2;
+
+ /** The default floor value for LE batch scan report delays greater than 0 */
+ @VisibleForTesting static final long DEFAULT_REPORT_DELAY_FLOOR = 5000;
+
+ private static final int NUM_SCAN_EVENTS_KEPT = 20;
+
+ // onFoundLost related constants
+ private static final int ADVT_STATE_ONFOUND = 0;
+ private static final int ADVT_STATE_ONLOST = 1;
+
+ private static final int ET_LEGACY_MASK = 0x10;
/** List of our registered scanners. */
- public static class ScannerMap
- extends ContextMap<IScannerCallback, GattService.PendingIntentInfo> {}
+ // TODO(b/327849650): Remove as this class adds no value. Using generics this ways is considered
+ // an anti-pattern.
+ public static class ScannerMap extends ContextMap<IScannerCallback, PendingIntentInfo> {}
+
+ /** Keep the arguments passed in for the PendingIntent. */
+ public static class PendingIntentInfo {
+ public PendingIntent intent;
+ public ScanSettings settings;
+ public List<ScanFilter> filters;
+ public String callingPackage;
+ public int callingUid;
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof PendingIntentInfo)) {
+ return false;
+ }
+ return intent.equals(((PendingIntentInfo) other).intent);
+ }
+
+ @Override
+ public int hashCode() {
+ return intent == null ? 0 : intent.hashCode();
+ }
+ }
+
+ public interface TestModeAccessor {
+ /** Indicates if bluetooth test mode is enabled. */
+ boolean isTestModeEnabled();
+ }
+
+ private enum MatchOrigin {
+ PSEUDO_ADDRESS,
+ ORIGINAL_ADDRESS
+ }
+
+ private static class MatchResult {
+ private final boolean mMatches;
+ private final MatchOrigin mOrigin;
+
+ private MatchResult(boolean matches, MatchOrigin origin) {
+ this.mMatches = matches;
+ this.mOrigin = origin;
+ }
+
+ public boolean getMatches() {
+ return mMatches;
+ }
+
+ public MatchOrigin getMatchOrigin() {
+ return mOrigin;
+ }
+ }
+
+ private final PendingIntent.CancelListener mScanIntentCancelListener =
+ new PendingIntent.CancelListener() {
+ public void onCanceled(PendingIntent intent) {
+ Log.d(TAG, "scanning PendingIntent canceled");
+ stopScan(intent, mContext.getAttributionSource());
+ }
+ };
+
+ private Context mContext;
+ private TestModeAccessor mTestModeAccessor;
+
+ private AppOpsManager mAppOps;
+ private CompanionDeviceManager mCompanionManager;
+ private PeriodicScanManager mPeriodicScanManager;
+ private ScanManager mScanManager;
+ private AdapterService mAdapterService;
private ScannerMap mScannerMap = new ScannerMap();
+ private String mExposureNotificationPackage;
public ScannerMap getScannerMap() {
return mScannerMap;
@@ -46,4 +187,1475 @@ public class TransitionalScanHelper {
public void setScannerMap(ScannerMap scannerMap) {
mScannerMap = scannerMap;
}
+
+ /** Internal list of scan events to use with the proto */
+ private final ArrayDeque<BluetoothMetricsProto.ScanEvent> mScanEvents =
+ new ArrayDeque<>(NUM_SCAN_EVENTS_KEPT);
+
+ /** */
+ private final Predicate<ScanResult> mLocationDenylistPredicate =
+ (scanResult) -> {
+ final MacAddress parsedAddress =
+ MacAddress.fromString(scanResult.getDevice().getAddress());
+ if (mAdapterService.getLocationDenylistMac().test(parsedAddress.toByteArray())) {
+ Log.v(TAG, "Skipping device matching denylist: " + scanResult.getDevice());
+ return true;
+ }
+ final ScanRecord scanRecord = scanResult.getScanRecord();
+ if (scanRecord.matchesAnyField(
+ mAdapterService.getLocationDenylistAdvertisingData())) {
+ Log.v(TAG, "Skipping data matching denylist: " + scanRecord);
+ return true;
+ }
+ return false;
+ };
+
+ public TransitionalScanHelper(Context context, TestModeAccessor testModeAccessor) {
+ mContext = context;
+ mTestModeAccessor = testModeAccessor;
+ }
+
+ /**
+ * Starts the LE scanning component.
+ *
+ * @param looper for scan operations
+ */
+ public void start(Looper looper) {
+ mExposureNotificationPackage = mContext.getString(R.string.exposure_notification_package);
+ mAppOps = mContext.getSystemService(AppOpsManager.class);
+ mCompanionManager = mContext.getSystemService(CompanionDeviceManager.class);
+ mAdapterService = AdapterService.getAdapterService();
+ mScanManager =
+ GattObjectsFactory.getInstance()
+ .createScanManager(
+ mContext,
+ this,
+ mAdapterService,
+ BluetoothAdapterProxy.getInstance(),
+ looper);
+
+ mPeriodicScanManager =
+ GattObjectsFactory.getInstance().createPeriodicScanManager(mAdapterService);
+ }
+
+ /** Stops the scanning component. */
+ public void stop() {
+ mScannerMap.clear();
+ }
+
+ /** Cleans up the scanning component. */
+ public void cleanup() {
+ if (mScanManager != null) {
+ mScanManager.cleanup();
+ }
+ if (mPeriodicScanManager != null) {
+ mPeriodicScanManager.cleanup();
+ }
+ }
+
+ /** Notifies scan manager of bluetooth profile connection state changes */
+ public void notifyProfileConnectionStateChange(int profile, int fromState, int toState) {
+ if (mScanManager == null) {
+ Log.w(TAG, "scan manager is null");
+ return;
+ }
+ mScanManager.handleBluetoothProfileConnectionStateChanged(profile, fromState, toState);
+ }
+
+ public int getCurrentUsedTrackingAdvertisement() {
+ return mScanManager.getCurrentUsedTrackingAdvertisement();
+ }
+
+ /**************************************************************************
+ * Callback functions - CLIENT
+ *************************************************************************/
+
+ // EN format defined here:
+ // https://blog.google/documents/70/Exposure_Notification_-_Bluetooth_Specification_v1.2.2.pdf
+ private static final byte[] EXPOSURE_NOTIFICATION_FLAGS_PREAMBLE =
+ new byte[] {
+ // size 2, flag field, flags byte (value is not important)
+ (byte) 0x02, (byte) 0x01
+ };
+
+ private static final int EXPOSURE_NOTIFICATION_FLAGS_LENGTH = 0x2 + 1;
+ private static final byte[] EXPOSURE_NOTIFICATION_PAYLOAD_PREAMBLE =
+ new byte[] {
+ // size 3, complete 16 bit UUID, EN UUID
+ (byte) 0x03, (byte) 0x03, (byte) 0x6F, (byte) 0xFD,
+ // size 23, data for 16 bit UUID, EN UUID
+ (byte) 0x17, (byte) 0x16, (byte) 0x6F, (byte) 0xFD,
+ // ...payload
+ };
+ private static final int EXPOSURE_NOTIFICATION_PAYLOAD_LENGTH = 0x03 + 0x17 + 2;
+
+ private static boolean arrayStartsWith(byte[] array, byte[] prefix) {
+ if (array.length < prefix.length) {
+ return false;
+ }
+ for (int i = 0; i < prefix.length; i++) {
+ if (prefix[i] != array[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private ScanResult getSanitizedExposureNotification(ScanResult result) {
+ ScanRecord record = result.getScanRecord();
+ // Remove the flags part of the payload, if present
+ if (record.getBytes().length > EXPOSURE_NOTIFICATION_FLAGS_LENGTH
+ && arrayStartsWith(record.getBytes(), EXPOSURE_NOTIFICATION_FLAGS_PREAMBLE)) {
+ record =
+ ScanRecord.parseFromBytes(
+ Arrays.copyOfRange(
+ record.getBytes(),
+ EXPOSURE_NOTIFICATION_FLAGS_LENGTH,
+ record.getBytes().length));
+ }
+
+ if (record.getBytes().length != EXPOSURE_NOTIFICATION_PAYLOAD_LENGTH) {
+ return null;
+ }
+ if (!arrayStartsWith(record.getBytes(), EXPOSURE_NOTIFICATION_PAYLOAD_PREAMBLE)) {
+ return null;
+ }
+
+ return new ScanResult(null, 0, 0, 0, 0, 0, result.getRssi(), 0, record, 0);
+ }
+
+ /** Callback method for a scan result. */
+ public void onScanResult(
+ int eventType,
+ int addressType,
+ String address,
+ int primaryPhy,
+ int secondaryPhy,
+ int advertisingSid,
+ int txPower,
+ int rssi,
+ int periodicAdvInt,
+ byte[] advData,
+ String originalAddress) {
+ // When in testing mode, ignore all real-world events
+ if (mTestModeAccessor.isTestModeEnabled()) return;
+
+ AppScanStats.recordScanRadioResultCount();
+ onScanResultInternal(
+ eventType,
+ addressType,
+ address,
+ primaryPhy,
+ secondaryPhy,
+ advertisingSid,
+ txPower,
+ rssi,
+ periodicAdvInt,
+ advData,
+ originalAddress);
+ }
+
+ // TODO(b/327849650): Refactor to reduce the visibility of this method.
+ public void onScanResultInternal(
+ int eventType,
+ int addressType,
+ String address,
+ int primaryPhy,
+ int secondaryPhy,
+ int advertisingSid,
+ int txPower,
+ int rssi,
+ int periodicAdvInt,
+ byte[] advData,
+ String originalAddress) {
+ if (VDBG) {
+ Log.d(
+ TAG,
+ "onScanResult() - eventType=0x"
+ + Integer.toHexString(eventType)
+ + ", addressType="
+ + addressType
+ + ", address="
+ + address
+ + ", primaryPhy="
+ + primaryPhy
+ + ", secondaryPhy="
+ + secondaryPhy
+ + ", advertisingSid=0x"
+ + Integer.toHexString(advertisingSid)
+ + ", txPower="
+ + txPower
+ + ", rssi="
+ + rssi
+ + ", periodicAdvInt=0x"
+ + Integer.toHexString(periodicAdvInt)
+ + ", originalAddress="
+ + originalAddress);
+ }
+
+ String identityAddress = mAdapterService.getIdentityAddress(address);
+ if (!address.equals(identityAddress)) {
+ if (VDBG) {
+ Log.d(
+ TAG,
+ "found identityAddress of "
+ + address
+ + ", replace originalAddress as "
+ + identityAddress);
+ }
+ originalAddress = identityAddress;
+ }
+
+ byte[] legacyAdvData = Arrays.copyOfRange(advData, 0, 62);
+
+ for (ScanClient client : mScanManager.getRegularScanQueue()) {
+ ScannerMap.App app = mScannerMap.getById(client.scannerId);
+ if (app == null) {
+ if (VDBG) {
+ Log.d(TAG, "App is null; skip.");
+ }
+ continue;
+ }
+
+ BluetoothDevice device =
+ BluetoothAdapter.getDefaultAdapter().getRemoteLeDevice(address, addressType);
+
+ ScanSettings settings = client.settings;
+ byte[] scanRecordData;
+ // This is for compatibility with applications that assume fixed size scan data.
+ if (settings.getLegacy()) {
+ if ((eventType & ET_LEGACY_MASK) == 0) {
+ // If this is legacy scan, but nonlegacy result - skip.
+ if (VDBG) {
+ Log.d(TAG, "Legacy scan, non legacy result; skip.");
+ }
+ continue;
+ } else {
+ // Some apps are used to fixed-size advertise data.
+ scanRecordData = legacyAdvData;
+ }
+ } else {
+ scanRecordData = advData;
+ }
+
+ ScanRecord scanRecord = ScanRecord.parseFromBytes(scanRecordData);
+ ScanResult result =
+ new ScanResult(
+ device,
+ eventType,
+ primaryPhy,
+ secondaryPhy,
+ advertisingSid,
+ txPower,
+ rssi,
+ periodicAdvInt,
+ scanRecord,
+ SystemClock.elapsedRealtimeNanos());
+
+ if (client.hasDisavowedLocation) {
+ if (mLocationDenylistPredicate.test(result)) {
+ Log.i(TAG, "Skipping client for location deny list");
+ continue;
+ }
+ }
+
+ boolean hasPermission = hasScanResultPermission(client);
+ if (!hasPermission) {
+ for (String associatedDevice : client.associatedDevices) {
+ if (associatedDevice.equalsIgnoreCase(address)) {
+ hasPermission = true;
+ break;
+ }
+ }
+ }
+ if (!hasPermission && client.eligibleForSanitizedExposureNotification) {
+ ScanResult sanitized = getSanitizedExposureNotification(result);
+ if (sanitized != null) {
+ hasPermission = true;
+ result = sanitized;
+ }
+ }
+ MatchResult matchResult = matchesFilters(client, result, originalAddress);
+ if (!hasPermission || !matchResult.getMatches()) {
+ if (VDBG) {
+ Log.d(
+ TAG,
+ "Skipping client: permission="
+ + hasPermission
+ + " matches="
+ + matchResult.getMatches());
+ }
+ continue;
+ }
+
+ int callbackType = settings.getCallbackType();
+ if (!(callbackType == ScanSettings.CALLBACK_TYPE_ALL_MATCHES
+ || callbackType == ScanSettings.CALLBACK_TYPE_ALL_MATCHES_AUTO_BATCH)) {
+ if (VDBG) {
+ Log.d(TAG, "Skipping client: CALLBACK_TYPE_ALL_MATCHES");
+ }
+ continue;
+ }
+
+ try {
+ app.appScanStats.addResult(client.scannerId);
+ if (app.callback != null) {
+ app.callback.onScanResult(result);
+ } else {
+ // Send the PendingIntent
+ ArrayList<ScanResult> results = new ArrayList<>();
+ results.add(result);
+ sendResultsByPendingIntent(
+ app.info, results, ScanSettings.CALLBACK_TYPE_ALL_MATCHES);
+ }
+ } catch (RemoteException | PendingIntent.CanceledException e) {
+ Log.e(TAG, "Exception: " + e);
+ if (Flags.leScanFixRemoteException()) {
+ handleDeadScanClient(client);
+ } else {
+ mScannerMap.remove(client.scannerId);
+ mScanManager.stopScan(client.scannerId);
+ }
+ }
+ }
+ }
+
+ private void sendResultByPendingIntent(
+ PendingIntentInfo pii, ScanResult result, int callbackType, ScanClient client) {
+ ArrayList<ScanResult> results = new ArrayList<>();
+ results.add(result);
+ try {
+ sendResultsByPendingIntent(pii, results, callbackType);
+ } catch (PendingIntent.CanceledException e) {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ stopScan(client.scannerId, mContext.getAttributionSource());
+ unregisterScanner(client.scannerId, mContext.getAttributionSource());
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+ }
+
+ private void sendResultsByPendingIntent(
+ PendingIntentInfo pii, ArrayList<ScanResult> results, int callbackType)
+ throws PendingIntent.CanceledException {
+ Intent extrasIntent = new Intent();
+ extrasIntent.putParcelableArrayListExtra(
+ BluetoothLeScanner.EXTRA_LIST_SCAN_RESULT, results);
+ extrasIntent.putExtra(BluetoothLeScanner.EXTRA_CALLBACK_TYPE, callbackType);
+ pii.intent.send(mContext, 0, extrasIntent);
+ }
+
+ private void sendErrorByPendingIntent(PendingIntentInfo pii, int errorCode)
+ throws PendingIntent.CanceledException {
+ Intent extrasIntent = new Intent();
+ extrasIntent.putExtra(BluetoothLeScanner.EXTRA_ERROR_CODE, errorCode);
+ pii.intent.send(mContext, 0, extrasIntent);
+ }
+
+ /** Callback method for scanner registration. */
+ public void onScannerRegistered(int status, int scannerId, long uuidLsb, long uuidMsb)
+ throws RemoteException {
+ UUID uuid = new UUID(uuidMsb, uuidLsb);
+ if (DBG) {
+ Log.d(
+ TAG,
+ "onScannerRegistered() - UUID="
+ + uuid
+ + ", scannerId="
+ + scannerId
+ + ", status="
+ + status);
+ }
+
+ // First check the callback map
+ ScannerMap.App cbApp = mScannerMap.getByUuid(uuid);
+ if (cbApp != null) {
+ if (status == 0) {
+ cbApp.id = scannerId;
+ // If app is callback based, setup a death recipient. App will initiate the start.
+ // Otherwise, if PendingIntent based, start the scan directly.
+ if (cbApp.callback != null) {
+ cbApp.linkToDeath(new ScannerDeathRecipient(scannerId, cbApp.name));
+ } else {
+ continuePiStartScan(scannerId, cbApp);
+ }
+ } else {
+ mScannerMap.remove(scannerId);
+ }
+ if (cbApp.callback != null) {
+ cbApp.callback.onScannerRegistered(status, scannerId);
+ }
+ }
+ }
+
+ /** Determines if the given scan client has the appropriate permissions to receive callbacks. */
+ private boolean hasScanResultPermission(final ScanClient client) {
+ if (client.hasNetworkSettingsPermission
+ || client.hasNetworkSetupWizardPermission
+ || client.hasScanWithoutLocationPermission) {
+ return true;
+ }
+ if (client.hasDisavowedLocation) {
+ return true;
+ }
+ return client.hasLocationPermission
+ && !Utils.blockedByLocationOff(mContext, client.userHandle);
+ }
+
+ // Check if a scan record matches a specific filters.
+ private MatchResult matchesFilters(ScanClient client, ScanResult scanResult) {
+ return matchesFilters(client, scanResult, null);
+ }
+
+ // Check if a scan record matches a specific filters or original address
+ private MatchResult matchesFilters(
+ ScanClient client, ScanResult scanResult, String originalAddress) {
+ if (client.filters == null || client.filters.isEmpty()) {
+ // TODO: Do we really wanna return true here?
+ return new MatchResult(true, MatchOrigin.PSEUDO_ADDRESS);
+ }
+ for (ScanFilter filter : client.filters) {
+ // Need to check the filter matches, and the original address without changing the API
+ if (filter.matches(scanResult)) {
+ return new MatchResult(true, MatchOrigin.PSEUDO_ADDRESS);
+ }
+ if (originalAddress != null
+ && originalAddress.equalsIgnoreCase(filter.getDeviceAddress())) {
+ return new MatchResult(true, MatchOrigin.ORIGINAL_ADDRESS);
+ }
+ }
+ return new MatchResult(false, MatchOrigin.PSEUDO_ADDRESS);
+ }
+
+ private void handleDeadScanClient(ScanClient client) {
+ if (client.appDied) {
+ Log.w(TAG, "Already dead client " + client.scannerId);
+ return;
+ }
+ client.appDied = true;
+ stopScan(client.scannerId, mContext.getAttributionSource());
+ }
+
+ /** Callback method for scan filter enablement/disablement. */
+ public void onScanFilterEnableDisabled(int action, int status, int clientIf) {
+ if (DBG) {
+ Log.d(
+ TAG,
+ "onScanFilterEnableDisabled() - clientIf="
+ + clientIf
+ + ", status="
+ + status
+ + ", action="
+ + action);
+ }
+ mScanManager.callbackDone(clientIf, status);
+ }
+
+ /** Callback method for configuration of scan filter params. */
+ public void onScanFilterParamsConfigured(
+ int action, int status, int clientIf, int availableSpace) {
+ if (DBG) {
+ Log.d(
+ TAG,
+ "onScanFilterParamsConfigured() - clientIf="
+ + clientIf
+ + ", status="
+ + status
+ + ", action="
+ + action
+ + ", availableSpace="
+ + availableSpace);
+ }
+ mScanManager.callbackDone(clientIf, status);
+ }
+
+ /** Callback method for configuration of scan filter. */
+ public void onScanFilterConfig(
+ int action, int status, int clientIf, int filterType, int availableSpace) {
+ if (DBG) {
+ Log.d(
+ TAG,
+ "onScanFilterConfig() - clientIf="
+ + clientIf
+ + ", action = "
+ + action
+ + " status = "
+ + status
+ + ", filterType="
+ + filterType
+ + ", availableSpace="
+ + availableSpace);
+ }
+
+ mScanManager.callbackDone(clientIf, status);
+ }
+
+ /** Callback method for configuration of batch scan storage. */
+ public void onBatchScanStorageConfigured(int status, int clientIf) {
+ if (DBG) {
+ Log.d(
+ TAG,
+ "onBatchScanStorageConfigured() - clientIf=" + clientIf + ", status=" + status);
+ }
+ mScanManager.callbackDone(clientIf, status);
+ }
+
+ /** Callback method for start/stop of batch scan. */
+ // TODO: split into two different callbacks : onBatchScanStarted and onBatchScanStopped.
+ public void onBatchScanStartStopped(int startStopAction, int status, int clientIf) {
+ if (DBG) {
+ Log.d(
+ TAG,
+ "onBatchScanStartStopped() - clientIf="
+ + clientIf
+ + ", status="
+ + status
+ + ", startStopAction="
+ + startStopAction);
+ }
+ mScanManager.callbackDone(clientIf, status);
+ }
+
+ ScanClient findBatchScanClientById(int scannerId) {
+ for (ScanClient client : mScanManager.getBatchScanQueue()) {
+ if (client.scannerId == scannerId) {
+ return client;
+ }
+ }
+ return null;
+ }
+
+ /** Callback method for batch scan reports */
+ public void onBatchScanReports(
+ int status, int scannerId, int reportType, int numRecords, byte[] recordData)
+ throws RemoteException {
+ // When in testing mode, ignore all real-world events
+ if (mTestModeAccessor.isTestModeEnabled()) return;
+
+ AppScanStats.recordBatchScanRadioResultCount(numRecords);
+ onBatchScanReportsInternal(status, scannerId, reportType, numRecords, recordData);
+ }
+
+ @VisibleForTesting
+ void onBatchScanReportsInternal(
+ int status, int scannerId, int reportType, int numRecords, byte[] recordData)
+ throws RemoteException {
+ if (DBG) {
+ Log.d(
+ TAG,
+ "onBatchScanReports() - scannerId="
+ + scannerId
+ + ", status="
+ + status
+ + ", reportType="
+ + reportType
+ + ", numRecords="
+ + numRecords);
+ }
+
+ Set<ScanResult> results = parseBatchScanResults(numRecords, reportType, recordData);
+ if (reportType == ScanManager.SCAN_RESULT_TYPE_TRUNCATED) {
+ // We only support single client for truncated mode.
+ ScannerMap.App app = mScannerMap.getById(scannerId);
+ if (app == null) {
+ return;
+ }
+
+ ScanClient client = findBatchScanClientById(scannerId);
+ if (client == null) {
+ return;
+ }
+
+ ArrayList<ScanResult> permittedResults;
+ if (hasScanResultPermission(client)) {
+ permittedResults = new ArrayList<ScanResult>(results);
+ } else {
+ permittedResults = new ArrayList<ScanResult>();
+ for (ScanResult scanResult : results) {
+ for (String associatedDevice : client.associatedDevices) {
+ if (associatedDevice.equalsIgnoreCase(
+ scanResult.getDevice().getAddress())) {
+ permittedResults.add(scanResult);
+ }
+ }
+ }
+ if (permittedResults.isEmpty()) {
+ return;
+ }
+ }
+
+ if (client.hasDisavowedLocation) {
+ permittedResults.removeIf(mLocationDenylistPredicate);
+ }
+
+ if (app.callback != null) {
+ app.callback.onBatchScanResults(permittedResults);
+ } else {
+ // PendingIntent based
+ try {
+ sendResultsByPendingIntent(
+ app.info, permittedResults, ScanSettings.CALLBACK_TYPE_ALL_MATCHES);
+ } catch (PendingIntent.CanceledException e) {
+ }
+ }
+ } else {
+ for (ScanClient client : mScanManager.getFullBatchScanQueue()) {
+ // Deliver results for each client.
+ deliverBatchScan(client, results);
+ }
+ }
+ mScanManager.callbackDone(scannerId, status);
+ }
+
+ private void sendBatchScanResults(
+ ScannerMap.App app, ScanClient client, ArrayList<ScanResult> results) {
+ try {
+ if (app.callback != null) {
+ if (mScanManager.isAutoBatchScanClientEnabled(client)) {
+ if (DBG) {
+ Log.d(TAG, "sendBatchScanResults() to onScanResult()" + client);
+ }
+ for (ScanResult result : results) {
+ app.appScanStats.addResult(client.scannerId);
+ app.callback.onScanResult(result);
+ }
+ } else {
+ if (DBG) {
+ Log.d(TAG, "sendBatchScanResults() to onBatchScanResults()" + client);
+ }
+ app.callback.onBatchScanResults(results);
+ }
+ } else {
+ sendResultsByPendingIntent(
+ app.info, results, ScanSettings.CALLBACK_TYPE_ALL_MATCHES);
+ }
+ } catch (RemoteException | PendingIntent.CanceledException e) {
+ Log.e(TAG, "Exception: " + e);
+ if (Flags.leScanFixRemoteException()) {
+ handleDeadScanClient(client);
+ } else {
+ mScannerMap.remove(client.scannerId);
+ mScanManager.stopScan(client.scannerId);
+ }
+ }
+ }
+
+ // Check and deliver scan results for different scan clients.
+ private void deliverBatchScan(ScanClient client, Set<ScanResult> allResults)
+ throws RemoteException {
+ ContextMap.App app = mScannerMap.getById(client.scannerId);
+ if (app == null) {
+ return;
+ }
+
+ ArrayList<ScanResult> permittedResults;
+ if (hasScanResultPermission(client)) {
+ permittedResults = new ArrayList<ScanResult>(allResults);
+ } else {
+ permittedResults = new ArrayList<ScanResult>();
+ for (ScanResult scanResult : allResults) {
+ for (String associatedDevice : client.associatedDevices) {
+ if (associatedDevice.equalsIgnoreCase(scanResult.getDevice().getAddress())) {
+ permittedResults.add(scanResult);
+ }
+ }
+ }
+ if (permittedResults.isEmpty()) {
+ return;
+ }
+ }
+
+ if (client.filters == null || client.filters.isEmpty()) {
+ sendBatchScanResults(app, client, permittedResults);
+ // TODO: Question to reviewer: Shouldn't there be a return here?
+ }
+ // Reconstruct the scan results.
+ ArrayList<ScanResult> results = new ArrayList<ScanResult>();
+ for (ScanResult scanResult : permittedResults) {
+ if (matchesFilters(client, scanResult).getMatches()) {
+ results.add(scanResult);
+ }
+ }
+ sendBatchScanResults(app, client, results);
+ }
+
+ private Set<ScanResult> parseBatchScanResults(
+ int numRecords, int reportType, byte[] batchRecord) {
+ if (numRecords == 0) {
+ return Collections.emptySet();
+ }
+ if (DBG) {
+ Log.d(TAG, "current time is " + SystemClock.elapsedRealtimeNanos());
+ }
+ if (reportType == ScanManager.SCAN_RESULT_TYPE_TRUNCATED) {
+ return parseTruncatedResults(numRecords, batchRecord);
+ } else {
+ return parseFullResults(numRecords, batchRecord);
+ }
+ }
+
+ private Set<ScanResult> parseTruncatedResults(int numRecords, byte[] batchRecord) {
+ if (DBG) {
+ Log.d(TAG, "batch record " + Arrays.toString(batchRecord));
+ }
+ Set<ScanResult> results = new HashSet<ScanResult>(numRecords);
+ long now = SystemClock.elapsedRealtimeNanos();
+ for (int i = 0; i < numRecords; ++i) {
+ byte[] record =
+ extractBytes(batchRecord, i * TRUNCATED_RESULT_SIZE, TRUNCATED_RESULT_SIZE);
+ byte[] address = extractBytes(record, 0, 6);
+ reverse(address);
+ BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
+ int rssi = record[8];
+ long timestampNanos = now - parseTimestampNanos(extractBytes(record, 9, 2));
+ results.add(
+ new ScanResult(
+ device, ScanRecord.parseFromBytes(new byte[0]), rssi, timestampNanos));
+ }
+ return results;
+ }
+
+ @VisibleForTesting
+ long parseTimestampNanos(byte[] data) {
+ long timestampUnit = NumberUtils.littleEndianByteArrayToInt(data);
+ // Timestamp is in every 50 ms.
+ return TimeUnit.MILLISECONDS.toNanos(timestampUnit * 50);
+ }
+
+ private Set<ScanResult> parseFullResults(int numRecords, byte[] batchRecord) {
+ if (DBG) {
+ Log.d(TAG, "Batch record : " + Arrays.toString(batchRecord));
+ }
+ Set<ScanResult> results = new HashSet<ScanResult>(numRecords);
+ int position = 0;
+ long now = SystemClock.elapsedRealtimeNanos();
+ while (position < batchRecord.length) {
+ byte[] address = extractBytes(batchRecord, position, 6);
+ // TODO: remove temp hack.
+ reverse(address);
+ BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
+ position += 6;
+ // Skip address type.
+ position++;
+ // Skip tx power level.
+ position++;
+ int rssi = batchRecord[position++];
+ long timestampNanos = now - parseTimestampNanos(extractBytes(batchRecord, position, 2));
+ position += 2;
+
+ // Combine advertise packet and scan response packet.
+ int advertisePacketLen = batchRecord[position++];
+ byte[] advertiseBytes = extractBytes(batchRecord, position, advertisePacketLen);
+ position += advertisePacketLen;
+ int scanResponsePacketLen = batchRecord[position++];
+ byte[] scanResponseBytes = extractBytes(batchRecord, position, scanResponsePacketLen);
+ position += scanResponsePacketLen;
+ byte[] scanRecord = new byte[advertisePacketLen + scanResponsePacketLen];
+ System.arraycopy(advertiseBytes, 0, scanRecord, 0, advertisePacketLen);
+ System.arraycopy(
+ scanResponseBytes, 0, scanRecord, advertisePacketLen, scanResponsePacketLen);
+ if (DBG) {
+ Log.d(TAG, "ScanRecord : " + Arrays.toString(scanRecord));
+ }
+ results.add(
+ new ScanResult(
+ device, ScanRecord.parseFromBytes(scanRecord), rssi, timestampNanos));
+ }
+ return results;
+ }
+
+ // Reverse byte array.
+ private void reverse(byte[] address) {
+ int len = address.length;
+ for (int i = 0; i < len / 2; ++i) {
+ byte b = address[i];
+ address[i] = address[len - 1 - i];
+ address[len - 1 - i] = b;
+ }
+ }
+
+ // Helper method to extract bytes from byte array.
+ private static byte[] extractBytes(byte[] scanRecord, int start, int length) {
+ byte[] bytes = new byte[length];
+ System.arraycopy(scanRecord, start, bytes, 0, length);
+ return bytes;
+ }
+
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ public void onBatchScanThresholdCrossed(int clientIf) {
+ if (DBG) {
+ Log.d(TAG, "onBatchScanThresholdCrossed() - clientIf=" + clientIf);
+ }
+ flushPendingBatchResults(clientIf, mContext.getAttributionSource());
+ }
+
+ public AdvtFilterOnFoundOnLostInfo createOnTrackAdvFoundLostObject(
+ int clientIf,
+ int advPktLen,
+ byte[] advPkt,
+ int scanRspLen,
+ byte[] scanRsp,
+ int filtIndex,
+ int advState,
+ int advInfoPresent,
+ String address,
+ int addrType,
+ int txPower,
+ int rssiValue,
+ int timeStamp) {
+
+ return new AdvtFilterOnFoundOnLostInfo(
+ clientIf,
+ advPktLen,
+ advPkt,
+ scanRspLen,
+ scanRsp,
+ filtIndex,
+ advState,
+ advInfoPresent,
+ address,
+ addrType,
+ txPower,
+ rssiValue,
+ timeStamp);
+ }
+
+ public void onTrackAdvFoundLost(AdvtFilterOnFoundOnLostInfo trackingInfo)
+ throws RemoteException {
+ if (DBG) {
+ Log.d(
+ TAG,
+ "onTrackAdvFoundLost() - scannerId= "
+ + trackingInfo.getClientIf()
+ + " address = "
+ + trackingInfo.getAddress()
+ + " adv_state = "
+ + trackingInfo.getAdvState());
+ }
+
+ ScannerMap.App app = mScannerMap.getById(trackingInfo.getClientIf());
+ if (app == null || (app.callback == null && app.info == null)) {
+ Log.e(TAG, "app or callback is null");
+ return;
+ }
+
+ BluetoothDevice device =
+ BluetoothAdapter.getDefaultAdapter().getRemoteDevice(trackingInfo.getAddress());
+ int advertiserState = trackingInfo.getAdvState();
+ ScanResult result =
+ new ScanResult(
+ device,
+ ScanRecord.parseFromBytes(trackingInfo.getResult()),
+ trackingInfo.getRSSIValue(),
+ SystemClock.elapsedRealtimeNanos());
+
+ for (ScanClient client : mScanManager.getRegularScanQueue()) {
+ if (client.scannerId == trackingInfo.getClientIf()) {
+ ScanSettings settings = client.settings;
+ if ((advertiserState == ADVT_STATE_ONFOUND)
+ && ((settings.getCallbackType() & ScanSettings.CALLBACK_TYPE_FIRST_MATCH)
+ != 0)) {
+ if (app.callback != null) {
+ app.callback.onFoundOrLost(true, result);
+ } else {
+ sendResultByPendingIntent(
+ app.info, result, ScanSettings.CALLBACK_TYPE_FIRST_MATCH, client);
+ }
+ } else if ((advertiserState == ADVT_STATE_ONLOST)
+ && ((settings.getCallbackType() & ScanSettings.CALLBACK_TYPE_MATCH_LOST)
+ != 0)) {
+ if (app.callback != null) {
+ app.callback.onFoundOrLost(false, result);
+ } else {
+ sendResultByPendingIntent(
+ app.info, result, ScanSettings.CALLBACK_TYPE_MATCH_LOST, client);
+ }
+ } else {
+ if (DBG) {
+ Log.d(
+ TAG,
+ "Not reporting onlost/onfound : "
+ + advertiserState
+ + " scannerId = "
+ + client.scannerId
+ + " callbackType "
+ + settings.getCallbackType());
+ }
+ }
+ }
+ }
+ }
+
+ public void onScanParamSetupCompleted(int status, int scannerId) throws RemoteException {
+ ContextMap.App app = mScannerMap.getById(scannerId);
+ if (app == null || app.callback == null) {
+ Log.e(TAG, "Advertise app or callback is null");
+ return;
+ }
+ if (DBG) {
+ Log.d(TAG, "onScanParamSetupCompleted : " + status);
+ }
+ }
+
+ // callback from ScanManager for dispatch of errors apps.
+ public void onScanManagerErrorCallback(int scannerId, int errorCode) throws RemoteException {
+ ScannerMap.App app = mScannerMap.getById(scannerId);
+ if (app == null || (app.callback == null && app.info == null)) {
+ Log.e(TAG, "App or callback is null");
+ return;
+ }
+ if (app.callback != null) {
+ app.callback.onScanManagerErrorCallback(errorCode);
+ } else {
+ try {
+ sendErrorByPendingIntent(app.info, errorCode);
+ } catch (PendingIntent.CanceledException e) {
+ Log.e(TAG, "Error sending error code via PendingIntent:" + e);
+ }
+ }
+ }
+
+ /**************************************************************************
+ * GATT Service functions - Shared CLIENT/SERVER
+ *************************************************************************/
+
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ public void registerScanner(
+ IScannerCallback callback, WorkSource workSource, AttributionSource attributionSource)
+ throws RemoteException {
+ if (!Utils.checkScanPermissionForDataDelivery(
+ mContext, attributionSource, "ScanHelper registerScanner")) {
+ return;
+ }
+
+ UUID uuid = UUID.randomUUID();
+ if (DBG) {
+ Log.d(TAG, "registerScanner() - UUID=" + uuid);
+ }
+
+ enforceImpersonatationPermissionIfNeeded(workSource);
+
+ AppScanStats app = mScannerMap.getAppScanStatsByUid(Binder.getCallingUid());
+ if (app != null
+ && app.isScanningTooFrequently()
+ && !Utils.checkCallerHasPrivilegedPermission(mContext)) {
+ Log.e(TAG, "App '" + app.appName + "' is scanning too frequently");
+ callback.onScannerRegistered(ScanCallback.SCAN_FAILED_SCANNING_TOO_FREQUENTLY, -1);
+ return;
+ }
+
+ mScannerMap.add(uuid, workSource, callback, null, mContext, this);
+ mScanManager.registerScanner(uuid);
+ }
+
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ public void unregisterScanner(int scannerId, AttributionSource attributionSource) {
+ if (!Utils.checkScanPermissionForDataDelivery(
+ mContext, attributionSource, "ScanHelper unregisterScanner")) {
+ return;
+ }
+
+ if (DBG) {
+ Log.d(TAG, "unregisterScanner() - scannerId=" + scannerId);
+ }
+ mScannerMap.remove(scannerId);
+ mScanManager.unregisterScanner(scannerId);
+ }
+
+ private List<String> getAssociatedDevices(String callingPackage) {
+ if (mCompanionManager == null) {
+ return Collections.emptyList();
+ }
+
+ List<String> macAddresses = new ArrayList();
+
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ for (AssociationInfo info : Utils.getCdmAssociations(mCompanionManager)) {
+ if (info.getPackageName().equals(callingPackage)
+ && !info.isSelfManaged()
+ && info.getDeviceMacAddress() != null) {
+ macAddresses.add(info.getDeviceMacAddress().toString());
+ }
+ }
+ } catch (SecurityException se) {
+ // Not an app with associated devices
+ } catch (Exception e) {
+ Log.e(TAG, "Cannot check device associations for " + callingPackage, e);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ return macAddresses;
+ }
+
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ public void startScan(
+ int scannerId,
+ ScanSettings settings,
+ List<ScanFilter> filters,
+ AttributionSource attributionSource) {
+ if (DBG) {
+ Log.d(TAG, "start scan with filters");
+ }
+
+ if (!Utils.checkScanPermissionForDataDelivery(
+ mContext, attributionSource, "Starting GATT scan.")) {
+ return;
+ }
+
+ enforcePrivilegedPermissionIfNeeded(settings);
+ String callingPackage = attributionSource.getPackageName();
+ settings = enforceReportDelayFloor(settings);
+ enforcePrivilegedPermissionIfNeeded(filters);
+ final ScanClient scanClient = new ScanClient(scannerId, settings, filters);
+ scanClient.userHandle = Binder.getCallingUserHandle();
+ mAppOps.checkPackage(Binder.getCallingUid(), callingPackage);
+ scanClient.eligibleForSanitizedExposureNotification =
+ callingPackage.equals(mExposureNotificationPackage);
+
+ scanClient.hasDisavowedLocation =
+ Utils.hasDisavowedLocationForScan(
+ mContext, attributionSource, mTestModeAccessor.isTestModeEnabled());
+
+ scanClient.isQApp = checkCallerTargetSdk(mContext, callingPackage, Build.VERSION_CODES.Q);
+ if (!scanClient.hasDisavowedLocation) {
+ if (scanClient.isQApp) {
+ scanClient.hasLocationPermission =
+ Utils.checkCallerHasFineLocation(
+ mContext, attributionSource, scanClient.userHandle);
+ } else {
+ scanClient.hasLocationPermission =
+ Utils.checkCallerHasCoarseOrFineLocation(
+ mContext, attributionSource, scanClient.userHandle);
+ }
+ }
+ scanClient.hasNetworkSettingsPermission =
+ Utils.checkCallerHasNetworkSettingsPermission(mContext);
+ scanClient.hasNetworkSetupWizardPermission =
+ Utils.checkCallerHasNetworkSetupWizardPermission(mContext);
+ scanClient.hasScanWithoutLocationPermission =
+ Utils.checkCallerHasScanWithoutLocationPermission(mContext);
+ scanClient.associatedDevices = getAssociatedDevices(callingPackage);
+
+ AppScanStats app = mScannerMap.getAppScanStatsById(scannerId);
+ ContextMap.App cbApp = mScannerMap.getById(scannerId);
+ if (app != null) {
+ scanClient.stats = app;
+ boolean isFilteredScan = (filters != null) && !filters.isEmpty();
+ boolean isCallbackScan = false;
+ if (cbApp != null) {
+ isCallbackScan = cbApp.callback != null;
+ }
+ app.recordScanStart(settings, filters, isFilteredScan, isCallbackScan, scannerId);
+ }
+
+ mScanManager.startScan(scanClient);
+ }
+
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ public void registerPiAndStartScan(
+ PendingIntent pendingIntent,
+ ScanSettings settings,
+ List<ScanFilter> filters,
+ AttributionSource attributionSource) {
+ if (DBG) {
+ Log.d(TAG, "start scan with filters, for PendingIntent");
+ }
+
+ if (!Utils.checkScanPermissionForDataDelivery(
+ mContext, attributionSource, "Starting GATT scan.")) {
+ return;
+ }
+ enforcePrivilegedPermissionIfNeeded(settings);
+ settings = enforceReportDelayFloor(settings);
+ enforcePrivilegedPermissionIfNeeded(filters);
+ UUID uuid = UUID.randomUUID();
+ String callingPackage = attributionSource.getPackageName();
+ int callingUid = attributionSource.getUid();
+ PendingIntentInfo piInfo = new PendingIntentInfo();
+ piInfo.intent = pendingIntent;
+ piInfo.settings = settings;
+ piInfo.filters = filters;
+ piInfo.callingPackage = callingPackage;
+ piInfo.callingUid = callingUid;
+ if (DBG) {
+ Log.d(
+ TAG,
+ "startScan(PI) -"
+ + (" UUID=" + uuid)
+ + (" Package=" + callingPackage)
+ + (" UID=" + callingUid));
+ }
+
+ // Don't start scan if the Pi scan already in mScannerMap.
+ if (mScannerMap.getByContextInfo(piInfo) != null) {
+ Log.d(TAG, "Don't startScan(PI) since the same Pi scan already in mScannerMap.");
+ return;
+ }
+
+ ContextMap.App app = mScannerMap.add(uuid, null, null, piInfo, mContext, this);
+
+ app.mUserHandle = UserHandle.getUserHandleForUid(Binder.getCallingUid());
+ mAppOps.checkPackage(Binder.getCallingUid(), callingPackage);
+ app.mEligibleForSanitizedExposureNotification =
+ callingPackage.equals(mExposureNotificationPackage);
+
+ app.mHasDisavowedLocation =
+ Utils.hasDisavowedLocationForScan(
+ mContext, attributionSource, mTestModeAccessor.isTestModeEnabled());
+
+ if (!app.mHasDisavowedLocation) {
+ try {
+ if (checkCallerTargetSdk(mContext, callingPackage, Build.VERSION_CODES.Q)) {
+ app.hasLocationPermission =
+ Utils.checkCallerHasFineLocation(
+ mContext, attributionSource, app.mUserHandle);
+ } else {
+ app.hasLocationPermission =
+ Utils.checkCallerHasCoarseOrFineLocation(
+ mContext, attributionSource, app.mUserHandle);
+ }
+ } catch (SecurityException se) {
+ // No need to throw here. Just mark as not granted.
+ app.hasLocationPermission = false;
+ }
+ }
+ app.mHasNetworkSettingsPermission = Utils.checkCallerHasNetworkSettingsPermission(mContext);
+ app.mHasNetworkSetupWizardPermission =
+ Utils.checkCallerHasNetworkSetupWizardPermission(mContext);
+ app.mHasScanWithoutLocationPermission =
+ Utils.checkCallerHasScanWithoutLocationPermission(mContext);
+ app.mAssociatedDevices = getAssociatedDevices(callingPackage);
+ mScanManager.registerScanner(uuid);
+
+ // If this fails, we should stop the scan immediately.
+ if (!pendingIntent.addCancelListener(Runnable::run, mScanIntentCancelListener)) {
+ Log.d(TAG, "scanning PendingIntent is already cancelled, stopping scan.");
+ stopScan(pendingIntent, attributionSource);
+ }
+ }
+
+ public void continuePiStartScan(int scannerId, ScannerMap.App app) {
+ final PendingIntentInfo piInfo = app.info;
+ final ScanClient scanClient =
+ new ScanClient(scannerId, piInfo.settings, piInfo.filters, piInfo.callingUid);
+ scanClient.hasLocationPermission = app.hasLocationPermission;
+ scanClient.userHandle = app.mUserHandle;
+ scanClient.isQApp = checkCallerTargetSdk(mContext, app.name, Build.VERSION_CODES.Q);
+ scanClient.eligibleForSanitizedExposureNotification =
+ app.mEligibleForSanitizedExposureNotification;
+ scanClient.hasNetworkSettingsPermission = app.mHasNetworkSettingsPermission;
+ scanClient.hasNetworkSetupWizardPermission = app.mHasNetworkSetupWizardPermission;
+ scanClient.hasScanWithoutLocationPermission = app.mHasScanWithoutLocationPermission;
+ scanClient.associatedDevices = app.mAssociatedDevices;
+ scanClient.hasDisavowedLocation = app.mHasDisavowedLocation;
+
+ AppScanStats scanStats = mScannerMap.getAppScanStatsById(scannerId);
+ if (scanStats != null) {
+ scanClient.stats = scanStats;
+ boolean isFilteredScan = (piInfo.filters != null) && !piInfo.filters.isEmpty();
+ scanStats.recordScanStart(
+ piInfo.settings, piInfo.filters, isFilteredScan, false, scannerId);
+ }
+
+ mScanManager.startScan(scanClient);
+ }
+
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ public void flushPendingBatchResults(int scannerId, AttributionSource attributionSource) {
+ if (!Utils.checkScanPermissionForDataDelivery(
+ mContext, attributionSource, "ScanHelper flushPendingBatchResults")) {
+ return;
+ }
+ if (DBG) {
+ Log.d(TAG, "flushPendingBatchResults - scannerId=" + scannerId);
+ }
+ mScanManager.flushBatchScanResults(new ScanClient(scannerId));
+ }
+
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ public void stopScan(int scannerId, AttributionSource attributionSource) {
+ if (!Utils.checkScanPermissionForDataDelivery(
+ mContext, attributionSource, "ScanHelper stopScan")) {
+ return;
+ }
+ int scanQueueSize =
+ mScanManager.getBatchScanQueue().size() + mScanManager.getRegularScanQueue().size();
+ if (DBG) {
+ Log.d(TAG, "stopScan() - queue size =" + scanQueueSize);
+ }
+
+ AppScanStats app = null;
+ app = mScannerMap.getAppScanStatsById(scannerId);
+ if (app != null) {
+ app.recordScanStop(scannerId);
+ }
+
+ mScanManager.stopScan(scannerId);
+ }
+
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ public void stopScan(PendingIntent intent, AttributionSource attributionSource) {
+ if (!Utils.checkScanPermissionForDataDelivery(
+ mContext, attributionSource, "ScanHelper stopScan")) {
+ return;
+ }
+ PendingIntentInfo pii = new PendingIntentInfo();
+ pii.intent = intent;
+ ContextMap.App app = mScannerMap.getByContextInfo(pii);
+ if (VDBG) {
+ Log.d(TAG, "stopScan(PendingIntent): app found = " + app);
+ }
+ if (app != null) {
+ intent.removeCancelListener(mScanIntentCancelListener);
+ final int scannerId = app.id;
+ stopScan(scannerId, attributionSource);
+ // Also unregister the scanner
+ unregisterScanner(scannerId, attributionSource);
+ }
+ }
+
+ /**************************************************************************
+ * PERIODIC SCANNING
+ *************************************************************************/
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ public void registerSync(
+ ScanResult scanResult,
+ int skip,
+ int timeout,
+ IPeriodicAdvertisingCallback callback,
+ AttributionSource attributionSource) {
+ if (!Utils.checkScanPermissionForDataDelivery(
+ mContext, attributionSource, "ScanHelper registerSync")) {
+ return;
+ }
+ mPeriodicScanManager.startSync(scanResult, skip, timeout, callback);
+ }
+
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ public void unregisterSync(
+ IPeriodicAdvertisingCallback callback, AttributionSource attributionSource) {
+ if (!Utils.checkScanPermissionForDataDelivery(
+ mContext, attributionSource, "ScanHelper unregisterSync")) {
+ return;
+ }
+ mPeriodicScanManager.stopSync(callback);
+ }
+
+ public void transferSync(
+ BluetoothDevice bda,
+ int serviceData,
+ int syncHandle,
+ AttributionSource attributionSource) {
+ if (!Utils.checkScanPermissionForDataDelivery(
+ mContext, attributionSource, "ScanHelper transferSync")) {
+ return;
+ }
+ mPeriodicScanManager.transferSync(bda, serviceData, syncHandle);
+ }
+
+ public void transferSetInfo(
+ BluetoothDevice bda,
+ int serviceData,
+ int advHandle,
+ IPeriodicAdvertisingCallback callback,
+ AttributionSource attributionSource) {
+ if (!Utils.checkScanPermissionForDataDelivery(
+ mContext, attributionSource, "ScanHelper transferSetInfo")) {
+ return;
+ }
+ mPeriodicScanManager.transferSetInfo(bda, serviceData, advHandle, callback);
+ }
+
+ /**
+ * DeathRecipient handler used to unregister applications that disconnect ungracefully (ie.
+ * crash or forced close).
+ */
+ class ScannerDeathRecipient implements IBinder.DeathRecipient {
+ int mScannerId;
+ private String mPackageName;
+
+ ScannerDeathRecipient(int scannerId, String packageName) {
+ mScannerId = scannerId;
+ mPackageName = packageName;
+ }
+
+ @Override
+ public void binderDied() {
+ if (DBG) {
+ Log.d(
+ TAG,
+ "Binder is dead - unregistering scanner ("
+ + mPackageName
+ + " "
+ + mScannerId
+ + ")!");
+ }
+
+ ScanClient client = getScanClient(mScannerId);
+ if (client != null) {
+ if (Flags.leScanFixRemoteException()) {
+ handleDeadScanClient(client);
+ } else {
+ client.appDied = true;
+ stopScan(client.scannerId, mContext.getAttributionSource());
+ }
+ }
+ }
+
+ private ScanClient getScanClient(int clientIf) {
+ for (ScanClient client : mScanManager.getRegularScanQueue()) {
+ if (client.scannerId == clientIf) {
+ return client;
+ }
+ }
+ for (ScanClient client : mScanManager.getBatchScanQueue()) {
+ if (client.scannerId == clientIf) {
+ return client;
+ }
+ }
+ return null;
+ }
+ }
+
+ private boolean needsPrivilegedPermissionForScan(ScanSettings settings) {
+ // BLE scan only mode needs special permission.
+ if (mAdapterService.getState() != BluetoothAdapter.STATE_ON) {
+ return true;
+ }
+
+ // Regular scan, no special permission.
+ if (settings == null) {
+ return false;
+ }
+
+ // Ambient discovery mode, needs privileged permission.
+ if (settings.getScanMode() == ScanSettings.SCAN_MODE_AMBIENT_DISCOVERY) {
+ return true;
+ }
+
+ // Regular scan, no special permission.
+ if (settings.getReportDelayMillis() == 0) {
+ return false;
+ }
+
+ // Batch scan, truncated mode needs permission.
+ return settings.getScanResultType() == ScanSettings.SCAN_RESULT_TYPE_ABBREVIATED;
+ }
+
+ /*
+ * The {@link ScanFilter#setDeviceAddress} API overloads are @SystemApi access methods. This
+ * requires that the permissions be BLUETOOTH_PRIVILEGED.
+ */
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ private void enforcePrivilegedPermissionIfNeeded(List<ScanFilter> filters) {
+ if (DBG) {
+ Log.d(TAG, "enforcePrivilegedPermissionIfNeeded(" + filters + ")");
+ }
+ // Some 3p API cases may have null filters, need to allow
+ if (filters != null) {
+ for (ScanFilter filter : filters) {
+ // The only case to enforce here is if there is an address
+ // If there is an address, enforce if the correct combination criteria is met.
+ if (filter.getDeviceAddress() != null) {
+ // At this point we have an address, that means a caller used the
+ // setDeviceAddress(address) public API for the ScanFilter
+ // We don't want to enforce if the type is PUBLIC and the IRK is null
+ // However, if we have a different type that means the caller used a new
+ // @SystemApi such as setDeviceAddress(address, type) or
+ // setDeviceAddress(address, type, irk) which are both @SystemApi and require
+ // permissions to be enforced
+ if (filter.getAddressType() == BluetoothDevice.ADDRESS_TYPE_PUBLIC
+ && filter.getIrk() == null) {
+ // Do not enforce
+ } else {
+ enforceBluetoothPrivilegedPermission(mContext);
+ }
+ }
+ }
+ }
+ }
+
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ private void enforcePrivilegedPermissionIfNeeded(ScanSettings settings) {
+ if (needsPrivilegedPermissionForScan(settings)) {
+ enforceBluetoothPrivilegedPermission(mContext);
+ }
+ }
+
+ // Enforce caller has UPDATE_DEVICE_STATS permission, which allows the caller to blame other
+ // apps for Bluetooth usage. A {@link SecurityException} will be thrown if the caller app does
+ // not have UPDATE_DEVICE_STATS permission.
+ @RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS)
+ private void enforceImpersonatationPermission() {
+ mContext.enforceCallingOrSelfPermission(
+ android.Manifest.permission.UPDATE_DEVICE_STATS,
+ "Need UPDATE_DEVICE_STATS permission");
+ }
+
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ private void enforceImpersonatationPermissionIfNeeded(WorkSource workSource) {
+ if (workSource != null) {
+ enforceImpersonatationPermission();
+ }
+ }
+
+ /**
+ * Ensures the report delay is either 0 or at least the floor value (5000ms)
+ *
+ * @param settings are the scan settings passed into a request to start le scanning
+ * @return the passed in ScanSettings object if the report delay is 0 or above the floor value;
+ * a new ScanSettings object with the report delay being the floor value if the original
+ * report delay was between 0 and the floor value (exclusive of both)
+ */
+ @VisibleForTesting
+ ScanSettings enforceReportDelayFloor(ScanSettings settings) {
+ if (settings.getReportDelayMillis() == 0) {
+ return settings;
+ }
+
+ // Need to clear identity to pass device config permission check
+ final long callerToken = Binder.clearCallingIdentity();
+ try {
+ long floor =
+ DeviceConfig.getLong(
+ DeviceConfig.NAMESPACE_BLUETOOTH,
+ "report_delay",
+ DEFAULT_REPORT_DELAY_FLOOR);
+
+ if (settings.getReportDelayMillis() > floor) {
+ return settings;
+ } else {
+ return new ScanSettings.Builder()
+ .setCallbackType(settings.getCallbackType())
+ .setLegacy(settings.getLegacy())
+ .setMatchMode(settings.getMatchMode())
+ .setNumOfMatches(settings.getNumOfMatches())
+ .setPhy(settings.getPhy())
+ .setReportDelay(floor)
+ .setScanMode(settings.getScanMode())
+ .setScanResultType(settings.getScanResultType())
+ .build();
+ }
+ } finally {
+ Binder.restoreCallingIdentity(callerToken);
+ }
+ }
+
+ public void addScanEvent(BluetoothMetricsProto.ScanEvent event) {
+ synchronized (mScanEvents) {
+ if (mScanEvents.size() == NUM_SCAN_EVENTS_KEPT) {
+ mScanEvents.remove();
+ }
+ mScanEvents.add(event);
+ }
+ }
+
+ public void dumpProto(BluetoothMetricsProto.BluetoothLog.Builder builder) {
+ synchronized (mScanEvents) {
+ builder.addAllScanEvent(mScanEvents);
+ }
+ }
}
diff --git a/android/app/tests/unit/src/com/android/bluetooth/gatt/ContextMapTest.java b/android/app/tests/unit/src/com/android/bluetooth/gatt/ContextMapTest.java
index 2771e728b0..d29e96d781 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/gatt/ContextMapTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/gatt/ContextMapTest.java
@@ -35,6 +35,7 @@ import androidx.test.runner.AndroidJUnit4;
import com.android.bluetooth.BluetoothMethodProxy;
import com.android.bluetooth.TestUtils;
import com.android.bluetooth.btservice.AdapterService;
+import com.android.bluetooth.le_scan.TransitionalScanHelper;
import org.junit.After;
import org.junit.Before;
@@ -64,6 +65,7 @@ public class ContextMapTest {
@Mock private AdapterService mAdapterService;
@Mock private AppAdvertiseStats appAdvertiseStats;
@Mock private GattService mMockGatt;
+ @Mock private TransitionalScanHelper mMockScanHelper;
@Mock private PackageManager mMockPackageManager;
@Spy
@@ -93,7 +95,7 @@ public class ContextMapTest {
int id = 12345;
contextMap.add(id, null, mMockGatt);
- contextMap.add(UUID.randomUUID(), null, null, null, mMockGatt);
+ contextMap.add(UUID.randomUUID(), null, null, null, mMockGatt, mMockScanHelper);
int appUid = Binder.getCallingUid();
@@ -171,7 +173,7 @@ public class ContextMapTest {
int id = 12345;
contextMap.add(id, null, mMockGatt);
- contextMap.add(UUID.randomUUID(), null, null, null, mMockGatt);
+ contextMap.add(UUID.randomUUID(), null, null, null, mMockGatt, mMockScanHelper);
contextMap.recordAdvertiseStop(id);
diff --git a/android/app/tests/unit/src/com/android/bluetooth/gatt/GattServiceBinderTest.java b/android/app/tests/unit/src/com/android/bluetooth/gatt/GattServiceBinderTest.java
index a829a7d6b3..0bec207562 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/gatt/GattServiceBinderTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/gatt/GattServiceBinderTest.java
@@ -46,6 +46,7 @@ import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
+import com.android.bluetooth.le_scan.TransitionalScanHelper;
import com.android.bluetooth.x.com.android.modules.utils.SynchronousResultReceiver;
import org.junit.Before;
@@ -70,6 +71,7 @@ public class GattServiceBinderTest {
@Mock
private GattService mService;
+ @Mock private TransitionalScanHelper mScanHelper;
private Context mContext;
private BluetoothDevice mDevice;
@@ -85,6 +87,7 @@ public class GattServiceBinderTest {
mPendingIntent = PendingIntent.getBroadcast(mContext, 0, intent,
PendingIntent.FLAG_IMMUTABLE);
when(mService.isAvailable()).thenReturn(true);
+ when(mService.getTransitionalScanHelper()).thenReturn(mScanHelper);
mBinder = new GattService.BluetoothGattBinder(mService);
mAttributionSource = new AttributionSource.Builder(1).build();
mDevice = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(REMOTE_DEVICE_ADDRESS);
@@ -129,7 +132,7 @@ public class GattServiceBinderTest {
mBinder.registerScanner(callback, workSource, mAttributionSource,
SynchronousResultReceiver.get());
- verify(mService).registerScanner(callback, workSource, mAttributionSource);
+ verify(mScanHelper).registerScanner(callback, workSource, mAttributionSource);
}
@Test
@@ -138,7 +141,7 @@ public class GattServiceBinderTest {
mBinder.unregisterScanner(scannerId, mAttributionSource, SynchronousResultReceiver.get());
- verify(mService).unregisterScanner(scannerId, mAttributionSource);
+ verify(mScanHelper).unregisterScanner(scannerId, mAttributionSource);
}
@Test
@@ -150,7 +153,7 @@ public class GattServiceBinderTest {
mBinder.startScan(scannerId, settings, filters, mAttributionSource,
SynchronousResultReceiver.get());
- verify(mService).startScan(scannerId, settings, filters, mAttributionSource);
+ verify(mScanHelper).startScan(scannerId, settings, filters, mAttributionSource);
}
@Test
@@ -161,8 +164,8 @@ public class GattServiceBinderTest {
mBinder.startScanForIntent(mPendingIntent, settings, filters, mAttributionSource,
SynchronousResultReceiver.get());
- verify(mService).registerPiAndStartScan(mPendingIntent, settings, filters,
- mAttributionSource);
+ verify(mScanHelper)
+ .registerPiAndStartScan(mPendingIntent, settings, filters, mAttributionSource);
}
@Test
@@ -170,7 +173,7 @@ public class GattServiceBinderTest {
mBinder.stopScanForIntent(mPendingIntent, mAttributionSource,
SynchronousResultReceiver.get());
- verify(mService).stopScan(mPendingIntent, mAttributionSource);
+ verify(mScanHelper).stopScan(mPendingIntent, mAttributionSource);
}
@Test
@@ -179,7 +182,7 @@ public class GattServiceBinderTest {
mBinder.stopScan(scannerId, mAttributionSource, SynchronousResultReceiver.get());
- verify(mService).stopScan(scannerId, mAttributionSource);
+ verify(mScanHelper).stopScan(scannerId, mAttributionSource);
}
@Test
@@ -189,7 +192,7 @@ public class GattServiceBinderTest {
mBinder.flushPendingBatchResults(scannerId, mAttributionSource,
SynchronousResultReceiver.get());
- verify(mService).flushPendingBatchResults(scannerId, mAttributionSource);
+ verify(mScanHelper).flushPendingBatchResults(scannerId, mAttributionSource);
}
@Test
@@ -713,7 +716,7 @@ public class GattServiceBinderTest {
mBinder.registerSync(scanResult, skip, timeout, callback,
mAttributionSource, SynchronousResultReceiver.get());
- verify(mService).registerSync(scanResult, skip, timeout, callback, mAttributionSource);
+ verify(mScanHelper).registerSync(scanResult, skip, timeout, callback, mAttributionSource);
}
@Test
@@ -724,7 +727,7 @@ public class GattServiceBinderTest {
mBinder.transferSync(mDevice, serviceData, syncHandle,
mAttributionSource, SynchronousResultReceiver.get());
- verify(mService).transferSync(mDevice, serviceData, syncHandle, mAttributionSource);
+ verify(mScanHelper).transferSync(mDevice, serviceData, syncHandle, mAttributionSource);
}
@Test
@@ -736,8 +739,8 @@ public class GattServiceBinderTest {
mBinder.transferSetInfo(mDevice, serviceData, advHandle, callback,
mAttributionSource, SynchronousResultReceiver.get());
- verify(mService).transferSetInfo(mDevice, serviceData, advHandle, callback,
- mAttributionSource);
+ verify(mScanHelper)
+ .transferSetInfo(mDevice, serviceData, advHandle, callback, mAttributionSource);
}
@Test
@@ -746,7 +749,7 @@ public class GattServiceBinderTest {
mBinder.unregisterSync(callback, mAttributionSource, SynchronousResultReceiver.get());
- verify(mService).unregisterSync(callback, mAttributionSource);
+ verify(mScanHelper).unregisterSync(callback, mAttributionSource);
}
@Test
diff --git a/android/app/tests/unit/src/com/android/bluetooth/gatt/GattServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/gatt/GattServiceTest.java
index 6603ef9d33..50083b6d96 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/gatt/GattServiceTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/gatt/GattServiceTest.java
@@ -33,18 +33,11 @@ import android.bluetooth.le.AdvertisingSetParameters;
import android.bluetooth.le.DistanceMeasurementMethod;
import android.bluetooth.le.DistanceMeasurementParams;
import android.bluetooth.le.IDistanceMeasurementCallback;
-import android.bluetooth.le.IPeriodicAdvertisingCallback;
-import android.bluetooth.le.IScannerCallback;
import android.bluetooth.le.PeriodicAdvertisingParameters;
-import android.bluetooth.le.ScanResult;
-import android.bluetooth.le.ScanSettings;
import android.content.AttributionSource;
import android.content.Context;
import android.content.res.Resources;
import android.location.LocationManager;
-import android.os.Binder;
-import android.os.RemoteException;
-import android.os.WorkSource;
import android.platform.test.flag.junit.SetFlagsRule;
import androidx.test.InstrumentationRegistry;
@@ -55,27 +48,21 @@ import androidx.test.runner.AndroidJUnit4;
import com.android.bluetooth.TestUtils;
import com.android.bluetooth.btservice.AdapterService;
import com.android.bluetooth.btservice.CompanionManager;
-import com.android.bluetooth.le_scan.AppScanStats;
-import com.android.bluetooth.le_scan.PeriodicScanManager;
-import com.android.bluetooth.le_scan.ScanClient;
import com.android.bluetooth.le_scan.ScanManager;
import com.android.bluetooth.le_scan.TransitionalScanHelper;
import com.android.bluetooth.flags.Flags;
import org.junit.After;
-import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.mockito.ArgumentMatcher;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -105,8 +92,6 @@ public class GattServiceTest {
@Mock
private TransitionalScanHelper.ScannerMap.App mApp;
- @Mock private GattService.PendingIntentInfo mPiInfo;
- @Mock private PeriodicScanManager mPeriodicScanManager;
@Mock private ScanManager mScanManager;
@Mock private Set<String> mReliableQueue;
@Mock private GattService.ServerMap mServerMap;
@@ -135,8 +120,7 @@ public class GattServiceTest {
GattObjectsFactory.setInstanceForTesting(mFactory);
doReturn(mNativeInterface).when(mFactory).getNativeInterface();
- doReturn(mScanManager).when(mFactory).createScanManager(any(), any(), any(), any());
- doReturn(mPeriodicScanManager).when(mFactory).createPeriodicScanManager(any());
+ doReturn(mScanManager).when(mFactory).createScanManager(any(), any(), any(), any(), any());
doReturn(mDistanceMeasurementManager).when(mFactory)
.createDistanceMeasurementManager(any());
@@ -192,14 +176,6 @@ public class GattServiceTest {
}
@Test
- public void testParseBatchTimestamp() {
- long timestampNanos = mService.parseTimestampNanos(new byte[]{
- -54, 7
- });
- Assert.assertEquals(99700000000L, timestampNanos);
- }
-
- @Test
public void emptyClearServices() {
int serverIf = 1;
@@ -263,81 +239,6 @@ public class GattServiceTest {
}
@Test
- public void continuePiStartScan() {
- int scannerId = 1;
-
- mPiInfo.settings = new ScanSettings.Builder().build();
- mApp.info = mPiInfo;
-
- AppScanStats appScanStats = mock(AppScanStats.class);
- doReturn(appScanStats).when(mScannerMap).getAppScanStatsById(scannerId);
-
- mService.continuePiStartScan(scannerId, mApp);
-
- verify(appScanStats).recordScanStart(
- mPiInfo.settings, mPiInfo.filters, false, false, scannerId);
- verify(mScanManager).startScan(any());
- }
-
- @Test
- public void continuePiStartScanCheckUid() {
- int scannerId = 1;
-
- mPiInfo.settings = new ScanSettings.Builder().build();
- mPiInfo.callingUid = 123;
- mApp.info = mPiInfo;
-
- AppScanStats appScanStats = mock(AppScanStats.class);
- doReturn(appScanStats).when(mScannerMap).getAppScanStatsById(scannerId);
-
- mService.continuePiStartScan(scannerId, mApp);
-
- verify(appScanStats)
- .recordScanStart(mPiInfo.settings, mPiInfo.filters, false, false, scannerId);
- verify(mScanManager)
- .startScan(
- argThat(
- new ArgumentMatcher<ScanClient>() {
- @Override
- public boolean matches(ScanClient client) {
- return mPiInfo.callingUid == client.appUid;
- }
- }));
- }
-
- @Test
- public void onBatchScanReportsInternal_deliverBatchScan() throws RemoteException {
- int status = 1;
- int scannerId = 2;
- int reportType = ScanManager.SCAN_RESULT_TYPE_FULL;
- int numRecords = 1;
- byte[] recordData = new byte[]{0x01, 0x02, 0x03, 0x04, 0x05,
- 0x06, 0x07, 0x08, 0x09, 0x00, 0x00, 0x00, 0x00};
-
- Set<ScanClient> scanClientSet = new HashSet<>();
- ScanClient scanClient = new ScanClient(scannerId);
- scanClient.associatedDevices = new ArrayList<>();
- scanClient.associatedDevices.add("02:00:00:00:00:00");
- scanClient.scannerId = scannerId;
- scanClientSet.add(scanClient);
- doReturn(scanClientSet).when(mScanManager).getFullBatchScanQueue();
- doReturn(mApp).when(mScannerMap).getById(scanClient.scannerId);
-
- mService.onBatchScanReportsInternal(status, scannerId, reportType, numRecords, recordData);
- verify(mScanManager).callbackDone(scannerId, status);
-
- reportType = ScanManager.SCAN_RESULT_TYPE_TRUNCATED;
- recordData = new byte[]{0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
- 0x06, 0x04, 0x02, 0x02, 0x00, 0x00, 0x02};
- doReturn(scanClientSet).when(mScanManager).getBatchScanQueue();
- IScannerCallback callback = mock(IScannerCallback.class);
- mApp.callback = callback;
-
- mService.onBatchScanReportsInternal(status, scannerId, reportType, numRecords, recordData);
- verify(callback).onBatchScanResults(any());
- }
-
- @Test
public void clientConnect() throws Exception {
int clientIf = 1;
String address = REMOTE_DEVICE_ADDRESS;
@@ -369,28 +270,6 @@ public class GattServiceTest {
}
@Test
- public void enforceReportDelayFloor() {
- long reportDelayFloorHigher = GattService.DEFAULT_REPORT_DELAY_FLOOR + 1;
- ScanSettings scanSettings = new ScanSettings.Builder()
- .setReportDelay(reportDelayFloorHigher)
- .build();
-
- ScanSettings newScanSettings = mService.enforceReportDelayFloor(scanSettings);
-
- assertThat(newScanSettings.getReportDelayMillis())
- .isEqualTo(scanSettings.getReportDelayMillis());
-
- ScanSettings scanSettingsFloor = new ScanSettings.Builder()
- .setReportDelay(1)
- .build();
-
- ScanSettings newScanSettingsFloor = mService.enforceReportDelayFloor(scanSettingsFloor);
-
- assertThat(newScanSettingsFloor.getReportDelayMillis())
- .isEqualTo(GattService.DEFAULT_REPORT_DELAY_FLOOR);
- }
-
- @Test
public void setAdvertisingData() {
int advertiserId = 1;
AdvertiseData data = new AdvertiseData.Builder().build();
@@ -483,84 +362,6 @@ public class GattServiceTest {
}
@Test
- public void registerScanner() throws Exception {
- IScannerCallback callback = mock(IScannerCallback.class);
- WorkSource workSource = mock(WorkSource.class);
-
- AppScanStats appScanStats = mock(AppScanStats.class);
- doReturn(appScanStats).when(mScannerMap).getAppScanStatsByUid(Binder.getCallingUid());
-
- mService.registerScanner(callback, workSource, mAttributionSource);
- verify(mScannerMap).add(any(), eq(workSource), eq(callback), eq(null), eq(mService));
- verify(mScanManager).registerScanner(any());
- }
-
- @Test
- public void flushPendingBatchResults() {
- int scannerId = 3;
-
- mService.flushPendingBatchResults(scannerId, mAttributionSource);
- verify(mScanManager).flushBatchScanResults(new ScanClient(scannerId));
- }
-
- @Test
- public void onScanResult_remoteException_clientDied() throws Exception {
- mSetFlagsRule.enableFlags(Flags.FLAG_LE_SCAN_FIX_REMOTE_EXCEPTION);
- int scannerId = 1;
-
- int eventType = 0;
- int addressType = 0;
- String address = "02:00:00:00:00:00";
- int primaryPhy = 0;
- int secondPhy = 0;
- int advertisingSid = 0;
- int txPower = 0;
- int rssi = 0;
- int periodicAdvInt = 0;
- byte[] advData = new byte[0];
-
- ScanClient scanClient = new ScanClient(scannerId);
- scanClient.scannerId = scannerId;
- scanClient.hasNetworkSettingsPermission = true;
- scanClient.settings =
- new ScanSettings.Builder()
- .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
- .setLegacy(false)
- .build();
-
- AppScanStats appScanStats = mock(AppScanStats.class);
- IScannerCallback callback = mock(IScannerCallback.class);
-
- mApp.callback = callback;
- mApp.appScanStats = appScanStats;
- Set<ScanClient> scanClientSet = Collections.singleton(scanClient);
-
- doReturn(address).when(mAdapterService).getIdentityAddress(anyString());
- doReturn(scanClientSet).when(mScanManager).getRegularScanQueue();
- doReturn(mApp).when(mScannerMap).getById(scanClient.scannerId);
- doReturn(appScanStats).when(mScannerMap).getAppScanStatsById(scanClient.scannerId);
-
- // Simulate remote client crash
- doThrow(new RemoteException()).when(callback).onScanResult(any());
-
- mService.onScanResult(
- eventType,
- addressType,
- address,
- primaryPhy,
- secondPhy,
- advertisingSid,
- txPower,
- rssi,
- periodicAdvInt,
- advData,
- address);
-
- assertThat(scanClient.appDied).isTrue();
- verify(appScanStats).recordScanStop(scannerId);
- }
-
- @Test
public void readCharacteristic() {
int clientIf = 1;
String address = REMOTE_DEVICE_ADDRESS;
@@ -791,45 +592,6 @@ public class GattServiceTest {
}
@Test
- public void registerSync() {
- ScanResult scanResult = new ScanResult(mDevice, 1, 2, 3, 4, 5, 6, 7, null, 8);
- int skip = 1;
- int timeout = 2;
- IPeriodicAdvertisingCallback callback = mock(IPeriodicAdvertisingCallback.class);
-
- mService.registerSync(scanResult, skip, timeout, callback, mAttributionSource);
- verify(mPeriodicScanManager).startSync(scanResult, skip, timeout, callback);
- }
-
- @Test
- public void transferSync() {
- int serviceData = 1;
- int syncHandle = 2;
-
- mService.transferSync(mDevice, serviceData, syncHandle, mAttributionSource);
- verify(mPeriodicScanManager).transferSync(mDevice, serviceData, syncHandle);
- }
-
- @Test
- public void transferSetInfo() {
- int serviceData = 1;
- int advHandle = 2;
- IPeriodicAdvertisingCallback callback = mock(IPeriodicAdvertisingCallback.class);
-
- mService.transferSetInfo(mDevice, serviceData, advHandle, callback,
- mAttributionSource);
- verify(mPeriodicScanManager).transferSetInfo(mDevice, serviceData, advHandle, callback);
- }
-
- @Test
- public void unregisterSync() {
- IPeriodicAdvertisingCallback callback = mock(IPeriodicAdvertisingCallback.class);
-
- mService.unregisterSync(callback, mAttributionSource);
- verify(mPeriodicScanManager).stopSync(callback);
- }
-
- @Test
public void unregAll() throws Exception {
int appId = 1;
List<Integer> appIds = new ArrayList<>();
diff --git a/android/app/tests/unit/src/com/android/bluetooth/gatt/AdvtFilterOnFoundOnLostInfoTest.java b/android/app/tests/unit/src/com/android/bluetooth/le_scan/AdvtFilterOnFoundOnLostInfoTest.java
index 3eade702b5..674278dee7 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/gatt/AdvtFilterOnFoundOnLostInfoTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/le_scan/AdvtFilterOnFoundOnLostInfoTest.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.bluetooth.gatt;
+package com.android.bluetooth.le_scan;
import static com.google.common.truth.Truth.assertThat;
diff --git a/android/app/tests/unit/src/com/android/bluetooth/le_scan/AppScanStatsTest.java b/android/app/tests/unit/src/com/android/bluetooth/le_scan/AppScanStatsTest.java
index da6196f257..d7b92e0ec6 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/le_scan/AppScanStatsTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/le_scan/AppScanStatsTest.java
@@ -33,7 +33,6 @@ import androidx.test.runner.AndroidJUnit4;
import com.android.bluetooth.TestUtils;
import com.android.bluetooth.btservice.AdapterService;
import com.android.bluetooth.gatt.ContextMap;
-import com.android.bluetooth.gatt.GattService;
import com.android.internal.app.IBatteryStats;
import org.junit.After;
@@ -63,7 +62,8 @@ public class AppScanStatsTest {
@Mock
private ContextMap map;
- @Mock private GattService mMockGatt;
+ @Mock private Context mMockContext;
+ @Mock private TransitionalScanHelper mMockScanHelper;
@Mock private AdapterService mAdapterService;
// BatteryStatsManager is final and cannot be mocked with regular mockito, so just mock the
@@ -77,7 +77,7 @@ public class AppScanStatsTest {
TestUtils.setAdapterService(mAdapterService);
TestUtils.mockGetSystemService(
- mMockGatt,
+ mMockContext,
Context.BATTERY_STATS_SERVICE,
BatteryStatsManager.class,
mBatteryStatsManager);
@@ -93,10 +93,11 @@ public class AppScanStatsTest {
String name = "appName";
WorkSource source = null;
- AppScanStats appScanStats = new AppScanStats(name, source, map, mMockGatt);
+ AppScanStats appScanStats =
+ new AppScanStats(name, source, map, mMockContext, mMockScanHelper);
assertThat(appScanStats.mContextMap).isEqualTo(map);
- assertThat(appScanStats.mGattService).isEqualTo(mMockGatt);
+ assertThat(appScanStats.mScanHelper).isEqualTo(mMockScanHelper);
assertThat(appScanStats.isScanning()).isEqualTo(false);
}
@@ -106,7 +107,8 @@ public class AppScanStatsTest {
String name = "appName";
WorkSource source = null;
- AppScanStats appScanStats = new AppScanStats(name, source, map, mMockGatt);
+ AppScanStats appScanStats =
+ new AppScanStats(name, source, map, mMockContext, mMockScanHelper);
ScanSettings settings = new ScanSettings.Builder().build();
List<ScanFilter> filters = new ArrayList<>();
diff --git a/android/app/tests/unit/src/com/android/bluetooth/le_scan/ScanManagerTest.java b/android/app/tests/unit/src/com/android/bluetooth/le_scan/ScanManagerTest.java
index 6177139ca4..b1804031b9 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/le_scan/ScanManagerTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/le_scan/ScanManagerTest.java
@@ -130,6 +130,7 @@ public class ScanManagerTest {
@Mock private AdapterService mAdapterService;
@Mock private GattService mMockGattService;
+ @Mock private TransitionalScanHelper mMockScanHelper;
@Mock private BluetoothAdapterProxy mBluetoothAdapterProxy;
@Mock private LocationManager mLocationManager;
@Spy private GattObjectsFactory mFactory = GattObjectsFactory.getInstance();
@@ -199,6 +200,7 @@ public class ScanManagerTest {
mScanManager =
new ScanManager(
mMockGattService,
+ mMockScanHelper,
mAdapterService,
mBluetoothAdapterProxy,
mTestLooper.getLooper());
@@ -210,7 +212,8 @@ public class ScanManagerTest {
assertThat(mLatch).isNotNull();
mScanReportDelay = DEFAULT_SCAN_REPORT_DELAY_MS;
- mMockAppScanStats = spy(new AppScanStats("Test", null, null, mMockGattService));
+ mMockAppScanStats =
+ spy(new AppScanStats("Test", null, null, mMockGattService, mMockScanHelper));
}
@After
diff --git a/android/app/tests/unit/src/com/android/bluetooth/le_scan/TransitionalScanHelperTest.java b/android/app/tests/unit/src/com/android/bluetooth/le_scan/TransitionalScanHelperTest.java
new file mode 100644
index 0000000000..45aff753b0
--- /dev/null
+++ b/android/app/tests/unit/src/com/android/bluetooth/le_scan/TransitionalScanHelperTest.java
@@ -0,0 +1,400 @@
+/*
+ * Copyright 2023 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.le_scan;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.argThat;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.le.IPeriodicAdvertisingCallback;
+import android.bluetooth.le.IScannerCallback;
+import android.bluetooth.le.ScanResult;
+import android.bluetooth.le.ScanSettings;
+import android.content.AttributionSource;
+import android.content.Context;
+import android.content.res.Resources;
+import android.location.LocationManager;
+import android.os.Binder;
+import android.os.RemoteException;
+import android.os.WorkSource;
+import android.os.test.TestLooper;
+import android.platform.test.flag.junit.SetFlagsRule;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.rule.ServiceTestRule;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.bluetooth.TestUtils;
+import com.android.bluetooth.btservice.AdapterService;
+import com.android.bluetooth.btservice.CompanionManager;
+import com.android.bluetooth.gatt.GattObjectsFactory;
+import com.android.bluetooth.gatt.GattNativeInterface;
+
+import com.android.bluetooth.flags.Flags;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatcher;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/** Test cases for {@link TransitionalScanHelper}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class TransitionalScanHelperTest {
+
+ private static final String REMOTE_DEVICE_ADDRESS = "00:00:00:00:00:00";
+
+ private TransitionalScanHelper mScanHelper;
+ @Mock private TransitionalScanHelper.ScannerMap mScannerMap;
+
+ @SuppressWarnings("NonCanonicalType")
+ @Mock
+ private TransitionalScanHelper.ScannerMap.App mApp;
+
+ @Mock private TransitionalScanHelper.PendingIntentInfo mPiInfo;
+ @Mock private PeriodicScanManager mPeriodicScanManager;
+ @Mock private ScanManager mScanManager;
+
+ @Rule public final ServiceTestRule mServiceRule = new ServiceTestRule();
+ @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+ private BluetoothDevice mDevice;
+ private BluetoothAdapter mAdapter;
+ private AttributionSource mAttributionSource;
+
+ @Mock private Resources mResources;
+ @Mock private AdapterService mAdapterService;
+ @Mock private GattObjectsFactory mFactory;
+ @Mock private GattNativeInterface mNativeInterface;
+ private CompanionManager mBtCompanionManager;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ TestUtils.setAdapterService(mAdapterService);
+
+ GattObjectsFactory.setInstanceForTesting(mFactory);
+ doReturn(mNativeInterface).when(mFactory).getNativeInterface();
+ doReturn(mScanManager).when(mFactory).createScanManager(any(), any(), any(), any(), any());
+ doReturn(mPeriodicScanManager).when(mFactory).createPeriodicScanManager(any());
+
+ mAdapter = BluetoothAdapter.getDefaultAdapter();
+ mAttributionSource = mAdapter.getAttributionSource();
+ mDevice = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(REMOTE_DEVICE_ADDRESS);
+
+ when(mAdapterService.getResources()).thenReturn(mResources);
+ when(mResources.getInteger(anyInt())).thenReturn(0);
+ when(mAdapterService.getSharedPreferences(anyString(), anyInt()))
+ .thenReturn(
+ InstrumentationRegistry.getTargetContext()
+ .getSharedPreferences(
+ "GattServiceTestPrefs", Context.MODE_PRIVATE));
+
+ TestUtils.mockGetSystemService(
+ mAdapterService, Context.LOCATION_SERVICE, LocationManager.class);
+
+ mBtCompanionManager = new CompanionManager(mAdapterService, null);
+ doReturn(mBtCompanionManager).when(mAdapterService).getCompanionManager();
+
+ TestLooper testLooper = new TestLooper();
+ testLooper.startAutoDispatch();
+
+ mScanHelper =
+ new TransitionalScanHelper(InstrumentationRegistry.getTargetContext(), () -> false);
+ mScanHelper.start(testLooper.getLooper());
+
+ mScanHelper.setScannerMap(mScannerMap);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mScanHelper.stop();
+ mScanHelper = null;
+
+ TestUtils.clearAdapterService(mAdapterService);
+ GattObjectsFactory.setInstanceForTesting(null);
+ }
+
+ @Test
+ public void testParseBatchTimestamp() {
+ long timestampNanos = mScanHelper.parseTimestampNanos(new byte[] {-54, 7});
+ assertThat(timestampNanos).isEqualTo(99700000000L);
+ }
+
+ @Test
+ public void continuePiStartScan() {
+ int scannerId = 1;
+
+ mPiInfo.settings = new ScanSettings.Builder().build();
+ mApp.info = mPiInfo;
+
+ AppScanStats appScanStats = mock(AppScanStats.class);
+ doReturn(appScanStats).when(mScannerMap).getAppScanStatsById(scannerId);
+
+ mScanHelper.continuePiStartScan(scannerId, mApp);
+
+ verify(appScanStats)
+ .recordScanStart(mPiInfo.settings, mPiInfo.filters, false, false, scannerId);
+ verify(mScanManager).startScan(any());
+ }
+
+ @Test
+ public void continuePiStartScanCheckUid() {
+ int scannerId = 1;
+
+ mPiInfo.settings = new ScanSettings.Builder().build();
+ mPiInfo.callingUid = 123;
+ mApp.info = mPiInfo;
+
+ AppScanStats appScanStats = mock(AppScanStats.class);
+ doReturn(appScanStats).when(mScannerMap).getAppScanStatsById(scannerId);
+
+ mScanHelper.continuePiStartScan(scannerId, mApp);
+
+ verify(appScanStats)
+ .recordScanStart(mPiInfo.settings, mPiInfo.filters, false, false, scannerId);
+ verify(mScanManager)
+ .startScan(
+ argThat(
+ new ArgumentMatcher<ScanClient>() {
+ @Override
+ public boolean matches(ScanClient client) {
+ return mPiInfo.callingUid == client.appUid;
+ }
+ }));
+ }
+
+ @Test
+ public void onBatchScanReportsInternal_deliverBatchScan() throws RemoteException {
+ int status = 1;
+ int scannerId = 2;
+ int reportType = ScanManager.SCAN_RESULT_TYPE_FULL;
+ int numRecords = 1;
+ byte[] recordData =
+ new byte[] {
+ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x00, 0x00, 0x00, 0x00
+ };
+
+ Set<ScanClient> scanClientSet = new HashSet<>();
+ ScanClient scanClient = new ScanClient(scannerId);
+ scanClient.associatedDevices = new ArrayList<>();
+ scanClient.associatedDevices.add("02:00:00:00:00:00");
+ scanClient.scannerId = scannerId;
+ scanClientSet.add(scanClient);
+ doReturn(scanClientSet).when(mScanManager).getFullBatchScanQueue();
+ doReturn(mApp).when(mScannerMap).getById(scanClient.scannerId);
+
+ mScanHelper.onBatchScanReportsInternal(
+ status, scannerId, reportType, numRecords, recordData);
+ verify(mScanManager).callbackDone(scannerId, status);
+
+ reportType = ScanManager.SCAN_RESULT_TYPE_TRUNCATED;
+ recordData =
+ new byte[] {
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x06, 0x04, 0x02, 0x02, 0x00, 0x00, 0x02
+ };
+ doReturn(scanClientSet).when(mScanManager).getBatchScanQueue();
+ IScannerCallback callback = mock(IScannerCallback.class);
+ mApp.callback = callback;
+
+ mScanHelper.onBatchScanReportsInternal(
+ status, scannerId, reportType, numRecords, recordData);
+ verify(callback).onBatchScanResults(any());
+ }
+
+ @Test
+ public void enforceReportDelayFloor() {
+ long reportDelayFloorHigher = TransitionalScanHelper.DEFAULT_REPORT_DELAY_FLOOR + 1;
+ ScanSettings scanSettings =
+ new ScanSettings.Builder().setReportDelay(reportDelayFloorHigher).build();
+
+ ScanSettings newScanSettings = mScanHelper.enforceReportDelayFloor(scanSettings);
+
+ assertThat(newScanSettings.getReportDelayMillis())
+ .isEqualTo(scanSettings.getReportDelayMillis());
+
+ ScanSettings scanSettingsFloor = new ScanSettings.Builder().setReportDelay(1).build();
+
+ ScanSettings newScanSettingsFloor = mScanHelper.enforceReportDelayFloor(scanSettingsFloor);
+
+ assertThat(newScanSettingsFloor.getReportDelayMillis())
+ .isEqualTo(TransitionalScanHelper.DEFAULT_REPORT_DELAY_FLOOR);
+ }
+
+ @Test
+ public void registerScanner() throws Exception {
+ IScannerCallback callback = mock(IScannerCallback.class);
+ WorkSource workSource = mock(WorkSource.class);
+
+ AppScanStats appScanStats = mock(AppScanStats.class);
+ doReturn(appScanStats).when(mScannerMap).getAppScanStatsByUid(Binder.getCallingUid());
+
+ mScanHelper.registerScanner(callback, workSource, mAttributionSource);
+ verify(mScannerMap)
+ .add(any(), eq(workSource), eq(callback), eq(null), any(), eq(mScanHelper));
+ verify(mScanManager).registerScanner(any());
+ }
+
+ @Test
+ public void flushPendingBatchResults() {
+ int scannerId = 3;
+
+ mScanHelper.flushPendingBatchResults(scannerId, mAttributionSource);
+ verify(mScanManager).flushBatchScanResults(new ScanClient(scannerId));
+ }
+
+ @Test
+ public void onScanResult_remoteException_clientDied() throws Exception {
+ mSetFlagsRule.enableFlags(Flags.FLAG_LE_SCAN_FIX_REMOTE_EXCEPTION);
+ int scannerId = 1;
+
+ int eventType = 0;
+ int addressType = 0;
+ String address = "02:00:00:00:00:00";
+ int primaryPhy = 0;
+ int secondPhy = 0;
+ int advertisingSid = 0;
+ int txPower = 0;
+ int rssi = 0;
+ int periodicAdvInt = 0;
+ byte[] advData = new byte[0];
+
+ ScanClient scanClient = new ScanClient(scannerId);
+ scanClient.scannerId = scannerId;
+ scanClient.hasNetworkSettingsPermission = true;
+ scanClient.settings =
+ new ScanSettings.Builder()
+ .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
+ .setLegacy(false)
+ .build();
+
+ AppScanStats appScanStats = mock(AppScanStats.class);
+ IScannerCallback callback = mock(IScannerCallback.class);
+
+ mApp.callback = callback;
+ mApp.appScanStats = appScanStats;
+ Set<ScanClient> scanClientSet = Collections.singleton(scanClient);
+
+ doReturn(address).when(mAdapterService).getIdentityAddress(anyString());
+ doReturn(scanClientSet).when(mScanManager).getRegularScanQueue();
+ doReturn(mApp).when(mScannerMap).getById(scanClient.scannerId);
+ doReturn(appScanStats).when(mScannerMap).getAppScanStatsById(scanClient.scannerId);
+
+ // Simulate remote client crash
+ doThrow(new RemoteException()).when(callback).onScanResult(any());
+
+ mScanHelper.onScanResult(
+ eventType,
+ addressType,
+ address,
+ primaryPhy,
+ secondPhy,
+ advertisingSid,
+ txPower,
+ rssi,
+ periodicAdvInt,
+ advData,
+ address);
+
+ assertThat(scanClient.appDied).isTrue();
+ verify(appScanStats).recordScanStop(scannerId);
+ }
+
+ @Test
+ public void registerSync() {
+ ScanResult scanResult = new ScanResult(mDevice, 1, 2, 3, 4, 5, 6, 7, null, 8);
+ int skip = 1;
+ int timeout = 2;
+ IPeriodicAdvertisingCallback callback = mock(IPeriodicAdvertisingCallback.class);
+
+ mScanHelper.registerSync(scanResult, skip, timeout, callback, mAttributionSource);
+ verify(mPeriodicScanManager).startSync(scanResult, skip, timeout, callback);
+ }
+
+ @Test
+ public void transferSync() {
+ int serviceData = 1;
+ int syncHandle = 2;
+
+ mScanHelper.transferSync(mDevice, serviceData, syncHandle, mAttributionSource);
+ verify(mPeriodicScanManager).transferSync(mDevice, serviceData, syncHandle);
+ }
+
+ @Test
+ public void transferSetInfo() {
+ int serviceData = 1;
+ int advHandle = 2;
+ IPeriodicAdvertisingCallback callback = mock(IPeriodicAdvertisingCallback.class);
+
+ mScanHelper.transferSetInfo(mDevice, serviceData, advHandle, callback, mAttributionSource);
+ verify(mPeriodicScanManager).transferSetInfo(mDevice, serviceData, advHandle, callback);
+ }
+
+ @Test
+ public void unregisterSync() {
+ IPeriodicAdvertisingCallback callback = mock(IPeriodicAdvertisingCallback.class);
+
+ mScanHelper.unregisterSync(callback, mAttributionSource);
+ verify(mPeriodicScanManager).stopSync(callback);
+ }
+
+ @Test
+ public void getCurrentUsedTrackingAdvertisement() {
+ mScanHelper.getCurrentUsedTrackingAdvertisement();
+ verify(mScanManager).getCurrentUsedTrackingAdvertisement();
+ }
+
+ @Test
+ public void cleanUp_doesNotCrash() {
+ mScanHelper.cleanup();
+ }
+
+ @Test
+ public void profileConnectionStateChanged_notifyScanManager() {
+ mScanHelper.notifyProfileConnectionStateChange(
+ BluetoothProfile.A2DP,
+ BluetoothProfile.STATE_CONNECTING,
+ BluetoothProfile.STATE_CONNECTED);
+ verify(mScanManager)
+ .handleBluetoothProfileConnectionStateChanged(
+ BluetoothProfile.A2DP,
+ BluetoothProfile.STATE_CONNECTING,
+ BluetoothProfile.STATE_CONNECTED);
+ }
+}