Merge changes I8d360d47,Iebf877d8 into udc-dev am: 3527ffe7f4

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/21873306

Change-Id: Ie49a320fffb152fa3888548cbe4986b78865fb71
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index b1d2e33..4759689 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -3730,7 +3730,12 @@
     @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     @RequiresPermission(Manifest.permission.BLUETOOTH_STACK)
     public void setA2dpSuspended(boolean enable) {
-        AudioSystem.setParameters("A2dpSuspended=" + enable);
+        final IAudioService service = getService();
+        try {
+            service.setA2dpSuspended(enable);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
     }
 
     /**
@@ -3743,7 +3748,12 @@
     @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     @RequiresPermission(Manifest.permission.BLUETOOTH_STACK)
     public void setLeAudioSuspended(boolean enable) {
-        AudioSystem.setParameters("LeAudioSuspended=" + enable);
+        final IAudioService service = getService();
+        try {
+            service.setLeAudioSuspended(enable);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
     }
 
     /**
diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java
index e73cf87..3123ee6 100644
--- a/media/java/android/media/AudioSystem.java
+++ b/media/java/android/media/AudioSystem.java
@@ -1237,6 +1237,9 @@
     public static final Set<Integer> DEVICE_IN_ALL_SCO_SET;
     /** @hide */
     public static final Set<Integer> DEVICE_IN_ALL_USB_SET;
+    /** @hide */
+    public static final Set<Integer> DEVICE_IN_ALL_BLE_SET;
+
     static {
         DEVICE_IN_ALL_SET = new HashSet<>();
         DEVICE_IN_ALL_SET.add(DEVICE_IN_COMMUNICATION);
@@ -1276,6 +1279,66 @@
         DEVICE_IN_ALL_USB_SET.add(DEVICE_IN_USB_ACCESSORY);
         DEVICE_IN_ALL_USB_SET.add(DEVICE_IN_USB_DEVICE);
         DEVICE_IN_ALL_USB_SET.add(DEVICE_IN_USB_HEADSET);
+
+        DEVICE_IN_ALL_BLE_SET = new HashSet<>();
+        DEVICE_IN_ALL_BLE_SET.add(DEVICE_IN_BLE_HEADSET);
+    }
+
+    /** @hide */
+    public static boolean isBluetoothDevice(int deviceType) {
+        return isBluetoothA2dpOutDevice(deviceType)
+                || isBluetoothScoDevice(deviceType)
+                || isBluetoothLeDevice(deviceType);
+    }
+
+    /** @hide */
+    public static boolean isBluetoothOutDevice(int deviceType) {
+        return isBluetoothA2dpOutDevice(deviceType)
+                || isBluetoothScoOutDevice(deviceType)
+                || isBluetoothLeOutDevice(deviceType);
+    }
+
+    /** @hide */
+    public static boolean isBluetoothInDevice(int deviceType) {
+        return isBluetoothScoInDevice(deviceType)
+                || isBluetoothLeInDevice(deviceType);
+    }
+
+    /** @hide */
+    public static boolean isBluetoothA2dpOutDevice(int deviceType) {
+        return DEVICE_OUT_ALL_A2DP_SET.contains(deviceType);
+    }
+
+    /** @hide */
+    public static boolean isBluetoothScoOutDevice(int deviceType) {
+        return DEVICE_OUT_ALL_SCO_SET.contains(deviceType);
+    }
+
+    /** @hide */
+    public static boolean isBluetoothScoInDevice(int deviceType) {
+        return DEVICE_IN_ALL_SCO_SET.contains(deviceType);
+    }
+
+    /** @hide */
+    public static boolean isBluetoothScoDevice(int deviceType) {
+        return isBluetoothScoOutDevice(deviceType)
+                || isBluetoothScoInDevice(deviceType);
+    }
+
+    /** @hide */
+    public static boolean isBluetoothLeOutDevice(int deviceType) {
+        return DEVICE_OUT_ALL_BLE_SET.contains(deviceType);
+    }
+
+    /** @hide */
+    public static boolean isBluetoothLeInDevice(int deviceType) {
+        return DEVICE_IN_ALL_BLE_SET.contains(deviceType);
+    }
+
+    /** @hide */
+    public static boolean isBluetoothLeDevice(int deviceType) {
+        return isBluetoothLeOutDevice(deviceType)
+                || isBluetoothLeInDevice(deviceType);
     }
 
     /** @hide */
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index fe5afc5..7ce189b 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -231,6 +231,12 @@
 
     void setBluetoothScoOn(boolean on);
 
+    @EnforcePermission("BLUETOOTH_STACK")
+    void setA2dpSuspended(boolean on);
+
+    @EnforcePermission("BLUETOOTH_STACK")
+    void setLeAudioSuspended(boolean enable);
+
     boolean isBluetoothScoOn();
 
     void setBluetoothA2dpOn(boolean on);
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index 1268156..462942e 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -87,14 +87,14 @@
     private final @NonNull Context mContext;
 
     /** ID for Communication strategy retrieved form audio policy manager */
-    private int mCommunicationStrategyId = -1;
+    /*package*/  int mCommunicationStrategyId = -1;
 
     /** ID for Accessibility strategy retrieved form audio policy manager */
     private int mAccessibilityStrategyId = -1;
 
 
     /** Active communication device reported by audio policy manager */
-    private AudioDeviceInfo mActiveCommunicationDevice;
+    /*package*/ AudioDeviceInfo mActiveCommunicationDevice;
     /** Last preferred device set for communication strategy */
     private AudioDeviceAttributes mPreferredCommunicationDevice;
 
@@ -204,6 +204,7 @@
     private void init() {
         setupMessaging(mContext);
 
+        initAudioHalBluetoothState();
         initRoutingStrategyIds();
         mPreferredCommunicationDevice = null;
         updateActiveCommunicationDevice();
@@ -749,6 +750,19 @@
             mIsLeOutput = false;
         }
 
+        BtDeviceInfo(@NonNull BtDeviceInfo src, int state) {
+            mDevice = src.mDevice;
+            mState = state;
+            mProfile = src.mProfile;
+            mSupprNoisy = src.mSupprNoisy;
+            mVolume = src.mVolume;
+            mIsLeOutput = src.mIsLeOutput;
+            mEventSource = src.mEventSource;
+            mAudioSystemDevice = src.mAudioSystemDevice;
+            mMusicDevice = src.mMusicDevice;
+            mCodec = src.mCodec;
+        }
+
         // redefine equality op so we can match messages intended for this device
         @Override
         public boolean equals(Object o) {
@@ -815,7 +829,7 @@
      * @param info struct with the (dis)connection information
      */
     /*package*/ void queueOnBluetoothActiveDeviceChanged(@NonNull BtDeviceChangedData data) {
-        if (data.mInfo.getProfile() == BluetoothProfile.A2DP && data.mPreviousDevice != null
+        if (data.mPreviousDevice != null
                 && data.mPreviousDevice.equals(data.mNewDevice)) {
             final String name = TextUtils.emptyIfNull(data.mNewDevice.getName());
             new MediaMetrics.Item(MediaMetrics.Name.AUDIO_DEVICE + MediaMetrics.SEPARATOR
@@ -824,7 +838,8 @@
                     .set(MediaMetrics.Property.STATUS, data.mInfo.getProfile())
                     .record();
             synchronized (mDeviceStateLock) {
-                postBluetoothA2dpDeviceConfigChange(data.mNewDevice);
+                postBluetoothDeviceConfigChange(createBtDeviceInfo(data, data.mNewDevice,
+                        BluetoothProfile.STATE_CONNECTED));
             }
         } else {
             synchronized (mDeviceStateLock) {
@@ -845,10 +860,100 @@
         }
     }
 
-    /**
-     * Current Bluetooth SCO audio active state indicated by BtHelper via setBluetoothScoOn().
-     */
+    // Current Bluetooth SCO audio active state indicated by BtHelper via setBluetoothScoOn().
+    @GuardedBy("mDeviceStateLock")
     private boolean mBluetoothScoOn;
+    // value of BT_SCO parameter currently applied to audio HAL.
+    @GuardedBy("mDeviceStateLock")
+    private boolean mBluetoothScoOnApplied;
+
+    // A2DP suspend state requested by AudioManager.setA2dpSuspended() API.
+    @GuardedBy("mDeviceStateLock")
+    private boolean mBluetoothA2dpSuspendedExt;
+    // A2DP suspend state requested by AudioDeviceInventory.
+    @GuardedBy("mDeviceStateLock")
+    private boolean mBluetoothA2dpSuspendedInt;
+    // value of BT_A2dpSuspendedSCO parameter currently applied to audio HAL.
+    @GuardedBy("mDeviceStateLock")
+    private boolean mBluetoothA2dpSuspendedApplied;
+
+    // LE Audio suspend state requested by AudioManager.setLeAudioSuspended() API.
+    @GuardedBy("mDeviceStateLock")
+    private boolean mBluetoothLeSuspendedExt;
+    // LE Audio suspend state requested by AudioDeviceInventory.
+    @GuardedBy("mDeviceStateLock")
+    private boolean mBluetoothLeSuspendedInt;
+    // value of LeAudioSuspended parameter currently applied to audio HAL.
+    @GuardedBy("mDeviceStateLock")
+    private boolean mBluetoothLeSuspendedApplied;
+
+    private void initAudioHalBluetoothState() {
+        mBluetoothScoOnApplied = false;
+        AudioSystem.setParameters("BT_SCO=off");
+        mBluetoothA2dpSuspendedApplied = false;
+        AudioSystem.setParameters("A2dpSuspended=false");
+        mBluetoothLeSuspendedApplied = false;
+        AudioSystem.setParameters("LeAudioSuspended=false");
+    }
+
+    @GuardedBy("mDeviceStateLock")
+    private void updateAudioHalBluetoothState() {
+        if (mBluetoothScoOn != mBluetoothScoOnApplied) {
+            if (AudioService.DEBUG_COMM_RTE) {
+                Log.v(TAG, "updateAudioHalBluetoothState() mBluetoothScoOn: "
+                        + mBluetoothScoOn + ", mBluetoothScoOnApplied: " + mBluetoothScoOnApplied);
+            }
+            if (mBluetoothScoOn) {
+                if (!mBluetoothA2dpSuspendedApplied) {
+                    AudioSystem.setParameters("A2dpSuspended=true");
+                    mBluetoothA2dpSuspendedApplied = true;
+                }
+                if (!mBluetoothLeSuspendedApplied) {
+                    AudioSystem.setParameters("LeAudioSuspended=true");
+                    mBluetoothLeSuspendedApplied = true;
+                }
+                AudioSystem.setParameters("BT_SCO=on");
+            } else {
+                AudioSystem.setParameters("BT_SCO=off");
+            }
+            mBluetoothScoOnApplied = mBluetoothScoOn;
+        }
+        if (!mBluetoothScoOnApplied) {
+            if ((mBluetoothA2dpSuspendedExt || mBluetoothA2dpSuspendedInt)
+                    != mBluetoothA2dpSuspendedApplied) {
+                if (AudioService.DEBUG_COMM_RTE) {
+                    Log.v(TAG, "updateAudioHalBluetoothState() mBluetoothA2dpSuspendedExt: "
+                            + mBluetoothA2dpSuspendedExt
+                            + ", mBluetoothA2dpSuspendedInt: " + mBluetoothA2dpSuspendedInt
+                            + ", mBluetoothA2dpSuspendedApplied: "
+                            + mBluetoothA2dpSuspendedApplied);
+                }
+                mBluetoothA2dpSuspendedApplied =
+                        mBluetoothA2dpSuspendedExt || mBluetoothA2dpSuspendedInt;
+                if (mBluetoothA2dpSuspendedApplied) {
+                    AudioSystem.setParameters("A2dpSuspended=true");
+                } else {
+                    AudioSystem.setParameters("A2dpSuspended=false");
+                }
+            }
+            if ((mBluetoothLeSuspendedExt || mBluetoothLeSuspendedInt)
+                    != mBluetoothLeSuspendedApplied) {
+                if (AudioService.DEBUG_COMM_RTE) {
+                    Log.v(TAG, "updateAudioHalBluetoothState() mBluetoothLeSuspendedExt: "
+                            + mBluetoothLeSuspendedExt
+                            + ", mBluetoothLeSuspendedInt: " + mBluetoothLeSuspendedInt
+                            + ", mBluetoothLeSuspendedApplied: " + mBluetoothLeSuspendedApplied);
+                }
+                mBluetoothLeSuspendedApplied =
+                        mBluetoothLeSuspendedExt || mBluetoothLeSuspendedInt;
+                if (mBluetoothLeSuspendedApplied) {
+                    AudioSystem.setParameters("LeAudioSuspended=true");
+                } else {
+                    AudioSystem.setParameters("LeAudioSuspended=false");
+                }
+            }
+        }
+    }
 
     /*package*/ void setBluetoothScoOn(boolean on, String eventSource) {
         if (AudioService.DEBUG_COMM_RTE) {
@@ -856,10 +961,67 @@
         }
         synchronized (mDeviceStateLock) {
             mBluetoothScoOn = on;
+            updateAudioHalBluetoothState();
             postUpdateCommunicationRouteClient(eventSource);
         }
     }
 
+    /*package*/ void setA2dpSuspended(boolean enable, boolean internal, String eventSource) {
+        if (AudioService.DEBUG_COMM_RTE) {
+            Log.v(TAG, "setA2dpSuspended source: " + eventSource + ", enable: "
+                    + enable + ", internal: " + internal
+                    + ", mBluetoothA2dpSuspendedInt: " + mBluetoothA2dpSuspendedInt
+                    + ", mBluetoothA2dpSuspendedExt: " + mBluetoothA2dpSuspendedExt);
+        }
+        synchronized (mDeviceStateLock) {
+            if (internal) {
+                mBluetoothA2dpSuspendedInt = enable;
+            } else {
+                mBluetoothA2dpSuspendedExt = enable;
+            }
+            updateAudioHalBluetoothState();
+        }
+    }
+
+    /*package*/ void clearA2dpSuspended() {
+        if (AudioService.DEBUG_COMM_RTE) {
+            Log.v(TAG, "clearA2dpSuspended");
+        }
+        synchronized (mDeviceStateLock) {
+            mBluetoothA2dpSuspendedInt = false;
+            mBluetoothA2dpSuspendedExt = false;
+            updateAudioHalBluetoothState();
+        }
+    }
+
+    /*package*/ void setLeAudioSuspended(boolean enable, boolean internal, String eventSource) {
+        if (AudioService.DEBUG_COMM_RTE) {
+            Log.v(TAG, "setLeAudioSuspended source: " + eventSource + ", enable: "
+                    + enable + ", internal: " + internal
+                    + ", mBluetoothLeSuspendedInt: " + mBluetoothA2dpSuspendedInt
+                    + ", mBluetoothLeSuspendedExt: " + mBluetoothA2dpSuspendedExt);
+        }
+        synchronized (mDeviceStateLock) {
+            if (internal) {
+                mBluetoothLeSuspendedInt = enable;
+            } else {
+                mBluetoothLeSuspendedExt = enable;
+            }
+            updateAudioHalBluetoothState();
+        }
+    }
+
+    /*package*/ void clearLeAudioSuspended() {
+        if (AudioService.DEBUG_COMM_RTE) {
+            Log.v(TAG, "clearLeAudioSuspended");
+        }
+        synchronized (mDeviceStateLock) {
+            mBluetoothLeSuspendedInt = false;
+            mBluetoothLeSuspendedExt = false;
+            updateAudioHalBluetoothState();
+        }
+    }
+
     /*package*/ AudioRoutesInfo startWatchingRoutes(IAudioRoutesObserver observer) {
         synchronized (mDeviceStateLock) {
             return mDeviceInventory.startWatchingRoutes(observer);
@@ -902,8 +1064,8 @@
                 new AudioModeInfo(mode, pid, uid));
     }
 
-    /*package*/ void postBluetoothA2dpDeviceConfigChange(@NonNull BluetoothDevice device) {
-        sendLMsgNoDelay(MSG_L_A2DP_DEVICE_CONFIG_CHANGE, SENDMSG_QUEUE, device);
+    /*package*/ void postBluetoothDeviceConfigChange(@NonNull BtDeviceInfo info) {
+        sendLMsgNoDelay(MSG_L_BLUETOOTH_DEVICE_CONFIG_CHANGE, SENDMSG_QUEUE, info);
     }
 
     /*package*/ void startBluetoothScoForClient(IBinder cb, int pid, int scoAudioMode,
@@ -1160,6 +1322,10 @@
         sendIMsgNoDelay(MSG_I_SCO_AUDIO_STATE_CHANGED, SENDMSG_QUEUE, state);
     }
 
+    /*package*/ void postNotifyPreferredAudioProfileApplied(BluetoothDevice btDevice) {
+        sendLMsgNoDelay(MSG_L_NOTIFY_PREFERRED_AUDIOPROFILE_APPLIED, SENDMSG_QUEUE, btDevice);
+    }
+
     /*package*/ static final class CommunicationDeviceInfo {
         final @NonNull IBinder mCb; // Identifies the requesting client for death handler
         final int mPid; // Requester process ID
@@ -1235,9 +1401,11 @@
         }
     }
 
-    /*package*/ boolean handleDeviceConnection(AudioDeviceAttributes attributes, boolean connect) {
+    /*package*/ boolean handleDeviceConnection(AudioDeviceAttributes attributes,
+                                boolean connect, @Nullable BluetoothDevice btDevice) {
         synchronized (mDeviceStateLock) {
-            return mDeviceInventory.handleDeviceConnection(attributes, connect, false /*for test*/);
+            return mDeviceInventory.handleDeviceConnection(
+                    attributes, connect, false /*for test*/, btDevice);
         }
     }
 
@@ -1478,13 +1646,10 @@
                                 (String) msg.obj, msg.arg1);
                     }
                     break;
-                case MSG_L_A2DP_DEVICE_CONFIG_CHANGE:
-                    final BluetoothDevice btDevice = (BluetoothDevice) msg.obj;
+                case MSG_L_BLUETOOTH_DEVICE_CONFIG_CHANGE:
                     synchronized (mDeviceStateLock) {
-                        final int a2dpCodec = mBtHelper.getA2dpCodec(btDevice);
-                        mDeviceInventory.onBluetoothA2dpDeviceConfigChange(
-                                new BtHelper.BluetoothA2dpDeviceInfo(btDevice, -1, a2dpCodec),
-                                        BtHelper.EVENT_DEVICE_CONFIG_CHANGE);
+                        mDeviceInventory.onBluetoothDeviceConfigChange(
+                                (BtDeviceInfo) msg.obj, BtHelper.EVENT_DEVICE_CONFIG_CHANGE);
                     }
                     break;
                 case MSG_BROADCAST_AUDIO_BECOMING_NOISY:
@@ -1642,6 +1807,10 @@
                     final int capturePreset = msg.arg1;
                     mDeviceInventory.onSaveClearPreferredDevicesForCapturePreset(capturePreset);
                 } break;
+                case MSG_L_NOTIFY_PREFERRED_AUDIOPROFILE_APPLIED: {
+                    final BluetoothDevice btDevice = (BluetoothDevice) msg.obj;
+                    BtHelper.onNotifyPreferredAudioProfileApplied(btDevice);
+                } break;
                 default:
                     Log.wtf(TAG, "Invalid message " + msg.what);
             }
@@ -1677,7 +1846,7 @@
     private static final int MSG_IL_BTA2DP_TIMEOUT = 10;
 
     // process change of A2DP device configuration, obj is BluetoothDevice
-    private static final int MSG_L_A2DP_DEVICE_CONFIG_CHANGE = 11;
+    private static final int MSG_L_BLUETOOTH_DEVICE_CONFIG_CHANGE = 11;
 
     private static final int MSG_BROADCAST_AUDIO_BECOMING_NOISY = 12;
     private static final int MSG_REPORT_NEW_ROUTES = 13;
@@ -1717,13 +1886,15 @@
     private static final int MSG_IL_SAVE_REMOVE_NDEF_DEVICE_FOR_STRATEGY = 48;
     private static final int MSG_IL_BTLEAUDIO_TIMEOUT = 49;
 
+    private static final int MSG_L_NOTIFY_PREFERRED_AUDIOPROFILE_APPLIED = 50;
+
     private static boolean isMessageHandledUnderWakelock(int msgId) {
         switch(msgId) {
             case MSG_L_SET_WIRED_DEVICE_CONNECTION_STATE:
             case MSG_L_SET_BT_ACTIVE_DEVICE:
             case MSG_IL_BTA2DP_TIMEOUT:
             case MSG_IL_BTLEAUDIO_TIMEOUT:
-            case MSG_L_A2DP_DEVICE_CONFIG_CHANGE:
+            case MSG_L_BLUETOOTH_DEVICE_CONFIG_CHANGE:
             case MSG_TOGGLE_HDMI:
             case MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT:
             case MSG_L_HEARING_AID_DEVICE_CONNECTION_CHANGE_EXT:
@@ -1815,7 +1986,7 @@
                 case MSG_L_SET_WIRED_DEVICE_CONNECTION_STATE:
                 case MSG_IL_BTA2DP_TIMEOUT:
                 case MSG_IL_BTLEAUDIO_TIMEOUT:
-                case MSG_L_A2DP_DEVICE_CONFIG_CHANGE:
+                case MSG_L_BLUETOOTH_DEVICE_CONFIG_CHANGE:
                     if (sLastDeviceConnectMsgTime >= time) {
                         // add a little delay to make sure messages are ordered as expected
                         time = sLastDeviceConnectMsgTime + 30;
@@ -1835,7 +2006,7 @@
     static {
         MESSAGES_MUTE_MUSIC = new HashSet<>();
         MESSAGES_MUTE_MUSIC.add(MSG_L_SET_BT_ACTIVE_DEVICE);
-        MESSAGES_MUTE_MUSIC.add(MSG_L_A2DP_DEVICE_CONFIG_CHANGE);
+        MESSAGES_MUTE_MUSIC.add(MSG_L_BLUETOOTH_DEVICE_CONFIG_CHANGE);
         MESSAGES_MUTE_MUSIC.add(MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT);
         MESSAGES_MUTE_MUSIC.add(MSG_IIL_SET_FORCE_BT_A2DP_USE);
     }
@@ -1856,7 +2027,7 @@
         // Do not mute on bluetooth event if music is playing on a wired headset.
         if ((message == MSG_L_SET_BT_ACTIVE_DEVICE
                 || message == MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT
-                || message == MSG_L_A2DP_DEVICE_CONFIG_CHANGE)
+                || message == MSG_L_BLUETOOTH_DEVICE_CONFIG_CHANGE)
                 && AudioSystem.isStreamActive(AudioSystem.STREAM_MUSIC, 0)
                 && hasIntersection(mDeviceInventory.DEVICE_OVERRIDE_A2DP_ROUTE_ON_PLUG_SET,
                         mAudioService.getDeviceSetForStream(AudioSystem.STREAM_MUSIC))) {
@@ -1992,27 +2163,22 @@
                 "updateCommunicationRoute, preferredCommunicationDevice: "
                 + preferredCommunicationDevice + " eventSource: " + eventSource)));
 
-        if (preferredCommunicationDevice == null
-                || preferredCommunicationDevice.getType() != AudioDeviceInfo.TYPE_BLUETOOTH_SCO) {
-            AudioSystem.setParameters("BT_SCO=off");
-        } else {
-            AudioSystem.setParameters("BT_SCO=on");
-        }
         if (preferredCommunicationDevice == null) {
             AudioDeviceAttributes defaultDevice = getDefaultCommunicationDevice();
             if (defaultDevice != null) {
-                setPreferredDevicesForStrategySync(
+                mDeviceInventory.setPreferredDevicesForStrategy(
                         mCommunicationStrategyId, Arrays.asList(defaultDevice));
-                setPreferredDevicesForStrategySync(
+                mDeviceInventory.setPreferredDevicesForStrategy(
                         mAccessibilityStrategyId, Arrays.asList(defaultDevice));
             } else {
-                removePreferredDevicesForStrategySync(mCommunicationStrategyId);
-                removePreferredDevicesForStrategySync(mAccessibilityStrategyId);
+                mDeviceInventory.removePreferredDevicesForStrategy(mCommunicationStrategyId);
+                mDeviceInventory.removePreferredDevicesForStrategy(mAccessibilityStrategyId);
             }
+            mDeviceInventory.applyConnectedDevicesRoles();
         } else {
-            setPreferredDevicesForStrategySync(
+            mDeviceInventory.setPreferredDevicesForStrategy(
                     mCommunicationStrategyId, Arrays.asList(preferredCommunicationDevice));
-            setPreferredDevicesForStrategySync(
+            mDeviceInventory.setPreferredDevicesForStrategy(
                     mAccessibilityStrategyId, Arrays.asList(preferredCommunicationDevice));
         }
         onUpdatePhoneStrategyDevice(preferredCommunicationDevice);
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index 43063af..1eb39f7 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -34,26 +34,36 @@
 import android.media.IStrategyNonDefaultDevicesDispatcher;
 import android.media.IStrategyPreferredDevicesDispatcher;
 import android.media.MediaMetrics;
+import android.media.MediaRecorder.AudioSource;
+import android.media.audiopolicy.AudioProductStrategy;
 import android.media.permission.ClearCallingIdentityContext;
 import android.media.permission.SafeCloseable;
 import android.os.Binder;
+import android.os.Bundle;
 import android.os.RemoteCallbackList;
 import android.os.RemoteException;
+import android.os.SystemProperties;
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.Log;
+import android.util.Pair;
 import android.util.Slog;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.utils.EventLogger;
 
+import com.google.android.collect.Sets;
+
 import java.io.PrintWriter;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 import java.util.UUID;
@@ -175,18 +185,26 @@
     final RemoteCallbackList<ICapturePresetDevicesRoleDispatcher> mDevRoleCapturePresetDispatchers =
             new RemoteCallbackList<ICapturePresetDevicesRoleDispatcher>();
 
+    final List<AudioProductStrategy> mStrategies;
+
     /*package*/ AudioDeviceInventory(@NonNull AudioDeviceBroker broker) {
-        mDeviceBroker = broker;
-        mAudioSystem = AudioSystemAdapter.getDefaultAdapter();
+        this(broker, AudioSystemAdapter.getDefaultAdapter());
     }
 
     //-----------------------------------------------------------
     /** for mocking only, allows to inject AudioSystem adapter */
     /*package*/ AudioDeviceInventory(@NonNull AudioSystemAdapter audioSystem) {
-        mDeviceBroker = null;
-        mAudioSystem = audioSystem;
+        this(null, audioSystem);
     }
 
+    private AudioDeviceInventory(@Nullable AudioDeviceBroker broker,
+                       @Nullable AudioSystemAdapter audioSystem) {
+        mDeviceBroker = broker;
+        mAudioSystem = audioSystem;
+        mStrategies = AudioProductStrategy.getAudioProductStrategies();
+        mBluetoothDualModeEnabled = SystemProperties.getBoolean(
+                "persist.bluetooth.enable_dual_mode_audio", false);
+    }
     /*package*/ void setDeviceBroker(@NonNull AudioDeviceBroker broker) {
         mDeviceBroker = broker;
     }
@@ -203,8 +221,13 @@
         int mDeviceCodecFormat;
         final UUID mSensorUuid;
 
+        /** Disabled operating modes for this device. Use a negative logic so that by default
+         * an empty list means all modes are allowed.
+         * See BluetoothAdapter.AUDIO_MODE_DUPLEX and BluetoothAdapter.AUDIO_MODE_OUTPUT_ONLY */
+        @NonNull ArraySet<String> mDisabledModes = new ArraySet(0);
+
         DeviceInfo(int deviceType, String deviceName, String deviceAddress,
-                   int deviceCodecFormat, UUID sensorUuid) {
+                   int deviceCodecFormat, @Nullable UUID sensorUuid) {
             mDeviceType = deviceType;
             mDeviceName = deviceName == null ? "" : deviceName;
             mDeviceAddress = deviceAddress == null ? "" : deviceAddress;
@@ -212,11 +235,31 @@
             mSensorUuid = sensorUuid;
         }
 
+        void setModeDisabled(String mode) {
+            mDisabledModes.add(mode);
+        }
+        void setModeEnabled(String mode) {
+            mDisabledModes.remove(mode);
+        }
+        boolean isModeEnabled(String mode) {
+            return !mDisabledModes.contains(mode);
+        }
+        boolean isOutputOnlyModeEnabled() {
+            return isModeEnabled(BluetoothAdapter.AUDIO_MODE_OUTPUT_ONLY);
+        }
+        boolean isDuplexModeEnabled() {
+            return isModeEnabled(BluetoothAdapter.AUDIO_MODE_DUPLEX);
+        }
+
         DeviceInfo(int deviceType, String deviceName, String deviceAddress,
                    int deviceCodecFormat) {
             this(deviceType, deviceName, deviceAddress, deviceCodecFormat, null);
         }
 
+        DeviceInfo(int deviceType, String deviceName, String deviceAddress) {
+            this(deviceType, deviceName, deviceAddress, AudioSystem.AUDIO_FORMAT_DEFAULT);
+        }
+
         @Override
         public String toString() {
             return "[DeviceInfo: type:0x" + Integer.toHexString(mDeviceType)
@@ -224,7 +267,8 @@
                     + ") name:" + mDeviceName
                     + " addr:" + mDeviceAddress
                     + " codec: " + Integer.toHexString(mDeviceCodecFormat)
-                    + " sensorUuid: " + Objects.toString(mSensorUuid) + "]";
+                    + " sensorUuid: " + Objects.toString(mSensorUuid)
+                    + " disabled modes: " + mDisabledModes + "]";
         }
 
         @NonNull String getKey() {
@@ -276,9 +320,18 @@
             pw.println("  " + prefix + " type:0x" + Integer.toHexString(keyType)
                     + " (" + AudioSystem.getDeviceName(keyType)
                     + ") addr:" + valueAddress); });
+        pw.println("\n" + prefix + "Preferred devices for capture preset:");
         mPreferredDevicesForCapturePreset.forEach((capturePreset, devices) -> {
             pw.println("  " + prefix + "capturePreset:" + capturePreset
                     + " devices:" + devices); });
+        pw.println("\n" + prefix + "Applied devices roles for strategies:");
+        mAppliedStrategyRoles.forEach((key, devices) -> {
+            pw.println("  " + prefix + "strategy: " + key.first
+                    +  " role:" + key.second + " devices:" + devices); });
+        pw.println("\n" + prefix + "Applied devices roles for presets:");
+        mAppliedPresetRoles.forEach((key, devices) -> {
+            pw.println("  " + prefix + "preset: " + key.first
+                    +  " role:" + key.second + " devices:" + devices); });
     }
 
     //------------------------------------------------------------
@@ -299,15 +352,16 @@
                         AudioSystem.DEVICE_STATE_AVAILABLE,
                         di.mDeviceCodecFormat);
             }
+            mAppliedStrategyRoles.clear();
+            applyConnectedDevicesRoles_l();
         }
         synchronized (mPreferredDevices) {
             mPreferredDevices.forEach((strategy, devices) -> {
-                mAudioSystem.setDevicesRoleForStrategy(
-                        strategy, AudioSystem.DEVICE_ROLE_PREFERRED, devices); });
+                setPreferredDevicesForStrategy(strategy, devices); });
         }
         synchronized (mNonDefaultDevices) {
             mNonDefaultDevices.forEach((strategy, devices) -> {
-                mAudioSystem.setDevicesRoleForStrategy(
+                addDevicesRoleForStrategy(
                         strategy, AudioSystem.DEVICE_ROLE_DISABLED, devices); });
         }
         synchronized (mPreferredDevicesForCapturePreset) {
@@ -380,8 +434,7 @@
                                     btInfo.mVolume * 10, btInfo.mAudioSystemDevice,
                                     "onSetBtActiveDevice");
                         }
