diff options
3 files changed, 366 insertions, 30 deletions
diff --git a/core/java/android/bluetooth/BluetoothAdapter.java b/core/java/android/bluetooth/BluetoothAdapter.java index 7eda50e5c9cb..ea7e5ea7c802 100644 --- a/core/java/android/bluetooth/BluetoothAdapter.java +++ b/core/java/android/bluetooth/BluetoothAdapter.java @@ -3198,6 +3198,61 @@ public final class BluetoothAdapter { } /** + * Register a callback to receive events whenever the bluetooth stack goes down and back up, + * e.g. in the event the bluetooth is turned off/on via settings. + * + * If the bluetooth stack is currently up, there will not be an initial callback call. + * You can use the return value as an indication of this being the case. + * + * Callbacks will be delivered on a binder thread. + * + * @return whether bluetooth is already up currently + * + * @hide + */ + public boolean registerServiceLifecycleCallback(ServiceLifecycleCallback callback) { + return getBluetoothService(callback.mRemote) != null; + } + + /** + * Unregister a callback registered via {@link #registerServiceLifecycleCallback} + * + * @hide + */ + public void unregisterServiceLifecycleCallback(ServiceLifecycleCallback callback) { + removeServiceStateCallback(callback.mRemote); + } + + /** + * A callback for {@link #registerServiceLifecycleCallback} + * + * @hide + */ + public abstract static class ServiceLifecycleCallback { + + /** Called when the bluetooth stack is up */ + public abstract void onBluetoothServiceUp(); + + /** Called when the bluetooth stack is down */ + public abstract void onBluetoothServiceDown(); + + IBluetoothManagerCallback mRemote = new IBluetoothManagerCallback.Stub() { + @Override + public void onBluetoothServiceUp(IBluetooth bluetoothService) { + ServiceLifecycleCallback.this.onBluetoothServiceUp(); + } + + @Override + public void onBluetoothServiceDown() { + ServiceLifecycleCallback.this.onBluetoothServiceDown(); + } + + @Override + public void onBrEdrDown() {} + }; + } + + /** * Starts a scan for Bluetooth LE devices. * * <p>Results of the scan are reported using the diff --git a/core/java/android/companion/Association.java b/core/java/android/companion/Association.java index 17bf11b3d4ef..960a08755cb8 100644 --- a/core/java/android/companion/Association.java +++ b/core/java/android/companion/Association.java @@ -38,7 +38,7 @@ public final class Association implements Parcelable { private final @NonNull String mDeviceMacAddress; private final @NonNull String mPackageName; private final @Nullable String mDeviceProfile; - private final boolean mKeepProfilePrivilegesWhenDeviceAway; + private final boolean mNotifyOnDeviceNearby; /** @hide */ public int getUserId() { @@ -47,7 +47,7 @@ public final class Association implements Parcelable { - // Code below generated by codegen v1.0.21. + // Code below generated by codegen v1.0.22. // // DO NOT MODIFY! // CHECKSTYLE:OFF Generated code @@ -71,7 +71,7 @@ public final class Association implements Parcelable { @NonNull String deviceMacAddress, @NonNull String packageName, @Nullable String deviceProfile, - boolean keepProfilePrivilegesWhenDeviceAway) { + boolean notifyOnDeviceNearby) { this.mUserId = userId; com.android.internal.util.AnnotationValidations.validate( UserIdInt.class, null, mUserId); @@ -82,7 +82,7 @@ public final class Association implements Parcelable { com.android.internal.util.AnnotationValidations.validate( NonNull.class, null, mPackageName); this.mDeviceProfile = deviceProfile; - this.mKeepProfilePrivilegesWhenDeviceAway = keepProfilePrivilegesWhenDeviceAway; + this.mNotifyOnDeviceNearby = notifyOnDeviceNearby; // onConstructed(); // You can define this method to get a callback } @@ -103,8 +103,8 @@ public final class Association implements Parcelable { } @DataClass.Generated.Member - public boolean isKeepProfilePrivilegesWhenDeviceAway() { - return mKeepProfilePrivilegesWhenDeviceAway; + public boolean isNotifyOnDeviceNearby() { + return mNotifyOnDeviceNearby; } @Override @@ -118,7 +118,7 @@ public final class Association implements Parcelable { "deviceMacAddress = " + mDeviceMacAddress + ", " + "packageName = " + mPackageName + ", " + "deviceProfile = " + mDeviceProfile + ", " + - "keepProfilePrivilegesWhenDeviceAway = " + mKeepProfilePrivilegesWhenDeviceAway + + "notifyOnDeviceNearby = " + mNotifyOnDeviceNearby + " }"; } @@ -139,7 +139,7 @@ public final class Association implements Parcelable { && Objects.equals(mDeviceMacAddress, that.mDeviceMacAddress) && Objects.equals(mPackageName, that.mPackageName) && Objects.equals(mDeviceProfile, that.mDeviceProfile) - && mKeepProfilePrivilegesWhenDeviceAway == that.mKeepProfilePrivilegesWhenDeviceAway; + && mNotifyOnDeviceNearby == that.mNotifyOnDeviceNearby; } @Override @@ -153,7 +153,7 @@ public final class Association implements Parcelable { _hash = 31 * _hash + Objects.hashCode(mDeviceMacAddress); _hash = 31 * _hash + Objects.hashCode(mPackageName); _hash = 31 * _hash + Objects.hashCode(mDeviceProfile); - _hash = 31 * _hash + Boolean.hashCode(mKeepProfilePrivilegesWhenDeviceAway); + _hash = 31 * _hash + Boolean.hashCode(mNotifyOnDeviceNearby); return _hash; } @@ -164,7 +164,7 @@ public final class Association implements Parcelable { // void parcelFieldName(Parcel dest, int flags) { ... } byte flg = 0; - if (mKeepProfilePrivilegesWhenDeviceAway) flg |= 0x10; + if (mNotifyOnDeviceNearby) flg |= 0x10; if (mDeviceProfile != null) flg |= 0x8; dest.writeByte(flg); dest.writeInt(mUserId); @@ -185,7 +185,7 @@ public final class Association implements Parcelable { // static FieldType unparcelFieldName(Parcel in) { ... } byte flg = in.readByte(); - boolean keepProfilePrivilegesWhenDeviceAway = (flg & 0x10) != 0; + boolean notifyOnDeviceNearby = (flg & 0x10) != 0; int userId = in.readInt(); String deviceMacAddress = in.readString(); String packageName = in.readString(); @@ -201,7 +201,7 @@ public final class Association implements Parcelable { com.android.internal.util.AnnotationValidations.validate( NonNull.class, null, mPackageName); this.mDeviceProfile = deviceProfile; - this.mKeepProfilePrivilegesWhenDeviceAway = keepProfilePrivilegesWhenDeviceAway; + this.mNotifyOnDeviceNearby = notifyOnDeviceNearby; // onConstructed(); // You can define this method to get a callback } @@ -221,10 +221,10 @@ public final class Association implements Parcelable { }; @DataClass.Generated( - time = 1606940835778L, - codegenVersion = "1.0.21", + time = 1610482674799L, + codegenVersion = "1.0.22", sourceFile = "frameworks/base/core/java/android/companion/Association.java", - inputSignatures = "private final @android.annotation.UserIdInt int mUserId\nprivate final @android.annotation.NonNull java.lang.String mDeviceMacAddress\nprivate final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.Nullable java.lang.String mDeviceProfile\nprivate final boolean mKeepProfilePrivilegesWhenDeviceAway\npublic int getUserId()\nclass Association extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genEqualsHashCode=true, genToString=true, genHiddenConstructor=true)") + inputSignatures = "private final @android.annotation.UserIdInt int mUserId\nprivate final @android.annotation.NonNull java.lang.String mDeviceMacAddress\nprivate final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.Nullable java.lang.String mDeviceProfile\nprivate final boolean mNotifyOnDeviceNearby\npublic int getUserId()\nclass Association extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genEqualsHashCode=true, genToString=true, genHiddenConstructor=true)") @Deprecated private void __metadata() {} diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java index 55e3ef2e427f..a36e7acd6612 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java @@ -17,9 +17,14 @@ package com.android.server.companion; +import static android.bluetooth.le.ScanSettings.CALLBACK_TYPE_ALL_MATCHES; +import static android.bluetooth.le.ScanSettings.SCAN_MODE_BALANCED; +import static android.content.Context.BIND_IMPORTANT; + import static com.android.internal.util.CollectionUtils.emptyIfNull; import static com.android.internal.util.CollectionUtils.find; import static com.android.internal.util.CollectionUtils.forEach; +import static com.android.internal.util.CollectionUtils.map; import static com.android.internal.util.FunctionalUtils.uncheckExceptions; import static com.android.internal.util.Preconditions.checkArgument; import static com.android.internal.util.Preconditions.checkNotNull; @@ -40,12 +45,18 @@ import android.app.PendingIntent; import android.app.role.RoleManager; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; +import android.bluetooth.le.ScanCallback; +import android.bluetooth.le.ScanFilter; +import android.bluetooth.le.ScanResult; +import android.bluetooth.le.ScanSettings; import android.companion.Association; import android.companion.AssociationRequest; import android.companion.CompanionDeviceManager; +import android.companion.CompanionDeviceService; import android.companion.DeviceNotAssociatedException; import android.companion.ICompanionDeviceDiscoveryService; import android.companion.ICompanionDeviceManager; +import android.companion.ICompanionDeviceService; import android.companion.IFindDeviceCallback; import android.content.ComponentName; import android.content.Context; @@ -77,6 +88,7 @@ import android.permission.PermissionControllerManager; import android.provider.Settings; import android.provider.SettingsStringUtil.ComponentNameSet; import android.text.BidiFormatter; +import android.util.ArrayMap; import android.util.ArraySet; import android.util.AtomicFile; import android.util.ExceptionUtils; @@ -115,6 +127,7 @@ import java.io.PrintWriter; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; +import java.util.Date; import java.util.List; import java.util.Objects; import java.util.Set; @@ -135,6 +148,9 @@ public class CompanionDeviceManagerService extends SystemService implements Bind CompanionDeviceManager.COMPANION_DEVICE_DISCOVERY_PACKAGE_NAME, ".DeviceDiscoveryService"); + private static final long DEVICE_DISAPPEARED_TIMEOUT_MS = 10 * 1000; + private static final long DEVICE_DISAPPEARED_UNBIND_TIMEOUT_MS = 10 * 60 * 1000; + private static final boolean DEBUG = false; private static final String LOG_TAG = "CompanionDeviceManagerService"; @@ -146,18 +162,23 @@ public class CompanionDeviceManagerService extends SystemService implements Bind private static final String XML_ATTR_PACKAGE = "package"; private static final String XML_ATTR_DEVICE = "device"; private static final String XML_ATTR_PROFILE = "profile"; - private static final String XML_ATTR_PERSISTENT_PROFILE_GRANTS = "persistent_profile_grants"; + private static final String XML_ATTR_NOTIFY_DEVICE_NEARBY = "notify_device_nearby"; private static final String XML_FILE_NAME = "companion_device_manager_associations.xml"; private final CompanionDeviceManagerImpl mImpl; private final ConcurrentMap<Integer, AtomicFile> mUidToStorage = new ConcurrentHashMap<>(); private PowerWhitelistManager mPowerWhitelistManager; private PerUser<ServiceConnector<ICompanionDeviceDiscoveryService>> mServiceConnectors; + /** userId -> packageName -> serviceConnector */ + private PerUser<ArrayMap<String, ServiceConnector<ICompanionDeviceService>>> + mDeviceListenerServiceConnectors; private IAppOpsService mAppOpsManager; private RoleManager mRoleManager; private BluetoothAdapter mBluetoothAdapter; + private UserManager mUserManager; private IFindDeviceCallback mFindDeviceCallback; + private ScanCallback mBleScanCallback = new BleScanCallback(); private AssociationRequest mRequest; private String mCallingPackage; private AndroidFuture<Association> mOngoingDeviceDiscovery; @@ -166,8 +187,14 @@ public class CompanionDeviceManagerService extends SystemService implements Bind private BluetoothDeviceConnectedListener mBluetoothDeviceConnectedListener = new BluetoothDeviceConnectedListener(); private List<String> mCurrentlyConnectedDevices = new ArrayList<>(); + private ArrayMap<String, Date> mDevicesLastNearby = new ArrayMap<>(); + private UnbindDeviceListenersRunnable + mUnbindDeviceListenersRunnable = new UnbindDeviceListenersRunnable(); + private ArrayMap<String, TriggerDeviceDisappearedRunnable> mTriggerDeviceDisappearedRunnables = + new ArrayMap<>(); private final Object mLock = new Object(); + private final Handler mMainHandler = Handler.getMain(); /** userId -> [association] */ @GuardedBy("mLock") @@ -189,6 +216,7 @@ public class CompanionDeviceManagerService extends SystemService implements Bind mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class); mPermissionControllerManager = requireNonNull( context.getSystemService(PermissionControllerManager.class)); + mUserManager = context.getSystemService(UserManager.class); Intent serviceIntent = new Intent().setComponent(SERVICE_TO_BIND_TO); mServiceConnectors = new PerUser<ServiceConnector<ICompanionDeviceDiscoveryService>>() { @@ -201,6 +229,16 @@ public class CompanionDeviceManagerService extends SystemService implements Bind } }; + mDeviceListenerServiceConnectors = new PerUser<ArrayMap<String, + ServiceConnector<ICompanionDeviceService>>>() { + @NonNull + @Override + protected ArrayMap<String, ServiceConnector<ICompanionDeviceService>> create( + int userId) { + return new ArrayMap<>(); + } + }; + registerPackageMonitor(); } @@ -208,10 +246,13 @@ public class CompanionDeviceManagerService extends SystemService implements Bind new PackageMonitor() { @Override public void onPackageRemoved(String packageName, int uid) { + int userId = getChangingUserId(); updateAssociations( as -> CollectionUtils.filter(as, a -> !Objects.equals(a.getPackageName(), packageName)), - getChangingUserId()); + userId); + + unbindDevicePresenceListener(packageName, userId); } @Override @@ -225,6 +266,15 @@ public class CompanionDeviceManagerService extends SystemService implements Bind }.register(getContext(), FgThread.get().getLooper(), UserHandle.ALL, true); } + private void unbindDevicePresenceListener(String packageName, int userId) { + ServiceConnector<ICompanionDeviceService> deviceListener = + mDeviceListenerServiceConnectors.forUser(userId) + .remove(packageName); + if (deviceListener != null) { + deviceListener.unbind(); + } + } + @Override public void onStart() { publishBinderService(Context.COMPANION_DEVICE_SERVICE, mImpl); @@ -238,6 +288,9 @@ public class CompanionDeviceManagerService extends SystemService implements Bind mBluetoothAdapter.registerBluetoothConnectionCallback( getContext().getMainExecutor(), mBluetoothDeviceConnectedListener); + initBleScanning(); + } else { + Log.w(LOG_TAG, "No BluetoothAdapter available"); } } } @@ -287,7 +340,7 @@ public class CompanionDeviceManagerService extends SystemService implements Bind @Override public void binderDied() { - Handler.getMain().post(this::cleanup); + mMainHandler.post(this::cleanup); } private void cleanup() { @@ -399,7 +452,7 @@ public class CompanionDeviceManagerService extends SystemService implements Bind checkCallerIsSystemOr(callingPackage, userId); checkUsesFeature(callingPackage, getCallingUserId()); } - return new ArrayList<>(CollectionUtils.map( + return new ArrayList<>(map( getAllAssociations(userId, callingPackage), a -> a.getDeviceMacAddress())); } @@ -506,22 +559,18 @@ public class CompanionDeviceManagerService extends SystemService implements Bind public void registerDevicePresenceListenerService( String packageName, String deviceAddress) throws RemoteException { - checkCanRegisterObserverService(packageName, deviceAddress); - - //TODO(eugenesusla) implement + registerDevicePresenceListenerActive(packageName, deviceAddress, true); } @Override public void unregisterDevicePresenceListenerService( String packageName, String deviceAddress) throws RemoteException { - checkCanRegisterObserverService(packageName, deviceAddress); - - //TODO(eugenesusla) implement + registerDevicePresenceListenerActive(packageName, deviceAddress, false); } - private void checkCanRegisterObserverService(String packageName, String deviceAddress) - throws RemoteException { + private void registerDevicePresenceListenerActive(String packageName, String deviceAddress, + boolean active) throws RemoteException { getContext().enforceCallingOrSelfPermission( android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE, "[un]registerDevicePresenceListenerService"); @@ -537,6 +586,20 @@ public class CompanionDeviceManagerService extends SystemService implements Bind + " is not associated with device " + deviceAddress + " for user " + userId)); } + + updateAssociations(associations -> map(associations, association -> { + if (Objects.equals(association.getPackageName(), packageName) + && Objects.equals(association.getDeviceMacAddress(), deviceAddress)) { + return new Association( + association.getUserId(), + association.getDeviceMacAddress(), + association.getPackageName(), + association.getDeviceProfile(), + active /* notifyOnDeviceNearby */); + } else { + return association; + } + })); } private void checkCanCallNotificationApi(String callingPackage) throws RemoteException { @@ -693,6 +756,10 @@ public class CompanionDeviceManagerService extends SystemService implements Bind if (mCurrentlyConnectedDevices.contains(association.getDeviceMacAddress())) { grantDeviceProfile(association); } + + if (association.isNotifyOnDeviceNearby()) { + restartBleScan(); + } } private void exemptFromAutoRevoke(String packageName, int uid) { @@ -795,9 +862,9 @@ public class CompanionDeviceManagerService extends SystemService implements Bind association.getDeviceMacAddress()); if (association.getDeviceProfile() != null) { tag.attribute(null, XML_ATTR_PROFILE, association.getDeviceProfile()); - tag.attribute(null, XML_ATTR_PERSISTENT_PROFILE_GRANTS, + tag.attribute(null, XML_ATTR_NOTIFY_DEVICE_NEARBY, Boolean.toString( - association.isKeepProfilePrivilegesWhenDeviceAway())); + association.isNotifyOnDeviceNearby())); } tag.endTag(null, XML_TAG_ASSOCIATION); }); @@ -845,6 +912,14 @@ public class CompanionDeviceManagerService extends SystemService implements Bind a -> Objects.equals(packageFilter, a.getPackageName())); } + private Set<Association> getAllAssociations() { + ArraySet<Association> result = new ArraySet<>(); + for (UserInfo user : mUserManager.getAliveUsers()) { + result.addAll(getAllAssociations(user.id)); + } + return result; + } + private Set<Association> readAllAssociations(int userId) { final AtomicFile file = getStorageFileForUser(userId); @@ -865,7 +940,7 @@ public class CompanionDeviceManagerService extends SystemService implements Bind final String profile = parser.getAttributeValue(null, XML_ATTR_PROFILE); final boolean persistentGrants = Boolean.valueOf( - parser.getAttributeValue(null, XML_ATTR_PERSISTENT_PROFILE_GRANTS)); + parser.getAttributeValue(null, XML_ATTR_NOTIFY_DEVICE_NEARBY)); if (appPackage == null || deviceAddress == null) continue; @@ -896,6 +971,8 @@ public class CompanionDeviceManagerService extends SystemService implements Bind } } } + + onDeviceNearby(address); } private void grantDeviceProfile(Association association) { @@ -919,6 +996,210 @@ public class CompanionDeviceManagerService extends SystemService implements Bind void onDeviceDisconnected(String address) { mCurrentlyConnectedDevices.remove(address); + + onDeviceDisappeared(address); + } + + private ServiceConnector<ICompanionDeviceService> getDeviceListenerServiceConnector( + Association a) { + return mDeviceListenerServiceConnectors.forUser(a.getUserId()).computeIfAbsent( + a.getPackageName(), + pkg -> new ServiceConnector.Impl<>(getContext(), + new Intent(CompanionDeviceService.SERVICE_INTERFACE), + BIND_IMPORTANT, + a.getUserId(), + ICompanionDeviceService.Stub::asInterface)); + } + + private class BleScanCallback extends ScanCallback { + @Override + public void onScanResult(int callbackType, ScanResult result) { + if (DEBUG) { + Log.i(LOG_TAG, "onScanResult(callbackType = " + + callbackType + ", result = " + result + ")"); + } + + onDeviceNearby(result.getDevice().getAddress()); + } + + @Override + public void onBatchScanResults(List<ScanResult> results) { + for (int i = 0, size = results.size(); i < size; i++) { + onScanResult(CALLBACK_TYPE_ALL_MATCHES, results.get(i)); + } + } + + @Override + public void onScanFailed(int errorCode) { + if (errorCode == SCAN_FAILED_ALREADY_STARTED) { + // ignore - this might happen if BT tries to auto-restore scans for us in the + // future + } else { + Log.wtf(LOG_TAG, "Failed to start BLE scan: error " + errorCode); + } + } + } + + private class UnbindDeviceListenersRunnable implements Runnable { + + public String getJobId(String address) { + return "CDM_deviceGone_unbind_" + address; + } + + @Override + public void run() { + int size = mDevicesLastNearby.size(); + for (int i = 0; i < size; i++) { + String address = mDevicesLastNearby.keyAt(i); + Date lastNearby = mDevicesLastNearby.valueAt(i); + + if (System.currentTimeMillis() - lastNearby.getTime() + >= DEVICE_DISAPPEARED_UNBIND_TIMEOUT_MS) { + for (Association association : getAllAssociations(address)) { + if (association.isNotifyOnDeviceNearby()) { + getDeviceListenerServiceConnector(association).unbind(); + } + } + } + } + } + } + + private class TriggerDeviceDisappearedRunnable implements Runnable { + + private final String mAddress; + + TriggerDeviceDisappearedRunnable(String address) { + mAddress = address; + } + + public void schedule() { + mMainHandler.removeCallbacks(this); + mMainHandler.postDelayed(this, this, DEVICE_DISAPPEARED_TIMEOUT_MS); + } + + @Override + public void run() { + onDeviceDisappeared(mAddress); + } + } + + private Set<Association> getAllAssociations(String deviceAddress) { + List<UserInfo> aliveUsers = mUserManager.getAliveUsers(); + Set<Association> result = new ArraySet<>(); + for (int i = 0, size = aliveUsers.size(); i < size; i++) { + UserInfo user = aliveUsers.get(i); + for (Association association : getAllAssociations(user.id)) { + if (Objects.equals(association.getDeviceMacAddress(), deviceAddress)) { + result.add(association); + } + } + } + return result; + } + + private void onDeviceNearby(String address) { + Date timestamp = new Date(); + mDevicesLastNearby.put(address, timestamp); + + cancelUnbindDeviceListener(address); + + mTriggerDeviceDisappearedRunnables + .computeIfAbsent(address, addr -> new TriggerDeviceDisappearedRunnable(address)) + .schedule(); + + for (Association association : getAllAssociations(address)) { + if (association.isNotifyOnDeviceNearby()) { + if (DEBUG) { + Log.i(LOG_TAG, "Device " + address + + " managed by " + association.getPackageName() + + " is nearby on " + timestamp); + } + getDeviceListenerServiceConnector(association).run( + service -> service.onDeviceAppeared(association.getDeviceMacAddress())); + } + } + } + + private void onDeviceDisappeared(String address) { + boolean hasDeviceListeners = false; + for (Association association : getAllAssociations(address)) { + if (association.isNotifyOnDeviceNearby()) { + if (DEBUG) { + Log.i(LOG_TAG, "Device " + address + + " managed by " + association.getPackageName() + + " disappeared; last seen on " + mDevicesLastNearby.get(address)); + } + + getDeviceListenerServiceConnector(association).run( + service -> service.onDeviceDisappeared(address)); + hasDeviceListeners = true; + } + } + + cancelUnbindDeviceListener(address); + if (hasDeviceListeners) { + mMainHandler.postDelayed( + mUnbindDeviceListenersRunnable, + mUnbindDeviceListenersRunnable.getJobId(address), + DEVICE_DISAPPEARED_UNBIND_TIMEOUT_MS); + } + } + + private void cancelUnbindDeviceListener(String address) { + mMainHandler.removeCallbacks( + mUnbindDeviceListenersRunnable, mUnbindDeviceListenersRunnable.getJobId(address)); + } + + private void initBleScanning() { + boolean bluetoothReady = mBluetoothAdapter.registerServiceLifecycleCallback( + new BluetoothAdapter.ServiceLifecycleCallback() { + @Override + public void onBluetoothServiceUp() { + Log.i(LOG_TAG, "Bluetooth stack is up"); + startBleScan(); + } + + @Override + public void onBluetoothServiceDown() { + Log.w(LOG_TAG, "Bluetooth stack is down"); + } + }); + if (bluetoothReady) { + startBleScan(); + } + } + + void startBleScan() { + List<ScanFilter> filters = getBleScanFilters(); + if (filters.isEmpty()) { + return; + } + mBluetoothAdapter.getBluetoothLeScanner().startScan( + filters, + new ScanSettings.Builder().setScanMode(SCAN_MODE_BALANCED).build(), + mBleScanCallback); + } + + void restartBleScan() { + mBluetoothAdapter.getBluetoothLeScanner().stopScan(mBleScanCallback); + startBleScan(); + } + + private List<ScanFilter> getBleScanFilters() { + ArrayList<ScanFilter> result = new ArrayList<>(); + ArraySet<String> addressesSeen = new ArraySet<>(); + for (Association association : getAllAssociations()) { + String address = association.getDeviceMacAddress(); + if (addressesSeen.contains(address)) { + continue; + } + if (association.isNotifyOnDeviceNearby()) { + result.add(new ScanFilter.Builder().setDeviceAddress(address).build()); + addressesSeen.add(address); + } + } + return result; } private AndroidFuture<String> getDeviceProfilePermissionDescription(String deviceProfile) { |