Merge "Fix EINTR handling"
diff --git a/Android.bp b/Android.bp
index fc713ff..3d087c0 100644
--- a/Android.bp
+++ b/Android.bp
@@ -563,6 +563,7 @@
         "android.hardware.vibrator-V1.3-java",
         "framework-protos",
         "art.module.public.api",
+        "sdk_module-lib_current_framework-tethering",
         // There are a few classes from modules used by the core that
         // need to be resolved by metalava. We use a prebuilt stub of the
         // full sdk to ensure we can resolve them. If a new class gets added,
diff --git a/StubLibraries.bp b/StubLibraries.bp
index 44c55c2..d090296 100644
--- a/StubLibraries.bp
+++ b/StubLibraries.bp
@@ -318,6 +318,7 @@
     defaults: ["android-non-updatable_defaults_stubs_current"],
     srcs: [":module-lib-api-stubs-docs-non-updatable"],
     libs: [
+        "sdk_module-lib_current_framework-tethering",
         "sdk_system_current_android",
         // NOTE: The below can be removed once the prebuilt stub contains IKE.
         "sdk_system_current_android.net.ipsec.ike",
diff --git a/core/api/Android.bp b/core/api/Android.bp
index 170febb..114a957 100644
--- a/core/api/Android.bp
+++ b/core/api/Android.bp
@@ -51,11 +51,17 @@
 filegroup {
     name: "non-updatable-module-lib-current.txt",
     srcs: ["module-lib-current.txt"],
-    visibility: ["//frameworks/base/api"],
+    visibility: [
+        "//frameworks/base/api",
+        "//cts/tests/signature/api",
+    ],
 }
 
 filegroup {
     name: "non-updatable-module-lib-removed.txt",
     srcs: ["module-lib-removed.txt"],
-    visibility: ["//frameworks/base/api"],
+    visibility: [
+        "//frameworks/base/api",
+        "//cts/tests/signature/api",
+    ],
 }
diff --git a/core/api/current.txt b/core/api/current.txt
index f21518c..f15d457 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -43474,8 +43474,10 @@
     method @RequiresPermission(anyOf={"android.permission.READ_PRIVILEGED_PHONE_STATE", android.Manifest.permission.READ_PRECISE_PHONE_STATE}) public boolean isVoWiFiSettingEnabled();
     method @RequiresPermission(anyOf={"android.permission.READ_PRIVILEGED_PHONE_STATE", android.Manifest.permission.READ_PRECISE_PHONE_STATE}) public boolean isVtSettingEnabled();
     method @RequiresPermission(anyOf={"android.permission.READ_PRIVILEGED_PHONE_STATE", android.Manifest.permission.READ_PRECISE_PHONE_STATE}) public void registerImsRegistrationCallback(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.ims.RegistrationManager.RegistrationCallback) throws android.telephony.ims.ImsException;
+    method @RequiresPermission(anyOf={android.Manifest.permission.READ_PRECISE_PHONE_STATE, "android.permission.READ_PRIVILEGED_PHONE_STATE"}) public void registerImsStateCallback(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.ims.ImsStateCallback) throws android.telephony.ims.ImsException;
     method @RequiresPermission(anyOf={"android.permission.READ_PRIVILEGED_PHONE_STATE", android.Manifest.permission.READ_PRECISE_PHONE_STATE}) public void registerMmTelCapabilityCallback(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.ims.ImsMmTelManager.CapabilityCallback) throws android.telephony.ims.ImsException;
     method @RequiresPermission(anyOf={"android.permission.READ_PRIVILEGED_PHONE_STATE", android.Manifest.permission.READ_PRECISE_PHONE_STATE}) public void unregisterImsRegistrationCallback(@NonNull android.telephony.ims.RegistrationManager.RegistrationCallback);
+    method public void unregisterImsStateCallback(@NonNull android.telephony.ims.ImsStateCallback);
     method @RequiresPermission(anyOf={"android.permission.READ_PRIVILEGED_PHONE_STATE", android.Manifest.permission.READ_PRECISE_PHONE_STATE}) public void unregisterMmTelCapabilityCallback(@NonNull android.telephony.ims.ImsMmTelManager.CapabilityCallback);
     field public static final int WIFI_MODE_CELLULAR_PREFERRED = 1; // 0x1
     field public static final int WIFI_MODE_WIFI_ONLY = 0; // 0x0
@@ -43492,7 +43494,9 @@
     method @RequiresPermission(android.Manifest.permission.READ_PRECISE_PHONE_STATE) public void getRegistrationTransportType(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
     method @NonNull public android.telephony.ims.RcsUceAdapter getUceAdapter();
     method @RequiresPermission(android.Manifest.permission.READ_PRECISE_PHONE_STATE) public void registerImsRegistrationCallback(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.ims.RegistrationManager.RegistrationCallback) throws android.telephony.ims.ImsException;
+    method @RequiresPermission(anyOf={android.Manifest.permission.READ_PRECISE_PHONE_STATE, "android.permission.READ_PRIVILEGED_PHONE_STATE", "android.permission.ACCESS_RCS_USER_CAPABILITY_EXCHANGE"}) public void registerImsStateCallback(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.ims.ImsStateCallback) throws android.telephony.ims.ImsException;
     method @RequiresPermission(android.Manifest.permission.READ_PRECISE_PHONE_STATE) public void unregisterImsRegistrationCallback(@NonNull android.telephony.ims.RegistrationManager.RegistrationCallback);
+    method public void unregisterImsStateCallback(@NonNull android.telephony.ims.ImsStateCallback);
     field public static final String ACTION_SHOW_CAPABILITY_DISCOVERY_OPT_IN = "android.telephony.ims.action.SHOW_CAPABILITY_DISCOVERY_OPT_IN";
   }
 
@@ -43691,6 +43695,19 @@
     field @NonNull public static final android.os.Parcelable.Creator<android.telephony.ims.ImsRegistrationAttributes> CREATOR;
   }
 
+  public abstract class ImsStateCallback {
+    ctor public ImsStateCallback();
+    method public abstract void onAvailable();
+    method public abstract void onError();
+    method public abstract void onUnavailable(int);
+    field public static final int REASON_IMS_SERVICE_DISCONNECTED = 3; // 0x3
+    field public static final int REASON_IMS_SERVICE_NOT_READY = 6; // 0x6
+    field public static final int REASON_NO_IMS_SERVICE_CONFIGURED = 4; // 0x4
+    field public static final int REASON_SUBSCRIPTION_INACTIVE = 5; // 0x5
+    field public static final int REASON_UNKNOWN_PERMANENT_ERROR = 2; // 0x2
+    field public static final int REASON_UNKNOWN_TEMPORARY_ERROR = 1; // 0x1
+  }
+
   public class RcsUceAdapter {
     method @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public boolean isUceSettingEnabled() throws android.telephony.ims.ImsException;
   }
diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt
index 9af6c1b..3a35f24 100644
--- a/core/api/module-lib-current.txt
+++ b/core/api/module-lib-current.txt
@@ -113,6 +113,7 @@
   public class AudioManager {
     method public void adjustStreamVolumeForUid(int, int, int, @NonNull String, int, int, int);
     method public void adjustSuggestedStreamVolumeForUid(int, int, int, @NonNull String, int, int, int);
+    method @RequiresPermission("android.permission.BLUETOOTH_STACK") public void handleBluetoothActiveDeviceChanged(@Nullable android.bluetooth.BluetoothDevice, @Nullable android.bluetooth.BluetoothDevice, @NonNull android.media.BtProfileConnectionInfo);
     method @RequiresPermission("android.permission.BLUETOOTH_STACK") public void setA2dpSuspended(boolean);
     method @RequiresPermission("android.permission.BLUETOOTH_STACK") public void setBluetoothHeadsetProperties(@NonNull String, boolean, boolean);
     method @RequiresPermission("android.permission.BLUETOOTH_STACK") public void setHfpEnabled(boolean);
@@ -122,6 +123,19 @@
     field public static final int FLAG_FROM_KEY = 4096; // 0x1000
   }
 
+  public final class BtProfileConnectionInfo implements android.os.Parcelable {
+    method @NonNull public static android.media.BtProfileConnectionInfo a2dpInfo(boolean, int);
+    method public int describeContents();
+    method public boolean getIsLeOutput();
+    method public int getProfile();
+    method public boolean getSuppressNoisyIntent();
+    method public int getVolume();
+    method @NonNull public static android.media.BtProfileConnectionInfo hearingAidInfo(boolean);
+    method @NonNull public static android.media.BtProfileConnectionInfo leAudio(boolean, boolean);
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.media.BtProfileConnectionInfo> CREATOR;
+  }
+
   public class MediaMetadataRetriever implements java.lang.AutoCloseable {
     field public static final int METADATA_KEY_VIDEO_CODEC_MIME_TYPE = 40; // 0x28
   }
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 78e6a07..84a4a44 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -13741,7 +13741,9 @@
     method @RequiresPermission(android.Manifest.permission.PERFORM_IMS_SINGLE_REGISTRATION) public void createSipDelegate(@NonNull android.telephony.ims.DelegateRequest, @NonNull java.util.concurrent.Executor, @NonNull android.telephony.ims.stub.DelegateConnectionStateCallback, @NonNull android.telephony.ims.stub.DelegateConnectionMessageCallback) throws android.telephony.ims.ImsException;
     method @RequiresPermission(android.Manifest.permission.PERFORM_IMS_SINGLE_REGISTRATION) public void destroySipDelegate(@NonNull android.telephony.ims.SipDelegateConnection, int);
     method @RequiresPermission(anyOf={android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE, android.Manifest.permission.PERFORM_IMS_SINGLE_REGISTRATION}) public boolean isSupported() throws android.telephony.ims.ImsException;
+    method @RequiresPermission(anyOf={android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE, android.Manifest.permission.PERFORM_IMS_SINGLE_REGISTRATION}) public void registerImsStateCallback(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.ims.ImsStateCallback) throws android.telephony.ims.ImsException;
     method @RequiresPermission(android.Manifest.permission.PERFORM_IMS_SINGLE_REGISTRATION) public void triggerFullNetworkRegistration(@NonNull android.telephony.ims.SipDelegateConnection, @IntRange(from=100, to=699) int, @Nullable String);
+    method public void unregisterImsStateCallback(@NonNull android.telephony.ims.ImsStateCallback);
     field public static final int DENIED_REASON_INVALID = 4; // 0x4
     field public static final int DENIED_REASON_IN_USE_BY_ANOTHER_DELEGATE = 1; // 0x1
     field public static final int DENIED_REASON_NOT_ALLOWED = 2; // 0x2
diff --git a/core/java/Android.bp b/core/java/Android.bp
index e08a493..ca9a468 100644
--- a/core/java/Android.bp
+++ b/core/java/Android.bp
@@ -343,7 +343,6 @@
 filegroup {
     name: "framework-telephony-common-shared-srcs",
     srcs: [
-        ":modules-utils-preconditions-srcs",
         "android/os/RegistrantList.java",
         "android/os/Registrant.java",
         "android/util/IndentingPrintWriter.java",
@@ -355,10 +354,7 @@
         "com/android/internal/util/BitwiseInputStream.java",
         "com/android/internal/util/FastXmlSerializer.java",
         "com/android/internal/util/HexDump.java",
-        "com/android/internal/util/IState.java",
         "com/android/internal/util/IndentingPrintWriter.java",
-        "com/android/internal/util/State.java",
-        "com/android/internal/util/StateMachine.java",
         "com/android/internal/util/UserIcons.java",
     ],
 }
diff --git a/core/java/android/appwidget/AppWidgetHostView.java b/core/java/android/appwidget/AppWidgetHostView.java
index 8aa2785..5143579 100644
--- a/core/java/android/appwidget/AppWidgetHostView.java
+++ b/core/java/android/appwidget/AppWidgetHostView.java
@@ -552,6 +552,11 @@
                 inflateAsync(rvToApply);
                 return;
             }
