Resolve CallAudioRouteController issues.

This CL resolves recent bugs that have been filed around the refactored
audio route changes based on the initial fix that was put up. Namely, it
takes care of a stack overflow issue with SPEAKER_ON, resolves a NPE
with the LE audio service being unavailable, handles late SCO audio
connected messages received via BluetoothStateReceiver, handles removing
BT routes when there's a service disconnect from the
BluetoothProfileServiceListener, avoids processing duplicate pending
messages in PendingAudioRoute, ensures that video calls are defaulted
to speaker (unless a wired headset or BT device is connected), ensures
that mute functionality is working as intended both during and at the
end of a call (reset mute), and in the case that BT_AUDIO_CONNECTED is
processed before the HFP device is connected, ensures that we don't wait
to process it again.

There are still known issues around WhatsApp calls that occur as a
result of timing issues that are due to how the new code is structured
which will be investigated in a subsequent CL. Use cases involving
multiple BT devices have not been verified as there is currently an
issue with the bluetooth stack that is blocking Telecom, which is also
being investigated.

Bug: 328287261
Test: Manual tests with single BT scenarios (LE/HFP) in simple audio
routing switching cases as well as toggling on/off bluetooth and
disconnecting/reconnecting device during oncall. Also ran through MO/MT
cases to ensure that BT audio is connected accordingly during subsequent
calls.
Test: atest CallAudioRouteControllerTest

Change-Id: Ief1f0fe5e0b311594087414ff6a9084063468c73
diff --git a/src/com/android/server/telecom/AudioRoute.java b/src/com/android/server/telecom/AudioRoute.java
index 7b593d7..8a5e858 100644
--- a/src/com/android/server/telecom/AudioRoute.java
+++ b/src/com/android/server/telecom/AudioRoute.java
@@ -28,6 +28,7 @@
 import android.media.AudioDeviceInfo;
 import android.media.AudioManager;
 import android.telecom.Log;
+import android.util.Pair;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.telecom.bluetooth.BluetoothRouteManager;
@@ -226,7 +227,7 @@
         AUDIO_ROUTE_TYPE_TO_DEVICE_INFO_TYPE.put(TYPE_BLUETOOTH_LE, bluetoothLeDeviceInfoTypes);
     }
 
-    int getType() {
+    public int getType() {
         return mAudioRouteType;
     }
 
@@ -237,7 +238,7 @@
     // Invoked when entered pending route whose dest route is this route
     void onDestRouteAsPendingRoute(boolean active, PendingAudioRoute pendingAudioRoute,
             BluetoothDevice device, AudioManager audioManager,
-            BluetoothRouteManager bluetoothRouteManager) {
+            BluetoothRouteManager bluetoothRouteManager, boolean isScoAudioConnected) {
         Log.i(this, "onDestRouteAsPendingRoute: active (%b), type (%d)", active, mAudioRouteType);
         if (pendingAudioRoute.isActive() && !active) {
             clearCommunicationDevice(pendingAudioRoute, bluetoothRouteManager, audioManager);
@@ -251,20 +252,19 @@
                     // 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) {
+                    if (connectedBtAudio || isScoAudioConnected) {
                         pendingAudioRoute.setCommunicationDeviceType(mAudioRouteType);
-                    }
-                    if (connectedBtAudio) {
-                        pendingAudioRoute.addMessage(BT_AUDIO_CONNECTED);
-                    } else if (!scoConnected) {
-                        pendingAudioRoute.onMessageReceived(
-                                PENDING_ROUTE_FAILED, mBluetoothAddress);
+                        if (!isScoAudioConnected) {
+                            pendingAudioRoute.addMessage(BT_AUDIO_CONNECTED, mBluetoothAddress);
+                        }
+                    } else {
+                        pendingAudioRoute.onMessageReceived(new Pair<>(PENDING_ROUTE_FAILED,
+                                mBluetoothAddress), mBluetoothAddress);
                     }
                     return;
                 }
             } else if (mAudioRouteType == TYPE_SPEAKER) {
-                pendingAudioRoute.addMessage(SPEAKER_ON);
+                pendingAudioRoute.addMessage(SPEAKER_ON, null);
             }
 
             boolean result = false;
@@ -291,7 +291,7 @@
             // 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);
+                pendingAudioRoute.onMessageReceived(new Pair<>(PENDING_ROUTE_FAILED, null), null);
             }
         }
     }
@@ -303,13 +303,13 @@
         Log.i(this, "onOrigRouteAsPendingRoute: active (%b), type (%d)", active, mAudioRouteType);
         if (active) {
             if (mAudioRouteType == TYPE_SPEAKER) {
-                pendingAudioRoute.addMessage(SPEAKER_OFF);
+                pendingAudioRoute.addMessage(SPEAKER_OFF, null);
             }
             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);
