Merge "clearCallingIdentity for every CallControl API in TSW" into main
diff --git a/res/values-night/styles.xml b/res/values-night/styles.xml
index 5b81fac..b94b9e4 100644
--- a/res/values-night/styles.xml
+++ b/res/values-night/styles.xml
@@ -23,6 +23,11 @@
         <item name="android:actionOverflowButtonStyle">@style/TelecomDialerSettingsActionOverflowButtonStyle</item>
         <item name="android:windowLightNavigationBar">true</item>
         <item name="android:windowContentOverlay">@null</item>
+
+        <!--
+            TODO(b/309578419): Make activities handle insets properly and then remove this.
+        -->
+        <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
     </style>
 
     <style name="Theme.Telecom.BlockedNumbers" parent="@android:style/Theme.DeviceDefault.Light">
@@ -31,6 +36,11 @@
         <item name="android:windowLightNavigationBar">true</item>
         <item name="android:windowContentOverlay">@null</item>
         <item name="android:listDivider">@null</item>
+
+        <!--
+            TODO(b/309578419): Make activities handle insets properly and then remove this.
+        -->
+        <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
     </style>
 
 </resources>
\ No newline at end of file
diff --git a/res/values/styles.xml b/res/values/styles.xml
index cd608f5..0624082 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -32,6 +32,11 @@
         <item name="android:navigationBarColor">@android:color/transparent</item>
         <item name="android:windowLightStatusBar">true</item>
         <item name="android:windowLightNavigationBar">true</item>
+
+        <!--
+            TODO(b/309578419): Make activities handle insets properly and then remove this.
+        -->
+        <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
     </style>
 
     <style name="Theme.Telecom.EnableAccount" parent="Theme.Telecom.DialerSettings">