-                        makeA2dpDeviceAvailable(address, BtHelper.getName(btInfo.mDevice),
-                                "onSetBtActiveDevice", btInfo.mCodec);
+                        makeA2dpDeviceAvailable(btInfo, "onSetBtActiveDevice");
                     }
                     break;
                 case BluetoothProfile.HEARING_AID:
@@ -397,10 +450,7 @@
                     if (switchToUnavailable) {
                         makeLeAudioDeviceUnavailableNow(address, btInfo.mAudioSystemDevice);
                     } else if (switchToAvailable) {
-                        makeLeAudioDeviceAvailable(address, BtHelper.getName(btInfo.mDevice),
-                                streamType, btInfo.mVolume == -1 ? -1 : btInfo.mVolume * 10,
-                                btInfo.mAudioSystemDevice,
-                                "onSetBtActiveDevice");
+                        makeLeAudioDeviceAvailable(btInfo, streamType, "onSetBtActiveDevice");
                     }
                     break;
                 default: throw new IllegalArgumentException("Invalid profile "
@@ -411,30 +461,30 @@
 
 
     @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
-        /*package*/ void onBluetoothA2dpDeviceConfigChange(
-            @NonNull BtHelper.BluetoothA2dpDeviceInfo btInfo, int event) {
+    /*package*/ void onBluetoothDeviceConfigChange(
+            @NonNull AudioDeviceBroker.BtDeviceInfo btInfo, int event) {
         MediaMetrics.Item mmi = new MediaMetrics.Item(mMetricsId
-                + "onBluetoothA2dpDeviceConfigChange")
-                .set(MediaMetrics.Property.EVENT, BtHelper.a2dpDeviceEventToString(event));
+                + "onBluetoothDeviceConfigChange")
+                .set(MediaMetrics.Property.EVENT, BtHelper.deviceEventToString(event));
 
-        final BluetoothDevice btDevice = btInfo.getBtDevice();
+        final BluetoothDevice btDevice = btInfo.mDevice;
         if (btDevice == null) {
             mmi.set(MediaMetrics.Property.EARLY_RETURN, "btDevice null").record();
             return;
         }
         if (AudioService.DEBUG_DEVICES) {
-            Log.d(TAG, "onBluetoothA2dpDeviceConfigChange btDevice=" + btDevice);
+            Log.d(TAG, "onBluetoothDeviceConfigChange btDevice=" + btDevice);
         }
-        int a2dpVolume = btInfo.getVolume();
-        @AudioSystem.AudioFormatNativeEnumForBtCodec final int a2dpCodec = btInfo.getCodec();
+        int volume = btInfo.mVolume;
+        @AudioSystem.AudioFormatNativeEnumForBtCodec final int audioCodec = btInfo.mCodec;
 
         String address = btDevice.getAddress();
         if (!BluetoothAdapter.checkBluetoothAddress(address)) {
             address = "";
         }
         AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
-                "onBluetoothA2dpDeviceConfigChange addr=" + address
-                    + " event=" + BtHelper.a2dpDeviceEventToString(event)));
+                "onBluetoothDeviceConfigChange addr=" + address
+                    + " event=" + BtHelper.deviceEventToString(event)));
 
         synchronized (mDevicesLock) {
             if (mDeviceBroker.hasScheduledA2dpConnection(btDevice)) {
@@ -449,53 +499,54 @@
                     AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address);
             final DeviceInfo di = mConnectedDevices.get(key);
             if (di == null) {
-                Log.e(TAG, "invalid null DeviceInfo in onBluetoothA2dpDeviceConfigChange");
+                Log.e(TAG, "invalid null DeviceInfo in onBluetoothDeviceConfigChange");
                 mmi.set(MediaMetrics.Property.EARLY_RETURN, "null DeviceInfo").record();
                 return;
             }
 
             mmi.set(MediaMetrics.Property.ADDRESS, address)
                     .set(MediaMetrics.Property.ENCODING,
-                            AudioSystem.audioFormatToString(a2dpCodec))
-                    .set(MediaMetrics.Property.INDEX, a2dpVolume)
+                            AudioSystem.audioFormatToString(audioCodec))
+                    .set(MediaMetrics.Property.INDEX, volume)
                     .set(MediaMetrics.Property.NAME, di.mDeviceName);
 
