Merge changes I67883374,I945c79ac
* changes:
Temporary disconnects for Bluetooth profiles.
Make ConnectionParams an immutable object.
diff --git a/car-lib/src/android/car/CarBluetoothManager.java b/car-lib/src/android/car/CarBluetoothManager.java
index 841a055..bb9c8ff 100644
--- a/car-lib/src/android/car/CarBluetoothManager.java
+++ b/car-lib/src/android/car/CarBluetoothManager.java
@@ -19,10 +19,6 @@
import android.annotation.IntDef;
import android.annotation.RequiresPermission;
import android.bluetooth.BluetoothDevice;
-import android.car.CarLibLog;
-import android.car.CarManagerBase;
-import android.car.CarNotConnectedException;
-import android.car.ICarBluetooth;
import android.content.Context;
import android.os.IBinder;
import android.os.RemoteException;
@@ -132,6 +128,49 @@
}
}
+ /**
+ * Request to disconnect the given profile on the given device, and prevent it from reconnecting
+ * until either the request is released, or the process owning the given token dies.
+ *
+ * @param device The device on which to disconnect a profile.
+ * @param profile The {@link android.bluetooth.BluetoothProfile} to disconnect.
+ * @param token A {@link IBinder} to be used as an identity for the request. If the process
+ * owning the token dies, the request will automatically be released.
+ * @return True if the profile was successfully disconnected, false if an error occurred.
+ */
+ @RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN)
+ public boolean requestTemporaryProfileDisconnect(
+ BluetoothDevice device, int profile, IBinder token) throws CarNotConnectedException {
+ try {
+ return mService.requestTemporaryDisconnect(device, profile, token);
+ } catch (RemoteException e) {
+ Log.e(CarLibLog.TAG_CAR, "requestTemporaryDisconnect failed", e);
+ throw new CarNotConnectedException(e);
+ }
+ }
+
+ /**
+ * Undo a previous call to {@link #requestTemporaryProfileDisconnect} with the same parameters,
+ * and reconnect the profile if no other requests are active.
+ *
+ * @param device The device on which to release the disconnect request.
+ * @param profile The profile on which to release the disconnect request.
+ * @param token The token provided in the original call to
+ * {@link #requestTemporaryProfileDisconnect}.
+ *
+ * @return True if the request was released, false if an error occurred.
+ */
+ @RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN)
+ public boolean releaseTemporaryProfileDisconnect(
+ BluetoothDevice device, int profile, IBinder token) throws CarNotConnectedException {
+ try {
+ return mService.releaseTemporaryDisconnect(device, profile, token);
+ } catch (RemoteException e) {
+ Log.e(CarLibLog.TAG_CAR, "requestTemporaryDisconnect failed", e);
+ throw new CarNotConnectedException(e);
+ }
+ }
+
/** @hide */
public CarBluetoothManager(IBinder service, Context context) {
mContext = context;
diff --git a/car-lib/src/android/car/ICarBluetooth.aidl b/car-lib/src/android/car/ICarBluetooth.aidl
index a5fbb73..ee7eb29 100644
--- a/car-lib/src/android/car/ICarBluetooth.aidl
+++ b/car-lib/src/android/car/ICarBluetooth.aidl
@@ -24,4 +24,6 @@
void clearBluetoothDeviceConnectionPriority(in int profileToClear,in int priorityToClear);
boolean isPriorityDevicePresent(in int profile, in int priorityToCheck);
String getDeviceNameWithPriority(in int profile, in int priorityToCheck);
+ boolean requestTemporaryDisconnect(in BluetoothDevice device, in int profile, in IBinder token);
+ boolean releaseTemporaryDisconnect(in BluetoothDevice device, in int profile, in IBinder token);
}
diff --git a/car-lib/src/android/car/ICarBluetoothUserService.aidl b/car-lib/src/android/car/ICarBluetoothUserService.aidl
index a906a3c..d69d08f 100644
--- a/car-lib/src/android/car/ICarBluetoothUserService.aidl
+++ b/car-lib/src/android/car/ICarBluetoothUserService.aidl
@@ -24,5 +24,7 @@
void closeBluetoothConnectionProxy();
boolean isBluetoothConnectionProxyAvailable(in int profile);
void bluetoothConnectToProfile(in int profile, in BluetoothDevice device);
+ void bluetoothDisconnectFromProfile(in int profile, in BluetoothDevice device);
+ int getProfilePriority(in int profile, in BluetoothDevice device);
void setProfilePriority(in int profile, in BluetoothDevice device, in int priority);
}
diff --git a/car-lib/src/android/car/settings/CarSettings.java b/car-lib/src/android/car/settings/CarSettings.java
index abbefc1..c3a2657 100644
--- a/car-lib/src/android/car/settings/CarSettings.java
+++ b/car-lib/src/android/car/settings/CarSettings.java
@@ -193,6 +193,12 @@
public static final String KEY_BLUETOOTH_AUTOCONNECT_NETWORK_DEVICE_PRIORITY_1 =
"android.car.BLUETOOTH_AUTOCONNECT_NETWORK_DEVICE_PRIORITY_1";
-
+ /**
+ * Key for storing temporarily-disconnected devices and profiles.
+ * Read and written by {@link com.android.car.BluetoothDeviceConnectionPolicy}.
+ * @hide
+ */
+ public static final String KEY_BLUETOOTH_TEMPORARY_DISCONNECTS =
+ "android.car.BLUETOOTH_TEMPORARY_DISCONNECTS";
}
}
diff --git a/service/src/com/android/car/BluetoothDeviceConnectionPolicy.java b/service/src/com/android/car/BluetoothDeviceConnectionPolicy.java
index 51bbf8d..dcd7e16 100644
--- a/service/src/com/android/car/BluetoothDeviceConnectionPolicy.java
+++ b/service/src/com/android/car/BluetoothDeviceConnectionPolicy.java
@@ -20,6 +20,7 @@
import static android.car.settings.CarSettings.Secure.KEY_BLUETOOTH_AUTOCONNECT_MUSIC_DEVICES;
import static android.car.settings.CarSettings.Secure.KEY_BLUETOOTH_AUTOCONNECT_NETWORK_DEVICES;
import static android.car.settings.CarSettings.Secure.KEY_BLUETOOTH_AUTOCONNECT_PHONE_DEVICES;
+import static android.car.settings.CarSettings.Secure.KEY_BLUETOOTH_TEMPORARY_DISCONNECTS;
import android.annotation.Nullable;
import android.app.ActivityManager;
@@ -46,11 +47,16 @@
import android.content.IntentFilter;
import android.hardware.automotive.vehicle.V2_0.VehicleIgnitionState;
import android.hardware.automotive.vehicle.V2_0.VehicleProperty;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
import android.os.ParcelUuid;
import android.os.Parcelable;
import android.os.RemoteException;
import android.os.UserHandle;
import android.provider.Settings;
+import android.text.TextUtils;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
@@ -62,8 +68,10 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
import java.util.concurrent.locks.ReentrantLock;
+import java.util.stream.Collectors;
/**
@@ -91,6 +99,10 @@
private static final String TAG = "BTDevConnectionPolicy";
private static final String SETTINGS_DELIMITER = ",";
private static final boolean DBG = Utils.DBG;
+
+ private static final Binder RESTORED_TEMPORARY_DISCONNECT_TOKEN = new Binder();
+ private static final long RESTORE_BACKOFF_MILLIS = 1000L;
+
private final Context mContext;
private boolean mInitialized = false;
private boolean mUserSpecificInfoInitialized = false;
@@ -146,6 +158,13 @@
// Maintain a list of Paired devices which haven't connected on any profiles yet.
private Set<BluetoothDevice> mPairedButUnconnectedDevices = new HashSet<>();
+ // State for temporary disconnects. Guarded by lock on `this`.
+ private final SetMultimap<ConnectionParams, DisconnectRecord> mTemporaryDisconnects;
+ private final HashSet<DisconnectRecord> mRestoredDisconnects = new HashSet<>();
+ private final HashSet<ConnectionParams> mAlreadyDisabledProfiles = new HashSet<>();
+
+ private final Handler mHandler = new Handler(Looper.getMainLooper());
+
public static BluetoothDeviceConnectionPolicy create(Context context,
CarPropertyService carPropertyService, PerUserCarServiceHelper userServiceHelper,
CarUxRestrictionsManagerService uxrService, CarBluetoothService bluetoothService) {
@@ -208,6 +227,8 @@
Log.w(TAG, "No Bluetooth Adapter Available");
}
mFastPairProvider = new FastPairProvider(mContext);
+
+ mTemporaryDisconnects = new SetMultimap<>();
}
/**
@@ -218,38 +239,142 @@
* Used as the currency that methods use to talk to each other in the policy.
*/
public static class ConnectionParams {
- private BluetoothDevice mBluetoothDevice;
- private Integer mBluetoothProfile;
+ // Examples:
+ // 01:23:45:67:89:AB/9
+ // null/0
+ // null/null
+ private static final String FLATTENED_PATTERN =
+ "^(([0-9A-F]{2}:){5}[0-9A-F]{2}|null)/([0-9]+|null)$";
+
+ @Nullable private final BluetoothDevice mBluetoothDevice;
+ @Nullable private final Integer mBluetoothProfile;
public ConnectionParams() {
- // default constructor
+ this(null, null);
}
- public ConnectionParams(Integer profile) {
- mBluetoothProfile = profile;
+ public ConnectionParams(@Nullable Integer profile) {
+ this(profile, null);
}
- public ConnectionParams(Integer profile, BluetoothDevice device) {
+ public ConnectionParams(@Nullable Integer profile, @Nullable BluetoothDevice device) {
mBluetoothProfile = profile;
mBluetoothDevice = device;
}
- // getters & Setters
- public void setBluetoothDevice(BluetoothDevice device) {
- mBluetoothDevice = device;
- }
-
- public void setBluetoothProfile(Integer profile) {
- mBluetoothProfile = profile;
- }
-
+ @Nullable
public BluetoothDevice getBluetoothDevice() {
return mBluetoothDevice;
}
+ @Nullable
public Integer getBluetoothProfile() {
return mBluetoothProfile;
}
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof ConnectionParams)) {
+ return false;
+ }
+ ConnectionParams otherParams = (ConnectionParams) other;
+ return Objects.equals(mBluetoothDevice, otherParams.mBluetoothDevice)
+ && Objects.equals(mBluetoothProfile, otherParams.mBluetoothProfile);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mBluetoothDevice, mBluetoothProfile);
+ }
+
+ @Override
+ public String toString() {
+ return flattenToString();
+ }
+
+ /** Converts these {@link ConnectionParams} to a parseable string representation. */
+ public String flattenToString() {
+ return mBluetoothDevice + "/" + mBluetoothProfile;
+ }
+
+ /**
+ * Creates a {@link ConnectionParams} from a previous output of {@link #flattenToString()}.
+ *
+ * @param flattenedParams A flattened string representation of a {@link ConnectionParams}.
+ * @param adapter A {@link BluetoothAdapter} used to convert Bluetooth addresses into
+ * {@link BluetoothDevice} objects.
+ */
+ public static ConnectionParams parse(String flattenedParams, BluetoothAdapter adapter) {
+ if (!flattenedParams.matches(FLATTENED_PATTERN)) {
+ throw new IllegalArgumentException("Bad format for flattened ConnectionParams");
+ }
+ String[] parts = flattenedParams.split("/");
+
+ BluetoothDevice device;
+ if (!"null".equals(parts[0])) {
+ device = adapter.getRemoteDevice(parts[0]);
+ } else {
+ device = null;
+ }
+
+ Integer profile;
+ if (!"null".equals(parts[1])) {
+ profile = Integer.valueOf(parts[1]);
+ } else {
+ profile = null;
+ }
+
+ return new ConnectionParams(profile, device);
+ }
+ }
+
+ private class DisconnectRecord implements IBinder.DeathRecipient {
+ private final ConnectionParams mParams;
+ private final IBinder mToken;
+
+ private boolean mRemoved = false;
+
+ DisconnectRecord(ConnectionParams params, IBinder token) {
+ this.mParams = params;
+ this.mToken = token;
+ }
+
+ public ConnectionParams getParams() {
+ return mParams;
+ }
+
+ public IBinder getToken() {
+ return mToken;
+ }
+
+ public boolean removeSelf() {
+ synchronized (BluetoothDeviceConnectionPolicy.this) {
+ if (mRemoved) {
+ return true;
+ }
+
+ if (removeDisconnectRecord(this)) {
+ mRemoved = true;
+ return true;
+ } else {
+ return false;
+ }
+ }
+ }
+
+ @Override
+ public void binderDied() {
+ if (DBG) {
+ Log.d(TAG, "Releasing disconnect request on profile "
+ + Utils.getProfileName(mParams.getBluetoothProfile())
+ + " for device " + mParams.getBluetoothDevice()
+ + ": requesting process died");
+ }
+ removeSelf();
+ }
}
/**
@@ -372,6 +497,19 @@
}
if (mCarBluetoothUserService != null) {
for (Integer profile : mProfilesToConnect) {
+ // If this profile is temporarily disconnected, don't try to change its priority
+ // until the temporary disconnect is released.
+ synchronized (this) {
+ ConnectionParams params = new ConnectionParams(profile, device);
+ if (mTemporaryDisconnects.keySet().contains(params)) {
+ if (DBG) {
+ Log.i(TAG, "Not setting profile " + profile + " priority of "
+ + device.getAddress() + " to " + priority + ": "
+ + "temporarily disconnected");
+ }
+ continue;
+ }
+ }
setBluetoothProfilePriorityIfUuidFound(uuids, profile, device, priority);
}
}
@@ -440,6 +578,10 @@
mCarBluetoothUserService = setupBluetoothUserService();
// re-initialize for current user.
initializeUserSpecificInfo();
+ // Restore temporary disconnects, if any, that were saved from last run...
+ restoreTemporaryDisconnectsFromSettings();
+ // ... and start trying to remove them.
+ removeRestoredTemporaryDisconnects();
}
@Override
@@ -447,6 +589,15 @@
if (DBG) {
Log.d(TAG, "Before Unbinding from UserService");
}
+
+ // Try to release temporary disconnects now, before CarBluetoothUserService goes away.
+ // This also stops any active attempts to remove restored disconnects.
+ //
+ // If any can't be released, they'll persist in settings and will be cleaned up
+ // next time this user starts. This can happen if the Bluetooth profile proxies in
+ // CarBluetoothUserService unbind before we get the chance to make calls on them.
+ releaseAllDisconnectRecordsBeforeUnbind();
+
try {
if (mCarBluetoothUserService != null) {
mCarBluetoothUserService.closeBluetoothConnectionProxy();
@@ -456,6 +607,7 @@
"Remote Exception during closeBluetoothConnectionProxy(): "
+ e.getMessage());
}
+
// Clean up information related to user who went background.
cleanupUserSpecificInfo();
}
@@ -820,6 +972,309 @@
}
/**
+ * Request to disconnect the given profile on the given device, and prevent it from reconnecting
+ * until either the request is released, or the process owning the given token dies.
+ * @return True if the profile was successfully disconnected, false if an error occurred.
+ */
+ public boolean requestProfileDisconnect(BluetoothDevice device, int profile, IBinder token) {
+ if (DBG) {
+ Log.d(TAG, "Request profile disconnect: profile " + Utils.getProfileName(profile)
+ + ", device " + device.getAddress());
+ }
+ ConnectionParams params = new ConnectionParams(profile, device);
+ DisconnectRecord record = new DisconnectRecord(params, token);
+ return addDisconnectRecord(record);
+ }
+
+ /**
+ * Undo a previous call to {@link #requestProfileDisconnect} with the same parameters,
+ * and reconnect the profile if no other requests are active.
+ *
+ * @return True if the request was released, false if an error occurred.
+ */
+ public boolean releaseProfileDisconnect(BluetoothDevice device, int profile, IBinder token) {
+ if (DBG) {
+ Log.d(TAG, "Release profile disconnect: profile " + Utils.getProfileName(profile)
+ + ", device " + device.getAddress());
+ }
+
+ ConnectionParams params = new ConnectionParams(profile, device);
+ DisconnectRecord record;
+ synchronized (this) {
+ record = findDisconnectRecordLocked(params, token);
+ }
+
+ if (record == null) {
+ Log.e(TAG, "Record not found");
+ return false;
+ }
+
+ return record.removeSelf();
+ }
+
+ /** Add a temporary disconnect record, disconnecting if necessary. */
+ private synchronized boolean addDisconnectRecord(DisconnectRecord record) {
+ ConnectionParams params = record.getParams();
+ if (!isProxyAvailable(params.getBluetoothProfile())) {
+ return false;
+ }
+
+ Set<DisconnectRecord> previousRecords = mTemporaryDisconnects.get(params);
+ if (findDisconnectRecordLocked(params, record.getToken()) != null) {
+ Log.e(TAG, "Disconnect request already registered - skipping duplicate");
+ return false;
+ }
+
+ try {
+ record.getToken().linkToDeath(record, 0);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Could not link to death on disconnect token (already dead?)", e);
+ return false;
+ }
+
+ boolean isNewlyAdded = previousRecords.isEmpty();
+ mTemporaryDisconnects.put(params, record);
+
+ if (isNewlyAdded) {
+ try {
+ int priority =
+ mCarBluetoothUserService.getProfilePriority(
+ params.getBluetoothProfile(),
+ params.getBluetoothDevice());
+ if (priority == BluetoothProfile.PRIORITY_OFF) {
+ // This profile was already disabled (and not as the result of a temporary
+ // disconnect). Add it to the already-disabled list, and do nothing else.
+ mAlreadyDisabledProfiles.add(params);
+
+ if (DBG) {
+ Log.d(TAG, "Profile " + Utils.getProfileName(params.getBluetoothProfile())
+ + " already disabled for device " + params.getBluetoothDevice()
+ + " - suppressing re-enable");
+ }
+ } else {
+ mCarBluetoothUserService.setProfilePriority(
+ params.getBluetoothProfile(),
+ params.getBluetoothDevice(),
+ BluetoothProfile.PRIORITY_OFF);
+ mCarBluetoothUserService.bluetoothDisconnectFromProfile(
+ params.getBluetoothProfile(),
+ params.getBluetoothDevice());
+ if (DBG) {
+ Log.d(TAG, "Disabled profile "
+ + Utils.getProfileName(params.getBluetoothProfile())
+ + " for device " + params.getBluetoothDevice());
+ }
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Could not disable profile", e);
+ record.getToken().unlinkToDeath(record, 0);
+ mTemporaryDisconnects.remove(params, record);
+ return false;
+ }
+ }
+
+ saveTemporaryDisconnectsToSettingsLocked();
+ return true;
+ }
+
+ /** Remove a given temporary disconnect record, reconnecting if necessary. */
+ private synchronized boolean removeDisconnectRecord(DisconnectRecord record) {
+ ConnectionParams params = record.getParams();
+ if (!isProxyAvailable(params.getBluetoothProfile())) {
+ return false;
+ }
+ if (!mTemporaryDisconnects.containsEntry(params, record)) {
+ Log.e(TAG, "Record already removed");
+ // Removing something a second time vacuously succeeds.
+ return true;
+ }
+
+ // Re-enable profile before unlinking and removing the record, in case of error.
+ // The profile should be re-enabled if this record is the only one left for that
+ // device and profile combination.
+ if (mTemporaryDisconnects.get(params).size() == 1) {
+ if (!restoreProfilePriority(params)) {
+ return false;
+ }
+ }
+
+ record.getToken().unlinkToDeath(record, 0);
+ mTemporaryDisconnects.remove(params, record);
+
+ saveTemporaryDisconnectsToSettingsLocked();
+ return true;
+ }
+
+ /** Find the disconnect record, if any, corresponding to the given parameters and token. */
+ @Nullable
+ private DisconnectRecord findDisconnectRecordLocked(ConnectionParams params, IBinder token) {
+ return mTemporaryDisconnects.get(params)
+ .stream()
+ .filter(r -> r.getToken() == token)
+ .findAny()
+ .orElse(null);
+ }
+
+ /** Re-enable and reconnect a given profile for a device. */
+ private boolean restoreProfilePriority(ConnectionParams params) {
+ if (!isProxyAvailable(params.getBluetoothProfile())) {
+ return false;
+ }
+
+ if (mAlreadyDisabledProfiles.remove(params)) {
+ // The profile does not need any state changes, since it was disabled
+ // before it was temporarily disconnected. Leave it disconnected.
+ if (DBG) {
+ Log.d(TAG, "Not restoring profile "
+ + Utils.getProfileName(params.getBluetoothProfile()) + " for device "
+ + params.getBluetoothDevice() + " - was manually disabled");
+ }
+ return true;
+ }
+
+ try {
+ mCarBluetoothUserService.setProfilePriority(
+ params.getBluetoothProfile(),
+ params.getBluetoothDevice(),
+ BluetoothProfile.PRIORITY_ON);
+ mCarBluetoothUserService.bluetoothConnectToProfile(
+ params.getBluetoothProfile(),
+ params.getBluetoothDevice());
+ if (DBG) {
+ Log.d(TAG, "Restored profile " + Utils.getProfileName(params.getBluetoothProfile())
+ + " for device " + params.getBluetoothDevice());
+ }
+ return true;
+ } catch (RemoteException e) {
+ Log.e(TAG, "Could not enable profile", e);
+ return false;
+ }
+ }
+
+ /** Dump all currently-active temporary disconnects to {@link Settings.Secure}. */
+ private void saveTemporaryDisconnectsToSettingsLocked() {
+ Set<ConnectionParams> disconnectedProfiles = new HashSet<>(mTemporaryDisconnects.keySet());
+ // Don't write out profiles that were disconnected before a request was made, since
+ // restoring those profiles is a no-op.
+ disconnectedProfiles.removeAll(mAlreadyDisabledProfiles);
+ String savedDisconnects =
+ disconnectedProfiles
+ .stream()
+ .map(ConnectionParams::flattenToString)
+ .collect(Collectors.joining(SETTINGS_DELIMITER));
+
+ if (DBG) {
+ Log.d(TAG, "Saving disconnects to settings for u" + mUserId + ": " + savedDisconnects);
+ }
+
+ Settings.Secure.putStringForUser(
+ mContext.getContentResolver(), KEY_BLUETOOTH_TEMPORARY_DISCONNECTS,
+ savedDisconnects, mUserId);
+ }
+
+ /** Create {@link DisconnectRecord}s for all temporary disconnects written to settings. */
+ private synchronized void restoreTemporaryDisconnectsFromSettings() {
+ if (mBluetoothAdapter == null) {
+ Log.e(TAG, "Cannot restore disconnect records - Bluetooth not available");
+ return;
+ }
+
+ String savedConnectionParams = Settings.Secure.getStringForUser(
+ mContext.getContentResolver(),
+ KEY_BLUETOOTH_TEMPORARY_DISCONNECTS,
+ mUserId);
+
+ if (TextUtils.isEmpty(savedConnectionParams)) {
+ return;
+ }
+
+ if (DBG) {
+ Log.d(TAG, "Restoring temporary disconnects: " + savedConnectionParams);
+ }
+
+ for (String paramsStr : savedConnectionParams.split(SETTINGS_DELIMITER)) {
+ try {
+ ConnectionParams params = ConnectionParams.parse(paramsStr, mBluetoothAdapter);
+ DisconnectRecord record =
+ new DisconnectRecord(params, RESTORED_TEMPORARY_DISCONNECT_TOKEN);
+ mTemporaryDisconnects.put(params, record);
+ mRestoredDisconnects.add(record);
+ if (DBG) {
+ Log.d(TAG, "Restored temporary disconnect for " + params);
+ }
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "Bad format for saved temporary disconnect: " + paramsStr, e);
+ // We won't ever be able to fix a bad parse, so skip it and move on.
+ }
+ }
+ }
+
+ /**
+ * Try once to remove all temporary disconnects.
+ *
+ * If the CarBluetoothUserService is not yet available, or it hasn't yet bound its profile
+ * proxies, the removal will fail, and will need to be retried later.
+ */
+ private void tryRemoveRestoredTemporaryDisconnectsLocked() {
+ HashSet<DisconnectRecord> successfullyRemoved = new HashSet<>();
+
+ for (DisconnectRecord record : mRestoredDisconnects) {
+ if (removeDisconnectRecord(record)) {
+ successfullyRemoved.add(record);
+ }
+ }
+
+ mRestoredDisconnects.removeAll(successfullyRemoved);
+ }
+
+ /**
+ * Keep trying to remove all temporary disconnects that were restored from settings
+ * until all such temporary disconnects have been removed.
+ */
+ private synchronized void removeRestoredTemporaryDisconnects() {
+ tryRemoveRestoredTemporaryDisconnectsLocked();
+
+ if (!mRestoredDisconnects.isEmpty()) {
+ if (DBG) {
+ Log.d(TAG, "Could not remove all restored temporary disconnects - "
+ + "trying again in " + RESTORE_BACKOFF_MILLIS + "ms");
+ }
+ mHandler.postDelayed(
+ this::removeRestoredTemporaryDisconnects,
+ RESTORED_TEMPORARY_DISCONNECT_TOKEN,
+ RESTORE_BACKOFF_MILLIS);
+ }
+ }
+
+ /** Release all active disconnect records prior to user switch or shutdown. */
+ private synchronized void releaseAllDisconnectRecordsBeforeUnbind() {
+ if (DBG) {
+ Log.d(TAG, "Unbinding CarBluetoothUserService - releasing all temporary disconnects");
+ }
+ for (ConnectionParams params : mTemporaryDisconnects.keySet()) {
+ for (DisconnectRecord record : mTemporaryDisconnects.get(params)) {
+ record.removeSelf();
+ }
+ }
+
+ // Some disconnects might be hanging around because they couldn't be cleaned up.
+ // Make sure they get persisted...
+ saveTemporaryDisconnectsToSettingsLocked();
+ // ...then clear them from the map.
+ mTemporaryDisconnects.clear();
+
+ // We don't need to maintain previously-disconnected profiles any more - they were already
+ // skipped in saveTemporaryDisconnectsToSettingsLocked() above, and they don't need any
+ // further handling when the user resumes.
+ mAlreadyDisabledProfiles.clear();
+
+ // Clean up bookkeeping for restored disconnects. (If any are still around, they'll be
+ // restored again when this user restarts.)
+ mHandler.removeCallbacksAndMessages(RESTORED_TEMPORARY_DISCONNECT_TOKEN);
+ mRestoredDisconnects.clear();
+ }
+
+ /**
* Add or remove a device based on the bonding state change.
*
* @param device - device to add/remove
@@ -946,13 +1401,9 @@
if (DBG) {
Log.d(TAG, "Found device to connect to");
}
- BluetoothDeviceConnectionPolicy.ConnectionParams btParams =
- new BluetoothDeviceConnectionPolicy.ConnectionParams(
- mConnectionInFlight.getBluetoothProfile(),
- mConnectionInFlight.getBluetoothDevice());
// set up a time out
mBluetoothAutoConnectStateMachine.sendMessageDelayed(
- BluetoothAutoConnectStateMachine.CONNECT_TIMEOUT, btParams,
+ BluetoothAutoConnectStateMachine.CONNECT_TIMEOUT, mConnectionInFlight,
BluetoothAutoConnectStateMachine.CONNECTION_TIMEOUT_MS);
break;
} else {
@@ -1072,8 +1523,7 @@
devInfo.setConnectionStateLocked(device, BluetoothProfile.STATE_CONNECTING);
// Increment the retry count & cache what is being connected to
// This method is already called from a synchronized context.
- mConnectionInFlight.setBluetoothDevice(device);
- mConnectionInFlight.setBluetoothProfile(profile);
+ mConnectionInFlight = new ConnectionParams(profile, device);
devInfo.incrementRetryCountLocked();
if (DBG) {
Log.d(TAG, "Increment Retry to: " + devInfo.getRetryCountLocked() +
@@ -1097,8 +1547,7 @@
* @param devInfo the {@link BluetoothDevicesInfo} where the info is to be reset.
*/
private void setProfileOnDeviceToUnavailable(BluetoothDevicesInfo devInfo) {
- mConnectionInFlight.setBluetoothProfile(0);
- mConnectionInFlight.setBluetoothDevice(null);
+ mConnectionInFlight = new ConnectionParams(0, null);
devInfo.setDeviceAvailableToConnectLocked(false);
}
@@ -1625,5 +2074,11 @@
writer.println("*BluetoothDeviceConnectionPolicy*");
printDeviceMap(writer);
mBluetoothAutoConnectStateMachine.dump(writer);
+ writer.println("Temporary disconnects active:");
+ String disconnects;
+ synchronized (this) {
+ disconnects = mTemporaryDisconnects.keySet().toString();
+ }
+ writer.println(disconnects);
}
}
diff --git a/service/src/com/android/car/CarBluetoothService.java b/service/src/com/android/car/CarBluetoothService.java
index d36db43..a8ffdcc 100644
--- a/service/src/com/android/car/CarBluetoothService.java
+++ b/service/src/com/android/car/CarBluetoothService.java
@@ -15,22 +15,14 @@
*/
package com.android.car;
-import static android.car.settings.CarSettings.Secure
- .KEY_BLUETOOTH_AUTOCONNECT_MESSAGING_DEVICE_PRIORITY_0;
-import static android.car.settings.CarSettings.Secure
- .KEY_BLUETOOTH_AUTOCONNECT_MESSAGING_DEVICE_PRIORITY_1;
-import static android.car.settings.CarSettings.Secure
- .KEY_BLUETOOTH_AUTOCONNECT_MUSIC_DEVICE_PRIORITY_0;
-import static android.car.settings.CarSettings.Secure
- .KEY_BLUETOOTH_AUTOCONNECT_MUSIC_DEVICE_PRIORITY_1;
-import static android.car.settings.CarSettings.Secure
- .KEY_BLUETOOTH_AUTOCONNECT_NETWORK_DEVICE_PRIORITY_0;
-import static android.car.settings.CarSettings.Secure
- .KEY_BLUETOOTH_AUTOCONNECT_NETWORK_DEVICE_PRIORITY_1;
-import static android.car.settings.CarSettings.Secure
- .KEY_BLUETOOTH_AUTOCONNECT_PHONE_DEVICE_PRIORITY_0;
-import static android.car.settings.CarSettings.Secure
- .KEY_BLUETOOTH_AUTOCONNECT_PHONE_DEVICE_PRIORITY_1;
+import static android.car.settings.CarSettings.Secure.KEY_BLUETOOTH_AUTOCONNECT_MESSAGING_DEVICE_PRIORITY_0;
+import static android.car.settings.CarSettings.Secure.KEY_BLUETOOTH_AUTOCONNECT_MESSAGING_DEVICE_PRIORITY_1;
+import static android.car.settings.CarSettings.Secure.KEY_BLUETOOTH_AUTOCONNECT_MUSIC_DEVICE_PRIORITY_0;
+import static android.car.settings.CarSettings.Secure.KEY_BLUETOOTH_AUTOCONNECT_MUSIC_DEVICE_PRIORITY_1;
+import static android.car.settings.CarSettings.Secure.KEY_BLUETOOTH_AUTOCONNECT_NETWORK_DEVICE_PRIORITY_0;
+import static android.car.settings.CarSettings.Secure.KEY_BLUETOOTH_AUTOCONNECT_NETWORK_DEVICE_PRIORITY_1;
+import static android.car.settings.CarSettings.Secure.KEY_BLUETOOTH_AUTOCONNECT_PHONE_DEVICE_PRIORITY_0;
+import static android.car.settings.CarSettings.Secure.KEY_BLUETOOTH_AUTOCONNECT_PHONE_DEVICE_PRIORITY_1;
import android.app.ActivityManager;
import android.bluetooth.BluetoothDevice;
@@ -39,6 +31,8 @@
import android.car.ICarBluetooth;
import android.content.Context;
import android.content.pm.PackageManager;
+import android.os.Binder;
+import android.os.IBinder;
import android.provider.Settings;
import android.util.Log;
@@ -57,7 +51,7 @@
private static final String TAG = "CarBluetoothService";
private final Context mContext;
private final BluetoothDeviceConnectionPolicy mBluetoothDeviceConnectionPolicy;
- private static final boolean DBG = false;
+ private static final boolean DBG = Utils.DBG;
public CarBluetoothService(Context context, CarPropertyService carPropertyService,
PerUserCarServiceHelper userSwitchService, CarUxRestrictionsManagerService uxrService) {
@@ -158,6 +152,66 @@
}
/**
+ * Request to disconnect the given profile on the given device, and prevent it from reconnecting
+ * until either the request is released, or the process owning the given token dies.
+ *
+ * @param device The device on which to disconnect a profile.
+ * @param profile The {@link android.bluetooth.BluetoothProfile} to disconnect.
+ * @param token A {@link IBinder} to be used as an identity for the request. If the process
+ * owning the token dies, the request will automatically be released.
+ * @return True if the profile was successfully disconnected, false if an error occurred.
+ */
+ @Override
+ public boolean requestTemporaryDisconnect(BluetoothDevice device, int profile, IBinder token) {
+ if (DBG) {
+ Log.d(TAG, "requestTemporaryDisconnect device=" + device + " profile=" + profile
+ + " from uid " + Binder.getCallingUid());
+ }
+ try {
+ enforceBluetoothAdminPermission();
+ if (device == null) {
+ // Will be caught by AIDL and thrown to caller.
+ throw new NullPointerException("Null device in requestTemporaryDisconnect");
+ }
+ return mBluetoothDeviceConnectionPolicy
+ .requestProfileDisconnect(device, profile, token);
+ } catch (RuntimeException e) {
+ Log.e(TAG, "Error in requestTemporaryDisconnect", e);
+ throw e;
+ }
+ }
+
+ /**
+ * Undo a previous call to {@link #requestProfileDisconnect} with the same parameters,
+ * and reconnect the profile if no other requests are active.
+ *
+ * @param device The device on which to release the disconnect request.
+ * @param profile The profile on which to release the disconnect request.
+ * @param token The token provided in the original call to {@link #requestTemporaryDisconnect}.
+ *
+ * @return True if the request was released, false if an error occurred.
+ */
+ @Override
+ public boolean releaseTemporaryDisconnect(BluetoothDevice device, int profile, IBinder token) {
+ if (DBG) {
+ Log.d(TAG, "releaseTemporaryDisconnect device=" + device + " profile=" + profile
+ + " from uid " + Binder.getCallingUid());
+ }
+ try {
+ enforceBluetoothAdminPermission();
+ if (device == null) {
+ // Will be caught by AIDL and thrown to caller.
+ throw new NullPointerException("Null device in releaseTemporaryDisconnect");
+ }
+ return mBluetoothDeviceConnectionPolicy
+ .releaseProfileDisconnect(device, profile, token);
+ } catch (RuntimeException e) {
+ Log.e(TAG, "Error in releaseTemporaryDisconnect", e);
+ throw e;
+ }
+ }
+
+ /**
* Returns the Bluetooth device address as a String that has been tagged with the given priority
* for the given profile.
*
diff --git a/service/src/com/android/car/CarBluetoothUserService.java b/service/src/com/android/car/CarBluetoothUserService.java
index 1b94742..5a57f19 100644
--- a/service/src/com/android/car/CarBluetoothUserService.java
+++ b/service/src/com/android/car/CarBluetoothUserService.java
@@ -15,22 +15,20 @@
*/
package com.android.car;
-
-import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothA2dpSink;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadsetClient;
import android.bluetooth.BluetoothMapClient;
-import android.bluetooth.BluetoothPbapClient;
import android.bluetooth.BluetoothPan;
+import android.bluetooth.BluetoothPbapClient;
+import android.bluetooth.BluetoothProfile;
import android.car.ICarBluetoothUserService;
import android.util.Log;
import java.util.Arrays;
import java.util.List;
-
public class CarBluetoothUserService extends ICarBluetoothUserService.Stub {
private static final boolean DBG = true;
private static final String TAG = "CarBluetoothUsrSvc";
@@ -147,8 +145,13 @@
Log.e(TAG, "Cannot connect to Profile. Proxy Unavailable");
return;
}
+ if (device == null) {
+ Log.e(TAG, "Cannot connect to profile on null device");
+ return;
+ }
if (DBG) {
- Log.d(TAG, "Trying to connect to " + device.getName() + " Profile: " + profile);
+ Log.d(TAG, "Trying to connect to " + device.getName() + " (" + device.getAddress()
+ + ") Profile: " + Utils.getProfileName(profile));
}
switch (profile) {
case BluetoothProfile.A2DP_SINK:
@@ -169,12 +172,93 @@
case BluetoothProfile.PAN:
mBluetoothPan.connect(device);
+ break;
default:
Log.d(TAG, "Unknown profile");
break;
}
- return;
+ }
+
+ @Override
+ public void bluetoothDisconnectFromProfile(int profile, BluetoothDevice device) {
+ if (!isBluetoothConnectionProxyAvailable(profile)) {
+ Log.e(TAG, "Cannot disconnect from profile. Proxy Unavailable");
+ return;
+ }
+ if (device == null) {
+ Log.e(TAG, "Cannot disconnect from profile on null device");
+ return;
+ }
+ if (DBG) {
+ Log.d(TAG, "Trying to disconnect from " + device.getName() + " (" + device.getAddress()
+ + ") Profile: " + Utils.getProfileName(profile));
+ }
+ switch (profile) {
+ case BluetoothProfile.A2DP_SINK:
+ mBluetoothA2dpSink.disconnect(device);
+ break;
+
+ case BluetoothProfile.HEADSET_CLIENT:
+ mBluetoothHeadsetClient.disconnect(device);
+ break;
+
+ case BluetoothProfile.MAP_CLIENT:
+ mBluetoothMapClient.disconnect(device);
+ break;
+
+ case BluetoothProfile.PBAP_CLIENT:
+ mBluetoothPbapClient.disconnect(device);
+ break;
+
+ case BluetoothProfile.PAN:
+ mBluetoothPan.disconnect(device);
+ break;
+
+ default:
+ Log.d(TAG, "Unknown profile");
+ break;
+ }
+ }
+
+ /**
+ * Get the priority of the given Bluetooth profile for the given remote device
+ * @param profile - Bluetooth profile
+ * @param device - remote Bluetooth device
+ */
+ @Override
+ public int getProfilePriority(int profile, BluetoothDevice device) {
+ if (!isBluetoothConnectionProxyAvailable(profile)) {
+ Log.e(TAG, "Cannot get profile priority. Proxy Unavailable");
+ return BluetoothProfile.PRIORITY_UNDEFINED;
+ }
+ if (device == null) {
+ Log.e(TAG, "Cannot get profile priority on null device");
+ return BluetoothProfile.PRIORITY_UNDEFINED;
+ }
+ int priority;
+ switch (profile) {
+ case BluetoothProfile.A2DP_SINK:
+ priority = mBluetoothA2dpSink.getPriority(device);
+ break;
+ case BluetoothProfile.HEADSET_CLIENT:
+ priority = mBluetoothHeadsetClient.getPriority(device);
+ break;
+ case BluetoothProfile.MAP_CLIENT:
+ priority = mBluetoothMapClient.getPriority(device);
+ break;
+ case BluetoothProfile.PBAP_CLIENT:
+ priority = mBluetoothPbapClient.getPriority(device);
+ break;
+ default:
+ Log.d(TAG, "Unknown Profile");
+ return BluetoothProfile.PRIORITY_UNDEFINED;
+ }
+ if (DBG) {
+ Log.d(TAG, Utils.getProfileName(profile) + " priority for " + device.getName() + " ("
+ + device.getAddress() + ") = " + priority);
+ }
+ return priority;
}
/**
@@ -186,9 +270,17 @@
@Override
public void setProfilePriority(int profile, BluetoothDevice device, int priority) {
if (!isBluetoothConnectionProxyAvailable(profile)) {
- Log.e(TAG, "Cannot connect to Profile. Proxy Unavailable");
+ Log.e(TAG, "Cannot set profile priority. Proxy Unavailable");
return;
}
+ if (device == null) {
+ Log.e(TAG, "Cannot set profile priority on null device");
+ return;
+ }
+ if (DBG) {
+ Log.d(TAG, "Setting " + Utils.getProfileName(profile) + " priority for "
+ + device.getName() + " (" + device.getAddress() + ") to " + priority);
+ }
switch (profile) {
case BluetoothProfile.A2DP_SINK:
mBluetoothA2dpSink.setPriority(device, priority);
diff --git a/service/src/com/android/car/SetMultimap.java b/service/src/com/android/car/SetMultimap.java
new file mode 100644
index 0000000..48e8739
--- /dev/null
+++ b/service/src/com/android/car/SetMultimap.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2019 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.car;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A simple implementation of a multimap that maps keys to sets of values.
+ *
+ * This class is (and should remain) drop-in replaceable with Guava's SetMultimap.
+ *
+ * @param <K> The type of the keys in the map.
+ * @param <V> The type of the values in the map.
+ */
+public class SetMultimap<K, V> {
+ private Map<K, Set<V>> mMap;
+
+ /** Creates a new {@link #SetMultimap}. */
+ public SetMultimap() {
+ mMap = new HashMap<>();
+ }
+
+ /** Gets the set of values associated with a given key. */
+ public Set<V> get(K key) {
+ return Collections.unmodifiableSet(mMap.getOrDefault(key, Collections.emptySet()));
+ }
+
+ /** Adds a value to the set associated with a key. */
+ public boolean put(K key, V value) {
+ return mMap.computeIfAbsent(key, k -> new HashSet<>()).add(value);
+ }
+
+ /** Checks if the multimap contains the given key and value. */
+ public boolean containsEntry(K key, V value) {
+ Set<V> set = mMap.get(key);
+ return set != null && set.contains(value);
+ }
+
+ /** Removes the given value from the set of the given key. */
+ public boolean remove(K key, V value) {
+ Set<V> set = mMap.get(key);
+ if (set == null) {
+ return false;
+ }
+
+ boolean removed = set.remove(value);
+ if (set.isEmpty()) {
+ mMap.remove(key);
+ }
+ return removed;
+ }
+
+ /** Clears all entries in the map. */
+ public void clear() {
+ mMap.clear();
+ }
+
+ /** Gets the set of keys stored in the map. */
+ public Set<K> keySet() {
+ return Collections.unmodifiableSet(mMap.keySet());
+ }
+}