diff --git a/src/com/android/server/telecom/AudioRoute.java b/src/com/android/server/telecom/AudioRoute.java
index cdf44a8..7b593d7 100644
--- a/src/com/android/server/telecom/AudioRoute.java
+++ b/src/com/android/server/telecom/AudioRoute.java
@@ -23,11 +23,14 @@
 import static com.android.server.telecom.CallAudioRouteAdapter.SPEAKER_ON;
 
 import android.annotation.IntDef;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothStatusCodes;
 import android.media.AudioDeviceInfo;
 import android.media.AudioManager;
 import android.telecom.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.telecom.bluetooth.BluetoothRouteManager;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -38,6 +41,7 @@
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
+import java.util.concurrent.RejectedExecutionException;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
@@ -46,9 +50,10 @@
     public static class Factory {
         private final ScheduledExecutorService mScheduledExecutorService =
                 new ScheduledThreadPoolExecutor(1);
-        private final CompletableFuture<AudioRoute> mAudioRouteFuture = new CompletableFuture<>();
+        private CompletableFuture<AudioRoute> mAudioRouteFuture;
         public AudioRoute create(@AudioRouteType int type, String bluetoothAddress,
                                  AudioManager audioManager) throws RuntimeException {
+            mAudioRouteFuture = new CompletableFuture();
             createRetry(type, bluetoothAddress, audioManager, MAX_CONNECTION_RETRIES);
             try {
                 return mAudioRouteFuture.get();
@@ -58,8 +63,10 @@
         }
         private void createRetry(@AudioRouteType int type, String bluetoothAddress,
                                        AudioManager audioManager, int retryCount) {
+            // Early exit if exceeded max number of retries (and complete the future).
             if (retryCount == 0) {
                 mAudioRouteFuture.complete(null);
+                return;
             }
 
             Log.i(this, "creating AudioRoute with type %s and address %s, retry count %d",
@@ -81,10 +88,17 @@
                     }
                 }
             }
-            if (routeInfo == null) {
-                mScheduledExecutorService.schedule(
-                        () -> createRetry(type, bluetoothAddress, audioManager, retryCount - 1),
-                        RETRY_TIME_DELAY, TimeUnit.MILLISECONDS);
+            // Try connecting BT device anyway (to handle wearables not showing as available
+            // communication device or LE device not showing up since it may not be the lead
+            // device).
+            if (routeInfo == null && bluetoothAddress == null) {
+                try {
+                    mScheduledExecutorService.schedule(
+                            () -> createRetry(type, bluetoothAddress, audioManager, retryCount - 1),
+                            RETRY_TIME_DELAY, TimeUnit.MILLISECONDS);
+                } catch (RejectedExecutionException e) {
+                    Log.e(this, e, "Could not schedule retry for audio routing.");
+                }
             } else {
                 mAudioRouteFuture.complete(new AudioRoute(type, bluetoothAddress, routeInfo));
             }
@@ -222,30 +236,81 @@
 
     // Invoked when entered pending route whose dest route is this route
     void onDestRouteAsPendingRoute(boolean active, PendingAudioRoute pendingAudioRoute,
-                                   AudioManager audioManager) {
+            BluetoothDevice device, AudioManager audioManager,
+            BluetoothRouteManager bluetoothRouteManager) {
+        Log.i(this, "onDestRouteAsPendingRoute: active (%b), type (%d)", active, mAudioRouteType);
         if (pendingAudioRoute.isActive() && !active) {
-            Log.i(this, "clearCommunicationDevice");
-            audioManager.clearCommunicationDevice();
+            clearCommunicationDevice(pendingAudioRoute, bluetoothRouteManager, audioManager);
         } else if (active) {
-            if (mAudioRouteType == TYPE_BLUETOOTH_SCO) {
-                pendingAudioRoute.addMessage(BT_AUDIO_CONNECTED);
+            // Handle BT routing case.
+            if (BT_AUDIO_ROUTE_TYPES.contains(mAudioRouteType)) {
+                boolean connectedBtAudio = connectBtAudio(pendingAudioRoute, device,
+                        audioManager, bluetoothRouteManager);
+                // Special handling for SCO case.
+                if (mAudioRouteType == TYPE_BLUETOOTH_SCO) {
+                    // Check if the communication device was set for the device, even if
+                    // BluetoothHeadset#connectAudio reports that the SCO connection wasn't
+                    // successfully established.
+                    boolean scoConnected = audioManager.getCommunicationDevice().equals(mInfo);
+                    if (connectedBtAudio || scoConnected) {
+                        pendingAudioRoute.setCommunicationDeviceType(mAudioRouteType);
+                    }
+                    if (connectedBtAudio) {
+                        pendingAudioRoute.addMessage(BT_AUDIO_CONNECTED);
+                    } else if (!scoConnected) {
+                        pendingAudioRoute.onMessageReceived(
+                                PENDING_ROUTE_FAILED, mBluetoothAddress);
+                    }
+                    return;
+                }
             } else if (mAudioRouteType == TYPE_SPEAKER) {
                 pendingAudioRoute.addMessage(SPEAKER_ON);
             }
-            if (!audioManager.setCommunicationDevice(mInfo)) {
-                pendingAudioRoute.onMessageReceived(PENDING_ROUTE_FAILED);
+
+            boolean result = false;
+            List<AudioDeviceInfo> devices = audioManager.getAvailableCommunicationDevices();
+            for (AudioDeviceInfo deviceInfo : devices) {
+                // It's possible for the AudioDeviceInfo to be updated for the BT device so adjust
+                // mInfo accordingly.
+                if (BT_AUDIO_ROUTE_TYPES.contains(mAudioRouteType) && mBluetoothAddress
+                        .equals(deviceInfo.getAddress())) {
+                    mInfo = deviceInfo;
+                }
+                if (deviceInfo.equals(mInfo)) {
+                    result = audioManager.setCommunicationDevice(mInfo);
+                    if (result) {
+                        pendingAudioRoute.setCommunicationDeviceType(mAudioRouteType);
+                    }
+                    Log.i(this, "Result of setting communication device for audio "
+                            + "route (%s) - %b", this, result);
+                    break;
+                }
+            }
+
+            // It's possible that BluetoothStateReceiver needs to report that the device is active
+            // before being able to successfully set the communication device. Refrain from sending
+            // pending route failed message for BT route until the second attempt fails.
+            if (!result && !BT_AUDIO_ROUTE_TYPES.contains(mAudioRouteType)) {
+                pendingAudioRoute.onMessageReceived(PENDING_ROUTE_FAILED, null);
             }
         }
     }
 
+    // Takes care of cleaning up original audio route (i.e. clearCommunicationDevice,
+    // sending SPEAKER_OFF, or disconnecting SCO).
     void onOrigRouteAsPendingRoute(boolean active, PendingAudioRoute pendingAudioRoute,
-                                   AudioManager audioManager) {
+            AudioManager audioManager, BluetoothRouteManager bluetoothRouteManager) {
+        Log.i(this, "onOrigRouteAsPendingRoute: active (%b), type (%d)", active, mAudioRouteType);
         if (active) {
-            if (mAudioRouteType == TYPE_BLUETOOTH_SCO) {
-                pendingAudioRoute.addMessage(BT_AUDIO_DISCONNECTED);
-            } else if (mAudioRouteType == TYPE_SPEAKER) {
+            if (mAudioRouteType == TYPE_SPEAKER) {
                 pendingAudioRoute.addMessage(SPEAKER_OFF);
             }
+            int result = clearCommunicationDevice(pendingAudioRoute, bluetoothRouteManager,
+                    audioManager);
+            // Only send BT_AUDIO_DISCONNECTED for SCO if disconnect was successful.
+            if (mAudioRouteType == TYPE_BLUETOOTH_SCO && result == BluetoothStatusCodes.SUCCESS) {
+                pendingAudioRoute.addMessage(BT_AUDIO_DISCONNECTED);
+            }
         }
     }
 
@@ -282,4 +347,50 @@
                 + ", Address=" + ((mBluetoothAddress != null) ? mBluetoothAddress : "invalid")
                 + "]";
     }
+
+    private boolean connectBtAudio(PendingAudioRoute pendingAudioRoute, BluetoothDevice device,
+            AudioManager audioManager, BluetoothRouteManager bluetoothRouteManager) {
+        // Ensure that if another BT device was set, it is disconnected before connecting
+        // the new one.
+        AudioRoute currentRoute = pendingAudioRoute.getOrigRoute();
+        if (currentRoute.getBluetoothAddress() != null &&
+                !currentRoute.getBluetoothAddress().equals(device.getAddress())) {
+            clearCommunicationDevice(pendingAudioRoute, bluetoothRouteManager, audioManager);
+        }
+
+        // Connect to the device (explicit handling for HFP devices).
+        boolean success = false;
+        if (device != null) {
+            success = bluetoothRouteManager.getDeviceManager()
+                    .connectAudio(device, mAudioRouteType);
+        }
+
+        Log.i(this, "connectBtAudio: routeToConnectTo = %s, successful = %b",
+                this, success);
+        return success;
+    }
+
+    private int clearCommunicationDevice(PendingAudioRoute pendingAudioRoute,
+            BluetoothRouteManager bluetoothRouteManager, AudioManager audioManager) {
+        // Try to see if there's a previously set device for communication that should be cleared.
+        // This only serves to help in the SCO case to ensure that we disconnect the headset.
+        if (pendingAudioRoute.getCommunicationDeviceType() == AudioRoute.TYPE_INVALID) {
+            return -1;
+        }
+
+        int result = BluetoothStatusCodes.SUCCESS;
+        if (pendingAudioRoute.getCommunicationDeviceType() == TYPE_BLUETOOTH_SCO) {
+            Log.i(this, "Disconnecting SCO device.");
+            result = bluetoothRouteManager.getDeviceManager().disconnectSco();
+        } else {
+            Log.i(this, "Clearing communication device for audio type %d.",
+                    pendingAudioRoute.getCommunicationDeviceType());
+            audioManager.clearCommunicationDevice();
+        }
+
+        if (result == BluetoothStatusCodes.SUCCESS) {
+            pendingAudioRoute.setCommunicationDeviceType(AudioRoute.TYPE_INVALID);
+        }
+        return result;
+    }
 }
diff --git a/src/com/android/server/telecom/CallAudioRouteAdapter.java b/src/com/android/server/telecom/CallAudioRouteAdapter.java
index 5585d09..9927c22 100644
--- a/src/com/android/server/telecom/CallAudioRouteAdapter.java
+++ b/src/com/android/server/telecom/CallAudioRouteAdapter.java
@@ -134,5 +134,6 @@
     CallAudioState getCurrentCallAudioState();
     boolean isHfpDeviceAvailable();
     Handler getAdapterHandler();
+    PendingAudioRoute getPendingAudioRoute();
     void dump(IndentingPrintWriter pw);
 }
diff --git a/src/com/android/server/telecom/CallAudioRouteController.java b/src/com/android/server/telecom/CallAudioRouteController.java
index 44f6430..7b29fc8 100644
--- a/src/com/android/server/telecom/CallAudioRouteController.java
+++ b/src/com/android/server/telecom/CallAudioRouteController.java
@@ -19,25 +19,24 @@
 import static com.android.server.telecom.AudioRoute.BT_AUDIO_ROUTE_TYPES;
 import static com.android.server.telecom.AudioRoute.TYPE_INVALID;
 
-import android.app.ActivityManager;
+import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeAudio;
+import android.bluetooth.BluetoothProfile;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
-import android.content.pm.UserInfo;
 import android.media.AudioAttributes;
 import android.media.AudioDeviceAttributes;
 import android.media.AudioDeviceInfo;
 import android.media.AudioManager;
 import android.media.IAudioService;
 import android.media.audiopolicy.AudioProductStrategy;
-import android.os.Binder;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Message;
 import android.os.RemoteException;
-import android.os.UserHandle;
 import android.telecom.CallAudioState;
 import android.telecom.Log;
 import android.telecom.Logging.Session;
@@ -49,8 +48,10 @@
 import com.android.internal.os.SomeArgs;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.server.telecom.bluetooth.BluetoothRouteManager;
+import com.android.server.telecom.flags.FeatureFlags;
 
 import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -90,6 +91,8 @@
     private Map<Integer, AudioRoute> mTypeRoutes;
     private PendingAudioRoute mPendingAudioRoute;
     private AudioRoute.Factory mAudioRouteFactory;
+    private StatusBarNotifier mStatusBarNotifier;
+    private FeatureFlags mFeatureFlags;
     private int mFocusType;
     private final Object mLock = new Object();
     private final BroadcastReceiver mSpeakerPhoneChangeReceiver = new BroadcastReceiver() {
@@ -152,12 +155,11 @@
     private boolean mIsActive;
 
     public CallAudioRouteController(
-            Context context,
-            CallsManager callsManager,
+            Context context, CallsManager callsManager,
             CallAudioManager.AudioServiceFactory audioServiceFactory,
-            AudioRoute.Factory audioRouteFactory,
-            WiredHeadsetManager wiredHeadsetManager,
-            BluetoothRouteManager bluetoothRouteManager) {
+            AudioRoute.Factory audioRouteFactory, WiredHeadsetManager wiredHeadsetManager,
+            BluetoothRouteManager bluetoothRouteManager, StatusBarNotifier statusBarNotifier,
+            FeatureFlags featureFlags) {
         mContext = context;
         mCallsManager = callsManager;
         mAudioManager = context.getSystemService(AudioManager.class);
@@ -166,6 +168,8 @@
         mWiredHeadsetManager = wiredHeadsetManager;
         mIsMute = false;
         mBluetoothRouteManager = bluetoothRouteManager;
+        mStatusBarNotifier = statusBarNotifier;
+        mFeatureFlags = featureFlags;
         mFocusType = NO_FOCUS;
         HandlerThread handlerThread = new HandlerThread(this.getClass().getSimpleName());
         handlerThread.start();
@@ -246,8 +250,10 @@
                         case USER_SWITCH_SPEAKER:
                             handleSwitchSpeaker();
                             break;
+                        case SWITCH_BASELINE_ROUTE:
                         case USER_SWITCH_BASELINE_ROUTE:
-                            handleSwitchBaselineRoute();
+                            address = (String) ((SomeArgs) msg.obj).arg2;
+                            handleSwitchBaselineRoute(address);
                             break;
                         case SPEAKER_ON:
                             handleSpeakerOn();
@@ -296,16 +302,16 @@
     @Override
     public void initialize() {
         mAvailableRoutes = new HashSet<>();
-        mBluetoothRoutes = new ArrayMap<>();
+        mBluetoothRoutes = new LinkedHashMap<>();
         mTypeRoutes = new ArrayMap<>();
         mStreamingRoutes = new HashSet<>();
-        mPendingAudioRoute = new PendingAudioRoute(this, mAudioManager);
+        mPendingAudioRoute = new PendingAudioRoute(this, mAudioManager, mBluetoothRouteManager);
         mStreamingRoute = new AudioRoute(AudioRoute.TYPE_STREAMING, null, null);
         mStreamingRoutes.add(mStreamingRoute);
 
-        int supportMask = calculateSupportedRouteMask();
+        int supportMask = calculateSupportedRouteMaskInit();
         if ((supportMask & CallAudioState.ROUTE_SPEAKER) != 0) {
-            // Create spekaer routes
+            // Create speaker routes
             mSpeakerDockRoute = mAudioRouteFactory.create(AudioRoute.TYPE_SPEAKER, null,
                     mAudioManager);
             if (mSpeakerDockRoute == null) {
@@ -405,6 +411,11 @@
     }
 
     @Override
+    public PendingAudioRoute getPendingAudioRoute() {
+        return mPendingAudioRoute;
+    }
+
+    @Override
     public void dump(IndentingPrintWriter pw) {
     }
 
@@ -434,6 +445,7 @@
 
     private void routeTo(boolean active, AudioRoute destRoute) {
         if (!destRoute.equals(mStreamingRoute) && !getAvailableRoutes().contains(destRoute)) {
+            Log.i(this, "Ignore routing to unavailable route: %s", destRoute);
             return;
         }
         if (mIsPending) {
@@ -445,9 +457,9 @@
                     mPendingAudioRoute.getDestRoute(), mIsActive, destRoute, active);
             // override pending route while keep waiting for still pending messages for the
             // previous pending route
-            mIsActive = active;
             mPendingAudioRoute.setOrigRoute(mIsActive, mPendingAudioRoute.getDestRoute());
-            mPendingAudioRoute.setDestRoute(active, destRoute);
+            mPendingAudioRoute.setDestRoute(active, destRoute, mBluetoothRoutes.get(destRoute));
+            mIsActive = active;
         } else {
             if (mCurrentRoute.equals(destRoute) && (mIsActive == active)) {
                 return;
@@ -461,7 +473,7 @@
                 // Avoid waiting for pending messages for an unavailable route
                 mPendingAudioRoute.setOrigRoute(mIsActive, DUMMY_ROUTE);
             }
-            mPendingAudioRoute.setDestRoute(active, destRoute);
+            mPendingAudioRoute.setDestRoute(active, destRoute, mBluetoothRoutes.get(destRoute));
             mIsActive = active;
             mIsPending = true;
         }
@@ -512,7 +524,7 @@
 
         // Route to expected state
         if (mCurrentRoute.equals(wiredHeadsetRoute)) {
-            routeTo(mIsActive, getBaseRoute(true));
+            routeTo(mIsActive, getBaseRoute(true, null));
         }
     }
 
@@ -551,7 +563,7 @@
 
         // Route to expected state
         if (mCurrentRoute.equals(dockRoute)) {
-            routeTo(mIsActive, getBaseRoute(true));
+            routeTo(mIsActive, getBaseRoute(true, null));
         }
     }
 
@@ -567,38 +579,69 @@
         if (mCurrentRoute.equals(mStreamingRoute)) {
             mCurrentRoute = DUMMY_ROUTE;
             onAvailableRoutesChanged();
-            routeTo(mIsActive, getBaseRoute(true));
+            routeTo(mIsActive, getBaseRoute(true, null));
         } else {
             Log.i(this, "ignore disable streaming, not in streaming");
         }
     }
 
+    /**
+     * Handles the case when SCO audio is connected for the BT headset. This follows shortly after
+     * the BT device has been established as an active device (BT_ACTIVE_DEVICE_PRESENT) and doesn't
+     * apply to other BT device types. In this case, the pending audio route will process the
+     * BT_AUDIO_CONNECTED message that will trigger routing to the pending destination audio route;
+     * otherwise, routing will be ignored if there aren't pending routes to be processed.
+     *
+     * Message being handled: BT_AUDIO_CONNECTED
+     */
     private void handleBtAudioActive(BluetoothDevice bluetoothDevice) {
         if (mIsPending) {
+            Log.i(this, "handleBtAudioActive: is pending path");
             if (Objects.equals(mPendingAudioRoute.getDestRoute().getBluetoothAddress(),
                     bluetoothDevice.getAddress())) {
-                mPendingAudioRoute.onMessageReceived(BT_AUDIO_CONNECTED);
+                mPendingAudioRoute.onMessageReceived(BT_AUDIO_CONNECTED, null);
             }
         } else {
             // ignore, not triggered by telecom
+            Log.i(this, "handleBtAudioActive: ignoring handling bt audio active.");
         }
     }
 
+    /**
+     * Handles the case when SCO audio is disconnected for the BT headset. In this case, the pending
+     * audio route will process the BT_AUDIO_DISCONNECTED message which will trigger routing to the
+     * pending destination audio route; otherwise, routing will be ignored if there aren't any
+     * pending routes to be processed.
+     *
+     * Message being handled: BT_AUDIO_DISCONNECTED
+     */
     private void handleBtAudioInactive(BluetoothDevice bluetoothDevice) {
         if (mIsPending) {
+            Log.i(this, "handleBtAudioInactive: is pending path");
             if (Objects.equals(mPendingAudioRoute.getOrigRoute().getBluetoothAddress(),
                     bluetoothDevice.getAddress())) {
-                mPendingAudioRoute.onMessageReceived(BT_AUDIO_DISCONNECTED);
+                mPendingAudioRoute.onMessageReceived(BT_AUDIO_DISCONNECTED, null);
             }
         } else {
             // ignore, not triggered by telecom
+            Log.i(this, "handleBtAudioInactive: ignoring handling bt audio inactive.");
         }
     }
 
+    /**
+     * This particular routing occurs when the BT device is trying to establish itself as a
+     * connected device (refer to BluetoothStateReceiver#handleConnectionStateChanged). The device
+     * is included as an available route and cached into the current BT routes.
+     *
+     * Message being handled: BT_DEVICE_ADDED
+     */
     private void handleBtConnected(@AudioRoute.AudioRouteType int type,
                                    BluetoothDevice bluetoothDevice) {
-        AudioRoute bluetoothRoute = null;
-        bluetoothRoute = mAudioRouteFactory.create(type, bluetoothDevice.getAddress(),
+        if (containsHearingAidPair(type, bluetoothDevice)) {
+            return;
+        }
+
+        AudioRoute bluetoothRoute = mAudioRouteFactory.create(type, bluetoothDevice.getAddress(),
                 mAudioManager);
         if (bluetoothRoute == null) {
             Log.w(this, "Can't find available audio device info for route type:"
@@ -611,6 +654,14 @@
         }
     }
 
+    /**
+     * Handles the case when the BT device is in a disconnecting/disconnected state. In this case,
+     * the audio route for the specified device is removed from the available BT routes and the
+     * audio is routed to an available route if the current route is pointing to the device which
+     * got disconnected.
+     *
+     * Message being handled: BT_DEVICE_REMOVED
+     */
     private void handleBtDisconnected(@AudioRoute.AudioRouteType int type,
                                       BluetoothDevice bluetoothDevice) {
         // Clean up unavailable routes
@@ -624,25 +675,45 @@
 
         // Fallback to an available route
         if (Objects.equals(mCurrentRoute, bluetoothRoute)) {
-            routeTo(mIsActive, getBaseRoute(false));
+            routeTo(mIsActive, getBaseRoute(true, null));
         }
     }
 
+    /**
+     * This particular routing occurs when the specified bluetooth device is marked as the active
+     * device (refer to BluetoothStateReceiver#handleActiveDeviceChanged). This takes care of
+     * moving the call audio route to the bluetooth route.
+     *
+     * Message being handled: BT_ACTIVE_DEVICE_PRESENT
+     */
     private void handleBtActiveDevicePresent(@AudioRoute.AudioRouteType int type,
                                              String deviceAddress) {
         AudioRoute bluetoothRoute = getBluetoothRoute(type, deviceAddress);
         if (bluetoothRoute != null) {
-            Log.i(this, "request to route to bluetooth route: %s(active=%b)", bluetoothRoute,
+            Log.i(this, "request to route to bluetooth route: %s (active=%b)", bluetoothRoute,
                     mIsActive);
             routeTo(mIsActive, bluetoothRoute);
+        } else {
+            Log.i(this, "request to route to unavailable bluetooth route - type (%s), address (%s)",
+                    type, deviceAddress);
         }
     }
 
+    /**
+     * Handles routing for when the active BT device is removed for a given audio route type. In
+     * this case, the audio is routed to another available route if the current route hasn't been
+     * adjusted yet or there is a pending destination route associated with the device type that
+     * went inactive. Note that BT_DEVICE_REMOVED will be processed first in this case, which will
+     * handle removing the BT route for the device that went inactive as well as falling back to
+     * an available route.
+     *
+     * Message being handled: BT_ACTIVE_DEVICE_GONE
+     */
     private void handleBtActiveDeviceGone(@AudioRoute.AudioRouteType int type) {
         if ((mIsPending && mPendingAudioRoute.getDestRoute().getType() == type)
                 || (!mIsPending && mCurrentRoute.getType() == type)) {
             // Fallback to an available route
-            routeTo(mIsActive, getBaseRoute(true));
+            routeTo(mIsActive, getBaseRoute(true, null));
         }
     }
 
@@ -667,30 +738,38 @@
     }
 
     private void handleSwitchFocus(int focus) {
+        Log.i(this, "handleSwitchFocus: focus (%s)", focus);
         mFocusType = focus;
         switch (focus) {
             case NO_FOCUS -> {
                 if (mIsActive) {
+                    // Reset mute state after call ends.
                     handleMuteChanged(false);
+                    // Route back to inactive route.
                     routeTo(false, mCurrentRoute);
+                    // Clear pending messages
+                    mPendingAudioRoute.clearPendingMessages();
                 }
             }
             case ACTIVE_FOCUS -> {
+                // Route to active baseline route, otherwise ignore if route is already active.
                 if (!mIsActive) {
-                    routeTo(true, getBaseRoute(true));
+                    routeTo(true, getBaseRoute(true, null));
                 }
             }
             case RINGING_FOCUS -> {
                 if (!mIsActive) {
-                    AudioRoute route = getBaseRoute(true);
+                    AudioRoute route = getBaseRoute(true, null);
                     BluetoothDevice device = mBluetoothRoutes.get(route);
+                    // Check if in-band ringtone is enabled for the device; if it isn't, move to
+                    // inactive route.
                     if (device != null && !mBluetoothRouteManager.isInbandRingEnabled(device)) {
                         routeTo(false, route);
                     } else {
                         routeTo(true, route);
                     }
                 } else {
-                    // active
+                    // Route is already active.
                     BluetoothDevice device = mBluetoothRoutes.get(mCurrentRoute);
                     if (device != null && !mBluetoothRouteManager.isInbandRingEnabled(device)) {
                         routeTo(false, mCurrentRoute);
@@ -729,7 +808,7 @@
                 routeTo(mIsActive, bluetoothRoute);
             }
         } else {
-            Log.i(this, "ignore switch bluetooth request");
+            Log.i(this, "ignore switch bluetooth request to unavailable address");
         }
     }
 
@@ -738,7 +817,7 @@
         if (headsetRoute != null && getAvailableRoutes().contains(headsetRoute)) {
             routeTo(mIsActive, headsetRoute);
         } else {
-            Log.i(this, "ignore switch speaker request");
+            Log.i(this, "ignore switch headset request");
         }
     }
 
@@ -750,13 +829,16 @@
         }
     }
 
-    private void handleSwitchBaselineRoute() {
-        routeTo(mIsActive, getBaseRoute(true));
+    private void handleSwitchBaselineRoute(String btAddressToExclude) {
+        routeTo(mIsActive, getBaseRoute(true, btAddressToExclude));
     }
 
     private void handleSpeakerOn() {
         if (isPending()) {
-            mPendingAudioRoute.onMessageReceived(SPEAKER_ON);
+            Log.i(this, "handleSpeakerOn: sending SPEAKER_ON to pending audio route");
+            mPendingAudioRoute.onMessageReceived(SPEAKER_ON, null);
+            // Update status bar notification if we are in a call.
+            mStatusBarNotifier.notifySpeakerphone(mCallsManager.hasAnyCalls());
         } else {
             if (mSpeakerDockRoute != null && getAvailableRoutes().contains(mSpeakerDockRoute)) {
                 routeTo(mIsActive, mSpeakerDockRoute);
@@ -771,9 +853,12 @@
 
     private void handleSpeakerOff() {
         if (isPending()) {
-            mPendingAudioRoute.onMessageReceived(SPEAKER_OFF);
+            Log.i(this, "handleSpeakerOff - sending SPEAKER_OFF to pending audio route");
+            mPendingAudioRoute.onMessageReceived(SPEAKER_OFF, null);
+            // Update status bar notification
+            mStatusBarNotifier.notifySpeakerphone(false);
         } else if (mCurrentRoute.getType() == AudioRoute.TYPE_SPEAKER) {
-            routeTo(mIsActive, getBaseRoute(true));
+            routeTo(mIsActive, getBaseRoute(true, null));
             // Since the route switching triggered by this message, we need to manually send it
             // again so that we won't stuck in the pending route
             if (mIsActive) {
@@ -783,11 +868,15 @@
         }
     }
 
+    /**
+     * This is invoked when there are no more pending audio routes to be processed, which signals
+     * a change for the current audio route and the call audio state to be updated accordingly.
+     */
     public void handleExitPendingRoute() {
         if (mIsPending) {
-            Log.i(this, "Exit pending route and enter %s(active=%b)",
-                    mPendingAudioRoute.getDestRoute(), mIsActive);
             mCurrentRoute = mPendingAudioRoute.getDestRoute();
+            Log.addEvent(mCallsManager.getForegroundCall(), LogUtils.Events.AUDIO_ROUTE,
+                    "Entering audio route: " + mCurrentRoute + " (active=" + mIsActive + ")");
             mIsPending = false;
             onCurrentRouteChanged();
         }
@@ -817,7 +906,20 @@
             for (AudioRoute route : getAvailableRoutes()) {
                 routeMask |= ROUTE_MAP.get(route.getType());
                 if (BT_AUDIO_ROUTE_TYPES.contains(route.getType())) {
-                    availableBluetoothDevices.add(mBluetoothRoutes.get(route));
+                    BluetoothDevice deviceToAdd = mBluetoothRoutes.get(route);
+                    // Only include the lead device for LE audio (otherwise, the routes will show
+                    // two separate devices in the UI).
+                    if (route.getType() == AudioRoute.TYPE_BLUETOOTH_LE) {
+                        int groupId = getLeAudioService().getGroupId(deviceToAdd);
+                        if (groupId != BluetoothLeAudio.GROUP_ID_INVALID) {
+                            deviceToAdd = getLeAudioService().getConnectedGroupLeadDevice(groupId);
+                        }
+                    }
+                    // This will only ever be null when the lead device (LE) is disconnected and
+                    // try to obtain the lead device for the 2nd bud.
+                    if (deviceToAdd != null) {
+                        availableBluetoothDevices.add(deviceToAdd);
+                    }
                 }
             }
             updateCallAudioState(new CallAudioState(mIsMute, mCallAudioState.getRoute(), routeMask,
@@ -831,10 +933,12 @@
                 mCallAudioState.getSupportedBluetoothDevices()));
     }
 
-    private void updateCallAudioState(CallAudioState callAudioState) {
-        Log.i(this, "updateCallAudioState: " + callAudioState);
+    private void updateCallAudioState(CallAudioState newCallAudioState) {
+        Log.i(this, "updateCallAudioState: updating call audio state to %s", newCallAudioState);
         CallAudioState oldState = mCallAudioState;
-        mCallAudioState = callAudioState;
+        mCallAudioState = newCallAudioState;
+        // Update status bar notification
+        mStatusBarNotifier.notifyMute(newCallAudioState.isMuted());
         mCallsManager.onCallAudioStateChanged(oldState, mCallAudioState);
         updateAudioStateForTrackedCalls(mCallAudioState);
     }
@@ -866,6 +970,7 @@
 
         // Get preferred device
         AudioDeviceAttributes deviceAttr = mAudioManager.getPreferredDeviceForStrategy(strategy);
+        Log.i(this, "getPreferredAudioRouteFromStrategy: preferred device is %s", deviceAttr);
         if (deviceAttr == null) {
             return null;
         }
@@ -881,16 +986,32 @@
         }
     }
 
-    private AudioRoute getPreferredAudioRouteFromDefault(boolean includeBluetooth) {
-        if (mBluetoothRoutes.isEmpty() || !includeBluetooth) {
+    private AudioRoute getPreferredAudioRouteFromDefault(boolean includeBluetooth,
+            String btAddressToExclude) {
+        // Route to earpiece, wired, or speaker route if there are not bluetooth routes or if there
+        // are only wearables available.
+        AudioRoute activeWatchOrNonWatchDeviceRoute =
+                getActiveWatchOrNonWatchDeviceRoute(btAddressToExclude);
+        if (mBluetoothRoutes.isEmpty() || !includeBluetooth
+                || activeWatchOrNonWatchDeviceRoute == null) {
+            Log.i(this, "getPreferredAudioRouteFromDefault: Audio routing defaulting to "
+                    + "available non-BT route.");
             return mEarpieceWiredRoute != null ? mEarpieceWiredRoute : mSpeakerDockRoute;
         } else {
-            // Most recent active route will always be the last in the array
-            return mBluetoothRoutes.keySet().stream().toList().get(mBluetoothRoutes.size() - 1);
+            // Most recent active route will always be the last in the array (ensure that we don't
+            // auto route to a wearable device unless it's already active).
+            String autoRoutingToWatchExcerpt = mFeatureFlags.ignoreAutoRouteToWatchDevice()
+                    ? " (except watch)"
+                    : "";
+            Log.i(this, "getPreferredAudioRouteFromDefault: Audio routing defaulting to "
+                    + "most recently active BT route" + autoRoutingToWatchExcerpt + ".");
+            return activeWatchOrNonWatchDeviceRoute;
         }
     }
 
-    private int calculateSupportedRouteMask() {
+    private int calculateSupportedRouteMaskInit() {
+        Log.i(this, "calculateSupportedRouteMaskInit: is wired headset plugged in - %s",
+                mWiredHeadsetManager.isPluggedIn());
         int routeMask = CallAudioState.ROUTE_SPEAKER;
 
         if (mWiredHeadsetManager.isPluggedIn()) {
@@ -931,17 +1052,139 @@
         return null;
     }
 
-    public AudioRoute getBaseRoute(boolean includeBluetooth) {
+    public AudioRoute getBaseRoute(boolean includeBluetooth, String btAddressToExclude) {
         AudioRoute destRoute = getPreferredAudioRouteFromStrategy();
         if (destRoute == null) {
-            destRoute = getPreferredAudioRouteFromDefault(includeBluetooth);
+            destRoute = getPreferredAudioRouteFromDefault(includeBluetooth, btAddressToExclude);
         }
         if (destRoute != null && !getAvailableRoutes().contains(destRoute)) {
             destRoute = null;
         }
+        Log.i(this, "getBaseRoute - audio routing to %s", destRoute);
         return destRoute;
     }
 
+    /**
+     * Don't add additional AudioRoute when a hearing aid pair is detected. The devices have
+     * separate addresses, so we need to perform explicit handling to ensure we don't
+     * treat them as two separate devices.
+     */
+    private boolean containsHearingAidPair(@AudioRoute.AudioRouteType int type,
+            BluetoothDevice bluetoothDevice) {
+        // Check if it is a hearing aid pair and skip connecting to the other device in this case.
+        // Traverse mBluetoothRoutes backwards as the most recently active device will be inserted
+        // last.
+        String existingHearingAidAddress = null;
+        List<AudioRoute> bluetoothRoutes = mBluetoothRoutes.keySet().stream().toList();
+        for (int i = bluetoothRoutes.size() - 1; i >= 0; i--) {
+            AudioRoute audioRoute = bluetoothRoutes.get(i);
+            if (audioRoute.getType() == AudioRoute.TYPE_BLUETOOTH_HA) {
+                existingHearingAidAddress = audioRoute.getBluetoothAddress();
+                break;
+            }
+        }
+
+        // Check that route is for hearing aid and that there exists another hearing aid route
+        // created for the first device (of the pair) that was connected.
+        if (type == AudioRoute.TYPE_BLUETOOTH_HA && existingHearingAidAddress != null) {
+            BluetoothAdapter bluetoothAdapter = mBluetoothRouteManager.getDeviceManager()
+                    .getBluetoothAdapter();
+            if (bluetoothAdapter != null) {
+                List<BluetoothDevice> activeHearingAids =
+                        bluetoothAdapter.getActiveDevices(BluetoothProfile.HEARING_AID);
+                for (BluetoothDevice hearingAid : activeHearingAids) {
+                    if (hearingAid != null && hearingAid.getAddress() != null) {
+                        String address = hearingAid.getAddress();
+                        if (address.equals(bluetoothDevice.getAddress())
+                                || address.equals(existingHearingAidAddress)) {
+                            Log.i(this, "containsHearingAidPair: Detected a hearing aid "
+                                    + "pair, ignoring creating a new AudioRoute");
+                            return true;
+                        }
+                    }
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Prevent auto routing to a wearable device when calculating the default bluetooth audio route
+     * to move to. This function ensures that the most recently active non-wearable device is
+     * selected for routing unless a wearable device has already been identified as an active
+     * device.
+     */
+    private AudioRoute getActiveWatchOrNonWatchDeviceRoute(String btAddressToExclude) {
+        if (!mFeatureFlags.ignoreAutoRouteToWatchDevice()) {
+            Log.i(this, "getActiveWatchOrNonWatchDeviceRoute: ignore_auto_route_to_watch_device "
+                    + "flag is disabled. Routing to most recently reported active device.");
+            return getMostRecentlyActiveBtRoute(btAddressToExclude);
+        }
+
+        List<AudioRoute> bluetoothRoutes = mBluetoothRoutes.keySet().stream().toList();
+        // Traverse the routes from the most recently active recorded devices first.
+        AudioRoute nonWatchDeviceRoute = null;
+        for (int i = bluetoothRoutes.size() - 1; i >= 0; i--) {
+            AudioRoute route = bluetoothRoutes.get(i);
+            BluetoothDevice device = mBluetoothRoutes.get(route);
+            // Skip excluded BT address and LE audio if it's not the lead device.
+            if (route.getBluetoothAddress().equals(btAddressToExclude)
+                    || isLeAudioNonLeadDevice(route.getType(), device)) {
+                continue;
+            }
+            // Check if the most recently active device is a watch device.
+            if (i == (bluetoothRoutes.size() - 1) && device.equals(mCallAudioState
+                    .getActiveBluetoothDevice()) && mBluetoothRouteManager.isWatch(device)) {
+                Log.i(this, "getActiveWatchOrNonWatchDeviceRoute: Routing to active watch - %s",
+                        bluetoothRoutes.get(0));
+                return bluetoothRoutes.get(0);
+            }
+            // Record the first occurrence of a non-watch device route if found.
+            if (!mBluetoothRouteManager.isWatch(device) && nonWatchDeviceRoute == null) {
+                nonWatchDeviceRoute = route;
+                break;
+            }
+        }
+
+        Log.i(this, "Routing to a non-watch device - %s", nonWatchDeviceRoute);
+        return nonWatchDeviceRoute;
+    }
+
+    /**
+     * Returns the most actively reported bluetooth route excluding the passed in route.
+     */
+    private AudioRoute getMostRecentlyActiveBtRoute(String btAddressToExclude) {
+        List<AudioRoute> bluetoothRoutes = mBluetoothRoutes.keySet().stream().toList();
+        for (int i = bluetoothRoutes.size() - 1; i >= 0; i--) {
+            AudioRoute route = bluetoothRoutes.get(i);
+            // Skip LE route if it's not the lead device.
+            if (isLeAudioNonLeadDevice(route.getType(), mBluetoothRoutes.get(route))) {
+                continue;
+            }
+            if (!route.getBluetoothAddress().equals(btAddressToExclude)) {
+                return route;
+            }
+        }
+        return null;
+    }
+
+    private boolean isLeAudioNonLeadDevice(@AudioRoute.AudioRouteType int type,
+            BluetoothDevice device) {
+        if (type != AudioRoute.TYPE_BLUETOOTH_LE) {
+            return false;
+        }
+        int groupId = getLeAudioService().getGroupId(device);
+        if (groupId != BluetoothLeAudio.GROUP_ID_INVALID) {
+            return !device.getAddress().equals(
+                    getLeAudioService().getConnectedGroupLeadDevice(groupId).getAddress());
+        }
+        return false;
+    }
+
+    private BluetoothLeAudio getLeAudioService() {
+        return mBluetoothRouteManager.getDeviceManager().getLeAudioService();
+    }
+
     @VisibleForTesting
     public void setAudioManager(AudioManager audioManager) {
         mAudioManager = audioManager;
diff --git a/src/com/android/server/telecom/CallAudioRouteStateMachine.java b/src/com/android/server/telecom/CallAudioRouteStateMachine.java
index 26c25e8..621ba36 100644
--- a/src/com/android/server/telecom/CallAudioRouteStateMachine.java
+++ b/src/com/android/server/telecom/CallAudioRouteStateMachine.java
@@ -2095,7 +2095,14 @@
         return base;
     }
 
+    @Override
     public Handler getAdapterHandler() {
         return getHandler();
     }
+
+    @Override
+    public PendingAudioRoute getPendingAudioRoute() {
+        // Only used by CallAudioRouteController.
+        return null;
+    }
 }
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index fbba7de..c3eb3b8 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -654,7 +654,8 @@
             );
         } else {
             callAudioRouteAdapter = new CallAudioRouteController(context, this, audioServiceFactory,
-                    new AudioRoute.Factory(), wiredHeadsetManager, mBluetoothRouteManager);
+                    new AudioRoute.Factory(), wiredHeadsetManager, mBluetoothRouteManager,
+                    statusBarNotifier, featureFlags);
         }
         callAudioRouteAdapter.initialize();
         bluetoothStateReceiver.setCallAudioRouteAdapter(callAudioRouteAdapter);
diff --git a/src/com/android/server/telecom/PendingAudioRoute.java b/src/com/android/server/telecom/PendingAudioRoute.java
index 8de62ed..6ba09a5 100644
--- a/src/com/android/server/telecom/PendingAudioRoute.java
+++ b/src/com/android/server/telecom/PendingAudioRoute.java
@@ -17,8 +17,13 @@
 package com.android.server.telecom;
 
 import static com.android.server.telecom.CallAudioRouteAdapter.PENDING_ROUTE_FAILED;
+import static com.android.server.telecom.CallAudioRouteAdapter.SWITCH_BASELINE_ROUTE;
 
+import android.bluetooth.BluetoothDevice;
 import android.media.AudioManager;
+import android.telecom.Log;
+
+import com.android.server.telecom.bluetooth.BluetoothRouteManager;
 
 import java.util.ArrayList;
 
@@ -32,6 +37,7 @@
 public class PendingAudioRoute {
     private CallAudioRouteController mCallAudioRouteController;
     private AudioManager mAudioManager;
+    private BluetoothRouteManager mBluetoothRouteManager;
     /**
      * The {@link AudioRoute} that this pending audio switching started with
      */
@@ -43,15 +49,23 @@
     private AudioRoute mDestRoute;
     private ArrayList<Integer> mPendingMessages;
     private boolean mActive;
-    PendingAudioRoute(CallAudioRouteController controller, AudioManager audioManager) {
+    /**
+     * The device that has been set for communication by Telecom
+     */
+    private @AudioRoute.AudioRouteType int mCommunicationDeviceType = AudioRoute.TYPE_INVALID;
+
+    PendingAudioRoute(CallAudioRouteController controller, AudioManager audioManager,
+            BluetoothRouteManager bluetoothRouteManager) {
         mCallAudioRouteController = controller;
         mAudioManager = audioManager;
+        mBluetoothRouteManager = bluetoothRouteManager;
         mPendingMessages = new ArrayList<>();
         mActive = false;
+        mCommunicationDeviceType = AudioRoute.TYPE_INVALID;
     }
 
     void setOrigRoute(boolean active, AudioRoute origRoute) {
-        origRoute.onOrigRouteAsPendingRoute(active, this, mAudioManager);
+        origRoute.onOrigRouteAsPendingRoute(active, this, mAudioManager, mBluetoothRouteManager);
         mOrigRoute = origRoute;
     }
 
@@ -59,8 +73,9 @@
         return mOrigRoute;
     }
 
-    void setDestRoute(boolean active, AudioRoute destRoute) {
-        destRoute.onDestRouteAsPendingRoute(active, this, mAudioManager);
+    void setDestRoute(boolean active, AudioRoute destRoute, BluetoothDevice device) {
+        destRoute.onDestRouteAsPendingRoute(active, this, device,
+                mAudioManager, mBluetoothRouteManager);
         mActive = active;
         mDestRoute = destRoute;
     }
@@ -73,12 +88,13 @@
         mPendingMessages.add(message);
     }
 
-    public void onMessageReceived(int message) {
+    public void onMessageReceived(int message, String btAddressToExclude) {
+        Log.i(this, "onMessageReceived: message - %s", message);
         if (message == PENDING_ROUTE_FAILED) {
             // Fallback to base route
-            mDestRoute = mCallAudioRouteController.getBaseRoute(true);
             mCallAudioRouteController.sendMessageWithSessionInfo(
-                    CallAudioRouteAdapter.EXIT_PENDING_ROUTE);
+                    SWITCH_BASELINE_ROUTE, 0, btAddressToExclude);
+            return;
         }
 
         // Removes the first occurrence of the specified message from this list, if it is present.
@@ -90,10 +106,27 @@
         if (mPendingMessages.isEmpty()) {
             mCallAudioRouteController.sendMessageWithSessionInfo(
                     CallAudioRouteAdapter.EXIT_PENDING_ROUTE);
+        } else {
+            for(Integer i: mPendingMessages) {
+                Log.d(this, "evaluatePendingState: pending Messages - %d", i);
+            }
         }
     }
 
+    public void clearPendingMessages() {
+        mPendingMessages.clear();
+    }
+
     public boolean isActive() {
         return mActive;
     }
+
+    public @AudioRoute.AudioRouteType int getCommunicationDeviceType() {
+        return mCommunicationDeviceType;
+    }
+
+    public void setCommunicationDeviceType(
+            @AudioRoute.AudioRouteType int communicationDeviceType) {
+        mCommunicationDeviceType = communicationDeviceType;
+    }
 }
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
index a0ffe63..50476fe 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
@@ -16,6 +16,9 @@
 
 package com.android.server.telecom.bluetooth;
 
+import static com.android.server.telecom.AudioRoute.TYPE_BLUETOOTH_HA;
+import static com.android.server.telecom.AudioRoute.TYPE_BLUETOOTH_SCO;
+
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothHeadset;
@@ -34,6 +37,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.telecom.AudioRoute;
 import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
 import com.android.server.telecom.flags.FeatureFlags;
 
@@ -489,12 +493,14 @@
         }
     }
 
-    public void disconnectSco() {
+    public int disconnectSco() {
+        int result = BluetoothStatusCodes.ERROR_UNKNOWN;
         if (getBluetoothHeadset() == null) {
             Log.w(this, "Trying to disconnect audio but no headset service exists.");
         } else {
-            mBluetoothHeadset.disconnectAudio();
+            result = mBluetoothHeadset.disconnectAudio();
         }
+        return result;
     }
 
     public boolean isLeAudioCommunicationDevice() {
@@ -651,6 +657,28 @@
         return result;
     }
 
+    public boolean setCommunicationDeviceForAddress(String address) {
+        AudioDeviceInfo deviceInfo = null;
+        List<AudioDeviceInfo> devices = mAudioManager.getAvailableCommunicationDevices();
+        if (devices.size() == 0) {
+            Log.w(this, " No communication devices available.");
+            return false;
+        }
+
+        for (AudioDeviceInfo device : devices) {
+            Log.i(this, " Available device type:  " + device.getType());
+            if (device.getAddress().equals(address)) {
+                deviceInfo = device;
+                break;
+            }
+        }
+
+        if (!mAudioManager.getCommunicationDevice().equals(deviceInfo)) {
+            return mAudioManager.setCommunicationDevice(deviceInfo);
+        }
+        return true;
+    }
+
     // Connect audio to the bluetooth device at address, checking to see whether it's
     // le audio, hearing aid or a HFP device, and using the proper BT API.
     public boolean connectAudio(String address, boolean switchingBtDevices) {
@@ -747,6 +775,54 @@
         }
     }
 
+    /**
+     * Used by CallAudioRouteController in order to connect the BT device.
+     * @param device {@link BluetoothDevice} to connect to.
+     * @param type {@link AudioRoute.AudioRouteType} associated with the device.
+     * @return {@code true} if device was successfully connected, {@code false} otherwise.
+     */
+    public boolean connectAudio(BluetoothDevice device, @AudioRoute.AudioRouteType int type) {
+        String address = device.getAddress();
+        int callProfile = BluetoothProfile.LE_AUDIO;
+        if (type == TYPE_BLUETOOTH_SCO) {
+            callProfile = BluetoothProfile.HEADSET;
+        } else if (type == TYPE_BLUETOOTH_HA) {
+            callProfile = BluetoothProfile.HEARING_AID;
+        }
+
+        Bundle preferredAudioProfiles = mBluetoothAdapter.getPreferredAudioProfiles(device);
+        if (preferredAudioProfiles != null && !preferredAudioProfiles.isEmpty()
+                && preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX) != 0) {
+            Log.i(this, "Preferred duplex profile for device=" + address + " is "
+                    + preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX));
+            callProfile = preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX);
+        }
+
+        if (callProfile == BluetoothProfile.LE_AUDIO
+                || callProfile == BluetoothProfile.HEARING_AID) {
+            return mBluetoothAdapter.setActiveDevice(device, BluetoothAdapter.ACTIVE_DEVICE_ALL);
+        } else if (callProfile == BluetoothProfile.HEADSET) {
+            boolean success = mBluetoothAdapter.setActiveDevice(device,
+                    BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL);
+            if (!success) {
+                Log.w(this, "Couldn't set active device to %s", address);
+                return false;
+            }
+            if (getBluetoothHeadset() != null) {
+                int scoConnectionRequest = mBluetoothHeadset.connectAudio();
+                return scoConnectionRequest == BluetoothStatusCodes.SUCCESS ||
+                        scoConnectionRequest
+                                == BluetoothStatusCodes.ERROR_AUDIO_DEVICE_ALREADY_CONNECTED;
+            } else {
+                Log.w(this, "Couldn't find bluetooth headset service");
+                return false;
+            }
+        } else {
+            Log.w(this, "Attempting to turn on audio for a disconnected device");
+            return false;
+        }
+    }
+
     public void cacheHearingAidDevice() {
         if (mBluetoothAdapter != null) {
             for (BluetoothDevice device : mBluetoothAdapter.getActiveDevices(
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java b/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
index 7da5339..d2686e7 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
@@ -40,14 +40,12 @@
 import com.android.server.telecom.Timeouts;
 import com.android.server.telecom.flags.FeatureFlags;
 
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.List;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
@@ -1048,4 +1046,8 @@
             mHfpActiveDeviceCache = device;
         }
     }
+
+    public BluetoothDeviceManager getDeviceManager() {
+        return mDeviceManager;
+    }
 }
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java b/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
index 6d80cd5..af9e07b 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
@@ -22,6 +22,8 @@
 import static com.android.server.telecom.CallAudioRouteAdapter.BT_AUDIO_DISCONNECTED;
 import static com.android.server.telecom.CallAudioRouteAdapter.BT_DEVICE_ADDED;
 import static com.android.server.telecom.CallAudioRouteAdapter.BT_DEVICE_REMOVED;
+import static com.android.server.telecom.CallAudioRouteAdapter.PENDING_ROUTE_FAILED;
+import static com.android.server.telecom.CallAudioRouteAdapter.SWITCH_BLUETOOTH;
 import static com.android.server.telecom.bluetooth.BluetoothRouteManager.BT_AUDIO_IS_ON;
 import static com.android.server.telecom.bluetooth.BluetoothRouteManager.BT_AUDIO_LOST;
 
@@ -44,6 +46,7 @@
 import com.android.server.telecom.AudioRoute;
 import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
 import com.android.server.telecom.CallAudioRouteAdapter;
+import com.android.server.telecom.CallAudioRouteController;
 import com.android.server.telecom.flags.FeatureFlags;
 import com.android.server.telecom.flags.Flags;
 
@@ -218,6 +221,24 @@
             } else {
                 mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
                         audioRouteType, device.getAddress());
+                if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID
+                        || deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO) {
+                    if (!mBluetoothDeviceManager.setCommunicationDeviceForAddress(
+                            device.getAddress())) {
+                        Log.i(this, "handleActiveDeviceChanged: Failed to set "
+                                + "communication device for %s. Sending PENDING_ROUTE_FAILED to "
+                                + "pending audio route.", device);
+                        mCallAudioRouteAdapter.getPendingAudioRoute()
+                                .onMessageReceived(PENDING_ROUTE_FAILED, device.getAddress());
+                    } else {
+                        // Track the currently set communication device.
+                        int routeType = deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO
+                                ? AudioRoute.TYPE_BLUETOOTH_LE
+                                : AudioRoute.TYPE_BLUETOOTH_HA;
+                        mCallAudioRouteAdapter.getPendingAudioRoute()
+                                .setCommunicationDeviceType(routeType);
+                    }
+                }
             }
         } else {
             mBluetoothRouteManager.onActiveDeviceChanged(device, deviceType);
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
index 0a53eb0..f770b6a 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
@@ -20,7 +20,6 @@
 import static com.android.server.telecom.CallAudioRouteAdapter.BT_ACTIVE_DEVICE_GONE;
 import static com.android.server.telecom.CallAudioRouteAdapter.BT_ACTIVE_DEVICE_PRESENT;
 import static com.android.server.telecom.CallAudioRouteAdapter.BT_AUDIO_CONNECTED;
-import static com.android.server.telecom.CallAudioRouteAdapter.BT_AUDIO_DISCONNECTED;
 import static com.android.server.telecom.CallAudioRouteAdapter.BT_DEVICE_ADDED;
 import static com.android.server.telecom.CallAudioRouteAdapter.BT_DEVICE_REMOVED;
 import static com.android.server.telecom.CallAudioRouteAdapter.CONNECT_DOCK;
@@ -35,7 +34,7 @@
 import static com.android.server.telecom.CallAudioRouteAdapter.SPEAKER_ON;
 import static com.android.server.telecom.CallAudioRouteAdapter.STREAMING_FORCE_DISABLED;
 import static com.android.server.telecom.CallAudioRouteAdapter.STREAMING_FORCE_ENABLED;
-import static com.android.server.telecom.CallAudioRouteAdapter.SWITCH_EARPIECE;
+import static com.android.server.telecom.CallAudioRouteAdapter.SWITCH_BLUETOOTH;
 import static com.android.server.telecom.CallAudioRouteAdapter.SWITCH_FOCUS;
 import static com.android.server.telecom.CallAudioRouteAdapter.USER_SWITCH_BLUETOOTH;
 import static com.android.server.telecom.CallAudioRouteAdapter.USER_SWITCH_EARPIECE;
@@ -48,16 +47,17 @@
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.nullable;
 import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.eq;
-import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeAudio;
 import android.media.AudioDeviceInfo;
 import android.media.AudioManager;
 import android.media.IAudioService;
@@ -71,7 +71,9 @@
 import com.android.server.telecom.CallAudioManager;
 import com.android.server.telecom.CallAudioRouteController;
 import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.StatusBarNotifier;
 import com.android.server.telecom.WiredHeadsetManager;
+import com.android.server.telecom.bluetooth.BluetoothDeviceManager;
 import com.android.server.telecom.bluetooth.BluetoothRouteManager;
 
 import org.junit.After;
@@ -82,6 +84,7 @@
 import org.mockito.Mock;
 
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 
 @RunWith(JUnit4.class)
@@ -94,6 +97,11 @@
     @Mock CallAudioManager.AudioServiceFactory mAudioServiceFactory;
     @Mock IAudioService mAudioService;
     @Mock BluetoothRouteManager mBluetoothRouteManager;
+    @Mock BluetoothDeviceManager mBluetoothDeviceManager;
+    @Mock BluetoothAdapter mBluetoothAdapter;
+    @Mock StatusBarNotifier mockStatusBarNotifier;
+    @Mock AudioDeviceInfo mAudioDeviceInfo;
+    @Mock BluetoothLeAudio mBluetoothLeAudio;
     private AudioRoute mEarpieceRoute;
     private AudioRoute mSpeakerRoute;
     private static final String BT_ADDRESS_1 = "00:00:00:00:00:01";
@@ -109,7 +117,7 @@
         @Override
         public AudioRoute create(@AudioRoute.AudioRouteType int type, String bluetoothAddress,
                                  AudioManager audioManager) {
-            return new AudioRoute(type, bluetoothAddress, null);
+            return new AudioRoute(type, bluetoothAddress, mAudioDeviceInfo);
         }
     };
 
@@ -124,18 +132,35 @@
                 });
         when(mAudioManager.getPreferredDeviceForStrategy(nullable(AudioProductStrategy.class)))
                 .thenReturn(null);
+        when(mAudioManager.getAvailableCommunicationDevices())
+                .thenReturn(List.of(mAudioDeviceInfo));
+        when(mAudioManager.getCommunicationDevice()).thenReturn(mAudioDeviceInfo);
+        when(mAudioManager.setCommunicationDevice(any(AudioDeviceInfo.class)))
+                .thenReturn(true);
         when(mAudioServiceFactory.getAudioService()).thenReturn(mAudioService);
         when(mContext.getAttributionTag()).thenReturn("");
         doNothing().when(mCallsManager).onCallAudioStateChanged(any(CallAudioState.class),
                 any(CallAudioState.class));
         when(mCallsManager.getCurrentUserHandle()).thenReturn(
                 new UserHandle(UserHandle.USER_SYSTEM));
+        when(mBluetoothRouteManager.getDeviceManager()).thenReturn(mBluetoothDeviceManager);
+        when(mBluetoothDeviceManager.connectAudio(any(BluetoothDevice.class), anyInt()))
+                .thenReturn(true);
+        when(mBluetoothDeviceManager.getBluetoothAdapter()).thenReturn(mBluetoothAdapter);
+        when(mBluetoothAdapter.getActiveDevices(anyInt())).thenReturn(List.of(BLUETOOTH_DEVICE_1));
+        when(mBluetoothDeviceManager.getLeAudioService()).thenReturn(mBluetoothLeAudio);
+        when(mBluetoothLeAudio.getGroupId(any(BluetoothDevice.class))).thenReturn(1);
+        when(mBluetoothLeAudio.getConnectedGroupLeadDevice(anyInt()))
+                .thenReturn(BLUETOOTH_DEVICE_1);
+        when(mAudioDeviceInfo.getAddress()).thenReturn(BT_ADDRESS_1);
         mController = new CallAudioRouteController(mContext, mCallsManager, mAudioServiceFactory,
-                mAudioRouteFactory, mWiredHeadsetManager, mBluetoothRouteManager);
+                mAudioRouteFactory, mWiredHeadsetManager,
+                mBluetoothRouteManager, mockStatusBarNotifier, mFeatureFlags);
         mController.setAudioRouteFactory(mAudioRouteFactory);
         mController.setAudioManager(mAudioManager);
         mEarpieceRoute = new AudioRoute(AudioRoute.TYPE_EARPIECE, null, null);
         mSpeakerRoute = new AudioRoute(AudioRoute.TYPE_SPEAKER, null, null);
+        when(mFeatureFlags.ignoreAutoRouteToWatchDevice()).thenReturn(false);
     }
 
     @After
@@ -166,44 +191,6 @@
 
     @SmallTest
     @Test
-    public void testActivateAndRemoveBluetoothDeviceDuringCall() {
-        doAnswer(invocation -> {
-            mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0, BLUETOOTH_DEVICE_1);
-            return true;
-        }).when(mAudioManager).setCommunicationDevice(nullable(AudioDeviceInfo.class));
-
-        mController.initialize();
-        mController.setActive(true);
-        mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
-                BLUETOOTH_DEVICE_1);
-        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
-                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
-                        | CallAudioState.ROUTE_SPEAKER, null, BLUETOOTH_DEVICES);
-        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
-                any(CallAudioState.class), eq(expectedState));
-
-        mController.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
-                AudioRoute.TYPE_BLUETOOTH_SCO, BT_ADDRESS_1);
-        verify(mAudioManager, timeout(TEST_TIMEOUT)).setCommunicationDevice(
-                nullable(AudioDeviceInfo.class));
-
-        expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
-                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
-                        | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
-        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
-                any(CallAudioState.class), eq(expectedState));
-
-        mController.sendMessageWithSessionInfo(BT_DEVICE_REMOVED, AudioRoute.TYPE_BLUETOOTH_SCO,
-                BLUETOOTH_DEVICE_1);
-        expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
-                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
-                new HashSet<>());
-        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
-                any(CallAudioState.class), eq(expectedState));
-    }
-
-    @SmallTest
-    @Test
     public void testActiveDeactivateBluetoothDevice() {
         mController.initialize();
         mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
@@ -229,14 +216,6 @@
     @SmallTest
     @Test
     public void testSwitchFocusForBluetoothDeviceSupportInbandRinging() {
-        doAnswer(invocation -> {
-            mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0, BLUETOOTH_DEVICE_1);
-            return true;
-        }).when(mAudioManager).setCommunicationDevice(nullable(AudioDeviceInfo.class));
-        doAnswer(invocation -> {
-            mController.sendMessageWithSessionInfo(BT_AUDIO_DISCONNECTED, 0, BLUETOOTH_DEVICE_1);
-            return true;
-        }).when(mAudioManager).clearCommunicationDevice();
         when(mBluetoothRouteManager.isInbandRingEnabled(eq(BLUETOOTH_DEVICE_1))).thenReturn(true);
 
         mController.initialize();
@@ -253,15 +232,15 @@
         assertFalse(mController.isActive());
 
         mController.sendMessageWithSessionInfo(SWITCH_FOCUS, RINGING_FOCUS);
-        verify(mAudioManager, timeout(TEST_TIMEOUT)).setCommunicationDevice(
-                nullable(AudioDeviceInfo.class));
+        verify(mBluetoothDeviceManager, timeout(TEST_TIMEOUT))
+                .connectAudio(BLUETOOTH_DEVICE_1, AudioRoute.TYPE_BLUETOOTH_SCO);
         assertTrue(mController.isActive());
 
         mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS);
         assertTrue(mController.isActive());
 
         mController.sendMessageWithSessionInfo(SWITCH_FOCUS, NO_FOCUS);