-            if (event == BtHelper.EVENT_ACTIVE_DEVICE_CHANGE) {
-                // Device is connected
-                if (a2dpVolume != -1) {
-                    mDeviceBroker.postSetVolumeIndexOnDevice(AudioSystem.STREAM_MUSIC,
-                            // convert index to internal representation in VolumeStreamState
-                            a2dpVolume * 10,
-                            AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP,
-                            "onBluetoothA2dpDeviceConfigChange");
-                }
-            } else if (event == BtHelper.EVENT_DEVICE_CONFIG_CHANGE) {
-                if (di.mDeviceCodecFormat != a2dpCodec) {
-                    di.mDeviceCodecFormat = a2dpCodec;
-                    mConnectedDevices.replace(key, di);
-                }
-            }
-            final int res = mAudioSystem.handleDeviceConfigChange(
-                    AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address,
-                    BtHelper.getName(btDevice), a2dpCodec);
 
-            if (res != AudioSystem.AUDIO_STATUS_OK) {
-                AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
-                        "APM handleDeviceConfigChange failed for A2DP device addr=" + address
-                                + " codec=" + AudioSystem.audioFormatToString(a2dpCodec))
-                        .printLog(TAG));
+            if (event == BtHelper.EVENT_DEVICE_CONFIG_CHANGE) {
+                boolean a2dpCodecChange = false;
+                if (btInfo.mProfile == BluetoothProfile.A2DP) {
+                    if (di.mDeviceCodecFormat != audioCodec) {
+                        di.mDeviceCodecFormat = audioCodec;
+                        mConnectedDevices.replace(key, di);
+                        a2dpCodecChange = true;
+                    }
+                    final int res = mAudioSystem.handleDeviceConfigChange(
+                            btInfo.mAudioSystemDevice, address,
+                            BtHelper.getName(btDevice), audioCodec);
 
-                int musicDevice = mDeviceBroker.getDeviceForStream(AudioSystem.STREAM_MUSIC);
-                // force A2DP device disconnection in case of error so that AudioService state is
-                // consistent with audio policy manager state
-                setBluetoothActiveDevice(new AudioDeviceBroker.BtDeviceInfo(btDevice,
-                                BluetoothProfile.A2DP, BluetoothProfile.STATE_DISCONNECTED,
-                                musicDevice, AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP));
-            } else {
-                AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
-                        "APM handleDeviceConfigChange success for A2DP device addr=" + address
-                                + " codec=" + AudioSystem.audioFormatToString(a2dpCodec))
-                        .printLog(TAG));
+                    if (res != AudioSystem.AUDIO_STATUS_OK) {
+                        AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
+                                "APM handleDeviceConfigChange failed for A2DP device addr="
+                                        + address + " codec="
+                                        + AudioSystem.audioFormatToString(audioCodec))
+                                .printLog(TAG));
+
+                        // force A2DP device disconnection in case of error so that AudioService
+                        // state is consistent with audio policy manager state
+                        setBluetoothActiveDevice(new AudioDeviceBroker.BtDeviceInfo(btInfo,
+                                BluetoothProfile.STATE_DISCONNECTED));
+                    } else {
+                        AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
+                                "APM handleDeviceConfigChange success for A2DP device addr="
+                                        + address
+                                        + " codec=" + AudioSystem.audioFormatToString(audioCodec))
+                                .printLog(TAG));
+
+                    }
+                }
+                if (!a2dpCodecChange) {
+                    updateBluetoothPreferredModes_l();
+                    mDeviceBroker.postNotifyPreferredAudioProfileApplied(btDevice);
+                }
             }
         }
         mmi.record();
