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