Merge "Formalize BlockedNumberContract.SystemContract APIs" into main
diff --git a/Android.bp b/Android.bp
index 7e57a3f..68a8e39 100644
--- a/Android.bp
+++ b/Android.bp
@@ -1,12 +1,13 @@
 package {
+    default_team: "trendy_team_fwk_telecom",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
 genrule {
     name: "statslog-telecom-java-gen",
     tools: ["stats-log-api-gen"],
-    cmd: "$(location stats-log-api-gen) --java $(out) --module telecom"
-        + " --javaPackage com.android.server.telecom --javaClass TelecomStatsLog",
+    cmd: "$(location stats-log-api-gen) --java $(out) --module telecom" +
+        " --javaPackage com.android.server.telecom --javaClass TelecomStatsLog",
     out: ["com/android/server/telecom/TelecomStatsLog.java"],
 }
 
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 90e4bd9..a9b6154 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -66,6 +66,7 @@
     <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
     <uses-permission android:name="android.permission.ACCESS_LAST_KNOWN_CELL_ID"/>
     <uses-permission android:name="android.permission.STATUS_BAR_SERVICE" />
+    <uses-permission android:name="android.permission.MODIFY_AUDIO_ROUTING" />
 
     <permission android:name="android.permission.BROADCAST_CALLLOG_INFO"
          android:label="Broadcast the call type/duration information"
diff --git a/OWNERS b/OWNERS
index 6f90d48..9c071f9 100644
--- a/OWNERS
+++ b/OWNERS
@@ -1,6 +1,6 @@
+# Bug component: 151185
 breadley@google.com
 tgunn@google.com
-xiaotonj@google.com
 tjstuart@google.com
 rgreenwalt@google.com
 pmadapurmath@google.com
diff --git a/flags/Android.bp b/flags/Android.bp
index 386831c..3497db8 100644
--- a/flags/Android.bp
+++ b/flags/Android.bp
@@ -21,23 +21,23 @@
 aconfig_declarations {
     name: "telecom_flags",
     package: "com.android.server.telecom.flags",
+    container: "system",
     srcs: [
-      "telecom_broadcast_flags.aconfig",
-      "telecom_ringer_flag_declarations.aconfig",
-      "telecom_api_flags.aconfig",
-      "telecom_call_filtering_flags.aconfig",
-      "telecom_incallservice_flags.aconfig",
-      "telecom_default_phone_account_flags.aconfig",
-      "telecom_callaudioroutestatemachine_flags.aconfig",
-      "telecom_call_flags.aconfig",
-      "telecom_calls_manager_flags.aconfig",
-      "telecom_anomaly_report_flags.aconfig",
-      "telecom_callaudiomodestatemachine_flags.aconfig",
-      "telecom_calllog_flags.aconfig",
-      "telecom_resolve_hidden_dependencies.aconfig",
-      "telecom_bluetoothroutemanager_flags.aconfig",
-      "telecom_work_profile_flags.aconfig",
-      "telecom_connection_service_wrapper_flags.aconfig",
+        "telecom_broadcast_flags.aconfig",
+        "telecom_ringer_flag_declarations.aconfig",
+        "telecom_api_flags.aconfig",
+        "telecom_call_filtering_flags.aconfig",
+        "telecom_incallservice_flags.aconfig",
+        "telecom_default_phone_account_flags.aconfig",
+        "telecom_callaudioroutestatemachine_flags.aconfig",
+        "telecom_call_flags.aconfig",
+        "telecom_calls_manager_flags.aconfig",
+        "telecom_anomaly_report_flags.aconfig",
+        "telecom_callaudiomodestatemachine_flags.aconfig",
+        "telecom_calllog_flags.aconfig",
+        "telecom_resolve_hidden_dependencies.aconfig",
+        "telecom_bluetoothroutemanager_flags.aconfig",
+        "telecom_work_profile_flags.aconfig",
+        "telecom_connection_service_wrapper_flags.aconfig",
     ],
 }
-
diff --git a/flags/telecom_anomaly_report_flags.aconfig b/flags/telecom_anomaly_report_flags.aconfig
index dbacc08..6879d86 100644
--- a/flags/telecom_anomaly_report_flags.aconfig
+++ b/flags/telecom_anomaly_report_flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.telecom.flags"
+container: "system"
 
 flag {
   name: "gen_anom_report_on_focus_timeout"
diff --git a/flags/telecom_api_flags.aconfig b/flags/telecom_api_flags.aconfig
index 21b83b2..3e01db9 100644
--- a/flags/telecom_api_flags.aconfig
+++ b/flags/telecom_api_flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.telecom.flags"
+container: "system"
 
 flag {
   name: "voip_app_actions_support"
diff --git a/flags/telecom_bluetoothroutemanager_flags.aconfig b/flags/telecom_bluetoothroutemanager_flags.aconfig
index ddd8571..1df1e9b 100644
--- a/flags/telecom_bluetoothroutemanager_flags.aconfig
+++ b/flags/telecom_bluetoothroutemanager_flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.telecom.flags"
+container: "system"
 
 flag {
   name: "use_actual_address_to_enter_connecting_state"
diff --git a/flags/telecom_broadcast_flags.aconfig b/flags/telecom_broadcast_flags.aconfig
index 348d574..de8dd27 100644
--- a/flags/telecom_broadcast_flags.aconfig
+++ b/flags/telecom_broadcast_flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.telecom.flags"
+container: "system"
 
 flag {
   name: "is_new_outgoing_call_broadcast_unblocking"
diff --git a/flags/telecom_call_filtering_flags.aconfig b/flags/telecom_call_filtering_flags.aconfig
index 95e74ce..72f9db3 100644
--- a/flags/telecom_call_filtering_flags.aconfig
+++ b/flags/telecom_call_filtering_flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.telecom.flags"
+container: "system"
 
 flag {
   name: "skip_filter_phone_account_perform_dnd_filter"
diff --git a/flags/telecom_call_flags.aconfig b/flags/telecom_call_flags.aconfig
index b5ea6a2..27a4b22 100644
--- a/flags/telecom_call_flags.aconfig
+++ b/flags/telecom_call_flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.telecom.flags"
+container: "system"
 
 flag {
   name: "transactional_cs_verifier"
diff --git a/flags/telecom_callaudiomodestatemachine_flags.aconfig b/flags/telecom_callaudiomodestatemachine_flags.aconfig
index b263113..1d81535 100644
--- a/flags/telecom_callaudiomodestatemachine_flags.aconfig
+++ b/flags/telecom_callaudiomodestatemachine_flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.telecom.flags"
+container: "system"
 
 flag {
   name: "set_audio_mode_before_abandon_focus"
diff --git a/flags/telecom_callaudioroutestatemachine_flags.aconfig b/flags/telecom_callaudioroutestatemachine_flags.aconfig
index fe21c92..f5da045 100644
--- a/flags/telecom_callaudioroutestatemachine_flags.aconfig
+++ b/flags/telecom_callaudioroutestatemachine_flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.telecom.flags"
+container: "system"
 
 flag {
   name: "available_routes_never_updated_after_set_system_audio_state"
diff --git a/flags/telecom_calllog_flags.aconfig b/flags/telecom_calllog_flags.aconfig
index 3ce7b63..593b7e5 100644
--- a/flags/telecom_calllog_flags.aconfig
+++ b/flags/telecom_calllog_flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.telecom.flags"
+container: "system"
 
 flag {
   name: "telecom_log_external_wearable_calls"
diff --git a/flags/telecom_calls_manager_flags.aconfig b/flags/telecom_calls_manager_flags.aconfig
index 1a19480..cdfcc30 100644
--- a/flags/telecom_calls_manager_flags.aconfig
+++ b/flags/telecom_calls_manager_flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.telecom.flags"
+container: "system"
 
 flag {
   name: "use_improved_listener_order"
diff --git a/flags/telecom_connection_service_wrapper_flags.aconfig b/flags/telecom_connection_service_wrapper_flags.aconfig
index 5f46c27..80a8dfe 100644
--- a/flags/telecom_connection_service_wrapper_flags.aconfig
+++ b/flags/telecom_connection_service_wrapper_flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.telecom.flags"
+container: "system"
 
 flag {
   name: "updated_rcs_call_count_tracking"
diff --git a/flags/telecom_default_phone_account_flags.aconfig b/flags/telecom_default_phone_account_flags.aconfig
index 03f324c..e6badde 100644
--- a/flags/telecom_default_phone_account_flags.aconfig
+++ b/flags/telecom_default_phone_account_flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.telecom.flags"
+container: "system"
 
 flag {
   name: "only_update_telephony_on_valid_sub_ids"
diff --git a/flags/telecom_incallservice_flags.aconfig b/flags/telecom_incallservice_flags.aconfig
index 1110ca4..08a82ba 100644
--- a/flags/telecom_incallservice_flags.aconfig
+++ b/flags/telecom_incallservice_flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.telecom.flags"
+container: "system"
 
 flag {
   name: "early_binding_to_incall_service"
diff --git a/flags/telecom_resolve_hidden_dependencies.aconfig b/flags/telecom_resolve_hidden_dependencies.aconfig
index 6def938..674a968 100644
--- a/flags/telecom_resolve_hidden_dependencies.aconfig
+++ b/flags/telecom_resolve_hidden_dependencies.aconfig
@@ -1,8 +1,9 @@
 package: "com.android.server.telecom.flags"
+container: "system"
 
 flag {
     name: "telecom_resolve_hidden_dependencies"
     namespace: "telecom"
     description: "Mainland cleanup for hidden dependencies"
-    bug: "b/324090590"
+    bug: "323414215"
 }
diff --git a/flags/telecom_ringer_flag_declarations.aconfig b/flags/telecom_ringer_flag_declarations.aconfig
index 54748d0..13577bb 100644
--- a/flags/telecom_ringer_flag_declarations.aconfig
+++ b/flags/telecom_ringer_flag_declarations.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.telecom.flags"
+container: "system"
 
 flag {
   name: "use_device_provided_serialized_ringer_vibration"
diff --git a/flags/telecom_work_profile_flags.aconfig b/flags/telecom_work_profile_flags.aconfig
index 180af59..854568b 100644
--- a/flags/telecom_work_profile_flags.aconfig
+++ b/flags/telecom_work_profile_flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.telecom.flags"
+container: "system"
 
 flag {
   name: "associated_user_refactor_for_work_profile"
diff --git a/src/com/android/server/telecom/Analytics.java b/src/com/android/server/telecom/Analytics.java
index bbcf858..45e3340 100644
--- a/src/com/android/server/telecom/Analytics.java
+++ b/src/com/android/server/telecom/Analytics.java
@@ -721,7 +721,7 @@
 
     private static int getCarrierId(Context context) {
         SubscriptionManager subscriptionManager =
-                context.getSystemService(SubscriptionManager.class);
+                context.getSystemService(SubscriptionManager.class).createForAllUserProfiles();
         List<SubscriptionInfo> subInfos = subscriptionManager.getActiveSubscriptionInfoList();
         if (subInfos == null) {
             return -1;
diff --git a/src/com/android/server/telecom/AudioRoute.java b/src/com/android/server/telecom/AudioRoute.java
new file mode 100644
index 0000000..5037cf5
--- /dev/null
+++ b/src/com/android/server/telecom/AudioRoute.java
@@ -0,0 +1,285 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.telecom;
+
+import static com.android.server.telecom.CallAudioRouteAdapter.BT_AUDIO_CONNECTED;
+import static com.android.server.telecom.CallAudioRouteAdapter.BT_AUDIO_DISCONNECTED;
+import static com.android.server.telecom.CallAudioRouteAdapter.PENDING_ROUTE_FAILED;
+import static com.android.server.telecom.CallAudioRouteAdapter.SPEAKER_OFF;
+import static com.android.server.telecom.CallAudioRouteAdapter.SPEAKER_ON;
+
+import android.annotation.IntDef;
+import android.media.AudioDeviceInfo;
+import android.media.AudioManager;
+import android.telecom.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+public class AudioRoute {
+    public static class Factory {
+        private final ScheduledExecutorService mScheduledExecutorService =
+                new ScheduledThreadPoolExecutor(1);
+        private final CompletableFuture<AudioRoute> mAudioRouteFuture = new CompletableFuture<>();
+        public AudioRoute create(@AudioRouteType int type, String bluetoothAddress,
+                                 AudioManager audioManager) throws RuntimeException {
+            createRetry(type, bluetoothAddress, audioManager, MAX_CONNECTION_RETRIES);
+            try {
+                return mAudioRouteFuture.get();
+            } catch (InterruptedException | ExecutionException e) {
+                throw new RuntimeException("Error when creating requested audio route");
+            }
+        }
+        private void createRetry(@AudioRouteType int type, String bluetoothAddress,
+                                       AudioManager audioManager, int retryCount) {
+            if (retryCount == 0) {
+                mAudioRouteFuture.complete(null);
+            }
+
+            Log.i(this, "creating AudioRoute with type %s and address %s, retry count %d",
+                    DEVICE_TYPE_STRINGS.get(type), bluetoothAddress, retryCount);
+            AudioDeviceInfo routeInfo = null;
+            List<AudioDeviceInfo> infos = audioManager.getAvailableCommunicationDevices();
+            List<Integer> possibleInfoTypes = AUDIO_ROUTE_TYPE_TO_DEVICE_INFO_TYPE.get(type);
+            for (AudioDeviceInfo info : infos) {
+                Log.i(this, "type: " + info.getType());
+                if (possibleInfoTypes != null && possibleInfoTypes.contains(info.getType())) {
+                    if (BT_AUDIO_ROUTE_TYPES.contains(type)) {
+                        if (bluetoothAddress.equals(info.getAddress())) {
+                            routeInfo = info;
+                            break;
+                        }
+                    } else {
+                        routeInfo = info;
+                        break;
+                    }
+                }
+            }
+            if (routeInfo == null) {
+                CompletableFuture<Boolean> future = new CompletableFuture<>();
+                mScheduledExecutorService.schedule(new Runnable() {
+                    @Override
+                    public void run() {
+                        createRetry(type, bluetoothAddress, audioManager, retryCount - 1);
+                    }
+                }, RETRY_TIME_DELAY, TimeUnit.MILLISECONDS);
+            } else {
+                mAudioRouteFuture.complete(new AudioRoute(type, bluetoothAddress, routeInfo));
+            }
+        }
+    }
+
+    private static final long RETRY_TIME_DELAY = 500L;
+    private static final int MAX_CONNECTION_RETRIES = 2;
+    public static final int TYPE_INVALID = 0;
+    public static final int TYPE_EARPIECE = 1;
+    public static final int TYPE_WIRED = 2;
+    public static final int TYPE_SPEAKER = 3;
+    public static final int TYPE_DOCK = 4;
+    public static final int TYPE_BLUETOOTH_SCO = 5;
+    public static final int TYPE_BLUETOOTH_HA = 6;
+    public static final int TYPE_BLUETOOTH_LE = 7;
+    @IntDef(prefix = "TYPE", value = {
+            TYPE_INVALID,
+            TYPE_EARPIECE,
+            TYPE_WIRED,
+            TYPE_SPEAKER,
+            TYPE_DOCK,
+            TYPE_BLUETOOTH_SCO,
+            TYPE_BLUETOOTH_HA,
+            TYPE_BLUETOOTH_LE
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface AudioRouteType {}
+
+    private @AudioRouteType int mAudioRouteType;
+    private String mBluetoothAddress;
+    private AudioDeviceInfo mInfo;
+    public static final Set<Integer> BT_AUDIO_DEVICE_INFO_TYPES = Set.of(
+            AudioDeviceInfo.TYPE_BLE_HEADSET,
+            AudioDeviceInfo.TYPE_BLE_SPEAKER,
+            AudioDeviceInfo.TYPE_BLE_BROADCAST,
+            AudioDeviceInfo.TYPE_HEARING_AID,
+            AudioDeviceInfo.TYPE_BLUETOOTH_SCO
+    );
+
+    public static final Set<Integer> BT_AUDIO_ROUTE_TYPES = Set.of(
+            AudioRoute.TYPE_BLUETOOTH_SCO,
+            AudioRoute.TYPE_BLUETOOTH_HA,
+            AudioRoute.TYPE_BLUETOOTH_LE
+    );
+
+    public static final HashMap<Integer, String> DEVICE_TYPE_STRINGS;
+    static {
+        DEVICE_TYPE_STRINGS = new HashMap<>();
+        DEVICE_TYPE_STRINGS.put(TYPE_EARPIECE, "TYPE_EARPIECE");
+        DEVICE_TYPE_STRINGS.put(TYPE_WIRED, "TYPE_WIRED_HEADSET");
+        DEVICE_TYPE_STRINGS.put(TYPE_SPEAKER, "TYPE_SPEAKER");
+        DEVICE_TYPE_STRINGS.put(TYPE_DOCK, "TYPE_DOCK");
+        DEVICE_TYPE_STRINGS.put(TYPE_BLUETOOTH_SCO, "TYPE_BLUETOOTH_SCO");
+        DEVICE_TYPE_STRINGS.put(TYPE_BLUETOOTH_HA, "TYPE_BLUETOOTH_HA");
+        DEVICE_TYPE_STRINGS.put(TYPE_BLUETOOTH_LE, "TYPE_BLUETOOTH_LE");
+    }
+
+    public static final HashMap<Integer, Integer> DEVICE_INFO_TYPETO_AUDIO_ROUTE_TYPE;
+    static {
+        DEVICE_INFO_TYPETO_AUDIO_ROUTE_TYPE = new HashMap<>();
+        DEVICE_INFO_TYPETO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_BUILTIN_EARPIECE,
+                TYPE_EARPIECE);
+        DEVICE_INFO_TYPETO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, TYPE_SPEAKER);
+        DEVICE_INFO_TYPETO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_WIRED_HEADSET, TYPE_WIRED);
+        DEVICE_INFO_TYPETO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_WIRED_HEADPHONES, TYPE_WIRED);
+        DEVICE_INFO_TYPETO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_BLUETOOTH_SCO,
+                TYPE_BLUETOOTH_SCO);
+        DEVICE_INFO_TYPETO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_USB_DEVICE, TYPE_WIRED);
+        DEVICE_INFO_TYPETO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_USB_ACCESSORY, TYPE_WIRED);
+        DEVICE_INFO_TYPETO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_DOCK, TYPE_DOCK);
+        DEVICE_INFO_TYPETO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_USB_HEADSET, TYPE_WIRED);
+        DEVICE_INFO_TYPETO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_HEARING_AID,
+                TYPE_BLUETOOTH_HA);
+        DEVICE_INFO_TYPETO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_BLE_HEADSET,
+                TYPE_BLUETOOTH_LE);
+        DEVICE_INFO_TYPETO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_BLE_SPEAKER,
+                TYPE_BLUETOOTH_LE);
+        DEVICE_INFO_TYPETO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_BLE_BROADCAST,
+                TYPE_BLUETOOTH_LE);
+        DEVICE_INFO_TYPETO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_DOCK_ANALOG, TYPE_DOCK);
+    }
+
+    private static final HashMap<Integer, List<Integer>> AUDIO_ROUTE_TYPE_TO_DEVICE_INFO_TYPE;
+    static {
+        AUDIO_ROUTE_TYPE_TO_DEVICE_INFO_TYPE = new HashMap<>();
+        List<Integer> earpieceDeviceInfoTypes = new ArrayList<>();
+        earpieceDeviceInfoTypes.add(AudioDeviceInfo.TYPE_BUILTIN_EARPIECE);
+        AUDIO_ROUTE_TYPE_TO_DEVICE_INFO_TYPE.put(TYPE_EARPIECE, earpieceDeviceInfoTypes);
+
+        List<Integer> wiredDeviceInfoTypes = new ArrayList<>();
+        wiredDeviceInfoTypes.add(AudioDeviceInfo.TYPE_WIRED_HEADSET);
+        wiredDeviceInfoTypes.add(AudioDeviceInfo.TYPE_WIRED_HEADPHONES);
+        wiredDeviceInfoTypes.add(AudioDeviceInfo.TYPE_USB_DEVICE);
+        wiredDeviceInfoTypes.add(AudioDeviceInfo.TYPE_USB_ACCESSORY);
+        wiredDeviceInfoTypes.add(AudioDeviceInfo.TYPE_USB_HEADSET);
+        AUDIO_ROUTE_TYPE_TO_DEVICE_INFO_TYPE.put(TYPE_WIRED, wiredDeviceInfoTypes);
+
+        List<Integer> speakerDeviceInfoTypes = new ArrayList<>();
+        speakerDeviceInfoTypes.add(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
+        AUDIO_ROUTE_TYPE_TO_DEVICE_INFO_TYPE.put(TYPE_SPEAKER, speakerDeviceInfoTypes);
+
+        List<Integer> dockDeviceInfoTypes = new ArrayList<>();
+        dockDeviceInfoTypes.add(AudioDeviceInfo.TYPE_DOCK);
+        dockDeviceInfoTypes.add(AudioDeviceInfo.TYPE_DOCK_ANALOG);
+        AUDIO_ROUTE_TYPE_TO_DEVICE_INFO_TYPE.put(TYPE_DOCK, dockDeviceInfoTypes);
+
+        List<Integer> bluetoothScoDeviceInfoTypes = new ArrayList<>();
+        bluetoothScoDeviceInfoTypes.add(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP);
+        bluetoothScoDeviceInfoTypes.add(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+        AUDIO_ROUTE_TYPE_TO_DEVICE_INFO_TYPE.put(TYPE_BLUETOOTH_SCO, bluetoothScoDeviceInfoTypes);
+
+        List<Integer> bluetoothHearingAidDeviceInfoTypes = new ArrayList<>();
+        bluetoothHearingAidDeviceInfoTypes.add(AudioDeviceInfo.TYPE_HEARING_AID);
+        AUDIO_ROUTE_TYPE_TO_DEVICE_INFO_TYPE.put(TYPE_BLUETOOTH_HA,
+                bluetoothHearingAidDeviceInfoTypes);
+
+        List<Integer> bluetoothLeDeviceInfoTypes = new ArrayList<>();
+        bluetoothLeDeviceInfoTypes.add(AudioDeviceInfo.TYPE_BLE_HEADSET);
+        bluetoothLeDeviceInfoTypes.add(AudioDeviceInfo.TYPE_BLE_SPEAKER);
+        bluetoothLeDeviceInfoTypes.add(AudioDeviceInfo.TYPE_BLE_BROADCAST);
+        AUDIO_ROUTE_TYPE_TO_DEVICE_INFO_TYPE.put(TYPE_BLUETOOTH_LE, bluetoothLeDeviceInfoTypes);
+    }
+
+    int getType() {
+        return mAudioRouteType;
+    }
+
+    String getBluetoothAddress() {
+        return mBluetoothAddress;
+    }
+
+    // Invoked when entered pending route whose dest route is this route
+    void onDestRouteAsPendingRoute(boolean active, PendingAudioRoute pendingAudioRoute,
+                                   AudioManager audioManager) {
+        if (pendingAudioRoute.isActive() && !active) {
+            audioManager.clearCommunicationDevice();
+        } else if (active) {
+            if (mAudioRouteType == TYPE_BLUETOOTH_SCO) {
+                pendingAudioRoute.addMessage(BT_AUDIO_CONNECTED);
+            } else if (mAudioRouteType == TYPE_SPEAKER) {
+                pendingAudioRoute.addMessage(SPEAKER_ON);
+            }
+            if (!audioManager.setCommunicationDevice(mInfo)) {
+                pendingAudioRoute.onMessageReceived(PENDING_ROUTE_FAILED);
+            }
+        }
+    }
+
+    void onOrigRouteAsPendingRoute(boolean active, PendingAudioRoute pendingAudioRoute,
+                                   AudioManager audioManager) {
+        if (active) {
+            if (mAudioRouteType == TYPE_BLUETOOTH_SCO) {
+                pendingAudioRoute.addMessage(BT_AUDIO_DISCONNECTED);
+            } else if (mAudioRouteType == TYPE_SPEAKER) {
+                pendingAudioRoute.addMessage(SPEAKER_OFF);
+            }
+        }
+    }
+
+    @VisibleForTesting
+    public AudioRoute(@AudioRouteType int type, String bluetoothAddress, AudioDeviceInfo info) {
+        mAudioRouteType = type;
+        mBluetoothAddress = bluetoothAddress;
+        mInfo = info;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (!(obj instanceof AudioRoute otherRoute)) {
+            return false;
+        }
+        if (mAudioRouteType != otherRoute.getType()) {
+            return false;
+        }
+        return !BT_AUDIO_ROUTE_TYPES.contains(mAudioRouteType) || mBluetoothAddress.equals(
+                otherRoute.getBluetoothAddress());
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mAudioRouteType, mBluetoothAddress);
+    }
+
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() + "[Type=" + DEVICE_TYPE_STRINGS.get(mAudioRouteType)
+                + ", Address=" + ((mBluetoothAddress != null) ? mBluetoothAddress : "invalid")
+                + "]";
+    }
+}
diff --git a/src/com/android/server/telecom/CallAudioCommunicationDeviceTracker.java b/src/com/android/server/telecom/CallAudioCommunicationDeviceTracker.java
index 5fc2414..3a05eb5 100644
--- a/src/com/android/server/telecom/CallAudioCommunicationDeviceTracker.java
+++ b/src/com/android/server/telecom/CallAudioCommunicationDeviceTracker.java
@@ -16,6 +16,8 @@
 
 package com.android.server.telecom;
 
+import static com.android.server.telecom.AudioRoute.BT_AUDIO_DEVICE_INFO_TYPES;
+
 import android.bluetooth.BluetoothDevice;
 import android.content.Context;
 import android.media.AudioDeviceInfo;
@@ -28,7 +30,6 @@
 
 import java.util.Arrays;
 import java.util.List;
-import java.util.Set;
 import java.util.concurrent.Semaphore;
 
 /**
@@ -41,12 +42,6 @@
 
     // Use -1 indicates device is not set for any communication use case
     private static final int sAUDIO_DEVICE_TYPE_INVALID = -1;
-    // Possible bluetooth audio device types
-    private static final Set<Integer> sBT_AUDIO_DEVICE_TYPES = Set.of(
-            AudioDeviceInfo.TYPE_BLE_HEADSET,
-            AudioDeviceInfo.TYPE_HEARING_AID,
-            AudioDeviceInfo.TYPE_BLUETOOTH_SCO
-    );
     private AudioManager mAudioManager;
     private BluetoothRouteManager mBluetoothRouteManager;
     private int mAudioDeviceType = sAUDIO_DEVICE_TYPE_INVALID;
@@ -101,7 +96,7 @@
             mLock.tryAcquire();
         }
         // There is only one audio device type associated with each type of BT device.
-        boolean isBtDevice = sBT_AUDIO_DEVICE_TYPES.contains(audioDeviceType);
+        boolean isBtDevice = BT_AUDIO_DEVICE_INFO_TYPES.contains(audioDeviceType);
         Log.i(this, "setCommunicationDevice: type = %s, isBtDevice = %s, btDevice = %s",
                 audioDeviceType, isBtDevice, btDevice);
 
@@ -182,7 +177,7 @@
             mLock.tryAcquire();
         }
         // There is only one audio device type associated with each type of BT device.
-        boolean isBtDevice = sBT_AUDIO_DEVICE_TYPES.contains(audioDeviceType);
+        boolean isBtDevice = BT_AUDIO_DEVICE_INFO_TYPES.contains(audioDeviceType);
         Log.i(this, "clearCommunicationDevice: type = %s, isBtDevice = %s",
                 audioDeviceType, isBtDevice);
 
diff --git a/src/com/android/server/telecom/CallAudioRouteAdapter.java b/src/com/android/server/telecom/CallAudioRouteAdapter.java
index 7f7b43c..f76d47d 100644
--- a/src/com/android/server/telecom/CallAudioRouteAdapter.java
+++ b/src/com/android/server/telecom/CallAudioRouteAdapter.java
@@ -1,15 +1,131 @@
 package com.android.server.telecom;
 
+import android.bluetooth.BluetoothDevice;
 import android.os.Handler;
 import android.telecom.CallAudioState;
+import android.util.SparseArray;
 
 import com.android.internal.util.IndentingPrintWriter;
 
 public interface CallAudioRouteAdapter {
+    /** Valid values for msg.what */
+    int CONNECT_WIRED_HEADSET = 1;
+    int DISCONNECT_WIRED_HEADSET = 2;
+    int CONNECT_DOCK = 5;
+    int DISCONNECT_DOCK = 6;
+    int BLUETOOTH_DEVICE_LIST_CHANGED = 7;
+    int BT_ACTIVE_DEVICE_PRESENT = 8;
+    int BT_ACTIVE_DEVICE_GONE = 9;
+    int BT_DEVICE_ADDED = 10;
+    int BT_DEVICE_REMOVED = 11;
+
+    int SWITCH_EARPIECE = 1001;
+    int SWITCH_BLUETOOTH = 1002;
+    int SWITCH_HEADSET = 1003;
+    int SWITCH_SPEAKER = 1004;
+    // Wired headset, earpiece, or speakerphone, in that order of precedence.
+    int SWITCH_BASELINE_ROUTE = 1005;
+
+    // Messages denoting that the speakerphone was turned on/off. Used to update state when we
+    // weren't the ones who turned it on/off
+    int SPEAKER_ON = 1006;
+    int SPEAKER_OFF = 1007;
+
+    // Messages denoting that the streaming route switch request was sent.
+    int STREAMING_FORCE_ENABLED = 1008;
+    int STREAMING_FORCE_DISABLED = 1009;
+
+    int USER_SWITCH_EARPIECE = 1101;
+    int USER_SWITCH_BLUETOOTH = 1102;
+    int USER_SWITCH_HEADSET = 1103;
+    int USER_SWITCH_SPEAKER = 1104;
+    int USER_SWITCH_BASELINE_ROUTE = 1105;
+
+    int UPDATE_SYSTEM_AUDIO_ROUTE = 1201;
+
+    // These three messages indicate state changes that come from BluetoothRouteManager.
+    // They may be triggered by the BT stack doing something on its own or they may be sent after
+    // we request that the BT stack do something. Any logic for these messages should take into
+    // account the possibility that the event indicated has already been processed (i.e. handling
+    // should be idempotent).
+    int BT_AUDIO_DISCONNECTED = 1301;
+    int BT_AUDIO_CONNECTED = 1302;
+    int BT_AUDIO_PENDING = 1303;
+
+    int MUTE_ON = 3001;
+    int MUTE_OFF = 3002;
+    int TOGGLE_MUTE = 3003;
+    int MUTE_EXTERNALLY_CHANGED = 3004;
+
+    int SWITCH_FOCUS = 4001;
+
+    // Used in testing to execute verifications. Not compatible with subsessions.
+    int RUN_RUNNABLE = 9001;
+
+    // Used for PendingAudioRoute to notify audio switch success
+    int EXIT_PENDING_ROUTE = 10001;
+    // Used for PendingAudioRoute to notify audio switch timeout
+    int PENDING_ROUTE_TIMEOUT = 10002;
+    // Used for PendingAudioRoute to notify audio switch failed
+    int PENDING_ROUTE_FAILED = 10003;
+
+    /** Valid values for mAudioFocusType */
+    int NO_FOCUS = 1;
+    int ACTIVE_FOCUS = 2;
+    int RINGING_FOCUS = 3;
+
+    /** Valid arg for BLUETOOTH_DEVICE_LIST_CHANGED */
+    int DEVICE_CONNECTED = 1;
+    int DEVICE_DISCONNECTED = 2;
+
+    SparseArray<String> MESSAGE_CODE_TO_NAME = new SparseArray<String>() {{
+        put(CONNECT_WIRED_HEADSET, "CONNECT_WIRED_HEADSET");
+        put(DISCONNECT_WIRED_HEADSET, "DISCONNECT_WIRED_HEADSET");
+        put(CONNECT_DOCK, "CONNECT_DOCK");
+        put(DISCONNECT_DOCK, "DISCONNECT_DOCK");
+        put(BLUETOOTH_DEVICE_LIST_CHANGED, "BLUETOOTH_DEVICE_LIST_CHANGED");
+        put(BT_ACTIVE_DEVICE_PRESENT, "BT_ACTIVE_DEVICE_PRESENT");
+        put(BT_ACTIVE_DEVICE_GONE, "BT_ACTIVE_DEVICE_GONE");
+        put(BT_DEVICE_ADDED, "BT_DEVICE_ADDED");
+        put(BT_DEVICE_REMOVED, "BT_DEVICE_REMOVED");
+
+        put(SWITCH_EARPIECE, "SWITCH_EARPIECE");
+        put(SWITCH_BLUETOOTH, "SWITCH_BLUETOOTH");
+        put(SWITCH_HEADSET, "SWITCH_HEADSET");
+        put(SWITCH_SPEAKER, "SWITCH_SPEAKER");
+        put(SWITCH_BASELINE_ROUTE, "SWITCH_BASELINE_ROUTE");
+        put(SPEAKER_ON, "SPEAKER_ON");
+        put(SPEAKER_OFF, "SPEAKER_OFF");
+
+        put(USER_SWITCH_EARPIECE, "USER_SWITCH_EARPIECE");
+        put(USER_SWITCH_BLUETOOTH, "USER_SWITCH_BLUETOOTH");
+        put(USER_SWITCH_HEADSET, "USER_SWITCH_HEADSET");
+        put(USER_SWITCH_SPEAKER, "USER_SWITCH_SPEAKER");
+        put(USER_SWITCH_BASELINE_ROUTE, "USER_SWITCH_BASELINE_ROUTE");
+
+        put(UPDATE_SYSTEM_AUDIO_ROUTE, "UPDATE_SYSTEM_AUDIO_ROUTE");
+
+        put(BT_AUDIO_DISCONNECTED, "BT_AUDIO_DISCONNECTED");
+        put(BT_AUDIO_CONNECTED, "BT_AUDIO_CONNECTED");
+        put(BT_AUDIO_PENDING, "BT_AUDIO_PENDING");
+
+        put(MUTE_ON, "MUTE_ON");
+        put(MUTE_OFF, "MUTE_OFF");
+        put(TOGGLE_MUTE, "TOGGLE_MUTE");
+        put(MUTE_EXTERNALLY_CHANGED, "MUTE_EXTERNALLY_CHANGED");
+
+        put(SWITCH_FOCUS, "SWITCH_FOCUS");
+
+        put(RUN_RUNNABLE, "RUN_RUNNABLE");
+
+        put(EXIT_PENDING_ROUTE, "EXIT_PENDING_ROUTE");
+    }};
+
     void initialize();
     void sendMessageWithSessionInfo(int message);
     void sendMessageWithSessionInfo(int message, int arg);
     void sendMessageWithSessionInfo(int message, int arg, String data);
+    void sendMessageWithSessionInfo(int message, int arg, BluetoothDevice bluetoothDevice);
     void sendMessage(int message, Runnable r);
     void setCallAudioManager(CallAudioManager callAudioManager);
     CallAudioState getCurrentCallAudioState();
diff --git a/src/com/android/server/telecom/CallAudioRouteController.java b/src/com/android/server/telecom/CallAudioRouteController.java
index f8c49bb..c1d7d0c 100644
--- a/src/com/android/server/telecom/CallAudioRouteController.java
+++ b/src/com/android/server/telecom/CallAudioRouteController.java
@@ -1,41 +1,226 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
 package com.android.server.telecom;
 
+import static com.android.server.telecom.AudioRoute.BT_AUDIO_ROUTE_TYPES;
+import static com.android.server.telecom.AudioRoute.TYPE_INVALID;
+
+import android.bluetooth.BluetoothDevice;
+import android.content.Context;
+import android.media.AudioAttributes;
+import android.media.AudioDeviceAttributes;
+import android.media.AudioDeviceInfo;
+import android.media.AudioManager;
+import android.media.audiopolicy.AudioProductStrategy;
 import android.os.Handler;
 import android.os.HandlerThread;
-import android.os.Looper;
+import android.os.Message;
 import android.telecom.CallAudioState;
+import android.telecom.Log;
+import android.telecom.Logging.Session;
+import android.util.ArrayMap;
 
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.SomeArgs;
 import com.android.internal.util.IndentingPrintWriter;
 
-public class CallAudioRouteController implements CallAudioRouteAdapter {
-    private Handler mHandler;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
 
-    public CallAudioRouteController() {
+public class CallAudioRouteController implements CallAudioRouteAdapter {
+    private static final long TIMEOUT_LIMIT = 2000L;
+    private static final AudioRoute DUMMY_ROUTE = new AudioRoute(TYPE_INVALID, null, null);
+    private static final Map<Integer, Integer> ROUTE_MAP;
+    static {
+        ROUTE_MAP = new ArrayMap<>();
+        ROUTE_MAP.put(AudioRoute.TYPE_EARPIECE, CallAudioState.ROUTE_EARPIECE);
+        ROUTE_MAP.put(AudioRoute.TYPE_WIRED, CallAudioState.ROUTE_WIRED_HEADSET);
+        ROUTE_MAP.put(AudioRoute.TYPE_SPEAKER, CallAudioState.ROUTE_SPEAKER);
+        ROUTE_MAP.put(AudioRoute.TYPE_DOCK, CallAudioState.ROUTE_SPEAKER);
+        ROUTE_MAP.put(AudioRoute.TYPE_BLUETOOTH_SCO, CallAudioState.ROUTE_BLUETOOTH);
+        ROUTE_MAP.put(AudioRoute.TYPE_BLUETOOTH_HA, CallAudioState.ROUTE_BLUETOOTH);
+        ROUTE_MAP.put(AudioRoute.TYPE_BLUETOOTH_LE, CallAudioState.ROUTE_BLUETOOTH);
+    }
+
+    private final CallsManager mCallsManager;
+    private AudioManager mAudioManager;
+    private final Handler mHandler;
+    private final WiredHeadsetManager mWiredHeadsetManager;
+    private Set<AudioRoute> mAvailableRoutes;
+    private AudioRoute mCurrentRoute;
+    private AudioRoute mEarpieceWiredRoute;
+    private AudioRoute mSpeakerDockRoute;
+    private Map<AudioRoute, BluetoothDevice> mBluetoothRoutes;
+    private Map<Integer, AudioRoute> mTypeRoutes;
+    private PendingAudioRoute mPendingAudioRoute;
+    private AudioRoute.Factory mAudioRouteFactory;
+    private CallAudioState mCallAudioState;
+    private boolean mIsMute;
+    private boolean mIsPending;
+    private boolean mIsActive;
+
+    public CallAudioRouteController(
+            Context context,
+            CallsManager callsManager,
+            AudioRoute.Factory audioRouteFactory,
+            WiredHeadsetManager wiredHeadsetManager) {
+        mCallsManager = callsManager;
+        mAudioManager = context.getSystemService(AudioManager.class);
+        mAudioRouteFactory = audioRouteFactory;
+        mWiredHeadsetManager = wiredHeadsetManager;
+        mIsMute = false;
         HandlerThread handlerThread = new HandlerThread(this.getClass().getSimpleName());
         handlerThread.start();
-        mHandler = new Handler(handlerThread.getLooper());
+        mHandler = new Handler(handlerThread.getLooper()) {
+            @Override
+            public void handleMessage(Message msg) {
+                preHandleMessage(msg);
+                String address;
+                BluetoothDevice bluetoothDevice;
+                @AudioRoute.AudioRouteType int type;
+                switch (msg.what) {
+                    case BT_AUDIO_CONNECTED:
+                        bluetoothDevice = (BluetoothDevice) ((SomeArgs) msg.obj).arg2;
+                        handleBtAudioActive(bluetoothDevice);
+                        break;
+                    case BT_AUDIO_DISCONNECTED:
+                        bluetoothDevice = (BluetoothDevice) ((SomeArgs) msg.obj).arg2;
+                        handleBtAudioInactive(bluetoothDevice);
+                        break;
+                    case BT_DEVICE_ADDED:
+                        type = msg.arg1;
+                        bluetoothDevice = (BluetoothDevice) ((SomeArgs) msg.obj).arg2;
+                        handleBtConnected(type, bluetoothDevice);
+                        break;
+                    case BT_DEVICE_REMOVED:
+                        type = msg.arg1;
+                        bluetoothDevice = (BluetoothDevice) ((SomeArgs) msg.obj).arg2;
+                        handleBtDisconnected(type, bluetoothDevice);
+                        break;
+                    case BLUETOOTH_DEVICE_LIST_CHANGED:
+                        break;
+                    case BT_ACTIVE_DEVICE_PRESENT:
+                        type = msg.arg1;
+                        address = (String) ((SomeArgs) msg.obj).arg2;
+                        handleBtActiveDevicePresent(type, address);
+                        break;
+                    case BT_ACTIVE_DEVICE_GONE:
+                        type = msg.arg1;
+                        handleBtActiveDeviceGone(type);
+                        break;
+                    case EXIT_PENDING_ROUTE:
+                        handleExitPendingRoute();
+                        break;
+                    default:
+                        break;
+                }
+                postHandleMessage(msg);
+            }
+        };
     }
     @Override
     public void initialize() {
+        mAvailableRoutes = new HashSet<>();
+        mBluetoothRoutes = new ArrayMap<>();
+        mTypeRoutes = new ArrayMap<>();
+        mPendingAudioRoute = new PendingAudioRoute(this, mAudioManager);
+
+        int supportMask = calculateSupportedRouteMask();
+        if ((supportMask & CallAudioState.ROUTE_SPEAKER) != 0) {
+            // Create spekaer routes
+            mSpeakerDockRoute = mAudioRouteFactory.create(AudioRoute.TYPE_SPEAKER, null,
+                    mAudioManager);
+            if (mSpeakerDockRoute == null) {
+                Log.w(this, "Can't find available audio device info for route TYPE_SPEAKER");
+            } else {
+                mTypeRoutes.put(AudioRoute.TYPE_SPEAKER, mSpeakerDockRoute);
+                mAvailableRoutes.add(mSpeakerDockRoute);
+            }
+        }
+
+        if ((supportMask & CallAudioState.ROUTE_WIRED_HEADSET) != 0) {
+            // Create wired headset routes
+            mEarpieceWiredRoute = mAudioRouteFactory.create(AudioRoute.TYPE_WIRED, null,
+                    mAudioManager);
+            if (mEarpieceWiredRoute == null) {
+                Log.w(this, "Can't find available audio device info for route TYPE_WIRED_HEADSET");
+            } else {
+                mTypeRoutes.put(AudioRoute.TYPE_WIRED, mEarpieceWiredRoute);
+                mAvailableRoutes.add(mEarpieceWiredRoute);
+            }
+        } else if ((supportMask & CallAudioState.ROUTE_EARPIECE) != 0) {
+            // Create earpiece routes
+            mEarpieceWiredRoute = mAudioRouteFactory.create(AudioRoute.TYPE_EARPIECE, null,
+                    mAudioManager);
+            if (mEarpieceWiredRoute == null) {
+                Log.w(this, "Can't find available audio device info for route TYPE_EARPIECE");
+            } else {
+                mTypeRoutes.put(AudioRoute.TYPE_EARPIECE, mEarpieceWiredRoute);
+                mAvailableRoutes.add(mEarpieceWiredRoute);
+            }
+        }
+
+        // set current route
+        if (mEarpieceWiredRoute != null) {
+            mCurrentRoute = mEarpieceWiredRoute;
+        } else {
+            mCurrentRoute = mSpeakerDockRoute;
+        }
+        mIsActive = false;
+        mCallAudioState = new CallAudioState(mIsMute, ROUTE_MAP.get(mCurrentRoute.getType()),
+                supportMask, null, new HashSet<>());
     }
 
     @Override
     public void sendMessageWithSessionInfo(int message) {
+        sendMessageWithSessionInfo(message, 0, (String) null);
     }
 
     @Override
     public void sendMessageWithSessionInfo(int message, int arg) {
-
+        sendMessageWithSessionInfo(message, arg, (String) null);
     }
 
     @Override
     public void sendMessageWithSessionInfo(int message, int arg, String data) {
+        SomeArgs args = SomeArgs.obtain();
+        args.arg1 = Log.createSubsession();
+        args.arg2 = data;
+        sendMessage(message, arg, 0, args);
+    }
 
+    @Override
+    public void sendMessageWithSessionInfo(int message, int arg, BluetoothDevice bluetoothDevice) {
+        SomeArgs args = SomeArgs.obtain();
+        args.arg1 = Log.createSubsession();
+        args.arg2 = bluetoothDevice;
+        sendMessage(message, arg, 0, args);
     }
 
     @Override
     public void sendMessage(int message, Runnable r) {
+        r.run();
+    }
 
+    private void sendMessage(int what, int arg1, int arg2, Object obj) {
+        mHandler.sendMessage(Message.obtain(mHandler, what, arg1, arg2, obj));
     }
 
     @Override
@@ -49,7 +234,7 @@
 
     @Override
     public boolean isHfpDeviceAvailable() {
-        return false;
+        return !mBluetoothRoutes.isEmpty();
     }
 
     @Override
@@ -59,6 +244,287 @@
 
     @Override
     public void dump(IndentingPrintWriter pw) {
+    }
 
+    private void preHandleMessage(Message msg) {
+        if (msg.obj instanceof SomeArgs) {
+            Session session = (Session) ((SomeArgs) msg.obj).arg1;
+            String messageCodeName = MESSAGE_CODE_TO_NAME.get(msg.what, "unknown");
+            Log.continueSession(session, "CARC.pM_" + messageCodeName);
+            Log.i(this, "Message received: %s=%d, arg1=%d", messageCodeName, msg.what, msg.arg1);
+        }
+    }
+
+    private void postHandleMessage(Message msg) {
+        Log.endSession();
+        if (msg.obj instanceof SomeArgs) {
+            ((SomeArgs) msg.obj).recycle();
+        }
+    }
+
+    public boolean isActive() {
+        return mIsActive;
+    }
+
+    public boolean isPending() {
+        return mIsPending;
+    }
+
+    private void routeTo(boolean active, AudioRoute destRoute) {
+        if (mIsPending) {
+            if (destRoute.equals(mPendingAudioRoute.getDestRoute()) && (mIsActive == active)) {
+                return;
+            }
+            Log.i(this, "Override current pending route destination from %s(active=%b) to "
+                            + "%s(active=%b)",
+                    mPendingAudioRoute.getDestRoute(), mIsActive, destRoute, active);
+            // override pending route while keep waiting for still pending messages for the
+            // previous pending route
+            mPendingAudioRoute.setOrigRoute(mIsActive, mPendingAudioRoute.getDestRoute());
+            mPendingAudioRoute.setDestRoute(active, destRoute);
+        } else {
+            if (mCurrentRoute.equals(destRoute) && (mIsActive = active)) {
+                return;
+            }
+            Log.i(this, "Enter pending route, orig%s(active=%b), dest%s(active=%b)", mCurrentRoute,
+                    mIsActive, destRoute, active);
+            // route to pending route
+            if (mAvailableRoutes.contains(mCurrentRoute)) {
+                mPendingAudioRoute.setOrigRoute(mIsActive, mCurrentRoute);
+            } else {
+                // Avoid waiting for pending messages for an unavailable route
+                mPendingAudioRoute.setOrigRoute(mIsActive, DUMMY_ROUTE);
+            }
+            mPendingAudioRoute.setDestRoute(active, destRoute);
+            mIsPending = true;
+        }
+        mPendingAudioRoute.evaluatePendingState();
+        postTimeoutMessage();
+    }
+
+    private void postTimeoutMessage() {
+        // reset timeout handler
+        mHandler.removeMessages(PENDING_ROUTE_TIMEOUT);
+        mHandler.postDelayed(() -> mHandler.sendMessage(
+                Message.obtain(mHandler, PENDING_ROUTE_TIMEOUT)), TIMEOUT_LIMIT);
+    }
+
+    private void handleBtAudioActive(BluetoothDevice bluetoothDevice) {
+        if (mIsPending) {
+            if (Objects.equals(mPendingAudioRoute.getDestRoute().getBluetoothAddress(),
+                    bluetoothDevice.getAddress())) {
+                mPendingAudioRoute.onMessageReceived(BT_AUDIO_CONNECTED);
+            }
+        } else {
+            // ignore, not triggered by telecom
+        }
+    }
+
+    private void handleBtAudioInactive(BluetoothDevice bluetoothDevice) {
+        if (mIsPending) {
+            if (Objects.equals(mPendingAudioRoute.getOrigRoute().getBluetoothAddress(),
+                    bluetoothDevice.getAddress())) {
+                mPendingAudioRoute.onMessageReceived(BT_AUDIO_DISCONNECTED);
+            }
+        } else {
+            // ignore, not triggered by telecom
+        }
+    }
+
+    private void handleBtConnected(@AudioRoute.AudioRouteType int type,
+                                   BluetoothDevice bluetoothDevice) {
+        AudioRoute bluetoothRoute = null;
+        bluetoothRoute = mAudioRouteFactory.create(type, bluetoothDevice.getAddress(),
+                mAudioManager);
+        if (bluetoothRoute == null) {
+            Log.w(this, "Can't find available audio device info for route type:"
+                    + AudioRoute.DEVICE_TYPE_STRINGS.get(type));
+        } else {
+            Log.i(this, "bluetooth route added: " + bluetoothRoute);
+            mAvailableRoutes.add(bluetoothRoute);
+            mBluetoothRoutes.put(bluetoothRoute, bluetoothDevice);
+            onAvailableRoutesChanged();
+        }
+    }
+
+    private void handleBtDisconnected(@AudioRoute.AudioRouteType int type,
+                                      BluetoothDevice bluetoothDevice) {
+        // Clean up unavailable routes
+        AudioRoute bluetoothRoute = getBluetoothRoute(type, bluetoothDevice.getAddress());
+        if (bluetoothRoute != null) {
+            Log.i(this, "bluetooth route removed: " + bluetoothRoute);
+            mBluetoothRoutes.remove(bluetoothRoute);
+            mAvailableRoutes.remove(bluetoothRoute);
+            onAvailableRoutesChanged();
+        }
+
+        // Fallback to an available route
+        if (Objects.equals(mCurrentRoute, bluetoothRoute)) {
+            // fallback policy
+            AudioRoute destRoute = getPreferredAudioRouteFromStrategy();
+            if (destRoute != null && mAvailableRoutes.contains(destRoute)) {
+                routeTo(mIsActive, destRoute);
+            } else {
+                routeTo(mIsActive, getPreferredAudioRouteFromDefault(true/* includeBluetooth */));
+            }
+        }
+    }
+
+    private void handleBtActiveDevicePresent(@AudioRoute.AudioRouteType int type,
+                                             String deviceAddress) {
+        AudioRoute bluetoothRoute = getBluetoothRoute(type, deviceAddress);
+        if (bluetoothRoute != null) {
+            Log.i(this, "request to route to bluetooth route: %s(active=%b)", bluetoothRoute,
+                    mIsActive);
+            routeTo(mIsActive, bluetoothRoute);
+        }
+    }
+
+    private void handleBtActiveDeviceGone(@AudioRoute.AudioRouteType int type) {
+        if ((mIsPending && mPendingAudioRoute.getDestRoute().getType() == type)
+                || (!mIsPending && mCurrentRoute.getType() == type)) {
+            // Fallback to an available route
+            AudioRoute destRoute = getPreferredAudioRouteFromStrategy();
+            if (destRoute != null && mAvailableRoutes.contains(destRoute)) {
+                routeTo(mIsActive, destRoute);
+            } else {
+                routeTo(mIsActive, getPreferredAudioRouteFromDefault(false/* includeBluetooth */));
+            }
+            onAvailableRoutesChanged();
+        }
+    }
+
+    public void handleExitPendingRoute() {
+        if (mIsPending) {
+            Log.i(this, "Exit pending route and enter %s(active=%b)",
+                    mPendingAudioRoute.getDestRoute(), mPendingAudioRoute.isActive());
+            mCurrentRoute = mPendingAudioRoute.getDestRoute();
+            mIsActive = mPendingAudioRoute.isActive();
+            mIsPending = false;
+            onCurrentRouteChanged();
+        }
+    }
+
+    private void onCurrentRouteChanged() {
+        BluetoothDevice activeBluetoothDevice = null;
+        int route = ROUTE_MAP.get(mCurrentRoute.getType());
+        if (route == CallAudioState.ROUTE_BLUETOOTH) {
+            activeBluetoothDevice = mBluetoothRoutes.get(mCurrentRoute);
+        }
+        updateCallAudioState(new CallAudioState(mIsMute, route,
+                mCallAudioState.getSupportedRouteMask(), activeBluetoothDevice,
+                mCallAudioState.getSupportedBluetoothDevices()));
+    }
+
+    private void onAvailableRoutesChanged() {
+        int routeMask = 0;
+        Set<BluetoothDevice> availableBluetoothDevices = new HashSet<>();
+        for (AudioRoute route : mAvailableRoutes) {
+            routeMask |= ROUTE_MAP.get(route.getType());
+            if (BT_AUDIO_ROUTE_TYPES.contains(route.getType())) {
+                availableBluetoothDevices.add(mBluetoothRoutes.get(route));
+            }
+        }
+        updateCallAudioState(new CallAudioState(mIsMute, mCallAudioState.getRoute(), routeMask,
+                mCallAudioState.getActiveBluetoothDevice(), availableBluetoothDevices));
+    }
+
+    private void updateCallAudioState(CallAudioState callAudioState) {
+        CallAudioState oldState = mCallAudioState;
+        mCallAudioState = callAudioState;
+        mCallsManager.onCallAudioStateChanged(oldState, mCallAudioState);
+    }
+
+    private AudioRoute getPreferredAudioRouteFromStrategy() {
+        // Get audio produce strategy
+        AudioProductStrategy strategy = null;
+        final AudioAttributes attr = new AudioAttributes.Builder()
+                .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
+                .build();
+        List<AudioProductStrategy> strategies = AudioManager.getAudioProductStrategies();
+        for (AudioProductStrategy s : strategies) {
+            if (s.supportsAudioAttributes(attr)) {
+                strategy = s;
+            }
+        }
+        if (strategy == null) {
+            return null;
+        }
+
+        // Get preferred device
+        AudioDeviceAttributes deviceAttr = mAudioManager.getPreferredDeviceForStrategy(strategy);
+        if (deviceAttr == null) {
+            return null;
+        }
+
+        // Get corresponding audio route
+        @AudioRoute.AudioRouteType int type = AudioRoute.DEVICE_INFO_TYPETO_AUDIO_ROUTE_TYPE.get(
+                deviceAttr.getType());
+        if (BT_AUDIO_ROUTE_TYPES.contains(type)) {
+            return getBluetoothRoute(type, deviceAttr.getAddress());
+        } else {
+            return mTypeRoutes.get(deviceAttr.getType());
+
+        }
+    }
+
+    public AudioRoute getPreferredAudioRouteFromDefault(boolean includeBluetooth) {
+        if (mBluetoothRoutes.isEmpty() || !includeBluetooth) {
+            return mEarpieceWiredRoute != null ? mEarpieceWiredRoute : mSpeakerDockRoute;
+        } else {
+            // Most recent active route will always be the last in the array
+            return mBluetoothRoutes.keySet().stream().toList().get(mBluetoothRoutes.size() - 1);
+        }
+    }
+
+    private int calculateSupportedRouteMask() {
+        int routeMask = CallAudioState.ROUTE_SPEAKER;
+
+        if (mWiredHeadsetManager.isPluggedIn()) {
+            routeMask |= CallAudioState.ROUTE_WIRED_HEADSET;
+        } else {
+            AudioDeviceInfo[] deviceList = mAudioManager.getDevices(
+                    AudioManager.GET_DEVICES_OUTPUTS);
+            for (AudioDeviceInfo device: deviceList) {
+                if (device.getType() == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE) {
+                    routeMask |= CallAudioState.ROUTE_EARPIECE;
+                    break;
+                }
+            }
+        }
+        return routeMask;
+    }
+
+    public Set<AudioRoute> getAvailableRoutes() {
+        return mAvailableRoutes;
+    }
+
+    public AudioRoute getCurrentRoute() {
+        return mCurrentRoute;
+    }
+
+    private AudioRoute getBluetoothRoute(@AudioRoute.AudioRouteType int audioRouteType,
+                                         String address) {
+        for (AudioRoute route : mBluetoothRoutes.keySet()) {
+            if (route.getType() == audioRouteType && route.getBluetoothAddress().equals(address)) {
+                return route;
+            }
+        }
+        return null;
+    }
+
+    @VisibleForTesting
+    public void setAudioManager(AudioManager audioManager) {
+        mAudioManager = audioManager;
+    }
+
+    @VisibleForTesting
+    public void setAudioRouteFactory(AudioRoute.Factory audioRouteFactory) {
+        mAudioRouteFactory = audioRouteFactory;
+    }
+
+    @VisibleForTesting
+    public void setActive(boolean active) {
+        mIsActive = active;
     }
 }
diff --git a/src/com/android/server/telecom/CallAudioRouteStateMachine.java b/src/com/android/server/telecom/CallAudioRouteStateMachine.java
index c0bb50e..26c25e8 100644
--- a/src/com/android/server/telecom/CallAudioRouteStateMachine.java
+++ b/src/com/android/server/telecom/CallAudioRouteStateMachine.java
@@ -120,63 +120,6 @@
     /** Direct the audio stream through another device. */
     public static final int ROUTE_STREAMING     = CallAudioState.ROUTE_STREAMING;
 
-    /** Valid values for msg.what */
-    public static final int CONNECT_WIRED_HEADSET = 1;
-    public static final int DISCONNECT_WIRED_HEADSET = 2;
-    public static final int CONNECT_DOCK = 5;
-    public static final int DISCONNECT_DOCK = 6;
-    public static final int BLUETOOTH_DEVICE_LIST_CHANGED = 7;
-    public static final int BT_ACTIVE_DEVICE_PRESENT = 8;
-    public static final int BT_ACTIVE_DEVICE_GONE = 9;
-
-    public static final int SWITCH_EARPIECE = 1001;
-    public static final int SWITCH_BLUETOOTH = 1002;
-    public static final int SWITCH_HEADSET = 1003;
-    public static final int SWITCH_SPEAKER = 1004;
-    // Wired headset, earpiece, or speakerphone, in that order of precedence.
-    public static final int SWITCH_BASELINE_ROUTE = 1005;
-
-    // Messages denoting that the speakerphone was turned on/off. Used to update state when we
-    // weren't the ones who turned it on/off
-    public static final int SPEAKER_ON = 1006;
-    public static final int SPEAKER_OFF = 1007;
-
-    // Messages denoting that the streaming route switch request was sent.
-    public static final int STREAMING_FORCE_ENABLED = 1008;
-    public static final int STREAMING_FORCE_DISABLED = 1009;
-
-    public static final int USER_SWITCH_EARPIECE = 1101;
-    public static final int USER_SWITCH_BLUETOOTH = 1102;
-    public static final int USER_SWITCH_HEADSET = 1103;
-    public static final int USER_SWITCH_SPEAKER = 1104;
-    public static final int USER_SWITCH_BASELINE_ROUTE = 1105;
-
-    public static final int UPDATE_SYSTEM_AUDIO_ROUTE = 1201;
-
-    // These three messages indicate state changes that come from BluetoothRouteManager.
-    // They may be triggered by the BT stack doing something on its own or they may be sent after
-    // we request that the BT stack do something. Any logic for these messages should take into
-    // account the possibility that the event indicated has already been processed (i.e. handling
-    // should be idempotent).
-    public static final int BT_AUDIO_DISCONNECTED = 1301;
-    public static final int BT_AUDIO_CONNECTED = 1302;
-    public static final int BT_AUDIO_PENDING = 1303;
-
-    public static final int MUTE_ON = 3001;
-    public static final int MUTE_OFF = 3002;
-    public static final int TOGGLE_MUTE = 3003;
-    public static final int MUTE_EXTERNALLY_CHANGED = 3004;
-
-    public static final int SWITCH_FOCUS = 4001;
-
-    // Used in testing to execute verifications. Not compatible with subsessions.
-    public static final int RUN_RUNNABLE = 9001;
-
-    /** Valid values for mAudioFocusType */
-    public static final int NO_FOCUS = 1;
-    public static final int ACTIVE_FOCUS = 2;
-    public static final int RINGING_FOCUS = 3;
-
     /** Valid values for the first argument for SWITCH_BASELINE_ROUTE */
     public static final int NO_INCLUDE_BLUETOOTH_IN_BASELINE = 0;
     public static final int INCLUDE_BLUETOOTH_IN_BASELINE = 1;
@@ -189,45 +132,6 @@
         put(CallAudioState.ROUTE_WIRED_HEADSET, LogUtils.Events.AUDIO_ROUTE_HEADSET);
     }};
 
-    private static final SparseArray<String> MESSAGE_CODE_TO_NAME = new SparseArray<String>() {{
-        put(CONNECT_WIRED_HEADSET, "CONNECT_WIRED_HEADSET");
-        put(DISCONNECT_WIRED_HEADSET, "DISCONNECT_WIRED_HEADSET");
-        put(CONNECT_DOCK, "CONNECT_DOCK");
-        put(DISCONNECT_DOCK, "DISCONNECT_DOCK");
-        put(BLUETOOTH_DEVICE_LIST_CHANGED, "BLUETOOTH_DEVICE_LIST_CHANGED");
-        put(BT_ACTIVE_DEVICE_PRESENT, "BT_ACTIVE_DEVICE_PRESENT");
-        put(BT_ACTIVE_DEVICE_GONE, "BT_ACTIVE_DEVICE_GONE");
-
-        put(SWITCH_EARPIECE, "SWITCH_EARPIECE");
-        put(SWITCH_BLUETOOTH, "SWITCH_BLUETOOTH");
-        put(SWITCH_HEADSET, "SWITCH_HEADSET");
-        put(SWITCH_SPEAKER, "SWITCH_SPEAKER");
-        put(SWITCH_BASELINE_ROUTE, "SWITCH_BASELINE_ROUTE");
-        put(SPEAKER_ON, "SPEAKER_ON");
-        put(SPEAKER_OFF, "SPEAKER_OFF");
-
-        put(USER_SWITCH_EARPIECE, "USER_SWITCH_EARPIECE");
-        put(USER_SWITCH_BLUETOOTH, "USER_SWITCH_BLUETOOTH");
-        put(USER_SWITCH_HEADSET, "USER_SWITCH_HEADSET");
-        put(USER_SWITCH_SPEAKER, "USER_SWITCH_SPEAKER");
-        put(USER_SWITCH_BASELINE_ROUTE, "USER_SWITCH_BASELINE_ROUTE");
-
-        put(UPDATE_SYSTEM_AUDIO_ROUTE, "UPDATE_SYSTEM_AUDIO_ROUTE");
-
-        put(BT_AUDIO_DISCONNECTED, "BT_AUDIO_DISCONNECTED");
-        put(BT_AUDIO_CONNECTED, "BT_AUDIO_CONNECTED");
-        put(BT_AUDIO_PENDING, "BT_AUDIO_PENDING");
-
-        put(MUTE_ON, "MUTE_ON");
-        put(MUTE_OFF, "MUTE_OFF");
-        put(TOGGLE_MUTE, "TOGGLE_MUTE");
-        put(MUTE_EXTERNALLY_CHANGED, "MUTE_EXTERNALLY_CHANGED");
-
-        put(SWITCH_FOCUS, "SWITCH_FOCUS");
-
-        put(RUN_RUNNABLE, "RUN_RUNNABLE");
-    }};
-
     private static final String ACTIVE_EARPIECE_ROUTE_NAME = "ActiveEarpieceRoute";
     private static final String ACTIVE_BLUETOOTH_ROUTE_NAME = "ActiveBluetoothRoute";
     private static final String ACTIVE_SPEAKER_ROUTE_NAME = "ActiveSpeakerRoute";
@@ -1737,11 +1641,11 @@
     }
 
     public void sendMessageWithSessionInfo(int message, int arg) {
-        sendMessageWithSessionInfo(message, arg, null);
+        sendMessageWithSessionInfo(message, arg, (String) null);
     }
 
     public void sendMessageWithSessionInfo(int message) {
-        sendMessageWithSessionInfo(message, 0, null);
+        sendMessageWithSessionInfo(message, 0, (String) null);
     }
 
     public void sendMessageWithSessionInfo(int message, int arg, String data) {
@@ -1751,6 +1655,10 @@
         sendMessage(message, arg, 0, args);
     }
 
+    public void sendMessageWithSessionInfo(int message, int arg, BluetoothDevice bluetoothDevice) {
+        // ignore, only used in CallAudioRouteController
+    }
+
     @Override
     public void sendMessage(int message, Runnable r) {
         super.sendMessage(message, r);
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index 2570aef..de7d84a 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -646,9 +646,11 @@
                     featureFlags
             );
         } else {
-            callAudioRouteAdapter = new CallAudioRouteController();
+            callAudioRouteAdapter = new CallAudioRouteController(
+                    context, this, new AudioRoute.Factory(), wiredHeadsetManager);
         }
         callAudioRouteAdapter.initialize();
+        bluetoothStateReceiver.setCallAudioRouteAdapter(callAudioRouteAdapter);
 
         CallAudioRoutePeripheralAdapter callAudioRoutePeripheralAdapter =
                 new CallAudioRoutePeripheralAdapter(
diff --git a/src/com/android/server/telecom/PendingAudioRoute.java b/src/com/android/server/telecom/PendingAudioRoute.java
new file mode 100644
index 0000000..5fa3048
--- /dev/null
+++ b/src/com/android/server/telecom/PendingAudioRoute.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.telecom;
+
+import static com.android.server.telecom.CallAudioRouteAdapter.PENDING_ROUTE_FAILED;
+
+import android.media.AudioManager;
+
+import java.util.ArrayList;
+
+/**
+ * Used to represent the intermediate state during audio route switching.
+ * Usually, audio route switching start with a communication device setting request to audio
+ * framework and will be completed with corresponding success broadcasts or messages. Instance of
+ * this class is responsible for tracking the pending success signals according to the original
+ * audio route and the destination audio route of this switching.
+ */
+public class PendingAudioRoute {
+    private CallAudioRouteController mCallAudioRouteController;
+    private AudioManager mAudioManager;
+    /**
+     * The {@link AudioRoute} that this pending audio switching started with
+     */
+    private AudioRoute mOrigRoute;
+    /**
+     * The expected destination {@link AudioRoute} of this pending audio switching, can be changed
+     * by new switching request during the ongoing switching
+     */
+    private AudioRoute mDestRoute;
+    private ArrayList<Integer> mPendingMessages;
+    private boolean mActive;
+    PendingAudioRoute(CallAudioRouteController controller, AudioManager audioManager) {
+        mCallAudioRouteController = controller;
+        mAudioManager = audioManager;
+        mPendingMessages = new ArrayList<>();
+        mActive = false;
+    }
+
+    void setOrigRoute(boolean active, AudioRoute origRoute) {
+        origRoute.onOrigRouteAsPendingRoute(active, this, mAudioManager);
+        mOrigRoute = origRoute;
+    }
+
+    AudioRoute getOrigRoute() {
+        return mOrigRoute;
+    }
+
+    void setDestRoute(boolean active, AudioRoute destRoute) {
+        destRoute.onDestRouteAsPendingRoute(active, this, mAudioManager);
+        mActive = active;
+        mDestRoute = destRoute;
+    }
+
+    AudioRoute getDestRoute() {
+        return mDestRoute;
+    }
+
+    public void addMessage(int message) {
+        mPendingMessages.add(message);
+    }
+
+    public void onMessageReceived(int message) {
+        if (message == PENDING_ROUTE_FAILED) {
+            // Fallback to base route
+            //TODO: Replace getPreferredAudioRouteFromDefault by getBaseRoute when available and
+            // make the replaced one private
+            mDestRoute = mCallAudioRouteController.getPreferredAudioRouteFromDefault(true);
+            mCallAudioRouteController.sendMessageWithSessionInfo(
+                    CallAudioRouteAdapter.EXIT_PENDING_ROUTE);
+        }
+
+        // Removes the first occurrence of the specified message from this list, if it is present.
+        mPendingMessages.remove((Object) message);
+        evaluatePendingState();
+    }
+
+    public void evaluatePendingState() {
+        if (mPendingMessages.isEmpty()) {
+            mCallAudioRouteController.sendMessageWithSessionInfo(
+                    CallAudioRouteAdapter.EXIT_PENDING_ROUTE);
+        }
+    }
+
+    public boolean isActive() {
+        return mActive;
+    }
+}
diff --git a/src/com/android/server/telecom/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java
index 0ce0094..08566c9 100644
--- a/src/com/android/server/telecom/TelecomServiceImpl.java
+++ b/src/com/android/server/telecom/TelecomServiceImpl.java
@@ -2506,22 +2506,28 @@
          * @param packageName    the package name of the app to check calls for.
          * @param userHandle     the user handle on which to check for calls.
          * @param callingPackage The caller's package name.
+         * @param detectForAllUsers indicates if calls should be detected across all users. If it is
+         *                          set to true, the userHandle parameter is disregarded.
          * @return {@code true} if there are ongoing calls, {@code false} otherwise.
          */
         @Override
         public boolean isInSelfManagedCall(String packageName, UserHandle userHandle,
-                String callingPackage, boolean hasCrossUserAccess) {
+                String callingPackage, boolean detectForAllUsers) {
             try {
                 mContext.enforceCallingOrSelfPermission(READ_PRIVILEGED_PHONE_STATE,
                         "READ_PRIVILEGED_PHONE_STATE required.");
-                enforceInAppCrossUserPermission();
+                // Ensure that the caller has the INTERACT_ACROSS_USERS permission if it's trying
+                // to access calls that don't belong to it.
+                if (detectForAllUsers || !Binder.getCallingUserHandle().equals(userHandle)) {
+                    enforceInAppCrossUserPermission();
+                }
 
                 Log.startSession("TSI.iISMC", Log.getPackageAbbreviation(callingPackage));
                 synchronized (mLock) {
                     long token = Binder.clearCallingIdentity();
                     try {
                         return mCallsManager.isInSelfManagedCallCrossUsers(
-                                packageName, userHandle, hasCrossUserAccess);
+                                packageName, userHandle, detectForAllUsers);
                     } finally {
                         Binder.restoreCallingIdentity(token);
                     }
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java b/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
index d2521ac..6d80cd5 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
@@ -16,6 +16,15 @@
 
 package com.android.server.telecom.bluetooth;
 
+import static com.android.server.telecom.CallAudioRouteAdapter.BT_ACTIVE_DEVICE_GONE;
+import static com.android.server.telecom.CallAudioRouteAdapter.BT_ACTIVE_DEVICE_PRESENT;
+import static com.android.server.telecom.CallAudioRouteAdapter.BT_AUDIO_CONNECTED;
+import static com.android.server.telecom.CallAudioRouteAdapter.BT_AUDIO_DISCONNECTED;
+import static com.android.server.telecom.CallAudioRouteAdapter.BT_DEVICE_ADDED;
+import static com.android.server.telecom.CallAudioRouteAdapter.BT_DEVICE_REMOVED;
+import static com.android.server.telecom.bluetooth.BluetoothRouteManager.BT_AUDIO_IS_ON;
+import static com.android.server.telecom.bluetooth.BluetoothRouteManager.BT_AUDIO_LOST;
+
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothHeadset;
@@ -32,11 +41,11 @@
 import android.telecom.Logging.Session;
 
 import com.android.internal.os.SomeArgs;
+import com.android.server.telecom.AudioRoute;
 import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
+import com.android.server.telecom.CallAudioRouteAdapter;
 import com.android.server.telecom.flags.FeatureFlags;
-
-import static com.android.server.telecom.bluetooth.BluetoothRouteManager.BT_AUDIO_IS_ON;
-import static com.android.server.telecom.bluetooth.BluetoothRouteManager.BT_AUDIO_LOST;
+import com.android.server.telecom.flags.Flags;
 
 
 public class BluetoothStateReceiver extends BroadcastReceiver {
@@ -61,6 +70,7 @@
     private final BluetoothDeviceManager mBluetoothDeviceManager;
     private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
     private FeatureFlags mFeatureFlags;
+    private CallAudioRouteAdapter mCallAudioRouteAdapter;
 
     public void onReceive(Context context, Intent intent) {
         Log.startSession("BSR.oR");
@@ -106,14 +116,24 @@
         args.arg2 = device.getAddress();
         switch (bluetoothHeadsetAudioState) {
             case BluetoothHeadset.STATE_AUDIO_CONNECTED:
-                if (!mIsInCall) {
-                    Log.i(LOG_TAG, "Ignoring BT audio on since we're not in a call");
-                    return;
+                if (Flags.useRefactoredAudioRouteSwitching()) {
+                    mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0,
+                            device);
+                } else {
+                    if (!mIsInCall) {
+                        Log.i(LOG_TAG, "Ignoring BT audio on since we're not in a call");
+                        return;
+                    }
+                    mBluetoothRouteManager.sendMessage(BT_AUDIO_IS_ON, args);
                 }
-                mBluetoothRouteManager.sendMessage(BT_AUDIO_IS_ON, args);
                 break;
             case BluetoothHeadset.STATE_AUDIO_DISCONNECTED:
-                mBluetoothRouteManager.sendMessage(BT_AUDIO_LOST, args);
+                if (Flags.useRefactoredAudioRouteSwitching()) {
+                    mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_AUDIO_DISCONNECTED, 0,
+                            device);
+                }  else {
+                    mBluetoothRouteManager.sendMessage(BT_AUDIO_LOST, args);
+                }
                 break;
         }
     }
@@ -131,12 +151,16 @@
         }
 
         int deviceType;
+        @AudioRoute.AudioRouteType int audioRouteType;
         if (BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED.equals(intent.getAction())) {
             deviceType = BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO;
+            audioRouteType = AudioRoute.TYPE_BLUETOOTH_LE;
         } else if (BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED.equals(intent.getAction())) {
             deviceType = BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID;
+            audioRouteType = AudioRoute.TYPE_BLUETOOTH_HA;
         } else if (BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED.equals(intent.getAction())) {
             deviceType = BluetoothDeviceManager.DEVICE_TYPE_HEADSET;
+            audioRouteType = AudioRoute.TYPE_BLUETOOTH_SCO;
         } else {
             Log.w(LOG_TAG, "handleConnectionStateChanged: %s invalid device type", device);
             return;
@@ -147,10 +171,20 @@
                 device.getAddress(), bluetoothHeadsetState);
 
         if (bluetoothHeadsetState == BluetoothProfile.STATE_CONNECTED) {
-            mBluetoothDeviceManager.onDeviceConnected(device, deviceType);
+            if (Flags.useRefactoredAudioRouteSwitching()) {
+                mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_DEVICE_ADDED,
+                        audioRouteType, device);
+            } else {
+                mBluetoothDeviceManager.onDeviceConnected(device, deviceType);
+            }
         } else if (bluetoothHeadsetState == BluetoothProfile.STATE_DISCONNECTED
                 || bluetoothHeadsetState == BluetoothProfile.STATE_DISCONNECTING) {
-            mBluetoothDeviceManager.onDeviceDisconnected(device, deviceType);
+            if (Flags.useRefactoredAudioRouteSwitching()) {
+                mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_DEVICE_REMOVED,
+                        audioRouteType, device);
+            } else {
+                mBluetoothDeviceManager.onDeviceDisconnected(device, deviceType);
+            }
         }
     }
 
@@ -159,12 +193,16 @@
                 intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice.class);
 
         int deviceType;
+        @AudioRoute.AudioRouteType int audioRouteType;
         if (BluetoothLeAudio.ACTION_LE_AUDIO_ACTIVE_DEVICE_CHANGED.equals(intent.getAction())) {
             deviceType = BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO;
+            audioRouteType = AudioRoute.TYPE_BLUETOOTH_LE;
         } else if (BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED.equals(intent.getAction())) {
             deviceType = BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID;
+            audioRouteType = AudioRoute.TYPE_BLUETOOTH_HA;
         } else if (BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED.equals(intent.getAction())) {
             deviceType = BluetoothDeviceManager.DEVICE_TYPE_HEADSET;
+            audioRouteType = AudioRoute.TYPE_BLUETOOTH_SCO;
         } else {
             Log.w(LOG_TAG, "handleActiveDeviceChanged: %s invalid device type", device);
             return;
@@ -173,72 +211,84 @@
         Log.i(LOG_TAG, "Device %s is now the preferred BT device for %s", device,
                 BluetoothDeviceManager.getDeviceTypeString(deviceType));
 
-        mBluetoothRouteManager.onActiveDeviceChanged(device, deviceType);
-        if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID ||
-            deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO) {
-            Session session = Log.createSubsession();
-            SomeArgs args = SomeArgs.obtain();
-            args.arg1 = session;
+        if (Flags.useRefactoredAudioRouteSwitching()) {
             if (device == null) {
-                mBluetoothRouteManager.sendMessage(BT_AUDIO_LOST, args);
+                mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_GONE,
+                        audioRouteType);
             } else {
-                if (!mIsInCall) {
-                    Log.i(LOG_TAG, "Ignoring audio on since we're not in a call");
-                    return;
-                }
-                args.arg2 = device.getAddress();
-
-                boolean usePreferredAudioProfile = false;
-                BluetoothAdapter bluetoothAdapter = mBluetoothDeviceManager.getBluetoothAdapter();
-                int preferredDuplexProfile = BluetoothProfile.LE_AUDIO;
-                if (bluetoothAdapter != null) {
-                    Bundle preferredAudioProfiles = bluetoothAdapter.getPreferredAudioProfiles(
-                            device);
-                    if (preferredAudioProfiles != null && !preferredAudioProfiles.isEmpty()
-                            && preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX)
-                            != 0) {
-                        Log.i(this, "Preferred duplex profile for device=" + device + " is "
-                                + preferredAudioProfiles.getInt(
-                                BluetoothAdapter.AUDIO_MODE_DUPLEX));
-                        usePreferredAudioProfile = true;
-                        preferredDuplexProfile =
-                                preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX);
-                    }
-                }
-
-                if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO) {
-                    /* In Le Audio case, once device got Active, the Telecom needs to make sure it
-                     * is set as communication device before we can say that BT_AUDIO_IS_ON
-                     */
-                    boolean isLeAudioSetForCommunication =
-                            mFeatureFlags.callAudioCommunicationDeviceRefactor()
-                                    ? mCommunicationDeviceTracker.setCommunicationDevice(
-                                    AudioDeviceInfo.TYPE_BLE_HEADSET, device)
-                                    : mBluetoothDeviceManager.setLeAudioCommunicationDevice();
-                    if ((!usePreferredAudioProfile
-                            || preferredDuplexProfile == BluetoothProfile.LE_AUDIO)
-                            && !isLeAudioSetForCommunication) {
-                        Log.w(LOG_TAG,
-                                "Device %s cannot be use as LE audio communication device.",
-                                device);
+                mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
+                        audioRouteType, device.getAddress());
+            }
+        } else {
+            mBluetoothRouteManager.onActiveDeviceChanged(device, deviceType);
+            if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID ||
+                    deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO) {
+                Session session = Log.createSubsession();
+                SomeArgs args = SomeArgs.obtain();
+                args.arg1 = session;
+                if (device == null) {
+                    mBluetoothRouteManager.sendMessage(BT_AUDIO_LOST, args);
+                } else {
+                    if (!mIsInCall) {
+                        Log.i(LOG_TAG, "Ignoring audio on since we're not in a call");
                         return;
                     }
-                } else {
-                    boolean isHearingAidSetForCommunication =
-                            mFeatureFlags.callAudioCommunicationDeviceRefactor()
-                            ? mCommunicationDeviceTracker.setCommunicationDevice(
-                                    AudioDeviceInfo.TYPE_HEARING_AID, null)
-                            : mBluetoothDeviceManager.setHearingAidCommunicationDevice();
-                    /* deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID */
-                    if (!isHearingAidSetForCommunication) {
-                        Log.w(LOG_TAG,
-                                "Device %s cannot be use as hearing aid communication device.",
+                    args.arg2 = device.getAddress();
+
+                    boolean usePreferredAudioProfile = false;
+                    BluetoothAdapter bluetoothAdapter = mBluetoothDeviceManager
+                            .getBluetoothAdapter();
+                    int preferredDuplexProfile = BluetoothProfile.LE_AUDIO;
+                    if (bluetoothAdapter != null) {
+                        Bundle preferredAudioProfiles = bluetoothAdapter.getPreferredAudioProfiles(
                                 device);
+                        if (preferredAudioProfiles != null && !preferredAudioProfiles.isEmpty()
+                                && preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX)
+                                != 0) {
+                            Log.i(this, "Preferred duplex profile for device=" + device + " is "
+                                    + preferredAudioProfiles.getInt(
+                                    BluetoothAdapter.AUDIO_MODE_DUPLEX));
+                            usePreferredAudioProfile = true;
+                            preferredDuplexProfile =
+                                    preferredAudioProfiles.getInt(
+                                            BluetoothAdapter.AUDIO_MODE_DUPLEX);
+                        }
+                    }
+
+                    if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO) {
+                        /* In Le Audio case, once device got Active, the Telecom needs to make sure
+                         * it is set as communication device before we can say that BT_AUDIO_IS_ON
+                         */
+                        boolean isLeAudioSetForCommunication =
+                                mFeatureFlags.callAudioCommunicationDeviceRefactor()
+                                        ? mCommunicationDeviceTracker.setCommunicationDevice(
+                                        AudioDeviceInfo.TYPE_BLE_HEADSET, device)
+                                        : mBluetoothDeviceManager.setLeAudioCommunicationDevice();
+                        if ((!usePreferredAudioProfile
+                                || preferredDuplexProfile == BluetoothProfile.LE_AUDIO)
+                                && !isLeAudioSetForCommunication) {
+                            Log.w(LOG_TAG,
+                                    "Device %s cannot be use as LE audio communication device.",
+                                    device);
+                        }
                     } else {
-                        mBluetoothRouteManager.sendMessage(BT_AUDIO_IS_ON, args);
+                        boolean isHearingAidSetForCommunication =
+                                mFeatureFlags.callAudioCommunicationDeviceRefactor()
+                                        ? mCommunicationDeviceTracker.setCommunicationDevice(
+                                        AudioDeviceInfo.TYPE_HEARING_AID, null)
+                                        : mBluetoothDeviceManager
+                                        .setHearingAidCommunicationDevice();
+                        /* deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID */
+                        if (!isHearingAidSetForCommunication) {
+                            Log.w(LOG_TAG,
+                                    "Device %s cannot be use as hearing aid communication device.",
+                                    device);
+                        } else {
+                            mBluetoothRouteManager.sendMessage(BT_AUDIO_IS_ON, args);
+                        }
                     }
                 }
-           }
+            }
         }
     }
 