@@ -578,7 +629,7 @@
             }
 
             if (!handleDeviceConnection(wdcs.mAttributes,
-                    wdcs.mState == AudioService.CONNECTION_STATE_CONNECTED, wdcs.mForTest)) {
+                    wdcs.mState == AudioService.CONNECTION_STATE_CONNECTED, wdcs.mForTest, null)) {
                 // change of connection state failed, bailout
                 mmi.set(MediaMetrics.Property.EARLY_RETURN, "change of connection state failed")
                         .record();
@@ -712,23 +763,35 @@
 
     /*package*/ int setPreferredDevicesForStrategySync(int strategy,
             @NonNull List<AudioDeviceAttributes> devices) {
-        int status = AudioSystem.ERROR;
-
-        try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
-            AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
-                            "setPreferredDevicesForStrategySync, strategy: " + strategy
-                            + " devices: " + devices)).printLog(TAG));
-            status = mAudioSystem.setDevicesRoleForStrategy(
-                    strategy, AudioSystem.DEVICE_ROLE_PREFERRED, devices);
-        }
-
+        final int status = setPreferredDevicesForStrategy(strategy, devices);
         if (status == AudioSystem.SUCCESS) {
             mDeviceBroker.postSaveSetPreferredDevicesForStrategy(strategy, devices);
         }
         return status;
     }
 
+    /*package*/ int setPreferredDevicesForStrategy(int strategy,
+            @NonNull List<AudioDeviceAttributes> devices) {
+        int status = AudioSystem.ERROR;
+        try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+            AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
+                    "setPreferredDevicesForStrategy, strategy: " + strategy
+                            + " devices: " + devices)).printLog(TAG));
+            status = setDevicesRoleForStrategy(
+                    strategy, AudioSystem.DEVICE_ROLE_PREFERRED, devices);
+        }
+        return status;
+    }
+
     /*package*/ int removePreferredDevicesForStrategySync(int strategy) {
+        final int status = removePreferredDevicesForStrategy(strategy);
+        if (status == AudioSystem.SUCCESS) {
+            mDeviceBroker.postSaveRemovePreferredDevicesForStrategy(strategy);
+        }
+        return status;
+    }
+
+    /*package*/ int removePreferredDevicesForStrategy(int strategy) {
         int status = AudioSystem.ERROR;
 
         try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
@@ -736,13 +799,9 @@
                             "removePreferredDevicesForStrategySync, strategy: "
                             + strategy)).printLog(TAG));
 
-            status = mAudioSystem.clearDevicesRoleForStrategy(
+            status = clearDevicesRoleForStrategy(
                     strategy, AudioSystem.DEVICE_ROLE_PREFERRED);
         }
-
-        if (status == AudioSystem.SUCCESS) {
-            mDeviceBroker.postSaveRemovePreferredDevicesForStrategy(strategy);
-        }
         return status;
     }
 
@@ -757,7 +816,7 @@
             AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
                             "setDeviceAsNonDefaultForStrategySync, strategy: " + strategy
                             + " device: " + device)).printLog(TAG));
-            status = mAudioSystem.setDevicesRoleForStrategy(
+            status = addDevicesRoleForStrategy(
                     strategy, AudioSystem.DEVICE_ROLE_DISABLED, devices);
         }
 
@@ -779,7 +838,7 @@
                             "removeDeviceAsNonDefaultForStrategySync, strategy: "
                             + strategy + " devices: " + device)).printLog(TAG));
 
-            status = mAudioSystem.removeDevicesRoleForStrategy(
+            status = removeDevicesRoleForStrategy(
                     strategy, AudioSystem.DEVICE_ROLE_DISABLED, devices);
         }
 
@@ -812,33 +871,70 @@
 
     /*package*/ int setPreferredDevicesForCapturePresetSync(
             int capturePreset, @NonNull List<AudioDeviceAttributes> devices) {
-        int status = AudioSystem.ERROR;
-
-        try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
-            status = mAudioSystem.setDevicesRoleForCapturePreset(
-                    capturePreset, AudioSystem.DEVICE_ROLE_PREFERRED, devices);
-        }
-
+        final int status = setPreferredDevicesForCapturePreset(capturePreset, devices);
         if (status == AudioSystem.SUCCESS) {
             mDeviceBroker.postSaveSetPreferredDevicesForCapturePreset(capturePreset, devices);
         }
         return status;
     }
 
-    /*package*/ int clearPreferredDevicesForCapturePresetSync(int capturePreset) {
-        int status  = AudioSystem.ERROR;
-
+    private int setPreferredDevicesForCapturePreset(
+            int capturePreset, @NonNull List<AudioDeviceAttributes> devices) {
+        int status = AudioSystem.ERROR;
         try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
-            status = mAudioSystem.clearDevicesRoleForCapturePreset(
-                    capturePreset, AudioSystem.DEVICE_ROLE_PREFERRED);
+            status = setDevicesRoleForCapturePreset(
+                    capturePreset, AudioSystem.DEVICE_ROLE_PREFERRED, devices);
         }
+        return status;
+    }
 
+    /*package*/ int clearPreferredDevicesForCapturePresetSync(int capturePreset) {
+        final int status  = clearPreferredDevicesForCapturePreset(capturePreset);
         if (status == AudioSystem.SUCCESS) {
             mDeviceBroker.postSaveClearPreferredDevicesForCapturePreset(capturePreset);
         }
         return status;
     }
 