+                pendingAudioRoute.addMessage(BT_AUDIO_DISCONNECTED, mBluetoothAddress);
             }
         }
     }
@@ -370,7 +370,7 @@
         return success;
     }
 
-    private int clearCommunicationDevice(PendingAudioRoute pendingAudioRoute,
+    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.
diff --git a/src/com/android/server/telecom/CallAudioRouteController.java b/src/com/android/server/telecom/CallAudioRouteController.java
index 7b29fc8..820219d 100644
--- a/src/com/android/server/telecom/CallAudioRouteController.java
+++ b/src/com/android/server/telecom/CallAudioRouteController.java
@@ -18,6 +18,7 @@
 
 import static com.android.server.telecom.AudioRoute.BT_AUDIO_ROUTE_TYPES;
 import static com.android.server.telecom.AudioRoute.TYPE_INVALID;
+import static com.android.server.telecom.AudioRoute.TYPE_SPEAKER;
 
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
@@ -40,7 +41,9 @@
 import android.telecom.CallAudioState;
 import android.telecom.Log;
 import android.telecom.Logging.Session;
+import android.telecom.VideoProfile;
 import android.util.ArrayMap;
+import android.util.Pair;
 
 import androidx.annotation.NonNull;
 
@@ -94,7 +97,9 @@
     private StatusBarNotifier mStatusBarNotifier;
     private FeatureFlags mFeatureFlags;
     private int mFocusType;
+    private boolean mIsScoAudioConnected;
     private final Object mLock = new Object();
+    private final TelecomSystem.SyncRoot mTelecomLock;
     private final BroadcastReceiver mSpeakerPhoneChangeReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
@@ -105,7 +110,9 @@
                         AudioDeviceInfo info = mAudioManager.getCommunicationDevice();
                         if ((info != null) &&
                                 (info.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER)) {
-                            sendMessageWithSessionInfo(SPEAKER_ON);
+                            if (mCurrentRoute.getType() != AudioRoute.TYPE_SPEAKER) {
+                                sendMessageWithSessionInfo(SPEAKER_ON);
+                            }
                         } else {
                             sendMessageWithSessionInfo(SPEAKER_OFF);
                         }
@@ -171,6 +178,8 @@
         mStatusBarNotifier = statusBarNotifier;
         mFeatureFlags = featureFlags;
         mFocusType = NO_FOCUS;
+        mIsScoAudioConnected = false;
+        mTelecomLock = callsManager.getLock();
         HandlerThread handlerThread = new HandlerThread(this.getClass().getSimpleName());
         handlerThread.start();
 
@@ -282,7 +291,7 @@
                             handleMuteChanged(false);
                             break;
                         case MUTE_EXTERNALLY_CHANGED:
-                            handleMuteChanged(mAudioManager.isMasterMute());
+                            handleMuteChanged(mAudioManager.isMicrophoneMute());
                             break;
                         case SWITCH_FOCUS:
                             focus = msg.arg1;
@@ -455,11 +464,13 @@
             Log.i(this, "Override current pending route destination from %s(active=%b) to "
                             + "%s(active=%b)",
                     mPendingAudioRoute.getDestRoute(), mIsActive, destRoute, active);
+            // Ensure we don't keep waiting for SPEAKER_ON if dest route gets overridden.
+            if (active && 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, mPendingAudioRoute.getDestRoute());
-            mPendingAudioRoute.setDestRoute(active, destRoute, mBluetoothRoutes.get(destRoute));
-            mIsActive = active;
         } else {
             if (mCurrentRoute.equals(destRoute) && (mIsActive == active)) {
                 return;
@@ -473,10 +484,11 @@
                 // Avoid waiting for pending messages for an unavailable route
                 mPendingAudioRoute.setOrigRoute(mIsActive, DUMMY_ROUTE);
             }
-            mPendingAudioRoute.setDestRoute(active, destRoute, mBluetoothRoutes.get(destRoute));
-            mIsActive = active;
             mIsPending = true;
         }
+        mPendingAudioRoute.setDestRoute(active, destRoute, mBluetoothRoutes.get(destRoute),
+                mIsScoAudioConnected);
+        mIsActive = active;
         mPendingAudioRoute.evaluatePendingState();
         postTimeoutMessage();
     }
