Merge "Resolve CallAudioRouteController issues." into main
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 defc9bb..0744502 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);