+    private int clearPreferredDevicesForCapturePreset(int capturePreset) {
+        int status  = AudioSystem.ERROR;
+
+        try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+            status = clearDevicesRoleForCapturePreset(
+                    capturePreset, AudioSystem.DEVICE_ROLE_PREFERRED);
+        }
+        return status;
+    }
+
+    private int addDevicesRoleForCapturePreset(int capturePreset, int role,
+                                               @NonNull List<AudioDeviceAttributes> devices) {
+        return addDevicesRole(mAppliedPresetRoles, (p, r, d) -> {
+            return mAudioSystem.addDevicesRoleForCapturePreset(p, r, d);
+        }, capturePreset, role, devices);
+    }
+
+    private int removeDevicesRoleForCapturePreset(int capturePreset, int role,
+                                                  @NonNull List<AudioDeviceAttributes> devices) {
+        return removeDevicesRole(mAppliedPresetRoles, (p, r, d) -> {
+            return mAudioSystem.removeDevicesRoleForCapturePreset(p, r, d);
+        }, capturePreset, role, devices);
+    }
+
+    private int setDevicesRoleForCapturePreset(int capturePreset, int role,
+                                               @NonNull List<AudioDeviceAttributes> devices) {
+        return setDevicesRole(mAppliedPresetRoles, (p, r, d) -> {
+            return mAudioSystem.addDevicesRoleForCapturePreset(p, r, d);
+        }, (p, r, d) -> {
+                return mAudioSystem.clearDevicesRoleForCapturePreset(p, r);
+            }, capturePreset, role, devices);
+    }
+
+    private int clearDevicesRoleForCapturePreset(int capturePreset, int role) {
+        return clearDevicesRole(mAppliedPresetRoles, (p, r, d) -> {
+            return mAudioSystem.clearDevicesRoleForCapturePreset(p, r);
+        }, capturePreset, role);
+    }
+
     /*package*/ void registerCapturePresetDevicesRoleDispatcher(
             @NonNull ICapturePresetDevicesRoleDispatcher dispatcher) {
         mDevRoleCapturePresetDispatchers.register(dispatcher);
@@ -849,7 +945,208 @@
         mDevRoleCapturePresetDispatchers.unregister(dispatcher);
     }
 
-    //-----------------------------------------------------------------------
+    private int addDevicesRoleForStrategy(int strategy, int role,
+                                          @NonNull List<AudioDeviceAttributes> devices) {
+        return addDevicesRole(mAppliedStrategyRoles, (s, r, d) -> {
+            return mAudioSystem.setDevicesRoleForStrategy(s, r, d);
+        }, strategy, role, devices);
+    }
+
+    private int removeDevicesRoleForStrategy(int strategy, int role,
+                                      @NonNull List<AudioDeviceAttributes> devices) {
+        return removeDevicesRole(mAppliedStrategyRoles, (s, r, d) -> {
+            return mAudioSystem.removeDevicesRoleForStrategy(s, r, d);
+        }, strategy, role, devices);
+    }
+
+    private int setDevicesRoleForStrategy(int strategy, int role,
+                                          @NonNull List<AudioDeviceAttributes> devices) {
+        return setDevicesRole(mAppliedStrategyRoles, (s, r, d) -> {
+            return mAudioSystem.setDevicesRoleForStrategy(s, r, d);
+        }, (s, r, d) -> {
+                return mAudioSystem.clearDevicesRoleForStrategy(s, r);
+            }, strategy, role, devices);
+    }
+
+    private int clearDevicesRoleForStrategy(int strategy, int role) {
+        return clearDevicesRole(mAppliedStrategyRoles, (s, r, d) -> {
+            return mAudioSystem.clearDevicesRoleForStrategy(s, r);
+        }, strategy, role);
+    }
+
+    //------------------------------------------------------------
+    // Cache for applied roles for strategies and devices. The cache avoids reapplying the
+    // same list of devices for a given role and strategy and the corresponding systematic
+    // redundant work in audio policy manager and audio flinger.
+    // The key is the pair <Strategy , Role> and the value is the current list of devices.
+
+    private final ArrayMap<Pair<Integer, Integer>, List<AudioDeviceAttributes>>
+            mAppliedStrategyRoles = new ArrayMap<>();
+
+    // Cache for applied roles for capture presets and devices. The cache avoids reapplying the
+    // same list of devices for a given role and capture preset and the corresponding systematic
+    // redundant work in audio policy manager and audio flinger.
+    // The key is the pair <Preset , Role> and the value is the current list of devices.
+    private final ArrayMap<Pair<Integer, Integer>, List<AudioDeviceAttributes>>
+            mAppliedPresetRoles = new ArrayMap<>();
+
+    interface AudioSystemInterface {
+        int deviceRoleAction(int usecase, int role, @Nullable List<AudioDeviceAttributes> devices);
+    }
+
+    private int addDevicesRole(
+            ArrayMap<Pair<Integer, Integer>, List<AudioDeviceAttributes>> rolesMap,
+            AudioSystemInterface asi,
+            int useCase, int role, @NonNull List<AudioDeviceAttributes> devices) {
+        synchronized (rolesMap) {
+            Pair<Integer, Integer> key = new Pair<>(useCase, role);
+            List<AudioDeviceAttributes> roleDevices = new ArrayList<>();
+            List<AudioDeviceAttributes> appliedDevices = new ArrayList<>();
+
+            if (rolesMap.containsKey(key)) {
+                roleDevices = rolesMap.get(key);
+                for (AudioDeviceAttributes device : devices) {
+                    if (!roleDevices.contains(device)) {
+                        appliedDevices.add(device);
+                    }
+                }
+            } else {
+                appliedDevices.addAll(devices);
+            }
+            if (appliedDevices.isEmpty()) {
+                return AudioSystem.SUCCESS;
+            }
+            final int status = asi.deviceRoleAction(useCase, role, appliedDevices);
+            if (status == AudioSystem.SUCCESS) {
+                roleDevices.addAll(appliedDevices);
+                rolesMap.put(key, roleDevices);
+            }
+            return status;
+        }
+    }
+
+    private int removeDevicesRole(
+            ArrayMap<Pair<Integer, Integer>, List<AudioDeviceAttributes>> rolesMap,
+            AudioSystemInterface asi,
+            int useCase, int role, @NonNull List<AudioDeviceAttributes> devices) {
+        synchronized (rolesMap) {
+            Pair<Integer, Integer> key = new Pair<>(useCase, role);
+            if (!rolesMap.containsKey(key)) {
+                return AudioSystem.SUCCESS;
+            }
+            List<AudioDeviceAttributes> roleDevices = rolesMap.get(key);
+            List<AudioDeviceAttributes> appliedDevices = new ArrayList<>();
+            for (AudioDeviceAttributes device : devices) {
+                if (roleDevices.contains(device)) {
+                    appliedDevices.add(device);
+                }
+            }
+            if (appliedDevices.isEmpty()) {
+                return AudioSystem.SUCCESS;
+            }
+            final int status = asi.deviceRoleAction(useCase, role, appliedDevices);
+            if (status == AudioSystem.SUCCESS) {
+                roleDevices.removeAll(appliedDevices);
+                if (roleDevices.isEmpty()) {
+                    rolesMap.remove(key);
+                } else {
+                    rolesMap.put(key, roleDevices);
+                }
+            }
+            return status;
+        }
+    }
+
+    private int setDevicesRole(
+            ArrayMap<Pair<Integer, Integer>, List<AudioDeviceAttributes>> rolesMap,
+            AudioSystemInterface addOp,
+            AudioSystemInterface clearOp,
+            int useCase, int role, @NonNull List<AudioDeviceAttributes> devices) {
+        synchronized (rolesMap) {
+            Pair<Integer, Integer> key = new Pair<>(useCase, role);
+            List<AudioDeviceAttributes> roleDevices = new ArrayList<>();
+            List<AudioDeviceAttributes> appliedDevices = new ArrayList<>();
+
+            if (rolesMap.containsKey(key)) {
+                roleDevices = rolesMap.get(key);
+                boolean equal = false;
+                if (roleDevices.size() == devices.size()) {
+                    roleDevices.retainAll(devices);
+                    equal = roleDevices.size() == devices.size();
+                }
+                if (!equal) {
+                    clearOp.deviceRoleAction(useCase, role, null);
+                    roleDevices.clear();
+                    appliedDevices.addAll(devices);
+                }
+            } else {
+                appliedDevices.addAll(devices);
+            }
+            if (appliedDevices.isEmpty()) {
+                return AudioSystem.SUCCESS;
+            }
+            final int status = addOp.deviceRoleAction(useCase, role, appliedDevices);
+            if (status == AudioSystem.SUCCESS) {
+                roleDevices.addAll(appliedDevices);
+                rolesMap.put(key, roleDevices);
+            }
+            return status;
+        }
+    }
+
+    private int clearDevicesRole(
+            ArrayMap<Pair<Integer, Integer>, List<AudioDeviceAttributes>> rolesMap,
+            AudioSystemInterface asi, int useCase, int role) {
+        synchronized (rolesMap) {
+            Pair<Integer, Integer> key = new Pair<>(useCase, role);
+            if (!rolesMap.containsKey(key)) {
+                return AudioSystem.SUCCESS;
+            }
+            final int status = asi.deviceRoleAction(useCase, role, null);
+            if (status == AudioSystem.SUCCESS) {
+                rolesMap.remove(key);
+            }
+            return status;
+        }
+    }
+
+    @GuardedBy("mDevicesLock")
+    private void purgeDevicesRoles_l() {
+        purgeRoles(mAppliedStrategyRoles, (s, r, d) -> {
+            return mAudioSystem.removeDevicesRoleForStrategy(s, r, d); });
+        purgeRoles(mAppliedPresetRoles, (p, r, d) -> {
+            return mAudioSystem.removeDevicesRoleForCapturePreset(p, r, d); });
+    }
+
+    @GuardedBy("mDevicesLock")
+    private void purgeRoles(
+            ArrayMap<Pair<Integer, Integer>, List<AudioDeviceAttributes>> rolesMap,
+            AudioSystemInterface asi) {
+        synchronized (rolesMap) {
+            Iterator<Map.Entry<Pair<Integer, Integer>, List<AudioDeviceAttributes>>> itRole =
+                    rolesMap.entrySet().iterator();
+            while (itRole.hasNext()) {
+                Map.Entry<Pair<Integer, Integer>, List<AudioDeviceAttributes>> entry =
+                        itRole.next();
+                Pair<Integer, Integer> keyRole = entry.getKey();
+                Iterator<AudioDeviceAttributes> itDev = rolesMap.get(keyRole).iterator();
+                while (itDev.hasNext()) {
+                    AudioDeviceAttributes ada = itDev.next();
+                    final String devKey = DeviceInfo.makeDeviceListKey(ada.getInternalType(),
+                            ada.getAddress());
+                    if (mConnectedDevices.get(devKey) == null) {
+                        asi.deviceRoleAction(keyRole.first, keyRole.second, Arrays.asList(ada));
+                        itDev.remove();
+                    }
+                }
+                if (rolesMap.get(keyRole).isEmpty()) {
+                    itRole.remove();
+                }
+            }
+        }
+    }
+
+//-----------------------------------------------------------------------
 
     /**
      * Check if a device is in the list of connected devices
@@ -871,10 +1168,11 @@
      * @param connect true if connection
      * @param isForTesting if true, not calling AudioSystem for the connection as this is
      *                    just for testing
+     * @param btDevice the corresponding Bluetooth device when relevant.
      * @return false if an error was reported by AudioSystem
      */
     /*package*/ boolean handleDeviceConnection(AudioDeviceAttributes attributes, boolean connect,
-            boolean isForTesting) {
+            boolean isForTesting, @Nullable BluetoothDevice btDevice) {
         int device = attributes.getInternalType();
         String address = attributes.getAddress();
         String deviceName = attributes.getName();
@@ -889,6 +1187,7 @@
                 .set(MediaMetrics.Property.MODE, connect
                         ? MediaMetrics.Value.CONNECT : MediaMetrics.Value.DISCONNECT)
                 .set(MediaMetrics.Property.NAME, deviceName);
+        boolean status = false;
         synchronized (mDevicesLock) {
             final String deviceKey = DeviceInfo.makeDeviceListKey(device, address);
             if (AudioService.DEBUG_DEVICES) {
@@ -916,24 +1215,33 @@
                             .record();
                     return false;
                 }
-                mConnectedDevices.put(deviceKey, new DeviceInfo(
-                        device, deviceName, address, AudioSystem.AUDIO_FORMAT_DEFAULT));
+                mConnectedDevices.put(deviceKey, new DeviceInfo(device, deviceName, address));
                 mDeviceBroker.postAccessoryPlugMediaUnmute(device);
-                mmi.set(MediaMetrics.Property.STATE, MediaMetrics.Value.CONNECTED).record();
-                return true;
+                status = true;
             } else if (!connect && isConnected) {
                 mAudioSystem.setDeviceConnectionState(attributes,
                         AudioSystem.DEVICE_STATE_UNAVAILABLE, AudioSystem.AUDIO_FORMAT_DEFAULT);
                 // always remove even if disconnection failed
                 mConnectedDevices.remove(deviceKey);
-                mmi.set(MediaMetrics.Property.STATE, MediaMetrics.Value.CONNECTED).record();
-                return true;
+                status = true;
             }
-            Log.w(TAG, "handleDeviceConnection() failed, deviceKey=" + deviceKey
-                    + ", deviceSpec=" + di + ", connect=" + connect);
+            if (status) {
+                if (AudioSystem.isBluetoothScoDevice(device)) {
+                    updateBluetoothPreferredModes_l();
+                    if (connect) {
+                        mDeviceBroker.postNotifyPreferredAudioProfileApplied(btDevice);
+                    } else {
+                        purgeDevicesRoles_l();
+                    }
+                }
+                mmi.set(MediaMetrics.Property.STATE, MediaMetrics.Value.CONNECTED).record();
+            } else {
+                Log.w(TAG, "handleDeviceConnection() failed, deviceKey=" + deviceKey
+                        + ", deviceSpec=" + di + ", connect=" + connect);
+                mmi.set(MediaMetrics.Property.STATE, MediaMetrics.Value.DISCONNECTED).record();
+            }
         }
