diff options
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); + } +} |