Resolve multiple BT device routing issues

This CL mainly resolves an issue found where routing needs to be updated
to inactive when we connect a second BT device during ringing and as a
result, in-band ringing is disabled. This was causing audio routing to
continually jump between both BT devices and the requests were failing.

The second is to ensure that we only send BT_AUDIO_CONNECTED if the
device that connected is SCO audio connected and to also ensure that we
refrain from disconnecting SCO where we are routing between the same BT
device types and the destination route is already connected to SCO. The
disconnect is done to clean up the original route but this will
inadvertently disconnect SCO for the destination route device if BT
stack has already reported that the device is connected to SCO.

Bug: 405071163
Flag: EXEMPT bugfix
Test: Manual test to connect second HFP device while incoming call is
ringing and answering call to verify that call audio is properly routed
to the active BT device.
Test: atest CallAudioRouteController
(cherry picked from https://googleplex-android-review.googlesource.com/q/commit:28296d0857f73fb02911dc2a9aad2510e8adf157)
Merged-In: If95b44ffcfee3076528a9336838ecaa61e66a694
Change-Id: If95b44ffcfee3076528a9336838ecaa61e66a694
diff --git a/src/com/android/server/telecom/AudioRoute.java b/src/com/android/server/telecom/AudioRoute.java
index bcc0b59..2b03182 100644
--- a/src/com/android/server/telecom/AudioRoute.java
+++ b/src/com/android/server/telecom/AudioRoute.java
@@ -256,26 +256,26 @@
     // Invoked when entered pending route whose dest route is this route
     void onDestRouteAsPendingRoute(boolean active, PendingAudioRoute pendingAudioRoute,
             BluetoothDevice device, AudioManager audioManager,
-            BluetoothRouteManager bluetoothRouteManager, boolean isScoAudioConnected) {
-        Log.i(this, "onDestRouteAsPendingRoute: active (%b), type (%s)", active,
-                DEVICE_TYPE_STRINGS.get(mAudioRouteType));
+            BluetoothRouteManager bluetoothRouteManager, boolean isScoAlreadyConnected) {
+        Log.i(this, "onDestRouteAsPendingRoute: active (%b), type (%s), isScoAlreadyConnected(%s)",
+                active, DEVICE_TYPE_STRINGS.get(mAudioRouteType), isScoAlreadyConnected);
         if (pendingAudioRoute.isActive() && !active) {
             clearCommunicationDevice(pendingAudioRoute, bluetoothRouteManager, audioManager);
         } else if (active) {
             // Handle BT routing case.
             if (BT_AUDIO_ROUTE_TYPES.contains(mAudioRouteType)) {
+                // Check if the communication device was set for the device, even if
+                // BluetoothHeadset#connectAudio reports that the SCO connection wasn't
+                // successfully established.
                 boolean connectedBtAudio = connectBtAudio(pendingAudioRoute, device,
-                        audioManager, bluetoothRouteManager);
+                        audioManager, bluetoothRouteManager, isScoAlreadyConnected);
                 // Special handling for SCO case.
                 if (!mIsScoManagedByAudio && mAudioRouteType == TYPE_BLUETOOTH_SCO) {
                     // Set whether the dest route is for the watch
                     mIsDestRouteForWatch = bluetoothRouteManager.isWatch(device);
-                    // Check if the communication device was set for the device, even if
-                    // BluetoothHeadset#connectAudio reports that the SCO connection wasn't
-                    // successfully established.
-                    if (connectedBtAudio || isScoAudioConnected) {
+                    if (connectedBtAudio || isScoAlreadyConnected) {
                         pendingAudioRoute.setCommunicationDeviceType(mAudioRouteType);
-                        if (!isScoAudioConnected) {
+                        if (!isScoAlreadyConnected) {
                             pendingAudioRoute.addMessage(BT_AUDIO_CONNECTED, mBluetoothAddress);
                         }
                     } else {
@@ -311,7 +311,8 @@
                     result = audioManager.setCommunicationDevice(mInfo);
                     if (result) {
                         pendingAudioRoute.setCommunicationDeviceType(mAudioRouteType);
-                        if (mAudioRouteType == TYPE_BLUETOOTH_SCO && !isScoAudioConnected
+                        if (mAudioRouteType == TYPE_BLUETOOTH_SCO
+                                && !isScoAlreadyConnected
                                 && mIsScoManagedByAudio) {
                             pendingAudioRoute.addMessage(BT_AUDIO_CONNECTED, mBluetoothAddress);
                         }
@@ -341,10 +342,12 @@
      * @param bluetoothRouteManager The BT route manager.
      */
     void onOrigRouteAsPendingRoute(boolean wasActive, PendingAudioRoute pendingAudioRoute,
-            AudioManager audioManager, BluetoothRouteManager bluetoothRouteManager) {
-        Log.i(this, "onOrigRouteAsPendingRoute: wasActive (%b), type (%s), pending(%s)", wasActive,
-                DEVICE_TYPE_STRINGS.get(mAudioRouteType), pendingAudioRoute);
-        if (wasActive) {
+            AudioManager audioManager, BluetoothRouteManager bluetoothRouteManager,
+            boolean isScoAlreadyConnected) {
+        Log.i(this, "onOrigRouteAsPendingRoute: wasActive (%b), type (%s), pending(%s),"
+                + "isScoAlreadyConnected(%s)", wasActive, DEVICE_TYPE_STRINGS.get(mAudioRouteType),
+                pendingAudioRoute, isScoAlreadyConnected);
+        if (wasActive && !isScoAlreadyConnected) {
             int result = clearCommunicationDevice(pendingAudioRoute, bluetoothRouteManager,
                     audioManager);
             if (mAudioRouteType == TYPE_SPEAKER) {
@@ -395,11 +398,12 @@
     }
 
     private boolean connectBtAudio(PendingAudioRoute pendingAudioRoute, BluetoothDevice device,
-            AudioManager audioManager, BluetoothRouteManager bluetoothRouteManager) {
+            AudioManager audioManager, BluetoothRouteManager bluetoothRouteManager,
+            boolean isScoAlreadyConnected) {
         // Ensure that if another BT device was set, it is disconnected before connecting
         // the new one.
         AudioRoute currentRoute = pendingAudioRoute.getOrigRoute();
-        if (currentRoute.getBluetoothAddress() != null &&
+        if (!isScoAlreadyConnected && currentRoute.getBluetoothAddress() != null &&
                 !currentRoute.getBluetoothAddress().equals(device.getAddress())) {
             clearCommunicationDevice(pendingAudioRoute, bluetoothRouteManager, audioManager);
         }
diff --git a/src/com/android/server/telecom/CallAudioRouteController.java b/src/com/android/server/telecom/CallAudioRouteController.java
index 2de894b..c9e0ab3 100644
--- a/src/com/android/server/telecom/CallAudioRouteController.java
+++ b/src/com/android/server/telecom/CallAudioRouteController.java
@@ -118,7 +118,7 @@
     private FeatureFlags mFeatureFlags;
     private int mFocusType;
     private int mCallSupportedRouteMask = -1;
-    private boolean mIsScoAudioConnected;
+    private BluetoothDevice mScoAudioConnectedDevice;
     private boolean mAvailableRoutesUpdated;
     private boolean mUsePreferredDeviceStrategy;
     private AudioDeviceInfo mCurrentCommunicationDevice;
@@ -209,7 +209,7 @@
         mFeatureFlags = featureFlags;
         mMetricsController = metricsController;
         mFocusType = NO_FOCUS;
-        mIsScoAudioConnected = false;
+        mScoAudioConnectedDevice = null;
         mUsePreferredDeviceStrategy = true;
         mWasOnSpeaker = false;
         setCurrentCommunicationDevice(null);
@@ -570,7 +570,7 @@
         return mIsPending;
     }
 
-    private void routeTo(boolean active, AudioRoute destRoute) {
+    private void routeTo(boolean isDestRouteActive, AudioRoute destRoute) {
         if (destRoute == null || (!destRoute.equals(mStreamingRoute)
                 && !getCallSupportedRoutes().contains(destRoute))) {
             Log.i(this, "Ignore routing to unavailable route: %s", destRoute);
@@ -580,42 +580,53 @@
             }
             return;
         }
+        // If another BT device connects during RINGING_FOCUS, in-band ringing will be disabled by
+        // default. In this case, we should adjust the active routing value so that we don't try
+        // to connect to the BT device as it will fail.
+        isDestRouteActive = maybeAdjustActiveRouting(destRoute, isDestRouteActive);
+        // It's possible that there are multiple HFP devices connected and if we receive SCO audio
+        // connected for the destination route's BT device, then we shouldn't disconnect SCO when
+        // clearing the communication device for the original route if it was also a HFP device.
+        boolean isScoDeviceAlreadyConnected = mScoAudioConnectedDevice != null
+                && Objects.equals(mScoAudioConnectedDevice, mBluetoothRoutes.get(destRoute));
         if (mIsPending) {
-            if (destRoute.equals(mPendingAudioRoute.getDestRoute()) && (mIsActive == active)) {
+            if (destRoute.equals(mPendingAudioRoute.getDestRoute())
+                    && (mIsActive == isDestRouteActive)) {
                 return;
             }
             Log.i(this, "Override current pending route destination from %s(active=%b) to "
                             + "%s(active=%b)",
-                    mPendingAudioRoute.getDestRoute(), mIsActive, destRoute, active);
+                    mPendingAudioRoute.getDestRoute(), mIsActive, destRoute, isDestRouteActive);
             // Ensure we don't keep waiting for SPEAKER_ON if dest route gets overridden.
-            if (!mFeatureFlags.resolveActiveBtRoutingAndBtTimingIssue() && active
+            if (!mFeatureFlags.resolveActiveBtRoutingAndBtTimingIssue() && isDestRouteActive
                     && mPendingAudioRoute.getDestRoute().getType() == TYPE_SPEAKER) {
                 mPendingAudioRoute.clearPendingMessage(new Pair<>(SPEAKER_ON, null));
             }
             // override pending route while keep waiting for still pending messages for the
             // previous pending route
             mPendingAudioRoute.setOrigRoute(mIsActive /* origin */,
-                    mPendingAudioRoute.getDestRoute(), active /* dest */);
+                    mPendingAudioRoute.getDestRoute(), isDestRouteActive /* dest */,
+                    isScoDeviceAlreadyConnected);
         } else {
-            if (mCurrentRoute.equals(destRoute) && (mIsActive == active)) {
+            if (mCurrentRoute.equals(destRoute) && (mIsActive == isDestRouteActive)) {
                 return;
             }
             Log.i(this, "Enter pending route, orig%s(active=%b), dest%s(active=%b)", mCurrentRoute,
-                    mIsActive, destRoute, active);
+                    mIsActive, destRoute, isDestRouteActive);
             // route to pending route
             if (getCallSupportedRoutes().contains(mCurrentRoute)) {
                 mPendingAudioRoute.setOrigRoute(mIsActive /* origin */, mCurrentRoute,
-                        active /* dest */);
+                        isDestRouteActive /* dest */, isScoDeviceAlreadyConnected);
             } else {
                 // Avoid waiting for pending messages for an unavailable route
                 mPendingAudioRoute.setOrigRoute(mIsActive /* origin */, DUMMY_ROUTE,
-                        active /* dest */);
+                        isDestRouteActive /* dest */, isScoDeviceAlreadyConnected);
             }
             mIsPending = true;
         }
-        mPendingAudioRoute.setDestRoute(active, destRoute, mBluetoothRoutes.get(destRoute),
-                mIsScoAudioConnected);
-        mIsActive = active;
+        mPendingAudioRoute.setDestRoute(isDestRouteActive, destRoute,
+                mBluetoothRoutes.get(destRoute), isScoDeviceAlreadyConnected);
+        mIsActive = isDestRouteActive;
         mPendingAudioRoute.evaluatePendingState();
         if (mFeatureFlags.telecomMetricsSupport()) {
             mMetricsController.getAudioRouteStats().onRouteEnter(mPendingAudioRoute);
@@ -1077,10 +1088,10 @@
             String btAddressToExclude) {
         Log.i(this, "handleSwitchBaselineRoute: includeBluetooth: %b, "
                 + "btAddressToExclude: %s", includeBluetooth, btAddressToExclude);
+        AudioRoute pendingDestRoute = mPendingAudioRoute.getDestRoute();
         boolean areExcludedBtAndDestBtSame = btAddressToExclude != null
-                && mPendingAudioRoute.getDestRoute() != null
-                && Objects.equals(btAddressToExclude, mPendingAudioRoute.getDestRoute()
-                .getBluetoothAddress());
+                && pendingDestRoute != null
+                && Objects.equals(btAddressToExclude, pendingDestRoute.getBluetoothAddress());
         Pair<Integer, String> btDevicePendingMsg =
                 new Pair<>(BT_AUDIO_CONNECTED, btAddressToExclude);
 
@@ -1088,8 +1099,8 @@
         // we know that the device has reconnected or is in the middle of connecting. Ignore routing
         // out of this BT device.
         boolean isExcludedDeviceConnectingOrConnected = areExcludedBtAndDestBtSame
-                && (mIsScoAudioConnected || mPendingAudioRoute.getPendingMessages()
-                .contains(btDevicePendingMsg));
+                && (Objects.equals(mBluetoothRoutes.get(pendingDestRoute), mScoAudioConnectedDevice)
+                || mPendingAudioRoute.getPendingMessages().contains(btDevicePendingMsg));
         // Check if the pending audio route or current route is already different from the route
         // including the BT device that should be excluded from route selection.
         boolean isCurrentOrDestRouteDifferent = btAddressToExclude != null
@@ -1659,8 +1670,9 @@
         mIsPending = isPending;
     }
 
-    public void setIsScoAudioConnected(boolean value) {
-        mIsScoAudioConnected = value;
+    @VisibleForTesting
+    public void setScoAudioConnectedDevice(BluetoothDevice device) {
+        mScoAudioConnectedDevice = device;
     }
 
     private void clearRingingBluetoothAddress() {
@@ -1773,4 +1785,31 @@
             mWasOnSpeaker = false;
         }
     }
+
+    /*
+     * Adjusts routing to go inactive if we're active in the case that we're processing
+     * RINGING_FOCUS and another BT headset is connected which causes in-band ringing to get
+     * disabled. If we stay in active routing, Telecom will send requests to connect to these BT
+     * devices while the call is ringing and each of these requests will fail at the BT stack side.
+     * By default, in-band ringtone is disabled when more than one BT device is paired. Instead,
+     * ringtone is played using the headset's default ringtone.
+     */
+    private boolean maybeAdjustActiveRouting(AudioRoute destRoute, boolean isDestRouteActive) {
+        BluetoothDevice device = mBluetoothRoutes.get(destRoute);
+        // If routing is active and in-band ringing is disabled while the call is ringing, move to
+        // inactive routing.
+        if (isDestRouteActive && mFocusType == RINGING_FOCUS && device != null
+                && !mBluetoothRouteManager.isInbandRingEnabled(destRoute.getType(), device)) {
+            return false;
+        }
+        else if (!isDestRouteActive && mFocusType == RINGING_FOCUS && (device == null
+                || mBluetoothRouteManager.isInbandRingEnabled(destRoute.getType(), device))) {
+            // If the routing is inactive while the call is ringing and we re-evaluate this to find
+            // that we're routing to a non-BT device or a BT device that does support in-band
+            // ringing, then re-enable active routing (i.e. second HFP headset is disconnected
+            // while call is ringing).
+            return true;
+        }
+        return isDestRouteActive;
+    }
 }
diff --git a/src/com/android/server/telecom/PendingAudioRoute.java b/src/com/android/server/telecom/PendingAudioRoute.java
index dde1d8d..37c70ad 100644
--- a/src/com/android/server/telecom/PendingAudioRoute.java
+++ b/src/com/android/server/telecom/PendingAudioRoute.java
@@ -83,10 +83,11 @@
      * @param origRoute The origin.
      * @param isDestActive Whether the destination will be active.
      */
-    void setOrigRoute(boolean isOriginActive, AudioRoute origRoute, boolean isDestActive) {
+    void setOrigRoute(boolean isOriginActive, AudioRoute origRoute, boolean isDestActive,
+            boolean isScoAlreadyConnected) {
         mActive = isDestActive;
         origRoute.onOrigRouteAsPendingRoute(isOriginActive, this, mAudioManager,
-                mBluetoothRouteManager);
+                mBluetoothRouteManager, isScoAlreadyConnected);
         mOrigRoute = origRoute;
     }
 
@@ -95,9 +96,9 @@
     }
 
     void setDestRoute(boolean active, AudioRoute destRoute, BluetoothDevice device,
-            boolean isScoAudioConnected) {
+            boolean isScoAlreadyConnected) {
         destRoute.onDestRouteAsPendingRoute(active, this, device,
-                mAudioManager, mBluetoothRouteManager, isScoAudioConnected);
+                mAudioManager, mBluetoothRouteManager, isScoAlreadyConnected);
         mActive = active;
         mDestRoute = destRoute;
     }
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java b/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
index 1330be4..0478fdc 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
@@ -52,6 +52,8 @@
 import com.android.server.telecom.CallAudioRouteController;
 import com.android.server.telecom.flags.FeatureFlags;
 
+import java.util.Objects;
+
 public class BluetoothStateReceiver extends BroadcastReceiver {
     private static final String LOG_TAG = BluetoothStateReceiver.class.getSimpleName();
     public static final IntentFilter INTENT_FILTER;
@@ -124,16 +126,17 @@
                 if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
                     CallAudioRouteController audioRouteController =
                             (CallAudioRouteController) mCallAudioRouteAdapter;
-                    audioRouteController.setIsScoAudioConnected(true);
-                    if (audioRouteController.isPending()) {
+                    audioRouteController.setScoAudioConnectedDevice(device);
+                    AudioRoute btRoute = audioRouteController.getBluetoothRoute(
+                            AudioRoute.TYPE_BLUETOOTH_SCO, device.getAddress());
+                    if (audioRouteController.isPending() && Objects.equals(audioRouteController
+                            .getPendingAudioRoute().getDestRoute(), btRoute)) {
                         mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0,
                                 device);
                     } else {
                         // It's possible that the initial BT connection fails but BT_AUDIO_CONNECTED
                         // is sent later, indicating that SCO audio is on. We should route
                         // appropriately in order for the UI to reflect this state.
-                        AudioRoute btRoute = audioRouteController.getBluetoothRoute(
-                                AudioRoute.TYPE_BLUETOOTH_SCO, device.getAddress());
                         if (btRoute != null) {
                             audioRouteController.getPendingAudioRoute().overrideDestRoute(btRoute);
                             audioRouteController.overrideIsPending(true);
@@ -155,7 +158,7 @@
                 if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
                     CallAudioRouteController audioRouteController =
                             (CallAudioRouteController) mCallAudioRouteAdapter;
-                    audioRouteController.setIsScoAudioConnected(false);
+                    audioRouteController.setScoAudioConnectedDevice(null);
                     if (audioRouteController.isPending()) {
                         mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_AUDIO_DISCONNECTED, 0,
                                 device);
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
index b31143a..f6f1686 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
@@ -1116,7 +1116,7 @@
         mController.sendMessageWithSessionInfo(SWITCH_BASELINE_ROUTE,
                 INCLUDE_BLUETOOTH_IN_BASELINE, BLUETOOTH_DEVICE_1.getAddress());
         // Process BT_AUDIO_CONNECTED from connecting to BT device in active focus request.
-        mController.setIsScoAudioConnected(true);
+        mController.setScoAudioConnectedDevice(BLUETOOTH_DEVICE_1);
         mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0, BLUETOOTH_DEVICE_1);
         // Verify SCO not disconnected and route stays on connected BT device.
         verify(mBluetoothDeviceManager, timeout(TEST_TIMEOUT).times(0)).disconnectSco();
@@ -1419,6 +1419,81 @@
                 any(CallAudioState.class), eq(expectedState));
     }
 
+    @Test
+    @SmallTest
+    public void testRouteToInactiveWhenInbandRingingDisabledDuringRinging() {
+        when(mBluetoothRouteManager.isInbandRingEnabled(eq(AudioRoute.TYPE_BLUETOOTH_SCO),
+                eq(BLUETOOTH_DEVICE_1))).thenReturn(true);
+        verifyConnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_SCO);
+        mController.sendMessageWithSessionInfo(SWITCH_FOCUS, RINGING_FOCUS, 0);
+        assertTrue(mController.isActive());
+
+        // Connect another HFP device while call is still ringing
+        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));
+
+        when(mBluetoothRouteManager.isInbandRingEnabled(eq(AudioRoute.TYPE_BLUETOOTH_SCO),
+                any(BluetoothDevice.class))).thenReturn(false);
+        // Emulate second device becoming active and first device getting disconnected as in-band
+        // ringing is disabled.
+        mController.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
+                AudioRoute.TYPE_BLUETOOTH_SCO, scoDevice.getAddress());
+        mController.sendMessageWithSessionInfo(BT_AUDIO_DISCONNECTED, 0,
+                BLUETOOTH_DEVICE_1);
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, scoDevice, BLUETOOTH_DEVICES);
+        // Verify routing goes to the new HFP device but that the routing is now inactive.
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+        assertFalse(mController.isActive());
+    }
+
+    @Test
+    @SmallTest
+    public void testSkipConnectBluetoothWhenScoAudioAlreadyConnected() {
+        verifyConnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_SCO);
+        // Connect another HFP device while call is still ringing
+        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));
+
+        // Emulate scenario where BT stack signals SCO audio connected for the second HFP device
+        // before Telecom finishes processing the route change to this device. We should ensure
+        // that we don't accidentally disconnect SCO in this case (thinking that we're disconnecting
+        // the first HFP device).
+        mController.setScoAudioConnectedDevice(scoDevice);
+        mController.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
+                AudioRoute.TYPE_BLUETOOTH_SCO, scoDevice.getAddress());
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, scoDevice, BLUETOOTH_DEVICES);
+        // Verify routing goes to the new HFP device and we never disconnect SCO when clearing the
+        // original pending route.
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+        verify(mBluetoothDeviceManager, timeout(TEST_TIMEOUT).times(0)).disconnectSco();
+    }
+
     private void verifyConnectBluetoothDevice(int audioType) {
         mController.initialize();
         mController.setActive(true);