-        mmi.set(MediaMetrics.Property.STATE, MediaMetrics.Value.DISCONNECTED).record();
-        return false;
+        return status;
     }
 
 
@@ -1142,15 +1450,20 @@
     // Internal utilities
 
     @GuardedBy("mDevicesLock")
-    private void makeA2dpDeviceAvailable(String address, String name, String eventSource,
-            int a2dpCodec) {
+    private void makeA2dpDeviceAvailable(AudioDeviceBroker.BtDeviceInfo btInfo,
+                                         String eventSource) {
+        final String address = btInfo.mDevice.getAddress();
+        final String name = BtHelper.getName(btInfo.mDevice);
+        final int a2dpCodec = btInfo.mCodec;
+
         // enable A2DP before notifying A2DP connection to avoid unnecessary processing in
         // audio policy manager
         mDeviceBroker.setBluetoothA2dpOnInt(true, true /*fromA2dp*/, eventSource);
         // at this point there could be another A2DP device already connected in APM, but it
         // doesn't matter as this new one will overwrite the previous one
-        final int res = mAudioSystem.setDeviceConnectionState(new AudioDeviceAttributes(
-                AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address, name),
+        AudioDeviceAttributes ada = new AudioDeviceAttributes(
+                AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address, name);
+        final int res = mAudioSystem.setDeviceConnectionState(ada,
                 AudioSystem.DEVICE_STATE_AVAILABLE, a2dpCodec);
 
         // TODO: log in MediaMetrics once distinction between connection failure and
@@ -1167,13 +1480,12 @@
         }
 
         // Reset A2DP suspend state each time a new sink is connected
-        mAudioSystem.setParameters("A2dpSuspended=false");
+        mDeviceBroker.clearA2dpSuspended();
 
         // The convention for head tracking sensors associated with A2DP devices is to
         // use a UUID derived from the MAC address as follows:
         //   time_low = 0, time_mid = 0, time_hi = 0, clock_seq = 0, node = MAC Address
-        UUID sensorUuid = UuidUtils.uuidFromAudioDeviceAttributes(
-                new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address));
+        UUID sensorUuid = UuidUtils.uuidFromAudioDeviceAttributes(ada);
         final DeviceInfo di = new DeviceInfo(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, name,
                 address, a2dpCodec, sensorUuid);
         final String diKey = di.getKey();
@@ -1184,6 +1496,206 @@
 
         mDeviceBroker.postAccessoryPlugMediaUnmute(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP);
         setCurrentAudioRouteNameIfPossible(name, true /*fromA2dp*/);
+
+        updateBluetoothPreferredModes_l();
+        mDeviceBroker.postNotifyPreferredAudioProfileApplied(btInfo.mDevice);
+    }
+
+    static final int[] CAPTURE_PRESETS = new int[] {AudioSource.MIC, AudioSource.CAMCORDER,
+            AudioSource.VOICE_RECOGNITION, AudioSource.VOICE_COMMUNICATION,
+            AudioSource.UNPROCESSED, AudioSource.VOICE_PERFORMANCE, AudioSource.HOTWORD};
+
+    // reflects system property persist.bluetooth.enable_dual_mode_audio
+    final boolean mBluetoothDualModeEnabled;
+    /**
+     * Goes over all connected Bluetooth devices and set the audio policy device role to DISABLED
+     * or not according to their own and other devices modes.
+     * The top priority is given to LE devices, then SCO ,then A2DP.
+     */
+    @GuardedBy("mDevicesLock")
+    private void applyConnectedDevicesRoles_l() {
+        if (!mBluetoothDualModeEnabled) {
+            return;
+        }
+        DeviceInfo leOutDevice =
+                getFirstConnectedDeviceOfTypes(AudioSystem.DEVICE_OUT_ALL_BLE_SET);
+        DeviceInfo leInDevice =
+                getFirstConnectedDeviceOfTypes(AudioSystem.DEVICE_IN_ALL_BLE_SET);
+        DeviceInfo a2dpDevice =
+                getFirstConnectedDeviceOfTypes(AudioSystem.DEVICE_OUT_ALL_A2DP_SET);
+        DeviceInfo scoOutDevice =
+                getFirstConnectedDeviceOfTypes(AudioSystem.DEVICE_OUT_ALL_SCO_SET);
+        DeviceInfo scoInDevice =
+                getFirstConnectedDeviceOfTypes(AudioSystem.DEVICE_IN_ALL_SCO_SET);
+        boolean disableA2dp = (leOutDevice != null && leOutDevice.isOutputOnlyModeEnabled());
+        boolean disableSco = (leOutDevice != null && leOutDevice.isDuplexModeEnabled())
+                || (leInDevice != null && leInDevice.isDuplexModeEnabled());
+        AudioDeviceAttributes communicationDevice =
+                mDeviceBroker.mActiveCommunicationDevice == null
+                        ? null : ((mDeviceBroker.isInCommunication()
+                                    && mDeviceBroker.mActiveCommunicationDevice != null)
+                            ? new AudioDeviceAttributes(mDeviceBroker.mActiveCommunicationDevice)
+                            : null);
+
+        if (AudioService.DEBUG_DEVICES) {
+            Log.i(TAG, "applyConnectedDevicesRoles_l\n - leOutDevice: " + leOutDevice
+                    + "\n - leInDevice: " + leInDevice
+                    + "\n - a2dpDevice: " + a2dpDevice
+                    + "\n - scoOutDevice: " + scoOutDevice
+                    + "\n - scoInDevice: " + scoInDevice
+                    + "\n - disableA2dp: " + disableA2dp
+                    + ", disableSco: " + disableSco);
+        }
+
+        for (DeviceInfo di : mConnectedDevices.values()) {
+            if (!AudioSystem.isBluetoothDevice(di.mDeviceType)) {
+                continue;
+            }
+            AudioDeviceAttributes ada =
+                    new AudioDeviceAttributes(di.mDeviceType, di.mDeviceAddress, di.mDeviceName);
+            if (AudioService.DEBUG_DEVICES) {
+                Log.i(TAG, "  + checking Device: " + ada);
+            }
+            if (ada.equalTypeAddress(communicationDevice)) {
+                continue;
+            }
+
+            if (AudioSystem.isBluetoothOutDevice(di.mDeviceType)) {
+                for (AudioProductStrategy strategy : mStrategies) {
+                    boolean disable = false;
+                    if (strategy.getId() == mDeviceBroker.mCommunicationStrategyId) {
+                        if (AudioSystem.isBluetoothScoDevice(di.mDeviceType)) {
+                            disable = disableSco || !di.isDuplexModeEnabled();
+                        } else if (AudioSystem.isBluetoothLeDevice(di.mDeviceType)) {
+                            disable = !di.isDuplexModeEnabled();
+                        }
+                    } else {
+                        if (AudioSystem.isBluetoothA2dpOutDevice(di.mDeviceType)) {
+                            disable = disableA2dp || !di.isOutputOnlyModeEnabled();
+                        } else if (AudioSystem.isBluetoothScoDevice(di.mDeviceType)) {
+                            disable = disableSco || !di.isOutputOnlyModeEnabled();
+                        } else if (AudioSystem.isBluetoothLeDevice(di.mDeviceType)) {
+                            disable = !di.isOutputOnlyModeEnabled();
+                        }
+                    }
+                    if (AudioService.DEBUG_DEVICES) {
+                        Log.i(TAG, "     - strategy: " + strategy.getId()
+                                + ", disable: " + disable);
+                    }
+                    if (disable) {
+                        addDevicesRoleForStrategy(strategy.getId(),
+                                AudioSystem.DEVICE_ROLE_DISABLED, Arrays.asList(ada));
+                    } else {
+                        removeDevicesRoleForStrategy(strategy.getId(),
+                                AudioSystem.DEVICE_ROLE_DISABLED, Arrays.asList(ada));
+                    }
+                }
+            }
+            if (AudioSystem.isBluetoothInDevice(di.mDeviceType)) {
+                for (int capturePreset : CAPTURE_PRESETS) {
+                    boolean disable = false;
+                    if (AudioSystem.isBluetoothScoDevice(di.mDeviceType)) {
+                        disable = disableSco || !di.isDuplexModeEnabled();
+                    } else if (AudioSystem.isBluetoothLeDevice(di.mDeviceType)) {
+                        disable = !di.isDuplexModeEnabled();
+                    }
+                    if (AudioService.DEBUG_DEVICES) {
+                        Log.i(TAG, "      - capturePreset: " + capturePreset
+                                + ", disable: " + disable);
+                    }
+                    if (disable) {
+                        addDevicesRoleForCapturePreset(capturePreset,
+                                AudioSystem.DEVICE_ROLE_DISABLED, Arrays.asList(ada));
+                    } else {
+                        removeDevicesRoleForCapturePreset(capturePreset,
+                                AudioSystem.DEVICE_ROLE_DISABLED, Arrays.asList(ada));
+                    }
+                }
+            }
+        }
+    }
+
+    /* package */ void applyConnectedDevicesRoles() {
+        synchronized (mDevicesLock) {
+            applyConnectedDevicesRoles_l();
+        }
+    }
+
+    @GuardedBy("mDevicesLock")
+    int checkProfileIsConnected(int profile) {
+        switch (profile) {
+            case BluetoothProfile.HEADSET:
+                if (getFirstConnectedDeviceOfTypes(
+                        AudioSystem.DEVICE_OUT_ALL_SCO_SET) != null
+                        || getFirstConnectedDeviceOfTypes(
+                                AudioSystem.DEVICE_IN_ALL_SCO_SET) != null) {
+                    return profile;
+                }
+                break;
+            case BluetoothProfile.A2DP:
+                if (getFirstConnectedDeviceOfTypes(
+                        AudioSystem.DEVICE_OUT_ALL_A2DP_SET) != null) {
+                    return profile;
+                }
+                break;
+            case BluetoothProfile.LE_AUDIO:
+            case BluetoothProfile.LE_AUDIO_BROADCAST:
+                if (getFirstConnectedDeviceOfTypes(
+                        AudioSystem.DEVICE_OUT_ALL_BLE_SET) != null
+                        || getFirstConnectedDeviceOfTypes(
+                                AudioSystem.DEVICE_IN_ALL_BLE_SET) != null) {
+                    return profile;
+                }
+                break;
+            default:
+                break;
+        }
+        return 0;
+    }
+
+    @GuardedBy("mDevicesLock")
+    private void updateBluetoothPreferredModes_l() {
+        if (!mBluetoothDualModeEnabled) {
+            return;
+        }
+        HashSet<String> processedAddresses = new HashSet<>(0);
+        for (DeviceInfo di : mConnectedDevices.values()) {
+            if (!AudioSystem.isBluetoothDevice(di.mDeviceType)
+                    || processedAddresses.contains(di.mDeviceAddress)) {
+                continue;
+            }
+            Bundle preferredProfiles = BtHelper.getPreferredAudioProfiles(di.mDeviceAddress);
+            if (AudioService.DEBUG_DEVICES) {
+                Log.i(TAG, "updateBluetoothPreferredModes_l processing device address: "
+                        + di.mDeviceAddress + ", preferredProfiles: " + preferredProfiles);
+            }
+            for (DeviceInfo di2 : mConnectedDevices.values()) {
+                if (!AudioSystem.isBluetoothDevice(di2.mDeviceType)
+                        || !di.mDeviceAddress.equals(di2.mDeviceAddress)) {
+                    continue;
+                }
+                int profile = BtHelper.getProfileFromType(di2.mDeviceType);
+                if (profile == 0) {
+                    continue;
+                }
+                int preferredProfile = checkProfileIsConnected(
+                        preferredProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX));
+                if (preferredProfile == profile || preferredProfile == 0) {
+                    di2.setModeEnabled(BluetoothAdapter.AUDIO_MODE_DUPLEX);
+                } else {
+                    di2.setModeDisabled(BluetoothAdapter.AUDIO_MODE_DUPLEX);
+                }
+                preferredProfile = checkProfileIsConnected(
+                        preferredProfiles.getInt(BluetoothAdapter.AUDIO_MODE_OUTPUT_ONLY));
+                if (preferredProfile == profile || preferredProfile == 0) {
+                    di2.setModeEnabled(BluetoothAdapter.AUDIO_MODE_OUTPUT_ONLY);
+                } else {
+                    di2.setModeDisabled(BluetoothAdapter.AUDIO_MODE_OUTPUT_ONLY);
+                }
+            }
+            processedAddresses.add(di.mDeviceAddress);
+        }
+        applyConnectedDevicesRoles_l();
     }
 
     @GuardedBy("mDevicesLock")