-        verify(mAudioManager, timeout(TEST_TIMEOUT)).clearCommunicationDevice();
+        verify(mBluetoothDeviceManager, timeout(TEST_TIMEOUT).atLeastOnce()).disconnectSco();
         assertFalse(mController.isActive());
     }
 
@@ -480,4 +459,191 @@
         verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
                 any(CallAudioState.class), eq(expectedState));
     }
+
+    @SmallTest
+    @Test
+    public void testIgnoreAutoRouteToWatch() {
+        when(mFeatureFlags.ignoreAutoRouteToWatchDevice()).thenReturn(true);
+        when(mBluetoothRouteManager.isWatch(any(BluetoothDevice.class))).thenReturn(true);
+
+        mController.initialize();
+        mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
+                BLUETOOTH_DEVICE_1);
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, null, BLUETOOTH_DEVICES);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        // Connect wired headset.
+        mController.sendMessageWithSessionInfo(CONNECT_WIRED_HEADSET);
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_WIRED_HEADSET,
+                CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER
+                        | CallAudioState.ROUTE_BLUETOOTH, null, BLUETOOTH_DEVICES);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        // Disconnect wired headset and ensure Telecom routes to earpiece instead of the BT route.
+        mController.sendMessageWithSessionInfo(DISCONNECT_WIRED_HEADSET);
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER
+                        | CallAudioState.ROUTE_BLUETOOTH, null , BLUETOOTH_DEVICES);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+    }
+
+    @SmallTest
+    @Test
+    public void testConnectDisconnectScoDuringCall() {
+        verifyConnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_SCO);
+        verifyDisconnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_SCO);
+    }
+
+    @SmallTest
+    @Test
+    public void testConnectAndDisconnectLeDeviceDuringCall() {
+        when(mBluetoothLeAudio.getConnectedGroupLeadDevice(anyInt()))
+                .thenReturn(BLUETOOTH_DEVICE_1);
+        verifyConnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_LE);
+        verifyDisconnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_LE);
+    }
+
+    @SmallTest
+    @Test
+    public void testConnectAndDisconnectHearingAidDuringCall() {
+        verifyConnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_HA);
+        verifyDisconnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_HA);
+    }
+
+    @SmallTest
+    @Test
+    public void testSwitchBetweenLeAndScoDevices() {
+        when(mBluetoothLeAudio.getConnectedGroupLeadDevice(anyInt()))
+                .thenReturn(BLUETOOTH_DEVICE_1);
+        verifyConnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_LE);
+        BluetoothDevice scoDevice =
+                BluetoothRouteManagerTest.makeBluetoothDevice("00:00:00:00:00:03");
+        BLUETOOTH_DEVICES.add(scoDevice);
+
+        // Add SCO device.
+        mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
+                scoDevice);
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        // Switch to SCO and verify active device is updated.
+        mController.sendMessageWithSessionInfo(USER_SWITCH_BLUETOOTH, 0, scoDevice.getAddress());
+        mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0, scoDevice);
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, scoDevice, BLUETOOTH_DEVICES);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        // Disconnect SCO and verify audio routed back to LE audio.
+        BLUETOOTH_DEVICES.remove(scoDevice);
+        mController.sendMessageWithSessionInfo(BT_DEVICE_REMOVED, AudioRoute.TYPE_BLUETOOTH_SCO,
+                scoDevice);
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+    }
+
+    @SmallTest
+    @Test
+    public void testFallbackWhenBluetoothConnectionFails() {
+        when(mBluetoothDeviceManager.connectAudio(any(BluetoothDevice.class), anyInt()))
+                .thenReturn(false);
+
+        AudioDeviceInfo mockAudioDeviceInfo = mock(AudioDeviceInfo.class);
+        when(mAudioManager.getCommunicationDevice()).thenReturn(mockAudioDeviceInfo);
+        verifyConnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_LE);
+        BluetoothDevice scoDevice =
+                BluetoothRouteManagerTest.makeBluetoothDevice("00:00:00:00:00:03");
+        BLUETOOTH_DEVICES.add(scoDevice);
+
+        // Add SCO device.
+        mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
+                scoDevice);
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        // Switch to SCO but reject connection and make sure audio is routed back to LE device.
+        mController.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
+                AudioRoute.TYPE_BLUETOOTH_SCO, scoDevice.getAddress());
+        verify(mBluetoothDeviceManager, timeout(TEST_TIMEOUT))
+                .connectAudio(scoDevice, AudioRoute.TYPE_BLUETOOTH_SCO);
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+        verify(mCallsManager, timeout(TEST_TIMEOUT).atLeastOnce()).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        // Cleanup supported devices for next test
+        BLUETOOTH_DEVICES.remove(scoDevice);
+    }
+
+    private void verifyConnectBluetoothDevice(int audioType) {
+        mController.initialize();
+        mController.setActive(true);
+
+        mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, audioType, BLUETOOTH_DEVICE_1);
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, null, BLUETOOTH_DEVICES);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        mController.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT, audioType, BT_ADDRESS_1);
+        if (audioType == AudioRoute.TYPE_BLUETOOTH_SCO) {
+            verify(mBluetoothDeviceManager, timeout(TEST_TIMEOUT))
+                    .connectAudio(BLUETOOTH_DEVICE_1, AudioRoute.TYPE_BLUETOOTH_SCO);
+            mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED,
+                    0, BLUETOOTH_DEVICE_1);
+        } else {
+            verify(mAudioManager, timeout(TEST_TIMEOUT))
+                    .setCommunicationDevice(nullable(AudioDeviceInfo.class));
+        }
+
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        // Test hearing aid pair and ensure second device isn't added as a route
+        if (audioType == AudioRoute.TYPE_BLUETOOTH_HA) {
+            BluetoothDevice hearingAidDevice2 =
+                    BluetoothRouteManagerTest.makeBluetoothDevice("00:00:00:00:00:02");
+            mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, audioType, hearingAidDevice2);
+            expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+                    CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                            | CallAudioState.ROUTE_SPEAKER, null, BLUETOOTH_DEVICES);
+            // Verify that supported BT devices only shows the first connected hearing aid device.
+            verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                    any(CallAudioState.class), eq(expectedState));
+        }
+    }
+
+    private void verifyDisconnectBluetoothDevice(int audioType) {
+        mController.sendMessageWithSessionInfo(BT_DEVICE_REMOVED, audioType, BLUETOOTH_DEVICE_1);
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        if (audioType == AudioRoute.TYPE_BLUETOOTH_SCO) {
+            verify(mBluetoothDeviceManager, timeout(TEST_TIMEOUT)).disconnectSco();
+        } else {
+            verify(mAudioManager, timeout(TEST_TIMEOUT)).clearCommunicationDevice();
+        }
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+    }
 }