@@ -259,4 +309,8 @@
     public void setIsInCall(boolean isInCall) {
         mIsInCall = isInCall;
     }
+
+    public void setCallAudioRouteAdapter(CallAudioRouteAdapter adapter) {
+        mCallAudioRouteAdapter = adapter;
+    }
 }
diff --git a/testapps/Android.bp b/testapps/Android.bp
index 11ea474..45ea753 100644
--- a/testapps/Android.bp
+++ b/testapps/Android.bp
@@ -15,6 +15,7 @@
 //
 
 package {
+    default_team: "trendy_team_fwk_telecom",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
diff --git a/testapps/callaudiotest/Android.bp b/testapps/callaudiotest/Android.bp
index 81164e6..d996236 100644
--- a/testapps/callaudiotest/Android.bp
+++ b/testapps/callaudiotest/Android.bp
@@ -15,6 +15,7 @@
 //
 
 package {
+    default_team: "trendy_team_fwk_telecom",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
diff --git a/testapps/carmodedialer/Android.bp b/testapps/carmodedialer/Android.bp
index 9f65b8c..f142bf4 100644
--- a/testapps/carmodedialer/Android.bp
+++ b/testapps/carmodedialer/Android.bp
@@ -15,6 +15,7 @@
 //
 
 package {
+    default_team: "trendy_team_fwk_telecom",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
diff --git a/testapps/companionapp/Android.bp b/testapps/companionapp/Android.bp
index 8718b37..84ee4d3 100644
--- a/testapps/companionapp/Android.bp
+++ b/testapps/companionapp/Android.bp
@@ -15,6 +15,7 @@
 //
 
 package {
+    default_team: "trendy_team_fwk_telecom",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
diff --git a/testapps/streamingtest/Android.bp b/testapps/streamingtest/Android.bp
index bd0a582..8d5cd6c 100644
--- a/testapps/streamingtest/Android.bp
+++ b/testapps/streamingtest/Android.bp
@@ -15,6 +15,7 @@
 //
 
 package {
+    default_team: "trendy_team_fwk_telecom",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
diff --git a/testapps/transactionalVoipApp/Android.bp b/testapps/transactionalVoipApp/Android.bp
index 68089e2..8bac8f1 100644
--- a/testapps/transactionalVoipApp/Android.bp
+++ b/testapps/transactionalVoipApp/Android.bp
@@ -15,6 +15,7 @@
 //
 
 package {
+    default_team: "trendy_team_fwk_telecom",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index 4ca6030..1c27b14 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -24,6 +24,7 @@
         android:targetSdkVersion="33" />
 
     <uses-permission android:name="android.permission.READ_DEVICE_CONFIG"/>
+    <uses-permission android:name="android.permission.MODIFY_AUDIO_ROUTING" />
     <!-- TODO: Needed because we call BluetoothAdapter.getDefaultAdapter() statically, and
          BluetoothAdapter is a final class. -->
     <uses-permission android:name="android.permission.BLUETOOTH" />
diff --git a/tests/src/com/android/server/telecom/tests/AnalyticsTests.java b/tests/src/com/android/server/telecom/tests/AnalyticsTests.java
index 148db51..9caf0b5 100644
--- a/tests/src/com/android/server/telecom/tests/AnalyticsTests.java
+++ b/tests/src/com/android/server/telecom/tests/AnalyticsTests.java
@@ -87,6 +87,7 @@
         super.setUp();
         // this is a mock
         mSubscriptionManager = mContext.getSystemService(SubscriptionManager.class);
+        when(mSubscriptionManager.createForAllUserProfiles()).thenReturn(mSubscriptionManager);
         when(mSubscriptionManager.getActiveSubscriptionInfoList())
                 .thenReturn(Collections.emptyList());
     }
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
new file mode 100644
index 0000000..08576fc
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.telecom.tests;
+
+import static com.android.server.telecom.CallAudioRouteAdapter.BT_ACTIVE_DEVICE_GONE;
+import static com.android.server.telecom.CallAudioRouteAdapter.BT_ACTIVE_DEVICE_PRESENT;
+import static com.android.server.telecom.CallAudioRouteAdapter.BT_AUDIO_CONNECTED;
+import static com.android.server.telecom.CallAudioRouteAdapter.BT_DEVICE_ADDED;
+import static com.android.server.telecom.CallAudioRouteAdapter.BT_DEVICE_REMOVED;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.bluetooth.BluetoothDevice;
+import android.media.AudioDeviceInfo;
+import android.media.AudioManager;
+import android.media.audiopolicy.AudioProductStrategy;
+import android.telecom.CallAudioState;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.server.telecom.AudioRoute;
+import com.android.server.telecom.CallAudioRouteController;
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.WiredHeadsetManager;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+
+import java.util.HashSet;
+import java.util.Set;
+
+@RunWith(JUnit4.class)
+public class CallAudioRouteControllerTest extends TelecomTestCase {
+    private CallAudioRouteController mController;
+    @Mock WiredHeadsetManager mWiredHeadsetManager;
+    @Mock AudioManager mAudioManager;
+    @Mock AudioDeviceInfo mEarpieceDeviceInfo;
+    @Mock CallsManager mCallsManager;
+    private AudioRoute mEarpieceRoute;
+    private AudioRoute mSpeakerRoute;
+    private static final String BT_ADDRESS_1 = "00:00:00:00:00:01";
+    private static final BluetoothDevice BLUETOOTH_DEVICE_1 =
+            BluetoothRouteManagerTest.makeBluetoothDevice("00:00:00:00:00:01");
+    private static final Set<BluetoothDevice> BLUETOOTH_DEVICES;
+    static {
+        BLUETOOTH_DEVICES = new HashSet<>();
+        BLUETOOTH_DEVICES.add(BLUETOOTH_DEVICE_1);
+    }
+    private static final int TEST_TIMEOUT = 500;
+    AudioRoute.Factory mAudioRouteFactory = new AudioRoute.Factory() {
+        @Override
+        public AudioRoute create(@AudioRoute.AudioRouteType int type, String bluetoothAddress,
+                                 AudioManager audioManager) {
+            return new AudioRoute(type, bluetoothAddress, null);
+        }
+    };
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        when(mWiredHeadsetManager.isPluggedIn()).thenReturn(false);
+        when(mEarpieceDeviceInfo.getType()).thenReturn(AudioDeviceInfo.TYPE_BUILTIN_EARPIECE);
+        when(mAudioManager.getDevices(eq(AudioManager.GET_DEVICES_OUTPUTS))).thenReturn(
+                new AudioDeviceInfo[] {
+                        mEarpieceDeviceInfo
+                });
+        doNothing().when(mCallsManager).onCallAudioStateChanged(any(CallAudioState.class),
+                any(CallAudioState.class));
+        mController = new CallAudioRouteController(mContext, mCallsManager, mAudioRouteFactory,
+                mWiredHeadsetManager);
+        mController.setAudioRouteFactory(mAudioRouteFactory);
+        mController.setAudioManager(mAudioManager);
+        mEarpieceRoute = new AudioRoute(AudioRoute.TYPE_EARPIECE, null, null);
+        mSpeakerRoute = new AudioRoute(AudioRoute.TYPE_SPEAKER, null, null);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mController.getAdapterHandler().getLooper().quit();
+        mController.getAdapterHandler().getLooper().getThread().join();
+        super.tearDown();
+    }
+
+    @SmallTest
+    @Test
+    public void testInitializeWithEarpiece() {
+        mController.initialize();
+        assertEquals(mEarpieceRoute, mController.getCurrentRoute());
+        assertEquals(2, mController.getAvailableRoutes().size());
+        assertTrue(mController.getAvailableRoutes().contains(mSpeakerRoute));
+    }
+
+    @SmallTest
+    @Test
+    public void testInitializeWithoutEarpiece() {
+        when(mAudioManager.getDevices(eq(AudioManager.GET_DEVICES_OUTPUTS))).thenReturn(
+                new AudioDeviceInfo[] {});
+
+        mController.initialize();
+        assertEquals(mSpeakerRoute, mController.getCurrentRoute());
+    }
+
+    @SmallTest
+    @Test
+    public void testActivateAndRemoveBluetoothDeviceDuringCall() {
+        doAnswer(invocation -> {
+            mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0, BLUETOOTH_DEVICE_1);
+            return true;
+        }).when(mAudioManager).setCommunicationDevice(nullable(AudioDeviceInfo.class));
+
+        mController.initialize();
+        mController.setActive(true);
+        mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
+                BLUETOOTH_DEVICE_1);
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, null, BLUETOOTH_DEVICES);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        mController.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
+                AudioRoute.TYPE_BLUETOOTH_SCO, BT_ADDRESS_1);
+        verify(mAudioManager, timeout(TEST_TIMEOUT)).setCommunicationDevice(
+                nullable(AudioDeviceInfo.class));
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        mController.sendMessageWithSessionInfo(BT_DEVICE_REMOVED, AudioRoute.TYPE_BLUETOOTH_SCO,
+                BLUETOOTH_DEVICE_1);
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+    }
+
+    @SmallTest
+    @Test
+    public void testActiveDeactivateBluetoothDevice() {
+        when(mAudioManager.getPreferredDeviceForStrategy(nullable(AudioProductStrategy.class)))
+                .thenReturn(null);
+
+        mController.initialize();
+        mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
+                BLUETOOTH_DEVICE_1);
+
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+        mController.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
+                AudioRoute.TYPE_BLUETOOTH_SCO, BT_ADDRESS_1);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, null, BLUETOOTH_DEVICES);
+        mController.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_GONE,
+                AudioRoute.TYPE_BLUETOOTH_SCO);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+    }
+
+    @SmallTest
+    @Test
+    public void testSwitchFocusInBluetoothRoute() {
+
+    }
+}