@@ -1231,13 +1743,17 @@
         // Remove A2DP routes as well
         setCurrentAudioRouteNameIfPossible(null, true /*fromA2dp*/);
         mmi.record();
+
+        updateBluetoothPreferredModes_l();
+        purgeDevicesRoles_l();
     }
 
     @GuardedBy("mDevicesLock")
     private void makeA2dpDeviceUnavailableLater(String address, int delayMs) {
         // prevent any activity on the A2DP audio output to avoid unwanted
         // reconnection of the sink.
-        mAudioSystem.setParameters("A2dpSuspended=true");
+        mDeviceBroker.setA2dpSuspended(
+                true /*enable*/, true /*internal*/, "makeA2dpDeviceUnavailableLater");
         // retrieve DeviceInfo before removing device
         final String deviceKey =
                 DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address);
@@ -1259,8 +1775,7 @@
                 AudioSystem.AUDIO_FORMAT_DEFAULT);
         mConnectedDevices.put(
                 DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, address),
-                new DeviceInfo(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, "",
-                        address, AudioSystem.AUDIO_FORMAT_DEFAULT));
+                new DeviceInfo(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, "", address));
     }
 
     @GuardedBy("mDevicesLock")
@@ -1286,8 +1801,7 @@
                 AudioSystem.AUDIO_FORMAT_DEFAULT);
         mConnectedDevices.put(
                 DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_HEARING_AID, address),
-                new DeviceInfo(AudioSystem.DEVICE_OUT_HEARING_AID, name,
-                        address, AudioSystem.AUDIO_FORMAT_DEFAULT));
+                new DeviceInfo(AudioSystem.DEVICE_OUT_HEARING_AID, name, address));
         mDeviceBroker.postAccessoryPlugMediaUnmute(AudioSystem.DEVICE_OUT_HEARING_AID);
         mDeviceBroker.postApplyVolumeOnDevice(streamType,
                 AudioSystem.DEVICE_OUT_HEARING_AID, "makeHearingAidDeviceAvailable");
@@ -1325,29 +1839,56 @@
      * @return true if a DEVICE_OUT_HEARING_AID is connected, false otherwise.
      */
     boolean isHearingAidConnected() {
+        return getFirstConnectedDeviceOfTypes(
+                Sets.newHashSet(AudioSystem.DEVICE_OUT_HEARING_AID)) != null;
+    }
+
+    /**
+     * Returns a DeviceInfo for the fist connected device matching one of the supplied types
+     */
+    private DeviceInfo getFirstConnectedDeviceOfTypes(Set<Integer> internalTypes) {
+        List<DeviceInfo> devices = getConnectedDevicesOfTypes(internalTypes);
+        return devices.isEmpty() ? null : devices.get(0);
+    }
+
+    /**
+     * Returns a list of connected devices matching one one of the supplied types
+     */
+    private List<DeviceInfo> getConnectedDevicesOfTypes(Set<Integer> internalTypes) {
+        ArrayList<DeviceInfo> devices = new ArrayList<>();
         synchronized (mDevicesLock) {
             for (DeviceInfo di : mConnectedDevices.values()) {
-                if (di.mDeviceType == AudioSystem.DEVICE_OUT_HEARING_AID) {
-                    return true;
+                if (internalTypes.contains(di.mDeviceType)) {
+                    devices.add(di);
                 }
             }
-            return false;
         }
+        return devices;
+    }
+
+    /* package */ AudioDeviceAttributes getDeviceOfType(int type) {
+        DeviceInfo di = getFirstConnectedDeviceOfTypes(Sets.newHashSet(type));
+        return di == null ? null : new AudioDeviceAttributes(
+                    di.mDeviceType, di.mDeviceAddress, di.mDeviceName);
     }
 
     @GuardedBy("mDevicesLock")
-    private void makeLeAudioDeviceAvailable(String address, String name, int streamType,
-            int volumeIndex, int device, String eventSource) {
+    private void makeLeAudioDeviceAvailable(
+            AudioDeviceBroker.BtDeviceInfo btInfo, int streamType, String eventSource) {
+        final String address = btInfo.mDevice.getAddress();
+        final String name = BtHelper.getName(btInfo.mDevice);
+        final int volumeIndex = btInfo.mVolume == -1 ? -1 : btInfo.mVolume * 10;
+        final int device = btInfo.mAudioSystemDevice;
+
         if (device != AudioSystem.DEVICE_NONE) {
             /* Audio Policy sees Le Audio similar to A2DP. Let's make sure
              * AUDIO_POLICY_FORCE_NO_BT_A2DP is not set
              */
             mDeviceBroker.setBluetoothA2dpOnInt(true, false /*fromA2dp*/, eventSource);
 
-            final int res = AudioSystem.setDeviceConnectionState(new AudioDeviceAttributes(
-                    device, address, name),
-                    AudioSystem.DEVICE_STATE_AVAILABLE,
-                    AudioSystem.AUDIO_FORMAT_DEFAULT);
+            AudioDeviceAttributes ada = new AudioDeviceAttributes(device, address, name);
+            final int res = AudioSystem.setDeviceConnectionState(ada,
+                    AudioSystem.DEVICE_STATE_AVAILABLE,  AudioSystem.AUDIO_FORMAT_DEFAULT);
             if (res != AudioSystem.AUDIO_STATUS_OK) {
                 AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
                         "APM failed to make available LE Audio device addr=" + address
@@ -1358,12 +1899,13 @@
                 AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
                         "LE Audio device addr=" + address + " now available").printLog(TAG));
             }
-
             // Reset LEA suspend state each time a new sink is connected
-            mAudioSystem.setParameters("LeAudioSuspended=false");
+            mDeviceBroker.clearLeAudioSuspended();
 
+            UUID sensorUuid = UuidUtils.uuidFromAudioDeviceAttributes(ada);
             mConnectedDevices.put(DeviceInfo.makeDeviceListKey(device, address),
-                    new DeviceInfo(device, name, address, AudioSystem.AUDIO_FORMAT_DEFAULT));
+                    new DeviceInfo(device, name, address, AudioSystem.AUDIO_FORMAT_DEFAULT,
+                            sensorUuid));
             mDeviceBroker.postAccessoryPlugMediaUnmute(device);
             setCurrentAudioRouteNameIfPossible(name, /*fromA2dp=*/false);
         }
@@ -1379,6 +1921,9 @@
         final int maxIndex = mDeviceBroker.getMaxVssVolumeForStream(streamType);
         mDeviceBroker.postSetLeAudioVolumeIndex(leAudioVolIndex, maxIndex, streamType);
         mDeviceBroker.postApplyVolumeOnDevice(streamType, device, "makeLeAudioDeviceAvailable");
+
+        updateBluetoothPreferredModes_l();
+        mDeviceBroker.postNotifyPreferredAudioProfileApplied(btInfo.mDevice);
     }
 
     @GuardedBy("mDevicesLock")
@@ -1403,13 +1948,17 @@
         }
 
         setCurrentAudioRouteNameIfPossible(null, false /*fromA2dp*/);