+
+            // Prepare a local reference to the remote Context so we're ready to
+            // inflate any requested LayoutParams
+            mRemoteContext = getRemoteContext();
+
             int layoutId = rvToApply.getLayoutId();
             if (rvToApply.canRecycleView(mView)) {
                 try {
diff --git a/core/java/android/util/Log.java b/core/java/android/util/Log.java
index 12bcd8b..b5fe4f5 100644
--- a/core/java/android/util/Log.java
+++ b/core/java/android/util/Log.java
@@ -62,6 +62,10 @@
  * another buffer allocation and copy, and even more pressure on the gc.
  * That means that if your log message is filtered out, you might be doing
  * significant work and incurring significant overhead.
+ *
+ * <p>When calling the log methods that take a Throwable parameter,
+ * if any of the throwables in the cause chain is an <code>UnknownHostException</code>,
+ * then the stack trace is not logged.
  */
 public final class Log {
     /** @hide */
@@ -341,6 +345,9 @@
 
     /**
      * Handy function to get a loggable stack trace from a Throwable
+
+     * <p>If any of the throwables in the cause chain is an <code>UnknownHostException</code>,
+     * this returns an empty string.
      * @param tr An exception to log
      */
     @NonNull
diff --git a/core/java/com/android/internal/view/OWNERS b/core/java/com/android/internal/view/OWNERS
index eb2478f..7a590d0 100644
--- a/core/java/com/android/internal/view/OWNERS
+++ b/core/java/com/android/internal/view/OWNERS
@@ -15,7 +15,7 @@
 
 # WindowManager
 per-file AppearanceRegion = file:/services/core/java/com/android/server/wm/OWNERS
-per-file BaseIWIndow.java = file:/services/core/java/com/android/server/wm/OWNERS
+per-file BaseIWindow.java = file:/services/core/java/com/android/server/wm/OWNERS
 per-file RotationPolicy.java = file:/services/core/java/com/android/server/wm/OWNERS
 per-file WindowManagerPolicyThread.java = file:/services/core/java/com/android/server/wm/OWNERS
 
diff --git a/core/java/com/android/server/NetworkManagementSocketTagger.java b/core/java/com/android/server/NetworkManagementSocketTagger.java
index 2959667..26ff192 100644
--- a/core/java/com/android/server/NetworkManagementSocketTagger.java
+++ b/core/java/com/android/server/NetworkManagementSocketTagger.java
@@ -17,7 +17,6 @@
 package com.android.server;
 
 import android.os.StrictMode;
-import android.os.SystemProperties;
 import android.util.Log;
 import android.util.Slog;
 
@@ -33,13 +32,6 @@
     private static final String TAG = "NetworkManagementSocketTagger";
     private static final boolean LOGD = false;
 
-    /**
-     * {@link SystemProperties} key that indicates if {@code qtaguid} bandwidth
-     * controls have been enabled.
-     */
-    // TODO: remove when always enabled, or once socket tagging silently fails.
-    public static final String PROP_QTAGUID_ENABLED = "net.qtaguid_enabled";
-
     private static ThreadLocal<SocketTags> threadSocketTags = new ThreadLocal<SocketTags>() {
         @Override
         protected SocketTags initialValue() {
@@ -88,13 +80,11 @@
     private void tagSocketFd(FileDescriptor fd, int tag, int uid) {
         if (tag == -1 && uid == -1) return;
 
-        if (SystemProperties.getBoolean(PROP_QTAGUID_ENABLED, false)) {
-            final int errno = native_tagSocketFd(fd, tag, uid);
-            if (errno < 0) {
-                Log.i(TAG, "tagSocketFd(" + fd.getInt$() + ", "
-                      + tag + ", " +
-                      + uid + ") failed with errno" + errno);
-            }
+        final int errno = native_tagSocketFd(fd, tag, uid);
+        if (errno < 0) {
+            Log.i(TAG, "tagSocketFd(" + fd.getInt$() + ", "
+                    + tag + ", "
+                    + uid + ") failed with errno" + errno);
         }
     }
 
@@ -110,11 +100,9 @@
         final SocketTags options = threadSocketTags.get();
         if (options.statsTag == -1 && options.statsUid == -1) return;
 
-        if (SystemProperties.getBoolean(PROP_QTAGUID_ENABLED, false)) {
-            final int errno = native_untagSocketFd(fd);
-            if (errno < 0) {
-                Log.w(TAG, "untagSocket(" + fd.getInt$() + ") failed with errno " + errno);
-            }
+        final int errno = native_untagSocketFd(fd);
+        if (errno < 0) {
+            Log.w(TAG, "untagSocket(" + fd.getInt$() + ") failed with errno " + errno);
         }
     }
 
@@ -124,21 +112,17 @@
     }
 
     public static void setKernelCounterSet(int uid, int counterSet) {
-        if (SystemProperties.getBoolean(PROP_QTAGUID_ENABLED, false)) {
-            final int errno = native_setCounterSet(counterSet, uid);
-            if (errno < 0) {
-                Log.w(TAG, "setKernelCountSet(" + uid + ", " + counterSet + ") failed with errno "
-                        + errno);
-            }
+        final int errno = native_setCounterSet(counterSet, uid);
+        if (errno < 0) {
+            Log.w(TAG, "setKernelCountSet(" + uid + ", " + counterSet + ") failed with errno "
+                    + errno);
         }
     }
 
     public static void resetKernelUidStats(int uid) {
-        if (SystemProperties.getBoolean(PROP_QTAGUID_ENABLED, false)) {
-            int errno = native_deleteTagData(0, uid);
-            if (errno < 0) {
-                Slog.w(TAG, "problem clearing counters for uid " + uid + " : errno " + errno);
-            }
+        int errno = native_deleteTagData(0, uid);
+        if (errno < 0) {
+            Slog.w(TAG, "problem clearing counters for uid " + uid + " : errno " + errno);
         }
     }
 
diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp
index 97cac29..a131111 100644
--- a/core/jni/AndroidRuntime.cpp
+++ b/core/jni/AndroidRuntime.cpp
@@ -746,8 +746,9 @@
     }
 
     const bool checkJni = GetBoolProperty("dalvik.vm.checkjni", false);
-    ALOGV("CheckJNI is %s\n", checkJni ? "ON" : "OFF");
     if (checkJni) {
+        ALOGD("CheckJNI is ON");
+
         /* extended JNI checking */
         addOption("-Xcheck:jni");
 
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index cf7039b..c7f5696 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -32,7 +32,6 @@
 import android.app.PendingIntent;
 import android.bluetooth.BluetoothCodecConfig;
 import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothProfile;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.ComponentName;
 import android.content.Context;
@@ -5796,112 +5795,25 @@
         }
     }
 
-     /**
-     * Indicate Hearing Aid connection state change and eventually suppress
-     * the {@link AudioManager.ACTION_AUDIO_BECOMING_NOISY} intent.
-     * This operation is asynchronous but its execution will still be sequentially scheduled
-     * relative to calls to {@link #setBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(
-     * * BluetoothDevice, int, int, boolean, int)} and
-     * and {@link #handleBluetoothA2dpDeviceConfigChange(BluetoothDevice)}.
-     * @param device Bluetooth device connected/disconnected
-     * @param state new connection state (BluetoothProfile.STATE_xxx)
-     * @param musicDevice Default get system volume for the connecting device.
-     * (either {@link android.bluetooth.BluetoothProfile.hearingaid} or
-     * {@link android.bluetooth.BluetoothProfile.HEARING_AID})
-     * @param suppressNoisyIntent if true the
-     * {@link AudioManager.ACTION_AUDIO_BECOMING_NOISY} intent will not be sent.
-     * {@hide}
-     */
-    public void setBluetoothHearingAidDeviceConnectionState(
-                BluetoothDevice device, int state, boolean suppressNoisyIntent,
-                int musicDevice) {
-        final IAudioService service = getService();
-        try {
-            service.setBluetoothHearingAidDeviceConnectionState(device,
-                state, suppressNoisyIntent, musicDevice);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
     /**
-    * Indicate Le Audio output device connection state change and eventually suppress
-    * the {@link AudioManager.ACTION_AUDIO_BECOMING_NOISY} intent.
-    * @param device Bluetooth device connected/disconnected
-    * @param state new connection state (BluetoothProfile.STATE_xxx)
-    * @param suppressNoisyIntent if true the
-    * {@link AudioManager.ACTION_AUDIO_BECOMING_NOISY} intent will not be sent.
-    * {@hide}
-    */
-    public void setBluetoothLeAudioOutDeviceConnectionState(BluetoothDevice device, int state,
-            boolean suppressNoisyIntent) {
-        final IAudioService service = getService();
-        try {
-            service.setBluetoothLeAudioOutDeviceConnectionState(device, state, suppressNoisyIntent);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-    /**
-    * Indicate Le Audio input connection state change.
-    * @param device Bluetooth device connected/disconnected
-    * @param state new connection state (BluetoothProfile.STATE_xxx)
-    * {@hide}
-    */
-    public void setBluetoothLeAudioInDeviceConnectionState(BluetoothDevice device, int state) {
-        final IAudioService service = getService();
-        try {
-            service.setBluetoothLeAudioInDeviceConnectionState(device, state);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-     /**
-     * Indicate A2DP source or sink connection state change and eventually suppress
-     * the {@link AudioManager.ACTION_AUDIO_BECOMING_NOISY} intent.
-     * This operation is asynchronous but its execution will still be sequentially scheduled
-     * relative to calls to {@link #setBluetoothHearingAidDeviceConnectionState(BluetoothDevice,
-     * int, boolean, int)} and
-     * {@link #handleBluetoothA2dpDeviceConfigChange(BluetoothDevice)}.
-     * @param device Bluetooth device connected/disconnected
-     * @param state  new connection state, {@link BluetoothProfile#STATE_CONNECTED}
-     *     or {@link BluetoothProfile#STATE_DISCONNECTED}
-     * @param profile profile for the A2DP device
-     * @param a2dpVolume New volume for the connecting device. Does nothing if disconnecting.
-     * (either {@link android.bluetooth.BluetoothProfile.A2DP} or
-     * {@link android.bluetooth.BluetoothProfile.A2DP_SINK})
-     * @param suppressNoisyIntent if true the
-     * {@link AudioManager.ACTION_AUDIO_BECOMING_NOISY} intent will not be sent.
+     * Indicate Bluetooth profile connection state change.
+     * Configuration changes for A2DP are indicated by having the same <code>newDevice</code> and
+     * <code>previousDevice</code>
+     * This operation is asynchronous.
+     *
+     * @param newDevice Bluetooth device connected or null if there is no new devices
+     * @param previousDevice Bluetooth device disconnected or null if there is no disconnected
+     * devices
+     * @param info contain all info related to the device. {@link BtProfileConnectionInfo}
      * {@hide}
      */
-    public void setBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(
-            BluetoothDevice device, int state,
-            int profile, boolean suppressNoisyIntent, int a2dpVolume) {
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    @RequiresPermission(android.Manifest.permission.BLUETOOTH_STACK)
+    public void handleBluetoothActiveDeviceChanged(@Nullable BluetoothDevice newDevice,
+            @Nullable BluetoothDevice previousDevice, @NonNull BtProfileConnectionInfo info) {
         final IAudioService service = getService();
         try {
-            service.setBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(device,
-                state, profile, suppressNoisyIntent, a2dpVolume);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-     /**
-     * Indicate A2DP device configuration has changed.
-     * This operation is asynchronous but its execution will still be sequentially scheduled
-     * relative to calls to
-     * {@link #setBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(BluetoothDevice, int, int,
-     * boolean, int)} and
-     * {@link #setBluetoothHearingAidDeviceConnectionState(BluetoothDevice, int, boolean, int)}
-     * @param device Bluetooth device whose configuration has changed.
-     * {@hide}
-     */
-    public void handleBluetoothA2dpDeviceConfigChange(BluetoothDevice device) {
-        final IAudioService service = getService();
-        try {
-            service.handleBluetoothA2dpDeviceConfigChange(device);
+            service.handleBluetoothActiveDeviceChanged(newDevice, previousDevice, info);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
diff --git a/media/java/android/media/BtProfileConnectionInfo.aidl b/media/java/android/media/BtProfileConnectionInfo.aidl
new file mode 100644
index 0000000..047f06b
--- /dev/null
+++ b/media/java/android/media/BtProfileConnectionInfo.aidl
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2021 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 android.media;
+
+parcelable BtProfileConnectionInfo;
+
diff --git a/media/java/android/media/BtProfileConnectionInfo.java b/media/java/android/media/BtProfileConnectionInfo.java
new file mode 100644
index 0000000..19ea2de
--- /dev/null
+++ b/media/java/android/media/BtProfileConnectionInfo.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright 2021 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 android.media;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.bluetooth.BluetoothProfile;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Contains information about Bluetooth profile connection state changed
+ * {@hide}
+ */
+@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+public final class BtProfileConnectionInfo implements Parcelable {
+    /** @hide */
+    @IntDef({
+            BluetoothProfile.A2DP,
+            BluetoothProfile.A2DP_SINK, // Can only be set by BtHelper
+            BluetoothProfile.HEADSET, // Can only be set by BtHelper
+            BluetoothProfile.HEARING_AID,
+            BluetoothProfile.LE_AUDIO,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface BtProfile {}
+
+    private final @BtProfile int mProfile;
+    private final boolean mSupprNoisy;
+    private final int mVolume;
+    private final boolean mIsLeOutput;
+
+    private BtProfileConnectionInfo(@BtProfile int profile, boolean suppressNoisyIntent, int volume,
+            boolean isLeOutput) {
+        mProfile = profile;
+        mSupprNoisy = suppressNoisyIntent;
+        mVolume = volume;
+        mIsLeOutput = isLeOutput;
+    }
+
+    /**
+     * Constructor used by BtHelper when a profile is connected
+     * {@hide}
+     */
+    public BtProfileConnectionInfo(@BtProfile int profile) {
+        this(profile, false, -1, false);
+    }
+
+    public static final @NonNull Parcelable.Creator<BtProfileConnectionInfo> CREATOR =
+            new Parcelable.Creator<BtProfileConnectionInfo>() {
+                @Override
+                public BtProfileConnectionInfo createFromParcel(Parcel source) {
+                    return new BtProfileConnectionInfo(source.readInt(), source.readBoolean(),
+                            source.readInt(), source.readBoolean());
+                }
+
+                @Override
+                public BtProfileConnectionInfo[] newArray(int size) {
+                    return new BtProfileConnectionInfo[size];
+                }
+            };
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, @WriteFlags int flags) {
+        dest.writeInt(mProfile);
+        dest.writeBoolean(mSupprNoisy);
+        dest.writeInt(mVolume);
+        dest.writeBoolean(mIsLeOutput);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Constructor for A2dp info
+     *
+     * @param suppressNoisyIntent if true the {@link AudioManager.ACTION_AUDIO_BECOMING_NOISY}
+     * intent will not be sent.
+     *
+     * @param volume of device -1 to ignore value
+     */
+    public static @NonNull BtProfileConnectionInfo a2dpInfo(boolean suppressNoisyIntent,
+            int volume) {
+        return new BtProfileConnectionInfo(BluetoothProfile.A2DP, suppressNoisyIntent, volume,
+            false);
+    }
+
+    /**
+     * Constructor for hearing aid info
+     *
+     * @param suppressNoisyIntent if true the {@link AudioManager.ACTION_AUDIO_BECOMING_NOISY}
+     * intent will not be sent.
+     */
+    public static @NonNull BtProfileConnectionInfo hearingAidInfo(boolean suppressNoisyIntent) {
+        return new BtProfileConnectionInfo(BluetoothProfile.HEARING_AID, suppressNoisyIntent, -1,
+            false);
+    }
+
+    /**
+     * constructor for le audio info
+     *
+     * @param suppressNoisyIntent if true the {@link AudioManager.ACTION_AUDIO_BECOMING_NOISY}
+     * intent will not be sent.
+     *
+     * @param isLeOutput if true mean the device is an output device, if false it's an input device
+     */
+    public static @NonNull BtProfileConnectionInfo leAudio(boolean suppressNoisyIntent,
+            boolean isLeOutput) {
+        return new BtProfileConnectionInfo(BluetoothProfile.LE_AUDIO, suppressNoisyIntent, -1,
+            isLeOutput);
+    }
+
+    /**
+     * @return The profile connection
+     */
+    public @BtProfile int getProfile() {
+        return mProfile;
+    }
+
+    /**
+     * @return {@code true} if {@link AudioManager.ACTION_AUDIO_BECOMING_NOISY} intent will not be
+     * sent
+     */
+    public boolean getSuppressNoisyIntent() {
+        return mSupprNoisy;
+    }
+
+    /**
+     * Only for {@link BluetoothProfile.A2DP} profile
+     * @return the volume of the connection or -1 if the value is ignored
+     */
+    public int getVolume() {
+        return mVolume;
+    }
+
+    /**
+     * Only for {@link BluetoothProfile.LE_AUDIO} profile
+     * @return {@code true} is the LE device is an output device, {@code false} if it's an input
+     * device
+     */
+    public boolean getIsLeOutput() {
+        return mIsLeOutput;
+    }
+}
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index dd44fdf..5ff56f9 100755
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -24,6 +24,7 @@
 import android.media.AudioPlaybackConfiguration;
 import android.media.AudioRecordingConfiguration;
 import android.media.AudioRoutesInfo;
+import android.media.BtProfileConnectionInfo;
 import android.media.IAudioFocusDispatcher;
 import android.media.IAudioModeDispatcher;
 import android.media.IAudioRoutesObserver;
@@ -207,8 +208,6 @@
     void setWiredDeviceConnectionState(int type, int state, String address, String name,
             String caller);
 
-    void handleBluetoothA2dpDeviceConfigChange(in BluetoothDevice device);
-
     @UnsupportedAppUsage
     AudioRoutesInfo startWatchingRoutes(in IAudioRoutesObserver observer);
 
@@ -268,16 +267,8 @@
 
     oneway void playerHasOpPlayAudio(in int piid, in boolean hasOpPlayAudio);
 
-    void setBluetoothHearingAidDeviceConnectionState(in BluetoothDevice device,
-            int state, boolean suppressNoisyIntent, int musicDevice);
-
-    void setBluetoothLeAudioOutDeviceConnectionState(in BluetoothDevice device, int state,
-            boolean suppressNoisyIntent);
-
-    void setBluetoothLeAudioInDeviceConnectionState(in BluetoothDevice device, int state);
-
-    void setBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(in BluetoothDevice device,
-            int state, int profile, boolean suppressNoisyIntent, int a2dpVolume);
+    void handleBluetoothActiveDeviceChanged(in BluetoothDevice newDevice,
+            in BluetoothDevice previousDevice, in BtProfileConnectionInfo info);
 
     oneway void setFocusRequestResultFromExtPolicy(in AudioFocusInfo afi, int requestResult,
             in IAudioPolicyCallback pcb);
diff --git a/media/jni/android_media_Utils.cpp b/media/jni/android_media_Utils.cpp
index ecd9cc1..39b560b 100644
--- a/media/jni/android_media_Utils.cpp
+++ b/media/jni/android_media_Utils.cpp
@@ -65,6 +65,7 @@
         case HAL_PIXEL_FORMAT_Y8:
         case HAL_PIXEL_FORMAT_Y16:
         case HAL_PIXEL_FORMAT_RAW16:
+        case HAL_PIXEL_FORMAT_RAW12:
         case HAL_PIXEL_FORMAT_RAW10:
         case HAL_PIXEL_FORMAT_RAW_OPAQUE:
         case HAL_PIXEL_FORMAT_BLOB:
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.java
index 15f77ff..8a213ec 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.java
@@ -239,18 +239,20 @@
         if (mGhbmView != null && surface == null) {
             Log.e(TAG, "doIlluminate | surface must be non-null for GHBM");
         }
-        mHbmProvider.enableHbm(mHbmType, surface, () -> {
-            if (mGhbmView != null) {
-                mGhbmView.drawIlluminationDot(mSensorRect);
-            }
-            if (onIlluminatedRunnable != null) {
-                // No framework API can reliably tell when a frame reaches the panel. A timeout
-                // is the safest solution.
-                postDelayed(onIlluminatedRunnable, mOnIlluminatedDelayMs);
-            } else {
-                Log.w(TAG, "doIlluminate | onIlluminatedRunnable is null");
-            }
-        });
+        if (mHbmProvider != null) {
+            mHbmProvider.enableHbm(mHbmType, surface, () -> {
+                if (mGhbmView != null) {
+                    mGhbmView.drawIlluminationDot(mSensorRect);
+                }
+                if (onIlluminatedRunnable != null) {
+                    // No framework API can reliably tell when a frame reaches the panel. A timeout
+                    // is the safest solution.
+                    postDelayed(onIlluminatedRunnable, mOnIlluminatedDelayMs);
+                } else {
+                    Log.w(TAG, "doIlluminate | onIlluminatedRunnable is null");
+                }
+            });
+        }
     }
 
     @Override
@@ -263,6 +265,8 @@
             mGhbmView.setGhbmIlluminationListener(null);
             mGhbmView.setVisibility(View.INVISIBLE);
         }
-        mHbmProvider.disableHbm(null /* onHbmDisabled */);
+        if (mHbmProvider != null) {
+            mHbmProvider.disableHbm(null /* onHbmDisabled */);
+        }
     }
 }
diff --git a/services/core/java/com/android/server/NetworkManagementService.java b/services/core/java/com/android/server/NetworkManagementService.java
index c7e9068..a2c2dbd 100644
--- a/services/core/java/com/android/server/NetworkManagementService.java
+++ b/services/core/java/com/android/server/NetworkManagementService.java
@@ -39,8 +39,6 @@
 import static android.net.NetworkStats.TAG_NONE;
 import static android.net.TrafficStats.UID_TETHERING;
 
-import static com.android.server.NetworkManagementSocketTagger.PROP_QTAGUID_ENABLED;
-
 import android.annotation.NonNull;
 import android.app.ActivityManager;
 import android.content.Context;
@@ -72,7 +70,6 @@
 import android.os.ServiceSpecificException;
 import android.os.StrictMode;
 import android.os.SystemClock;
-import android.os.SystemProperties;
 import android.os.Trace;
 import android.text.TextUtils;
 import android.util.Log;
@@ -446,9 +443,6 @@
         // push any existing quota or UID rules
         synchronized (mQuotaLock) {
 
-            // Netd unconditionally enable bandwidth control
-            SystemProperties.set(PROP_QTAGUID_ENABLED, "1");
-
             mStrictEnabled = true;
 
             setDataSaverModeEnabled(mDataSaverMode);
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index c383f51..0b2311b 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -17,12 +17,9 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.bluetooth.BluetoothA2dp;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothHeadset;
-import android.bluetooth.BluetoothHearingAid;
 import android.bluetooth.BluetoothProfile;
-import android.bluetooth.BluetoothLeAudio;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
@@ -32,6 +29,7 @@
 import android.media.AudioManager;
 import android.media.AudioRoutesInfo;
 import android.media.AudioSystem;
+import android.media.BtProfileConnectionInfo;
 import android.media.IAudioRoutesObserver;
 import android.media.ICapturePresetDevicesRoleDispatcher;
 import android.media.ICommunicationDeviceDispatcher;
@@ -516,29 +514,82 @@
         }
     };
 
-    /*package*/ static final class BtDeviceConnectionInfo {
-        final @NonNull BluetoothDevice mDevice;
-        final @AudioService.BtProfileConnectionState int mState;
-        final int mProfile;
-        final boolean mSupprNoisy;
-        final int mVolume;
+    /*package*/ static final class BtDeviceChangedData {
+        final @Nullable BluetoothDevice mNewDevice;
+        final @Nullable BluetoothDevice mPreviousDevice;
+        final @NonNull BtProfileConnectionInfo mInfo;
+        final @NonNull String mEventSource;
 
-        BtDeviceConnectionInfo(@NonNull BluetoothDevice device,
-                @AudioService.BtProfileConnectionState int state,
-                int profile, boolean suppressNoisyIntent, int vol) {
-            mDevice = device;
-            mState = state;
-            mProfile = profile;
-            mSupprNoisy = suppressNoisyIntent;
-            mVolume = vol;
+        BtDeviceChangedData(@Nullable BluetoothDevice newDevice,
+                @Nullable BluetoothDevice previousDevice,
+                @NonNull BtProfileConnectionInfo info, @NonNull String eventSource) {
+            mNewDevice = newDevice;
+            mPreviousDevice = previousDevice;
+            mInfo = info;
+            mEventSource = eventSource;
         }
 
-        BtDeviceConnectionInfo(@NonNull BtDeviceConnectionInfo info) {
-            mDevice = info.mDevice;
-            mState = info.mState;
-            mProfile = info.mProfile;
-            mSupprNoisy = info.mSupprNoisy;
-            mVolume = info.mVolume;
+        @Override
+        public String toString() {
+            return "BtDeviceChangedData profile=" + BluetoothProfile.getProfileName(
+                    mInfo.getProfile())
+                + ", switch device: [" + mPreviousDevice + "] -> [" + mNewDevice + "]";
+        }
+    }
+
+    /*package*/ static final class BtDeviceInfo {
+        final @NonNull BluetoothDevice mDevice;
+        final @AudioService.BtProfileConnectionState int mState;
+        final @AudioService.BtProfile int mProfile;
+        final boolean mSupprNoisy;
+        final int mVolume;
+        final boolean mIsLeOutput;
+        final @NonNull String mEventSource;
+        final @AudioSystem.AudioFormatNativeEnumForBtCodec int mCodec;
+        final int mAudioSystemDevice;
+        final int mMusicDevice;
+
+        BtDeviceInfo(@NonNull BtDeviceChangedData d, @NonNull BluetoothDevice device, int state,
+                    int audioDevice, @AudioSystem.AudioFormatNativeEnumForBtCodec int codec) {
+            mDevice = device;
+            mState = state;
+            mProfile = d.mInfo.getProfile();
+            mSupprNoisy = d.mInfo.getSuppressNoisyIntent();
+            mVolume = d.mInfo.getVolume();
+            mIsLeOutput = d.mInfo.getIsLeOutput();
+            mEventSource = d.mEventSource;
+            mAudioSystemDevice = audioDevice;
+            mMusicDevice = AudioSystem.DEVICE_NONE;
+            mCodec = codec;
+        }
+
+        // constructor used by AudioDeviceBroker to search similar message
+        BtDeviceInfo(@NonNull BluetoothDevice device, int profile) {
+            mDevice = device;
+            mProfile = profile;
+            mEventSource = "";
+            mMusicDevice = AudioSystem.DEVICE_NONE;
+            mCodec = AudioSystem.AUDIO_FORMAT_DEFAULT;
+            mAudioSystemDevice = 0;
+            mState = 0;
+            mSupprNoisy = false;
+            mVolume = -1;
+            mIsLeOutput = false;
+        }
+
+        // constructor used by AudioDeviceInventory when config change failed
+        BtDeviceInfo(@NonNull BluetoothDevice device, int profile, int state, int musicDevice,
+                    int audioSystemDevice) {
+            mDevice = device;
+            mProfile = profile;
+            mEventSource = "";
+            mMusicDevice = musicDevice;
+            mCodec = AudioSystem.AUDIO_FORMAT_DEFAULT;
+            mAudioSystemDevice = audioSystemDevice;
+            mState = state;
+            mSupprNoisy = false;
+            mVolume = -1;
+            mIsLeOutput = false;
         }
 
         // redefine equality op so we can match messages intended for this device
@@ -550,16 +601,52 @@
             if (this == o) {
                 return true;
             }
-            if (o instanceof BtDeviceConnectionInfo) {
-                return mDevice.equals(((BtDeviceConnectionInfo) o).mDevice);
+            if (o instanceof BtDeviceInfo) {
+                return mProfile == ((BtDeviceInfo) o).mProfile
+                    && mDevice.equals(((BtDeviceInfo) o).mDevice);
             }
             return false;
         }
+    }
 
-        @Override
-        public String toString() {
-            return "BtDeviceConnectionInfo dev=" + mDevice.toString();
+    BtDeviceInfo createBtDeviceInfo(@NonNull BtDeviceChangedData d, @NonNull BluetoothDevice device,
+                int state) {
+        int audioDevice;
+        int codec = AudioSystem.AUDIO_FORMAT_DEFAULT;
+        switch (d.mInfo.getProfile()) {
+            case BluetoothProfile.A2DP_SINK:
+                audioDevice = AudioSystem.DEVICE_IN_BLUETOOTH_A2DP;
+                break;
+            case BluetoothProfile.A2DP:
+                audioDevice = AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP;
+                synchronized (mDeviceStateLock) {
+                    codec = mBtHelper.getA2dpCodec(device);
+                }
+                break;
+            case BluetoothProfile.HEARING_AID:
+                audioDevice = AudioSystem.DEVICE_OUT_HEARING_AID;
+                break;
+            case BluetoothProfile.LE_AUDIO:
+                if (d.mInfo.getIsLeOutput()) {
+                    audioDevice = AudioSystem.DEVICE_OUT_BLE_HEADSET;
+                } else {
+                    audioDevice = AudioSystem.DEVICE_IN_BLE_HEADSET;
+                }
+                break;
+            default: throw new IllegalArgumentException("Invalid profile " + d.mInfo.getProfile());
         }
+        return new BtDeviceInfo(d, device, state, audioDevice, codec);
+    }
+
+    private void btMediaMetricRecord(@NonNull BluetoothDevice device, String state,
+            @NonNull BtDeviceChangedData data) {
+        final String name = TextUtils.emptyIfNull(device.getName());
+        new MediaMetrics.Item(MediaMetrics.Name.AUDIO_DEVICE + MediaMetrics.SEPARATOR
+                + "queueOnBluetoothActiveDeviceChanged")
+            .set(MediaMetrics.Property.STATE, state)
+            .set(MediaMetrics.Property.STATUS, data.mInfo.getProfile())
+            .set(MediaMetrics.Property.NAME, name)
+            .record();
     }
 
     /**
@@ -567,116 +654,37 @@
      * not just a simple message post
      * @param info struct with the (dis)connection information
      */
-    /*package*/ void queueBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(
-            @NonNull BtDeviceConnectionInfo info) {
-        final String name = TextUtils.emptyIfNull(info.mDevice.getName());
-        new MediaMetrics.Item(MediaMetrics.Name.AUDIO_DEVICE + MediaMetrics.SEPARATOR
-                + "postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent")
-                .set(MediaMetrics.Property.STATE, info.mState == BluetoothProfile.STATE_CONNECTED
-                        ? MediaMetrics.Value.CONNECTED : MediaMetrics.Value.DISCONNECTED)
-                .set(MediaMetrics.Property.INDEX, info.mVolume)
-                .set(MediaMetrics.Property.NAME, name)
-                .record();
-
-        // operations of removing and posting messages related to A2DP device state change must be
-        // mutually exclusive
-        synchronized (mDeviceStateLock) {
-            // when receiving a request to change the connection state of a device, this last
-            // request is the source of truth, so cancel all previous requests that are already in
-            // the handler
-            removeScheduledA2dpEvents(info.mDevice);
-
-            sendLMsgNoDelay(
-                    info.mState == BluetoothProfile.STATE_CONNECTED
-                            ? MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_CONNECTION
-                            : MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_DISCONNECTION,
-                    SENDMSG_QUEUE, info);
+    /*package*/ void queueOnBluetoothActiveDeviceChanged(@NonNull BtDeviceChangedData data) {
+        if (data.mInfo.getProfile() == BluetoothProfile.A2DP && 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
+                    + "queueOnBluetoothActiveDeviceChanged_update")
+                    .set(MediaMetrics.Property.NAME, name)
+                    .set(MediaMetrics.Property.STATUS, data.mInfo.getProfile())
+                    .record();
+            synchronized (mDeviceStateLock) {
+                postBluetoothA2dpDeviceConfigChange(data.mNewDevice);
+            }
+        } else {
+            synchronized (mDeviceStateLock) {
+                if (data.mPreviousDevice != null) {
+                    btMediaMetricRecord(data.mPreviousDevice, MediaMetrics.Value.DISCONNECTED,
+                            data);
+                    sendLMsgNoDelay(MSG_L_BT_ACTIVE_DEVICE_CHANGE_EXT, SENDMSG_QUEUE,
+                            createBtDeviceInfo(data, data.mPreviousDevice,
+                                    BluetoothProfile.STATE_DISCONNECTED));
+                }
+                if (data.mNewDevice != null) {
+                    btMediaMetricRecord(data.mNewDevice, MediaMetrics.Value.CONNECTED, data);
+                    sendLMsgNoDelay(MSG_L_BT_ACTIVE_DEVICE_CHANGE_EXT, SENDMSG_QUEUE,
+                            createBtDeviceInfo(data, data.mNewDevice,
+                                    BluetoothProfile.STATE_CONNECTED));
+                }
+            }
         }
     }
 
-    /** remove all previously scheduled connection and state change events for the given device */
-    @GuardedBy("mDeviceStateLock")
-    private void removeScheduledA2dpEvents(@NonNull BluetoothDevice device) {
-        mBrokerHandler.removeEqualMessages(MSG_L_A2DP_DEVICE_CONFIG_CHANGE, device);
-
-        final BtDeviceConnectionInfo connectionInfoToRemove = new BtDeviceConnectionInfo(device,
-                // the next parameters of the constructor will be ignored when finding the message
-                // to remove as the equality of the message's object is tested on the device itself
-                // (see BtDeviceConnectionInfo.equals() method override)
-                BluetoothProfile.STATE_CONNECTED, 0, false, -1);
-        mBrokerHandler.removeEqualMessages(MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_DISCONNECTION,
-                connectionInfoToRemove);
-        mBrokerHandler.removeEqualMessages(MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_CONNECTION,
-                connectionInfoToRemove);
-
-        final BtHelper.BluetoothA2dpDeviceInfo devInfoToRemove =
-                new BtHelper.BluetoothA2dpDeviceInfo(device);
-        mBrokerHandler.removeEqualMessages(MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_CONNECTED,
-                devInfoToRemove);
-        mBrokerHandler.removeEqualMessages(MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_DISCONNECTED,
-                devInfoToRemove);
-        mBrokerHandler.removeEqualMessages(MSG_L_A2DP_ACTIVE_DEVICE_CHANGE,
-                devInfoToRemove);
-    }
-
-    private static final class HearingAidDeviceConnectionInfo {
-        final @NonNull BluetoothDevice mDevice;
-        final @AudioService.BtProfileConnectionState int mState;
-        final boolean mSupprNoisy;
-        final int mMusicDevice;
-        final @NonNull String mEventSource;
-
-        HearingAidDeviceConnectionInfo(@NonNull BluetoothDevice device,
-                @AudioService.BtProfileConnectionState int state,
-                boolean suppressNoisyIntent, int musicDevice, @NonNull String eventSource) {
-            mDevice = device;
-            mState = state;
-            mSupprNoisy = suppressNoisyIntent;
-            mMusicDevice = musicDevice;
-            mEventSource = eventSource;
-        }
-    }
-
-    /*package*/ void postBluetoothHearingAidDeviceConnectionState(
-            @NonNull BluetoothDevice device, @AudioService.BtProfileConnectionState int state,
-            boolean suppressNoisyIntent, int musicDevice, @NonNull String eventSource) {
-        final HearingAidDeviceConnectionInfo info = new HearingAidDeviceConnectionInfo(
-                device, state, suppressNoisyIntent, musicDevice, eventSource);
-        sendLMsgNoDelay(MSG_L_HEARING_AID_DEVICE_CONNECTION_CHANGE_EXT, SENDMSG_QUEUE, info);
-    }
-
-    private static final class LeAudioDeviceConnectionInfo {
-        final @NonNull BluetoothDevice mDevice;
-        final @AudioService.BtProfileConnectionState int mState;
-        final boolean mSupprNoisy;
-        final @NonNull String mEventSource;
-
-        LeAudioDeviceConnectionInfo(@NonNull BluetoothDevice device,
-                @AudioService.BtProfileConnectionState int state,
-                boolean suppressNoisyIntent, @NonNull String eventSource) {
-            mDevice = device;
-            mState = state;
-            mSupprNoisy = suppressNoisyIntent;
-            mEventSource = eventSource;
-        }
-    }
-
-    /*package*/ void postBluetoothLeAudioOutDeviceConnectionState(
-            @NonNull BluetoothDevice device, @AudioService.BtProfileConnectionState int state,
-            boolean suppressNoisyIntent, @NonNull String eventSource) {
-        final LeAudioDeviceConnectionInfo info = new LeAudioDeviceConnectionInfo(
-                device, state, suppressNoisyIntent, eventSource);
-        sendLMsgNoDelay(MSG_L_LE_AUDIO_DEVICE_OUT_CONNECTION_CHANGE_EXT, SENDMSG_QUEUE, info);
-    }
-
-    /*package*/ void postBluetoothLeAudioInDeviceConnectionState(
-            @NonNull BluetoothDevice device, @AudioService.BtProfileConnectionState int state,
-            @NonNull String eventSource) {
-        final LeAudioDeviceConnectionInfo info = new LeAudioDeviceConnectionInfo(
-                device, state, false, eventSource);
-        sendLMsgNoDelay(MSG_L_LE_AUDIO_DEVICE_IN_CONNECTION_CHANGE_EXT, SENDMSG_QUEUE, info);
-    }
-
     /**
      * Current Bluetooth SCO audio active state indicated by BtHelper via setBluetoothScoOn().
      */
@@ -926,19 +934,8 @@
     }
 
     @GuardedBy("mDeviceStateLock")
-    /*package*/ void postA2dpSinkConnection(@AudioService.BtProfileConnectionState int state,
-            @NonNull BtHelper.BluetoothA2dpDeviceInfo btDeviceInfo, int delay) {
-        sendILMsg(state == BluetoothA2dp.STATE_CONNECTED
-                        ? MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_CONNECTED
-                        : MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_DISCONNECTED,
-                SENDMSG_QUEUE,
-                state, btDeviceInfo, delay);
-    }
-
-    /*package*/ void postA2dpSourceConnection(@AudioService.BtProfileConnectionState int state,
-            @NonNull BtHelper.BluetoothA2dpDeviceInfo btDeviceInfo, int delay) {
-        sendILMsg(MSG_IL_SET_A2DP_SOURCE_CONNECTION_STATE, SENDMSG_QUEUE,
-                state, btDeviceInfo, delay);
+    /*package*/ void postBluetoothActiveDevice(BtDeviceInfo info, int delay) {
+        sendLMsg(MSG_L_SET_BT_ACTIVE_DEVICE, SENDMSG_QUEUE, info, delay);
     }
 
     /*package*/ void postSetWiredDeviceConnectionState(
@@ -946,72 +943,12 @@
         sendLMsg(MSG_L_SET_WIRED_DEVICE_CONNECTION_STATE, SENDMSG_QUEUE, connectionState, delay);
     }
 
-    /*package*/ void postSetHearingAidConnectionState(
-            @AudioService.BtProfileConnectionState int state,
-            @NonNull BluetoothDevice device, int delay) {
-        sendILMsg(MSG_IL_SET_HEARING_AID_CONNECTION_STATE, SENDMSG_QUEUE,
-                state,
-                device,
-                delay);
+    /*package*/ void postBtProfileDisconnected(int profile) {
+        sendIMsgNoDelay(MSG_I_BT_SERVICE_DISCONNECTED_PROFILE, SENDMSG_QUEUE, profile);
     }
 
-    /*package*/ void postSetLeAudioOutConnectionState(
-            @AudioService.BtProfileConnectionState int state,
-            @NonNull BluetoothDevice device, int delay) {
-        sendILMsg(MSG_IL_SET_LE_AUDIO_OUT_CONNECTION_STATE, SENDMSG_QUEUE,
-                state,
-                device,
-                delay);
-    }
-
-    /*package*/ void postSetLeAudioInConnectionState(
-            @AudioService.BtProfileConnectionState int state,
-            @NonNull BluetoothDevice device) {
-        sendILMsgNoDelay(MSG_IL_SET_LE_AUDIO_IN_CONNECTION_STATE, SENDMSG_QUEUE,
-                state,
-                device);
-    }
-
-    /*package*/ void postDisconnectA2dp() {
-        sendMsgNoDelay(MSG_DISCONNECT_A2DP, SENDMSG_QUEUE);
-    }
-
-    /*package*/ void postDisconnectA2dpSink() {
-        sendMsgNoDelay(MSG_DISCONNECT_A2DP_SINK, SENDMSG_QUEUE);
-    }
-
-    /*package*/ void postDisconnectHearingAid() {
-        sendMsgNoDelay(MSG_DISCONNECT_BT_HEARING_AID, SENDMSG_QUEUE);
-    }
-
-    /*package*/ void postDisconnectLeAudio() {
-        sendMsgNoDelay(MSG_DISCONNECT_BT_LE_AUDIO, SENDMSG_QUEUE);
-    }
-
-    /*package*/ void postDisconnectHeadset() {
-        sendMsgNoDelay(MSG_DISCONNECT_BT_HEADSET, SENDMSG_QUEUE);
-    }
-
-    /*package*/ void postBtA2dpProfileConnected(BluetoothA2dp a2dpProfile) {
-        sendLMsgNoDelay(MSG_L_BT_SERVICE_CONNECTED_PROFILE_A2DP, SENDMSG_QUEUE, a2dpProfile);
-    }
-
-    /*package*/ void postBtA2dpSinkProfileConnected(BluetoothProfile profile) {
-        sendLMsgNoDelay(MSG_L_BT_SERVICE_CONNECTED_PROFILE_A2DP_SINK, SENDMSG_QUEUE, profile);
-    }
-
-    /*package*/ void postBtHeasetProfileConnected(BluetoothHeadset headsetProfile) {
-        sendLMsgNoDelay(MSG_L_BT_SERVICE_CONNECTED_PROFILE_HEADSET, SENDMSG_QUEUE, headsetProfile);
-    }
-
-    /*package*/ void postBtHearingAidProfileConnected(BluetoothHearingAid hearingAidProfile) {
-        sendLMsgNoDelay(MSG_L_BT_SERVICE_CONNECTED_PROFILE_HEARING_AID, SENDMSG_QUEUE,
-                hearingAidProfile);
-    }
-
-    /*package*/ void postBtLeAudioProfileConnected(BluetoothLeAudio leAudioProfile) {
-        sendLMsgNoDelay(MSG_L_BT_SERVICE_CONNECTED_PROFILE_LE_AUDIO, SENDMSG_QUEUE,
-                leAudioProfile);
+    /*package*/ void postBtProfileConnected(int profile, BluetoothProfile proxy) {
+        sendILMsgNoDelay(MSG_IL_BT_SERVICE_CONNECTED_PROFILE, SENDMSG_QUEUE, profile, proxy);
     }
 
     /*package*/ void postCommunicationRouteClientDied(CommunicationRouteClient client) {
@@ -1069,13 +1006,6 @@
         }
     }
 
-    /*package*/ void postSetA2dpSourceConnectionState(@BluetoothProfile.BtProfileState int state,
-            @NonNull BtHelper.BluetoothA2dpDeviceInfo btDeviceInfo) {
-        final int intState = (state == BluetoothA2dp.STATE_CONNECTED) ? 1 : 0;
-        sendILMsgNoDelay(MSG_IL_SET_A2DP_SOURCE_CONNECTION_STATE, SENDMSG_QUEUE, state,
-                btDeviceInfo);
-    }
-
     /*package*/ void handleFailureToConnectToBtHeadsetService(int delay) {
         sendMsg(MSG_BT_HEADSET_CNCT_FAILED, SENDMSG_REPLACE, delay);
     }
@@ -1088,19 +1018,10 @@
         sendMsgNoDelay(fromA2dp ? MSG_REPORT_NEW_ROUTES_A2DP : MSG_REPORT_NEW_ROUTES, SENDMSG_NOOP);
     }
 
-    /*package*/ void postA2dpActiveDeviceChange(
-                    @NonNull BtHelper.BluetoothA2dpDeviceInfo btDeviceInfo) {
-        sendLMsgNoDelay(MSG_L_A2DP_ACTIVE_DEVICE_CHANGE, SENDMSG_QUEUE, btDeviceInfo);
-    }
-
     // must be called synchronized on mConnectedDevices
-    /*package*/ boolean hasScheduledA2dpSinkConnectionState(BluetoothDevice btDevice) {
-        final BtHelper.BluetoothA2dpDeviceInfo devInfoToCheck =
-                new BtHelper.BluetoothA2dpDeviceInfo(btDevice);
-        return (mBrokerHandler.hasEqualMessages(
-                    MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_CONNECTED, devInfoToCheck)
-            || mBrokerHandler.hasEqualMessages(
-                    MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_DISCONNECTED, devInfoToCheck));
+    /*package*/ boolean hasScheduledA2dpConnection(BluetoothDevice btDevice) {
+        final BtDeviceInfo devInfoToCheck = new BtDeviceInfo(btDevice, BluetoothProfile.A2DP);
+        return mBrokerHandler.hasEqualMessages(MSG_L_SET_BT_ACTIVE_DEVICE, devInfoToCheck);
     }
 
     /*package*/ void setA2dpTimeout(String address, int a2dpCodec, int delayMs) {
@@ -1124,12 +1045,6 @@
         }
     }
 
-    /*package*/ int getA2dpCodec(@NonNull BluetoothDevice device) {
-        synchronized (mDeviceStateLock) {
-            return mBtHelper.getA2dpCodec(device);
-        }
-    }
-
     /*package*/ void broadcastStickyIntentToCurrentProfileGroup(Intent intent) {
         mSystemServer.broadcastStickyIntentToCurrentProfileGroup(intent);
     }
@@ -1285,39 +1200,11 @@
                         mDeviceInventory.onReportNewRoutes();
                     }
                     break;
-                case MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_CONNECTED:
-                case MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_DISCONNECTED:
+                case MSG_L_SET_BT_ACTIVE_DEVICE:
                     synchronized (mDeviceStateLock) {
-                        mDeviceInventory.onSetA2dpSinkConnectionState(
-                                (BtHelper.BluetoothA2dpDeviceInfo) msg.obj, msg.arg1);
-                    }
-                    break;
-                case MSG_IL_SET_A2DP_SOURCE_CONNECTION_STATE:
-                    synchronized (mDeviceStateLock) {
-                        mDeviceInventory.onSetA2dpSourceConnectionState(
-                                (BtHelper.BluetoothA2dpDeviceInfo) msg.obj, msg.arg1);
-                    }
-                    break;
-                case MSG_IL_SET_HEARING_AID_CONNECTION_STATE:
-                    synchronized (mDeviceStateLock) {
-                        mDeviceInventory.onSetHearingAidConnectionState(
-                                (BluetoothDevice) msg.obj, msg.arg1,
+                        mDeviceInventory.onSetBtActiveDevice((BtDeviceInfo) msg.obj,
                                 mAudioService.getBluetoothContextualVolumeStream());
                     }
-                    break;
-                case MSG_IL_SET_LE_AUDIO_OUT_CONNECTION_STATE:
-                    synchronized (mDeviceStateLock) {
-                        mDeviceInventory.onSetLeAudioOutConnectionState(
-                                (BluetoothDevice) msg.obj, msg.arg1,
-                                mAudioService.getBluetoothContextualVolumeStream());
-                    }
-                    break;
-                case MSG_IL_SET_LE_AUDIO_IN_CONNECTION_STATE:
-                    synchronized (mDeviceStateLock) {
-                        mDeviceInventory.onSetLeAudioInConnectionState(
-                                (BluetoothDevice) msg.obj, msg.arg1);
-                    }
-                    break;
                 case MSG_BT_HEADSET_CNCT_FAILED:
                     synchronized (mSetModeLock) {
                         synchronized (mDeviceStateLock) {
@@ -1332,14 +1219,10 @@
                     }
                     break;
                 case MSG_L_A2DP_DEVICE_CONFIG_CHANGE:
-                    final int a2dpCodec;
                     final BluetoothDevice btDevice = (BluetoothDevice) msg.obj;
                     synchronized (mDeviceStateLock) {
-                        a2dpCodec = mBtHelper.getA2dpCodec(btDevice);
-                        // TODO: name of method being called on AudioDeviceInventory is currently
-                        //       misleading (config change vs active device change), to be
-                        //       reconciliated once the BT side has been updated.
-                        mDeviceInventory.onBluetoothA2dpActiveDeviceChange(
+                        final int a2dpCodec = mBtHelper.getA2dpCodec(btDevice);
+                        mDeviceInventory.onBluetoothA2dpDeviceConfigChange(
                                 new BtHelper.BluetoothA2dpDeviceInfo(btDevice, -1, a2dpCodec),
                                         BtHelper.EVENT_DEVICE_CONFIG_CHANGE);
                     }
@@ -1392,96 +1275,47 @@
                         mDeviceInventory.onToggleHdmi();
                     }
                     break;
-                case MSG_L_A2DP_ACTIVE_DEVICE_CHANGE:
-                    synchronized (mDeviceStateLock) {
-                        mDeviceInventory.onBluetoothA2dpActiveDeviceChange(
-                                (BtHelper.BluetoothA2dpDeviceInfo) msg.obj,
-                                 BtHelper.EVENT_ACTIVE_DEVICE_CHANGE);
-                    }
-                    break;
-                case MSG_DISCONNECT_A2DP:
-                    synchronized (mDeviceStateLock) {
-                        mDeviceInventory.disconnectA2dp();
-                    }
-                    break;
-                case MSG_DISCONNECT_A2DP_SINK:
-                    synchronized (mDeviceStateLock) {
-                        mDeviceInventory.disconnectA2dpSink();
-                    }
-                    break;
-                case MSG_DISCONNECT_BT_HEARING_AID:
-                    synchronized (mDeviceStateLock) {
-                        mDeviceInventory.disconnectHearingAid();
-                    }
-                    break;
-                case MSG_DISCONNECT_BT_HEADSET:
-                    synchronized (mSetModeLock) {
+                case MSG_I_BT_SERVICE_DISCONNECTED_PROFILE:
+                    if (msg.arg1 != BluetoothProfile.HEADSET) {
                         synchronized (mDeviceStateLock) {
-                            mBtHelper.disconnectHeadset();
+                            mDeviceInventory.onBtProfileDisconnected(msg.arg1);
+                        }
+                    } else {
+                        synchronized (mSetModeLock) {
+                            synchronized (mDeviceStateLock) {
+                                mBtHelper.disconnectHeadset();
+                            }
                         }
                     }
                     break;
-                case MSG_DISCONNECT_BT_LE_AUDIO:
-                    synchronized(mDeviceStateLock) {
-                        mDeviceInventory.disconnectLeAudio();
-                    }
-                    break;
-                case MSG_L_BT_SERVICE_CONNECTED_PROFILE_A2DP:
-                    synchronized (mDeviceStateLock) {
-                        mBtHelper.onA2dpProfileConnected((BluetoothA2dp) msg.obj);
-                    }
-                    break;
-                case MSG_L_BT_SERVICE_CONNECTED_PROFILE_A2DP_SINK:
-                    synchronized (mDeviceStateLock) {
-                        mBtHelper.onA2dpSinkProfileConnected((BluetoothProfile) msg.obj);
-                    }
-                    break;
-                case MSG_L_BT_SERVICE_CONNECTED_PROFILE_HEARING_AID:
-                    synchronized (mDeviceStateLock) {
-                        mBtHelper.onHearingAidProfileConnected((BluetoothHearingAid) msg.obj);
-                    }
-                    break;
-
-                case MSG_L_BT_SERVICE_CONNECTED_PROFILE_LE_AUDIO:
-                    synchronized(mDeviceStateLock) {
-                        mBtHelper.onLeAudioProfileConnected((BluetoothLeAudio) msg.obj);
-                    }
-                    break;
-                case MSG_L_BT_SERVICE_CONNECTED_PROFILE_HEADSET:
-                    synchronized (mSetModeLock) {
+                case MSG_IL_BT_SERVICE_CONNECTED_PROFILE:
+                    if (msg.arg1 != BluetoothProfile.HEADSET) {
                         synchronized (mDeviceStateLock) {
-                            mBtHelper.onHeadsetProfileConnected((BluetoothHeadset) msg.obj);
+                            mBtHelper.onBtProfileConnected(msg.arg1, (BluetoothProfile) msg.obj);
+                        }
+                    } else {
+                        synchronized (mSetModeLock) {
+                            synchronized (mDeviceStateLock) {
+                                mBtHelper.onHeadsetProfileConnected((BluetoothHeadset) msg.obj);
+                            }
                         }
                     }
                     break;
-                case MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_CONNECTION:
-                case MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_DISCONNECTION: {
-                    final BtDeviceConnectionInfo info = (BtDeviceConnectionInfo) msg.obj;
+                case MSG_L_BT_ACTIVE_DEVICE_CHANGE_EXT: {
+                    final BtDeviceInfo info = (BtDeviceInfo) msg.obj;
+                    if (info.mDevice == null) break;
                     AudioService.sDeviceLogger.log((new AudioEventLogger.StringEvent(
-                            "msg: setBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent "
+                            "msg: onBluetoothActiveDeviceChange "
                                     + " state=" + info.mState
                                     // only querying address as this is the only readily available
                                     // field on the device
                                     + " addr=" + info.mDevice.getAddress()
-                                    + " prof=" + info.mProfile + " supprNoisy=" + info.mSupprNoisy
-                                    + " vol=" + info.mVolume)).printLog(TAG));
-                    synchronized (mDeviceStateLock) {
-                        mDeviceInventory.setBluetoothA2dpDeviceConnectionState(
-                                info.mDevice, info.mState, info.mProfile, info.mSupprNoisy,
-                                AudioSystem.DEVICE_NONE, info.mVolume);
-                    }
-                } break;
-                case MSG_L_HEARING_AID_DEVICE_CONNECTION_CHANGE_EXT: {
-                    final HearingAidDeviceConnectionInfo info =
-                            (HearingAidDeviceConnectionInfo) msg.obj;
-                    AudioService.sDeviceLogger.log((new AudioEventLogger.StringEvent(
-                            "msg: setHearingAidDeviceConnectionState state=" + info.mState
-                                    + " addr=" + info.mDevice.getAddress()
+                                    + " prof=" + info.mProfile
                                     + " supprNoisy=" + info.mSupprNoisy
-                                    + " src=" + info.mEventSource)).printLog(TAG));
+                                    + " src=" + info.mEventSource
+                                    )).printLog(TAG));
                     synchronized (mDeviceStateLock) {
-                        mDeviceInventory.setBluetoothHearingAidDeviceConnectionState(
-                                info.mDevice, info.mState, info.mSupprNoisy, info.mMusicDevice);
+                        mDeviceInventory.setBluetoothActiveDevice(info);
                     }
                 } break;
                 case MSG_IL_SAVE_PREF_DEVICES_FOR_STRATEGY: {
@@ -1524,31 +1358,6 @@
                     final int capturePreset = msg.arg1;
                     mDeviceInventory.onSaveClearPreferredDevicesForCapturePreset(capturePreset);
                 } break;
-                case MSG_L_LE_AUDIO_DEVICE_OUT_CONNECTION_CHANGE_EXT: {
-                    final LeAudioDeviceConnectionInfo info =
-                            (LeAudioDeviceConnectionInfo) msg.obj;
-                    AudioService.sDeviceLogger.log((new AudioEventLogger.StringEvent(
-                            "setLeAudioDeviceOutConnectionState state=" + info.mState
-                                    + " addr=" + info.mDevice.getAddress()
-                                    + " supprNoisy=" + info.mSupprNoisy
-                                    + " src=" + info.mEventSource)).printLog(TAG));
-                    synchronized (mDeviceStateLock) {
-                        mDeviceInventory.setBluetoothLeAudioOutDeviceConnectionState(
-                                info.mDevice, info.mState, info.mSupprNoisy);
-                    }
-                } break;
-                case MSG_L_LE_AUDIO_DEVICE_IN_CONNECTION_CHANGE_EXT: {
-                    final LeAudioDeviceConnectionInfo info =
-                            (LeAudioDeviceConnectionInfo) msg.obj;
-                    AudioService.sDeviceLogger.log((new AudioEventLogger.StringEvent(
-                            "setLeAudioDeviceInConnectionState state=" + info.mState
-                                    + " addr=" + info.mDevice.getAddress()
-                                    + " src=" + info.mEventSource)).printLog(TAG));
-                    synchronized (mDeviceStateLock) {
-                        mDeviceInventory.setBluetoothLeAudioInDeviceConnectionState(info.mDevice,
-                                info.mState);
-                    }
-                } break;
                 default:
                     Log.wtf(TAG, "Invalid message " + msg.what);
             }
@@ -1579,8 +1388,7 @@
     private static final int MSG_IIL_SET_FORCE_USE = 4;
     private static final int MSG_IIL_SET_FORCE_BT_A2DP_USE = 5;
     private static final int MSG_TOGGLE_HDMI = 6;
-    private static final int MSG_IL_SET_A2DP_SOURCE_CONNECTION_STATE = 7;
-    private static final int MSG_IL_SET_HEARING_AID_CONNECTION_STATE = 8;
+    private static final int MSG_L_SET_BT_ACTIVE_DEVICE = 7;
     private static final int MSG_BT_HEADSET_CNCT_FAILED = 9;
     private static final int MSG_IL_BTA2DP_TIMEOUT = 10;
 
@@ -1593,25 +1401,11 @@
     private static final int MSG_I_SET_AVRCP_ABSOLUTE_VOLUME = 15;
     private static final int MSG_I_SET_MODE_OWNER_PID = 16;
 
-    // process active A2DP device change, obj is BtHelper.BluetoothA2dpDeviceInfo
-    private static final int MSG_L_A2DP_ACTIVE_DEVICE_CHANGE = 18;
-
-    private static final int MSG_DISCONNECT_A2DP = 19;
-    private static final int MSG_DISCONNECT_A2DP_SINK = 20;
-    private static final int MSG_DISCONNECT_BT_HEARING_AID = 21;
-    private static final int MSG_DISCONNECT_BT_HEADSET = 22;
-    private static final int MSG_L_BT_SERVICE_CONNECTED_PROFILE_A2DP = 23;
-    private static final int MSG_L_BT_SERVICE_CONNECTED_PROFILE_A2DP_SINK = 24;
-    private static final int MSG_L_BT_SERVICE_CONNECTED_PROFILE_HEARING_AID = 25;
-    private static final int MSG_L_BT_SERVICE_CONNECTED_PROFILE_HEADSET = 26;
-
-    // process change of state, obj is BtHelper.BluetoothA2dpDeviceInfo
-    private static final int MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_CONNECTED = 27;
-    private static final int MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_DISCONNECTED = 28;
+    private static final int MSG_I_BT_SERVICE_DISCONNECTED_PROFILE = 22;
+    private static final int MSG_IL_BT_SERVICE_CONNECTED_PROFILE = 23;
 
     // process external command to (dis)connect an A2DP device, obj is BtDeviceConnectionInfo
-    private static final int MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_CONNECTION = 29;
-    private static final int MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_DISCONNECTION = 30;
+    private static final int MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT = 29;
 
     // process external command to (dis)connect a hearing aid device
     private static final int MSG_L_HEARING_AID_DEVICE_CONNECTION_CHANGE_EXT = 31;
@@ -1630,33 +1424,21 @@
     private static final int MSG_IL_SET_PREF_DEVICES_FOR_STRATEGY = 40;
     private static final int MSG_I_REMOVE_PREF_DEVICES_FOR_STRATEGY = 41;
 
-    private static final int MSG_IL_SET_LE_AUDIO_OUT_CONNECTION_STATE = 42;
-    private static final int MSG_IL_SET_LE_AUDIO_IN_CONNECTION_STATE = 43;
-    private static final int MSG_L_LE_AUDIO_DEVICE_OUT_CONNECTION_CHANGE_EXT = 44;
-    private static final int MSG_L_LE_AUDIO_DEVICE_IN_CONNECTION_CHANGE_EXT = 45;
+    private static final int MSG_L_BT_ACTIVE_DEVICE_CHANGE_EXT = 45;
+    //
     // process set volume for Le Audio, obj is BleVolumeInfo
     private static final int MSG_II_SET_LE_AUDIO_OUT_VOLUME = 46;
 
-    private static final int MSG_L_BT_SERVICE_CONNECTED_PROFILE_LE_AUDIO = 47;
-    private static final int MSG_DISCONNECT_BT_LE_AUDIO = 48;
-
     private static boolean isMessageHandledUnderWakelock(int msgId) {
         switch(msgId) {
             case MSG_L_SET_WIRED_DEVICE_CONNECTION_STATE:
-            case MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_CONNECTED:
-            case MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_DISCONNECTED:
-            case MSG_IL_SET_A2DP_SOURCE_CONNECTION_STATE:
-            case MSG_IL_SET_HEARING_AID_CONNECTION_STATE:
+            case MSG_L_SET_BT_ACTIVE_DEVICE:
             case MSG_IL_BTA2DP_TIMEOUT:
             case MSG_L_A2DP_DEVICE_CONFIG_CHANGE:
             case MSG_TOGGLE_HDMI:
-            case MSG_L_A2DP_ACTIVE_DEVICE_CHANGE:
-            case MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_CONNECTION:
-            case MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_DISCONNECTION:
+            case MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT:
             case MSG_L_HEARING_AID_DEVICE_CONNECTION_CHANGE_EXT:
             case MSG_CHECK_MUTE_MUSIC:
-            case MSG_L_LE_AUDIO_DEVICE_OUT_CONNECTION_CHANGE_EXT:
-            case MSG_L_LE_AUDIO_DEVICE_IN_CONNECTION_CHANGE_EXT:
                 return true;
             default:
                 return false;
@@ -1739,14 +1521,10 @@
             long time = SystemClock.uptimeMillis() + delay;
 
             switch (msg) {
-                case MSG_IL_SET_A2DP_SOURCE_CONNECTION_STATE:
-                case MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_CONNECTED:
-                case MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_DISCONNECTED:
-                case MSG_IL_SET_HEARING_AID_CONNECTION_STATE:
+                case MSG_L_SET_BT_ACTIVE_DEVICE:
                 case MSG_L_SET_WIRED_DEVICE_CONNECTION_STATE:
                 case MSG_IL_BTA2DP_TIMEOUT:
                 case MSG_L_A2DP_DEVICE_CONFIG_CHANGE:
-                case MSG_L_A2DP_ACTIVE_DEVICE_CHANGE:
                     if (sLastDeviceConnectMsgTime >= time) {
                         // add a little delay to make sure messages are ordered as expected
                         time = sLastDeviceConnectMsgTime + 30;
@@ -1765,14 +1543,9 @@
     private static final Set<Integer> MESSAGES_MUTE_MUSIC;
     static {
         MESSAGES_MUTE_MUSIC = new HashSet<>();
-        MESSAGES_MUTE_MUSIC.add(MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_CONNECTED);
-        MESSAGES_MUTE_MUSIC.add(MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_DISCONNECTED);
-        MESSAGES_MUTE_MUSIC.add(MSG_IL_SET_LE_AUDIO_OUT_CONNECTION_STATE);
+        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_A2DP_ACTIVE_DEVICE_CHANGE);
-        MESSAGES_MUTE_MUSIC.add(MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_CONNECTION);
-        MESSAGES_MUTE_MUSIC.add(MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_DISCONNECTION);
-        MESSAGES_MUTE_MUSIC.add(MSG_L_LE_AUDIO_DEVICE_OUT_CONNECTION_CHANGE_EXT);
+        MESSAGES_MUTE_MUSIC.add(MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT);
         MESSAGES_MUTE_MUSIC.add(MSG_IIL_SET_FORCE_BT_A2DP_USE);
         MESSAGES_MUTE_MUSIC.add(MSG_REPORT_NEW_ROUTES_A2DP);
     }
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index 6c3c736..0a114b9 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -16,11 +16,8 @@
 package com.android.server.audio;
 
 import android.annotation.NonNull;
-import android.bluetooth.BluetoothA2dp;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothHearingAid;
-import android.bluetooth.BluetoothLeAudio;
 import android.bluetooth.BluetoothProfile;
 import android.content.Intent;
 import android.media.AudioDeviceAttributes;
@@ -286,186 +283,102 @@
         }
     }
 
-    // only public for mocking/spying
     @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
-    @VisibleForTesting
-    public void onSetA2dpSinkConnectionState(@NonNull BtHelper.BluetoothA2dpDeviceInfo btInfo,
-            @AudioService.BtProfileConnectionState int state) {
-        final BluetoothDevice btDevice = btInfo.getBtDevice();
-        int a2dpVolume = btInfo.getVolume();
+    void onSetBtActiveDevice(@NonNull AudioDeviceBroker.BtDeviceInfo btInfo, int streamType) {
         if (AudioService.DEBUG_DEVICES) {
-            Log.d(TAG, "onSetA2dpSinkConnectionState btDevice=" + btDevice + " state="
-                    + state + " vol=" + a2dpVolume);
+            Log.d(TAG, "onSetBtActiveDevice"
+                    + " btDevice=" + btInfo.mDevice
+                    + " profile=" + BluetoothProfile.getProfileName(btInfo.mProfile)
+                    + " state=" + BluetoothProfile.getConnectionStateName(btInfo.mState));
         }
-        String address = btDevice.getAddress();
-        if (address == null) {
-            address = "";
-        }
+        String address = btInfo.mDevice.getAddress();
         if (!BluetoothAdapter.checkBluetoothAddress(address)) {
             address = "";
         }
 
-        final @AudioSystem.AudioFormatNativeEnumForBtCodec int a2dpCodec = btInfo.getCodec();
+        AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent("BT connected:"
+                        + " addr=" + address
+                        + " profile=" + btInfo.mProfile
+                        + " state=" + btInfo.mState
+                        + " codec=" + AudioSystem.audioFormatToString(btInfo.mCodec)));
 
-        AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(
-                "A2DP sink connected: device addr=" + address + " state=" + state
-                        + " codec=" + AudioSystem.audioFormatToString(a2dpCodec)
-                        + " vol=" + a2dpVolume));
-
-        new MediaMetrics.Item(mMetricsId + "a2dp")
+        new MediaMetrics.Item(mMetricsId + "onSetBtActiveDevice")
+                .set(MediaMetrics.Property.STATUS, btInfo.mProfile)
+                .set(MediaMetrics.Property.DEVICE,
+                        AudioSystem.getDeviceName(btInfo.mAudioSystemDevice))
                 .set(MediaMetrics.Property.ADDRESS, address)
-                .set(MediaMetrics.Property.ENCODING, AudioSystem.audioFormatToString(a2dpCodec))
-                .set(MediaMetrics.Property.EVENT, "onSetA2dpSinkConnectionState")
-                .set(MediaMetrics.Property.INDEX, a2dpVolume)
+                .set(MediaMetrics.Property.ENCODING,
+                        AudioSystem.audioFormatToString(btInfo.mCodec))
+                .set(MediaMetrics.Property.EVENT, "onSetBtActiveDevice")
+                .set(MediaMetrics.Property.STREAM_TYPE,
+                        AudioSystem.streamToString(streamType))
                 .set(MediaMetrics.Property.STATE,
-                        state == BluetoothProfile.STATE_CONNECTED
+                        btInfo.mState == BluetoothProfile.STATE_CONNECTED
                         ? MediaMetrics.Value.CONNECTED : MediaMetrics.Value.DISCONNECTED)
                 .record();
 
         synchronized (mDevicesLock) {
-            final String key = DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP,
-                    btDevice.getAddress());
+            final String key = DeviceInfo.makeDeviceListKey(btInfo.mAudioSystemDevice, address);
             final DeviceInfo di = mConnectedDevices.get(key);
-            boolean isConnected = di != null;
 
-            if (isConnected) {
-                if (state == BluetoothProfile.STATE_CONNECTED) {
-                    // device is already connected, but we are receiving a connection again,
-                    // it could be for a codec change
-                    if (a2dpCodec != di.mDeviceCodecFormat) {
-                        mDeviceBroker.postBluetoothA2dpDeviceConfigChange(btDevice);
+            final boolean isConnected = di != null;
+
+            final boolean switchToUnavailable = isConnected
+                    && btInfo.mState != BluetoothProfile.STATE_CONNECTED;
+            final boolean switchToAvailable = !isConnected
+                    && btInfo.mState == BluetoothProfile.STATE_CONNECTED;
+
+            switch (btInfo.mProfile) {
+                case BluetoothProfile.A2DP_SINK:
+                    if (switchToUnavailable) {
+                        makeA2dpSrcUnavailable(address);
+                    } else if (switchToAvailable) {
+                        makeA2dpSrcAvailable(address);
                     }
-                } else {
-                    makeA2dpDeviceUnavailableNow(address, di.mDeviceCodecFormat);
-                }
-            } else if (state == BluetoothProfile.STATE_CONNECTED) {
-                // device is not already connected
-                if (a2dpVolume != -1) {
-                    mDeviceBroker.postSetVolumeIndexOnDevice(AudioSystem.STREAM_MUSIC,
-                            // convert index to internal representation in VolumeStreamState
-                            a2dpVolume * 10,
-                            AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, "onSetA2dpSinkConnectionState");
-                }
-                makeA2dpDeviceAvailable(address, BtHelper.getName(btDevice),
-                        "onSetA2dpSinkConnectionState", a2dpCodec);
+                    break;
+                case BluetoothProfile.A2DP:
+                    if (switchToUnavailable) {
+                        makeA2dpDeviceUnavailableNow(address, di.mDeviceCodecFormat);
+                    } else if (switchToAvailable) {
+                        // device is not already connected
+                        if (btInfo.mVolume != -1) {
+                            mDeviceBroker.postSetVolumeIndexOnDevice(AudioSystem.STREAM_MUSIC,
+                                    // convert index to internal representation in VolumeStreamState
+                                    btInfo.mVolume * 10, btInfo.mAudioSystemDevice,
+                                    "onSetBtActiveDevice");
+                        }
+                        makeA2dpDeviceAvailable(address, BtHelper.getName(btInfo.mDevice),
+                                "onSetBtActiveDevice", btInfo.mCodec);
+                    }
+                    break;
+                case BluetoothProfile.HEARING_AID:
+                    if (switchToUnavailable) {
+                        makeHearingAidDeviceUnavailable(address);
+                    } else if (switchToAvailable) {
+                        makeHearingAidDeviceAvailable(address, BtHelper.getName(btInfo.mDevice),
+                                streamType, "onSetBtActiveDevice");
+                    }
+                    break;
+                case BluetoothProfile.LE_AUDIO:
+                    if (switchToUnavailable) {
+                        makeLeAudioDeviceUnavailable(address, btInfo.mAudioSystemDevice);
+                    } else if (switchToAvailable) {
+                        makeLeAudioDeviceAvailable(address, BtHelper.getName(btInfo.mDevice),
+                                streamType, btInfo.mAudioSystemDevice, "onSetBtActiveDevice");
+                    }
+                    break;
+                default: throw new IllegalArgumentException("Invalid profile "
+                                 + BluetoothProfile.getProfileName(btInfo.mProfile));
             }
         }
     }
 
-    /*package*/ void onSetA2dpSourceConnectionState(
-            @NonNull BtHelper.BluetoothA2dpDeviceInfo btInfo, int state) {
-        final BluetoothDevice btDevice = btInfo.getBtDevice();
-        if (AudioService.DEBUG_DEVICES) {
-            Log.d(TAG, "onSetA2dpSourceConnectionState btDevice=" + btDevice + " state="
-                    + state);
-        }
-        String address = btDevice.getAddress();
-        if (!BluetoothAdapter.checkBluetoothAddress(address)) {
-            address = "";
-        }
-
-        synchronized (mDevicesLock) {
-            final String key = DeviceInfo.makeDeviceListKey(
-                    AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, address);
-            final DeviceInfo di = mConnectedDevices.get(key);
-            boolean isConnected = di != null;
-
-            new MediaMetrics.Item(mMetricsId + "onSetA2dpSourceConnectionState")
-                    .set(MediaMetrics.Property.ADDRESS, address)
-                    .set(MediaMetrics.Property.DEVICE,
-                            AudioSystem.getDeviceName(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP))
-                    .set(MediaMetrics.Property.STATE,
-                            state == BluetoothProfile.STATE_CONNECTED
-                            ? MediaMetrics.Value.CONNECTED : MediaMetrics.Value.DISCONNECTED)
-                    .record();
-
-            if (isConnected && state != BluetoothProfile.STATE_CONNECTED) {
-                makeA2dpSrcUnavailable(address);
-            } else if (!isConnected && state == BluetoothProfile.STATE_CONNECTED) {
-                makeA2dpSrcAvailable(address);
-            }
-        }
-    }
-
-    /*package*/ void onSetHearingAidConnectionState(BluetoothDevice btDevice,
-                @AudioService.BtProfileConnectionState int state, int streamType) {
-        String address = btDevice.getAddress();
-        if (!BluetoothAdapter.checkBluetoothAddress(address)) {
-            address = "";
-        }
-        AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(
-                "onSetHearingAidConnectionState addr=" + address));
-
-        new MediaMetrics.Item(mMetricsId + "onSetHearingAidConnectionState")
-                .set(MediaMetrics.Property.ADDRESS, address)
-                .set(MediaMetrics.Property.DEVICE,
-                        AudioSystem.getDeviceName(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP))
-                .set(MediaMetrics.Property.STATE,
-                        state == BluetoothProfile.STATE_CONNECTED
-                                ? MediaMetrics.Value.CONNECTED : MediaMetrics.Value.DISCONNECTED)
-                .set(MediaMetrics.Property.STREAM_TYPE,
-                        AudioSystem.streamToString(streamType))
-                .record();
-
-        synchronized (mDevicesLock) {
-            final String key = DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_HEARING_AID,
-                    btDevice.getAddress());
-            final DeviceInfo di = mConnectedDevices.get(key);
-            boolean isConnected = di != null;
-
-            if (isConnected && state != BluetoothProfile.STATE_CONNECTED) {
-                makeHearingAidDeviceUnavailable(address);
-            } else if (!isConnected && state == BluetoothProfile.STATE_CONNECTED) {
-                makeHearingAidDeviceAvailable(address, BtHelper.getName(btDevice), streamType,
-                        "onSetHearingAidConnectionState");
-            }
-        }
-    }
-
-    /*package*/ void onSetLeAudioConnectionState(BluetoothDevice btDevice,
-                @AudioService.BtProfileConnectionState int state, int streamType, int device) {
-        String address = btDevice.getAddress();
-        if (!BluetoothAdapter.checkBluetoothAddress(address)) {
-            address = "";
-        }
-        AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(
-                "onSetLeAudioConnectionState addr=" + address));
-
-        synchronized (mDevicesLock) {
-            DeviceInfo di = null;
-            boolean isConnected = false;
-
-            String key = DeviceInfo.makeDeviceListKey(device, btDevice.getAddress());
-            di = mConnectedDevices.get(key);
-            isConnected = di != null;
-
-            if (isConnected && state != BluetoothProfile.STATE_CONNECTED) {
-                makeLeAudioDeviceUnavailable(address, device);
-            } else if (!isConnected && state == BluetoothProfile.STATE_CONNECTED) {
-                makeLeAudioDeviceAvailable(address, BtHelper.getName(btDevice), streamType,
-                        device, "onSetLeAudioConnectionState");
-            }
-        }
-    }
-
-    /*package*/ void onSetLeAudioOutConnectionState(BluetoothDevice btDevice,
-                @AudioService.BtProfileConnectionState int state, int streamType) {
-        // TODO: b/198610537 clarify DEVICE_OUT_BLE_HEADSET vs DEVICE_OUT_BLE_SPEAKER criteria
-        onSetLeAudioConnectionState(btDevice, state, streamType,
-                AudioSystem.DEVICE_OUT_BLE_HEADSET);
-    }
-
-    /*package*/ void onSetLeAudioInConnectionState(BluetoothDevice btDevice,
-                @AudioService.BtProfileConnectionState int state) {
-        onSetLeAudioConnectionState(btDevice, state, AudioSystem.STREAM_DEFAULT,
-                AudioSystem.DEVICE_IN_BLE_HEADSET);
-    }
 
     @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
-        /*package*/ void onBluetoothA2dpActiveDeviceChange(
+        /*package*/ void onBluetoothA2dpDeviceConfigChange(
             @NonNull BtHelper.BluetoothA2dpDeviceInfo btInfo, int event) {
         MediaMetrics.Item mmi = new MediaMetrics.Item(mMetricsId
-                + "onBluetoothA2dpActiveDeviceChange")
+                + "onBluetoothA2dpDeviceConfigChange")
                 .set(MediaMetrics.Property.EVENT, BtHelper.a2dpDeviceEventToString(event));
 
         final BluetoothDevice btDevice = btInfo.getBtDevice();
@@ -474,7 +387,7 @@
             return;
         }
         if (AudioService.DEBUG_DEVICES) {
-            Log.d(TAG, "onBluetoothA2dpActiveDeviceChange btDevice=" + btDevice);
+            Log.d(TAG, "onBluetoothA2dpDeviceConfigChange btDevice=" + btDevice);
         }
         int a2dpVolume = btInfo.getVolume();
         @AudioSystem.AudioFormatNativeEnumForBtCodec final int a2dpCodec = btInfo.getCodec();
@@ -484,11 +397,11 @@
             address = "";
         }
         AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(
-                "onBluetoothA2dpActiveDeviceChange addr=" + address
+                "onBluetoothA2dpDeviceConfigChange addr=" + address
                     + " event=" + BtHelper.a2dpDeviceEventToString(event)));
 
         synchronized (mDevicesLock) {
-            if (mDeviceBroker.hasScheduledA2dpSinkConnectionState(btDevice)) {
+            if (mDeviceBroker.hasScheduledA2dpConnection(btDevice)) {
                 AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(
                         "A2dp config change ignored (scheduled connection change)")
                         .printLog(TAG));
@@ -500,7 +413,7 @@
                     AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address);
             final DeviceInfo di = mConnectedDevices.get(key);
             if (di == null) {
-                Log.e(TAG, "invalid null DeviceInfo in onBluetoothA2dpActiveDeviceChange");
+                Log.e(TAG, "invalid null DeviceInfo in onBluetoothA2dpDeviceConfigChange");
                 mmi.set(MediaMetrics.Property.EARLY_RETURN, "null DeviceInfo").record();
                 return;
             }
@@ -518,7 +431,7 @@
                             // convert index to internal representation in VolumeStreamState
                             a2dpVolume * 10,
                             AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP,
-                            "onBluetoothA2dpActiveDeviceChange");
+                            "onBluetoothA2dpDeviceConfigChange");
                 }
             } else if (event == BtHelper.EVENT_DEVICE_CONFIG_CHANGE) {
                 if (di.mDeviceCodecFormat != a2dpCodec) {
@@ -539,10 +452,9 @@
                 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
-                setBluetoothA2dpDeviceConnectionState(
-                        btDevice, BluetoothA2dp.STATE_DISCONNECTED, BluetoothProfile.A2DP,
-                        false /* suppressNoisyIntent */, musicDevice,
-                        -1 /* a2dpVolume */);
+                setBluetoothActiveDevice(new AudioDeviceBroker.BtDeviceInfo(btDevice,
+                                BluetoothProfile.A2DP, BluetoothProfile.STATE_DISCONNECTED,
+                                musicDevice, AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP));
             } else {
                 AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(
                         "APM handleDeviceConfigChange success for A2DP device addr=" + address
@@ -828,7 +740,7 @@
     }
 
 
-    /*package*/ void disconnectA2dp() {
+    private void disconnectA2dp() {
         synchronized (mDevicesLock) {
             final ArraySet<String> toRemove = new ArraySet<>();
             // Disconnect ALL DEVICE_OUT_BLUETOOTH_A2DP devices
@@ -850,7 +762,7 @@
         }
     }
 
-    /*package*/ void disconnectA2dpSink() {
+    private void disconnectA2dpSink() {
         synchronized (mDevicesLock) {
             final ArraySet<String> toRemove = new ArraySet<>();
             // Disconnect ALL DEVICE_IN_BLUETOOTH_A2DP devices
@@ -865,7 +777,7 @@
         }
     }
 
-    /*package*/ void disconnectHearingAid() {
+    private void disconnectHearingAid() {
         synchronized (mDevicesLock) {
             final ArraySet<String> toRemove = new ArraySet<>();
             // Disconnect ALL DEVICE_OUT_HEARING_AID devices
@@ -887,6 +799,28 @@
         }
     }
 
+    /*package*/ synchronized void onBtProfileDisconnected(int profile) {
+        switch (profile) {
+            case BluetoothProfile.A2DP:
+                disconnectA2dp();
+                break;
+            case BluetoothProfile.A2DP_SINK:
+                disconnectA2dpSink();
+                break;
+            case BluetoothProfile.HEARING_AID:
+                disconnectHearingAid();
+                break;
+            case BluetoothProfile.LE_AUDIO:
+                disconnectLeAudio();
+                break;
+            default:
+                // Not a valid profile to disconnect
+                Log.e(TAG, "onBtProfileDisconnected: Not a valid profile to disconnect "
+                        + BluetoothProfile.getProfileName(profile));
+                break;
+        }
+    }
+
      /*package*/ void disconnectLeAudio() {
         synchronized (mDevicesLock) {
             final ArraySet<String> toRemove = new ArraySet<>();
@@ -934,46 +868,39 @@
     // only public for mocking/spying
     @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
     @VisibleForTesting
-    public void setBluetoothA2dpDeviceConnectionState(
-            @NonNull BluetoothDevice device, @AudioService.BtProfileConnectionState int state,
-            int profile, boolean suppressNoisyIntent, int musicDevice, int a2dpVolume) {
+    public int setBluetoothActiveDevice(@NonNull AudioDeviceBroker.BtDeviceInfo info) {
         int delay;
-        if (profile != BluetoothProfile.A2DP && profile != BluetoothProfile.A2DP_SINK) {
-            throw new IllegalArgumentException("invalid profile " + profile);
-        }
         synchronized (mDevicesLock) {
-            if (profile == BluetoothProfile.A2DP && !suppressNoisyIntent) {
+            if (!info.mSupprNoisy
+                    && ((info.mProfile == BluetoothProfile.LE_AUDIO && info.mIsLeOutput)
+                        || info.mProfile == BluetoothProfile.HEARING_AID
+                        || info.mProfile == BluetoothProfile.A2DP)) {
                 @AudioService.ConnectionState int asState =
-                        (state == BluetoothA2dp.STATE_CONNECTED)
+                        (info.mState == BluetoothProfile.STATE_CONNECTED)
                                 ? AudioService.CONNECTION_STATE_CONNECTED
                                 : AudioService.CONNECTION_STATE_DISCONNECTED;
-                delay = checkSendBecomingNoisyIntentInt(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP,
-                        asState, musicDevice);
+                delay = checkSendBecomingNoisyIntentInt(info.mAudioSystemDevice, asState,
+                        info.mMusicDevice);
             } else {
                 delay = 0;
             }
 
-            final int a2dpCodec = mDeviceBroker.getA2dpCodec(device);
-
             if (AudioService.DEBUG_DEVICES) {
-                Log.i(TAG, "setBluetoothA2dpDeviceConnectionState device: " + device
-                        + " state: " + state + " delay(ms): " + delay
-                        + " codec:" + Integer.toHexString(a2dpCodec)
-                        + " suppressNoisyIntent: " + suppressNoisyIntent);
+                Log.i(TAG, "setBluetoothActiveDevice device: " + info.mDevice
+                        + " profile: " + BluetoothProfile.getProfileName(info.mProfile)
+                        + " state: " + BluetoothProfile.getConnectionStateName(info.mState)
+                        + " delay(ms): " + delay
+                        + " codec:" + Integer.toHexString(info.mCodec)
+                        + " suppressNoisyIntent: " + info.mSupprNoisy);
             }
-
-            final BtHelper.BluetoothA2dpDeviceInfo a2dpDeviceInfo =
-                    new BtHelper.BluetoothA2dpDeviceInfo(device, a2dpVolume, a2dpCodec);
-            if (profile == BluetoothProfile.A2DP) {
-                mDeviceBroker.postA2dpSinkConnection(state,
-                            a2dpDeviceInfo,
-                            delay);
-            } else { //profile == BluetoothProfile.A2DP_SINK
-                mDeviceBroker.postA2dpSourceConnection(state,
-                        a2dpDeviceInfo,
-                        delay);
+            mDeviceBroker.postBluetoothActiveDevice(info, delay);
+            if (info.mProfile == BluetoothProfile.HEARING_AID
+                    && info.mState == BluetoothProfile.STATE_CONNECTED) {
+                mDeviceBroker.setForceUse_Async(AudioSystem.FOR_MEDIA, AudioSystem.FORCE_NONE,
+                                "HEARING_AID set to CONNECTED");
             }
         }
+        return delay;
     }
 
     /*package*/ int setWiredDeviceConnectionState(int type, @AudioService.ConnectionState int state,
@@ -987,50 +914,6 @@
         }
     }
 
-    /*package*/ int  setBluetoothHearingAidDeviceConnectionState(
-            @NonNull BluetoothDevice device, @AudioService.BtProfileConnectionState int state,
-            boolean suppressNoisyIntent, int musicDevice) {
-        int delay;
-        synchronized (mDevicesLock) {
-            if (!suppressNoisyIntent) {
-                int intState = (state == BluetoothHearingAid.STATE_CONNECTED) ? 1 : 0;
-                delay = checkSendBecomingNoisyIntentInt(AudioSystem.DEVICE_OUT_HEARING_AID,
-                        intState, musicDevice);
-            } else {
-                delay = 0;
-            }
-            mDeviceBroker.postSetHearingAidConnectionState(state, device, delay);
-            if (state == BluetoothHearingAid.STATE_CONNECTED) {
-                mDeviceBroker.setForceUse_Async(AudioSystem.FOR_MEDIA, AudioSystem.FORCE_NONE,
-                                "HEARING_AID set to CONNECTED");
-            }
-            return delay;
-        }
-    }
-
-    /*package*/ int setBluetoothLeAudioOutDeviceConnectionState(
-            @NonNull BluetoothDevice device, @AudioService.BtProfileConnectionState int state,
-            boolean suppressNoisyIntent) {
-        synchronized (mDevicesLock) {
-            /* Active device become null and it's previous device is not connected anymore */
-            int delay = 0;
-            if (!suppressNoisyIntent) {
-                int intState = (state == BluetoothLeAudio.STATE_CONNECTED) ? 1 : 0;
-                delay = checkSendBecomingNoisyIntentInt(AudioSystem.DEVICE_OUT_BLE_HEADSET,
-                        intState, AudioSystem.DEVICE_NONE);
-            }
-            mDeviceBroker.postSetLeAudioOutConnectionState(state, device, delay);
-            return delay;
-        }
-    }
-
-    /*package*/ void setBluetoothLeAudioInDeviceConnectionState(
-            @NonNull BluetoothDevice device, @AudioService.BtProfileConnectionState int state) {
-        synchronized (mDevicesLock) {
-            mDeviceBroker.postSetLeAudioInConnectionState(state, device);
-        }
-    }
-
     //-------------------------------------------------------------------
     // Internal utilities
 
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index d75f21c..51784bb 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -83,6 +83,7 @@
 import android.media.AudioRecordingConfiguration;
 import android.media.AudioRoutesInfo;
 import android.media.AudioSystem;
+import android.media.BtProfileConnectionInfo;
 import android.media.IAudioFocusDispatcher;
 import android.media.IAudioModeDispatcher;
 import android.media.IAudioRoutesObserver;
@@ -309,8 +310,8 @@
     private static final int MSG_UPDATE_A11Y_SERVICE_UIDS = 35;
     private static final int MSG_UPDATE_AUDIO_MODE = 36;
     private static final int MSG_RECORDING_CONFIG_CHANGE = 37;
-    private static final int MSG_SET_A2DP_DEV_CONNECTION_STATE = 38;
-    private static final int MSG_A2DP_DEV_CONFIG_CHANGE = 39;
+    private static final int MSG_BT_DEV_CHANGED = 38;
+
     private static final int MSG_DISPATCH_AUDIO_MODE = 40;
 
     // start of messages handled under wakelock
@@ -6266,77 +6267,44 @@
     public @interface BtProfileConnectionState {}
 
     /**
-     * See AudioManager.setBluetoothHearingAidDeviceConnectionState()
+     * @hide
+     * The profiles that can be used with AudioService.handleBluetoothActiveDeviceChanged()
      */
-    public void setBluetoothHearingAidDeviceConnectionState(
-            @NonNull BluetoothDevice device, @BtProfileConnectionState int state,
-            boolean suppressNoisyIntent, int musicDevice)
-    {
-        if (device == null) {
-            throw new IllegalArgumentException("Illegal null device");
-        }
-        if (state != BluetoothProfile.STATE_CONNECTED
-                && state != BluetoothProfile.STATE_DISCONNECTED) {
-            throw new IllegalArgumentException("Illegal BluetoothProfile state for device "
-                    + " (dis)connection, got " + state);
-        }
-        mDeviceBroker.postBluetoothHearingAidDeviceConnectionState(
-                device, state, suppressNoisyIntent, musicDevice, "AudioService");
-    }
+    @IntDef({
+            BluetoothProfile.HEARING_AID,
+            BluetoothProfile.A2DP,
+            BluetoothProfile.A2DP_SINK,
+            BluetoothProfile.LE_AUDIO,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface BtProfile {}
 
-    private void setBluetoothLeAudioDeviceConnectionState(@NonNull BluetoothDevice device,
-            @BtProfileConnectionState int state) {
-        if (device == null) {
-            throw new IllegalArgumentException("Illegal null device");
-        }
-        if (state != BluetoothProfile.STATE_CONNECTED
-                && state != BluetoothProfile.STATE_DISCONNECTED) {
-            throw new IllegalArgumentException("Illegal BluetoothProfile state for device "
-                    + " (dis)connection, got " + state);
-        }
-    }
 
     /**
-     * See AudioManager.setBluetoothLeAudioOutDeviceConnectionState()
+     * See AudioManager.handleBluetoothActiveDeviceChanged(...)
      */
-    public void setBluetoothLeAudioOutDeviceConnectionState(
-            @NonNull BluetoothDevice device, @BtProfileConnectionState int state,
-            boolean suppressNoisyIntent) {
-        setBluetoothLeAudioDeviceConnectionState(device, state);
-        mDeviceBroker.postBluetoothLeAudioOutDeviceConnectionState(device, state,
-                suppressNoisyIntent, "AudioService");
-    }
-
-    /**
-     * See AudioManager.setBluetoothLeAudioInDeviceConnectionState()
-     */
-    public void setBluetoothLeAudioInDeviceConnectionState(
-            @NonNull BluetoothDevice device, @BtProfileConnectionState int state) {
-        setBluetoothLeAudioDeviceConnectionState(device, state);
-        mDeviceBroker.postBluetoothLeAudioInDeviceConnectionState(device, state, "AudioService");
-    }
-
-    /**
-     * See AudioManager.setBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent()
-     */
-    public void setBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(
-            @NonNull BluetoothDevice device, @BtProfileConnectionState int state,
-            int profile, boolean suppressNoisyIntent, int a2dpVolume) {
-        if (device == null) {
-            throw new IllegalArgumentException("Illegal null device");
+    public void handleBluetoothActiveDeviceChanged(BluetoothDevice newDevice,
+            BluetoothDevice previousDevice, @NonNull BtProfileConnectionInfo info) {
+        if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.BLUETOOTH_STACK)
+                != PackageManager.PERMISSION_GRANTED) {
+            throw new SecurityException("Bluetooth is the only caller allowed");
         }
-        if (state != BluetoothProfile.STATE_CONNECTED
-                && state != BluetoothProfile.STATE_DISCONNECTED) {
-            throw new IllegalArgumentException("Illegal BluetoothProfile state for device "
-                    + " (dis)connection, got " + state);
+        if (info == null) {
+            throw new IllegalArgumentException("Illegal null BtProfileConnectionInfo for device "
+                    + previousDevice + " -> " + newDevice);
         }
-
-        AudioDeviceBroker.BtDeviceConnectionInfo info =
-                new AudioDeviceBroker.BtDeviceConnectionInfo(device, state,
-                        profile, suppressNoisyIntent, a2dpVolume);
-        sendMsg(mAudioHandler, MSG_SET_A2DP_DEV_CONNECTION_STATE, SENDMSG_QUEUE,
-                0 /*arg1*/, 0 /*arg2*/,
-                /*obj*/ info, 0 /*delay*/);
+        final int profile = info.getProfile();
+        if (profile != BluetoothProfile.A2DP && profile != BluetoothProfile.A2DP_SINK
+                && profile != BluetoothProfile.LE_AUDIO
+                && profile != BluetoothProfile.HEARING_AID) {
+            throw new IllegalArgumentException("Illegal BluetoothProfile profile for device "
+                    + previousDevice + " -> " + newDevice + ". Got: " + profile);
+        }
+        AudioDeviceBroker.BtDeviceChangedData data =
+                new AudioDeviceBroker.BtDeviceChangedData(newDevice, previousDevice, info,
+                        "AudioService");
+        sendMsg(mAudioHandler, MSG_BT_DEV_CHANGED, SENDMSG_QUEUE, 0, 0,
+                /*obj*/ data, /*delay*/ 0);
     }
 
     /** only public for mocking/spying, do not call outside of AudioService */
@@ -6345,19 +6313,6 @@
         mStreamStates[AudioSystem.STREAM_MUSIC].muteInternally(mute);
     }
 
-    /**
-     * See AudioManager.handleBluetoothA2dpDeviceConfigChange()
-     * @param device
-     */
-    public void handleBluetoothA2dpDeviceConfigChange(BluetoothDevice device)
-    {
-        if (device == null) {
-            throw new IllegalArgumentException("Illegal null device");
-        }
-        sendMsg(mAudioHandler, MSG_A2DP_DEV_CONFIG_CHANGE, SENDMSG_QUEUE, 0, 0,
-                /*obj*/ device, /*delay*/ 0);
-    }
-
     private static final Set<Integer> DEVICE_MEDIA_UNMUTED_ON_PLUG_SET;
     static {
         DEVICE_MEDIA_UNMUTED_ON_PLUG_SET = new HashSet<>();
@@ -7709,13 +7664,9 @@
                     }
                     break;
 
-                case MSG_SET_A2DP_DEV_CONNECTION_STATE:
-                    mDeviceBroker.queueBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(
-                            (AudioDeviceBroker.BtDeviceConnectionInfo) msg.obj);
-                    break;
-
-                case MSG_A2DP_DEV_CONFIG_CHANGE:
-                    mDeviceBroker.postBluetoothA2dpDeviceConfigChange((BluetoothDevice) msg.obj);
+                case MSG_BT_DEV_CHANGED:
+                    mDeviceBroker.queueOnBluetoothActiveDeviceChanged(
+                            (AudioDeviceBroker.BtDeviceChangedData) msg.obj);
                     break;
 
                 case MSG_DISPATCH_AUDIO_MODE:
diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java
index c924fde..9273a5d 100644
--- a/services/core/java/com/android/server/audio/BtHelper.java
+++ b/services/core/java/com/android/server/audio/BtHelper.java
@@ -31,6 +31,7 @@
 import android.media.AudioDeviceAttributes;
 import android.media.AudioManager;
 import android.media.AudioSystem;
+import android.media.BtProfileConnectionInfo;
 import android.os.Binder;
 import android.os.UserHandle;
 import android.provider.Settings;
@@ -451,11 +452,11 @@
     }
 
     /*package*/ synchronized void disconnectAllBluetoothProfiles() {
-        mDeviceBroker.postDisconnectA2dp();
-        mDeviceBroker.postDisconnectA2dpSink();
-        mDeviceBroker.postDisconnectHeadset();
-        mDeviceBroker.postDisconnectHearingAid();
-        mDeviceBroker.postDisconnectLeAudio();
+        mDeviceBroker.postBtProfileDisconnected(BluetoothProfile.A2DP);
+        mDeviceBroker.postBtProfileDisconnected(BluetoothProfile.A2DP_SINK);
+        mDeviceBroker.postBtProfileDisconnected(BluetoothProfile.HEADSET);
+        mDeviceBroker.postBtProfileDisconnected(BluetoothProfile.HEARING_AID);
+        mDeviceBroker.postBtProfileDisconnected(BluetoothProfile.LE_AUDIO);
     }
 
     // @GuardedBy("AudioDeviceBroker.mSetModeLock")
@@ -474,63 +475,32 @@
         mBluetoothHeadset = null;
     }
 
-    /*package*/ synchronized void onA2dpProfileConnected(BluetoothA2dp a2dp) {
-        mA2dp = a2dp;
-        final List<BluetoothDevice> deviceList = mA2dp.getConnectedDevices();
-        if (deviceList.isEmpty()) {
+    /*package*/ synchronized void onBtProfileConnected(int profile, BluetoothProfile proxy) {
+        if (profile == BluetoothProfile.HEADSET) {
+            onHeadsetProfileConnected((BluetoothHeadset) proxy);
             return;
         }
-        final BluetoothDevice btDevice = deviceList.get(0);
-        // the device is guaranteed CONNECTED
-        mDeviceBroker.queueBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(
-                new AudioDeviceBroker.BtDeviceConnectionInfo(btDevice,
-                    BluetoothA2dp.STATE_CONNECTED, BluetoothProfile.A2DP_SINK,
-                        true, -1));
-    }
-
-    /*package*/ synchronized void onA2dpSinkProfileConnected(BluetoothProfile profile) {
-        final List<BluetoothDevice> deviceList = profile.getConnectedDevices();
+        if (profile == BluetoothProfile.A2DP) {
+            mA2dp = (BluetoothA2dp) proxy;
+        } else if (profile == BluetoothProfile.LE_AUDIO) {
+            mLeAudio = (BluetoothLeAudio) proxy;
+        }
+        final List<BluetoothDevice> deviceList = proxy.getConnectedDevices();
         if (deviceList.isEmpty()) {
             return;
         }
         final BluetoothDevice btDevice = deviceList.get(0);
         final @BluetoothProfile.BtProfileState int state =
-                profile.getConnectionState(btDevice);
-        mDeviceBroker.postSetA2dpSourceConnectionState(
-                state, new BluetoothA2dpDeviceInfo(btDevice));
-    }
-
-    /*package*/ synchronized void onHearingAidProfileConnected(BluetoothHearingAid hearingAid) {
-        mHearingAid = hearingAid;
-        final List<BluetoothDevice> deviceList = mHearingAid.getConnectedDevices();
-        if (deviceList.isEmpty()) {
-            return;
+                proxy.getConnectionState(btDevice);
+        if (state == BluetoothProfile.STATE_CONNECTED) {
+            mDeviceBroker.queueOnBluetoothActiveDeviceChanged(
+                    new AudioDeviceBroker.BtDeviceChangedData(btDevice, null,
+                        new BtProfileConnectionInfo(profile), "mBluetoothProfileServiceListener"));
+        } else {
+            mDeviceBroker.queueOnBluetoothActiveDeviceChanged(
+                    new AudioDeviceBroker.BtDeviceChangedData(null, btDevice,
+                        new BtProfileConnectionInfo(profile), "mBluetoothProfileServiceListener"));
         }
-        final BluetoothDevice btDevice = deviceList.get(0);
-        final @BluetoothProfile.BtProfileState int state =
-                mHearingAid.getConnectionState(btDevice);
-        mDeviceBroker.postBluetoothHearingAidDeviceConnectionState(
-                btDevice, state,
-                /*suppressNoisyIntent*/ false,
-                /*musicDevice*/ android.media.AudioSystem.DEVICE_NONE,
-                /*eventSource*/ "mBluetoothProfileServiceListener");
-    }
-
-    /*package*/ synchronized void onLeAudioProfileConnected(BluetoothLeAudio leAudio) {
-        mLeAudio = leAudio;
-        final List<BluetoothDevice> deviceList = mLeAudio.getConnectedDevices();
-        if (deviceList.isEmpty()) {
-            return;
-        }
-
-        final BluetoothDevice btDevice = deviceList.get(0);
-        final @BluetoothProfile.BtProfileState int state =
-        mLeAudio.getConnectionState(btDevice);
-        mDeviceBroker.postBluetoothLeAudioOutDeviceConnectionState(
-                btDevice, state,
-                /*suppressNoisyIntent*/ false,
-                /*musicDevice android.media.AudioSystem.DEVICE_NONE,*/
-                /*eventSource*/ "mBluetoothProfileServiceListener");
     }
 
     // @GuardedBy("AudioDeviceBroker.mSetModeLock")
@@ -677,36 +647,16 @@
                 public void onServiceConnected(int profile, BluetoothProfile proxy) {
                     switch(profile) {
                         case BluetoothProfile.A2DP:
-                            AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(
-                                    "BT profile service: connecting A2DP profile"));
-                            mDeviceBroker.postBtA2dpProfileConnected((BluetoothA2dp) proxy);
-                            break;
-
                         case BluetoothProfile.A2DP_SINK:
-                            AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(
-                                    "BT profile service: connecting A2DP_SINK profile"));
-                            mDeviceBroker.postBtA2dpSinkProfileConnected(proxy);
-                            break;
-
                         case BluetoothProfile.HEADSET:
-                            AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(
-                                    "BT profile service: connecting HEADSET profile"));
-                            mDeviceBroker.postBtHeasetProfileConnected((BluetoothHeadset) proxy);
-                            break;
-
                         case BluetoothProfile.HEARING_AID:
-                            AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(
-                                    "BT profile service: connecting HEARING_AID profile"));
-                            mDeviceBroker.postBtHearingAidProfileConnected(
-                                    (BluetoothHearingAid) proxy);
-                            break;
-
                         case BluetoothProfile.LE_AUDIO:
                             AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(
-                                    "BT profile service: connecting LE_AUDIO profile"));
-                            mDeviceBroker.postBtLeAudioProfileConnected(
-                                    (BluetoothLeAudio) proxy);
+                                    "BT profile service: connecting "
+                                    + BluetoothProfile.getProfileName(profile) + " profile"));
+                            mDeviceBroker.postBtProfileConnected(profile, proxy);
                             break;
+
                         default:
                             break;
                     }
@@ -715,22 +665,11 @@
 
                     switch (profile) {
                         case BluetoothProfile.A2DP:
-                            mDeviceBroker.postDisconnectA2dp();
-                            break;
-
                         case BluetoothProfile.A2DP_SINK:
-                            mDeviceBroker.postDisconnectA2dpSink();
-                            break;
-
                         case BluetoothProfile.HEADSET:
-                            mDeviceBroker.postDisconnectHeadset();
-                            break;
-
                         case BluetoothProfile.HEARING_AID:
-                            mDeviceBroker.postDisconnectHearingAid();
-                            break;
                         case BluetoothProfile.LE_AUDIO:
-                            mDeviceBroker.postDisconnectLeAudio();
+                            mDeviceBroker.postBtProfileDisconnected(profile);
                             break;
 
                         default:
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 1a0a885..20bcc5e 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -21917,8 +21917,10 @@
             ApexManager.ActiveApexInfo apexInfo) {
         for (int i = 0, size = SYSTEM_PARTITIONS.size(); i < size; i++) {
             ScanPartition sp = SYSTEM_PARTITIONS.get(i);
-            if (apexInfo.preInstalledApexPath.getAbsolutePath().startsWith(
-                    sp.getFolder().getAbsolutePath() + File.separator)) {
+            if (apexInfo.preInstalledApexPath.getAbsolutePath().equals(
+                    sp.getFolder().getAbsolutePath())
+                    || apexInfo.preInstalledApexPath.getAbsolutePath().startsWith(
+                        sp.getFolder().getAbsolutePath() + File.separator)) {
                 return new ScanPartition(apexInfo.apexDirectory, sp, SCAN_AS_APK_IN_APEX);
             }
         }
diff --git a/services/core/java/com/android/server/timezonedetector/ReferenceWithHistory.java b/services/core/java/com/android/server/timezonedetector/ReferenceWithHistory.java
index 4eb1b99..60068cb 100644
--- a/services/core/java/com/android/server/timezonedetector/ReferenceWithHistory.java
+++ b/services/core/java/com/android/server/timezonedetector/ReferenceWithHistory.java
@@ -25,6 +25,7 @@
 
 import java.time.Duration;
 import java.util.ArrayDeque;
+import java.util.Iterator;
 
 /**
  * A class that behaves like the following definition, except it stores the history of values set
@@ -112,9 +113,11 @@
         if (mValues == null) {
             ipw.println("{Empty}");
         } else {
-            int i = mSetCount;
-            for (TimestampedValue<V> valueHolder : mValues) {
-                ipw.print(--i);
+            int i = mSetCount - mValues.size();
+            Iterator<TimestampedValue<V>> reverseIterator = mValues.descendingIterator();
+            while (reverseIterator.hasNext()) {
+                TimestampedValue<V> valueHolder = reverseIterator.next();
+                ipw.print(i++);
                 ipw.print("@");
                 ipw.print(Duration.ofMillis(valueHolder.getReferenceTimeMillis()).toString());
                 ipw.print(": ");
diff --git a/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java b/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java
index 4fb801e..8369319 100644
--- a/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java
+++ b/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java
@@ -35,6 +35,7 @@
 import android.util.Log;
 
 import com.android.internal.R;
+import com.android.internal.os.BackgroundThread;
 import com.android.server.IoThread;
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
@@ -146,7 +147,7 @@
                     connectNativeService();
                     break;
                 default:
-                    throw new AssertionError("Unknown message: " + message.toString());
+                    throw new AssertionError("Unknown message: " + message);
             }
         }
     }
@@ -190,11 +191,14 @@
                 Log.d(LOG_TAG, "Starting background process job");
             }
 
-            try {
-                sSelfService.mIProfcollect.process(false);
-            } catch (RemoteException e) {
-                Log.e(LOG_TAG, e.getMessage());
-            }
+            BackgroundThread.get().getThreadHandler().post(
+                    () -> {
+                        try {
+                            sSelfService.mIProfcollect.process();
+                        } catch (RemoteException e) {
+                            Log.e(LOG_TAG, e.getMessage());
+                        }
+                    });
             return true;
         }
 
@@ -301,7 +305,7 @@
         }
 
         Context context = getContext();
-        new Thread(() -> {
+        BackgroundThread.get().getThreadHandler().post(() -> {
             try {
                 // Prepare profile report
                 String reportName = mIProfcollect.report() + ".zip";
@@ -321,6 +325,6 @@
             } catch (RemoteException e) {
                 Log.e(LOG_TAG, e.getMessage());
             }
-        }).start();
+        });
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java
index 5c53d43..9e1445c 100644
--- a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java
@@ -26,11 +26,11 @@
 
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothProfile;
 import android.content.Context;
 import android.content.Intent;
 import android.media.AudioManager;
 import android.media.AudioSystem;
+import android.media.BtProfileConnectionInfo;
 import android.util.Log;
 
 import androidx.test.InstrumentationRegistry;
@@ -98,16 +98,12 @@
         Log.i(TAG, "starting testPostA2dpDeviceConnectionChange");
         Assert.assertNotNull("invalid null BT device", mFakeBtDevice);
 
-        mAudioDeviceBroker.queueBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(
-                new AudioDeviceBroker.BtDeviceConnectionInfo(mFakeBtDevice,
-                        BluetoothProfile.STATE_CONNECTED, BluetoothProfile.A2DP, true, 1));
+        mAudioDeviceBroker.queueOnBluetoothActiveDeviceChanged(
+                new AudioDeviceBroker.BtDeviceChangedData(mFakeBtDevice, null,
+                    BtProfileConnectionInfo.a2dpInfo(true, 1), "testSource"));
         Thread.sleep(2 * MAX_MESSAGE_HANDLING_DELAY_MS);
-        verify(mSpyDevInventory, times(1)).setBluetoothA2dpDeviceConnectionState(
-                any(BluetoothDevice.class),
-                ArgumentMatchers.eq(BluetoothProfile.STATE_CONNECTED) /*state*/,
-                ArgumentMatchers.eq(BluetoothProfile.A2DP) /*profile*/,
-                ArgumentMatchers.eq(true) /*suppressNoisyIntent*/, anyInt() /*musicDevice*/,
-                ArgumentMatchers.eq(1) /*a2dpVolume*/
+        verify(mSpyDevInventory, times(1)).setBluetoothActiveDevice(
+                any(AudioDeviceBroker.BtDeviceInfo.class)
         );
 
         // verify the connection was reported to AudioSystem
@@ -210,30 +206,29 @@
         ((NoOpAudioSystemAdapter) mSpyAudioSystem).configureIsStreamActive(mockMediaPlayback);
 
         // first connection: ensure the device is connected as a starting condition for the test
-        mAudioDeviceBroker.queueBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(
-                new AudioDeviceBroker.BtDeviceConnectionInfo(mFakeBtDevice,
-                        BluetoothProfile.STATE_CONNECTED, BluetoothProfile.A2DP, true, 1));
+        mAudioDeviceBroker.queueOnBluetoothActiveDeviceChanged(
+                new AudioDeviceBroker.BtDeviceChangedData(mFakeBtDevice, null,
+                    BtProfileConnectionInfo.a2dpInfo(true, 1), "testSource"));
         Thread.sleep(MAX_MESSAGE_HANDLING_DELAY_MS);
 
         // disconnection
-        mAudioDeviceBroker.queueBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(
-                new AudioDeviceBroker.BtDeviceConnectionInfo(mFakeBtDevice,
-                        BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.A2DP, false, -1));
+        mAudioDeviceBroker.queueOnBluetoothActiveDeviceChanged(
+                new AudioDeviceBroker.BtDeviceChangedData(null, mFakeBtDevice,
+                    BtProfileConnectionInfo.a2dpInfo(false, -1), "testSource"));
         if (delayAfterDisconnection > 0) {
             Thread.sleep(delayAfterDisconnection);
         }
 
         // reconnection
-        mAudioDeviceBroker.queueBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(
-                new AudioDeviceBroker.BtDeviceConnectionInfo(mFakeBtDevice,
-                        BluetoothProfile.STATE_CONNECTED, BluetoothProfile.A2DP, true, 2));
+        mAudioDeviceBroker.queueOnBluetoothActiveDeviceChanged(
+                new AudioDeviceBroker.BtDeviceChangedData(mFakeBtDevice, null,
+                    BtProfileConnectionInfo.a2dpInfo(true, 2), "testSource"));
         Thread.sleep(AudioService.BECOMING_NOISY_DELAY_MS + MAX_MESSAGE_HANDLING_DELAY_MS);
 
         // Verify disconnection has been cancelled and we're seeing two connections attempts,
         // with the device connected at the end of the test
-        verify(mSpyDevInventory, times(2)).onSetA2dpSinkConnectionState(
-                any(BtHelper.BluetoothA2dpDeviceInfo.class),
-                ArgumentMatchers.eq(BluetoothProfile.STATE_CONNECTED));
+        verify(mSpyDevInventory, times(2)).onSetBtActiveDevice(
+                any(AudioDeviceBroker.BtDeviceInfo.class), anyInt());
         Assert.assertTrue("Mock device not connected",
                 mSpyDevInventory.isA2dpDeviceConnected(mFakeBtDevice));
 
diff --git a/services/tests/servicestests/src/com/android/server/timedetector/ReferenceWithHistoryTest.java b/services/tests/servicestests/src/com/android/server/timedetector/ReferenceWithHistoryTest.java
index ce72499..d5d2cbd 100644
--- a/services/tests/servicestests/src/com/android/server/timedetector/ReferenceWithHistoryTest.java
+++ b/services/tests/servicestests/src/com/android/server/timedetector/ReferenceWithHistoryTest.java
@@ -17,17 +17,19 @@
 package com.android.server.timedetector;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.util.IndentingPrintWriter;
 
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.internal.util.IndentingPrintWriter;
 import com.android.server.timezonedetector.ReferenceWithHistory;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 import java.io.StringWriter;
+import java.util.Arrays;
 
 @RunWith(AndroidJUnit4.class)
 public class ReferenceWithHistoryTest {
@@ -41,31 +43,34 @@
 
         // Check unset behavior.
         compareGet(referenceWithHistory, reference, null);
-        assertNotNull(dumpReferenceWithHistory(referenceWithHistory));
+        assertDumpContent(referenceWithHistory);
         compareToString(referenceWithHistory, reference, "null");
 
         // Try setting null.
         setAndCompareReturnValue(referenceWithHistory, reference, null);
         compareGet(referenceWithHistory, reference, null);
-        assertNotNull(dumpReferenceWithHistory(referenceWithHistory));
+        assertDumpContent(referenceWithHistory, new DumpLine(0, "null"));
         compareToString(referenceWithHistory, reference, "null");
 
         // Try setting a non-null value.
         setAndCompareReturnValue(referenceWithHistory, reference, "Foo");
         compareGet(referenceWithHistory, reference, "Foo");
-        assertNotNull(dumpReferenceWithHistory(referenceWithHistory));
+        assertDumpContent(referenceWithHistory,
+                new DumpLine(0, "null"), new DumpLine(1, "Foo"));
         compareToString(referenceWithHistory, reference, "Foo");
 
         // Try setting null again.
-        setAndCompareReturnValue(referenceWithHistory, reference, "Foo");
-        compareGet(referenceWithHistory, reference, "Foo");
-        assertNotNull(dumpReferenceWithHistory(referenceWithHistory));
-        compareToString(referenceWithHistory, reference, "Foo");
+        setAndCompareReturnValue(referenceWithHistory, reference, null);
+        compareGet(referenceWithHistory, reference, null);
+        assertDumpContent(referenceWithHistory,
+                new DumpLine(1, "Foo"), new DumpLine(2, "null"));
+        compareToString(referenceWithHistory, reference, "null");
 
         // Try a non-null value again.
         setAndCompareReturnValue(referenceWithHistory, reference, "Bar");
         compareGet(referenceWithHistory, reference, "Bar");
-        assertNotNull(dumpReferenceWithHistory(referenceWithHistory));
+        assertDumpContent(referenceWithHistory,
+                new DumpLine(2, "null"), new DumpLine(3, "Bar"));
         compareToString(referenceWithHistory, reference, "Bar");
     }
 
@@ -132,11 +137,54 @@
         assertEquals(expected, referenceWithHistory.toString());
     }
 
-    private static String dumpReferenceWithHistory(ReferenceWithHistory<?> referenceWithHistory) {
+    private static void assertDumpContent(
+            ReferenceWithHistory<?> referenceWithHistory, DumpLine... expectedLines) {
+        String[] actualLines = dumpReferenceWithHistory(referenceWithHistory);
+
+        if (expectedLines.length == 0) {
+            String expectedEmptyOutput = "{Empty}";
+            assertEquals(expectedEmptyOutput, 1, actualLines.length);
+            assertEquals(expectedEmptyOutput, actualLines[0]);
+        } else {
+            assertEquals("Expected=" + Arrays.toString(expectedLines)
+                            + ", actual=" + Arrays.toString(actualLines),
+                    expectedLines.length, actualLines.length);
+            for (int i = 0; i < expectedLines.length; i++) {
+                DumpLine expectedLine = expectedLines[i];
+                String actualLine = actualLines[i];
+                assertTrue("i=" + i + ", expected=" + expectedLine + ", actual=" + actualLine,
+                        actualLine.startsWith(Integer.toString(expectedLine.mIndex)));
+                assertTrue("i=" + i + ", expected=" + expectedLine + ", actual=" + actualLine,
+                        actualLine.endsWith(expectedLine.mLine));
+            }
+        }
+    }
+
+    private static String[] dumpReferenceWithHistory(ReferenceWithHistory<?> referenceWithHistory) {
         StringWriter stringWriter = new StringWriter();
         try (IndentingPrintWriter ipw = new IndentingPrintWriter(stringWriter, " ")) {
             referenceWithHistory.dump(ipw);
-            return stringWriter.toString();
+            return stringWriter.toString().split("\n");
+        }
+    }
+
+    /** An expected line of {@link ReferenceWithHistory#dump} output. */
+    private static class DumpLine {
+
+        final int mIndex;
+        final String mLine;
+
+        DumpLine(int index, String line) {
+            mIndex = index;
+            mLine = line;
+        }
+
+        @Override
+        public String toString() {
+            return "DumpLine{"
+                    + "mIndex=" + mIndex
+                    + ", mLine='" + mLine + '\''
+                    + '}';
         }
     }
 }
diff --git a/telephony/java/android/telephony/ImsManager.java b/telephony/java/android/telephony/ImsManager.java
index 42d7707..fc76f99 100644
--- a/telephony/java/android/telephony/ImsManager.java
+++ b/telephony/java/android/telephony/ImsManager.java
@@ -119,7 +119,7 @@
             throw new IllegalArgumentException("Invalid subscription ID: " + subscriptionId);
         }
 
-        return new ImsRcsManager(mContext, subscriptionId, sRcsCache);
+        return new ImsRcsManager(mContext, subscriptionId, sRcsCache, sTelephonyCache);
     }
 
     /**
@@ -135,7 +135,7 @@
             throw new IllegalArgumentException("Invalid subscription ID: " + subscriptionId);
         }
 
-        return new ImsMmTelManager(subscriptionId, sTelephonyCache);
+        return new ImsMmTelManager(mContext, subscriptionId, sTelephonyCache);
     }
 
     /**
@@ -157,7 +157,7 @@
             throw new IllegalArgumentException("Invalid subscription ID: " + subscriptionId);
         }
 
-        return new SipDelegateManager(mContext, subscriptionId, sRcsCache);
+        return new SipDelegateManager(mContext, subscriptionId, sRcsCache, sTelephonyCache);
     }
 
     private static IImsRcsController getIImsRcsControllerInterface() {
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 6ffdc6b..a1d68b2 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -315,6 +315,12 @@
      */
     public static final int UNINITIALIZED_CARD_ID = -2;
 
+    /**
+     * Default port index for the UICC Card
+     * @hide
+     */
+    public static final int DEFAULT_PORT_INDEX = 0;
+
     private final Context mContext;
     private final int mSubId;
     @UnsupportedAppUsage
diff --git a/telephony/java/android/telephony/ims/ImsMmTelManager.java b/telephony/java/android/telephony/ims/ImsMmTelManager.java
index 36082dc..683bb92 100644
--- a/telephony/java/android/telephony/ims/ImsMmTelManager.java
+++ b/telephony/java/android/telephony/ims/ImsMmTelManager.java
@@ -25,6 +25,7 @@
 import android.annotation.SuppressAutoDoc;
 import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
+import android.content.Context;
 import android.os.Binder;
 import android.os.RemoteException;
 import android.os.ServiceSpecificException;
@@ -45,6 +46,7 @@
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
 import java.util.concurrent.Executor;
 import java.util.function.Consumer;
 
@@ -214,6 +216,7 @@
         }
     }
 
+    private final Context mContext;
     private final int mSubId;
     private final BinderCacheManager<ITelephony> mBinderCache;
 
@@ -255,6 +258,16 @@
      */
     @VisibleForTesting
     public ImsMmTelManager(int subId, BinderCacheManager<ITelephony> binderCache) {
+        this(null, subId, binderCache);
+    }
+
+    /**
+     * Only visible for testing, use {@link ImsManager#getImsMmTelManager(int)} instead.
+     * @hide
+     */
+    @VisibleForTesting
+    public ImsMmTelManager(Context context, int subId, BinderCacheManager<ITelephony> binderCache) {
+        mContext = context;
         mSubId = subId;
         mBinderCache = binderCache;
     }
@@ -1482,6 +1495,74 @@
         }
     }
 
+    /**
+     * Register a new callback, which is used to notify the registrant of changes to
+     * the state of the underlying IMS service that is attached to telephony to
+     * implement IMS functionality. If the manager is created for
+     * the {@link SubscriptionManager#DEFAULT_SUBSCRIPTION_ID},
+     * this throws an {@link ImsException}.
+     *
+     * <p>Requires Permission:
+     * {@link android.Manifest.permission#READ_PRECISE_PHONE_STATE READ_PRECISE_PHONE_STATE}
+     * or that the calling app has carrier privileges
+     * (see {@link android.telephony.TelephonyManager#hasCarrierPrivileges}).
+     *
+     * @param executor the Executor that will be used to call the {@link ImsStateCallback}.
+     * @param callback The callback instance being registered.
+     * @throws ImsException in the case that the callback can not be registered.
+     * See {@link ImsException#getCode} for more information on when this is called.
+     */
+    @RequiresPermission(anyOf = {Manifest.permission.READ_PRECISE_PHONE_STATE,
+            Manifest.permission.READ_PRIVILEGED_PHONE_STATE})
+    public void registerImsStateCallback(@NonNull Executor executor,
+            @NonNull ImsStateCallback callback) throws ImsException {
+        Objects.requireNonNull(callback, "Must include a non-null ImsStateCallback.");
+        Objects.requireNonNull(executor, "Must include a non-null Executor.");
+
+        callback.init(executor);
+        ITelephony telephony = mBinderCache.listenOnBinder(callback, callback::binderDied);
+        if (telephony == null) {
+            throw new ImsException("Telephony server is down",
+                    ImsException.CODE_ERROR_SERVICE_UNAVAILABLE);
+        }
+
+        try {
+            telephony.registerImsStateCallback(
+                    mSubId, ImsFeature.FEATURE_MMTEL,
+                    callback.getCallbackBinder(), getOpPackageName());
+        } catch (ServiceSpecificException e) {
+            throw new ImsException(e.getMessage(), e.errorCode);
+        } catch (RemoteException | IllegalStateException e) {
+            throw new ImsException(e.getMessage(), ImsException.CODE_ERROR_SERVICE_UNAVAILABLE);
+        }
+    }
+
+    /**
+     * Unregisters a previously registered callback.
+     *
+     * @param callback The callback instance to be unregistered.
+     */
+    public void unregisterImsStateCallback(@NonNull ImsStateCallback callback) {
+        Objects.requireNonNull(callback, "Must include a non-null ImsStateCallback.");
+
+        ITelephony telephony = mBinderCache.removeRunnable(callback);
+        try {
+            if (telephony != null) {
+                telephony.unregisterImsStateCallback(callback.getCallbackBinder());
+            }
+        } catch (RemoteException ignore) {
+            // ignore it
+        }
+    }
+
+    private String getOpPackageName() {
+        if (mContext != null) {
+            return mContext.getOpPackageName();
+        } else {
+            return null;
+        }
+    }
+
     private ITelephony getITelephony() {
         return mBinderCache.getBinder();
     }
diff --git a/telephony/java/android/telephony/ims/ImsRcsManager.java b/telephony/java/android/telephony/ims/ImsRcsManager.java
index 8d6fa41..1b047c7 100644
--- a/telephony/java/android/telephony/ims/ImsRcsManager.java
+++ b/telephony/java/android/telephony/ims/ImsRcsManager.java
@@ -39,9 +39,11 @@
 import android.util.Log;
 
 import com.android.internal.telephony.IIntegerConsumer;
+import com.android.internal.telephony.ITelephony;
 
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Objects;
 import java.util.concurrent.Executor;
 import java.util.function.Consumer;
 
@@ -159,6 +161,7 @@
     private final int mSubId;
     private final Context mContext;
     private final BinderCacheManager<IImsRcsController> mBinderCache;
+    private final BinderCacheManager<ITelephony> mTelephonyBinderCache;
     private final Map<OnAvailabilityChangedListener, AvailabilityCallbackAdapter>
             mAvailabilityChangedCallbacks;
 
@@ -167,11 +170,13 @@
      * @hide
      */
     public ImsRcsManager(Context context, int subId,
-            BinderCacheManager<IImsRcsController> binderCache) {
+            BinderCacheManager<IImsRcsController> binderCache,
+            BinderCacheManager<ITelephony> telephonyBinderCache) {
         mSubId = subId;
         mContext = context;
         mBinderCache = binderCache;
         mAvailabilityChangedCallbacks = new HashMap<>();
+        mTelephonyBinderCache = telephonyBinderCache;
     }
 
     /**
@@ -534,6 +539,67 @@
     }
 
     /**
+     * Register a new callback, which is used to notify the registrant of changes to
+     * the state of the underlying IMS service that is attached to telephony to
+     * implement IMS functionality. If the manager is created for
+     * the {@link android.telephony.SubscriptionManager#DEFAULT_SUBSCRIPTION_ID},
+     * this throws an {@link ImsException}.
+     *
+     * <p>Requires Permission:
+     * {@link android.Manifest.permission#READ_PRECISE_PHONE_STATE READ_PRECISE_PHONE_STATE}
+     * or that the calling app has carrier privileges
+     * (see {@link android.telephony.TelephonyManager#hasCarrierPrivileges}).
+     *
+     * @param executor the Executor that will be used to call the {@link ImsStateCallback}.
+     * @param callback The callback instance being registered.
+     * @throws ImsException in the case that the callback can not be registered.
+     * See {@link ImsException#getCode} for more information on when this is called.
+     */
+    @RequiresPermission(anyOf = {Manifest.permission.READ_PRECISE_PHONE_STATE,
+            Manifest.permission.READ_PRIVILEGED_PHONE_STATE,
+            Manifest.permission.ACCESS_RCS_USER_CAPABILITY_EXCHANGE})
+    public void registerImsStateCallback(@NonNull Executor executor,
+            @NonNull ImsStateCallback callback) throws ImsException {
+        Objects.requireNonNull(callback, "Must include a non-null ImsStateCallback.");
+        Objects.requireNonNull(executor, "Must include a non-null Executor.");
+
+        callback.init(executor);
+        ITelephony telephony = mTelephonyBinderCache.listenOnBinder(callback, callback::binderDied);
+        if (telephony == null) {
+            throw new ImsException("Telephony server is down",
+                    ImsException.CODE_ERROR_SERVICE_UNAVAILABLE);
+        }
+
+        try {
+            telephony.registerImsStateCallback(
+                    mSubId, ImsFeature.FEATURE_RCS,
+                    callback.getCallbackBinder(), mContext.getOpPackageName());
+        } catch (ServiceSpecificException e) {
+            throw new ImsException(e.getMessage(), e.errorCode);
+        } catch (RemoteException | IllegalStateException e) {
+            throw new ImsException(e.getMessage(), ImsException.CODE_ERROR_SERVICE_UNAVAILABLE);
+        }
+    }
+
+    /**
+     * Unregisters a previously registered callback.
+     *
+     * @param callback The callback instance to be unregistered.
+     */
+    public void unregisterImsStateCallback(@NonNull ImsStateCallback callback) {
+        Objects.requireNonNull(callback, "Must include a non-null ImsStateCallback.");
+
+        ITelephony telephony = mTelephonyBinderCache.removeRunnable(callback);
+        try {
+            if (telephony != null) {
+                telephony.unregisterImsStateCallback(callback.getCallbackBinder());
+            }
+        } catch (RemoteException ignore) {
+            // ignore it
+        }
+    }
+
+    /**
      * Add the {@link OnAvailabilityChangedListener} to collection for tracking.
      * @param executor The executor that will be used when the publish state is changed and the
      * {@link OnAvailabilityChangedListener} is called.
diff --git a/telephony/java/android/telephony/ims/ImsStateCallback.java b/telephony/java/android/telephony/ims/ImsStateCallback.java
new file mode 100644
index 0000000..b9ba93f
--- /dev/null
+++ b/telephony/java/android/telephony/ims/ImsStateCallback.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2021 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 android.telephony.ims;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.os.Binder;
+
+import com.android.internal.telephony.IImsStateCallback;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.ref.WeakReference;
+import java.util.concurrent.Executor;
+
+/**
+ * A callback class used for monitoring changes in IMS service connection states
+ * for a specific subscription.
+ * <p>
+ * @see ImsMmTelManager#registerImsStateCallback(Executor, ImsStateCallback)
+ * @see ImsRcsManager#registerImsStateCallback(Executor, ImsStateCallback)
+ */
+public abstract class ImsStateCallback {
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = "REASON_", value = {
+            REASON_UNKNOWN_TEMPORARY_ERROR,
+            REASON_UNKNOWN_PERMANENT_ERROR,
+            REASON_IMS_SERVICE_DISCONNECTED,
+            REASON_NO_IMS_SERVICE_CONFIGURED,
+            REASON_SUBSCRIPTION_INACTIVE,
+            REASON_IMS_SERVICE_NOT_READY
+    })
+    public @interface DisconnectedReason {}
+
+    /**
+     * The underlying IMS service is temporarily unavailable for the
+     * associated subscription.
+     * {@link #onAvailable} will be called when the IMS service becomes
+     * available again.
+     */
+    public static final int REASON_UNKNOWN_TEMPORARY_ERROR     = 1;
+
+    /**
+     * The underlying IMS service is permanently unavailable for the
+     * associated subscription and there will be no Manager available for
+     * this subscription.
+     */
+    public static final int REASON_UNKNOWN_PERMANENT_ERROR     = 2;
+
+    /**
+     * The underlying IMS service has died, is reconfiguring, or has never
+     * come up yet and as a result is currently unavailable.
+     * {@link #onAvailable} will be called when the IMS service becomes
+     * available. All callbacks should be unregistered now and registered again
+     * if the IMS service moves back to available.
+     */
+    public static final int REASON_IMS_SERVICE_DISCONNECTED    = 3;
+
+    /**
+     * There is no IMS service configured for the subscription ID specified.
+     * This is a permanent error and there will be no Manager available for
+     * this subscription.
+     */
+    public static final int REASON_NO_IMS_SERVICE_CONFIGURED   = 4;
+
+    /**
+     * The subscription associated with this Manager has moved to an inactive
+     * state (e.g. SIM removed) and the IMS service has torn down the resources
+     * related to this subscription. This has caused this callback
+     * to be deregistered. The callback must be re-registered when this subscription
+     * becomes active in order to continue listening to the IMS service state.
+     */
+    public static final int REASON_SUBSCRIPTION_INACTIVE       = 5;
+
+    /**
+     * The IMS service is connected, but in a NOT_READY state. Once the
+     * service moves to ready, {@link #onAvailable} will be called.
+     */
+    public static final int REASON_IMS_SERVICE_NOT_READY       = 6;
+
+    private IImsStateCallbackStub mCallback;
+
+    /**
+     * @hide
+     */
+    public void init(@NonNull @CallbackExecutor Executor executor) {
+        if (executor == null) {
+            throw new IllegalArgumentException("ImsStateCallback Executor must be non-null");
+        }
+        mCallback = new IImsStateCallbackStub(this, executor);
+    }
+
+    /**
+     * Using a static class and weak reference here to avoid memory leak caused by the
+     * IImsStateCallback.Stub callback retaining references to the outside ImsStateCallback.
+     */
+    private static class IImsStateCallbackStub extends IImsStateCallback.Stub {
+        private WeakReference<ImsStateCallback> mImsStateCallbackWeakRef;
+        private Executor mExecutor;
+
+        IImsStateCallbackStub(ImsStateCallback imsStateCallback, Executor executor) {
+            mImsStateCallbackWeakRef = new WeakReference<ImsStateCallback>(imsStateCallback);
+            mExecutor = executor;
+        }
+
+        Executor getExecutor() {
+            return mExecutor;
+        }
+
+        public void onAvailable() {
+            ImsStateCallback callback = mImsStateCallbackWeakRef.get();
+            if (callback == null) return;
+
+            Binder.withCleanCallingIdentity(
+                    () -> mExecutor.execute(() -> callback.onAvailable()));
+        }
+
+        public void onUnavailable(int reason) {
+            ImsStateCallback callback = mImsStateCallbackWeakRef.get();
+            if (callback == null) return;
+
+            Binder.withCleanCallingIdentity(
+                    () -> mExecutor.execute(() -> callback.onUnavailable(reason)));
+        }
+    }
+
+    /**
+     * The IMS service has disconnected or is reporting NOT_READY and is no longer
+     * available to users. The user should clean up all related state and
+     * unregister callbacks. If it is a temporary error, {@link #onAvailable} will
+     * be called when the IMS service becomes available again.
+     *
+     * @param reason the specified reason
+     */
+    public abstract void onUnavailable(@DisconnectedReason int reason);
+
+    /**
+     * The IMS service is connected and is ready for communication over the
+     * provided Manager.
+     */
+    public abstract void onAvailable();
+
+    /**
+     * An unexpected error has occurred and the Telephony process has crashed. This
+     * has caused this callback to be deregistered. The callback must be
+     * re-registered in order to continue listening to the IMS service state.
+     */
+    public abstract void onError();
+
+    /**
+     * The callback to notify the death of telephony process
+     * @hide
+     */
+    public final void binderDied() {
+        if (mCallback != null) {
+            mCallback.getExecutor().execute(() -> onError());
+        }
+    }
+
+    /**
+     * Return the callback binder
+     * @hide
+     */
+    public IImsStateCallbackStub getCallbackBinder() {
+        return mCallback;
+    }
+}
diff --git a/telephony/java/android/telephony/ims/SipDelegateManager.java b/telephony/java/android/telephony/ims/SipDelegateManager.java
index 5a80663..f913df5 100644
--- a/telephony/java/android/telephony/ims/SipDelegateManager.java
+++ b/telephony/java/android/telephony/ims/SipDelegateManager.java
@@ -28,15 +28,16 @@
 import android.os.RemoteException;
 import android.os.ServiceSpecificException;
 import android.telephony.BinderCacheManager;
-import android.telephony.CarrierConfigManager;
 import android.telephony.ims.aidl.IImsRcsController;
 import android.telephony.ims.aidl.SipDelegateConnectionAidlWrapper;
+import android.telephony.ims.feature.ImsFeature;
 import android.telephony.ims.stub.DelegateConnectionMessageCallback;
 import android.telephony.ims.stub.DelegateConnectionStateCallback;
 import android.telephony.ims.stub.SipDelegate;
 import android.util.ArrayMap;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.ITelephony;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -282,6 +283,7 @@
     private final Context mContext;
     private final int mSubId;
     private final BinderCacheManager<IImsRcsController> mBinderCache;
+    private final BinderCacheManager<ITelephony> mTelephonyBinderCache;
 
     /**
      * Only visible for testing. To instantiate an instance of this class, please use
@@ -290,10 +292,12 @@
      */
     @VisibleForTesting
     public SipDelegateManager(Context context, int subId,
-            BinderCacheManager<IImsRcsController> binderCache) {
+            BinderCacheManager<IImsRcsController> binderCache,
+            BinderCacheManager<ITelephony> telephonyBinderCache) {
         mContext = context;
         mSubId = subId;
         mBinderCache = binderCache;
+        mTelephonyBinderCache = telephonyBinderCache;
     }
 
     /**
@@ -446,4 +450,65 @@
                     + " into this method");
         }
     }
+
+    /**
+     * Register a new callback, which is used to notify the registrant of changes to
+     * the state of the underlying  IMS service that is attached to telephony to
+     * implement IMS functionality. If the manager is created for
+     * the {@link android.telephony.SubscriptionManager#DEFAULT_SUBSCRIPTION_ID},
+     * this throws an {@link ImsException}.
+     *
+     * <p>Requires Permission:
+     * {@link android.Manifest.permission#READ_PRECISE_PHONE_STATE READ_PRECISE_PHONE_STATE}
+     * or that the calling app has carrier privileges
+     * (see {@link android.telephony.TelephonyManager#hasCarrierPrivileges}).
+     *
+     * @param executor the Executor that will be used to call the {@link ImsStateCallback}.
+     * @param callback The callback instance being registered.
+     * @throws ImsException in the case that the callback can not be registered.
+     * See {@link ImsException#getCode} for more information on when this is called.
+     */
+    @RequiresPermission(anyOf = {Manifest.permission.READ_PRIVILEGED_PHONE_STATE,
+            Manifest.permission.PERFORM_IMS_SINGLE_REGISTRATION})
+    public void registerImsStateCallback(@NonNull Executor executor,
+            @NonNull ImsStateCallback callback) throws ImsException {
+        Objects.requireNonNull(callback, "Must include a non-null ImsStateCallback.");
+        Objects.requireNonNull(executor, "Must include a non-null Executor.");
+
+        callback.init(executor);
+        ITelephony telephony = mTelephonyBinderCache.listenOnBinder(callback, callback::binderDied);
+        if (telephony == null) {
+            throw new ImsException("Telephony server is down",
+                    ImsException.CODE_ERROR_SERVICE_UNAVAILABLE);
+        }
+
+        try {
+            telephony.registerImsStateCallback(
+                    mSubId, ImsFeature.FEATURE_RCS,
+                    callback.getCallbackBinder(), mContext.getOpPackageName());
+        } catch (ServiceSpecificException e) {
+            throw new ImsException(e.getMessage(), e.errorCode);
+        } catch (RemoteException | IllegalStateException e) {
+            throw new ImsException(e.getMessage(), ImsException.CODE_ERROR_SERVICE_UNAVAILABLE);
+        }
+    }
+
+    /**
+     * Unregisters a previously registered callback.
+     *
+     * @param callback The callback instance to be unregistered.
+     */
+    public void unregisterImsStateCallback(@NonNull ImsStateCallback callback) {
+        Objects.requireNonNull(callback, "Must include a non-null ImsStateCallback.");
+
+        ITelephony telephony = mTelephonyBinderCache.removeRunnable(callback);
+
+        try {
+            if (telephony != null) {
+                telephony.unregisterImsStateCallback(callback.getCallbackBinder());
+            }
+        } catch (RemoteException ignore) {
+            // ignore it
+        }
+    }
 }
diff --git a/telephony/java/com/android/internal/telephony/IImsStateCallback.aidl b/telephony/java/com/android/internal/telephony/IImsStateCallback.aidl
new file mode 100644
index 0000000..e04b01d
--- /dev/null
+++ b/telephony/java/com/android/internal/telephony/IImsStateCallback.aidl
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2021 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.internal.telephony;
+
+oneway interface IImsStateCallback {
+    void onUnavailable(int reason);
+    void onAvailable();
+}
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index d586a4a..6b33a68 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -67,6 +67,7 @@
 import com.android.internal.telephony.CellNetworkScanResult;
 import com.android.internal.telephony.IBooleanConsumer;
 import com.android.internal.telephony.ICallForwardingInfoCallback;
+import com.android.internal.telephony.IImsStateCallback;
 import com.android.internal.telephony.IIntegerConsumer;
 import com.android.internal.telephony.INumberVerificationCallback;
 import com.android.internal.telephony.OperatorInfo;
@@ -2497,4 +2498,15 @@
      * NSSAIs (configured, allowed and rejected).
      */
     void getSlicingConfig(in ResultReceiver callback);
+
+    /**
+     * Register an IMS connection state callback
+     */
+    void registerImsStateCallback(int subId, int feature, in IImsStateCallback cb,
+            in String callingPackage);
+
+    /**
+     * Unregister an IMS connection state callback
+     */
+    void unregisterImsStateCallback(in IImsStateCallback cb);
 }