@@ -599,7 +611,8 @@
             Log.i(this, "handleBtAudioActive: is pending path");
             if (Objects.equals(mPendingAudioRoute.getDestRoute().getBluetoothAddress(),
                     bluetoothDevice.getAddress())) {
-                mPendingAudioRoute.onMessageReceived(BT_AUDIO_CONNECTED, null);
+                mPendingAudioRoute.onMessageReceived(new Pair<>(BT_AUDIO_CONNECTED,
+                        bluetoothDevice.getAddress()), null);
             }
         } else {
             // ignore, not triggered by telecom
@@ -620,7 +633,8 @@
             Log.i(this, "handleBtAudioInactive: is pending path");
             if (Objects.equals(mPendingAudioRoute.getOrigRoute().getBluetoothAddress(),
                     bluetoothDevice.getAddress())) {
-                mPendingAudioRoute.onMessageReceived(BT_AUDIO_DISCONNECTED, null);
+                mPendingAudioRoute.onMessageReceived(new Pair<>(BT_AUDIO_DISCONNECTED,
+                        bluetoothDevice.getAddress()), null);
             }
         } else {
             // ignore, not triggered by telecom
@@ -719,7 +733,7 @@
 
     private void handleMuteChanged(boolean mute) {
         mIsMute = mute;
-        if (mIsMute != mAudioManager.isMasterMute() && mIsActive) {
+        if (mIsMute != mAudioManager.isMicrophoneMute() && mIsActive) {
             IAudioService audioService = mAudioServiceFactory.getAudioService();
             Log.i(this, "changing microphone mute state to: %b [serviceIsNull=%b]", mute,
                     audioService == null);
@@ -752,10 +766,9 @@
                 }
             }
             case ACTIVE_FOCUS -> {
-                // Route to active baseline route, otherwise ignore if route is already active.
-                if (!mIsActive) {
-                    routeTo(true, getBaseRoute(true, null));
-                }
+                // Route to active baseline route (we may need to change audio route in the case
+                // when a video call is put on hold).
+                routeTo(true, getBaseRoute(true, null));
             }
             case RINGING_FOCUS -> {
                 if (!mIsActive) {
@@ -836,7 +849,7 @@
     private void handleSpeakerOn() {
         if (isPending()) {
             Log.i(this, "handleSpeakerOn: sending SPEAKER_ON to pending audio route");
-            mPendingAudioRoute.onMessageReceived(SPEAKER_ON, null);
+            mPendingAudioRoute.onMessageReceived(new Pair<>(SPEAKER_ON, null), null);
             // Update status bar notification if we are in a call.
             mStatusBarNotifier.notifySpeakerphone(mCallsManager.hasAnyCalls());
         } else {
@@ -854,7 +867,7 @@
     private void handleSpeakerOff() {
         if (isPending()) {
             Log.i(this, "handleSpeakerOff - sending SPEAKER_OFF to pending audio route");
-            mPendingAudioRoute.onMessageReceived(SPEAKER_OFF, null);
+            mPendingAudioRoute.onMessageReceived(new Pair<>(SPEAKER_OFF, null), null);
             // Update status bar notification
             mStatusBarNotifier.notifySpeakerphone(false);
         } else if (mCurrentRoute.getType() == AudioRoute.TYPE_SPEAKER) {
@@ -878,6 +891,7 @@
             Log.addEvent(mCallsManager.getForegroundCall(), LogUtils.Events.AUDIO_ROUTE,
                     "Entering audio route: " + mCurrentRoute + " (active=" + mIsActive + ")");
             mIsPending = false;
+            mPendingAudioRoute.clearPendingMessages();
             onCurrentRouteChanged();
         }
     }
@@ -909,7 +923,8 @@
                     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) {
+                    if (route.getType() == AudioRoute.TYPE_BLUETOOTH_LE
+                            && getLeAudioService() != null) {
                         int groupId = getLeAudioService().getGroupId(deviceToAdd);
                         if (groupId != BluetoothLeAudio.GROUP_ID_INVALID) {
                             deviceToAdd = getLeAudioService().getConnectedGroupLeadDevice(groupId);
@@ -988,6 +1003,12 @@
 
     private AudioRoute getPreferredAudioRouteFromDefault(boolean includeBluetooth,
             String btAddressToExclude) {
+        boolean skipEarpiece;
+        Call foregroundCall = mCallAudioManager.getForegroundCall();
+        synchronized (mTelecomLock) {
+            skipEarpiece = foregroundCall != null
+                    && VideoProfile.isVideo(foregroundCall.getVideoState());
+        }
         // Route to earpiece, wired, or speaker route if there are not bluetooth routes or if there
         // are only wearables available.
         AudioRoute activeWatchOrNonWatchDeviceRoute =
@@ -996,7 +1017,17 @@
                 || activeWatchOrNonWatchDeviceRoute == null) {
             Log.i(this, "getPreferredAudioRouteFromDefault: Audio routing defaulting to "
                     + "available non-BT route.");
-            return mEarpieceWiredRoute != null ? mEarpieceWiredRoute : mSpeakerDockRoute;
+            AudioRoute defaultRoute = mEarpieceWiredRoute != null
+                    ? mEarpieceWiredRoute
+                    : mSpeakerDockRoute;
+            // Ensure that we default to speaker route if we're in a video call, but disregard it if
+            // a wired headset is plugged in.
+            if (skipEarpiece && defaultRoute.getType() == AudioRoute.TYPE_EARPIECE) {
+                Log.i(this, "getPreferredAudioRouteFromDefault: Audio routing defaulting to "
+                        + "speaker route for video call.");
+                defaultRoute = mSpeakerDockRoute;
+            }
+            return defaultRoute;
         } else {
             // 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).
@@ -1042,8 +1073,8 @@
         return mCurrentRoute;
     }
 
-    private AudioRoute getBluetoothRoute(@AudioRoute.AudioRouteType int audioRouteType,
-                                         String address) {
+    public AudioRoute getBluetoothRoute(@AudioRoute.AudioRouteType int audioRouteType,
+            String address) {
         for (AudioRoute route : mBluetoothRoutes.keySet()) {
             if (route.getType() == audioRouteType && route.getBluetoothAddress().equals(address)) {
                 return route;
@@ -1129,7 +1160,7 @@
             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)) {
+                    || isLeAudioNonLeadDeviceOrServiceUnavailable(route.getType(), device)) {
                 continue;
             }
             // Check if the most recently active device is a watch device.
@@ -1158,7 +1189,8 @@
         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))) {
+            if (isLeAudioNonLeadDeviceOrServiceUnavailable(
+                    route.getType(), mBluetoothRoutes.get(route))) {
                 continue;
             }
             if (!route.getBluetoothAddress().equals(btAddressToExclude)) {
@@ -1168,15 +1200,19 @@
         return null;
     }
 
-    private boolean isLeAudioNonLeadDevice(@AudioRoute.AudioRouteType int type,
+    private boolean isLeAudioNonLeadDeviceOrServiceUnavailable(@AudioRoute.AudioRouteType int type,
             BluetoothDevice device) {
         if (type != AudioRoute.TYPE_BLUETOOTH_LE) {
             return false;
+        } else if (getLeAudioService() == null) {
+            return true;
         }
+
         int groupId = getLeAudioService().getGroupId(device);
         if (groupId != BluetoothLeAudio.GROUP_ID_INVALID) {
-            return !device.getAddress().equals(
-                    getLeAudioService().getConnectedGroupLeadDevice(groupId).getAddress());
+            BluetoothDevice leadDevice = getLeAudioService().getConnectedGroupLeadDevice(groupId);
+            Log.i(this, "Lead device for device (%s) is %s.", device, leadDevice);
+            return leadDevice == null || !device.getAddress().equals(leadDevice.getAddress());
         }
         return false;
     }
@@ -1195,6 +1231,18 @@
         mAudioRouteFactory = audioRouteFactory;
     }
 
+    public Map<AudioRoute, BluetoothDevice> getBluetoothRoutes() {
+        return mBluetoothRoutes;
+    }
+
+    public void overrideIsPending(boolean isPending) {
+        mIsPending = isPending;
+    }
+
+    public void setIsScoAudioConnected(boolean value) {
+        mIsScoAudioConnected = value;
+    }
+
     @VisibleForTesting
     public void setActive(boolean active) {
         if (active) {
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index c3eb3b8..dbf7de5 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -659,6 +659,7 @@
         }
         callAudioRouteAdapter.initialize();
         bluetoothStateReceiver.setCallAudioRouteAdapter(callAudioRouteAdapter);
+        bluetoothDeviceManager.setCallAudioRouteAdapter(callAudioRouteAdapter);
 
         CallAudioRoutePeripheralAdapter callAudioRoutePeripheralAdapter =
                 new CallAudioRoutePeripheralAdapter(
diff --git a/src/com/android/server/telecom/PendingAudioRoute.java b/src/com/android/server/telecom/PendingAudioRoute.java
index 6ba09a5..f9cdc35 100644
--- a/src/com/android/server/telecom/PendingAudioRoute.java
+++ b/src/com/android/server/telecom/PendingAudioRoute.java
@@ -22,10 +22,12 @@
 import android.bluetooth.BluetoothDevice;
 import android.media.AudioManager;
 import android.telecom.Log;
+import android.util.ArraySet;
+import android.util.Pair;
 
 import com.android.server.telecom.bluetooth.BluetoothRouteManager;
 
-import java.util.ArrayList;
+import java.util.Set;
 
 /**
  * Used to represent the intermediate state during audio route switching.
@@ -47,7 +49,7 @@
      * by new switching request during the ongoing switching
      */
     private AudioRoute mDestRoute;
-    private ArrayList<Integer> mPendingMessages;
+    private Set<Pair<Integer, String>> mPendingMessages;
     private boolean mActive;
     /**
      * The device that has been set for communication by Telecom
@@ -59,7 +61,7 @@
         mCallAudioRouteController = controller;
         mAudioManager = audioManager;
         mBluetoothRouteManager = bluetoothRouteManager;
-        mPendingMessages = new ArrayList<>();
+        mPendingMessages = new ArraySet<>();
         mActive = false;
         mCommunicationDeviceType = AudioRoute.TYPE_INVALID;
     }
@@ -73,24 +75,25 @@
         return mOrigRoute;
     }
 
-    void setDestRoute(boolean active, AudioRoute destRoute, BluetoothDevice device) {
+    void setDestRoute(boolean active, AudioRoute destRoute, BluetoothDevice device,
+            boolean isScoAudioConnected) {
         destRoute.onDestRouteAsPendingRoute(active, this, device,
-                mAudioManager, mBluetoothRouteManager);
+                mAudioManager, mBluetoothRouteManager, isScoAudioConnected);
         mActive = active;
         mDestRoute = destRoute;
     }
 
-    AudioRoute getDestRoute() {
+    public AudioRoute getDestRoute() {
         return mDestRoute;
     }
 
-    public void addMessage(int message) {
-        mPendingMessages.add(message);
+    public void addMessage(int message, String bluetoothDevice) {
+        mPendingMessages.add(new Pair<>(message, bluetoothDevice));
     }
 
-    public void onMessageReceived(int message, String btAddressToExclude) {
+    public void onMessageReceived(Pair<Integer, String> message, String btAddressToExclude) {
         Log.i(this, "onMessageReceived: message - %s", message);
-        if (message == PENDING_ROUTE_FAILED) {
+        if (message.first == PENDING_ROUTE_FAILED) {
             // Fallback to base route
             mCallAudioRouteController.sendMessageWithSessionInfo(
                     SWITCH_BASELINE_ROUTE, 0, btAddressToExclude);
@@ -98,7 +101,7 @@
         }
 
         // Removes the first occurrence of the specified message from this list, if it is present.
-        mPendingMessages.remove((Object) message);
+        mPendingMessages.remove(message);
         evaluatePendingState();
     }
 
@@ -107,9 +110,7 @@
             mCallAudioRouteController.sendMessageWithSessionInfo(
                     CallAudioRouteAdapter.EXIT_PENDING_ROUTE);
         } else {
-            for(Integer i: mPendingMessages) {
-                Log.d(this, "evaluatePendingState: pending Messages - %d", i);
-            }
+            Log.i(this, "evaluatePendingState: mPendingMessages - %s", mPendingMessages);
         }
     }
 
@@ -117,6 +118,10 @@
         mPendingMessages.clear();
     }
 
+    public void clearPendingMessage(Pair<Integer, String> message) {
+        mPendingMessages.remove(message);
+    }
+
     public boolean isActive() {
         return mActive;
     }
@@ -129,4 +134,8 @@
             @AudioRoute.AudioRouteType int communicationDeviceType) {
         mCommunicationDeviceType = communicationDeviceType;
     }
+
+    public void overrideDestRoute(AudioRoute route) {
+        mDestRoute = route;
+    }
 }
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
index f220648..0a8ce5a 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
@@ -18,6 +18,8 @@
 
 import static com.android.server.telecom.AudioRoute.TYPE_BLUETOOTH_HA;
 import static com.android.server.telecom.AudioRoute.TYPE_BLUETOOTH_SCO;
+import static com.android.server.telecom.CallAudioRouteAdapter.BT_DEVICE_REMOVED;
+import static com.android.server.telecom.CallAudioRouteAdapter.SWITCH_BASELINE_ROUTE;
 
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
@@ -34,20 +36,25 @@
 import android.telecom.Log;
 import android.util.ArraySet;
 import android.util.LocalLog;
+import android.util.Pair;
 
 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.CallAudioRouteAdapter;
+import com.android.server.telecom.CallAudioRouteController;
 import com.android.server.telecom.flags.FeatureFlags;
 
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
@@ -62,6 +69,16 @@
     public static final int DEVICE_TYPE_HEARING_AID = 1;
     public static final int DEVICE_TYPE_LE_AUDIO = 2;
 
+    private static final Map<Integer, Integer> PROFILE_TO_AUDIO_ROUTE_MAP = new HashMap<>();
+    static {
+        PROFILE_TO_AUDIO_ROUTE_MAP.put(BluetoothProfile.HEADSET,
+                AudioRoute.TYPE_BLUETOOTH_SCO);
+        PROFILE_TO_AUDIO_ROUTE_MAP.put(BluetoothProfile.LE_AUDIO,
+                AudioRoute.TYPE_BLUETOOTH_LE);
+        PROFILE_TO_AUDIO_ROUTE_MAP.put(BluetoothProfile.HEARING_AID,
+                TYPE_BLUETOOTH_HA);
+    }
+
     private BluetoothLeAudio.Callback mLeAudioCallbacks =
         new BluetoothLeAudio.Callback() {
             @Override
@@ -198,11 +215,15 @@
                             Log.i(BluetoothDeviceManager.this, logString);
                             mLocalLog.log(logString);
 
-                            List<BluetoothDevice> devicesToRemove = new LinkedList<>(
-                                    lostServiceDevices.values());
-                            lostServiceDevices.clear();
-                            for (BluetoothDevice device : devicesToRemove) {
-                                mBluetoothRouteManager.onDeviceLost(device.getAddress());
+                            if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+                                handleAudioRefactoringServiceDisconnected(profile);
+                            } else {
+                                List<BluetoothDevice> devicesToRemove = new LinkedList<>(
+                                        lostServiceDevices.values());
+                                lostServiceDevices.clear();
+                                for (BluetoothDevice device : devicesToRemove) {
+                                    mBluetoothRouteManager.onDeviceLost(device.getAddress());
+                                }
                             }
                         }
                     } finally {
@@ -211,6 +232,34 @@
                 }
            };
 
+    private void handleAudioRefactoringServiceDisconnected(int profile) {
+        CallAudioRouteController controller = (CallAudioRouteController)
+                mCallAudioRouteAdapter;
+        Map<AudioRoute, BluetoothDevice> btRoutes = controller
+                .getBluetoothRoutes();
+        List<Pair<AudioRoute, BluetoothDevice>> btRoutesToRemove =
+                new ArrayList<>();
+        for (AudioRoute route: btRoutes.keySet()) {
+            if (route.getType() != PROFILE_TO_AUDIO_ROUTE_MAP.get(profile)) {
+                continue;
+            }
+            BluetoothDevice device = btRoutes.get(route);
+            // Prevent concurrent modification exception by just iterating through keys instead of
+            // simultaneously removing them.
+            btRoutesToRemove.add(new Pair<>(route, device));
+        }
+
+        for (Pair<AudioRoute, BluetoothDevice> routeToRemove:
+                btRoutesToRemove) {
+            AudioRoute route = routeToRemove.first;
+            BluetoothDevice device = routeToRemove.second;
+            mCallAudioRouteAdapter.sendMessageWithSessionInfo(
+                    BT_DEVICE_REMOVED, route.getType(), device);
+        }
+        mCallAudioRouteAdapter.sendMessageWithSessionInfo(
+                SWITCH_BASELINE_ROUTE, 0, (String) null);
+    }
+
     private final LinkedHashMap<String, BluetoothDevice> mHfpDevicesByAddress =
             new LinkedHashMap<>();
     private final LinkedHashMap<String, BluetoothDevice> mHearingAidDevicesByAddress =
@@ -249,6 +298,7 @@
     private AudioManager mAudioManager;
     private Executor mExecutor;
     private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
+    private CallAudioRouteAdapter mCallAudioRouteAdapter;
     private FeatureFlags mFeatureFlags;
 
     public BluetoothDeviceManager(Context context, BluetoothAdapter bluetoothAdapter,
@@ -889,6 +939,10 @@
         }
     }
 
+    public void setCallAudioRouteAdapter(CallAudioRouteAdapter adapter) {
+        mCallAudioRouteAdapter = adapter;
+    }
+
     public void dump(IndentingPrintWriter pw) {
         mLocalLog.dump(pw);
     }
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java b/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
index af9e07b..9168388 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
@@ -23,7 +23,6 @@
 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;
 
@@ -41,6 +40,7 @@
 import android.os.Bundle;
 import android.telecom.Log;
 import android.telecom.Logging.Session;
+import android.util.Pair;
 
 import com.android.internal.os.SomeArgs;
 import com.android.server.telecom.AudioRoute;
@@ -50,7 +50,6 @@
 import com.android.server.telecom.flags.FeatureFlags;
 import com.android.server.telecom.flags.Flags;
 
-
 public class BluetoothStateReceiver extends BroadcastReceiver {
     private static final String LOG_TAG = BluetoothStateReceiver.class.getSimpleName();
     public static final IntentFilter INTENT_FILTER;
@@ -119,9 +118,28 @@
         args.arg2 = device.getAddress();
         switch (bluetoothHeadsetAudioState) {
             case BluetoothHeadset.STATE_AUDIO_CONNECTED:
-                if (Flags.useRefactoredAudioRouteSwitching()) {
-                    mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0,
-                            device);
+                if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+                    CallAudioRouteController audioRouteController =
+                            (CallAudioRouteController) mCallAudioRouteAdapter;
+                    audioRouteController.setIsScoAudioConnected(true);
+                    if (audioRouteController.isPending()) {
+                        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);
+                            audioRouteController.getPendingAudioRoute()
+                                    .setCommunicationDeviceType(AudioRoute.TYPE_BLUETOOTH_SCO);
+                            mCallAudioRouteAdapter.sendMessageWithSessionInfo(
+                                    CallAudioRouteAdapter.EXIT_PENDING_ROUTE);
+                        }
+                    }
                 } else {
                     if (!mIsInCall) {
                         Log.i(LOG_TAG, "Ignoring BT audio on since we're not in a call");
@@ -132,6 +150,9 @@
                 break;
             case BluetoothHeadset.STATE_AUDIO_DISCONNECTED:
                 if (Flags.useRefactoredAudioRouteSwitching()) {
+                    CallAudioRouteController audioRouteController =
+                            (CallAudioRouteController) mCallAudioRouteAdapter;
+                    audioRouteController.setIsScoAudioConnected(false);
                     mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_AUDIO_DISCONNECTED, 0,
                             device);
                 }  else {
@@ -229,7 +250,8 @@
                                 + "communication device for %s. Sending PENDING_ROUTE_FAILED to "
                                 + "pending audio route.", device);
                         mCallAudioRouteAdapter.getPendingAudioRoute()
-                                .onMessageReceived(PENDING_ROUTE_FAILED, device.getAddress());
+                                .onMessageReceived(new Pair<>(PENDING_ROUTE_FAILED,
+                                        device.getAddress()), device.getAddress());
                     } else {
                         // Track the currently set communication device.
                         int routeType = deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
index f770b6a..59473bd 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
@@ -34,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_BLUETOOTH;
+import static com.android.server.telecom.CallAudioRouteAdapter.SWITCH_BASELINE_ROUTE;
 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;
@@ -64,14 +64,18 @@
 import android.media.audiopolicy.AudioProductStrategy;
 import android.os.UserHandle;
 import android.telecom.CallAudioState;
+import android.telecom.VideoProfile;
 
 import androidx.test.filters.SmallTest;
 
 import com.android.server.telecom.AudioRoute;
+import com.android.server.telecom.Call;
 import com.android.server.telecom.CallAudioManager;
 import com.android.server.telecom.CallAudioRouteController;
 import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.PendingAudioRoute;
 import com.android.server.telecom.StatusBarNotifier;
+import com.android.server.telecom.TelecomSystem;
 import com.android.server.telecom.WiredHeadsetManager;
 import com.android.server.telecom.bluetooth.BluetoothDeviceManager;
 import com.android.server.telecom.bluetooth.BluetoothRouteManager;
@@ -102,6 +106,9 @@
     @Mock StatusBarNotifier mockStatusBarNotifier;
     @Mock AudioDeviceInfo mAudioDeviceInfo;
     @Mock BluetoothLeAudio mBluetoothLeAudio;
+    @Mock CallAudioManager mCallAudioManager;
+    @Mock Call mCall;
+    @Mock private TelecomSystem.SyncRoot mLock;
     private AudioRoute mEarpieceRoute;
     private AudioRoute mSpeakerRoute;
     private static final String BT_ADDRESS_1 = "00:00:00:00:00:01";
@@ -143,6 +150,7 @@
                 any(CallAudioState.class));
         when(mCallsManager.getCurrentUserHandle()).thenReturn(
                 new UserHandle(UserHandle.USER_SYSTEM));
+        when(mCallsManager.getLock()).thenReturn(mLock);
         when(mBluetoothRouteManager.getDeviceManager()).thenReturn(mBluetoothDeviceManager);
         when(mBluetoothDeviceManager.connectAudio(any(BluetoothDevice.class), anyInt()))
                 .thenReturn(true);
@@ -160,6 +168,9 @@
         mController.setAudioManager(mAudioManager);
         mEarpieceRoute = new AudioRoute(AudioRoute.TYPE_EARPIECE, null, null);
         mSpeakerRoute = new AudioRoute(AudioRoute.TYPE_SPEAKER, null, null);
+        mController.setCallAudioManager(mCallAudioManager);
+        when(mCallAudioManager.getForegroundCall()).thenReturn(mCall);
+        when(mCall.getVideoState()).thenReturn(VideoProfile.STATE_AUDIO_ONLY);
         when(mFeatureFlags.ignoreAutoRouteToWatchDevice()).thenReturn(false);
     }
 
@@ -191,6 +202,78 @@
 
     @SmallTest
     @Test
+    public void testInitializeWithWiredHeadset() {
+        AudioRoute wiredHeadsetRoute = new AudioRoute(AudioRoute.TYPE_WIRED, null, null);
+        when(mWiredHeadsetManager.isPluggedIn()).thenReturn(true);
+        mController.initialize();
+        assertEquals(wiredHeadsetRoute, mController.getCurrentRoute());
+        assertEquals(2, mController.getAvailableRoutes().size());
+        assertTrue(mController.getAvailableRoutes().contains(mSpeakerRoute));
+    }
+
+    @SmallTest
+    @Test
+    public void testNormalCallRouteToEarpiece() {
+        mController.initialize();
+        mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS);
+        // Verify that pending audio destination route is set to speaker. This will trigger pending
+        // message to wait for SPEAKER_ON message once communication device is set before routing.
+        waitForHandlerAction(mController.getAdapterHandler(), TEST_TIMEOUT);
+        PendingAudioRoute pendingRoute = mController.getPendingAudioRoute();
+        assertEquals(AudioRoute.TYPE_EARPIECE, pendingRoute.getDestRoute().getType());
+
+        CallAudioState 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 testVideoCallHoldRouteToEarpiece() {
+        mController.initialize();
+        mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS);
+        // Verify that pending audio destination route is not defaulted to speaker when a video call
+        // is not the foreground call.
+        waitForHandlerAction(mController.getAdapterHandler(), TEST_TIMEOUT);
+        PendingAudioRoute pendingRoute = mController.getPendingAudioRoute();
+        assertEquals(AudioRoute.TYPE_EARPIECE, pendingRoute.getDestRoute().getType());
+    }
+
+    @SmallTest
+    @Test
+    public void testVideoCallRouteToSpeaker() {
+        when(mCall.getVideoState()).thenReturn(VideoProfile.STATE_BIDIRECTIONAL);
+        mController.initialize();
+        mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS);
+        // Verify that pending audio destination route is set to speaker. This will trigger pending
+        // message to wait for SPEAKER_ON message once communication device is set before routing.
+        waitForHandlerAction(mController.getAdapterHandler(), TEST_TIMEOUT);
+        PendingAudioRoute pendingRoute = mController.getPendingAudioRoute();
+        assertEquals(AudioRoute.TYPE_SPEAKER, pendingRoute.getDestRoute().getType());
+
+        // Mock SPEAKER_ON message received by controller.
+        mController.sendMessageWithSessionInfo(SPEAKER_ON);
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        // Verify that audio is routed to wired headset if it's present.
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_WIRED_HEADSET,
+                CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        mController.sendMessageWithSessionInfo(CONNECT_WIRED_HEADSET);
+        waitForHandlerAction(mController.getAdapterHandler(), TEST_TIMEOUT);
+        mController.sendMessageWithSessionInfo(SPEAKER_OFF);
+        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,
@@ -435,8 +518,7 @@
     @SmallTest
     @Test
     public void testToggleMute() throws Exception {
-        when(mAudioManager.isMasterMute()).thenReturn(false);
-
+        when(mAudioManager.isMicrophoneMute()).thenReturn(false);
         mController.initialize();
         mController.setActive(true);
 
@@ -449,7 +531,7 @@
         verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
                 any(CallAudioState.class), eq(expectedState));
 
-        when(mAudioManager.isMasterMute()).thenReturn(true);
+        when(mAudioManager.isMicrophoneMute()).thenReturn(true);
         mController.sendMessageWithSessionInfo(MUTE_OFF);
         expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
                 CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
@@ -462,6 +544,34 @@
 
     @SmallTest
     @Test
+    public void testMuteOffAfterCallEnds() throws Exception {
+        when(mAudioManager.isMicrophoneMute()).thenReturn(false);
+        mController.initialize();
+        mController.setActive(true);
+
+        mController.sendMessageWithSessionInfo(MUTE_ON);
+        CallAudioState expectedState = new CallAudioState(true, CallAudioState.ROUTE_EARPIECE,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        verify(mAudioService, timeout(TEST_TIMEOUT)).setMicrophoneMute(eq(true), anyString(),
+                anyInt(), anyString());
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        // Switch to NO_FOCUS to indicate call termination and verify mute is reset.
+        when(mAudioManager.isMicrophoneMute()).thenReturn(true);
+        mController.sendMessageWithSessionInfo(SWITCH_FOCUS, NO_FOCUS);
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        verify(mAudioService, timeout(TEST_TIMEOUT)).setMicrophoneMute(eq(false), anyString(),
+                anyInt(), anyString());
+        verify(mCallsManager, timeout(TEST_TIMEOUT).atLeastOnce()).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+    }
+
+    @SmallTest
+    @Test
     public void testIgnoreAutoRouteToWatch() {
         when(mFeatureFlags.ignoreAutoRouteToWatchDevice()).thenReturn(true);
         when(mBluetoothRouteManager.isWatch(any(BluetoothDevice.class))).thenReturn(true);
@@ -591,6 +701,23 @@
         BLUETOOTH_DEVICES.remove(scoDevice);
     }
 
+    @SmallTest
+    @Test
+    public void testIgnoreLeRouteWhenServiceUnavailable() {
+        when(mBluetoothLeAudio.getConnectedGroupLeadDevice(anyInt()))
+                .thenReturn(BLUETOOTH_DEVICE_1);
+        verifyConnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_LE);
+
+        when(mBluetoothDeviceManager.getLeAudioService()).thenReturn(null);
+        // Switch baseline to verify that we don't route back to LE audio this time.
+        mController.sendMessageWithSessionInfo(SWITCH_BASELINE_ROUTE, 0, (String) null);
+        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).atLeastOnce()).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+    }
+
     private void verifyConnectBluetoothDevice(int audioType) {
         mController.initialize();
         mController.setActive(true);