+
+        updateBluetoothPreferredModes_l();
+        purgeDevicesRoles_l();
     }
 
     @GuardedBy("mDevicesLock")
     private void makeLeAudioDeviceUnavailableLater(String address, int device, int delayMs) {
         // prevent any activity on the LEA output to avoid unwanted
         // reconnection of the sink.
-        mAudioSystem.setParameters("LeAudioSuspended=true");
+        mDeviceBroker.setLeAudioSuspended(
+                true /*enable*/, true /*internal*/, "makeLeAudioDeviceUnavailableLater");
         // the device will be made unavailable later, so consider it disconnected right away
         mConnectedDevices.remove(DeviceInfo.makeDeviceListKey(device, address));
         // send the delayed message to make the device unavailable later
@@ -1737,18 +2286,6 @@
         }
     }
 
-    /* package */ AudioDeviceAttributes getDeviceOfType(int type) {
-        synchronized (mDevicesLock) {
-            for (DeviceInfo di : mConnectedDevices.values()) {
-                if (di.mDeviceType == type) {
-                    return new AudioDeviceAttributes(
-                            di.mDeviceType, di.mDeviceAddress, di.mDeviceName);
-                }
-            }
-        }
-        return null;
-    }
-
     //----------------------------------------------------------
     // For tests only
 
@@ -1759,10 +2296,12 @@
      */
     @VisibleForTesting
     public boolean isA2dpDeviceConnected(@NonNull BluetoothDevice device) {
-        final String key = DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP,
-                device.getAddress());
-        synchronized (mDevicesLock) {
-            return (mConnectedDevices.get(key) != null);
+        for (DeviceInfo di : getConnectedDevicesOfTypes(
+                Sets.newHashSet(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP))) {
+            if (di.mDeviceAddress.equals(device.getAddress())) {
+                return true;
+            }
         }
+        return false;
     }
 }
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index d1cbbfc..a3163e0 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -6376,6 +6376,26 @@
         mDeviceBroker.setBluetoothScoOn(on, eventSource);
     }
 
+    /** @see AudioManager#setA2dpSuspended(boolean) */
+    @android.annotation.EnforcePermission(android.Manifest.permission.BLUETOOTH_STACK)
+    public void setA2dpSuspended(boolean enable) {
+        super.setA2dpSuspended_enforcePermission();
+        final String eventSource = new StringBuilder("setA2dpSuspended(").append(enable)
+                .append(") from u/pid:").append(Binder.getCallingUid()).append("/")
+                .append(Binder.getCallingPid()).toString();
+        mDeviceBroker.setA2dpSuspended(enable, false /*internal*/, eventSource);
+    }
+
+    /** @see AudioManager#setA2dpSuspended(boolean) */
+    @android.annotation.EnforcePermission(android.Manifest.permission.BLUETOOTH_STACK)
+    public void setLeAudioSuspended(boolean enable) {
+        super.setLeAudioSuspended_enforcePermission();
+        final String eventSource = new StringBuilder("setLeAudioSuspended(").append(enable)
+                .append(") from u/pid:").append(Binder.getCallingUid()).append("/")
+                .append(Binder.getCallingPid()).toString();
+        mDeviceBroker.setLeAudioSuspended(enable, false /*internal*/, eventSource);
+    }
+
     /** @see AudioManager#isBluetoothScoOn()
      * Note that it doesn't report internal state, but state seen by apps (which may have
      * called setBluetoothScoOn() */
diff --git a/services/core/java/com/android/server/audio/AudioSystemAdapter.java b/services/core/java/com/android/server/audio/AudioSystemAdapter.java
index 7af7ed5..1142a8d 100644
--- a/services/core/java/com/android/server/audio/AudioSystemAdapter.java
+++ b/services/core/java/com/android/server/audio/AudioSystemAdapter.java
@@ -435,7 +435,7 @@
     }
 
     /**
-     * Same as {@link AudioSystem#removeDevicesRoleForCapturePreset(int, int, int[], String[])}
+     * Same as {@link AudioSystem#removeDevicesRoleForCapturePreset(int, int, List)}
      * @param capturePreset
      * @param role
      * @param devicesToRemove
@@ -448,6 +448,19 @@
     }
 
     /**
+     * Same as {@link AudioSystem#addDevicesRoleForCapturePreset(int, int, List)}
+     * @param capturePreset the capture preset to configure
+     * @param role the role of the devices
+     * @param devices the list of devices to be added as role for the given capture preset
+     * @return {@link #SUCCESS} if successfully add
+     */
+    public int addDevicesRoleForCapturePreset(
+            int capturePreset, int role, @NonNull List<AudioDeviceAttributes> devices) {
+        invalidateRoutingCache();
+        return AudioSystem.addDevicesRoleForCapturePreset(capturePreset, role, devices);
+    }
+
+    /**
      * Same as {@link AudioSystem#}
      * @param capturePreset
      * @param role
diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java
index 631d7f5..e46c3cc 100644
--- a/services/core/java/com/android/server/audio/BtHelper.java
+++ b/services/core/java/com/android/server/audio/BtHelper.java
@@ -33,6 +33,7 @@
 import android.media.AudioSystem;
 import android.media.BluetoothProfileConnectionInfo;
 import android.os.Binder;
+import android.os.Bundle;
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.text.TextUtils;
@@ -150,60 +151,12 @@
         }
     }
 
-    //----------------------------------------------------------------------
-    /*package*/ static class BluetoothA2dpDeviceInfo {
-        private final @NonNull BluetoothDevice mBtDevice;
-        private final int mVolume;
-        private final @AudioSystem.AudioFormatNativeEnumForBtCodec int mCodec;
-
-        BluetoothA2dpDeviceInfo(@NonNull BluetoothDevice btDevice) {
-            this(btDevice, -1, AudioSystem.AUDIO_FORMAT_DEFAULT);
-        }
-
-        BluetoothA2dpDeviceInfo(@NonNull BluetoothDevice btDevice, int volume, int codec) {
-            mBtDevice = btDevice;
-            mVolume = volume;
-            mCodec = codec;
-        }
-
-        public @NonNull BluetoothDevice getBtDevice() {
-            return mBtDevice;
-        }
-
-        public int getVolume() {
-            return mVolume;
-        }
-
-        public @AudioSystem.AudioFormatNativeEnumForBtCodec int getCodec() {
-            return mCodec;
-        }
-
-        // redefine equality op so we can match messages intended for this device
-        @Override
-        public boolean equals(Object o) {
-            if (o == null) {
-                return false;
-            }
-            if (this == o) {
-                return true;
-            }
-            if (o instanceof BluetoothA2dpDeviceInfo) {
-                return mBtDevice.equals(((BluetoothA2dpDeviceInfo) o).getBtDevice());
-            }
-            return false;
-        }
-
-
-    }
-
     // A2DP device events
     /*package*/ static final int EVENT_DEVICE_CONFIG_CHANGE = 0;
-    /*package*/ static final int EVENT_ACTIVE_DEVICE_CHANGE = 1;
 
-    /*package*/ static String a2dpDeviceEventToString(int event) {
+    /*package*/ static String deviceEventToString(int event) {
         switch (event) {
             case EVENT_DEVICE_CONFIG_CHANGE: return "DEVICE_CONFIG_CHANGE";
-            case EVENT_ACTIVE_DEVICE_CHANGE: return "ACTIVE_DEVICE_CHANGE";
             default:
                 return new String("invalid event:" + event);
         }
@@ -492,8 +445,8 @@
     /*package*/ synchronized void resetBluetoothSco() {
         mScoAudioState = SCO_STATE_INACTIVE;
         broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
-        AudioSystem.setParameters("A2dpSuspended=false");
-        AudioSystem.setParameters("LeAudioSuspended=false");
+        mDeviceBroker.clearA2dpSuspended();
+        mDeviceBroker.clearLeAudioSuspended();
         mDeviceBroker.setBluetoothScoOn(false, "resetBluetoothSco");
     }
 
@@ -620,11 +573,12 @@
         return btHeadsetDeviceToAudioDevice(mBluetoothHeadsetDevice);
     }
 
-    private AudioDeviceAttributes btHeadsetDeviceToAudioDevice(BluetoothDevice btDevice) {
+    private static AudioDeviceAttributes btHeadsetDeviceToAudioDevice(BluetoothDevice btDevice) {
         if (btDevice == null) {
             return new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_BLUETOOTH_SCO, "");
         }
         String address = btDevice.getAddress();
+        String name = getName(btDevice);
         if (!BluetoothAdapter.checkBluetoothAddress(address)) {
             address = "";
         }
@@ -646,7 +600,7 @@
                     + " btClass: " + (btClass == null ? "Unknown" : btClass)
                     + " nativeType: " + nativeType + " address: " + address);
         }
-        return new AudioDeviceAttributes(nativeType, address);
+        return new AudioDeviceAttributes(nativeType, address, name);
     }
 
     private boolean handleBtScoActiveDeviceChange(BluetoothDevice btDevice, boolean isActive) {
@@ -655,12 +609,9 @@
         }
         int inDevice = AudioSystem.DEVICE_IN_BLUETOOTH_SCO_HEADSET;
         AudioDeviceAttributes audioDevice =  btHeadsetDeviceToAudioDevice(btDevice);
-        String btDeviceName =  getName(btDevice);
         boolean result = false;
         if (isActive) {
-            result |= mDeviceBroker.handleDeviceConnection(new AudioDeviceAttributes(
-                    audioDevice.getInternalType(), audioDevice.getAddress(), btDeviceName),
-                    isActive);
+            result |= mDeviceBroker.handleDeviceConnection(audioDevice, isActive, btDevice);
         } else {
             int[] outDeviceTypes = {
                     AudioSystem.DEVICE_OUT_BLUETOOTH_SCO,
@@ -669,14 +620,14 @@
             };
             for (int outDeviceType : outDeviceTypes) {
                 result |= mDeviceBroker.handleDeviceConnection(new AudioDeviceAttributes(
-                        outDeviceType, audioDevice.getAddress(), btDeviceName),
-                        isActive);
+                        outDeviceType, audioDevice.getAddress(), audioDevice.getName()),
+                        isActive, btDevice);
             }
         }
         // handleDeviceConnection() && result to make sure the method get executed
         result = mDeviceBroker.handleDeviceConnection(new AudioDeviceAttributes(
-                        inDevice, audioDevice.getAddress(), btDeviceName),
-                isActive) && result;
+                        inDevice, audioDevice.getAddress(), audioDevice.getName()),
+                isActive, btDevice) && result;
         return result;
     }
 
@@ -973,6 +924,30 @@
         }
     }
 
+    /*package */ static int getProfileFromType(int deviceType) {
+        if (AudioSystem.isBluetoothA2dpOutDevice(deviceType)) {
+            return BluetoothProfile.A2DP;
+        } else if (AudioSystem.isBluetoothScoDevice(deviceType)) {
+            return BluetoothProfile.HEADSET;
+        } else if (AudioSystem.isBluetoothLeDevice(deviceType)) {
+            return BluetoothProfile.LE_AUDIO;
+        }
+        return 0; // 0 is not a valid profile
+    }
+
+    /*package */ static Bundle getPreferredAudioProfiles(String address) {
+        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+        return adapter.getPreferredAudioProfiles(adapter.getRemoteDevice(address));
+    }
+
+    /**
+     * Notifies Bluetooth framework that new preferred audio profiles for Bluetooth devices
+     * have been applied.
+     */
+    public static void onNotifyPreferredAudioProfileApplied(BluetoothDevice btDevice) {
+        BluetoothAdapter.getDefaultAdapter().notifyActiveDeviceChangeApplied(btDevice);
+    }
+
     /**
      * Returns the string equivalent for the btDeviceClass class.
      */