Merge "Modified to display valid value set in Role when capturing log dump" into main
diff --git a/Android.bp b/Android.bp
index 501b438..0d1c81d 100644
--- a/Android.bp
+++ b/Android.bp
@@ -1,36 +1,36 @@
 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"],
 }
 
 filegroup {
-    name: "Telecom-srcs",
+    name: "telecom-shell-commands-src",
+    srcs: [
+        "src/com/android/server/telecom/TelecomShellCommand.java",
+    ],
+    path: "src",
+}
+
+android_library {
+    name: "TelecomLib",
+    manifest: "AndroidManifestLib.xml",
     srcs: [
         "src/**/*.java",
         ":statslog-telecom-java-gen",
-    ],
-}
-
-// Build the Telecom service.
-android_app {
-    name: "Telecom",
-    srcs: [
-        ":Telecom-srcs",
         "proto/**/*.proto",
     ],
     static_libs: [
         "androidx.annotation_annotation",
         "androidx.core_core",
-    ],
-    libs: [
-        "services",
+        "telecom_flags_core_java_lib",
     ],
     resource_dirs: ["res"],
     proto: {
@@ -39,6 +39,21 @@
         output_params: ["optional_field_style=accessors"],
     },
     platform_apis: true,
+}
+
+// Build the Telecom service.
+android_app {
+    name: "Telecom",
+    srcs: [
+    ],
+    static_libs: [
+        "TelecomLib",
+    ],
+    libs: [
+        "services",
+    ],
+    resource_dirs: [],
+    platform_apis: true,
     certificate: "platform",
     privileged: true,
     optimize: {
@@ -49,36 +64,29 @@
 android_test {
     name: "TelecomUnitTests",
     static_libs: [
+        "TelecomLib",
         "android-ex-camera2",
+        "flag-junit",
         "guava",
         "mockito-target-extended",
         "androidx.test.rules",
         "platform-test-annotations",
         "androidx.legacy_legacy-support-core-ui",
         "androidx.legacy_legacy-support-core-utils",
-        "androidx.core_core",
         "androidx.fragment_fragment",
         "androidx.test.ext.junit",
         "platform-compat-test-rules",
     ],
     srcs: [
         "tests/src/**/*.java",
-        ":Telecom-srcs",
-        "proto/**/*.proto",
     ],
-    proto: {
-        type: "nano",
-        local_include_dirs: ["proto/"],
-        output_params: ["optional_field_style=accessors"],
-    },
     resource_dirs: [
         "tests/res",
-        "res",
     ],
     libs: [
-        "android.test.mock",
-        "android.test.base",
-        "android.test.runner",
+        "android.test.mock.stubs.system",
+        "android.test.base.stubs.system",
+        "android.test.runner.stubs.system",
     ],
 
     jni_libs: [
@@ -86,11 +94,6 @@
         "libstaticjvmtiagent",
     ],
 
-    aaptflags: [
-        "--auto-add-overlay",
-        "--extra-packages",
-        "com.android.server.telecom",
-    ],
     manifest: "tests/AndroidManifest.xml",
     optimize: {
         enabled: false,
@@ -98,8 +101,8 @@
     platform_apis: true,
     certificate: "platform",
     jacoco: {
-        include_filter: ["com.android.server.telecom.*"],
-        exclude_filter: ["com.android.server.telecom.tests.*"],
+        include_filter: ["com.android.server.telecom.**"],
+        exclude_filter: ["com.android.server.telecom.tests.**"],
     },
     test_suites: ["device-tests"],
     defaults: ["SettingsLibDefaults"],
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index ab067d9..a9b6154 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -28,7 +28,6 @@
     <!-- Prevents the activity manager from delaying any activity-start
          requests by this package, including requests immediately after
          the user presses "home". -->
-    <uses-permission android:name="android.permission.BIND_CONNECTION_SERVICE"/>
     <uses-permission android:name="android.permission.BIND_INCALL_SERVICE"/>
     <uses-permission android:name="android.permission.BLUETOOTH"/>
     <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
@@ -65,8 +64,9 @@
     <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
     <uses-permission android:name="android.permission.USE_COLORIZED_NOTIFICATIONS"/>
     <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
-    <uses-permission android:name="com.android.phone.permission.ACCESS_LAST_KNOWN_CELL_ID"/>
+    <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"
@@ -319,18 +319,6 @@
              android:exported="false"
              android:process=":ui"/>
 
-        <service android:name=".components.BluetoothPhoneService"
-             android:singleUser="true"
-             android:process="system"
-             android:exported="true">
-            <intent-filter>
-                <action android:name="android.bluetooth.IBluetoothHeadsetPhone"/>
-            </intent-filter>
-            <intent-filter>
-                <action android:name="android.bluetooth.IBluetoothLeCallControlCallback" />
-            </intent-filter>
-        </service>
-
         <service android:name=".components.TelecomService"
              android:singleUser="true"
              android:process="system"
diff --git a/AndroidManifestLib.xml b/AndroidManifestLib.xml
new file mode 100644
index 0000000..9b40f6b
--- /dev/null
+++ b/AndroidManifestLib.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 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.
+-->
+
+<manifest package="com.android.server.telecom">
+</manifest>
diff --git a/OWNERS b/OWNERS
index 7e68aea..9c071f9 100644
--- a/OWNERS
+++ b/OWNERS
@@ -1,7 +1,8 @@
+# Bug component: 151185
 breadley@google.com
 tgunn@google.com
-xiaotonj@google.com
 tjstuart@google.com
 rgreenwalt@google.com
 pmadapurmath@google.com
 grantmenke@google.com
+huiwang@google.com
diff --git a/flags/Android.bp b/flags/Android.bp
new file mode 100644
index 0000000..45acacf
--- /dev/null
+++ b/flags/Android.bp
@@ -0,0 +1,47 @@
+//
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+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_remote_connection_service.aconfig",
+        "telecom_profile_user_flags.aconfig",
+        "telecom_bluetoothdevicemanager_flags.aconfig",
+        "telecom_non_critical_security_flags.aconfig",
+    ],
+}
diff --git a/flags/telecom_anomaly_report_flags.aconfig b/flags/telecom_anomaly_report_flags.aconfig
new file mode 100644
index 0000000..296b300
--- /dev/null
+++ b/flags/telecom_anomaly_report_flags.aconfig
@@ -0,0 +1,10 @@
+package: "com.android.server.telecom.flags"
+container: "system"
+
+# OWNER=tjstuart TARGET=24Q3
+flag {
+  name: "gen_anom_report_on_focus_timeout"
+  namespace: "telecom"
+  description: "When getCurrentFocusCall times out, generate an anom. report"
+  bug: "309541253"
+}
diff --git a/flags/telecom_api_flags.aconfig b/flags/telecom_api_flags.aconfig
new file mode 100644
index 0000000..75efdfa
--- /dev/null
+++ b/flags/telecom_api_flags.aconfig
@@ -0,0 +1,75 @@
+package: "com.android.server.telecom.flags"
+container: "system"
+
+# OWNER=grantmenke TARGET=24Q3
+flag {
+  name: "voip_app_actions_support"
+  is_exported: true
+  namespace: "telecom"
+  description: "When set, Telecom support for additional VOIP application actions is active."
+  bug: "296934278"
+}
+
+# OWNER=grantmenke TARGET=24Q3
+flag {
+  name: "call_details_id_changes"
+  is_exported: true
+  namespace: "telecom"
+  description: "When set, call details/extras id updates to Telecom APIs for Android V are active."
+  bug: "301713560"
+}
+
+# OWNER=kunduz TARGET=24Q2
+flag {
+  name: "add_call_uri_for_missed_calls"
+  is_exported: true
+  namespace: "telecom"
+  description: "The key is used for dialer apps to mark missed calls as read when it gets the notification on reboot."
+  bug: "292597423"
+}
+
+# OWNER=tjstuart TARGET=24Q3
+flag {
+  name: "set_mute_state"
+  is_exported: true
+  namespace: "telecom"
+  description: "transactional calls need the ability to mute the call audio input"
+  bug: "310669304"
+}
+
+# OWNER=tjstuart TARGET=24Q3
+flag {
+  name: "get_registered_phone_accounts"
+  is_exported: true
+  namespace: "telecom"
+  description: "When set, self-managed clients can get their own phone accounts"
+  bug: "317132586"
+}
+
+# OWNER=tjstuart TARGET=24Q3
+flag {
+  name: "transactional_video_state"
+  is_exported: true
+  namespace: "telecom"
+  description: "when set, clients using transactional implementations will be able to set & get the video state"
+  bug: "311265260"
+}
+
+# OWNER=tjstuart TARGET=24Q3
+flag {
+  name: "business_call_composer"
+  is_exported: true
+  namespace: "telecom"
+  description: "Enables enriched calling features (e.g. Business name will show for a call)"
+  bug: "311688497"
+  is_exported: true
+}
+
+# OWNER=tgunn TARGET=25Q3
+flag {
+  name: "get_last_known_cell_identity"
+  is_exported: true
+  namespace: "telecom"
+  description: "Formalizes the getLastKnownCellIdentity API that Telecom reliees on as a system api"
+  bug: "327454165"
+}
diff --git a/flags/telecom_bluetoothdevicemanager_flags.aconfig b/flags/telecom_bluetoothdevicemanager_flags.aconfig
new file mode 100644
index 0000000..4c91491
--- /dev/null
+++ b/flags/telecom_bluetoothdevicemanager_flags.aconfig
@@ -0,0 +1,10 @@
+package: "com.android.server.telecom.flags"
+container: "system"
+
+# OWNER=tgunn TARGET=24Q4
+flag {
+  name: "postpone_register_to_leaudio"
+  namespace: "telecom"
+  description: "Fix for Log.wtf in the BinderProxy"
+  bug: "333417369"
+}
diff --git a/flags/telecom_bluetoothroutemanager_flags.aconfig b/flags/telecom_bluetoothroutemanager_flags.aconfig
new file mode 100644
index 0000000..dc69dd5
--- /dev/null
+++ b/flags/telecom_bluetoothroutemanager_flags.aconfig
@@ -0,0 +1,10 @@
+package: "com.android.server.telecom.flags"
+container: "system"
+
+# OWNER=tgunn TARGET=24Q3
+flag {
+  name: "use_actual_address_to_enter_connecting_state"
+  namespace: "telecom"
+  description: "Fix bugs that may add bluetooth device with null address."
+  bug: "306113816"
+}
diff --git a/flags/telecom_broadcast_flags.aconfig b/flags/telecom_broadcast_flags.aconfig
new file mode 100644
index 0000000..8314376
--- /dev/null
+++ b/flags/telecom_broadcast_flags.aconfig
@@ -0,0 +1,10 @@
+package: "com.android.server.telecom.flags"
+container: "system"
+
+# OWNER=tgunn TARGET=24Q3
+flag {
+  name: "is_new_outgoing_call_broadcast_unblocking"
+  namespace: "telecom"
+  description: "When set, the ACTION_NEW_OUTGOING_CALL broadcast is unblocking."
+  bug: "224550864"
+}
\ No newline at end of file
diff --git a/flags/telecom_call_filtering_flags.aconfig b/flags/telecom_call_filtering_flags.aconfig
new file mode 100644
index 0000000..d80cfa3
--- /dev/null
+++ b/flags/telecom_call_filtering_flags.aconfig
@@ -0,0 +1,10 @@
+package: "com.android.server.telecom.flags"
+container: "system"
+
+# OWNER=qingzhong TARGET=24Q2
+flag {
+  name: "skip_filter_phone_account_perform_dnd_filter"
+  namespace: "telecom"
+  description: "Gates whether to still perform Dnd filter when phone account has skip_filter call extra."
+  bug: "222333869"
+}
\ No newline at end of file
diff --git a/flags/telecom_call_flags.aconfig b/flags/telecom_call_flags.aconfig
new file mode 100644
index 0000000..71d51fe
--- /dev/null
+++ b/flags/telecom_call_flags.aconfig
@@ -0,0 +1,35 @@
+package: "com.android.server.telecom.flags"
+container: "system"
+
+# OWNER=tjstuart TARGET=24Q3
+flag {
+  name: "prevent_redundant_location_permission_grant_and_revoke"
+  namespace: "telecom"
+  description: "avoid redundant action of grant and revoke location permission for multiple emergency calls"
+  bug: "345386002"
+}
+
+flag {
+  name: "transactional_cs_verifier"
+  namespace: "telecom"
+  description: "verify connection service callbacks via a transaction"
+  bug: "309541257"
+}
+
+flag {
+  name: "cache_call_audio_callbacks"
+  namespace: "telecom"
+  description: "cache call audio callbacks if the service is not available and execute when set"
+  bug: "321369729"
+}
+
+# OWNER = breadley TARGET=24Q3
+flag {
+  name: "cancel_removal_on_emergency_redial"
+  namespace: "telecom"
+  description: "When redialing an emergency call on another connection service, ensure any pending removal operation is cancelled"
+  bug: "341157874"
+  metadata {
+      purpose: PURPOSE_BUGFIX
+    }
+}
diff --git a/flags/telecom_callaudiomodestatemachine_flags.aconfig b/flags/telecom_callaudiomodestatemachine_flags.aconfig
new file mode 100644
index 0000000..63761ec
--- /dev/null
+++ b/flags/telecom_callaudiomodestatemachine_flags.aconfig
@@ -0,0 +1,10 @@
+package: "com.android.server.telecom.flags"
+container: "system"
+
+# OWNER=pmadapurmath TARGET=24Q3
+flag {
+  name: "set_audio_mode_before_abandon_focus"
+  namespace: "telecom"
+  description: "Set audio mode to MODE_NORMAL before abandon the audio focus."
+  bug: "281841785"
+}
diff --git a/flags/telecom_callaudioroutestatemachine_flags.aconfig b/flags/telecom_callaudioroutestatemachine_flags.aconfig
new file mode 100644
index 0000000..33bccba
--- /dev/null
+++ b/flags/telecom_callaudioroutestatemachine_flags.aconfig
@@ -0,0 +1,101 @@
+package: "com.android.server.telecom.flags"
+container: "system"
+
+# OWNER=kunduz TARGET=24Q2
+flag {
+  name: "available_routes_never_updated_after_set_system_audio_state"
+  namespace: "telecom"
+  description: "Fix supported routes wrongly include bluetooth issue."
+  bug: "292599751"
+}
+
+# OWNER=pmadapurmath TARGET=24Q3
+flag {
+  name: "use_refactored_audio_route_switching"
+  namespace: "telecom"
+  description: "Refactored audio routing"
+  bug: "306395598"
+}
+
+# OWNER=tgunn TARGET=24Q3
+flag {
+  name: "ensure_audio_mode_updates_on_foreground_call_change"
+  namespace: "telecom"
+  description: "Ensure that the audio mode is updated anytime the foreground call changes."
+  bug: "289861657"
+}
+
+# OWNER=pmadapurmath TARGET=24Q1
+flag {
+  name: "ignore_auto_route_to_watch_device"
+  namespace: "telecom"
+  description: "Ignore auto routing to wearable devices."
+  bug: "294378768"
+}
+
+# OWNER=pmadapurmath TARGET=24Q3
+flag {
+  name: "transit_route_before_audio_disconnect_bt"
+  namespace: "telecom"
+  description: "Fix audio route transition issue on call disconnection when bt audio connected."
+  bug: "306113816"
+}
+
+# OWNER=pmadapurmath TARGET=24Q3
+flag {
+  name: "call_audio_communication_device_refactor"
+  namespace: "telecom"
+  description: "Refactor call audio set/clear communication device and include unsupported routes."
+  bug: "308968392"
+}
+
+# OWNER=pmadapurmath TARGET=24Q3
+flag {
+  name: "communication_device_protected_by_lock"
+  namespace: "telecom"
+  description: "Protect set/clear communication device operation with lock to avoid race condition."
+  bug: "303001133"
+}
+
+# OWNER=pmadapurmath TARGET=24Q3
+flag {
+  name: "reset_mute_when_entering_quiescent_bt_route"
+  namespace: "telecom"
+  description: "Reset mute state when entering quiescent bluetooth route."
+  bug: "311313250"
+}
+
+# OWNER=pmadapurmath TARGET=24Q3
+flag {
+  name: "update_route_mask_when_bt_connected"
+  namespace: "telecom"
+  description: "Update supported route mask when Bluetooth devices audio connected."
+  bug: "301695370"
+}
+
+# OWNER=pmadapurmath TARGET=24Q3
+flag {
+  name: "clear_communication_device_after_audio_ops_complete"
+  namespace: "telecom"
+  description: "Clear the requested communication device after the audio operations are completed."
+  bug: "315865533"
+}
+
+# OWNER=pmadapurmath TARGET=24Q3
+flag {
+  name: "resolve_switching_bt_devices_computation"
+  namespace: "telecom"
+  description: "Update switching bt devices based on arbitrary device chosen if no device is specified."
+  bug: "333751408"
+}
+
+# OWNER=pmadapurmath TARGET=24Q3
+flag {
+  name: "early_update_internal_call_audio_state"
+  namespace: "telecom"
+  description: "Update internal call audio state before sending updated state to ICS"
+  bug: "335538831"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
diff --git a/flags/telecom_calllog_flags.aconfig b/flags/telecom_calllog_flags.aconfig
new file mode 100644
index 0000000..c0eebf1
--- /dev/null
+++ b/flags/telecom_calllog_flags.aconfig
@@ -0,0 +1,18 @@
+package: "com.android.server.telecom.flags"
+container: "system"
+
+# OWNER=qingzhong TARGET=24Q2
+flag {
+  name: "telecom_log_external_wearable_calls"
+  namespace: "telecom"
+  description: "log external call if current device is a wearable one"
+  bug: "292600751"
+}
+
+# OWNER=ranamouawi TARGET=24Q2
+flag {
+  name: "telecom_skip_log_based_on_extra"
+  namespace: "telecom"
+  description: "skipping logging a call based on passed extra"
+  bug: "295530944"
+}
diff --git a/flags/telecom_calls_manager_flags.aconfig b/flags/telecom_calls_manager_flags.aconfig
new file mode 100644
index 0000000..f46e844
--- /dev/null
+++ b/flags/telecom_calls_manager_flags.aconfig
@@ -0,0 +1,37 @@
+package: "com.android.server.telecom.flags"
+container: "system"
+
+# OWNER=pmadapurmath TARGET=24Q3
+flag {
+  name: "use_improved_listener_order"
+  namespace: "telecom"
+  description: "Make InCallController the first listener to trigger"
+  bug: "24244713"
+}
+
+# OWNER=tjstuart TARGET=24Q3
+flag {
+  name: "fix_audio_flicker_for_outgoing_calls"
+  namespace: "telecom"
+  description: "This fix ensures the MO calls won't switch from Active to Quite b/c setDialing was not called"
+  bug: "309540769"
+}
+
+# OWNER=breadley TARGET=24Q3
+flag {
+  name: "enable_call_sequencing"
+  namespace: "telecom"
+  description: "Enables simultaneous call sequencing for SIM PhoneAccounts"
+  bug: "327038818"
+}
+
+# OWNER=tjstuart TARGET=24Q4
+flag {
+  name: "transactional_hold_disconnects_unholdable"
+  namespace: "telecom"
+  description: "Disconnect ongoing unholdable calls for CallControlCallbacks"
+  bug: "340621152"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
diff --git a/flags/telecom_connection_service_wrapper_flags.aconfig b/flags/telecom_connection_service_wrapper_flags.aconfig
new file mode 100644
index 0000000..38e5e13
--- /dev/null
+++ b/flags/telecom_connection_service_wrapper_flags.aconfig
@@ -0,0 +1,10 @@
+package: "com.android.server.telecom.flags"
+container: "system"
+
+# OWNER=grantmenke TARGET=24Q2
+flag {
+  name: "updated_rcs_call_count_tracking"
+  namespace: "telecom"
+  description: "Ensure that the associatedCallCount of CS and RCS is accurately being tracked."
+  bug: "286154316"
+}
\ No newline at end of file
diff --git a/flags/telecom_default_phone_account_flags.aconfig b/flags/telecom_default_phone_account_flags.aconfig
new file mode 100644
index 0000000..161b674
--- /dev/null
+++ b/flags/telecom_default_phone_account_flags.aconfig
@@ -0,0 +1,18 @@
+package: "com.android.server.telecom.flags"
+container: "system"
+
+# OWNER=tjstuart TARGET=24Q3
+flag {
+  name: "only_update_telephony_on_valid_sub_ids"
+  namespace: "telecom"
+  description: "For testing purposes, only update Telephony when the default calling subId is non-zero"
+  bug: "234846282"
+}
+
+# OWNER=tjstuart TARGET=24Q3
+flag {
+  name: "telephony_has_default_but_telecom_does_not"
+  namespace: "telecom"
+  description: "Telecom is requesting the user to select a sim account to place the outgoing call on but the user has a default account in the settings"
+  bug: "302397094"
+}
\ No newline at end of file
diff --git a/flags/telecom_incallservice_flags.aconfig b/flags/telecom_incallservice_flags.aconfig
new file mode 100644
index 0000000..ea842ac
--- /dev/null
+++ b/flags/telecom_incallservice_flags.aconfig
@@ -0,0 +1,26 @@
+package: "com.android.server.telecom.flags"
+container: "system"
+
+# OWNER=qingzhong TARGET=24Q2
+flag {
+  name: "early_binding_to_incall_service"
+  namespace: "telecom"
+  description: "Binds to InCallServices when call requires no call filtering on watch"
+  bug: "282113261"
+}
+
+# OWNER=pmadapurmath TARGET=24Q2
+flag {
+  name: "ecc_keyguard"
+  namespace: "telecom"
+  description: "Ensure that users are able to return to call from keyguard UI for ECC"
+  bug: "306582821"
+}
+
+# OWNER=pmadapurmath TARGET=24Q3
+flag {
+  name: "separately_bind_to_bt_incall_service"
+  namespace: "telecom"
+  description: "Binding/Unbinding to BluetoothInCallServices in proper time to improve call audio"
+  bug: "306395598"
+}
diff --git a/flags/telecom_non_critical_security_flags.aconfig b/flags/telecom_non_critical_security_flags.aconfig
new file mode 100644
index 0000000..37929a8
--- /dev/null
+++ b/flags/telecom_non_critical_security_flags.aconfig
@@ -0,0 +1,10 @@
+package: "com.android.server.telecom.flags"
+container: "system"
+
+# OWNER=tjstuart TARGET=24Q4
+flag {
+  name: "unregister_unresolvable_accounts"
+  namespace: "telecom"
+  description: "When set, Telecom will unregister accounts if the service is not resolvable"
+  bug: "281061708"
+}
\ No newline at end of file
diff --git a/flags/telecom_profile_user_flags.aconfig b/flags/telecom_profile_user_flags.aconfig
new file mode 100644
index 0000000..feee07d
--- /dev/null
+++ b/flags/telecom_profile_user_flags.aconfig
@@ -0,0 +1,13 @@
+package: "com.android.server.telecom.flags"
+container: "system"
+
+# OWNER=huiwang TARGET=24Q3
+flag {
+  name: "profile_user_support"
+  namespace: "telecom"
+  description: "Fix issues related to the profile user like private profile"
+  bug: "326270861"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
diff --git a/flags/telecom_remote_connection_service.aconfig b/flags/telecom_remote_connection_service.aconfig
new file mode 100644
index 0000000..a30f0b2
--- /dev/null
+++ b/flags/telecom_remote_connection_service.aconfig
@@ -0,0 +1,10 @@
+package: "com.android.server.telecom.flags"
+container: "system"
+
+# OWNER=pmadapurmath TARGET=24Q3
+flag {
+  name: "set_remote_connection_call_id"
+  namespace: "telecom"
+  description: "Sets the telecom call id for remote connections/ conferences."
+  bug: "320242200"
+}
diff --git a/flags/telecom_resolve_hidden_dependencies.aconfig b/flags/telecom_resolve_hidden_dependencies.aconfig
new file mode 100644
index 0000000..a120b85
--- /dev/null
+++ b/flags/telecom_resolve_hidden_dependencies.aconfig
@@ -0,0 +1,19 @@
+package: "com.android.server.telecom.flags"
+container: "system"
+
+# OWNER=tgunn TARGET=24Q3
+flag {
+    name: "telecom_resolve_hidden_dependencies"
+    is_exported: true
+    namespace: "telecom"
+    description: "Mainland cleanup for hidden dependencies"
+    bug: "323414215"
+}
+
+flag {
+    name: "telecom_mainline_blocked_numbers_manager"
+    namespace: "telecom"
+    description: "Fixed read only flag used for setting up BlockedNumbersManager to be retrieved via context"
+    bug: "325049252"
+    is_fixed_read_only: true
+}
diff --git a/flags/telecom_ringer_flag_declarations.aconfig b/flags/telecom_ringer_flag_declarations.aconfig
new file mode 100644
index 0000000..f126bf3
--- /dev/null
+++ b/flags/telecom_ringer_flag_declarations.aconfig
@@ -0,0 +1,10 @@
+package: "com.android.server.telecom.flags"
+container: "system"
+
+# OWNER=yeabkal TARGET=24Q2
+flag {
+  name: "use_device_provided_serialized_ringer_vibration"
+  namespace: "telecom"
+  description: "Gates whether to use a serialized, device-specific ring vibration."
+  bug: "282113261"
+}
\ No newline at end of file
diff --git a/flags/telecom_work_profile_flags.aconfig b/flags/telecom_work_profile_flags.aconfig
new file mode 100644
index 0000000..1891423
--- /dev/null
+++ b/flags/telecom_work_profile_flags.aconfig
@@ -0,0 +1,10 @@
+package: "com.android.server.telecom.flags"
+container: "system"
+
+# OWNER=pmadapurmath TARGET=24Q3
+flag {
+  name: "associated_user_refactor_for_work_profile"
+  namespace: "telecom"
+  description: "Redefines the associated user for calls in the context of work profile support (U+)"
+  bug: "315035693"
+}
\ No newline at end of file
diff --git a/proguard.flags b/proguard.flags
index 635eba6..7c71a15 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -9,17 +9,3 @@
 -keep class android.telecom.Log {
   *;
 }
-
-# Keep classes, annotations and members used by Lifecycle. Remove this once aapt2 is enabled
--keepattributes *Annotation*
-
--keep class * implements android.arch.lifecycle.LifecycleObserver {
-}
-
--keep class * implements android.arch.lifecycle.GeneratedAdapter {
-    <init>(...);
-}
-
--keepclassmembers class ** {
-    @android.arch.lifecycle.OnLifecycleEvent *;
-}
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
index ab74d61..0adb30c 100644
--- a/res/values-cs/strings.xml
+++ b/res/values-cs/strings.xml
@@ -36,7 +36,7 @@
     <string name="accessibility_call_muted" msgid="2968461092554300779">"Hovor ztlumen."</string>
     <string name="accessibility_speakerphone_enabled" msgid="555386652061614267">"Reproduktor je zapnutý."</string>
     <string name="respond_via_sms_canned_response_1" msgid="6332561460870382561">"Teď nemůžu mluvit, o co jde?"</string>
-    <string name="respond_via_sms_canned_response_2" msgid="2052951316129952406">"Zavolám později."</string>
+    <string name="respond_via_sms_canned_response_2" msgid="2052951316129952406">"Zavolám zpátky."</string>
     <string name="respond_via_sms_canned_response_3" msgid="6656147963478092035">"Zavolám později."</string>
     <string name="respond_via_sms_canned_response_4" msgid="9141132488345561047">"Nemůžu telefonovat. Zavoláš později?"</string>
     <string name="respond_via_sms_setting_title" msgid="4762275482898830160">"Rychlé odpovědi"</string>
@@ -63,7 +63,7 @@
     <string name="change_default_call_screening_dialog_affirmative" msgid="7162433828280058647">"Nastavit jako výchozí"</string>
     <string name="change_default_call_screening_dialog_negative" msgid="1839266125623106342">"Zrušit"</string>
     <string name="blocked_numbers" msgid="8322134197039865180">"Blokovaná čísla"</string>
-    <string name="blocked_numbers_msg" msgid="2797422132329662697">"Ze zablokovaných čísel už nebudete přijímat hovory ani zprávy SMS."</string>
+    <string name="blocked_numbers_msg" msgid="2797422132329662697">"Od zablokovaných čísel už nebudete přijímat hovory ani zprávy SMS."</string>
     <string name="block_number" msgid="3784343046852802722">"Přidat číslo"</string>
     <string name="unblock_dialog_body" msgid="2723393535797217261">"Odblokovat číslo <xliff:g id="NUMBER_TO_BLOCK">%1$s</xliff:g>?"</string>
     <string name="unblock_button" msgid="8732021675729981781">"Odblokovat"</string>
@@ -113,7 +113,7 @@
     <string name="phone_settings_private_num_summary_txt" msgid="6755758240544021037">"Blokovat volající, kteří skrývají své číslo"</string>
     <string name="phone_settings_payphone_txt" msgid="5003987966052543965">"Z veřejných telefonů"</string>
     <string name="phone_settings_payphone_summary_txt" msgid="3936631076065563665">"Blokovat hovory z veřejných telefonů"</string>
-    <string name="phone_settings_unknown_txt" msgid="3577926178354772728">"Z nerozpoznaných čísel"</string>
+    <string name="phone_settings_unknown_txt" msgid="3577926178354772728">"Nerozpoznaná čísla"</string>
     <string name="phone_settings_unknown_summary_txt" msgid="5446657192535779645">"Blokovat hovory od nerozpoznaných volajících"</string>
     <string name="phone_settings_unavailable_txt" msgid="825918186053980858">"Neznámé"</string>
     <string name="phone_settings_unavailable_summary_txt" msgid="8221686031038282633">"Blokovat hovory z neznámých čísel"</string>
diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml
index fff7257..4eead66 100644
--- a/res/values-da/strings.xml
+++ b/res/values-da/strings.xml
@@ -73,7 +73,7 @@
     <string name="non_primary_user" msgid="315564589279622098">"Det er kun ejeren af en enhed, der kan se og administrere blokerede numre."</string>
     <string name="delete_icon_description" msgid="5335959254954774373">"Fjern blokering"</string>
     <string name="blocked_numbers_butter_bar_title" msgid="582982373755950791">"Blokering er midlertidigt slået fra"</string>
-    <string name="blocked_numbers_butter_bar_body" msgid="1261213114919301485">"Når du har ringet eller sendt en sms-besked til alarmcentralen, bliver blokering slået fra for at sikre, at alarmcentralen kan komme i kontakt med dig."</string>
+    <string name="blocked_numbers_butter_bar_body" msgid="1261213114919301485">"Når du har ringet eller sendt en besked til alarmcentralen, bliver blokering slået fra for at sikre, at alarmcentralen kan komme i kontakt med dig."</string>
     <string name="blocked_numbers_butter_bar_button" msgid="2704456308072489793">"Genaktiver nu"</string>
     <string name="blocked_numbers_number_blocked_message" msgid="4314736791180919167">"<xliff:g id="BLOCKED_NUMBER">%1$s</xliff:g> blev blokeret"</string>
     <string name="blocked_numbers_number_unblocked_message" msgid="2933071624674945601">"Blokeringen af <xliff:g id="UNBLOCKED_NUMBER">%1$s</xliff:g> blev ophævet"</string>
diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml
index 2cf961d..6b58863 100644
--- a/res/values-el/strings.xml
+++ b/res/values-el/strings.xml
@@ -101,7 +101,7 @@
     <string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Αποσυνδεδεμένες κλήσεις"</string>
     <string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Εφαρμογές τηλεφώνου που αντιμετώπισαν σφάλμα λειτουργίας"</string>
     <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Ροή κλήσης"</string>
-    <string name="alert_outgoing_call" msgid="5319895109298927431">"Εάν πραγματοποιήσετε αυτήν την κλήση, η κλήση σας μέσω <xliff:g id="OTHER_APP">%1$s</xliff:g> θα τερματιστεί."</string>
+    <string name="alert_outgoing_call" msgid="5319895109298927431">"Εάν πραγματοποιήσετε αυτή την κλήση, η κλήση σας μέσω <xliff:g id="OTHER_APP">%1$s</xliff:g> θα τερματιστεί."</string>
     <string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Επιλέξτε πώς θα πραγματοποιήσετε την κλήση"</string>
     <string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Ανακατεύθυνση της κλήσης μέσω <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
     <string name="alert_place_unredirect_outgoing_call" msgid="2467608535225764006">"Κλήση μέσω του αριθμού τηλεφώνου μου"</string>
diff --git a/res/values-fr-rCA/strings.xml b/res/values-fr-rCA/strings.xml
index aaf651f..031b25d 100644
--- a/res/values-fr-rCA/strings.xml
+++ b/res/values-fr-rCA/strings.xml
@@ -30,9 +30,9 @@
     <string name="notification_disconnectedCall_body" msgid="600491714584417536">"L\'appel à <xliff:g id="CALLER">%s</xliff:g> a été déconnecté en raison d\'un appel d\'urgence qui a été passé."</string>
     <string name="notification_disconnectedCall_generic_body" msgid="5282765206349184853">"Votre appel a été déconnecté en raison d\'un appel d\'urgence en cours de lancement."</string>
     <string name="notification_audioProcessing_title" msgid="1619035039880584575">"Appel en arrière-plan"</string>
-    <string name="notification_audioProcessing_body" msgid="8811420157964118913">"<xliff:g id="AUDIO_PROCESSING_APP_NAME">%s</xliff:g> traite un appel en arrière-plan. Cette application peut accéder à l\'audio de l\'appel et faire jouer un contenu audio par l\'intermédiaire de l\'appel."</string>
+    <string name="notification_audioProcessing_body" msgid="8811420157964118913">"<xliff:g id="AUDIO_PROCESSING_APP_NAME">%s</xliff:g> traite un appel en arrière-plan. Cette appli peut accéder à l\'audio de l\'appel et faire jouer un contenu audio par l\'intermédiaire de l\'appel."</string>
     <string name="notification_incallservice_not_responding_title" msgid="5347557574288598548">"<xliff:g id="IN_CALL_SERVICE_APP_NAME">%s</xliff:g> a arrêté de répondre"</string>
-    <string name="notification_incallservice_not_responding_body" msgid="9209308270131968623">"Votre appel a utilisé l\'application Téléphone intégrée à votre appareil"</string>
+    <string name="notification_incallservice_not_responding_body" msgid="9209308270131968623">"Votre appel a utilisé l\'appli Téléphone intégrée à votre appareil"</string>
     <string name="accessibility_call_muted" msgid="2968461092554300779">"Son coupé"</string>
     <string name="accessibility_speakerphone_enabled" msgid="555386652061614267">"Haut-parleur activé"</string>
     <string name="respond_via_sms_canned_response_1" msgid="6332561460870382561">"Peux pas parler. Quoi de neuf?"</string>
@@ -47,20 +47,20 @@
     <string name="respond_via_sms_failure_format" msgid="5198680980054596391">"Échec de l\'envoi du message au <xliff:g id="PHONE_NUMBER">%s</xliff:g>."</string>
     <string name="enable_account_preference_title" msgid="6949224486748457976">"Comptes d\'appel"</string>
     <string name="outgoing_call_not_allowed_user_restriction" msgid="3424338207838851646">"Seuls les appels d\'urgence sont autorisés."</string>
-    <string name="outgoing_call_not_allowed_no_permission" msgid="8590468836581488679">"Cette application ne peut pas faire d\'appels sans l\'autorisation de l\'application Téléphone."</string>
+    <string name="outgoing_call_not_allowed_no_permission" msgid="8590468836581488679">"Cette appli ne peut pas faire d\'appels sans l\'autorisation de l\'appli Téléphone."</string>
     <string name="outgoing_call_error_no_phone_number_supplied" msgid="7665135102566099778">"Pour faire un appel, entrez un numéro valide."</string>
     <string name="duplicate_video_call_not_allowed" msgid="5754746140185781159">"Impossible d\'ajouter l\'appel pour le moment."</string>
     <string name="no_vm_number" msgid="2179959110602180844">"Numéro de messagerie vocale manquant"</string>
     <string name="no_vm_number_msg" msgid="1339245731058529388">"Aucun numéro de messagerie vocale n\'est enregistré sur la carte SIM."</string>
     <string name="add_vm_number_str" msgid="5179510133063168998">"Ajouter un numéro"</string>
-    <string name="change_default_dialer_dialog_title" msgid="5861469279421508060">"Définir <xliff:g id="NEW_APP">%s</xliff:g> comme votre application de téléphonie par défaut?"</string>
-    <string name="change_default_dialer_dialog_affirmative" msgid="8604665314757739550">"Définir comme application de téléphonie par défaut"</string>
+    <string name="change_default_dialer_dialog_title" msgid="5861469279421508060">"Définir <xliff:g id="NEW_APP">%s</xliff:g> comme votre appli de téléphonie par défaut?"</string>
+    <string name="change_default_dialer_dialog_affirmative" msgid="8604665314757739550">"Définir comme appli de téléphonie par défaut"</string>
     <string name="change_default_dialer_dialog_negative" msgid="8648669840052697821">"Annuler"</string>
-    <string name="change_default_dialer_warning_message" msgid="8461963987376916114">"<xliff:g id="NEW_APP">%s</xliff:g> sera en mesure de passer des appels et de contrôler tous les aspects des appels. Seules les applications auxquelles vous avez confiance doivent être définies comme l\'application de téléphonie par défaut."</string>
+    <string name="change_default_dialer_warning_message" msgid="8461963987376916114">"<xliff:g id="NEW_APP">%s</xliff:g> sera en mesure de passer des appels et de contrôler tous les aspects des appels. Seules les applis auxquelles vous avez confiance doivent être définies comme appli de téléphonie par défaut."</string>
     <string name="change_default_call_screening_dialog_title" msgid="5365787219927262408">"Définir <xliff:g id="NEW_APP">%s</xliff:g> comme appli de filtrage d\'appels par défaut?"</string>
     <string name="change_default_call_screening_warning_message_for_disable_old_app" msgid="2039830033533243164">"<xliff:g id="OLD_APP">%s</xliff:g> ne pourra plus filtrer les appels."</string>
-    <string name="change_default_call_screening_warning_message" msgid="9020537562292754269">"<xliff:g id="NEW_APP">%s</xliff:g> pourra accéder aux renseignements sur les appelants qui ne figurent pas dans vos contacts et pourra bloquer ces appels. L\'application de filtrage d\'appels par défaut doit être une application de confiance."</string>
-    <string name="change_default_call_screening_dialog_affirmative" msgid="7162433828280058647">"Définir comme application de filtrage d\'appels par défaut"</string>
+    <string name="change_default_call_screening_warning_message" msgid="9020537562292754269">"<xliff:g id="NEW_APP">%s</xliff:g> pourra accéder aux renseignements sur les appelants qui ne figurent pas dans vos contacts et pourra bloquer ces appels. L\'appli de filtrage d\'appels par défaut doit être une appli de confiance."</string>
+    <string name="change_default_call_screening_dialog_affirmative" msgid="7162433828280058647">"Définir comme appli de filtrage d\'appels par défaut"</string>
     <string name="change_default_call_screening_dialog_negative" msgid="1839266125623106342">"Annuler"</string>
     <string name="blocked_numbers" msgid="8322134197039865180">"Numéros bloqués"</string>
     <string name="blocked_numbers_msg" msgid="2797422132329662697">"Vous ne recevrez pas d\'appels ni de messages texte provenant des numéros bloqués."</string>
@@ -93,19 +93,19 @@
     <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Impossible de passer cet appel, car aucun compte d\'appel ne prend en charge les appels de ce type."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Impossible de faire l\'appel en raison de votre appel <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Impossible de faire l\'appel en raison de vos appels <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
-    <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Impossible de faire l\'appel en raison d\'un appel dans une autre application."</string>
+    <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Impossible de faire l\'appel en raison d\'un appel dans une autre appli."</string>
     <string name="notification_channel_incoming_call" msgid="5245550964701715662">"Appels entrants"</string>
     <string name="notification_channel_missed_call" msgid="7168893015283909012">"Appels manqués"</string>
     <string name="notification_channel_call_blocking" msgid="2028807677868598710">"Blocage des appels"</string>
     <string name="notification_channel_background_calls" msgid="7785659903711350506">"Appels en arrière-plan"</string>
     <string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Appels déconnectés"</string>
-    <string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Applications téléphoniques qui ont planté"</string>
+    <string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Applis téléphoniques qui ont planté"</string>
     <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Diffusion en continu d\'appels"</string>
     <string name="alert_outgoing_call" msgid="5319895109298927431">"Si vous passez cet appel, vous mettrez fin à l\'appel <xliff:g id="OTHER_APP">%1$s</xliff:g>."</string>
     <string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Choisissez comment passer cet appel"</string>
     <string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Rediriger l\'appel en utilisant <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
     <string name="alert_place_unredirect_outgoing_call" msgid="2467608535225764006">"Appeler en utilisant mon numéro de téléphone"</string>
-    <string name="alert_redirect_outgoing_call_timeout" msgid="5568101425637373060">"Impossible de passer l\'appel au moyen de l\'application <xliff:g id="OTHER_APP">%1$s</xliff:g>. Essayez d\'utiliser une autre application de redirection d\'appels ou de communiquer avec le développeur de l\'application pour obtenir de l\'aide."</string>
+    <string name="alert_redirect_outgoing_call_timeout" msgid="5568101425637373060">"Impossible de passer l\'appel au moyen de l\'appli <xliff:g id="OTHER_APP">%1$s</xliff:g>. Essayez d\'utiliser une autre appli de redirection d\'appels ou de communiquer avec le développeur de l\'appli pour obtenir de l\'aide."</string>
     <string name="phone_settings_call_blocking_txt" msgid="7311523114822507178">"Blocage des appels"</string>
     <string name="phone_settings_number_not_in_contact_txt" msgid="2602249106007265757">"Numéros non répertoriés dans les contacts"</string>
     <string name="phone_settings_number_not_in_contact_summary_txt" msgid="963327038085718969">"Bloquer les numéros non répertoriés dans vos contacts"</string>
diff --git a/res/values-gu/strings.xml b/res/values-gu/strings.xml
index 1b5c5ce..3bb2454 100644
--- a/res/values-gu/strings.xml
+++ b/res/values-gu/strings.xml
@@ -43,8 +43,8 @@
     <string name="respond_via_sms_setting_title_2" msgid="4914853536609553457">"હાજરજવાબમાં ફેરફાર કરો"</string>
     <string name="respond_via_sms_setting_summary" msgid="8054571501085436868"></string>
     <string name="respond_via_sms_edittext_dialog_title" msgid="6579353156073272157">"ઝડપી પ્રતિસાદ"</string>
-    <string name="respond_via_sms_confirmation_format" msgid="2932395476561267842">"<xliff:g id="PHONE_NUMBER">%s</xliff:g> પર સંદેશ મોકલ્યો."</string>
-    <string name="respond_via_sms_failure_format" msgid="5198680980054596391">"<xliff:g id="PHONE_NUMBER">%s</xliff:g>ને સંદેશ મોકલવામાં નિષ્ફળ રહ્યાં."</string>
+    <string name="respond_via_sms_confirmation_format" msgid="2932395476561267842">"<xliff:g id="PHONE_NUMBER">%s</xliff:g> પર મેસેજ મોકલ્યો."</string>
+    <string name="respond_via_sms_failure_format" msgid="5198680980054596391">"<xliff:g id="PHONE_NUMBER">%s</xliff:g> પર મેસેજ મોકલવામાં નિષ્ફળ રહ્યાં."</string>
     <string name="enable_account_preference_title" msgid="6949224486748457976">"કૉલ કરવા માટેના એકાઉન્ટ"</string>
     <string name="outgoing_call_not_allowed_user_restriction" msgid="3424338207838851646">"ફક્ત કટોકટીના કૉલ્સને મંજૂરી છે."</string>
     <string name="outgoing_call_not_allowed_no_permission" msgid="8590468836581488679">"ફોન પરવાનગી વિના આ ઍપ્લિકેશન આઉટગોઇંગ કૉલ્સ કરી શકતી નથી."</string>
diff --git a/res/values-kk/strings.xml b/res/values-kk/strings.xml
index 628b440..e53631b 100644
--- a/res/values-kk/strings.xml
+++ b/res/values-kk/strings.xml
@@ -62,15 +62,15 @@
     <string name="change_default_call_screening_warning_message" msgid="9020537562292754269">"<xliff:g id="NEW_APP">%s</xliff:g> контактілер тізімінде жоқ қоңырау шалушылар туралы ақпаратты көріп, бұндай қоңырауларды бөгей алады. Әдепкі қоңырауды тексеру қолданбасы ретінде тек өзіңіз сенетін қолданбаларды ғана орнатқан дұрыс."</string>
     <string name="change_default_call_screening_dialog_affirmative" msgid="7162433828280058647">"Әдепкі ретінде орнату"</string>
     <string name="change_default_call_screening_dialog_negative" msgid="1839266125623106342">"Жабу"</string>
-    <string name="blocked_numbers" msgid="8322134197039865180">"Бөгелген нөмірлер"</string>
+    <string name="blocked_numbers" msgid="8322134197039865180">"Блокталған нөмірлер"</string>
     <string name="blocked_numbers_msg" msgid="2797422132329662697">"Тыйым салынған нөмірлерден қоңыраулар немесе мәтіндік хабарлар алмайсыз."</string>
     <string name="block_number" msgid="3784343046852802722">"Нөмір қосу"</string>
     <string name="unblock_dialog_body" msgid="2723393535797217261">"<xliff:g id="NUMBER_TO_BLOCK">%1$s</xliff:g> бөгеуден шығарылсын ба?"</string>
     <string name="unblock_button" msgid="8732021675729981781">"Бөгеуден шығару"</string>
     <string name="add_blocked_dialog_body" msgid="8599974422407139255">"Қоңыраулары мен мәтіндік хабарлары бөгелетін нөмір"</string>
     <string name="add_blocked_number_hint" msgid="8769422085658041097">"Телефон нөмірі"</string>
-    <string name="block_button" msgid="485080149164258770">"Бөгеу"</string>
-    <string name="non_primary_user" msgid="315564589279622098">"Бөгелген нөмірлерді тек құрылғы иесі көре және басқара алады."</string>
+    <string name="block_button" msgid="485080149164258770">"Блоктау"</string>
+    <string name="non_primary_user" msgid="315564589279622098">"Блокталған нөмірлерді тек құрылғы иесі көре және басқара алады."</string>
     <string name="delete_icon_description" msgid="5335959254954774373">"Бөгеуді алу"</string>
     <string name="blocked_numbers_butter_bar_title" msgid="582982373755950791">"Тыйым уақытша алынды"</string>
     <string name="blocked_numbers_butter_bar_body" msgid="1261213114919301485">"Төтенше жағдай нөмірін терген немесе мәтіндік хабар жіберген соң, төтенше жағдай қызметтері сізге хабарласа алуы үшін тыйым алынады."</string>
diff --git a/res/values-my/strings.xml b/res/values-my/strings.xml
index 9ead5f4..c9e5593 100644
--- a/res/values-my/strings.xml
+++ b/res/values-my/strings.xml
@@ -19,7 +19,7 @@
     <string name="telecommAppLabel" product="default" msgid="1825598513414129827">"ဖုန်းခေါ်ဆိုမှုများ"</string>
     <string name="userCallActivityLabel" product="default" msgid="3605391260292846248">"ဖုန်း"</string>
     <string name="unknown" msgid="6993977514360123431">"မသိပါ"</string>
-    <string name="notification_missedCallTitle" msgid="5060387047205532974">"လွဲသွားသော ဖုန်းခေါ်မှု"</string>
+    <string name="notification_missedCallTitle" msgid="5060387047205532974">"လွတ်သွားသော ခေါ်ဆိုမှု"</string>
     <string name="notification_missedWorkCallTitle" msgid="6965463282259034953">"လွတ်သွားသည့် အလုပ်ဆိုင်ရာ ခ​ေါ်ဆိုမှု"</string>
     <string name="notification_missedCallsTitle" msgid="3910479625507893809">"လွဲသွားသော ဖုန်းခေါ်မှုများ"</string>
     <string name="notification_missedCallsMsg" msgid="5055782736170916682">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> လွဲသွားသော ဖုန်းခေါ်မှုများ"</string>
diff --git a/res/values-ne/strings.xml b/res/values-ne/strings.xml
index 44645dc..df2c70c 100644
--- a/res/values-ne/strings.xml
+++ b/res/values-ne/strings.xml
@@ -27,7 +27,7 @@
     <string name="notification_missedCall_call_back" msgid="7900333283939789732">"फेरि कल गर्नुहोस्"</string>
     <string name="notification_missedCall_message" msgid="4054698824390076431">"सन्देश"</string>
     <string name="notification_disconnectedCall_title" msgid="1790131923692416928">"विच्छेद गरिएको कल"</string>
-    <string name="notification_disconnectedCall_body" msgid="600491714584417536">"आपत्‌कालीन कल गरिएको हुनाले <xliff:g id="CALLER">%s</xliff:g> लाई गरिएको कल विच्छेद गरियो।"</string>
+    <string name="notification_disconnectedCall_body" msgid="600491714584417536">"आपत्‌कालीन कल गरिएको हुनाले <xliff:g id="CALLER">%s</xliff:g> लाई गरिएको कल डिस्कनेक्ट गरियो।"</string>
     <string name="notification_disconnectedCall_generic_body" msgid="5282765206349184853">"आपत्‌कालीन कल जारी रहेको हुनाले तपाईंको कल विच्छेद गरिएको छ।"</string>
     <string name="notification_audioProcessing_title" msgid="1619035039880584575">"पृष्ठभूमिको कल"</string>
     <string name="notification_audioProcessing_body" msgid="8811420157964118913">"<xliff:g id="AUDIO_PROCESSING_APP_NAME">%s</xliff:g> ले ब्याकग्राउन्डमा कुनै कल प्रोसेस गर्दै छ। यो एपले तपाईंको कलको अडियो प्रयोग गरिरहेको र सोही अडियो प्ले गरिरहेको हुन सक्छ।"</string>
diff --git a/res/values-night/styles.xml b/res/values-night/styles.xml
index 5b81fac..b94b9e4 100644
--- a/res/values-night/styles.xml
+++ b/res/values-night/styles.xml
@@ -23,6 +23,11 @@
         <item name="android:actionOverflowButtonStyle">@style/TelecomDialerSettingsActionOverflowButtonStyle</item>
         <item name="android:windowLightNavigationBar">true</item>
         <item name="android:windowContentOverlay">@null</item>
+
+        <!--
+            TODO(b/309578419): Make activities handle insets properly and then remove this.
+        -->
+        <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
     </style>
 
     <style name="Theme.Telecom.BlockedNumbers" parent="@android:style/Theme.DeviceDefault.Light">
@@ -31,6 +36,11 @@
         <item name="android:windowLightNavigationBar">true</item>
         <item name="android:windowContentOverlay">@null</item>
         <item name="android:listDivider">@null</item>
+
+        <!--
+            TODO(b/309578419): Make activities handle insets properly and then remove this.
+        -->
+        <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
     </style>
 
 </resources>
\ No newline at end of file
diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml
index 8dfee81..48c7957 100644
--- a/res/values-nl/strings.xml
+++ b/res/values-nl/strings.xml
@@ -16,7 +16,7 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="telecommAppLabel" product="default" msgid="1825598513414129827">"Telefoongesprekken"</string>
+    <string name="telecommAppLabel" product="default" msgid="1825598513414129827">"Telefoon­gesprekken"</string>
     <string name="userCallActivityLabel" product="default" msgid="3605391260292846248">"Telefoon"</string>
     <string name="unknown" msgid="6993977514360123431">"Onbekend"</string>
     <string name="notification_missedCallTitle" msgid="5060387047205532974">"Gemist gesprek"</string>
diff --git a/res/values-or/strings.xml b/res/values-or/strings.xml
index 787711b..6f3ebe3 100644
--- a/res/values-or/strings.xml
+++ b/res/values-or/strings.xml
@@ -24,7 +24,7 @@
     <string name="notification_missedCallsTitle" msgid="3910479625507893809">"ମିସ୍ଡ କଲ୍"</string>
     <string name="notification_missedCallsMsg" msgid="5055782736170916682">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g>ଟି ମିସ୍ଡ କଲ୍"</string>
     <string name="notification_missedCallTicker" msgid="6731461957487087769">"<xliff:g id="MISSED_CALL_FROM">%s</xliff:g>ଙ୍କ ଠାରୁ ମିସ୍-କଲ୍ ମିଳିଛି"</string>
-    <string name="notification_missedCall_call_back" msgid="7900333283939789732">"କଲବ୍ୟାକ୍ କରନ୍ତୁ"</string>
+    <string name="notification_missedCall_call_back" msgid="7900333283939789732">"କଲବେକ କରନ୍ତୁ"</string>
     <string name="notification_missedCall_message" msgid="4054698824390076431">"ମେସେଜ୍‍ ଦିଅନ୍ତୁ"</string>
     <string name="notification_disconnectedCall_title" msgid="1790131923692416928">"କଲ୍ ବିଚ୍ଛିନ୍ନ କରାଯାଇଛି"</string>
     <string name="notification_disconnectedCall_body" msgid="600491714584417536">"ଏକ ଜରୁରୀକାଳୀନ କଲ୍ କରାଯାଇଥିବାରୁ <xliff:g id="CALLER">%s</xliff:g>ଙ୍କୁ କରାଯାଇଥିବା କଲ୍ ବିଚ୍ଛିନ୍ନ କରାଯାଇଛି।"</string>
diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml
index afb8eca..a7fc3c7 100644
--- a/res/values-pt/strings.xml
+++ b/res/values-pt/strings.xml
@@ -57,7 +57,7 @@
     <string name="change_default_dialer_dialog_affirmative" msgid="8604665314757739550">"Definir padrão"</string>
     <string name="change_default_dialer_dialog_negative" msgid="8648669840052697821">"Cancelar"</string>
     <string name="change_default_dialer_warning_message" msgid="8461963987376916114">"O <xliff:g id="NEW_APP">%s</xliff:g> poderá ligar e controlar todos os aspectos das chamadas. Defina como aplicativo Telefone padrão somente aqueles em que você confia."</string>
-    <string name="change_default_call_screening_dialog_title" msgid="5365787219927262408">"Usar o <xliff:g id="NEW_APP">%s</xliff:g> como seu app de seleção de chamadas padrão?"</string>
+    <string name="change_default_call_screening_dialog_title" msgid="5365787219927262408">"Usar o <xliff:g id="NEW_APP">%s</xliff:g> como seu app de filtro de ligações padrão?"</string>
     <string name="change_default_call_screening_warning_message_for_disable_old_app" msgid="2039830033533243164">"O <xliff:g id="OLD_APP">%s</xliff:g> não selecionará mais as chamadas."</string>
     <string name="change_default_call_screening_warning_message" msgid="9020537562292754269">"O <xliff:g id="NEW_APP">%s</xliff:g> poderá ver as informações sobre os autores das chamadas que não estão entre seus contatos e bloqueá-los. Defina como app de seleção de chamadas padrão somente aqueles em que você confia."</string>
     <string name="change_default_call_screening_dialog_affirmative" msgid="7162433828280058647">"Definir padrão"</string>
diff --git a/res/values-sw/strings.xml b/res/values-sw/strings.xml
index 1b49990..ef58c00 100644
--- a/res/values-sw/strings.xml
+++ b/res/values-sw/strings.xml
@@ -48,11 +48,11 @@
     <string name="enable_account_preference_title" msgid="6949224486748457976">"Akaunti za simu"</string>
     <string name="outgoing_call_not_allowed_user_restriction" msgid="3424338207838851646">"Piga simu za dharura pekee."</string>
     <string name="outgoing_call_not_allowed_no_permission" msgid="8590468836581488679">"Programu hii haiwezi kupiga simu bila ruhusa ya Simu."</string>
-    <string name="outgoing_call_error_no_phone_number_supplied" msgid="7665135102566099778">"Ili upige simu, weka nambari sahihi."</string>
+    <string name="outgoing_call_error_no_phone_number_supplied" msgid="7665135102566099778">"Ili upige simu, weka namba sahihi."</string>
     <string name="duplicate_video_call_not_allowed" msgid="5754746140185781159">"Hangout ya video haiwezi kuongezwa kwa wakati huu."</string>
     <string name="no_vm_number" msgid="2179959110602180844">"Nambari ya sauti inayokosekana"</string>
-    <string name="no_vm_number_msg" msgid="1339245731058529388">"Hakuna nambari ya ujumbe wa sauti iliyohifadhiwa katika SIM kadi."</string>
-    <string name="add_vm_number_str" msgid="5179510133063168998">"Ongeza nambari"</string>
+    <string name="no_vm_number_msg" msgid="1339245731058529388">"Hakuna namba ya ujumbe wa sauti iliyohifadhiwa katika SIM kadi."</string>
+    <string name="add_vm_number_str" msgid="5179510133063168998">"Ongeza namba"</string>
     <string name="change_default_dialer_dialog_title" msgid="5861469279421508060">"Unataka kufanya <xliff:g id="NEW_APP">%s</xliff:g> iwe programu chaguomsingi ya simu?"</string>
     <string name="change_default_dialer_dialog_affirmative" msgid="8604665314757739550">"Fanya iwe Chaguo-Msingi"</string>
     <string name="change_default_dialer_dialog_negative" msgid="8648669840052697821">"Ghairi"</string>
@@ -63,21 +63,21 @@
     <string name="change_default_call_screening_dialog_affirmative" msgid="7162433828280058647">"Weka iwe Chaguomsingi"</string>
     <string name="change_default_call_screening_dialog_negative" msgid="1839266125623106342">"Ghairi"</string>
     <string name="blocked_numbers" msgid="8322134197039865180">"Nambari zilizozuiwa"</string>
-    <string name="blocked_numbers_msg" msgid="2797422132329662697">"Hutapokea simu au SMS kutoka kwa nambari zilizozuiwa."</string>
-    <string name="block_number" msgid="3784343046852802722">"Ongeza nambari"</string>
+    <string name="blocked_numbers_msg" msgid="2797422132329662697">"Hutapokea simu au SMS kutoka kwa namba zilizozuiwa."</string>
+    <string name="block_number" msgid="3784343046852802722">"Ongeza namba"</string>
     <string name="unblock_dialog_body" msgid="2723393535797217261">"Ungependa kuacha kuzuia <xliff:g id="NUMBER_TO_BLOCK">%1$s</xliff:g>?"</string>
     <string name="unblock_button" msgid="8732021675729981781">"Acha kuzuia"</string>
     <string name="add_blocked_dialog_body" msgid="8599974422407139255">"Zuia simu na SMS kutoka kwa"</string>
     <string name="add_blocked_number_hint" msgid="8769422085658041097">"Nambari ya simu"</string>
     <string name="block_button" msgid="485080149164258770">"Zuia"</string>
-    <string name="non_primary_user" msgid="315564589279622098">"Ni mmiliki wa kifaa pekee anayeweza kuangalia na kuthibiti nambari zilizozuiwa."</string>
+    <string name="non_primary_user" msgid="315564589279622098">"Ni mmiliki wa kifaa pekee anayeweza kuangalia na kuthibiti namba zilizozuiwa."</string>
     <string name="delete_icon_description" msgid="5335959254954774373">"Acha kuzuia"</string>
     <string name="blocked_numbers_butter_bar_title" msgid="582982373755950791">"Kipengele cha kuzuia kimezimwa kwa muda"</string>
-    <string name="blocked_numbers_butter_bar_body" msgid="1261213114919301485">"Ukishapiga au kutuma ujumbe kwa nambari ya dharura, kipengele cha kuzuia anwani huzimwa ili watoa huduma za dharura waweze kuwasiliana nawe."</string>
+    <string name="blocked_numbers_butter_bar_body" msgid="1261213114919301485">"Ukishapiga au kutuma ujumbe kwa namba ya dharura, kipengele cha kuzuia anwani huzimwa ili watoa huduma za dharura waweze kuwasiliana nawe."</string>
     <string name="blocked_numbers_butter_bar_button" msgid="2704456308072489793">"Kiwashe tena sasa"</string>
     <string name="blocked_numbers_number_blocked_message" msgid="4314736791180919167">"<xliff:g id="BLOCKED_NUMBER">%1$s</xliff:g> imezuiwa"</string>
     <string name="blocked_numbers_number_unblocked_message" msgid="2933071624674945601">"<xliff:g id="UNBLOCKED_NUMBER">%1$s</xliff:g> imeacha kuzuiwa"</string>
-    <string name="blocked_numbers_block_emergency_number_message" msgid="4198550501500893890">"Huwezi kuzuia nambari ya dharura."</string>
+    <string name="blocked_numbers_block_emergency_number_message" msgid="4198550501500893890">"Huwezi kuzuia namba ya dharura."</string>
     <string name="blocked_numbers_number_already_blocked_message" msgid="2301270825735665458">"<xliff:g id="BLOCKED_NUMBER">%1$s</xliff:g> tayari imezuiwa."</string>
     <string name="toast_personal_call_msg" msgid="5817631570381795610">"Kupiga simu kwa kutumia kipiga simu cha binafsi"</string>
     <string name="notification_incoming_call" msgid="1233481138362230894">"Simu ya <xliff:g id="CALL_VIA">%1$s</xliff:g> kutoka kwa <xliff:g id="CALL_FROM">%2$s</xliff:g>"</string>
@@ -104,19 +104,19 @@
     <string name="alert_outgoing_call" msgid="5319895109298927431">"Ukipiga simu hii, simu yako kwenye <xliff:g id="OTHER_APP">%1$s</xliff:g> itakatwa."</string>
     <string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Chagua jinsi utakavyopiga simu hii"</string>
     <string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Elekeza simu ukitumia <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
-    <string name="alert_place_unredirect_outgoing_call" msgid="2467608535225764006">"Piga simu ukitumia nambari yangu ya simu"</string>
+    <string name="alert_place_unredirect_outgoing_call" msgid="2467608535225764006">"Piga simu ukitumia namba yangu ya simu"</string>
     <string name="alert_redirect_outgoing_call_timeout" msgid="5568101425637373060">"<xliff:g id="OTHER_APP">%1$s</xliff:g> imeshindwa kupiga simu. Jaribu kutumia programu nyingine inayoelekeza simu kwingine au uwasiliane na msanidi programu kwa usaidizi."</string>
     <string name="phone_settings_call_blocking_txt" msgid="7311523114822507178">"Kuzuia Simu"</string>
     <string name="phone_settings_number_not_in_contact_txt" msgid="2602249106007265757">"Nambari ambazo haziko kwenye Anwani"</string>
-    <string name="phone_settings_number_not_in_contact_summary_txt" msgid="963327038085718969">"Zuia nambari ambazo hazipo kwenye orodha ya Anwani zako"</string>
+    <string name="phone_settings_number_not_in_contact_summary_txt" msgid="963327038085718969">"Zuia namba ambazo hazipo kwenye orodha ya Anwani zako"</string>
     <string name="phone_settings_private_num_txt" msgid="6339272760338475619">"Faragha"</string>
-    <string name="phone_settings_private_num_summary_txt" msgid="6755758240544021037">"Zuia wapigaji ambao wameficha nambari zao za simu"</string>
+    <string name="phone_settings_private_num_summary_txt" msgid="6755758240544021037">"Zuia wapigaji ambao wameficha namba zao za simu"</string>
     <string name="phone_settings_payphone_txt" msgid="5003987966052543965">"Simu ya kulipia"</string>
-    <string name="phone_settings_payphone_summary_txt" msgid="3936631076065563665">"Zuia simu kutoka kwa nambari ya simu za kulipia"</string>
+    <string name="phone_settings_payphone_summary_txt" msgid="3936631076065563665">"Zuia simu kutoka kwa namba ya simu za kulipia"</string>
     <string name="phone_settings_unknown_txt" msgid="3577926178354772728">"Zisizojulikani"</string>
     <string name="phone_settings_unknown_summary_txt" msgid="5446657192535779645">"Zuia simu kutoka kwa wapigaji wasiojulikana"</string>
     <string name="phone_settings_unavailable_txt" msgid="825918186053980858">"Zisizotambulishwa"</string>
-    <string name="phone_settings_unavailable_summary_txt" msgid="8221686031038282633">"Zuia simu zinazopigwa bila kutambulisha nambari ya simu"</string>
+    <string name="phone_settings_unavailable_summary_txt" msgid="8221686031038282633">"Zuia simu zinazopigwa bila kutambulisha namba ya simu"</string>
     <string name="phone_strings_call_blocking_turned_off_notification_title_txt" msgid="2895809176537908791">"Kuzuia Simu"</string>
     <string name="phone_strings_call_blocking_turned_off_notification_text_txt" msgid="1713632946174016619">"Kipengele cha Kuzuia Simu kimezimwa"</string>
     <string name="phone_strings_emergency_call_made_dialog_title_txt" msgid="6629412508584507377">"Simu ya dharura imepigwa"</string>
diff --git a/res/values-vi/strings.xml b/res/values-vi/strings.xml
index 5ae2e79..70f9bfc 100644
--- a/res/values-vi/strings.xml
+++ b/res/values-vi/strings.xml
@@ -133,5 +133,5 @@
     <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Không xác định"</string>
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Đang truyền trực tuyến âm thanh tới thiết bị khác"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Kết thúc"</string>
-    <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Chuyển đổi tại đây"</string>
+    <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Chuyển qua thiết bị này"</string>
 </resources>
diff --git a/res/values/config.xml b/res/values/config.xml
index 15f765b..ae5d88e 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -49,8 +49,10 @@
     <bool name="grant_location_permission_enabled">false</bool>
 
     <!-- When true, a simple full intensity on/off vibration pattern will be used when calls ring.
-         When false, a fancy vibration pattern which ramps up and down will be used.
-         Devices should overlay this value based on the type of vibration hardware they employ. -->
+
+         When false, the vibration effect serialized in the raw `default_ringtone_vibration_effect`
+         resource (under `frameworks/base/core/res/res/raw/`) is used. Devices should overlay this
+         value based on the type of vibration hardware they employ. -->
     <bool name="use_simple_vibration_pattern">false</bool>
 
     <!-- Threshold for the X+Y component of gravity needed for the device orientation to be
@@ -77,4 +79,17 @@
     <!-- When true, the options in the call blocking settings to block unavailable and unknown
      callers are combined into a single toggle. -->
     <bool name="combine_options_to_block_unavailable_and_unknown_callers">true</bool>
+
+    <!-- When true, skip fetching quick reply response -->
+    <bool name="skip_loading_canned_text_response">false</bool>
+
+    <!-- When true, skip fetching incoming caller info -->
+    <bool name="skip_incoming_caller_info_query">false</bool>
+
+    <string-array name="system_bluetooth_stack_package_name" translatable="false">
+        <!-- AOSP -->
+        <item>com.android.bluetooth</item>
+        <!-- Used for internal targets -->
+        <item>com.google.android.bluetooth</item>
+    </string-array>
 </resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index c8b24d3..0624082 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -32,6 +32,11 @@
         <item name="android:navigationBarColor">@android:color/transparent</item>
         <item name="android:windowLightStatusBar">true</item>
         <item name="android:windowLightNavigationBar">true</item>
+
+        <!--
+            TODO(b/309578419): Make activities handle insets properly and then remove this.
+        -->
+        <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
     </style>
 
     <style name="Theme.Telecom.EnableAccount" parent="Theme.Telecom.DialerSettings">
@@ -61,6 +66,7 @@
     </style>
 
     <style name="BlockedNumbersButton" parent="BlockedNumbersTextPrimary2">
+        <item name="android:textColor">#202124</item>
     </style>
 
     <style name="BlockedNumbersTextHead1"
diff --git a/res/xml/activity_blocked_numbers.xml b/res/xml/activity_blocked_numbers.xml
index e77184d..b6298e9 100644
--- a/res/xml/activity_blocked_numbers.xml
+++ b/res/xml/activity_blocked_numbers.xml
@@ -41,8 +41,8 @@
                     android:layout_height="wrap_content"
                     android:text="@string/non_primary_user"
                     android:paddingTop="@dimen/blocked_numbers_large_padding"
-                    android:paddingLeft="@dimen/blocked_numbers_large_padding"
-                    android:paddingRight="@dimen/blocked_numbers_large_padding"
+                    android:paddingStart="@dimen/blocked_numbers_large_padding"
+                    android:paddingEnd="@dimen/blocked_numbers_large_padding"
                     style="@style/BlockedNumbersTextPrimary2"
                     android:visibility="gone" />
 
@@ -62,8 +62,8 @@
                         android:layout_width="match_parent"
                         android:layout_height="wrap_content"
                         android:paddingTop="@dimen/blocked_numbers_large_padding"
-                        android:paddingLeft="@dimen/blocked_numbers_large_padding"
-                        android:paddingRight="@dimen/blocked_numbers_large_padding">
+                        android:paddingStart="@dimen/blocked_numbers_large_padding"
+                        android:paddingEnd="@dimen/blocked_numbers_large_padding">
 
                     <TextView
                             android:layout_width="wrap_content"
diff --git a/res/xml/blocking_suppressed_butterbar.xml b/res/xml/blocking_suppressed_butterbar.xml
index 8b941b9..2947340 100644
--- a/res/xml/blocking_suppressed_butterbar.xml
+++ b/res/xml/blocking_suppressed_butterbar.xml
@@ -25,19 +25,19 @@
             android:id="@+id/icon"
             android:layout_height="wrap_content"
             android:layout_width="wrap_content"
-            android:layout_alignParentLeft="true"
+            android:layout_alignParentStart="true"
             android:paddingTop="@dimen/blocked_numbers_large_padding"
-            android:paddingRight="@dimen/blocked_numbers_large_padding"
-            android:paddingLeft="@dimen/blocked_numbers_large_padding"
+            android:paddingEnd="@dimen/blocked_numbers_large_padding"
+            android:paddingStart="@dimen/blocked_numbers_large_padding"
             android:src="@drawable/ic_status_blocked_orange_40dp"/>
 
     <TextView
             android:id="@+id/title"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:layout_toRightOf="@id/icon"
+            android:layout_toEndOf="@id/icon"
             android:paddingTop="@dimen/blocked_numbers_large_padding"
-            android:paddingRight="@dimen/blocked_numbers_large_padding"
+            android:paddingEnd="@dimen/blocked_numbers_large_padding"
             android:text="@string/blocked_numbers_butter_bar_title"
             style="@style/BlockedNumbersTextPrimary2" />
 
@@ -45,11 +45,11 @@
             android:id="@+id/description"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:layout_toRightOf="@id/icon"
+            android:layout_toEndOf="@id/icon"
             android:layout_below="@id/title"
             android:paddingTop="@dimen/blocked_numbers_large_padding"
             android:paddingBottom="@dimen/blocked_numbers_large_padding"
-            android:paddingRight="@dimen/blocked_numbers_large_padding"
+            android:paddingEnd="@dimen/blocked_numbers_large_padding"
             android:text="@string/blocked_numbers_butter_bar_body"
             style="@style/BlockedNumbersTextSecondary" />
 
@@ -57,9 +57,9 @@
             android:id="@+id/reenable_button"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:layout_toRightOf="@id/icon"
+            android:layout_toEndOf="@id/icon"
             android:layout_below="@id/description"
-            android:paddingRight="@dimen/blocked_numbers_large_padding"
+            android:paddingEnd="@dimen/blocked_numbers_large_padding"
             android:text="@string/blocked_numbers_butter_bar_button"
             style="@style/BlockedNumbersButton"
             android:background="?android:attr/selectableItemBackgroundBorderless" />
diff --git a/src/com/android/server/telecom/Analytics.java b/src/com/android/server/telecom/Analytics.java
index bbcf858..4cf54ed 100644
--- a/src/com/android/server/telecom/Analytics.java
+++ b/src/com/android/server/telecom/Analytics.java
@@ -720,15 +720,19 @@
     }
 
     private static int getCarrierId(Context context) {
-        SubscriptionManager subscriptionManager =
-                context.getSystemService(SubscriptionManager.class);
-        List<SubscriptionInfo> subInfos = subscriptionManager.getActiveSubscriptionInfoList();
-        if (subInfos == null) {
+        try {
+            SubscriptionManager subscriptionManager =
+                    context.getSystemService(SubscriptionManager.class).createForAllUserProfiles();
+            List<SubscriptionInfo> subInfos = subscriptionManager.getActiveSubscriptionInfoList();
+            if (subInfos == null) {
+                return -1;
+            }
+            return subInfos.stream()
+                    .max(Comparator.comparing(Analytics::scoreSubscriptionInfo))
+                    .map(SubscriptionInfo::getCarrierId).orElse(-1);
+        } catch (UnsupportedOperationException ignored) {
             return -1;
         }
-        return subInfos.stream()
-                .max(Comparator.comparing(Analytics::scoreSubscriptionInfo))
-                .map(SubscriptionInfo::getCarrierId).orElse(-1);
     }
 
     // Copied over from Telephony's server-side logic for consistency
diff --git a/src/com/android/server/telecom/AsyncRingtonePlayer.java b/src/com/android/server/telecom/AsyncRingtonePlayer.java
index 3fbac1f..3b5e342 100644
--- a/src/com/android/server/telecom/AsyncRingtonePlayer.java
+++ b/src/com/android/server/telecom/AsyncRingtonePlayer.java
@@ -26,10 +26,15 @@
 import android.os.Message;
 import android.telecom.Log;
 import android.telecom.Logging.Session;
+import android.util.Pair;
+
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.os.SomeArgs;
 import com.android.internal.util.Preconditions;
 
+import java.util.ArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 import java.util.function.BiConsumer;
 import java.util.function.Supplier;
 
@@ -39,6 +44,9 @@
  */
 @VisibleForTesting
 public class AsyncRingtonePlayer {
+    // Maximum amount of time we will delay playing a ringtone while waiting for audio routing to
+    // be ready.
+    private static final int PLAY_DELAY_TIMEOUT_MS = 1000;
     // Message codes used with the ringtone thread.
     private static final int EVENT_PLAY = 1;
     private static final int EVENT_STOP = 2;
@@ -49,6 +57,23 @@
     /** The current ringtone. Only used by the ringtone thread. */
     private Ringtone mRingtone;
 
+    /**
+     * Set to true if we are setting up to play or are currently playing. False if we are stopping
+     * or have stopped playing.
+     */
+    private boolean mIsPlaying = false;
+
+    /**
+     * Set to true if BT HFP is active and audio is connected.
+     */
+    private boolean mIsBtActive = false;
+
+    /**
+     * A list of pending ringing ready latches, which are used to delay the ringing command until
+     * audio paths are set and ringing is ready.
+     */
+    private final ArrayList<CountDownLatch> mPendingRingingLatches = new ArrayList<>();
+
     public AsyncRingtonePlayer() {
         // Empty
     }
@@ -58,23 +83,84 @@
      * If {@link VolumeShaper.Configuration} is specified, it is applied to the ringtone to change
      * the volume of the ringtone as it plays.
      *
-     * @param ringtoneSupplier The {@link Ringtone} factory.
+     * @param ringtoneInfoSupplier The {@link Ringtone} factory.
      * @param ringtoneConsumer The {@link Ringtone} post-creation callback (to start the vibration).
+     * @param isHfpDeviceConnected True if there is a HFP BT device connected, false otherwise.
      */
-    public void play(@NonNull Supplier<Ringtone> ringtoneSupplier,
-            BiConsumer<Ringtone, Boolean> ringtoneConsumer) {
+    public void play(@NonNull Supplier<Pair<Uri, Ringtone>> ringtoneInfoSupplier,
+            BiConsumer<Pair<Uri, Ringtone>, Boolean> ringtoneConsumer,
+            boolean isHfpDeviceConnected) {
         Log.d(this, "Posting play.");
+        mIsPlaying = true;
         SomeArgs args = SomeArgs.obtain();
-        args.arg1 = ringtoneSupplier;
+        args.arg1 = ringtoneInfoSupplier;
         args.arg2 = ringtoneConsumer;
         args.arg3 = Log.createSubsession();
+        args.arg4 = prepareRingingReadyLatch(isHfpDeviceConnected);
         postMessage(EVENT_PLAY, true /* shouldCreateHandler */, args);
     }
 
     /** Stops playing the ringtone. */
     public void stop() {
         Log.d(this, "Posting stop.");
+        mIsPlaying = false;
         postMessage(EVENT_STOP, false /* shouldCreateHandler */, null);
+        // Clear any pending ringing latches so that we do not have to wait for its timeout to pass
+        // before calling stop.
+        clearPendingRingingLatches();
+    }
+
+    /**
+     * Called when the BT HFP profile active state changes.
+     * @param isBtActive A BT device is connected and audio is active.
+     */
+    public void updateBtActiveState(boolean isBtActive) {
+        Log.i(this, "updateBtActiveState: " + isBtActive);
+        synchronized (mPendingRingingLatches) {
+            mIsBtActive = isBtActive;
+            if (isBtActive) mPendingRingingLatches.forEach(CountDownLatch::countDown);
+        }
+    }
+
+    /**
+     * Prepares a new ringing ready latch and tracks it in a list. Once the ready latch has been
+     * used, {@link #removePendingRingingReadyLatch(CountDownLatch)} must be called on this latch.
+     * @param isHfpDeviceConnected true if there is a HFP device connected.
+     * @return the newly prepared CountDownLatch
+     */
+    private CountDownLatch prepareRingingReadyLatch(boolean isHfpDeviceConnected) {
+        CountDownLatch latch = new CountDownLatch(1);
+        synchronized (mPendingRingingLatches) {
+            // We only want to delay ringing if BT is connected but not active yet.
+            boolean isDelayRequired = isHfpDeviceConnected && !mIsBtActive;
+            Log.i(this, "prepareRingingReadyLatch:"
+                    + " connected=" + isHfpDeviceConnected
+                    + ", BT active=" + mIsBtActive
+                    + ", isDelayRequired=" + isDelayRequired);
+            if (!isDelayRequired) latch.countDown();
+            mPendingRingingLatches.add(latch);
+        }
+        return latch;
+    }
+
+    /**
+     * Remove a ringing ready latch that has been used and is no longer pending.
+     * @param l The latch to remove.
+     */
+    private void removePendingRingingReadyLatch(CountDownLatch l) {
+        synchronized (mPendingRingingLatches) {
+            mPendingRingingLatches.remove(l);
+        }
+    }
+
+    /**
+     * Count down all pending ringing ready latches and then clear the list.
+     */
+    private void clearPendingRingingLatches() {
+        synchronized (mPendingRingingLatches) {
+            mPendingRingingLatches.forEach(CountDownLatch::countDown);
+            mPendingRingingLatches.clear();
+        }
     }
 
     /**
@@ -126,9 +212,12 @@
      * Starts the actual playback of the ringtone. Executes on ringtone-thread.
      */
     private void handlePlay(SomeArgs args) {
-        Supplier<Ringtone> ringtoneSupplier = (Supplier<Ringtone>) args.arg1;
-        BiConsumer<Ringtone, Boolean> ringtoneConsumer = (BiConsumer<Ringtone, Boolean>) args.arg2;
+        Supplier<Pair<Uri, Ringtone>> ringtoneInfoSupplier =
+                (Supplier<Pair<Uri, Ringtone>>) args.arg1;
+        BiConsumer<Pair<Uri, Ringtone>, Boolean> ringtoneConsumer =
+                (BiConsumer<Pair<Uri, Ringtone>, Boolean>) args.arg2;
         Session session = (Session) args.arg3;
+        CountDownLatch ringingReadyLatch = (CountDownLatch) args.arg4;
         args.recycle();
 
         Log.continueSession(session, "ARP.hP");
@@ -136,17 +225,34 @@
             // Don't bother with any of this if there is an EVENT_STOP waiting, but give the
             // consumer a chance to do anything no matter what.
             if (mHandler.hasMessages(EVENT_STOP)) {
+                Log.i(this, "handlePlay: skipping play early due to pending STOP");
+                removePendingRingingReadyLatch(ringingReadyLatch);
                 ringtoneConsumer.accept(null, /* stopped= */ true);
                 return;
             }
             Ringtone ringtone = null;
+            Uri ringtoneUri = null;
             boolean hasStopped = false;
             try {
-                ringtone = ringtoneSupplier.get();
-                // Ringtone supply can be slow. Re-check for stop event.
+                try {
+                    Log.i(this, "handlePlay: delay ring for ready signal...");
+                    boolean reachedZero = ringingReadyLatch.await(PLAY_DELAY_TIMEOUT_MS,
+                            TimeUnit.MILLISECONDS);
+                    Log.i(this, "handlePlay: ringing ready, timeout=" + !reachedZero);
+                } catch (InterruptedException e) {
+                    Log.w(this, "handlePlay: latch exception: " + e);
+                }
+                if (ringtoneInfoSupplier != null && ringtoneInfoSupplier.get() != null) {
+                    ringtoneUri = ringtoneInfoSupplier.get().first;
+                    ringtone = ringtoneInfoSupplier.get().second;
+                }
+
+                // Ringtone supply can be slow or stop command could have been issued while waiting
+                // for BT to move to CONNECTED state. Re-check for stop event.
                 if (mHandler.hasMessages(EVENT_STOP)) {
+                    Log.i(this, "handlePlay: skipping play due to pending STOP");
                     hasStopped = true;
-                    ringtone.stop();  // proactively release the ringtone.
+                    if (ringtone != null) ringtone.stop();  // proactively release the ringtone.
                     return;
                 }
                 // setRingtone even if null - it also stops any current ringtone to be consistent
@@ -157,8 +263,7 @@
                     Log.w(this, "No ringtone was found bail out from playing.");
                     return;
                 }
-                Uri uri = mRingtone.getUri();
-                String uriString = (uri != null ? uri.toSafeString() : "");
+                String uriString = ringtoneUri != null ? ringtoneUri.toSafeString() : "";
                 Log.i(this, "handlePlay: Play ringtone. Uri: " + uriString);
                 mRingtone.setLooping(true);
                 if (mRingtone.isPlaying()) {
@@ -168,7 +273,8 @@
                 mRingtone.play();
                 Log.i(this, "Play ringtone, looping.");
             } finally {
-                ringtoneConsumer.accept(ringtone, hasStopped);
+                removePendingRingingReadyLatch(ringingReadyLatch);
+                ringtoneConsumer.accept(new Pair(ringtoneUri, ringtone), hasStopped);
             }
         } finally {
             Log.cancelSubsession(session);
@@ -196,11 +302,15 @@
         }
     }
 
+    /**
+     * @return true if we are currently preparing or playing a ringtone, false if we are not.
+     */
     public boolean isPlaying() {
-        return mRingtone != null;
+        return mIsPlaying;
     }
 
     private void setRingtone(@Nullable Ringtone ringtone) {
+        Log.i(this, "setRingtone: ringtone null="  + (ringtone == null));
         // Make sure that any previously created instance of Ringtone is stopped so the MediaPlayer
         // can be released, before replacing mRingtone with a new instance. This is always created
         // as a looping Ringtone, so if not stopped it will keep playing on the background.
diff --git a/src/com/android/server/telecom/AudioRoute.java b/src/com/android/server/telecom/AudioRoute.java
new file mode 100644
index 0000000..8a5e858
--- /dev/null
+++ b/src/com/android/server/telecom/AudioRoute.java
@@ -0,0 +1,396 @@
+/*
+ * 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.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothStatusCodes;
+import android.media.AudioDeviceInfo;
+import android.media.AudioManager;
+import android.telecom.Log;
+import android.util.Pair;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.telecom.bluetooth.BluetoothRouteManager;
+
+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.RejectedExecutionException;
+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 CompletableFuture<AudioRoute> mAudioRouteFuture;
+        public AudioRoute create(@AudioRouteType int type, String bluetoothAddress,
+                                 AudioManager audioManager) throws RuntimeException {
+            mAudioRouteFuture = new CompletableFuture();
+            createRetry(type, bluetoothAddress, audioManager, MAX_CONNECTION_RETRIES);
+            try {
+                return mAudioRouteFuture.get();
+            } 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) {
+            // Early exit if exceeded max number of retries (and complete the future).
+            if (retryCount == 0) {
+                mAudioRouteFuture.complete(null);
+                return;
+            }
+
+            Log.i(this, "creating AudioRoute with type %s and address %s, retry count %d",
+                    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;
+                    }
+                }
+            }
+            // Try connecting BT device anyway (to handle wearables not showing as available
+            // communication device or LE device not showing up since it may not be the lead
+            // device).
+            if (routeInfo == null && bluetoothAddress == null) {
+                try {
+                    mScheduledExecutorService.schedule(
+                            () -> createRetry(type, bluetoothAddress, audioManager, retryCount - 1),
+                            RETRY_TIME_DELAY, TimeUnit.MILLISECONDS);
+                } catch (RejectedExecutionException e) {
+                    Log.e(this, e, "Could not schedule retry for audio routing.");
+                }
+            } else {
+                mAudioRouteFuture.complete(new AudioRoute(type, bluetoothAddress, routeInfo));
+            }
+        }
+    }
+
+    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;
+    public static final int TYPE_STREAMING = 8;
+    @IntDef(prefix = "TYPE", value = {
+            TYPE_INVALID,
+            TYPE_EARPIECE,
+            TYPE_WIRED,
+            TYPE_SPEAKER,
+            TYPE_DOCK,
+            TYPE_BLUETOOTH_SCO,
+            TYPE_BLUETOOTH_HA,
+            TYPE_BLUETOOTH_LE,
+            TYPE_STREAMING
+    })
+    @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");
+        DEVICE_TYPE_STRINGS.put(TYPE_STREAMING, "TYPE_STREAMING");
+    }
+
+    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);
+    }
+
+    public 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,
+            BluetoothDevice device, AudioManager audioManager,
+            BluetoothRouteManager bluetoothRouteManager, boolean isScoAudioConnected) {
+        Log.i(this, "onDestRouteAsPendingRoute: active (%b), type (%d)", active, mAudioRouteType);
+        if (pendingAudioRoute.isActive() && !active) {
+            clearCommunicationDevice(pendingAudioRoute, bluetoothRouteManager, audioManager);
+        } else if (active) {
+            // Handle BT routing case.
+            if (BT_AUDIO_ROUTE_TYPES.contains(mAudioRouteType)) {
+                boolean connectedBtAudio = connectBtAudio(pendingAudioRoute, device,
+                        audioManager, bluetoothRouteManager);
+                // Special handling for SCO case.
+                if (mAudioRouteType == TYPE_BLUETOOTH_SCO) {
+                    // Check if the communication device was set for the device, even if
+                    // BluetoothHeadset#connectAudio reports that the SCO connection wasn't
+                    // successfully established.
+                    if (connectedBtAudio || isScoAudioConnected) {
+                        pendingAudioRoute.setCommunicationDeviceType(mAudioRouteType);
+                        if (!isScoAudioConnected) {
+                            pendingAudioRoute.addMessage(BT_AUDIO_CONNECTED, mBluetoothAddress);
+                        }
+                    } else {
+                        pendingAudioRoute.onMessageReceived(new Pair<>(PENDING_ROUTE_FAILED,
+                                mBluetoothAddress), mBluetoothAddress);
+                    }
+                    return;
+                }
+            } else if (mAudioRouteType == TYPE_SPEAKER) {
+                pendingAudioRoute.addMessage(SPEAKER_ON, null);
+            }
+
+            boolean result = false;
+            List<AudioDeviceInfo> devices = audioManager.getAvailableCommunicationDevices();
+            for (AudioDeviceInfo deviceInfo : devices) {
+                // It's possible for the AudioDeviceInfo to be updated for the BT device so adjust
+                // mInfo accordingly.
+                if (BT_AUDIO_ROUTE_TYPES.contains(mAudioRouteType) && mBluetoothAddress
+                        .equals(deviceInfo.getAddress())) {
+                    mInfo = deviceInfo;
+                }
+                if (deviceInfo.equals(mInfo)) {
+                    result = audioManager.setCommunicationDevice(mInfo);
+                    if (result) {
+                        pendingAudioRoute.setCommunicationDeviceType(mAudioRouteType);
+                    }
+                    Log.i(this, "Result of setting communication device for audio "
+                            + "route (%s) - %b", this, result);
+                    break;
+                }
+            }
+
+            // It's possible that BluetoothStateReceiver needs to report that the device is active
+            // before being able to successfully set the communication device. Refrain from sending
+            // pending route failed message for BT route until the second attempt fails.
+            if (!result && !BT_AUDIO_ROUTE_TYPES.contains(mAudioRouteType)) {
+                pendingAudioRoute.onMessageReceived(new Pair<>(PENDING_ROUTE_FAILED, null), null);
+            }
+        }
+    }
+
+    // Takes care of cleaning up original audio route (i.e. clearCommunicationDevice,
+    // sending SPEAKER_OFF, or disconnecting SCO).
+    void onOrigRouteAsPendingRoute(boolean active, PendingAudioRoute pendingAudioRoute,
+            AudioManager audioManager, BluetoothRouteManager bluetoothRouteManager) {
+        Log.i(this, "onOrigRouteAsPendingRoute: active (%b), type (%d)", active, mAudioRouteType);
+        if (active) {
+            if (mAudioRouteType == TYPE_SPEAKER) {
+                pendingAudioRoute.addMessage(SPEAKER_OFF, null);
+            }
+            int result = clearCommunicationDevice(pendingAudioRoute, bluetoothRouteManager,
+                    audioManager);
+            // Only send BT_AUDIO_DISCONNECTED for SCO if disconnect was successful.
+            if (mAudioRouteType == TYPE_BLUETOOTH_SCO && result == BluetoothStatusCodes.SUCCESS) {
+                pendingAudioRoute.addMessage(BT_AUDIO_DISCONNECTED, mBluetoothAddress);
+            }
+        }
+    }
+
+    @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")
+                + "]";
+    }
+
+    private boolean connectBtAudio(PendingAudioRoute pendingAudioRoute, BluetoothDevice device,
+            AudioManager audioManager, BluetoothRouteManager bluetoothRouteManager) {
+        // Ensure that if another BT device was set, it is disconnected before connecting
+        // the new one.
+        AudioRoute currentRoute = pendingAudioRoute.getOrigRoute();
+        if (currentRoute.getBluetoothAddress() != null &&
+                !currentRoute.getBluetoothAddress().equals(device.getAddress())) {
+            clearCommunicationDevice(pendingAudioRoute, bluetoothRouteManager, audioManager);
+        }
+
+        // Connect to the device (explicit handling for HFP devices).
+        boolean success = false;
+        if (device != null) {
+            success = bluetoothRouteManager.getDeviceManager()
+                    .connectAudio(device, mAudioRouteType);
+        }
+
+        Log.i(this, "connectBtAudio: routeToConnectTo = %s, successful = %b",
+                this, success);
+        return success;
+    }
+
+    int clearCommunicationDevice(PendingAudioRoute pendingAudioRoute,
+            BluetoothRouteManager bluetoothRouteManager, AudioManager audioManager) {
+        // Try to see if there's a previously set device for communication that should be cleared.
+        // This only serves to help in the SCO case to ensure that we disconnect the headset.
+        if (pendingAudioRoute.getCommunicationDeviceType() == AudioRoute.TYPE_INVALID) {
+            return -1;
+        }
+
+        int result = BluetoothStatusCodes.SUCCESS;
+        if (pendingAudioRoute.getCommunicationDeviceType() == TYPE_BLUETOOTH_SCO) {
+            Log.i(this, "Disconnecting SCO device.");
+            result = bluetoothRouteManager.getDeviceManager().disconnectSco();
+        } else {
+            Log.i(this, "Clearing communication device for audio type %d.",
+                    pendingAudioRoute.getCommunicationDeviceType());
+            audioManager.clearCommunicationDevice();
+        }
+
+        if (result == BluetoothStatusCodes.SUCCESS) {
+            pendingAudioRoute.setCommunicationDeviceType(AudioRoute.TYPE_INVALID);
+        }
+        return result;
+    }
+}
diff --git a/src/com/android/server/telecom/CachedAvailableEndpointsChange.java b/src/com/android/server/telecom/CachedAvailableEndpointsChange.java
new file mode 100644
index 0000000..232f00d
--- /dev/null
+++ b/src/com/android/server/telecom/CachedAvailableEndpointsChange.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2024 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 android.telecom.CallEndpoint;
+
+import java.util.Objects;
+import java.util.Set;
+
+public class CachedAvailableEndpointsChange implements CachedCallback {
+    public static final String ID = CachedAvailableEndpointsChange.class.getSimpleName();
+    Set<CallEndpoint> mAvailableEndpoints;
+
+    public Set<CallEndpoint> getAvailableEndpoints() {
+        return mAvailableEndpoints;
+    }
+
+    public CachedAvailableEndpointsChange(Set<CallEndpoint> endpoints) {
+        mAvailableEndpoints = endpoints;
+    }
+
+    @Override
+    public void executeCallback(CallSourceService service, Call call) {
+        service.onAvailableCallEndpointsChanged(call, mAvailableEndpoints);
+    }
+
+    @Override
+    public String getCallbackId() {
+        return ID;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(mAvailableEndpoints);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (!(obj instanceof CachedAvailableEndpointsChange other)) {
+            return false;
+        }
+        if (mAvailableEndpoints.size() != other.mAvailableEndpoints.size()) {
+            return false;
+        }
+        for (CallEndpoint e : mAvailableEndpoints) {
+            if (!other.getAvailableEndpoints().contains(e)) {
+                return false;
+            }
+        }
+        return true;
+    }
+}
+
diff --git a/src/com/android/server/telecom/CachedCallback.java b/src/com/android/server/telecom/CachedCallback.java
new file mode 100644
index 0000000..88dad07
--- /dev/null
+++ b/src/com/android/server/telecom/CachedCallback.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2024 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;
+
+/**
+ * Any android.telecom.Call service (e.g. ConnectionService, TransactionalService) that declares
+ * a {@link CallSourceService} should implement this interface in order to cache the callback.
+ * The callback will be executed once the service is set.
+ */
+public interface CachedCallback {
+    /**
+     * This method executes the callback that was cached because the service was not available
+     * at the time the callback was ready.
+     *
+     * @param service that was recently set (e.g. ConnectionService)
+     * @param call    that had a null service at the time the callback was ready. The service is now
+     *                non-null in the call and can be executed/
+     */
+    void executeCallback(CallSourceService service, Call call);
+
+    /**
+     * This method is helpful for caching the callbacks.  If the callback is called multiple times
+     * while the service is not set, ONLY the last callback should be sent to the client since the
+     * last callback is the most relevant
+     *
+     * @return the callback id that is used in a map to only store the last callback value
+     */
+    String getCallbackId();
+}
diff --git a/src/com/android/server/telecom/CachedCurrentEndpointChange.java b/src/com/android/server/telecom/CachedCurrentEndpointChange.java
new file mode 100644
index 0000000..0d5bac9
--- /dev/null
+++ b/src/com/android/server/telecom/CachedCurrentEndpointChange.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2024 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 android.telecom.CallEndpoint;
+
+import java.util.Objects;
+
+public class CachedCurrentEndpointChange implements CachedCallback {
+    public static final String ID = CachedCurrentEndpointChange.class.getSimpleName();
+    CallEndpoint mCurrentCallEndpoint;
+
+    public CallEndpoint getCurrentCallEndpoint() {
+        return mCurrentCallEndpoint;
+    }
+
+    public CachedCurrentEndpointChange(CallEndpoint callEndpoint) {
+        mCurrentCallEndpoint = callEndpoint;
+    }
+
+    @Override
+    public void executeCallback(CallSourceService service, Call call) {
+        service.onCallEndpointChanged(call, mCurrentCallEndpoint);
+    }
+
+    @Override
+    public String getCallbackId() {
+        return ID;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(mCurrentCallEndpoint);
+    }
+
+    @Override
+    public boolean equals(Object obj){
+        if (obj == null) {
+            return false;
+        }
+        if (!(obj instanceof CachedCurrentEndpointChange other)) {
+            return false;
+        }
+        return mCurrentCallEndpoint.equals(other.mCurrentCallEndpoint);
+    }
+}
+
diff --git a/src/com/android/server/telecom/CachedMuteStateChange.java b/src/com/android/server/telecom/CachedMuteStateChange.java
new file mode 100644
index 0000000..45cbfaa
--- /dev/null
+++ b/src/com/android/server/telecom/CachedMuteStateChange.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2024 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;
+
+public class CachedMuteStateChange implements CachedCallback {
+    public static final String ID = CachedMuteStateChange.class.getSimpleName();
+    boolean mIsMuted;
+
+    public boolean isMuted() {
+        return mIsMuted;
+    }
+
+    public CachedMuteStateChange(boolean isMuted) {
+        mIsMuted = isMuted;
+    }
+
+    @Override
+    public void executeCallback(CallSourceService service, Call call) {
+        service.onMuteStateChanged(call, mIsMuted);
+    }
+
+    @Override
+    public String getCallbackId() {
+        return ID;
+    }
+
+    @Override
+    public int hashCode() {
+        return Boolean.hashCode(mIsMuted);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (!(obj instanceof CachedMuteStateChange other)) {
+            return false;
+        }
+        return mIsMuted == other.mIsMuted;
+    }
+}
+
diff --git a/src/com/android/server/telecom/CachedVideoStateChange.java b/src/com/android/server/telecom/CachedVideoStateChange.java
new file mode 100644
index 0000000..0892c33
--- /dev/null
+++ b/src/com/android/server/telecom/CachedVideoStateChange.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2024 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.voip.VideoStateTranslation.TransactionalVideoStateToString;
+
+import android.telecom.Log;
+
+public class CachedVideoStateChange implements CachedCallback {
+    public static final String ID = CachedVideoStateChange.class.getSimpleName();
+    int mCurrentVideoState;
+
+    public int getCurrentCallEndpoint() {
+        return mCurrentVideoState;
+    }
+
+    public CachedVideoStateChange(int videoState) {
+        mCurrentVideoState = videoState;
+    }
+
+    @Override
+    public void executeCallback(CallSourceService service, Call call) {
+        service.onVideoStateChanged(call, mCurrentVideoState);
+        Log.addEvent(call, LogUtils.Events.VIDEO_STATE_CHANGED,
+                TransactionalVideoStateToString(mCurrentVideoState));
+    }
+
+    @Override
+    public String getCallbackId() {
+        return ID;
+    }
+
+    @Override
+    public int hashCode() {
+        return mCurrentVideoState;
+    }
+
+    @Override
+    public boolean equals(Object obj){
+        if (obj == null) {
+            return false;
+        }
+        if (!(obj instanceof CachedVideoStateChange other)) {
+            return false;
+        }
+        return mCurrentVideoState == other.mCurrentVideoState;
+    }
+}
+
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index dd8e7e8..760028d 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -17,21 +17,24 @@
 package com.android.server.telecom;
 
 import static android.provider.CallLog.Calls.MISSED_REASON_NOT_MISSED;
-import static android.telecom.Call.EVENT_DISPLAY_SOS_MESSAGE;
+import static android.telephony.TelephonyManager.EVENT_DISPLAY_EMERGENCY_MESSAGE;
+
+import static com.android.server.telecom.voip.VideoStateTranslation.TransactionalVideoStateToString;
+import static com.android.server.telecom.voip.VideoStateTranslation.VideoProfileStateToTransactionalVideoState;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.PackageManager;
 import android.graphics.Bitmap;
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
-import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Looper;
+import android.os.OutcomeReceiver;
 import android.os.ParcelFileDescriptor;
-import android.os.Parcelable;
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.os.Trace;
@@ -43,6 +46,7 @@
 import android.telecom.CallAudioState;
 import android.telecom.CallDiagnosticService;
 import android.telecom.CallDiagnostics;
+import android.telecom.CallException;
 import android.telecom.CallerInfo;
 import android.telecom.Conference;
 import android.telecom.Connection;
@@ -55,7 +59,6 @@
 import android.telecom.ParcelableConnection;
 import android.telecom.PhoneAccount;
 import android.telecom.PhoneAccountHandle;
-import android.telecom.Response;
 import android.telecom.StatusHints;
 import android.telecom.TelecomManager;
 import android.telecom.VideoProfile;
@@ -70,9 +73,13 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.telecom.IVideoProvider;
 import com.android.internal.util.Preconditions;
+import com.android.server.telecom.flags.FeatureFlags;
 import com.android.server.telecom.stats.CallFailureCause;
 import com.android.server.telecom.stats.CallStateChangedAtomWriter;
 import com.android.server.telecom.ui.ToastFactory;
+import com.android.server.telecom.voip.TransactionManager;
+import com.android.server.telecom.voip.VerifyCallStateChangeTransaction;
+import com.android.server.telecom.voip.VoipCallTransactionResult;
 
 import java.io.IOException;
 import java.text.SimpleDateFormat;
@@ -80,6 +87,7 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Date;
+import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Locale;
@@ -88,6 +96,7 @@
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
@@ -118,6 +127,24 @@
 
     private static final char NO_DTMF_TONE = '\0';
 
+
+    /**
+     * Listener for CallState changes which can be leveraged by a Transaction.
+     */
+    public interface CallStateListener {
+        void onCallStateChanged(int newCallState);
+    }
+
+    public List<CallStateListener> mCallStateListeners = new ArrayList<>();
+
+    public void addCallStateListener(CallStateListener newListener) {
+        mCallStateListeners.add(newListener);
+    }
+
+    public boolean removeCallStateListener(CallStateListener newListener) {
+        return mCallStateListeners.remove(newListener);
+    }
+
     /**
      * Listener for events on the call.
      */
@@ -283,18 +310,25 @@
                 @Override
                 public void onCallerInfoQueryComplete(Uri handle, CallerInfo callerInfo) {
                     synchronized (mLock) {
-                        Call.this.setCallerInfo(handle, callerInfo);
+                        Call call = Call.this;
+                        if (call != null) {
+                            call.setCallerInfo(handle, callerInfo);
+                        }
                     }
                 }
 
                 @Override
                 public void onContactPhotoQueryComplete(Uri handle, CallerInfo callerInfo) {
                     synchronized (mLock) {
-                        Call.this.setCallerInfo(handle, callerInfo);
+                        Call call = Call.this;
+                        if (call != null) {
+                            call.setCallerInfo(handle, callerInfo);
+                        }
                     }
                 }
             };
 
+    private final boolean mIsModifyStatePermissionGranted;
     /**
      * One of CALL_DIRECTION_INCOMING, CALL_DIRECTION_OUTGOING, or CALL_DIRECTION_UNKNOWN
      */
@@ -406,6 +440,13 @@
     private int mCallerDisplayNamePresentation;
 
     /**
+     * The remote connection service which is attempted or already connecting this call. This is set
+     * to a non-null value only when a connection manager phone account is in use. When set, this
+     * will correspond to the target phone account of the {@link Call}.
+     */
+    private ConnectionServiceWrapper mRemoteConnectionService;
+
+    /**
      * The connection service which is attempted or already connecting this call.
      */
     private ConnectionServiceWrapper mConnectionService;
@@ -615,6 +656,36 @@
     private boolean mIsVideoCallingSupportedByPhoneAccount = false;
 
     /**
+     * Indicates whether this individual calls video state can be changed as opposed to be gated
+     * by the {@link PhoneAccount}.
+     *
+     * {@code True} if the call is Transactional && has the CallAttributes.SUPPORTS_VIDEO_CALLING
+     * capability {@code false} otherwise.
+     */
+    private boolean mTransactionalCallSupportsVideoCalling = false;
+
+    public void setTransactionalCallSupportsVideoCalling(CallAttributes callAttributes) {
+        if (!mIsTransactionalCall) {
+            Log.i(this, "setTransactionalCallSupportsVideoCalling: call is not transactional");
+            return;
+        }
+        if (callAttributes == null) {
+            Log.i(this, "setTransactionalCallSupportsVideoCalling: callAttributes is null");
+            return;
+        }
+        if ((callAttributes.getCallCapabilities() & CallAttributes.SUPPORTS_VIDEO_CALLING)
+                == CallAttributes.SUPPORTS_VIDEO_CALLING) {
+            mTransactionalCallSupportsVideoCalling = true;
+        } else {
+            mTransactionalCallSupportsVideoCalling = false;
+        }
+    }
+
+    public boolean isTransactionalCallSupportsVideoCalling() {
+        return mTransactionalCallSupportsVideoCalling;
+    }
+
+    /**
      * Indicates whether or not this call can be pulled if it is an external call. If true, respect
      * the Connection Capability set by the ConnectionService. If false, override the capability
      * set and always remove the ability to pull this external call.
@@ -755,7 +826,41 @@
      * disconnect message via {@link CallDiagnostics#onCallDisconnected(ImsReasonInfo)} or
      * {@link CallDiagnostics#onCallDisconnected(int, int)}.
      */
-    private CompletableFuture<Boolean> mDisconnectFuture;
+    private CompletableFuture<Boolean> mDiagnosticCompleteFuture;
+
+    /**
+     * {@link CompletableFuture} used to perform disconnect operations after
+     * {@link #mDiagnosticCompleteFuture} has completed.
+     */
+    private CompletableFuture<Void> mDisconnectFuture;
+
+    /**
+     * {@link CompletableFuture} used to perform call removal operations after the
+     * {@link #mDisconnectFuture} has completed.
+     * <p>
+     * Note: It is possible for this future to be cancelled in the case that an internal operation
+     * will be handling clean up. (See {@link #setState}.)
+     */
+    private CompletableFuture<Void> mRemovalFuture;
+
+    /**
+     * {@link CompletableFuture} used to delay audio routing change for a ringing call until the
+     * corresponding bluetooth {@link android.telecom.InCallService} is successfully bound or timed
+     * out.
+     */
+    private CompletableFuture<Boolean> mBtIcsFuture;
+
+    Map<String, CachedCallback> mCachedServiceCallbacks = new HashMap<>();
+
+    public void cacheServiceCallback(CachedCallback callback) {
+        mCachedServiceCallbacks.put(callback.getCallbackId(), callback);
+    }
+
+    public Map<String, CachedCallback> getCachedServiceCallbacks() {
+        return mCachedServiceCallbacks;
+    }
+
+    private FeatureFlags mFlags;
 
     /**
      * Persists the specified parameters and initializes the new instance.
@@ -788,11 +893,12 @@
             boolean shouldAttachToExistingConnection,
             boolean isConference,
             ClockProxy clockProxy,
-            ToastFactory toastFactory) {
+            ToastFactory toastFactory,
+            FeatureFlags featureFlags) {
         this(callId, context, callsManager, lock, repository, phoneNumberUtilsAdapter,
                handle, null, gatewayInfo, connectionManagerPhoneAccountHandle,
                targetPhoneAccountHandle, callDirection, shouldAttachToExistingConnection,
-               isConference, clockProxy, toastFactory);
+               isConference, clockProxy, toastFactory, featureFlags);
 
     }
 
@@ -812,8 +918,9 @@
             boolean shouldAttachToExistingConnection,
             boolean isConference,
             ClockProxy clockProxy,
-            ToastFactory toastFactory) {
-
+            ToastFactory toastFactory,
+            FeatureFlags featureFlags) {
+        mFlags = featureFlags;
         mId = callId;
         mConnectionId = callId;
         mState = (isConference && callDirection != CALL_DIRECTION_INCOMING &&
@@ -844,6 +951,8 @@
         mStartRingTime = 0;
 
         mCallStateChangedAtomWriter.setExistingCallCount(callsManager.getCalls().size());
+        mIsModifyStatePermissionGranted =
+                isModifyPhoneStatePermissionGranted(getDelegatePhoneAccountHandle());
     }
 
     /**
@@ -863,6 +972,7 @@
      * connection, regardless of whether it's incoming or outgoing.
      * @param connectTimeMillis The connection time of the call.
      * @param clockProxy
+     * @param featureFlags The telecom feature flags.
      */
     Call(
             String callId,
@@ -881,11 +991,13 @@
             long connectTimeMillis,
             long connectElapsedTimeMillis,
             ClockProxy clockProxy,
-            ToastFactory toastFactory) {
+            ToastFactory toastFactory,
+            FeatureFlags featureFlags) {
         this(callId, context, callsManager, lock, repository,
                 phoneNumberUtilsAdapter, handle, gatewayInfo,
                 connectionManagerPhoneAccountHandle, targetPhoneAccountHandle, callDirection,
-                shouldAttachToExistingConnection, isConference, clockProxy, toastFactory);
+                shouldAttachToExistingConnection, isConference, clockProxy, toastFactory,
+                featureFlags);
 
         mConnectTimeMillis = connectTimeMillis;
         mConnectElapsedTimeMillis = connectElapsedTimeMillis;
@@ -1219,7 +1331,7 @@
                         message, null));
             }
 
-            mDisconnectFuture.complete(true);
+            mDiagnosticCompleteFuture.complete(true);
         } else {
             Log.w(this, "handleOverrideDisconnectMessage; callid=%s - got override when unbound",
                     getId());
@@ -1241,6 +1353,12 @@
 
             if (newState == CallState.DISCONNECTED && shouldContinueProcessingAfterDisconnect()) {
                 Log.w(this, "continuing processing disconnected call with another service");
+                if (mFlags.cancelRemovalOnEmergencyRedial() && isDisconnectHandledViaFuture()
+                        && isRemovalPending()) {
+                    Log.i(this, "cancelling removal future in favor of "
+                            + "CreateConnectionProcessor handling removal");
+                    mRemovalFuture.cancel(true);
+                }
                 mCreateConnectionProcessor.continueProcessingIfPossible(this, mDisconnectCause);
                 return false;
             } else if (newState == CallState.ANSWERED && mState == CallState.ACTIVE) {
@@ -1328,6 +1446,12 @@
                 Log.addEvent(this, event, stringData);
             }
 
+            if (mFlags.transactionalCsVerifier()) {
+                for (CallStateListener listener : mCallStateListeners) {
+                    listener.onCallStateChanged(newState);
+                }
+            }
+
             mCallStateChangedAtomWriter
                     .setDisconnectCause(getDisconnectCause())
                     .setSelfManaged(isSelfManaged())
@@ -1471,6 +1595,9 @@
                     mIsEmergencyCall = mHandle != null &&
                             getTelephonyManager().isEmergencyNumber(
                                     mHandle.getSchemeSpecificPart());
+                } catch (UnsupportedOperationException use) {
+                    Log.i(this, "setHandle: no FEATURE_TELEPHONY; emergency state unknown.");
+                    mIsEmergencyCall = false;
                 } catch (IllegalStateException ise) {
                     Log.e(this, ise, "setHandle: can't determine if number is emergency");
                     mIsEmergencyCall = false;
@@ -1484,7 +1611,11 @@
                 mIsTestEmergencyCall = mHandle != null &&
                         isTestEmergencyCall(mHandle.getSchemeSpecificPart());
             }
-            startCallerInfoLookup();
+            if (!mContext.getResources().getBoolean(R.bool.skip_incoming_caller_info_query)) {
+                startCallerInfoLookup();
+            } else  {
+                Log.i(this, "skip incoming caller info lookup");
+            }
             for (Listener l : mListeners) {
                 l.onHandleChanged(this);
             }
@@ -1499,6 +1630,9 @@
                     .anyMatch(eNumber ->
                             eNumber.isFromSources(EmergencyNumber.EMERGENCY_NUMBER_SOURCE_TEST) &&
                                     number.equals(eNumber.getNumber()));
+        } catch (UnsupportedOperationException uoe) {
+            // No Telephony feature, so unable to determine.
+            return false;
         } catch (IllegalStateException ise) {
             return false;
         } catch (RuntimeException r) {
@@ -1733,8 +1867,12 @@
                     accountHandle.getComponentName().getPackageName(),
                     mContext.getPackageManager());
             // Set the associated user for the call for MT calls based on the target phone account.
-            if (isIncoming() && !accountHandle.getUserHandle().equals(mAssociatedUser)) {
-                setAssociatedUser(accountHandle.getUserHandle());
+            UserHandle associatedUser = UserUtil.getAssociatedUserForCall(
+                    mFlags.associatedUserRefactorForWorkProfile(),
+                    mCallsManager.getPhoneAccountRegistrar(), mCallsManager.getCurrentUserHandle(),
+                    accountHandle);
+            if (isIncoming() && !associatedUser.equals(mAssociatedUser)) {
+                setAssociatedUser(associatedUser);
             }
         }
     }
@@ -1906,7 +2044,27 @@
     }
 
     public void setTransactionServiceWrapper(TransactionalServiceWrapper service) {
+        Log.i(this, "setTransactionServiceWrapper: service=[%s]", service);
         mTransactionalService = service;
+        processCachedCallbacks(service);
+    }
+
+    private void processCachedCallbacks(CallSourceService service) {
+        if(mFlags.cacheCallAudioCallbacks()) {
+            for (CachedCallback callback : mCachedServiceCallbacks.values()) {
+                callback.executeCallback(service, this);
+            }
+            // clear list for memory cleanup purposes. The Service should never be reset
+            mCachedServiceCallbacks.clear();
+        }
+    }
+
+    public CallSourceService getService() {
+        if (isTransactionalCall()) {
+            return mTransactionalService;
+        } else {
+            return mConnectionService;
+        }
     }
 
     public TransactionalServiceWrapper getTransactionServiceWrapper() {
@@ -1988,7 +2146,7 @@
                 userHandle = mTargetPhoneAccountHandle.getUserHandle();
             }
             if (userHandle != null) {
-                isWorkCall = UserUtil.isManagedProfile(mContext, userHandle);
+                isWorkCall = UserUtil.isManagedProfile(mContext, userHandle, mFlags);
             }
 
             isCallRecordingToneSupported = (phoneAccount.hasCapabilities(
@@ -2313,22 +2471,40 @@
 
     @VisibleForTesting
     public void setConnectionService(ConnectionServiceWrapper service) {
+        Log.i(this, "setConnectionService: service=[%s]", service);
+        setConnectionService(service, null);
+    }
+
+    @VisibleForTesting
+    public void setConnectionService(
+            ConnectionServiceWrapper service,
+            ConnectionServiceWrapper remoteService
+    ) {
         Preconditions.checkNotNull(service);
 
         clearConnectionService();
 
         service.incrementAssociatedCallCount();
+
+        if (mFlags.updatedRcsCallCountTracking() && remoteService != null) {
+            remoteService.incrementAssociatedCallCount();
+            mRemoteConnectionService = remoteService;
+        }
+
         mConnectionService = service;
         mAnalytics.setCallConnectionService(service.getComponentName().flattenToShortString());
         mConnectionService.addCall(this);
+        processCachedCallbacks(service);
     }
 
     /**
      * Perform an in-place replacement of the {@link ConnectionServiceWrapper} for this Call.
-     * Removes the call from its former {@link ConnectionServiceWrapper}, ensuring that the
-     * ConnectionService is NOT unbound if the call count hits zero.
-     * This is used by the {@link ConnectionServiceWrapper} when handling {@link Connection} and
-     * {@link Conference} additions via a ConnectionManager.
+     * Removes the call from its former {@link ConnectionServiceWrapper}, while still ensuring the
+     * former {@link ConnectionServiceWrapper} is tracked as the mRemoteConnectionService for this
+     * call so that the associatedCallCount of that {@link ConnectionServiceWrapper} is accurately
+     * tracked until it is supposed to be unbound.
+     * This method is used by the {@link ConnectionServiceWrapper} when handling {@link Connection}
+     * and {@link Conference} additions via a ConnectionManager.
      * The original {@link android.telecom.ConnectionService} will directly add external calls and
      * conferences to Telecom as well as the ConnectionManager, which will add to Telecom.  In these
      * cases since its first added to via the original CS, we want to change the CS responsible for
@@ -2341,9 +2517,18 @@
 
         if (mConnectionService != null) {
             ConnectionServiceWrapper serviceTemp = mConnectionService;
+
+            if (mFlags.updatedRcsCallCountTracking()) {
+                // Continue to track the former CS for this call so that it doesn't unbind early:
+                mRemoteConnectionService = serviceTemp;
+            }
+
             mConnectionService = null;
             serviceTemp.removeCall(this);
-            serviceTemp.decrementAssociatedCallCount(true /*isSuppressingUnbind*/);
+
+            if (!mFlags.updatedRcsCallCountTracking()) {
+                serviceTemp.decrementAssociatedCallCount(true /*isSuppressingUnbind*/);
+            }
         }
 
         service.incrementAssociatedCallCount();
@@ -2357,6 +2542,8 @@
     void clearConnectionService() {
         if (mConnectionService != null) {
             ConnectionServiceWrapper serviceTemp = mConnectionService;
+            ConnectionServiceWrapper remoteServiceTemp = mRemoteConnectionService;
+            mRemoteConnectionService = null;
             mConnectionService = null;
             serviceTemp.removeCall(this);
 
@@ -2367,6 +2554,10 @@
             // necessary, but cleaning up mConnectionService prior to triggering an unbind is good
             // to do.
             decrementAssociatedCallCount(serviceTemp);
+
+            if (mFlags.updatedRcsCallCountTracking() && remoteServiceTemp != null) {
+                decrementAssociatedCallCount(remoteServiceTemp);
+            }
         }
     }
 
@@ -2385,7 +2576,7 @@
             return;
         }
         mCreateConnectionProcessor = new CreateConnectionProcessor(this, mRepository, this,
-                phoneAccountRegistrar, mContext);
+                phoneAccountRegistrar, mContext, mFlags, new Timeouts.Adapter());
         mCreateConnectionProcessor.process();
     }
 
@@ -2898,11 +3089,19 @@
         hold(null /* reason */);
     }
 
+    /**
+     * This method requests the ConnectionService or TransactionalService hosting the call to put
+     * the call on hold
+     */
     public void hold(String reason) {
         if (mState == CallState.ACTIVE) {
             if (mTransactionalService != null) {
                 mTransactionalService.onSetInactive(this);
             } else if (mConnectionService != null) {
+                if (mFlags.transactionalCsVerifier()) {
+                    awaitCallStateChangeAndMaybeDisconnectCall(CallState.ON_HOLD, isSelfManaged(),
+                            "hold");
+                }
                 mConnectionService.hold(this);
             } else {
                 Log.e(this, new NullPointerException(),
@@ -2913,6 +3112,35 @@
     }
 
     /**
+     * helper that can be used for any callback that requests a call state change and wants to
+     * verify the change
+     */
+    public void awaitCallStateChangeAndMaybeDisconnectCall(int targetCallState,
+            boolean shouldDisconnectUponTimeout, String callingMethod) {
+        TransactionManager tm = TransactionManager.getInstance();
+        tm.addTransaction(new VerifyCallStateChangeTransaction(mCallsManager.getLock(),
+                this, targetCallState), new OutcomeReceiver<>() {
+            @Override
+            public void onResult(VoipCallTransactionResult result) {
+                Log.i(this, "awaitCallStateChangeAndMaybeDisconnectCall: %s: onResult:"
+                        + " due to CallException=[%s]", callingMethod, result);
+            }
+
+            @Override
+            public void onError(CallException e) {
+                Log.i(this, "awaitCallStateChangeAndMaybeDisconnectCall: %s: onError"
+                        + " due to CallException=[%s]", callingMethod, e);
+                if (shouldDisconnectUponTimeout) {
+                    mCallsManager.markCallAsDisconnected(Call.this,
+                            new DisconnectCause(DisconnectCause.ERROR,
+                                    "did not hold in timeout window"));
+                    mCallsManager.markCallAsRemoved(Call.this);
+                }
+            }
+        });
+    }
+
+    /**
      * Releases the call from hold if it is currently active.
      */
     @VisibleForTesting
@@ -3039,6 +3267,12 @@
                     Connection.EXTRA_REMOTE_PHONE_ACCOUNT_HANDLE));
         }
 
+        if (mExtras.containsKey(TelecomManager.EXTRA_DO_NOT_LOG_CALL)) {
+            if (source != SOURCE_CONNECTION_SERVICE || !mIsModifyStatePermissionGranted) {
+                mExtras.remove(TelecomManager.EXTRA_DO_NOT_LOG_CALL);
+            }
+        }
+
         // If the change originated from an InCallService, notify the connection service.
         if (source == SOURCE_INCALL_SERVICE) {
             Log.addEvent(this, LogUtils.Events.ICS_EXTRAS_CHANGED);
@@ -3047,12 +3281,20 @@
             } else if (mConnectionService != null) {
                 mConnectionService.onExtrasChanged(this, mExtras);
             } else {
-                Log.e(this, new NullPointerException(),
-                        "putExtras failed due to null CS callId=%s", getId());
+                Log.w(this, "putExtras failed due to null CS callId=%s", getId());
             }
         }
     }
 
+    private boolean isModifyPhoneStatePermissionGranted(PhoneAccountHandle phoneAccountHandle) {
+        if (phoneAccountHandle == null) {
+            return false;
+        }
+        String packageName = phoneAccountHandle.getComponentName().getPackageName();
+        return PackageManager.PERMISSION_GRANTED == mContext.getPackageManager().checkPermission(
+                android.Manifest.permission.MODIFY_PHONE_STATE, packageName);
+    }
+
     /**
      * Removes extras from the extras bundle associated with this {@link Call}.
      *
@@ -3263,7 +3505,7 @@
             Log.w(this, "pullExternalCall = pullExternalCall - call %s is external but can not be"
                     + " pulled while an emergency call is in progress.", mId);
             mToastFactory.makeText(mContext, R.string.toast_emergency_can_not_pull_call,
-                    Toast.LENGTH_LONG).show();
+                    Toast.LENGTH_LONG);
             return;
         }
 
@@ -3293,62 +3535,16 @@
      */
     public void sendCallEvent(String event, int targetSdkVer, Bundle extras) {
         if (mConnectionService != null || mTransactionalService != null) {
-            if (android.telecom.Call.EVENT_REQUEST_HANDOVER.equals(event)) {
-                if (targetSdkVer > Build.VERSION_CODES.P) {
-                    Log.e(this, new Exception(), "sendCallEvent failed. Use public api handoverTo" +
-                            " for API > 28(P)");
-                    // Event-based Handover APIs are deprecated, so inform the user.
-                    mHandler.post(new Runnable() {
-                        @Override
-                        public void run() {
-                            mToastFactory.makeText(mContext,
-                                    "WARNING: Event-based handover APIs are deprecated and will no"
-                                            + " longer function in Android Q.",
-                                    Toast.LENGTH_LONG).show();
-                        }
-                    });
-
-                    // Uncomment and remove toast at feature complete: return;
-                }
-
-                // Handover requests are targeted at Telecom, not the ConnectionService.
-                if (extras == null) {
-                    Log.w(this, "sendCallEvent: %s event received with null extras.",
-                            android.telecom.Call.EVENT_REQUEST_HANDOVER);
-                    sendEventToService(this, android.telecom.Call.EVENT_HANDOVER_FAILED,
-                            null);
-                    return;
-                }
-                Parcelable parcelable = extras.getParcelable(
-                        android.telecom.Call.EXTRA_HANDOVER_PHONE_ACCOUNT_HANDLE);
-                if (!(parcelable instanceof PhoneAccountHandle) || parcelable == null) {
-                    Log.w(this, "sendCallEvent: %s event received with invalid handover acct.",
-                            android.telecom.Call.EVENT_REQUEST_HANDOVER);
-                    sendEventToService(this, android.telecom.Call.EVENT_HANDOVER_FAILED, null);
-                    return;
-                }
-                PhoneAccountHandle phoneAccountHandle = (PhoneAccountHandle) parcelable;
-                int videoState = extras.getInt(android.telecom.Call.EXTRA_HANDOVER_VIDEO_STATE,
-                        VideoProfile.STATE_AUDIO_ONLY);
-                Parcelable handoverExtras = extras.getParcelable(
-                        android.telecom.Call.EXTRA_HANDOVER_EXTRAS);
-                Bundle handoverExtrasBundle = null;
-                if (handoverExtras instanceof Bundle) {
-                    handoverExtrasBundle = (Bundle) handoverExtras;
-                }
-                requestHandover(phoneAccountHandle, videoState, handoverExtrasBundle, true);
-            } else {
-                // Relay bluetooth call quality reports to the call diagnostic service.
-                if (BluetoothCallQualityReport.EVENT_BLUETOOTH_CALL_QUALITY_REPORT.equals(event)
-                        && extras.containsKey(
-                        BluetoothCallQualityReport.EXTRA_BLUETOOTH_CALL_QUALITY_REPORT)) {
-                    notifyBluetoothCallQualityReport(extras.getParcelable(
-                            BluetoothCallQualityReport.EXTRA_BLUETOOTH_CALL_QUALITY_REPORT
-                    ));
-                }
-                Log.addEvent(this, LogUtils.Events.CALL_EVENT, event);
-                sendEventToService(this, event, extras);
+            // Relay bluetooth call quality reports to the call diagnostic service.
+            if (BluetoothCallQualityReport.EVENT_BLUETOOTH_CALL_QUALITY_REPORT.equals(event)
+                    && extras.containsKey(
+                    BluetoothCallQualityReport.EXTRA_BLUETOOTH_CALL_QUALITY_REPORT)) {
+                notifyBluetoothCallQualityReport(extras.getParcelable(
+                        BluetoothCallQualityReport.EXTRA_BLUETOOTH_CALL_QUALITY_REPORT
+                ));
             }
+            Log.addEvent(this, LogUtils.Events.CALL_EVENT, event);
+            sendEventToService(this, event, extras);
         } else {
             Log.e(this, new NullPointerException(),
                     "sendCallEvent failed due to null CS callId=%s", getId());
@@ -3556,6 +3752,11 @@
      * SMSes to that number will silently fail.
      */
     public boolean isRespondViaSmsCapable() {
+        if (mContext.getResources().getBoolean(R.bool.skip_loading_canned_text_response)) {
+            Log.d(this, "maybeLoadCannedSmsResponses: skip loading due to setting");
+            return false;
+        }
+
         if (mState != CallState.RINGING) {
             return false;
         }
@@ -3576,8 +3777,12 @@
         }
 
         // Is there a valid SMS application on the phone?
-        if (mContext.getSystemService(TelephonyManager.class)
-                .getAndUpdateDefaultRespondViaMessageApplication() == null) {
+        try {
+            if (mContext.getSystemService(TelephonyManager.class)
+                    .getAndUpdateDefaultRespondViaMessageApplication() == null) {
+                return false;
+            }
+        } catch (UnsupportedOperationException uoe) {
             return false;
         }
 
@@ -3663,7 +3868,8 @@
         }
 
         String newName = callerInfo.getName();
-        boolean contactNameChanged = mCallerInfo == null || !mCallerInfo.getName().equals(newName);
+        boolean contactNameChanged = mCallerInfo == null ||
+                !Objects.equals(mCallerInfo.getName(), newName);
 
         mCallerInfo = callerInfo;
         Log.i(this, "CallerInfo received for %s: %s", Log.piiHandle(mHandle), callerInfo);
@@ -3689,7 +3895,7 @@
             Log.d(this, "maybeLoadCannedSmsResponses: starting task to load messages");
             mCannedSmsResponsesLoadingStarted = true;
             mCallsManager.getRespondViaSmsManager().loadCannedTextMessages(
-                    new Response<Void, List<String>>() {
+                    new CallsManager.Response<Void, List<String>>() {
                         @Override
                         public void onResult(Void request, List<String>... result) {
                             if (result.length > 0) {
@@ -3930,6 +4136,9 @@
             videoState = VideoProfile.STATE_AUDIO_ONLY;
         }
 
+        // TODO:: b/338280297. If a transactional call does not have the
+        //   CallAttributes.SUPPORTS_VIDEO_CALLING capability, the videoState should be set to audio
+
         // Track Video State history during the duration of the call.
         // Only update the history when the call is active or disconnected. This ensures we do
         // not include the video state history when:
@@ -3945,13 +4154,26 @@
         int previousVideoState = mVideoState;
         mVideoState = videoState;
         if (mVideoState != previousVideoState) {
-            Log.addEvent(this, LogUtils.Events.VIDEO_STATE_CHANGED,
-                    VideoProfile.videoStateToString(videoState));
+            if (!mIsTransactionalCall) {
+                Log.addEvent(this, LogUtils.Events.VIDEO_STATE_CHANGED,
+                        VideoProfile.videoStateToString(videoState));
+            }
             for (Listener l : mListeners) {
                 l.onVideoStateChanged(this, previousVideoState, mVideoState);
             }
         }
 
+        if (mFlags.transactionalVideoState() && mIsTransactionalCall) {
+            int transactionalVS = VideoProfileStateToTransactionalVideoState(mVideoState);
+            if (mTransactionalService != null) {
+                Log.addEvent(this, LogUtils.Events.VIDEO_STATE_CHANGED,
+                        TransactionalVideoStateToString(transactionalVS));
+                mTransactionalService.onVideoStateChanged(this, transactionalVS);
+            } else {
+                cacheServiceCallback(new CachedVideoStateChange(transactionalVS));
+            }
+        }
+
         if (VideoProfile.isVideo(videoState)) {
             mAnalytics.setCallIsVideo(true);
         }
@@ -4032,7 +4254,7 @@
      * @param associatedUser
      */
     public void setAssociatedUser(UserHandle associatedUser) {
-        Log.i(this, "Setting associated user for call");
+        Log.i(this, "Setting associated user for call: %s", associatedUser);
         Preconditions.checkNotNull(associatedUser);
         mAssociatedUser = associatedUser;
     }
@@ -4182,8 +4404,8 @@
                 l.onReceivedCallQualityReport(this, callQuality);
             }
         } else {
-            if (event.equals(EVENT_DISPLAY_SOS_MESSAGE) && !isEmergencyCall()) {
-                Log.w(this, "onConnectionEvent: EVENT_DISPLAY_SOS_MESSAGE is sent "
+            if (event.equals(EVENT_DISPLAY_EMERGENCY_MESSAGE) && !isEmergencyCall()) {
+                Log.w(this, "onConnectionEvent: EVENT_DISPLAY_EMERGENCY_MESSAGE is sent "
                         + "without an emergency call");
                 return;
             }
@@ -4504,6 +4726,7 @@
     }
 
     public void setStartFailCause(CallFailureCause cause) {
+        Log.i(this, "setStartFailCause: cause = %s; callId = %s", cause, this.getId());
         mCallStateChangedAtomWriter.setStartFailCause(cause);
     }
 
@@ -4556,17 +4779,17 @@
      * @param timeoutMillis Timeout we use for waiting for the response.
      * @return the {@link CompletableFuture}.
      */
-    public CompletableFuture<Boolean> initializeDisconnectFuture(long timeoutMillis) {
-        if (mDisconnectFuture == null) {
-            mDisconnectFuture = new CompletableFuture<Boolean>()
+    public CompletableFuture<Boolean> initializeDiagnosticCompleteFuture(long timeoutMillis) {
+        if (mDiagnosticCompleteFuture == null) {
+            mDiagnosticCompleteFuture = new CompletableFuture<Boolean>()
                     .completeOnTimeout(false, timeoutMillis, TimeUnit.MILLISECONDS);
             // After all the chained stuff we will report where the CDS timed out.
-            mDisconnectFuture.thenRunAsync(() -> {
+            mDiagnosticCompleteFuture.thenRunAsync(() -> {
                 if (!mReceivedCallDiagnosticPostCallResponse) {
                     Log.addEvent(this, LogUtils.Events.CALL_DIAGNOSTIC_SERVICE_TIMEOUT);
                 }
                 // Clear the future as a final step.
-                mDisconnectFuture = null;
+                mDiagnosticCompleteFuture = null;
                 },
                 new LoggedHandlerExecutor(mHandler, "C.iDF", mLock))
                     .exceptionally((throwable) -> {
@@ -4574,14 +4797,14 @@
                         return null;
                     });
         }
-        return mDisconnectFuture;
+        return mDiagnosticCompleteFuture;
     }
 
     /**
      * @return the disconnect future, if initialized.  Used for chaining operations after creation.
      */
-    public CompletableFuture<Boolean> getDisconnectFuture() {
-        return mDisconnectFuture;
+    public CompletableFuture<Boolean> getDiagnosticCompleteFuture() {
+        return mDiagnosticCompleteFuture;
     }
 
     /**
@@ -4589,7 +4812,7 @@
      * if this is handled immediately.
      */
     public boolean isDisconnectHandledViaFuture() {
-        return mDisconnectFuture != null;
+        return mDiagnosticCompleteFuture != null;
     }
 
     /**
@@ -4597,9 +4820,70 @@
      * {@code cleanupStuckCalls} request.
      */
     public void cleanup() {
-        if (mDisconnectFuture != null) {
-            mDisconnectFuture.complete(false);
-            mDisconnectFuture = null;
+        if (mDiagnosticCompleteFuture != null) {
+            mDiagnosticCompleteFuture.complete(false);
+            mDiagnosticCompleteFuture = null;
+        }
+    }
+
+    /**
+     * Set the pending future to use when the call is disconnected.
+     */
+    public void setDisconnectFuture(CompletableFuture<Void> future) {
+        mDisconnectFuture = future;
+    }
+
+    /**
+     * @return The future that will be executed when the call is disconnected.
+     */
+    public CompletableFuture<Void> getDisconnectFuture() {
+        return mDisconnectFuture;
+    }
+
+    /**
+     * Set the future that will be used when call removal is taking place.
+     */
+    public void setRemovalFuture(CompletableFuture<Void> future) {
+        mRemovalFuture = future;
+    }
+
+    /**
+     * @return {@code true} if there is a pending removal operation that hasn't taken place yet, or
+     * {@code false} if there is no removal pending.
+     */
+    public boolean isRemovalPending() {
+        return mRemovalFuture != null && !mRemovalFuture.isDone();
+    }
+
+    /**
+     * Set the bluetooth {@link android.telecom.InCallService} binding completion or timeout future
+     * which is used to delay the audio routing change after the bluetooth stack get notified about
+     * the ringing calls.
+     * @param btIcsFuture the {@link CompletableFuture}
+     */
+    public void setBtIcsFuture(CompletableFuture<Boolean> btIcsFuture) {
+        mBtIcsFuture = btIcsFuture;
+    }
+
+    /**
+     * @return The binding {@link CompletableFuture} for the BT ICS.
+     */
+    public CompletableFuture<Boolean> getBtIcsFuture() {
+        return mBtIcsFuture;
+    }
+
+    /**
+     * Wait for bluetooth {@link android.telecom.InCallService} binding completion or timeout. Used
+     * for audio routing operations for a ringing call.
+     */
+    public void waitForBtIcs() {
+        if (mBtIcsFuture != null) {
+            try {
+                Log.i(this, "waitForBtIcs: waiting for BT service to bind");
+                mBtIcsFuture.get();
+            } catch (InterruptedException | ExecutionException e) {
+                // ignore
+            }
         }
     }
 
diff --git a/src/com/android/server/telecom/CallAudioCommunicationDeviceTracker.java b/src/com/android/server/telecom/CallAudioCommunicationDeviceTracker.java
new file mode 100644
index 0000000..8130685
--- /dev/null
+++ b/src/com/android/server/telecom/CallAudioCommunicationDeviceTracker.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright (C) 2022 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_DEVICE_INFO_TYPES;
+
+import android.bluetooth.BluetoothDevice;
+import android.content.Context;
+import android.media.AudioDeviceInfo;
+import android.media.AudioManager;
+import android.telecom.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.telecom.bluetooth.BluetoothRouteManager;
+import com.android.server.telecom.flags.Flags;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * Helper class used to keep track of the requested communication device within Telecom for audio
+ * use cases. Handles the set/clear communication use case logic for all audio routes (speaker, BT,
+ * headset, and earpiece). For BT devices, this handles switches between hearing aids, SCO, and LE
+ * audio (also takes into account switching between multiple LE audio devices).
+ */
+public class CallAudioCommunicationDeviceTracker {
+
+    // Use -1 indicates device is not set for any communication use case
+    private static final int sAUDIO_DEVICE_TYPE_INVALID = -1;
+    private AudioManager mAudioManager;
+    private BluetoothRouteManager mBluetoothRouteManager;
+    private int mAudioDeviceType = sAUDIO_DEVICE_TYPE_INVALID;
+    // Keep track of the locally requested BT audio device if set
+    private String mBtAudioDevice = null;
+    private final Lock mLock = new ReentrantLock();
+
+    public CallAudioCommunicationDeviceTracker(Context context) {
+        mAudioManager = context.getSystemService(AudioManager.class);
+    }
+
+    public void setBluetoothRouteManager(BluetoothRouteManager bluetoothRouteManager) {
+        mBluetoothRouteManager = bluetoothRouteManager;
+    }
+
+    public boolean isAudioDeviceSetForType(int audioDeviceType) {
+        if (Flags.communicationDeviceProtectedByLock()) {
+            mLock.lock();
+        }
+        try {
+            return mAudioDeviceType == audioDeviceType;
+        } finally {
+            if (Flags.communicationDeviceProtectedByLock()) {
+                mLock.unlock();
+            }
+        }
+    }
+
+    public int getCurrentLocallyRequestedCommunicationDevice() {
+        if (Flags.communicationDeviceProtectedByLock()) {
+            mLock.lock();
+        }
+        try {
+            return mAudioDeviceType;
+        } finally {
+            if (Flags.communicationDeviceProtectedByLock()) {
+                mLock.unlock();
+            }
+        }
+    }
+
+    @VisibleForTesting
+    public void setTestCommunicationDevice(int audioDeviceType) {
+        mAudioDeviceType = audioDeviceType;
+    }
+
+    public void clearBtCommunicationDevice() {
+        if (Flags.communicationDeviceProtectedByLock()) {
+            mLock.lock();
+        }
+        try {
+            if (mBtAudioDevice == null) {
+                Log.i(this, "No bluetooth device was set for communication that can be cleared.");
+            } else {
+                // If mBtAudioDevice is set, we know a BT audio device was set for communication so
+                // mAudioDeviceType corresponds to a BT device type (e.g. hearing aid, SCO, LE).
+                processClearCommunicationDevice(mAudioDeviceType);
+            }
+        } finally {
+            if (Flags.communicationDeviceProtectedByLock()) {
+                mLock.unlock();
+            }
+        }
+    }
+
+    /*
+     * Sets the communication device for the passed in audio device type, if it's available for
+     * communication use cases. Tries to clear any communication device which was previously
+     * requested for communication before setting the new device.
+     * @param audioDeviceTypes The supported audio device types for the device.
+     * @param btDevice The bluetooth device to connect to (only used for switching between multiple
+     *        LE audio devices).
+     * @return {@code true} if the device was set for communication, {@code false} if the device
+     * wasn't set.
+     */
+    public boolean setCommunicationDevice(int audioDeviceType,
+            BluetoothDevice btDevice) {
+        if (Flags.communicationDeviceProtectedByLock()) {
+            mLock.lock();
+        }
+        try {
+            return processSetCommunicationDevice(audioDeviceType, btDevice);
+        } finally {
+            if (Flags.communicationDeviceProtectedByLock()) {
+                mLock.unlock();
+            }
+        }
+    }
+
+    private boolean processSetCommunicationDevice(int audioDeviceType,
+            BluetoothDevice btDevice) {
+        // There is only one audio device type associated with each type of BT device.
+        boolean isBtDevice = BT_AUDIO_DEVICE_INFO_TYPES.contains(audioDeviceType);
+        Log.i(this, "setCommunicationDevice: type = %s, isBtDevice = %s, btDevice = %s",
+                audioDeviceType, isBtDevice, btDevice);
+
+        // Account for switching between multiple LE audio devices.
+        boolean handleLeAudioDeviceSwitch = btDevice != null
+                && !btDevice.getAddress().equals(mBtAudioDevice);
+        if ((audioDeviceType == mAudioDeviceType
+                || isUsbHeadsetType(audioDeviceType, mAudioDeviceType))
+                && !handleLeAudioDeviceSwitch) {
+            Log.i(this, "Communication device is already set for this audio type");
+            return false;
+        }
+
+        AudioDeviceInfo activeDevice = null;
+        List<AudioDeviceInfo> devices = mAudioManager.getAvailableCommunicationDevices();
+        if (devices.size() == 0) {
+            Log.w(this, "No communication devices available");
+            return false;
+        }
+
+        for (AudioDeviceInfo device : devices) {
+            Log.i(this, "Available device type: " + device.getType());
+            // Ensure that we do not select the same BT LE audio device for communication.
+            if ((audioDeviceType == device.getType()
+                    || isUsbHeadsetType(audioDeviceType, device.getType()))
+                    && !device.getAddress().equals(mBtAudioDevice)) {
+                activeDevice = device;
+                break;
+            }
+        }
+
+        if (activeDevice == null) {
+            Log.i(this, "No active device of type(s) %s available",
+                    audioDeviceType == AudioDeviceInfo.TYPE_WIRED_HEADSET
+                            ? Arrays.asList(AudioDeviceInfo.TYPE_WIRED_HEADSET,
+                            AudioDeviceInfo.TYPE_USB_HEADSET)
+                            : audioDeviceType);
+            return false;
+        }
+
+        // Force clear previous communication device, if one was set, before setting the new device.
+        if (mAudioDeviceType != sAUDIO_DEVICE_TYPE_INVALID) {
+            processClearCommunicationDevice(mAudioDeviceType);
+        }
+
+        // Turn activeDevice ON.
+        boolean result = mAudioManager.setCommunicationDevice(activeDevice);
+        if (!result) {
+            Log.w(this, "Could not set active device");
+        } else {
+            Log.i(this, "Active device set");
+            mAudioDeviceType = activeDevice.getType();
+            if (isBtDevice) {
+                mBtAudioDevice = activeDevice.getAddress();
+                if (audioDeviceType == AudioDeviceInfo.TYPE_BLE_HEADSET) {
+                    mBluetoothRouteManager.onAudioOn(mBtAudioDevice);
+                }
+            } else if (Flags.communicationDeviceProtectedByLock()) {
+                // Clear BT device if it's still stored. Handles race condition for when a non-BT
+                // device is set for communication shortly after a BT (LE) device is set for
+                // communication but the selection hasn't been cleared yet.
+                mBtAudioDevice = null;
+            }
+        }
+        return result;
+    }
+    /*
+     * Clears the communication device for the passed in audio device types, given that the device
+     * has previously been set for communication.
+     * @param audioDeviceTypes The supported audio device types for the device.
+     */
+    public void clearCommunicationDevice(int audioDeviceType) {
+        if (Flags.communicationDeviceProtectedByLock()) {
+            mLock.lock();
+        }
+        try {
+            processClearCommunicationDevice(audioDeviceType);
+        } finally {
+            if (Flags.communicationDeviceProtectedByLock()) {
+                mLock.unlock();
+            }
+        }
+    }
+
+    public void processClearCommunicationDevice(int audioDeviceType) {
+        if (audioDeviceType == sAUDIO_DEVICE_TYPE_INVALID) {
+            Log.i(this, "clearCommunicationDevice: Skip clearing communication device"
+                    + "for invalid audio type (-1).");
+        }
+
+        // There is only one audio device type associated with each type of BT device.
+        boolean isBtDevice = BT_AUDIO_DEVICE_INFO_TYPES.contains(audioDeviceType);
+        Log.i(this, "clearCommunicationDevice: type = %s, isBtDevice = %s",
+                audioDeviceType, isBtDevice);
+
+        if (audioDeviceType != mAudioDeviceType
+                && !isUsbHeadsetType(audioDeviceType, mAudioDeviceType)) {
+            Log.i(this, "Unable to clear communication device of type(s), %s. "
+                            + "Device does not correspond to the locally requested device type.",
+                    audioDeviceType == AudioDeviceInfo.TYPE_WIRED_HEADSET
+                            ? Arrays.asList(AudioDeviceInfo.TYPE_WIRED_HEADSET,
+                            AudioDeviceInfo.TYPE_USB_HEADSET)
+                            : audioDeviceType
+            );
+            return;
+        }
+
+        if (mAudioManager == null) {
+            Log.i(this, "clearCommunicationDevice: mAudioManager is null");
+            return;
+        }
+
+        // Clear device and reset locally saved device type.
+        mAudioManager.clearCommunicationDevice();
+        mAudioDeviceType = sAUDIO_DEVICE_TYPE_INVALID;
+
+        if (isBtDevice && mBtAudioDevice != null) {
+            // Signal that BT audio was lost for device.
+            mBluetoothRouteManager.onAudioLost(mBtAudioDevice);
+            mBtAudioDevice = null;
+        }
+    }
+
+    private boolean isUsbHeadsetType(int audioDeviceType, int sourceType) {
+        return audioDeviceType == AudioDeviceInfo.TYPE_WIRED_HEADSET
+                && sourceType == AudioDeviceInfo.TYPE_USB_HEADSET;
+    }
+}
diff --git a/src/com/android/server/telecom/CallAudioManager.java b/src/com/android/server/telecom/CallAudioManager.java
index ff76b9e..1f1ca9d 100644
--- a/src/com/android/server/telecom/CallAudioManager.java
+++ b/src/com/android/server/telecom/CallAudioManager.java
@@ -20,10 +20,11 @@
 import android.content.Context;
 import android.media.IAudioService;
 import android.media.ToneGenerator;
+import android.os.Handler;
+import android.os.HandlerThread;
 import android.os.UserHandle;
 import android.telecom.CallAudioState;
 import android.telecom.Log;
-import android.telecom.PhoneAccount;
 import android.telecom.VideoProfile;
 import android.util.SparseArray;
 
@@ -31,11 +32,15 @@
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.server.telecom.CallAudioModeStateMachine.MessageArgs.Builder;
 import com.android.server.telecom.bluetooth.BluetoothStateReceiver;
+import com.android.server.telecom.flags.FeatureFlags;
 
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.Set;
 import java.util.LinkedHashSet;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.Collectors;
+
 
 public class CallAudioManager extends CallsManagerListenerBase {
 
@@ -52,7 +57,7 @@
     private final Set<Call> mCalls;
     private final SparseArray<LinkedHashSet<Call>> mCallStateToCalls;
 
-    private final CallAudioRouteStateMachine mCallAudioRouteStateMachine;
+    private final CallAudioRouteAdapter mCallAudioRouteAdapter;
     private final CallAudioModeStateMachine mCallAudioModeStateMachine;
     private final BluetoothStateReceiver mBluetoothStateReceiver;
     private final CallsManager mCallsManager;
@@ -60,21 +65,27 @@
     private final Ringer mRinger;
     private final RingbackPlayer mRingbackPlayer;
     private final DtmfLocalTonePlayer mDtmfLocalTonePlayer;
+    private final FeatureFlags mFeatureFlags;
 
     private Call mStreamingCall;
     private Call mForegroundCall;
+    private CompletableFuture<Boolean> mCallRingingFuture;
+    private Thread mBtIcsBindingThread;
     private boolean mIsTonePlaying = false;
     private boolean mIsDisconnectedTonePlaying = false;
     private InCallTonePlayer mHoldTonePlayer;
+    private final HandlerThread mHandlerThread;
+    private final Handler mHandler;
 
-    public CallAudioManager(CallAudioRouteStateMachine callAudioRouteStateMachine,
+    public CallAudioManager(CallAudioRouteAdapter callAudioRouteAdapter,
             CallsManager callsManager,
             CallAudioModeStateMachine callAudioModeStateMachine,
             InCallTonePlayer.Factory playerFactory,
             Ringer ringer,
             RingbackPlayer ringbackPlayer,
             BluetoothStateReceiver bluetoothStateReceiver,
-            DtmfLocalTonePlayer dtmfLocalTonePlayer) {
+            DtmfLocalTonePlayer dtmfLocalTonePlayer,
+            FeatureFlags featureFlags) {
         mActiveDialingOrConnectingCalls = new LinkedHashSet<>(1);
         mRingingCalls = new LinkedHashSet<>(1);
         mHoldingCalls = new LinkedHashSet<>(1);
@@ -92,7 +103,7 @@
             put(CallState.AUDIO_PROCESSING, mAudioProcessingCalls);
         }};
 
-        mCallAudioRouteStateMachine = callAudioRouteStateMachine;
+        mCallAudioRouteAdapter = callAudioRouteAdapter;
         mCallAudioModeStateMachine = callAudioModeStateMachine;
         mCallsManager = callsManager;
         mPlayerFactory = playerFactory;
@@ -100,10 +111,14 @@
         mRingbackPlayer = ringbackPlayer;
         mBluetoothStateReceiver = bluetoothStateReceiver;
         mDtmfLocalTonePlayer = dtmfLocalTonePlayer;
+        mFeatureFlags = featureFlags;
+        mHandlerThread = new HandlerThread(this.getClass().getSimpleName());
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper());
 
         mPlayerFactory.setCallAudioManager(this);
         mCallAudioModeStateMachine.setCallAudioManager(this);
-        mCallAudioRouteStateMachine.setCallAudioManager(this);
+        mCallAudioRouteAdapter.setCallAudioManager(this);
     }
 
     @Override
@@ -116,7 +131,7 @@
             // State did not change, so no need to do anything.
             return;
         }
-        Log.d(LOG_TAG, "Call state changed for TC@%s: %s -> %s", call.getId(),
+        Log.i(this, "onCallStateChanged: Call state changed for TC@%s: %s -> %s", call.getId(),
                 CallState.toString(oldState), CallState.toString(newState));
 
         removeCallFromAllBins(call);
@@ -220,7 +235,7 @@
                 // When pulling a video call, automatically enable the speakerphone.
                 Log.d(LOG_TAG, "Switching to speaker because external video call %s was pulled." +
                         call.getId());
-                mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+                mCallAudioRouteAdapter.sendMessageWithSessionInfo(
                         CallAudioRouteStateMachine.SWITCH_SPEAKER);
             }
         }
@@ -302,7 +317,7 @@
                 VideoProfile.isReceptionEnabled(newVideoState);
 
         if (isUpgradeRequest) {
-            mPlayerFactory.createPlayer(InCallTonePlayer.TONE_VIDEO_UPGRADE).startTone();
+            mPlayerFactory.createPlayer(call, InCallTonePlayer.TONE_VIDEO_UPGRADE).startTone();
         }
     }
 
@@ -311,7 +326,7 @@
             // We only play tones for foreground calls.
             return;
         }
-        mPlayerFactory.createPlayer(InCallTonePlayer.TONE_RTT_REQUEST).startTone();
+        mPlayerFactory.createPlayer(call, InCallTonePlayer.TONE_RTT_REQUEST).startTone();
     }
 
     /**
@@ -324,7 +339,7 @@
      */
     @Override
     public void onHoldToneRequested(Call call) {
-        maybePlayHoldTone();
+        maybePlayHoldTone(call);
     }
 
     @Override
@@ -372,7 +387,7 @@
     @Override
     public void onConnectionServiceChanged(Call call, ConnectionServiceWrapper oldCs,
             ConnectionServiceWrapper newCs) {
-        mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+        mCallAudioRouteAdapter.sendMessageWithSessionInfo(
                 CallAudioRouteStateMachine.UPDATE_SYSTEM_AUDIO_ROUTE);
     }
 
@@ -390,13 +405,13 @@
             Log.d(LOG_TAG, "Switching to speaker because call %s transitioned video state from %s" +
                     " to %s", call.getId(), VideoProfile.videoStateToString(previousVideoState),
                     VideoProfile.videoStateToString(newVideoState));
-            mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+            mCallAudioRouteAdapter.sendMessageWithSessionInfo(
                     CallAudioRouteStateMachine.SWITCH_SPEAKER);
         }
     }
 
     public CallAudioState getCallAudioState() {
-        return mCallAudioRouteStateMachine.getCurrentCallAudioState();
+        return mCallAudioRouteAdapter.getCurrentCallAudioState();
     }
 
     public Call getPossiblyHeldForegroundCall() {
@@ -417,7 +432,7 @@
             Log.v(this, "ignoring toggleMute for emergency call");
             return;
         }
-        mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+        mCallAudioRouteAdapter.sendMessageWithSessionInfo(
                 CallAudioRouteStateMachine.TOGGLE_MUTE);
     }
 
@@ -437,7 +452,7 @@
             Log.v(this, "ignoring mute for emergency call");
         }
 
-        mCallAudioRouteStateMachine.sendMessageWithSessionInfo(shouldMute
+        mCallAudioRouteAdapter.sendMessageWithSessionInfo(shouldMute
                 ? CallAudioRouteStateMachine.MUTE_ON : CallAudioRouteStateMachine.MUTE_OFF);
     }
 
@@ -453,23 +468,23 @@
         Log.v(this, "setAudioRoute, route: %s", CallAudioState.audioRouteToString(route));
         switch (route) {
             case CallAudioState.ROUTE_BLUETOOTH:
-                mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+                mCallAudioRouteAdapter.sendMessageWithSessionInfo(
                         CallAudioRouteStateMachine.USER_SWITCH_BLUETOOTH, 0, bluetoothAddress);
                 return;
             case CallAudioState.ROUTE_SPEAKER:
-                mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+                mCallAudioRouteAdapter.sendMessageWithSessionInfo(
                         CallAudioRouteStateMachine.USER_SWITCH_SPEAKER);
                 return;
             case CallAudioState.ROUTE_WIRED_HEADSET:
-                mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+                mCallAudioRouteAdapter.sendMessageWithSessionInfo(
                         CallAudioRouteStateMachine.USER_SWITCH_HEADSET);
                 return;
             case CallAudioState.ROUTE_EARPIECE:
-                mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+                mCallAudioRouteAdapter.sendMessageWithSessionInfo(
                         CallAudioRouteStateMachine.USER_SWITCH_EARPIECE);
                 return;
             case CallAudioState.ROUTE_WIRED_OR_EARPIECE:
-                mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+                mCallAudioRouteAdapter.sendMessageWithSessionInfo(
                         CallAudioRouteStateMachine.USER_SWITCH_BASELINE_ROUTE,
                         CallAudioRouteStateMachine.NO_INCLUDE_BLUETOOTH_IN_BASELINE);
                 return;
@@ -484,7 +499,7 @@
      */
     void switchBaseline() {
         Log.i(this, "switchBaseline");
-        mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+        mCallAudioRouteAdapter.sendMessageWithSessionInfo(
                 CallAudioRouteStateMachine.USER_SWITCH_BASELINE_ROUTE,
                 CallAudioRouteStateMachine.INCLUDE_BLUETOOTH_IN_BASELINE);
     }
@@ -528,7 +543,7 @@
         synchronized (mCallsManager.getLock()) {
             Call localForegroundCall = mForegroundCall;
             boolean result = mRinger.startRinging(localForegroundCall,
-                    mCallAudioRouteStateMachine.isHfpDeviceAvailable());
+                    mCallAudioRouteAdapter.isHfpDeviceAvailable());
             if (result) {
                 localForegroundCall.setStartRingTime();
             }
@@ -561,7 +576,7 @@
 
     @VisibleForTesting
     public void setCallAudioRouteFocusState(int focusState) {
-        mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+        mCallAudioRouteAdapter.sendMessageWithSessionInfo(
                 CallAudioRouteStateMachine.SWITCH_FOCUS, focusState);
     }
 
@@ -571,8 +586,8 @@
     }
 
     @VisibleForTesting
-    public CallAudioRouteStateMachine getCallAudioRouteStateMachine() {
-        return mCallAudioRouteStateMachine;
+    public CallAudioRouteAdapter getCallAudioRouteAdapter() {
+        return mCallAudioRouteAdapter;
     }
 
     @VisibleForTesting
@@ -609,9 +624,9 @@
         mCallAudioModeStateMachine.dump(pw);
         pw.decreaseIndent();
 
-        pw.println("CallAudioRouteStateMachine:");
+        pw.println("mCallAudioRouteAdapter:");
         pw.increaseIndent();
-        mCallAudioRouteStateMachine.dump(pw);
+        mCallAudioRouteAdapter.dump(pw);
         pw.decreaseIndent();
 
         pw.println("BluetoothDeviceManager:");
@@ -623,7 +638,7 @@
     }
 
     @VisibleForTesting
-    public void setIsTonePlaying(boolean isTonePlaying) {
+    public void setIsTonePlaying(Call call, boolean isTonePlaying) {
         Log.i(this, "setIsTonePlaying; isTonePlaying=%b", isTonePlaying);
         mIsTonePlaying = isTonePlaying;
         mCallAudioModeStateMachine.sendMessageWithArgs(
@@ -632,7 +647,7 @@
                 makeArgsForModeStateMachine());
 
         if (!isTonePlaying && mIsDisconnectedTonePlaying) {
-            mCallsManager.onDisconnectedTonePlaying(false);
+            mCallsManager.onDisconnectedTonePlaying(call, false);
             mIsDisconnectedTonePlaying = false;
         }
     }
@@ -745,10 +760,42 @@
 
     private void onCallEnteringRinging() {
         if (mRingingCalls.size() == 1) {
+            Log.i(this, "onCallEnteringRinging: mFeatureFlags.separatelyBindToBtIncallService() ? %s",
+                    mFeatureFlags.separatelyBindToBtIncallService());
+            Log.i(this, "onCallEnteringRinging: mRingingCalls.getFirst().getBtIcsFuture() = %s",
+                    mRingingCalls.getFirst().getBtIcsFuture());
+            if (mFeatureFlags.separatelyBindToBtIncallService()
+                    && mRingingCalls.getFirst().getBtIcsFuture() != null) {
+                mCallRingingFuture  = mRingingCalls.getFirst().getBtIcsFuture()
+                        .thenComposeAsync((completed) -> {
+                            mCallAudioModeStateMachine.sendMessageWithArgs(
+                                    CallAudioModeStateMachine.NEW_RINGING_CALL,
+                                    makeArgsForModeStateMachine());
+                            return CompletableFuture.completedFuture(completed);
+                        }, new LoggedHandlerExecutor(mHandler, "CAM.oCER", mCallsManager.getLock()))
+                        .exceptionally((throwable) -> {
+                            Log.e(this, throwable, "Error while executing BT ICS future");
+                            // Fallback on performing computation on a separate thread.
+                            handleBtBindingWaitFallback();
+                            return null;
+                        });
+            } else {
+                mCallAudioModeStateMachine.sendMessageWithArgs(
+                        CallAudioModeStateMachine.NEW_RINGING_CALL,
+                        makeArgsForModeStateMachine());
+            }
+        }
+    }
+
+    private void handleBtBindingWaitFallback() {
+        // Wait until the BT ICS binding completed to request further audio route change
+        mBtIcsBindingThread = new Thread(() -> {
+            mRingingCalls.getFirst().waitForBtIcs();
             mCallAudioModeStateMachine.sendMessageWithArgs(
                     CallAudioModeStateMachine.NEW_RINGING_CALL,
                     makeArgsForModeStateMachine());
-        }
+        });
+        mBtIcsBindingThread.start();
     }
 
     private void onCallEnteringHold() {
@@ -761,6 +808,7 @@
 
     private void updateForegroundCall() {
         Call oldForegroundCall = mForegroundCall;
+
         if (mActiveDialingOrConnectingCalls.size() > 0) {
             // Give preference for connecting calls over active/dialing for foreground-ness.
             Call possibleConnectingCall = null;
@@ -769,8 +817,27 @@
                     possibleConnectingCall = call;
                 }
             }
-            mForegroundCall = possibleConnectingCall == null ?
-                    mActiveDialingOrConnectingCalls.iterator().next() : possibleConnectingCall;
+            if (mFeatureFlags.ensureAudioModeUpdatesOnForegroundCallChange()) {
+                // Prefer a connecting call
+                if (possibleConnectingCall != null) {
+                    mForegroundCall = possibleConnectingCall;
+                } else {
+                    // Next, prefer an active or dialing call which is not in the process of being
+                    // disconnected.
+                    mForegroundCall = mActiveDialingOrConnectingCalls
+                            .stream()
+                            .filter(c -> (c.getState() == CallState.ACTIVE
+                                    || c.getState() == CallState.DIALING)
+                                    && !c.isLocallyDisconnecting())
+                            .findFirst()
+                            // If we can't find one, then just fall back to the first one.
+                            .orElse(mActiveDialingOrConnectingCalls.iterator().next());
+                }
+            } else {
+                // Legacy (buggy) behavior.
+                mForegroundCall = possibleConnectingCall == null ?
+                        mActiveDialingOrConnectingCalls.iterator().next() : possibleConnectingCall;
+            }
         } else if (mRingingCalls.size() > 0) {
             mForegroundCall = mRingingCalls.iterator().next();
         } else if (mHoldingCalls.size() > 0) {
@@ -778,12 +845,27 @@
         } else {
             mForegroundCall = null;
         }
-
+        Log.i(this, "updateForegroundCall; oldFg=%s, newFg=%s, aDC=%s, ring=%s, hold=%s",
+                (oldForegroundCall == null ? "none" : oldForegroundCall.getId()),
+                (mForegroundCall == null ? "none" : mForegroundCall.getId()),
+                mActiveDialingOrConnectingCalls.stream().map(c -> c.getId()).collect(
+                        Collectors.joining(",")),
+                mRingingCalls.stream().map(c -> c.getId()).collect(Collectors.joining(",")),
+                mHoldingCalls.stream().map(c -> c.getId()).collect(Collectors.joining(","))
+        );
         if (mForegroundCall != oldForegroundCall) {
-            mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+            mCallAudioRouteAdapter.sendMessageWithSessionInfo(
                     CallAudioRouteStateMachine.UPDATE_SYSTEM_AUDIO_ROUTE);
+
+            if (mForegroundCall != null
+                    && mFeatureFlags.ensureAudioModeUpdatesOnForegroundCallChange()) {
+                // Ensure the voip audio mode for the new foreground call is taken into account.
+                mCallAudioModeStateMachine.sendMessageWithArgs(
+                        CallAudioModeStateMachine.FOREGROUND_VOIP_MODE_CHANGE,
+                        makeArgsForModeStateMachine());
+            }
             mDtmfLocalTonePlayer.onForegroundCallChanged(oldForegroundCall, mForegroundCall);
-            maybePlayHoldTone();
+            maybePlayHoldTone(oldForegroundCall);
         }
     }
 
@@ -845,12 +927,14 @@
         // we will not play a disconnect tone.
         if (call.isHandoverInProgress()) {
             Log.i(LOG_TAG, "Omitting tone because %s is being handed over.", call);
+            completeDisconnectToneFuture(call);
             return;
         }
 
         if (mForegroundCall != null && call != mForegroundCall && mCalls.size() > 1) {
             Log.v(LOG_TAG, "Omitting tone because we are not foreground" +
                     " and there is another call.");
+            completeDisconnectToneFuture(call);
             return;
         }
 
@@ -886,11 +970,13 @@
             Log.d(this, "Found a disconnected call with tone to play %d.", toneToPlay);
 
             if (toneToPlay != InCallTonePlayer.TONE_INVALID) {
-                boolean didToneStart = mPlayerFactory.createPlayer(toneToPlay).startTone();
+                boolean didToneStart = mPlayerFactory.createPlayer(call, toneToPlay).startTone();
                 if (didToneStart) {
-                    mCallsManager.onDisconnectedTonePlaying(true);
+                    mCallsManager.onDisconnectedTonePlaying(call, true);
                     mIsDisconnectedTonePlaying = true;
                 }
+            } else {
+                completeDisconnectToneFuture(call);
             }
         }
     }
@@ -908,10 +994,11 @@
     /**
      * Determines if a hold tone should be played and then starts or stops it accordingly.
      */
-    private void maybePlayHoldTone() {
+    private void maybePlayHoldTone(Call call) {
         if (shouldPlayHoldTone()) {
             if (mHoldTonePlayer == null) {
-                mHoldTonePlayer = mPlayerFactory.createPlayer(InCallTonePlayer.TONE_CALL_WAITING);
+                mHoldTonePlayer = mPlayerFactory.createPlayer(call,
+                        InCallTonePlayer.TONE_CALL_WAITING);
                 mHoldTonePlayer.startTone();
             }
         } else {
@@ -977,6 +1064,14 @@
                 oldState == CallState.ON_HOLD;
     }
 
+    private void completeDisconnectToneFuture(Call call) {
+        CompletableFuture<Void> disconnectedToneFuture = mCallsManager.getInCallController()
+                .getDisconnectedToneBtFutures().get(call.getId());
+        if (disconnectedToneFuture != null) {
+            disconnectedToneFuture.complete(null);
+        }
+    }
+
     @VisibleForTesting
     public Set<Call> getTrackedCalls() {
         return mCalls;
@@ -986,4 +1081,9 @@
     public SparseArray<LinkedHashSet<Call>> getCallStateToCalls() {
         return mCallStateToCalls;
     }
+
+    @VisibleForTesting
+    public CompletableFuture<Boolean> getCallRingingFuture() {
+        return mCallRingingFuture;
+    }
 }
diff --git a/src/com/android/server/telecom/CallAudioModeStateMachine.java b/src/com/android/server/telecom/CallAudioModeStateMachine.java
index 9ad9094..6420f2e 100644
--- a/src/com/android/server/telecom/CallAudioModeStateMachine.java
+++ b/src/com/android/server/telecom/CallAudioModeStateMachine.java
@@ -16,6 +16,8 @@
 
 package com.android.server.telecom;
 
+import android.media.AudioAttributes;
+import android.media.AudioFocusRequest;
 import android.media.AudioManager;
 import android.os.Looper;
 import android.os.Message;
@@ -29,6 +31,7 @@
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.util.State;
 import com.android.internal.util.StateMachine;
+import com.android.server.telecom.flags.FeatureFlags;
 
 public class CallAudioModeStateMachine extends StateMachine {
     /**
@@ -38,11 +41,29 @@
     private LocalLog mLocalLog = new LocalLog(20);
     public static class Factory {
         public CallAudioModeStateMachine create(SystemStateHelper systemStateHelper,
-                AudioManager am) {
-            return new CallAudioModeStateMachine(systemStateHelper, am);
+                AudioManager am, FeatureFlags featureFlags,
+                CallAudioCommunicationDeviceTracker callAudioCommunicationDeviceTracker) {
+            return new CallAudioModeStateMachine(systemStateHelper, am,
+                    featureFlags, callAudioCommunicationDeviceTracker);
         }
     }
 
+    private static final AudioAttributes RING_AUDIO_ATTRIBUTES = new AudioAttributes.Builder()
+            .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
+            .setLegacyStreamType(AudioManager.STREAM_RING)
+            .build();
+    public static final AudioFocusRequest RING_AUDIO_FOCUS_REQUEST = new AudioFocusRequest
+            .Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
+            .setAudioAttributes(RING_AUDIO_ATTRIBUTES).build();
+
+    private static final AudioAttributes CALL_AUDIO_ATTRIBUTES = new AudioAttributes.Builder()
+            .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
+            .setLegacyStreamType(AudioManager.STREAM_VOICE_CALL)
+            .build();
+    public static final AudioFocusRequest CALL_AUDIO_FOCUS_REQUEST = new AudioFocusRequest
+            .Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
+            .setAudioAttributes(CALL_AUDIO_ATTRIBUTES).build();
+
     public static class MessageArgs {
         public boolean hasActiveOrDialingCalls;
         public boolean hasRingingCalls;
@@ -212,6 +233,8 @@
     public static final String STREAMING_STATE_NAME = StreamingFocusState.class.getSimpleName();
     public static final String COMMS_STATE_NAME = VoipCallFocusState.class.getSimpleName();
 
+    private AudioFocusRequest mCurrentAudioFocusRequest = null;
+
     private class BaseState extends State {
         @Override
         public boolean processMessage(Message msg) {
@@ -256,8 +279,23 @@
             Log.i(LOG_TAG, "Audio focus entering UNFOCUSED state");
             mLocalLog.log("Enter UNFOCUSED");
             if (mIsInitialized) {
-                mCallAudioManager.setCallAudioRouteFocusState(CallAudioRouteStateMachine.NO_FOCUS);
-                mAudioManager.setMode(AudioManager.MODE_NORMAL);
+                // Clear any communication device that was requested previously.
+                // Todo: Remove once clearCommunicationDeviceAfterAudioOpsComplete is
+                // completely rolled out.
+                if (mFeatureFlags.callAudioCommunicationDeviceRefactor()
+                        && !mFeatureFlags.clearCommunicationDeviceAfterAudioOpsComplete()) {
+                    mCommunicationDeviceTracker.clearCommunicationDevice(mCommunicationDeviceTracker
+                            .getCurrentLocallyRequestedCommunicationDevice());
+                }
+                if (mFeatureFlags.setAudioModeBeforeAbandonFocus()) {
+                    mAudioManager.setMode(AudioManager.MODE_NORMAL);
+                    mCallAudioManager.setCallAudioRouteFocusState(
+                            CallAudioRouteStateMachine.NO_FOCUS);
+                } else {
+                    mCallAudioManager.setCallAudioRouteFocusState(
+                            CallAudioRouteStateMachine.NO_FOCUS);
+                    mAudioManager.setMode(AudioManager.MODE_NORMAL);
+                }
                 mLocalLog.log("Mode MODE_NORMAL");
                 mMostRecentMode = AudioManager.MODE_NORMAL;
                 // Don't release focus here -- wait until we get a signal that any other audio
@@ -310,7 +348,20 @@
                     return HANDLED;
                 case AUDIO_OPERATIONS_COMPLETE:
                     Log.i(LOG_TAG, "Abandoning audio focus: now UNFOCUSED");
-                    mAudioManager.abandonAudioFocusForCall();
+                    if (mFeatureFlags.telecomResolveHiddenDependencies()) {
+                        if (mCurrentAudioFocusRequest != null) {
+                            mAudioManager.abandonAudioFocusRequest(mCurrentAudioFocusRequest);
+                            mCurrentAudioFocusRequest = null;
+                        }
+                    } else {
+                        mAudioManager.abandonAudioFocusForCall();
+                    }
+                    // Clear requested communication device after the call ends.
+                    if (mFeatureFlags.clearCommunicationDeviceAfterAudioOpsComplete()) {
+                        mCommunicationDeviceTracker.clearCommunicationDevice(
+                                mCommunicationDeviceTracker
+                                        .getCurrentLocallyRequestedCommunicationDevice());
+                    }
                     return HANDLED;
                 default:
                     // The forced focus switch commands are handled by BaseState.
@@ -381,7 +432,14 @@
                     return HANDLED;
                 case AUDIO_OPERATIONS_COMPLETE:
                     Log.i(LOG_TAG, "Abandoning audio focus: now AUDIO_PROCESSING");
-                    mAudioManager.abandonAudioFocusForCall();
+                    if (mFeatureFlags.telecomResolveHiddenDependencies()) {
+                        if (mCurrentAudioFocusRequest != null) {
+                            mAudioManager.abandonAudioFocusRequest(mCurrentAudioFocusRequest);
+                            mCurrentAudioFocusRequest = null;
+                        }
+                    } else {
+                        mAudioManager.abandonAudioFocusForCall();
+                    }
                     return HANDLED;
                 default:
                     // The forced focus switch commands are handled by BaseState.
@@ -406,8 +464,13 @@
                 }
 
                 if (mCallAudioManager.startRinging()) {
-                    mAudioManager.requestAudioFocusForCall(
-                        AudioManager.STREAM_RING, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+                    if (mFeatureFlags.telecomResolveHiddenDependencies()) {
+                        mCurrentAudioFocusRequest = RING_AUDIO_FOCUS_REQUEST;
+                        mAudioManager.requestAudioFocus(RING_AUDIO_FOCUS_REQUEST);
+                    } else {
+                        mAudioManager.requestAudioFocusForCall(
+                                AudioManager.STREAM_RING, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+                    }
                     // Do not set MODE_RINGTONE if we were previously in the CALL_SCREENING mode --
                     // this trips up the audio system.
                     if (mAudioManager.getMode() != AudioManager.MODE_CALL_SCREENING) {
@@ -504,8 +567,13 @@
         public void enter() {
             Log.i(LOG_TAG, "Audio focus entering SIM CALL state");
             mLocalLog.log("Enter SIM_CALL");
-            mAudioManager.requestAudioFocusForCall(AudioManager.STREAM_VOICE_CALL,
-                    AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+            if (mFeatureFlags.telecomResolveHiddenDependencies()) {
+                mCurrentAudioFocusRequest = CALL_AUDIO_FOCUS_REQUEST;
+                mAudioManager.requestAudioFocus(CALL_AUDIO_FOCUS_REQUEST);
+            } else {
+                mAudioManager.requestAudioFocusForCall(AudioManager.STREAM_VOICE_CALL,
+                        AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+            }
             mAudioManager.setMode(AudioManager.MODE_IN_CALL);
             mLocalLog.log("Mode MODE_IN_CALL");
             mMostRecentMode = AudioManager.MODE_IN_CALL;
@@ -587,8 +655,13 @@
         public void enter() {
             Log.i(LOG_TAG, "Audio focus entering VOIP CALL state");
             mLocalLog.log("Enter VOIP_CALL");
-            mAudioManager.requestAudioFocusForCall(AudioManager.STREAM_VOICE_CALL,
-                    AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+            if (mFeatureFlags.telecomResolveHiddenDependencies()) {
+                mCurrentAudioFocusRequest = CALL_AUDIO_FOCUS_REQUEST;
+                mAudioManager.requestAudioFocus(CALL_AUDIO_FOCUS_REQUEST);
+            } else {
+                mAudioManager.requestAudioFocusForCall(AudioManager.STREAM_VOICE_CALL,
+                        AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+            }
             mAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
             mLocalLog.log("Mode MODE_IN_COMMUNICATION");
             mMostRecentMode = AudioManager.MODE_IN_COMMUNICATION;
@@ -670,12 +743,12 @@
             mAudioManager.setMode(AudioManager.MODE_COMMUNICATION_REDIRECT);
             mMostRecentMode = AudioManager.MODE_NORMAL;
             mCallAudioManager.setCallAudioRouteFocusState(CallAudioRouteStateMachine.ACTIVE_FOCUS);
-            mCallAudioManager.getCallAudioRouteStateMachine().sendMessageWithSessionInfo(
+            mCallAudioManager.getCallAudioRouteAdapter().sendMessageWithSessionInfo(
                     CallAudioRouteStateMachine.STREAMING_FORCE_ENABLED);
         }
 
         private void preExit() {
-            mCallAudioManager.getCallAudioRouteStateMachine().sendMessageWithSessionInfo(
+            mCallAudioManager.getCallAudioRouteAdapter().sendMessageWithSessionInfo(
                     CallAudioRouteStateMachine.STREAMING_FORCE_DISABLED);
         }
 
@@ -742,8 +815,13 @@
         public void enter() {
             Log.i(LOG_TAG, "Audio focus entering TONE/HOLDING state");
             mLocalLog.log("Enter TONE/HOLDING");
-            mAudioManager.requestAudioFocusForCall(AudioManager.STREAM_VOICE_CALL,
-                    AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+            if (mFeatureFlags.telecomResolveHiddenDependencies()) {
+                mCurrentAudioFocusRequest = CALL_AUDIO_FOCUS_REQUEST;
+                mAudioManager.requestAudioFocus(CALL_AUDIO_FOCUS_REQUEST);
+            } else {
+                mAudioManager.requestAudioFocusForCall(AudioManager.STREAM_VOICE_CALL,
+                        AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+            }
             mAudioManager.setMode(mMostRecentMode);
             mLocalLog.log("Mode " + mMostRecentMode);
             mCallAudioManager.setCallAudioRouteFocusState(CallAudioRouteStateMachine.ACTIVE_FOCUS);
@@ -815,16 +893,21 @@
     private final AudioManager mAudioManager;
     private final SystemStateHelper mSystemStateHelper;
     private CallAudioManager mCallAudioManager;
+    private FeatureFlags mFeatureFlags;
+    private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
 
     private int mMostRecentMode;
     private boolean mIsInitialized = false;
 
     public CallAudioModeStateMachine(SystemStateHelper systemStateHelper,
-            AudioManager audioManager) {
+            AudioManager audioManager, FeatureFlags featureFlags,
+            CallAudioCommunicationDeviceTracker callAudioCommunicationDeviceTracker) {
         super(CallAudioModeStateMachine.class.getSimpleName());
         mAudioManager = audioManager;
         mSystemStateHelper = systemStateHelper;
         mMostRecentMode = AudioManager.MODE_NORMAL;
+        mFeatureFlags = featureFlags;
+        mCommunicationDeviceTracker = callAudioCommunicationDeviceTracker;
 
         createStates();
     }
@@ -833,11 +916,14 @@
      * Used for testing
      */
     public CallAudioModeStateMachine(SystemStateHelper systemStateHelper,
-            AudioManager audioManager, Looper looper) {
+            AudioManager audioManager, Looper looper, FeatureFlags featureFlags,
+            CallAudioCommunicationDeviceTracker communicationDeviceTracker) {
         super(CallAudioModeStateMachine.class.getSimpleName(), looper);
         mAudioManager = audioManager;
         mSystemStateHelper = systemStateHelper;
         mMostRecentMode = AudioManager.MODE_NORMAL;
+        mFeatureFlags = featureFlags;
+        mCommunicationDeviceTracker = communicationDeviceTracker;
 
         createStates();
     }
diff --git a/src/com/android/server/telecom/CallAudioRouteAdapter.java b/src/com/android/server/telecom/CallAudioRouteAdapter.java
new file mode 100644
index 0000000..9927c22
--- /dev/null
+++ b/src/com/android/server/telecom/CallAudioRouteAdapter.java
@@ -0,0 +1,139 @@
+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(STREAMING_FORCE_ENABLED, "STREAMING_FORCE_ENABLED");
+        put(STREAMING_FORCE_DISABLED, "STREAMING_FORCE_DISABLED");
+
+        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();
+    boolean isHfpDeviceAvailable();
+    Handler getAdapterHandler();
+    PendingAudioRoute getPendingAudioRoute();
+    void dump(IndentingPrintWriter pw);
+}
diff --git a/src/com/android/server/telecom/CallAudioRouteController.java b/src/com/android/server/telecom/CallAudioRouteController.java
new file mode 100644
index 0000000..4bb5373
--- /dev/null
+++ b/src/com/android/server/telecom/CallAudioRouteController.java
@@ -0,0 +1,1322 @@
+/*
+ * 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 static com.android.server.telecom.AudioRoute.TYPE_SPEAKER;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeAudio;
+import android.bluetooth.BluetoothProfile;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.media.AudioAttributes;
+import android.media.AudioDeviceAttributes;
+import android.media.AudioDeviceInfo;
+import android.media.AudioManager;
+import android.media.IAudioService;
+import android.media.audiopolicy.AudioProductStrategy;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.os.RemoteException;
+import android.telecom.CallAudioState;
+import android.telecom.Log;
+import android.telecom.Logging.Session;
+import android.telecom.VideoProfile;
+import android.util.ArrayMap;
+import android.util.Pair;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.SomeArgs;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.telecom.bluetooth.BluetoothRouteManager;
+import com.android.server.telecom.flags.FeatureFlags;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+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);
+        ROUTE_MAP.put(AudioRoute.TYPE_STREAMING, CallAudioState.ROUTE_STREAMING);
+    }
+
+    /** Valid values for the first argument for SWITCH_BASELINE_ROUTE */
+    public static final int INCLUDE_BLUETOOTH_IN_BASELINE = 1;
+
+    private final CallsManager mCallsManager;
+    private final Context mContext;
+    private AudioManager mAudioManager;
+    private CallAudioManager mCallAudioManager;
+    private final BluetoothRouteManager mBluetoothRouteManager;
+    private final CallAudioManager.AudioServiceFactory mAudioServiceFactory;
+    private final Handler mHandler;
+    private final WiredHeadsetManager mWiredHeadsetManager;
+    private Set<AudioRoute> mAvailableRoutes;
+    private AudioRoute mCurrentRoute;
+    private AudioRoute mEarpieceWiredRoute;
+    private AudioRoute mSpeakerDockRoute;
+    private AudioRoute mStreamingRoute;
+    private Set<AudioRoute> mStreamingRoutes;
+    private Map<AudioRoute, BluetoothDevice> mBluetoothRoutes;
+    private Pair<Integer, String> mActiveBluetoothDevice;
+    private Map<Integer, String> mActiveDeviceCache;
+    private Map<Integer, AudioRoute> mTypeRoutes;
+    private PendingAudioRoute mPendingAudioRoute;
+    private AudioRoute.Factory mAudioRouteFactory;
+    private StatusBarNotifier mStatusBarNotifier;
+    private FeatureFlags mFeatureFlags;
+    private int mFocusType;
+    private boolean mIsScoAudioConnected;
+    private final Object mLock = new Object();
+    private final TelecomSystem.SyncRoot mTelecomLock;
+    private final BroadcastReceiver mSpeakerPhoneChangeReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            Log.startSession("CARC.mSPCR");
+            try {
+                if (AudioManager.ACTION_SPEAKERPHONE_STATE_CHANGED.equals(intent.getAction())) {
+                    if (mAudioManager != null) {
+                        AudioDeviceInfo info = mAudioManager.getCommunicationDevice();
+                        if ((info != null) &&
+                                (info.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER)) {
+                            if (mCurrentRoute.getType() != AudioRoute.TYPE_SPEAKER) {
+                                sendMessageWithSessionInfo(SPEAKER_ON);
+                            }
+                        } else {
+                            sendMessageWithSessionInfo(SPEAKER_OFF);
+                        }
+                    }
+                } else {
+                    Log.w(this, "Received non-speakerphone-change intent");
+                }
+            } finally {
+                Log.endSession();
+            }
+        }
+    };
+    private final BroadcastReceiver mMuteChangeReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            Log.startSession("CARC.mCR");
+            try {
+                if (AudioManager.ACTION_MICROPHONE_MUTE_CHANGED.equals(intent.getAction())) {
+                    if (mCallsManager.isInEmergencyCall()) {
+                        Log.i(this, "Mute was externally changed when there's an emergency call. "
+                                + "Forcing mute back off.");
+                        sendMessageWithSessionInfo(MUTE_OFF);
+                    } else {
+                        sendMessageWithSessionInfo(MUTE_EXTERNALLY_CHANGED);
+                    }
+                } else if (AudioManager.STREAM_MUTE_CHANGED_ACTION.equals(intent.getAction())) {
+                    int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1);
+                    boolean isStreamMuted = intent.getBooleanExtra(
+                            AudioManager.EXTRA_STREAM_VOLUME_MUTED, false);
+
+                    if (streamType == AudioManager.STREAM_RING && !isStreamMuted
+                            && mCallAudioManager != null) {
+                        Log.i(this, "Ring stream was un-muted.");
+                        mCallAudioManager.onRingerModeChange();
+                    }
+                } else {
+                    Log.w(this, "Received non-mute-change intent");
+                }
+            } finally {
+                Log.endSession();
+            }
+        }
+    };
+    private CallAudioState mCallAudioState;
+    private boolean mIsMute;
+    private boolean mIsPending;
+    private boolean mIsActive;
+
+    public CallAudioRouteController(
+            Context context, CallsManager callsManager,
+            CallAudioManager.AudioServiceFactory audioServiceFactory,
+            AudioRoute.Factory audioRouteFactory, WiredHeadsetManager wiredHeadsetManager,
+            BluetoothRouteManager bluetoothRouteManager, StatusBarNotifier statusBarNotifier,
+            FeatureFlags featureFlags) {
+        mContext = context;
+        mCallsManager = callsManager;
+        mAudioManager = context.getSystemService(AudioManager.class);
+        mAudioServiceFactory = audioServiceFactory;
+        mAudioRouteFactory = audioRouteFactory;
+        mWiredHeadsetManager = wiredHeadsetManager;
+        mIsMute = false;
+        mBluetoothRouteManager = bluetoothRouteManager;
+        mStatusBarNotifier = statusBarNotifier;
+        mFeatureFlags = featureFlags;
+        mFocusType = NO_FOCUS;
+        mIsScoAudioConnected = false;
+        mTelecomLock = callsManager.getLock();
+        HandlerThread handlerThread = new HandlerThread(this.getClass().getSimpleName());
+        handlerThread.start();
+
+        // Register broadcast receivers
+        IntentFilter speakerChangedFilter = new IntentFilter(
+                AudioManager.ACTION_SPEAKERPHONE_STATE_CHANGED);
+        speakerChangedFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
+        context.registerReceiver(mSpeakerPhoneChangeReceiver, speakerChangedFilter);
+
+        IntentFilter micMuteChangedFilter = new IntentFilter(
+                AudioManager.ACTION_MICROPHONE_MUTE_CHANGED);
+        micMuteChangedFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
+        context.registerReceiver(mMuteChangeReceiver, micMuteChangedFilter);
+
+        IntentFilter muteChangedFilter = new IntentFilter(AudioManager.STREAM_MUTE_CHANGED_ACTION);
+        muteChangedFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
+        context.registerReceiver(mMuteChangeReceiver, muteChangedFilter);
+
+        // Create handler
+        mHandler = new Handler(handlerThread.getLooper()) {
+            @Override
+            public void handleMessage(@NonNull Message msg) {
+                synchronized (this) {
+                    preHandleMessage(msg);
+                    String address;
+                    BluetoothDevice bluetoothDevice;
+                    int focus;
+                    @AudioRoute.AudioRouteType int type;
+                    switch (msg.what) {
+                        case CONNECT_WIRED_HEADSET:
+                            handleWiredHeadsetConnected();
+                            break;
+                        case DISCONNECT_WIRED_HEADSET:
+                            handleWiredHeadsetDisconnected();
+                            break;
+                        case CONNECT_DOCK:
+                            handleDockConnected();
+                            break;
+                        case DISCONNECT_DOCK:
+                            handleDockDisconnected();
+                            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 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 SWITCH_EARPIECE:
+                        case USER_SWITCH_EARPIECE:
+                            handleSwitchEarpiece();
+                            break;
+                        case SWITCH_BLUETOOTH:
+                        case USER_SWITCH_BLUETOOTH:
+                            address = (String) ((SomeArgs) msg.obj).arg2;
+                            handleSwitchBluetooth(address);
+                            break;
+                        case SWITCH_HEADSET:
+                        case USER_SWITCH_HEADSET:
+                            handleSwitchHeadset();
+                            break;
+                        case SWITCH_SPEAKER:
+                        case USER_SWITCH_SPEAKER:
+                            handleSwitchSpeaker();
+                            break;
+                        case SWITCH_BASELINE_ROUTE:
+                            address = (String) ((SomeArgs) msg.obj).arg2;
+                            handleSwitchBaselineRoute(msg.arg1 == INCLUDE_BLUETOOTH_IN_BASELINE,
+                                    address);
+                            break;
+                        case USER_SWITCH_BASELINE_ROUTE:
+                            handleSwitchBaselineRoute(msg.arg1 == INCLUDE_BLUETOOTH_IN_BASELINE,
+                                    null);
+                            break;
+                        case SPEAKER_ON:
+                            handleSpeakerOn();
+                            break;
+                        case SPEAKER_OFF:
+                            handleSpeakerOff();
+                            break;
+                        case STREAMING_FORCE_ENABLED:
+                            handleStreamingEnabled();
+                            break;
+                        case STREAMING_FORCE_DISABLED:
+                            handleStreamingDisabled();
+                            break;
+                        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 MUTE_ON:
+                            handleMuteChanged(true);
+                            break;
+                        case MUTE_OFF:
+                            handleMuteChanged(false);
+                            break;
+                        case MUTE_EXTERNALLY_CHANGED:
+                            handleMuteChanged(mAudioManager.isMicrophoneMute());
+                            break;
+                        case SWITCH_FOCUS:
+                            focus = msg.arg1;
+                            handleSwitchFocus(focus);
+                            break;
+                        case EXIT_PENDING_ROUTE:
+                            handleExitPendingRoute();
+                            break;
+                        default:
+                            break;
+                    }
+                    postHandleMessage(msg);
+                }
+            }
+        };
+    }
+    @Override
+    public void initialize() {
+        mAvailableRoutes = new HashSet<>();
+        mBluetoothRoutes = new LinkedHashMap<>();
+        mActiveDeviceCache = new HashMap<>();
+        mActiveDeviceCache.put(AudioRoute.TYPE_BLUETOOTH_SCO, null);
+        mActiveDeviceCache.put(AudioRoute.TYPE_BLUETOOTH_HA, null);
+        mActiveDeviceCache.put(AudioRoute.TYPE_BLUETOOTH_LE, null);
+        mActiveBluetoothDevice = null;
+        mTypeRoutes = new ArrayMap<>();
+        mStreamingRoutes = new HashSet<>();
+        mPendingAudioRoute = new PendingAudioRoute(this, mAudioManager, mBluetoothRouteManager);
+        mStreamingRoute = new AudioRoute(AudioRoute.TYPE_STREAMING, null, null);
+        mStreamingRoutes.add(mStreamingRoute);
+
+        int supportMask = calculateSupportedRouteMaskInit();
+        if ((supportMask & CallAudioState.ROUTE_SPEAKER) != 0) {
+            // Create speaker 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
+    public void setCallAudioManager(CallAudioManager callAudioManager) {
+        mCallAudioManager = callAudioManager;
+    }
+
+    @Override
+    public CallAudioState getCurrentCallAudioState() {
+        return mCallAudioState;
+    }
+
+    @Override
+    public boolean isHfpDeviceAvailable() {
+        return !mBluetoothRoutes.isEmpty();
+    }
+
+    @Override
+    public Handler getAdapterHandler() {
+        return mHandler;
+    }
+
+    @Override
+    public PendingAudioRoute getPendingAudioRoute() {
+        return mPendingAudioRoute;
+    }
+
+    @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 (!destRoute.equals(mStreamingRoute) && !getAvailableRoutes().contains(destRoute)) {
+            Log.i(this, "Ignore routing to unavailable route: %s", destRoute);
+            return;
+        }
+        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);
+            // Ensure we don't keep waiting for SPEAKER_ON if dest route gets overridden.
+            if (active && mPendingAudioRoute.getDestRoute().getType() == TYPE_SPEAKER) {
+                mPendingAudioRoute.clearPendingMessage(new Pair<>(SPEAKER_ON, null));
+            }
+            // override pending route while keep waiting for still pending messages for the
+            // previous pending route
+            mPendingAudioRoute.setOrigRoute(mIsActive, mPendingAudioRoute.getDestRoute());
+        } 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 (getAvailableRoutes().contains(mCurrentRoute)) {
+                mPendingAudioRoute.setOrigRoute(mIsActive, mCurrentRoute);
+            } else {
+                // Avoid waiting for pending messages for an unavailable route
+                mPendingAudioRoute.setOrigRoute(mIsActive, DUMMY_ROUTE);
+            }
+            mIsPending = true;
+        }
+        mPendingAudioRoute.setDestRoute(active, destRoute, mBluetoothRoutes.get(destRoute),
+                mIsScoAudioConnected);
+        mIsActive = active;
+        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 handleWiredHeadsetConnected() {
+        AudioRoute wiredHeadsetRoute = null;
+        try {
+            wiredHeadsetRoute = mAudioRouteFactory.create(AudioRoute.TYPE_WIRED, null,
+                    mAudioManager);
+        } catch (IllegalArgumentException e) {
+            Log.e(this, e, "Can't find available audio device info for route type:"
+                    + AudioRoute.DEVICE_TYPE_STRINGS.get(AudioRoute.TYPE_WIRED));
+        }
+
+        if (wiredHeadsetRoute != null) {
+            mAvailableRoutes.add(wiredHeadsetRoute);
+            mAvailableRoutes.remove(mEarpieceWiredRoute);
+            mTypeRoutes.put(AudioRoute.TYPE_WIRED, wiredHeadsetRoute);
+            mEarpieceWiredRoute = wiredHeadsetRoute;
+            routeTo(mIsActive, wiredHeadsetRoute);
+            onAvailableRoutesChanged();
+        }
+    }
+
+    public void handleWiredHeadsetDisconnected() {
+        // Update audio route states
+        AudioRoute wiredHeadsetRoute = mTypeRoutes.remove(AudioRoute.TYPE_WIRED);
+        if (wiredHeadsetRoute != null) {
+            mAvailableRoutes.remove(wiredHeadsetRoute);
+            mEarpieceWiredRoute = null;
+        }
+        AudioRoute earpieceRoute = mTypeRoutes.get(AudioRoute.TYPE_EARPIECE);
+        if (earpieceRoute != null) {
+            mAvailableRoutes.add(earpieceRoute);
+            mEarpieceWiredRoute = earpieceRoute;
+        }
+        onAvailableRoutesChanged();
+
+        // Route to expected state
+        if (mCurrentRoute.equals(wiredHeadsetRoute)) {
+            routeTo(mIsActive, getBaseRoute(true, null));
+        }
+    }
+
+    private void handleDockConnected() {
+        AudioRoute dockRoute = null;
+        try {
+            dockRoute = mAudioRouteFactory.create(AudioRoute.TYPE_DOCK, null, mAudioManager);
+        } catch (IllegalArgumentException e) {
+            Log.e(this, e, "Can't find available audio device info for route type:"
+                    + AudioRoute.DEVICE_TYPE_STRINGS.get(AudioRoute.TYPE_WIRED));
+        }
+
+        if (dockRoute != null) {
+            mAvailableRoutes.add(dockRoute);
+            mAvailableRoutes.remove(mSpeakerDockRoute);
+            mTypeRoutes.put(AudioRoute.TYPE_DOCK, dockRoute);
+            mSpeakerDockRoute = dockRoute;
+            routeTo(mIsActive, dockRoute);
+            onAvailableRoutesChanged();
+        }
+    }
+
+    public void handleDockDisconnected() {
+        // Update audio route states
+        AudioRoute dockRoute = mTypeRoutes.get(AudioRoute.TYPE_DOCK);
+        if (dockRoute != null) {
+            mAvailableRoutes.remove(dockRoute);
+            mSpeakerDockRoute = null;
+        }
+        AudioRoute speakerRoute = mTypeRoutes.get(AudioRoute.TYPE_SPEAKER);
+        if (speakerRoute != null) {
+            mAvailableRoutes.add(speakerRoute);
+            mSpeakerDockRoute = speakerRoute;
+        }
+        onAvailableRoutesChanged();
+
+        // Route to expected state
+        if (mCurrentRoute.equals(dockRoute)) {
+            routeTo(mIsActive, getBaseRoute(true, null));
+        }
+    }
+
+    private void handleStreamingEnabled() {
+        if (!mCurrentRoute.equals(mStreamingRoute)) {
+            routeTo(mIsActive, mStreamingRoute);
+        } else {
+            Log.i(this, "ignore enable streaming, already in streaming");
+        }
+    }
+
+    private void handleStreamingDisabled() {
+        if (mCurrentRoute.equals(mStreamingRoute)) {
+            mCurrentRoute = DUMMY_ROUTE;
+            onAvailableRoutesChanged();
+            routeTo(mIsActive, getBaseRoute(true, null));
+        } else {
+            Log.i(this, "ignore disable streaming, not in streaming");
+        }
+    }
+
+    /**
+     * Handles the case when SCO audio is connected for the BT headset. This follows shortly after
+     * the BT device has been established as an active device (BT_ACTIVE_DEVICE_PRESENT) and doesn't
+     * apply to other BT device types. In this case, the pending audio route will process the
+     * BT_AUDIO_CONNECTED message that will trigger routing to the pending destination audio route;
+     * otherwise, routing will be ignored if there aren't pending routes to be processed.
+     *
+     * Message being handled: BT_AUDIO_CONNECTED
+     */
+    private void handleBtAudioActive(BluetoothDevice bluetoothDevice) {
+        if (mIsPending) {
+            Log.i(this, "handleBtAudioActive: is pending path");
+            if (Objects.equals(mPendingAudioRoute.getDestRoute().getBluetoothAddress(),
+                    bluetoothDevice.getAddress())) {
+                mPendingAudioRoute.onMessageReceived(new Pair<>(BT_AUDIO_CONNECTED,
+                        bluetoothDevice.getAddress()), null);
+            }
+        } else {
+            // ignore, not triggered by telecom
+            Log.i(this, "handleBtAudioActive: ignoring handling bt audio active.");
+        }
+    }
+
+    /**
+     * Handles the case when SCO audio is disconnected for the BT headset. In this case, the pending
+     * audio route will process the BT_AUDIO_DISCONNECTED message which will trigger routing to the
+     * pending destination audio route; otherwise, routing will be ignored if there aren't any
+     * pending routes to be processed.
+     *
+     * Message being handled: BT_AUDIO_DISCONNECTED
+     */
+    private void handleBtAudioInactive(BluetoothDevice bluetoothDevice) {
+        if (mIsPending) {
+            Log.i(this, "handleBtAudioInactive: is pending path");
+            if (Objects.equals(mPendingAudioRoute.getOrigRoute().getBluetoothAddress(),
+                    bluetoothDevice.getAddress())) {
+                mPendingAudioRoute.onMessageReceived(new Pair<>(BT_AUDIO_DISCONNECTED,
+                        bluetoothDevice.getAddress()), null);
+            }
+        } else {
+            // ignore, not triggered by telecom
+            Log.i(this, "handleBtAudioInactive: ignoring handling bt audio inactive.");
+        }
+    }
+
+    /**
+     * This particular routing occurs when the BT device is trying to establish itself as a
+     * connected device (refer to BluetoothStateReceiver#handleConnectionStateChanged). The device
+     * is included as an available route and cached into the current BT routes.
+     *
+     * Message being handled: BT_DEVICE_ADDED
+     */
+    private void handleBtConnected(@AudioRoute.AudioRouteType int type,
+                                   BluetoothDevice bluetoothDevice) {
+        if (containsHearingAidPair(type, bluetoothDevice)) {
+            return;
+        }
+
+        AudioRoute bluetoothRoute = mAudioRouteFactory.create(type, bluetoothDevice.getAddress(),
+                mAudioManager);
+        if (bluetoothRoute == null) {
+            Log.w(this, "Can't find available audio device info for route type:"
+                    + AudioRoute.DEVICE_TYPE_STRINGS.get(type));
+        } else {
+            Log.i(this, "bluetooth route added: " + bluetoothRoute);
+            mAvailableRoutes.add(bluetoothRoute);
+            mBluetoothRoutes.put(bluetoothRoute, bluetoothDevice);
+            onAvailableRoutesChanged();
+        }
+    }
+
+    /**
+     * Handles the case when the BT device is in a disconnecting/disconnected state. In this case,
+     * the audio route for the specified device is removed from the available BT routes and the
+     * audio is routed to an available route if the current route is pointing to the device which
+     * got disconnected.
+     *
+     * Message being handled: BT_DEVICE_REMOVED
+     */
+    private void handleBtDisconnected(@AudioRoute.AudioRouteType int type,
+                                      BluetoothDevice bluetoothDevice) {
+        // Clean up unavailable routes
+        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)) {
+            routeTo(mIsActive, getBaseRoute(true, null));
+        }
+    }
+
+    /**
+     * This particular routing occurs when the specified bluetooth device is marked as the active
+     * device (refer to BluetoothStateReceiver#handleActiveDeviceChanged). This takes care of
+     * moving the call audio route to the bluetooth route.
+     *
+     * Message being handled: BT_ACTIVE_DEVICE_PRESENT
+     */
+    private void handleBtActiveDevicePresent(@AudioRoute.AudioRouteType int type,
+                                             String deviceAddress) {
+        AudioRoute bluetoothRoute = getBluetoothRoute(type, deviceAddress);
+        if (bluetoothRoute != null) {
+            Log.i(this, "request to route to bluetooth route: %s (active=%b)", bluetoothRoute,
+                    mIsActive);
+            routeTo(mIsActive, bluetoothRoute);
+        } else {
+            Log.i(this, "request to route to unavailable bluetooth route - type (%s), address (%s)",
+                    type, deviceAddress);
+        }
+    }
+
+    /**
+     * Handles routing for when the active BT device is removed for a given audio route type. In
+     * this case, the audio is routed to another available route if the current route hasn't been
+     * adjusted yet or there is a pending destination route associated with the device type that
+     * went inactive. Note that BT_DEVICE_REMOVED will be processed first in this case, which will
+     * handle removing the BT route for the device that went inactive as well as falling back to
+     * an available route.
+     *
+     * Message being handled: BT_ACTIVE_DEVICE_GONE
+     */
+    private void handleBtActiveDeviceGone(@AudioRoute.AudioRouteType int type) {
+        if ((mIsPending && mPendingAudioRoute.getDestRoute().getType() == type)
+                || (!mIsPending && mCurrentRoute.getType() == type)) {
+            // Fallback to an available route
+            routeTo(mIsActive, getBaseRoute(true, null));
+        }
+    }
+
+    private void handleMuteChanged(boolean mute) {
+        mIsMute = mute;
+        if (mIsMute != mAudioManager.isMicrophoneMute() && mIsActive) {
+            IAudioService audioService = mAudioServiceFactory.getAudioService();
+            Log.i(this, "changing microphone mute state to: %b [serviceIsNull=%b]", mute,
+                    audioService == null);
+            if (audioService != null) {
+                try {
+                    audioService.setMicrophoneMute(mute, mContext.getOpPackageName(),
+                            mCallsManager.getCurrentUserHandle().getIdentifier(),
+                            mContext.getAttributionTag());
+                } catch (RemoteException e) {
+                    Log.e(this, e, "Remote exception while toggling mute.");
+                    return;
+                }
+            }
+        }
+        onMuteStateChanged(mIsMute);
+    }
+
+    private void handleSwitchFocus(int focus) {
+        Log.i(this, "handleSwitchFocus: focus (%s)", focus);
+        mFocusType = focus;
+        switch (focus) {
+            case NO_FOCUS -> {
+                if (mIsActive) {
+                    // Reset mute state after call ends.
+                    handleMuteChanged(false);
+                    // Route back to inactive route.
+                    routeTo(false, mCurrentRoute);
+                    // Clear pending messages
+                    mPendingAudioRoute.clearPendingMessages();
+                }
+            }
+            case ACTIVE_FOCUS -> {
+                // Route to active baseline route (we may need to change audio route in the case
+                // when a video call is put on hold).
+                routeTo(true, getBaseRoute(true, null));
+            }
+            case RINGING_FOCUS -> {
+                if (!mIsActive) {
+                    AudioRoute route = getBaseRoute(true, null);
+                    BluetoothDevice device = mBluetoothRoutes.get(route);
+                    // Check if in-band ringtone is enabled for the device; if it isn't, move to
+                    // inactive route.
+                    if (device != null && !mBluetoothRouteManager.isInbandRingEnabled(device)) {
+                        routeTo(false, route);
+                    } else {
+                        routeTo(true, route);
+                    }
+                } else {
+                    // Route is already active.
+                    BluetoothDevice device = mBluetoothRoutes.get(mCurrentRoute);
+                    if (device != null && !mBluetoothRouteManager.isInbandRingEnabled(device)) {
+                        routeTo(false, mCurrentRoute);
+                    }
+                }
+            }
+        }
+    }
+
+    public void handleSwitchEarpiece() {
+        AudioRoute earpieceRoute = mTypeRoutes.get(AudioRoute.TYPE_EARPIECE);
+        if (earpieceRoute != null && getAvailableRoutes().contains(earpieceRoute)) {
+            routeTo(mIsActive, earpieceRoute);
+        } else {
+            Log.i(this, "ignore switch earpiece request");
+        }
+    }
+
+    private void handleSwitchBluetooth(String address) {
+        Log.i(this, "handle switch to bluetooth with address %s", address);
+        AudioRoute bluetoothRoute = null;
+        BluetoothDevice bluetoothDevice = null;
+        if (address == null) {
+            bluetoothRoute = getArbitraryBluetoothDevice();
+            bluetoothDevice = mBluetoothRoutes.get(bluetoothRoute);
+        } else {
+            for (AudioRoute route : getAvailableRoutes()) {
+                if (Objects.equals(address, route.getBluetoothAddress())) {
+                    bluetoothRoute = route;
+                    bluetoothDevice = mBluetoothRoutes.get(route);
+                    break;
+                }
+            }
+        }
+
+        if (bluetoothRoute != null && bluetoothDevice != null) {
+            if (mFocusType == RINGING_FOCUS) {
+                routeTo(mBluetoothRouteManager.isInbandRingEnabled(bluetoothDevice) && mIsActive,
+                        bluetoothRoute);
+            } else {
+                routeTo(mIsActive, bluetoothRoute);
+            }
+        } else {
+            Log.i(this, "ignore switch bluetooth request to unavailable address");
+        }
+    }
+
+    /**
+     * Retrieve the active BT device, if available, otherwise return the most recently tracked
+     * active device, or null if none are available.
+     * @return {@link AudioRoute} of the BT device.
+     */
+    private AudioRoute getArbitraryBluetoothDevice() {
+        if (mActiveBluetoothDevice != null) {
+            return getBluetoothRoute(mActiveBluetoothDevice.first, mActiveBluetoothDevice.second);
+        } else if (!mBluetoothRoutes.isEmpty()) {
+            return mBluetoothRoutes.keySet().stream().toList().get(mBluetoothRoutes.size() - 1);
+        }
+        return null;
+    }
+
+    private void handleSwitchHeadset() {
+        AudioRoute headsetRoute = mTypeRoutes.get(AudioRoute.TYPE_WIRED);
+        if (headsetRoute != null && getAvailableRoutes().contains(headsetRoute)) {
+            routeTo(mIsActive, headsetRoute);
+        } else {
+            Log.i(this, "ignore switch headset request");
+        }
+    }
+
+    private void handleSwitchSpeaker() {
+        if (mSpeakerDockRoute != null && getAvailableRoutes().contains(mSpeakerDockRoute)) {
+            routeTo(mIsActive, mSpeakerDockRoute);
+        } else {
+            Log.i(this, "ignore switch speaker request");
+        }
+    }
+
+    private void handleSwitchBaselineRoute(boolean includeBluetooth, String btAddressToExclude) {
+        routeTo(mIsActive, getBaseRoute(includeBluetooth, btAddressToExclude));
+    }
+
+    private void handleSpeakerOn() {
+        if (isPending()) {
+            Log.i(this, "handleSpeakerOn: sending SPEAKER_ON to pending audio route");
+            mPendingAudioRoute.onMessageReceived(new Pair<>(SPEAKER_ON, null), null);
+            // Update status bar notification if we are in a call.
+            mStatusBarNotifier.notifySpeakerphone(mCallsManager.hasAnyCalls());
+        } else {
+            if (mSpeakerDockRoute != null && getAvailableRoutes().contains(mSpeakerDockRoute)) {
+                routeTo(mIsActive, mSpeakerDockRoute);
+                // Since the route switching triggered by this message, we need to manually send it
+                // again so that we won't stuck in the pending route
+                if (mIsActive) {
+                    sendMessageWithSessionInfo(SPEAKER_ON);
+                }
+            }
+        }
+    }
+
+    private void handleSpeakerOff() {
+        if (isPending()) {
+            Log.i(this, "handleSpeakerOff - sending SPEAKER_OFF to pending audio route");
+            mPendingAudioRoute.onMessageReceived(new Pair<>(SPEAKER_OFF, null), null);
+            // Update status bar notification
+            mStatusBarNotifier.notifySpeakerphone(false);
+        } else if (mCurrentRoute.getType() == AudioRoute.TYPE_SPEAKER) {
+            routeTo(mIsActive, getBaseRoute(true, null));
+            // Since the route switching triggered by this message, we need to manually send it
+            // again so that we won't stuck in the pending route
+            if (mIsActive) {
+                sendMessageWithSessionInfo(SPEAKER_OFF);
+            }
+            onAvailableRoutesChanged();
+        }
+    }
+
+    /**
+     * This is invoked when there are no more pending audio routes to be processed, which signals
+     * a change for the current audio route and the call audio state to be updated accordingly.
+     */
+    public void handleExitPendingRoute() {
+        if (mIsPending) {
+            mCurrentRoute = mPendingAudioRoute.getDestRoute();
+            Log.addEvent(mCallsManager.getForegroundCall(), LogUtils.Events.AUDIO_ROUTE,
+                    "Entering audio route: " + mCurrentRoute + " (active=" + mIsActive + ")");
+            mIsPending = false;
+            mPendingAudioRoute.clearPendingMessages();
+            onCurrentRouteChanged();
+        }
+    }
+
+    private void onCurrentRouteChanged() {
+        synchronized (mLock) {
+            BluetoothDevice activeBluetoothDevice = null;
+            int route = ROUTE_MAP.get(mCurrentRoute.getType());
+            if (route == CallAudioState.ROUTE_STREAMING) {
+                updateCallAudioState(new CallAudioState(mIsMute, route, route));
+                return;
+            }
+            if (route == CallAudioState.ROUTE_BLUETOOTH) {
+                activeBluetoothDevice = mBluetoothRoutes.get(mCurrentRoute);
+            }
+            updateCallAudioState(new CallAudioState(mIsMute, route,
+                    mCallAudioState.getRawSupportedRouteMask(), activeBluetoothDevice,
+                    mCallAudioState.getSupportedBluetoothDevices()));
+        }
+    }
+
+    private void onAvailableRoutesChanged() {
+        synchronized (mLock) {
+            int routeMask = 0;
+            Set<BluetoothDevice> availableBluetoothDevices = new HashSet<>();
+            for (AudioRoute route : getAvailableRoutes()) {
+                routeMask |= ROUTE_MAP.get(route.getType());
+                if (BT_AUDIO_ROUTE_TYPES.contains(route.getType())) {
+                    BluetoothDevice deviceToAdd = mBluetoothRoutes.get(route);
+                    // Only include the lead device for LE audio (otherwise, the routes will show
+                    // two separate devices in the UI).
+                    if (deviceToAdd != null && route.getType() == AudioRoute.TYPE_BLUETOOTH_LE
+                            && getLeAudioService() != null) {
+                        int groupId = getLeAudioService().getGroupId(deviceToAdd);
+                        if (groupId != BluetoothLeAudio.GROUP_ID_INVALID) {
+                            deviceToAdd = getLeAudioService().getConnectedGroupLeadDevice(groupId);
+                        }
+                    }
+                    // This will only ever be null when the lead device (LE) is disconnected and
+                    // try to obtain the lead device for the 2nd bud.
+                    if (deviceToAdd != null) {
+                        availableBluetoothDevices.add(deviceToAdd);
+                    }
+                }
+            }
+            updateCallAudioState(new CallAudioState(mIsMute, mCallAudioState.getRoute(), routeMask,
+                    mCallAudioState.getActiveBluetoothDevice(), availableBluetoothDevices));
+        }
+    }
+
+    private void onMuteStateChanged(boolean mute) {
+        updateCallAudioState(new CallAudioState(mute, mCallAudioState.getRoute(),
+                mCallAudioState.getSupportedRouteMask(), mCallAudioState.getActiveBluetoothDevice(),
+                mCallAudioState.getSupportedBluetoothDevices()));
+    }
+
+    private void updateCallAudioState(CallAudioState newCallAudioState) {
+        Log.i(this, "updateCallAudioState: updating call audio state to %s", newCallAudioState);
+        CallAudioState oldState = mCallAudioState;
+        mCallAudioState = newCallAudioState;
+        // Update status bar notification
+        mStatusBarNotifier.notifyMute(newCallAudioState.isMuted());
+        mCallsManager.onCallAudioStateChanged(oldState, mCallAudioState);
+        updateAudioStateForTrackedCalls(mCallAudioState);
+    }
+
+    private void updateAudioStateForTrackedCalls(CallAudioState newCallAudioState) {
+        Set<Call> calls = mCallsManager.getTrackedCalls();
+        for (Call call : calls) {
+            if (call != null && call.getConnectionService() != null) {
+                call.getConnectionService().onCallAudioStateChanged(call, newCallAudioState);
+            }
+        }
+    }
+
+    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);
+        Log.i(this, "getPreferredAudioRouteFromStrategy: preferred device is %s", deviceAttr);
+        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());
+
+        }
+    }
+
+    private AudioRoute getPreferredAudioRouteFromDefault(boolean includeBluetooth,
+            String btAddressToExclude) {
+        boolean skipEarpiece;
+        Call foregroundCall = mCallAudioManager.getForegroundCall();
+        synchronized (mTelecomLock) {
+            skipEarpiece = foregroundCall != null
+                    && VideoProfile.isVideo(foregroundCall.getVideoState());
+        }
+        // Route to earpiece, wired, or speaker route if there are not bluetooth routes or if there
+        // are only wearables available.
+        AudioRoute activeWatchOrNonWatchDeviceRoute =
+                getActiveWatchOrNonWatchDeviceRoute(btAddressToExclude);
+        if (mBluetoothRoutes.isEmpty() || !includeBluetooth
+                || activeWatchOrNonWatchDeviceRoute == null) {
+            Log.i(this, "getPreferredAudioRouteFromDefault: Audio routing defaulting to "
+                    + "available non-BT route.");
+            AudioRoute defaultRoute = mEarpieceWiredRoute != null
+                    ? mEarpieceWiredRoute
+                    : mSpeakerDockRoute;
+            // Ensure that we default to speaker route if we're in a video call, but disregard it if
+            // a wired headset is plugged in.
+            if (skipEarpiece && defaultRoute.getType() == AudioRoute.TYPE_EARPIECE) {
+                Log.i(this, "getPreferredAudioRouteFromDefault: Audio routing defaulting to "
+                        + "speaker route for video call.");
+                defaultRoute = mSpeakerDockRoute;
+            }
+            return defaultRoute;
+        } else {
+            // Most recent active route will always be the last in the array (ensure that we don't
+            // auto route to a wearable device unless it's already active).
+            String autoRoutingToWatchExcerpt = mFeatureFlags.ignoreAutoRouteToWatchDevice()
+                    ? " (except watch)"
+                    : "";
+            Log.i(this, "getPreferredAudioRouteFromDefault: Audio routing defaulting to "
+                    + "most recently active BT route" + autoRoutingToWatchExcerpt + ".");
+            return activeWatchOrNonWatchDeviceRoute;
+        }
+    }
+
+    private int calculateSupportedRouteMaskInit() {
+        Log.i(this, "calculateSupportedRouteMaskInit: is wired headset plugged in - %s",
+                mWiredHeadsetManager.isPluggedIn());
+        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;
+    }
+
+    @VisibleForTesting
+    public Set<AudioRoute> getAvailableRoutes() {
+        if (mCurrentRoute.equals(mStreamingRoute)) {
+            return mStreamingRoutes;
+        } else {
+            return mAvailableRoutes;
+        }
+    }
+
+    public AudioRoute getCurrentRoute() {
+        return mCurrentRoute;
+    }
+
+    public 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;
+    }
+
+    public AudioRoute getBaseRoute(boolean includeBluetooth, String btAddressToExclude) {
+        AudioRoute destRoute = getPreferredAudioRouteFromStrategy();
+        if (destRoute == null || (destRoute.getBluetoothAddress() != null && !includeBluetooth)) {
+            destRoute = getPreferredAudioRouteFromDefault(includeBluetooth, btAddressToExclude);
+        }
+        if (destRoute != null && !getAvailableRoutes().contains(destRoute)) {
+            destRoute = null;
+        }
+        Log.i(this, "getBaseRoute - audio routing to %s", destRoute);
+        return destRoute;
+    }
+
+    /**
+     * Don't add additional AudioRoute when a hearing aid pair is detected. The devices have
+     * separate addresses, so we need to perform explicit handling to ensure we don't
+     * treat them as two separate devices.
+     */
+    private boolean containsHearingAidPair(@AudioRoute.AudioRouteType int type,
+            BluetoothDevice bluetoothDevice) {
+        // Check if it is a hearing aid pair and skip connecting to the other device in this case.
+        // Traverse mBluetoothRoutes backwards as the most recently active device will be inserted
+        // last.
+        String existingHearingAidAddress = null;
+        List<AudioRoute> bluetoothRoutes = mBluetoothRoutes.keySet().stream().toList();
+        for (int i = bluetoothRoutes.size() - 1; i >= 0; i--) {
+            AudioRoute audioRoute = bluetoothRoutes.get(i);
+            if (audioRoute.getType() == AudioRoute.TYPE_BLUETOOTH_HA) {
+                existingHearingAidAddress = audioRoute.getBluetoothAddress();
+                break;
+            }
+        }
+
+        // Check that route is for hearing aid and that there exists another hearing aid route
+        // created for the first device (of the pair) that was connected.
+        if (type == AudioRoute.TYPE_BLUETOOTH_HA && existingHearingAidAddress != null) {
+            BluetoothAdapter bluetoothAdapter = mBluetoothRouteManager.getDeviceManager()
+                    .getBluetoothAdapter();
+            if (bluetoothAdapter != null) {
+                List<BluetoothDevice> activeHearingAids =
+                        bluetoothAdapter.getActiveDevices(BluetoothProfile.HEARING_AID);
+                for (BluetoothDevice hearingAid : activeHearingAids) {
+                    if (hearingAid != null && hearingAid.getAddress() != null) {
+                        String address = hearingAid.getAddress();
+                        if (address.equals(bluetoothDevice.getAddress())
+                                || address.equals(existingHearingAidAddress)) {
+                            Log.i(this, "containsHearingAidPair: Detected a hearing aid "
+                                    + "pair, ignoring creating a new AudioRoute");
+                            return true;
+                        }
+                    }
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Prevent auto routing to a wearable device when calculating the default bluetooth audio route
+     * to move to. This function ensures that the most recently active non-wearable device is
+     * selected for routing unless a wearable device has already been identified as an active
+     * device.
+     */
+    private AudioRoute getActiveWatchOrNonWatchDeviceRoute(String btAddressToExclude) {
+        if (!mFeatureFlags.ignoreAutoRouteToWatchDevice()) {
+            Log.i(this, "getActiveWatchOrNonWatchDeviceRoute: ignore_auto_route_to_watch_device "
+                    + "flag is disabled. Routing to most recently reported active device.");
+            return getMostRecentlyActiveBtRoute(btAddressToExclude);
+        }
+
+        List<AudioRoute> bluetoothRoutes = mBluetoothRoutes.keySet().stream().toList();
+        // Traverse the routes from the most recently active recorded devices first.
+        AudioRoute nonWatchDeviceRoute = null;
+        for (int i = bluetoothRoutes.size() - 1; i >= 0; i--) {
+            AudioRoute route = bluetoothRoutes.get(i);
+            BluetoothDevice device = mBluetoothRoutes.get(route);
+            // Skip excluded BT address and LE audio if it's not the lead device.
+            if (route.getBluetoothAddress().equals(btAddressToExclude)
+                    || isLeAudioNonLeadDeviceOrServiceUnavailable(route.getType(), device)) {
+                continue;
+            }
+            // Check if the most recently active device is a watch device.
+            if (i == (bluetoothRoutes.size() - 1) && device.equals(mCallAudioState
+                    .getActiveBluetoothDevice()) && mBluetoothRouteManager.isWatch(device)) {
+                Log.i(this, "getActiveWatchOrNonWatchDeviceRoute: Routing to active watch - %s",
+                        bluetoothRoutes.get(0));
+                return bluetoothRoutes.get(0);
+            }
+            // Record the first occurrence of a non-watch device route if found.
+            if (!mBluetoothRouteManager.isWatch(device) && nonWatchDeviceRoute == null) {
+                nonWatchDeviceRoute = route;
+                break;
+            }
+        }
+
+        Log.i(this, "Routing to a non-watch device - %s", nonWatchDeviceRoute);
+        return nonWatchDeviceRoute;
+    }
+
+    /**
+     * Returns the most actively reported bluetooth route excluding the passed in route.
+     */
+    private AudioRoute getMostRecentlyActiveBtRoute(String btAddressToExclude) {
+        List<AudioRoute> bluetoothRoutes = mBluetoothRoutes.keySet().stream().toList();
+        for (int i = bluetoothRoutes.size() - 1; i >= 0; i--) {
+            AudioRoute route = bluetoothRoutes.get(i);
+            // Skip LE route if it's not the lead device.
+            if (isLeAudioNonLeadDeviceOrServiceUnavailable(
+                    route.getType(), mBluetoothRoutes.get(route))) {
+                continue;
+            }
+            if (!route.getBluetoothAddress().equals(btAddressToExclude)) {
+                return route;
+            }
+        }
+        return null;
+    }
+
+    private boolean isLeAudioNonLeadDeviceOrServiceUnavailable(@AudioRoute.AudioRouteType int type,
+            BluetoothDevice device) {
+        if (type != AudioRoute.TYPE_BLUETOOTH_LE) {
+            return false;
+        } else if (getLeAudioService() == null) {
+            return true;
+        }
+
+        int groupId = getLeAudioService().getGroupId(device);
+        if (groupId != BluetoothLeAudio.GROUP_ID_INVALID) {
+            BluetoothDevice leadDevice = getLeAudioService().getConnectedGroupLeadDevice(groupId);
+            Log.i(this, "Lead device for device (%s) is %s.", device, leadDevice);
+            return leadDevice == null || !device.getAddress().equals(leadDevice.getAddress());
+        }
+        return false;
+    }
+
+    private BluetoothLeAudio getLeAudioService() {
+        return mBluetoothRouteManager.getDeviceManager().getLeAudioService();
+    }
+
+    @VisibleForTesting
+    public void setAudioManager(AudioManager audioManager) {
+        mAudioManager = audioManager;
+    }
+
+    @VisibleForTesting
+    public void setAudioRouteFactory(AudioRoute.Factory audioRouteFactory) {
+        mAudioRouteFactory = audioRouteFactory;
+    }
+
+    public Map<AudioRoute, BluetoothDevice> getBluetoothRoutes() {
+        return mBluetoothRoutes;
+    }
+
+    public void overrideIsPending(boolean isPending) {
+        mIsPending = isPending;
+    }
+
+    public void setIsScoAudioConnected(boolean value) {
+        mIsScoAudioConnected = value;
+    }
+
+    /**
+     * Update the active bluetooth device being tracked (as well as for individual profiles).
+     * We need to keep track of active devices for individual profiles because of potential
+     * inconsistencies found in BluetoothStateReceiver#handleActiveDeviceChanged. When multiple
+     * profiles are paired, we could have a scenario where an active device A is replaced
+     * with an active device B (from a different profile), which is then removed as an active
+     * device shortly after, causing device A to be reactive. It's possible that the active device
+     * changed intent is never received again for device A so an active device cache is necessary
+     * to track these devices at a profile level.
+     * @param device {@link Pair} containing the BT audio route type (i.e. SCO/HA/LE) and the
+     *                           address of the device.
+     */
+    public void updateActiveBluetoothDevice(Pair<Integer, String> device) {
+        mActiveDeviceCache.put(device.first, device.second);
+        // Update most recently active device if address isn't null (meaning some device is active).
+        if (device.second != null) {
+            mActiveBluetoothDevice = device;
+        } else {
+            // If a device was removed, check to ensure that no other device is still considered
+            // active.
+            boolean hasActiveDevice = false;
+            for (String address : mActiveDeviceCache.values()) {
+                if (address != null) {
+                    hasActiveDevice = true;
+                    break;
+                }
+            }
+            if (!hasActiveDevice) {
+                mActiveBluetoothDevice = null;
+            }
+        }
+    }
+
+    @VisibleForTesting
+    public void setActive(boolean active) {
+        if (active) {
+            mFocusType = ACTIVE_FOCUS;
+        } else {
+            mFocusType = NO_FOCUS;
+        }
+        mIsActive = active;
+    }
+}
diff --git a/src/com/android/server/telecom/CallAudioRoutePeripheralAdapter.java b/src/com/android/server/telecom/CallAudioRoutePeripheralAdapter.java
index d96f953..8a87c22 100644
--- a/src/com/android/server/telecom/CallAudioRoutePeripheralAdapter.java
+++ b/src/com/android/server/telecom/CallAudioRoutePeripheralAdapter.java
@@ -25,16 +25,19 @@
 public class CallAudioRoutePeripheralAdapter implements WiredHeadsetManager.Listener,
         DockManager.Listener, BluetoothRouteManager.BluetoothStateListener {
 
-    private final CallAudioRouteStateMachine mCallAudioRouteStateMachine;
+    private final CallAudioRouteAdapter mCallAudioAdapter;
     private final BluetoothRouteManager mBluetoothRouteManager;
+    private final AsyncRingtonePlayer mRingtonePlayer;
 
     public CallAudioRoutePeripheralAdapter(
-            CallAudioRouteStateMachine callAudioRouteStateMachine,
+            CallAudioRouteAdapter callAudioRouteAdapter,
             BluetoothRouteManager bluetoothManager,
             WiredHeadsetManager wiredHeadsetManager,
-            DockManager dockManager) {
-        mCallAudioRouteStateMachine = callAudioRouteStateMachine;
+            DockManager dockManager,
+            AsyncRingtonePlayer ringtonePlayer) {
+        mCallAudioAdapter = callAudioRouteAdapter;
         mBluetoothRouteManager = bluetoothManager;
+        mRingtonePlayer = ringtonePlayer;
 
         mBluetoothRouteManager.setListener(this);
         wiredHeadsetManager.addListener(this);
@@ -57,37 +60,47 @@
 
     @Override
     public void onBluetoothDeviceListChanged() {
-        mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+        mCallAudioAdapter.sendMessageWithSessionInfo(
                 CallAudioRouteStateMachine.BLUETOOTH_DEVICE_LIST_CHANGED);
     }
 
     @Override
     public void onBluetoothActiveDevicePresent() {
-        mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+        mCallAudioAdapter.sendMessageWithSessionInfo(
                 CallAudioRouteStateMachine.BT_ACTIVE_DEVICE_PRESENT);
     }
 
     @Override
     public void onBluetoothActiveDeviceGone() {
-        mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+        mCallAudioAdapter.sendMessageWithSessionInfo(
                 CallAudioRouteStateMachine.BT_ACTIVE_DEVICE_GONE);
     }
 
     @Override
     public void onBluetoothAudioConnected() {
-        mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+        mRingtonePlayer.updateBtActiveState(true);
+        mCallAudioAdapter.sendMessageWithSessionInfo(
+                CallAudioRouteStateMachine.BT_AUDIO_CONNECTED);
+    }
+
+    @Override
+    public void onBluetoothAudioConnecting() {
+        mRingtonePlayer.updateBtActiveState(false);
+        // Pretend like audio is connected when communicating w/ CARSM.
+        mCallAudioAdapter.sendMessageWithSessionInfo(
                 CallAudioRouteStateMachine.BT_AUDIO_CONNECTED);
     }
 
     @Override
     public void onBluetoothAudioDisconnected() {
-        mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+        mRingtonePlayer.updateBtActiveState(false);
+        mCallAudioAdapter.sendMessageWithSessionInfo(
                 CallAudioRouteStateMachine.BT_AUDIO_DISCONNECTED);
     }
 
     @Override
     public void onUnexpectedBluetoothStateChange() {
-        mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+        mCallAudioAdapter.sendMessageWithSessionInfo(
                 CallAudioRouteStateMachine.UPDATE_SYSTEM_AUDIO_ROUTE);
     }
 
@@ -98,17 +111,17 @@
     @Override
     public void onWiredHeadsetPluggedInChanged(boolean oldIsPluggedIn, boolean newIsPluggedIn) {
         if (!oldIsPluggedIn && newIsPluggedIn) {
-            mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+            mCallAudioAdapter.sendMessageWithSessionInfo(
                     CallAudioRouteStateMachine.CONNECT_WIRED_HEADSET);
         } else if (oldIsPluggedIn && !newIsPluggedIn){
-            mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+            mCallAudioAdapter.sendMessageWithSessionInfo(
                     CallAudioRouteStateMachine.DISCONNECT_WIRED_HEADSET);
         }
     }
 
     @Override
     public void onDockChanged(boolean isDocked) {
-        mCallAudioRouteStateMachine.sendMessageWithSessionInfo(
+        mCallAudioAdapter.sendMessageWithSessionInfo(
                 isDocked ? CallAudioRouteStateMachine.CONNECT_DOCK
                         : CallAudioRouteStateMachine.DISCONNECT_DOCK
         );
diff --git a/src/com/android/server/telecom/CallAudioRouteStateMachine.java b/src/com/android/server/telecom/CallAudioRouteStateMachine.java
index 4a03726..74d23a9 100644
--- a/src/com/android/server/telecom/CallAudioRouteStateMachine.java
+++ b/src/com/android/server/telecom/CallAudioRouteStateMachine.java
@@ -28,6 +28,7 @@
 import android.media.AudioManager;
 import android.media.IAudioService;
 import android.os.Binder;
+import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
 import android.os.RemoteException;
@@ -44,6 +45,7 @@
 import com.android.internal.util.State;
 import com.android.internal.util.StateMachine;
 import com.android.server.telecom.bluetooth.BluetoothRouteManager;
+import com.android.server.telecom.flags.FeatureFlags;
 
 import java.util.Collection;
 import java.util.HashMap;
@@ -72,7 +74,7 @@
  *     from a wired headset
  * mIsMuted: a boolean indicating whether the audio is muted
  */
-public class CallAudioRouteStateMachine extends StateMachine {
+public class CallAudioRouteStateMachine extends StateMachine implements CallAudioRouteAdapter {
 
     public static class Factory {
         public CallAudioRouteStateMachine create(
@@ -83,7 +85,9 @@
                 StatusBarNotifier statusBarNotifier,
                 CallAudioManager.AudioServiceFactory audioServiceFactory,
                 int earpieceControl,
-                Executor asyncTaskExecutor) {
+                Executor asyncTaskExecutor,
+                CallAudioCommunicationDeviceTracker communicationDeviceTracker,
+                FeatureFlags featureFlags) {
             return new CallAudioRouteStateMachine(context,
                     callsManager,
                     bluetoothManager,
@@ -91,7 +95,9 @@
                     statusBarNotifier,
                     audioServiceFactory,
                     earpieceControl,
-                    asyncTaskExecutor);
+                    asyncTaskExecutor,
+                    communicationDeviceTracker,
+                    featureFlags);
         }
     }
     /** Values for CallAudioRouteStateMachine constructor's earPieceRouting arg. */
@@ -114,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;
@@ -183,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";
@@ -371,11 +281,20 @@
         public void enter() {
             super.enter();
             setSpeakerphoneOn(false);
+            if (mFeatureFlags.callAudioCommunicationDeviceRefactor()) {
+                mCommunicationDeviceTracker.setCommunicationDevice(
+                        AudioDeviceInfo.TYPE_BUILTIN_EARPIECE, null);
+            }
             CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_EARPIECE,
                     mAvailableRoutes, null,
                     mBluetoothRouteManager.getConnectedDevices());
-            setSystemAudioState(newState, true);
-            updateInternalCallAudioState();
+            if (mFeatureFlags.earlyUpdateInternalCallAudioState()) {
+                updateInternalCallAudioState();
+                setSystemAudioState(newState, true);
+            } else {
+                setSystemAudioState(newState, true);
+                updateInternalCallAudioState();
+            }
         }
 
         @Override
@@ -401,6 +320,10 @@
                 case SWITCH_BLUETOOTH:
                 case USER_SWITCH_BLUETOOTH:
                     if ((mAvailableRoutes & ROUTE_BLUETOOTH) != 0) {
+                        if (mFeatureFlags.callAudioCommunicationDeviceRefactor()) {
+                            mCommunicationDeviceTracker.clearCommunicationDevice(
+                                    AudioDeviceInfo.TYPE_BUILTIN_EARPIECE);
+                        }
                         if (mAudioFocusType == ACTIVE_FOCUS
                                 || mBluetoothRouteManager.isInbandRingingEnabled()) {
                             String address = (msg.obj instanceof SomeArgs) ?
@@ -417,6 +340,10 @@
                 case SWITCH_HEADSET:
                 case USER_SWITCH_HEADSET:
                     if ((mAvailableRoutes & ROUTE_WIRED_HEADSET) != 0) {
+                        if (mFeatureFlags.callAudioCommunicationDeviceRefactor()) {
+                            mCommunicationDeviceTracker.clearCommunicationDevice(
+                                    AudioDeviceInfo.TYPE_BUILTIN_EARPIECE);
+                        }
                         transitionTo(mActiveHeadsetRoute);
                     } else {
                         Log.w(this, "Ignoring switch to headset command. Not available.");
@@ -426,6 +353,10 @@
                     // fall through; we want to switch to speaker mode when docked and in a call.
                 case SWITCH_SPEAKER:
                 case USER_SWITCH_SPEAKER:
+                    if (mFeatureFlags.callAudioCommunicationDeviceRefactor()) {
+                        mCommunicationDeviceTracker.clearCommunicationDevice(
+                                AudioDeviceInfo.TYPE_BUILTIN_EARPIECE);
+                    }
                     setSpeakerphoneOn(true);
                     // fall through
                 case SPEAKER_ON:
@@ -579,10 +510,19 @@
         public void enter() {
             super.enter();
             setSpeakerphoneOn(false);
+            if (mFeatureFlags.callAudioCommunicationDeviceRefactor()) {
+                mCommunicationDeviceTracker.setCommunicationDevice(
+                        AudioDeviceInfo.TYPE_WIRED_HEADSET, null);
+            }
             CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_WIRED_HEADSET,
                     mAvailableRoutes, null, mBluetoothRouteManager.getConnectedDevices());
-            setSystemAudioState(newState, true);
-            updateInternalCallAudioState();
+            if (mFeatureFlags.earlyUpdateInternalCallAudioState()) {
+                updateInternalCallAudioState();
+                setSystemAudioState(newState, true);
+            } else {
+                setSystemAudioState(newState, true);
+                updateInternalCallAudioState();
+            }
         }
 
         @Override
@@ -600,6 +540,10 @@
                 case SWITCH_EARPIECE:
                 case USER_SWITCH_EARPIECE:
                     if ((mAvailableRoutes & ROUTE_EARPIECE) != 0) {
+                        if (mFeatureFlags.callAudioCommunicationDeviceRefactor()) {
+                            mCommunicationDeviceTracker.clearCommunicationDevice(
+                                    AudioDeviceInfo.TYPE_WIRED_HEADSET);
+                        }
                         transitionTo(mActiveEarpieceRoute);
                     } else {
                         Log.w(this, "Ignoring switch to earpiece command. Not available.");
@@ -615,6 +559,10 @@
                                 || mBluetoothRouteManager.isInbandRingingEnabled()) {
                             String address = (msg.obj instanceof SomeArgs) ?
                                     (String) ((SomeArgs) msg.obj).arg2 : null;
+                            if (mFeatureFlags.callAudioCommunicationDeviceRefactor()) {
+                                mCommunicationDeviceTracker.clearCommunicationDevice(
+                                        AudioDeviceInfo.TYPE_WIRED_HEADSET);
+                            }
                             // Omit transition to ActiveBluetoothRoute until actual connection.
                             setBluetoothOn(address);
                         } else {
@@ -631,6 +579,10 @@
                     return HANDLED;
                 case SWITCH_SPEAKER:
                 case USER_SWITCH_SPEAKER:
+                    if (mFeatureFlags.callAudioCommunicationDeviceRefactor()) {
+                        mCommunicationDeviceTracker.clearCommunicationDevice(
+                                AudioDeviceInfo.TYPE_WIRED_HEADSET);
+                    }
                     setSpeakerphoneOn(true);
                     // fall through
                 case SPEAKER_ON:
@@ -793,11 +745,27 @@
         public void enter() {
             super.enter();
             setSpeakerphoneOn(false);
+            // Try arbitrarily connecting to BT audio if we haven't already. This handles
+            // the edge case of when the audio route is in a quiescent route while in-call and
+            // the BT connection fails to be set. Previously, the logic was to setBluetoothOn in
+            // ACTIVE_FOCUS but the route would still remain in a quiescent route, so instead we
+            // should be transitioning directly into the active route.
+            if (mFeatureFlags.callAudioCommunicationDeviceRefactor()) {
+                setBluetoothOn(null);
+            }
+            if (mFeatureFlags.updateRouteMaskWhenBtConnected()) {
+                mAvailableRoutes |= ROUTE_BLUETOOTH;
+            }
             CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_BLUETOOTH,
                     mAvailableRoutes, mBluetoothRouteManager.getBluetoothAudioConnectedDevice(),
                     mBluetoothRouteManager.getConnectedDevices());
-            setSystemAudioState(newState, true);
-            updateInternalCallAudioState();
+            if (mFeatureFlags.earlyUpdateInternalCallAudioState()) {
+                updateInternalCallAudioState();
+                setSystemAudioState(newState, true);
+            } else {
+                setSystemAudioState(newState, true);
+                updateInternalCallAudioState();
+            }
             // Do not send RINGER_MODE_CHANGE if no Bluetooth SCO audio device is available
             if (mBluetoothRouteManager.getBluetoothAudioConnectedDevice() != null) {
                 mCallAudioManager.onRingerModeChange();
@@ -893,8 +861,21 @@
                 case SWITCH_FOCUS:
                     if (msg.arg1 == NO_FOCUS) {
                         // Only disconnect audio here instead of routing away from BT entirely.
-                        mBluetoothRouteManager.disconnectAudio();
-                        reinitialize();
+                        if (mFeatureFlags.transitRouteBeforeAudioDisconnectBt()) {
+                            // Note: We have to turn off mute here rather than when entering the
+                            // QuiescentBluetooth route because setMuteOn will only work when there the
+                            // current state is active.
+                            // We don't need to do this in the unflagged path since reinitialize
+                            // will turn off mute.
+                            if (mFeatureFlags.resetMuteWhenEnteringQuiescentBtRoute()) {
+                                setMuteOn(false);
+                            }
+                            transitionTo(mQuiescentBluetoothRoute);
+                            mBluetoothRouteManager.disconnectAudio();
+                        } else {
+                            mBluetoothRouteManager.disconnectAudio();
+                            reinitialize();
+                        }
                         mCallAudioManager.notifyAudioOperationsComplete();
                     } else if (msg.arg1 == RINGING_FOCUS
                             && !mBluetoothRouteManager.isInbandRingingEnabled()) {
@@ -932,8 +913,13 @@
             CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_BLUETOOTH,
                     mAvailableRoutes, mBluetoothRouteManager.getBluetoothAudioConnectedDevice(),
                     mBluetoothRouteManager.getConnectedDevices());
-            setSystemAudioState(newState);
-            updateInternalCallAudioState();
+            if (mFeatureFlags.earlyUpdateInternalCallAudioState()) {
+                updateInternalCallAudioState();
+                setSystemAudioState(newState, true);
+            } else {
+                setSystemAudioState(newState, true);
+                updateInternalCallAudioState();
+            }
         }
 
         @Override
@@ -1065,7 +1051,13 @@
                     return HANDLED;
                 case SWITCH_FOCUS:
                     if (msg.arg1 == ACTIVE_FOCUS) {
-                        setBluetoothOn(null);
+                        // It is possible that the connection to BT will fail while in-call, in
+                        // which case, we want to transition into the active route.
+                        if (mFeatureFlags.callAudioCommunicationDeviceRefactor()) {
+                            transitionTo(mActiveBluetoothRoute);
+                        } else {
+                            setBluetoothOn(null);
+                        }
                     } else if (msg.arg1 == RINGING_FOCUS) {
                         if (mBluetoothRouteManager.isInbandRingingEnabled()) {
                             setBluetoothOn(null);
@@ -1150,8 +1142,13 @@
             mWasOnSpeaker = true;
             CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_SPEAKER,
                     mAvailableRoutes, null, mBluetoothRouteManager.getConnectedDevices());
-            setSystemAudioState(newState, true);
-            updateInternalCallAudioState();
+            if (mFeatureFlags.earlyUpdateInternalCallAudioState()) {
+                updateInternalCallAudioState();
+                setSystemAudioState(newState, true);
+            } else {
+                setSystemAudioState(newState, true);
+                updateInternalCallAudioState();
+            }
         }
 
         @Override
@@ -1215,7 +1212,13 @@
                     // Expected, since we just transitioned here
                     return HANDLED;
                 case SPEAKER_OFF:
-                    sendInternalMessage(SWITCH_BASELINE_ROUTE, INCLUDE_BLUETOOTH_IN_BASELINE);
+                    // Check if we already requested to connect to other devices and just waiting
+                    // for their response. In some cases, this SPEAKER_OFF message may come in
+                    // before the response, we can just ignore the message here to not re-evaluate
+                    // the baseline route incorrectly
+                    if (!mBluetoothRouteManager.isBluetoothAudioConnectedOrPending()) {
+                        sendInternalMessage(SWITCH_BASELINE_ROUTE, INCLUDE_BLUETOOTH_IN_BASELINE);
+                    }
                     return HANDLED;
                 case SWITCH_FOCUS:
                     if (msg.arg1 == NO_FOCUS) {
@@ -1514,6 +1517,8 @@
     private CallAudioState mLastKnownCallAudioState;
 
     private CallAudioManager mCallAudioManager;
+    private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
+    private FeatureFlags mFeatureFlags;
 
     public CallAudioRouteStateMachine(
             Context context,
@@ -1523,7 +1528,9 @@
             StatusBarNotifier statusBarNotifier,
             CallAudioManager.AudioServiceFactory audioServiceFactory,
             int earpieceControl,
-            Executor asyncTaskExecutor) {
+            Executor asyncTaskExecutor,
+            CallAudioCommunicationDeviceTracker communicationDeviceTracker,
+            FeatureFlags featureFlags) {
         super(NAME);
         mContext = context;
         mCallsManager = callsManager;
@@ -1534,6 +1541,8 @@
         mAudioServiceFactory = audioServiceFactory;
         mLock = callsManager.getLock();
         mAsyncTaskExecutor = asyncTaskExecutor;
+        mCommunicationDeviceTracker = communicationDeviceTracker;
+        mFeatureFlags = featureFlags;
         createStates(earpieceControl);
     }
 
@@ -1545,7 +1554,9 @@
             WiredHeadsetManager wiredHeadsetManager,
             StatusBarNotifier statusBarNotifier,
             CallAudioManager.AudioServiceFactory audioServiceFactory,
-            int earpieceControl, Looper looper, Executor asyncTaskExecutor) {
+            int earpieceControl, Looper looper, Executor asyncTaskExecutor,
+            CallAudioCommunicationDeviceTracker communicationDeviceTracker,
+            FeatureFlags featureFlags) {
         super(NAME, looper);
         mContext = context;
         mCallsManager = callsManager;
@@ -1556,7 +1567,8 @@
         mAudioServiceFactory = audioServiceFactory;
         mLock = callsManager.getLock();
         mAsyncTaskExecutor = asyncTaskExecutor;
-
+        mCommunicationDeviceTracker = communicationDeviceTracker;
+        mFeatureFlags = featureFlags;
         createStates(earpieceControl);
     }
 
@@ -1659,11 +1671,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) {
@@ -1673,6 +1685,15 @@
         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);
+    }
+
     /**
      * This is for state-independent changes in audio route (i.e. muting or runnables)
      * @param msg that couldn't be handled.
@@ -1702,9 +1723,19 @@
                 }
                 return;
             case UPDATE_SYSTEM_AUDIO_ROUTE:
-                updateInternalCallAudioState();
-                updateRouteForForegroundCall();
-                resendSystemAudioState();
+                if (mFeatureFlags.availableRoutesNeverUpdatedAfterSetSystemAudioState()) {
+                    // Ensure available routes is updated.
+                    updateRouteForForegroundCall();
+                    // Ensure current audio state gets updated to take this into account.
+                    updateInternalCallAudioState();
+                    // Either resend the current audio state as it stands, or update to reflect any
+                    // changes put into place based on mAvailableRoutes
+                    setSystemAudioState(mCurrentCallAudioState, true);
+                } else {
+                    updateInternalCallAudioState();
+                    updateRouteForForegroundCall();
+                    resendSystemAudioState();
+                }
                 return;
             case RUN_RUNNABLE:
                 java.lang.Runnable r = (java.lang.Runnable) msg.obj;
@@ -1729,7 +1760,7 @@
     }
 
     public void dumpPendingMessages(IndentingPrintWriter pw) {
-        getHandler().getLooper().dump(pw::println, "");
+        getAdapterHandler().getLooper().dump(pw::println, "");
     }
 
     public boolean isHfpDeviceAvailable() {
@@ -1741,31 +1772,19 @@
         final boolean hasAnyCalls = mCallsManager.hasAnyCalls();
         // These APIs are all via two-way binder calls so can potentially block Telecom.  Since none
         // of this has to happen in the Telecom lock we'll offload it to the async executor.
-
-        AudioDeviceInfo speakerDevice = null;
-        for (AudioDeviceInfo info : mAudioManager.getAvailableCommunicationDevices()) {
-            if (info.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) {
-                speakerDevice = info;
-                break;
-            }
-        }
         boolean speakerOn = false;
-        if (speakerDevice != null && on) {
-            boolean result = mAudioManager.setCommunicationDevice(speakerDevice);
-            if (result) {
-                speakerOn = true;
+        if (mFeatureFlags.callAudioCommunicationDeviceRefactor()) {
+            if (on) {
+                speakerOn = mCommunicationDeviceTracker.setCommunicationDevice(
+                        AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, null);
+            } else {
+                mCommunicationDeviceTracker.clearCommunicationDevice(
+                        AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
             }
         } else {
-            AudioDeviceInfo curDevice = mAudioManager.getCommunicationDevice();
-            if (curDevice != null
-                    && curDevice.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) {
-                mAudioManager.clearCommunicationDevice();
-            }
+            speakerOn = processLegacySpeakerCommunicationDevice(on);
         }
-        final boolean isSpeakerOn = speakerOn;
-        mAsyncTaskExecutor.execute(() -> {
-            mStatusBarNotifier.notifySpeakerphone(hasAnyCalls && isSpeakerOn);
-        });
+        mStatusBarNotifier.notifySpeakerphone(hasAnyCalls && speakerOn);
     }
 
     private void setBluetoothOn(String address) {
@@ -1863,6 +1882,11 @@
         setSystemAudioState(mLastKnownCallAudioState, true);
     }
 
+    @VisibleForTesting
+    public CallAudioState getLastKnownCallAudioState() {
+        return mLastKnownCallAudioState;
+    }
+
     private void setSystemAudioState(CallAudioState newCallAudioState, boolean force) {
         synchronized (mLock) {
             Log.i(this, "setSystemAudioState: changing from %s to %s", mLastKnownCallAudioState,
@@ -1980,6 +2004,58 @@
         return false;
     }
 
+    private boolean isWatchActiveOrOnlyWatchesAvailable() {
+        if (!mFeatureFlags.ignoreAutoRouteToWatchDevice()) {
+            Log.i(this, "isWatchActiveOrOnlyWatchesAvailable: Flag is disabled.");
+            return false;
+        }
+
+        boolean containsWatchDevice = false;
+        boolean containsNonWatchDevice = false;
+        Collection<BluetoothDevice> connectedBtDevices =
+                mBluetoothRouteManager.getConnectedDevices();
+
+        for (BluetoothDevice connectedDevice: connectedBtDevices) {
+            if (mBluetoothRouteManager.isWatch(connectedDevice)) {
+                containsWatchDevice = true;
+            } else {
+                containsNonWatchDevice = true;
+            }
+        }
+
+        // Don't ignore switch if watch is already the active device.
+        boolean isActiveDeviceWatch = mBluetoothRouteManager.isWatch(
+                mBluetoothRouteManager.getBluetoothAudioConnectedDevice());
+        Log.i(this, "isWatchActiveOrOnlyWatchesAvailable: contains watch: %s, contains "
+                + "non-wearable device: %s, is active device a watch: %s.",
+                containsWatchDevice, containsNonWatchDevice, isActiveDeviceWatch);
+        return containsWatchDevice && !containsNonWatchDevice && !isActiveDeviceWatch;
+    }
+
+    private boolean processLegacySpeakerCommunicationDevice(boolean on) {
+        AudioDeviceInfo speakerDevice = null;
+        for (AudioDeviceInfo info : mAudioManager.getAvailableCommunicationDevices()) {
+            if (info.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) {
+                speakerDevice = info;
+                break;
+            }
+        }
+        boolean speakerOn = false;
+        if (speakerDevice != null && on) {
+            boolean result = mAudioManager.setCommunicationDevice(speakerDevice);
+            if (result) {
+                speakerOn = true;
+            }
+        } else {
+            AudioDeviceInfo curDevice = mAudioManager.getCommunicationDevice();
+            if (curDevice != null
+                    && curDevice.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) {
+                mAudioManager.clearCommunicationDevice();
+            }
+        }
+        return speakerOn;
+    }
+
     private int calculateBaselineRouteMessage(boolean isExplicitUserRequest,
             boolean includeBluetooth) {
         boolean isSkipEarpiece = false;
@@ -1992,7 +2068,7 @@
         }
         if ((mAvailableRoutes & ROUTE_BLUETOOTH) != 0
                 && !mHasUserExplicitlyLeftBluetooth
-                && includeBluetooth) {
+                && includeBluetooth && !isWatchActiveOrOnlyWatchesAvailable()) {
             return isExplicitUserRequest ? USER_SWITCH_BLUETOOTH : SWITCH_BLUETOOTH;
         } else if ((mAvailableRoutes & ROUTE_EARPIECE) != 0 && !isSkipEarpiece) {
             return isExplicitUserRequest ? USER_SWITCH_EARPIECE : SWITCH_EARPIECE;
@@ -2048,4 +2124,15 @@
 
         return base;
     }
+
+    @Override
+    public Handler getAdapterHandler() {
+        return getHandler();
+    }
+
+    @Override
+    public PendingAudioRoute getPendingAudioRoute() {
+        // Only used by CallAudioRouteController.
+        return null;
+    }
 }
diff --git a/src/com/android/server/telecom/CallDiagnosticServiceController.java b/src/com/android/server/telecom/CallDiagnosticServiceController.java
index 6c7ee38..1077f0d 100644
--- a/src/com/android/server/telecom/CallDiagnosticServiceController.java
+++ b/src/com/android/server/telecom/CallDiagnosticServiceController.java
@@ -522,7 +522,7 @@
                 callId, messageId, message);
         if (mPlayerFactory != null) {
             // Play that tone!
-            mPlayerFactory.createPlayer(InCallTonePlayer.TONE_IN_CALL_QUALITY_NOTIFICATION)
+            mPlayerFactory.createPlayer(call, InCallTonePlayer.TONE_IN_CALL_QUALITY_NOTIFICATION)
                     .startTone();
         }
         call.displayDiagnosticMessage(messageId, message);
diff --git a/src/com/android/server/telecom/CallEndpointController.java b/src/com/android/server/telecom/CallEndpointController.java
index 7e11b47..49c0d51 100644
--- a/src/com/android/server/telecom/CallEndpointController.java
+++ b/src/com/android/server/telecom/CallEndpointController.java
@@ -27,6 +27,7 @@
 import android.telecom.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.telecom.flags.FeatureFlags;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -49,6 +50,7 @@
 
     private final Context mContext;
     private final CallsManager mCallsManager;
+    private final FeatureFlags mFeatureFlags;
     private final HashMap<Integer, Integer> mRouteToTypeMap;
     private final HashMap<Integer, Integer> mTypeToRouteMap;
     private final Map<ParcelUuid, String> mBluetoothAddressMap = new HashMap<>();
@@ -57,10 +59,10 @@
     private ParcelUuid mRequestedEndpointId;
     private CompletableFuture<Integer> mPendingChangeRequest;
 
-    public CallEndpointController(Context context, CallsManager callsManager) {
+    public CallEndpointController(Context context, CallsManager callsManager, FeatureFlags flags) {
         mContext = context;
         mCallsManager = callsManager;
-
+        mFeatureFlags = flags;
         mRouteToTypeMap = new HashMap<>(5);
         mRouteToTypeMap.put(CallAudioState.ROUTE_EARPIECE, CallEndpoint.TYPE_EARPIECE);
         mRouteToTypeMap.put(CallAudioState.ROUTE_BLUETOOTH, CallEndpoint.TYPE_BLUETOOTH);
@@ -87,7 +89,7 @@
     }
 
     public void requestCallEndpointChange(CallEndpoint endpoint, ResultReceiver callback) {
-        Log.d(this, "requestCallEndpointChange %s", endpoint);
+        Log.i(this, "requestCallEndpointChange %s", endpoint);
         int route = mTypeToRouteMap.get(endpoint.getEndpointType());
         String bluetoothAddress = getBluetoothAddress(endpoint);
 
@@ -99,7 +101,6 @@
         }
 
         if (isCurrentEndpointRequestedEndpoint(route, bluetoothAddress)) {
-            Log.d(this, "requestCallEndpointChange: requested endpoint is already active");
             callback.send(CallEndpoint.ENDPOINT_OPERATION_SUCCESS, new Bundle());
             return;
         }
@@ -130,21 +131,27 @@
             return false;
         }
         CallAudioState currentAudioState = mCallsManager.getCallAudioManager().getCallAudioState();
-        // requested non-bt endpoint is already active
-        if (requestedRoute != CallAudioState.ROUTE_BLUETOOTH &&
-                requestedRoute == currentAudioState.getRoute()) {
-            return true;
-        }
-        // requested bt endpoint is already active
-        if (requestedRoute == CallAudioState.ROUTE_BLUETOOTH &&
-                currentAudioState.getActiveBluetoothDevice() != null &&
-                requestedAddress.equals(
-                        currentAudioState.getActiveBluetoothDevice().getAddress())) {
-            return true;
+        if (requestedRoute == currentAudioState.getRoute()) {
+            if (requestedRoute != CallAudioState.ROUTE_BLUETOOTH) {
+                // The audio route (earpiece, speaker, etc.) is already active
+                // and Telecom can ignore the spam request!
+                Log.i(this, "iCERE: user requested a non-BT route that is already active");
+                return true;
+            } else if (hasSameBluetoothAddress(currentAudioState, requestedAddress)) {
+                // if the requested (BT route, device) is active, ignore the request...
+                Log.i(this, "iCERE: user requested a BT endpoint that is already active");
+                return true;
+            }
         }
         return false;
     }
 
+    public boolean hasSameBluetoothAddress(CallAudioState audioState, String requestedAddress) {
+        boolean hasActiveBtDevice = audioState.getActiveBluetoothDevice() != null;
+        return hasActiveBtDevice && requestedAddress.equals(
+                audioState.getActiveBluetoothDevice().getAddress());
+    }
+
     private Bundle getErrorResult(int result) {
         String message;
         int resultCode;
@@ -192,43 +199,91 @@
 
         Set<Call> calls = mCallsManager.getTrackedCalls();
         for (Call call : calls) {
-            if (call != null && call.getConnectionService() != null) {
-                call.getConnectionService().onCallEndpointChanged(call, mActiveCallEndpoint);
-            } else if (call != null && call.getTransactionServiceWrapper() != null) {
-                call.getTransactionServiceWrapper()
-                        .onCallEndpointChanged(call, mActiveCallEndpoint);
+            if (mFeatureFlags.cacheCallAudioCallbacks()) {
+                onCallEndpointChangedOrCache(call);
+            } else {
+                if (call != null && call.getConnectionService() != null) {
+                    call.getConnectionService().onCallEndpointChanged(call, mActiveCallEndpoint);
+                } else if (call != null && call.getTransactionServiceWrapper() != null) {
+                    call.getTransactionServiceWrapper()
+                            .onCallEndpointChanged(call, mActiveCallEndpoint);
+                }
             }
         }
     }
 
+    private void onCallEndpointChangedOrCache(Call call) {
+        if (call == null) {
+            return;
+        }
+        CallSourceService service = call.getService();
+        if (service != null) {
+            service.onCallEndpointChanged(call, mActiveCallEndpoint);
+        } else {
+            call.cacheServiceCallback(new CachedCurrentEndpointChange(mActiveCallEndpoint));
+        }
+    }
+
     private void notifyAvailableCallEndpointsChange() {
         mCallsManager.updateAvailableCallEndpoints(mAvailableCallEndpoints);
 
         Set<Call> calls = mCallsManager.getTrackedCalls();
         for (Call call : calls) {
-            if (call != null && call.getConnectionService() != null) {
-                call.getConnectionService().onAvailableCallEndpointsChanged(call,
-                        mAvailableCallEndpoints);
-            } else if (call != null && call.getTransactionServiceWrapper() != null) {
-                call.getTransactionServiceWrapper()
-                        .onAvailableCallEndpointsChanged(call, mAvailableCallEndpoints);
+            if (mFeatureFlags.cacheCallAudioCallbacks()) {
+                onAvailableEndpointsChangedOrCache(call);
+            } else {
+                if (call != null && call.getConnectionService() != null) {
+                    call.getConnectionService().onAvailableCallEndpointsChanged(call,
+                            mAvailableCallEndpoints);
+                } else if (call != null && call.getTransactionServiceWrapper() != null) {
+                    call.getTransactionServiceWrapper().onAvailableCallEndpointsChanged(call,
+                            mAvailableCallEndpoints);
+                }
             }
         }
     }
 
+    private void onAvailableEndpointsChangedOrCache(Call call) {
+        if (call == null) {
+            return;
+        }
+        CallSourceService service = call.getService();
+        if (service != null) {
+            service.onAvailableCallEndpointsChanged(call, mAvailableCallEndpoints);
+        } else {
+            call.cacheServiceCallback(new CachedAvailableEndpointsChange(mAvailableCallEndpoints));
+        }
+    }
+
     private void notifyMuteStateChange(boolean isMuted) {
         mCallsManager.updateMuteState(isMuted);
 
         Set<Call> calls = mCallsManager.getTrackedCalls();
         for (Call call : calls) {
-            if (call != null && call.getConnectionService() != null) {
-                call.getConnectionService().onMuteStateChanged(call, isMuted);
-            } else if (call != null && call.getTransactionServiceWrapper() != null) {
-                call.getTransactionServiceWrapper().onMuteStateChanged(call, isMuted);
+            if (mFeatureFlags.cacheCallAudioCallbacks()) {
+                onMuteStateChangedOrCache(call, isMuted);
+            } else {
+                if (call != null && call.getConnectionService() != null) {
+                    call.getConnectionService().onMuteStateChanged(call, isMuted);
+                } else if (call != null && call.getTransactionServiceWrapper() != null) {
+                    call.getTransactionServiceWrapper().onMuteStateChanged(call, isMuted);
+                }
             }
         }
     }
 
+    private void onMuteStateChangedOrCache(Call call, boolean isMuted){
+        if (call == null) {
+            return;
+        }
+        CallSourceService service = call.getService();
+        if (service != null) {
+            service.onMuteStateChanged(call, isMuted);
+        } else {
+            call.cacheServiceCallback(new CachedMuteStateChange(isMuted));
+        }
+    }
+
     private void createAvailableCallEndpoints(CallAudioState state) {
         Set<CallEndpoint> newAvailableEndpoints = new HashSet<>();
         Map<ParcelUuid, String> newBluetoothDevices = new HashMap<>();
diff --git a/src/com/android/server/telecom/CallIntentProcessor.java b/src/com/android/server/telecom/CallIntentProcessor.java
index 7953324..8e1f754 100644
--- a/src/com/android/server/telecom/CallIntentProcessor.java
+++ b/src/com/android/server/telecom/CallIntentProcessor.java
@@ -1,9 +1,16 @@
 package com.android.server.telecom;
 
-import com.android.server.telecom.components.ErrorDialogActivity;
+import static android.content.pm.PackageManager.MATCH_DEFAULT_ONLY;
 
+import com.android.internal.app.IntentForwarderActivity;
+import com.android.server.telecom.components.ErrorDialogActivity;
+import com.android.server.telecom.flags.FeatureFlags;
+
+import android.content.ActivityNotFoundException;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Looper;
@@ -19,6 +26,7 @@
 import android.telecom.VideoProfile;
 import android.telephony.DisconnectCause;
 import android.telephony.PhoneNumberUtils;
+import android.telephony.TelephonyManager;
 import android.widget.Toast;
 
 import java.util.concurrent.CompletableFuture;
@@ -32,7 +40,7 @@
 public class CallIntentProcessor {
     public interface Adapter {
         void processOutgoingCallIntent(Context context, CallsManager callsManager,
-                Intent intent, String callingPackage);
+                Intent intent, String callingPackage, FeatureFlags featureFlags);
         void processIncomingCallIntent(CallsManager callsManager, Intent intent);
         void processUnknownCallIntent(CallsManager callsManager, Intent intent);
     }
@@ -45,9 +53,9 @@
 
         @Override
         public void processOutgoingCallIntent(Context context, CallsManager callsManager,
-                Intent intent, String callingPackage) {
+                Intent intent, String callingPackage, FeatureFlags featureFlags) {
             CallIntentProcessor.processOutgoingCallIntent(context, callsManager, intent,
-                    callingPackage, mDefaultDialerCache);
+                    callingPackage, mDefaultDialerCache, featureFlags);
         }
 
         @Override
@@ -73,12 +81,14 @@
     private final Context mContext;
     private final CallsManager mCallsManager;
     private final DefaultDialerCache mDefaultDialerCache;
+    private final FeatureFlags mFeatureFlags;
 
     public CallIntentProcessor(Context context, CallsManager callsManager,
-            DefaultDialerCache defaultDialerCache) {
+            DefaultDialerCache defaultDialerCache, FeatureFlags featureFlags) {
         this.mContext = context;
         this.mCallsManager = callsManager;
         this.mDefaultDialerCache = defaultDialerCache;
+        this.mFeatureFlags = featureFlags;
     }
 
     public void processIntent(Intent intent, String callingPackage) {
@@ -90,7 +100,7 @@
             processUnknownCallIntent(mCallsManager, intent);
         } else {
             processOutgoingCallIntent(mContext, mCallsManager, intent, callingPackage,
-                    mDefaultDialerCache);
+                    mDefaultDialerCache, mFeatureFlags);
         }
         Trace.endSection();
     }
@@ -107,7 +117,8 @@
             CallsManager callsManager,
             Intent intent,
             String callingPackage,
-            DefaultDialerCache defaultDialerCache) {
+            DefaultDialerCache defaultDialerCache,
+            FeatureFlags featureFlags) {
 
         Uri handle = intent.getData();
         String scheme = handle.getScheme();
@@ -164,13 +175,20 @@
 
         if (!callsManager.isSelfManaged(phoneAccountHandle,
                 (UserHandle) intent.getParcelableExtra(KEY_INITIATING_USER))) {
-            boolean fixedInitiatingUser = fixInitiatingUserIfNecessary(context, intent);
+            boolean fixedInitiatingUser = fixInitiatingUserIfNecessary(
+                    context, intent, featureFlags);
             // Show the toast to warn user that it is a personal call though initiated in work
             // profile.
             if (fixedInitiatingUser) {
-                Toast.makeText(context, Looper.getMainLooper(),
-                        context.getString(R.string.toast_personal_call_msg),
-                        Toast.LENGTH_LONG).show();
+                if (featureFlags.telecomResolveHiddenDependencies()) {
+                    context.getMainExecutor().execute(() ->
+                            Toast.makeText(context, context.getString(
+                                    R.string.toast_personal_call_msg), Toast.LENGTH_LONG).show());
+                } else {
+                    Toast.makeText(context, Looper.getMainLooper(),
+                            context.getString(R.string.toast_personal_call_msg),
+                            Toast.LENGTH_LONG).show();
+                }
             }
         } else {
             Log.i(CallIntentProcessor.class,
@@ -182,10 +200,21 @@
         boolean isPrivilegedDialer = defaultDialerCache.isDefaultOrSystemDialer(callingPackage,
                 initiatingUser.getIdentifier());
 
+        if (privateSpaceFlagsEnabled()) {
+            if (!callsManager.isSelfManaged(phoneAccountHandle, initiatingUser)
+                    && !TelephonyUtil.shouldProcessAsEmergency(context, handle)
+                    && UserUtil.isPrivateProfile(initiatingUser, context)) {
+                boolean dialogShown = maybeRedirectToIntentForwarderForPrivate(context, intent,
+                        initiatingUser);
+                if (dialogShown) {
+                    return;
+                }
+            }
+        }
 
         NewOutgoingCallIntentBroadcaster broadcaster = new NewOutgoingCallIntentBroadcaster(
                 context, callsManager, intent, callsManager.getPhoneNumberUtilsAdapter(),
-                isPrivilegedDialer, defaultDialerCache, new MmiUtils());
+                isPrivilegedDialer, defaultDialerCache, new MmiUtils(), featureFlags);
 
         // If the broadcaster comes back with an immediate error, disconnect and show a dialog.
         NewOutgoingCallIntentBroadcaster.CallDisposition disposition = broadcaster.evaluateCall();
@@ -218,16 +247,18 @@
      *
      * @return whether the initiating user is fixed.
      */
-    static boolean fixInitiatingUserIfNecessary(Context context, Intent intent) {
+    static boolean fixInitiatingUserIfNecessary(Context context, Intent intent,
+            FeatureFlags featureFlags) {
         final UserHandle initiatingUser = intent.getParcelableExtra(KEY_INITIATING_USER);
-        if (UserUtil.isManagedProfile(context, initiatingUser)) {
+        if (UserUtil.isManagedProfile(context, initiatingUser, featureFlags)) {
             boolean noDialerInstalled = DefaultDialerManager.getInstalledDialerApplications(context,
                     initiatingUser.getIdentifier()).size() == 0;
             if (noDialerInstalled) {
-                final UserManager userManager = UserManager.get(context);
-                UserHandle parentUserHandle =
-                        userManager.getProfileParent(
-                                initiatingUser.getIdentifier()).getUserHandle();
+                final UserManager userManager = context.getSystemService(UserManager.class);
+                UserHandle parentUserHandle = featureFlags.telecomResolveHiddenDependencies()
+                        ? userManager.getProfileParent(initiatingUser)
+                        : userManager.getProfileParent(initiatingUser.getIdentifier())
+                                .getUserHandle();
                 intent.putExtra(KEY_INITIATING_USER, parentUserHandle);
 
                 Log.i(CallIntentProcessor.class, "fixInitiatingUserIfNecessary: no dialer installed"
@@ -298,4 +329,43 @@
             context.startActivityAsUser(errorIntent, UserHandle.CURRENT);
         }
     }
+
+    private static boolean privateSpaceFlagsEnabled() {
+        return android.multiuser.Flags.enablePrivateSpaceFeatures()
+                && android.multiuser.Flags.enablePrivateSpaceIntentRedirection();
+    }
+
+    private static boolean maybeRedirectToIntentForwarderForPrivate(
+            Context context,
+            Intent forwardCallIntent,
+            UserHandle initiatingUser) {
+
+        // If CALL intent filters are set to SKIP_CURRENT_PROFILE, PM will resolve this to an
+        // intent forwarder activity.
+        forwardCallIntent.setComponent(null);
+        forwardCallIntent.setPackage(null);
+        ResolveInfo resolveInfos =
+                context.getPackageManager()
+                        .resolveActivityAsUser(
+                                forwardCallIntent,
+                                PackageManager.ResolveInfoFlags.of(MATCH_DEFAULT_ONLY),
+                                initiatingUser.getIdentifier());
+
+        if (resolveInfos == null
+                || !resolveInfos
+                .getComponentInfo()
+                .getComponentName()
+                .getShortClassName()
+                .equals(IntentForwarderActivity.FORWARD_INTENT_TO_PARENT)) {
+            return false;
+        }
+
+        try {
+            context.startActivityAsUser(forwardCallIntent, initiatingUser);
+            return true;
+        } catch (ActivityNotFoundException e) {
+            Log.e(CallIntentProcessor.class, e, "Unable to start call intent in the main user");
+            return false;
+        }
+    }
 }
diff --git a/src/com/android/server/telecom/CallLogManager.java b/src/com/android/server/telecom/CallLogManager.java
index 3005656..4484e23 100644
--- a/src/com/android/server/telecom/CallLogManager.java
+++ b/src/com/android/server/telecom/CallLogManager.java
@@ -16,20 +16,29 @@
 
 package com.android.server.telecom;
 
+import static android.provider.CallLog.AddCallParams.AddCallParametersBuilder.MAX_NUMBER_OF_CHARACTERS;
 import static android.provider.CallLog.Calls.BLOCK_REASON_NOT_BLOCKED;
 import static android.telephony.CarrierConfigManager.KEY_SUPPORT_IMS_CONFERENCE_EVENT_PACKAGE_BOOL;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
 import android.location.Country;
 import android.location.CountryDetector;
 import android.location.Location;
 import android.net.Uri;
 import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerExecutor;
 import android.os.Looper;
 import android.os.UserHandle;
 import android.os.PersistableBundle;
+import android.os.UserManager;
 import android.provider.CallLog;
 import android.provider.CallLog.Calls;
 import android.telecom.Connection;
@@ -42,13 +51,17 @@
 import android.telephony.CarrierConfigManager;
 import android.telephony.PhoneNumberUtils;
 import android.telephony.SubscriptionManager;
+import android.util.Pair;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.telecom.callfiltering.CallFilteringResult;
+import com.android.server.telecom.flags.FeatureFlags;
+import com.android.server.telecom.flags.Flags;
 
 import java.util.Arrays;
 import java.util.Locale;
 import java.util.Objects;
+import java.util.UUID;
 import java.util.stream.Stream;
 
 /**
@@ -68,16 +81,19 @@
      */
     private static class AddCallArgs {
         public AddCallArgs(Context context, CallLog.AddCallParams params,
-                @Nullable LogCallCompletedListener logCallCompletedListener) {
+                @Nullable LogCallCompletedListener logCallCompletedListener,
+                @NonNull Call call) {
             this.context = context;
             this.params = params;
             this.logCallCompletedListener = logCallCompletedListener;
+            this.call = call;
 
         }
         // Since the members are accessed directly, we don't use the
         // mXxxx notation.
         public final Context context;
         public final CallLog.AddCallParams params;
+        public final Call call;
         @Nullable
         public final LogCallCompletedListener logCallCompletedListener;
     }
@@ -88,29 +104,41 @@
     // TODO: come up with a better way to indicate in a android.telecom.DisconnectCause that
     // a conference was merged successfully
     private static final String REASON_IMS_MERGED_SUCCESSFULLY = "IMS_MERGED_SUCCESSFULLY";
+    private static final UUID LOG_CALL_FAILED_ANOMALY_ID =
+            UUID.fromString("d9b38771-ff36-417b-8723-2363a870c702");
+    private static final String LOG_CALL_FAILED_ANOMALY_DESC =
+            "Based on the current user, Telecom detected failure to record a call to the call log.";
 
     private final Context mContext;
     private final CarrierConfigManager mCarrierConfigManager;
     private final PhoneAccountRegistrar mPhoneAccountRegistrar;
     private final MissedCallNotifier mMissedCallNotifier;
+    private AnomalyReporterAdapter mAnomalyReporterAdapter;
     private static final String ACTION_CALLS_TABLE_ADD_ENTRY =
-                "com.android.server.telecom.intent.action.CALLS_ADD_ENTRY";
+            "com.android.server.telecom.intent.action.CALLS_ADD_ENTRY";
     private static final String PERMISSION_PROCESS_CALLLOG_INFO =
-                "android.permission.PROCESS_CALLLOG_INFO";
+            "android.permission.PROCESS_CALLLOG_INFO";
     private static final String CALL_TYPE = "callType";
     private static final String CALL_DURATION = "duration";
 
-    private Object mLock;
+    private final Object mLock = new Object();
+    private Country mCurrentCountry;
     private String mCurrentCountryIso;
+    private HandlerExecutor mCountryCodeExecutor;
+
+    private final FeatureFlags mFeatureFlags;
 
     public CallLogManager(Context context, PhoneAccountRegistrar phoneAccountRegistrar,
-            MissedCallNotifier missedCallNotifier) {
+            MissedCallNotifier missedCallNotifier, AnomalyReporterAdapter anomalyReporterAdapter,
+            FeatureFlags featureFlags) {
         mContext = context;
         mCarrierConfigManager = (CarrierConfigManager) mContext
                 .getSystemService(Context.CARRIER_CONFIG_SERVICE);
         mPhoneAccountRegistrar = phoneAccountRegistrar;
         mMissedCallNotifier = missedCallNotifier;
-        mLock = new Object();
+        mAnomalyReporterAdapter = anomalyReporterAdapter;
+        mCountryCodeExecutor = new HandlerExecutor(new Handler(Looper.getMainLooper()));
+        mFeatureFlags = featureFlags;
     }
 
     @Override
@@ -149,9 +177,10 @@
      * Call was NOT in the "choose account" phase when disconnected
      * Call is NOT a conference call which had children (unless it was remotely hosted).
      * Call is NOT a child call from a conference which was remotely hosted.
+     * Call has NOT indicated it should be skipped for logging in its extras
      * Call is NOT simulating a single party conference.
      * Call was NOT explicitly canceled, except for disconnecting from a conference.
-     * Call is NOT an external call
+     * Call is NOT an external call or an external call on watch.
      * Call is NOT disconnected because of merging into a conference.
      * Call is NOT a self-managed call OR call is a self-managed call which has indicated it
      * should be logged in its PhoneAccount
@@ -180,6 +209,11 @@
             return false;
         }
 
+        if (mFeatureFlags.telecomSkipLogBasedOnExtra() && call.getExtras() != null
+                && call.getExtras().containsKey(TelecomManager.EXTRA_DO_NOT_LOG_CALL)) {
+            return false;
+        }
+
         // A child call of a conference which was remotely hosted; these didn't originate on this
         // device and should not be logged.
         if (call.getParentCall() != null && call.hasProperty(Connection.PROPERTY_REMOTELY_HOSTED)) {
@@ -200,8 +234,10 @@
                     & Connection.CAPABILITY_DISCONNECT_FROM_CONFERENCE)
                     == Connection.CAPABILITY_DISCONNECT_FROM_CONFERENCE;
         }
-        // An external call
-        if (call.isExternalCall()) {
+        // An external and non-watch call
+        if (call.isExternalCall() && (!mContext.getPackageManager().hasSystemFeature(
+                PackageManager.FEATURE_WATCH)
+                || !mFeatureFlags.telecomLogExternalWearableCalls())) {
             return false;
         }
 
@@ -218,12 +254,11 @@
                 return false;
             }
 
-            PersistableBundle b = mCarrierConfigManager.getConfigForSubId(subscriptionId);
-            if (b == null) {
+            if (mCarrierConfigManager == null) {
                 return false;
             }
-
-            if (b.getBoolean(KEY_SUPPORT_IMS_CONFERENCE_EVENT_PACKAGE_BOOL, true)) {
+            PersistableBundle b = mCarrierConfigManager.getConfigForSubId(subscriptionId);
+            if (b == null || b.getBoolean(KEY_SUPPORT_IMS_CONFERENCE_EVENT_PACKAGE_BOOL, true)) {
                 return false;
             }
         }
@@ -240,8 +275,13 @@
             logCall(call, type, new LogCallCompletedListener() {
                 @Override
                 public void onLogCompleted(@Nullable Uri uri) {
-                    mMissedCallNotifier.showMissedCallNotification(
-                            new MissedCallNotifier.CallInfo(call));
+                    if (mFeatureFlags.addCallUriForMissedCalls()){
+                        mMissedCallNotifier.showMissedCallNotification(
+                                new MissedCallNotifier.CallInfo(call), uri);
+                    } else {
+                        mMissedCallNotifier.showMissedCallNotification(
+                                new MissedCallNotifier.CallInfo(call), /* uri= */ null);
+                    }
                 }
             }, result);
         } else {
@@ -263,7 +303,7 @@
      *     {@link android.provider.CallLog.Calls#BLOCKED_TYPE}.
      */
     void logCall(Call call, int callLogType,
-        @Nullable LogCallCompletedListener logCallCompletedListener, CallFilteringResult result) {
+            @Nullable LogCallCompletedListener logCallCompletedListener, CallFilteringResult result) {
 
         CallLog.AddCallParams.AddCallParametersBuilder paramBuilder =
                 new CallLog.AddCallParams.AddCallParametersBuilder();
@@ -328,7 +368,7 @@
         if (phoneAccount != null &&
                 phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_MULTI_USER)) {
             if (initiatingUser != null &&
-                    UserUtil.isManagedProfile(mContext, initiatingUser)) {
+                    UserUtil.isProfile(mContext, initiatingUser, mFeatureFlags)) {
                 paramBuilder.setUserToBeInsertedTo(initiatingUser);
                 paramBuilder.setAddForAllUsers(false);
             } else {
@@ -378,14 +418,32 @@
         paramBuilder.setCallType(callLogType);
         paramBuilder.setIsRead(call.isSelfManaged());
         paramBuilder.setMissedReason(call.getMissedReason());
-
+        if (mFeatureFlags.businessCallComposer() && call.getExtras() != null) {
+            Bundle extras = call.getExtras();
+            boolean isBusinessCall =
+                    extras.getBoolean(android.telecom.Call.EXTRA_IS_BUSINESS_CALL, false);
+            paramBuilder.setIsBusinessCall(isBusinessCall);
+            if (isBusinessCall) {
+                Log.i(TAG, "logging business call");
+                String assertedDisplayName =
+                        extras.getString(android.telecom.Call.EXTRA_ASSERTED_DISPLAY_NAME, "");
+                if (assertedDisplayName.length() > MAX_NUMBER_OF_CHARACTERS) {
+                    // avoid throwing an IllegalArgumentException and only log the first 256
+                    // characters of the name.
+                    paramBuilder.setAssertedDisplayName(
+                            assertedDisplayName.substring(0, MAX_NUMBER_OF_CHARACTERS));
+                } else {
+                    paramBuilder.setAssertedDisplayName(assertedDisplayName);
+                }
+            }
+        }
         sendAddCallBroadcast(callLogType, call.getAgeMillis());
 
         boolean okayToLog =
                 okayToLogCall(accountHandle, logNumber, call.isEmergencyCall());
         if (okayToLog) {
             AddCallArgs args = new AddCallArgs(mContext, paramBuilder.build(),
-                    logCallCompletedListener);
+                    logCallCompletedListener, call);
             Log.addEvent(call, LogUtils.Events.LOG_CALL, "number=" + Log.piiHandle(logNumber)
                     + ",postDial=" + Log.piiHandle(call.getPostDialDigits()) + ",pres="
                     + call.getHandlePresentation());
@@ -402,8 +460,8 @@
         boolean okToLogEmergencyNumber = false;
         CarrierConfigManager configManager = (CarrierConfigManager) mContext.getSystemService(
                 Context.CARRIER_CONFIG_SERVICE);
-        PersistableBundle configBundle = configManager.getConfigForSubId(
-                mPhoneAccountRegistrar.getSubscriptionIdForPhoneAccount(accountHandle));
+        PersistableBundle configBundle = (configManager != null) ? configManager.getConfigForSubId(
+                mPhoneAccountRegistrar.getSubscriptionIdForPhoneAccount(accountHandle)) : null;
         if (configBundle != null) {
             okToLogEmergencyNumber = configBundle.getBoolean(
                     CarrierConfigManager.KEY_ALLOW_EMERGENCY_NUMBERS_IN_CALL_LOG_BOOL);
@@ -516,8 +574,18 @@
                 AddCallArgs c = callList[i];
                 mListeners[i] = c.logCallCompletedListener;
                 try {
-                    // May block.
                     result[i] = Calls.addCall(c.context, c.params);
+                    Log.i(TAG, "LogCall; logged callId=%s, uri=%s",
+                            c.call.getId(), result[i]);
+                    if (result[i] == null) {
+                        // No call was added or even worse we lost a call in the log.  Trigger an
+                        // anomaly report.  Note: it technically possible that an app modified the
+                        // call log while we were writing to it here; that is pretty unlikely, and
+                        // the goal here is to try and identify potential anomalous conditions with
+                        // logging calls.
+                        mAnomalyReporterAdapter.reportAnomaly(LOG_CALL_FAILED_ANOMALY_ID,
+                                LOG_CALL_FAILED_ANOMALY_DESC);
+                    }
                 } catch (Exception e) {
                     // This is very rare but may happen in legitimate cases.
                     // E.g. If the phone is encrypted and thus write request fails, it may cause
@@ -526,8 +594,10 @@
                     //
                     // We don't want to crash the whole process just because of that, so just log
                     // it instead.
-                    Log.e(TAG, e, "Exception raised during adding CallLog entry.");
+                    Log.e(TAG, e, "LogCall: Exception raised adding callId=%s", c.call.getId());
                     result[i] = null;
+                    mAnomalyReporterAdapter.reportAnomaly(LOG_CALL_FAILED_ANOMALY_ID,
+                            LOG_CALL_FAILED_ANOMALY_DESC);
                 }
             }
             return result;
@@ -567,7 +637,7 @@
             return Locale.getDefault().getCountry();
         }
 
-        return country.getCountryIso();
+        return country.getCountryCode();
     }
 
     /**
@@ -578,28 +648,38 @@
     public String getCountryIso() {
         synchronized (mLock) {
             if (mCurrentCountryIso == null) {
-                Log.i(TAG, "Country cache is null. Detecting Country and Setting Cache...");
+                // Moving this into the constructor will pose issues if the service is not yet set
+                // up, causing a RemoteException to be thrown. Note that the callback is only
+                // registered if the country iso cache is null (so in an ideal setting, this should
+                // only require a one-time configuration).
                 final CountryDetector countryDetector =
                         (CountryDetector) mContext.getSystemService(Context.COUNTRY_DETECTOR);
-                Country country = null;
                 if (countryDetector != null) {
-                    country = countryDetector.detectCountry();
-
-                    countryDetector.addCountryListener((newCountry) -> {
-                        Log.startSession("CLM.oCD");
-                        try {
-                            synchronized (mLock) {
-                                Log.i(TAG, "Country ISO changed. Retrieving new ISO...");
-                                mCurrentCountryIso = getCountryIsoFromCountry(newCountry);
-                            }
-                        } finally {
-                            Log.endSession();
-                        }
-                    }, Looper.getMainLooper());
+                    countryDetector.registerCountryDetectorCallback(
+                            mCountryCodeExecutor, this::countryCodeConsumer);
                 }
-                mCurrentCountryIso = getCountryIsoFromCountry(country);
+                mCurrentCountryIso = getCountryIsoFromCountry(mCurrentCountry);
             }
             return mCurrentCountryIso;
         }
     }
+
+    /** Consumer to receive the country code if it changes. */
+    private void countryCodeConsumer(Country newCountry) {
+        Log.startSession("CLM.cCC");
+        try {
+            Log.i(TAG, "Country ISO changed. Retrieving new ISO...");
+            synchronized (mLock) {
+                mCurrentCountry = newCountry;
+                mCurrentCountryIso = getCountryIsoFromCountry(newCountry);
+            }
+        } finally {
+            Log.endSession();
+        }
+    }
+
+    @VisibleForTesting
+    public void setAnomalyReporterAdapter(AnomalyReporterAdapter anomalyReporterAdapter){
+        mAnomalyReporterAdapter = anomalyReporterAdapter;
+    }
 }
diff --git a/src/com/android/server/telecom/CallScreeningServiceHelper.java b/src/com/android/server/telecom/CallScreeningServiceHelper.java
index 9426100..fa436d4 100644
--- a/src/com/android/server/telecom/CallScreeningServiceHelper.java
+++ b/src/com/android/server/telecom/CallScreeningServiceHelper.java
@@ -176,6 +176,10 @@
                             Log.w(TAG, "Cancelling call id process due to timeout");
                         }
                         mFuture.complete(null);
+                        mContext.unbindService(serviceConnection);
+                    } catch (IllegalArgumentException e) {
+                        Log.i(this, "Exception when unbinding service %s : %s", serviceConnection,
+                                e.getMessage());
                     } finally {
                         Log.endSession();
                     }
diff --git a/src/com/android/server/telecom/CallSourceService.java b/src/com/android/server/telecom/CallSourceService.java
new file mode 100644
index 0000000..d579542
--- /dev/null
+++ b/src/com/android/server/telecom/CallSourceService.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2024 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 android.telecom.CallEndpoint;
+
+import java.util.Set;
+
+/**
+ * android.telecom.Call backed Services (i.e. ConnectionService, TransactionalService, etc.) that
+ * have callbacks that can be executed before the service is set (within the Call object) should
+ * implement this interface in order for clients to receive the callback.
+ *
+ * It has been shown that clients can miss important callback information (e.g. available audio
+ * endpoints) if the service is null within the call at the time the callback is sent.  This is a
+ * way to eliminate the timing issue and for clients to receive all callbacks.
+ */
+public interface CallSourceService {
+    void onMuteStateChanged(Call activeCall, boolean isMuted);
+
+    void onCallEndpointChanged(Call activeCall, CallEndpoint callEndpoint);
+
+    void onAvailableCallEndpointsChanged(Call activeCall, Set<CallEndpoint> availableCallEndpoints);
+
+    void onVideoStateChanged(Call activeCall, int videoState);
+}
diff --git a/src/com/android/server/telecom/CallStreamingController.java b/src/com/android/server/telecom/CallStreamingController.java
index 1323633..efd458e 100644
--- a/src/com/android/server/telecom/CallStreamingController.java
+++ b/src/com/android/server/telecom/CallStreamingController.java
@@ -127,7 +127,7 @@
 
             if (mCallsManager.getCallStreamingController().isStreaming()) {
                 future.complete(new VoipCallTransactionResult(
-                        VoipCallTransactionResult.RESULT_FAILED,
+                        CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
                         "STREAMING_FAILED_ALREADY_STREAMING"));
             } else {
                 future.complete(new VoipCallTransactionResult(
@@ -196,7 +196,8 @@
             if (roleManager == null || packageManager == null) {
                 Log.w(this, "processTransaction: Can't find system service");
                 future.complete(new VoipCallTransactionResult(
-                        VoipCallTransactionResult.RESULT_FAILED, MESSAGE));
+                        CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
+                        MESSAGE));
                 return future;
             }
 
@@ -205,7 +206,8 @@
             if (holders.isEmpty()) {
                 Log.w(this, "processTransaction: Can't find streaming app");
                 future.complete(new VoipCallTransactionResult(
-                        VoipCallTransactionResult.RESULT_FAILED, MESSAGE));
+                        CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
+                        MESSAGE));
                 return future;
             }
             Log.i(this, "processTransaction: servicePackage=%s", holders.get(0));
@@ -216,7 +218,8 @@
             if (infos.isEmpty()) {
                 Log.w(this, "processTransaction: Can't find streaming service");
                 future.complete(new VoipCallTransactionResult(
-                        VoipCallTransactionResult.RESULT_FAILED, MESSAGE));
+                        CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
+                        MESSAGE));
                 return future;
             }
 
@@ -227,7 +230,8 @@
                 Log.w(this, "Must require BIND_CALL_STREAMING_SERVICE: " +
                         serviceInfo.packageName);
                 future.complete(new VoipCallTransactionResult(
-                        VoipCallTransactionResult.RESULT_FAILED, MESSAGE));
+                        CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
+                        MESSAGE));
                 return future;
             }
             Intent intent = new Intent(CallStreamingService.SERVICE_INTERFACE);
@@ -239,7 +243,7 @@
                     | Context.BIND_SCHEDULE_LIKE_TOP_APP, mUserHandle)) {
                 Log.w(this, "Can't bind to streaming service");
                 future.complete(new VoipCallTransactionResult(
-                        VoipCallTransactionResult.RESULT_FAILED,
+                        CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
                         "STREAMING_FAILED_SENDER_BINDING_ERROR"));
             }
             return future;
@@ -379,7 +383,8 @@
                         VoipCallTransactionResult.RESULT_SUCCEED, null));
             } catch (RemoteException e) {
                 future.complete(new VoipCallTransactionResult(
-                        VoipCallTransactionResult.RESULT_FAILED, "Exception when request "
+                        CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
+                        "Exception when request "
                         + "setting state to streaming app."));
             }
             return future;
@@ -409,7 +414,7 @@
             } catch (RemoteException e) {
                 resetController();
                 mFuture.complete(new VoipCallTransactionResult(
-                        VoipCallTransactionResult.RESULT_FAILED,
+                        CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
                         StreamingServiceTransaction.MESSAGE));
             }
         }
@@ -433,7 +438,7 @@
             resetController();
             if (!mFuture.isDone()) {
                 mFuture.complete(new VoipCallTransactionResult(
-                        VoipCallTransactionResult.RESULT_FAILED,
+                        CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
                         "STREAMING_FAILED_SENDER_BINDING_ERROR"));
             } else {
                 mWrapper.onCallStreamingFailed(mCall, STREAMING_FAILED_SENDER_BINDING_ERROR);
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index 77570c3..600f847 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -73,15 +73,15 @@
 import android.os.Process;
 import android.os.ResultReceiver;
 import android.os.SystemClock;
+import android.os.SystemProperties;
 import android.os.SystemVibrator;
 import android.os.Trace;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.provider.BlockedNumberContract;
-import android.provider.BlockedNumberContract.SystemContract;
+import android.provider.BlockedNumbersManager;
 import android.provider.CallLog.Calls;
 import android.provider.Settings;
-import android.sysprop.TelephonyProperties;
 import android.telecom.CallAttributes;
 import android.telecom.CallAudioState;
 import android.telecom.CallEndpoint;
@@ -117,6 +117,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.IntentForwarderActivity;
 import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.telecom.bluetooth.BluetoothDeviceManager;
 import com.android.server.telecom.bluetooth.BluetoothRouteManager;
 import com.android.server.telecom.bluetooth.BluetoothStateReceiver;
 import com.android.server.telecom.callfiltering.BlockCheckerAdapter;
@@ -129,9 +130,11 @@
 import com.android.server.telecom.callfiltering.DirectToVoicemailFilter;
 import com.android.server.telecom.callfiltering.DndCallFilter;
 import com.android.server.telecom.callfiltering.IncomingCallFilterGraph;
+import com.android.server.telecom.callfiltering.IncomingCallFilterGraphProvider;
 import com.android.server.telecom.callredirection.CallRedirectionProcessor;
 import com.android.server.telecom.components.ErrorDialogActivity;
 import com.android.server.telecom.components.TelecomBroadcastReceiver;
+import com.android.server.telecom.flags.FeatureFlags;
 import com.android.server.telecom.stats.CallFailureCause;
 import com.android.server.telecom.ui.AudioProcessingNotification;
 import com.android.server.telecom.ui.CallRedirectionTimeoutDialogActivity;
@@ -140,8 +143,8 @@
 import com.android.server.telecom.ui.DisconnectedCallNotifier;
 import com.android.server.telecom.ui.IncomingCallNotifier;
 import com.android.server.telecom.ui.ToastFactory;
-import com.android.server.telecom.voip.TransactionManager;
 import com.android.server.telecom.voip.VoipCallMonitor;
+import com.android.server.telecom.voip.TransactionManager;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -210,7 +213,7 @@
         void onHoldToneRequested(Call call);
         void onExternalCallChanged(Call call, boolean isExternalCall);
         void onCallStreamingStateChanged(Call call, boolean isStreaming);
-        void onDisconnectedTonePlaying(boolean isTonePlaying);
+        void onDisconnectedTonePlaying(Call call, boolean isTonePlaying);
         void onConnectionTimeChanged(Call call);
         void onConferenceStateChanged(Call call, boolean isConference);
         void onCdmaConferenceSwap(Call call);
@@ -222,6 +225,29 @@
         void performAction();
     }
 
+    /**
+     * @hide
+     */
+    public interface Response<IN, OUT> {
+
+        /**
+         * Provide a set of results.
+         *
+         * @param request The original request.
+         * @param result The results.
+         */
+        void onResult(IN request, OUT... result);
+
+        /**
+         * Indicates the inability to provide results.
+         *
+         * @param request The original request.
+         * @param code An integer code indicating the reason for failure.
+         * @param msg A message explaining the reason for failure.
+         */
+        void onError(IN request, int code, String msg);
+    }
+
     private static final String TAG = "CallsManager";
 
     /**
@@ -286,15 +312,14 @@
     public static final String EXCEPTION_RETRIEVING_PHONE_ACCOUNTS_EMERGENCY_ERROR_MSG =
             "Exception thrown while retrieving list of potential phone accounts when placing an "
                     + "emergency call.";
-    public static final UUID EMERGENCY_CALL_DISCONNECTED_BEFORE_BEING_ADDED_ERROR_UUID =
-            UUID.fromString("f9a916c8-8d61-4550-9ad3-11c2e84f6364");
-    public static final String EMERGENCY_CALL_DISCONNECTED_BEFORE_BEING_ADDED_ERROR_MSG =
-            "An emergency call was disconnected after the connection was created but before the "
-                    + "call was successfully added to CallsManager.";
     public static final UUID EMERGENCY_CALL_ABORTED_NO_PHONE_ACCOUNTS_ERROR_UUID =
             UUID.fromString("2e994acb-1997-4345-8bf3-bad04303de26");
     public static final String EMERGENCY_CALL_ABORTED_NO_PHONE_ACCOUNTS_ERROR_MSG =
             "An emergency call was aborted since there were no available phone accounts.";
+    public static final UUID TELEPHONY_HAS_DEFAULT_BUT_TELECOM_DOES_NOT_UUID =
+            UUID.fromString("0a86157c-50ca-11ee-be56-0242ac120002");
+    public static final String TELEPHONY_HAS_DEFAULT_BUT_TELECOM_DOES_NOT_MSG =
+            "Telephony has a default MO acct but Telecom prompted user for MO";
 
     private static final int[] OUTGOING_CALL_STATES =
             {CallState.CONNECTING, CallState.SELECT_PHONE_ACCOUNT, CallState.DIALING,
@@ -463,14 +488,23 @@
     private final TransactionManager mTransactionManager;
     private final UserManager mUserManager;
     private final CallStreamingNotification mCallStreamingNotification;
+    private final BlockedNumbersManager mBlockedNumbersManager;
+    private final FeatureFlags mFeatureFlags;
+    private final com.android.internal.telephony.flags.FeatureFlags mTelephonyFeatureFlags;
+
+    private final IncomingCallFilterGraphProvider mIncomingCallFilterGraphProvider;
 
     private final ConnectionServiceFocusManager.CallsManagerRequester mRequester =
             new ConnectionServiceFocusManager.CallsManagerRequester() {
                 @Override
                 public void releaseConnectionService(
                         ConnectionServiceFocusManager.ConnectionServiceFocus connectionService) {
+                    if (connectionService == null) {
+                        Log.i(this, "releaseConnectionService: connectionService is null");
+                        return;
+                    }
                     mCalls.stream()
-                            .filter(c -> c.getConnectionServiceWrapper().equals(connectionService))
+                            .filter(c -> connectionService.equals(c.getConnectionServiceWrapper()))
                             .forEach(c -> c.disconnect("release " +
                                     connectionService.getComponentName().getPackageName()));
                 }
@@ -527,7 +561,8 @@
         public void onReceive(Context context, Intent intent) {
             String action = intent.getAction();
             if (CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED.equals(action)
-                    || SystemContract.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED.equals(action)) {
+                    || BlockedNumbersManager
+                    .ACTION_BLOCK_SUPPRESSION_STATE_CHANGED.equals(action)) {
                 updateEmergencyCallNotificationAsync(context);
             }
         }
@@ -572,10 +607,16 @@
             CallAnomalyWatchdog callAnomalyWatchdog,
             Ringer.AccessibilityManagerAdapter accessibilityManagerAdapter,
             Executor asyncTaskExecutor,
+            Executor asyncCallAudioTaskExecutor,
             BlockedNumbersAdapter blockedNumbersAdapter,
             TransactionManager transactionManager,
             EmergencyCallDiagnosticLogger emergencyCallDiagnosticLogger,
-            CallStreamingNotification callStreamingNotification) {
+            CallAudioCommunicationDeviceTracker communicationDeviceTracker,
+            CallStreamingNotification callStreamingNotification,
+            BluetoothDeviceManager bluetoothDeviceManager,
+            FeatureFlags featureFlags,
+            com.android.internal.telephony.flags.FeatureFlags telephonyFlags,
+            IncomingCallFilterGraphProvider incomingCallFilterGraphProvider) {
 
         mContext = context;
         mLock = lock;
@@ -594,28 +635,43 @@
         mEmergencyCallHelper = emergencyCallHelper;
         mCallerInfoLookupHelper = callerInfoLookupHelper;
         mEmergencyCallDiagnosticLogger = emergencyCallDiagnosticLogger;
+        mIncomingCallFilterGraphProvider = incomingCallFilterGraphProvider;
 
         mDtmfLocalTonePlayer =
                 new DtmfLocalTonePlayer(new DtmfLocalTonePlayer.ToneGeneratorProxy());
-        CallAudioRouteStateMachine callAudioRouteStateMachine =
-                callAudioRouteStateMachineFactory.create(
-                        context,
-                        this,
-                        bluetoothManager,
-                        wiredHeadsetManager,
-                        statusBarNotifier,
-                        audioServiceFactory,
-                        CallAudioRouteStateMachine.EARPIECE_AUTO_DETECT,
-                        asyncTaskExecutor
-                );
-        callAudioRouteStateMachine.initialize();
+        CallAudioRouteAdapter callAudioRouteAdapter;
+        // TODO: add another flag check when
+        // bluetoothDeviceManager.getBluetoothHeadset().isScoManagedByAudio()
+        // available and return true
+        if (!featureFlags.useRefactoredAudioRouteSwitching()) {
+            callAudioRouteAdapter = callAudioRouteStateMachineFactory.create(
+                    context,
+                    this,
+                    bluetoothManager,
+                    wiredHeadsetManager,
+                    statusBarNotifier,
+                    audioServiceFactory,
+                    CallAudioRouteStateMachine.EARPIECE_AUTO_DETECT,
+                    asyncCallAudioTaskExecutor,
+                    communicationDeviceTracker,
+                    featureFlags
+            );
+        } else {
+            callAudioRouteAdapter = new CallAudioRouteController(context, this, audioServiceFactory,
+                    new AudioRoute.Factory(), wiredHeadsetManager, mBluetoothRouteManager,
+                    statusBarNotifier, featureFlags);
+        }
+        callAudioRouteAdapter.initialize();
+        bluetoothStateReceiver.setCallAudioRouteAdapter(callAudioRouteAdapter);
+        bluetoothDeviceManager.setCallAudioRouteAdapter(callAudioRouteAdapter);
 
         CallAudioRoutePeripheralAdapter callAudioRoutePeripheralAdapter =
                 new CallAudioRoutePeripheralAdapter(
-                        callAudioRouteStateMachine,
+                        callAudioRouteAdapter,
                         bluetoothManager,
                         wiredHeadsetManager,
-                        mDockManager);
+                        mDockManager,
+                        asyncRingtonePlayer);
         AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
         InCallTonePlayer.MediaPlayerFactory mediaPlayerFactory =
                 (resourceId, attributes) ->
@@ -627,7 +683,7 @@
                 () -> audioManager.getStreamVolume(AudioManager.STREAM_RING) > 0);
 
         SystemSettingsUtil systemSettingsUtil = new SystemSettingsUtil();
-        RingtoneFactory ringtoneFactory = new RingtoneFactory(this, context);
+        RingtoneFactory ringtoneFactory = new RingtoneFactory(this, context, featureFlags);
         SystemVibrator systemVibrator = new SystemVibrator(context);
         mInCallController = inCallControllerFactory.create(context, mLock, this,
                 systemStateHelper, defaultDialerCache, mTimeoutsAdapter,
@@ -639,37 +695,50 @@
                 ringtoneFactory, systemVibrator,
                 new Ringer.VibrationEffectProxy(), mInCallController,
                 mContext.getSystemService(NotificationManager.class),
-                accessibilityManagerAdapter);
+                accessibilityManagerAdapter, featureFlags);
         mCallRecordingTonePlayer = new CallRecordingTonePlayer(mContext, audioManager,
                 mTimeoutsAdapter, mLock);
-        mCallAudioManager = new CallAudioManager(callAudioRouteStateMachine,
+        mCallAudioManager = new CallAudioManager(callAudioRouteAdapter,
                 this, callAudioModeStateMachineFactory.create(systemStateHelper,
-                (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE)),
+                (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE),
+                featureFlags, communicationDeviceTracker),
                 playerFactory, mRinger, new RingbackPlayer(playerFactory),
-                bluetoothStateReceiver, mDtmfLocalTonePlayer);
+                bluetoothStateReceiver, mDtmfLocalTonePlayer, featureFlags);
 
         mConnectionSvrFocusMgr = connectionServiceFocusManagerFactory.create(mRequester);
         mHeadsetMediaButton = headsetMediaButtonFactory.create(context, this, mLock);
         mTtyManager = new TtyManager(context, mWiredHeadsetManager);
         mProximitySensorManager = proximitySensorManagerFactory.create(context, this);
         mPhoneStateBroadcaster = new PhoneStateBroadcaster(this);
-        mCallLogManager = new CallLogManager(context, phoneAccountRegistrar, mMissedCallNotifier);
+        mCallLogManager = new CallLogManager(context, phoneAccountRegistrar, mMissedCallNotifier,
+                mAnomalyReporter, featureFlags);
         mConnectionServiceRepository =
-                new ConnectionServiceRepository(mPhoneAccountRegistrar, mContext, mLock, this);
+                new ConnectionServiceRepository(mPhoneAccountRegistrar, mContext, mLock, this,
+                        featureFlags);
         mInCallWakeLockController = inCallWakeLockControllerFactory.create(context, this);
         mClockProxy = clockProxy;
         mToastFactory = toastFactory;
         mRoleManagerAdapter = roleManagerAdapter;
+        mVoipCallMonitor = new VoipCallMonitor(mContext, mLock);
         mTransactionManager = transactionManager;
         mBlockedNumbersAdapter = blockedNumbersAdapter;
         mCallStreamingController = new CallStreamingController(mContext, mLock);
-        mVoipCallMonitor = new VoipCallMonitor(mContext, mLock);
         mCallStreamingNotification = callStreamingNotification;
+        mFeatureFlags = featureFlags;
+        mTelephonyFeatureFlags = telephonyFlags;
+        mBlockedNumbersManager = mFeatureFlags.telecomMainlineBlockedNumbersManager()
+                ? mContext.getSystemService(BlockedNumbersManager.class)
+                : null;
 
+        if (mFeatureFlags.useImprovedListenerOrder()) {
+            mListeners.add(mInCallController);
+        }
         mListeners.add(mInCallWakeLockController);
         mListeners.add(statusBarNotifier);
         mListeners.add(mCallLogManager);
-        mListeners.add(mInCallController);
+        if (!mFeatureFlags.useImprovedListenerOrder()) {
+            mListeners.add(mInCallController);
+        }
         mListeners.add(mCallEndpointController);
         mListeners.add(mCallDiagnosticServiceController);
         mListeners.add(mCallAudioManager);
@@ -691,7 +760,7 @@
         mVoipCallMonitor.startMonitor();
 
         // There is no USER_SWITCHED broadcast for user 0, handle it here explicitly.
-        final UserManager userManager = UserManager.get(mContext);
+        final UserManager userManager = mContext.getSystemService(UserManager.class);
         // Don't load missed call if it is run in split user model.
         if (userManager.isPrimaryUser()) {
             onUserSwitch(Process.myUserHandle());
@@ -700,7 +769,7 @@
         IntentFilter intentFilter = new IntentFilter(
                 CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED);
         intentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
-        intentFilter.addAction(SystemContract.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED);
+        intentFilter.addAction(BlockedNumbersManager.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED);
         context.registerReceiver(mReceiver, intentFilter, Context.RECEIVER_EXPORTED);
         mGraphHandlerThreads = new LinkedList<>();
 
@@ -744,11 +813,14 @@
     @Override
     @VisibleForTesting
     public void onSuccessfulOutgoingCall(Call call, int callState) {
-        Log.v(this, "onSuccessfulOutgoingCall, %s", call);
+        Log.v(this, "onSuccessfulOutgoingCall, call=[%s], state=[%d]", call, callState);
         call.setPostCallPackageName(getRoleManagerAdapter().getDefaultCallScreeningApp(
                 call.getAssociatedUser()));
 
-        setCallState(call, callState, "successful outgoing call");
+        if (!mFeatureFlags.fixAudioFlickerForOutgoingCalls()) {
+            setCallState(call, callState, "successful outgoing call");
+        }
+
         if (!mCalls.contains(call)) {
             // Call was not added previously in startOutgoingCall due to it being a potential MMI
             // code, so add it now.
@@ -760,7 +832,18 @@
             listener.onConnectionServiceChanged(call, null, call.getConnectionService());
         }
 
-        markCallAsDialing(call);
+        if (mFeatureFlags.fixAudioFlickerForOutgoingCalls()) {
+            // Allow the ConnectionService to start the call in the active state. This case is
+            // helpful for conference calls or meetings that can skip the dialing stage.
+            if (callState == CallState.ACTIVE) {
+                setCallState(call, callState, "skipping the dialing state and setting active");
+            } else {
+                markCallAsDialing(call);
+            }
+        }
+        else{
+            markCallAsDialing(call);
+        }
     }
 
     @Override
@@ -779,18 +862,25 @@
                 ? new Bundle()
                 : phoneAccount.getExtras();
         TelephonyManager telephonyManager = getTelephonyManager();
+        boolean isInEmergencySmsMode;
+        try {
+            isInEmergencySmsMode = telephonyManager.isInEmergencySmsMode();
+        } catch (UnsupportedOperationException uoe) {
+            isInEmergencySmsMode = false;
+        }
+        boolean performDndFilter = mFeatureFlags.skipFilterPhoneAccountPerformDndFilter();
         if (incomingCall.hasProperty(Connection.PROPERTY_EMERGENCY_CALLBACK_MODE) ||
                 incomingCall.hasProperty(Connection.PROPERTY_NETWORK_IDENTIFIED_EMERGENCY_CALL) ||
-                telephonyManager.isInEmergencySmsMode() ||
+                isInEmergencySmsMode ||
                 incomingCall.isSelfManaged() ||
-                extras.getBoolean(PhoneAccount.EXTRA_SKIP_CALL_FILTERING)) {
+                (!performDndFilter && extras.getBoolean(PhoneAccount.EXTRA_SKIP_CALL_FILTERING))) {
             Log.i(this, "Skipping call filtering for %s (ecm=%b, "
                             + "networkIdentifiedEmergencyCall = %b, emergencySmsMode = %b, "
                             + "selfMgd=%b, skipExtra=%b)",
                     incomingCall.getId(),
                     incomingCall.hasProperty(Connection.PROPERTY_EMERGENCY_CALLBACK_MODE),
                     incomingCall.hasProperty(Connection.PROPERTY_NETWORK_IDENTIFIED_EMERGENCY_CALL),
-                    telephonyManager.isInEmergencySmsMode(),
+                    isInEmergencySmsMode,
                     incomingCall.isSelfManaged(),
                     extras.getBoolean(PhoneAccount.EXTRA_SKIP_CALL_FILTERING));
             onCallFilteringComplete(incomingCall, new Builder()
@@ -801,30 +891,45 @@
                     .build(), false);
             incomingCall.setIsUsingCallFiltering(false);
             return;
+        } else if (performDndFilter && extras.getBoolean(PhoneAccount.EXTRA_SKIP_CALL_FILTERING)) {
+            IncomingCallFilterGraph graph = setupDndFilterOnlyGraph(incomingCall);
+            graph.performFiltering();
+            return;
         }
 
         IncomingCallFilterGraph graph = setUpCallFilterGraph(incomingCall);
         graph.performFiltering();
     }
 
+    private IncomingCallFilterGraph setupDndFilterOnlyGraph(Call incomingHfpCall) {
+        incomingHfpCall.setIsUsingCallFiltering(true);
+        DndCallFilter dndCallFilter = new DndCallFilter(incomingHfpCall, mRinger);
+        IncomingCallFilterGraph graph = mIncomingCallFilterGraphProvider.createGraph(
+                incomingHfpCall,
+                this::onCallFilteringComplete, mContext, mTimeoutsAdapter, mLock);
+        graph.addFilter(dndCallFilter);
+        mGraphHandlerThreads.add(graph.getHandlerThread());
+        return graph;
+    }
+
     private IncomingCallFilterGraph setUpCallFilterGraph(Call incomingCall) {
+        TelecomManager telecomManager = mContext.getSystemService(TelecomManager.class);
         incomingCall.setIsUsingCallFiltering(true);
         String carrierPackageName = getCarrierPackageName();
         UserHandle userHandle = incomingCall.getAssociatedUser();
-        String defaultDialerPackageName = TelecomManager.from(mContext).
-                getDefaultDialerPackage(userHandle);
+        String defaultDialerPackageName = telecomManager.getDefaultDialerPackage(userHandle);
         String userChosenPackageName = getRoleManagerAdapter().
                 getDefaultCallScreeningApp(userHandle);
         AppLabelProxy appLabelProxy = packageName -> AppLabelProxy.Util.getAppLabel(
                 mContext.getPackageManager(), packageName);
         ParcelableCallUtils.Converter converter = new ParcelableCallUtils.Converter();
 
-        IncomingCallFilterGraph graph = new IncomingCallFilterGraph(incomingCall,
+        IncomingCallFilterGraph graph = mIncomingCallFilterGraphProvider.createGraph(incomingCall,
                 this::onCallFilteringComplete, mContext, mTimeoutsAdapter, mLock);
         DirectToVoicemailFilter voicemailFilter = new DirectToVoicemailFilter(incomingCall,
                 mCallerInfoLookupHelper);
         BlockCheckerFilter blockCheckerFilter = new BlockCheckerFilter(mContext, incomingCall,
-                mCallerInfoLookupHelper, new BlockCheckerAdapter());
+                mCallerInfoLookupHelper, new BlockCheckerAdapter(mFeatureFlags));
         DndCallFilter dndCallFilter = new DndCallFilter(incomingCall, getRinger());
         CallScreeningServiceFilter carrierCallScreeningServiceFilter =
                 new CallScreeningServiceFilter(incomingCall, carrierPackageName,
@@ -859,6 +964,7 @@
         ComponentName componentName = null;
         CarrierConfigManager configManager = (CarrierConfigManager) mContext.getSystemService
                 (Context.CARRIER_CONFIG_SERVICE);
+        if (configManager == null) return null;
         PersistableBundle configBundle = configManager.getConfig();
         if (configBundle != null) {
             componentName = ComponentName.unflattenFromString(configBundle.getString
@@ -884,8 +990,10 @@
 
         if (incomingCall.getState() != CallState.DISCONNECTED &&
                 incomingCall.getState() != CallState.DISCONNECTING) {
-            setCallState(incomingCall, CallState.RINGING,
-                    result.shouldAllowCall ? "successful incoming call" : "blocking call");
+            if (!mFeatureFlags.separatelyBindToBtIncallService()) {
+                setCallState(incomingCall, CallState.RINGING,
+                        result.shouldAllowCall ? "successful incoming call" : "blocking call");
+            }
         } else {
             Log.i(this, "onCallFilteringCompleted: call already disconnected.");
             return;
@@ -930,6 +1038,11 @@
         }
 
         if (result.shouldAllowCall) {
+            if (mFeatureFlags.separatelyBindToBtIncallService()) {
+                mInCallController.bindToBTService(incomingCall, null);
+                incomingCall.setBtIcsFuture(mInCallController.getBtBindingFuture(incomingCall));
+                setCallState(incomingCall, CallState.RINGING, "successful incoming call");
+            }
             incomingCall.setPostCallPackageName(
                     getRoleManagerAdapter().getDefaultCallScreeningApp(
                             incomingCall.getAssociatedUser()
@@ -944,7 +1057,6 @@
                             "Exceeds maximum number of ringing calls.");
                     incomingCall.setMissedReason(AUTO_MISSED_MAXIMUM_RINGING);
                     autoMissCallAndLog(incomingCall, result);
-                    return;
                 }
             } else if (hasMaximumManagedDialingCalls(incomingCall)) {
                 if (shouldSilenceInsteadOfReject(incomingCall)) {
@@ -954,7 +1066,6 @@
                             "dialing calls.");
                     incomingCall.setMissedReason(AUTO_MISSED_MAXIMUM_DIALING);
                     autoMissCallAndLog(incomingCall, result);
-                    return;
                 }
             } else if (result.shouldScreenViaAudio) {
                 Log.i(this, "onCallFilteringCompleted: starting background audio processing");
@@ -973,6 +1084,9 @@
         } else {
             if (result.shouldReject) {
                 Log.i(this, "onCallFilteringCompleted: blocked call, rejecting.");
+                if (mFeatureFlags.separatelyBindToBtIncallService()) {
+                    setCallState(incomingCall, CallState.RINGING, "blocking call");
+                }
                 incomingCall.reject(false, null);
             }
             if (result.shouldAddToCallLog) {
@@ -986,7 +1100,7 @@
             if (result.shouldShowNotification) {
                 Log.i(this, "onCallScreeningCompleted: blocked call, showing notification.");
                 mMissedCallNotifier.showMissedCallNotification(
-                        new MissedCallNotifier.CallInfo(incomingCall));
+                        new MissedCallNotifier.CallInfo(incomingCall), /* uri= */ null);
             }
         }
     }
@@ -1244,9 +1358,7 @@
     @Override
     public void onHandoverRequested(Call call, PhoneAccountHandle handoverTo, int videoState,
                                     Bundle extras, boolean isLegacy) {
-        if (isLegacy) {
-            requestHandoverViaEvents(call, handoverTo, videoState, extras);
-        } else {
+        if (!isLegacy) {
             requestHandover(call, handoverTo, videoState, extras);
         }
     }
@@ -1299,7 +1411,7 @@
         return mCallAudioManager;
     }
 
-    InCallController getInCallController() {
+    public InCallController getInCallController() {
         return mInCallController;
     }
 
@@ -1307,7 +1419,7 @@
         return mCallEndpointController;
     }
 
-    EmergencyCallHelper getEmergencyCallHelper() {
+    public EmergencyCallHelper getEmergencyCallHelper() {
         return mEmergencyCallHelper;
     }
 
@@ -1379,8 +1491,11 @@
     }
 
     @VisibleForTesting
-    public void setAnomalyReporterAdapter(AnomalyReporterAdapter mAnomalyReporterAdapter){
-        mAnomalyReporter = mAnomalyReporterAdapter;
+    public void setAnomalyReporterAdapter(AnomalyReporterAdapter anomalyReporterAdapter){
+        mAnomalyReporter = anomalyReporterAdapter;
+        if (mCallLogManager != null) {
+            mCallLogManager.setAnomalyReporterAdapter(anomalyReporterAdapter);
+        }
     }
 
     void processIncomingConference(PhoneAccountHandle phoneAccountHandle, Bundle extras) {
@@ -1425,7 +1540,8 @@
                 false /* forceAttachToExistingConnection */,
                 isConference, /* isConference */
                 mClockProxy,
-                mToastFactory);
+                mToastFactory,
+                mFeatureFlags);
         // Ensure new calls related to self-managed calls/connections are set as such. This will
         // be overridden when the actual connection is returned in startCreateConnection, however
         // doing this now ensures the logs and any other logic will treat this call as self-managed
@@ -1452,7 +1568,10 @@
                 }
             }
             // Incoming address was set via EXTRA_INCOMING_CALL_ADDRESS above.
-            call.setAssociatedUser(phoneAccountHandle.getUserHandle());
+            UserHandle associatedUser = UserUtil.getAssociatedUserForCall(
+                    mFeatureFlags.associatedUserRefactorForWorkProfile(),
+                    getPhoneAccountRegistrar(), getCurrentUserHandle(), phoneAccountHandle);
+            call.setAssociatedUser(associatedUser);
         }
 
         if (phoneAccount != null) {
@@ -1572,15 +1691,25 @@
         // Check if the target phone account is possibly in ECBM.
         call.setIsInECBM(getEmergencyCallHelper()
                 .isLastOutgoingEmergencyCallPAH(call.getTargetPhoneAccount()));
-        // If the phone account user profile is paused or the call isn't visible to the secondary/
-        // guest user, reject the non-emergency incoming call. When the current user is the admin,
-        // we need to allow the calls to go through if the work profile isn't paused. We should
-        // always allow emergency calls and also allow non-emergency calls when ECBM is active for
-        // the phone account.
-        if ((mUserManager.isQuietModeEnabled(call.getAssociatedUser())
-                || (!mUserManager.isUserAdmin(mCurrentUserHandle.getIdentifier())
-                && !isCallVisibleForUser(call, mCurrentUserHandle)))
-                && !call.isEmergencyCall() && !call.isInECBM()) {
+
+        // Check if call is visible to the current user.
+        boolean isCallHiddenFromProfile = !isCallVisibleForUser(call, mCurrentUserHandle);
+        // For admins, we should check if the work profile is paused in order to reject
+        // the call.
+        UserManager currentUserManager = mContext.createContextAsUser(mCurrentUserHandle, 0)
+                .getSystemService(UserManager.class);
+        boolean isCurrentUserAdmin = mFeatureFlags.telecomResolveHiddenDependencies()
+                ? currentUserManager.isAdminUser()
+                : mUserManager.isUserAdmin(mCurrentUserHandle.getIdentifier());
+        if (isCurrentUserAdmin) {
+            isCallHiddenFromProfile &= mFeatureFlags.telecomResolveHiddenDependencies()
+                    ? currentUserManager.isQuietModeEnabled(call.getAssociatedUser())
+                    : mUserManager.isQuietModeEnabled(call.getAssociatedUser());
+        }
+
+        // We should always allow emergency calls and also allow non-emergency calls when ECBM
+        // is active for the phone account.
+        if (isCallHiddenFromProfile && !call.isEmergencyCall() && !call.isInECBM()) {
             Log.d(TAG, "Rejecting non-emergency call because the owner %s is not running.",
                     phoneAccountHandle.getUserHandle());
             call.setMissedReason(USER_MISSED_NOT_RUNNING);
@@ -1653,11 +1782,15 @@
                 true /* forceAttachToExistingConnection */,
                 false, /* isConference */
                 mClockProxy,
-                mToastFactory);
+                mToastFactory,
+                mFeatureFlags);
         call.initAnalytics();
 
         // For unknown calls, base the associated user off of the target phone account handle.
-        call.setAssociatedUser(phoneAccountHandle.getUserHandle());
+        UserHandle associatedUser = UserUtil.getAssociatedUserForCall(
+                mFeatureFlags.associatedUserRefactorForWorkProfile(),
+                getPhoneAccountRegistrar(), getCurrentUserHandle(), phoneAccountHandle);
+        call.setAssociatedUser(associatedUser);
         setIntentExtrasAndStartTime(call, extras);
         call.addListener(this);
         notifyStartCreateConnection(call);
@@ -1771,7 +1904,8 @@
                     false /* forceAttachToExistingConnection */,
                     isConference, /* isConference */
                     mClockProxy,
-                    mToastFactory);
+                    mToastFactory,
+                    mFeatureFlags);
 
             if (extras.containsKey(TelecomManager.TRANSACTION_CALL_ID_KEY)) {
                 call.setIsTransactionalCall(true);
@@ -1793,19 +1927,25 @@
 
             // Log info for emergency call
             if (call.isEmergencyCall()) {
-                String simNumeric = "";
-                String networkNumeric = "";
-                int defaultVoiceSubId = SubscriptionManager.getDefaultVoiceSubscriptionId();
-                if (defaultVoiceSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
-                    TelephonyManager tm = getTelephonyManager().createForSubscriptionId(
-                            defaultVoiceSubId);
-                    CellIdentity cellIdentity = tm.getLastKnownCellIdentity();
-                    simNumeric = tm.getSimOperatorNumeric();
-                    networkNumeric = (cellIdentity != null) ? cellIdentity.getPlmn() : "";
-                }
-                TelecomStatsLog.write(TelecomStatsLog.EMERGENCY_NUMBER_DIALED,
+                try {
+                    String simNumeric = "";
+                    String networkNumeric = "";
+                    int defaultVoiceSubId = SubscriptionManager.getDefaultVoiceSubscriptionId();
+                    if (defaultVoiceSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+                        TelephonyManager tm = getTelephonyManager().createForSubscriptionId(
+                                defaultVoiceSubId);
+                        CellIdentity cellIdentity = tm.getLastKnownCellIdentity();
+                        simNumeric = tm.getSimOperatorNumeric();
+                        networkNumeric = (cellIdentity != null) ? cellIdentity.getPlmn() : "";
+                    }
+                    TelecomStatsLog.write(TelecomStatsLog.EMERGENCY_NUMBER_DIALED,
                             handle.getSchemeSpecificPart(),
                             callingPackage, simNumeric, networkNumeric);
+                } catch (UnsupportedOperationException uoe) {
+                    // Ignore; likely we should not be able to get here since emergency calls
+                    // require Telephony at the current time, however that could change in the
+                    // future, so we best be safe.
+                }
             }
 
             // Ensure new calls related to self-managed calls/connections are set as such.  This
@@ -2027,7 +2167,7 @@
                                 Uri callUri = callToPlace.getHandle();
                                 if (PhoneAccount.SCHEME_TEL.equals(callUri.getScheme())) {
                                     int managedProfileUserId = getManagedProfileUserId(mContext,
-                                            initiatingUser.getIdentifier());
+                                            initiatingUser.getIdentifier(), mFeatureFlags);
                                     if (managedProfileUserId != UserHandle.USER_NULL
                                             &&
                                             mPhoneAccountRegistrar.getCallCapablePhoneAccounts(
@@ -2047,7 +2187,7 @@
                                         + " available accounts.");
                                 showErrorMessage(R.string.cant_call_due_to_no_supported_service);
                                 mListeners.forEach(l -> l.onCreateConnectionFailed(callToPlace));
-                                if (callToPlace.isEmergencyCall()){
+                                if (callToPlace.isEmergencyCall()) {
                                     mAnomalyReporter.reportAnomaly(
                                             EMERGENCY_CALL_ABORTED_NO_PHONE_ACCOUNTS_ERROR_UUID,
                                             EMERGENCY_CALL_ABORTED_NO_PHONE_ACCOUNTS_ERROR_MSG);
@@ -2060,6 +2200,26 @@
                                 return CompletableFuture.completedFuture(Pair.create(callToPlace,
                                         accountSuggestions.get(0).getPhoneAccountHandle()));
                             }
+
+                            // At this point Telecom is requesting the user to select a phone
+                            // account. However, Telephony is reporting that the user has a default
+                            // outgoing account (which is denoted by a non-negative subId number).
+                            // At some point, Telecom and Telephony are out of sync with the default
+                            // outgoing calling account.
+                            if(mFeatureFlags.telephonyHasDefaultButTelecomDoesNot()) {
+                                // SubscriptionManager will throw if FEATURE_TELEPHONY_SUBSCRIPTION
+                                // is not present.
+                                if (mContext.getPackageManager().hasSystemFeature(
+                                        PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION)) {
+                                    if (SubscriptionManager.getDefaultVoiceSubscriptionId() !=
+                                            SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+                                        mAnomalyReporter.reportAnomaly(
+                                                TELEPHONY_HAS_DEFAULT_BUT_TELECOM_DOES_NOT_UUID,
+                                                TELEPHONY_HAS_DEFAULT_BUT_TELECOM_DOES_NOT_MSG);
+                                    }
+                                }
+                            }
+
                             // This is the state where the user is expected to select an account
                             callToPlace.setState(CallState.SELECT_PHONE_ACCOUNT,
                                     "needs account selection");
@@ -2187,15 +2347,35 @@
         return mLatestPostSelectionProcessingFuture;
     }
 
-    private static int getManagedProfileUserId(Context context, int userId) {
-        UserManager um = context.getSystemService(UserManager.class);
-        List<UserInfo> userProfiles = um.getProfiles(userId);
-        for (UserInfo uInfo : userProfiles) {
-            if (uInfo.id == userId) {
-                continue;
+    private static int getManagedProfileUserId(Context context, int userId,
+            FeatureFlags featureFlags) {
+        UserManager um;
+        UserHandle userHandle = UserHandle.of(userId);
+        um = featureFlags.telecomResolveHiddenDependencies()
+                ? context.createContextAsUser(userHandle, 0).getSystemService(UserManager.class)
+                : context.getSystemService(UserManager.class);
+
+        if (featureFlags.telecomResolveHiddenDependencies()) {
+            List<UserHandle> userProfiles = um.getAllProfiles();
+            for (UserHandle userProfile : userProfiles) {
+                UserManager profileUserManager = context.createContextAsUser(userProfile, 0)
+                        .getSystemService(UserManager.class);
+                if (userProfile.getIdentifier() == userId) {
+                    continue;
+                }
+                if (profileUserManager.isManagedProfile()) {
+                    return userProfile.getIdentifier();
+                }
             }
-            if (uInfo.isManagedProfile()) {
-                return uInfo.id;
+        } else {
+            List<UserInfo> userInfoProfiles = um.getProfiles(userId);
+            for (UserInfo uInfo : userInfoProfiles) {
+                if (uInfo.id == userId) {
+                    continue;
+                }
+                if (uInfo.isManagedProfile()) {
+                    return uInfo.id;
+                }
             }
         }
         return UserHandle.USER_NULL;
@@ -2303,6 +2483,15 @@
 
          PhoneAccountHandle phoneAccountHandle = clientExtras.getParcelable(
                  TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE);
+         PhoneAccount account =
+                mPhoneAccountRegistrar.getPhoneAccount(phoneAccountHandle, initiatingUser);
+         boolean isSelfManaged = account != null && account.isSelfManaged();
+         // Enforce outgoing call restriction for conference calls. This is handled via
+         // UserCallIntentProcessor for normal MO calls.
+         if (UserUtil.hasOutgoingCallsUserRestriction(mContext, initiatingUser, null,
+                 isSelfManaged, CallsManager.class.getCanonicalName(), mFeatureFlags)) {
+             return;
+         }
          CompletableFuture<Call> callFuture = startOutgoingCall(participants, phoneAccountHandle,
                  clientExtras, initiatingUser, null/* originalIntent */, callingPackage,
                  true/* isconference*/);
@@ -2511,6 +2700,9 @@
             isEmergencyNumber =
                     handle != null && getTelephonyManager().isEmergencyNumber(
                             handle.getSchemeSpecificPart());
+        } catch (UnsupportedOperationException uoe) {
+            // If device has no telephony, we can't check if it is an emergency call.
+            isEmergencyNumber = false;
         } catch (IllegalStateException ise) {
             isEmergencyNumber = false;
         } catch (RuntimeException r) {
@@ -2799,9 +2991,13 @@
         }
 
         if (call.isEmergencyCall()) {
-            Executors.defaultThreadFactory().newThread(() ->
-                    BlockedNumberContract.SystemContract.notifyEmergencyContact(mContext))
-                    .start();
+            Executors.defaultThreadFactory().newThread(() -> {
+                if (mBlockedNumbersManager != null) {
+                    mBlockedNumbersManager.notifyEmergencyContact();
+                } else {
+                    BlockedNumberContract.SystemContract.notifyEmergencyContact(mContext);
+                }
+            }).start();
         }
 
         final boolean requireCallCapableAccountByHandle = mContext.getResources().getBoolean(
@@ -2879,9 +3075,11 @@
             // from the client via a transaction before answering.
             call.answer(videoState);
         } else {
+            if (!mFeatureFlags.genAnomReportOnFocusTimeout()) {
+                Call activeCall = (Call) mConnectionSvrFocusMgr.getCurrentFocusCall();
+                Log.d(this, "answerCall: Incoming call = %s Ongoing call %s", call, activeCall);
+            }
             // Hold or disconnect the active call and request call focus for the incoming call.
-            Call activeCall = (Call) mConnectionSvrFocusMgr.getCurrentFocusCall();
-            Log.d(this, "answerCall: Incoming call = %s Ongoing call %s", call, activeCall);
             holdActiveCallForNewCall(call);
             mConnectionSvrFocusMgr.requestFocus(
                     call,
@@ -3033,9 +3231,8 @@
      * @return {@code true} if the speakerphone should automatically be enabled.
      */
     private static boolean isSpeakerEnabledForVideoCalls() {
-        return TelephonyProperties.videocall_audio_output()
-                .orElse(TelecomManager.AUDIO_OUTPUT_DEFAULT)
-                == TelecomManager.AUDIO_OUTPUT_ENABLE_SPEAKER;
+        return SystemProperties.getInt(TelecomManager.PROPERTY_VIDEOCALL_AUDIO_OUTPUT,
+                TelecomManager.AUDIO_OUTPUT_DEFAULT) == TelecomManager.AUDIO_OUTPUT_ENABLE_SPEAKER;
     }
 
     /**
@@ -3295,23 +3492,33 @@
     // then include only that SIM based PhoneAccount and any non-SIM PhoneAccounts, such as SIP.
     @VisibleForTesting
     public List<PhoneAccountHandle> constructPossiblePhoneAccounts(Uri handle, UserHandle user,
-            boolean isVideo, boolean isEmergency) {
-        return constructPossiblePhoneAccounts(handle, user, isVideo, isEmergency, false);
+            boolean isVideo, boolean isEmergency,  boolean isConference) {
+        if (mTelephonyFeatureFlags.simultaneousCallingIndications()) {
+            return constructPossiblePhoneAccountsNew(handle, user, isVideo, isEmergency,
+                    isConference);
+        } else {
+            return constructPossiblePhoneAccountsOld(handle, user, isVideo, isEmergency,
+                    isConference);
+        }
     }
 
     // Returns whether the device is capable of 2 simultaneous active voice calls on different subs.
-    private boolean isDsdaCallingPossible() {
+    @VisibleForTesting
+    public boolean isDsdaCallingPossible() {
         try {
             return getTelephonyManager().getMaxNumberOfSimultaneouslyActiveSims() > 1
                     || getTelephonyManager().getPhoneCapability()
                            .getMaxActiveVoiceSubscriptions() > 1;
+        } catch(UnsupportedOperationException uoe) {
+            Log.w(this, "Telephony not supported");
+            return false;
         } catch (Exception e) {
             Log.w(this, "exception in isDsdaCallingPossible(): ", e);
             return false;
         }
     }
 
-    public List<PhoneAccountHandle> constructPossiblePhoneAccounts(Uri handle, UserHandle user,
+    private List<PhoneAccountHandle> constructPossiblePhoneAccountsOld(Uri handle, UserHandle user,
             boolean isVideo, boolean isEmergency, boolean isConference) {
 
         if (handle == null) {
@@ -3352,6 +3559,82 @@
         return allAccounts;
     }
 
+    /**
+     * Filters the list of all PhoneAccounts that match the outgoing call Handle's schema against
+     * the outgoing call request criteria and the state of the already ongoing calls on the
+     * device and their potential simultaneous calling restrictions.
+     * @return The filtered List
+     */
+    private List<PhoneAccountHandle> constructPossiblePhoneAccountsNew(Uri handle, UserHandle user,
+            boolean isVideo, boolean isEmergency, boolean isConference) {
+        if (handle == null) {
+            return Collections.emptyList();
+        }
+        // If we're specifically looking for video capable accounts, then include that capability,
+        // otherwise specify no additional capability constraints. When handling the emergency call,
+        // it also needs to find the phone accounts excluded by CAPABILITY_EMERGENCY_CALLS_ONLY.
+        int capabilities = isVideo ? PhoneAccount.CAPABILITY_VIDEO_CALLING : 0;
+        capabilities |= isConference ? PhoneAccount.CAPABILITY_ADHOC_CONFERENCE_CALLING : 0;
+        List<PhoneAccountHandle> allAccounts =
+                mPhoneAccountRegistrar.getCallCapablePhoneAccounts(handle.getScheme(), false, user,
+                        capabilities,
+                        isEmergency ? 0 : PhoneAccount.CAPABILITY_EMERGENCY_CALLS_ONLY,
+                        isEmergency);
+        Log.v(this, "constructPossiblePhoneAccountsNew: allAccounts=" + allAccounts);
+        Set<PhoneAccountHandle> activeCallAccounts = mCalls.stream()
+                .filter(c -> !c.isDisconnected() && !c.isNew()).map(Call::getTargetPhoneAccount)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+        Log.v(this, "constructPossiblePhoneAccountsNew: activeCallAccounts="
+                + activeCallAccounts);
+        // No Active calls - all accounts are valid
+        if (activeCallAccounts.isEmpty()) return allAccounts;
+        // The emergency call should be attempted only over the same SIM PhoneAccounts where there
+        // are already ongoing calls - filter out inactive SIM PhoneAccounts in this case.
+        if (isEmergency) {
+            Set<PhoneAccountHandle> simAccounts =
+                    new HashSet<>(mPhoneAccountRegistrar.getSimPhoneAccountsOfCurrentUser());
+            if (activeCallAccounts.stream().anyMatch(simAccounts::contains)) {
+                allAccounts.removeIf(h -> {
+                    boolean isRemoved = simAccounts.contains(h) && !activeCallAccounts.contains(h);
+                    if (isRemoved) {
+                        Log.i(this, "constructPossiblePhoneAccountsNew: removing candidate PAH ["
+                                + h + "] because another SIM account is active with an emergency "
+                                + "call");
+                    }
+                    return isRemoved;
+                });
+            }
+        }
+        // Apply restrictions to which PhoneAccounts can be used to place a call by looking at
+        // active calls and removing candidate PhoneAccounts if they are from the same source
+        // as the active call and the candidate PhoneAccount is not part of the restriction.
+        for (PhoneAccountHandle callHandle : activeCallAccounts) {
+            allAccounts.removeIf(candidateHandle -> {
+                PhoneAccount callAcct = mPhoneAccountRegistrar.getPhoneAccount(callHandle,
+                        user);
+                if (callAcct == null) {
+                    Log.w(this, "constructPossiblePhoneAccountsNew: unexpected"
+                            + "null PA for PAH, removing : " + candidateHandle);
+                    return true;
+                }
+                boolean isRemoved = !Objects.equals(candidateHandle, callHandle)
+                        && Objects.equals(candidateHandle.getComponentName(),
+                                callHandle.getComponentName())
+                        && callAcct.hasSimultaneousCallingRestriction()
+                        && !callAcct.getSimultaneousCallingRestriction().contains(candidateHandle);
+                if (isRemoved) {
+                    Log.i(this, "constructPossiblePhoneAccountsNew: removing candidate"
+                            + " PAH [" + candidateHandle + "] because it is not part of the"
+                            + " restriction set by [" + callHandle + "], restriction="
+                            + callAcct.getSimultaneousCallingRestriction());
+                }
+                return isRemoved;
+            });
+        }
+        return allAccounts;
+    }
+
     private TelephonyManager getTelephonyManager() {
         return mContext.getSystemService(TelephonyManager.class);
     }
@@ -3402,7 +3685,7 @@
     }
 
     /** Called by the in-call UI to change the mute state. */
-    void mute(boolean shouldMute) {
+    public void mute(boolean shouldMute) {
         if (isInEmergencyCall() && shouldMute) {
             Log.i(this, "Refusing to turn on mute because we're in an emergency call");
             shouldMute = false;
@@ -3454,6 +3737,7 @@
         int subscriptionId = mPhoneAccountRegistrar.getSubscriptionIdForPhoneAccount(handle);
         CarrierConfigManager carrierConfigManager =
                 mContext.getSystemService(CarrierConfigManager.class);
+        if (carrierConfigManager == null) return new PersistableBundle();
         PersistableBundle result = carrierConfigManager.getConfigForSubId(subscriptionId);
         return result == null ? new PersistableBundle() : result;
     }
@@ -3512,14 +3796,15 @@
      * Called when disconnect tone is started or stopped, including any InCallTone
      * after disconnected call.
      *
+     * @param call
      * @param isTonePlaying true if the disconnected tone is started, otherwise the disconnected
-     * tone is stopped.
+     *                      tone is stopped.
      */
     @VisibleForTesting
-    public void onDisconnectedTonePlaying(boolean isTonePlaying) {
+    public void onDisconnectedTonePlaying(Call call, boolean isTonePlaying) {
         Log.v(this, "onDisconnectedTonePlaying, %s", isTonePlaying ? "started" : "stopped");
         for (CallsManagerListener listener : mListeners) {
-            listener.onDisconnectedTonePlaying(isTonePlaying);
+            listener.onDisconnectedTonePlaying(call, isTonePlaying);
         }
     }
 
@@ -3551,8 +3836,7 @@
             if (canHold(activeCall)) {
                 activeCall.hold("swap to " + call.getId());
                 return true;
-            } else if (supportsHold(activeCall)
-                    && areFromSameSource(activeCall, call)) {
+            } else if (sameSourceHoldCase(activeCall, call)) {
 
                 // Handle the case where the active call and the new call are from the same CS or
                 // connection manager, and the currently active call supports hold but cannot
@@ -3601,43 +3885,85 @@
         return false;
     }
 
-    // attempt to hold the requested call and complete the callback on the result
+    /**
+     * attempt to hold or swap the current active call in favor of a new call request. The
+     * OutcomeReceiver will return onResult if the current active call is held or disconnected.
+     * Otherwise, the OutcomeReceiver will fail.
+     */
     public void transactionHoldPotentialActiveCallForNewCall(Call newCall,
-            OutcomeReceiver<Boolean, CallException> callback) {
+            boolean isCallControlRequest, OutcomeReceiver<Boolean, CallException> callback) {
+        String mTag = "transactionHoldPotentialActiveCallForNewCall: ";
         Call activeCall = (Call) mConnectionSvrFocusMgr.getCurrentFocusCall();
-        Log.i(this, "transactionHoldPotentialActiveCallForNewCall: "
-                + "newCall=[%s], activeCall=[%s]", newCall, activeCall);
+        Log.i(this, mTag + "newCall=[%s], activeCall=[%s]", newCall, activeCall);
 
-        // early exit if there is no need to hold an active call
         if (activeCall == null || activeCall == newCall) {
-            Log.i(this, "transactionHoldPotentialActiveCallForNewCall:"
-                    + " no need to hold activeCall");
+            Log.i(this, mTag + "no need to hold activeCall");
             callback.onResult(true);
             return;
         }
 
-        // before attempting CallsManager#holdActiveCallForNewCall(Call), check if it'll fail early
-        if (!canHold(activeCall) &&
-                !(supportsHold(activeCall) && areFromSameSource(activeCall, newCall))) {
-            Log.i(this, "transactionHoldPotentialActiveCallForNewCall: "
-                    + "conditions show the call cannot be held.");
-            callback.onError(new CallException("call does not support hold",
-                    CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
-            return;
-        }
+        if (mFeatureFlags.transactionalHoldDisconnectsUnholdable()) {
+            // prevent bad actors from disconnecting the activeCall. Instead, clients will need to
+            // notify the user that they need to disconnect the ongoing call before making the
+            // new call ACTIVE.
+            if (isCallControlRequest && !canHoldOrSwapActiveCall(activeCall, newCall)) {
+                Log.i(this, mTag + "CallControlRequest exit");
+                callback.onError(new CallException("activeCall is NOT holdable or swappable, please"
+                        + " request the user disconnect the call.",
+                        CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
+                return;
+            }
 
-        // attempt to hold the active call
-        if (!holdActiveCallForNewCall(newCall)) {
-            Log.i(this, "transactionHoldPotentialActiveCallForNewCall: "
-                    + "attempted to hold call but failed.");
-            callback.onError(new CallException("cannot hold active call failed",
-                    CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
-            return;
-        }
+            if (holdActiveCallForNewCall(newCall)) {
+                // Transactional clients do not call setHold but the request was sent to set the
+                // call as inactive and it has already been acked by this point.
+                markCallAsOnHold(activeCall);
+                callback.onResult(true);
+            } else {
+                // It's possible that holdActiveCallForNewCall disconnected the activeCall.
+                // Therefore, the activeCalls state should be checked before failing.
+                if (activeCall.isLocallyDisconnecting()) {
+                    callback.onResult(true);
+                } else {
+                    Log.i(this, mTag + "active call could not be held or disconnected");
+                    callback.onError(
+                            new CallException("activeCall could not be held or disconnected",
+                                    CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
+                }
+            }
+        } else {
+            // before attempting CallsManager#holdActiveCallForNewCall(Call), check if it'll fail
+            // early
+            if (!canHold(activeCall) &&
+                    !(supportsHold(activeCall) && areFromSameSource(activeCall, newCall))) {
+                Log.i(this, "transactionHoldPotentialActiveCallForNewCall: "
+                        + "conditions show the call cannot be held.");
+                callback.onError(new CallException("call does not support hold",
+                        CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
+                return;
+            }
 
-        // officially mark the activeCall as held
-        markCallAsOnHold(activeCall);
-        callback.onResult(true);
+            // attempt to hold the active call
+            if (!holdActiveCallForNewCall(newCall)) {
+                Log.i(this, "transactionHoldPotentialActiveCallForNewCall: "
+                        + "attempted to hold call but failed.");
+                callback.onError(new CallException("cannot hold active call failed",
+                        CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
+                return;
+            }
+
+            // officially mark the activeCall as held
+            markCallAsOnHold(activeCall);
+            callback.onResult(true);
+        }
+    }
+
+    private boolean canHoldOrSwapActiveCall(Call activeCall, Call newCall) {
+        return canHold(activeCall) || sameSourceHoldCase(activeCall, newCall);
+    }
+
+    private boolean sameSourceHoldCase(Call activeCall, Call call) {
+        return supportsHold(activeCall) && areFromSameSource(activeCall, call);
     }
 
     @VisibleForTesting
@@ -3714,11 +4040,6 @@
         // Notify listeners that the call was disconnected before being added to CallsManager.
         // Listeners will not receive onAdded or onRemoved callbacks.
         if (!mCalls.contains(call)) {
-            if (call.isEmergencyCall()) {
-                mAnomalyReporter.reportAnomaly(
-                        EMERGENCY_CALL_DISCONNECTED_BEFORE_BEING_ADDED_ERROR_UUID,
-                        EMERGENCY_CALL_DISCONNECTED_BEFORE_BEING_ADDED_ERROR_MSG);
-            }
             mListeners.forEach(l -> l.onCreateConnectionFailed(call));
         }
 
@@ -3736,20 +4057,21 @@
             Log.addEvent(call, LogUtils.Events.SET_DISCONNECTED_ORIG, disconnectCause);
 
             // Setup the future with a timeout so that the CDS is time boxed.
-            CompletableFuture<Boolean> future = call.initializeDisconnectFuture(
+            CompletableFuture<Boolean> future = call.initializeDiagnosticCompleteFuture(
                     mTimeoutsAdapter.getCallDiagnosticServiceTimeoutMillis(
                             mContext.getContentResolver()));
 
             // Post the disconnection updates to the future for completion once the CDS returns
             // with it's overridden disconnect message.
-            future.thenRunAsync(() -> {
+            CompletableFuture<Void> disconnectFuture = future.thenRunAsync(() -> {
                 call.setDisconnectCause(disconnectCause);
                 setCallState(call, CallState.DISCONNECTED, "disconnected set explicitly");
-            }, new LoggedHandlerExecutor(mHandler, "CM.mCAD", mLock))
-                    .exceptionally((throwable) -> {
-                        Log.e(TAG, throwable, "Error while executing disconnect future.");
-                        return null;
-                    });
+            }, new LoggedHandlerExecutor(mHandler, "CM.mCAD", mLock));
+            disconnectFuture.exceptionally((throwable) -> {
+                Log.e(TAG, throwable, "Error while executing disconnect future.");
+                return null;
+            });
+            call.setDisconnectFuture(disconnectFuture);
         } else {
             // No CallDiagnosticService, or it doesn't handle this call, so just do this
             // synchronously as always.
@@ -3769,16 +4091,7 @@
     public void markCallAsRemoved(Call call) {
         if (call.isDisconnectHandledViaFuture()) {
             Log.i(this, "markCallAsRemoved; callid=%s, postingToFuture.", call.getId());
-            // A future is being used due to a CallDiagnosticService handling the call.  We will
-            // chain the removal operation to the end of any outstanding disconnect work.
-            call.getDisconnectFuture().thenRunAsync(() -> {
-                performRemoval(call);
-            }, new LoggedHandlerExecutor(mHandler, "CM.mCAR", mLock))
-                    .exceptionally((throwable) -> {
-                        Log.e(TAG, throwable, "Error while executing disconnect future");
-                        return null;
-                    });
-
+            configureRemovalFuture(call);
         } else {
             Log.i(this, "markCallAsRemoved; callid=%s, immediate.", call.getId());
             performRemoval(call);
@@ -3786,8 +4099,52 @@
     }
 
     /**
+     * Configure the removal as a dependent stage after the disconnect future completes, which could
+     * be cancelled as part of {@link Call#setState(int, String)} when need to retry dial on another
+     * ConnectionService.
+     * <p>
+     * We can not remove the call yet, we need to wait for the DisconnectCause to be processed and
+     * potentially re-written via the {@link android.telecom.CallDiagnosticService} first.
+     *
+     * @param call The call to configure the removal future for.
+     */
+    private void configureRemovalFuture(Call call) {
+        if (!mFeatureFlags.cancelRemovalOnEmergencyRedial()) {
+            call.getDiagnosticCompleteFuture().thenRunAsync(() -> performRemoval(call),
+                            new LoggedHandlerExecutor(mHandler, "CM.cRF-O", mLock))
+                    .exceptionally((throwable) -> {
+                        Log.e(TAG, throwable, "Error while executing disconnect future");
+                        return null;
+                    });
+        } else {
+            // A future is being used due to a CallDiagnosticService handling the call.  We will
+            // chain the removal operation to the end of any outstanding disconnect work.
+            CompletableFuture<Void> removalFuture;
+            if (call.getDisconnectFuture() == null) {
+                // Unexpected - can not get the disconnect future, attach to the diagnostic complete
+                // future in this case.
+                removalFuture = call.getDiagnosticCompleteFuture().thenRun(() ->
+                        Log.w(this, "configureRemovalFuture: remove called without disconnecting"
+                                + " first."));
+            } else {
+                removalFuture = call.getDisconnectFuture();
+            }
+            removalFuture = removalFuture.thenRunAsync(() -> performRemoval(call),
+                    new LoggedHandlerExecutor(mHandler, "CM.cRF-N", mLock));
+            removalFuture.exceptionally((throwable) -> {
+                Log.e(TAG, throwable, "Error while executing disconnect future");
+                return null;
+            });
+            // Cache the future to remove the call initiated by the ConnectionService in case we
+            // need to cancel it in favor of removing the call internally as part of creating a
+            // new connection (CreateConnectionProcessor#continueProcessingIfPossible)
+            call.setRemovalFuture(removalFuture);
+        }
+    }
+
+    /**
      * Work which is completed when a call is to be removed. Can either be be run synchronously or
-     * posted to a {@link Call#getDisconnectFuture()}.
+     * posted to a {@link Call#getDiagnosticCompleteFuture()}.
      * @param call The call.
      */
     private void performRemoval(Call call) {
@@ -4153,7 +4510,8 @@
                 connectTime,
                 connectElapsedTime,
                 mClockProxy,
-                mToastFactory);
+                mToastFactory,
+                mFeatureFlags);
 
         // Unlike connections, conferences are not created first and then notified as create
         // connection complete from the CS.  They originate from the CS and are reported directly to
@@ -4171,7 +4529,10 @@
         call.setStatusHints(parcelableConference.getStatusHints());
         call.putConnectionServiceExtras(parcelableConference.getExtras());
         // For conference calls, set the associated user from the target phone account user handle.
-        call.setAssociatedUser(phoneAccount.getUserHandle());
+        UserHandle associatedUser = UserUtil.getAssociatedUserForCall(
+                mFeatureFlags.associatedUserRefactorForWorkProfile(), getPhoneAccountRegistrar(),
+                getCurrentUserHandle(), phoneAccount);
+        call.setAssociatedUser(associatedUser);
         // In case this Conference was added via a ConnectionManager, keep track of the original
         // Connection ID as created by the originating ConnectionService.
         Bundle extras = parcelableConference.getExtras();
@@ -4457,10 +4818,6 @@
             if (handoverState == HandoverState.HANDOVER_FROM_STARTED) {
                 // Disconnect before handover was accepted.
                 Log.i(this, "setCallState: disconnect before handover accepted");
-                // Let the handover destination know that the source has disconnected prior to
-                // completion of the handover.
-                call.getHandoverDestinationCall().sendCallEvent(
-                        android.telecom.Call.EVENT_HANDOVER_SOURCE_DISCONNECTED, null);
             } else if (handoverState == HandoverState.HANDOVER_ACCEPTED) {
                 Log.i(this, "setCallState: handover from complete");
                 completeHandoverFrom(call);
@@ -4478,11 +4835,9 @@
         // Inform the "from" Call (ie the source call) that the handover from it has
         // completed; this allows the InCallService to be notified that a handover it
         // initiated completed.
-        call.onConnectionEvent(Connection.EVENT_HANDOVER_COMPLETE, null);
         call.onHandoverComplete();
 
         // Inform the "to" ConnectionService that handover to it has completed.
-        handoverTo.sendCallEvent(android.telecom.Call.EVENT_HANDOVER_COMPLETE, null);
         handoverTo.onHandoverComplete();
         answerCall(handoverTo, handoverTo.getVideoState());
         call.markFinishedHandoverStateAndCleanup(HandoverState.HANDOVER_COMPLETE);
@@ -4505,7 +4860,6 @@
         // Inform the "from" Call (ie the source call) that the handover from it has
         // failed; this allows the InCallService to be notified that a handover it
         // initiated failed.
-        handoverFrom.onConnectionEvent(Connection.EVENT_HANDOVER_FAILED, null);
         handoverFrom.onHandoverFailed(android.telecom.Call.Callback.HANDOVER_FAILURE_USER_REJECTED);
 
         // Inform the "to" ConnectionService that handover to it has failed.  This
@@ -4514,7 +4868,6 @@
             // Only attempt if the call has a bound ConnectionService if handover failed
             // early on in the handover process, the CS will be unbound and we won't be
             // able to send the call event.
-            handoverTo.sendCallEvent(android.telecom.Call.EVENT_HANDOVER_FAILED, null);
             handoverTo.getConnectionService().handoverFailed(handoverTo,
                     android.telecom.Call.Callback.HANDOVER_FAILURE_USER_REJECTED);
         }
@@ -4560,18 +4913,23 @@
     }
 
     /**
-     * Determines if there are any ongoing self managed calls for the given package/user.
+     * Determines if there are any ongoing self-managed calls for the given package/user.
      * @param packageName The package name to check.
-     * @param userHandle The userhandle to check.
+     * @param userHandle The {@link UserHandle} to check.
      * @return {@code true} if the app has ongoing calls, or {@code false} otherwise.
      */
     public boolean isInSelfManagedCall(String packageName, UserHandle userHandle) {
+        boolean hasCrossUserAccess = userHandle.equals(UserHandle.ALL);
         return mSelfManagedCallsBeingSetup.stream().anyMatch(c -> c.isSelfManaged()
                 && c.getTargetPhoneAccount().getComponentName().getPackageName().equals(packageName)
-                && c.getTargetPhoneAccount().getUserHandle().equals(userHandle)) ||
-                mCalls.stream().anyMatch(c -> c.isSelfManaged()
+                && (!hasCrossUserAccess
+                        ? c.getTargetPhoneAccount().getUserHandle().equals(userHandle)
+                        : true))
+                || mCalls.stream().anyMatch(c -> c.isSelfManaged()
                 && c.getTargetPhoneAccount().getComponentName().getPackageName().equals(packageName)
-                && c.getTargetPhoneAccount().getUserHandle().equals(userHandle));
+                && (!hasCrossUserAccess
+                        ? c.getTargetPhoneAccount().getUserHandle().equals(userHandle)
+                        : true));
     }
 
     @VisibleForTesting
@@ -4737,15 +5095,22 @@
 
     /**
      * Determines the number of unholdable calls present in a connection service other than the one
-     * the passed phone account belonds to.
+     * the passed phone account belongs to. If a ConnectionService has not been associated with an
+     * outgoing call yet (for example, it is in the SELECT_PHONE_ACCOUNT state), then we do not
+     * count that call because it is not tracked as an active call yet.
      * @param phoneAccountHandle The handle of the PhoneAccount.
      * @return Number of unholdable calls owned by other connection service.
      */
     public int getNumUnholdableCallsForOtherConnectionService(
             PhoneAccountHandle phoneAccountHandle) {
         return (int) mCalls.stream().filter(call ->
-                !phoneAccountHandle.getComponentName().equals(
-                        call.getTargetPhoneAccount().getComponentName())
+                // If this convention needs to be changed, answerCall will need to be modified to
+                // change what an "active call" is so that the call in SELECT_PHONE_ACCOUNT state
+                // will be properly cancelled.
+                call.getTargetPhoneAccount() != null
+                        && phoneAccountHandle != null
+                        && !phoneAccountHandle.getComponentName().equals(
+                                call.getTargetPhoneAccount().getComponentName())
                         && call.getParentCall() == null
                         && !call.isExternalCall()
                         && !canHold(call)).count();
@@ -5203,7 +5568,8 @@
                 connection.getConnectTimeMillis() /* connectTimeMillis */,
                 connection.getConnectElapsedTimeMillis(), /* connectElapsedTimeMillis */
                 mClockProxy,
-                mToastFactory);
+                mToastFactory,
+                mFeatureFlags);
 
         call.initAnalytics();
         call.getAnalytics().setCreatedFromExistingConnection(true);
@@ -5218,7 +5584,10 @@
                 connection.getCallerDisplayNamePresentation());
         // For existing connections, use the phone account user handle to determine the user
         // association with the call.
-        call.setAssociatedUser(connection.getPhoneAccount().getUserHandle());
+        UserHandle associatedUser = UserUtil.getAssociatedUserForCall(
+                mFeatureFlags.associatedUserRefactorForWorkProfile(), getPhoneAccountRegistrar(),
+                getCurrentUserHandle(), connection.getPhoneAccount());
+        call.setAssociatedUser(associatedUser);
         call.addListener(this);
         call.putConnectionServiceExtras(connection.getExtras());
 
@@ -5301,10 +5670,21 @@
         mCurrentUserHandle = userHandle;
         mMissedCallNotifier.setCurrentUserHandle(userHandle);
         mRoleManagerAdapter.setCurrentUserHandle(userHandle);
-        final UserManager userManager = UserManager.get(mContext);
-        List<UserInfo> profiles = userManager.getEnabledProfiles(userHandle.getIdentifier());
-        for (UserInfo profile : profiles) {
-            reloadMissedCallsOfUser(profile.getUserHandle());
+        final UserManager userManager = mFeatureFlags.telecomResolveHiddenDependencies()
+                ? mContext.createContextAsUser(userHandle, 0).getSystemService(
+                        UserManager.class)
+                : mContext.getSystemService(UserManager.class);
+        List<UserHandle> profiles = userManager.getUserProfiles();
+        List<UserInfo> userInfoProfiles = userManager.getEnabledProfiles(
+                userHandle.getIdentifier());
+        if (mFeatureFlags.telecomResolveHiddenDependencies()) {
+            for (UserHandle profileUser : profiles) {
+                reloadMissedCallsOfUser(profileUser);
+            }
+        } else {
+            for (UserInfo profile : userInfoProfiles) {
+                reloadMissedCallsOfUser(profile.getUserHandle());
+            }
         }
     }
 
@@ -5313,7 +5693,7 @@
      * switched, we reload missed calls of profile that are just started here.
      */
     void onUserStarting(UserHandle userHandle) {
-        if (UserUtil.isProfile(mContext, userHandle)) {
+        if (UserUtil.isProfile(mContext, userHandle, mFeatureFlags)) {
             reloadMissedCallsOfUser(userHandle);
         }
     }
@@ -5422,8 +5802,10 @@
         UserManager userManager = mContext.getSystemService(UserManager.class);
         KeyguardManager keyguardManager = mContext.getSystemService(KeyguardManager.class);
 
-        boolean isUserRestricted = userManager != null
-                && userManager.hasUserRestriction(UserManager.DISALLOW_SMS, callingUser);
+        boolean hasUserRestriction = mFeatureFlags.telecomResolveHiddenDependencies()
+                ? userManager.hasUserRestrictionForUser(UserManager.DISALLOW_SMS, callingUser)
+                : userManager.hasUserRestriction(UserManager.DISALLOW_SMS, callingUser);
+        boolean isUserRestricted = userManager != null && hasUserRestriction;
         boolean isLockscreenRestricted = keyguardManager != null
                 && keyguardManager.isDeviceLocked();
         Log.d(this, "isReplyWithSmsAllowed: isUserRestricted: %s, isLockscreenRestricted: %s",
@@ -5443,7 +5825,7 @@
         mCallAudioManager.getCallAudioModeStateMachine().getHandler().post(() -> {
             mainHandlerLatch.countDown();
         });
-        mCallAudioManager.getCallAudioRouteStateMachine().getHandler().post(() -> {
+        mCallAudioManager.getCallAudioRouteAdapter().getAdapterHandler().post(() -> {
             mainHandlerLatch.countDown();
         });
 
@@ -5469,9 +5851,10 @@
             // We are going to place the new outgoing call, so disconnect any ongoing self-managed
             // calls which are ongoing at this time.
             disconnectSelfManagedCalls("outgoing call " + callId);
-
-            mPendingCallConfirm.complete(mPendingCall);
-            mPendingCallConfirm = null;
+            if (mPendingCallConfirm != null) {
+                mPendingCallConfirm.complete(mPendingCall);
+                mPendingCallConfirm = null;
+            }
             mPendingCall = null;
         }
     }
@@ -5490,8 +5873,10 @@
             markCallAsDisconnected(mPendingCall, new DisconnectCause(DisconnectCause.CANCELED));
             markCallAsRemoved(mPendingCall);
             mPendingCall = null;
-            mPendingCallConfirm.complete(null);
-            mPendingCallConfirm = null;
+            if (mPendingCallConfirm != null) {
+                mPendingCallConfirm.complete(null);
+                mPendingCallConfirm = null;
+            }
         }
     }
 
@@ -5793,28 +6178,6 @@
     }
 
     /**
-     * Called in response to a {@link Call} receiving a {@link Call#sendCallEvent(String, Bundle)}
-     * of type {@link android.telecom.Call#EVENT_REQUEST_HANDOVER} indicating the
-     * {@link android.telecom.InCallService} has requested a handover to another
-     * {@link android.telecom.ConnectionService}.
-     *
-     * We will explicitly disallow a handover when there is an emergency call present.
-     *
-     * @param handoverFromCall The {@link Call} to be handed over.
-     * @param handoverToHandle The {@link PhoneAccountHandle} to hand over the call to.
-     * @param videoState The desired video state of {@link Call} after handover.
-     * @param initiatingExtras Extras associated with the handover, to be passed to the handover
-     *               {@link android.telecom.ConnectionService}.
-     */
-    private void requestHandoverViaEvents(Call handoverFromCall,
-                                          PhoneAccountHandle handoverToHandle,
-                                          int videoState, Bundle initiatingExtras) {
-
-        handoverFromCall.sendCallEvent(android.telecom.Call.EVENT_HANDOVER_FAILED, null);
-        Log.addEvent(handoverFromCall, LogUtils.Events.HANDOVER_REQUEST, "legacy request denied");
-    }
-
-    /**
      * Called in response to a {@link Call} receiving a {@link Call#handoverTo(PhoneAccountHandle,
      * int, Bundle)} indicating the {@link android.telecom.InCallService} has requested a
      * handover to another {@link android.telecom.ConnectionService}.
@@ -5860,7 +6223,7 @@
                 handoverFromCall.getHandle(), null,
                 null, null,
                 Call.CALL_DIRECTION_OUTGOING, false,
-                false, mClockProxy, mToastFactory);
+                false, mClockProxy, mToastFactory, mFeatureFlags);
         call.initAnalytics();
 
         // Set self-managed and voipAudioMode if destination is self-managed CS
@@ -6067,7 +6430,8 @@
                 false /* forceAttachToExistingConnection */,
                 false, /* isConference */
                 mClockProxy,
-                mToastFactory);
+                mToastFactory,
+                mFeatureFlags);
 
         if (fromCall == null || isHandoverInProgress() ||
                 !isHandoverFromPhoneAccountSupported(fromCall.getTargetPhoneAccount()) ||
diff --git a/src/com/android/server/telecom/CallsManagerListenerBase.java b/src/com/android/server/telecom/CallsManagerListenerBase.java
index 43f3b90..0c54be3 100644
--- a/src/com/android/server/telecom/CallsManagerListenerBase.java
+++ b/src/com/android/server/telecom/CallsManagerListenerBase.java
@@ -16,10 +16,10 @@
 
 package com.android.server.telecom;
 
-import android.telecom.AudioState;
 import android.telecom.CallAudioState;
 import android.telecom.CallEndpoint;
 import android.telecom.VideoProfile;
+
 import java.util.Set;
 
 /**
@@ -112,7 +112,7 @@
     }
 
     @Override
-    public void onDisconnectedTonePlaying(boolean isTonePlaying) {
+    public void onDisconnectedTonePlaying(Call call, boolean isTonePlaying) {
     }
 
     @Override
diff --git a/src/com/android/server/telecom/ConnectionServiceFocusManager.java b/src/com/android/server/telecom/ConnectionServiceFocusManager.java
index 3694727..35be0f8 100644
--- a/src/com/android/server/telecom/ConnectionServiceFocusManager.java
+++ b/src/com/android/server/telecom/ConnectionServiceFocusManager.java
@@ -26,8 +26,11 @@
 import android.telecom.Logging.Session;
 import android.text.TextUtils;
 import android.util.LocalLog;
+import android.util.LogPrinter;
+import android.util.Printer;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.telecom.flags.Flags;
 import com.android.internal.util.IndentingPrintWriter;
 
 import java.util.ArrayList;
@@ -35,6 +38,7 @@
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
+import java.util.UUID;
 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.TimeUnit;
@@ -44,6 +48,11 @@
     private static final String TAG = "ConnectionSvrFocusMgr";
     private static final int GET_CURRENT_FOCUS_TIMEOUT_MILLIS = 1000;
     private final LocalLog mLocalLog = new LocalLog(20);
+    private final AnomalyReporterAdapter mAnomalyReporter = new AnomalyReporterAdapterImpl();
+    public static final UUID WATCHDOG_GET_CALL_FOCUS_TIMEOUT_UUID =
+            UUID.fromString("edd7334a-ef87-432b-a1d0-a2f23959c73e");
+    public static final String WATCHDOG_GET_CALL_FOCUS_TIMEOUT_MSG =
+            "Telecom CallAnomalyWatchdog detected a timeout while getting the call focus.";
 
     /** Factory interface used to create the {@link ConnectionServiceFocusManager} instance. */
     public interface ConnectionServiceFocusManagerFactory {
@@ -332,8 +341,23 @@
             if (syncCallFocus != null) {
                 return syncCallFocus.orElse(null);
             } else {
-                Log.w(TAG, "Timed out waiting for synchronous current focus. Returning possibly"
-                        + " inaccurate result");
+                if (Flags.genAnomReportOnFocusTimeout()) {
+                    Log.w(TAG, "Timed out waiting for synchronous current focus. Returning possibly"
+                                    + " inaccurate result. returning currentFocusCall=[%s]",
+                            mCurrentFocusCall);
+
+                    // dump the state of the handler to better understand the timeout
+                    mEventHandler.dump(
+                            new LogPrinter(android.util.Log.INFO, TAG), "CsFocusMgr_timeout");
+
+                    // report the timeout
+                    mAnomalyReporter.reportAnomaly(
+                            WATCHDOG_GET_CALL_FOCUS_TIMEOUT_UUID,
+                            WATCHDOG_GET_CALL_FOCUS_TIMEOUT_MSG);
+                } else {
+                    Log.w(TAG, "Timed out waiting for synchronous current focus. Returning possibly"
+                            + " inaccurate result");
+                }
                 return mCurrentFocusCall;
             }
         } catch (InterruptedException e) {
diff --git a/src/com/android/server/telecom/ConnectionServiceRepository.java b/src/com/android/server/telecom/ConnectionServiceRepository.java
index 3991ed5..e4ed220 100644
--- a/src/com/android/server/telecom/ConnectionServiceRepository.java
+++ b/src/com/android/server/telecom/ConnectionServiceRepository.java
@@ -23,6 +23,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.telecom.flags.FeatureFlags;
 
 import java.util.HashMap;
 
@@ -37,6 +38,7 @@
     private final Context mContext;
     private final TelecomSystem.SyncRoot mLock;
     private final CallsManager mCallsManager;
+    private final FeatureFlags mFeatureFlags;
 
     private final ServiceBinder.Listener<ConnectionServiceWrapper> mUnbindListener =
             new ServiceBinder.Listener<ConnectionServiceWrapper>() {
@@ -53,15 +55,19 @@
             PhoneAccountRegistrar phoneAccountRegistrar,
             Context context,
             TelecomSystem.SyncRoot lock,
-            CallsManager callsManager) {
+            CallsManager callsManager,
+            FeatureFlags featureFlags) {
         mPhoneAccountRegistrar = phoneAccountRegistrar;
         mContext = context;
         mLock = lock;
         mCallsManager = callsManager;
+        mFeatureFlags = featureFlags;
     }
 
     @VisibleForTesting
-    public ConnectionServiceWrapper getService(ComponentName componentName, UserHandle userHandle) {
+    public ConnectionServiceWrapper getService(
+            ComponentName componentName,
+            UserHandle userHandle) {
         Pair<ComponentName, UserHandle> cacheKey = Pair.create(componentName, userHandle);
         ConnectionServiceWrapper service = mServiceCache.get(cacheKey);
         if (service == null) {
@@ -72,7 +78,8 @@
                     mCallsManager,
                     mContext,
                     mLock,
-                    userHandle);
+                    userHandle,
+                    mFeatureFlags);
             service.addListener(mUnbindListener);
             mServiceCache.put(cacheKey, service);
         }
diff --git a/src/com/android/server/telecom/ConnectionServiceWrapper.java b/src/com/android/server/telecom/ConnectionServiceWrapper.java
index 728e121..c3c0c1c 100644
--- a/src/com/android/server/telecom/ConnectionServiceWrapper.java
+++ b/src/com/android/server/telecom/ConnectionServiceWrapper.java
@@ -23,17 +23,16 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.PackageManager;
+import android.graphics.drawable.Icon;
 import android.location.Location;
 import android.location.LocationManager;
 import android.location.LocationRequest;
-import android.graphics.drawable.Icon;
 import android.net.Uri;
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.CancellationSignal;
 import android.os.IBinder;
 import android.os.ParcelFileDescriptor;
-import android.os.Process;
 import android.os.RemoteException;
 import android.os.ResultReceiver;
 import android.os.UserHandle;
@@ -45,6 +44,7 @@
 import android.telecom.DisconnectCause;
 import android.telecom.GatewayInfo;
 import android.telecom.Log;
+import android.telecom.Logging.Runnable;
 import android.telecom.Logging.Session;
 import android.telecom.ParcelableConference;
 import android.telecom.ParcelableConnection;
@@ -65,17 +65,22 @@
 import com.android.internal.telecom.IVideoProvider;
 import com.android.internal.telecom.RemoteServiceCallback;
 import com.android.internal.util.Preconditions;
+import com.android.server.telecom.flags.FeatureFlags;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.UUID;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
 import java.util.Objects;
 
@@ -87,13 +92,30 @@
  */
 @VisibleForTesting
 public class ConnectionServiceWrapper extends ServiceBinder implements
-        ConnectionServiceFocusManager.ConnectionServiceFocus {
+        ConnectionServiceFocusManager.ConnectionServiceFocus, CallSourceService {
+
+    /**
+     * Anomaly Report UUIDs and corresponding error descriptions specific to CallsManager.
+     */
+    public static final UUID CREATE_CONNECTION_TIMEOUT_ERROR_UUID =
+            UUID.fromString("54b7203d-a79f-4cbd-b639-85cd93a39cbb");
+    public static final String CREATE_CONNECTION_TIMEOUT_ERROR_MSG =
+            "Timeout expired before Telecom connection was created.";
+    public static final UUID CREATE_CONFERENCE_TIMEOUT_ERROR_UUID =
+            UUID.fromString("caafe5ea-2472-4c61-b2d8-acb9d47e13dd");
+    public static final String CREATE_CONFERENCE_TIMEOUT_ERROR_MSG =
+            "Timeout expired before Telecom conference was created.";
 
     private static final String TELECOM_ABBREVIATION = "cast";
+    private static final long SERVICE_BINDING_TIMEOUT = 15000L;
     private CompletableFuture<Pair<Integer, Location>> mQueryLocationFuture = null;
     private @Nullable CancellationSignal mOngoingQueryLocationRequest = null;
     private final ExecutorService mQueryLocationExecutor = Executors.newSingleThreadExecutor();
-
+    private ScheduledExecutorService mScheduledExecutor =
+            Executors.newSingleThreadScheduledExecutor();
+    // Pre-allocate space for 2 calls; realistically thats all we should ever need (tm)
+    private final Map<Call, ScheduledFuture<?>> mScheduledFutureMap = new ConcurrentHashMap<>(2);
+    private AnomalyReporterAdapter mAnomalyReporter = new AnomalyReporterAdapterImpl();
     private final class Adapter extends IConnectionServiceAdapter.Stub {
 
         @Override
@@ -106,6 +128,12 @@
             try {
                 synchronized (mLock) {
                     logIncoming("handleCreateConnectionComplete %s", callId);
+                    Call call = mCallIdMapper.getCall(callId);
+                    if (mScheduledFutureMap.containsKey(call)) {
+                        ScheduledFuture<?> existingTimeout = mScheduledFutureMap.get(call);
+                        existingTimeout.cancel(false /* cancelIfRunning */);
+                        mScheduledFutureMap.remove(call);
+                    }
                     // Check status hints image for cross user access
                     if (connection.getStatusHints() != null) {
                         Icon icon = connection.getStatusHints().getIcon();
@@ -139,10 +167,23 @@
                 ParcelableConference conference, Session.Info sessionInfo) {
             Log.startSession(sessionInfo, LogUtils.Sessions.CSW_HANDLE_CREATE_CONNECTION_COMPLETE,
                     mPackageAbbreviation);
+            UserHandle callingUserHandle = Binder.getCallingUserHandle();
             long token = Binder.clearCallingIdentity();
             try {
                 synchronized (mLock) {
                     logIncoming("handleCreateConferenceComplete %s", callId);
+                    // Check status hints image for cross user access
+                    if (conference.getStatusHints() != null) {
+                        Icon icon = conference.getStatusHints().getIcon();
+                        conference.getStatusHints().setIcon(StatusHints.
+                                validateAccountIconUserBoundary(icon, callingUserHandle));
+                    }
+                    Call call = mCallIdMapper.getCall(callId);
+                    if (mScheduledFutureMap.containsKey(call)) {
+                        ScheduledFuture<?> existingTimeout = mScheduledFutureMap.get(call);
+                        existingTimeout.cancel(false /* cancelIfRunning */);
+                        mScheduledFutureMap.remove(call);
+                    }
                     ConnectionServiceWrapper.this
                             .handleCreateConferenceComplete(callId, request, conference);
 
@@ -380,7 +421,12 @@
                     logIncoming("removeCall %s", callId);
                     Call call = mCallIdMapper.getCall(callId);
                     if (call != null) {
-                        if (call.isAlive() && !call.isDisconnectHandledViaFuture()) {
+                        boolean isRemovalPending = mFlags.cancelRemovalOnEmergencyRedial()
+                                && call.isRemovalPending();
+                        if (call.isAlive() && !call.isDisconnectHandledViaFuture()
+                                && !isRemovalPending) {
+                            Log.w(this, "call not disconnected when removeCall"
+                                    + " called, marking disconnected first.");
                             mCallsManager.markCallAsDisconnected(
                                     call, new DisconnectCause(DisconnectCause.REMOTE));
                         }
@@ -516,8 +562,8 @@
             // Check status hints image for cross user access
             if (parcelableConference.getStatusHints() != null) {
                 Icon icon = parcelableConference.getStatusHints().getIcon();
-                parcelableConference.getStatusHints().setIcon(StatusHints.
-                        validateAccountIconUserBoundary(icon, callingUserHandle));
+                parcelableConference.getStatusHints().setIcon(StatusHints
+                        .validateAccountIconUserBoundary(icon, callingUserHandle));
             }
 
             if (parcelableConference.getConnectElapsedTimeMillis() != 0
@@ -992,6 +1038,12 @@
                             connectIdToCheck = callId;
                         }
 
+                        // Check status hints image for cross user access
+                        if (connection.getStatusHints() != null) {
+                            Icon icon = connection.getStatusHints().getIcon();
+                            connection.getStatusHints().setIcon(StatusHints.
+                                    validateAccountIconUserBoundary(icon, userHandle));
+                        }
                         // Handle the case where an existing connection was added by Telephony via
                         // a connection manager.  The remote connection service API does not include
                         // the ability to specify a parent connection when adding an existing
@@ -1030,14 +1082,6 @@
                                     connection.getCallDirection(),
                                     connection.getCallerNumberVerificationStatus());
                         }
-
-                        // Check status hints image for cross user access
-                        if (connection.getStatusHints() != null) {
-                            Icon icon = connection.getStatusHints().getIcon();
-                            connection.getStatusHints().setIcon(StatusHints.
-                                    validateAccountIconUserBoundary(icon, userHandle));
-                        }
-
                         // Check to see if this Connection has already been added.
                         Call alreadyAddedConnection = mCallsManager
                                 .getAlreadyAddedConnection(connectIdToCheck);
@@ -1371,8 +1415,10 @@
             CallsManager callsManager,
             Context context,
             TelecomSystem.SyncRoot lock,
-            UserHandle userHandle) {
-        super(ConnectionService.SERVICE_INTERFACE, componentName, context, lock, userHandle);
+            UserHandle userHandle,
+            FeatureFlags featureFlags) {
+        super(ConnectionService.SERVICE_INTERFACE, componentName, context, lock, userHandle,
+                featureFlags);
         mConnectionServiceRepository = connectionServiceRepository;
         phoneAccountRegistrar.addListener(new PhoneAccountRegistrar.Listener() {
             // TODO -- Upon changes to PhoneAccountRegistrar, need to re-wire connections
@@ -1406,20 +1452,23 @@
         }
     }
 
-    private CellIdentity getLastKnownCellIdentity() {
+    @VisibleForTesting
+    public CellIdentity getLastKnownCellIdentity() {
         TelephonyManager telephonyManager = mContext.getSystemService(TelephonyManager.class);
         if (telephonyManager != null) {
-            CellIdentity lastKnownCellIdentity = telephonyManager.getLastKnownCellIdentity();
             try {
+                CellIdentity lastKnownCellIdentity = telephonyManager.getLastKnownCellIdentity();
                 mAppOpsManager.noteOp(AppOpsManager.OP_FINE_LOCATION,
                         mContext.getPackageManager().getPackageUid(
                                 getComponentName().getPackageName(), 0),
                         getComponentName().getPackageName());
+                return lastKnownCellIdentity;
+            } catch (UnsupportedOperationException ignored) {
+                Log.w(this, "getLastKnownCellIdentity - no telephony on this device");
             } catch (PackageManager.NameNotFoundException nameNotFoundException) {
                 Log.e(this, nameNotFoundException, "could not find the package -- %s",
                         getComponentName().getPackageName());
             }
-            return lastKnownCellIdentity;
         }
         return null;
     }
@@ -1595,7 +1644,29 @@
                         .setParticipants(call.getParticipants())
                         .setIsAdhocConferenceCall(call.isAdhocConferenceCall())
                         .build();
-
+                Runnable r = new Runnable("CSW.cC", mLock) {
+                            @Override
+                            public void loggedRun() {
+                                if (!call.isCreateConnectionComplete()) {
+                                    Log.e(this, new Exception(),
+                                            "Conference %s creation timeout",
+                                            getComponentName());
+                                    Log.addEvent(call, LogUtils.Events.CREATE_CONFERENCE_TIMEOUT,
+                                            Log.piiHandle(call.getHandle()) + " via:" +
+                                                    getComponentName().getPackageName());
+                                    mAnomalyReporter.reportAnomaly(
+                                            CREATE_CONFERENCE_TIMEOUT_ERROR_UUID,
+                                            CREATE_CONFERENCE_TIMEOUT_ERROR_MSG);
+                                    response.handleCreateConferenceFailure(
+                                            new DisconnectCause(DisconnectCause.ERROR));
+                                }
+                            }
+                        };
+                // Post cleanup to the executor service and cache the future, so we can cancel it if
+                // needed.
+                ScheduledFuture<?> future = mScheduledExecutor.schedule(r.getRunnableToCancel(),
+                        SERVICE_BINDING_TIMEOUT, TimeUnit.MILLISECONDS);
+                mScheduledFutureMap.put(call, future);
                 try {
                     mServiceInterface.createConference(
                             call.getConnectionManagerPhoneAccount(),
@@ -1604,7 +1675,6 @@
                             call.shouldAttachToExistingConnection(),
                             call.isUnknown(),
                             Log.getExternalSession(TELECOM_ABBREVIATION));
-
                 } catch (RemoteException e) {
                     Log.e(this, e, "Failure to createConference -- %s", getComponentName());
                     mPendingResponses.remove(callId).handleCreateConferenceFailure(
@@ -1697,7 +1767,29 @@
                         .setRttPipeFromInCall(call.getInCallToCsRttPipeForCs())
                         .setRttPipeToInCall(call.getCsToInCallRttPipeForCs())
                         .build();
-
+                Runnable r = new Runnable("CSW.cC", mLock) {
+                            @Override
+                            public void loggedRun() {
+                                if (!call.isCreateConnectionComplete()) {
+                                    Log.e(this, new Exception(),
+                                            "Connection %s creation timeout",
+                                            getComponentName());
+                                    Log.addEvent(call, LogUtils.Events.CREATE_CONNECTION_TIMEOUT,
+                                            Log.piiHandle(call.getHandle()) + " via:" +
+                                                    getComponentName().getPackageName());
+                                    mAnomalyReporter.reportAnomaly(
+                                            CREATE_CONNECTION_TIMEOUT_ERROR_UUID,
+                                            CREATE_CONNECTION_TIMEOUT_ERROR_MSG);
+                                    response.handleCreateConnectionFailure(
+                                            new DisconnectCause(DisconnectCause.ERROR));
+                                }
+                            }
+                        };
+                // Post cleanup to the executor service and cache the future, so we can cancel it if
+                // needed.
+                ScheduledFuture<?> future = mScheduledExecutor.schedule(r.getRunnableToCancel(),
+                        SERVICE_BINDING_TIMEOUT, TimeUnit.MILLISECONDS);
+                mScheduledFutureMap.put(call, future);
                 try {
                     mServiceInterface.createConnection(
                             call.getConnectionManagerPhoneAccount(),
@@ -1953,6 +2045,7 @@
 
     /** @see IConnectionService#onCallEndpointChanged(String, CallEndpoint, Session.Info) */
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    @Override
     public void onCallEndpointChanged(Call activeCall, CallEndpoint callEndpoint) {
         final String callId = mCallIdMapper.getCallId(activeCall);
         if (callId != null && isServiceValid("onCallEndpointChanged")) {
@@ -1968,6 +2061,7 @@
 
     /** @see IConnectionService#onAvailableCallEndpointsChanged(String, List, Session.Info) */
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    @Override
     public void onAvailableCallEndpointsChanged(Call activeCall,
             Set<CallEndpoint> availableCallEndpoints) {
         final String callId = mCallIdMapper.getCallId(activeCall);
@@ -1984,8 +2078,14 @@
         }
     }
 
+    @Override
+    public void onVideoStateChanged(Call call, int videoState){
+        // pass through. ConnectionService does not implement this method.
+    }
+
     /** @see IConnectionService#onMuteStateChanged(String, boolean, Session.Info) */
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    @Override
     public void onMuteStateChanged(Call activeCall, boolean isMuted) {
         final String callId = mCallIdMapper.getCallId(activeCall);
         if (callId != null && isServiceValid("onMuteStateChanged")) {
@@ -2028,7 +2128,8 @@
     }
 
     /** @see IConnectionService#disconnect(String, Session.Info) */
-    void disconnect(Call call) {
+    @VisibleForTesting
+    public void disconnect(Call call) {
         final String callId = mCallIdMapper.getCallId(call);
         if (callId != null && isServiceValid("disconnect")) {
             try {
@@ -2155,7 +2256,8 @@
         }
     }
 
-    void addCall(Call call) {
+    @VisibleForTesting
+    public void addCall(Call call) {
         if (mCallIdMapper.getCallId(call) == null) {
             mCallIdMapper.addCall(call);
         }
@@ -2490,12 +2592,11 @@
      */
     private void handleConnectionServiceDeath() {
         if (!mPendingResponses.isEmpty()) {
-            CreateConnectionResponse[] responses = mPendingResponses.values().toArray(
-                    new CreateConnectionResponse[mPendingResponses.values().size()]);
+            Collection<CreateConnectionResponse> responses = mPendingResponses.values();
             mPendingResponses.clear();
-            for (int i = 0; i < responses.length; i++) {
-                responses[i].handleCreateConnectionFailure(
-                        new DisconnectCause(DisconnectCause.ERROR, "CS_DEATH"));
+            for (CreateConnectionResponse response : responses) {
+                response.handleCreateConnectionFailure(new DisconnectCause(DisconnectCause.ERROR,
+                        "CS_DEATH"));
             }
         }
         mCallIdMapper.clear();
@@ -2549,9 +2650,11 @@
             }
         }
 
-        // Bail early if the caller isn't the sim connection mgr.
-        if (!isCallerConnectionManager) {
-            Log.d(this, "queryRemoteConnectionServices: none; not sim call mgr.");
+        Log.i(this, "queryRemoteConnectionServices, simServices = %s", simServices);
+        // Bail early if the caller isn't the sim connection mgr or no sim connection service
+        // other than caller available.
+        if (!isCallerConnectionManager || simServices.isEmpty()) {
+            Log.d(this, "queryRemoteConnectionServices: not sim call mgr or no simservices.");
             noRemoteServices(callback);
             return;
         }
@@ -2559,8 +2662,6 @@
         final List<ComponentName> simServiceComponentNames = new ArrayList<>();
         final List<IBinder> simServiceBinders = new ArrayList<>();
 
-        Log.i(this, "queryRemoteConnectionServices, simServices = %s", simServices);
-
         for (ConnectionServiceWrapper simService : simServices) {
             final ConnectionServiceWrapper currentSimService = simService;
 
@@ -2624,4 +2725,14 @@
         sb.append("]");
         return sb.toString();
     }
+
+    @VisibleForTesting
+    public void setScheduledExecutorService(ScheduledExecutorService service) {
+        mScheduledExecutor = service;
+    }
+
+    @VisibleForTesting
+    public void setAnomalyReporterAdapter(AnomalyReporterAdapter mAnomalyReporterAdapter){
+        mAnomalyReporter = mAnomalyReporterAdapter;
+    }
 }
diff --git a/src/com/android/server/telecom/CreateConnectionProcessor.java b/src/com/android/server/telecom/CreateConnectionProcessor.java
index 19691c1..a2c742d 100644
--- a/src/com/android/server/telecom/CreateConnectionProcessor.java
+++ b/src/com/android/server/telecom/CreateConnectionProcessor.java
@@ -32,6 +32,8 @@
 // TODO: Needed for move to system service: import com.android.internal.R;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.telecom.flags.Flags;
+import com.android.server.telecom.flags.FeatureFlags;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -101,22 +103,32 @@
         int getSlotIndex(int subId);
     }
 
-    private ITelephonyManagerAdapter mTelephonyAdapter = new ITelephonyManagerAdapter() {
+    public static class ITelephonyManagerAdapterImpl implements ITelephonyManagerAdapter {
         @Override
         public int getSubIdForPhoneAccount(Context context, PhoneAccount account) {
             TelephonyManager manager = context.getSystemService(TelephonyManager.class);
             if (manager == null) {
                 return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
             }
-            return manager.getSubscriptionId(account.getAccountHandle());
+            try {
+                return manager.getSubscriptionId(account.getAccountHandle());
+            } catch (UnsupportedOperationException uoe) {
+                return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+            }
         }
 
         @Override
         public int getSlotIndex(int subId) {
-            return SubscriptionManager.getSlotIndex(subId);
+            try {
+                return SubscriptionManager.getSlotIndex(subId);
+            } catch (UnsupportedOperationException uoe) {
+                return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+            }
         }
     };
 
+    private ITelephonyManagerAdapter mTelephonyAdapter = new ITelephonyManagerAdapterImpl();
+
     private final Call mCall;
     private final ConnectionServiceRepository mRepository;
     private List<CallAttemptRecord> mAttemptRecords;
@@ -125,14 +137,20 @@
     private DisconnectCause mLastErrorDisconnectCause;
     private final PhoneAccountRegistrar mPhoneAccountRegistrar;
     private final Context mContext;
+    private final FeatureFlags mFlags;
+    private final Timeouts.Adapter mTimeoutsAdapter;
     private CreateConnectionTimeout mTimeout;
     private ConnectionServiceWrapper mService;
     private int mConnectionAttempt;
 
     @VisibleForTesting
-    public CreateConnectionProcessor(
-            Call call, ConnectionServiceRepository repository, CreateConnectionResponse response,
-            PhoneAccountRegistrar phoneAccountRegistrar, Context context) {
+    public CreateConnectionProcessor(Call call,
+            ConnectionServiceRepository repository,
+            CreateConnectionResponse response,
+            PhoneAccountRegistrar phoneAccountRegistrar,
+            Context context,
+            FeatureFlags featureFlags,
+            Timeouts.Adapter timeoutsAdapter) {
         Log.v(this, "CreateConnectionProcessor created for Call = %s", call);
         mCall = call;
         mRepository = repository;
@@ -140,6 +158,8 @@
         mPhoneAccountRegistrar = phoneAccountRegistrar;
         mContext = context;
         mConnectionAttempt = 0;
+        mFlags = featureFlags;
+        mTimeoutsAdapter = timeoutsAdapter;
     }
 
     boolean isProcessingComplete() {
@@ -247,7 +267,25 @@
                 mConnectionAttempt++;
                 mCall.setConnectionManagerPhoneAccount(attempt.connectionManagerPhoneAccount);
                 mCall.setTargetPhoneAccount(attempt.targetPhoneAccount);
-                mCall.setConnectionService(mService);
+                if (mFlags.updatedRcsCallCountTracking()) {
+                    if (Objects.equals(attempt.connectionManagerPhoneAccount,
+                            attempt.targetPhoneAccount)) {
+                        mCall.setConnectionService(mService);
+                    } else {
+                        PhoneAccountHandle remotePhoneAccount = attempt.targetPhoneAccount;
+                        ConnectionServiceWrapper mRemoteService =
+                                mRepository.getService(remotePhoneAccount.getComponentName(),
+                                remotePhoneAccount.getUserHandle());
+                        if (mRemoteService == null) {
+                            mCall.setConnectionService(mService);
+                        } else {
+                            Log.v(this, "attemptNextPhoneAccount Setting RCS = %s", mRemoteService);
+                            mCall.setConnectionService(mService, mRemoteService);
+                        }
+                    }
+                } else {
+                    mCall.setConnectionService(mService);
+                }
                 setTimeoutIfNeeded(mService, attempt);
                 if (mCall.isIncoming()) {
                     if (mCall.isAdhocConferenceCall()) {
@@ -294,7 +332,7 @@
         clearTimeout();
 
         CreateConnectionTimeout timeout = new CreateConnectionTimeout(
-                mContext, mPhoneAccountRegistrar, service, mCall);
+                mContext, mPhoneAccountRegistrar, service, mCall, mTimeoutsAdapter);
         if (timeout.isTimeoutNeededForCall(getConnectionServices(mAttemptRecords),
                 attempt.connectionManagerPhoneAccount)) {
             mTimeout = timeout;
diff --git a/src/com/android/server/telecom/CreateConnectionTimeout.java b/src/com/android/server/telecom/CreateConnectionTimeout.java
index 14e5bf0..3046ca4 100644
--- a/src/com/android/server/telecom/CreateConnectionTimeout.java
+++ b/src/com/android/server/telecom/CreateConnectionTimeout.java
@@ -16,21 +16,27 @@
 
 package com.android.server.telecom;
 
+import static com.android.internal.telephony.flags.Flags.carrierEnabledSatelliteFlag;
+
 import android.content.Context;
 import android.os.Handler;
 import android.os.Looper;
 import android.telecom.Log;
 import android.telecom.Logging.Runnable;
+import android.telecom.PhoneAccount;
 import android.telecom.PhoneAccountHandle;
 import android.telephony.TelephonyManager;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 import java.util.Collection;
 import java.util.Objects;
 
 /**
  * Registers a timeout for a call and disconnects the call when the timeout expires.
  */
-final class CreateConnectionTimeout extends Runnable {
+@VisibleForTesting
+public final class CreateConnectionTimeout extends Runnable {
     private final Context mContext;
     private final PhoneAccountRegistrar mPhoneAccountRegistrar;
     private final ConnectionServiceWrapper mConnectionService;
@@ -38,20 +44,25 @@
     private final Handler mHandler = new Handler(Looper.getMainLooper());
     private boolean mIsRegistered;
     private boolean mIsCallTimedOut;
+    private final Timeouts.Adapter mTimeoutsAdapter;
 
-    CreateConnectionTimeout(Context context, PhoneAccountRegistrar phoneAccountRegistrar,
-            ConnectionServiceWrapper service, Call call) {
+    @VisibleForTesting
+    public CreateConnectionTimeout(Context context, PhoneAccountRegistrar phoneAccountRegistrar,
+            ConnectionServiceWrapper service, Call call, Timeouts.Adapter timeoutsAdapter) {
         super("CCT", null /*lock*/);
         mContext = context;
         mPhoneAccountRegistrar = phoneAccountRegistrar;
         mConnectionService = service;
         mCall = call;
+        mTimeoutsAdapter = timeoutsAdapter;
     }
 
-    boolean isTimeoutNeededForCall(Collection<PhoneAccountHandle> accounts,
+    @VisibleForTesting
+    public boolean isTimeoutNeededForCall(Collection<PhoneAccountHandle> accounts,
             PhoneAccountHandle currentAccount) {
         // Non-emergency calls timeout automatically at the radio layer. No need for a timeout here.
         if (!mCall.isEmergencyCall()) {
+            Log.d(this, "isTimeoutNeededForCall, not an emergency call");
             return false;
         }
 
@@ -60,11 +71,13 @@
         PhoneAccountHandle connectionManager =
                 mPhoneAccountRegistrar.getSimCallManagerFromCall(mCall);
         if (!accounts.contains(connectionManager)) {
+            Log.d(this, "isTimeoutNeededForCall, no connection manager");
             return false;
         }
 
         // No need to add a timeout if the current attempt is over the connection manager.
         if (Objects.equals(connectionManager, currentAccount)) {
+            Log.d(this, "isTimeoutNeededForCall, already attempting over connection manager");
             return false;
         }
 
@@ -104,8 +117,34 @@
 
     @Override
     public void loggedRun() {
+        if (!carrierEnabledSatelliteFlag()) {
+            timeoutCallIfNeeded();
+            return;
+        }
+
+        PhoneAccountHandle connectionManager =
+                mPhoneAccountRegistrar.getSimCallManagerFromCall(mCall);
+        if (connectionManager != null) {
+            PhoneAccount account = mPhoneAccountRegistrar.getPhoneAccount(connectionManager,
+                    connectionManager.getUserHandle());
+            if (account != null && account.hasCapabilities(
+                    (PhoneAccount.CAPABILITY_SUPPORTS_VOICE_CALLING_INDICATIONS
+                            | PhoneAccount.CAPABILITY_VOICE_CALLING_AVAILABLE))) {
+                // If we have encountered the timeout and there is an in service
+                // ConnectionManager, disconnect the call so that it can be attempted over
+                // the ConnectionManager.
+                timeoutCallIfNeeded();
+                return;
+            }
+            Log.i(
+                this,
+               "loggedRun, no PhoneAccount with voice calling capabilities, not timing out call");
+        }
+    }
+
+    private void timeoutCallIfNeeded() {
         if (mIsRegistered && isCallBeingPlaced(mCall)) {
-            Log.i(this, "run, call timed out, calling disconnect");
+            Log.i(this, "timeoutCallIfNeeded, call timed out, calling disconnect");
             mIsCallTimedOut = true;
             mConnectionService.disconnect(mCall);
         }
@@ -122,13 +161,19 @@
     private long getTimeoutLengthMillis() {
         // If the radio is off then use a longer timeout. This gives us more time to power on the
         // radio.
-        TelephonyManager telephonyManager =
-            (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
-        if (telephonyManager.isRadioOn()) {
-            return Timeouts.getEmergencyCallTimeoutMillis(mContext.getContentResolver());
-        } else {
-            return Timeouts.getEmergencyCallTimeoutRadioOffMillis(
-                    mContext.getContentResolver());
+        try {
+            TelephonyManager telephonyManager =
+                    (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
+            if (telephonyManager.isRadioOn()) {
+                return mTimeoutsAdapter.getEmergencyCallTimeoutMillis(
+                        mContext.getContentResolver());
+            } else {
+                return mTimeoutsAdapter.getEmergencyCallTimeoutRadioOffMillis(
+                        mContext.getContentResolver());
+            }
+        } catch (UnsupportedOperationException uoe) {
+            Log.e(this, uoe, "getTimeoutLengthMillis - telephony is not supported");
+            return mTimeoutsAdapter.getEmergencyCallTimeoutMillis(mContext.getContentResolver());
         }
     }
 }
diff --git a/src/com/android/server/telecom/DefaultDialerCache.java b/src/com/android/server/telecom/DefaultDialerCache.java
index dc79715..44b426a 100644
--- a/src/com/android/server/telecom/DefaultDialerCache.java
+++ b/src/com/android/server/telecom/DefaultDialerCache.java
@@ -142,9 +142,9 @@
     private ComponentName mOverrideSystemDialerComponentName;
 
     public DefaultDialerCache(Context context,
-            DefaultDialerManagerAdapter defaultDialerManagerAdapter,
-            RoleManagerAdapter roleManagerAdapter,
-            TelecomSystem.SyncRoot lock) {
+                              DefaultDialerManagerAdapter defaultDialerManagerAdapter,
+                              RoleManagerAdapter roleManagerAdapter,
+                              TelecomSystem.SyncRoot lock) {
         mContext = context;
         mDefaultDialerManagerAdapter = defaultDialerManagerAdapter;
         mRoleManagerAdapter = roleManagerAdapter;
@@ -176,6 +176,10 @@
                         UserHandle.USER_ALL);
     }
 
+    public String[] getBTInCallServicePackages() {
+        return mRoleManagerAdapter.getBTInCallService();
+    }
+
     public String getDefaultDialerApplication(int userId) {
         if (userId == UserHandle.USER_CURRENT) {
             userId = ActivityManager.getCurrentUser();
diff --git a/src/com/android/server/telecom/EmergencyCallDiagnosticLogger.java b/src/com/android/server/telecom/EmergencyCallDiagnosticLogger.java
index af79da3..cce8c66 100644
--- a/src/com/android/server/telecom/EmergencyCallDiagnosticLogger.java
+++ b/src/com/android/server/telecom/EmergencyCallDiagnosticLogger.java
@@ -16,7 +16,7 @@
 
 package com.android.server.telecom;
 
-import static android.telephony.TelephonyManager.EmergencyCallDiagnosticParams;
+import static android.telephony.TelephonyManager.EmergencyCallDiagnosticData;
 
 import android.os.BugreportManager;
 import android.os.DropBoxManager;
@@ -156,25 +156,26 @@
         List<Integer> dataCollectionTypes = getDataCollectionTypes(reason);
         boolean invokeTelephonyPersistApi = false;
         CallEventTimestamps ts = mEmergencyCallsMap.get(call);
-        EmergencyCallDiagnosticParams dp =
-                new EmergencyCallDiagnosticParams();
+        EmergencyCallDiagnosticData.Builder callDiagnosticBuilder =
+                new EmergencyCallDiagnosticData.Builder();
         for (Integer dataCollectionType : dataCollectionTypes) {
             switch (dataCollectionType) {
                 case COLLECTION_TYPE_TELECOM_STATE:
                     if (isTelecomDumpCollectionEnabled()) {
-                        dp.setTelecomDumpSysCollection(true);
+                        callDiagnosticBuilder.setTelecomDumpsysCollectionEnabled(true);
                         invokeTelephonyPersistApi = true;
                     }
                     break;
                 case COLLECTION_TYPE_TELEPHONY_STATE:
                     if (isTelephonyDumpCollectionEnabled()) {
-                        dp.setTelephonyDumpSysCollection(true);
+                        callDiagnosticBuilder.setTelephonyDumpsysCollectionEnabled(true);
                         invokeTelephonyPersistApi = true;
                     }
                     break;
                 case COLLECTION_TYPE_LOGCAT_BUFFERS:
                     if (isLogcatCollectionEnabled()) {
-                        dp.setLogcatCollection(true, ts.getCallCreatedTime());
+                        callDiagnosticBuilder.setLogcatCollectionStartTimeMillis(
+                                ts.getCallCreatedTime());
                         invokeTelephonyPersistApi = true;
                     }
                     break;
@@ -191,13 +192,14 @@
                 default:
             }
         }
+        EmergencyCallDiagnosticData ecdData = callDiagnosticBuilder.build();
         if (invokeTelephonyPersistApi) {
             mAsyncTaskExecutor.execute(new Runnable() {
                 @Override
                 public void run() {
-                    Log.i(this, "Requesting Telephony to persist data %s", dp.toString());
+                    Log.i(this, "Requesting Telephony to persist data %s", ecdData.toString());
                     try {
-                        mTelephonyManager.persistEmergencyCallDiagnosticData(DROPBOX_TAG, dp);
+                        mTelephonyManager.persistEmergencyCallDiagnosticData(DROPBOX_TAG, ecdData);
                     } catch (Exception e) {
                         Log.w(this,
                                 "Exception while invoking "
@@ -337,12 +339,11 @@
     @Override
     public void onCallStateChanged(Call call, int oldState, int newState) {
 
-        if (call != null && mEmergencyCallsMap.get(call) != null && newState == CallState.ACTIVE) {
-            CallEventTimestamps ts = mEmergencyCallsMap.get(call);
-            if (ts != null) {
-                long currentTime = mClockProxy.currentTimeMillis();
-                ts.setCallActiveTime(currentTime);
-            }
+        CallEventTimestamps ts = mEmergencyCallsMap.get(call);
+        if (call != null && ts != null && newState == CallState.ACTIVE
+                && ts.getCallActiveTime() == 0) {
+            long currentTime = mClockProxy.currentTimeMillis();
+            ts.setCallActiveTime(currentTime);
         }
     }
 
@@ -402,7 +403,12 @@
             Log.i(this, "skipped dumping diagnostic data");
             return;
         }
-        dumpDiagnosticDataFromDropbox(pw);
+        try {
+            dumpDiagnosticDataFromDropbox(pw);
+        } catch (Exception e) {
+            pw.println("Exception was thrown while dumping diagnostic data from DropBox");
+            e.printStackTrace();
+        }
     }
 
     private static class CallEventTimestamps {
diff --git a/src/com/android/server/telecom/EmergencyCallHelper.java b/src/com/android/server/telecom/EmergencyCallHelper.java
index 5ab0e99..c0e38ca 100644
--- a/src/com/android/server/telecom/EmergencyCallHelper.java
+++ b/src/com/android/server/telecom/EmergencyCallHelper.java
@@ -24,6 +24,7 @@
 import android.telecom.PhoneAccountHandle;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.telecom.flags.FeatureFlags;
 
 /**
  * Helps with emergency calls by:
@@ -51,19 +52,25 @@
     private long mLastEmergencyCallTimestampMillis;
     private long mLastOutgoingEmergencyCallTimestampMillis;
 
+    private final FeatureFlags mFeatureFlags;
+
     @VisibleForTesting
     public EmergencyCallHelper(
             Context context,
             DefaultDialerCache defaultDialerCache,
-            Timeouts.Adapter timeoutsAdapter) {
+            Timeouts.Adapter timeoutsAdapter,
+            FeatureFlags featureFlags) {
         mContext = context;
         mDefaultDialerCache = defaultDialerCache;
         mTimeoutsAdapter = timeoutsAdapter;
+        mFeatureFlags = featureFlags;
     }
 
     @VisibleForTesting
     public void maybeGrantTemporaryLocationPermission(Call call, UserHandle userHandle) {
-        if (shouldGrantTemporaryLocationPermission(call)) {
+        if (shouldGrantTemporaryLocationPermission(call) && (
+                !mFeatureFlags.preventRedundantLocationPermissionGrantAndRevoke()
+                || !wasGrantedTemporaryLocationPermission())) {
             grantLocationPermission(userHandle);
         }
         if (call != null && call.isEmergencyCall()) {
diff --git a/src/com/android/server/telecom/InCallAdapter.java b/src/com/android/server/telecom/InCallAdapter.java
index 9ce10bd..514ba48 100755
--- a/src/com/android/server/telecom/InCallAdapter.java
+++ b/src/com/android/server/telecom/InCallAdapter.java
@@ -20,6 +20,7 @@
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.ResultReceiver;
+import android.os.UserHandle;
 import android.telecom.CallEndpoint;
 import android.telecom.Log;
 import android.telecom.PhoneAccountHandle;
@@ -420,7 +421,8 @@
             Log.startSession(LogUtils.Sessions.ICA_ENTER_AUDIO_PROCESSING,
                     mOwnerPackageAbbreviation);
             // TODO: enforce the extra permission.
-            Binder.withCleanCallingIdentity(() -> {
+            long token = Binder.clearCallingIdentity();
+            try {
                 synchronized (mLock) {
                     Call call = mCallIdMapper.getCall(callId);
                     if (call != null) {
@@ -429,7 +431,9 @@
                         Log.w(this, "enterBackgroundAudioProcessing, unknown call id: %s", callId);
                     }
                 }
-            });
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
         } finally {
             Log.endSession();
         }
@@ -440,7 +444,8 @@
         try {
             Log.startSession(LogUtils.Sessions.ICA_EXIT_AUDIO_PROCESSING,
                     mOwnerPackageAbbreviation);
-            Binder.withCleanCallingIdentity(() -> {
+            long token = Binder.clearCallingIdentity();
+            try {
                 synchronized (mLock) {
                     Call call = mCallIdMapper.getCall(callId);
                     if (call != null) {
@@ -450,7 +455,9 @@
                                 "exitBackgroundAudioProcessing, unknown call id: %s", callId);
                     }
                 }
-            });
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
         } finally {
             Log.endSession();
         }
diff --git a/src/com/android/server/telecom/InCallController.java b/src/com/android/server/telecom/InCallController.java
index 598664e..14f4439 100644
--- a/src/com/android/server/telecom/InCallController.java
+++ b/src/com/android/server/telecom/InCallController.java
@@ -23,6 +23,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.AppOpsManager;
+import android.app.KeyguardManager;
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.content.AttributionSource;
@@ -47,7 +48,6 @@
 import android.os.Trace;
 import android.os.UserHandle;
 import android.os.UserManager;
-import android.permission.PermissionManager;
 import android.telecom.CallAudioState;
 import android.telecom.CallEndpoint;
 import android.telecom.ConnectionService;
@@ -59,6 +59,7 @@
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.ArraySet;
+import android.util.Pair;
 
 import com.android.internal.annotations.VisibleForTesting;
 // TODO: Needed for move to system service: import com.android.internal.R;
@@ -66,11 +67,13 @@
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.server.telecom.SystemStateHelper.SystemStateListener;
+import com.android.server.telecom.flags.FeatureFlags;
 import com.android.server.telecom.ui.NotificationChannelManager;
 
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
@@ -80,6 +83,7 @@
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 /**
  * Binds to {@link IInCallService} and provides the service to {@link CallsManager} through which it
@@ -324,8 +328,14 @@
                     addCall(call);
 
                     // Notify this new added call
-                    sendCallToService(call, mInCallServiceInfo,
-                            mInCallServices.get(userFromCall).get(mInCallServiceInfo));
+                    if (mFeatureFlags.separatelyBindToBtIncallService()
+                            && mInCallServiceInfo.getType() == IN_CALL_SERVICE_TYPE_BLUETOOTH) {
+                        sendCallToService(call, mInCallServiceInfo, mBTInCallServices
+                                .get(userFromCall).second);
+                    } else {
+                        sendCallToService(call, mInCallServiceInfo,
+                                mInCallServices.get(userFromCall).get(mInCallServiceInfo));
+                    }
                 }
                 return CONNECTION_SUCCEEDED;
             }
@@ -351,7 +361,8 @@
             Log.i(this, "Attempting to bind to InCall %s, with %s", mInCallServiceInfo, intent);
             mIsConnected = true;
             mInCallServiceInfo.setBindingStartTime(mClockProxy.elapsedRealtime());
-            boolean isManagedProfile = UserUtil.isManagedProfile(mContext, userFromCall);
+            boolean isManagedProfile = UserUtil.isManagedProfile(mContext,
+                    userFromCall, mFeatureFlags);
             // Note that UserHandle.CURRENT fails to capture the work profile, so we need to handle
             // it separately to ensure that the ICS is bound to the appropriate user. If ECBM is
             // active, we know that a work sim was previously used to place a MO emergency call. We
@@ -359,7 +370,8 @@
             // not be running (handled in getUserFromCall).
             UserHandle userToBind = isManagedProfile ? userFromCall : UserHandle.CURRENT;
             if ((mInCallServiceInfo.mType == IN_CALL_SERVICE_TYPE_NON_UI
-                    || mInCallServiceInfo.mType == IN_CALL_SERVICE_TYPE_CAR_MODE_UI) && (
+                    || mInCallServiceInfo.mType == IN_CALL_SERVICE_TYPE_CAR_MODE_UI
+                    || mInCallServiceInfo.mType == IN_CALL_SERVICE_TYPE_BLUETOOTH) && (
                     mUserHandleToUseForBinding != null)) {
                 //guarding change for non-UI/carmode-UI services which may not be present for
                 // work profile.
@@ -1069,6 +1081,7 @@
                 if (Intent.ACTION_PACKAGE_CHANGED.equals(intent.getAction())) {
                     synchronized (mLock) {
                         int uid = intent.getIntExtra(Intent.EXTRA_UID, 0);
+                        String changedPackage = intent.getData().getSchemeSpecificPart();
                         UserHandle userHandle = UserHandle.getUserHandleForUid(uid);
                         boolean isManagedProfile = um.isManagedProfile(userHandle.getIdentifier());
 
@@ -1098,12 +1111,36 @@
                                         childManagedProfileUser);
                         List<InCallServiceBindingConnection> componentsToBindForUser = null;
                         List<InCallServiceBindingConnection> componentsToBindForChild = null;
+                        // Separate binding for BT logic.
+                        boolean isBluetoothPkg = isBluetoothPackage(changedPackage);
+                        Call callToConnectWith = mCallIdMapper.getCalls().isEmpty()
+                                ? null
+                                : mCallIdMapper.getCalls().iterator().next();
+
+                        // Bind to BT service if there's an available call. When the flag isn't
+                        // enabled, the service will be included as part of
+                        // getNonUiInCallServiceBindingConnectionList.
+                        if (mFeatureFlags.separatelyBindToBtIncallService()
+                                && isBluetoothPkg && callToConnectWith != null) {
+                            // mNonUIInCallServiceConnections will always contain a key for
+                            // userHandle and/or the child user if there is an ongoing call with
+                            // that user, regardless if there aren't any non-UI ICS bound.
+                            if (isUserKeyPresent) {
+                                bindToBTService(callToConnectWith, userHandle);
+                            }
+                            if (isChildUserKeyPresent) {
+                                // This will try to use the ICS found in the parent if one isn't
+                                // available for the child.
+                                bindToBTService(callToConnectWith, childManagedProfileUser);
+                            }
+                        }
 
                         if(isUserKeyPresent) {
                             componentsToBindForUser =
                                     getNonUiInCallServiceBindingConnectionList(intent,
                                             userHandle, null);
                         }
+
                         if (isChildUserKeyPresent) {
                             componentsToBindForChild =
                                     getNonUiInCallServiceBindingConnectionList(intent,
@@ -1116,11 +1153,11 @@
                                 isUserKeyPresent, isChildUserKeyPresent, isManagedProfile,
                                 userHandle.getIdentifier());
 
-                        if (isUserKeyPresent && componentsToBindForUser != null) {
+                        if (isUserKeyPresent && !componentsToBindForUser.isEmpty()) {
                             mNonUIInCallServiceConnections.get(userHandle).
                                     addConnections(componentsToBindForUser);
                         }
-                        if (isChildUserKeyPresent && componentsToBindForChild != null) {
+                        if (isChildUserKeyPresent && !componentsToBindForChild.isEmpty()) {
                             mNonUIInCallServiceConnections.get(childManagedProfileUser).
                                     addConnections(componentsToBindForChild);
                         }
@@ -1173,6 +1210,11 @@
     private static final int IN_CALL_SERVICE_TYPE_CAR_MODE_UI = 3;
     private static final int IN_CALL_SERVICE_TYPE_NON_UI = 4;
     private static final int IN_CALL_SERVICE_TYPE_COMPANION = 5;
+    private static final int IN_CALL_SERVICE_TYPE_BLUETOOTH = 6;
+
+    // Timeout value to be used to ensure future completion for mDisconnectedToneBtFutures. This is
+    // set to 4 seconds to account for the exceptional case (TONE_CONGESTION).
+    private static final int DISCONNECTED_TONE_TIMEOUT = 4000;
 
     private static final int[] LIVE_CALL_STATES = { CallState.ACTIVE, CallState.PULLING,
             CallState.DISCONNECTING };
@@ -1180,8 +1222,13 @@
     /** The in-call app implementations, see {@link IInCallService}. */
     private final Map<UserHandle, Map<InCallServiceInfo, IInCallService>>
             mInCallServices = new ArrayMap<>();
+    private final Map<UserHandle, Pair<InCallServiceInfo, IInCallService>> mBTInCallServices =
+            new ArrayMap<>();
+    private final Map<UserHandle, Map<InCallServiceInfo, IInCallService>>
+            mCombinedInCallServiceMap = new ArrayMap<>();
 
     private final CallIdMapper mCallIdMapper = new CallIdMapper(Call::getId);
+    private final Collection<Call> mPendingEndToneCall = new ArraySet<>();
 
     private final Context mContext;
     private final AppOpsManager mAppOpsManager;
@@ -1197,8 +1244,11 @@
             mInCallServiceConnections = new ArrayMap<>();
     private final Map<UserHandle, NonUIInCallServiceConnectionCollection>
             mNonUIInCallServiceConnections = new ArrayMap<>();
+    private final Map<UserHandle, InCallServiceConnection> mBTInCallServiceConnections =
+            new ArrayMap<>();
     private final ClockProxy mClockProxy;
     private final IBinder mToken = new Binder();
+    private final FeatureFlags mFeatureFlags;
 
     // A set of known non-UI in call services on the device, including those that are disabled.
     // We track this so that we can efficiently bind to them when we're notified that a new
@@ -1209,6 +1259,15 @@
     // The future will complete with true if binding succeeds, false if it timed out.
     private CompletableFuture<Boolean> mBindingFuture = CompletableFuture.completedFuture(true);
 
+    // Future that's in a completed state unless we're in the middle of a binding to a bluetooth
+    // in-call service.
+    // The future will complete with true if bluetooth in-call service succeeds, false if it timed
+    // out.
+    private Map<UserHandle, CompletableFuture<Boolean>> mBtBindingFuture = new ArrayMap<>();
+    // Future used to delay terminating the BT InCallService before the call disconnect tone
+    // finishes playing.
+    private Map<String, CompletableFuture<Void>> mDisconnectedToneBtFutures = new ArrayMap<>();
+
     private final CarModeTracker mCarModeTracker;
 
     /**
@@ -1230,6 +1289,8 @@
 
     private boolean mIsStartCallDelayScheduled = false;
 
+    private boolean mDisconnectedToneStartedPlaying = false;
+
     /**
      * A list of call IDs which are currently using the camera.
      */
@@ -1238,10 +1299,22 @@
     private ArraySet<String> mAllCarrierPrivilegedApps = new ArraySet<>();
     private ArraySet<String> mActiveCarrierPrivilegedApps = new ArraySet<>();
 
+    private java.lang.Runnable mCallRemovedRunnable;
+
     public InCallController(Context context, TelecomSystem.SyncRoot lock, CallsManager callsManager,
             SystemStateHelper systemStateHelper, DefaultDialerCache defaultDialerCache,
             Timeouts.Adapter timeoutsAdapter, EmergencyCallHelper emergencyCallHelper,
-            CarModeTracker carModeTracker, ClockProxy clockProxy) {
+            CarModeTracker carModeTracker, ClockProxy clockProxy, FeatureFlags featureFlags) {
+      this(context, lock, callsManager, systemStateHelper, defaultDialerCache, timeoutsAdapter,
+              emergencyCallHelper, carModeTracker, clockProxy, featureFlags, null);
+    }
+
+    @VisibleForTesting
+    public InCallController(Context context, TelecomSystem.SyncRoot lock, CallsManager callsManager,
+            SystemStateHelper systemStateHelper, DefaultDialerCache defaultDialerCache,
+            Timeouts.Adapter timeoutsAdapter, EmergencyCallHelper emergencyCallHelper,
+            CarModeTracker carModeTracker, ClockProxy clockProxy, FeatureFlags featureFlags,
+            com.android.internal.telephony.flags.FeatureFlags telephonyFeatureFlags) {
         mContext = context;
         mAppOpsManager = context.getSystemService(AppOpsManager.class);
         mSensorPrivacyManager = context.getSystemService(SensorPrivacyManager.class);
@@ -1258,6 +1331,7 @@
         IntentFilter userAddedFilter = new IntentFilter(Intent.ACTION_USER_ADDED);
         userAddedFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
         mContext.registerReceiver(mUserAddedReceiver, userAddedFilter);
+        mFeatureFlags = featureFlags;
     }
 
     private void restrictPhoneCallOps() {
@@ -1337,87 +1411,133 @@
         // Track the call if we don't already know about it.
         addCall(call);
 
-        if (!isBoundAndConnectedToServices(userFromCall)) {
-            Log.i(this, "onCallAdded: %s; not bound or connected.", call);
-            // We are not bound, or we're not connected.
-            bindToServices(call);
+        if (mFeatureFlags.separatelyBindToBtIncallService()) {
+            boolean bindingToBtRequired = false;
+            boolean bindingToOtherServicesRequired = false;
+            if (!isBoundAndConnectedToBTService(userFromCall)) {
+                Log.i(this, "onCallAdded: %s; not bound or connected to BT ICS.", call);
+                bindingToBtRequired = true;
+                bindToBTService(call, null);
+            }
+            if (!isBoundAndConnectedToServices(userFromCall)) {
+                Log.i(this, "onCallAdded: %s; not bound or connected to other ICS.", call);
+                // We are not bound, or we're not connected.
+                bindingToOtherServicesRequired = true;
+                bindToServices(call);
+            }
+            // If either BT service are already bound or other services are already bound, attempt
+            // to add the new call to the connected incall services.
+            if (!bindingToBtRequired || !bindingToOtherServicesRequired) {
+                addCallToConnectedServices(call, userFromCall);
+            }
         } else {
-            InCallServiceConnection inCallServiceConnection =
-                    mInCallServiceConnections.get(userFromCall);
-
-            // We are bound, and we are connected.
-            adjustServiceBindingsForEmergency(userFromCall);
-
-            // This is in case an emergency call is added while there is an existing call.
-            mEmergencyCallHelper.maybeGrantTemporaryLocationPermission(call,
-                    userFromCall);
-
-            if (inCallServiceConnection != null) {
-                Log.i(this, "mInCallServiceConnection isConnected=%b",
-                        inCallServiceConnection.isConnected());
+            if (!isBoundAndConnectedToServices(userFromCall)) {
+                Log.i(this, "onCallAdded: %s; not bound or connected.", call);
+                // We are not bound, or we're not connected.
+                bindToServices(call);
+            } else {
+                addCallToConnectedServices(call, userFromCall);
             }
+        }
+    }
 
-            List<ComponentName> componentsUpdated = new ArrayList<>();
-            if (mInCallServices.containsKey(userFromCall)) {
-                for (Map.Entry<InCallServiceInfo, IInCallService> entry : mInCallServices.
-                        get(userFromCall).entrySet()) {
-                    InCallServiceInfo info = entry.getKey();
+    private void addCallToConnectedServices(Call call, UserHandle userFromCall) {
+        InCallServiceConnection inCallServiceConnection =
+                mInCallServiceConnections.get(userFromCall);
 
-                    if (call.isExternalCall() && !info.isExternalCallsSupported()) {
-                        continue;
-                    }
+        // We are bound, and we are connected.
+        adjustServiceBindingsForEmergency(userFromCall);
 
-                    if (call.isSelfManaged() && (!call.visibleToInCallService()
-                            || !info.isSelfManagedCallsSupported())) {
-                        continue;
-                    }
+        // This is in case an emergency call is added while there is an existing call.
+        mEmergencyCallHelper.maybeGrantTemporaryLocationPermission(call, userFromCall);
 
-                    // Only send the RTT call if it's a UI in-call service
-                    boolean includeRttCall = false;
-                    if (inCallServiceConnection != null) {
-                        includeRttCall = info.equals(inCallServiceConnection.getInfo());
-                    }
+        if (inCallServiceConnection != null) {
+            Log.i(this, "mInCallServiceConnection isConnected=%b",
+                    inCallServiceConnection.isConnected());
+        }
 
-                    componentsUpdated.add(info.getComponentName());
-                    IInCallService inCallService = entry.getValue();
+        List<ComponentName> componentsUpdated = new ArrayList<>();
+        Map<UserHandle, Map<InCallController.InCallServiceInfo, IInCallService>> serviceMap =
+                getCombinedInCallServiceMap();
+        if (serviceMap.containsKey(userFromCall)) {
+            for (Map.Entry<InCallServiceInfo, IInCallService> entry :
+                    serviceMap.get(userFromCall).entrySet()) {
+                InCallServiceInfo info = entry.getKey();
 
-                    ParcelableCall parcelableCall = ParcelableCallUtils.toParcelableCall(call,
-                            true /* includeVideoProvider */,
-                            mCallsManager.getPhoneAccountRegistrar(),
-                            info.isExternalCallsSupported(), includeRttCall,
-                            info.getType() == IN_CALL_SERVICE_TYPE_SYSTEM_UI ||
-                                    info.getType() == IN_CALL_SERVICE_TYPE_NON_UI);
-                    try {
-                        inCallService.addCall(
-                                sanitizeParcelableCallForService(info, parcelableCall));
-                        updateCallTracking(call, info, true /* isAdd */);
-                    } catch (RemoteException ignored) {
-                    }
+                if (call.isExternalCall() && !info.isExternalCallsSupported()) {
+                    continue;
                 }
-                Log.i(this, "Call added to components: %s", componentsUpdated);
+
+                if (call.isSelfManaged() && (!call.visibleToInCallService()
+                        || !info.isSelfManagedCallsSupported())) {
+                    continue;
+                }
+
+                // Only send the RTT call if it's a UI in-call service
+                boolean includeRttCall = false;
+                if (inCallServiceConnection != null) {
+                    includeRttCall = info.equals(inCallServiceConnection.getInfo());
+                }
+
+                componentsUpdated.add(info.getComponentName());
+                IInCallService inCallService = entry.getValue();
+
+                ParcelableCall parcelableCall = ParcelableCallUtils.toParcelableCall(call,
+                        true /* includeVideoProvider */,
+                        mCallsManager.getPhoneAccountRegistrar(),
+                        info.isExternalCallsSupported(), includeRttCall,
+                        info.getType() == IN_CALL_SERVICE_TYPE_SYSTEM_UI ||
+                                info.getType() == IN_CALL_SERVICE_TYPE_NON_UI);
+                try {
+                    inCallService.addCall(
+                            sanitizeParcelableCallForService(info, parcelableCall));
+                    updateCallTracking(call, info, true /* isAdd */);
+                } catch (RemoteException ignored) {
+                }
             }
+            Log.i(this, "Call added to ICS: %s", componentsUpdated);
         }
     }
 
     @Override
     public void onCallRemoved(Call call) {
         Log.i(this, "onCallRemoved: %s", call);
-        if (mCallsManager.getCalls().isEmpty()) {
+        // Instead of checking if there are no active calls, we should check if there any calls with
+        // the same associated user returned from getUserFromCall. For instance, it's possible to
+        // have calls coexist on the personal profile and work profile, in which case, we would only
+        // remove the ICS connection for the user associated with the call to be disconnected.
+        UserHandle userFromCall = getUserFromCall(call);
+        Stream<Call> callsAssociatedWithUserFromCall = mCallsManager.getCalls().stream()
+                .filter((c) -> getUserFromCall(c).equals(userFromCall));
+        boolean isCallCountZero = mFeatureFlags.associatedUserRefactorForWorkProfile()
+                ? callsAssociatedWithUserFromCall.count() == 0
+                : mCallsManager.getCalls().isEmpty();
+        if (isCallCountZero) {
             /** Let's add a 2 second delay before we send unbind to the services to hopefully
              *  give them enough time to process all the pending messages.
              */
-            mHandler.postDelayed(new Runnable("ICC.oCR", mLock) {
+            if (mCallRemovedRunnable != null
+                    && mFeatureFlags.preventRedundantLocationPermissionGrantAndRevoke()) {
+                mHandler.removeCallbacks(mCallRemovedRunnable);
+            }
+            mCallRemovedRunnable = new Runnable("ICC.oCR", mLock) {
                 @Override
                 public void loggedRun() {
-                    // Check again to make sure there are no active calls.
-                    if (mCallsManager.getCalls().isEmpty()) {
-                        unbindFromServices(getUserFromCall(call));
-
+                    // Check again to make sure there are no active calls for the associated user.
+                    Stream<Call> callsAssociatedWithUserFromCall = mCallsManager.getCalls().stream()
+                            .filter((c) -> getUserFromCall(c).equals(userFromCall));
+                    boolean isCallCountZero = mFeatureFlags.associatedUserRefactorForWorkProfile()
+                            ? callsAssociatedWithUserFromCall.count() == 0
+                            : mCallsManager.getCalls().isEmpty();
+                    if (isCallCountZero) {
+                        unbindFromServices(userFromCall);
                         mEmergencyCallHelper.maybeRevokeTemporaryLocationPermission();
                     }
                 }
-            }.prepare(), mTimeoutsAdapter.getCallRemoveUnbindInCallServicesDelay(
-                    mContext.getContentResolver()));
+            }.prepare();
+            mHandler.postDelayed(mCallRemovedRunnable,
+                    mTimeoutsAdapter.getCallRemoveUnbindInCallServicesDelay(
+                            mContext.getContentResolver()));
         }
         call.removeListener(mCallListener);
         mCallIdMapper.removeCall(call);
@@ -1430,15 +1550,50 @@
     }
 
     @Override
+    public void onDisconnectedTonePlaying(Call call, boolean isTonePlaying) {
+        Log.i(this, "onDisconnectedTonePlaying: %s -> %b", call, isTonePlaying);
+        if (mFeatureFlags.separatelyBindToBtIncallService()) {
+            synchronized (mLock) {
+                if (isTonePlaying) {
+                    mDisconnectedToneStartedPlaying = true;
+                } else if (mDisconnectedToneStartedPlaying) {
+                    mDisconnectedToneStartedPlaying = false;
+                    if (mDisconnectedToneBtFutures.containsKey(call.getId())) {
+                        Log.i(this, "onDisconnectedTonePlaying: completing BT "
+                                + "disconnected tone future");
+                        mDisconnectedToneBtFutures.get(call.getId()).complete(null);
+                    }
+                    mPendingEndToneCall.remove(call);
+                    if (!mPendingEndToneCall.isEmpty()) {
+                        return;
+                    }
+                    UserHandle userHandle = getUserFromCall(call);
+                    if (mBTInCallServiceConnections.containsKey(userHandle)) {
+                        Log.i(this, "onDisconnectedTonePlaying: Unbinding BT service");
+                        mBTInCallServiceConnections.get(userHandle).disconnect();
+                        mBTInCallServiceConnections.remove(userHandle);
+                    }
+                    // Ensure that BT ICS instance is cleaned up
+                    if (mBTInCallServices.remove(userHandle) != null) {
+                        updateCombinedInCallServiceMap(userHandle);
+                    }
+                }
+            }
+        }
+    }
+
+    @Override
     public void onExternalCallChanged(Call call, boolean isExternalCall) {
         Log.i(this, "onExternalCallChanged: %s -> %b", call, isExternalCall);
 
         List<ComponentName> componentsUpdated = new ArrayList<>();
         UserHandle userFromCall = getUserFromCall(call);
-        if (!isExternalCall && mInCallServices.containsKey(userFromCall)) {
+        Map<UserHandle, Map<InCallController.InCallServiceInfo, IInCallService>> serviceMap =
+                getCombinedInCallServiceMap();
+        if (!isExternalCall && serviceMap.containsKey(userFromCall)) {
             // The call was external but it is no longer external.  We must now add it to any
             // InCallServices which do not support external calls.
-            for (Map.Entry<InCallServiceInfo, IInCallService> entry : mInCallServices.
+            for (Map.Entry<InCallServiceInfo, IInCallService> entry : serviceMap.
                     get(userFromCall).entrySet()) {
                 InCallServiceInfo info = entry.getKey();
 
@@ -1477,9 +1632,9 @@
             // InCallServices which do not support external calls.
             // Remove the call by sending a call update indicating the call was disconnected.
             Log.i(this, "Removing external call %s", call);
-            if (mInCallServices.containsKey(userFromCall)) {
-                for (Map.Entry<InCallServiceInfo, IInCallService> entry : mInCallServices.
-                        get(userFromCall).entrySet()) {
+            if (serviceMap.containsKey(userFromCall)) {
+                for (Map.Entry<InCallServiceInfo, IInCallService> entry :
+                        serviceMap.get(userFromCall).entrySet()) {
                     InCallServiceInfo info = entry.getKey();
                     if (info.isExternalCallsSupported()) {
                         // For InCallServices which support external calls, we do not need to remove
@@ -1515,6 +1670,8 @@
 
     @Override
     public void onCallStateChanged(Call call, int oldState, int newState) {
+        Log.i(this, "onCallStateChanged: Call state changed for TC@%s: %s -> %s", call.getId(),
+                CallState.toString(oldState), CallState.toString(newState));
         maybeTrackMicrophoneUse(isMuted());
         updateCall(call);
     }
@@ -1530,11 +1687,13 @@
     @Override
     public void onCallAudioStateChanged(CallAudioState oldCallAudioState,
             CallAudioState newCallAudioState) {
-        if (!mInCallServices.isEmpty()) {
+        Map<UserHandle, Map<InCallController.InCallServiceInfo, IInCallService>> serviceMap =
+                getCombinedInCallServiceMap();
+        if (!serviceMap.isEmpty()) {
             Log.i(this, "Calling onAudioStateChanged, audioState: %s -> %s", oldCallAudioState,
                     newCallAudioState);
             maybeTrackMicrophoneUse(newCallAudioState.isMuted());
-            mInCallServices.values().forEach(inCallServices -> {
+            serviceMap.values().forEach(inCallServices -> {
                 for (IInCallService inCallService : inCallServices.values()) {
                     try {
                         inCallService.onCallAudioStateChanged(newCallAudioState);
@@ -1547,9 +1706,11 @@
 
     @Override
     public void onCallEndpointChanged(CallEndpoint callEndpoint) {
-        if (!mInCallServices.isEmpty()) {
+        Map<UserHandle, Map<InCallController.InCallServiceInfo, IInCallService>> serviceMap =
+                getCombinedInCallServiceMap();
+        if (!serviceMap.isEmpty()) {
             Log.i(this, "Calling onCallEndpointChanged");
-            mInCallServices.values().forEach(inCallServices -> {
+            serviceMap.values().forEach(inCallServices -> {
                 for (IInCallService inCallService : inCallServices.values()) {
                     try {
                         inCallService.onCallEndpointChanged(callEndpoint);
@@ -1563,10 +1724,12 @@
 
     @Override
     public void onAvailableCallEndpointsChanged(Set<CallEndpoint> availableCallEndpoints) {
-        if (!mInCallServices.isEmpty()) {
+        Map<UserHandle, Map<InCallController.InCallServiceInfo, IInCallService>> serviceMap =
+                getCombinedInCallServiceMap();
+        if (!serviceMap.isEmpty()) {
             Log.i(this, "Calling onAvailableCallEndpointsChanged");
             List<CallEndpoint> availableEndpoints = new ArrayList<>(availableCallEndpoints);
-            mInCallServices.values().forEach(inCallServices -> {
+            serviceMap.values().forEach(inCallServices -> {
                 for (IInCallService inCallService : inCallServices.values()) {
                     try {
                         inCallService.onAvailableCallEndpointsChanged(availableEndpoints);
@@ -1580,9 +1743,11 @@
 
     @Override
     public void onMuteStateChanged(boolean isMuted) {
-        if (!mInCallServices.isEmpty()) {
+        Map<UserHandle, Map<InCallController.InCallServiceInfo, IInCallService>> serviceMap =
+                getCombinedInCallServiceMap();
+        if (!serviceMap.isEmpty()) {
             Log.i(this, "Calling onMuteStateChanged");
-            mInCallServices.values().forEach(inCallServices -> {
+            serviceMap.values().forEach(inCallServices -> {
                 for (IInCallService inCallService : inCallServices.values()) {
                     try {
                         inCallService.onMuteStateChanged(isMuted);
@@ -1596,9 +1761,11 @@
 
     @Override
     public void onCanAddCallChanged(boolean canAddCall) {
-        if (!mInCallServices.isEmpty()) {
+        Map<UserHandle, Map<InCallController.InCallServiceInfo, IInCallService>> serviceMap =
+                getCombinedInCallServiceMap();
+        if (!serviceMap.isEmpty()) {
             Log.i(this, "onCanAddCallChanged : %b", canAddCall);
-            mInCallServices.values().forEach(inCallServices -> {
+            serviceMap.values().forEach(inCallServices -> {
                 for (IInCallService inCallService : inCallServices.values()) {
                     try {
                         inCallService.onCanAddCallChanged(canAddCall);
@@ -1611,9 +1778,11 @@
 
     void onPostDialWait(Call call, String remaining) {
         UserHandle userFromCall = getUserFromCall(call);
-        if (mInCallServices.containsKey(userFromCall)) {
+        Map<UserHandle, Map<InCallController.InCallServiceInfo, IInCallService>> serviceMap =
+                getCombinedInCallServiceMap();
+        if (serviceMap.containsKey(userFromCall)) {
             Log.i(this, "Calling onPostDialWait, remaining = %s", remaining);
-            for (IInCallService inCallService : mInCallServices.get(userFromCall).values()) {
+            for (IInCallService inCallService: serviceMap.get(userFromCall).values()) {
                 try {
                     inCallService.setPostDialWait(mCallIdMapper.getCallId(call), remaining);
                 } catch (RemoteException ignored) {
@@ -1690,8 +1859,33 @@
 
     @VisibleForTesting
     public void bringToForeground(boolean showDialpad, UserHandle callingUser) {
-        if (mInCallServices.containsKey(callingUser)) {
-            for (IInCallService inCallService : mInCallServices.get(callingUser).values()) {
+        KeyguardManager keyguardManager = mContext.getSystemService(KeyguardManager.class);
+        boolean isLockscreenRestricted = keyguardManager != null
+                && keyguardManager.isKeyguardLocked();
+        UserHandle currentUser = mCallsManager.getCurrentUserHandle();
+        Map<UserHandle, Map<InCallController.InCallServiceInfo, IInCallService>> serviceMap =
+                getCombinedInCallServiceMap();
+        // Handle cases when calls are placed from the keyguard UI screen, which operates under
+        // the admin user. This needs to account for emergency calls placed from secondary/guest
+        // users as well as the work profile. Once the screen is locked, the user should be able to
+        // return to the call (from the keyguard UI).
+        if (mFeatureFlags.eccKeyguard() && mCallsManager.isInEmergencyCall()
+                && isLockscreenRestricted && !serviceMap.containsKey(callingUser)) {
+            // If screen is locked and the current user is the system, query calls for the work
+            // profile user, if available. Otherwise, the user is in the secondary/guest profile,
+            // so we can default to the system user.
+            if (currentUser.isSystem()) {
+                UserManager um = mContext.getSystemService(UserManager.class);
+                UserHandle workProfileUser = findChildManagedProfileUser(currentUser, um);
+                boolean hasWorkCalls = mCallsManager.getCalls().stream()
+                        .filter((c) -> getUserFromCall(c).equals(workProfileUser)).count() > 0;
+                callingUser = hasWorkCalls ? workProfileUser : currentUser;
+            } else {
+                callingUser = currentUser;
+            }
+        }
+        if (serviceMap.containsKey(callingUser)) {
+            for (IInCallService inCallService : serviceMap.get(callingUser).values()) {
                 try {
                     inCallService.bringToForeground(showDialpad);
                 } catch (RemoteException ignored) {
@@ -1704,7 +1898,7 @@
 
     @VisibleForTesting
     public Map<UserHandle, Map<InCallServiceInfo, IInCallService>> getInCallServices() {
-        return mInCallServices;
+        return getCombinedInCallServiceMap();
     }
 
     @VisibleForTesting
@@ -1713,9 +1907,11 @@
     }
 
     void silenceRinger(Set<UserHandle> userHandles) {
+        Map<UserHandle, Map<InCallController.InCallServiceInfo, IInCallService>> serviceMap =
+                getCombinedInCallServiceMap();
         userHandles.forEach(userHandle -> {
-            if (mInCallServices.containsKey(userHandle)) {
-                for (IInCallService inCallService : mInCallServices.get(userHandle).values()) {
+            if (serviceMap.containsKey(userHandle)) {
+                for (IInCallService inCallService : serviceMap.get(userHandle).values()) {
                     try {
                         inCallService.silenceRinger();
                     } catch (RemoteException ignored) {
@@ -1727,8 +1923,10 @@
 
     private void notifyConnectionEvent(Call call, String event, Bundle extras) {
         UserHandle userFromCall = getUserFromCall(call);
-        if (mInCallServices.containsKey(userFromCall)) {
-            for (IInCallService inCallService : mInCallServices.get(userFromCall).values()) {
+        Map<UserHandle, Map<InCallController.InCallServiceInfo, IInCallService>> serviceMap =
+                getCombinedInCallServiceMap();
+        if (serviceMap.containsKey(userFromCall)) {
+            for (IInCallService inCallService : serviceMap.get(userFromCall).values()) {
                 try {
                     Log.i(this, "notifyConnectionEvent {Call: %s, Event: %s, Extras:[%s]}",
                             (call != null ? call.toString() : "null"),
@@ -1743,8 +1941,10 @@
 
     private void notifyRttInitiationFailure(Call call, int reason) {
         UserHandle userFromCall = getUserFromCall(call);
-        if (mInCallServices.containsKey(userFromCall)) {
-            mInCallServices.get(userFromCall).entrySet().stream()
+        Map<UserHandle, Map<InCallController.InCallServiceInfo, IInCallService>> serviceMap =
+                getCombinedInCallServiceMap();
+        if (serviceMap.containsKey(userFromCall)) {
+            serviceMap.get(userFromCall).entrySet().stream()
                     .filter((entry) -> entry.getKey().equals(mInCallServiceConnections.
                             get(userFromCall).getInfo()))
                     .forEach((entry) -> {
@@ -1761,8 +1961,10 @@
 
     private void notifyRemoteRttRequest(Call call, int requestId) {
         UserHandle userFromCall = getUserFromCall(call);
-        if (mInCallServices.containsKey(userFromCall)) {
-            mInCallServices.get(userFromCall).entrySet().stream()
+        Map<UserHandle, Map<InCallController.InCallServiceInfo, IInCallService>> serviceMap =
+                getCombinedInCallServiceMap();
+        if (serviceMap.containsKey(userFromCall)) {
+            serviceMap.get(userFromCall).entrySet().stream()
                     .filter((entry) -> entry.getKey().equals(mInCallServiceConnections.
                             get(userFromCall).getInfo()))
                     .forEach((entry) -> {
@@ -1779,8 +1981,10 @@
 
     private void notifyHandoverFailed(Call call, int error) {
         UserHandle userFromCall = getUserFromCall(call);
-        if (mInCallServices.containsKey(userFromCall)) {
-            for (IInCallService inCallService : mInCallServices.get(userFromCall).values()) {
+        Map<UserHandle, Map<InCallController.InCallServiceInfo, IInCallService>> serviceMap =
+                getCombinedInCallServiceMap();
+        if (serviceMap.containsKey(userFromCall)) {
+            for (IInCallService inCallService : serviceMap.get(userFromCall).values()) {
                 try {
                     inCallService.onHandoverFailed(mCallIdMapper.getCallId(call), error);
                 } catch (RemoteException ignored) {
@@ -1791,8 +1995,10 @@
 
     private void notifyHandoverComplete(Call call) {
         UserHandle userFromCall = getUserFromCall(call);
-        if (mInCallServices.containsKey(userFromCall)) {
-            for (IInCallService inCallService : mInCallServices.get(userFromCall).values()) {
+        Map<UserHandle, Map<InCallController.InCallServiceInfo, IInCallService>> serviceMap =
+                getCombinedInCallServiceMap();
+        if (serviceMap.containsKey(userFromCall)) {
+            for (IInCallService inCallService : serviceMap.get(userFromCall).values()) {
                 try {
                     inCallService.onHandoverComplete(mCallIdMapper.getCallId(call));
                 } catch (RemoteException ignored) {
@@ -1805,6 +2011,7 @@
      * Unbinds an existing bound connection to the in-call app.
      */
     public void unbindFromServices(UserHandle userHandle) {
+        Log.i(this, "Unbinding from services for user %s", userHandle);
         try {
             mContext.unregisterReceiver(mPackageChangedReceiver);
         } catch (IllegalArgumentException e) {
@@ -1819,26 +2026,84 @@
             mNonUIInCallServiceConnections.get(userHandle).disconnect();
             mNonUIInCallServiceConnections.remove(userHandle);
         }
-        mInCallServices.remove(userHandle);
+        getCombinedInCallServiceMap().remove(userHandle);
+        if (mFeatureFlags.separatelyBindToBtIncallService()) {
+            // Note that the BT ICS will be repopulated as part of the combined map if the
+            // BT ICS is still bound (disconnected tone hasn't finished playing).
+            updateCombinedInCallServiceMap(userHandle);
+        }
+    }
+
+    /**
+     * Binds to Bluetooth InCallServices. Method-invoker must check
+     * {@link #isBoundAndConnectedToBTService(UserHandle)} before invoking.
+     *
+     * @param call The newly added call that triggered the binding to the in-call services.
+     */
+    public void bindToBTService(Call call, UserHandle userHandle) {
+        Log.i(this, "bindToBtService");
+        UserHandle userToBind = userHandle == null
+                ? getUserFromCall(call)
+                : userHandle;
+        UserManager um = mContext.getSystemService(UserManager.class);
+        UserHandle parentUser = mFeatureFlags.profileUserSupport()
+                ? um.getProfileParent(userToBind) : null;
+
+        if (!mFeatureFlags.profileUserSupport()
+                && um.isManagedProfile(userToBind.getIdentifier())) {
+            parentUser = um.getProfileParent(userToBind);
+        }
+
+        // Track the call if we don't already know about it.
+        addCall(call);
+        List<InCallServiceInfo> infos = getInCallServiceComponents(userToBind,
+                IN_CALL_SERVICE_TYPE_BLUETOOTH);
+        boolean serviceUnavailableForUser = false;
+        if (infos.size() == 0 || infos.get(0) == null) {
+            Log.i(this, "No available BT ICS for user (%s). Trying with parent instead.",
+                    userToBind);
+            serviceUnavailableForUser = true;
+            // Check if the service is available under the parent user instead.
+            if (parentUser != null) {
+                infos = getInCallServiceComponents(parentUser, IN_CALL_SERVICE_TYPE_BLUETOOTH);
+            }
+            if (infos.size() == 0 || infos.get(0) == null) {
+                Log.w(this, "No available BT ICS to bind to for user %s or its parent %s.",
+                        userToBind, parentUser);
+                mBtBindingFuture.put(userToBind, CompletableFuture.completedFuture(false));
+                return;
+            }
+        }
+
+        mBtBindingFuture.put(userToBind, new CompletableFuture<Boolean>().completeOnTimeout(false,
+                mTimeoutsAdapter.getCallBindBluetoothInCallServicesDelay(
+                        mContext.getContentResolver()), TimeUnit.MILLISECONDS));
+        InCallServiceBindingConnection btIcsBindingConnection =
+                new InCallServiceBindingConnection(infos.get(0),
+                        serviceUnavailableForUser ? parentUser : userToBind);
+        mBTInCallServiceConnections.put(userToBind, btIcsBindingConnection);
+        btIcsBindingConnection.connect(call);
     }
 
     /**
      * Binds to all the UI-providing InCallService as well as system-implemented non-UI
-     * InCallServices. Method-invoker must check {@link #isBoundAndConnectedToServices()}
+     * InCallServices. Method-invoker must check {@link #isBoundAndConnectedToServices(UserHandle)}
      * before invoking.
      *
-     * @param call The newly added call that triggered the binding to the in-call services.
+     * @param call           The newly added call that triggered the binding to the in-call
+     *                      services.
      */
     @VisibleForTesting
     public void bindToServices(Call call) {
         UserHandle userFromCall = getUserFromCall(call);
-        UserHandle parentUser = null;
         UserManager um = mContext.getSystemService(UserManager.class);
-
-        if (um.isManagedProfile(userFromCall.getIdentifier())) {
+        UserHandle parentUser = mFeatureFlags.profileUserSupport()
+                ? um.getProfileParent(userFromCall) : null;
+        if (!mFeatureFlags.profileUserSupport()
+                && um.isManagedProfile(userFromCall.getIdentifier())) {
             parentUser = um.getProfileParent(userFromCall);
-            Log.i(this, "child:%s  parent:%s", userFromCall, parentUser);
         }
+        Log.i(this, "child:%s  parent:%s", userFromCall, parentUser);
 
         if (!mInCallServiceConnections.containsKey(userFromCall)) {
             InCallServiceConnection dialerInCall = null;
@@ -1889,7 +2154,8 @@
 
         // Actually try binding to the UI InCallService.
         if (inCallServiceConnection.connect(call) ==
-                InCallServiceConnection.CONNECTION_SUCCEEDED || call.isSelfManaged()) {
+                InCallServiceConnection.CONNECTION_SUCCEEDED || (call != null
+                && call.isSelfManaged())) {
             // Only connect to the non-ui InCallServices if we actually connected to the main UI
             // one, or if the call is self-managed (in which case we'd still want to keep Wear, BT,
             // etc. informed.
@@ -1910,19 +2176,20 @@
 
     private void updateNonUiInCallServices(Call call) {
         UserHandle userFromCall = getUserFromCall(call);
-        UserHandle parentUser = null;
 
         UserManager um = mContext.getSystemService(UserManager.class);
-        if(um.isManagedProfile(userFromCall.getIdentifier()))
-        {
+        UserHandle parentUser = mFeatureFlags.profileUserSupport()
+                ? um.getProfileParent(userFromCall) : null;
+
+        if (!mFeatureFlags.profileUserSupport()
+                && um.isManagedProfile(userFromCall.getIdentifier())) {
             parentUser = um.getProfileParent(userFromCall);
         }
 
         List<InCallServiceInfo> nonUIInCallComponents =
                 getInCallServiceComponents(userFromCall, IN_CALL_SERVICE_TYPE_NON_UI);
         List<InCallServiceInfo> nonUIInCallComponentsForParent = new ArrayList<>();
-        if(parentUser != null)
-        {
+        if(parentUser != null) {
             //also get Non-UI services using parent handle.
             nonUIInCallComponentsForParent =
                     getInCallServiceComponents(parentUser, IN_CALL_SERVICE_TYPE_NON_UI);
@@ -2234,6 +2501,12 @@
             return IN_CALL_SERVICE_TYPE_DEFAULT_DIALER_UI;
         }
 
+        boolean processingBluetoothPackage = isBluetoothPackage(serviceInfo.packageName);
+        if (mFeatureFlags.separatelyBindToBtIncallService() && processingBluetoothPackage
+                && (hasControlInCallPermission || hasAppOpsPermittedManageOngoingCalls)) {
+            return IN_CALL_SERVICE_TYPE_BLUETOOTH;
+        }
+
         // Also allow any in-call service that has the control-experience permission (to ensure
         // that it is a system app) and doesn't claim to show any UI.
         if (!isUIService && !isCarModeUIService && (hasControlInCallPermission ||
@@ -2274,9 +2547,26 @@
             trackCallingUserInterfaceStarted(info);
         }
         IInCallService inCallService = IInCallService.Stub.asInterface(service);
-        mInCallServices.putIfAbsent(userHandle,
-                new ArrayMap<InCallController.InCallServiceInfo, IInCallService>());
-        mInCallServices.get(userHandle).put(info, inCallService);
+        if (mFeatureFlags.separatelyBindToBtIncallService()
+                && info.getType() == IN_CALL_SERVICE_TYPE_BLUETOOTH) {
+            if (!mBtBindingFuture.containsKey(userHandle)
+                    || mBtBindingFuture.get(userHandle).isDone()) {
+                Log.i(this, "onConnected: BT binding future timed out.");
+                // Binding completed after the timeout. Clean up this binding
+                return false;
+            } else {
+                mBtBindingFuture.get(userHandle).complete(true);
+            }
+            mBTInCallServices.put(userHandle, new Pair<>(info, inCallService));
+        } else {
+            mInCallServices.putIfAbsent(userHandle, new ArrayMap<>());
+            mInCallServices.get(userHandle).put(info, inCallService);
+        }
+
+        if (mFeatureFlags.separatelyBindToBtIncallService()) {
+            updateCombinedInCallServiceMap(userHandle);
+        }
+
         try {
             inCallService.setInCallAdapter(
                     new InCallAdapter(
@@ -2293,7 +2583,9 @@
         }
 
         // Upon successful connection, send the state of the world to the service.
-        List<Call> calls = orderCallsWithChildrenFirst(mCallsManager.getCalls());
+        List<Call> calls = orderCallsWithChildrenFirst(mCallsManager.getCalls().stream().filter(
+                call -> getUserFromCall(call).equals(userHandle))
+                .collect(Collectors.toUnmodifiableList()));
         Log.i(this, "Adding %s calls to InCallService after onConnected: %s, including external " +
                 "calls", calls.size(), info.getComponentName());
         int numCallsSent = 0;
@@ -2363,6 +2655,11 @@
         if (mInCallServices.containsKey(userHandle)) {
             mInCallServices.get(userHandle).remove(disconnectedInfo);
         }
+        if (mFeatureFlags.separatelyBindToBtIncallService()
+                && disconnectedInfo.getType() == IN_CALL_SERVICE_TYPE_BLUETOOTH) {
+            mBTInCallServices.remove(userHandle);
+            updateCombinedInCallServiceMap(userHandle);
+        }
     }
 
     /**
@@ -2383,17 +2680,19 @@
      * @param rttInfoChanged       {@code true} if any information about the RTT session changed,
      *                             {@code false} otherwise.
      * @param exceptPackageName    When specified, this package name will not get a call update.
-     *                             Used ONLY from {@link Call#putConnectionServiceExtras(int, Bundle, String)} to
+     *                             Used ONLY from {@link Call#putConnectionServiceExtras(Bundle)} to
      *                             ensure we can propagate extras changes between InCallServices but
      *                             not inform the requestor of their own change.
      */
     private void updateCall(Call call, boolean videoProviderChanged, boolean rttInfoChanged,
             String exceptPackageName) {
         UserHandle userFromCall = getUserFromCall(call);
-        if (mInCallServices.containsKey(userFromCall)) {
+        Map<UserHandle, Map<InCallController.InCallServiceInfo, IInCallService>> serviceMap =
+                getCombinedInCallServiceMap();
+        if (serviceMap.containsKey(userFromCall)) {
             Log.i(this, "Sending updateCall %s", call);
             List<ComponentName> componentsUpdated = new ArrayList<>();
-            for (Map.Entry<InCallServiceInfo, IInCallService> entry : mInCallServices.
+            for (Map.Entry<InCallServiceInfo, IInCallService> entry : serviceMap.
                     get(userFromCall).entrySet()) {
                 InCallServiceInfo info = entry.getKey();
                 ComponentName componentName = info.getComponentName();
@@ -2427,13 +2726,37 @@
                 IInCallService inCallService = entry.getValue();
                 componentsUpdated.add(componentName);
 
-                try {
-                    inCallService.updateCall(
-                            sanitizeParcelableCallForService(info, parcelableCall));
-                } catch (RemoteException ignored) {
+                if (info.getType() == IN_CALL_SERVICE_TYPE_BLUETOOTH
+                        && call.getState() == CallState.DISCONNECTED
+                        && !mDisconnectedToneBtFutures.containsKey(call.getId())) {
+                    CompletableFuture<Void> disconnectedToneFuture = new CompletableFuture<Void>()
+                            .completeOnTimeout(null, DISCONNECTED_TONE_TIMEOUT,
+                                    TimeUnit.MILLISECONDS);
+                    mDisconnectedToneBtFutures.put(call.getId(), disconnectedToneFuture);
+                    mDisconnectedToneBtFutures.get(call.getId()).thenRunAsync(() -> {
+                        Log.i(this, "updateCall: Sending call disconnected update to BT ICS.");
+                        updateCallToIcs(inCallService, info, parcelableCall, componentName);
+                        mDisconnectedToneBtFutures.remove(call.getId());
+                    }, new LoggedHandlerExecutor(mHandler, "ICC.uC", mLock));
+                } else {
+                    updateCallToIcs(inCallService, info, parcelableCall, componentName);
                 }
             }
             Log.i(this, "Components updated: %s", componentsUpdated);
+        } else {
+            Log.i(this,
+                    "Unable to update call. InCallService not found for user: %s", userFromCall);
+        }
+    }
+
+    private void updateCallToIcs(IInCallService inCallService, InCallServiceInfo info,
+            ParcelableCall parcelableCall, ComponentName componentName) {
+        try {
+            inCallService.updateCall(
+                    sanitizeParcelableCallForService(info, parcelableCall));
+        } catch (RemoteException exception) {
+            Log.w(this, "Call status update did not send to: "
+                    + componentName + " successfully with error " + exception);
         }
     }
 
@@ -2443,6 +2766,10 @@
      */
     @VisibleForTesting
     public void addCall(Call call) {
+        if (call == null) {
+            return;
+        }
+
         if (mCallIdMapper.getCalls().size() == 0) {
             mAppOpsManager.startWatchingActive(new String[] { OPSTR_RECORD_AUDIO },
                     java.lang.Runnable::run, this);
@@ -2453,6 +2780,9 @@
         if (mCallIdMapper.getCallId(call) == null) {
             mCallIdMapper.addCall(call);
             call.addListener(mCallListener);
+            if (mFeatureFlags.separatelyBindToBtIncallService()) {
+                mPendingEndToneCall.add(call);
+            }
         }
 
         maybeTrackMicrophoneUse(isMuted());
@@ -2468,6 +2798,14 @@
         return mInCallServiceConnections.get(userHandle).isConnected();
     }
 
+    @VisibleForTesting
+    public boolean isBoundAndConnectedToBTService(UserHandle userHandle) {
+        if (!mBTInCallServiceConnections.containsKey(userHandle)) {
+            return false;
+        }
+        return mBTInCallServiceConnections.get(userHandle).isConnected();
+    }
+
     /**
      * @return A future that is pending whenever we are in the middle of binding to an
      *         incall service.
@@ -2477,14 +2815,33 @@
     }
 
     /**
+     * @return A future that is pending whenever we are in the middle of binding to the BT
+     *         incall service.
+     */
+    public CompletableFuture<Boolean> getBtBindingFuture(Call call) {
+        UserHandle userHandle = getUserFromCall(call);
+        return mBtBindingFuture.get(userHandle);
+    }
+
+    /**
+     * @return A future that is pending whenever we are in the process of sending the call
+     *         disconnected state to the BT ICS so that the disconnect tone can finish playing.
+     */
+    public Map<String, CompletableFuture<Void>> getDisconnectedToneBtFutures() {
+        return mDisconnectedToneBtFutures;
+    }
+
+    /**
      * Dumps the state of the {@link InCallController}.
      *
      * @param pw The {@code IndentingPrintWriter} to write the state to.
      */
     public void dump(IndentingPrintWriter pw) {
-        pw.println("mInCallServices (InCalls registered):");
+        pw.println("combinedInCallServiceMap (InCalls registered):");
         pw.increaseIndent();
-        mInCallServices.values().forEach(inCallServices -> {
+        Map<UserHandle, Map<InCallController.InCallServiceInfo, IInCallService>> serviceMap =
+                getCombinedInCallServiceMap();
+        serviceMap.values().forEach(inCallServices -> {
             for (InCallServiceInfo info : inCallServices.keySet()) {
                 pw.println(info);
             }
@@ -2779,19 +3136,38 @@
         mIsCallUsingMicrophone = mIsTrackingManagedAliveCall && !isMuted
                 && !isCarrierPrivilegedUsingMicDuringVoipCall();
         if (wasUsingMicrophone != mIsCallUsingMicrophone) {
+            int opPackageUid = getOpPackageUid();
             if (mIsCallUsingMicrophone) {
                 // Note, not checking return value, as this op call is merely for tracing use
-                mAppOpsManager.startOp(AppOpsManager.OP_PHONE_CALL_MICROPHONE, myUid(),
+                mAppOpsManager.startOp(AppOpsManager.OP_PHONE_CALL_MICROPHONE, opPackageUid,
                         mContext.getOpPackageName(), false, null, null);
                 mSensorPrivacyManager.showSensorUseDialog(SensorPrivacyManager.Sensors.MICROPHONE);
             } else {
-                mAppOpsManager.finishOp(AppOpsManager.OP_PHONE_CALL_MICROPHONE, myUid(),
+                mAppOpsManager.finishOp(AppOpsManager.OP_PHONE_CALL_MICROPHONE, opPackageUid,
                         mContext.getOpPackageName(), null);
             }
         }
     }
 
     /**
+     * Returns the uid of the package in the current user to be used for app ops attribution.
+     */
+    private int getOpPackageUid() {
+        UserHandle user = mCallsManager.getCurrentUserHandle();
+
+        try {
+            PackageManager pkgManager = mContext.getPackageManager();
+            return pkgManager.getPackageUidAsUser(mContext.getOpPackageName(),
+                    user.getIdentifier());
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.e(this, e, "getPackageForAssociatedUser: could not find package %s"
+                    + " for user %s", mContext.getOpPackageName(), user);
+            // fallback to current process id - this should not happen
+            return myUid();
+        }
+    }
+
+    /**
      * @return {@code true} if InCallController is tracking a managed call (i.e. not self managed
      * and not external) that is active.
      */
@@ -2871,18 +3247,91 @@
             return mCallsManager.getCurrentUserHandle();
         } else {
             UserHandle userFromCall = call.getAssociatedUser();
-            UserManager userManager = mContext.getSystemService(UserManager.class);
-            // Emergency call should never be blocked, so if the user associated with call is in
-            // quite mode, use the primary user for the emergency call.
+            UserManager userManager = mFeatureFlags.telecomResolveHiddenDependencies()
+                    ? mContext.createContextAsUser(mCallsManager.getCurrentUserHandle(), 0)
+                            .getSystemService(UserManager.class)
+                    : mContext.getSystemService(UserManager.class);
+            boolean isCurrentUserAdmin = mFeatureFlags.telecomResolveHiddenDependencies()
+                    ? userManager.isAdminUser()
+                    : userManager.isUserAdmin(mCallsManager.getCurrentUserHandle().getIdentifier());
+            // Emergency call should never be blocked, so if the user associated with the target
+            // phone account handle user is in quiet mode, use the current user for the ecall.
+            // Note, that this only applies to incoming calls that are received on assigned
+            // sims (i.e. work sim), where the associated user would be the target phone account
+            // handle user.
             if ((call.isEmergencyCall() || call.isInECBM())
                     && (userManager.isQuietModeEnabled(userFromCall)
                     // We should also account for secondary/guest users where the profile may not
                     // necessarily be paused.
-                    || !userManager.isUserAdmin(mCallsManager.getCurrentUserHandle()
-                    .getIdentifier()))) {
+                    || !isCurrentUserAdmin)) {
                 return mCallsManager.getCurrentUserHandle();
             }
             return userFromCall;
         }
     }
+
+    /**
+     * Useful for debugging purposes and called on the command line via
+     * an "adb shell telecom command".
+     *
+     * @return true if a particular non-ui InCallService package is bound in a call.
+     */
+    public boolean isNonUiInCallServiceBound(String packageName) {
+        for (NonUIInCallServiceConnectionCollection ics : mNonUIInCallServiceConnections.values()) {
+            for (InCallServiceBindingConnection connection : ics.getSubConnections()) {
+                InCallServiceInfo serviceInfo = connection.mInCallServiceInfo;
+                Log.i(this, "isNonUiInCallServiceBound: found serviceInfo=[%s]", serviceInfo);
+                if (serviceInfo != null &&
+                        serviceInfo.mComponentName.getPackageName().contains(packageName)) {
+                    Log.i(this, "isNonUiInCallServiceBound: found target package");
+                    return true;
+                }
+            }
+        }
+        // If early binding for BT ICS is enabled, ensure that it is included into consideration as
+        // a bound non-UI ICS.
+        return mFeatureFlags.separatelyBindToBtIncallService() && !mBTInCallServices.isEmpty()
+                && isBluetoothPackage(packageName);
+    }
+
+    private void updateCombinedInCallServiceMap(UserHandle user) {
+        synchronized (mLock) {
+            Map<InCallServiceInfo, IInCallService> serviceMap;
+            if (mInCallServices.containsKey(user)) {
+                serviceMap = mInCallServices.get(user);
+            } else {
+                serviceMap = new HashMap<>();
+            }
+            if (mFeatureFlags.separatelyBindToBtIncallService()
+                    && mBTInCallServices.containsKey(user)) {
+                Pair<InCallServiceInfo, IInCallService> btServicePair = mBTInCallServices.get(user);
+                serviceMap.put(btServicePair.first, btServicePair.second);
+            }
+            if (!serviceMap.isEmpty()) {
+                mCombinedInCallServiceMap.put(user, serviceMap);
+            } else {
+                mCombinedInCallServiceMap.remove(user);
+            }
+        }
+    }
+
+    private Map<UserHandle,
+            Map<InCallController.InCallServiceInfo, IInCallService>> getCombinedInCallServiceMap() {
+        synchronized (mLock) {
+            if (mFeatureFlags.separatelyBindToBtIncallService()) {
+                return mCombinedInCallServiceMap;
+            } else {
+                return mInCallServices;
+            }
+        }
+    }
+
+    private boolean isBluetoothPackage(String packageName) {
+        for (String pkgName : mDefaultDialerCache.getBTInCallServicePackages()) {
+            if (pkgName.equals(packageName)) {
+                return true;
+            }
+        }
+        return false;
+    }
 }
diff --git a/src/com/android/server/telecom/InCallTonePlayer.java b/src/com/android/server/telecom/InCallTonePlayer.java
index 3cc4aac..a5942f0 100644
--- a/src/com/android/server/telecom/InCallTonePlayer.java
+++ b/src/com/android/server/telecom/InCallTonePlayer.java
@@ -69,8 +69,8 @@
             mCallAudioManager = callAudioManager;
         }
 
-        public InCallTonePlayer createPlayer(int tone) {
-            return new InCallTonePlayer(tone, mCallAudioManager,
+        public InCallTonePlayer createPlayer(Call call, int tone) {
+            return new InCallTonePlayer(call, tone, mCallAudioManager,
                     mCallAudioRoutePeripheralAdapter, mLock, mToneGeneratorFactory,
                     mMediaPlayerFactory, mAudioManagerAdapter);
         }
@@ -212,6 +212,7 @@
     private Session mSession;
     private final Object mSessionLock = new Object();
 
+    private final Call mCall;
     private final ToneGeneratorFactory mToneGenerator;
     private final MediaPlayerFactory mMediaPlayerFactory;
     private final AudioManagerAdapter mAudioManagerAdapter;
@@ -228,6 +229,7 @@
      * @param toneId ID of the tone to play, see TONE_* constants.
      */
     private InCallTonePlayer(
+            Call call,
             int toneId,
             CallAudioManager callAudioManager,
             CallAudioRoutePeripheralAdapter callAudioRoutePeripheralAdapter,
@@ -235,6 +237,7 @@
             ToneGeneratorFactory toneGeneratorFactory,
             MediaPlayerFactory mediaPlayerFactor,
             AudioManagerAdapter audioManagerAdapter) {
+        mCall = call;
         mState = STATE_OFF;
         mToneId = toneId;
         mCallAudioManager = callAudioManager;
@@ -476,7 +479,7 @@
         }
 
         if (sTonesPlaying.incrementAndGet() == 1) {
-            mCallAudioManager.setIsTonePlaying(true);
+            mCallAudioManager.setIsTonePlaying(mCall, true);
         }
 
         synchronized (mSessionLock) {
@@ -524,7 +527,7 @@
                     Log.i(InCallTonePlayer.this,
                             "cleanUpTonePlayer(): tonesPlaying=%d, tone completed", newToneCount);
                     if (mCallAudioManager != null) {
-                        mCallAudioManager.setIsTonePlaying(false);
+                        mCallAudioManager.setIsTonePlaying(mCall, false);
                     } else {
                         Log.w(InCallTonePlayer.this,
                                 "cleanUpTonePlayer(): mCallAudioManager is null!");
diff --git a/src/com/android/server/telecom/LogUtils.java b/src/com/android/server/telecom/LogUtils.java
index 0d6acd5..d98ebfe 100644
--- a/src/com/android/server/telecom/LogUtils.java
+++ b/src/com/android/server/telecom/LogUtils.java
@@ -139,8 +139,10 @@
         public static final String STOP_CALL_WAITING_TONE = "STOP_CALL_WAITING_TONE";
         public static final String START_CONNECTION = "START_CONNECTION";
         public static final String CREATE_CONNECTION_FAILED = "CREATE_CONNECTION_FAILED";
+        public static final String CREATE_CONNECTION_TIMEOUT = "CREATE_CONNECTION_TIMEOUT";
         public static final String START_CONFERENCE = "START_CONFERENCE";
         public static final String CREATE_CONFERENCE_FAILED = "CREATE_CONFERENCE_FAILED";
+        public static final String CREATE_CONFERENCE_TIMEOUT = "CREATE_CONFERENCE_TIMEOUT";
         public static final String BIND_CS = "BIND_CS";
         public static final String CS_BOUND = "CS_BOUND";
         public static final String CONFERENCE_WITH = "CONF_WITH";
diff --git a/src/com/android/server/telecom/MissedCallNotifier.java b/src/com/android/server/telecom/MissedCallNotifier.java
index 0e5a287..b0a7c8e 100644
--- a/src/com/android/server/telecom/MissedCallNotifier.java
+++ b/src/com/android/server/telecom/MissedCallNotifier.java
@@ -16,6 +16,7 @@
 
 package com.android.server.telecom;
 
+import android.annotation.Nullable;
 import android.net.Uri;
 import android.os.UserHandle;
 import android.telecom.PhoneAccountHandle;
@@ -85,7 +86,7 @@
 
     void clearMissedCalls(UserHandle userHandle);
 
-    void showMissedCallNotification(CallInfo call);
+    void showMissedCallNotification(CallInfo call, @Nullable Uri uri);
 
     void reloadAfterBootComplete(CallerInfoLookupHelper callerInfoLookupHelper,
             CallInfoFactory callInfoFactory);
diff --git a/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java b/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java
index 3b402b1..c24ac97 100644
--- a/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java
+++ b/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java
@@ -16,6 +16,7 @@
 
 package com.android.server.telecom;
 
+import android.Manifest;
 import android.app.Activity;
 import android.app.AppOpsManager;
 import android.app.BroadcastOptions;
@@ -37,6 +38,7 @@
 import android.text.TextUtils;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.telecom.flags.FeatureFlags;
 import com.android.server.telecom.callredirection.CallRedirectionProcessor;
 
 // TODO: Needed for move to system service: import com.android.internal.R;
@@ -77,6 +79,7 @@
     private final TelecomSystem.SyncRoot mLock;
     private final DefaultDialerCache mDefaultDialerCache;
     private final MmiUtils mMmiUtils;
+    private final FeatureFlags mFeatureFlags;
 
     /*
      * Whether or not the outgoing call intent originated from the default phone application. If
@@ -100,7 +103,8 @@
     @VisibleForTesting
     public NewOutgoingCallIntentBroadcaster(Context context, CallsManager callsManager,
             Intent intent, PhoneNumberUtilsAdapter phoneNumberUtilsAdapter,
-            boolean isDefaultPhoneApp, DefaultDialerCache defaultDialerCache, MmiUtils mmiUtils) {
+            boolean isDefaultPhoneApp, DefaultDialerCache defaultDialerCache, MmiUtils mmiUtils,
+            FeatureFlags featureFlags) {
         mContext = context;
         mCallsManager = callsManager;
         mIntent = intent;
@@ -109,6 +113,7 @@
         mLock = mCallsManager.getLock();
         mDefaultDialerCache = defaultDialerCache;
         mMmiUtils = mmiUtils;
+        mFeatureFlags = featureFlags;
     }
 
     /**
@@ -128,7 +133,8 @@
                     // Once the NEW_OUTGOING_CALL broadcast is finished, the resultData is
                     // used as the actual number to call. (If null, no call will be placed.)
                     String resultNumber = getResultData();
-                    Log.i(this, "Received new-outgoing-call-broadcast for %s with data %s", mCall,
+                    Log.i(NewOutgoingCallIntentBroadcaster.this,
+                            "Received new-outgoing-call-broadcast for %s with data %s", mCall,
                             Log.pii(resultNumber));
 
                     boolean endEarly = false;
@@ -320,6 +326,7 @@
         String scheme = mPhoneNumberUtilsAdapter.isUriNumber(number)
                 ? PhoneAccount.SCHEME_SIP : PhoneAccount.SCHEME_TEL;
         result.callingAddress = Uri.fromParts(scheme, number, null);
+
         return result;
     }
 
@@ -351,14 +358,57 @@
 
     public void processCall(Call call, CallDisposition disposition) {
         mCall = call;
+
+        // If the new outgoing call broadast doesn't block, trigger the legacy process call
+        // behavior and exit out here.
+        if (!mFeatureFlags.isNewOutgoingCallBroadcastUnblocking()) {
+            legacyProcessCall(disposition);
+            return;
+        }
+        boolean callRedirectionWithService = false;
+        // Only try to do redirection if it was requested and we're not calling immediately.
+        // We can expect callImmediately to be true for emergency calls and voip calls.
+        if (disposition.requestRedirection && !disposition.callImmediately) {
+            CallRedirectionProcessor callRedirectionProcessor = new CallRedirectionProcessor(
+                    mContext, mCallsManager, mCall, disposition.callingAddress,
+                    mCallsManager.getPhoneAccountRegistrar(),
+                    getGateWayInfoFromIntent(mIntent, mIntent.getData()),
+                    mIntent.getBooleanExtra(TelecomManager.EXTRA_START_CALL_WITH_SPEAKERPHONE,
+                            false),
+                    mIntent.getIntExtra(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
+                            VideoProfile.STATE_AUDIO_ONLY));
+            /**
+             * If there is an available {@link android.telecom.CallRedirectionService}, use the
+             * {@link CallRedirectionProcessor} to perform call redirection instead of using
+             * broadcasting.
+             */
+            callRedirectionWithService = callRedirectionProcessor
+                    .canMakeCallRedirectionWithServiceAsUser(mCall.getAssociatedUser());
+            if (callRedirectionWithService) {
+                callRedirectionProcessor.performCallRedirection(mCall.getAssociatedUser());
+            }
+        }
+
+        // If no redirection was kicked off, place the call now.
+        if (!callRedirectionWithService) {
+            callImmediately(disposition);
+        }
+
+        // Finally, send the non-blocking broadcast if we're supposed to (ie for any non-voip call).
+        if (disposition.sendBroadcast) {
+            UserHandle targetUser = mCall.getAssociatedUser();
+            broadcastIntent(mIntent, disposition.number, false /* receiverRequired */, targetUser);
+        }
+    }
+
+    /**
+     * The legacy non-flagged version of processing a call.  Although there is some code duplication
+     * if makes the new flow cleaner to read.
+     * @param disposition
+     */
+    private void legacyProcessCall(CallDisposition disposition) {
         if (disposition.callImmediately) {
-            boolean speakerphoneOn = mIntent.getBooleanExtra(
-                    TelecomManager.EXTRA_START_CALL_WITH_SPEAKERPHONE, false);
-            int videoState = mIntent.getIntExtra(
-                    TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
-                    VideoProfile.STATE_AUDIO_ONLY);
-            placeOutgoingCallImmediately(mCall, disposition.callingAddress, null,
-                    speakerphoneOn, videoState);
+            callImmediately(disposition);
 
             // Don't return but instead continue and send the ACTION_NEW_OUTGOING_CALL broadcast
             // so that third parties can still inspect (but not intercept) the outgoing call. When
@@ -390,13 +440,26 @@
 
         if (disposition.sendBroadcast) {
             UserHandle targetUser = mCall.getAssociatedUser();
-            Log.i(this, "Sending NewOutgoingCallBroadcast for %s to %s", mCall, targetUser);
             broadcastIntent(mIntent, disposition.number,
                     !disposition.callImmediately && !callRedirectionWithService, targetUser);
         }
     }
 
     /**
+     * Place a call immediately.
+     * @param disposition The disposition; used for retrieving the address of the call.
+     */
+    private void callImmediately(CallDisposition disposition) {
+        boolean speakerphoneOn = mIntent.getBooleanExtra(
+                TelecomManager.EXTRA_START_CALL_WITH_SPEAKERPHONE, false);
+        int videoState = mIntent.getIntExtra(
+                TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
+                VideoProfile.STATE_AUDIO_ONLY);
+        placeOutgoingCallImmediately(mCall, disposition.callingAddress, null,
+                speakerphoneOn, videoState);
+    }
+
+    /**
      * Sends a new outgoing call ordered broadcast so that third party apps can cancel the
      * placement of the call or redirect it to a different number.
      *
@@ -415,28 +478,51 @@
         if (number != null) {
             broadcastIntent.putExtra(Intent.EXTRA_PHONE_NUMBER, number);
         }
-
-        // Force receivers of this broadcast intent to run at foreground priority because we
-        // want to finish processing the broadcast intent as soon as possible.
-        broadcastIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND
-                | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
         Log.v(this, "Broadcasting intent: %s.", broadcastIntent);
 
         checkAndCopyProviderExtras(originalCallIntent, broadcastIntent);
 
-        final BroadcastOptions options = BroadcastOptions.makeBasic();
-        options.setBackgroundActivityStartsAllowed(true);
-        mContext.sendOrderedBroadcastAsUser(
-                broadcastIntent,
-                targetUser,
-                android.Manifest.permission.PROCESS_OUTGOING_CALLS,
-                AppOpsManager.OP_PROCESS_OUTGOING_CALLS,
-                options.toBundle(),
-                receiverRequired ? new NewOutgoingCallBroadcastIntentReceiver() : null,
-                null,  // scheduler
-                Activity.RESULT_OK,  // initialCode
-                number,  // initialData: initial value for the result data (number to be modified)
-                null);  // initialExtras
+        if (mFeatureFlags.isNewOutgoingCallBroadcastUnblocking()) {
+            // Where the new outgoing call broadcast is unblocking, do not give receiver FG priority
+            // and do not allow background activity starts.
+            broadcastIntent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
+            Log.i(this, "broadcastIntent: Sending non-blocking for %s to %s", mCall.getId(),
+                    targetUser);
+            if (mFeatureFlags.telecomResolveHiddenDependencies()) {
+                mContext.sendBroadcastAsUser(
+                        broadcastIntent,
+                        targetUser,
+                        Manifest.permission.PROCESS_OUTGOING_CALLS);
+            } else {
+                mContext.sendBroadcastAsUser(
+                        broadcastIntent,
+                        targetUser,
+                        android.Manifest.permission.PROCESS_OUTGOING_CALLS,
+                        AppOpsManager.OP_PROCESS_OUTGOING_CALLS);  // initialExtras
+            }
+        } else {
+            Log.i(this, "broadcastIntent: Sending ordered for %s to %s, waitForResult=%b",
+                    mCall.getId(), targetUser, receiverRequired);
+            final BroadcastOptions options = BroadcastOptions.makeBasic();
+            options.setBackgroundActivityStartsAllowed(true);
+            // Force receivers of this broadcast intent to run at foreground priority because we
+            // want to finish processing the broadcast intent as soon as possible.
+            broadcastIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND
+                    | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
+
+            mContext.sendOrderedBroadcastAsUser(
+                    broadcastIntent,
+                    targetUser,
+                    android.Manifest.permission.PROCESS_OUTGOING_CALLS,
+                    AppOpsManager.OP_PROCESS_OUTGOING_CALLS,
+                    options.toBundle(),
+                    receiverRequired ? new NewOutgoingCallBroadcastIntentReceiver() : null,
+                    null,  // scheduler
+                    Activity.RESULT_OK,  // initialCode
+                    number,  // initialData: initial value for the result data (number to be
+                             // modified)
+                    null);  // initialExtras
+        }
     }
 
     /**
@@ -537,6 +623,9 @@
         try {
             return mContext.getSystemService(TelephonyManager.class).isEmergencyNumber(
                     number);
+        } catch (UnsupportedOperationException uoe) {
+            Log.w(this, "isEmergencyNumber: Telephony not supported");
+            return false;
         } catch (Exception e) {
             Log.e(this, e, "isEmergencyNumber: Telephony threw an exception.");
             return false;
diff --git a/src/com/android/server/telecom/ParcelableCallUtils.java b/src/com/android/server/telecom/ParcelableCallUtils.java
index 673b99a..3573de8 100644
--- a/src/com/android/server/telecom/ParcelableCallUtils.java
+++ b/src/com/android/server/telecom/ParcelableCallUtils.java
@@ -63,6 +63,7 @@
         RESTRICTED_CALL_SCREENING_EXTRA_KEYS = new ArrayList<>();
         RESTRICTED_CALL_SCREENING_EXTRA_KEYS.add(android.telecom.Connection.EXTRA_SIP_INVITE);
         RESTRICTED_CALL_SCREENING_EXTRA_KEYS.add(ImsCallProfile.EXTRA_IS_BUSINESS_CALL);
+        RESTRICTED_CALL_SCREENING_EXTRA_KEYS.add(ImsCallProfile.EXTRA_ASSERTED_DISPLAY_NAME);
     }
 
     public static class Converter {
@@ -158,6 +159,10 @@
             properties |= android.telecom.Call.Details.PROPERTY_VOIP_AUDIO_MODE;
         }
 
+        if (call.isTransactionalCall()) {
+            properties |= android.telecom.Call.Details.PROPERTY_IS_TRANSACTIONAL;
+        }
+
         // If this is a single-SIM device, the "default SIM" will always be the only SIM.
         boolean isDefaultSmsAccount = phoneAccountRegistrar != null &&
                 phoneAccountRegistrar.isUserSelectedSmsPhoneAccount(call.getTargetPhoneAccount());
diff --git a/src/com/android/server/telecom/PendingAudioRoute.java b/src/com/android/server/telecom/PendingAudioRoute.java
new file mode 100644
index 0000000..396aca0
--- /dev/null
+++ b/src/com/android/server/telecom/PendingAudioRoute.java
@@ -0,0 +1,142 @@
+/*
+ * 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 static com.android.server.telecom.CallAudioRouteAdapter.SWITCH_BASELINE_ROUTE;
+import static com.android.server.telecom.CallAudioRouteController.INCLUDE_BLUETOOTH_IN_BASELINE;
+
+import android.bluetooth.BluetoothDevice;
+import android.media.AudioManager;
+import android.telecom.Log;
+import android.util.ArraySet;
+import android.util.Pair;
+
+import com.android.server.telecom.bluetooth.BluetoothRouteManager;
+
+import java.util.Set;
+
+/**
+ * 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;
+    private BluetoothRouteManager mBluetoothRouteManager;
+    /**
+     * 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 Set<Pair<Integer, String>> mPendingMessages;
+    private boolean mActive;
+    /**
+     * The device that has been set for communication by Telecom
+     */
+    private @AudioRoute.AudioRouteType int mCommunicationDeviceType = AudioRoute.TYPE_INVALID;
+
+    PendingAudioRoute(CallAudioRouteController controller, AudioManager audioManager,
+            BluetoothRouteManager bluetoothRouteManager) {
+        mCallAudioRouteController = controller;
+        mAudioManager = audioManager;
+        mBluetoothRouteManager = bluetoothRouteManager;
+        mPendingMessages = new ArraySet<>();
+        mActive = false;
+        mCommunicationDeviceType = AudioRoute.TYPE_INVALID;
+    }
+
+    void setOrigRoute(boolean active, AudioRoute origRoute) {
+        origRoute.onOrigRouteAsPendingRoute(active, this, mAudioManager, mBluetoothRouteManager);
+        mOrigRoute = origRoute;
+    }
+
+    AudioRoute getOrigRoute() {
+        return mOrigRoute;
+    }
+
+    void setDestRoute(boolean active, AudioRoute destRoute, BluetoothDevice device,
+            boolean isScoAudioConnected) {
+        destRoute.onDestRouteAsPendingRoute(active, this, device,
+                mAudioManager, mBluetoothRouteManager, isScoAudioConnected);
+        mActive = active;
+        mDestRoute = destRoute;
+    }
+
+    public AudioRoute getDestRoute() {
+        return mDestRoute;
+    }
+
+    public void addMessage(int message, String bluetoothDevice) {
+        mPendingMessages.add(new Pair<>(message, bluetoothDevice));
+    }
+
+    public void onMessageReceived(Pair<Integer, String> message, String btAddressToExclude) {
+        Log.i(this, "onMessageReceived: message - %s", message);
+        if (message.first == PENDING_ROUTE_FAILED) {
+            // Fallback to base route
+            mCallAudioRouteController.sendMessageWithSessionInfo(
+                    SWITCH_BASELINE_ROUTE, INCLUDE_BLUETOOTH_IN_BASELINE, btAddressToExclude);
+            return;
+        }
+
+        // Removes the first occurrence of the specified message from this list, if it is present.
+        mPendingMessages.remove(message);
+        evaluatePendingState();
+    }
+
+    public void evaluatePendingState() {
+        if (mPendingMessages.isEmpty()) {
+            mCallAudioRouteController.sendMessageWithSessionInfo(
+                    CallAudioRouteAdapter.EXIT_PENDING_ROUTE);
+        } else {
+            Log.i(this, "evaluatePendingState: mPendingMessages - %s", mPendingMessages);
+        }
+    }
+
+    public void clearPendingMessages() {
+        mPendingMessages.clear();
+    }
+
+    public void clearPendingMessage(Pair<Integer, String> message) {
+        mPendingMessages.remove(message);
+    }
+
+    public boolean isActive() {
+        return mActive;
+    }
+
+    public @AudioRoute.AudioRouteType int getCommunicationDeviceType() {
+        return mCommunicationDeviceType;
+    }
+
+    public void setCommunicationDeviceType(
+            @AudioRoute.AudioRouteType int communicationDeviceType) {
+        mCommunicationDeviceType = communicationDeviceType;
+    }
+
+    public void overrideDestRoute(AudioRoute route) {
+        mDestRoute = route;
+    }
+}
diff --git a/src/com/android/server/telecom/PhoneAccountRegistrar.java b/src/com/android/server/telecom/PhoneAccountRegistrar.java
index acf07e3..f0423c3 100644
--- a/src/com/android/server/telecom/PhoneAccountRegistrar.java
+++ b/src/com/android/server/telecom/PhoneAccountRegistrar.java
@@ -58,9 +58,11 @@
 
 // TODO: Needed for move to system service: import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.flags.FeatureFlags;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.util.XmlUtils;
 import com.android.modules.utils.ModifiedUtf8;
+import com.android.server.telecom.flags.Flags;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
@@ -80,10 +82,12 @@
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.stream.Collectors;
@@ -180,28 +184,43 @@
     private interface PhoneAccountRegistrarWriteLock {}
     private final PhoneAccountRegistrarWriteLock mWriteLock =
             new PhoneAccountRegistrarWriteLock() {};
+    private final FeatureFlags mTelephonyFeatureFlags;
+    private final com.android.server.telecom.flags.FeatureFlags mTelecomFeatureFlags;
 
     @VisibleForTesting
     public PhoneAccountRegistrar(Context context, TelecomSystem.SyncRoot lock,
-            DefaultDialerCache defaultDialerCache, AppLabelProxy appLabelProxy) {
-        this(context, lock, FILE_NAME, defaultDialerCache, appLabelProxy);
+            DefaultDialerCache defaultDialerCache, AppLabelProxy appLabelProxy,
+            FeatureFlags telephonyFeatureFlags,
+            com.android.server.telecom.flags.FeatureFlags telecomFeatureFlags) {
+        this(context, lock, FILE_NAME, defaultDialerCache, appLabelProxy,
+                telephonyFeatureFlags, telecomFeatureFlags);
     }
 
     @VisibleForTesting
     public PhoneAccountRegistrar(Context context, TelecomSystem.SyncRoot lock, String fileName,
-            DefaultDialerCache defaultDialerCache, AppLabelProxy appLabelProxy) {
+            DefaultDialerCache defaultDialerCache, AppLabelProxy appLabelProxy,
+            FeatureFlags telephonyFeatureFlags,
+            com.android.server.telecom.flags.FeatureFlags telecomFeatureFlags) {
 
         mAtomicFile = new AtomicFile(new File(context.getFilesDir(), fileName));
 
         mState = new State();
         mContext = context;
         mLock = lock;
-        mUserManager = UserManager.get(context);
+        mUserManager = context.getSystemService(UserManager.class);
         mDefaultDialerCache = defaultDialerCache;
         mSubscriptionManager = SubscriptionManager.from(mContext);
         mTelephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
         mAppLabelProxy = appLabelProxy;
         mCurrentUserHandle = Process.myUserHandle();
+        mTelecomFeatureFlags = telecomFeatureFlags;
+
+        if (telephonyFeatureFlags != null) {
+            mTelephonyFeatureFlags = telephonyFeatureFlags;
+        } else {
+            mTelephonyFeatureFlags =
+                    new com.android.internal.telephony.flags.FeatureFlagsImpl();
+        }
 
         // register context based receiver to clean up orphan phone accounts
         IntentFilter intentFilter = new IntentFilter(Intent.ACTION_MANAGED_PROFILE_REMOVED);
@@ -223,7 +242,11 @@
         PhoneAccount account = getPhoneAccountUnchecked(accountHandle);
 
         if (account != null && account.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) {
-            return mTelephonyManager.getSubscriptionId(accountHandle);
+            try {
+                return mTelephonyManager.getSubscriptionId(accountHandle);
+            } catch (UnsupportedOperationException ignored) {
+                // Ignore; fall back to invalid below.
+            }
         }
         return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
     }
@@ -389,10 +412,30 @@
                             account.getGroupId()));
         }
 
-        // Potentially update the default voice subid in SubscriptionManager.
+        // Potentially update the default voice subid in SubscriptionManager so that Telephony and
+        // Telecom are in sync.
         int newSubId = accountHandle == null ? SubscriptionManager.INVALID_SUBSCRIPTION_ID :
                 getSubscriptionIdForPhoneAccount(accountHandle);
-        if (isSimAccount || accountHandle == null) {
+        if (Flags.onlyUpdateTelephonyOnValidSubIds()) {
+            if (shouldUpdateTelephonyDefaultVoiceSubId(accountHandle, isSimAccount, newSubId)) {
+                updateDefaultVoiceSubId(newSubId, accountHandle);
+            } else {
+                Log.i(this, "setUserSelectedOutgoingPhoneAccount: %s is not a sub", accountHandle);
+            }
+        } else {
+            if (isSimAccount || accountHandle == null) {
+                updateDefaultVoiceSubId(newSubId, accountHandle);
+            } else {
+                Log.i(this, "setUserSelectedOutgoingPhoneAccount: %s is not a sub", accountHandle);
+            }
+        }
+
+        write();
+        fireDefaultOutgoingChanged();
+    }
+
+    private void updateDefaultVoiceSubId(int newSubId, PhoneAccountHandle accountHandle){
+        try {
             int currentVoiceSubId = mSubscriptionManager.getDefaultVoiceSubscriptionId();
             if (newSubId != currentVoiceSubId) {
                 Log.i(this, "setUserSelectedOutgoingPhoneAccount: update voice sub; "
@@ -401,17 +444,43 @@
             } else {
                 Log.i(this, "setUserSelectedOutgoingPhoneAccount: no change to voice sub");
             }
-        } else {
-            Log.i(this, "setUserSelectedOutgoingPhoneAccount: %s is not a sub", accountHandle);
+        } catch (UnsupportedOperationException uoe) {
+            Log.w(this, "setUserSelectedOutgoingPhoneAccount: no telephony");
         }
+    }
 
-        write();
-        fireDefaultOutgoingChanged();
+    // This helper is important for CTS testing.  [PhoneAccount]s created by Telecom in CTS are
+    // assigned a  subId value of INVALID_SUBSCRIPTION_ID (-1) by Telephony.  However, when
+    // Telephony has a default outgoing calling voice account of -1, that translates to no default
+    // account (user should be prompted to select an acct when making MOs).  In order to avoid
+    // Telephony clearing out the newly changed default [PhoneAccount] in Telecom, Telephony should
+    // not be updated. This situation will never occur in production since [PhoneAccount]s in
+    // production are assigned non-negative subId values.
+    private boolean shouldUpdateTelephonyDefaultVoiceSubId(PhoneAccountHandle phoneAccountHandle,
+            boolean isSimAccount, int newSubId) {
+        // user requests no call preference
+        if (phoneAccountHandle == null) {
+            return true;
+        }
+        // do not update Telephony if the newSubId is invalid
+        if (newSubId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+            Log.w(this, "shouldUpdateTelephonyDefaultVoiceSubId: "
+                            + "invalid subId scenario, not updating Telephony. "
+                            + "phoneAccountHandle=[%s], isSimAccount=[%b], newSubId=[%s]",
+                    phoneAccountHandle, isSimAccount, newSubId);
+            return false;
+        }
+        return isSimAccount;
     }
 
     boolean isUserSelectedSmsPhoneAccount(PhoneAccountHandle accountHandle) {
-        return getSubscriptionIdForPhoneAccount(accountHandle) ==
-                SubscriptionManager.getDefaultSmsSubscriptionId();
+        try {
+            return getSubscriptionIdForPhoneAccount(accountHandle) ==
+                    SubscriptionManager.getDefaultSmsSubscriptionId();
+        } catch (UnsupportedOperationException uoe) {
+            Log.w(this, "isUserSelectedSmsPhoneAccount: no telephony");
+            return false;
+        }
     }
 
     public ComponentName getSystemSimCallManagerComponent() {
@@ -420,12 +489,18 @@
 
     public ComponentName getSystemSimCallManagerComponent(int subId) {
         String defaultSimCallManager = null;
-        CarrierConfigManager configManager = (CarrierConfigManager) mContext.getSystemService(
-                Context.CARRIER_CONFIG_SERVICE);
-        PersistableBundle configBundle = configManager.getConfigForSubId(subId);
-        if (configBundle != null) {
-            defaultSimCallManager = configBundle.getString(
-                    CarrierConfigManager.KEY_DEFAULT_SIM_CALL_MANAGER_STRING);
+        try {
+            CarrierConfigManager configManager = (CarrierConfigManager) mContext.getSystemService(
+                    Context.CARRIER_CONFIG_SERVICE);
+            if (configManager == null) return null;
+            PersistableBundle configBundle = configManager.getConfigForSubId(subId);
+            if (configBundle != null) {
+                defaultSimCallManager = configBundle.getString(
+                        CarrierConfigManager.KEY_DEFAULT_SIM_CALL_MANAGER_STRING);
+            }
+        } catch (UnsupportedOperationException ignored) {
+            Log.w(this, "getSystemSimCallManagerComponent: no telephony");
+            // Fall through to empty below.
         }
         return TextUtils.isEmpty(defaultSimCallManager)
                 ?  null : ComponentName.unflattenFromString(defaultSimCallManager);
@@ -660,6 +735,24 @@
         }
     }
 
+    private boolean isMatchedUser(PhoneAccount account, UserHandle userHandle) {
+        if (account == null) {
+            return false;
+        }
+
+        if (userHandle == null) {
+            Log.w(this, "userHandle is null in isVisibleForUser");
+            return false;
+        }
+
+        UserHandle phoneAccountUserHandle = account.getAccountHandle().getUserHandle();
+        if (phoneAccountUserHandle == null) {
+            return false;
+        }
+
+        return phoneAccountUserHandle.equals(userHandle);
+    }
+
     private boolean isVisibleForUser(PhoneAccount account, UserHandle userHandle,
             boolean acrossProfiles) {
         if (account == null) {
@@ -689,8 +782,11 @@
         }
 
         if (acrossProfiles) {
-            return UserManager.get(mContext).isSameProfileGroup(userHandle.getIdentifier(),
-                    phoneAccountUserHandle.getIdentifier());
+            UserManager um = mContext.getSystemService(UserManager.class);
+            return mTelecomFeatureFlags.telecomResolveHiddenDependencies()
+                    ? um.isSameProfileGroup(userHandle, phoneAccountUserHandle)
+                    : um.isSameProfileGroup(userHandle.getIdentifier(),
+                            phoneAccountUserHandle.getIdentifier());
         } else {
             return phoneAccountUserHandle.equals(userHandle);
         }
@@ -726,11 +822,11 @@
      */
     public List<PhoneAccountHandle> getAllPhoneAccountHandles(UserHandle userHandle,
             boolean crossUserAccess) {
-        return getPhoneAccountHandles(0, null, null, false, userHandle, crossUserAccess);
+        return getPhoneAccountHandles(0, null, null, false, userHandle, crossUserAccess, true);
     }
 
     public List<PhoneAccount> getAllPhoneAccounts(UserHandle userHandle, boolean crossUserAccess) {
-        return getPhoneAccounts(0, null, null, false, mCurrentUserHandle, crossUserAccess);
+        return getPhoneAccounts(0, null, null, false, mCurrentUserHandle, crossUserAccess, true);
     }
 
     /**
@@ -821,7 +917,7 @@
     public List<PhoneAccountHandle> getAllPhoneAccountHandlesForPackage(UserHandle userHandle,
             String packageName) {
         return getPhoneAccountHandles(0, null, packageName, true /* includeDisabled */, userHandle,
-                true /* crossUserAccess */);
+                true /* crossUserAccess */, true);
     }
 
     /**
@@ -885,11 +981,36 @@
         }
         enforceCharacterLimit(account);
         enforceIconSizeLimit(account);
+        if (mTelecomFeatureFlags.unregisterUnresolvableAccounts()) {
+            enforcePhoneAccountTargetService(account);
+        }
         enforceMaxPhoneAccountLimit(account);
+        if (mTelephonyFeatureFlags.simultaneousCallingIndications()) {
+            enforceSimultaneousCallingRestrictionLimit(account);
+        }
         addOrReplacePhoneAccount(account);
     }
 
     /**
+     * This method ensures that {@link PhoneAccount}s that have the {@link
+     * PhoneAccount#CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS} capability are not
+     * backed by a {@link ConnectionService}
+     *
+     * @param account enforce the check on
+     */
+    private void enforcePhoneAccountTargetService(PhoneAccount account) {
+        if (phoneAccountRequiresBindPermission(account.getAccountHandle()) &&
+                hasTransactionalCallCapabilities(account)) {
+            throw new IllegalArgumentException(
+                    "Error, the PhoneAccount you are registering has"
+                            + " CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS and the"
+                            + " PhoneAccountHandle's ComponentName#ClassName points to a"
+                            + " ConnectionService class.  Either remove the capability or use a"
+                            + " different ClassName in the PhoneAccountHandle.");
+        }
+    }
+
+    /**
      * Enforce an upper bound on the number of PhoneAccount's a package can register.
      * Most apps should only require 1-2.  * Include disabled accounts.
      *
@@ -897,13 +1018,17 @@
      * @throws IllegalArgumentException if MAX_PHONE_ACCOUNT_REGISTRATIONS are reached
      */
     private void enforceMaxPhoneAccountLimit(@NonNull PhoneAccount account) {
-        final PhoneAccountHandle accountHandle = account.getAccountHandle();
-        final UserHandle user = accountHandle.getUserHandle();
-        final ComponentName componentName = accountHandle.getComponentName();
-
-        if (getPhoneAccountHandles(0, null, componentName.getPackageName(),
-                true /* includeDisabled */, user, false /* crossUserAccess */).size()
-                >= MAX_PHONE_ACCOUNT_REGISTRATIONS) {
+        int numOfAcctsRegisteredForPackage = mTelecomFeatureFlags.unregisterUnresolvableAccounts()
+                ? cleanupAndGetVerifiedAccounts(account).size()
+                : getPhoneAccountHandles(
+                        0/* capabilities */,
+                        null /* uriScheme */,
+                        account.getAccountHandle().getComponentName().getPackageName(),
+                        true /* includeDisabled */,
+                        account.getAccountHandle().getUserHandle(),
+                        false /* crossUserAccess */).size();
+        // enforce the max phone account limit for the application registering accounts
+        if (numOfAcctsRegisteredForPackage >= MAX_PHONE_ACCOUNT_REGISTRATIONS) {
             EventLog.writeEvent(0x534e4554, "259064622", Binder.getCallingUid(),
                     "enforceMaxPhoneAccountLimit");
             throw new IllegalArgumentException(
@@ -913,6 +1038,64 @@
         }
     }
 
+    @VisibleForTesting
+    public List<PhoneAccount> getRegisteredAccountsForPackageName(String packageName,
+            UserHandle userHandle) {
+        if (packageName == null) {
+            return new ArrayList<>();
+        }
+        List<PhoneAccount> accounts = new ArrayList<>(mState.accounts.size());
+        for (PhoneAccount m : mState.accounts) {
+            PhoneAccountHandle handle = m.getAccountHandle();
+            if (!packageName.equals(handle.getComponentName().getPackageName())) {
+                // Not the right package name; skip this one.
+                continue;
+            }
+            // Do not count accounts registered under different users on the device. Otherwise, an
+            // application can only have MAX_PHONE_ACCOUNT_REGISTRATIONS across all users. If the
+            // DUT has multiple users, they should each get to register 10 accounts. Also, 3rd
+            // party applications cannot create new UserHandles without highly privileged
+            // permissions.
+            if (!isVisibleForUser(m, userHandle, false)) {
+                // Account is not visible for the current user; skip this one.
+                continue;
+            }
+            accounts.add(m);
+        }
+        return accounts;
+    }
+
+    /**
+     * Unregister {@link ConnectionService} accounts that no longer have a resolvable Service. This
+     * means the Service has been disabled or died.  Skip the verification for transactional
+     * accounts.
+     *
+     * @param newAccount being registered
+     * @return all the verified accounts. These accounts are now guaranteed to be backed by a
+     * {@link ConnectionService} or do not need one (transactional accounts).
+     */
+    @VisibleForTesting
+    public List<PhoneAccount> cleanupAndGetVerifiedAccounts(PhoneAccount newAccount) {
+        ArrayList<PhoneAccount> verifiedAccounts = new ArrayList<>();
+        List<PhoneAccount> unverifiedAccounts = getRegisteredAccountsForPackageName(
+                newAccount.getAccountHandle().getComponentName().getPackageName(),
+                newAccount.getAccountHandle().getUserHandle());
+        for (PhoneAccount account : unverifiedAccounts) {
+            PhoneAccountHandle handle = account.getAccountHandle();
+            if (/* skip for transactional accounts since they don't require a ConnectionService */
+                    !hasTransactionalCallCapabilities(account) &&
+                    /* check if the {@link ConnectionService} has been disabled or can longer be
+                       found */ resolveComponent(handle).isEmpty()) {
+                Log.i(this, " cAGVA: Cannot resolve the ConnectionService for"
+                        + " handle=[%s]; unregistering account", handle);
+                unregisterPhoneAccount(handle);
+            } else {
+                verifiedAccounts.add(account);
+            }
+        }
+        return verifiedAccounts;
+    }
+
     /**
      * determine if there will be an issue writing the icon to memory
      *
@@ -1014,6 +1197,43 @@
     }
 
     /**
+     * Enforce size limits on the simultaneous calling restriction of a PhoneAccount.
+     * If a PhoneAccount has a simultaneous calling restriction on it, enforce the following: the
+     * number of PhoneAccountHandles in the Set can not exceed the per app restriction on
+     * PhoneAccounts registered and each PhoneAccountHandle's fields must not exceed the per field
+     * character limit.
+     * @param account The PhoneAccount to enforce simultaneous calling restrictions on.
+     * @throws IllegalArgumentException if the PhoneAccount exceeds size limits.
+     */
+    public void enforceSimultaneousCallingRestrictionLimit(@NonNull PhoneAccount account) {
+        if (!account.hasSimultaneousCallingRestriction()) return;
+        Set<PhoneAccountHandle> restrictions = account.getSimultaneousCallingRestriction();
+        if (restrictions.size() > MAX_PHONE_ACCOUNT_REGISTRATIONS) {
+            throw new IllegalArgumentException("Can not register a PhoneAccount with a number"
+                    + "of simultaneous calling restrictions that is greater than "
+                    + MAX_PHONE_ACCOUNT_REGISTRATIONS);
+        }
+        for (PhoneAccountHandle handle : restrictions) {
+            ComponentName component = handle.getComponentName();
+            if (component.getPackageName().length() > MAX_PHONE_ACCOUNT_FIELD_CHAR_LIMIT) {
+                throw new IllegalArgumentException("A PhoneAccountHandle added as part of "
+                        + "a simultaneous calling restriction has a package name that has exceeded "
+                        + "the character limit of " + MAX_PHONE_ACCOUNT_FIELD_CHAR_LIMIT);
+            }
+            if (component.getClassName().length() > MAX_PHONE_ACCOUNT_FIELD_CHAR_LIMIT) {
+                throw new IllegalArgumentException("A PhoneAccountHandle added as part of "
+                        + "a simultaneous calling restriction has a class name that has exceeded "
+                        + "the character limit of " + MAX_PHONE_ACCOUNT_FIELD_CHAR_LIMIT);
+            }
+            if (handle.getId().length() > MAX_PHONE_ACCOUNT_FIELD_CHAR_LIMIT) {
+                throw new IllegalArgumentException("A PhoneAccountHandle added as part of "
+                        + "a simultaneous calling restriction has an ID that has exceeded "
+                        + "the character limit of " + MAX_PHONE_ACCOUNT_FIELD_CHAR_LIMIT);
+            }
+        }
+    }
+
+    /**
      * Enforce a character limit on all PA and PAH string or char-sequence fields.
      *
      * @param account to enforce check on
@@ -1348,16 +1568,22 @@
                 "Notifying telephony of voice service override change for %d SIMs, hasService = %b",
                 simHandlesToNotify.size(),
                 hasService);
-        for (PhoneAccountHandle simHandle : simHandlesToNotify) {
-            // This may be null if there are no active SIMs but the device is still camped for
-            // emergency calls and registered a SIM_SUBSCRIPTION for that purpose.
-            TelephonyManager simTm = mTelephonyManager.createForPhoneAccountHandle(simHandle);
-            if (simTm == null) {
-                Log.i(this, "maybeNotifyTelephonyForVoiceServiceState: "
-                        + "simTm is null.");
-                continue;
+        try {
+            for (PhoneAccountHandle simHandle : simHandlesToNotify) {
+                // This may be null if there are no active SIMs but the device is still camped for
+                // emergency calls and registered a SIM_SUBSCRIPTION for that purpose.
+                TelephonyManager simTm = mTelephonyManager.createForPhoneAccountHandle(simHandle);
+                if (simTm == null) {
+                    Log.i(this, "maybeNotifyTelephonyForVoiceServiceState: "
+                            + "simTm is null.");
+                    continue;
+                }
+                simTm.setVoiceServiceStateOverride(hasService);
             }
-            simTm.setVoiceServiceStateOverride(hasService);
+        } catch (UnsupportedOperationException ignored) {
+            // No telephony, so we can't override the sim service state.
+            // Realistically we shouldn't get here because there should be no sim subs in this case.
+            Log.w(this, "maybeNotifyTelephonyForVoiceServiceState: no telephony");
         }
     }
 
@@ -1451,7 +1677,19 @@
             UserHandle userHandle,
             boolean crossUserAccess) {
         return getPhoneAccountHandles(capabilities, 0 /*excludedCapabilities*/, uriScheme,
-                packageName, includeDisabledAccounts, userHandle, crossUserAccess);
+                packageName, includeDisabledAccounts, userHandle, crossUserAccess, false);
+    }
+
+    private List<PhoneAccountHandle> getPhoneAccountHandles(
+            int capabilities,
+            String uriScheme,
+            String packageName,
+            boolean includeDisabledAccounts,
+            UserHandle userHandle,
+            boolean crossUserAccess,
+            boolean includeAll) {
+        return getPhoneAccountHandles(capabilities, 0 /*excludedCapabilities*/, uriScheme,
+                packageName, includeDisabledAccounts, userHandle, crossUserAccess, includeAll);
     }
 
     /**
@@ -1466,11 +1704,24 @@
             boolean includeDisabledAccounts,
             UserHandle userHandle,
             boolean crossUserAccess) {
+        return getPhoneAccountHandles(capabilities, excludedCapabilities, uriScheme, packageName,
+                includeDisabledAccounts, userHandle, crossUserAccess, false);
+    }
+
+    private List<PhoneAccountHandle> getPhoneAccountHandles(
+            int capabilities,
+            int excludedCapabilities,
+            String uriScheme,
+            String packageName,
+            boolean includeDisabledAccounts,
+            UserHandle userHandle,
+            boolean crossUserAccess,
+            boolean includeAll) {
         List<PhoneAccountHandle> handles = new ArrayList<>();
 
         for (PhoneAccount account : getPhoneAccounts(
                 capabilities, excludedCapabilities, uriScheme, packageName,
-                includeDisabledAccounts, userHandle, crossUserAccess)) {
+                includeDisabledAccounts, userHandle, crossUserAccess, includeAll)) {
             handles.add(account.getAccountHandle());
         }
         return handles;
@@ -1484,7 +1735,19 @@
             UserHandle userHandle,
             boolean crossUserAccess) {
         return getPhoneAccounts(capabilities, 0 /*excludedCapabilities*/, uriScheme, packageName,
-                includeDisabledAccounts, userHandle, crossUserAccess);
+                includeDisabledAccounts, userHandle, crossUserAccess, false);
+    }
+
+    private List<PhoneAccount> getPhoneAccounts(
+            int capabilities,
+            String uriScheme,
+            String packageName,
+            boolean includeDisabledAccounts,
+            UserHandle userHandle,
+            boolean crossUserAccess,
+            boolean includeAll) {
+        return getPhoneAccounts(capabilities, 0 /*excludedCapabilities*/, uriScheme, packageName,
+                includeDisabledAccounts, userHandle, crossUserAccess, includeAll);
     }
 
     /**
@@ -1506,7 +1769,22 @@
             boolean includeDisabledAccounts,
             UserHandle userHandle,
             boolean crossUserAccess) {
+        return getPhoneAccounts(capabilities, excludedCapabilities, uriScheme, packageName,
+                includeDisabledAccounts, userHandle, crossUserAccess, false);
+    }
+
+    @VisibleForTesting
+    public List<PhoneAccount> getPhoneAccounts(
+            int capabilities,
+            int excludedCapabilities,
+            String uriScheme,
+            String packageName,
+            boolean includeDisabledAccounts,
+            UserHandle userHandle,
+            boolean crossUserAccess,
+            boolean includeAll) {
         List<PhoneAccount> accounts = new ArrayList<>(mState.accounts.size());
+        List<PhoneAccount> matchedAccounts = new ArrayList<>(mState.accounts.size());
         for (PhoneAccount m : mState.accounts) {
             if (!(m.isEnabled() || includeDisabledAccounts)) {
                 // Do not include disabled accounts.
@@ -1540,12 +1818,22 @@
                 // Not the right package name; skip this one.
                 continue;
             }
+            if (isMatchedUser(m, userHandle)) {
+                matchedAccounts.add(m);
+            }
             if (!crossUserAccess && !isVisibleForUser(m, userHandle, false)) {
                 // Account is not visible for the current user; skip this one.
                 continue;
             }
             accounts.add(m);
         }
+
+        // Return the account if it exactly matches. Otherwise, return any account that's visible
+        if (mTelephonyFeatureFlags.workProfileApiSplit() && !crossUserAccess && !includeAll
+                && !matchedAccounts.isEmpty()) {
+            return matchedAccounts;
+        }
+
         return accounts;
     }
 
@@ -1662,6 +1950,12 @@
             } else {
                 pw.println(defaultOutgoing);
             }
+            // SubscriptionManager will throw if FEATURE_TELEPHONY_SUBSCRIPTION is not present.
+            if (mContext.getPackageManager().hasSystemFeature(
+                    PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION)) {
+                pw.println("defaultVoiceSubId: "
+                        + SubscriptionManager.getDefaultVoiceSubscriptionId());
+            }
             pw.println("simCallManager: " + getSimCallManager(mCurrentUserHandle));
             pw.println("phoneAccounts:");
             pw.increaseIndent();
@@ -1750,7 +2044,7 @@
             sortPhoneAccounts();
             ByteArrayOutputStream os = new ByteArrayOutputStream();
             XmlSerializer serializer = Xml.resolveSerializer(os);
-            writeToXml(mState, serializer, mContext);
+            writeToXml(mState, serializer, mContext, mTelephonyFeatureFlags);
             serializer.flush();
             new AsyncXmlWriter().execute(os);
         } catch (IOException e) {
@@ -1771,7 +2065,7 @@
         try {
             XmlPullParser parser = Xml.resolvePullParser(is);
             parser.nextTag();
-            mState = readFromXml(parser, mContext);
+            mState = readFromXml(parser, mContext, mTelephonyFeatureFlags, mTelecomFeatureFlags);
             migratePhoneAccountHandle(mState);
             versionChanged = mState.versionNumber < EXPECTED_STATE_VERSION;
 
@@ -1806,14 +2100,17 @@
         }
     }
 
-    private static void writeToXml(State state, XmlSerializer serializer, Context context)
-            throws IOException {
-        sStateXml.writeToXml(state, serializer, context);
+    private static void writeToXml(State state, XmlSerializer serializer, Context context,
+            FeatureFlags telephonyFeatureFlags) throws IOException {
+        sStateXml.writeToXml(state, serializer, context, telephonyFeatureFlags);
     }
 
-    private static State readFromXml(XmlPullParser parser, Context context)
+    private static State readFromXml(XmlPullParser parser, Context context,
+            FeatureFlags telephonyFeatureFlags,
+            com.android.server.telecom.flags.FeatureFlags telecomFeatureFlags)
             throws IOException, XmlPullParserException {
-        State s = sStateXml.readFromXml(parser, 0, context);
+        State s = sStateXml.readFromXml(parser, 0, context,
+                telephonyFeatureFlags, telecomFeatureFlags);
         return s != null ? s : new State();
     }
 
@@ -1879,8 +2176,8 @@
         /**
          * Write the supplied object to XML
          */
-        public abstract void writeToXml(T o, XmlSerializer serializer, Context context)
-                throws IOException;
+        public abstract void writeToXml(T o, XmlSerializer serializer, Context context,
+                FeatureFlags telephonyFeatureFlags) throws IOException;
 
         /**
          * Read from the supplied XML into a new object, returning null in case of an
@@ -1889,7 +2186,9 @@
          * object's writeToXml(). This object tries to fail early without modifying
          * 'parser' if it does not recognize the data it sees.
          */
-        public abstract T readFromXml(XmlPullParser parser, int version, Context context)
+        public abstract T readFromXml(XmlPullParser parser, int version, Context context,
+                FeatureFlags telephonyFeatureFlags,
+                com.android.server.telecom.flags.FeatureFlags featureFlags)
                 throws IOException, XmlPullParserException;
 
         protected void writeTextIfNonNull(String tagName, Object value, XmlSerializer serializer)
@@ -1902,6 +2201,29 @@
         }
 
         /**
+         * Serializes a List of PhoneAccountHandles.
+         * @param tagName The tag for the List
+         * @param handles The List of PhoneAccountHandles to serialize
+         * @param serializer The serializer
+         * @throws IOException if serialization fails.
+         */
+        protected void writePhoneAccountHandleSet(String tagName, Set<PhoneAccountHandle> handles,
+                XmlSerializer serializer, Context context, FeatureFlags telephonyFeatureFlags)
+                throws IOException {
+            serializer.startTag(null, tagName);
+            if (handles != null) {
+                serializer.attribute(null, ATTRIBUTE_LENGTH, Objects.toString(handles.size()));
+                for (PhoneAccountHandle handle : handles) {
+                    sPhoneAccountHandleXml.writeToXml(handle, serializer, context,
+                            telephonyFeatureFlags);
+                }
+            } else {
+                serializer.attribute(null, ATTRIBUTE_LENGTH, "0");
+            }
+            serializer.endTag(null, tagName);
+        }
+
+        /**
          * Serializes a string array.
          *
          * @param tagName The tag name for the string array.
@@ -1995,6 +2317,22 @@
             serializer.endTag(null, tagName);
         }
 
+        protected Set<PhoneAccountHandle> readPhoneAccountHandleSet(XmlPullParser parser,
+                int version, Context context, FeatureFlags telephonyFeatureFlags,
+                com.android.server.telecom.flags.FeatureFlags telecomFeatureFlags)
+                throws IOException, XmlPullParserException {
+            int length = Integer.parseInt(parser.getAttributeValue(null, ATTRIBUTE_LENGTH));
+            Set<PhoneAccountHandle> handles = new HashSet<>(length);
+            if (length == 0) return handles;
+
+            int outerDepth = parser.getDepth();
+            while (XmlUtils.nextElementWithin(parser, outerDepth)) {
+                handles.add(sPhoneAccountHandleXml.readFromXml(parser, version, context,
+                        telephonyFeatureFlags, telecomFeatureFlags));
+            }
+            return handles;
+        }
+
         /**
          * Reads a string array from the XML parser.
          *
@@ -2102,8 +2440,8 @@
         private static final String VERSION = "version";
 
         @Override
-        public void writeToXml(State o, XmlSerializer serializer, Context context)
-                throws IOException {
+        public void writeToXml(State o, XmlSerializer serializer, Context context,
+                FeatureFlags telephonyFeatureFlags) throws IOException {
             if (o != null) {
                 serializer.startTag(null, CLASS_STATE);
                 serializer.attribute(null, VERSION, Objects.toString(EXPECTED_STATE_VERSION));
@@ -2111,14 +2449,15 @@
                 serializer.startTag(null, DEFAULT_OUTGOING);
                 for (DefaultPhoneAccountHandle defaultPhoneAccountHandle : o
                         .defaultOutgoingAccountHandles.values()) {
-                    sDefaultPhoneAcountHandleXml
-                            .writeToXml(defaultPhoneAccountHandle, serializer, context);
+                    sDefaultPhoneAccountHandleXml
+                            .writeToXml(defaultPhoneAccountHandle, serializer, context,
+                                    telephonyFeatureFlags);
                 }
                 serializer.endTag(null, DEFAULT_OUTGOING);
 
                 serializer.startTag(null, ACCOUNTS);
                 for (PhoneAccount m : o.accounts) {
-                    sPhoneAccountXml.writeToXml(m, serializer, context);
+                    sPhoneAccountXml.writeToXml(m, serializer, context, telephonyFeatureFlags);
                 }
                 serializer.endTag(null, ACCOUNTS);
 
@@ -2127,7 +2466,9 @@
         }
 
         @Override
-        public State readFromXml(XmlPullParser parser, int version, Context context)
+        public State readFromXml(XmlPullParser parser, int version, Context context,
+                FeatureFlags telephonyFeatureFlags,
+                com.android.server.telecom.flags.FeatureFlags telecomFeatureFlags)
                 throws IOException, XmlPullParserException {
             if (parser.getName().equals(CLASS_STATE)) {
                 State s = new State();
@@ -2144,23 +2485,32 @@
                             // assume there are no groups.
                             parser.nextTag();
                             PhoneAccountHandle phoneAccountHandle = sPhoneAccountHandleXml
-                                    .readFromXml(parser, s.versionNumber, context);
-                            UserManager userManager = UserManager.get(context);
-                            UserInfo primaryUser = userManager.getPrimaryUser();
+                                    .readFromXml(parser, s.versionNumber, context,
+                                            telephonyFeatureFlags, telecomFeatureFlags);
+                            UserManager userManager = context.getSystemService(UserManager.class);
+                            // UserManager#getMainUser requires either the MANAGE_USERS,
+                            // CREATE_USERS, or QUERY_USERS permission.
+                            UserHandle primaryUser = userManager.getMainUser();
+                            UserInfo primaryUserInfo = userManager.getPrimaryUser();
+                            if (!telecomFeatureFlags.telecomResolveHiddenDependencies()) {
+                                primaryUser = primaryUserInfo != null
+                                        ? primaryUserInfo.getUserHandle()
+                                        : null;
+                            }
                             if (primaryUser != null) {
-                                UserHandle userHandle = primaryUser.getUserHandle();
                                 DefaultPhoneAccountHandle defaultPhoneAccountHandle
-                                        = new DefaultPhoneAccountHandle(userHandle,
+                                        = new DefaultPhoneAccountHandle(primaryUser,
                                         phoneAccountHandle, "" /* groupId */);
                                 s.defaultOutgoingAccountHandles
-                                        .put(userHandle, defaultPhoneAccountHandle);
+                                        .put(primaryUser, defaultPhoneAccountHandle);
                             }
                         } else {
                             int defaultAccountHandlesDepth = parser.getDepth();
                             while (XmlUtils.nextElementWithin(parser, defaultAccountHandlesDepth)) {
                                 DefaultPhoneAccountHandle accountHandle
-                                        = sDefaultPhoneAcountHandleXml
-                                        .readFromXml(parser, s.versionNumber, context);
+                                        = sDefaultPhoneAccountHandleXml
+                                        .readFromXml(parser, s.versionNumber, context,
+                                                telephonyFeatureFlags, telecomFeatureFlags);
                                 if (accountHandle != null && s.accounts != null) {
                                     s.defaultOutgoingAccountHandles
                                             .put(accountHandle.userHandle, accountHandle);
@@ -2171,7 +2521,8 @@
                         int accountsDepth = parser.getDepth();
                         while (XmlUtils.nextElementWithin(parser, accountsDepth)) {
                             PhoneAccount account = sPhoneAccountXml.readFromXml(parser,
-                                    s.versionNumber, context);
+                                    s.versionNumber, context, telephonyFeatureFlags,
+                                    telecomFeatureFlags);
 
                             if (account != null && s.accounts != null) {
                                 s.accounts.add(account);
@@ -2186,7 +2537,7 @@
     };
 
     @VisibleForTesting
-    public static final XmlSerialization<DefaultPhoneAccountHandle> sDefaultPhoneAcountHandleXml  =
+    public static final XmlSerialization<DefaultPhoneAccountHandle> sDefaultPhoneAccountHandleXml =
             new XmlSerialization<DefaultPhoneAccountHandle>() {
                 private static final String CLASS_DEFAULT_OUTGOING_PHONE_ACCOUNT_HANDLE
                         = "default_outgoing_phone_account_handle";
@@ -2196,9 +2547,9 @@
 
                 @Override
                 public void writeToXml(DefaultPhoneAccountHandle o, XmlSerializer serializer,
-                        Context context) throws IOException {
+                        Context context, FeatureFlags telephonyFeatureFlags) throws IOException {
                     if (o != null) {
-                        final UserManager userManager = UserManager.get(context);
+                        final UserManager userManager = context.getSystemService(UserManager.class);
                         final long serialNumber = userManager.getSerialNumberForUser(o.userHandle);
                         if (serialNumber != -1) {
                             serializer.startTag(null, CLASS_DEFAULT_OUTGOING_PHONE_ACCOUNT_HANDLE);
@@ -2206,7 +2557,7 @@
                             writeNonNullString(GROUP_ID, o.groupId, serializer);
                             serializer.startTag(null, ACCOUNT_HANDLE);
                             sPhoneAccountHandleXml.writeToXml(o.phoneAccountHandle, serializer,
-                                    context);
+                                    context, telephonyFeatureFlags);
                             serializer.endTag(null, ACCOUNT_HANDLE);
                             serializer.endTag(null, CLASS_DEFAULT_OUTGOING_PHONE_ACCOUNT_HANDLE);
                         }
@@ -2215,7 +2566,8 @@
 
                 @Override
                 public DefaultPhoneAccountHandle readFromXml(XmlPullParser parser, int version,
-                        Context context)
+                        Context context, FeatureFlags telephonyFeatureFlags,
+                        com.android.server.telecom.flags.FeatureFlags telecomFeatureFlags)
                         throws IOException, XmlPullParserException {
                     if (parser.getName().equals(CLASS_DEFAULT_OUTGOING_PHONE_ACCOUNT_HANDLE)) {
                         int outerDepth = parser.getDepth();
@@ -2226,7 +2578,7 @@
                             if (parser.getName().equals(ACCOUNT_HANDLE)) {
                                 parser.nextTag();
                                 accountHandle = sPhoneAccountHandleXml.readFromXml(parser, version,
-                                        context);
+                                        context, telephonyFeatureFlags, telecomFeatureFlags);
                             } else if (parser.getName().equals(USER_SERIAL_NUMBER)) {
                                 parser.next();
                                 userSerialNumberString = parser.getText();
@@ -2240,7 +2592,7 @@
                         if (userSerialNumberString != null) {
                             try {
                                 long serialNumber = Long.parseLong(userSerialNumberString);
-                                userHandle = UserManager.get(context)
+                                userHandle = context.getSystemService(UserManager.class)
                                         .getUserForSerialNumber(serialNumber);
                             } catch (NumberFormatException e) {
                                 Log.e(this, e,
@@ -2277,16 +2629,19 @@
         private static final String ICON = "icon";
         private static final String EXTRAS = "extras";
         private static final String ENABLED = "enabled";
+        private static final String SIMULTANEOUS_CALLING_RESTRICTION
+                = "simultaneous_calling_restriction";
 
         @Override
-        public void writeToXml(PhoneAccount o, XmlSerializer serializer, Context context)
-                throws IOException {
+        public void writeToXml(PhoneAccount o, XmlSerializer serializer, Context context,
+                FeatureFlags telephonyFeatureFlags) throws IOException {
             if (o != null) {
                 serializer.startTag(null, CLASS_PHONE_ACCOUNT);
 
                 if (o.getAccountHandle() != null) {
                     serializer.startTag(null, ACCOUNT_HANDLE);
-                    sPhoneAccountHandleXml.writeToXml(o.getAccountHandle(), serializer, context);
+                    sPhoneAccountHandleXml.writeToXml(o.getAccountHandle(), serializer, context,
+                            telephonyFeatureFlags);
                     serializer.endTag(null, ACCOUNT_HANDLE);
                 }
 
@@ -2303,13 +2658,20 @@
                 writeTextIfNonNull(ENABLED, o.isEnabled() ? "true" : "false" , serializer);
                 writeTextIfNonNull(SUPPORTED_AUDIO_ROUTES, Integer.toString(
                         o.getSupportedAudioRoutes()), serializer);
+                if (o.hasSimultaneousCallingRestriction()
+                        && telephonyFeatureFlags.simultaneousCallingIndications()) {
+                    writePhoneAccountHandleSet(SIMULTANEOUS_CALLING_RESTRICTION,
+                            o.getSimultaneousCallingRestriction(), serializer, context,
+                            telephonyFeatureFlags);
+                }
 
                 serializer.endTag(null, CLASS_PHONE_ACCOUNT);
             }
         }
 
-        public PhoneAccount readFromXml(XmlPullParser parser, int version, Context context)
-                throws IOException, XmlPullParserException {
+        public PhoneAccount readFromXml(XmlPullParser parser, int version, Context context,
+                FeatureFlags telephonyFeatureFlags,
+                com.android.server.telecom.flags.FeatureFlags telecomFeatureFlags) throws IOException, XmlPullParserException {
             if (parser.getName().equals(CLASS_PHONE_ACCOUNT)) {
                 int outerDepth = parser.getDepth();
                 PhoneAccountHandle accountHandle = null;
@@ -2328,12 +2690,13 @@
                 Icon icon = null;
                 boolean enabled = false;
                 Bundle extras = null;
+                Set<PhoneAccountHandle> simultaneousCallingRestriction = null;
 
                 while (XmlUtils.nextElementWithin(parser, outerDepth)) {
                     if (parser.getName().equals(ACCOUNT_HANDLE)) {
                         parser.nextTag();
                         accountHandle = sPhoneAccountHandleXml.readFromXml(parser, version,
-                                context);
+                                context, telephonyFeatureFlags, telecomFeatureFlags);
                     } else if (parser.getName().equals(ADDRESS)) {
                         parser.next();
                         address = Uri.parse(parser.getText());
@@ -2378,6 +2741,12 @@
                     } else if (parser.getName().equals(SUPPORTED_AUDIO_ROUTES)) {
                         parser.next();
                         supportedAudioRoutes = Integer.parseInt(parser.getText());
+                    } else if (parser.getName().equals(SIMULTANEOUS_CALLING_RESTRICTION)) {
+                        // We can not flag this because we always need to handle the case where
+                        // this info is in the XML for parsing reasons. We only flag setting the
+                        // parsed value below based on the flag.
+                        simultaneousCallingRestriction = readPhoneAccountHandleSet(parser, version,
+                                context, telephonyFeatureFlags, telecomFeatureFlags);
                     }
                 }
 
@@ -2459,6 +2828,9 @@
                 } else if (!TextUtils.isEmpty(iconPackageName)) {
                     builder.setIcon(Icon.createWithResource(iconPackageName, iconResId));
                     // TODO: Need to set tint.
+                } else if (simultaneousCallingRestriction != null
+                        && telephonyFeatureFlags.simultaneousCallingIndications()) {
+                    builder.setSimultaneousCallingRestriction(simultaneousCallingRestriction);
                 }
 
                 return builder.build();
@@ -2490,8 +2862,8 @@
         private static final String USER_SERIAL_NUMBER = "user_serial_number";
 
         @Override
-        public void writeToXml(PhoneAccountHandle o, XmlSerializer serializer, Context context)
-                throws IOException {
+        public void writeToXml(PhoneAccountHandle o, XmlSerializer serializer, Context context,
+                FeatureFlags telephonyFeatureFlags) throws IOException {
             if (o != null) {
                 serializer.startTag(null, CLASS_PHONE_ACCOUNT_HANDLE);
 
@@ -2503,7 +2875,7 @@
                 writeTextIfNonNull(ID, o.getId(), serializer);
 
                 if (o.getUserHandle() != null && context != null) {
-                    UserManager userManager = UserManager.get(context);
+                    UserManager userManager = context.getSystemService(UserManager.class);
                     writeLong(USER_SERIAL_NUMBER,
                             userManager.getSerialNumberForUser(o.getUserHandle()), serializer);
                 }
@@ -2513,7 +2885,9 @@
         }
 
         @Override
-        public PhoneAccountHandle readFromXml(XmlPullParser parser, int version, Context context)
+        public PhoneAccountHandle readFromXml(XmlPullParser parser, int version, Context context,
+                FeatureFlags telephonyFeatureFlags,
+                com.android.server.telecom.flags.FeatureFlags telecomFeatureFlags)
                 throws IOException, XmlPullParserException {
             if (parser.getName().equals(CLASS_PHONE_ACCOUNT_HANDLE)) {
                 String componentNameString = null;
@@ -2521,7 +2895,7 @@
                 String userSerialNumberString = null;
                 int outerDepth = parser.getDepth();
 
-                UserManager userManager = UserManager.get(context);
+                UserManager userManager = context.getSystemService(UserManager.class);
 
                 while (XmlUtils.nextElementWithin(parser, outerDepth)) {
                     if (parser.getName().equals(COMPONENT_NAME)) {
@@ -2554,8 +2928,4 @@
             return null;
         }
     };
-
-    private String nullToEmpty(String str) {
-        return str == null ? "" : str;
-    }
 }
diff --git a/src/com/android/server/telecom/PhoneStateBroadcaster.java b/src/com/android/server/telecom/PhoneStateBroadcaster.java
index 490db85..11f5e02 100644
--- a/src/com/android/server/telecom/PhoneStateBroadcaster.java
+++ b/src/com/android/server/telecom/PhoneStateBroadcaster.java
@@ -16,6 +16,7 @@
 
 package com.android.server.telecom;
 
+import android.content.pm.PackageManager;
 import android.telecom.Log;
 import android.telephony.PhoneNumberUtils;
 import android.telephony.SubscriptionInfo;
@@ -24,6 +25,8 @@
 import android.telephony.TelephonyRegistryManager;
 import android.telephony.emergency.EmergencyNumber;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 import java.util.List;
 import java.util.Objects;
 import java.util.Optional;
@@ -32,7 +35,7 @@
  * Send a {@link TelephonyManager#ACTION_PHONE_STATE_CHANGED} broadcast when the call state
  * changes.
  */
-final class PhoneStateBroadcaster extends CallsManagerListenerBase {
+public final class PhoneStateBroadcaster extends CallsManagerListenerBase {
 
     private final CallsManager mCallsManager;
     private final TelephonyRegistryManager mRegistry;
@@ -136,25 +139,34 @@
                     .flatMap(List::stream)
                     .filter(numberObj -> Objects.equals(numberObj.getNumber(), strippedNumber))
                     .findFirst();
+        } catch (UnsupportedOperationException ignored) {
+            emergencyNumber = Optional.empty();
         } catch (IllegalStateException ie) {
             emergencyNumber = Optional.empty();
         } catch (RuntimeException r) {
             emergencyNumber = Optional.empty();
         }
 
-        int subscriptionId = tm.getSubscriptionId(call.getTargetPhoneAccount());
-        SubscriptionManager subscriptionManager =
-                mCallsManager.getContext().getSystemService(SubscriptionManager.class);
-        int simSlotIndex = SubscriptionManager.DEFAULT_PHONE_INDEX;
-        if (subscriptionManager != null) {
-            SubscriptionInfo subInfo =
-                    subscriptionManager.getActiveSubscriptionInfo(subscriptionId);
-            if (subInfo != null) {
-                simSlotIndex = subInfo.getSimSlotIndex();
-            }
-        }
-
         if (emergencyNumber.isPresent()) {
+            int subscriptionId;
+            int simSlotIndex;
+            try {
+                subscriptionId = tm.getSubscriptionId(call.getTargetPhoneAccount());
+                SubscriptionManager subscriptionManager =
+                        mCallsManager.getContext().getSystemService(SubscriptionManager.class);
+                simSlotIndex = SubscriptionManager.DEFAULT_PHONE_INDEX;
+                if (subscriptionManager != null) {
+                    SubscriptionInfo subInfo =
+                            subscriptionManager.getActiveSubscriptionInfo(subscriptionId);
+                    if (subInfo != null) {
+                        simSlotIndex = subInfo.getSimSlotIndex();
+                    }
+                }
+            } catch (UnsupportedOperationException ignored) {
+                subscriptionId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+                simSlotIndex = SubscriptionManager.INVALID_SIM_SLOT_INDEX;
+            }
+
             mRegistry.notifyOutgoingEmergencyCall(
                     simSlotIndex, subscriptionId, emergencyNumber.get());
         }
diff --git a/src/com/android/server/telecom/RespondViaSmsManager.java b/src/com/android/server/telecom/RespondViaSmsManager.java
index 1d42db4..2dcd093 100644
--- a/src/com/android/server/telecom/RespondViaSmsManager.java
+++ b/src/com/android/server/telecom/RespondViaSmsManager.java
@@ -27,7 +27,6 @@
 import android.content.res.Resources;
 import android.telecom.Connection;
 import android.telecom.Log;
-import android.telecom.Response;
 import android.telephony.PhoneNumberUtils;
 import android.telephony.SmsManager;
 import android.telephony.SubscriptionManager;
@@ -92,7 +91,7 @@
      *                 the main thread.
      * @param context The context.
      */
-    public void loadCannedTextMessages(final Response<Void, List<String>> response,
+    public void loadCannedTextMessages(final CallsManager.Response<Void, List<String>> response,
             final Context context) {
         new Thread() {
             @Override
diff --git a/src/com/android/server/telecom/RingbackPlayer.java b/src/com/android/server/telecom/RingbackPlayer.java
index a8af3ac..5ace9ba 100644
--- a/src/com/android/server/telecom/RingbackPlayer.java
+++ b/src/com/android/server/telecom/RingbackPlayer.java
@@ -19,6 +19,7 @@
 import static com.android.server.telecom.LogUtils.Events.START_RINBACK;
 import static com.android.server.telecom.LogUtils.Events.STOP_RINGBACK;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.Preconditions;
 import android.telecom.Log;
 
@@ -42,8 +43,12 @@
      */
     private InCallTonePlayer mTonePlayer;
 
-    RingbackPlayer(InCallTonePlayer.Factory playerFactory) {
+    private final Object mLock;
+
+    @VisibleForTesting
+    public RingbackPlayer(InCallTonePlayer.Factory playerFactory) {
         mPlayerFactory = playerFactory;
+        mLock = new Object();
     }
 
     /**
@@ -52,25 +57,27 @@
      * @param call The call for which to ringback.
      */
     public void startRingbackForCall(Call call) {
-        Preconditions.checkState(call.getState() == CallState.DIALING);
+        synchronized (mLock) {
+            Preconditions.checkState(call.getState() == CallState.DIALING);
 
-        if (mCall == call) {
-            Log.w(this, "Ignoring duplicate requests to ring for %s.", call);
-            return;
-        }
+            if (mCall == call) {
+                Log.w(this, "Ignoring duplicate requests to ring for %s.", call);
+                return;
+            }
 
-        if (mCall != null) {
-            // We only get here for the foreground call so, there's no reason why there should
-            // exist a current dialing call.
-            Log.wtf(this, "Ringback player thinks there are two foreground-dialing calls.");
-        }
+            if (mCall != null) {
+                // We only get here for the foreground call so, there's no reason why there should
+                // exist a current dialing call.
+                Log.wtf(this, "Ringback player thinks there are two foreground-dialing calls.");
+            }
 
-        mCall = call;
-        if (mTonePlayer == null) {
-            Log.i(this, "Playing the ringback tone for %s.", call);
-            Log.addEvent(call, START_RINBACK);
-            mTonePlayer = mPlayerFactory.createPlayer(InCallTonePlayer.TONE_RING_BACK);
-            mTonePlayer.startTone();
+            mCall = call;
+            if (mTonePlayer == null) {
+                Log.i(this, "Playing the ringback tone for %s.", call);
+                Log.addEvent(call, START_RINBACK);
+                mTonePlayer = mPlayerFactory.createPlayer(call, InCallTonePlayer.TONE_RING_BACK);
+                mTonePlayer.startTone();
+            }
         }
     }
 
@@ -80,19 +87,27 @@
      * @param call The call for which to stop ringback.
      */
     public void stopRingbackForCall(Call call) {
-        if (mCall == call) {
-            // The foreground call is no longer dialing or is no longer the foreground call. In
-            // either case, stop the ringback tone.
-            mCall = null;
+        synchronized (mLock) {
+            if (mCall == call) {
+                // The foreground call is no longer dialing or is no longer the foreground call. In
+                // either case, stop the ringback tone.
+                mCall = null;
 
-            if (mTonePlayer == null) {
-                Log.w(this, "No player found to stop.");
-            } else {
-                Log.i(this, "Stopping the ringback tone for %s.", call);
-                Log.addEvent(call, STOP_RINGBACK);
-                mTonePlayer.stopTone();
-                mTonePlayer = null;
+                if (mTonePlayer == null) {
+                    Log.w(this, "No player found to stop.");
+                } else {
+                    Log.i(this, "Stopping the ringback tone for %s.", call);
+                    Log.addEvent(call, STOP_RINGBACK);
+                    mTonePlayer.stopTone();
+                    mTonePlayer = null;
+                }
             }
         }
     }
+
+    public boolean isRingbackPlaying() {
+        synchronized (mLock) {
+            return mTonePlayer != null;
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/com/android/server/telecom/Ringer.java b/src/com/android/server/telecom/Ringer.java
index 1710604..e148ef5 100644
--- a/src/com/android/server/telecom/Ringer.java
+++ b/src/com/android/server/telecom/Ringer.java
@@ -22,10 +22,12 @@
 import static android.provider.Settings.Global.ZEN_MODE_OFF;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.Person;
 import android.content.Context;
+import android.content.res.Resources;
 import android.media.AudioManager;
 import android.media.Ringtone;
 import android.media.VolumeShaper;
@@ -38,13 +40,21 @@
 import android.os.VibrationAttributes;
 import android.os.VibrationEffect;
 import android.os.Vibrator;
+import android.os.vibrator.persistence.ParsedVibration;
+import android.os.vibrator.persistence.VibrationXmlParser;
 import android.telecom.Log;
 import android.telecom.TelecomManager;
+import android.util.Pair;
 import android.view.accessibility.AccessibilityManager;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.telecom.LogUtils.EventTimer;
+import com.android.server.telecom.flags.FeatureFlags;
 
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CountDownLatch;
@@ -59,6 +69,8 @@
  */
 @VisibleForTesting
 public class Ringer {
+    private static final String TAG = "TelecomRinger";
+
     public interface AccessibilityManagerAdapter {
         boolean startFlashNotificationSequence(@NonNull Context context,
                 @AccessibilityManager.FlashNotificationReason int reason);
@@ -84,6 +96,16 @@
     // Used for test to notify the completion of RingerAttributes
     private CountDownLatch mAttributesLatch;
 
+    /**
+     * Delay to be used between consecutive vibrations when a non-repeating vibration effect is
+     * provided by the device.
+     *
+     * <p>If looking to customize the loop delay for a device's ring vibration, the desired repeat
+     * behavior should be encoded directly in the effect specification in the device configuration
+     * rather than changing the here (i.e. in `R.raw.default_ringtone_vibration_effect` resource).
+     */
+    private static int DEFAULT_RING_VIBRATION_LOOP_DELAY_MS = 1000;
+
     private static final long[] PULSE_PRIMING_PATTERN = {0,12,250,12,500}; // priming  + interval
 
     private static final int[] PULSE_PRIMING_AMPLITUDE = {0,255,0,255,0};  // priming  + interval
@@ -96,9 +118,11 @@
     private static final int[] PULSE_RAMPING_AMPLITUDE = {
         77,77,78,79,81,84,87,93,101,114,133,162,205,255,255,0};
 
-    private static final long[] PULSE_PATTERN;
+    @VisibleForTesting
+    public static final long[] PULSE_PATTERN;
 
-    private static final int[] PULSE_AMPLITUDE;
+    @VisibleForTesting
+    public static final int[] PULSE_AMPLITUDE;
 
     private static final int RAMPING_RINGER_VIBRATION_DURATION = 5000;
     private static final int RAMPING_RINGER_DURATION = 10000;
@@ -162,6 +186,7 @@
     private final InCallController mInCallController;
     private final VibrationEffectProxy mVibrationEffectProxy;
     private final boolean mIsHapticPlaybackSupportedByDevice;
+    private final FeatureFlags mFlags;
     /**
      * For unit testing purposes only; when set, {@link #startRinging(Call, boolean)} will complete
      * the future provided by the test using {@link #setBlockOnRingingFuture(CompletableFuture)}.
@@ -207,7 +232,8 @@
             VibrationEffectProxy vibrationEffectProxy,
             InCallController inCallController,
             NotificationManager notificationManager,
-            AccessibilityManagerAdapter accessibilityManagerAdapter) {
+            AccessibilityManagerAdapter accessibilityManagerAdapter,
+            FeatureFlags featureFlags) {
 
         mLock = new Object();
         mSystemSettingsUtil = systemSettingsUtil;
@@ -223,18 +249,15 @@
         mNotificationManager = notificationManager;
         mAccessibilityManagerAdapter = accessibilityManagerAdapter;
 
-        if (mContext.getResources().getBoolean(R.bool.use_simple_vibration_pattern)) {
-            mDefaultVibrationEffect = mVibrationEffectProxy.createWaveform(SIMPLE_VIBRATION_PATTERN,
-                    SIMPLE_VIBRATION_AMPLITUDE, REPEAT_SIMPLE_VIBRATION_AT);
-        } else {
-            mDefaultVibrationEffect = mVibrationEffectProxy.createWaveform(PULSE_PATTERN,
-                    PULSE_AMPLITUDE, REPEAT_VIBRATION_AT);
-        }
+        mDefaultVibrationEffect =
+                loadDefaultRingVibrationEffect(
+                        mContext, mVibrator, mVibrationEffectProxy, featureFlags);
 
         mIsHapticPlaybackSupportedByDevice =
                 mSystemSettingsUtil.isHapticPlaybackSupported(mContext);
 
         mAudioManager = mContext.getSystemService(AudioManager.class);
+        mFlags = featureFlags;
     }
 
     @VisibleForTesting
@@ -391,18 +414,18 @@
                         isVibratorEnabled, mIsHapticPlaybackSupportedByDevice);
             }
             // Defer ringtone creation to the async player thread.
-            Supplier<Ringtone> ringtoneSupplier;
+            Supplier<Pair<Uri, Ringtone>> ringtoneInfoSupplier;
             final boolean finalHapticChannelsMuted = hapticChannelsMuted;
             if (isHapticOnly) {
                 if (hapticChannelsMuted) {
                     Log.i(this,
                             "want haptic only ringtone but haptics are muted, skip ringtone play");
-                    ringtoneSupplier = null;
+                    ringtoneInfoSupplier = null;
                 } else {
-                    ringtoneSupplier = mRingtoneFactory::getHapticOnlyRingtone;
+                    ringtoneInfoSupplier = mRingtoneFactory::getHapticOnlyRingtone;
                 }
             } else {
-                ringtoneSupplier = () -> mRingtoneFactory.getRingtone(
+                ringtoneInfoSupplier = () -> mRingtoneFactory.getRingtone(
                         foregroundCall, mVolumeShaperConfig, finalHapticChannelsMuted);
             }
 
@@ -425,9 +448,18 @@
             // if the loaded ringtone is null. However if a stop event arrives before the ringtone
             // creation finishes, then this consumer can be skipped.
             final boolean finalUseCustomVibrationEffect = useCustomVibrationEffect;
-            BiConsumer<Ringtone, Boolean> afterRingtoneLogic =
-                    (Ringtone ringtone, Boolean stopped) -> {
+            BiConsumer<Pair<Uri, Ringtone>, Boolean> afterRingtoneLogic =
+                    (Pair<Uri, Ringtone> ringtoneInfo, Boolean stopped) -> {
                 try {
+                    Uri ringtoneUri = null;
+                    Ringtone ringtone = null;
+                    if (ringtoneInfo != null) {
+                        ringtoneUri = ringtoneInfo.first;
+                        ringtone = ringtoneInfo.second;
+                    } else {
+                        Log.w(this, "The ringtone could not be loaded.");
+                    }
+
                     if (stopped.booleanValue() || !vibratorReserved) {
                         // don't start vibration if the ringing is already abandoned, or the
                         // vibrator wasn't reserved. This still triggers the mBlockOnRingingFuture.
@@ -438,7 +470,7 @@
                         if (DEBUG_RINGER) {
                             Log.d(this, "Using ringtone defined vibration effect.");
                         }
-                        vibrationEffect = getVibrationEffectForRingtone(ringtone);
+                        vibrationEffect = getVibrationEffectForRingtone(ringtoneUri);
                     } else {
                         vibrationEffect = mDefaultVibrationEffect;
                     }
@@ -455,10 +487,10 @@
                 }
             };
             deferBlockOnRingingFuture = true;  // Run in vibrationLogic.
-            if (ringtoneSupplier != null) {
-                mRingtonePlayer.play(ringtoneSupplier, afterRingtoneLogic);
+            if (ringtoneInfoSupplier != null) {
+                mRingtonePlayer.play(ringtoneInfoSupplier, afterRingtoneLogic, isHfpDeviceAttached);
             } else {
-                afterRingtoneLogic.accept(/* ringtone= */ null, /* stopped= */ false);
+                afterRingtoneLogic.accept(/* ringtoneUri, ringtone = */ null, /* stopped= */ false);
             }
 
             // shouldAcquireAudioFocus is meant to be true, but that check is deferred to here
@@ -520,8 +552,7 @@
         }
     }
 
-    private VibrationEffect getVibrationEffectForRingtone(@NonNull Ringtone ringtone) {
-        Uri ringtoneUri = ringtone.getUri();
+    private VibrationEffect getVibrationEffectForRingtone(Uri ringtoneUri) {
         if (ringtoneUri == null) {
             return mDefaultVibrationEffect;
         }
@@ -570,7 +601,7 @@
             Log.addEvent(call, LogUtils.Events.START_CALL_WAITING_TONE, reason);
             mCallWaitingCall = call;
             mCallWaitingPlayer =
-                    mPlayerFactory.createPlayer(InCallTonePlayer.TONE_CALL_WAITING);
+                    mPlayerFactory.createPlayer(call, InCallTonePlayer.TONE_CALL_WAITING);
             mCallWaitingPlayer.startTone();
         }
     }
@@ -629,14 +660,21 @@
             Log.i(this, "shouldRingForContact: returning computation from DndCallFilter.");
             return !call.isCallSuppressedByDoNotDisturb();
         }
-        final Uri contactUri = call.getHandle();
-        final Bundle peopleExtras = new Bundle();
-        if (contactUri != null) {
-            ArrayList<Person> personList = new ArrayList<>();
-            personList.add(new Person.Builder().setUri(contactUri.toString()).build());
-            peopleExtras.putParcelableArrayList(Notification.EXTRA_PEOPLE_LIST, personList);
+        Uri contactUri = call.getHandle();
+        if (mFlags.telecomResolveHiddenDependencies()) {
+            if (contactUri == null) {
+                contactUri = Uri.EMPTY;
+            }
+            return mNotificationManager.matchesCallFilter(contactUri);
+        } else {
+            final Bundle peopleExtras = new Bundle();
+            if (contactUri != null) {
+                ArrayList<Person> personList = new ArrayList<>();
+                personList.add(new Person.Builder().setUri(contactUri.toString()).build());
+                peopleExtras.putParcelableArrayList(Notification.EXTRA_PEOPLE_LIST, personList);
+            }
+            return mNotificationManager.matchesCallFilter(peopleExtras);
         }
-        return mNotificationManager.matchesCallFilter(peopleExtras);
     }
 
     private boolean hasExternalRinger(Call foregroundCall) {
@@ -757,4 +795,65 @@
             return false;
         }
     }
+
+    @Nullable
+    private static VibrationEffect loadSerializedDefaultRingVibration(
+            Resources resources, Vibrator vibrator) {
+        try {
+            InputStream vibrationInputStream =
+                    resources.openRawResource(
+                            com.android.internal.R.raw.default_ringtone_vibration_effect);
+            ParsedVibration parsedVibration = VibrationXmlParser
+                    .parseDocument(
+                            new InputStreamReader(vibrationInputStream, StandardCharsets.UTF_8));
+            if (parsedVibration == null) {
+                Log.w(TAG, "Got null parsed default ring vibration effect.");
+                return null;
+            }
+            return parsedVibration.resolve(vibrator);
+        } catch (IOException | Resources.NotFoundException e) {
+            Log.e(TAG, e, "Error parsing default ring vibration effect.");
+            return null;
+        }
+    }
+
+    private static VibrationEffect loadDefaultRingVibrationEffect(
+            Context context,
+            Vibrator vibrator,
+            VibrationEffectProxy vibrationEffectProxy,
+            FeatureFlags featureFlags) {
+        Resources resources = context.getResources();
+
+        if (resources.getBoolean(R.bool.use_simple_vibration_pattern)) {
+            Log.i(TAG, "Using simple default ring vibration.");
+            return createSimpleRingVibration(vibrationEffectProxy);
+        }
+
+        if (featureFlags.useDeviceProvidedSerializedRingerVibration()) {
+            VibrationEffect parsedEffect = loadSerializedDefaultRingVibration(resources, vibrator);
+            if (parsedEffect != null) {
+                Log.i(TAG, "Using parsed default ring vibration.");
+                // Make the parsed effect repeating to make it vibrate continuously during ring.
+                // If the effect is already repeating, this API call is a no-op.
+                // Otherwise, it  uses `DEFAULT_RING_VIBRATION_LOOP_DELAY_MS` when changing a
+                // non-repeating vibration to a repeating vibration.
+                // This is so that we ensure consecutive loops of the vibration play with some gap
+                // in between.
+                return parsedEffect.applyRepeatingIndefinitely(
+                        /* wantRepeating= */ true, DEFAULT_RING_VIBRATION_LOOP_DELAY_MS);
+            }
+            // Fallback to the simple vibration if the serialized effect cannot be loaded.
+            return createSimpleRingVibration(vibrationEffectProxy);
+        }
+
+        Log.i(TAG, "Using pulse default ring vibration.");
+        return vibrationEffectProxy.createWaveform(
+                PULSE_PATTERN, PULSE_AMPLITUDE, REPEAT_VIBRATION_AT);
+    }
+
+    private static VibrationEffect createSimpleRingVibration(
+            VibrationEffectProxy vibrationEffectProxy) {
+        return vibrationEffectProxy.createWaveform(SIMPLE_VIBRATION_PATTERN,
+                SIMPLE_VIBRATION_AMPLITUDE, REPEAT_SIMPLE_VIBRATION_AT);
+    }
 }
diff --git a/src/com/android/server/telecom/RingtoneFactory.java b/src/com/android/server/telecom/RingtoneFactory.java
index 6bcfb4c..16fa0c4 100644
--- a/src/com/android/server/telecom/RingtoneFactory.java
+++ b/src/com/android/server/telecom/RingtoneFactory.java
@@ -33,7 +33,10 @@
 import android.text.TextUtils;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.telecom.flags.FeatureFlags;
+
 import android.telecom.CallerInfo;
+import android.util.Pair;
 
 import java.util.List;
 
@@ -47,24 +50,15 @@
 
     private final Context mContext;
     private final CallsManager mCallsManager;
+    private FeatureFlags mFeatureFlags;
 
-    public RingtoneFactory(CallsManager callsManager, Context context) {
+    public RingtoneFactory(CallsManager callsManager, Context context, FeatureFlags featureFlags) {
         mContext = context;
         mCallsManager = callsManager;
+        mFeatureFlags = featureFlags;
     }
 
-    /**
-     * Determines if a ringtone has haptic channels.
-     * @param ringtone The ringtone URI.
-     * @return {@code true} if there is a haptic channel, {@code false} otherwise.
-     */
-    public boolean hasHapticChannels(Ringtone ringtone) {
-        boolean hasHapticChannels = RingtoneManager.hasHapticChannels(ringtone.getUri());
-        Log.i(this, "hasHapticChannels %s -> %b", ringtone.getUri(), hasHapticChannels);
-        return hasHapticChannels;
-    }
-
-    public Ringtone getRingtone(Call incomingCall,
+    public Pair<Uri, Ringtone> getRingtone(Call incomingCall,
             @Nullable VolumeShaper.Configuration volumeShaperConfig, boolean hapticChannelsMuted) {
         // Initializing ringtones on the main thread can deadlock
         ThreadUtil.checkNotOnMainThread();
@@ -92,8 +86,12 @@
             // Contact didn't specify ringtone or custom Ringtone creation failed. Get default
             // ringtone for user or profile.
             Context contextToUse = hasDefaultRingtoneForUser(userContext) ? userContext : mContext;
+            UserManager um = contextToUse.getSystemService(UserManager.class);
+            boolean isUserUnlocked = mFeatureFlags.telecomResolveHiddenDependencies()
+                    ? um.isUserUnlocked(contextToUse.getUser())
+                    : um.isUserUnlocked(contextToUse.getUserId());
             Uri defaultRingtoneUri;
-            if (UserManager.get(contextToUse).isUserUnlocked(contextToUse.getUserId())) {
+            if (isUserUnlocked) {
                 defaultRingtoneUri = RingtoneManager.getActualDefaultRingtoneUri(contextToUse,
                         RingtoneManager.TYPE_RINGTONE);
                 if (defaultRingtoneUri == null) {
@@ -106,18 +104,19 @@
                 }
             }
 
-            if (defaultRingtoneUri == null) {
+            ringtoneUri = defaultRingtoneUri;
+            if (ringtoneUri == null) {
                 return null;
             }
 
             try {
                 ringtone = RingtoneManager.getRingtone(
-                        contextToUse, defaultRingtoneUri, volumeShaperConfig, audioAttrs);
+                        contextToUse, ringtoneUri, volumeShaperConfig, audioAttrs);
             } catch (Exception e) {
                 Log.e(this, e, "getRingtone: exception while getting ringtone.");
             }
         }
-        return ringtone;
+        return new Pair(ringtoneUri, ringtone);
     }
 
     private AudioAttributes getDefaultRingtoneAudioAttributes(boolean hapticChannelsMuted) {
@@ -130,7 +129,7 @@
 
     /** Returns a ringtone to be used when ringer is not audible for the incoming call. */
     @Nullable
-    public Ringtone getHapticOnlyRingtone() {
+    public Pair<Uri, Ringtone> getHapticOnlyRingtone() {
         // Initializing ringtones on the main thread can deadlock
         ThreadUtil.checkNotOnMainThread();
         Uri ringtoneUri = Uri.parse("file://" + mContext.getString(
@@ -138,31 +137,47 @@
         AudioAttributes audioAttrs = getDefaultRingtoneAudioAttributes(
             /* hapticChannelsMuted */ false);
         Ringtone ringtone = RingtoneManager.getRingtone(
-            mContext, ringtoneUri, /* volumeShaperConfig */ null, audioAttrs);
+                mContext, ringtoneUri, /* volumeShaperConfig */ null, audioAttrs);
         if (ringtone != null) {
             // Make sure the sound is muted.
             ringtone.setVolume(0);
         }
-        return ringtone;
+        return new Pair(ringtoneUri, ringtone);
     }
 
     private Context getWorkProfileContextForUser(UserHandle userHandle) {
-        // UserManager.getEnabledProfiles returns the enabled profiles along with the user's handle
-        // itself (so we must filter out the user).
-        List<UserInfo> profiles = UserManager.get(mContext).getEnabledProfiles(
-                userHandle.getIdentifier());
-        UserInfo workprofile = null;
+        // UserManager.getUserProfiles returns the enabled profiles along with the context user's
+        // handle itself (so we must filter out the user).
+        Context userContext = mContext.createContextAsUser(userHandle, 0);
+        UserManager um = mFeatureFlags.telecomResolveHiddenDependencies()
+                ? userContext.getSystemService(UserManager.class)
+                : mContext.getSystemService(UserManager.class);
+        List<UserHandle> profiles = um.getUserProfiles();
+        List<UserInfo> userInfoProfiles = um.getEnabledProfiles(userHandle.getIdentifier());
+        UserHandle workProfileUser = null;
         int managedProfileCount = 0;
-        for (UserInfo profile : profiles) {
-            UserHandle profileUserHandle = profile.getUserHandle();
-            if (profileUserHandle != userHandle && profile.isManagedProfile()) {
-                managedProfileCount++;
-                workprofile = profile;
+
+        if (mFeatureFlags.telecomResolveHiddenDependencies()) {
+            for (UserHandle profileUser : profiles) {
+                UserManager userManager = mContext.createContextAsUser(profileUser, 0)
+                        .getSystemService(UserManager.class);
+                if (!userHandle.equals(profileUser) && userManager.isManagedProfile()) {
+                    managedProfileCount++;
+                    workProfileUser = profileUser;
+                }
+            }
+        } else {
+            for(UserInfo profile: userInfoProfiles) {
+                UserHandle profileUserHandle = profile.getUserHandle();
+                if (!profileUserHandle.equals(userHandle) && profile.isManagedProfile()) {
+                    managedProfileCount++;
+                    workProfileUser = profileUserHandle;
+                }
             }
         }
         // There may be many different types of profiles, so only count Managed (Work) Profiles.
         if(managedProfileCount == 1) {
-            return getContextForUserHandle(workprofile.getUserHandle());
+            return getContextForUserHandle(workProfileUser);
         }
         // There are multiple managed profiles for the associated user and we do not have enough
         // info to determine which profile is the work profile. Just use the default.
diff --git a/src/com/android/server/telecom/RoleManagerAdapter.java b/src/com/android/server/telecom/RoleManagerAdapter.java
index 8fdfb11..1b5c71b 100644
--- a/src/com/android/server/telecom/RoleManagerAdapter.java
+++ b/src/com/android/server/telecom/RoleManagerAdapter.java
@@ -54,7 +54,7 @@
     /**
      * Returns the package name of the app which fills the {@link android.app.role.RoleManager} call
      * screening role.
-     * @return the package name of the app filling the role, {@code null} otherwise}.
+     * @return the package name of the app filling the role, {@code null} otherwise.
      */
     String getDefaultCallScreeningApp(UserHandle userHandle);
 
@@ -67,9 +67,25 @@
     void setTestDefaultCallScreeningApp(String packageName);
 
     /**
+     * Returns the package name of the package which fills the {@link android.app.role.RoleManager}
+     * bt in-call service role.
+     * @return the package name of the package filling the role, {@code null} otherwise.
+     */
+    String[] getBTInCallService();
+
+    /**
+     * Override the {@link android.app.role.RoleManager} bt in-call service package with another
+     * value.
+     * Used for testing purposes only.
+     * @param packageName Package name of the package to fill the bt in-call service role. Where
+     *                    {@code null}, the override is removed.
+     */
+    void setTestBTInCallService(String packageName);
+
+    /**
      * Returns the package name of the app which fills the {@link android.app.role.RoleManager}
      * {@link android.app.role.RoleManager#ROLE_DIALER} role.
-     * @return the package name of the app filling the role, {@code null} otherwise}.
+     * @return the package name of the app filling the role, {@code null} otherwise.
      */
     String getDefaultDialerApp(int user);
 
diff --git a/src/com/android/server/telecom/RoleManagerAdapterImpl.java b/src/com/android/server/telecom/RoleManagerAdapterImpl.java
index e8f5604..55326e8 100644
--- a/src/com/android/server/telecom/RoleManagerAdapterImpl.java
+++ b/src/com/android/server/telecom/RoleManagerAdapterImpl.java
@@ -41,6 +41,7 @@
     private String mOverrideDefaultCallScreeningApp = null;
     private String mOverrideDefaultDialerApp = null;
     private List<String> mOverrideCallCompanionApps = new ArrayList<>();
+    private String mOverrideBTInCallService = null;
     private Context mContext;
     private RoleManager mRoleManager;
     private UserHandle mCurrentUserHandle;
@@ -77,6 +78,20 @@
     }
 
     @Override
+    public String[] getBTInCallService() {
+        if (mOverrideBTInCallService != null) {
+            return new String [] {mOverrideBTInCallService};
+        }
+        return getBluetoothInCallServicePackageName();
+    }
+
+    @Override
+    public void setTestBTInCallService(String packageName) {
+        mOverrideBTInCallService = packageName;
+    }
+
+
+    @Override
     public String getDefaultDialerApp(int user) {
         if (mOverrideDefaultDialerApp != null) {
             return mOverrideDefaultDialerApp;
@@ -151,6 +166,10 @@
         return roleHolders.get(0);
     }
 
+    private String[] getBluetoothInCallServicePackageName() {
+        return mContext.getResources().getStringArray(R.array.system_bluetooth_stack_package_name);
+    }
+
     /**
      * Returns the application label that corresponds to the given package name
      *
diff --git a/src/com/android/server/telecom/ServiceBinder.java b/src/com/android/server/telecom/ServiceBinder.java
index 7274993..a18042b 100644
--- a/src/com/android/server/telecom/ServiceBinder.java
+++ b/src/com/android/server/telecom/ServiceBinder.java
@@ -29,6 +29,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.Preconditions;
+import com.android.server.telecom.flags.FeatureFlags;
 
 import java.util.Collections;
 import java.util.Set;
@@ -240,6 +241,8 @@
      * Abbreviated form of the package name from {@link #mComponentName}; used for session logging.
      */
     protected final String mPackageAbbreviation;
+    protected final FeatureFlags mFlags;
+
 
     /** The set of callbacks waiting for notification of the binding's success or failure. */
     private final Set<BindCallback> mCallbacks = new ArraySet<>();
@@ -282,7 +285,7 @@
      * @param userHandle The {@link UserHandle} to use for binding.
      */
     protected ServiceBinder(String serviceAction, ComponentName componentName, Context context,
-            TelecomSystem.SyncRoot lock, UserHandle userHandle) {
+            TelecomSystem.SyncRoot lock, UserHandle userHandle, FeatureFlags featureFlags) {
         Preconditions.checkState(!TextUtils.isEmpty(serviceAction));
         Preconditions.checkNotNull(componentName);
 
@@ -292,6 +295,7 @@
         mComponentName = componentName;
         mPackageAbbreviation = Log.getPackageAbbreviation(componentName);
         mUserHandle = userHandle;
+        mFlags = featureFlags;
     }
 
     final UserHandle getUserHandle() {
@@ -305,10 +309,16 @@
     }
 
     final void decrementAssociatedCallCount() {
-        decrementAssociatedCallCount(false /*isSuppressingUnbind*/);
+        if (mFlags.updatedRcsCallCountTracking()) {
+            decrementAssociatedCallCountUpdated();
+        } else {
+            decrementAssociatedCallCount(false /*isSuppressingUnbind*/);
+        }
     }
 
     final void decrementAssociatedCallCount(boolean isSuppressingUnbind) {
+        // This is the legacy method - will be removed after the Flags.updatedRcsCallCountTracking
+        // mendel study completes.
         if (mAssociatedCallCount > 0) {
             mAssociatedCallCount--;
             Log.v(this, "Call count decrement %d, %s", mAssociatedCallCount,
@@ -323,6 +333,21 @@
         }
     }
 
+    final void decrementAssociatedCallCountUpdated() {
+        if (mAssociatedCallCount > 0) {
+            mAssociatedCallCount--;
+            Log.i(this, "Call count decrement %d, %s", mAssociatedCallCount,
+                    mComponentName.flattenToShortString());
+
+            if (mAssociatedCallCount == 0) {
+                unbind();
+            }
+        } else {
+            Log.wtf(this, "%s: ignoring a request to decrement mAssociatedCallCount below zero",
+                    mComponentName.getClassName());
+        }
+    }
+
     final int getAssociatedCallCount() {
         return mAssociatedCallCount;
     }
diff --git a/src/com/android/server/telecom/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java
index f33b185..20320f2 100644
--- a/src/com/android/server/telecom/TelecomServiceImpl.java
+++ b/src/com/android/server/telecom/TelecomServiceImpl.java
@@ -53,13 +53,18 @@
 import android.os.Build;
 import android.os.Bundle;
 import android.os.OutcomeReceiver;
+import android.os.ParcelFileDescriptor;
 import android.os.Process;
 import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.os.ShellCallback;
 import android.os.UserHandle;
 import android.provider.BlockedNumberContract;
+import android.provider.BlockedNumbersManager;
 import android.provider.Settings;
 import android.telecom.CallAttributes;
 import android.telecom.CallException;
+import android.telecom.DisconnectCause;
 import android.telecom.Log;
 import android.telecom.PhoneAccount;
 import android.telecom.PhoneAccountHandle;
@@ -72,13 +77,16 @@
 import android.util.EventLog;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.telecom.ICallControl;
 import com.android.internal.telecom.ICallEventCallback;
 import com.android.internal.telecom.ITelecomService;
 import com.android.internal.util.IndentingPrintWriter;
+import com.android.modules.utils.BasicShellCommandHandler;
 import com.android.server.telecom.components.UserCallIntentProcessorFactory;
+import com.android.server.telecom.flags.FeatureFlags;
 import com.android.server.telecom.settings.BlockedNumbersActivity;
 import com.android.server.telecom.voip.IncomingCallTransaction;
 import com.android.server.telecom.voip.OutgoingCallTransaction;
@@ -88,8 +96,11 @@
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
+import java.lang.reflect.Method;
+import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Locale;
 import java.util.Objects;
 import java.util.Set;
 import java.util.UUID;
@@ -204,11 +215,11 @@
                 switch (callAttributes.getDirection()) {
                     case DIRECTION_OUTGOING:
                         transaction = new OutgoingCallTransaction(callId, mContext, callAttributes,
-                                mCallsManager, extras);
+                                mCallsManager, extras, mFeatureFlags);
                         break;
                     case DIRECTION_INCOMING:
                         transaction = new IncomingCallTransaction(callId, callAttributes,
-                                mCallsManager, extras);
+                                mCallsManager, extras, mFeatureFlags);
                         break;
                     default:
                         throw new IllegalArgumentException(String.format("Invalid Call Direction. "
@@ -236,6 +247,10 @@
                                                 callEventCallback, mCallsManager, call);
 
                         call.setTransactionServiceWrapper(serviceWrapper);
+
+                        if (mFeatureFlags.transactionalVideoState()) {
+                            call.setTransactionalCallSupportsVideoCalling(callAttributes);
+                        }
                         ICallControl clientCallControl = serviceWrapper.getICallControl();
 
                         if (clientCallControl == null) {
@@ -354,9 +369,28 @@
 
         @Override
         public ParceledListSlice<PhoneAccountHandle> getCallCapablePhoneAccounts(
-                boolean includeDisabledAccounts, String callingPackage, String callingFeatureId) {
+                boolean includeDisabledAccounts, String callingPackage,
+                String callingFeatureId, boolean acrossProfiles) {
             try {
                 Log.startSession("TSI.gCCPA", Log.getPackageAbbreviation(callingPackage));
+
+                if (mTelephonyFeatureFlags.workProfileApiSplit()) {
+                    if (acrossProfiles) {
+                        enforceInAppCrossProfilePermission();
+                    }
+
+                    if (includeDisabledAccounts && !canReadPrivilegedPhoneState(
+                            callingPackage, "getCallCapablePhoneAccounts")) {
+                        throw new SecurityException(
+                                "Requires READ_PRIVILEGED_PHONE_STATE permission.");
+                    }
+
+                    if (!includeDisabledAccounts && !canReadPhoneState(callingPackage,
+                            callingFeatureId, "Requires READ_PHONE_STATE permission.")) {
+                        throw new SecurityException("Requires READ_PHONE_STATE permission.");
+                    }
+                }
+
                 if (includeDisabledAccounts &&
                         !canReadPrivilegedPhoneState(
                                 callingPackage, "getCallCapablePhoneAccounts")) {
@@ -368,7 +402,11 @@
                 }
                 synchronized (mLock) {
                     final UserHandle callingUserHandle = Binder.getCallingUserHandle();
-                    boolean crossUserAccess = hasInAppCrossUserPermission();
+                    boolean crossUserAccess = mTelephonyFeatureFlags.workProfileApiSplit()
+                            && !acrossProfiles ? false
+                            : (mTelephonyFeatureFlags.workProfileApiSplit()
+                                    ? hasInAppCrossProfilePermission()
+                                    : hasInAppCrossUserPermission());
                     long token = Binder.clearCallingIdentity();
                     try {
                         return new ParceledListSlice<>(
@@ -574,6 +612,53 @@
         }
 
         @Override
+        public ParceledListSlice<PhoneAccount> getRegisteredPhoneAccounts(String callingPackage,
+                String callingFeatureId) {
+            try {
+                Log.startSession("TSI.gRPA", Log.getPackageAbbreviation(callingPackage));
+                try {
+                    enforceCallingPackage(callingPackage, "getRegisteredPhoneAccounts");
+                } catch (SecurityException se) {
+                    EventLog.writeEvent(0x534e4554, "307609763", Binder.getCallingUid(),
+                            "getRegisteredPhoneAccounts: invalid calling package");
+                    throw se;
+                }
+
+                boolean hasCrossUserAccess = false;
+                try {
+                    enforceInAppCrossUserPermission();
+                    hasCrossUserAccess = true;
+                } catch (SecurityException e) {
+                    // pass through
+                }
+
+                synchronized (mLock) {
+                    final UserHandle callingUserHandle = Binder.getCallingUserHandle();
+                    long token = Binder.clearCallingIdentity();
+                    try {
+                        return new ParceledListSlice<>(
+                                mPhoneAccountRegistrar.getPhoneAccounts(
+                                        0 /* capabilities */,
+                                        0 /* excludedCapabilities */,
+                                        null /* UriScheme */,
+                                        callingPackage,
+                                        true /* includeDisabledAccounts */,
+                                        callingUserHandle,
+                                        hasCrossUserAccess /* crossUserAccess */,
+                                        false /* includeAll */));
+                    } catch (Exception e) {
+                        Log.e(this, e, "getRegisteredPhoneAccounts");
+                        throw e;
+                    } finally {
+                        Binder.restoreCallingIdentity(token);
+                    }
+                }
+            } finally {
+                Log.endSession();
+            }
+        }
+
+        @Override
         public int getAllPhoneAccountsCount() {
             try {
                 Log.startSession("TSI.gAPAC");
@@ -636,6 +721,7 @@
         public ParceledListSlice<PhoneAccountHandle> getAllPhoneAccountHandles() {
             try {
                 Log.startSession("TSI.gAPAH");
+
                 try {
                     enforceModifyPermission(
                             "getAllPhoneAccountHandles requires MODIFY_PHONE_STATE permission.");
@@ -654,7 +740,7 @@
                                 .getAllPhoneAccountHandles(callingUserHandle,
                                         crossUserAccess));
                     } catch (Exception e) {
-                        Log.e(this, e, "getAllPhoneAccounts");
+                        Log.e(this, e, "getAllPhoneAccountsHandles");
                         throw e;
                     } finally {
                         Binder.restoreCallingIdentity(token);
@@ -760,6 +846,9 @@
                         Bundle extras = account.getExtras();
                         if (extras != null
                                 && extras.getBoolean(PhoneAccount.EXTRA_SKIP_CALL_FILTERING)) {
+                            // System apps should be granted the MODIFY_PHONE_STATE permission.
+                            enforceModifyPermission(
+                                    "registerPhoneAccount requires MODIFY_PHONE_STATE permission.");
                             enforceRegisterSkipCallFiltering();
                         }
                         final int callingUid = Binder.getCallingUid();
@@ -781,6 +870,13 @@
                         // Validate the profile boundary of the given image URI.
                         validateAccountIconUserBoundary(account.getIcon());
 
+                        if (mTelephonyFeatureFlags.simultaneousCallingIndications()
+                                && account.hasSimultaneousCallingRestriction()) {
+                            validateSimultaneousCallingPackageNames(
+                                    account.getAccountHandle().getComponentName().getPackageName(),
+                                    account.getSimultaneousCallingRestriction());
+                        }
+
                         final long token = Binder.clearCallingIdentity();
                         try {
                             Log.i(this, "registerPhoneAccount: account=%s",
@@ -902,6 +998,9 @@
                         }
                     }
                     return getTelephonyManager(subId).getVoiceMailNumber();
+                } catch (UnsupportedOperationException ignored) {
+                    Log.w(this, "getVoiceMailNumber: no Telephony");
+                    return null;
                 } catch (Exception e) {
                     Log.e(this, e, "getSubscriptionIdForPhoneAccount");
                     throw e;
@@ -938,6 +1037,9 @@
                                 accountHandle);
                     }
                     return getTelephonyManager(subId).getLine1Number();
+                } catch (UnsupportedOperationException ignored) {
+                    Log.w(this, "getLine1Number: no telephony");
+                    return null;
                 } catch (Exception e) {
                     Log.e(this, e, "getSubscriptionIdForPhoneAccount");
                     throw e;
@@ -1416,8 +1518,13 @@
                         subId = mPhoneAccountRegistrar.getSubscriptionIdForPhoneAccount(
                                 accountHandle);
                     }
-                    retval = getTelephonyManager(subId)
-                            .handlePinMmiForSubscriber(subId, dialString);
+                    try {
+                        retval = getTelephonyManager(subId)
+                                .handlePinMmiForSubscriber(subId, dialString);
+                    } catch (UnsupportedOperationException uoe) {
+                        Log.w(this, "handlePinMmiForPhoneAccount: no telephony");
+                        retval = false;
+                    }
                 } finally {
                     Binder.restoreCallingIdentity(token);
                 }
@@ -1551,6 +1658,29 @@
                             }
                             mCallIntentProcessorAdapter.processIncomingCallIntent(
                                     mCallsManager, intent);
+                            if (mFeatureFlags.earlyBindingToIncallService()) {
+                                PhoneAccount account =
+                                        mPhoneAccountRegistrar.getPhoneAccountUnchecked(
+                                                phoneAccountHandle);
+                                Bundle accountExtra =
+                                        account == null ? new Bundle() : account.getExtras();
+                                PackageManager packageManager = mContext.getPackageManager();
+                                // Start binding to InCallServices for wearable calls that do not
+                                // require call filtering. This is to wake up default dialer earlier
+                                // to mitigate InCallService binding latency.
+                                if (packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)
+                                        && accountExtra != null && accountExtra.getBoolean(
+                                        PhoneAccount.EXTRA_SKIP_CALL_FILTERING,
+                                        false)) {
+                                    if (mFeatureFlags.separatelyBindToBtIncallService()) {
+                                        mCallsManager.getInCallController().bindToBTService(
+                                                null, null);
+                                    }
+                                    // Should be able to run this as is even if above flag is
+                                    // enabled (BT binding should be skipped automatically).
+                                    mCallsManager.getInCallController().bindToServices(null);
+                                }
+                            }
                         } finally {
                             Binder.restoreCallingIdentity(token);
                         }
@@ -1730,11 +1860,12 @@
                     throw new SecurityException("Package " + callingPackage + " is not allowed"
                             + " to start conference call");
                 }
-
+                // Binder is clearing the identity, so we need to keep the store the handle
+                UserHandle currentUserHandle = Binder.getCallingUserHandle();
                 long token = Binder.clearCallingIdentity();
                 try {
                     mCallsManager.startConference(participants, extras, callingPackage,
-                            Binder.getCallingUserHandle());
+                            currentUserHandle);
                 } finally {
                     Binder.restoreCallingIdentity(token);
                 }
@@ -1900,7 +2031,11 @@
                 synchronized (mLock) {
                     long token = Binder.clearCallingIdentity();
                     try {
-                        BlockedNumberContract.SystemContract.endBlockSuppression(mContext);
+                        if (mBlockedNumbersManager != null) {
+                            mBlockedNumbersManager.endBlockSuppression();
+                        } else {
+                            BlockedNumberContract.SystemContract.endBlockSuppression(mContext);
+                        }
                     } finally {
                         Binder.restoreCallingIdentity(token);
                     }
@@ -1943,8 +2078,12 @@
 
             if (args != null && args.length > 0 && Analytics.ANALYTICS_DUMPSYS_ARG.equals(
                     args[0])) {
-                Binder.withCleanCallingIdentity(() ->
-                        Analytics.dumpToEncodedProto(mContext, writer, args));
+                long token = Binder.clearCallingIdentity();
+                try {
+                    Analytics.dumpToEncodedProto(mContext, writer, args);
+                } finally {
+                    Binder.restoreCallingIdentity(token);
+                }
                 return;
             }
 
@@ -1967,6 +2106,16 @@
                 pw.increaseIndent();
                 Analytics.dump(pw);
                 pw.decreaseIndent();
+
+                pw.println("Flag Configurations: ");
+                pw.increaseIndent();
+                reflectAndPrintFlagConfigs(pw);
+                pw.decreaseIndent();
+
+                pw.println("TransactionManager: ");
+                pw.increaseIndent();
+                TransactionManager.getInstance().dump(pw);
+                pw.decreaseIndent();
             }
             if (isTimeLineView) {
                 Log.dumpEventsTimeline(pw);
@@ -1975,6 +2124,48 @@
             }
         }
 
+        @Override
+        public int handleShellCommand(@NonNull ParcelFileDescriptor in,
+                @NonNull ParcelFileDescriptor out, @NonNull ParcelFileDescriptor err,
+                @NonNull String[] args) {
+            return new TelecomShellCommand(this, mContext).exec(this,
+                    in.getFileDescriptor(), out.getFileDescriptor(), err.getFileDescriptor(), args);
+        }
+
+        /**
+         * Print all feature flag configurations that Telecom is using for debugging purposes.
+         */
+        private void reflectAndPrintFlagConfigs(IndentingPrintWriter pw) {
+
+            try {
+                // Look away, a forbidden technique (reflection) is being used to allow us to get
+                // all flag configs without having to add them manually to this method.
+                Method[] methods = FeatureFlags.class.getMethods();
+                int maxLength = Arrays.stream(methods)
+                        .map(Method::getName)
+                        .map(String::length)
+                        .max(Integer::compare)
+                        .get();
+                String format = "\t%s: %-" + maxLength + "s %s";
+
+                if (methods.length == 0) {
+                    pw.println("NONE");
+                    return;
+                }
+
+                for (Method m : methods) {
+                    String flagEnabled = (Boolean) m.invoke(mFeatureFlags) ? "[✅]": "[❌]";
+                    String methodName = m.getName();
+                    String camelCaseName = methodName.replaceAll("([a-z])([A-Z]+)", "$1_$2")
+                            .toLowerCase(Locale.US);
+                    pw.println(String.format(format, flagEnabled, methodName, camelCaseName));
+                }
+            } catch (Exception e) {
+                pw.println("[ERROR]");
+            }
+
+        }
+
         /**
          * @see android.telecom.TelecomManager#createManageBlockedNumbersIntent
          */
@@ -2138,7 +2329,7 @@
                     try {
                         Log.i(this, "handleCallIntent: handling call intent");
                         mCallIntentProcessorAdapter.processOutgoingCallIntent(mContext,
-                                mCallsManager, intent, callingPackage);
+                                mCallsManager, intent, callingPackage, mFeatureFlags);
                     } finally {
                         Binder.restoreCallingIdentity(token);
                     }
@@ -2149,14 +2340,10 @@
         }
 
         /**
-         * A method intended for use in testing to clean up any calls that get stuck in the
-         * {@link CallState#DISCONNECTED} or {@link CallState#DISCONNECTING} states. Stuck
-         * calls
-         * during CTS cause cascading failures, so if the CTS test detects such a state, it
-         * should
-         * call this method via a shell command to clean up before moving on to the next
-         * test.
-         * Also cleans up any pending futures related to
+         * A method intended for use in testing to clean up any calls are ongoing. Stuck
+         * calls during CTS cause cascading failures, so if the CTS test detects such a state, it
+         * should call this method via a shell command to clean up before moving on to the next
+         * test. Also cleans up any pending futures related to
          * {@link android.telecom.CallDiagnosticService}s.
          */
         @Override
@@ -2165,26 +2352,42 @@
             try {
                 synchronized (mLock) {
                     enforceShellOnly(Binder.getCallingUid(), "cleanupStuckCalls");
-                    Binder.withCleanCallingIdentity(() -> {
+                    long token = Binder.clearCallingIdentity();
+                    try {
                         Set<UserHandle> userHandles = new HashSet<>();
                         for (Call call : mCallsManager.getCalls()) {
-                            call.cleanup();
-                            if (call.getState() == CallState.DISCONNECTED
-                                    || call.getState() == CallState.DISCONNECTING) {
-                                mCallsManager.markCallAsRemoved(call);
+                            // Any call that is not in a disconnect* state should be moved to the
+                            // disconnected state
+                            if (!isDisconnectingOrDisconnected(call)) {
+                                mCallsManager.markCallAsDisconnected(
+                                        call,
+                                        new DisconnectCause(DisconnectCause.OTHER,
+                                                "cleaning up stuck calls"));
                             }
+                            // ensure the call is immediately removed from CallsManager instead of
+                            // using a Future to do the work.
+                            call.cleanup();
+                            // finally, officially remove the call from CallsManager tracking
+                            mCallsManager.markCallAsRemoved(call);
                             userHandles.add(call.getAssociatedUser());
                         }
                         for (UserHandle userHandle : userHandles) {
                             mCallsManager.getInCallController().unbindFromServices(userHandle);
                         }
-                    });
+                    } finally {
+                        Binder.restoreCallingIdentity(token);
+                    }
                 }
             } finally {
                 Log.endSession();
             }
         }
 
+        private boolean isDisconnectingOrDisconnected(Call call){
+            return call.getState() == CallState.DISCONNECTED
+                    || call.getState() == CallState.DISCONNECTING;
+        }
+
         /**
          * A method intended for test to clean up orphan {@link PhoneAccount}. An orphan
          * {@link PhoneAccount} is a phone account belongs to an invalid {@link UserHandle}
@@ -2212,6 +2415,39 @@
         }
 
         /**
+         * A method intended for use in testing to query whether a particular non-ui inCallService
+         * is bound in a call.
+         * @param packageName of the service to query.
+         * @return whether it is bound or not.
+         */
+        @Override
+        public boolean isNonUiInCallServiceBound(String packageName) {
+            Log.startSession("TCI.iNUICSB");
+            try {
+                synchronized (mLock) {
+                    enforceShellOnly(Binder.getCallingUid(), "isNonUiInCallServiceBound");
+                    if (!(mContext.checkCallingOrSelfPermission(READ_PHONE_STATE)
+                            == PackageManager.PERMISSION_GRANTED) ||
+                            !(mContext.checkCallingOrSelfPermission(READ_PRIVILEGED_PHONE_STATE)
+                                    == PackageManager.PERMISSION_GRANTED)) {
+                        throw new SecurityException("isNonUiInCallServiceBound requires the"
+                                + " READ_PHONE_STATE or READ_PRIVILEGED_PHONE_STATE permission");
+                    }
+                    long token = Binder.clearCallingIdentity();
+                    try {
+                        return mCallsManager
+                                .getInCallController()
+                                .isNonUiInCallServiceBound(packageName);
+                    } finally {
+                        Binder.restoreCallingIdentity(token);
+                    }
+                }
+            } finally {
+                Log.endSession();
+            }
+        }
+
+        /**
          * A method intended for use in testing to reset car mode at all priorities.
          *
          * Runs during setup to avoid cascading failures from failing car mode CTS.
@@ -2222,11 +2458,14 @@
             try {
                 synchronized (mLock) {
                     enforceShellOnly(Binder.getCallingUid(), "resetCarMode");
-                    Binder.withCleanCallingIdentity(() -> {
+                    long token = Binder.clearCallingIdentity();
+                    try {
                         UiModeManager uiModeManager =
                                 mContext.getSystemService(UiModeManager.class);
                         uiModeManager.disableCarMode(UiModeManager.DISABLE_CAR_MODE_ALL_PRIORITIES);
-                    });
+                    } finally {
+                        Binder.restoreCallingIdentity(token);
+                    }
                 }
             } finally {
                 Log.endSession();
@@ -2391,17 +2630,20 @@
         public boolean isInSelfManagedCall(String packageName, UserHandle userHandle,
                 String callingPackage) {
             try {
-                if (Binder.getCallingUid() != Process.SYSTEM_UID) {
-                    throw new SecurityException("Only the system can call this API");
-                }
                 mContext.enforceCallingOrSelfPermission(READ_PRIVILEGED_PHONE_STATE,
                         "READ_PRIVILEGED_PHONE_STATE required.");
+                // Ensure that the caller has the INTERACT_ACROSS_USERS permission if it's trying
+                // to access calls that don't belong to it.
+                if (!Binder.getCallingUserHandle().equals(userHandle)) {
+                    enforceInAppCrossUserPermission();
+                }
 
                 Log.startSession("TSI.iISMC", Log.getPackageAbbreviation(callingPackage));
                 synchronized (mLock) {
                     long token = Binder.clearCallingIdentity();
                     try {
-                        return mCallsManager.isInSelfManagedCall(packageName, userHandle);
+                        return mCallsManager.isInSelfManagedCall(
+                                packageName, userHandle);
                     } finally {
                         Binder.restoreCallingIdentity(token);
                     }
@@ -2478,6 +2720,9 @@
     private final TelecomSystem.SyncRoot mLock;
     private TransactionManager mTransactionManager;
     private final TransactionalServiceRepository mTransactionalServiceRepository;
+    private final BlockedNumbersManager mBlockedNumbersManager;
+    private final FeatureFlags mFeatureFlags;
+    private final com.android.internal.telephony.flags.FeatureFlags mTelephonyFeatureFlags;
 
     public TelecomServiceImpl(
             Context context,
@@ -2488,6 +2733,8 @@
             DefaultDialerCache defaultDialerCache,
             SubscriptionManagerAdapter subscriptionManagerAdapter,
             SettingsSecureAdapter settingsSecureAdapter,
+            FeatureFlags featureFlags,
+            com.android.internal.telephony.flags.FeatureFlags telephonyFeatureFlags,
             TelecomSystem.SyncRoot lock) {
         mContext = context;
         mAppOpsManager = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
@@ -2495,6 +2742,13 @@
         mPackageManager = mContext.getPackageManager();
 
         mCallsManager = callsManager;
+        mFeatureFlags = featureFlags;
+        if (telephonyFeatureFlags != null) {
+            mTelephonyFeatureFlags = telephonyFeatureFlags;
+        } else {
+            mTelephonyFeatureFlags =
+                    new com.android.internal.telephony.flags.FeatureFlagsImpl();
+        }
         mLock = lock;
         mPhoneAccountRegistrar = phoneAccountRegistrar;
         mUserCallIntentProcessorFactory = userCallIntentProcessorFactory;
@@ -2517,6 +2771,9 @@
 
         mTransactionManager = TransactionManager.getInstance();
         mTransactionalServiceRepository = new TransactionalServiceRepository();
+        mBlockedNumbersManager = mFeatureFlags.telecomMainlineBlockedNumbersManager()
+                ? mContext.getSystemService(BlockedNumbersManager.class)
+                : null;
     }
 
     @VisibleForTesting
@@ -2707,6 +2964,7 @@
         int packageUid = -1;
         int callingUid = Binder.getCallingUid();
         PackageManager pm;
+        long token = Binder.clearCallingIdentity();
         try{
             pm = mContext.createContextAsUser(
                     UserHandle.getUserHandleForUid(callingUid), 0).getPackageManager();
@@ -2715,6 +2973,8 @@
             Log.i(this, "callingUidMatchesPackageManagerRecords:"
                             + " createContextAsUser hit exception=[%s]", e.toString());
             return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
         }
         if (pm != null) {
             try {
@@ -2862,12 +3122,24 @@
                         + " INTERACT_ACROSS_USERS permission");
     }
 
+    private void enforceInAppCrossProfilePermission() {
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.INTERACT_ACROSS_PROFILES, "Must be system or have"
+                        + " INTERACT_ACROSS_PROFILES permission");
+    }
+
     private boolean hasInAppCrossUserPermission() {
         return mContext.checkCallingOrSelfPermission(
                 Manifest.permission.INTERACT_ACROSS_USERS)
                 == PackageManager.PERMISSION_GRANTED;
     }
 
+    private boolean hasInAppCrossProfilePermission() {
+        return mContext.checkCallingOrSelfPermission(
+                Manifest.permission.INTERACT_ACROSS_PROFILES)
+                == PackageManager.PERMISSION_GRANTED;
+    }
+
     // to be used for TestApi methods that can only be called with SHELL UID.
     private void enforceShellOnly(int callingUid, String message) {
         if (callingUid == Process.SHELL_UID || callingUid == Process.ROOT_UID) {
@@ -2961,7 +3233,6 @@
                 + " does not meet the requirements to access the phone number");
     }
 
-
     private boolean canReadPrivilegedPhoneState(String callingPackage, String message) {
         // The system/default dialer can always read phone state - so that emergency calls will
         // still work.
@@ -3162,4 +3433,20 @@
             }
         }
     }
+
+    private void validateSimultaneousCallingPackageNames(String appPackageName,
+            Set<PhoneAccountHandle> handles) {
+        for (PhoneAccountHandle handle : handles) {
+            ComponentName name = handle.getComponentName();
+            if (name == null) {
+                throw new IllegalArgumentException("ComponentName is null");
+            }
+            String restrictionPackageName = name.getPackageName();
+            if (!appPackageName.equals(restrictionPackageName)) {
+                throw new SecurityException("The package name of the PhoneAccount does not "
+                        + "match one or more of the package names set in the simultaneous "
+                        + "calling restriction.");
+            }
+        }
+    }
 }
diff --git a/src/com/android/server/telecom/TelecomShellCommand.java b/src/com/android/server/telecom/TelecomShellCommand.java
new file mode 100644
index 0000000..557002c
--- /dev/null
+++ b/src/com/android/server/telecom/TelecomShellCommand.java
@@ -0,0 +1,513 @@
+/*
+ * Copyright (C) 2024 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 android.content.ComponentName;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.sysprop.TelephonyProperties;
+import android.telecom.Log;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+
+import com.android.internal.telecom.ITelecomService;
+import com.android.modules.utils.BasicShellCommandHandler;
+
+import java.util.Arrays;
+import java.util.stream.Collectors;
+
+/**
+ * Implements shell commands sent to telecom using the "adb shell cmd telecom..." command from shell
+ * or CTS.
+ */
+public class TelecomShellCommand extends BasicShellCommandHandler {
+    private static final String CALLING_PACKAGE = TelecomShellCommand.class.getPackageName();
+    private static final String COMMAND_SET_PHONE_ACCOUNT_ENABLED = "set-phone-account-enabled";
+    private static final String COMMAND_SET_PHONE_ACCOUNT_DISABLED = "set-phone-account-disabled";
+    private static final String COMMAND_REGISTER_PHONE_ACCOUNT = "register-phone-account";
+    private static final String COMMAND_SET_USER_SELECTED_OUTGOING_PHONE_ACCOUNT =
+            "set-user-selected-outgoing-phone-account";
+    private static final String COMMAND_REGISTER_SIM_PHONE_ACCOUNT = "register-sim-phone-account";
+    private static final String COMMAND_SET_TEST_CALL_REDIRECTION_APP =
+            "set-test-call-redirection-app";
+    private static final String COMMAND_SET_TEST_CALL_SCREENING_APP = "set-test-call-screening-app";
+    private static final String COMMAND_ADD_OR_REMOVE_CALL_COMPANION_APP =
+            "add-or-remove-call-companion-app";
+    private static final String COMMAND_SET_PHONE_ACCOUNT_SUGGESTION_COMPONENT =
+            "set-phone-acct-suggestion-component";
+    private static final String COMMAND_UNREGISTER_PHONE_ACCOUNT = "unregister-phone-account";
+    private static final String COMMAND_SET_CALL_DIAGNOSTIC_SERVICE = "set-call-diagnostic-service";
+    private static final String COMMAND_SET_DEFAULT_DIALER = "set-default-dialer";
+    private static final String COMMAND_GET_DEFAULT_DIALER = "get-default-dialer";
+    private static final String COMMAND_STOP_BLOCK_SUPPRESSION = "stop-block-suppression";
+    private static final String COMMAND_CLEANUP_STUCK_CALLS = "cleanup-stuck-calls";
+    private static final String COMMAND_CLEANUP_ORPHAN_PHONE_ACCOUNTS =
+            "cleanup-orphan-phone-accounts";
+    private static final String COMMAND_RESET_CAR_MODE = "reset-car-mode";
+    private static final String COMMAND_IS_NON_IN_CALL_SERVICE_BOUND =
+            "is-non-ui-in-call-service-bound";
+
+    /**
+     * Change the system dialer package name if a package name was specified,
+     * Example: adb shell telecom set-system-dialer <PACKAGE>
+     *
+     * Restore it to the default if if argument is "default" or no argument is passed.
+     * Example: adb shell telecom set-system-dialer default
+     */
+    private static final String COMMAND_SET_SYSTEM_DIALER = "set-system-dialer";
+    private static final String COMMAND_GET_SYSTEM_DIALER = "get-system-dialer";
+    private static final String COMMAND_WAIT_ON_HANDLERS = "wait-on-handlers";
+    private static final String COMMAND_SET_SIM_COUNT = "set-sim-count";
+    private static final String COMMAND_GET_SIM_CONFIG = "get-sim-config";
+    private static final String COMMAND_GET_MAX_PHONES = "get-max-phones";
+    private static final String COMMAND_SET_TEST_EMERGENCY_PHONE_ACCOUNT_PACKAGE_FILTER =
+            "set-test-emergency-phone-account-package-filter";
+    /**
+     * Command used to emit a distinct "mark" in the logs.
+     */
+    private static final String COMMAND_LOG_MARK = "log-mark";
+
+    private final Context mContext;
+    private final ITelecomService mTelecomService;
+    private TelephonyManager mTelephonyManager;
+    private UserManager mUserManager;
+
+    public TelecomShellCommand(ITelecomService binder, Context context) {
+        mTelecomService = binder;
+        mContext = context;
+    }
+
+    @Override
+    public int onCommand(String command) {
+        if (command == null || command.isEmpty()) {
+            onHelp();
+            return 0;
+        }
+        try {
+            switch (command) {
+                case COMMAND_SET_PHONE_ACCOUNT_ENABLED:
+                    runSetPhoneAccountEnabled(true);
+                    break;
+                case COMMAND_SET_PHONE_ACCOUNT_DISABLED:
+                    runSetPhoneAccountEnabled(false);
+                    break;
+                case COMMAND_REGISTER_PHONE_ACCOUNT:
+                    runRegisterPhoneAccount();
+                    break;
+                case COMMAND_SET_TEST_CALL_REDIRECTION_APP:
+                    runSetTestCallRedirectionApp();
+                    break;
+                case COMMAND_SET_TEST_CALL_SCREENING_APP:
+                    runSetTestCallScreeningApp();
+                    break;
+                case COMMAND_ADD_OR_REMOVE_CALL_COMPANION_APP:
+                    runAddOrRemoveCallCompanionApp();
+                    break;
+                case COMMAND_SET_PHONE_ACCOUNT_SUGGESTION_COMPONENT:
+                    runSetTestPhoneAcctSuggestionComponent();
+                    break;
+                case COMMAND_SET_CALL_DIAGNOSTIC_SERVICE:
+                    runSetCallDiagnosticService();
+                    break;
+                case COMMAND_REGISTER_SIM_PHONE_ACCOUNT:
+                    runRegisterSimPhoneAccount();
+                    break;
+                case COMMAND_SET_USER_SELECTED_OUTGOING_PHONE_ACCOUNT:
+                    runSetUserSelectedOutgoingPhoneAccount();
+                    break;
+                case COMMAND_UNREGISTER_PHONE_ACCOUNT:
+                    runUnregisterPhoneAccount();
+                    break;
+                case COMMAND_STOP_BLOCK_SUPPRESSION:
+                    runStopBlockSuppression();
+                    break;
+                case COMMAND_CLEANUP_STUCK_CALLS:
+                    runCleanupStuckCalls();
+                    break;
+                case COMMAND_CLEANUP_ORPHAN_PHONE_ACCOUNTS:
+                    runCleanupOrphanPhoneAccounts();
+                    break;
+                case COMMAND_RESET_CAR_MODE:
+                    runResetCarMode();
+                    break;
+                case COMMAND_SET_DEFAULT_DIALER:
+                    runSetDefaultDialer();
+                    break;
+                case COMMAND_GET_DEFAULT_DIALER:
+                    runGetDefaultDialer();
+                    break;
+                case COMMAND_SET_SYSTEM_DIALER:
+                    runSetSystemDialer();
+                    break;
+                case COMMAND_GET_SYSTEM_DIALER:
+                    runGetSystemDialer();
+                    break;
+                case COMMAND_WAIT_ON_HANDLERS:
+                    runWaitOnHandler();
+                    break;
+                case COMMAND_SET_SIM_COUNT:
+                    runSetSimCount();
+                    break;
+                case COMMAND_GET_SIM_CONFIG:
+                    runGetSimConfig();
+                    break;
+                case COMMAND_GET_MAX_PHONES:
+                    runGetMaxPhones();
+                    break;
+                case COMMAND_IS_NON_IN_CALL_SERVICE_BOUND:
+                    runIsNonUiInCallServiceBound();
+                    break;
+                case COMMAND_SET_TEST_EMERGENCY_PHONE_ACCOUNT_PACKAGE_FILTER:
+                    runSetEmergencyPhoneAccountPackageFilter();
+                    break;
+                case COMMAND_LOG_MARK:
+                    runLogMark();
+                    break;
+                default:
+                    return handleDefaultCommands(command);
+            }
+        } catch (Exception e) {
+            getErrPrintWriter().println("Command["+ command + "]: Error: " + e);
+            return -1;
+        }
+        return 0;
+    }
+
+    @Override
+    public void onHelp() {
+        getOutPrintWriter().println("usage: telecom [subcommand] [options]\n"
+                + "usage: telecom set-phone-account-enabled <COMPONENT> <ID> <USER_SN>\n"
+                + "usage: telecom set-phone-account-disabled <COMPONENT> <ID> <USER_SN>\n"
+                + "usage: telecom register-phone-account <COMPONENT> <ID> <USER_SN> <LABEL>\n"
+                + "usage: telecom register-sim-phone-account [-e] <COMPONENT> <ID> <USER_SN>"
+                + " <LABEL>: registers a PhoneAccount with CAPABILITY_SIM_SUBSCRIPTION"
+                + " and optionally CAPABILITY_PLACE_EMERGENCY_CALLS if \"-e\" is provided\n"
+                + "usage: telecom set-user-selected-outgoing-phone-account [-e] <COMPONENT> <ID> "
+                + "<USER_SN>\n"
+                + "usage: telecom set-test-call-redirection-app <PACKAGE>\n"
+                + "usage: telecom set-test-call-screening-app <PACKAGE>\n"
+                + "usage: telecom set-phone-acct-suggestion-component <COMPONENT>\n"
+                + "usage: telecom add-or-remove-call-companion-app <PACKAGE> <1/0>\n"
+                + "usage: telecom register-sim-phone-account <COMPONENT> <ID> <USER_SN>"
+                + " <LABEL> <ADDRESS>\n"
+                + "usage: telecom unregister-phone-account <COMPONENT> <ID> <USER_SN>\n"
+                + "usage: telecom set-call-diagnostic-service <PACKAGE>\n"
+                + "usage: telecom set-default-dialer <PACKAGE>\n"
+                + "usage: telecom get-default-dialer\n"
+                + "usage: telecom get-system-dialer\n"
+                + "usage: telecom wait-on-handlers\n"
+                + "usage: telecom set-sim-count <COUNT>\n"
+                + "usage: telecom get-sim-config\n"
+                + "usage: telecom get-max-phones\n"
+                + "usage: telecom stop-block-suppression: Stop suppressing the blocked number"
+                + " provider after a call to emergency services.\n"
+                + "usage: telecom cleanup-stuck-calls: Clear any disconnected calls that have"
+                + " gotten wedged in Telecom.\n"
+                + "usage: telecom cleanup-orphan-phone-accounts: remove any phone accounts that"
+                + " no longer have a valid UserHandle or accounts that no longer belongs to an"
+                + " installed package.\n"
+                + "usage: telecom set-emer-phone-account-filter <PACKAGE>\n"
+                + "\n"
+                + "telecom set-phone-account-enabled: Enables the given phone account, if it has"
+                + " already been registered with Telecom.\n"
+                + "\n"
+                + "telecom set-phone-account-disabled: Disables the given phone account, if it"
+                + " has already been registered with telecom.\n"
+                + "\n"
+                + "telecom set-call-diagnostic-service: overrides call diagnostic service.\n"
+                + "telecom set-default-dialer: Sets the override default dialer to the given"
+                + " component; this will override whatever the dialer role is set to.\n"
+                + "\n"
+                + "telecom get-default-dialer: Displays the current default dialer.\n"
+                + "\n"
+                + "telecom get-system-dialer: Displays the current system dialer.\n"
+                + "telecom set-system-dialer: Set the override system dialer to the given"
+                + " component. To remove the override, send \"default\"\n"
+                + "\n"
+                + "telecom wait-on-handlers: Wait until all handlers finish their work.\n"
+                + "\n"
+                + "telecom set-sim-count: Set num SIMs (2 for DSDS, 1 for single SIM."
+                + " This may restart the device.\n"
+                + "\n"
+                + "telecom get-sim-config: Get the mSIM config string. \"DSDS\" for DSDS mode,"
+                + " or \"\" for single SIM\n"
+                + "\n"
+                + "telecom get-max-phones: Get the max supported phones from the modem.\n"
+                + "telecom set-test-emergency-phone-account-package-filter <PACKAGE>: sets a"
+                + " package name that will be used for test emergency calls. To clear,"
+                + " send an empty package name. Real emergency calls will still be placed"
+                + " over Telephony.\n"
+                + "telecom log-mark <MESSAGE>: emits a message into the telecom logs.  Useful for "
+                + "testers to indicate where in the logs various test steps take place.\n"
+                + "telecom is-non-ui-in-call-service-bound <PACKAGE>: queries a particular "
+                + "non-ui-InCallService in InCallController to determine if it is bound \n"
+        );
+    }
+    private void runSetPhoneAccountEnabled(boolean enabled) throws RemoteException {
+        final PhoneAccountHandle handle = getPhoneAccountHandleFromArgs();
+        final boolean success =  mTelecomService.enablePhoneAccount(handle, enabled);
+        if (success) {
+            getOutPrintWriter().println("Success - " + handle
+                    + (enabled ? " enabled." : " disabled."));
+        } else {
+            getOutPrintWriter().println("Error - is " + handle + " a valid PhoneAccount?");
+        }
+    }
+
+    private void runRegisterPhoneAccount() throws RemoteException {
+        final PhoneAccountHandle handle = getPhoneAccountHandleFromArgs();
+        final String label = getNextArgRequired();
+        PhoneAccount account = PhoneAccount.builder(handle, label)
+                .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER).build();
+        mTelecomService.registerPhoneAccount(account, CALLING_PACKAGE);
+        getOutPrintWriter().println("Success - " + handle + " registered.");
+    }
+
+    private void runRegisterSimPhoneAccount() throws RemoteException {
+        boolean isEmergencyAccount = false;
+        String opt;
+        while ((opt = getNextOption()) != null) {
+            switch (opt) {
+                case "-e": {
+                    isEmergencyAccount = true;
+                    break;
+                }
+            }
+        }
+        final PhoneAccountHandle handle = getPhoneAccountHandleFromArgs();
+        final String label = getNextArgRequired();
+        final String address = getNextArgRequired();
+        int capabilities = PhoneAccount.CAPABILITY_CALL_PROVIDER
+                | PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION
+                | (isEmergencyAccount ? PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS : 0);
+        PhoneAccount account = PhoneAccount.builder(
+                        handle, label)
+                .setAddress(Uri.parse(address))
+                .setSubscriptionAddress(Uri.parse(address))
+                .setCapabilities(capabilities)
+                .setShortDescription(label)
+                .addSupportedUriScheme(PhoneAccount.SCHEME_TEL)
+                .addSupportedUriScheme(PhoneAccount.SCHEME_VOICEMAIL)
+                .build();
+        mTelecomService.registerPhoneAccount(account, CALLING_PACKAGE);
+        getOutPrintWriter().println("Success - " + handle + " registered.");
+    }
+
+    private void runSetTestCallRedirectionApp() throws RemoteException {
+        final String packageName = getNextArg();
+        mTelecomService.setTestDefaultCallRedirectionApp(packageName);
+    }
+
+    private void runSetTestCallScreeningApp() throws RemoteException {
+        final String packageName = getNextArg();
+        mTelecomService.setTestDefaultCallScreeningApp(packageName);
+    }
+
+    private void runAddOrRemoveCallCompanionApp() throws RemoteException {
+        final String packageName = getNextArgRequired();
+        String isAdded = getNextArgRequired();
+        boolean isAddedBool = "1".equals(isAdded);
+        mTelecomService.addOrRemoveTestCallCompanionApp(packageName, isAddedBool);
+    }
+
+    private void runSetCallDiagnosticService() throws RemoteException {
+        String packageName = getNextArg();
+        if ("default".equals(packageName)) packageName = null;
+        mTelecomService.setTestCallDiagnosticService(packageName);
+        getOutPrintWriter().println("Success - " + packageName
+                + " set as call diagnostic service.");
+    }
+
+    private void runSetTestPhoneAcctSuggestionComponent() throws RemoteException {
+        final String componentName = getNextArg();
+        mTelecomService.setTestPhoneAcctSuggestionComponent(componentName);
+    }
+
+    private void runSetUserSelectedOutgoingPhoneAccount() throws RemoteException {
+        Log.i(this, "runSetUserSelectedOutgoingPhoneAccount");
+        final PhoneAccountHandle handle = getPhoneAccountHandleFromArgs();
+        mTelecomService.setUserSelectedOutgoingPhoneAccount(handle);
+        getOutPrintWriter().println("Success - " + handle + " set as default outgoing account.");
+    }
+
+    private void runUnregisterPhoneAccount() throws RemoteException {
+        final PhoneAccountHandle handle = getPhoneAccountHandleFromArgs();
+        mTelecomService.unregisterPhoneAccount(handle, CALLING_PACKAGE);
+        getOutPrintWriter().println("Success - " + handle + " unregistered.");
+    }
+
+    private void runStopBlockSuppression() throws RemoteException {
+        mTelecomService.stopBlockSuppression();
+    }
+
+    private void runCleanupStuckCalls() throws RemoteException {
+        mTelecomService.cleanupStuckCalls();
+    }
+
+    private void runCleanupOrphanPhoneAccounts() throws RemoteException {
+        getOutPrintWriter().println("Success - cleaned up "
+                + mTelecomService.cleanupOrphanPhoneAccounts()
+                + "  phone accounts.");
+    }
+
+    private void runResetCarMode() throws RemoteException {
+        mTelecomService.resetCarMode();
+    }
+
+    private void runSetDefaultDialer() throws RemoteException {
+        String packageName = getNextArg();
+        if ("default".equals(packageName)) packageName = null;
+        mTelecomService.setTestDefaultDialer(packageName);
+        getOutPrintWriter().println("Success - " + packageName
+                + " set as override default dialer.");
+    }
+
+    private void runSetSystemDialer() throws RemoteException {
+        final String flatComponentName = getNextArg();
+        final ComponentName componentName = (flatComponentName.equals("default")
+                ? null : parseComponentName(flatComponentName));
+        mTelecomService.setSystemDialer(componentName);
+        getOutPrintWriter().println("Success - " + componentName + " set as override system dialer.");
+    }
+
+    private void runGetDefaultDialer() throws RemoteException {
+        getOutPrintWriter().println(mTelecomService.getDefaultDialerPackage(CALLING_PACKAGE));
+    }
+
+    private void runGetSystemDialer() throws RemoteException {
+        getOutPrintWriter().println(mTelecomService.getSystemDialerPackage(CALLING_PACKAGE));
+    }
+
+    private void runWaitOnHandler() throws RemoteException {
+
+    }
+
+    private void runSetSimCount() throws RemoteException {
+        if (!callerIsRoot()) {
+            getOutPrintWriter().println("set-sim-count requires adb root");
+            return;
+        }
+        int numSims = Integer.parseInt(getNextArgRequired());
+        getOutPrintWriter().println("Setting sim count to " + numSims + ". Device may reboot");
+        getTelephonyManager().switchMultiSimConfig(numSims);
+    }
+
+    /**
+     * prints out whether a particular non-ui InCallServices is bound in a call
+     */
+    public void runIsNonUiInCallServiceBound() throws RemoteException {
+        if (TextUtils.isEmpty(peekNextArg())) {
+            getOutPrintWriter().println("No Argument passed. Please pass a <PACKAGE_NAME> to "
+                    + "lookup.");
+        } else {
+            getOutPrintWriter().println(
+                    String.valueOf(mTelecomService.isNonUiInCallServiceBound(getNextArg())));
+        }
+    }
+
+    /**
+     * Prints the mSIM config to the console.
+     * "DSDS" for a phone in DSDS mode
+     * "" (empty string) for a phone in SS mode
+     */
+    private void runGetSimConfig() throws RemoteException {
+        getOutPrintWriter().println(TelephonyProperties.multi_sim_config().orElse(""));
+    }
+
+    private void runGetMaxPhones() throws RemoteException {
+        // how many logical modems can be potentially active simultaneously
+        getOutPrintWriter().println(getTelephonyManager().getSupportedModemCount());
+    }
+
+    private void runSetEmergencyPhoneAccountPackageFilter() throws RemoteException {
+        String packageName = getNextArg();
+        if (TextUtils.isEmpty(packageName)) {
+            mTelecomService.setTestEmergencyPhoneAccountPackageNameFilter(null);
+            getOutPrintWriter().println("Success - filter cleared");
+        } else {
+            mTelecomService.setTestEmergencyPhoneAccountPackageNameFilter(packageName);
+            getOutPrintWriter().println("Success = filter set to " + packageName);
+        }
+
+    }
+
+    private void runLogMark() throws RemoteException {
+        String message = Arrays.stream(peekRemainingArgs()).collect(Collectors.joining(" "));
+        mTelecomService.requestLogMark(message);
+    }
+
+    private PhoneAccountHandle getPhoneAccountHandleFromArgs() throws RemoteException {
+        if (TextUtils.isEmpty(peekNextArg())) {
+            return null;
+        }
+        final ComponentName component = parseComponentName(getNextArgRequired());
+        final String accountId = getNextArgRequired();
+        final String userSnInStr = getNextArgRequired();
+        UserHandle userHandle;
+        try {
+            final int userSn = Integer.parseInt(userSnInStr);
+            userHandle = UserHandle.of(getUserManager().getUserHandle(userSn));
+        } catch (NumberFormatException ex) {
+            Log.w(this, "getPhoneAccountHandleFromArgs - invalid user %s", userSnInStr);
+            throw new IllegalArgumentException ("Invalid user serial number " + userSnInStr);
+        }
+        return new PhoneAccountHandle(component, accountId, userHandle);
+    }
+
+    private boolean callerIsRoot() {
+        return Process.ROOT_UID == Process.myUid();
+    }
+
+    private ComponentName parseComponentName(String component) {
+        ComponentName cn = ComponentName.unflattenFromString(component);
+        if (cn == null) {
+            throw new IllegalArgumentException ("Invalid component " + component);
+        }
+        return cn;
+    }
+
+    private TelephonyManager getTelephonyManager() throws IllegalStateException {
+        if (mTelephonyManager == null) {
+            mTelephonyManager = mContext.getSystemService(TelephonyManager.class);
+        }
+        if (mTelephonyManager == null) {
+            Log.w(this, "getTelephonyManager: Can't access telephony service.");
+            throw new IllegalStateException("Could not access the Telephony Service. Is the system "
+                    + "running?");
+        }
+        return mTelephonyManager;
+    }
+
+    private UserManager getUserManager() throws IllegalStateException {
+        if (mUserManager == null) {
+            mUserManager = mContext.getSystemService(UserManager.class);
+        }
+        if (mUserManager == null) {
+            Log.w(this, "getUserManager: Can't access UserManager service.");
+            throw new IllegalStateException("Could not access the UserManager Service. Is the "
+                    + "system running?");
+        }
+        return mUserManager;
+    }
+}
diff --git a/src/com/android/server/telecom/TelecomSystem.java b/src/com/android/server/telecom/TelecomSystem.java
index 67bb81f..e8cbd55 100644
--- a/src/com/android/server/telecom/TelecomSystem.java
+++ b/src/com/android/server/telecom/TelecomSystem.java
@@ -29,7 +29,6 @@
 import android.os.BugreportManager;
 import android.os.DropBoxManager;
 import android.os.UserHandle;
-import android.os.UserManager;
 import android.telecom.Log;
 import android.telecom.PhoneAccountHandle;
 import android.telephony.AnomalyReporter;
@@ -45,8 +44,10 @@
 import com.android.server.telecom.bluetooth.BluetoothRouteManager;
 import com.android.server.telecom.bluetooth.BluetoothStateReceiver;
 import com.android.server.telecom.callfiltering.BlockedNumbersAdapter;
+import com.android.server.telecom.callfiltering.IncomingCallFilterGraph;
 import com.android.server.telecom.components.UserCallIntentProcessor;
 import com.android.server.telecom.components.UserCallIntentProcessorFactory;
+import com.android.server.telecom.flags.FeatureFlags;
 import com.android.server.telecom.ui.AudioProcessingNotification;
 import com.android.server.telecom.ui.CallStreamingNotification;
 import com.android.server.telecom.ui.DisconnectedCallNotifier;
@@ -137,6 +138,7 @@
     private final TelecomServiceImpl mTelecomServiceImpl;
     private final ContactsAsyncHelper mContactsAsyncHelper;
     private final DialerCodeReceiver mDialerCodeReceiver;
+    private final FeatureFlags mFeatureFlags;
 
     private boolean mIsBootComplete = false;
 
@@ -223,8 +225,12 @@
             DeviceIdleControllerAdapter deviceIdleControllerAdapter,
             Ringer.AccessibilityManagerAdapter accessibilityManagerAdapter,
             Executor asyncTaskExecutor,
-            BlockedNumbersAdapter blockedNumbersAdapter) {
+            Executor asyncCallAudioTaskExecutor,
+            BlockedNumbersAdapter blockedNumbersAdapter,
+            FeatureFlags featureFlags,
+            com.android.internal.telephony.flags.FeatureFlags telephonyFlags) {
         mContext = context.getApplicationContext();
+        mFeatureFlags = featureFlags;
         LogUtils.initLogging(mContext);
         android.telecom.Log.setLock(mLock);
         AnomalyReporter.initialize(mContext);
@@ -239,7 +245,7 @@
         try {
             mPhoneAccountRegistrar = new PhoneAccountRegistrar(mContext, mLock, defaultDialerCache,
                     packageName -> AppLabelProxy.Util.getAppLabel(
-                            mContext.getPackageManager(), packageName));
+                            mContext.getPackageManager(), packageName), null, mFeatureFlags);
 
             mContactsAsyncHelper = contactsAsyncHelperFactory.create(
                     new ContactsAsyncHelper.ContentResolverAdapter() {
@@ -249,13 +255,19 @@
                             return context.getContentResolver().openInputStream(uri);
                         }
                     });
+            CallAudioCommunicationDeviceTracker communicationDeviceTracker = new
+                    CallAudioCommunicationDeviceTracker(mContext);
             BluetoothDeviceManager bluetoothDeviceManager = new BluetoothDeviceManager(mContext,
-                    mContext.getSystemService(BluetoothManager.class).getAdapter());
+                    mContext.getSystemService(BluetoothManager.class).getAdapter(),
+                    communicationDeviceTracker, featureFlags);
             BluetoothRouteManager bluetoothRouteManager = new BluetoothRouteManager(mContext, mLock,
-                    bluetoothDeviceManager, new Timeouts.Adapter());
+                    bluetoothDeviceManager, new Timeouts.Adapter(),
+                    communicationDeviceTracker, featureFlags);
             BluetoothStateReceiver bluetoothStateReceiver = new BluetoothStateReceiver(
-                    bluetoothDeviceManager, bluetoothRouteManager);
+                    bluetoothDeviceManager, bluetoothRouteManager,
+                    communicationDeviceTracker, featureFlags);
             mContext.registerReceiver(bluetoothStateReceiver, BluetoothStateReceiver.INTENT_FILTER);
+            communicationDeviceTracker.setBluetoothRouteManager(bluetoothRouteManager);
 
             WiredHeadsetManager wiredHeadsetManager = new WiredHeadsetManager(mContext);
             SystemStateHelper systemStateHelper = new SystemStateHelper(mContext, mLock);
@@ -263,7 +275,8 @@
             mMissedCallNotifier = missedCallNotifierImplFactory
                     .makeMissedCallNotifierImpl(mContext, mPhoneAccountRegistrar,
                             defaultDialerCache,
-                            deviceIdleControllerAdapter);
+                            deviceIdleControllerAdapter,
+                            featureFlags);
             DisconnectedCallNotifier.Factory disconnectedCallNotifierFactory =
                     new DisconnectedCallNotifier.Default();
 
@@ -272,7 +285,7 @@
                             mContactsAsyncHelper, mLock);
 
             EmergencyCallHelper emergencyCallHelper = new EmergencyCallHelper(mContext,
-                    defaultDialerCache, timeoutsAdapter);
+                    defaultDialerCache, timeoutsAdapter, mFeatureFlags);
 
             InCallControllerFactory inCallControllerFactory = new InCallControllerFactory() {
                 @Override
@@ -282,7 +295,7 @@
                         EmergencyCallHelper emergencyCallHelper) {
                     return new InCallController(context, lock, callsManager, systemStateProvider,
                             defaultDialerCache, timeoutsAdapter, emergencyCallHelper,
-                            new CarModeTracker(), clockProxy);
+                            new CarModeTracker(), clockProxy, featureFlags);
                 }
             };
 
@@ -291,7 +304,7 @@
                 @Override
                 public CallEndpointController create(Context context, SyncRoot lock,
                         CallsManager callsManager) {
-                    return new CallEndpointController(context, callsManager);
+                    return new CallEndpointController(context, callsManager, featureFlags);
                 }
             };
 
@@ -333,15 +346,24 @@
 
             ToastFactory toastFactory = new ToastFactory() {
                 @Override
-                public Toast makeText(Context context, int resId, int duration) {
-                    return Toast.makeText(context, context.getMainLooper(),
-                            context.getString(resId),
-                            duration);
+                public void makeText(Context context, int resId, int duration) {
+                    if (mFeatureFlags.telecomResolveHiddenDependencies()) {
+                        context.getMainExecutor().execute(() ->
+                                Toast.makeText(context, resId, duration).show());
+                    } else {
+                        Toast.makeText(context, context.getMainLooper(),
+                                context.getString(resId), duration).show();
+                    }
                 }
 
                 @Override
-                public Toast makeText(Context context, CharSequence text, int duration) {
-                    return Toast.makeText(context, context.getMainLooper(), text, duration);
+                public void makeText(Context context, CharSequence text, int duration) {
+                    if (mFeatureFlags.telecomResolveHiddenDependencies()) {
+                        context.getMainExecutor().execute(() ->
+                                Toast.makeText(context, text, duration).show());
+                    } else {
+                        Toast.makeText(context, context.getMainLooper(), text, duration).show();
+                    }
                 }
             };
 
@@ -396,10 +418,16 @@
                     callAnomalyWatchdog,
                     accessibilityManagerAdapter,
                     asyncTaskExecutor,
+                    asyncCallAudioTaskExecutor,
                     blockedNumbersAdapter,
                     transactionManager,
                     emergencyCallDiagnosticLogger,
-                    callStreamingNotification);
+                    communicationDeviceTracker,
+                    callStreamingNotification,
+                    bluetoothDeviceManager,
+                    featureFlags,
+                    telephonyFlags,
+                    IncomingCallFilterGraph::new);
 
             mIncomingCallNotifier = incomingCallNotifier;
             incomingCallNotifier.setCallsManagerProxy(new IncomingCallNotifier.CallsManagerProxy() {
@@ -443,7 +471,7 @@
             }
 
             mCallIntentProcessor = new CallIntentProcessor(mContext, mCallsManager,
-                    defaultDialerCache);
+                    defaultDialerCache, featureFlags);
             mTelecomBroadcastIntentProcessor = new TelecomBroadcastIntentProcessor(
                     mContext, mCallsManager);
 
@@ -453,7 +481,6 @@
                     Manifest.permission.CONTROL_INCALL_EXPERIENCE, null);
 
             // There is no USER_SWITCHED broadcast for user 0, handle it here explicitly.
-            final UserManager userManager = UserManager.get(mContext);
             mTelecomServiceImpl = new TelecomServiceImpl(
                     mContext, mCallsManager, mPhoneAccountRegistrar,
                     new CallIntentProcessor.AdapterImpl(defaultDialerCache),
@@ -461,12 +488,14 @@
                         @Override
                         public UserCallIntentProcessor create(Context context,
                                 UserHandle userHandle) {
-                            return new UserCallIntentProcessor(context, userHandle);
+                            return new UserCallIntentProcessor(context, userHandle, featureFlags);
                         }
                     },
                     defaultDialerCache,
                     new TelecomServiceImpl.SubscriptionManagerAdapterImpl(),
                     new TelecomServiceImpl.SettingsSecureAdapterImpl(),
+                    featureFlags,
+                    null,
                     mLock);
         } finally {
             Log.endSession();
@@ -502,4 +531,8 @@
     public boolean isBootComplete() {
         return mIsBootComplete;
     }
+
+    public FeatureFlags getFeatureFlags() {
+        return mFeatureFlags;
+    }
 }
diff --git a/src/com/android/server/telecom/TelephonyUtil.java b/src/com/android/server/telecom/TelephonyUtil.java
index 7eb08d7..0a7dc19 100644
--- a/src/com/android/server/telecom/TelephonyUtil.java
+++ b/src/com/android/server/telecom/TelephonyUtil.java
@@ -74,7 +74,7 @@
             TelephonyManager tm = (TelephonyManager) context.getSystemService(
                     Context.TELEPHONY_SERVICE);
             return handle != null && tm.isEmergencyNumber(handle.getSchemeSpecificPart());
-        } catch (IllegalStateException ise) {
+        } catch (UnsupportedOperationException | IllegalStateException ignored) {
             return false;
         }
     }
diff --git a/src/com/android/server/telecom/Timeouts.java b/src/com/android/server/telecom/Timeouts.java
index c5fdd4c..0ed71df 100644
--- a/src/com/android/server/telecom/Timeouts.java
+++ b/src/com/android/server/telecom/Timeouts.java
@@ -41,6 +41,10 @@
             return Timeouts.getCallScreeningTimeoutMillis(cr);
         }
 
+        public long getCallBindBluetoothInCallServicesDelay(ContentResolver cr) {
+            return Timeouts.getCallBindBluetoothInCallServicesDelay(cr);
+        }
+
         public long getCallRemoveUnbindInCallServicesDelay(ContentResolver cr) {
             return Timeouts.getCallRemoveUnbindInCallServicesDelay(cr);
         }
@@ -57,6 +61,14 @@
             return Timeouts.getEmergencyCallbackWindowMillis(cr);
         }
 
+        public long getEmergencyCallTimeoutMillis(ContentResolver cr) {
+            return Timeouts.getEmergencyCallTimeoutMillis(cr);
+        }
+
+        public long getEmergencyCallTimeoutRadioOffMillis(ContentResolver cr) {
+            return Timeouts.getEmergencyCallTimeoutRadioOffMillis(cr);
+        }
+
         public long getUserDefinedCallRedirectionTimeoutMillis(ContentResolver cr) {
             return Timeouts.getUserDefinedCallRedirectionTimeoutMillis(cr);
         }
@@ -123,7 +135,6 @@
 
         public int getDaysBackToSearchEmergencyDiagnosticEntries(){
             return Timeouts.getDaysBackToSearchEmergencyDiagnosticEntries();
-
         }
     }
 
@@ -270,6 +281,11 @@
                 60000L /* 1 minute */);
     }
 
+    public static long getCallBindBluetoothInCallServicesDelay(ContentResolver contentResolver) {
+        return get(contentResolver, "call_bind_bluetooth_in_call_services_delay",
+                2000L /* 2 seconds */);
+    }
+
     /**
      * Returns the amount of delay before unbinding the in-call services after all the calls
      * are removed.
diff --git a/src/com/android/server/telecom/TransactionalServiceRepository.java b/src/com/android/server/telecom/TransactionalServiceRepository.java
index 15278e1..793840e 100644
--- a/src/com/android/server/telecom/TransactionalServiceRepository.java
+++ b/src/com/android/server/telecom/TransactionalServiceRepository.java
@@ -61,11 +61,11 @@
         return service;
     }
 
-    public TransactionalServiceWrapper getTransactionalServiceWrapper(PhoneAccountHandle pah) {
+    private TransactionalServiceWrapper getTransactionalServiceWrapper(PhoneAccountHandle pah) {
         return mServiceLookupTable.get(pah);
     }
 
-    public boolean hasExistingServiceWrapper(PhoneAccountHandle pah) {
+    private boolean hasExistingServiceWrapper(PhoneAccountHandle pah) {
         return mServiceLookupTable.containsKey(pah);
     }
 
diff --git a/src/com/android/server/telecom/TransactionalServiceWrapper.java b/src/com/android/server/telecom/TransactionalServiceWrapper.java
index 25aaad7..50ef2e8 100644
--- a/src/com/android/server/telecom/TransactionalServiceWrapper.java
+++ b/src/com/android/server/telecom/TransactionalServiceWrapper.java
@@ -21,6 +21,7 @@
 import static android.telecom.TelecomManager.TELECOM_TRANSACTION_SUCCESS;
 
 import android.content.ComponentName;
+import android.os.Binder;
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.OutcomeReceiver;
@@ -43,9 +44,10 @@
 import com.android.server.telecom.voip.HoldCallTransaction;
 import com.android.server.telecom.voip.EndCallTransaction;
 import com.android.server.telecom.voip.MaybeHoldCallForNewCallTransaction;
-import com.android.server.telecom.voip.ParallelTransaction;
 import com.android.server.telecom.voip.RequestNewActiveCallTransaction;
 import com.android.server.telecom.voip.SerialTransaction;
+import com.android.server.telecom.voip.SetMuteStateTransaction;
+import com.android.server.telecom.voip.RequestVideoStateTransaction;
 import com.android.server.telecom.voip.TransactionManager;
 import com.android.server.telecom.voip.VoipCallTransaction;
 import com.android.server.telecom.voip.VoipCallTransactionResult;
@@ -61,7 +63,7 @@
  * on a per-client basis which is tied to a {@link PhoneAccountHandle}
  */
 public class TransactionalServiceWrapper implements
-        ConnectionServiceFocusManager.ConnectionServiceFocus {
+        ConnectionServiceFocusManager.ConnectionServiceFocus, CallSourceService {
     private static final String TAG = TransactionalServiceWrapper.class.getSimpleName();
 
     // CallControl : Client (ex. voip app) --> Telecom
@@ -70,6 +72,7 @@
     public static final String ANSWER = "Answer";
     public static final String DISCONNECT = "Disconnect";
     public static final String START_STREAMING = "StartStreaming";
+    public static final String REQUEST_VIDEO_STATE = "RequestVideoState";
 
     // CallEventCallback : Telecom --> Client (ex. voip app)
     public static final String ON_SET_ACTIVE = "onSetActive";
@@ -129,6 +132,7 @@
         return mTransactionManager;
     }
 
+    @VisibleForTesting
     public PhoneAccountHandle getPhoneAccountHandle() {
         return mPhoneAccountHandle;
     }
@@ -165,7 +169,7 @@
         return callCount;
     }
 
-    public void cleanupTransactionalServiceWrapper() {
+    private void cleanupTransactionalServiceWrapper() {
         for (Call call : mTrackedCalls.values()) {
             mCallsManager.markCallAsDisconnected(call,
                     new DisconnectCause(DisconnectCause.ERROR, "process died"));
@@ -178,14 +182,16 @@
      **                        ICallControl: Client --> Server                                **
      **********************************************************************************************
      */
-    public final ICallControl mICallControl = new ICallControl.Stub() {
+    private final ICallControl mICallControl = new ICallControl.Stub() {
         @Override
         public void setActive(String callId, android.os.ResultReceiver callback)
                 throws RemoteException {
+            long token = Binder.clearCallingIdentity();
             try {
                 Log.startSession("TSW.sA");
                 createTransactions(callId, callback, SET_ACTIVE);
             } finally {
+                Binder.restoreCallingIdentity(token);
                 Log.endSession();
             }
         }
@@ -193,10 +199,12 @@
         @Override
         public void answer(int videoState, String callId, android.os.ResultReceiver callback)
                 throws RemoteException {
+            long token = Binder.clearCallingIdentity();
             try {
                 Log.startSession("TSW.a");
                 createTransactions(callId, callback, ANSWER, videoState);
             } finally {
+                Binder.restoreCallingIdentity(token);
                 Log.endSession();
             }
         }
@@ -204,10 +212,12 @@
         @Override
         public void setInactive(String callId, android.os.ResultReceiver callback)
                 throws RemoteException {
+            long token = Binder.clearCallingIdentity();
             try {
                 Log.startSession("TSW.sI");
                 createTransactions(callId, callback, SET_INACTIVE);
             } finally {
+                Binder.restoreCallingIdentity(token);
                 Log.endSession();
             }
         }
@@ -216,10 +226,26 @@
         public void disconnect(String callId, DisconnectCause disconnectCause,
                 android.os.ResultReceiver callback)
                 throws RemoteException {
+            long token = Binder.clearCallingIdentity();
             try {
                 Log.startSession("TSW.d");
                 createTransactions(callId, callback, DISCONNECT, disconnectCause);
             } finally {
+                Binder.restoreCallingIdentity(token);
+                Log.endSession();
+            }
+        }
+
+        @Override
+        public void setMuteState(boolean isMuted, android.os.ResultReceiver callback)
+                throws RemoteException {
+            long token = Binder.clearCallingIdentity();
+            try {
+                Log.startSession("TSW.sMS");
+                addTransactionsToManager(
+                        new SetMuteStateTransaction(mCallsManager, isMuted), callback);
+            } finally {
+                Binder.restoreCallingIdentity(token);
                 Log.endSession();
             }
         }
@@ -227,10 +253,25 @@
         @Override
         public void startCallStreaming(String callId, android.os.ResultReceiver callback)
                 throws RemoteException {
+            long token = Binder.clearCallingIdentity();
             try {
                 Log.startSession("TSW.sCS");
                 createTransactions(callId, callback, START_STREAMING);
             } finally {
+                Binder.restoreCallingIdentity(token);
+                Log.endSession();
+            }
+        }
+
+        @Override
+        public void requestVideoState(int videoState, String callId, ResultReceiver callback)
+                throws RemoteException {
+            long token = Binder.clearCallingIdentity();
+            try {
+                Log.startSession("TSW.rVS");
+                createTransactions(callId, callback, REQUEST_VIDEO_STATE, videoState);
+            } finally {
+                Binder.restoreCallingIdentity(token);
                 Log.endSession();
             }
         }
@@ -261,6 +302,11 @@
                         addTransactionsToManager(mStreamingController.getStartStreamingTransaction(mCallsManager,
                                 TransactionalServiceWrapper.this, call, mLock), callback);
                         break;
+                    case REQUEST_VIDEO_STATE:
+                        addTransactionsToManager(
+                                new RequestVideoStateTransaction(mCallsManager, call,
+                                        (int) objects[0]), callback);
+                        break;
                 }
             } else {
                 Bundle exceptionBundle = new Bundle();
@@ -279,7 +325,8 @@
         // This request is originating from the VoIP application.
         private void handleCallControlNewCallFocusTransactions(Call call, String action,
                 boolean isAnswer, int potentiallyNewVideoState, ResultReceiver callback) {
-            mTransactionManager.addTransaction(createSetActiveTransactions(call),
+            mTransactionManager.addTransaction(
+                    createSetActiveTransactions(call, true /* isCallControlRequest */),
                     new OutcomeReceiver<>() {
                         @Override
                         public void onResult(VoipCallTransactionResult result) {
@@ -303,11 +350,13 @@
 
         @Override
         public void requestCallEndpointChange(CallEndpoint endpoint, ResultReceiver callback) {
+            long token = Binder.clearCallingIdentity();
             try {
                 Log.startSession("TSW.rCEC");
                 addTransactionsToManager(new EndpointChangeTransaction(endpoint, mCallsManager),
                         callback);
             } finally {
+                Binder.restoreCallingIdentity(token);
                 Log.endSession();
             }
         }
@@ -317,6 +366,7 @@
          */
         @Override
         public void sendEvent(String callId, String event, Bundle extras) {
+            long token = Binder.clearCallingIdentity();
             try {
                 Log.startSession("TSW.sE");
                 Call call = mTrackedCalls.get(callId);
@@ -328,12 +378,13 @@
                                     + "found. Most likely the call has been disconnected");
                 }
             } finally {
+                Binder.restoreCallingIdentity(token);
                 Log.endSession();
             }
         }
     };
 
-    public void addTransactionsToManager(VoipCallTransaction transaction,
+    private void addTransactionsToManager(VoipCallTransaction transaction,
             ResultReceiver callback) {
         Log.d(TAG, "addTransactionsToManager");
 
@@ -395,7 +446,8 @@
         Call foregroundCallBeforeSwap = mCallsManager.getForegroundCall();
         boolean wasActive = foregroundCallBeforeSwap != null && foregroundCallBeforeSwap.isActive();
 
-        SerialTransaction serialTransactions = createSetActiveTransactions(call);
+        SerialTransaction serialTransactions = createSetActiveTransactions(call,
+                false /* isCallControlRequest */);
         // 3. get ack from client (that the requested call can go active)
         if (isAnswerRequest) {
             serialTransactions.appendTransaction(
@@ -451,7 +503,8 @@
 
                         @Override
                         public void onError(CallException exception) {
-                            Log.i(TAG, "onSetInactive: onError: with e=[%e]", exception);
+                            Log.w(TAG, "onSetInactive: onError: e.code=[%d], e.msg=[%s]",
+                                    exception.getCode(), exception.getMessage());
                         }
                     });
         } finally {
@@ -498,8 +551,9 @@
 
                         @Override
                         public void onError(CallException exception) {
-                            Log.i(TAG, "onCallStreamingStarted: onError: with e=[%e]",
-                                    exception);
+                            Log.w(TAG, "onCallStreamingStarted: onError: "
+                                            + "e.code=[%d], e.msg=[%s]",
+                                    exception.getCode(), exception.getMessage());
                             stopCallStreaming(call);
                         }
                     }
@@ -519,6 +573,7 @@
         }
     }
 
+    @Override
     public void onCallEndpointChanged(Call call, CallEndpoint endpoint) {
         if (call != null) {
             try {
@@ -528,6 +583,7 @@
         }
     }
 
+    @Override
     public void onAvailableCallEndpointsChanged(Call call, Set<CallEndpoint> endpoints) {
         if (call != null) {
             try {
@@ -538,6 +594,7 @@
         }
     }
 
+    @Override
     public void onMuteStateChanged(Call call, boolean isMuted) {
         if (call != null) {
             try {
@@ -547,6 +604,16 @@
         }
     }
 
+    @Override
+    public void onVideoStateChanged(Call call, int videoState) {
+        if (call != null) {
+            try {
+                mICallEventCallback.onVideoStateChanged(call.getId(), videoState);
+            } catch (RemoteException e) {
+            }
+        }
+    }
+
     public void removeCallFromWrappers(Call call) {
         if (call != null) {
             try {
@@ -589,12 +656,13 @@
         mCallsManager.removeCall(call);
     }
 
-    private SerialTransaction createSetActiveTransactions(Call call) {
+    private SerialTransaction createSetActiveTransactions(Call call, boolean isCallControlRequest) {
         // create list for multiple transactions
         List<VoipCallTransaction> transactions = new ArrayList<>();
 
         // potentially hold the current active call in order to set a new call (active/answered)
-        transactions.add(new MaybeHoldCallForNewCallTransaction(mCallsManager, call));
+        transactions.add(
+                new MaybeHoldCallForNewCallTransaction(mCallsManager, call, isCallControlRequest));
         // And request a new focus call update
         transactions.add(new RequestNewActiveCallTransaction(mCallsManager, call));
 
diff --git a/src/com/android/server/telecom/UserUtil.java b/src/com/android/server/telecom/UserUtil.java
index a304401..57906d4 100644
--- a/src/com/android/server/telecom/UserUtil.java
+++ b/src/com/android/server/telecom/UserUtil.java
@@ -16,10 +16,19 @@
 
 package com.android.server.telecom;
 
+import android.app.admin.DevicePolicyManager;
 import android.content.Context;
+import android.content.Intent;
 import android.content.pm.UserInfo;
+import android.net.Uri;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.telecom.Log;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+
+import com.android.server.telecom.components.ErrorDialogActivity;
+import com.android.server.telecom.flags.FeatureFlags;
 
 public final class UserUtil {
 
@@ -27,17 +36,124 @@
     }
 
     private static UserInfo getUserInfoFromUserHandle(Context context, UserHandle userHandle) {
-        UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
+        UserManager userManager = context.getSystemService(UserManager.class);
         return userManager.getUserInfo(userHandle.getIdentifier());
     }
 
-    public static boolean isManagedProfile(Context context, UserHandle userHandle) {
+    public static boolean isManagedProfile(Context context, UserHandle userHandle,
+            FeatureFlags featureFlags) {
+        UserManager userManager = context.createContextAsUser(userHandle, 0)
+                .getSystemService(UserManager.class);
         UserInfo userInfo = getUserInfoFromUserHandle(context, userHandle);
-        return userInfo != null && userInfo.isManagedProfile();
+        return featureFlags.telecomResolveHiddenDependencies()
+                ? userManager != null && userManager.isManagedProfile()
+                : userInfo != null && userInfo.isManagedProfile();
     }
 
-    public static boolean isProfile(Context context, UserHandle userHandle) {
+    public static boolean isPrivateProfile(UserHandle userHandle, Context context) {
+        UserManager um = context.createContextAsUser(userHandle, 0).getSystemService(
+                UserManager.class);
+        return um != null && um.isPrivateProfile();
+    }
+
+    public static boolean isProfile(Context context, UserHandle userHandle,
+            FeatureFlags featureFlags) {
+        UserManager userManager = context.createContextAsUser(userHandle, 0)
+                .getSystemService(UserManager.class);
         UserInfo userInfo = getUserInfoFromUserHandle(context, userHandle);
-        return userInfo != null && userInfo.profileGroupId != userInfo.id;
+        return featureFlags.telecomResolveHiddenDependencies()
+                ? userManager != null && userManager.isProfile()
+                : userInfo != null && userInfo.profileGroupId != userInfo.id
+                        && userInfo.profileGroupId != UserInfo.NO_PROFILE_GROUP_ID;
+    }
+
+    public static void showErrorDialogForRestrictedOutgoingCall(Context context,
+            int stringId, String tag, String reason) {
+        final Intent intent = new Intent(context, ErrorDialogActivity.class);
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        intent.putExtra(ErrorDialogActivity.ERROR_MESSAGE_ID_EXTRA, stringId);
+        context.startActivityAsUser(intent, UserHandle.CURRENT);
+        Log.w(tag, "Rejecting non-emergency phone call because "
+                + reason);
+    }
+
+    public static boolean hasOutgoingCallsUserRestriction(Context context,
+            UserHandle userHandle, Uri handle, boolean isSelfManaged, String tag,
+            FeatureFlags featureFlags) {
+        // Set handle for conference calls. Refer to {@link Connection#ADHOC_CONFERENCE_ADDRESS}.
+        if (handle == null) {
+            handle = Uri.parse("tel:conf-factory");
+        }
+
+        if(!isSelfManaged) {
+            // Check DISALLOW_OUTGOING_CALLS restriction. Note: We are skipping this
+            // check in a managed profile user because this check can always be bypassed
+            // by copying and pasting the phone number into the personal dialer.
+            if (!UserUtil.isManagedProfile(context, userHandle, featureFlags)) {
+                final UserManager userManager = context.getSystemService(UserManager.class);
+                boolean hasUserRestriction = featureFlags.telecomResolveHiddenDependencies()
+                        ? userManager.hasUserRestrictionForUser(
+                                UserManager.DISALLOW_OUTGOING_CALLS, userHandle)
+                        : userManager.hasUserRestriction(
+                                UserManager.DISALLOW_OUTGOING_CALLS, userHandle);
+                // Only emergency calls are allowed for users with the DISALLOW_OUTGOING_CALLS
+                // restriction.
+                if (!TelephonyUtil.shouldProcessAsEmergency(context, handle)) {
+                    if (userManager.hasBaseUserRestriction(UserManager.DISALLOW_OUTGOING_CALLS,
+                            userHandle)) {
+                        String reason = "of DISALLOW_OUTGOING_CALLS restriction";
+                        showErrorDialogForRestrictedOutgoingCall(context,
+                                R.string.outgoing_call_not_allowed_user_restriction, tag, reason);
+                        return true;
+                    } else if (hasUserRestriction) {
+                        final DevicePolicyManager dpm =
+                                context.getSystemService(DevicePolicyManager.class);
+                        if (dpm == null) {
+                            return true;
+                        }
+                        final Intent adminSupportIntent = dpm.createAdminSupportIntent(
+                                UserManager.DISALLOW_OUTGOING_CALLS);
+                        if (adminSupportIntent != null) {
+                            context.startActivityAsUser(adminSupportIntent, userHandle);
+                        }
+                        return true;
+                    }
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Gets the associated user for the given call. Note: this is applicable to all calls except
+     * outgoing calls as the associated user is already based off of the user placing the
+     * call.
+     *
+     * @param phoneAccountRegistrar
+     * @param currentUser Current user profile (this can either be the admin or a secondary/guest
+     *                    user). Note that work profile users fall under the admin user.
+     * @param targetPhoneAccount The phone account to retrieve the {@link UserHandle} from.
+     * @return current user if it isn't the admin or if the work profile is paused for the target
+     * phone account handle user, otherwise return the target phone account handle user. If the
+     * flag is disabled, return the legacy {@link UserHandle}.
+     */
+    public static UserHandle getAssociatedUserForCall(boolean isAssociatedUserFlagEnabled,
+            PhoneAccountRegistrar phoneAccountRegistrar, UserHandle currentUser,
+            PhoneAccountHandle targetPhoneAccount) {
+        if (!isAssociatedUserFlagEnabled) {
+            return targetPhoneAccount.getUserHandle();
+        }
+        // For multi-user phone accounts, associate the call with the profile receiving/placing
+        // the call. For SIM accounts (that are assigned to specific users), the user association
+        // will be placed on the target phone account handle user.
+        PhoneAccount account = phoneAccountRegistrar.getPhoneAccountUnchecked(targetPhoneAccount);
+        if (account != null) {
+            return account.hasCapabilities(PhoneAccount.CAPABILITY_MULTI_USER)
+                    ? currentUser
+                    : targetPhoneAccount.getUserHandle();
+        }
+        // If target phone account handle is null or account cannot be found,
+        // return the current user.
+        return currentUser;
     }
 }
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
index b5e7457..3c97d41 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
@@ -16,6 +16,12 @@
 
 package com.android.server.telecom.bluetooth;
 
+import static com.android.server.telecom.AudioRoute.TYPE_BLUETOOTH_HA;
+import static com.android.server.telecom.AudioRoute.TYPE_BLUETOOTH_SCO;
+import static com.android.server.telecom.CallAudioRouteAdapter.BT_DEVICE_REMOVED;
+import static com.android.server.telecom.CallAudioRouteAdapter.SWITCH_BASELINE_ROUTE;
+import static com.android.server.telecom.CallAudioRouteController.INCLUDE_BLUETOOTH_IN_BASELINE;
+
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothHeadset;
@@ -25,27 +31,38 @@
 import android.bluetooth.BluetoothProfile;
 import android.bluetooth.BluetoothStatusCodes;
 import android.content.Context;
-import android.media.AudioManager;
 import android.media.AudioDeviceInfo;
-import android.media.audio.common.AudioDevice;
+import android.media.AudioManager;
 import android.os.Bundle;
 import android.telecom.Log;
 import android.util.ArraySet;
 import android.util.LocalLog;
+import android.util.Pair;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.telecom.AudioRoute;
+import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
+import com.android.server.telecom.CallAudioRouteAdapter;
+import com.android.server.telecom.CallAudioRouteController;
+import com.android.server.telecom.flags.FeatureFlags;
 
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.concurrent.Executor;
+import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 
 public class BluetoothDeviceManager {
 
@@ -53,6 +70,16 @@
     public static final int DEVICE_TYPE_HEARING_AID = 1;
     public static final int DEVICE_TYPE_LE_AUDIO = 2;
 
+    private static final Map<Integer, Integer> PROFILE_TO_AUDIO_ROUTE_MAP = new HashMap<>();
+    static {
+        PROFILE_TO_AUDIO_ROUTE_MAP.put(BluetoothProfile.HEADSET,
+                AudioRoute.TYPE_BLUETOOTH_SCO);
+        PROFILE_TO_AUDIO_ROUTE_MAP.put(BluetoothProfile.LE_AUDIO,
+                AudioRoute.TYPE_BLUETOOTH_LE);
+        PROFILE_TO_AUDIO_ROUTE_MAP.put(BluetoothProfile.HEARING_AID,
+                TYPE_BLUETOOTH_HA);
+    }
+
     private BluetoothLeAudio.Callback mLeAudioCallbacks =
         new BluetoothLeAudio.Callback() {
             @Override
@@ -96,6 +123,9 @@
                         synchronized (mLock) {
                             String logString;
                             if (profile == BluetoothProfile.HEADSET) {
+                                if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+                                    mBluetoothHeadsetFuture.complete((BluetoothHeadset) proxy);
+                                }
                                 mBluetoothHeadset = (BluetoothHeadset) proxy;
                                 logString = "Got BluetoothHeadset: " + mBluetoothHeadset;
                             } else if (profile == BluetoothProfile.HEARING_AID) {
@@ -104,15 +134,14 @@
                                         + mBluetoothHearingAid;
                             } else if (profile == BluetoothProfile.LE_AUDIO) {
                                 mBluetoothLeAudioService = (BluetoothLeAudio) proxy;
-                                logString = "Got BluetoothLeAudio: "
-                                        + mBluetoothLeAudioService;
+                                logString = ("Got BluetoothLeAudio: " + mBluetoothLeAudioService )
+                                        + (", mLeAudioCallbackRegistered: "
+                                        + mLeAudioCallbackRegistered);
                                 if (!mLeAudioCallbackRegistered) {
-                                    try {
-                                        mBluetoothLeAudioService.registerCallback(
-                                                    mExecutor, mLeAudioCallbacks);
-                                        mLeAudioCallbackRegistered = true;
-                                    } catch (IllegalStateException e) {
-                                        logString += ", but Bluetooth is down";
+                                    if (mFeatureFlags.postponeRegisterToLeaudio()) {
+                                        mExecutor.execute(this::registerToLeAudio);
+                                    } else {
+                                        registerToLeAudio();
                                     }
                                 }
                             } else {
@@ -127,6 +156,29 @@
                     }
                 }
 
+                private void registerToLeAudio() {
+                    synchronized (mLock) {
+                        String logString = "Register to leAudio";
+
+                        if (mLeAudioCallbackRegistered) {
+                            logString +=  ", but already registered";
+                            Log.i(BluetoothDeviceManager.this, logString);
+                            mLocalLog.log(logString);
+                            return;
+                        }
+                        try {
+                            mLeAudioCallbackRegistered = true;
+                            mBluetoothLeAudioService.registerCallback(
+                                            mExecutor, mLeAudioCallbacks);
+                        } catch (IllegalStateException e) {
+                            mLeAudioCallbackRegistered = false;
+                            logString += ", but failed: " + e;
+                        }
+                        Log.i(BluetoothDeviceManager.this, logString);
+                        mLocalLog.log(logString);
+                    }
+                }
+
                 @Override
                 public void onServiceDisconnected(int profile) {
                     Log.startSession("BPSL.oSD");
@@ -135,6 +187,9 @@
                             LinkedHashMap<String, BluetoothDevice> lostServiceDevices;
                             String logString;
                             if (profile == BluetoothProfile.HEADSET) {
+                                if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+                                    mBluetoothHeadsetFuture.complete(null);
+                                }
                                 mBluetoothHeadset = null;
                                 lostServiceDevices = mHfpDevicesByAddress;
                                 mBluetoothRouteManager.onActiveDeviceChanged(null,
@@ -161,11 +216,15 @@
                             Log.i(BluetoothDeviceManager.this, logString);
                             mLocalLog.log(logString);
 
-                            List<BluetoothDevice> devicesToRemove = new LinkedList<>(
-                                    lostServiceDevices.values());
-                            lostServiceDevices.clear();
-                            for (BluetoothDevice device : devicesToRemove) {
-                                mBluetoothRouteManager.onDeviceLost(device.getAddress());
+                            if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+                                handleAudioRefactoringServiceDisconnected(profile);
+                            } else {
+                                List<BluetoothDevice> devicesToRemove = new LinkedList<>(
+                                        lostServiceDevices.values());
+                                lostServiceDevices.clear();
+                                for (BluetoothDevice device : devicesToRemove) {
+                                    mBluetoothRouteManager.onDeviceLost(device.getAddress());
+                                }
                             }
                         }
                     } finally {
@@ -174,6 +233,34 @@
                 }
            };
 
+    private void handleAudioRefactoringServiceDisconnected(int profile) {
+        CallAudioRouteController controller = (CallAudioRouteController)
+                mCallAudioRouteAdapter;
+        Map<AudioRoute, BluetoothDevice> btRoutes = controller
+                .getBluetoothRoutes();
+        List<Pair<AudioRoute, BluetoothDevice>> btRoutesToRemove =
+                new ArrayList<>();
+        for (AudioRoute route: btRoutes.keySet()) {
+            if (route.getType() != PROFILE_TO_AUDIO_ROUTE_MAP.get(profile)) {
+                continue;
+            }
+            BluetoothDevice device = btRoutes.get(route);
+            // Prevent concurrent modification exception by just iterating through keys instead of
+            // simultaneously removing them.
+            btRoutesToRemove.add(new Pair<>(route, device));
+        }
+
+        for (Pair<AudioRoute, BluetoothDevice> routeToRemove:
+                btRoutesToRemove) {
+            AudioRoute route = routeToRemove.first;
+            BluetoothDevice device = routeToRemove.second;
+            mCallAudioRouteAdapter.sendMessageWithSessionInfo(
+                    BT_DEVICE_REMOVED, route.getType(), device);
+        }
+        mCallAudioRouteAdapter.sendMessageWithSessionInfo(
+                SWITCH_BASELINE_ROUTE, INCLUDE_BLUETOOTH_IN_BASELINE, (String) null);
+    }
+
     private final LinkedHashMap<String, BluetoothDevice> mHfpDevicesByAddress =
             new LinkedHashMap<>();
     private final LinkedHashMap<String, BluetoothDevice> mHearingAidDevicesByAddress =
@@ -199,6 +286,7 @@
 
     private BluetoothRouteManager mBluetoothRouteManager;
     private BluetoothHeadset mBluetoothHeadset;
+    private CompletableFuture<BluetoothHeadset> mBluetoothHeadsetFuture;
     private BluetoothHearingAid mBluetoothHearingAid;
     private boolean mLeAudioCallbackRegistered = false;
     private BluetoothLeAudio mBluetoothLeAudioService;
@@ -210,10 +298,19 @@
     private BluetoothAdapter mBluetoothAdapter;
     private AudioManager mAudioManager;
     private Executor mExecutor;
+    private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
+    private CallAudioRouteAdapter mCallAudioRouteAdapter;
+    private FeatureFlags mFeatureFlags;
 
-    public BluetoothDeviceManager(Context context, BluetoothAdapter bluetoothAdapter) {
+    public BluetoothDeviceManager(Context context, BluetoothAdapter bluetoothAdapter,
+            CallAudioCommunicationDeviceTracker communicationDeviceTracker,
+            FeatureFlags featureFlags) {
+        mFeatureFlags = featureFlags;
         if (bluetoothAdapter != null) {
             mBluetoothAdapter = bluetoothAdapter;
+            if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+                mBluetoothHeadsetFuture = new CompletableFuture<>();
+            }
             bluetoothAdapter.getProfileProxy(context, mBluetoothProfileServiceListener,
                     BluetoothProfile.HEADSET);
             bluetoothAdapter.getProfileProxy(context, mBluetoothProfileServiceListener,
@@ -222,6 +319,7 @@
                     BluetoothProfile.LE_AUDIO);
             mAudioManager = context.getSystemService(AudioManager.class);
             mExecutor = context.getMainExecutor();
+            mCommunicationDeviceTracker = communicationDeviceTracker;
         }
     }
 
@@ -325,7 +423,19 @@
     }
 
     public BluetoothHeadset getBluetoothHeadset() {
-        return mBluetoothHeadset;
+        if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+            try {
+                mBluetoothHeadset = mBluetoothHeadsetFuture.get(500L,
+                        TimeUnit.MILLISECONDS);
+                return mBluetoothHeadset;
+            } catch (TimeoutException | InterruptedException | ExecutionException e) {
+                // ignore
+                Log.w(this, "Acquire BluetoothHeadset service failed due to: " + e);
+                return null;
+            }
+        } else {
+            return mBluetoothHeadset;
+        }
     }
 
     public BluetoothAdapter getBluetoothAdapter() {
@@ -394,7 +504,7 @@
                 mHearingAidDeviceSyncIds.put(device, hiSyncId);
                 targetDeviceMap = mHearingAidDevicesByAddress;
             } else if (deviceType == DEVICE_TYPE_HEADSET) {
-                if (mBluetoothHeadset == null) {
+                if (getBluetoothHeadset() == null) {
                     Log.w(this, "Headset service null when receiving device added broadcast");
                     return;
                 }
@@ -446,17 +556,24 @@
     }
 
     public void disconnectAudio() {
-        disconnectSco();
-        clearLeAudioCommunicationDevice();
-        clearHearingAidCommunicationDevice();
+        if (mFeatureFlags.callAudioCommunicationDeviceRefactor()) {
+            mCommunicationDeviceTracker.clearBtCommunicationDevice();
+            disconnectSco();
+        } else {
+            disconnectSco();
+            clearLeAudioCommunicationDevice();
+            clearHearingAidCommunicationDevice();
+        }
     }
 
-    public void disconnectSco() {
-        if (mBluetoothHeadset == null) {
+    public int disconnectSco() {
+        int result = BluetoothStatusCodes.ERROR_UNKNOWN;
+        if (getBluetoothHeadset() == null) {
             Log.w(this, "Trying to disconnect audio but no headset service exists.");
         } else {
-            mBluetoothHeadset.disconnectAudio();
+            result = mBluetoothHeadset.disconnectAudio();
         }
+        return result;
     }
 
     public boolean isLeAudioCommunicationDevice() {
@@ -613,6 +730,28 @@
         return result;
     }
 
+    public boolean setCommunicationDeviceForAddress(String address) {
+        AudioDeviceInfo deviceInfo = null;
+        List<AudioDeviceInfo> devices = mAudioManager.getAvailableCommunicationDevices();
+        if (devices.size() == 0) {
+            Log.w(this, " No communication devices available.");
+            return false;
+        }
+
+        for (AudioDeviceInfo device : devices) {
+            Log.i(this, " Available device type:  " + device.getType());
+            if (device.getAddress().equals(address)) {
+                deviceInfo = device;
+                break;
+            }
+        }
+
+        if (!mAudioManager.getCommunicationDevice().equals(deviceInfo)) {
+            return mAudioManager.setCommunicationDevice(deviceInfo);
+        }
+        return true;
+    }
+
     // Connect audio to the bluetooth device at address, checking to see whether it's
     // le audio, hearing aid or a HFP device, and using the proper BT API.
     public boolean connectAudio(String address, boolean switchingBtDevices) {
@@ -637,7 +776,7 @@
             callProfile = BluetoothProfile.HEARING_AID;
         } else if (mHfpDevicesByAddress.containsKey(address)) {
             Log.i(this, "Telecomm found HFP device for address: " + address);
-            if (mBluetoothHeadset == null) {
+            if (getBluetoothHeadset() == null) {
                 Log.w(this, "Attempting to turn on audio when the headset service is null");
                 return false;
             }
@@ -665,7 +804,10 @@
                  * Only after receiving ACTION_ACTIVE_DEVICE_CHANGED it is known that device that
                  * will be audio switched to is available to be choose as communication device */
                 if (!switchingBtDevices) {
-                    return setLeAudioCommunicationDevice();
+                    return mFeatureFlags.callAudioCommunicationDeviceRefactor() ?
+                            mCommunicationDeviceTracker.setCommunicationDevice(
+                                    AudioDeviceInfo.TYPE_BLE_HEADSET, device)
+                            : setLeAudioCommunicationDevice();
                 }
                 return true;
             }
@@ -676,7 +818,10 @@
                  * Only after receiving ACTION_ACTIVE_DEVICE_CHANGED it is known that device that
                  * will be audio switched to is available to be choose as communication device */
                 if (!switchingBtDevices) {
-                    return setHearingAidCommunicationDevice();
+                    return mFeatureFlags.callAudioCommunicationDeviceRefactor() ?
+                            mCommunicationDeviceTracker.setCommunicationDevice(
+                                    AudioDeviceInfo.TYPE_HEARING_AID, null)
+                            : setHearingAidCommunicationDevice();
                 }
                 return true;
             }
@@ -688,9 +833,63 @@
                 Log.w(this, "Couldn't set active device to %s", address);
                 return false;
             }
-            int scoConnectionRequest = mBluetoothHeadset.connectAudio();
-            return scoConnectionRequest == BluetoothStatusCodes.SUCCESS ||
-                scoConnectionRequest == BluetoothStatusCodes.ERROR_AUDIO_DEVICE_ALREADY_CONNECTED;
+            if (getBluetoothHeadset() != null) {
+                int scoConnectionRequest = mBluetoothHeadset.connectAudio();
+                return scoConnectionRequest == BluetoothStatusCodes.SUCCESS ||
+                        scoConnectionRequest
+                                == BluetoothStatusCodes.ERROR_AUDIO_DEVICE_ALREADY_CONNECTED;
+            } else {
+                Log.w(this, "Couldn't find bluetooth headset service");
+                return false;
+            }
+        } else {
+            Log.w(this, "Attempting to turn on audio for a disconnected device");
+            return false;
+        }
+    }
+
+    /**
+     * Used by CallAudioRouteController in order to connect the BT device.
+     * @param device {@link BluetoothDevice} to connect to.
+     * @param type {@link AudioRoute.AudioRouteType} associated with the device.
+     * @return {@code true} if device was successfully connected, {@code false} otherwise.
+     */
+    public boolean connectAudio(BluetoothDevice device, @AudioRoute.AudioRouteType int type) {
+        String address = device.getAddress();
+        int callProfile = BluetoothProfile.LE_AUDIO;
+        if (type == TYPE_BLUETOOTH_SCO) {
+            callProfile = BluetoothProfile.HEADSET;
+        } else if (type == TYPE_BLUETOOTH_HA) {
+            callProfile = BluetoothProfile.HEARING_AID;
+        }
+
+        Bundle preferredAudioProfiles = mBluetoothAdapter.getPreferredAudioProfiles(device);
+        if (preferredAudioProfiles != null && !preferredAudioProfiles.isEmpty()
+                && preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX) != 0) {
+            Log.i(this, "Preferred duplex profile for device=" + address + " is "
+                    + preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX));
+            callProfile = preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX);
+        }
+
+        if (callProfile == BluetoothProfile.LE_AUDIO
+                || callProfile == BluetoothProfile.HEARING_AID) {
+            return mBluetoothAdapter.setActiveDevice(device, BluetoothAdapter.ACTIVE_DEVICE_ALL);
+        } else if (callProfile == BluetoothProfile.HEADSET) {
+            boolean success = mBluetoothAdapter.setActiveDevice(device,
+                    BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL);
+            if (!success) {
+                Log.w(this, "Couldn't set active device to %s", address);
+                return false;
+            }
+            if (getBluetoothHeadset() != null) {
+                int scoConnectionRequest = mBluetoothHeadset.connectAudio();
+                return scoConnectionRequest == BluetoothStatusCodes.SUCCESS ||
+                        scoConnectionRequest
+                                == BluetoothStatusCodes.ERROR_AUDIO_DEVICE_ALREADY_CONNECTED;
+            } else {
+                Log.w(this, "Couldn't find bluetooth headset service");
+                return false;
+            }
         } else {
             Log.w(this, "Attempting to turn on audio for a disconnected device");
             return false;
@@ -717,17 +916,23 @@
     }
 
     public boolean isInbandRingingEnabled() {
-        BluetoothDevice activeDevice = mBluetoothRouteManager.getBluetoothAudioConnectedDevice();
-        Log.i(this, "isInbandRingingEnabled: activeDevice: " + activeDevice);
-        if (mBluetoothRouteManager.isCachedLeAudioDevice(activeDevice)) {
+        // Get the inband ringing enabled status of expected BT device to route call audio instead
+        // of using the address of currently connected device.
+        BluetoothDevice activeDevice = mBluetoothRouteManager.getMostRecentlyReportedActiveDevice();
+        return isInbandRingEnabled(activeDevice);
+    }
+
+    public boolean isInbandRingEnabled(BluetoothDevice bluetoothDevice) {
+        Log.i(this, "isInbandRingEnabled: device: " + bluetoothDevice);
+        if (mBluetoothRouteManager.isCachedLeAudioDevice(bluetoothDevice)) {
             if (mBluetoothLeAudioService == null) {
                 Log.i(this, "isInbandRingingEnabled: no leaudio service available.");
                 return false;
             }
-            int groupId = mBluetoothLeAudioService.getGroupId(activeDevice);
+            int groupId = mBluetoothLeAudioService.getGroupId(bluetoothDevice);
             return mBluetoothLeAudioService.isInbandRingtoneEnabled(groupId);
         } else {
-            if (mBluetoothHeadset == null) {
+            if (getBluetoothHeadset() == null) {
                 Log.i(this, "isInbandRingingEnabled: no headset service available.");
                 return false;
             }
@@ -735,6 +940,10 @@
         }
     }
 
+    public void setCallAudioRouteAdapter(CallAudioRouteAdapter adapter) {
+        mCallAudioRouteAdapter = adapter;
+    }
+
     public void dump(IndentingPrintWriter pw) {
         mLocalLog.dump(pw);
     }
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java b/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
index 7966f73..5a44041 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
@@ -17,15 +17,18 @@
 package com.android.server.telecom.bluetooth;
 
 import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothClass;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothHeadset;
 import android.bluetooth.BluetoothHearingAid;
 import android.bluetooth.BluetoothProfile;
 import android.bluetooth.BluetoothLeAudio;
 import android.content.Context;
+import android.media.AudioDeviceInfo;
 import android.os.Message;
 import android.telecom.Log;
 import android.telecom.Logging.Session;
+import android.util.Pair;
 import android.util.SparseArray;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -33,10 +36,11 @@
 import com.android.internal.util.IState;
 import com.android.internal.util.State;
 import com.android.internal.util.StateMachine;
+import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
 import com.android.server.telecom.TelecomSystem;
 import com.android.server.telecom.Timeouts;
+import com.android.server.telecom.flags.FeatureFlags;
 
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -78,6 +82,7 @@
         void onBluetoothActiveDevicePresent();
         void onBluetoothActiveDeviceGone();
         void onBluetoothAudioConnected();
+        void onBluetoothAudioConnecting();
         void onBluetoothAudioDisconnected();
         /**
          * This gets called when we get an unexpected state change from Bluetooth. Their stack does
@@ -131,7 +136,8 @@
         @Override
         public void enter() {
             BluetoothDevice erroneouslyConnectedDevice = getBluetoothAudioConnectedDevice();
-            if (erroneouslyConnectedDevice != null) {
+            if (erroneouslyConnectedDevice != null &&
+                !erroneouslyConnectedDevice.equals(mHearingAidActiveDeviceCache)) {
                 Log.w(LOG_TAG, "Entering AudioOff state but device %s appears to be connected. " +
                         "Switching to audio-on state for that device.", erroneouslyConnectedDevice);
                 // change this to just transition to the new audio on state
@@ -160,10 +166,23 @@
                         removeDevice((String) args.arg2);
                         break;
                     case CONNECT_BT:
-                        String actualAddress = connectBtAudio((String) args.arg2,
-                            false /* switchingBtDevices*/);
+                        String actualAddress;
+                        boolean connected;
+                        if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+                            Pair<String, Boolean> addressInfo = computeAddressToConnectTo(
+                                    (String) args.arg2, false, null);
+                            // See if we need to transition route if the device is already
+                            // connected. If connected, another connection will not occur.
+                            addressInfo = handleDeviceAlreadyConnected(addressInfo);
+                            actualAddress = addressInfo.first;
+                            connected = connectBtAudio(actualAddress, 0,
+                                    false /* switchingBtDevices*/);
+                        } else {
+                            actualAddress = connectBtAudioLegacy((String) args.arg2, false);
+                            connected = actualAddress != null;
+                        }
 
-                        if (actualAddress != null) {
+                        if (connected) {
                             transitionTo(getConnectingStateForAddress(actualAddress,
                                     "AudioOff/CONNECT_BT"));
                         } else {
@@ -176,10 +195,24 @@
                         break;
                     case RETRY_BT_CONNECTION:
                         Log.i(LOG_TAG, "Retrying BT connection to %s", (String) args.arg2);
-                        String retryAddress = connectBtAudio((String) args.arg2, args.argi1,
-                            false /* switchingBtDevices*/);
+                        String retryAddress;
+                        boolean retrySuccessful;
+                        if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+                            Pair<String, Boolean> retryAddressInfo = computeAddressToConnectTo(
+                                    (String) args.arg2, false, null);
+                            // See if we need to transition route if the device is already
+                            // connected. If connected, another connection will not occur.
+                            retryAddressInfo = handleDeviceAlreadyConnected(retryAddressInfo);
+                            retryAddress = retryAddressInfo.first;
+                            retrySuccessful = connectBtAudio(retryAddress, args.argi1,
+                                    false /* switchingBtDevices*/);
+                        } else {
+                            retryAddress = connectBtAudioLegacy((String) args.arg2, args.argi1,
+                                    false /* switchingBtDevices*/);
+                            retrySuccessful = retryAddress != null;
+                        }
 
-                        if (retryAddress != null) {
+                        if (retrySuccessful) {
                             transitionTo(getConnectingStateForAddress(retryAddress,
                                     "AudioOff/RETRY_BT_CONNECTION"));
                         } else {
@@ -231,8 +264,7 @@
             sendMessageDelayed(CONNECTION_TIMEOUT, args,
                     mTimeoutsAdapter.getBluetoothPendingTimeoutMillis(
                             mContext.getContentResolver()));
-            // Pretend like audio is connected when communicating w/ CARSM.
-            mListener.onBluetoothAudioConnected();
+            mListener.onBluetoothAudioConnecting();
         }
 
         @Override
@@ -250,6 +282,29 @@
             SomeArgs args = (SomeArgs) msg.obj;
             String address = (String) args.arg2;
             boolean switchingBtDevices = !Objects.equals(mDeviceAddress, address);
+
+            if (switchingBtDevices) { // check if it is an hearing aid pair
+                BluetoothAdapter bluetoothAdapter = mDeviceManager.getBluetoothAdapter();
+                if (bluetoothAdapter != null) {
+                    List<BluetoothDevice> activeHearingAids =
+                      bluetoothAdapter.getActiveDevices(BluetoothProfile.HEARING_AID);
+                    for (BluetoothDevice hearingAid : activeHearingAids) {
+                        if (hearingAid != null) {
+                            String hearingAidAddress = hearingAid.getAddress();
+                            if (hearingAidAddress != null) {
+                                if (hearingAidAddress.equals(address) ||
+                                    hearingAidAddress.equals(mDeviceAddress)) {
+                                    switchingBtDevices = false;
+                                    break;
+                                }
+                            }
+                        }
+                    }
+                }
+                if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+                    switchingBtDevices &= (mDeviceAddress != null);
+                }
+            }
             try {
                 switch (msg.what) {
                     case NEW_DEVICE_CONNECTED:
@@ -263,14 +318,30 @@
                         }
                         break;
                     case CONNECT_BT:
+                        String actualAddress = null;
+                        if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+                            Pair<String, Boolean> addressInfo = computeAddressToConnectTo(address,
+                                    switchingBtDevices, mDeviceAddress);
+                            // See if we need to transition route if the device is already
+                            // connected. If connected, another connection will not occur.
+                            addressInfo = handleDeviceAlreadyConnected(addressInfo);
+                            actualAddress = addressInfo.first;
+                            switchingBtDevices = addressInfo.second;
+                        }
+
                         if (!switchingBtDevices) {
                             // Ignore repeated connection attempts to the same device
                             break;
                         }
 
-                        String actualAddress = connectBtAudio(address,
-                            true /* switchingBtDevices*/);
-                        if (actualAddress != null) {
+                        if (!mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+                            actualAddress = connectBtAudioLegacy(address,
+                                    true /* switchingBtDevices*/);
+                        }
+                        boolean connected = mFeatureFlags.resolveSwitchingBtDevicesComputation()
+                                ? connectBtAudio(actualAddress, 0, true /* switchingBtDevices*/)
+                                : actualAddress != null;
+                        if (connected) {
                             transitionTo(getConnectingStateForAddress(actualAddress,
                                     "AudioConnecting/CONNECT_BT"));
                         } else {
@@ -282,14 +353,32 @@
                         mDeviceManager.disconnectAudio();
                         break;
                     case RETRY_BT_CONNECTION:
+                        String retryAddress = null;
+                        if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+                            Pair<String, Boolean> retryAddressInfo = computeAddressToConnectTo(
+                                    address, switchingBtDevices, mDeviceAddress);
+                            // See if we need to transition route if the device is already
+                            // connected. If connected, another connection will not occur.
+                            retryAddressInfo = handleDeviceAlreadyConnected(retryAddressInfo);
+                            retryAddress = retryAddressInfo.first;
+                            switchingBtDevices = retryAddressInfo.second;
+                        }
+
                         if (!switchingBtDevices) {
                             Log.d(LOG_TAG, "Retry message came through while connecting.");
                             break;
                         }
 
-                        String retryAddress = connectBtAudio(address, args.argi1,
-                            true /* switchingBtDevices*/);
-                        if (retryAddress != null) {
+                        if (!mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+                            retryAddress = connectBtAudioLegacy(address, args.argi1,
+                                    true /* switchingBtDevices*/);
+                        }
+                        boolean retrySuccessful = mFeatureFlags
+                                .resolveSwitchingBtDevicesComputation()
+                                ? connectBtAudio(retryAddress, args.argi1,
+                                        true /* switchingBtDevices*/)
+                                : retryAddress != null;
+                        if (retrySuccessful) {
                             transitionTo(getConnectingStateForAddress(retryAddress,
                                     "AudioConnecting/RETRY_BT_CONNECTION"));
                         } else {
@@ -368,6 +457,10 @@
             SomeArgs args = (SomeArgs) msg.obj;
             String address = (String) args.arg2;
             boolean switchingBtDevices = !Objects.equals(mDeviceAddress, address);
+            if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+                switchingBtDevices &= (mDeviceAddress != null);
+            }
+
             try {
                 switch (msg.what) {
                     case NEW_DEVICE_CONNECTED:
@@ -380,6 +473,17 @@
                         }
                         break;
                     case CONNECT_BT:
+                        String actualAddress = null;
+                        if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+                            Pair<String, Boolean> addressInfo = computeAddressToConnectTo(address,
+                                    switchingBtDevices, mDeviceAddress);
+                            // See if we need to transition route if the device is already
+                            // connected. If connected, another connection will not occur.
+                            addressInfo = handleDeviceAlreadyConnected(addressInfo);
+                            actualAddress = addressInfo.first;
+                            switchingBtDevices = addressInfo.second;
+                        }
+
                         if (!switchingBtDevices) {
                             // Ignore connection to already connected device but still notify
                             // CallAudioRouteStateMachine since this might be a switch from other
@@ -388,11 +492,21 @@
                             break;
                         }
 
-                        String actualAddress = connectBtAudio(address,
-                            true /* switchingBtDevices*/);
-                        if (actualAddress != null) {
-                            transitionTo(getConnectingStateForAddress(address,
-                                    "AudioConnected/CONNECT_BT"));
+                        if (!mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+                            actualAddress = connectBtAudioLegacy(address,
+                                    true /* switchingBtDevices*/);
+                        }
+                        boolean connected = mFeatureFlags.resolveSwitchingBtDevicesComputation()
+                                ? connectBtAudio(actualAddress, 0, true /* switchingBtDevices*/)
+                                : actualAddress != null;
+                        if (connected) {
+                            if (mFeatureFlags.useActualAddressToEnterConnectingState()) {
+                                transitionTo(getConnectingStateForAddress(actualAddress,
+                                        "AudioConnected/CONNECT_BT"));
+                            } else {
+                                transitionTo(getConnectingStateForAddress(address,
+                                        "AudioConnected/CONNECT_BT"));
+                            }
                         } else {
                             Log.w(LOG_TAG, "Tried to connect to %s but failed" +
                                     " to connect to any BT device.", (String) args.arg2);
@@ -402,14 +516,32 @@
                         mDeviceManager.disconnectAudio();
                         break;
                     case RETRY_BT_CONNECTION:
+                        String retryAddress = null;
+                        if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+                            Pair<String, Boolean> retryAddressInfo = computeAddressToConnectTo(
+                                    address, switchingBtDevices, mDeviceAddress);
+                            // See if we need to transition route if the device is already
+                            // connected. If connected, another connection will not occur.
+                            retryAddressInfo = handleDeviceAlreadyConnected(retryAddressInfo);
+                            retryAddress = retryAddressInfo.first;
+                            switchingBtDevices = retryAddressInfo.second;
+                        }
+
                         if (!switchingBtDevices) {
                             Log.d(LOG_TAG, "Retry message came through while connected.");
                             break;
                         }
 
-                        String retryAddress = connectBtAudio(address, args.argi1,
-                            true /* switchingBtDevices*/);
-                        if (retryAddress != null) {
+                        if (!mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+                            retryAddress = connectBtAudioLegacy(address, args.argi1,
+                                    true /* switchingBtDevices*/);
+                        }
+                        boolean retrySuccessful = mFeatureFlags
+                                .resolveSwitchingBtDevicesComputation()
+                                ? connectBtAudio(retryAddress, args.argi1,
+                                        true /* switchingBtDevices*/)
+                                : retryAddress != null;
+                        if (retrySuccessful) {
                             transitionTo(getConnectingStateForAddress(retryAddress,
                                     "AudioConnected/RETRY_BT_CONNECTION"));
                         } else {
@@ -469,15 +601,21 @@
     private BluetoothDevice mHearingAidActiveDeviceCache = null;
     private BluetoothDevice mLeAudioActiveDeviceCache = null;
     private BluetoothDevice mMostRecentlyReportedActiveDevice = null;
+    private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
+    private FeatureFlags mFeatureFlags;
 
     public BluetoothRouteManager(Context context, TelecomSystem.SyncRoot lock,
-            BluetoothDeviceManager deviceManager, Timeouts.Adapter timeoutsAdapter) {
+            BluetoothDeviceManager deviceManager, Timeouts.Adapter timeoutsAdapter,
+            CallAudioCommunicationDeviceTracker communicationDeviceTracker,
+            FeatureFlags featureFlags) {
         super(BluetoothRouteManager.class.getSimpleName());
         mContext = context;
         mLock = lock;
         mDeviceManager = deviceManager;
         mDeviceManager.setBluetoothRouteManager(this);
         mTimeoutsAdapter = timeoutsAdapter;
+        mCommunicationDeviceTracker = communicationDeviceTracker;
+        mFeatureFlags = featureFlags;
 
         mAudioOffState = new AudioOffState();
         addState(mAudioOffState);
@@ -621,12 +759,22 @@
         if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO) {
             mLeAudioActiveDeviceCache = device;
             if (device == null) {
-                mDeviceManager.clearLeAudioCommunicationDevice();
+                if (mFeatureFlags.callAudioCommunicationDeviceRefactor()) {
+                    mCommunicationDeviceTracker.clearCommunicationDevice(
+                            AudioDeviceInfo.TYPE_BLE_HEADSET);
+                } else {
+                    mDeviceManager.clearLeAudioCommunicationDevice();
+                }
             }
         } else if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID) {
             mHearingAidActiveDeviceCache = device;
             if (device == null) {
-                mDeviceManager.clearHearingAidCommunicationDevice();
+                if (mFeatureFlags.callAudioCommunicationDeviceRefactor()) {
+                    mCommunicationDeviceTracker.clearCommunicationDevice(
+                            AudioDeviceInfo.TYPE_HEARING_AID);
+                } else {
+                    mDeviceManager.clearHearingAidCommunicationDevice();
+                }
             }
         } else if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEADSET) {
             mHfpActiveDeviceCache = device;
@@ -645,6 +793,10 @@
         }
     }
 
+    public BluetoothDevice getMostRecentlyReportedActiveDevice() {
+        return mMostRecentlyReportedActiveDevice;
+    }
+
     public boolean hasBtActiveDevice() {
         return mLeAudioActiveDeviceCache != null ||
                 mHearingAidActiveDeviceCache != null ||
@@ -663,8 +815,151 @@
         return mDeviceManager.getUniqueConnectedDevices();
     }
 
-    private String connectBtAudio(String address, boolean switchingBtDevices) {
-        return connectBtAudio(address, 0, switchingBtDevices);
+    public boolean isWatch(BluetoothDevice device) {
+        if (device == null) {
+            Log.i(this, "isWatch: device is null. Returning false");
+            return false;
+        }
+
+        BluetoothClass deviceClass = device.getBluetoothClass();
+        if (deviceClass != null && deviceClass.getDeviceClass()
+                == BluetoothClass.Device.WEARABLE_WRIST_WATCH) {
+            Log.i(this, "isWatch: bluetooth class component is a WEARABLE_WRIST_WATCH.");
+            return true;
+        }
+
+        // Check metadata
+        byte[] deviceType = device.getMetadata(BluetoothDevice.METADATA_DEVICE_TYPE);
+        if (deviceType == null) {
+            return false;
+        }
+        String deviceTypeStr = new String(deviceType);
+        if (deviceTypeStr.equals(BluetoothDevice.DEVICE_TYPE_WATCH)) {
+            Log.i(this, "isWatch: bluetooth device type is DEVICE_TYPE_WATCH.");
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Determines the address that should be used for the connection attempt. In the case that the
+     * specified address to be used is null, Telecom will try to find an arbitrary address to
+     * connect instead.
+     *
+     * @param address The address that should be prioritized for the connection attempt
+     * @param switchingBtDevices Used when there is existing audio connection to other Bt device.
+     * @param stateAddress The address stored in the state that indicates the connecting/connected
+     *                     device.
+     * @return {@link Pair} containing the address to connect to and whether an existing BT audio
+     *                      connection for a different device exists.
+     */
+    private Pair<String, Boolean> computeAddressToConnectTo(
+            String address, boolean switchingBtDevices, String stateAddress) {
+        Collection<BluetoothDevice> deviceList = mDeviceManager.getConnectedDevices();
+        Optional<BluetoothDevice> matchingDevice = deviceList.stream()
+                .filter(d -> Objects.equals(d.getAddress(), address))
+                .findAny();
+
+        String actualAddress = matchingDevice.isPresent()
+                ? address : getActiveDeviceAddress();
+        if (actualAddress == null) {
+            Log.i(this, "No device specified and BT stack has no active device."
+                    + " Using arbitrary device - except watch");
+            if (deviceList.size() > 0) {
+                for (BluetoothDevice device : deviceList) {
+                    if (mFeatureFlags.ignoreAutoRouteToWatchDevice() && isWatch(device)) {
+                        Log.i(this, "Skipping a watch device: " + device);
+                        continue;
+                    }
+                    actualAddress = device.getAddress();
+                    break;
+                }
+            }
+
+            if (actualAddress == null) {
+                Log.i(this, "No devices available at all. Not connecting.");
+                return new Pair<>(null, false);
+            }
+            if (switchingBtDevices && actualAddress.equals(stateAddress)) {
+                switchingBtDevices = false;
+            }
+        }
+        if (!matchingDevice.isPresent()) {
+            Log.i(this, "No device with address %s available. Using %s instead.",
+                    address, actualAddress);
+        }
+        return new Pair<>(actualAddress, switchingBtDevices);
+    }
+
+    /**
+     * Handles route switching to the connected state for a device. This currently handles the case
+     * for hearing aids when the route manager reports AudioOff since Telecom doesn't treat HA as
+     * the active device outside of a call.
+     *
+     * @param addressInfo A {@link Pair} containing the BT address to connect to as well as if we're
+     *                    handling a switch of BT devices.
+     * @return {@link Pair} indicating the address to connect to as well as if we're handling a
+     *                      switch of BT devices. If the device is already connected, then the
+     *                      return value will be {null, false} to indicate that a connection attempt
+     *                      is not required.
+     */
+    private Pair<String, Boolean> handleDeviceAlreadyConnected(Pair<String, Boolean> addressInfo) {
+        String address = addressInfo.first;
+        BluetoothDevice alreadyConnectedDevice = getBluetoothAudioConnectedDevice();
+        if (alreadyConnectedDevice != null && alreadyConnectedDevice.getAddress().equals(
+                address)) {
+            Log.i(this, "trying to connect to already connected device -- skipping connection"
+                    + " and going into the actual connected state.");
+            transitionToActualState();
+            return new Pair<>(null, false);
+        }
+        return addressInfo;
+    }
+
+    /**
+     * Initiates a connection to the BT address specified.
+     * Note: This method is not synchronized on the Telecom lock, so don't try and call back into
+     * Telecom from within it.
+     * @param address The address that should be tried first. May be null.
+     * @param retryCount The number of times this connection attempt has been retried.
+     * @param switchingBtDevices Used when there is existing audio connection to other Bt device.
+     * @return {@code true} if the connection to the address was successful, otherwise {@code false}
+     *          if the connection fails.
+     *
+     * Note: This should only be used in par with the resolveSwitchingBtDevicesComputation flag.
+     */
+    private boolean connectBtAudio(String address, int retryCount, boolean switchingBtDevices) {
+        if (address == null) {
+            return false;
+        }
+
+        if (switchingBtDevices) {
+            /* When new Bluetooth connects audio, make sure previous one has disconnected audio. */
+            mDeviceManager.disconnectAudio();
+        }
+
+        if (!mDeviceManager.connectAudio(address, switchingBtDevices)) {
+            boolean shouldRetry = retryCount < MAX_CONNECTION_RETRIES;
+            Log.w(LOG_TAG, "Could not connect to %s. Will %s", address,
+                    shouldRetry ? "retry" : "not retry");
+            if (shouldRetry) {
+                SomeArgs args = SomeArgs.obtain();
+                args.arg1 = Log.createSubsession();
+                args.arg2 = address;
+                args.argi1 = retryCount + 1;
+                sendMessageDelayed(RETRY_BT_CONNECTION, args,
+                        mTimeoutsAdapter.getRetryBluetoothConnectAudioBackoffMillis(
+                                mContext.getContentResolver()));
+            }
+            return false;
+        }
+
+        return true;
+    }
+
+    private String connectBtAudioLegacy(String address, boolean switchingBtDevices) {
+        return connectBtAudioLegacy(address, 0, switchingBtDevices);
     }
 
     /**
@@ -677,7 +972,8 @@
      * @return The address of the device that's actually being connected to, or null if no
      * connection was successful.
      */
-    private String connectBtAudio(String address, int retryCount, boolean switchingBtDevices) {
+    private String connectBtAudioLegacy(String address, int retryCount,
+            boolean switchingBtDevices) {
         Collection<BluetoothDevice> deviceList = mDeviceManager.getConnectedDevices();
         Optional<BluetoothDevice> matchingDevice = deviceList.stream()
                 .filter(d -> Objects.equals(d.getAddress(), address))
@@ -692,10 +988,19 @@
                 ? address : getActiveDeviceAddress();
         if (actualAddress == null) {
             Log.i(this, "No device specified and BT stack has no active device."
-                    + " Using arbitrary device");
+                    + " Using arbitrary device - except watch");
             if (deviceList.size() > 0) {
-                actualAddress = deviceList.iterator().next().getAddress();
-            } else {
+                for (BluetoothDevice device : deviceList) {
+                    if (mFeatureFlags.ignoreAutoRouteToWatchDevice() && isWatch(device)) {
+                        Log.i(this, "Skipping a watch device: " + device);
+                        continue;
+                    }
+                    actualAddress = device.getAddress();
+                    break;
+                }
+            }
+
+            if (actualAddress == null) {
                 Log.i(this, "No devices available at all. Not connecting.");
                 return null;
             }
@@ -797,21 +1102,37 @@
             }
         }
 
+        boolean isHearingAidSetForCommunication =
+                mFeatureFlags.callAudioCommunicationDeviceRefactor()
+                ? mCommunicationDeviceTracker.isAudioDeviceSetForType(
+                        AudioDeviceInfo.TYPE_HEARING_AID)
+                : mDeviceManager.isHearingAidSetAsCommunicationDevice();
         if (bluetoothHearingAid != null) {
-            if (mDeviceManager.isHearingAidSetAsCommunicationDevice()) {
-                for (BluetoothDevice device : bluetoothAdapter.getActiveDevices(
-                        BluetoothProfile.HEARING_AID)) {
-                    if (device != null) {
-                        hearingAidActiveDevice = device;
-                        activeDevices++;
-                        break;
+            if (isHearingAidSetForCommunication) {
+                List<BluetoothDevice> hearingAidsActiveDevices = bluetoothAdapter.getActiveDevices(
+                        BluetoothProfile.HEARING_AID);
+                if (hearingAidsActiveDevices.contains(mHearingAidActiveDeviceCache)) {
+                    hearingAidActiveDevice = mHearingAidActiveDeviceCache;
+                    activeDevices++;
+                } else {
+                    for (BluetoothDevice device : hearingAidsActiveDevices) {
+                        if (device != null) {
+                            hearingAidActiveDevice = device;
+                            activeDevices++;
+                            break;
+                        }
                     }
                 }
             }
         }
 
+        boolean isLeAudioSetForCommunication =
+                mFeatureFlags.callAudioCommunicationDeviceRefactor()
+                        ? mCommunicationDeviceTracker.isAudioDeviceSetForType(
+                        AudioDeviceInfo.TYPE_BLE_HEADSET)
+                        : mDeviceManager.isLeAudioCommunicationDevice();
         if (bluetoothLeAudio != null) {
-            if (mDeviceManager.isLeAudioCommunicationDevice()) {
+            if (isLeAudioSetForCommunication) {
                 for (BluetoothDevice device : bluetoothAdapter.getActiveDevices(
                         BluetoothProfile.LE_AUDIO)) {
                     if (device != null) {
@@ -853,6 +1174,11 @@
         return mDeviceManager.isInbandRingingEnabled();
     }
 
+    @VisibleForTesting
+    public boolean isInbandRingEnabled(BluetoothDevice bluetoothDevice) {
+        return mDeviceManager.isInbandRingEnabled(bluetoothDevice);
+    }
+
     private boolean addDevice(String address) {
         if (mAudioConnectingStates.containsKey(address)) {
             Log.i(this, "Attempting to add device %s twice.", address);
@@ -939,4 +1265,8 @@
             mHfpActiveDeviceCache = device;
         }
     }
+
+    public BluetoothDeviceManager getDeviceManager() {
+        return mDeviceManager;
+    }
 }
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java b/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
index 09b8f76..f76391c 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
@@ -16,6 +16,16 @@
 
 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.CallAudioRouteAdapter.PENDING_ROUTE_FAILED;
+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;
@@ -26,15 +36,19 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.media.AudioDeviceInfo;
 import android.os.Bundle;
 import android.telecom.Log;
 import android.telecom.Logging.Session;
+import android.util.Pair;
 
 import com.android.internal.os.SomeArgs;
-
-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.AudioRoute;
+import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
+import com.android.server.telecom.CallAudioRouteAdapter;
+import com.android.server.telecom.CallAudioRouteController;
+import com.android.server.telecom.flags.FeatureFlags;
+import com.android.server.telecom.flags.Flags;
 
 public class BluetoothStateReceiver extends BroadcastReceiver {
     private static final String LOG_TAG = BluetoothStateReceiver.class.getSimpleName();
@@ -56,6 +70,9 @@
     private boolean mIsInCall = false;
     private final BluetoothRouteManager mBluetoothRouteManager;
     private final BluetoothDeviceManager mBluetoothDeviceManager;
+    private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
+    private FeatureFlags mFeatureFlags;
+    private CallAudioRouteAdapter mCallAudioRouteAdapter;
 
     public void onReceive(Context context, Intent intent) {
         Log.startSession("BSR.oR");
@@ -101,14 +118,46 @@
         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 (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+                    CallAudioRouteController audioRouteController =
+                            (CallAudioRouteController) mCallAudioRouteAdapter;
+                    audioRouteController.setIsScoAudioConnected(true);
+                    if (audioRouteController.isPending()) {
+                        mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0,
+                                device);
+                    } else {
+                        // It's possible that the initial BT connection fails but BT_AUDIO_CONNECTED
+                        // is sent later, indicating that SCO audio is on. We should route
+                        // appropriately in order for the UI to reflect this state.
+                        AudioRoute btRoute = audioRouteController.getBluetoothRoute(
+                                AudioRoute.TYPE_BLUETOOTH_SCO, device.getAddress());
+                        if (btRoute != null) {
+                            audioRouteController.getPendingAudioRoute().overrideDestRoute(btRoute);
+                            audioRouteController.overrideIsPending(true);
+                            audioRouteController.getPendingAudioRoute()
+                                    .setCommunicationDeviceType(AudioRoute.TYPE_BLUETOOTH_SCO);
+                            mCallAudioRouteAdapter.sendMessageWithSessionInfo(
+                                    CallAudioRouteAdapter.EXIT_PENDING_ROUTE);
+                        }
+                    }
+                } else {
+                    if (!mIsInCall) {
+                        Log.i(LOG_TAG, "Ignoring BT audio on since we're not in a call");
+                        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()) {
+                    CallAudioRouteController audioRouteController =
+                            (CallAudioRouteController) mCallAudioRouteAdapter;
+                    audioRouteController.setIsScoAudioConnected(false);
+                    mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_AUDIO_DISCONNECTED, 0,
+                            device);
+                }  else {
+                    mBluetoothRouteManager.sendMessage(BT_AUDIO_LOST, args);
+                }
                 break;
         }
     }
@@ -126,12 +175,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;
@@ -142,10 +195,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);
+            }
         }
     }
 
@@ -154,12 +217,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;
@@ -168,62 +235,108 @@
         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()) {
+            CallAudioRouteController audioRouteController = (CallAudioRouteController)
+                    mCallAudioRouteAdapter;
             if (device == null) {
-                mBluetoothRouteManager.sendMessage(BT_AUDIO_LOST, args);
+                audioRouteController.updateActiveBluetoothDevice(new Pair(audioRouteType, null));
+                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);
+                audioRouteController.updateActiveBluetoothDevice(
+                        new Pair(audioRouteType, device.getAddress()));
+                mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
+                        audioRouteType, device.getAddress());
+                if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID
+                        || deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO) {
+                    if (!mBluetoothDeviceManager.setCommunicationDeviceForAddress(
+                            device.getAddress())) {
+                        Log.i(this, "handleActiveDeviceChanged: Failed to set "
+                                + "communication device for %s. Sending PENDING_ROUTE_FAILED to "
+                                + "pending audio route.", device);
+                        mCallAudioRouteAdapter.getPendingAudioRoute()
+                                .onMessageReceived(new Pair<>(PENDING_ROUTE_FAILED,
+                                        device.getAddress()), device.getAddress());
+                    } else {
+                        // Track the currently set communication device.
+                        int routeType = deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO
+                                ? AudioRoute.TYPE_BLUETOOTH_LE
+                                : AudioRoute.TYPE_BLUETOOTH_HA;
+                        mCallAudioRouteAdapter.getPendingAudioRoute()
+                                .setCommunicationDeviceType(routeType);
                     }
                 }
-
-                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
-                     */
-                    if ((!usePreferredAudioProfile
-                            || preferredDuplexProfile == BluetoothProfile.LE_AUDIO)
-                            && !mBluetoothDeviceManager.setLeAudioCommunicationDevice()) {
-                        Log.w(LOG_TAG,
-                                "Device %s cannot be use as LE audio communication device.",
-                                device);
+            }
+        } 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 {
-                    /* deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID */
-                    if (!mBluetoothDeviceManager.setHearingAidCommunicationDevice()) {
-                        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);
+                        }
                     }
                 }
-           }
+            }
         }
     }
 
@@ -232,12 +345,20 @@
     }
 
     public BluetoothStateReceiver(BluetoothDeviceManager deviceManager,
-            BluetoothRouteManager routeManager) {
+            BluetoothRouteManager routeManager,
+            CallAudioCommunicationDeviceTracker communicationDeviceTracker,
+            FeatureFlags featureFlags) {
         mBluetoothDeviceManager = deviceManager;
         mBluetoothRouteManager = routeManager;
+        mCommunicationDeviceTracker = communicationDeviceTracker;
+        mFeatureFlags = featureFlags;
     }
 
     public void setIsInCall(boolean isInCall) {
         mIsInCall = isInCall;
     }
+
+    public void setCallAudioRouteAdapter(CallAudioRouteAdapter adapter) {
+        mCallAudioRouteAdapter = adapter;
+    }
 }
diff --git a/src/com/android/server/telecom/callfiltering/BlockCheckerAdapter.java b/src/com/android/server/telecom/callfiltering/BlockCheckerAdapter.java
index a83f314..55540de 100644
--- a/src/com/android/server/telecom/callfiltering/BlockCheckerAdapter.java
+++ b/src/com/android/server/telecom/callfiltering/BlockCheckerAdapter.java
@@ -19,12 +19,19 @@
 import android.content.Context;
 import android.os.Bundle;
 import android.provider.BlockedNumberContract;
+import android.provider.BlockedNumbersManager;
 import android.telecom.Log;
 
+import com.android.server.telecom.flags.FeatureFlags;
+
 public class BlockCheckerAdapter {
     private static final String TAG = BlockCheckerAdapter.class.getSimpleName();
 
-    public BlockCheckerAdapter() { }
+    private FeatureFlags mFeatureFlags;
+
+    public BlockCheckerAdapter(FeatureFlags featureFlags) {
+        mFeatureFlags = featureFlags;
+    }
 
     /**
      * Returns the call blocking status for the {@code phoneNumber}.
@@ -32,24 +39,35 @@
      * This method catches all underlying exceptions to ensure that this method never throws any
      * exception.
      *
-     * @param context the context of the caller.
      * @param phoneNumber the number to check.
-     * @param extras the extra attribute of the number.
+     * @param numberPresentation the presentation code associated with the call.
+     * @param isNumberInContacts indicates if the provided number exists as a contact.
      * @return result code indicating if the number should be blocked, and if so why.
-     *         Valid values are: {@link BlockedNumberContract#STATUS_NOT_BLOCKED},
-     *         {@link BlockedNumberContract#STATUS_BLOCKED_IN_LIST},
-     *         {@link BlockedNumberContract#STATUS_BLOCKED_NOT_IN_CONTACTS},
-     *         {@link BlockedNumberContract#STATUS_BLOCKED_PAYPHONE},
-     *         {@link BlockedNumberContract#STATUS_BLOCKED_RESTRICTED},
-     *         {@link BlockedNumberContract#STATUS_BLOCKED_UNKNOWN_NUMBER}.
+     *         Valid values are: {@link BlockCheckerFilter#STATUS_NOT_BLOCKED},
+     *         {@link BlockCheckerFilter#STATUS_BLOCKED_IN_LIST},
+     *         {@link BlockCheckerFilter#STATUS_BLOCKED_NOT_IN_CONTACTS},
+     *         {@link BlockCheckerFilter#STATUS_BLOCKED_PAYPHONE},
+     *         {@link BlockCheckerFilter#STATUS_BLOCKED_RESTRICTED},
+     *         {@link BlockCheckerFilter#STATUS_BLOCKED_UNKNOWN_NUMBER}.
      */
-    public int getBlockStatus(Context context, String phoneNumber, Bundle extras) {
+    public int getBlockStatus(Context context, String phoneNumber,
+            int numberPresentation, boolean isNumberInContacts) {
         int blockStatus = BlockedNumberContract.STATUS_NOT_BLOCKED;
         long startTimeNano = System.nanoTime();
+        BlockedNumbersManager blockedNumbersManager = mFeatureFlags
+                .telecomMainlineBlockedNumbersManager()
+                ? context.getSystemService(BlockedNumbersManager.class)
+                : null;
 
         try {
-            blockStatus = BlockedNumberContract.SystemContract.shouldSystemBlockNumber(
-                    context, phoneNumber, extras);
+            Bundle extras = new Bundle();
+            extras.putInt(BlockedNumberContract.EXTRA_CALL_PRESENTATION, numberPresentation);
+            extras.putBoolean(BlockedNumberContract.EXTRA_CONTACT_EXIST, isNumberInContacts);
+            blockStatus = blockedNumbersManager != null
+                    ? blockedNumbersManager.shouldSystemBlockNumber(phoneNumber,
+                    numberPresentation, isNumberInContacts)
+                    : BlockedNumberContract.SystemContract.shouldSystemBlockNumber(context,
+                            phoneNumber, extras);
             if (blockStatus != BlockedNumberContract.STATUS_NOT_BLOCKED) {
                 Log.d(TAG, phoneNumber + " is blocked.");
             }
diff --git a/src/com/android/server/telecom/callfiltering/BlockCheckerFilter.java b/src/com/android/server/telecom/callfiltering/BlockCheckerFilter.java
index 36f2077..5beb5f0 100644
--- a/src/com/android/server/telecom/callfiltering/BlockCheckerFilter.java
+++ b/src/com/android/server/telecom/callfiltering/BlockCheckerFilter.java
@@ -48,6 +48,61 @@
 
     public static final long CALLER_INFO_QUERY_TIMEOUT = 5000;
 
+    /**
+     * Integer reason indicating whether a call was blocked, and if so why.
+     * @hide
+     */
+    public static final String RES_BLOCK_STATUS = "block_status";
+
+    /**
+     * Integer reason code used with {@link #RES_BLOCK_STATUS} to indicate that a call was not
+     * blocked.
+     * @hide
+     */
+    public static final int STATUS_NOT_BLOCKED = 0;
+
+    /**
+     * Integer reason code used with {@link #RES_BLOCK_STATUS} to indicate that a call was blocked
+     * because it is in the list of blocked numbers maintained by the provider.
+     * @hide
+     */
+    public static final int STATUS_BLOCKED_IN_LIST = 1;
+
+    /**
+     * Integer reason code used with {@link #RES_BLOCK_STATUS} to indicate that a call was blocked
+     * because it is from a restricted number.
+     * @hide
+     */
+    public static final int STATUS_BLOCKED_RESTRICTED = 2;
+
+    /**
+     * Integer reason code used with {@link #RES_BLOCK_STATUS} to indicate that a call was blocked
+     * because it is from an unknown number.
+     * @hide
+     */
+    public static final int STATUS_BLOCKED_UNKNOWN_NUMBER = 3;
+
+    /**
+     * Integer reason code used with {@link #RES_BLOCK_STATUS} to indicate that a call was blocked
+     * because it is from a pay phone.
+     * @hide
+     */
+    public static final int STATUS_BLOCKED_PAYPHONE = 4;
+
+    /**
+     * Integer reason code used with {@link #RES_BLOCK_STATUS} to indicate that a call was blocked
+     * because it is from a number not in the users contacts.
+     * @hide
+     */
+    public static final int STATUS_BLOCKED_NOT_IN_CONTACTS = 5;
+
+    /**
+     * Integer reason code used with {@link #RES_BLOCK_STATUS} to indicate that a call was blocked
+     * because it is from a number not available.
+     * @hide
+     */
+    public static final int STATUS_BLOCKED_UNAVAILABLE = 6;
+
     public BlockCheckerFilter(Context context, Call call,
             CallerInfoLookupHelper callerInfoLookupHelper,
             BlockCheckerAdapter blockCheckerAdapter) {
@@ -96,14 +151,21 @@
 
     private void getBlockStatus(
             CompletableFuture<CallFilteringResult> resultFuture) {
-        // Set extras
-        Bundle extras = new Bundle();
+        // Set presentation and if contact exists. Used in determining if the system should block
+        // the passed in number. Use default values as they would be returned if the keys didn't
+        // exist in the extras to maintain existing behavior.
+        int presentation;
+        boolean isNumberInContacts;
         if (BlockedNumbersUtil.isEnhancedCallBlockingEnabledByPlatform(mContext)) {
-            int presentation = mCall.getHandlePresentation();
-            extras.putInt(BlockedNumberContract.EXTRA_CALL_PRESENTATION, presentation);
-            if (presentation == TelecomManager.PRESENTATION_ALLOWED) {
-                extras.putBoolean(BlockedNumberContract.EXTRA_CONTACT_EXIST, mContactExists);
-            }
+            presentation = mCall.getHandlePresentation();
+        } else {
+            presentation = 0;
+        }
+
+        if (presentation == TelecomManager.PRESENTATION_ALLOWED) {
+            isNumberInContacts = mContactExists;
+        } else {
+            isNumberInContacts = false;
         }
 
         // Set number
@@ -111,7 +173,8 @@
                 mCall.getHandle().getSchemeSpecificPart();
 
         CompletableFuture.supplyAsync(
-                () -> mBlockCheckerAdapter.getBlockStatus(mContext, number, extras),
+                () -> mBlockCheckerAdapter.getBlockStatus(mContext, number,
+                        presentation, isNumberInContacts),
                 new LoggedHandlerExecutor(mHandler, "BCF.gBS", null))
                 .thenApplyAsync((x) -> completeResult(resultFuture, x),
                         new LoggedHandlerExecutor(mHandler, "BCF.gBS", null));
@@ -120,12 +183,13 @@
     private int completeResult(CompletableFuture<CallFilteringResult> resultFuture,
             int blockStatus) {
         CallFilteringResult result;
-        if (blockStatus != BlockedNumberContract.STATUS_NOT_BLOCKED) {
+        if (blockStatus != STATUS_NOT_BLOCKED) {
             result = new CallFilteringResult.Builder()
                     .setShouldAllowCall(false)
                     .setShouldReject(true)
                     .setShouldAddToCallLog(true)
                     .setShouldShowNotification(false)
+                    .setShouldSilence(true)
                     .setCallBlockReason(getBlockReason(blockStatus))
                     .setCallScreeningAppName(null)
                     .setCallScreeningComponentName(null)
@@ -142,8 +206,7 @@
                     .build();
         }
         Log.addEvent(mCall, LogUtils.Events.BLOCK_CHECK_FINISHED,
-                BlockedNumberContract.SystemContract.blockStatusToString(blockStatus) + " "
-                        + result);
+                blockStatusToString(blockStatus) + " " + result);
         resultFuture.complete(result);
         mHandlerThread.quitSafely();
         return blockStatus;
@@ -151,20 +214,20 @@
 
     private int getBlockReason(int blockStatus) {
         switch (blockStatus) {
-            case BlockedNumberContract.STATUS_BLOCKED_IN_LIST:
+            case STATUS_BLOCKED_IN_LIST:
                 return CallLog.Calls.BLOCK_REASON_BLOCKED_NUMBER;
 
-            case BlockedNumberContract.STATUS_BLOCKED_UNKNOWN_NUMBER:
-            case BlockedNumberContract.STATUS_BLOCKED_UNAVAILABLE:
+            case STATUS_BLOCKED_UNKNOWN_NUMBER:
+            case STATUS_BLOCKED_UNAVAILABLE:
                 return CallLog.Calls.BLOCK_REASON_UNKNOWN_NUMBER;
 
-            case BlockedNumberContract.STATUS_BLOCKED_RESTRICTED:
+            case STATUS_BLOCKED_RESTRICTED:
                 return CallLog.Calls.BLOCK_REASON_RESTRICTED_NUMBER;
 
-            case BlockedNumberContract.STATUS_BLOCKED_PAYPHONE:
+            case STATUS_BLOCKED_PAYPHONE:
                 return CallLog.Calls.BLOCK_REASON_PAY_PHONE;
 
-            case BlockedNumberContract.STATUS_BLOCKED_NOT_IN_CONTACTS:
+            case STATUS_BLOCKED_NOT_IN_CONTACTS:
                 return CallLog.Calls.BLOCK_REASON_NOT_IN_CONTACTS;
 
             default:
@@ -173,4 +236,27 @@
                 return CallLog.Calls.BLOCK_REASON_BLOCKED_NUMBER;
         }
     }
+
+    /**
+     * Converts a block status constant to a string equivalent for logging.
+     */
+    private String blockStatusToString(int blockStatus) {
+        switch (blockStatus) {
+            case STATUS_NOT_BLOCKED:
+                return "not blocked";
+            case STATUS_BLOCKED_IN_LIST:
+                return "blocked - in list";
+            case STATUS_BLOCKED_RESTRICTED:
+                return "blocked - restricted";
+            case STATUS_BLOCKED_UNKNOWN_NUMBER:
+                return "blocked - unknown";
+            case STATUS_BLOCKED_PAYPHONE:
+                return "blocked - payphone";
+            case STATUS_BLOCKED_NOT_IN_CONTACTS:
+                return "blocked - not in contacts";
+            case STATUS_BLOCKED_UNAVAILABLE:
+                return "blocked - unavailable";
+        }
+        return "unknown";
+    }
 }
diff --git a/src/com/android/server/telecom/callfiltering/BlockedNumbersAdapter.java b/src/com/android/server/telecom/callfiltering/BlockedNumbersAdapter.java
index b8658d8..66137d5 100644
--- a/src/com/android/server/telecom/callfiltering/BlockedNumbersAdapter.java
+++ b/src/com/android/server/telecom/callfiltering/BlockedNumbersAdapter.java
@@ -20,10 +20,10 @@
 
 /**
  * Adapter interface that wraps methods from
- * {@link android.provider.BlockedNumberContract.SystemContract} and
+ * {@link android.provider.BlockedNumbersManager} and
  * {@link com.android.server.telecom.settings.BlockedNumbersUtil} to make things testable.
  */
 public interface BlockedNumbersAdapter {
-    boolean shouldShowEmergencyCallNotification (Context context);
+    boolean shouldShowEmergencyCallNotification(Context context);
     void updateEmergencyCallNotification(Context context, boolean showNotification);
 }
diff --git a/src/com/android/server/telecom/callfiltering/IncomingCallFilterGraphProvider.java b/src/com/android/server/telecom/callfiltering/IncomingCallFilterGraphProvider.java
new file mode 100644
index 0000000..1501280
--- /dev/null
+++ b/src/com/android/server/telecom/callfiltering/IncomingCallFilterGraphProvider.java
@@ -0,0 +1,44 @@
+/*
+ * 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.callfiltering;
+
+import android.content.Context;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.TelecomSystem;
+import com.android.server.telecom.Timeouts;
+
+/**
+ * Interface to provide a {@link IncomingCallFilterGraph}. This class serve for unit test purpose
+ * to mock an incoming call filter graph in test code.
+ */
+public interface IncomingCallFilterGraphProvider {
+
+
+    /**
+     * Provide a {@link  IncomingCallFilterGraph}
+     * @param call The call for the filters.
+     * @param listener Callback object to trigger when filtering is done.
+     * @param context An android context.
+     * @param timeoutsAdapter Adapter to provide timeout value for call filtering.
+     * @param lock Telecom lock.
+     * @return
+     */
+    IncomingCallFilterGraph createGraph(Call call, CallFilterResultCallback listener,
+            Context context,
+            Timeouts.Adapter timeoutsAdapter, TelecomSystem.SyncRoot lock);
+}
diff --git a/src/com/android/server/telecom/callredirection/CallRedirectionProcessorHelper.java b/src/com/android/server/telecom/callredirection/CallRedirectionProcessorHelper.java
index 7a2d415..a6f089f 100644
--- a/src/com/android/server/telecom/callredirection/CallRedirectionProcessorHelper.java
+++ b/src/com/android/server/telecom/callredirection/CallRedirectionProcessorHelper.java
@@ -143,15 +143,25 @@
         return PhoneNumberUtils.extractPostDialPortion(handle.getSchemeSpecificPart());
     }
 
-    protected Uri formatNumberToE164(Uri handle) {
+    @VisibleForTesting
+    public Uri formatNumberToE164(Uri handle) {
         String number = handle.getSchemeSpecificPart();
 
         // Format number to E164
         TelephonyManager tm = (TelephonyManager) mContext.getSystemService(
                 Context.TELEPHONY_SERVICE);
         Log.i(this, "formatNumberToE164, original number: " + Log.pii(number));
-        number = PhoneNumberUtils.formatNumberToE164(number, tm.getNetworkCountryIso());
-        Log.i(this, "formatNumberToE164, formatted E164 number: " + Log.pii(number));
+        String networkCountryIso;
+        try {
+            number = PhoneNumberUtils.formatNumberToE164(number, tm.getNetworkCountryIso());
+            Log.i(this, "formatNumberToE164, formatted E164 number: " + Log.pii(number));
+        } catch (UnsupportedOperationException ignored) {
+            // Note: Yes, this could default back to the system locale, however redirection when
+            // there is no telephony is NOT expected.  Hence in reality we shouldn't really hit this
+            // code path in practice; this is a "just in case" to ensure we don't crash.
+            Log.w(this, "formatNumberToE164: no telephony; use original format");
+            number = null;
+        }
         // if there is a problem with parsing the phone number, formatNumberToE164 will return null;
         // and should just use the original number in that case.
         if (number == null) {
diff --git a/src/com/android/server/telecom/components/AppUninstallBroadcastReceiver.java b/src/com/android/server/telecom/components/AppUninstallBroadcastReceiver.java
index 3a0d517..b89fe94 100644
--- a/src/com/android/server/telecom/components/AppUninstallBroadcastReceiver.java
+++ b/src/com/android/server/telecom/components/AppUninstallBroadcastReceiver.java
@@ -61,9 +61,14 @@
                 return;
             }
 
-            String packageName = uri.getSchemeSpecificPart();
-            handlePackageRemoved(context, packageName);
-            handleUninstallOfCallScreeningService(context, packageName);
+            final PendingResult result = goAsync();
+            // Move computation off into a separate thread to prevent ANR.
+            new Thread(() -> {
+                String packageName = uri.getSchemeSpecificPart();
+                handlePackageRemoved(context, packageName);
+                handleUninstallOfCallScreeningService(context, packageName);
+                result.finish();
+            }).start();
         }
     }
 
@@ -74,7 +79,7 @@
      * @param packageName The name of the removed package.
      */
     private void handlePackageRemoved(Context context, String packageName) {
-        final TelecomManager telecomManager = TelecomManager.from(context);
+        final TelecomManager telecomManager = context.getSystemService(TelecomManager.class);
         if (telecomManager != null) {
             telecomManager.clearAccountsForPackage(packageName);
         }
diff --git a/src/com/android/server/telecom/components/TelecomService.java b/src/com/android/server/telecom/components/TelecomService.java
index ef85fc7..2d8c78e 100644
--- a/src/com/android/server/telecom/components/TelecomService.java
+++ b/src/com/android/server/telecom/components/TelecomService.java
@@ -27,6 +27,7 @@
 import android.os.ServiceManager;
 import android.os.SystemClock;
 import android.provider.BlockedNumberContract;
+import android.provider.BlockedNumbersManager;
 import android.telecom.Log;
 
 import android.telecom.CallerInfoAsyncQuery;
@@ -45,6 +46,7 @@
 import com.android.server.telecom.ContactsAsyncHelper;
 import com.android.server.telecom.DefaultDialerCache;
 import com.android.server.telecom.DeviceIdleControllerAdapter;
+import com.android.server.telecom.flags.FeatureFlags;
 import com.android.server.telecom.HeadsetMediaButton;
 import com.android.server.telecom.HeadsetMediaButtonFactory;
 import com.android.server.telecom.InCallWakeLockControllerFactory;
@@ -61,6 +63,7 @@
 import com.android.server.telecom.TelecomWakeLock;
 import com.android.server.telecom.Timeouts;
 import com.android.server.telecom.callfiltering.BlockedNumbersAdapter;
+import com.android.server.telecom.flags.FeatureFlagsImpl;
 import com.android.server.telecom.settings.BlockedNumbersUtil;
 import com.android.server.telecom.ui.IncomingCallNotifier;
 import com.android.server.telecom.ui.MissedCallNotifierImpl;
@@ -102,6 +105,7 @@
     static void initializeTelecomSystem(Context context,
             InternalServiceRetrieverAdapter internalServiceRetriever) {
         if (TelecomSystem.getInstance() == null) {
+            FeatureFlags featureFlags = new FeatureFlagsImpl();
             NotificationChannelManager notificationChannelManager =
                     new NotificationChannelManager();
             notificationChannelManager.createChannels(context);
@@ -115,10 +119,11 @@
                                         Context context,
                                         PhoneAccountRegistrar phoneAccountRegistrar,
                                         DefaultDialerCache defaultDialerCache,
-                                        DeviceIdleControllerAdapter idleControllerAdapter) {
+                                        DeviceIdleControllerAdapter idleControllerAdapter,
+                                        FeatureFlags featureFlags) {
                                     return new MissedCallNotifierImpl(context,
                                             phoneAccountRegistrar, defaultDialerCache,
-                                            idleControllerAdapter);
+                                            idleControllerAdapter, featureFlags);
                                 }
                             },
                             new CallerInfoAsyncQueryFactory() {
@@ -215,12 +220,16 @@
                                 }
                             },
                             Executors.newCachedThreadPool(),
+                            Executors.newSingleThreadExecutor(),
                             new BlockedNumbersAdapter() {
                                 @Override
                                 public boolean shouldShowEmergencyCallNotification(Context
                                         context) {
-                                    return BlockedNumberContract.SystemContract
-                                            .shouldShowEmergencyCallNotification(context);
+                                    return featureFlags.telecomMainlineBlockedNumbersManager()
+                                            ? context.getSystemService(BlockedNumbersManager.class)
+                                            .shouldShowEmergencyCallNotification()
+                                            : BlockedNumberContract.SystemContract
+                                                    .shouldShowEmergencyCallNotification(context);
                                 }
 
                                 @Override
@@ -229,7 +238,9 @@
                                     BlockedNumbersUtil.updateEmergencyCallNotification(context,
                                             showNotification);
                                 }
-                            }));
+                            },
+                            featureFlags,
+                            new com.android.internal.telephony.flags.FeatureFlagsImpl()));
         }
     }
 
diff --git a/src/com/android/server/telecom/components/UserCallActivity.java b/src/com/android/server/telecom/components/UserCallActivity.java
index d7b2001..6f5dfea 100644
--- a/src/com/android/server/telecom/components/UserCallActivity.java
+++ b/src/com/android/server/telecom/components/UserCallActivity.java
@@ -18,6 +18,8 @@
 
 import com.android.server.telecom.CallIntentProcessor;
 import com.android.server.telecom.TelecomSystem;
+import com.android.server.telecom.flags.FeatureFlags;
+import com.android.server.telecom.flags.FeatureFlagsImpl;
 
 import android.app.Activity;
 import android.content.Context;
@@ -64,8 +66,13 @@
             // See OutgoingCallBroadcaster in services/Telephony for more.
             Intent intent = getIntent();
             verifyCallAction(intent);
-            final UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE);
-            final UserHandle userHandle = new UserHandle(userManager.getProcessUserId());
+            FeatureFlags featureFlags = getTelecomSystem() != null
+                    ? getTelecomSystem().getFeatureFlags()
+                    : new FeatureFlagsImpl();
+            final UserManager userManager = getSystemService(UserManager.class);
+            final UserHandle userHandle = new UserHandle(
+                    featureFlags.telecomResolveHiddenDependencies()
+                            ? UserHandle.myUserId() : userManager.getProcessUserId());
             // Once control flow has passed to this activity, it is no longer guaranteed that we can
             // accurately determine whether the calling package has the CALL_PHONE runtime permission.
             // At this point in time we trust that the ActivityManager has already performed this
@@ -73,9 +80,9 @@
             // Create a new instance of intent to avoid modifying the
             // ActivityThread.ActivityClientRecord#intent directly.
             // Modifying directly may be a potential risk when relaunching this activity.
-            new UserCallIntentProcessor(this, userHandle).processIntent(new Intent(intent),
-                    getCallingPackage(), false, true /* hasCallAppOp*/,
-                    false /* isLocalInvocation */);
+            new UserCallIntentProcessor(this, userHandle, featureFlags)
+                    .processIntent(new Intent(intent), getCallingPackage(), false,
+                            true /* hasCallAppOp*/, false /* isLocalInvocation */);
         } finally {
             Log.endSession();
             wakelock.release();
diff --git a/src/com/android/server/telecom/components/UserCallIntentProcessor.java b/src/com/android/server/telecom/components/UserCallIntentProcessor.java
index a4602c1..c3dc963 100755
--- a/src/com/android/server/telecom/components/UserCallIntentProcessor.java
+++ b/src/com/android/server/telecom/components/UserCallIntentProcessor.java
@@ -34,6 +34,7 @@
 import com.android.server.telecom.TelecomSystem;
 import com.android.server.telecom.TelephonyUtil;
 import com.android.server.telecom.UserUtil;
+import com.android.server.telecom.flags.FeatureFlags;
 
 // TODO: Needed for move to system service: import com.android.internal.R;
 
@@ -58,10 +59,13 @@
 
     private final Context mContext;
     private final UserHandle mUserHandle;
+    private FeatureFlags mFeatureFlags;
 
-    public UserCallIntentProcessor(Context context, UserHandle userHandle) {
+    public UserCallIntentProcessor(Context context, UserHandle userHandle,
+            FeatureFlags featureFlags) {
         mContext = context;
         mUserHandle = userHandle;
+        mFeatureFlags = featureFlags;
     }
 
     /**
@@ -105,47 +109,17 @@
             handle = Uri.fromParts(PhoneAccount.SCHEME_SIP, uriString, null);
         }
 
-       if(!isSelfManaged) {
-            // Check DISALLOW_OUTGOING_CALLS restriction. Note: We are skipping this
-            // check in a managed profile user because this check can always be bypassed
-            // by copying and pasting the phone number into the personal dialer.
-            if (!UserUtil.isManagedProfile(mContext, mUserHandle)) {
-                // Only emergency calls are allowed for users with the DISALLOW_OUTGOING_CALLS
-                // restriction.
-                if (!TelephonyUtil.shouldProcessAsEmergency(mContext, handle)) {
-                    final UserManager userManager =
-                            (UserManager) mContext.getSystemService(Context.USER_SERVICE);
-                    if (userManager.hasBaseUserRestriction(UserManager.DISALLOW_OUTGOING_CALLS,
-                            mUserHandle)) {
-                        showErrorDialogForRestrictedOutgoingCall(mContext,
-                                R.string.outgoing_call_not_allowed_user_restriction);
-                        Log.w(this, "Rejecting non-emergency phone call "
-                                + "due to DISALLOW_OUTGOING_CALLS restriction");
-                        return;
-                    } else if (userManager.hasUserRestriction(UserManager.DISALLOW_OUTGOING_CALLS,
-                            mUserHandle)) {
-                        final DevicePolicyManager dpm =
-                                mContext.getSystemService(DevicePolicyManager.class);
-                        if (dpm == null) {
-                            return;
-                        }
-                        final Intent adminSupportIntent = dpm.createAdminSupportIntent(
-                                UserManager.DISALLOW_OUTGOING_CALLS);
-                        if (adminSupportIntent != null) {
-                            mContext.startActivity(adminSupportIntent);
-                        }
-                        return;
-                    }
-                }
-            }
-        }
+       if (UserUtil.hasOutgoingCallsUserRestriction(mContext, mUserHandle, handle, isSelfManaged,
+               UserCallIntentProcessor.class.getCanonicalName(), mFeatureFlags)) {
+           return;
+       }
 
         if (!isSelfManaged && !canCallNonEmergency &&
                 !TelephonyUtil.shouldProcessAsEmergency(mContext, handle)) {
-            showErrorDialogForRestrictedOutgoingCall(mContext,
-                    R.string.outgoing_call_not_allowed_no_permission);
-            Log.w(this, "Rejecting non-emergency phone call because "
-                    + android.Manifest.permission.CALL_PHONE + " permission is not granted.");
+            String reason = android.Manifest.permission.CALL_PHONE + " permission is not granted.";
+            UserUtil.showErrorDialogForRestrictedOutgoingCall(mContext,
+                    R.string.outgoing_call_not_allowed_no_permission,
+                    this.getClass().getCanonicalName(), reason);
             return;
         }
 
@@ -187,11 +161,4 @@
         }
         return true;
     }
-
-    private static void showErrorDialogForRestrictedOutgoingCall(Context context, int stringId) {
-        final Intent intent = new Intent(context, ErrorDialogActivity.class);
-        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        intent.putExtra(ErrorDialogActivity.ERROR_MESSAGE_ID_EXTRA, stringId);
-        context.startActivityAsUser(intent, UserHandle.CURRENT);
-    }
 }
diff --git a/src/com/android/server/telecom/settings/BlockNumberTaskFragment.java b/src/com/android/server/telecom/settings/BlockNumberTaskFragment.java
index d96b3e1..6ca4d2a 100644
--- a/src/com/android/server/telecom/settings/BlockNumberTaskFragment.java
+++ b/src/com/android/server/telecom/settings/BlockNumberTaskFragment.java
@@ -23,7 +23,6 @@
 import android.os.AsyncTask;
 import android.os.Bundle;
 import android.provider.BlockedNumberContract;
-import com.android.server.telecom.R;
 
 /**
  * Retained fragment that runs an async task to add a blocked number.
diff --git a/src/com/android/server/telecom/settings/BlockedNumbersActivity.java b/src/com/android/server/telecom/settings/BlockedNumbersActivity.java
index 5fa5f06..edc8da6 100644
--- a/src/com/android/server/telecom/settings/BlockedNumbersActivity.java
+++ b/src/com/android/server/telecom/settings/BlockedNumbersActivity.java
@@ -34,6 +34,7 @@
 import android.database.Cursor;
 import android.os.Bundle;
 import android.provider.BlockedNumberContract;
+import android.provider.BlockedNumbersManager;
 import android.telephony.PhoneNumberFormattingTextWatcher;
 import android.telephony.PhoneNumberUtils;
 import android.telephony.TelephonyManager;
@@ -53,7 +54,10 @@
 import android.widget.TextView;
 import android.widget.Toast;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.telecom.R;
+import com.android.server.telecom.flags.FeatureFlags;
+import com.android.server.telecom.flags.FeatureFlagsImpl;
 
 
 /**
@@ -75,8 +79,10 @@
             BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER + " NOTNULL) AND (" +
             BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER + " != '' ))";
 
+    private BlockedNumbersManager mBlockedNumbersManager;
     private BlockNumberTaskFragment mBlockNumberTaskFragment;
     private BlockedNumbersAdapter mAdapter;
+    private FeatureFlags mFeatureFlags;
     private TextView mAddButton;
     private ProgressBar mProgressBar;
     private RelativeLayout mButterBar;
@@ -114,6 +120,7 @@
             return;
         }
 
+        mFeatureFlags = new FeatureFlagsImpl();
         FragmentManager fm = getFragmentManager();
         mBlockNumberTaskFragment =
                 (BlockNumberTaskFragment) fm.findFragmentByTag(TAG_BLOCK_NUMBER_TASK_FRAGMENT);
@@ -155,12 +162,15 @@
             }
         };
         IntentFilter blockStatusIntentFilter = new IntentFilter(
-                BlockedNumberContract.SystemContract.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED);
+                BlockedNumbersManager.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED);
         blockStatusIntentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
         registerReceiver(mBlockingStatusReceiver, blockStatusIntentFilter,
                 Context.RECEIVER_EXPORTED);
 
         getLoaderManager().initLoader(0, null, this);
+        mBlockedNumbersManager = mFeatureFlags.telecomMainlineBlockedNumbersManager()
+                ? getSystemService(BlockedNumbersManager.class)
+                : null;
     }
 
     @Override
@@ -183,7 +193,10 @@
     }
 
     private void updateButterBar() {
-        if (BlockedNumberContract.SystemContract.getBlockSuppressionStatus(this).isSuppressed) {
+        boolean isBlockSuppressionEnabled = mBlockedNumbersManager != null
+                ? mBlockedNumbersManager.getBlockSuppressionStatus().getIsSuppressed()
+                : BlockedNumberContract.SystemContract.getBlockSuppressionStatus(this).isSuppressed;
+        if (isBlockSuppressionEnabled) {
             mButterBar.setVisibility(View.VISIBLE);
         } else {
             mButterBar.setVisibility(View.GONE);
@@ -238,7 +251,11 @@
         if (view == mAddButton) {
             showAddBlockedNumberDialog();
         } else if (view == mReEnableButton) {
-            BlockedNumberContract.SystemContract.endBlockSuppression(this);
+            if (mBlockedNumbersManager != null) {
+                mBlockedNumbersManager.endBlockSuppression();
+            } else {
+                BlockedNumberContract.SystemContract.endBlockSuppression(this);
+            }
             mButterBar.setVisibility(View.GONE);
         }
     }
@@ -301,12 +318,13 @@
         }
     }
 
-    private boolean isEmergencyNumber(Context context, String number) {
+    @VisibleForTesting
+    public static boolean isEmergencyNumber(Context context, String number) {
         try {
             TelephonyManager tm = (TelephonyManager) context.getSystemService(
                     Context.TELEPHONY_SERVICE);
             return tm.isEmergencyNumber(number);
-        } catch (IllegalStateException ise) {
+        } catch (UnsupportedOperationException | IllegalStateException ignored) {
             return false;
         }
     }
diff --git a/src/com/android/server/telecom/settings/BlockedNumbersUtil.java b/src/com/android/server/telecom/settings/BlockedNumbersUtil.java
index 4be75f8..3e1da17 100644
--- a/src/com/android/server/telecom/settings/BlockedNumbersUtil.java
+++ b/src/com/android/server/telecom/settings/BlockedNumbersUtil.java
@@ -23,7 +23,8 @@
 import android.content.Intent;
 import android.os.PersistableBundle;
 import android.os.UserHandle;
-import android.provider.BlockedNumberContract.SystemContract;
+import android.provider.BlockedNumberContract;
+import android.provider.BlockedNumbersManager;
 import android.telephony.CarrierConfigManager;
 import android.telephony.PhoneNumberUtils;
 import android.text.BidiFormatter;
@@ -34,6 +35,7 @@
 
 import com.android.server.telecom.R;
 import com.android.server.telecom.SystemSettingsUtil;
+import com.android.server.telecom.flags.FeatureFlags;
 import com.android.server.telecom.ui.NotificationChannelManager;
 
 import java.util.Locale;
@@ -148,8 +150,11 @@
      * @return If {@code true} means the key enabled in the SharedPreferences,
      *            {@code false} otherwise.
      */
-    public static boolean getEnhancedBlockSetting(Context context, String key) {
-        return SystemContract.getEnhancedBlockSetting(context, key);
+    public static boolean getBlockedNumberSetting(Context context, String key,
+            FeatureFlags featureFlags) {
+        return featureFlags.telecomMainlineBlockedNumbersManager()
+                ? context.getSystemService(BlockedNumbersManager.class).getBlockedNumberSetting(key)
+                : BlockedNumberContract.SystemContract.getEnhancedBlockSetting(context, key);
     }
 
     /**
@@ -159,7 +164,13 @@
      * @param key preference key of SharedPreferences.
      * @param value the register value to the SharedPreferences.
      */
-    public static void setEnhancedBlockSetting(Context context, String key, boolean value) {
-        SystemContract.setEnhancedBlockSetting(context, key, value);
+    public static void setBlockedNumberSetting(Context context, String key, boolean value,
+            FeatureFlags featureFlags) {
+        if (featureFlags.telecomMainlineBlockedNumbersManager()) {
+            context.getSystemService(BlockedNumbersManager.class).setBlockedNumberSetting(key,
+                    value);
+        } else {
+            BlockedNumberContract.SystemContract.setEnhancedBlockSetting(context, key, value);
+        }
     }
 }
diff --git a/src/com/android/server/telecom/settings/CallBlockDisabledActivity.java b/src/com/android/server/telecom/settings/CallBlockDisabledActivity.java
index 5f42b37..cc66a2d 100644
--- a/src/com/android/server/telecom/settings/CallBlockDisabledActivity.java
+++ b/src/com/android/server/telecom/settings/CallBlockDisabledActivity.java
@@ -20,19 +20,23 @@
 import android.app.AlertDialog;
 import android.content.DialogInterface;
 import android.os.Bundle;
-import android.provider.BlockedNumberContract;
+import android.provider.BlockedNumbersManager;
 
 import com.android.server.telecom.R;
+import com.android.server.telecom.flags.FeatureFlags;
+import com.android.server.telecom.flags.FeatureFlagsImpl;
 
 /**
  * Shows a dialog when user taps an notification in notification tray.
  */
 public class CallBlockDisabledActivity extends Activity {
     private AlertDialog mDialog;
+    private FeatureFlags mFeatureFlags;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        mFeatureFlags = new FeatureFlagsImpl();
         showCallBlockingOffDialog();
     }
 
@@ -58,11 +62,11 @@
                 .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
                     @Override
                     public void onClick(DialogInterface dialog, int which) {
-                        BlockedNumbersUtil.setEnhancedBlockSetting(
+                        BlockedNumbersUtil.setBlockedNumberSetting(
                                 CallBlockDisabledActivity.this,
-                                BlockedNumberContract.SystemContract
+                                BlockedNumbersManager
                                         .ENHANCED_SETTING_KEY_SHOW_EMERGENCY_CALL_NOTIFICATION,
-                                false);
+                                false, mFeatureFlags);
                         BlockedNumbersUtil.updateEmergencyCallNotification(
                                 CallBlockDisabledActivity.this, false);
                         finish();
diff --git a/src/com/android/server/telecom/settings/EnableAccountPreferenceFragment.java b/src/com/android/server/telecom/settings/EnableAccountPreferenceFragment.java
index c2a0500..d9feaff 100644
--- a/src/com/android/server/telecom/settings/EnableAccountPreferenceFragment.java
+++ b/src/com/android/server/telecom/settings/EnableAccountPreferenceFragment.java
@@ -72,7 +72,8 @@
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        mTelecomManager = TelecomManager.from(getActivity());
+        Context context = getActivity();
+        mTelecomManager = context.getSystemService(TelecomManager.class);
     }
 
 
diff --git a/src/com/android/server/telecom/settings/EnhancedCallBlockingFragment.java b/src/com/android/server/telecom/settings/EnhancedCallBlockingFragment.java
index b1a1b0e..b54e273 100644
--- a/src/com/android/server/telecom/settings/EnhancedCallBlockingFragment.java
+++ b/src/com/android/server/telecom/settings/EnhancedCallBlockingFragment.java
@@ -23,16 +23,17 @@
 import android.preference.PreferenceFragment;
 import android.preference.PreferenceScreen;
 import android.preference.SwitchPreference;
-import android.provider.BlockedNumberContract.SystemContract;
+import android.provider.BlockedNumbersManager;
 import android.telephony.CarrierConfigManager;
 import android.telephony.SubscriptionManager;
 import android.telecom.Log;
 import android.view.LayoutInflater;
-import android.view.MenuItem;
 import android.view.View;
 import android.view.ViewGroup;
 
 import com.android.server.telecom.R;
+import com.android.server.telecom.flags.FeatureFlags;
+import com.android.server.telecom.flags.FeatureFlagsImpl;
 
 public class EnhancedCallBlockingFragment extends PreferenceFragment
         implements Preference.OnPreferenceChangeListener {
@@ -46,21 +47,25 @@
             "block_unavailable_calls_setting";
     private boolean mIsCombiningRestrictedAndUnknownOption = false;
     private boolean mIsCombiningUnavailableAndUnknownOption = false;
+    private FeatureFlags mFeatureFlags;
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         addPreferencesFromResource(R.xml.enhanced_call_blocking_settings);
+        mFeatureFlags = new FeatureFlagsImpl();
 
         maybeConfigureCallBlockingOptions();
 
-        setOnPreferenceChangeListener(SystemContract.ENHANCED_SETTING_KEY_BLOCK_UNREGISTERED);
-        setOnPreferenceChangeListener(SystemContract.ENHANCED_SETTING_KEY_BLOCK_PRIVATE);
-        setOnPreferenceChangeListener(SystemContract.ENHANCED_SETTING_KEY_BLOCK_PAYPHONE);
-        setOnPreferenceChangeListener(SystemContract.ENHANCED_SETTING_KEY_BLOCK_UNKNOWN);
-        setOnPreferenceChangeListener(SystemContract.ENHANCED_SETTING_KEY_BLOCK_UNAVAILABLE);
+        setOnPreferenceChangeListener(
+                BlockedNumbersManager.ENHANCED_SETTING_KEY_BLOCK_UNREGISTERED);
+        setOnPreferenceChangeListener(BlockedNumbersManager.ENHANCED_SETTING_KEY_BLOCK_PRIVATE);
+        setOnPreferenceChangeListener(BlockedNumbersManager.ENHANCED_SETTING_KEY_BLOCK_PAYPHONE);
+        setOnPreferenceChangeListener(BlockedNumbersManager.ENHANCED_SETTING_KEY_BLOCK_UNKNOWN);
+        setOnPreferenceChangeListener(BlockedNumbersManager.ENHANCED_SETTING_KEY_BLOCK_UNAVAILABLE);
         if (!showPayPhoneBlocking()) {
-            Preference payPhoneOption = getPreferenceScreen().findPreference(SystemContract.ENHANCED_SETTING_KEY_BLOCK_PAYPHONE);
+            Preference payPhoneOption = getPreferenceScreen()
+                    .findPreference(BlockedNumbersManager.ENHANCED_SETTING_KEY_BLOCK_PAYPHONE);
             getPreferenceScreen().removePreference(payPhoneOption);
         }
     }
@@ -122,13 +127,13 @@
     public void onResume() {
         super.onResume();
 
-        updateEnhancedBlockPref(SystemContract.ENHANCED_SETTING_KEY_BLOCK_UNREGISTERED);
-        updateEnhancedBlockPref(SystemContract.ENHANCED_SETTING_KEY_BLOCK_PRIVATE);
+        updateEnhancedBlockPref(BlockedNumbersManager.ENHANCED_SETTING_KEY_BLOCK_UNREGISTERED);
+        updateEnhancedBlockPref(BlockedNumbersManager.ENHANCED_SETTING_KEY_BLOCK_PRIVATE);
         if (showPayPhoneBlocking()) {
-            updateEnhancedBlockPref(SystemContract.ENHANCED_SETTING_KEY_BLOCK_PAYPHONE);
+            updateEnhancedBlockPref(BlockedNumbersManager.ENHANCED_SETTING_KEY_BLOCK_PAYPHONE);
         }
-        updateEnhancedBlockPref(SystemContract.ENHANCED_SETTING_KEY_BLOCK_UNKNOWN);
-        updateEnhancedBlockPref(SystemContract.ENHANCED_SETTING_KEY_BLOCK_UNAVAILABLE);
+        updateEnhancedBlockPref(BlockedNumbersManager.ENHANCED_SETTING_KEY_BLOCK_UNKNOWN);
+        updateEnhancedBlockPref(BlockedNumbersManager.ENHANCED_SETTING_KEY_BLOCK_UNAVAILABLE);
     }
 
     /**
@@ -137,7 +142,8 @@
     private void updateEnhancedBlockPref(String key) {
         SwitchPreference pref = (SwitchPreference) findPreference(key);
         if (pref != null) {
-            pref.setChecked(BlockedNumbersUtil.getEnhancedBlockSetting(getActivity(), key));
+            pref.setChecked(BlockedNumbersUtil.getBlockedNumberSetting(
+                    getActivity(), key, mFeatureFlags));
         }
     }
 
@@ -147,19 +153,19 @@
             if (mIsCombiningRestrictedAndUnknownOption) {
                 Log.i(this, "onPreferenceChange: changing %s and %s to %b",
                         preference.getKey(), BLOCK_RESTRICTED_NUMBERS_KEY, (boolean) objValue);
-                BlockedNumbersUtil.setEnhancedBlockSetting(getActivity(),
-                        BLOCK_RESTRICTED_NUMBERS_KEY, (boolean) objValue);
+                BlockedNumbersUtil.setBlockedNumberSetting(getActivity(),
+                        BLOCK_RESTRICTED_NUMBERS_KEY, (boolean) objValue, mFeatureFlags);
             }
 
             if (mIsCombiningUnavailableAndUnknownOption) {
                 Log.i(this, "onPreferenceChange: changing %s and %s to %b",
                         preference.getKey(), BLOCK_UNAVAILABLE_NUMBERS_KEY, (boolean) objValue);
-                BlockedNumbersUtil.setEnhancedBlockSetting(getActivity(),
-                        BLOCK_UNAVAILABLE_NUMBERS_KEY, (boolean) objValue);
+                BlockedNumbersUtil.setBlockedNumberSetting(getActivity(),
+                        BLOCK_UNAVAILABLE_NUMBERS_KEY, (boolean) objValue, mFeatureFlags);
             }
         }
-        BlockedNumbersUtil.setEnhancedBlockSetting(getActivity(), preference.getKey(),
-                (boolean) objValue);
+        BlockedNumbersUtil.setBlockedNumberSetting(getActivity(), preference.getKey(),
+                (boolean) objValue, mFeatureFlags);
         return true;
     }
 
diff --git a/src/com/android/server/telecom/ui/DisconnectedCallNotifier.java b/src/com/android/server/telecom/ui/DisconnectedCallNotifier.java
index 1604285..04228c1 100644
--- a/src/com/android/server/telecom/ui/DisconnectedCallNotifier.java
+++ b/src/com/android/server/telecom/ui/DisconnectedCallNotifier.java
@@ -41,6 +41,7 @@
 import android.text.TextDirectionHeuristics;
 import android.text.TextUtils;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallState;
 import com.android.server.telecom.CallsManager;
@@ -285,12 +286,18 @@
      *      network location.  If the network location does not exist, fall back to the locale
      *      setting.
      */
-    private String getCurrentCountryIso(Context context) {
+    @VisibleForTesting
+    public String getCurrentCountryIso(Context context) {
         // Without framework function calls, this seems to be the most accurate location service
         // we can rely on.
         final TelephonyManager telephonyManager =
                 (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
-        String countryIso = telephonyManager.getNetworkCountryIso().toUpperCase();
+        String countryIso;
+        try {
+            countryIso = telephonyManager.getNetworkCountryIso().toUpperCase();
+        } catch (UnsupportedOperationException ignored) {
+            countryIso = null;
+        }
 
         if (countryIso == null) {
             countryIso = Locale.getDefault().getCountry();
diff --git a/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java b/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java
index 6b97f97..220b44e 100644
--- a/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java
+++ b/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java
@@ -20,6 +20,7 @@
 import static android.app.admin.DevicePolicyResources.Strings.Telecomm.NOTIFICATION_MISSED_WORK_CALL_TITLE;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.BroadcastOptions;
 import android.app.Notification;
 import android.app.NotificationManager;
@@ -42,6 +43,7 @@
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.UserHandle;
+import android.provider.CallLog;
 import android.provider.CallLog.Calls;
 import android.telecom.CallerInfo;
 import android.telecom.Log;
@@ -57,11 +59,13 @@
 import android.util.ArrayMap;
 import android.util.ArraySet;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.telecom.CallerInfoLookupHelper;
 import com.android.server.telecom.CallsManagerListenerBase;
 import com.android.server.telecom.Constants;
 import com.android.server.telecom.DefaultDialerCache;
 import com.android.server.telecom.DeviceIdleControllerAdapter;
+import com.android.server.telecom.flags.FeatureFlags;
 import com.android.server.telecom.MissedCallNotifier;
 import com.android.server.telecom.PhoneAccountRegistrar;
 import com.android.server.telecom.R;
@@ -87,7 +91,8 @@
         MissedCallNotifier makeMissedCallNotifierImpl(Context context,
                 PhoneAccountRegistrar phoneAccountRegistrar,
                 DefaultDialerCache defaultDialerCache,
-                DeviceIdleControllerAdapter deviceIdleControllerAdapter);
+                DeviceIdleControllerAdapter deviceIdleControllerAdapter,
+                FeatureFlags featureFlags);
     }
 
     public interface NotificationBuilderFactory {
@@ -141,19 +146,22 @@
     private final Map<UserHandle, Integer> mMissedCallCounts;
 
     private Set<UserHandle> mUsersToLoadAfterBootComplete = new ArraySet<>();
+    private FeatureFlags mFeatureFlags;
 
     public MissedCallNotifierImpl(Context context, PhoneAccountRegistrar phoneAccountRegistrar,
             DefaultDialerCache defaultDialerCache,
-            DeviceIdleControllerAdapter deviceIdleControllerAdapter) {
+            DeviceIdleControllerAdapter deviceIdleControllerAdapter,
+            FeatureFlags featureFlags) {
         this(context, phoneAccountRegistrar, defaultDialerCache,
-                new DefaultNotificationBuilderFactory(), deviceIdleControllerAdapter);
+                new DefaultNotificationBuilderFactory(), deviceIdleControllerAdapter, featureFlags);
     }
 
     public MissedCallNotifierImpl(Context context,
             PhoneAccountRegistrar phoneAccountRegistrar,
             DefaultDialerCache defaultDialerCache,
             NotificationBuilderFactory notificationBuilderFactory,
-            DeviceIdleControllerAdapter deviceIdleControllerAdapter) {
+            DeviceIdleControllerAdapter deviceIdleControllerAdapter,
+            FeatureFlags featureFlags) {
         mContext = context;
         mPhoneAccountRegistrar = phoneAccountRegistrar;
         mNotificationManager =
@@ -163,6 +171,7 @@
 
         mNotificationBuilderFactory = notificationBuilderFactory;
         mMissedCallCounts = new ArrayMap<>();
+        mFeatureFlags = featureFlags;
     }
 
     /** Clears missed call notification and marks the call log's missed calls as read. */
@@ -261,17 +270,17 @@
     }
 
     private void sendNotificationThroughDefaultDialer(String dialerPackage, CallInfo callInfo,
-            UserHandle userHandle, int missedCallCount) {
+            UserHandle userHandle, int missedCallCount, @Nullable Uri uri) {
         Intent intent = getShowMissedCallIntentForDefaultDialer(dialerPackage)
             .setFlags(Intent.FLAG_RECEIVER_FOREGROUND)
             .putExtra(TelecomManager.EXTRA_CLEAR_MISSED_CALLS_INTENT,
                     createClearMissedCallsPendingIntent(userHandle))
             .putExtra(TelecomManager.EXTRA_NOTIFICATION_COUNT, missedCallCount)
+            .putExtra(TelecomManager.EXTRA_CALL_LOG_URI, uri)
             .putExtra(TelecomManager.EXTRA_NOTIFICATION_PHONE_NUMBER,
                     callInfo == null ? null : callInfo.getPhoneNumber())
             .putExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE,
                     callInfo == null ? null : callInfo.getPhoneAccountHandle());
-
         if (missedCallCount == 1 && callInfo != null) {
             final Uri handleUri = callInfo.getHandle();
             String handle = handleUri == null ? null : handleUri.getSchemeSpecificPart();
@@ -295,7 +304,7 @@
      * @param callInfo The missed call.
      */
     @Override
-    public void showMissedCallNotification(@NonNull CallInfo callInfo) {
+    public void showMissedCallNotification(@NonNull CallInfo callInfo, @Nullable Uri uri) {
         final PhoneAccountHandle phoneAccountHandle = callInfo.getPhoneAccountHandle();
         final PhoneAccount phoneAccount =
                 mPhoneAccountRegistrar.getPhoneAccountUnchecked(phoneAccountHandle);
@@ -306,10 +315,11 @@
         } else {
             userHandle = phoneAccountHandle.getUserHandle();
         }
-        showMissedCallNotification(callInfo, userHandle);
+        showMissedCallNotification(callInfo, userHandle, uri);
     }
 
-    private void showMissedCallNotification(@NonNull CallInfo callInfo, UserHandle userHandle) {
+    private void showMissedCallNotification(@NonNull CallInfo callInfo, UserHandle userHandle,
+            @Nullable Uri uri) {
         int missedCallCounts;
         synchronized (mMissedCallCountsLock) {
             Integer currentCount = mMissedCallCounts.get(userHandle);
@@ -324,7 +334,7 @@
         String dialerPackage = getDefaultDialerPackage(userHandle);
         if (shouldManageNotificationThroughDefaultDialer(dialerPackage, userHandle)) {
             sendNotificationThroughDefaultDialer(dialerPackage, callInfo, userHandle,
-                    missedCallCounts);
+                    missedCallCounts, uri);
             return;
         }
 
@@ -446,7 +456,7 @@
         String dialerPackage = getDefaultDialerPackage(userHandle);
         if (shouldManageNotificationThroughDefaultDialer(dialerPackage, userHandle)) {
             sendNotificationThroughDefaultDialer(dialerPackage, null, userHandle,
-                    0 /* missedCallCount */);
+                    /* missedCallCount= */ 0, /* uri= */ null);
             return;
         }
 
@@ -497,12 +507,18 @@
      *      network location.  If the network location does not exist, fall back to the locale
      *      setting.
      */
-    private String getCurrentCountryIso(Context context) {
+    @VisibleForTesting
+    public String getCurrentCountryIso(Context context) {
         // Without framework function calls, this seems to be the most accurate location service
         // we can rely on.
         final TelephonyManager telephonyManager =
                 (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
-        String countryIso = telephonyManager.getNetworkCountryIso().toUpperCase();
+        String countryIso;
+        try {
+            countryIso = telephonyManager.getNetworkCountryIso().toUpperCase();
+        } catch (UnsupportedOperationException ignored) {
+            countryIso = null;
+        }
 
         if (countryIso == null) {
             countryIso = Locale.getDefault().getCountry();
@@ -631,6 +647,13 @@
                         while (cursor.moveToNext()) {
                             // Get data about the missed call from the cursor
                             final String handleString = cursor.getString(CALL_LOG_COLUMN_NUMBER);
+                            final Uri uri;
+                            if (mFeatureFlags.addCallUriForMissedCalls()){
+                                uri = Calls.CONTENT_URI.buildUpon().appendPath(
+                                        Long.toString(cursor.getInt(CALL_LOG_COLUMN_ID))).build();
+                            }else{
+                                uri = null;
+                            }
                             final int presentation =
                                     cursor.getInt(CALL_LOG_COLUMN_NUMBER_PRESENTATION);
                             final long date = cursor.getLong(CALL_LOG_COLUMN_DATE);
@@ -663,7 +686,8 @@
                                                 // null, just show the notification.
                                                 CallInfo callInfo = callInfoFactory.makeCallInfo(
                                                         info, null, handle, date);
-                                                showMissedCallNotification(callInfo, userHandle);
+                                                showMissedCallNotification(callInfo, userHandle,
+                                                        /* uri= */ uri);
                                             }
                                         }
 
@@ -678,7 +702,8 @@
                                             }
                                             CallInfo callInfo = callInfoFactory.makeCallInfo(
                                                     info, null, handle, date);
-                                            showMissedCallNotification(callInfo, userHandle);
+                                            showMissedCallNotification(callInfo, userHandle,
+                                                    /* uri= */ uri);
                                         }
                                     }
                             );
diff --git a/src/com/android/server/telecom/ui/ToastFactory.java b/src/com/android/server/telecom/ui/ToastFactory.java
index 75b69da..1561f0b 100644
--- a/src/com/android/server/telecom/ui/ToastFactory.java
+++ b/src/com/android/server/telecom/ui/ToastFactory.java
@@ -21,6 +21,6 @@
 import android.widget.Toast;
 
 public interface ToastFactory {
-    Toast makeText(Context context, @StringRes int resId, int duration);
-    Toast makeText(Context context, CharSequence text, int duration);
+    void makeText(Context context, @StringRes int resId, int duration);
+    void makeText(Context context, CharSequence text, int duration);
 }
diff --git a/src/com/android/server/telecom/voip/CallEventCallbackAckTransaction.java b/src/com/android/server/telecom/voip/CallEventCallbackAckTransaction.java
index 93d9836..9e140a7 100644
--- a/src/com/android/server/telecom/voip/CallEventCallbackAckTransaction.java
+++ b/src/com/android/server/telecom/voip/CallEventCallbackAckTransaction.java
@@ -125,7 +125,7 @@
 
         try {
             // wait for the client to ack that CallEventCallback
-            boolean success = latch.await(VoipCallTransaction.TIMEOUT_LIMIT, TimeUnit.MILLISECONDS);
+            boolean success = latch.await(mTransactionTimeoutMs, TimeUnit.MILLISECONDS);
             if (!success) {
                 // client send onError and failed to complete transaction
                 Log.i(TAG, String.format("CallEventCallbackAckTransaction:"
diff --git a/src/com/android/server/telecom/voip/EndpointChangeTransaction.java b/src/com/android/server/telecom/voip/EndpointChangeTransaction.java
index e037a79..6841fcf 100644
--- a/src/com/android/server/telecom/voip/EndpointChangeTransaction.java
+++ b/src/com/android/server/telecom/voip/EndpointChangeTransaction.java
@@ -19,6 +19,7 @@
 import android.os.Bundle;
 import android.os.ResultReceiver;
 import android.telecom.CallEndpoint;
+import android.telecom.CallException;
 import android.util.Log;
 
 import com.android.server.telecom.CallsManager;
@@ -49,8 +50,9 @@
                     future.complete(new VoipCallTransactionResult(
                             VoipCallTransactionResult.RESULT_SUCCEED, null));
                 } else {
+                    // TODO:: define errors in CallException class. b/335703584
                     future.complete(new VoipCallTransactionResult(
-                            VoipCallTransactionResult.RESULT_FAILED, null));
+                            CallException.CODE_ERROR_UNKNOWN, null));
                 }
             }
         });
diff --git a/src/com/android/server/telecom/voip/IncomingCallTransaction.java b/src/com/android/server/telecom/voip/IncomingCallTransaction.java
index d35030c..ed0c7d6 100644
--- a/src/com/android/server/telecom/voip/IncomingCallTransaction.java
+++ b/src/com/android/server/telecom/voip/IncomingCallTransaction.java
@@ -19,14 +19,18 @@
 import static android.telecom.CallAttributes.CALL_CAPABILITIES_KEY;
 import static android.telecom.CallAttributes.DISPLAY_NAME_KEY;
 
+import static com.android.server.telecom.voip.VideoStateTranslation.TransactionalVideoStateToVideoProfileState;
+
 import android.os.Bundle;
 import android.telecom.CallAttributes;
 import android.telecom.CallException;
 import android.telecom.TelecomManager;
 import android.util.Log;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.flags.FeatureFlags;
 
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionStage;
@@ -38,19 +42,25 @@
     private final CallAttributes mCallAttributes;
     private final CallsManager mCallsManager;
     private final Bundle mExtras;
+    private FeatureFlags mFeatureFlags;
+
+    public void setFeatureFlags(FeatureFlags featureFlags) {
+        mFeatureFlags = featureFlags;
+    }
 
     public IncomingCallTransaction(String callId, CallAttributes callAttributes,
-            CallsManager callsManager, Bundle extras) {
+            CallsManager callsManager, Bundle extras, FeatureFlags featureFlags) {
         super(callsManager.getLock());
         mExtras = extras;
         mCallId = callId;
         mCallAttributes = callAttributes;
         mCallsManager = callsManager;
+        mFeatureFlags = featureFlags;
     }
 
     public IncomingCallTransaction(String callId, CallAttributes callAttributes,
-            CallsManager callsManager) {
-        this(callId, callAttributes, callsManager, new Bundle());
+            CallsManager callsManager, FeatureFlags featureFlags) {
+        this(callId, callAttributes, callsManager, new Bundle(), featureFlags);
     }
 
     @Override
@@ -77,10 +87,19 @@
         }
     }
 
-    private Bundle generateExtras(CallAttributes callAttributes) {
+    @VisibleForTesting
+    public Bundle generateExtras(CallAttributes callAttributes) {
         mExtras.putString(TelecomManager.TRANSACTION_CALL_ID_KEY, mCallId);
         mExtras.putInt(CALL_CAPABILITIES_KEY, callAttributes.getCallCapabilities());
-        mExtras.putInt(TelecomManager.EXTRA_INCOMING_VIDEO_STATE, callAttributes.getCallType());
+        if (mFeatureFlags.transactionalVideoState()) {
+            // Transactional calls need to remap the CallAttributes video state to the existing
+            // VideoProfile for consistency.
+            mExtras.putInt(TelecomManager.EXTRA_INCOMING_VIDEO_STATE,
+                    TransactionalVideoStateToVideoProfileState(callAttributes.getCallType()));
+        } else {
+            mExtras.putInt(TelecomManager.EXTRA_INCOMING_VIDEO_STATE,
+                    callAttributes.getCallType());
+        }
         mExtras.putCharSequence(DISPLAY_NAME_KEY, callAttributes.getDisplayName());
         mExtras.putParcelable(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS,
                 callAttributes.getAddress());
diff --git a/src/com/android/server/telecom/voip/MaybeHoldCallForNewCallTransaction.java b/src/com/android/server/telecom/voip/MaybeHoldCallForNewCallTransaction.java
index a245c1c..3bed088 100644
--- a/src/com/android/server/telecom/voip/MaybeHoldCallForNewCallTransaction.java
+++ b/src/com/android/server/telecom/voip/MaybeHoldCallForNewCallTransaction.java
@@ -26,16 +26,23 @@
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionStage;
 
+/**
+ * This VoipCallTransaction is responsible for holding any active call in favor of a new call
+ * request. If the active call cannot be held or disconnected, the transaction will fail.
+ */
 public class MaybeHoldCallForNewCallTransaction extends VoipCallTransaction {
 
     private static final String TAG = MaybeHoldCallForNewCallTransaction.class.getSimpleName();
     private final CallsManager mCallsManager;
     private final Call mCall;
+    private final boolean mIsCallControlRequest;
 
-    public MaybeHoldCallForNewCallTransaction(CallsManager callsManager, Call call) {
+    public MaybeHoldCallForNewCallTransaction(CallsManager callsManager, Call call,
+            boolean isCallControlRequest) {
         super(callsManager.getLock());
         mCallsManager = callsManager;
         mCall = call;
+        mIsCallControlRequest = isCallControlRequest;
     }
 
     @Override
@@ -43,7 +50,8 @@
         Log.d(TAG, "processTransaction");
         CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
 
-        mCallsManager.transactionHoldPotentialActiveCallForNewCall(mCall, new OutcomeReceiver<>() {
+        mCallsManager.transactionHoldPotentialActiveCallForNewCall(mCall, mIsCallControlRequest,
+                new OutcomeReceiver<>() {
             @Override
             public void onResult(Boolean result) {
                 Log.d(TAG, "processTransaction: onResult");
diff --git a/src/com/android/server/telecom/voip/OutgoingCallTransaction.java b/src/com/android/server/telecom/voip/OutgoingCallTransaction.java
index b2625e6..8c970db 100644
--- a/src/com/android/server/telecom/voip/OutgoingCallTransaction.java
+++ b/src/com/android/server/telecom/voip/OutgoingCallTransaction.java
@@ -21,6 +21,8 @@
 import static android.telecom.CallAttributes.DISPLAY_NAME_KEY;
 import static android.telecom.CallException.CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME;
 
+import static com.android.server.telecom.voip.VideoStateTranslation.TransactionalVideoStateToVideoProfileState;
+
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
@@ -29,9 +31,11 @@
 import android.telecom.TelecomManager;
 import android.util.Log;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallsManager;
 import com.android.server.telecom.LoggedHandlerExecutor;
+import com.android.server.telecom.flags.FeatureFlags;
 
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionStage;
@@ -45,9 +49,14 @@
     private final CallAttributes mCallAttributes;
     private final CallsManager mCallsManager;
     private final Bundle mExtras;
+    private FeatureFlags mFeatureFlags;
+
+    public void setFeatureFlags(FeatureFlags featureFlags) {
+        mFeatureFlags = featureFlags;
+    }
 
     public OutgoingCallTransaction(String callId, Context context, CallAttributes callAttributes,
-            CallsManager callsManager, Bundle extras) {
+            CallsManager callsManager, Bundle extras, FeatureFlags featureFlags) {
         super(callsManager.getLock());
         mCallId = callId;
         mContext = context;
@@ -55,11 +64,12 @@
         mCallsManager = callsManager;
         mExtras = extras;
         mCallingPackage = mContext.getOpPackageName();
+        mFeatureFlags = featureFlags;
     }
 
     public OutgoingCallTransaction(String callId, Context context, CallAttributes callAttributes,
-            CallsManager callsManager) {
-        this(callId, context, callAttributes, callsManager, new Bundle());
+            CallsManager callsManager, FeatureFlags featureFlags) {
+        this(callId, context, callAttributes, callsManager, new Bundle(), featureFlags);
     }
 
     @Override
@@ -121,12 +131,20 @@
         }
     }
 
-    private Bundle generateExtras(CallAttributes callAttributes) {
+    @VisibleForTesting
+    public Bundle generateExtras(CallAttributes callAttributes) {
         mExtras.setDefusable(true);
         mExtras.putString(TelecomManager.TRANSACTION_CALL_ID_KEY, mCallId);
         mExtras.putInt(CALL_CAPABILITIES_KEY, callAttributes.getCallCapabilities());
-        mExtras.putInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
-                callAttributes.getCallType());
+        if (mFeatureFlags.transactionalVideoState()) {
+            // Transactional calls need to remap the CallAttributes video state to the existing
+            // VideoProfile for consistency.
+            mExtras.putInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
+                    TransactionalVideoStateToVideoProfileState(callAttributes.getCallType()));
+        } else {
+            mExtras.putInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
+                    callAttributes.getCallType());
+        }
         mExtras.putCharSequence(DISPLAY_NAME_KEY, callAttributes.getDisplayName());
         return mExtras;
     }
diff --git a/src/com/android/server/telecom/voip/ParallelTransaction.java b/src/com/android/server/telecom/voip/ParallelTransaction.java
index 6176087..e235ead 100644
--- a/src/com/android/server/telecom/voip/ParallelTransaction.java
+++ b/src/com/android/server/telecom/voip/ParallelTransaction.java
@@ -16,6 +16,8 @@
 
 package com.android.server.telecom.voip;
 
+import android.telecom.CallException;
+
 import com.android.server.telecom.LoggedHandlerExecutor;
 import com.android.server.telecom.TelecomSystem;
 
@@ -33,77 +35,56 @@
     }
 
     @Override
-    public void start() {
-        // post timeout work
-        CompletableFuture<Void> future = new CompletableFuture<>();
-        mHandler.postDelayed(() -> future.complete(null), TIMEOUT_LIMIT);
-        future.thenApplyAsync((x) -> {
-            if (mCompleted.getAndSet(true)) {
-                return null;
-            }
-            if (mCompleteListener != null) {
-                mCompleteListener.onTransactionTimeout(mTransactionName);
-            }
-            finish();
-            return null;
-        }, new LoggedHandlerExecutor(mHandler, mTransactionName + "@" + hashCode()
-                + ".s", mLock));
+    public void processTransactions() {
+        if (mSubTransactions == null || mSubTransactions.isEmpty()) {
+            scheduleTransaction();
+            return;
+        }
+        TransactionManager.TransactionCompleteListener subTransactionListener =
+                new TransactionManager.TransactionCompleteListener() {
+                    private final AtomicInteger mCount = new AtomicInteger(mSubTransactions.size());
 
-        if (mSubTransactions != null && mSubTransactions.size() > 0) {
-            TransactionManager.TransactionCompleteListener subTransactionListener =
-                    new TransactionManager.TransactionCompleteListener() {
-                        private final AtomicInteger mCount = new AtomicInteger(mSubTransactions.size());
-
-                        @Override
-                        public void onTransactionCompleted(VoipCallTransactionResult result,
-                                String transactionName) {
-                            if (result.getResult() != VoipCallTransactionResult.RESULT_SUCCEED) {
-                                CompletableFuture.completedFuture(null).thenApplyAsync(
-                                        (x) -> {
-                                            VoipCallTransactionResult mainResult =
-                                                    new VoipCallTransactionResult(
-                                                            VoipCallTransactionResult.RESULT_FAILED,
-                                                            String.format(
-                                                                    "sub transaction %s failed",
-                                                                    transactionName));
-                                            mCompleteListener.onTransactionCompleted(mainResult,
-                                                    mTransactionName);
-                                            finish();
-                                            return null;
-                                        }, new LoggedHandlerExecutor(mHandler,
-                                                mTransactionName + "@" + hashCode()
-                                                        + ".oTC", mLock));
-                            } else {
-                                if (mCount.decrementAndGet() == 0) {
-                                    scheduleTransaction();
-                                }
-                            }
-                        }
-
-                        @Override
-                        public void onTransactionTimeout(String transactionName) {
+                    @Override
+                    public void onTransactionCompleted(VoipCallTransactionResult result,
+                            String transactionName) {
+                        if (result.getResult() != VoipCallTransactionResult.RESULT_SUCCEED) {
                             CompletableFuture.completedFuture(null).thenApplyAsync(
                                     (x) -> {
-                                        VoipCallTransactionResult mainResult =
-                                                new VoipCallTransactionResult(
-                                                VoipCallTransactionResult.RESULT_FAILED,
-                                                String.format("sub transaction %s timed out",
-                                                        transactionName));
-                                        mCompleteListener.onTransactionCompleted(mainResult,
+                                        finish(result);
+                                        mCompleteListener.onTransactionCompleted(result,
                                                 mTransactionName);
-                                        finish();
                                         return null;
                                     }, new LoggedHandlerExecutor(mHandler,
                                             mTransactionName + "@" + hashCode()
-                                                    + ".oTT", mLock));
+                                                    + ".oTC", mLock));
+                        } else {
+                            if (mCount.decrementAndGet() == 0) {
+                                scheduleTransaction();
+                            }
                         }
-                    };
-            for (VoipCallTransaction transaction : mSubTransactions) {
-                transaction.setCompleteListener(subTransactionListener);
-                transaction.start();
-            }
-        } else {
-            scheduleTransaction();
+                    }
+
+                    @Override
+                    public void onTransactionTimeout(String transactionName) {
+                        CompletableFuture.completedFuture(null).thenApplyAsync(
+                                (x) -> {
+                                    VoipCallTransactionResult mainResult =
+                                            new VoipCallTransactionResult(
+                                                    CallException.CODE_OPERATION_TIMED_OUT,
+                                            String.format("sub transaction %s timed out",
+                                                    transactionName));
+                                    finish(mainResult);
+                                    mCompleteListener.onTransactionCompleted(mainResult,
+                                            mTransactionName);
+                                    return null;
+                                }, new LoggedHandlerExecutor(mHandler,
+                                        mTransactionName + "@" + hashCode()
+                                                + ".oTT", mLock));
+                    }
+                };
+        for (VoipCallTransaction transaction : mSubTransactions) {
+            transaction.setCompleteListener(subTransactionListener);
+            transaction.start();
         }
     }
 }
diff --git a/src/com/android/server/telecom/voip/RequestNewActiveCallTransaction.java b/src/com/android/server/telecom/voip/RequestNewActiveCallTransaction.java
index f586cc3..e3aed8e 100644
--- a/src/com/android/server/telecom/voip/RequestNewActiveCallTransaction.java
+++ b/src/com/android/server/telecom/voip/RequestNewActiveCallTransaction.java
@@ -17,7 +17,6 @@
 package com.android.server.telecom.voip;
 
 import android.os.OutcomeReceiver;
-import android.telecom.CallAttributes;
 import android.telecom.CallException;
 import android.util.Log;
 
@@ -25,6 +24,7 @@
 import com.android.server.telecom.CallState;
 import com.android.server.telecom.CallsManager;
 import com.android.server.telecom.ConnectionServiceFocusManager;
+import com.android.server.telecom.flags.Flags;
 
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionStage;
@@ -69,7 +69,8 @@
             return future;
         }
 
-        if (mCallsManager.getActiveCall() != null) {
+        if (!Flags.transactionalHoldDisconnectsUnholdable() &&
+                mCallsManager.getActiveCall() != null) {
             future.complete(new VoipCallTransactionResult(
                     CallException.CODE_CALL_CANNOT_BE_SET_TO_ACTIVE,
                     "Already an active call. Request hold on current active call."));
diff --git a/src/com/android/server/telecom/voip/RequestVideoStateTransaction.java b/src/com/android/server/telecom/voip/RequestVideoStateTransaction.java
new file mode 100644
index 0000000..c1bc343
--- /dev/null
+++ b/src/com/android/server/telecom/voip/RequestVideoStateTransaction.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2024 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.voip;
+
+import static com.android.server.telecom.voip.VideoStateTranslation.TransactionalVideoStateToVideoProfileState;
+
+import android.telecom.CallException;
+import android.telecom.VideoProfile;
+import android.util.Log;
+
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.Call;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+
+public class RequestVideoStateTransaction extends VoipCallTransaction {
+
+    private static final String TAG = RequestVideoStateTransaction.class.getSimpleName();
+    private final Call mCall;
+    private final int mVideoProfileState;
+
+    public RequestVideoStateTransaction(CallsManager callsManager, Call call,
+            int transactionalVideoState) {
+        super(callsManager.getLock());
+        mCall = call;
+        mVideoProfileState = TransactionalVideoStateToVideoProfileState(transactionalVideoState);
+    }
+
+    @Override
+    public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+        Log.d(TAG, "processTransaction");
+        CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
+
+        if (isRequestingVideoTransmission(mVideoProfileState) &&
+                !mCall.isVideoCallingSupportedByPhoneAccount()) {
+            future.complete(new VoipCallTransactionResult(
+                    CallException.CODE_ERROR_UNKNOWN /*TODO:: define error code. b/335703584 */,
+                    "Video calling is not supported by the target account"));
+        } else {
+            mCall.setVideoState(mVideoProfileState);
+            future.complete(new VoipCallTransactionResult(
+                    VoipCallTransactionResult.RESULT_SUCCEED,
+                    "The Video State was changed successfully"));
+        }
+        return future;
+    }
+
+    private boolean isRequestingVideoTransmission(int targetVideoState) {
+        return targetVideoState != VideoProfile.STATE_AUDIO_ONLY;
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/server/telecom/voip/SerialTransaction.java b/src/com/android/server/telecom/voip/SerialTransaction.java
index b35b471..748f285 100644
--- a/src/com/android/server/telecom/voip/SerialTransaction.java
+++ b/src/com/android/server/telecom/voip/SerialTransaction.java
@@ -16,11 +16,14 @@
 
 package com.android.server.telecom.voip;
 
+import android.telecom.CallException;
+
 import com.android.server.telecom.LoggedHandlerExecutor;
 import com.android.server.telecom.TelecomSystem;
 
 import java.util.List;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicInteger;
 
 /**
  * A VoipCallTransaction implementation that its sub transactions will be executed in serial
@@ -36,82 +39,65 @@
     }
 
     @Override
-    public void start() {
-        // post timeout work
-        CompletableFuture<Void> future = new CompletableFuture<>();
-        mHandler.postDelayed(() -> future.complete(null), TIMEOUT_LIMIT);
-        future.thenApplyAsync((x) -> {
-            if (mCompleted.getAndSet(true)) {
-                return null;
-            }
-            if (mCompleteListener != null) {
-                mCompleteListener.onTransactionTimeout(mTransactionName);
-            }
-            finish();
-            return null;
-        }, new LoggedHandlerExecutor(mHandler, mTransactionName + "@" + hashCode()
-                + ".s", mLock));
+    public void processTransactions() {
+        if (mSubTransactions == null || mSubTransactions.isEmpty()) {
+            scheduleTransaction();
+            return;
+        }
+        TransactionManager.TransactionCompleteListener subTransactionListener =
+                new TransactionManager.TransactionCompleteListener() {
+                    private final AtomicInteger mTransactionIndex = new AtomicInteger(0);
 
-        if (mSubTransactions != null && mSubTransactions.size() > 0) {
-            TransactionManager.TransactionCompleteListener subTransactionListener =
-                    new TransactionManager.TransactionCompleteListener() {
-
-                        @Override
-                        public void onTransactionCompleted(VoipCallTransactionResult result,
-                                String transactionName) {
-                            if (result.getResult() != VoipCallTransactionResult.RESULT_SUCCEED) {
-                                handleTransactionFailure();
-                                CompletableFuture.completedFuture(null).thenApplyAsync(
-                                        (x) -> {
-                                            VoipCallTransactionResult mainResult =
-                                                    new VoipCallTransactionResult(
-                                                            VoipCallTransactionResult.RESULT_FAILED,
-                                                            String.format(
-                                                                    "sub transaction %s failed",
-                                                                    transactionName));
-                                            mCompleteListener.onTransactionCompleted(mainResult,
-                                                    mTransactionName);
-                                            finish();
-                                            return null;
-                                        }, new LoggedHandlerExecutor(mHandler,
-                                                mTransactionName + "@" + hashCode()
-                                                        + ".oTC", mLock));
-                            } else {
-                                if (mSubTransactions.size() > 0) {
-                                    VoipCallTransaction transaction = mSubTransactions.remove(0);
-                                    transaction.setCompleteListener(this);
-                                    transaction.start();
-                                } else {
-                                    scheduleTransaction();
-                                }
-                            }
-                        }
-
-                        @Override
-                        public void onTransactionTimeout(String transactionName) {
+                    @Override
+                    public void onTransactionCompleted(VoipCallTransactionResult result,
+                            String transactionName) {
+                        if (result.getResult() != VoipCallTransactionResult.RESULT_SUCCEED) {
                             handleTransactionFailure();
                             CompletableFuture.completedFuture(null).thenApplyAsync(
                                     (x) -> {
-                                        VoipCallTransactionResult mainResult =
-                                                new VoipCallTransactionResult(
-                                                VoipCallTransactionResult.RESULT_FAILED,
-                                                String.format("sub transaction %s timed out",
-                                                        transactionName));
-                                        mCompleteListener.onTransactionCompleted(mainResult,
+                                        finish(result);
+                                        mCompleteListener.onTransactionCompleted(result,
                                                 mTransactionName);
-                                        finish();
                                         return null;
                                     }, new LoggedHandlerExecutor(mHandler,
                                             mTransactionName + "@" + hashCode()
-                                                    + ".oTT", mLock));
+                                                    + ".oTC", mLock));
+                        } else {
+                            int currTransactionIndex = mTransactionIndex.incrementAndGet();
+                            if (currTransactionIndex < mSubTransactions.size()) {
+                                VoipCallTransaction transaction = mSubTransactions.get(
+                                        currTransactionIndex);
+                                transaction.setCompleteListener(this);
+                                transaction.start();
+                            } else {
+                                scheduleTransaction();
+                            }
                         }
-                    };
-            VoipCallTransaction transaction = mSubTransactions.remove(0);
-            transaction.setCompleteListener(subTransactionListener);
-            transaction.start();
-        } else {
-            scheduleTransaction();
-        }
+                    }
+
+                    @Override
+                    public void onTransactionTimeout(String transactionName) {
+                        handleTransactionFailure();
+                        CompletableFuture.completedFuture(null).thenApplyAsync(
+                                (x) -> {
+                                    VoipCallTransactionResult mainResult =
+                                            new VoipCallTransactionResult(
+                                                    CallException.CODE_OPERATION_TIMED_OUT,
+                                            String.format("sub transaction %s timed out",
+                                                    transactionName));
+                                    finish(mainResult);
+                                    mCompleteListener.onTransactionCompleted(mainResult,
+                                            mTransactionName);
+                                    return null;
+                                }, new LoggedHandlerExecutor(mHandler,
+                                        mTransactionName + "@" + hashCode()
+                                                + ".oTT", mLock));
+                    }
+                };
+        VoipCallTransaction transaction = mSubTransactions.get(0);
+        transaction.setCompleteListener(subTransactionListener);
+        transaction.start();
+
     }
 
     public void handleTransactionFailure() {}
diff --git a/src/com/android/server/telecom/voip/SetMuteStateTransaction.java b/src/com/android/server/telecom/voip/SetMuteStateTransaction.java
new file mode 100644
index 0000000..d9f7329
--- /dev/null
+++ b/src/com/android/server/telecom/voip/SetMuteStateTransaction.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2024 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.voip;
+
+import android.util.Log;
+
+import com.android.server.telecom.CallsManager;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+
+/**
+ * This transaction should be used to change the global mute state for transactional
+ * calls. There is currently no way for this transaction to fail.
+ */
+public class SetMuteStateTransaction extends VoipCallTransaction {
+
+    private static final String TAG = SetMuteStateTransaction.class.getSimpleName();
+    private final CallsManager mCallsManager;
+    private final boolean mIsMuted;
+
+    public SetMuteStateTransaction(CallsManager callsManager, boolean isMuted) {
+        super(callsManager.getLock());
+        mCallsManager = callsManager;
+        mIsMuted = isMuted;
+    }
+
+    @Override
+    public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+        Log.d(TAG, "processTransaction");
+        CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
+
+        mCallsManager.mute(mIsMuted);
+
+        future.complete(new VoipCallTransactionResult(
+                VoipCallTransactionResult.RESULT_SUCCEED,
+                "The Mute State was changed successfully"));
+
+        return future;
+    }
+}
diff --git a/src/com/android/server/telecom/voip/TransactionManager.java b/src/com/android/server/telecom/voip/TransactionManager.java
index 228bdde..0086d07 100644
--- a/src/com/android/server/telecom/voip/TransactionManager.java
+++ b/src/com/android/server/telecom/voip/TransactionManager.java
@@ -21,20 +21,25 @@
 import android.os.OutcomeReceiver;
 import android.telecom.TelecomManager;
 import android.telecom.CallException;
+import android.util.IndentingPrintWriter;
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
-
+import com.android.server.telecom.flags.Flags;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
+import java.util.Deque;
 import java.util.List;
+import java.util.Locale;
 import java.util.Queue;
 
 public class TransactionManager {
     private static final String TAG = "VoipCallTransactionManager";
+    private static final int TRANSACTION_HISTORY_SIZE = 20;
     private static TransactionManager INSTANCE = null;
     private static final Object sLock = new Object();
-    private Queue<VoipCallTransaction> mTransactions;
+    private final Queue<VoipCallTransaction> mTransactions;
+    private final Deque<VoipCallTransaction> mCompletedTransactions;
     private VoipCallTransaction mCurrentTransaction;
 
     public interface TransactionCompleteListener {
@@ -45,6 +50,10 @@
     private TransactionManager() {
         mTransactions = new ArrayDeque<>();
         mCurrentTransaction = null;
+        if (Flags.enableCallSequencing()) {
+            mCompletedTransactions = new ArrayDeque<>();
+        } else
+            mCompletedTransactions = null;
     }
 
     public static TransactionManager getInstance() {
@@ -69,15 +78,20 @@
         transaction.setCompleteListener(new TransactionCompleteListener() {
             @Override
             public void onTransactionCompleted(VoipCallTransactionResult result,
-                    String transactionName){
+                    String transactionName) {
                 Log.i(TAG, String.format("transaction %s completed: with result=[%d]",
                         transactionName, result.getResult()));
-                if (result.getResult() == TelecomManager.TELECOM_TRANSACTION_SUCCESS) {
-                    receiver.onResult(result);
-                } else {
-                    receiver.onError(
-                            new CallException(result.getMessage(),
-                                    result.getResult()));
+                try {
+                    if (result.getResult() == TelecomManager.TELECOM_TRANSACTION_SUCCESS) {
+                        receiver.onResult(result);
+                    } else {
+                        receiver.onError(
+                                new CallException(result.getMessage(),
+                                        result.getResult()));
+                    }
+                } catch (Exception e) {
+                    Log.e(TAG, String.format("onTransactionCompleted: Notifying transaction result"
+                            + " %s resulted in an Exception.", result), e);
                 }
                 finishTransaction();
             }
@@ -85,8 +99,13 @@
             @Override
             public void onTransactionTimeout(String transactionName){
                 Log.i(TAG, String.format("transaction %s timeout", transactionName));
-                receiver.onError(new CallException(transactionName + " timeout",
-                        CODE_OPERATION_TIMED_OUT));
+                try {
+                    receiver.onError(new CallException(transactionName + " timeout",
+                            CODE_OPERATION_TIMED_OUT));
+                } catch (Exception e) {
+                    Log.e(TAG, String.format("onTransactionTimeout: Notifying transaction "
+                            + " %s resulted in an Exception.", transactionName), e);
+                }
                 finishTransaction();
             }
         });
@@ -112,7 +131,10 @@
 
     private void finishTransaction() {
         synchronized (sLock) {
-            mCurrentTransaction = null;
+            if (mCurrentTransaction != null) {
+                addTransactionToHistory(mCurrentTransaction);
+                mCurrentTransaction = null;
+            }
         }
         startTransactions();
     }
@@ -123,8 +145,115 @@
         synchronized (sLock) {
             pendingTransactions = new ArrayList<>(mTransactions);
         }
-        for (VoipCallTransaction transaction : pendingTransactions) {
-            transaction.finish();
+        for (VoipCallTransaction t : pendingTransactions) {
+            t.finish(new VoipCallTransactionResult(CallException.CODE_ERROR_UNKNOWN
+                    /* TODO:: define error b/335703584 */, "clear called"));
         }
     }
+
+    private void addTransactionToHistory(VoipCallTransaction t) {
+        if (!Flags.enableCallSequencing()) return;
+
+        mCompletedTransactions.add(t);
+        if (mCompletedTransactions.size() > TRANSACTION_HISTORY_SIZE) {
+            mCompletedTransactions.poll();
+        }
+    }
+
+    /**
+     * Called when the dumpsys is created for telecom to capture the current state.
+     */
+    public void dump(IndentingPrintWriter pw) {
+        if (!Flags.enableCallSequencing()) {
+            pw.println("<<Flag not enabled>>");
+            return;
+        }
+        synchronized (sLock) {
+            pw.println("Pending Transactions:");
+            pw.increaseIndent();
+            for (VoipCallTransaction t : mTransactions) {
+                printPendingTransactionStats(t, pw);
+            }
+            pw.decreaseIndent();
+
+            pw.println("Ongoing Transaction:");
+            pw.increaseIndent();
+            if (mCurrentTransaction != null) {
+                printPendingTransactionStats(mCurrentTransaction, pw);
+            }
+            pw.decreaseIndent();
+
+            pw.println("Completed Transactions:");
+            pw.increaseIndent();
+            for (VoipCallTransaction t : mCompletedTransactions) {
+                printCompleteTransactionStats(t, pw);
+            }
+            pw.decreaseIndent();
+        }
+    }
+
+    /**
+     * Recursively print the pending {@link VoipCallTransaction} stats for logging purposes.
+     * @param t The transaction that stats should be printed for
+     * @param pw The IndentingPrintWriter to print the result to
+     */
+    private void printPendingTransactionStats(VoipCallTransaction t, IndentingPrintWriter pw) {
+        VoipCallTransaction.Stats s = t.getStats();
+        if (s == null) {
+            pw.println(String.format(Locale.getDefault(), "%s: <NO STATS>", t.mTransactionName));
+            return;
+        }
+        pw.println(String.format(Locale.getDefault(),
+                "[%s] %s: (result=[%s]), (created -> now : [%+d] mS),"
+                        + " (created -> started : [%+d] mS),"
+                        + " (started -> now : [%+d] mS)",
+                s.addedTimeStamp, t.mTransactionName, parseTransactionResult(s),
+                s.measureTimeSinceCreatedMs(), s.measureCreatedToStartedMs(),
+                s.measureTimeSinceStartedMs()));
+
+        if (t.mSubTransactions == null || t.mSubTransactions.isEmpty()) {
+            return;
+        }
+        pw.increaseIndent();
+        for (VoipCallTransaction subTransaction : t.mSubTransactions) {
+            printPendingTransactionStats(subTransaction, pw);
+        }
+        pw.decreaseIndent();
+    }
+
+    /**
+     * Recursively print the complete Transaction stats for logging purposes.
+     * @param t The transaction that stats should be printed for
+     * @param pw The IndentingPrintWriter to print the result to
+     */
+    private void printCompleteTransactionStats(VoipCallTransaction t, IndentingPrintWriter pw) {
+        VoipCallTransaction.Stats s = t.getStats();
+        if (s == null) {
+            pw.println(String.format(Locale.getDefault(), "%s: <NO STATS>", t.mTransactionName));
+            return;
+        }
+        pw.println(String.format(Locale.getDefault(),
+                "[%s] %s: (result=[%s]), (created -> started : [%+d] mS), "
+                        + "(started -> completed : [%+d] mS)",
+                s.addedTimeStamp, t.mTransactionName, parseTransactionResult(s),
+                s.measureCreatedToStartedMs(), s.measureStartedToCompletedMs()));
+
+        if (t.mSubTransactions == null || t.mSubTransactions.isEmpty()) {
+            return;
+        }
+        pw.increaseIndent();
+        for (VoipCallTransaction subTransaction : t.mSubTransactions) {
+            printCompleteTransactionStats(subTransaction, pw);
+        }
+        pw.decreaseIndent();
+    }
+
+    private String parseTransactionResult(VoipCallTransaction.Stats s) {
+        if (s.isTimedOut()) return "TIMED OUT";
+        if (s.getTransactionResult() == null) return "PENDING";
+        if (s.getTransactionResult().getResult() == VoipCallTransactionResult.RESULT_SUCCEED) {
+            return "SUCCESS";
+        }
+        return s.getTransactionResult().toString();
+    }
 }
diff --git a/src/com/android/server/telecom/voip/VerifyCallStateChangeTransaction.java b/src/com/android/server/telecom/voip/VerifyCallStateChangeTransaction.java
new file mode 100644
index 0000000..5de4b1d
--- /dev/null
+++ b/src/com/android/server/telecom/voip/VerifyCallStateChangeTransaction.java
@@ -0,0 +1,93 @@
+/*
+ * 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.voip;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.telecom.Call;
+import com.android.server.telecom.TelecomSystem;
+
+import android.telecom.Log;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+
+/**
+ * VerifyCallStateChangeTransaction is a transaction that verifies a CallState change and has
+ * the ability to disconnect if the CallState is not changed within the timeout window.
+ * <p>
+ * Note: This transaction has a timeout of 2 seconds.
+ */
+public class VerifyCallStateChangeTransaction extends VoipCallTransaction {
+    private static final String TAG = VerifyCallStateChangeTransaction.class.getSimpleName();
+    private static final long CALL_STATE_TIMEOUT_MILLISECONDS = 2000L;
+    private final Call mCall;
+    private final int mTargetCallState;
+    private final CompletableFuture<VoipCallTransactionResult> mTransactionResult =
+            new CompletableFuture<>();
+
+    private final Call.CallStateListener mCallStateListenerImpl = new Call.CallStateListener() {
+        @Override
+        public void onCallStateChanged(int newCallState) {
+            Log.d(TAG, "newState=[%d], expectedState=[%d]", newCallState, mTargetCallState);
+            if (newCallState == mTargetCallState) {
+                mTransactionResult.complete(new VoipCallTransactionResult(
+                        VoipCallTransactionResult.RESULT_SUCCEED, TAG));
+            }
+            // NOTE:: keep listening to the call state until the timeout is reached. It's possible
+            // another call state is reached in between...
+        }
+    };
+
+    public VerifyCallStateChangeTransaction(TelecomSystem.SyncRoot lock,  Call call,
+            int targetCallState) {
+        super(lock, CALL_STATE_TIMEOUT_MILLISECONDS);
+        mCall = call;
+        mTargetCallState = targetCallState;
+    }
+
+    @Override
+    public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+        Log.d(TAG, "processTransaction:");
+        // It's possible the Call is already in the expected call state
+        if (isNewCallStateTargetCallState()) {
+            mTransactionResult.complete(new VoipCallTransactionResult(
+                    VoipCallTransactionResult.RESULT_SUCCEED, TAG));
+            return mTransactionResult;
+        }
+        mCall.addCallStateListener(mCallStateListenerImpl);
+        return mTransactionResult;
+    }
+
+    @Override
+    public void finishTransaction() {
+        mCall.removeCallStateListener(mCallStateListenerImpl);
+    }
+
+    private boolean isNewCallStateTargetCallState() {
+        return mCall.getState() == mTargetCallState;
+    }
+
+    @VisibleForTesting
+    public CompletableFuture<VoipCallTransactionResult> getTransactionResult() {
+        return mTransactionResult;
+    }
+
+    @VisibleForTesting
+    public Call.CallStateListener getCallStateListenerImpl() {
+        return mCallStateListenerImpl;
+    }
+}
diff --git a/src/com/android/server/telecom/voip/VideoStateTranslation.java b/src/com/android/server/telecom/voip/VideoStateTranslation.java
new file mode 100644
index 0000000..3812d15
--- /dev/null
+++ b/src/com/android/server/telecom/voip/VideoStateTranslation.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2024 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.voip;
+
+import android.telecom.CallAttributes;
+import android.telecom.Log;
+import android.telecom.VideoProfile;
+
+import com.android.server.telecom.AnomalyReporterAdapter;
+import com.android.server.telecom.AnomalyReporterAdapterImpl;
+
+import java.util.UUID;
+
+/**
+ * This remapping class is needed because {@link VideoProfile} has more fine grain levels of video
+ * states as apposed to Transactional video states (defined in  {@link CallAttributes.CallType}.
+ * To be more specific, there are 3 video states (rx, tx, and bi-directional).
+ * {@link CallAttributes.CallType} only has 2 states (audio and video).
+ *
+ * The reason why Transactional calls have fewer states is due to the fact that the framework is
+ * only used by VoIP apps and Telecom only cares to know if the call is audio or video.
+ *
+ * Calls that are backed by a {@link android.telecom.ConnectionService} have the ability to be
+ * managed calls (non-VoIP) and Dialer needs more fine grain video states to update the UI. Thus,
+ * {@link VideoProfile} is used for {@link android.telecom.ConnectionService} backed calls.
+ */
+public class VideoStateTranslation {
+    private static final String TAG = VideoStateTranslation.class.getSimpleName();
+
+    /**
+     * Client --> Telecom
+     * This should be used when the client application is signaling they are changing the video
+     * state.
+     */
+    public static int TransactionalVideoStateToVideoProfileState(int callType) {
+        if (callType == CallAttributes.AUDIO_CALL) {
+            Log.i(TAG, "CallAttributes.AUDIO_CALL --> VideoProfile.STATE_AUDIO_ONLY");
+            return VideoProfile.STATE_AUDIO_ONLY;
+        } else if (callType == CallAttributes.VIDEO_CALL) {
+            Log.i(TAG, "CallAttributes.VIDEO_CALL--> VideoProfile.STATE_BIDIRECTIONAL");
+            return VideoProfile.STATE_BIDIRECTIONAL;
+        } else {
+            Log.w(TAG, "CallType=[%d] does not have a VideoProfile mapping", callType);
+            return VideoProfile.STATE_AUDIO_ONLY;
+        }
+    }
+
+    /**
+     * Telecom --> Client
+     * This should be used when Telecom is informing the client of a video state change.
+     */
+    public static int VideoProfileStateToTransactionalVideoState(int videoProfileState) {
+        switch (videoProfileState) {
+            case VideoProfile.STATE_AUDIO_ONLY -> {
+                Log.i(TAG, "%s --> CallAttributes.AUDIO_CALL",
+                        VideoProfileStateToString(videoProfileState));
+                return CallAttributes.AUDIO_CALL;
+            }
+            case VideoProfile.STATE_BIDIRECTIONAL, VideoProfile.STATE_TX_ENABLED,
+                    VideoProfile.STATE_RX_ENABLED -> {
+                Log.i(TAG, "%s --> CallAttributes.VIDEO_CALL",
+                        VideoProfileStateToString(videoProfileState));
+                return CallAttributes.VIDEO_CALL;
+            }
+            default -> {
+                Log.w(TAG, "VideoProfile=[%d] does not have a CallType mapping", videoProfileState);
+                return CallAttributes.AUDIO_CALL;
+            }
+        }
+    }
+
+    public static String TransactionalVideoStateToString(int transactionalVideoState) {
+        if (transactionalVideoState == CallAttributes.AUDIO_CALL) {
+            return "CallAttributes.AUDIO_CALL";
+        } else if (transactionalVideoState == CallAttributes.VIDEO_CALL) {
+            return "CallAttributes.VIDEO_CALL";
+        } else {
+            return "CallAttributes.UNKNOWN";
+        }
+    }
+
+    private static String VideoProfileStateToString(int videoProfileState) {
+        switch (videoProfileState) {
+            case VideoProfile.STATE_BIDIRECTIONAL -> {
+                return "VideoProfile.STATE_BIDIRECTIONAL";
+            }
+            case VideoProfile.STATE_RX_ENABLED -> {
+                return "VideoProfile.STATE_RX_ENABLED";
+            }
+            case VideoProfile.STATE_TX_ENABLED -> {
+                return "VideoProfile.STATE_TX_ENABLED";
+            }
+            case VideoProfile.STATE_AUDIO_ONLY -> {
+                return "VideoProfile.STATE_AUDIO_ONLY";
+            }
+            default -> {
+                return "VideoProfile.UNKNOWN";
+            }
+        }
+    }
+}
diff --git a/src/com/android/server/telecom/voip/VoipCallMonitor.java b/src/com/android/server/telecom/voip/VoipCallMonitor.java
index 3779a6d..8f6ad51 100644
--- a/src/com/android/server/telecom/voip/VoipCallMonitor.java
+++ b/src/com/android/server/telecom/voip/VoipCallMonitor.java
@@ -16,6 +16,12 @@
 
 package com.android.server.telecom.voip;
 
+import static android.app.ForegroundServiceDelegationOptions.DELEGATION_SERVICE_PHONE_CALL;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL;
+
 import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
 import android.app.ForegroundServiceDelegationOptions;
@@ -199,8 +205,11 @@
             ForegroundServiceDelegationOptions options = new ForegroundServiceDelegationOptions(pid,
                     uid, handle.getComponentName().getPackageName(), null /* clientAppThread */,
                     false /* isSticky */, String.valueOf(handle.hashCode()),
-                    0 /* foregroundServiceType */,
-                    ForegroundServiceDelegationOptions.DELEGATION_SERVICE_PHONE_CALL);
+                    FOREGROUND_SERVICE_TYPE_PHONE_CALL |
+                    FOREGROUND_SERVICE_TYPE_MICROPHONE |
+                    FOREGROUND_SERVICE_TYPE_CAMERA |
+                    FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE /* foregroundServiceTypes */,
+                    DELEGATION_SERVICE_PHONE_CALL /* delegationService */);
             ServiceConnection fgsConnection = new ServiceConnection() {
                 @Override
                 public void onServiceConnected(ComponentName name, IBinder service) {
diff --git a/src/com/android/server/telecom/voip/VoipCallTransaction.java b/src/com/android/server/telecom/voip/VoipCallTransaction.java
index a952eb1..a589a6d 100644
--- a/src/com/android/server/telecom/voip/VoipCallTransaction.java
+++ b/src/com/android/server/telecom/voip/VoipCallTransaction.java
@@ -18,93 +18,254 @@
 
 import android.os.Handler;
 import android.os.HandlerThread;
+import android.telecom.CallException;
 import android.telecom.Log;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.telecom.LoggedHandlerExecutor;
 import com.android.server.telecom.TelecomSystem;
+import com.android.server.telecom.flags.Flags;
 
+import java.time.LocalDateTime;
 import java.util.List;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionStage;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Function;
 
 public class VoipCallTransaction {
     //TODO: add log events
-    protected static final long TIMEOUT_LIMIT = 5000L;
+    private static final long DEFAULT_TRANSACTION_TIMEOUT_MS = 5000L;
+
+    /**
+     * Tracks stats about a transaction for logging purposes.
+     */
+    public static class Stats {
+        // the logging visible timestamp for ease of debugging
+        public final LocalDateTime addedTimeStamp;
+        // the time in nS that the transaction was first created
+        private final long mCreatedTimeNs;
+        // the time that the transaction was started.
+        private long mStartedTimeNs = -1L;
+        // the time that the transaction was finished.
+        private long mFinishedTimeNs = -1L;
+        // If finished, did this transaction finish because it timed out?
+        private boolean mIsTimedOut = false;
+        private VoipCallTransactionResult  mTransactionResult = null;
+
+        public Stats() {
+            addedTimeStamp = LocalDateTime.now();
+            mCreatedTimeNs = System.nanoTime();
+        }
+
+        /**
+         * Mark the transaction as started and record the time.
+         */
+        public void markStarted() {
+            if (mStartedTimeNs > -1) return;
+            mStartedTimeNs = System.nanoTime();
+        }
+
+        /**
+         * Mark the transaction as completed and record the time.
+         */
+        public void markComplete(boolean isTimedOut, VoipCallTransactionResult result) {
+            if (mFinishedTimeNs > -1) return;
+            mFinishedTimeNs = System.nanoTime();
+            mIsTimedOut = isTimedOut;
+            mTransactionResult = result;
+        }
+
+        /**
+         * @return Time in mS since the transaction was created.
+         */
+        public long measureTimeSinceCreatedMs() {
+            return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - mCreatedTimeNs);
+        }
+
+        /**
+         * @return Time in mS between when transaction was created and when it was marked as
+         * started. Returns -1 if the transaction was not started yet.
+         */
+        public long measureCreatedToStartedMs() {
+            return mStartedTimeNs > 0 ?
+                    TimeUnit.NANOSECONDS.toMillis(mStartedTimeNs - mCreatedTimeNs) : -1;
+        }
+
+        /**
+         * @return Time in mS since the transaction was marked started to the TransactionManager.
+         * Returns -1 if the transaction hasn't been started yet.
+         */
+        public long measureTimeSinceStartedMs() {
+            return mStartedTimeNs > 0 ?
+                    TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - mStartedTimeNs) : -1;
+        }
+
+        /**
+         * @return Time in mS between when the transaction was marked as started and when it was
+         * marked as completed. Returns -1 if the transaction hasn't started or finished yet.
+         */
+        public long measureStartedToCompletedMs() {
+            return (mStartedTimeNs > 0 && mFinishedTimeNs > 0) ?
+                    TimeUnit.NANOSECONDS.toMillis(mFinishedTimeNs - mStartedTimeNs) : -1;
+
+        }
+
+        /**
+         * @return true if this transaction completed due to timing out, false if the transaction
+         * hasn't completed yet or it completed and did not time out.
+         */
+        public boolean isTimedOut() {
+            return mIsTimedOut;
+        }
+
+        /**
+         * @return the result if the transaction completed, null if it timed out or hasn't completed
+         * yet.
+         */
+        public VoipCallTransactionResult getTransactionResult() {
+            return mTransactionResult;
+        }
+    }
+
     protected final AtomicBoolean mCompleted = new AtomicBoolean(false);
-    protected String mTransactionName = this.getClass().getSimpleName();
-    private HandlerThread mHandlerThread;
-    protected Handler mHandler;
+    protected final String mTransactionName = this.getClass().getSimpleName();
+    private final HandlerThread mHandlerThread;
+    protected final Handler mHandler;
     protected TransactionManager.TransactionCompleteListener mCompleteListener;
-    protected List<VoipCallTransaction> mSubTransactions;
-    protected TelecomSystem.SyncRoot mLock;
+    protected final List<VoipCallTransaction> mSubTransactions;
+    protected final TelecomSystem.SyncRoot mLock;
+    protected final long mTransactionTimeoutMs;
+    protected final Stats mStats;
 
     public VoipCallTransaction(
-            List<VoipCallTransaction> subTransactions, TelecomSystem.SyncRoot lock) {
+            List<VoipCallTransaction> subTransactions, TelecomSystem.SyncRoot lock,
+            long timeoutMs) {
         mSubTransactions = subTransactions;
         mHandlerThread = new HandlerThread(this.toString());
         mHandlerThread.start();
         mHandler = new Handler(mHandlerThread.getLooper());
         mLock = lock;
+        mTransactionTimeoutMs = timeoutMs;
+        mStats = Flags.enableCallSequencing() ? new Stats() : null;
+    }
+
+    public VoipCallTransaction(List<VoipCallTransaction> subTransactions,
+            TelecomSystem.SyncRoot lock) {
+        this(subTransactions, lock, DEFAULT_TRANSACTION_TIMEOUT_MS);
+    }
+    public VoipCallTransaction(TelecomSystem.SyncRoot lock, long timeoutMs) {
+        this(null /* mSubTransactions */, lock, timeoutMs);
     }
 
     public VoipCallTransaction(TelecomSystem.SyncRoot lock) {
-        this(null /** mSubTransactions */, lock);
+        this(null /* mSubTransactions */, lock);
     }
 
-    public void start() {
+    public final void start() {
+        if (mStats != null) mStats.markStarted();
         // post timeout work
         CompletableFuture<Void> future = new CompletableFuture<>();
-        mHandler.postDelayed(() -> future.complete(null), TIMEOUT_LIMIT);
+        mHandler.postDelayed(() -> future.complete(null), mTransactionTimeoutMs);
         future.thenApplyAsync((x) -> {
-            if (mCompleted.getAndSet(true)) {
-                return null;
-            }
-            if (mCompleteListener != null) {
-                mCompleteListener.onTransactionTimeout(mTransactionName);
-            }
-            finish();
+            timeout();
             return null;
         }, new LoggedHandlerExecutor(mHandler, mTransactionName + "@" + hashCode()
                 + ".s", mLock));
 
+        processTransactions();
+    }
+
+    /**
+     * By default, this processes this transaction. For VoipCallTransactions with sub-transactions,
+     * this implementation should be overwritten to handle also processing sub-transactions.
+     */
+    protected void processTransactions() {
         scheduleTransaction();
     }
 
-    protected void scheduleTransaction() {
+    /**
+     * This method is called when the transaction has finished either successfully or exceptionally.
+     * VoipCallTransactions that are extending this class should override this method to clean up
+     * any leftover state.
+     */
+    protected void finishTransaction() {
+
+    }
+
+    protected final void scheduleTransaction() {
         LoggedHandlerExecutor executor = new LoggedHandlerExecutor(mHandler,
-                mTransactionName + "@" + hashCode() + ".pT", mLock);
+                mTransactionName + "@" + hashCode() + ".sT", mLock);
         CompletableFuture<Void> future = CompletableFuture.completedFuture(null);
         future.thenComposeAsync(this::processTransaction, executor)
                 .thenApplyAsync((Function<VoipCallTransactionResult, Void>) result -> {
-                    mCompleted.set(true);
-                    if (mCompleteListener != null) {
-                        mCompleteListener.onTransactionCompleted(result, mTransactionName);
-                    }
-                    finish();
+                    notifyListenersOfResult(result);
                     return null;
-                    }, executor)
-                .exceptionallyAsync((throwable -> {
+                }, executor)
+                .exceptionally((throwable -> {
+                    // Do NOT wait for the timeout in order to finish this failed transaction.
+                    // Instead, propagate the failure to the other transactions immediately!
+                    String errorMessage = throwable != null ? throwable.getMessage() :
+                            "encountered an exception while processing " + mTransactionName;
+                    notifyListenersOfResult(new VoipCallTransactionResult(
+                            CallException.CODE_ERROR_UNKNOWN, errorMessage));
                     Log.e(this, throwable, "Error while executing transaction.");
                     return null;
-                }), executor);
+                }));
     }
 
-    public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+    protected void notifyListenersOfResult(VoipCallTransactionResult result){
+        mCompleted.set(true);
+        finish(result);
+        if (mCompleteListener != null) {
+            mCompleteListener.onTransactionCompleted(result, mTransactionName);
+        }
+    }
+
+    protected CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
         return CompletableFuture.completedFuture(
                 new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_SUCCEED, null));
     }
 
-    public void setCompleteListener(TransactionManager.TransactionCompleteListener listener) {
+    public final void setCompleteListener(TransactionManager.TransactionCompleteListener listener) {
         mCompleteListener = listener;
     }
 
-    public void finish() {
-        // finish all sub transactions
-        if (mSubTransactions != null && mSubTransactions.size() > 0) {
-            mSubTransactions.forEach(VoipCallTransaction::finish);
+    @VisibleForTesting
+    public final void timeout() {
+        if (mCompleted.getAndSet(true)) {
+            return;
         }
-        mHandlerThread.quit();
+        finish(true, null);
+        if (mCompleteListener != null) {
+            mCompleteListener.onTransactionTimeout(mTransactionName);
+        }
+    }
+
+    @VisibleForTesting
+    public final Handler getHandler() {
+        return mHandler;
+    }
+
+    public final void finish(VoipCallTransactionResult result) {
+        finish(false, result);
+    }
+
+    private void finish(boolean isTimedOut, VoipCallTransactionResult result) {
+        if (mStats != null) mStats.markComplete(isTimedOut, result);
+        finishTransaction();
+        // finish all sub transactions
+        if (mSubTransactions != null && !mSubTransactions.isEmpty()) {
+            mSubTransactions.forEach( t -> t.finish(isTimedOut, result));
+        }
+        mHandlerThread.quitSafely();
+    }
+
+    /**
+     * @return Stats related to this transaction if stats are enabled, null otherwise.
+     */
+    public final Stats getStats() {
+        return mStats;
     }
 }
diff --git a/src/com/android/server/telecom/voip/VoipCallTransactionResult.java b/src/com/android/server/telecom/voip/VoipCallTransactionResult.java
index 2916fc6..50871f2 100644
--- a/src/com/android/server/telecom/voip/VoipCallTransactionResult.java
+++ b/src/com/android/server/telecom/voip/VoipCallTransactionResult.java
@@ -22,15 +22,19 @@
 
 public class VoipCallTransactionResult {
     public static final int RESULT_SUCCEED = 0;
-    public static final int RESULT_FAILED = 1;
 
-    private int mResult;
-    private String mMessage;
-    private Call mCall;
+    // NOTE: if the VoipCallTransactionResult should not use the RESULT_SUCCEED to represent a
+    // successful transaction, use an error code defined in the
+    // {@link android.telecom.CallException} class
+
+    private final int mResult;
+    private final String mMessage;
+    private final Call mCall;
 
     public VoipCallTransactionResult(int result, String message) {
         mResult = result;
         mMessage = message;
+        mCall = null;
     }
 
     public VoipCallTransactionResult(int result, Call call, String message) {
@@ -70,7 +74,7 @@
                 append("{ VoipCallTransactionResult: [mResult: ").
                 append(mResult).
                 append("], [mCall: ").
-                append(mCall.toString()).
+                append((mCall != null) ? mCall : "null").
                 append("], [mMessage=").
                 append(mMessage).append("]  }").toString();
     }
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/AndroidManifest.xml b/testapps/AndroidManifest.xml
index 645a42b..e048f21 100644
--- a/testapps/AndroidManifest.xml
+++ b/testapps/AndroidManifest.xml
@@ -34,6 +34,7 @@
     <uses-permission android:name="android.permission.REGISTER_CONNECTION_MANAGER"/>
     <uses-permission android:name="android.permission.REGISTER_SIM_SUBSCRIPTION"/>
     <uses-permission android:name="android.permission.WRITE_CALL_LOG"/>
+    <uses-permission android:name="android.permission.MODIFY_PHONE_STATE"/>
 
     <application android:label="@string/app_name">
         <uses-library android:name="android.test.runner"/>
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/res/layout/testdialer_main.xml b/testapps/res/layout/testdialer_main.xml
index 749d236..e4b5bef 100644
--- a/testapps/res/layout/testdialer_main.xml
+++ b/testapps/res/layout/testdialer_main.xml
@@ -54,6 +54,23 @@
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:text="@string/cancelMissedButton" />
+
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal" >
+    <EditText
+        android:id="@+id/set_composer_edit_text"
+        android:inputType="number"
+        android:layout_width="200dp"
+        android:layout_height="wrap_content" />
+    <Button
+        android:id="@+id/submit_composer_value"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/submitCallComposerLabel" />
+    </LinearLayout>
+
     <CheckBox
         android:id="@+id/call_with_rtt_checkbox"
         android:layout_width="wrap_content"
diff --git a/testapps/res/values/donottranslate_strings.xml b/testapps/res/values/donottranslate_strings.xml
index b1a1f80..39c2deb 100644
--- a/testapps/res/values/donottranslate_strings.xml
+++ b/testapps/res/values/donottranslate_strings.xml
@@ -100,6 +100,8 @@
 
     <string name="postCallActivityLabel">Test Post Call Screen</string>
 
+    <string name="submitCallComposerLabel">Set Call Composer</string>
+
     <string-array name="rtt_mode_array">
         <item>Full</item>
         <item>HCO</item>
diff --git a/testapps/src/com/android/server/telecom/testapps/CallNotificationReceiver.java b/testapps/src/com/android/server/telecom/testapps/CallNotificationReceiver.java
index 1549443..ede06c6 100644
--- a/testapps/src/com/android/server/telecom/testapps/CallNotificationReceiver.java
+++ b/testapps/src/com/android/server/telecom/testapps/CallNotificationReceiver.java
@@ -82,6 +82,7 @@
      * @param videoState The video state requested for the incoming call.
      */
     public static void sendIncomingCallIntent(Context context, Uri handle, int videoState) {
+        TelecomManager telecomManager = context.getSystemService(TelecomManager.class);
         PhoneAccountHandle phoneAccount = new PhoneAccountHandle(
                 new ComponentName(context, TestConnectionService.class),
                 CallServiceNotifier.SIM_SUBSCRIPTION_ID);
@@ -94,10 +95,11 @@
             extras.putParcelable(TestConnectionService.EXTRA_HANDLE, handle);
         }
 
-        TelecomManager.from(context).addNewIncomingCall(phoneAccount, extras);
+        telecomManager.addNewIncomingCall(phoneAccount, extras);
     }
 
     public static void sendIncomingRttCallIntent(Context context, Uri handle, int videoState) {
+        TelecomManager telecomManager = context.getSystemService(TelecomManager.class);
         PhoneAccountHandle phoneAccount = new PhoneAccountHandle(
                 new ComponentName(context, TestConnectionService.class),
                 CallServiceNotifier.SIM_SUBSCRIPTION_ID);
@@ -111,11 +113,12 @@
         }
         extras.putBoolean(TelecomManager.EXTRA_START_CALL_WITH_RTT, true);
 
-        TelecomManager.from(context).addNewIncomingCall(phoneAccount, extras);
+        telecomManager.addNewIncomingCall(phoneAccount, extras);
     }
 
     public static void addNewUnknownCall(Context context, Uri handle, Bundle extras) {
         Log.i(TAG, "Adding new unknown call with handle " + handle);
+        TelecomManager telecomManager = context.getSystemService(TelecomManager.class);
         PhoneAccountHandle phoneAccount = new PhoneAccountHandle(
                 new ComponentName(context, TestConnectionService.class),
                 CallServiceNotifier.SIM_SUBSCRIPTION_ID);
@@ -129,7 +132,7 @@
             extras.putParcelable(TestConnectionService.EXTRA_HANDLE, handle);
         }
 
-        TelecomManager.from(context).addNewUnknownCall(phoneAccount, extras);
+        telecomManager.addNewUnknownCall(phoneAccount, extras);
     }
 
     public static void hangupCalls(Context context) {
diff --git a/testapps/src/com/android/server/telecom/testapps/HandoverActivity.java b/testapps/src/com/android/server/telecom/testapps/HandoverActivity.java
index f33022c..d5ddc9b 100644
--- a/testapps/src/com/android/server/telecom/testapps/HandoverActivity.java
+++ b/testapps/src/com/android/server/telecom/testapps/HandoverActivity.java
@@ -17,6 +17,7 @@
 package com.android.server.telecom.testapps;
 
 import android.app.Activity;
+import android.content.Context;
 import android.content.Intent;
 import android.os.Bundle;
 import android.telecom.Log;
@@ -61,7 +62,7 @@
             if (connection != null) {
                 connection.setConnectionDisconnected(DisconnectCause.INCOMING_REJECTED);
                 connection.destroy();
-                TelecomManager tm = TelecomManager.from(this);
+                TelecomManager tm = this.getSystemService(TelecomManager.class);
                 tm.showInCallScreen(false);
             }
             finish();
diff --git a/testapps/src/com/android/server/telecom/testapps/SelfManagedCallList.java b/testapps/src/com/android/server/telecom/testapps/SelfManagedCallList.java
index 273b060..4a7312c 100644
--- a/testapps/src/com/android/server/telecom/testapps/SelfManagedCallList.java
+++ b/testapps/src/com/android/server/telecom/testapps/SelfManagedCallList.java
@@ -123,6 +123,7 @@
 
     public void registerPhoneAccount(Context context, ComponentName componentName, String id,
             Uri address, String name, boolean areCallsLogged) {
+        TelecomManager telecomManager = context.getSystemService(TelecomManager.class);
         PhoneAccountHandle handle = new PhoneAccountHandle(componentName, id);
         mPhoneAccounts.put(id, handle);
         Bundle extras = new Bundle();
@@ -144,7 +145,7 @@
                 .setExtras(extras)
                 .setShortDescription(name);
 
-        TelecomManager.from(context).registerPhoneAccount(builder.build());
+        telecomManager.registerPhoneAccount(builder.build());
     }
 
     public PhoneAccountHandle getPhoneAccountHandle(String id) {
diff --git a/testapps/src/com/android/server/telecom/testapps/SelfManagedCallingActivity.java b/testapps/src/com/android/server/telecom/testapps/SelfManagedCallingActivity.java
index 5cdaf3d..708bae9 100644
--- a/testapps/src/com/android/server/telecom/testapps/SelfManagedCallingActivity.java
+++ b/testapps/src/com/android/server/telecom/testapps/SelfManagedCallingActivity.java
@@ -23,6 +23,7 @@
 import android.app.NotificationManager;
 import android.app.UiModeManager;
 import android.app.role.RoleManager;
+import android.content.Context;
 import android.content.Intent;
 import android.media.AudioAttributes;
 import android.media.RingtoneManager;
@@ -190,7 +191,7 @@
     }
 
     private void placeOutgoingCall() {
-        TelecomManager tm = TelecomManager.from(this);
+        TelecomManager tm = this.getSystemService(TelecomManager.class);
         PhoneAccountHandle phoneAccountHandle = getSelectedPhoneAccountHandle();
 
         if (mCheckIfPermittedBeforeCalling.isChecked()) {
@@ -215,7 +216,7 @@
     }
 
     private void placeSelfManagedOutgoingCall() {
-        TelecomManager tm = TelecomManager.from(this);
+        TelecomManager tm = this.getSystemService(TelecomManager.class);
         PhoneAccountHandle phoneAccountHandle = getSelectedPhoneAccountHandle();
 
         if (mCheckIfPermittedBeforeCalling.isChecked()) {
@@ -233,14 +234,14 @@
     }
 
     private void initiateHandover() {
-        TelecomManager tm = TelecomManager.from(this);
+        TelecomManager tm = this.getSystemService(TelecomManager.class);
         PhoneAccountHandle phoneAccountHandle = getSelectedPhoneAccountHandle();
         Uri address = Uri.parse(mNumber.getText().toString());
         tm.acceptHandover(address, VideoProfile.STATE_BIDIRECTIONAL, phoneAccountHandle);
     }
 
     private void placeIncomingCall() {
-        TelecomManager tm = TelecomManager.from(this);
+        TelecomManager tm = this.getSystemService(TelecomManager.class);
         PhoneAccountHandle phoneAccountHandle = getSelectedPhoneAccountHandle();
 
         if (mCheckIfPermittedBeforeCalling.isChecked()) {
@@ -263,7 +264,7 @@
     }
 
     private void placeSelfManagedIncomingCall() {
-        TelecomManager tm = TelecomManager.from(this);
+        TelecomManager tm = this.getSystemService(TelecomManager.class);
         PhoneAccountHandle phoneAccountHandle = mCallList.getPhoneAccountHandle(
                 SelfManagedCallList.SELF_MANAGED_ACCOUNT_1A);
 
diff --git a/testapps/src/com/android/server/telecom/testapps/TestDialerActivity.java b/testapps/src/com/android/server/telecom/testapps/TestDialerActivity.java
index f17af2c..5a59c24 100644
--- a/testapps/src/com/android/server/telecom/testapps/TestDialerActivity.java
+++ b/testapps/src/com/android/server/telecom/testapps/TestDialerActivity.java
@@ -20,6 +20,7 @@
 import android.telecom.TelecomManager;
 import android.telephony.CarrierConfigManager;
 import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
 import android.telephony.ims.ImsRcsManager;
 import android.util.Log;
 import android.view.View;
@@ -30,9 +31,11 @@
 import android.widget.Toast;
 
 public class TestDialerActivity extends Activity {
+    private static final String TAG = TestDialerActivity.class.getSimpleName();
     private static final int REQUEST_CODE_SET_DEFAULT_DIALER = 1;
 
     private EditText mNumberView;
+    private EditText mCallComposerView;
     private CheckBox mRttCheckbox;
     private CheckBox mComposerCheckbox;
     private EditText mPriorityView;
@@ -57,6 +60,13 @@
             }
         });
 
+        findViewById(R.id.submit_composer_value).setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                setCallComposer();
+            }
+        });
+
         findViewById(R.id.place_call_button).setOnClickListener(new OnClickListener() {
             @Override
             public void onClick(View v) {
@@ -79,6 +89,7 @@
         });
 
         mNumberView = (EditText) findViewById(R.id.number);
+        mCallComposerView = (EditText) findViewById(R.id.set_composer_edit_text);
         mRttCheckbox = (CheckBox) findViewById(R.id.call_with_rtt_checkbox);
         mComposerCheckbox = (CheckBox) findViewById(R.id.add_composer_attachments_checkbox);
         findViewById(R.id.enable_car_mode).setOnClickListener(new OnClickListener() {
@@ -141,6 +152,23 @@
         }
     }
 
+    // Testers need a way of setting the call composer since this is currently not supported by
+    // Dialer.  In the future, this will be a Dialer setting that users can enable/disable.
+    private void setCallComposer() {
+        final TelephonyManager telephonyManager =
+                (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
+        String number = mCallComposerView.getText().toString();
+        try {
+            Log.i(TAG, "setCallComposer: value=[" + number + "]");
+            telephonyManager.setCallComposerStatus(Integer.parseInt(number));
+            Log.i(TAG, "setCallComposer: successfully set composer");
+        } catch (Exception e) {
+            Log.i(TAG, "setCallComposer: hit exception while setting the call composer."
+                    + " See stack trace below for more info!");
+            e.printStackTrace();
+        }
+    }
+
     private void placeCall() {
         final TelecomManager telecomManager =
                 (TelecomManager) getSystemService(Context.TELECOM_SERVICE);
diff --git a/testapps/src/com/android/server/telecom/testapps/TestInCallUI.java b/testapps/src/com/android/server/telecom/testapps/TestInCallUI.java
index bdd4c1a..d2aca78 100644
--- a/testapps/src/com/android/server/telecom/testapps/TestInCallUI.java
+++ b/testapps/src/com/android/server/telecom/testapps/TestInCallUI.java
@@ -40,6 +40,8 @@
 import android.widget.TextView;
 import android.widget.Toast;
 
+import com.android.server.telecom.flags.Flags;
+
 import java.util.Collection;
 import java.util.List;
 import java.util.Optional;
@@ -263,6 +265,8 @@
                         .getIntentExtras().getString(TelecomManager.EXTRA_CALL_SUBJECT);
                 boolean isBusiness = call.getDetails()
                         .getExtras().getBoolean(ImsCallProfile.EXTRA_IS_BUSINESS_CALL);
+                String businessName = call.getDetails()
+                            .getExtras().getString(ImsCallProfile.EXTRA_ASSERTED_DISPLAY_NAME);
 
                 StringBuilder display = new StringBuilder();
                 display.append("priority=");
@@ -286,6 +290,7 @@
 
                 display.append(" subject=" + subject);
                 display.append(" isBusiness=" + isBusiness);
+                display.append(" businessName=" + businessName);
                 TextView attachmentsTextView = findViewById(R.id.incoming_composer_attachments);
                 attachmentsTextView.setText(display.toString());
                 break;
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/testapps/transactionalVoipApp/res/values-ca/strings.xml b/testapps/transactionalVoipApp/res/values-ca/strings.xml
index 06f1655..00e028e 100644
--- a/testapps/transactionalVoipApp/res/values-ca/strings.xml
+++ b/testapps/transactionalVoipApp/res/values-ca/strings.xml
@@ -31,7 +31,7 @@
     <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Auricular"</string>
     <string name="request_speaker_endpoint" msgid="1033259535289845405">"Altaveu"</string>
     <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
-    <string name="start_stream" msgid="3567634786280097431">"inicia la reproducció en continu"</string>
+    <string name="start_stream" msgid="3567634786280097431">"inicia l\'estríming"</string>
     <string name="crash_app" msgid="2548690390730057704">"llança una excepció"</string>
     <string name="update_notification" msgid="8677916482672588779">"actualitza la notificació a l\'estil de trucada en curs"</string>
 </resources>
diff --git a/testapps/transactionalVoipApp/res/values-fr-rCA/strings.xml b/testapps/transactionalVoipApp/res/values-fr-rCA/strings.xml
index d58aa13..414caa8 100644
--- a/testapps/transactionalVoipApp/res/values-fr-rCA/strings.xml
+++ b/testapps/transactionalVoipApp/res/values-fr-rCA/strings.xml
@@ -20,7 +20,7 @@
     <string name="app_name" msgid="2907804426411305091">"Activité de test de l\'API transactionnelle"</string>
     <string name="in_call_activity_name" msgid="7545884666442897585">"Activité transactionnelle durant l\'appel"</string>
     <string name="register_phone_account" msgid="1920315963082350332">"Inscrire un compte téléphonique"</string>
-    <string name="start_foreground_service" msgid="8968755699895128574">"Démarrer FGS (simuler TA + application en arrière-plan)"</string>
+    <string name="start_foreground_service" msgid="8968755699895128574">"Démarrer FGS (simuler TA + appli en arrière-plan)"</string>
     <string name="start_outgoing" msgid="1441644037370361864">"Démarrer un appel sortant"</string>
     <string name="start_incoming" msgid="6444983300186361271">"Démarrer un appel entrant"</string>
     <string name="get_call_id" msgid="5513943242738347108">"identifiant de l\'appel non défini"</string>
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 5da7f31..9caf0b5 100644
--- a/tests/src/com/android/server/telecom/tests/AnalyticsTests.java
+++ b/tests/src/com/android/server/telecom/tests/AnalyticsTests.java
@@ -25,8 +25,8 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyInt;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
@@ -47,11 +47,11 @@
 import android.telecom.VideoProfile;
 import android.telephony.SubscriptionInfo;
 import android.telephony.SubscriptionManager;
-import android.test.suitebuilder.annotation.MediumTest;
-import android.test.suitebuilder.annotation.SmallTest;
 import android.util.Base64;
 
 import androidx.test.filters.FlakyTest;
+import androidx.test.filters.MediumTest;
+import androidx.test.filters.SmallTest;
 
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.server.telecom.Analytics;
@@ -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());
     }
@@ -381,7 +382,7 @@
 
         waitForHandlerAction(
                 mTelecomSystem.getCallsManager().getCallAudioManager()
-                        .getCallAudioRouteStateMachine().getHandler(),
+                        .getCallAudioRouteAdapter().getAdapterHandler(),
                 TEST_TIMEOUT);
         waitForHandlerAction(
                 mTelecomSystem.getCallsManager().getCallAudioManager()
@@ -391,7 +392,7 @@
         mInCallServiceFixtureX.getInCallAdapter().setAudioRoute(CallAudioState.ROUTE_SPEAKER, null);
         waitForHandlerAction(
                 mTelecomSystem.getCallsManager().getCallAudioManager()
-                        .getCallAudioRouteStateMachine().getHandler(),
+                        .getCallAudioRouteAdapter().getAdapterHandler(),
                 TEST_TIMEOUT);
         waitForHandlerAction(
                 mTelecomSystem.getCallsManager().getCallAudioManager()
diff --git a/tests/src/com/android/server/telecom/tests/BasicCallTests.java b/tests/src/com/android/server/telecom/tests/BasicCallTests.java
index bd81a2f..7646c2d 100644
--- a/tests/src/com/android/server/telecom/tests/BasicCallTests.java
+++ b/tests/src/com/android/server/telecom/tests/BasicCallTests.java
@@ -16,6 +16,8 @@
 
 package com.android.server.telecom.tests;
 
+import static com.android.server.telecom.callfiltering.BlockCheckerFilter.RES_BLOCK_STATUS;
+import static com.android.server.telecom.callfiltering.BlockCheckerFilter.STATUS_BLOCKED_IN_LIST;
 import static com.android.server.telecom.tests.ConnectionServiceFixture.STATUS_HINTS_EXTRA;
 
 import static org.junit.Assert.assertEquals;
@@ -24,11 +26,11 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Matchers.anyString;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Matchers.isNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.timeout;
@@ -39,22 +41,20 @@
 
 import android.content.Context;
 import android.content.IContentProvider;
-import android.content.pm.PackageManager;
-import android.media.AudioDeviceInfo;
 import android.content.Intent;
+import android.content.pm.PackageManager;
 import android.graphics.drawable.Icon;
+import android.media.AudioDeviceInfo;
 import android.media.AudioManager;
 import android.net.Uri;
 import android.os.Binder;
 import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
 import android.os.Process;
 import android.os.UserHandle;
-import android.os.UserManager;
 import android.provider.BlockedNumberContract;
 import android.telecom.Call;
 import android.telecom.CallAudioState;
+import android.telecom.CallerInfo;
 import android.telecom.Connection;
 import android.telecom.ConnectionRequest;
 import android.telecom.DisconnectCause;
@@ -65,14 +65,13 @@
 import android.telecom.StatusHints;
 import android.telecom.TelecomManager;
 import android.telecom.VideoProfile;
-import android.test.suitebuilder.annotation.LargeTest;
-import android.test.suitebuilder.annotation.MediumTest;
 
 import androidx.test.filters.FlakyTest;
+import androidx.test.filters.LargeTest;
+import androidx.test.filters.MediumTest;
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.telecom.IInCallAdapter;
-import android.telecom.CallerInfo;
 
 import com.google.common.base.Predicate;
 
@@ -110,6 +109,7 @@
         doReturn(mContext).when(mContext).createContextAsUser(any(UserHandle.class), anyInt());
         mPackageManager = mContext.getPackageManager();
         when(mPackageManager.getPackageUid(anyString(), eq(0))).thenReturn(Binder.getCallingUid());
+        when(mFeatureFlags.telecomResolveHiddenDependencies()).thenReturn(false);
     }
 
     @Override
@@ -626,6 +626,48 @@
 
     @LargeTest
     @Test
+    public void testIncomingThenOutgoingCalls_AssociatedUsersNotEqual() throws Exception {
+        when(mFeatureFlags.associatedUserRefactorForWorkProfile()).thenReturn(true);
+        InCallServiceFixture.setIgnoreOverrideAdapterFlag(true);
+
+        // Receive incoming call via mPhoneAccountMultiUser
+        IdPair incoming = startAndMakeActiveIncomingCall("650-555-2323",
+                mPhoneAccountMultiUser.getAccountHandle(), mConnectionServiceFixtureA);
+        waitForHandlerAction(mConnectionServiceFixtureA.mConnectionServiceDelegate.getHandler(),
+                TEST_TIMEOUT);
+        // Make outgoing call on mPhoneAccountMultiUser (unassociated sim to simulate guest/
+        // secondary user scenario where both MO/MT calls exist).
+        IdPair outgoing = startAndMakeActiveOutgoingCall("650-555-1212",
+                mPhoneAccountMultiUser.getAccountHandle(), mConnectionServiceFixtureA);
+        waitForHandlerAction(mConnectionServiceFixtureA.mConnectionServiceDelegate.getHandler(),
+                TEST_TIMEOUT);
+
+        // Outgoing call should be on hold while incoming call is made active
+        mConnectionServiceFixtureA.mConnectionById.get(incoming.mConnectionId).state =
+                Connection.STATE_HOLDING;
+
+        // Swap calls and verify that outgoing call is now the active call while the incoming call
+        // is the held call.
+        mConnectionServiceFixtureA.sendSetOnHold(outgoing.mConnectionId);
+        waitForHandlerAction(mConnectionServiceFixtureA.mConnectionServiceDelegate.getHandler(),
+                TEST_TIMEOUT);
+        assertEquals(Call.STATE_HOLDING,
+                mInCallServiceFixtureX.getCall(outgoing.mCallId).getState());
+        assertEquals(Call.STATE_ACTIVE,
+                mInCallServiceFixtureX.getCall(incoming.mCallId).getState());
+
+        // Ensure no issues with call disconnect.
+        mInCallServiceFixtureX.mInCallAdapter.disconnectCall(incoming.mCallId);
+        mInCallServiceFixtureX.mInCallAdapter.disconnectCall(outgoing.mCallId);
+        assertEquals(Call.STATE_DISCONNECTING,
+                mInCallServiceFixtureX.getCall(incoming.mCallId).getState());
+        assertEquals(Call.STATE_DISCONNECTING,
+                mInCallServiceFixtureX.getCall(outgoing.mCallId).getState());
+        InCallServiceFixture.setIgnoreOverrideAdapterFlag(false);
+    }
+
+    @LargeTest
+    @Test
     public void testAudioManagerOperations() throws Exception {
         AudioManager audioManager = (AudioManager) mComponentContextFixture.getTestDouble()
                 .getApplicationContext().getSystemService(Context.AUDIO_SERVICE);
@@ -648,15 +690,15 @@
 
         mInCallServiceFixtureX.mInCallAdapter.setAudioRoute(CallAudioState.ROUTE_SPEAKER, null);
         waitForHandlerAction(mTelecomSystem.getCallsManager().getCallAudioManager()
-                .getCallAudioRouteStateMachine().getHandler(), TEST_TIMEOUT);
+                .getCallAudioRouteAdapter().getAdapterHandler(), TEST_TIMEOUT);
         ArgumentCaptor<AudioDeviceInfo> infoArgumentCaptor =
                 ArgumentCaptor.forClass(AudioDeviceInfo.class);
-        verify(audioManager, timeout(TEST_TIMEOUT)).setCommunicationDevice(
-                infoArgumentCaptor.capture());
+        verify(audioManager, timeout(TEST_TIMEOUT).atLeast(1))
+                .setCommunicationDevice(infoArgumentCaptor.capture());
         assertEquals(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, infoArgumentCaptor.getValue().getType());
         mInCallServiceFixtureX.mInCallAdapter.setAudioRoute(CallAudioState.ROUTE_EARPIECE, null);
         waitForHandlerAction(mTelecomSystem.getCallsManager().getCallAudioManager()
-                .getCallAudioRouteStateMachine().getHandler(), TEST_TIMEOUT);
+                .getCallAudioRouteAdapter().getAdapterHandler(), TEST_TIMEOUT);
         // setSpeakerPhoneOn(false) gets called once during the call initiation phase
         verify(audioManager, timeout(TEST_TIMEOUT).atLeast(1))
                 .clearCommunicationDevice();
@@ -667,7 +709,7 @@
         waitForHandlerAction(mTelecomSystem.getCallsManager().getCallAudioManager()
                 .getCallAudioModeStateMachine().getHandler(), TEST_TIMEOUT);
         waitForHandlerAction(mTelecomSystem.getCallsManager().getCallAudioManager()
-                .getCallAudioRouteStateMachine().getHandler(), TEST_TIMEOUT);
+                .getCallAudioRouteAdapter().getAdapterHandler(), TEST_TIMEOUT);
         verify(audioManager, timeout(TEST_TIMEOUT))
                 .abandonAudioFocusForCall();
         verify(audioManager, timeout(TEST_TIMEOUT).atLeastOnce())
@@ -847,8 +889,7 @@
             @Override
             public Bundle answer(InvocationOnMock invocation) throws Throwable {
                 Bundle bundle = new Bundle();
-                bundle.putInt(BlockedNumberContract.RES_BLOCK_STATUS,
-                        BlockedNumberContract.STATUS_BLOCKED_IN_LIST);
+                bundle.putInt(RES_BLOCK_STATUS, STATUS_BLOCKED_IN_LIST);
                 return bundle;
             }
         });
@@ -995,6 +1036,7 @@
         call.setTargetPhoneAccount(mPhoneAccountA1.getAccountHandle());
         assert(call.isVideoCallingSupportedByPhoneAccount());
         assertEquals(VideoProfile.STATE_BIDIRECTIONAL, call.getVideoState());
+        call.setIsCreateConnectionComplete(true);
     }
 
     /**
@@ -1018,6 +1060,7 @@
         call.setTargetPhoneAccount(mPhoneAccountA2.getAccountHandle());
         assert(!call.isVideoCallingSupportedByPhoneAccount());
         assertEquals(VideoProfile.STATE_AUDIO_ONLY, call.getVideoState());
+        call.setIsCreateConnectionComplete(true);
     }
 
     /**
@@ -1195,7 +1238,7 @@
                 .getState());
         mInCallServiceFixtureX.mInCallAdapter.mute(true);
         waitForHandlerAction(mTelecomSystem.getCallsManager().getCallAudioManager()
-                .getCallAudioRouteStateMachine().getHandler(), TEST_TIMEOUT);
+                .getCallAudioRouteAdapter().getAdapterHandler(), TEST_TIMEOUT);
         assertTrue(mTelecomSystem.getCallsManager().getAudioState().isMuted());
 
         // Make an emergency call.
@@ -1204,14 +1247,14 @@
         assertEquals(Call.STATE_DIALING, mInCallServiceFixtureX.getCall(emergencyCall.mCallId)
                 .getState());
         waitForHandlerAction(mTelecomSystem.getCallsManager().getCallAudioManager()
-                .getCallAudioRouteStateMachine().getHandler(), TEST_TIMEOUT);
+                .getCallAudioRouteAdapter().getAdapterHandler(), TEST_TIMEOUT);
         // Should be unmute automatically.
         assertFalse(mTelecomSystem.getCallsManager().getAudioState().isMuted());
 
         // Toggle mute during an emergency call.
         mTelecomSystem.getCallsManager().getCallAudioManager().toggleMute();
         waitForHandlerAction(mTelecomSystem.getCallsManager().getCallAudioManager()
-                .getCallAudioRouteStateMachine().getHandler(), TEST_TIMEOUT);
+                .getCallAudioRouteAdapter().getAdapterHandler(), TEST_TIMEOUT);
         // Should keep unmute.
         assertFalse(mTelecomSystem.getCallsManager().getAudioState().isMuted());
 
@@ -1339,7 +1382,6 @@
     public void testValidateStatusHintsImage_addExistingConnection() throws Exception {
         IdPair outgoing = startAndMakeActiveOutgoingCall("650-555-1214",
                 mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA);
-        Connection existingConnection = mConnectionServiceFixtureA.mLatestConnection;
 
         // Modify existing connection with StatusHints image exploit
         Icon icon = Icon.createWithContentUri("content://10@media/external/images/media/");
diff --git a/tests/src/com/android/server/telecom/tests/BlockCheckerFilterTest.java b/tests/src/com/android/server/telecom/tests/BlockCheckerFilterTest.java
index 7df4f29..e76989c 100644
--- a/tests/src/com/android/server/telecom/tests/BlockCheckerFilterTest.java
+++ b/tests/src/com/android/server/telecom/tests/BlockCheckerFilterTest.java
@@ -16,12 +16,14 @@
 
 package com.android.server.telecom.tests;
 
-import static android.provider.BlockedNumberContract.STATUS_BLOCKED_IN_LIST;
-import static android.provider.BlockedNumberContract.STATUS_NOT_BLOCKED;
+import static com.android.server.telecom.callfiltering.BlockCheckerFilter.STATUS_BLOCKED_IN_LIST;
+import static com.android.server.telecom.callfiltering.BlockCheckerFilter.STATUS_NOT_BLOCKED;
 
 import static junit.framework.TestCase.assertEquals;
 
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.verify;
@@ -32,11 +34,10 @@
 import android.os.Bundle;
 import android.os.PersistableBundle;
 import android.provider.CallLog;
-import android.telecom.CallerInfo;
 import android.telecom.TelecomManager;
 import android.telephony.CarrierConfigManager;
-import android.test.suitebuilder.annotation.SmallTest;
-import android.util.Pair;
+
+import androidx.test.filters.SmallTest;
 
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallerInfoLookupHelper;
@@ -51,7 +52,6 @@
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 
-import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionStage;
 import java.util.concurrent.TimeUnit;
 
@@ -72,6 +72,7 @@
     private static final CallFilteringResult BLOCK_RESULT = new CallFilteringResult.Builder()
             .setShouldAllowCall(false)
             .setShouldReject(true)
+            .setShouldSilence(true)
             .setShouldAddToCallLog(true)
             .setShouldShowNotification(false)
             .setCallBlockReason(CallLog.Calls.BLOCK_REASON_BLOCKED_NUMBER)
@@ -94,7 +95,7 @@
     @Test
     public void testBlockNumber() throws Exception {
         when(mBlockCheckerAdapter.getBlockStatus(any(Context.class),
-                eq(TEST_HANDLE.getSchemeSpecificPart()), any(Bundle.class)))
+                eq(TEST_HANDLE.getSchemeSpecificPart()), anyInt(), anyBoolean()))
                 .thenReturn(STATUS_BLOCKED_IN_LIST);
 
         setEnhancedBlockingEnabled(false);
@@ -106,7 +107,7 @@
     @Test
     public void testBlockNumberWhenEnhancedBlockingEnabled() throws Exception {
         when(mBlockCheckerAdapter.getBlockStatus(any(Context.class),
-                eq(TEST_HANDLE.getSchemeSpecificPart()), any(Bundle.class)))
+                eq(TEST_HANDLE.getSchemeSpecificPart()), anyInt(), anyBoolean()))
                 .thenReturn(STATUS_BLOCKED_IN_LIST);
 
         setEnhancedBlockingEnabled(true);
@@ -120,7 +121,7 @@
     @Test
     public void testDontBlockNumber() throws Exception {
         when(mBlockCheckerAdapter.getBlockStatus(any(Context.class),
-                eq(TEST_HANDLE.getSchemeSpecificPart()), any(Bundle.class)))
+                eq(TEST_HANDLE.getSchemeSpecificPart()), anyInt(), anyBoolean()))
                 .thenReturn(STATUS_NOT_BLOCKED);
 
         setEnhancedBlockingEnabled(false);
@@ -132,7 +133,7 @@
     @Test
     public void testDontBlockNumberWhenEnhancedBlockingEnabled() throws Exception {
         when(mBlockCheckerAdapter.getBlockStatus(any(Context.class),
-                eq(TEST_HANDLE.getSchemeSpecificPart()), any(Bundle.class)))
+                eq(TEST_HANDLE.getSchemeSpecificPart()), anyInt(), anyBoolean()))
                 .thenReturn(STATUS_NOT_BLOCKED);
 
         setEnhancedBlockingEnabled(true);
diff --git a/tests/src/com/android/server/telecom/tests/BlockedNumbersUtilTests.java b/tests/src/com/android/server/telecom/tests/BlockedNumbersUtilTests.java
index 56cb735..57aee62 100644
--- a/tests/src/com/android/server/telecom/tests/BlockedNumbersUtilTests.java
+++ b/tests/src/com/android/server/telecom/tests/BlockedNumbersUtilTests.java
@@ -16,16 +16,21 @@
 
 package com.android.server.telecom.tests;
 
+import static org.junit.Assert.assertFalse;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.os.UserHandle;
-import android.test.suitebuilder.annotation.SmallTest;
 
+import androidx.test.filters.SmallTest;
+
+import com.android.server.telecom.settings.BlockedNumbersActivity;
 import com.android.server.telecom.settings.BlockedNumbersUtil;
 
 import org.junit.Before;
@@ -57,4 +62,16 @@
         NotificationManager mgr = mComponentContextFixture.getNotificationManager();
         verify(mgr).cancelAsUser(isNull(), anyInt(), any(UserHandle.class));
     }
+
+    /**
+     * Verify that when Telephony isn't present we can still check if a number is an emergency
+     * number in the {@link BlockedNumbersActivity} and not crash.
+     */
+    @SmallTest
+    @Test
+    public void testBlockedNumbersActivityEmergencyCheckWithNoTelephony() {
+        when(mComponentContextFixture.getTelephonyManager().isEmergencyNumber(anyString()))
+                .thenThrow(new UnsupportedOperationException("Bee boop"));
+        assertFalse(BlockedNumbersActivity.isEmergencyNumber(mContext, "911"));
+    }
 }
diff --git a/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java b/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java
index 7d77013..d5e903b 100644
--- a/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java
@@ -18,20 +18,36 @@
 
 import static android.media.AudioDeviceInfo.TYPE_BUILTIN_SPEAKER;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothHeadset;
 import android.bluetooth.BluetoothHearingAid;
 import android.bluetooth.BluetoothLeAudio;
 import android.bluetooth.BluetoothProfile;
-import android.content.BroadcastReceiver;
 import android.content.Intent;
 import android.media.AudioDeviceInfo;
 import android.media.AudioManager;
 import android.os.Bundle;
 import android.os.Parcel;
-import android.test.suitebuilder.annotation.SmallTest;
 
+import androidx.test.filters.SmallTest;
+
+import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
 import com.android.server.telecom.bluetooth.BluetoothDeviceManager;
 import com.android.server.telecom.bluetooth.BluetoothRouteManager;
 import com.android.server.telecom.bluetooth.BluetoothStateReceiver;
@@ -44,23 +60,11 @@
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.ArgumentMatchers.isNull;
-import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import java.util.Arrays;
+import static org.mockito.Mockito.reset;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
+import java.util.concurrent.Executor;
 
 @RunWith(JUnit4.class)
 public class BluetoothDeviceManagerTest extends TelecomTestCase {
@@ -73,10 +77,12 @@
     @Mock BluetoothLeAudio mBluetoothLeAudio;
     @Mock AudioManager mockAudioManager;
     @Mock AudioDeviceInfo mSpeakerInfo;
+    @Mock Executor mExecutor;
 
     BluetoothDeviceManager mBluetoothDeviceManager;
     BluetoothProfile.ServiceListener serviceListenerUnderTest;
     BluetoothStateReceiver receiverUnderTest;
+    CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
     ArgumentCaptor<BluetoothLeAudio.Callback> leAudioCallbacksTest;
 
     private BluetoothDevice device1;
@@ -104,10 +110,14 @@
         when(mBluetoothHearingAid.getHiSyncId(device4)).thenReturn(100L);
 
         mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
-        mBluetoothDeviceManager = new BluetoothDeviceManager(mContext, mAdapter);
+        mCommunicationDeviceTracker = new CallAudioCommunicationDeviceTracker(mContext);
+        mBluetoothDeviceManager = new BluetoothDeviceManager(mContext, mAdapter,
+                mCommunicationDeviceTracker, mFeatureFlags);
         mBluetoothDeviceManager.setBluetoothRouteManager(mRouteManager);
+        mCommunicationDeviceTracker.setBluetoothRouteManager(mRouteManager);
 
         mockAudioManager = mContext.getSystemService(AudioManager.class);
+        mExecutor = mContext.getMainExecutor();
 
         ArgumentCaptor<BluetoothProfile.ServiceListener> serviceCaptor =
                 ArgumentCaptor.forClass(BluetoothProfile.ServiceListener.class);
@@ -115,7 +125,8 @@
                 serviceCaptor.capture(), eq(BluetoothProfile.HEADSET));
         serviceListenerUnderTest = serviceCaptor.getValue();
 
-        receiverUnderTest = new BluetoothStateReceiver(mBluetoothDeviceManager, mRouteManager);
+        receiverUnderTest = new BluetoothStateReceiver(mBluetoothDeviceManager,
+                mRouteManager, mCommunicationDeviceTracker, mFeatureFlags);
 
         mBluetoothDeviceManager.setHeadsetServiceForTesting(mBluetoothHeadset);
         mBluetoothDeviceManager.setHearingAidServiceForTesting(mBluetoothHearingAid);
@@ -126,6 +137,7 @@
         verify(mBluetoothLeAudio).registerCallback(any(), leAudioCallbacksTest.capture());
 
         when(mSpeakerInfo.getType()).thenReturn(TYPE_BUILTIN_SPEAKER);
+        when(mFeatureFlags.callAudioCommunicationDeviceRefactor()).thenReturn(false);
     }
 
     @Override
@@ -395,7 +407,8 @@
         when(mAdapter.setActiveDevice(nullable(BluetoothDevice.class),
                     eq(BluetoothAdapter.ACTIVE_DEVICE_ALL))).thenReturn(true);
         mBluetoothDeviceManager.connectAudio(device1.getAddress(), false);
-        verify(mAdapter).setActiveDevice(device1, BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL);
+        verify(mAdapter).setActiveDevice(eq(device1),
+                eq(BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL));
         verify(mAdapter, never()).setActiveDevice(nullable(BluetoothDevice.class),
                 eq(BluetoothAdapter.ACTIVE_DEVICE_ALL));
         mBluetoothDeviceManager.disconnectAudio();
@@ -413,8 +426,8 @@
         when(mAdapter.setActiveDevice(nullable(BluetoothDevice.class),
                 eq(BluetoothAdapter.ACTIVE_DEVICE_ALL))).thenReturn(true);
 
-        AudioDeviceInfo mockAudioDeviceInfo = mock(AudioDeviceInfo.class);
-        when(mockAudioDeviceInfo.getType()).thenReturn(AudioDeviceInfo.TYPE_HEARING_AID);
+        AudioDeviceInfo mockAudioDeviceInfo = createMockAudioDeviceInfo(device5.getAddress(),
+                AudioDeviceInfo.TYPE_HEARING_AID);
         List<AudioDeviceInfo> devices = new ArrayList<>();
         devices.add(mockAudioDeviceInfo);
 
@@ -434,7 +447,7 @@
 
         when(mockAudioManager.getCommunicationDevice()).thenReturn(mockAudioDeviceInfo);
         mBluetoothDeviceManager.disconnectAudio();
-        verify(mockAudioManager).clearCommunicationDevice();
+        verify(mockAudioManager, atLeastOnce()).clearCommunicationDevice();
     }
 
     @SmallTest
@@ -448,8 +461,8 @@
         when(mAdapter.setActiveDevice(nullable(BluetoothDevice.class),
                 eq(BluetoothAdapter.ACTIVE_DEVICE_ALL))).thenReturn(true);
 
-        AudioDeviceInfo mockAudioDeviceInfo = mock(AudioDeviceInfo.class);
-        when(mockAudioDeviceInfo.getType()).thenReturn(AudioDeviceInfo.TYPE_BLE_HEADSET);
+        AudioDeviceInfo mockAudioDeviceInfo = createMockAudioDeviceInfo(device5.getAddress(),
+                AudioDeviceInfo.TYPE_BLE_HEADSET);
         List<AudioDeviceInfo> devices = new ArrayList<>();
         devices.add(mockAudioDeviceInfo);
 
@@ -470,7 +483,7 @@
                 BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO));
 
         mBluetoothDeviceManager.disconnectAudio();
-        verify(mockAudioManager).clearCommunicationDevice();
+        verify(mockAudioManager, atLeastOnce()).clearCommunicationDevice();
     }
 
     @SmallTest
@@ -508,7 +521,86 @@
 
     @SmallTest
     @Test
+    public void testConnectMultipleLeAudioDevices() {
+        when(mFeatureFlags.callAudioCommunicationDeviceRefactor()).thenReturn(true);
+        receiverUnderTest.setIsInCall(true);
+        receiverUnderTest.onReceive(mContext,
+                buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device1,
+                        BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO));
+        leAudioCallbacksTest.getValue().onGroupNodeAdded(device1, 1);
+        receiverUnderTest.onReceive(mContext,
+                buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device2,
+                        BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO));
+        leAudioCallbacksTest.getValue().onGroupNodeAdded(device2, 1);
+        when(mAdapter.setActiveDevice(nullable(BluetoothDevice.class),
+                eq(BluetoothAdapter.ACTIVE_DEVICE_ALL))).thenReturn(true);
+
+        List<AudioDeviceInfo> devices = new ArrayList<>();
+        AudioDeviceInfo leAudioDevice1 = createMockAudioDeviceInfo(device1.getAddress(),
+                AudioDeviceInfo.TYPE_BLE_HEADSET);
+        AudioDeviceInfo leAudioDevice2 = createMockAudioDeviceInfo(device2.getAddress(),
+                AudioDeviceInfo.TYPE_BLE_HEADSET);
+        devices.add(leAudioDevice1);
+        devices.add(leAudioDevice2);
+
+        when(mockAudioManager.getAvailableCommunicationDevices())
+                .thenReturn(devices);
+        when(mockAudioManager.setCommunicationDevice(any(AudioDeviceInfo.class)))
+                .thenReturn(true);
+
+        // Connect LE audio device
+        mBluetoothDeviceManager.connectAudio(device1.getAddress(), false);
+        verify(mAdapter).setActiveDevice(device1, BluetoothAdapter.ACTIVE_DEVICE_ALL);
+        verify(mBluetoothHeadset, never()).connectAudio();
+        verify(mAdapter, never()).setActiveDevice(nullable(BluetoothDevice.class),
+                eq(BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL));
+        // Verify that we set the communication device for device 1
+        verify(mockAudioManager).setCommunicationDevice(leAudioDevice1);
+
+        // Change active device to other LE audio device
+        receiverUnderTest.onReceive(mContext, buildActiveDeviceChangeActionIntent(device2,
+                BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO));
+
+        // Verify call to clearLeAudioCommunicationDevice
+        verify(mRouteManager).onAudioLost(eq(DEVICE_ADDRESS_1));
+        // Verify that we set the communication device for device2
+        verify(mockAudioManager).setCommunicationDevice(leAudioDevice2);
+    }
+
+    @SmallTest
+    @Test
+    public void testClearCommunicationDeviceOnActiveDeviceChange() {
+        when(mFeatureFlags.callAudioCommunicationDeviceRefactor()).thenReturn(true);
+        receiverUnderTest.setIsInCall(true);
+
+        List<AudioDeviceInfo> devices = new ArrayList<>();
+        AudioDeviceInfo leAudioDevice1 = createMockAudioDeviceInfo(device1.getAddress(),
+                AudioDeviceInfo.TYPE_BLE_HEADSET);
+        devices.add(leAudioDevice1);
+
+        when(mockAudioManager.getAvailableCommunicationDevices())
+                .thenReturn(devices);
+        when(mockAudioManager.setCommunicationDevice(any(AudioDeviceInfo.class)))
+                .thenReturn(true);
+
+        // Pretend that the speaker device is currently the requested device set for communication.
+        // This test ensures that the set/clear communication logic for audio switching in/out of BT
+        // is properly working when the receiver processes an active device change intent.
+        mCommunicationDeviceTracker.setTestCommunicationDevice(TYPE_BUILTIN_SPEAKER);
+
+        // Notify that LE audio device has been turned on
+        receiverUnderTest.onReceive(mContext, buildActiveDeviceChangeActionIntent(device1,
+                BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO));
+        // Verify call to clear speaker communication device
+        verify(mockAudioManager).clearCommunicationDevice();
+        // Verify that LE audio communication device was set after clearing the speaker device
+        verify(mockAudioManager).setCommunicationDevice(leAudioDevice1);
+    }
+
+    @SmallTest
+    @Test
     public void testConnectDualModeEarbud() {
+        when(mFeatureFlags.callAudioCommunicationDeviceRefactor()).thenReturn(true);
         receiverUnderTest.setIsInCall(true);
 
         // LE Audio earbuds connected
@@ -527,11 +619,11 @@
         when(mAdapter.setActiveDevice(nullable(BluetoothDevice.class),
                 eq(BluetoothAdapter.ACTIVE_DEVICE_ALL))).thenReturn(true);
 
-        AudioDeviceInfo mockAudioDevice5Info = mock(AudioDeviceInfo.class);
-        when(mockAudioDevice5Info.getAddress()).thenReturn(device5.getAddress());
+        AudioDeviceInfo mockAudioDevice5Info = createMockAudioDeviceInfo(device5.getAddress(),
+                AudioDeviceInfo.TYPE_BLE_HEADSET);
+        AudioDeviceInfo mockAudioDevice6Info = createMockAudioDeviceInfo(device6.getAddress(),
+                AudioDeviceInfo.TYPE_BLE_HEADSET);
         when(mockAudioDevice5Info.getType()).thenReturn(AudioDeviceInfo.TYPE_BLE_HEADSET);
-        AudioDeviceInfo mockAudioDevice6Info = mock(AudioDeviceInfo.class);
-        when(mockAudioDevice6Info.getAddress()).thenReturn(device6.getAddress());
         when(mockAudioDevice6Info.getType()).thenReturn(AudioDeviceInfo.TYPE_BLE_HEADSET);
         List<AudioDeviceInfo> devices = new ArrayList<>();
         devices.add(mockAudioDevice5Info);
@@ -572,6 +664,7 @@
         verify(mBluetoothHeadset, never()).connectAudio();
         verify(mAdapter, never()).setActiveDevice(nullable(BluetoothDevice.class),
                 eq(BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL));
+        verify(mockAudioManager, times(1)).clearCommunicationDevice();
 
         // Reconnect other LE Audio earbud
         devices.add(mockAudioDevice5Info);
@@ -582,7 +675,7 @@
 
         // Disconnects audio
         mBluetoothDeviceManager.disconnectAudio();
-        verify(mockAudioManager, times(1)).clearCommunicationDevice();
+        verify(mockAudioManager, times(2)).clearCommunicationDevice();
         verify(mBluetoothHeadset, times(1)).disconnectAudio();
 
         // TEST 2: HFP preferred for DUPLEX
@@ -592,7 +685,8 @@
                 eq(BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL))).thenReturn(true);
         mBluetoothDeviceManager.connectAudio(device5.getAddress(), false);
         verify(mAdapter).setActiveDevice(device5, BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL);
-        verify(mAdapter, times(1)).setActiveDevice(device5, BluetoothAdapter.ACTIVE_DEVICE_ALL);
+        verify(mAdapter, times(1)).setActiveDevice(device5,
+                BluetoothAdapter.ACTIVE_DEVICE_ALL);
         verify(mBluetoothHeadset).connectAudio();
         mBluetoothDeviceManager.disconnectAudio();
         verify(mBluetoothHeadset, times(2)).disconnectAudio();
@@ -600,23 +694,28 @@
 
     @SmallTest
     @Test
-    public void testClearHearingAidCommunicationDevice() {
-        AudioDeviceInfo mockAudioDeviceInfo = mock(AudioDeviceInfo.class);
-        when(mockAudioDeviceInfo.getAddress()).thenReturn(DEVICE_ADDRESS_1);
-        when(mockAudioDeviceInfo.getType()).thenReturn(AudioDeviceInfo.TYPE_HEARING_AID);
-        List<AudioDeviceInfo> devices = new ArrayList<>();
-        devices.add(mockAudioDeviceInfo);
+    public void testClearHearingAidCommunicationDeviceLegacy() {
+        assertClearHearingAidOrLeCommunicationDevice(false, AudioDeviceInfo.TYPE_HEARING_AID);
+    }
 
-        when(mockAudioManager.getAvailableCommunicationDevices())
-                .thenReturn(devices);
-        when(mockAudioManager.setCommunicationDevice(eq(mockAudioDeviceInfo)))
-                .thenReturn(true);
+    @SmallTest
+    @Test
+    public void testClearHearingAidCommunicationDeviceWithFlag() {
+        when(mFeatureFlags.callAudioCommunicationDeviceRefactor()).thenReturn(true);
+        assertClearHearingAidOrLeCommunicationDevice(true, AudioDeviceInfo.TYPE_HEARING_AID);
+    }
 
-        mBluetoothDeviceManager.setHearingAidCommunicationDevice();
-        when(mockAudioManager.getCommunicationDevice()).thenReturn(mSpeakerInfo);
-        mBluetoothDeviceManager.clearHearingAidCommunicationDevice();
-        verify(mRouteManager).onAudioLost(eq(DEVICE_ADDRESS_1));
-        assertFalse(mBluetoothDeviceManager.isHearingAidSetAsCommunicationDevice());
+    @SmallTest
+    @Test
+    public void testClearLeAudioCommunicationDeviceLegacy() {
+        assertClearHearingAidOrLeCommunicationDevice(false, AudioDeviceInfo.TYPE_BLE_HEADSET);
+    }
+
+    @SmallTest
+    @Test
+    public void testClearLeAudioCommunicationDeviceWithFlag() {
+        when(mFeatureFlags.callAudioCommunicationDeviceRefactor()).thenReturn(true);
+        assertClearHearingAidOrLeCommunicationDevice(true, AudioDeviceInfo.TYPE_BLE_HEADSET);
     }
 
     @SmallTest
@@ -644,22 +743,92 @@
         when(mBluetoothLeAudio.isInbandRingtoneEnabled(1)).thenReturn(true);
         when(mBluetoothLeAudio.getGroupId(eq(device3))).thenReturn(1);
         receiverUnderTest.onReceive(mContext,
-                buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device1,
-                        BluetoothDeviceManager.DEVICE_TYPE_HEADSET));
-        receiverUnderTest.onReceive(mContext,
-                buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device2,
-                        BluetoothDeviceManager.DEVICE_TYPE_HEADSET));
-        receiverUnderTest.onReceive(mContext,
                 buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device3,
                         BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO));
         leAudioCallbacksTest.getValue().onGroupNodeAdded(device3, 1);
-        when(mBluetoothLeAudio.getConnectedGroupLeadDevice(1)).thenReturn(device3);
         when(mRouteManager.getBluetoothAudioConnectedDevice()).thenReturn(device3);
         when(mRouteManager.isCachedLeAudioDevice(eq(device3))).thenReturn(true);
-        assertEquals(3, mBluetoothDeviceManager.getNumConnectedDevices());
+        when(mBluetoothLeAudio.getConnectedGroupLeadDevice(1)).thenReturn(device3);
+        when(mRouteManager.getMostRecentlyReportedActiveDevice()).thenReturn(device3);
+        assertEquals(1, mBluetoothDeviceManager.getNumConnectedDevices());
         assertTrue(mBluetoothDeviceManager.isInbandRingingEnabled());
     }
 
+    @SmallTest
+    @Test
+    public void testRegisterLeAudioCallbackNoPostpone() {
+        reset(mBluetoothLeAudio);
+        when(mFeatureFlags.postponeRegisterToLeaudio()).thenReturn(false);
+        serviceListenerUnderTest.onServiceConnected(BluetoothProfile.LE_AUDIO,
+                        (BluetoothProfile) mBluetoothLeAudio);
+        // Second time on purpose
+        serviceListenerUnderTest.onServiceConnected(BluetoothProfile.LE_AUDIO,
+                        (BluetoothProfile) mBluetoothLeAudio);
+        verify(mExecutor, times(0)).execute(any());
+        verify(mBluetoothLeAudio, times(1)).registerCallback(any(Executor.class),
+                        any(BluetoothLeAudio.Callback.class));
+    }
+
+    @SmallTest
+    @Test
+    public void testRegisterLeAudioCallbackWithPostpone() {
+        reset(mBluetoothLeAudio);
+        when(mFeatureFlags.postponeRegisterToLeaudio()).thenReturn(true);
+        serviceListenerUnderTest.onServiceConnected(BluetoothProfile.LE_AUDIO,
+                        (BluetoothProfile) mBluetoothLeAudio);
+        verify(mExecutor, times(1)).execute(any());
+    }
+
+    private void assertClearHearingAidOrLeCommunicationDevice(
+            boolean flagEnabled, int device_type
+    ) {
+        AudioDeviceInfo mockAudioDeviceInfo = mock(AudioDeviceInfo.class);
+        when(mockAudioDeviceInfo.getAddress()).thenReturn(DEVICE_ADDRESS_1);
+        when(mockAudioDeviceInfo.getType()).thenReturn(device_type);
+        List<AudioDeviceInfo> devices = new ArrayList<>();
+        devices.add(mockAudioDeviceInfo);
+
+        when(mockAudioManager.getAvailableCommunicationDevices())
+                .thenReturn(devices);
+        when(mockAudioManager.setCommunicationDevice(eq(mockAudioDeviceInfo)))
+                .thenReturn(true);
+
+        if (flagEnabled) {
+            BluetoothDevice btDevice = device_type == AudioDeviceInfo.TYPE_BLE_HEADSET
+                    ? device1 : null;
+            mCommunicationDeviceTracker.setCommunicationDevice(device_type, btDevice);
+        } else {
+            if (device_type == AudioDeviceInfo.TYPE_BLE_HEADSET) {
+                mBluetoothDeviceManager.setLeAudioCommunicationDevice();
+            } else {
+                mBluetoothDeviceManager.setHearingAidCommunicationDevice();
+            }
+        }
+        when(mockAudioManager.getCommunicationDevice()).thenReturn(mSpeakerInfo);
+        if (flagEnabled) {
+            mCommunicationDeviceTracker.clearCommunicationDevice(device_type);
+            assertFalse(mCommunicationDeviceTracker.isAudioDeviceSetForType(device_type));
+        } else {
+            if (device_type == AudioDeviceInfo.TYPE_BLE_HEADSET) {
+                mBluetoothDeviceManager.clearLeAudioCommunicationDevice();
+                assertFalse(mBluetoothDeviceManager.isLeAudioCommunicationDevice());
+            } else {
+                mBluetoothDeviceManager.clearHearingAidCommunicationDevice();
+                assertFalse(mBluetoothDeviceManager.isHearingAidSetAsCommunicationDevice());
+            }
+        }
+        verify(mRouteManager).onAudioLost(eq(DEVICE_ADDRESS_1));
+    }
+
+    private AudioDeviceInfo createMockAudioDeviceInfo(String address, int audioType) {
+        AudioDeviceInfo mockAudioDeviceInfo = mock(AudioDeviceInfo.class);
+        when(mockAudioDeviceInfo.getType()).thenReturn(audioType);
+        if (address != null) {
+            when(mockAudioDeviceInfo.getAddress()).thenReturn(address);
+        }
+        return mockAudioDeviceInfo;
+    }
+
     private Intent buildConnectionActionIntent(int state, BluetoothDevice device, int deviceType) {
         String intentString;
 
diff --git a/tests/src/com/android/server/telecom/tests/BluetoothRouteManagerTest.java b/tests/src/com/android/server/telecom/tests/BluetoothRouteManagerTest.java
index 1a6fb88..1c885c1 100644
--- a/tests/src/com/android/server/telecom/tests/BluetoothRouteManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/BluetoothRouteManagerTest.java
@@ -16,6 +16,17 @@
 
 package com.android.server.telecom.tests;
 
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothHeadset;
@@ -26,9 +37,11 @@
 import android.content.ContentResolver;
 import android.os.Parcel;
 import android.telecom.Log;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.filters.SmallTest;
 
 import com.android.internal.os.SomeArgs;
+import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
 import com.android.server.telecom.TelecomSystem;
 import com.android.server.telecom.Timeouts;
 import com.android.server.telecom.bluetooth.BluetoothDeviceManager;
@@ -46,23 +59,20 @@
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
-import static org.junit.Assert.assertEquals;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
 @RunWith(JUnit4.class)
 public class BluetoothRouteManagerTest extends TelecomTestCase {
     private static final int TEST_TIMEOUT = 1000;
     static final BluetoothDevice DEVICE1 = makeBluetoothDevice("00:00:00:00:00:01");
     static final BluetoothDevice DEVICE2 = makeBluetoothDevice("00:00:00:00:00:02");
     static final BluetoothDevice DEVICE3 = makeBluetoothDevice("00:00:00:00:00:03");
-    static final BluetoothDevice HEARING_AID_DEVICE = makeBluetoothDevice("00:00:00:00:00:04");
+    static final BluetoothDevice HEARING_AID_DEVICE_LEFT = makeBluetoothDevice("CA:FE:DE:CA:00:01");
+    static final BluetoothDevice HEARING_AID_DEVICE_RIGHT =
+      makeBluetoothDevice("CA:FE:DE:CA:00:02");
+    // See HearingAidService#getActiveDevices
+    // Note: It is really important that the left HA is the first one. The left HA is always
+    // in the first index (0) and the right one in the second index (1).
+    static final BluetoothDevice[] HEARING_AIDS =
+      new BluetoothDevice[]{HEARING_AID_DEVICE_LEFT, HEARING_AID_DEVICE_RIGHT};
 
     @Mock private BluetoothAdapter mBluetoothAdapter;
     @Mock private BluetoothDeviceManager mDeviceManager;
@@ -71,6 +81,7 @@
     @Mock private BluetoothLeAudio mBluetoothLeAudio;
     @Mock private Timeouts.Adapter mTimeoutsAdapter;
     @Mock private BluetoothRouteManager.BluetoothStateListener mListener;
+    @Mock private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
 
     @Override
     @Before
@@ -86,6 +97,59 @@
 
     @SmallTest
     @Test
+    public void testConnectLeftHearingAidWhenLeftIsActive() {
+        BluetoothRouteManager sm = setupStateMachine(
+                BluetoothRouteManager.AUDIO_OFF_STATE_NAME, HEARING_AID_DEVICE_LEFT);
+        sm.onActiveDeviceChanged(HEARING_AID_DEVICE_LEFT,
+            BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID);
+        when(mDeviceManager.connectAudio(anyString(), anyBoolean())).thenReturn(true);
+        when(mDeviceManager.isHearingAidSetAsCommunicationDevice()).thenReturn(true);
+
+        setupConnectedDevices(null, HEARING_AIDS, null, null, HEARING_AIDS, null);
+        when(mBluetoothHeadset.getAudioState(nullable(BluetoothDevice.class)))
+          .thenReturn(BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
+
+        executeRoutingAction(sm,
+            BluetoothRouteManager.NEW_DEVICE_CONNECTED, HEARING_AID_DEVICE_LEFT.getAddress());
+
+        executeRoutingAction(sm,
+            BluetoothRouteManager.CONNECT_BT, HEARING_AID_DEVICE_LEFT.getAddress());
+
+        assertEquals(BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX
+            + ":" + HEARING_AID_DEVICE_LEFT.getAddress(), sm.getCurrentState().getName());
+
+        sm.quitNow();
+    }
+
+    @SmallTest
+    @Test
+    public void testConnectRightHearingAidWhenLeftIsActive() {
+        BluetoothRouteManager sm = setupStateMachine(
+                BluetoothRouteManager.AUDIO_OFF_STATE_NAME, HEARING_AID_DEVICE_RIGHT);
+        sm.onActiveDeviceChanged(HEARING_AID_DEVICE_LEFT,
+            BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID);
+        when(mDeviceManager.connectAudio(anyString(), anyBoolean())).thenReturn(true);
+        when(mDeviceManager.isHearingAidSetAsCommunicationDevice()).thenReturn(true);
+
+
+        setupConnectedDevices(null, HEARING_AIDS, null, null, HEARING_AIDS, null);
+        when(mBluetoothHeadset.getAudioState(nullable(BluetoothDevice.class)))
+          .thenReturn(BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
+
+        executeRoutingAction(sm,
+            BluetoothRouteManager.NEW_DEVICE_CONNECTED, HEARING_AID_DEVICE_LEFT.getAddress());
+
+        executeRoutingAction(sm,
+            BluetoothRouteManager.CONNECT_BT, HEARING_AID_DEVICE_LEFT.getAddress());
+
+        assertEquals(BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX
+            + ":" + HEARING_AID_DEVICE_LEFT.getAddress(), sm.getCurrentState().getName());
+
+        sm.quitNow();
+    }
+
+    @SmallTest
+    @Test
     public void testConnectBtRetryWhileNotConnected() {
         BluetoothRouteManager sm = setupStateMachine(
                 BluetoothRouteManager.AUDIO_OFF_STATE_NAME, null);
@@ -112,15 +176,15 @@
         BluetoothRouteManager sm = setupStateMachine(
                 BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX, DEVICE1);
         setupConnectedDevices(new BluetoothDevice[]{DEVICE1},
-                new BluetoothDevice[]{HEARING_AID_DEVICE}, new BluetoothDevice[]{DEVICE2},
-                DEVICE1, HEARING_AID_DEVICE, DEVICE2);
+                HEARING_AIDS, new BluetoothDevice[]{DEVICE2},
+                DEVICE1,  HEARING_AIDS, DEVICE2);
         sm.onActiveDeviceChanged(DEVICE1, BluetoothDeviceManager.DEVICE_TYPE_HEADSET);
         sm.onActiveDeviceChanged(DEVICE2, BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO);
-        sm.onActiveDeviceChanged(HEARING_AID_DEVICE,
+        sm.onActiveDeviceChanged(HEARING_AID_DEVICE_LEFT,
                 BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID);
         executeRoutingAction(sm, BluetoothRouteManager.BT_AUDIO_LOST, DEVICE1.getAddress());
 
-        verifyConnectionAttempt(HEARING_AID_DEVICE, 0);
+        verifyConnectionAttempt(HEARING_AID_DEVICE_LEFT, 0);
         verifyConnectionAttempt(DEVICE1, 0);
         verifyConnectionAttempt(DEVICE2, 0);
         assertEquals(BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX
@@ -171,11 +235,71 @@
         sm.quitNow();
     }
 
+    @SmallTest
+    @Test
+    public void testSkipInactiveBtDeviceWhenEvaluateActualState() {
+        BluetoothRouteManager sm = setupStateMachine(
+                BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX, HEARING_AID_DEVICE_LEFT);
+        setupConnectedDevices(null, HEARING_AIDS,
+                null, null, HEARING_AIDS, null);
+        executeRoutingAction(sm, BluetoothRouteManager.BT_AUDIO_LOST,
+                HEARING_AID_DEVICE_LEFT.getAddress());
+        assertEquals(BluetoothRouteManager.AUDIO_OFF_STATE_NAME, sm.getCurrentState().getName());
+        sm.quitNow();
+    }
+
+    @SmallTest
+    @Test
+    public void testConnectBtWithoutAddress_SwitchingBtDeviceFlag() {
+        when(mFeatureFlags.resolveSwitchingBtDevicesComputation()).thenReturn(true);
+        verifyConnectBtWithoutAddress();
+    }
+
+    @SmallTest
+    @Test
+    public void testConnectBtWithoutAddress_SwitchingBtDeviceFlagDisabled() {
+        verifyConnectBtWithoutAddress();
+    }
+
+    private void verifyConnectBtWithoutAddress() {
+        when(mFeatureFlags.useActualAddressToEnterConnectingState()).thenReturn(true);
+        BluetoothRouteManager sm = setupStateMachine(
+                BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX, DEVICE1);
+        setupConnectedDevices(new BluetoothDevice[]{DEVICE1, DEVICE2}, null, null, null, null,
+                null);
+        when(mTimeoutsAdapter.getRetryBluetoothConnectAudioBackoffMillis(
+                nullable(ContentResolver.class))).thenReturn(0L);
+        when(mBluetoothHeadset.connectAudio()).thenReturn(BluetoothStatusCodes.ERROR_UNKNOWN);
+        executeRoutingAction(sm, BluetoothRouteManager.CONNECT_BT, null);
+        // Wait 3 times: the first connection attempt is accounted for in executeRoutingAction,
+        // so wait twice for the retry attempt, again to make sure there are only three attempts,
+        // and once more for good luck.
+        waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
+        waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
+        waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
+        waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
+        // We should not expect explicit connection attempt (BluetoothDeviceManager#connectAudio)
+        // as the device is already "connected" as per how the state machine was initialized.
+        if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+            verify(mDeviceManager, never()).disconnectAudio();
+        } else {
+            // Legacy behavior
+            verifyConnectionAttempt(DEVICE1, 1);
+            verify(mDeviceManager, times(1)).disconnectAudio();
+        }
+        assertEquals(BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX
+                        + ":" + DEVICE1.getAddress(),
+                sm.getCurrentState().getName());
+        sm.getHandler().removeMessages(BluetoothRouteManager.CONNECTION_TIMEOUT);
+        sm.quitNow();
+    }
+
     private BluetoothRouteManager setupStateMachine(String initialState,
             BluetoothDevice initialDevice) {
         resetMocks();
         BluetoothRouteManager sm = new BluetoothRouteManager(mContext,
-                new TelecomSystem.SyncRoot() { }, mDeviceManager, mTimeoutsAdapter);
+                new TelecomSystem.SyncRoot() { }, mDeviceManager,
+                mTimeoutsAdapter, mCommunicationDeviceTracker, mFeatureFlags);
         sm.setListener(mListener);
         sm.setInitialStateForTesting(initialState, initialDevice);
         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
@@ -185,10 +309,11 @@
 
     private void setupConnectedDevices(BluetoothDevice[] hfpDevices,
             BluetoothDevice[] hearingAidDevices, BluetoothDevice[] leAudioDevices,
-            BluetoothDevice hfpActiveDevice, BluetoothDevice hearingAidActiveDevice,
+            BluetoothDevice hfpActiveDevice, BluetoothDevice[] hearingAidActiveDevices,
             BluetoothDevice leAudioDevice) {
         if (hfpDevices == null) hfpDevices = new BluetoothDevice[]{};
         if (hearingAidDevices == null) hearingAidDevices = new BluetoothDevice[]{};
+        if (hearingAidActiveDevices == null) hearingAidActiveDevices = new BluetoothDevice[]{};
         if (leAudioDevice == null) leAudioDevices = new BluetoothDevice[]{};
 
         when(mDeviceManager.getNumConnectedDevices()).thenReturn(
@@ -207,7 +332,7 @@
         when(mBluetoothHearingAid.getConnectedDevices())
                 .thenReturn(Arrays.asList(hearingAidDevices));
         when(mBluetoothAdapter.getActiveDevices(eq(BluetoothProfile.HEARING_AID)))
-                .thenReturn(Arrays.asList(hearingAidActiveDevice, null));
+                .thenReturn(Arrays.asList(hearingAidActiveDevices));
         when(mBluetoothAdapter.getActiveDevices(eq(BluetoothProfile.LE_AUDIO)))
                 .thenReturn(Arrays.asList(leAudioDevice, null));
     }
diff --git a/tests/src/com/android/server/telecom/tests/BluetoothRouteTransitionTests.java b/tests/src/com/android/server/telecom/tests/BluetoothRouteTransitionTests.java
index 5eecccc..c546c3f 100644
--- a/tests/src/com/android/server/telecom/tests/BluetoothRouteTransitionTests.java
+++ b/tests/src/com/android/server/telecom/tests/BluetoothRouteTransitionTests.java
@@ -16,6 +16,21 @@
 
 package com.android.server.telecom.tests;
 
+import static com.android.server.telecom.tests.BluetoothRouteManagerTest.DEVICE1;
+import static com.android.server.telecom.tests.BluetoothRouteManagerTest.DEVICE2;
+import static com.android.server.telecom.tests.BluetoothRouteManagerTest.DEVICE3;
+import static com.android.server.telecom.tests.BluetoothRouteManagerTest.executeRoutingAction;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothHeadset;
@@ -25,9 +40,11 @@
 import android.bluetooth.BluetoothStatusCodes;
 import android.content.ContentResolver;
 import android.telecom.Log;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.filters.SmallTest;
 
 import com.android.internal.os.SomeArgs;
+import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
 import com.android.server.telecom.TelecomSystem;
 import com.android.server.telecom.Timeouts;
 import com.android.server.telecom.bluetooth.BluetoothDeviceManager;
@@ -45,28 +62,13 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
-import java.util.stream.Stream;
 import java.util.stream.Collectors;
 
-import static com.android.server.telecom.tests.BluetoothRouteManagerTest.DEVICE1;
-import static com.android.server.telecom.tests.BluetoothRouteManagerTest.DEVICE2;
-import static com.android.server.telecom.tests.BluetoothRouteManagerTest.DEVICE3;
-import static com.android.server.telecom.tests.BluetoothRouteManagerTest.executeRoutingAction;
-import static org.junit.Assert.assertEquals;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Mockito.clearInvocations;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
 @RunWith(Parameterized.class)
 public class BluetoothRouteTransitionTests extends TelecomTestCase {
     private enum ListenerUpdate {
         DEVICE_LIST_CHANGED, ACTIVE_DEVICE_PRESENT, ACTIVE_DEVICE_GONE,
-        AUDIO_CONNECTED, AUDIO_DISCONNECTED, UNEXPECTED_STATE_CHANGE
+        AUDIO_CONNECTING, AUDIO_CONNECTED, AUDIO_DISCONNECTED, UNEXPECTED_STATE_CHANGE
     }
 
     private static class BluetoothRouteTestParametersBuilder {
@@ -263,6 +265,7 @@
     @Mock private BluetoothLeAudio mBluetoothLeAudio;
     @Mock private Timeouts.Adapter mTimeoutsAdapter;
     @Mock private BluetoothRouteManager.BluetoothStateListener mListener;
+    @Mock private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
 
     @Override
     @Before
@@ -348,6 +351,9 @@
                 case ACTIVE_DEVICE_GONE:
                     verify(mListener).onBluetoothActiveDeviceGone();
                     break;
+                case AUDIO_CONNECTING:
+                    verify(mListener).onBluetoothAudioConnecting();
+                    break;
                 case AUDIO_CONNECTED:
                     verify(mListener).onBluetoothAudioConnected();
                     break;
@@ -413,7 +419,8 @@
         when(mTimeoutsAdapter.getBluetoothPendingTimeoutMillis(
                 nullable(ContentResolver.class))).thenReturn(100000L);
         BluetoothRouteManager sm = new BluetoothRouteManager(mContext,
-                new TelecomSystem.SyncRoot() { }, mDeviceManager, mTimeoutsAdapter);
+                new TelecomSystem.SyncRoot() { }, mDeviceManager,
+                mTimeoutsAdapter, mCommunicationDeviceTracker, mFeatureFlags);
         sm.setListener(mListener);
         sm.setInitialStateForTesting(initialState, initialDevice);
         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
@@ -449,7 +456,7 @@
                 .setConnectedDevices(DEVICE2, DEVICE1)
                 .setActiveDevice(DEVICE1)
                 .setMessageType(BluetoothRouteManager.CONNECT_BT)
-                .setExpectedListenerUpdates(ListenerUpdate.AUDIO_CONNECTED)
+                .setExpectedListenerUpdates(ListenerUpdate.AUDIO_CONNECTING)
                 .setExpectedBluetoothInteraction(CONNECT)
                 .setExpectedConnectionDevice(DEVICE1)
                 .setExpectedFinalStateName(BluetoothRouteManager.AUDIO_CONNECTING_STATE_NAME_PREFIX
@@ -505,7 +512,7 @@
                 .setConnectedDevices(DEVICE2, DEVICE1, DEVICE3)
                 .setMessageType(BluetoothRouteManager.CONNECT_BT)
                 .setMessageDevice(DEVICE3)
-                .setExpectedListenerUpdates(ListenerUpdate.AUDIO_CONNECTED)
+                .setExpectedListenerUpdates(ListenerUpdate.AUDIO_CONNECTING)
                 .setExpectedBluetoothInteraction(CONNECT_SWITCH_DEVICE)
                 .setExpectedConnectionDevice(DEVICE3)
                 .setExpectedFinalStateName(BluetoothRouteManager.AUDIO_CONNECTING_STATE_NAME_PREFIX
@@ -519,7 +526,7 @@
                 .setConnectedDevices(DEVICE2, DEVICE1, DEVICE3)
                 .setMessageType(BluetoothRouteManager.CONNECT_BT)
                 .setMessageDevice(DEVICE3)
-                .setExpectedListenerUpdates(ListenerUpdate.AUDIO_CONNECTED)
+                .setExpectedListenerUpdates(ListenerUpdate.AUDIO_CONNECTING)
                 .setExpectedBluetoothInteraction(CONNECT_SWITCH_DEVICE)
                 .setExpectedConnectionDevice(DEVICE3)
                 .setExpectedFinalStateName(BluetoothRouteManager.AUDIO_CONNECTING_STATE_NAME_PREFIX
diff --git a/tests/src/com/android/server/telecom/tests/CallAnomalyWatchdogTest.java b/tests/src/com/android/server/telecom/tests/CallAnomalyWatchdogTest.java
index 7e197fe..86d24f9 100644
--- a/tests/src/com/android/server/telecom/tests/CallAnomalyWatchdogTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAnomalyWatchdogTest.java
@@ -27,6 +27,7 @@
 
 import android.content.ComponentName;
 import android.net.Uri;
+import android.os.UserHandle;
 import android.telecom.DisconnectCause;
 import android.telecom.PhoneAccount;
 import android.telecom.PhoneAccountHandle;
@@ -123,6 +124,7 @@
         mCallAnomalyWatchdog = new CallAnomalyWatchdog(mTestScheduledExecutorService, mLock,
                 mTimeouts, mMockClockProxy, mMockEmergencyCallDiagnosticLogger);
         mCallAnomalyWatchdog.setAnomalyReporterAdapter(mAnomalyReporterAdapter);
+        when(mMockCallsManager.getCurrentUserHandle()).thenReturn(UserHandle.CURRENT);
     }
 
     @Override
@@ -862,6 +864,7 @@
                 false /* shouldAttachToExistingConnection*/,
                 false /* isConference */,
                 mMockClockProxy,
-                mMockToastProxy);
+                mMockToastProxy,
+                mFeatureFlags);
     }
 }
\ No newline at end of file
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioManagerTest.java b/tests/src/com/android/server/telecom/tests/CallAudioManagerTest.java
index 3d06ad0..1d641ba 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioManagerTest.java
@@ -16,26 +16,49 @@
 
 package com.android.server.telecom.tests;
 
+import static com.android.server.telecom.tests.TelecomSystemTest.TEST_TIMEOUT;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
 import android.media.ToneGenerator;
+import android.os.Handler;
+import android.os.Looper;
 import android.telecom.DisconnectCause;
-import android.test.suitebuilder.annotation.MediumTest;
-import android.test.suitebuilder.annotation.SmallTest;
 import android.util.SparseArray;
 
+import androidx.test.filters.MediumTest;
+import androidx.test.filters.SmallTest;
+
 import com.android.server.telecom.Call;
+import com.android.server.telecom.CallAudioManager;
 import com.android.server.telecom.CallAudioModeStateMachine;
 import com.android.server.telecom.CallAudioModeStateMachine.MessageArgs;
+import com.android.server.telecom.CallAudioModeStateMachine.MessageArgs.Builder;
 import com.android.server.telecom.CallAudioRouteStateMachine;
 import com.android.server.telecom.CallState;
 import com.android.server.telecom.CallsManager;
-import com.android.server.telecom.CallAudioManager;
 import com.android.server.telecom.DtmfLocalTonePlayer;
 import com.android.server.telecom.InCallTonePlayer;
-import com.android.server.telecom.CallAudioModeStateMachine.MessageArgs.Builder;
 import com.android.server.telecom.RingbackPlayer;
 import com.android.server.telecom.Ringer;
 import com.android.server.telecom.TelecomSystem;
 import com.android.server.telecom.bluetooth.BluetoothStateReceiver;
+import com.android.server.telecom.flags.FeatureFlags;
 
 import org.junit.After;
 import org.junit.Before;
@@ -48,24 +71,9 @@
 import java.util.Arrays;
 import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.concurrent.CompletableFuture;
 import java.util.stream.Collectors;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.atLeastOnce;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
 @RunWith(JUnit4.class)
 public class CallAudioManagerTest extends TelecomTestCase {
     @Mock private CallAudioRouteStateMachine mCallAudioRouteStateMachine;
@@ -78,6 +86,8 @@
     @Mock private BluetoothStateReceiver mBluetoothStateReceiver;
     @Mock private TelecomSystem.SyncRoot mLock;
 
+    @Mock private FeatureFlags mFlags;
+
     private CallAudioManager mCallAudioManager;
 
     @Override
@@ -87,12 +97,13 @@
         doAnswer((invocation) -> {
             InCallTonePlayer mockInCallTonePlayer = mock(InCallTonePlayer.class);
             doAnswer((invocation2) -> {
-                mCallAudioManager.setIsTonePlaying(true);
+                mCallAudioManager.setIsTonePlaying(invocation.getArgument(0), true);
                 return true;
             }).when(mockInCallTonePlayer).startTone();
             return mockInCallTonePlayer;
-        }).when(mPlayerFactory).createPlayer(anyInt());
+        }).when(mPlayerFactory).createPlayer(any(Call.class), anyInt());
         when(mCallsManager.getLock()).thenReturn(mLock);
+        when(mFlags.ensureAudioModeUpdatesOnForegroundCallChange()).thenReturn(true);
         mCallAudioManager = new CallAudioManager(
                 mCallAudioRouteStateMachine,
                 mCallsManager,
@@ -101,7 +112,8 @@
                 mRinger,
                 mRingbackPlayer,
                 mBluetoothStateReceiver,
-                mDtmfLocalTonePlayer);
+                mDtmfLocalTonePlayer,
+                mFlags);
     }
 
     @Override
@@ -204,7 +216,7 @@
         assertMessageArgEquality(correctArgs, captor.getValue());
 
         disconnectCall(call);
-        stopTone();
+        stopTone(call);
 
         mCallAudioManager.onCallRemoved(call);
         verifyProperCleanup();
@@ -241,7 +253,7 @@
         mCallAudioManager.onCallStateChanged(call, CallState.ANSWERED, CallState.ACTIVE);
 
         disconnectCall(call);
-        stopTone();
+        stopTone(call);
 
         mCallAudioManager.onCallRemoved(call);
         verifyProperCleanup();
@@ -277,25 +289,40 @@
         verify(mCallAudioModeStateMachine, times(2)).sendMessageWithArgs(
                 eq(CallAudioModeStateMachine.NEW_ACTIVE_OR_DIALING_CALL), captor.capture());
         assertMessageArgEquality(expectedArgs, captor.getValue());
-        verify(mCallAudioModeStateMachine, times(2)).sendMessageWithArgs(
-                anyInt(), any(CallAudioModeStateMachine.MessageArgs.class));
-
+        if (mFlags.ensureAudioModeUpdatesOnForegroundCallChange()) {
+            // Expect another invocation due to audio mode change signal.
+            verify(mCallAudioModeStateMachine, times(3)).sendMessageWithArgs(
+                    anyInt(), any(CallAudioModeStateMachine.MessageArgs.class));
+        } else {
+            verify(mCallAudioModeStateMachine, times(2)).sendMessageWithArgs(
+                    anyInt(), any(CallAudioModeStateMachine.MessageArgs.class));
+        }
 
         when(call.getState()).thenReturn(CallState.ACTIVE);
         mCallAudioManager.onCallStateChanged(call, CallState.DIALING, CallState.ACTIVE);
         verify(mCallAudioModeStateMachine, times(3)).sendMessageWithArgs(
                 eq(CallAudioModeStateMachine.NEW_ACTIVE_OR_DIALING_CALL), captor.capture());
         assertMessageArgEquality(expectedArgs, captor.getValue());
-        verify(mCallAudioModeStateMachine, times(3)).sendMessageWithArgs(
-                anyInt(), any(CallAudioModeStateMachine.MessageArgs.class));
-
+        if (mFlags.ensureAudioModeUpdatesOnForegroundCallChange()) {
+            verify(mCallAudioModeStateMachine, times(4)).sendMessageWithArgs(
+                    anyInt(), any(CallAudioModeStateMachine.MessageArgs.class));
+        } else {
+            verify(mCallAudioModeStateMachine, times(3)).sendMessageWithArgs(
+                    anyInt(), any(CallAudioModeStateMachine.MessageArgs.class));
+        }
         disconnectCall(call);
-        stopTone();
+        stopTone(call);
 
         mCallAudioManager.onCallRemoved(call);
         verifyProperCleanup();
     }
 
+    @Test
+    public void testSingleOutgoingCallWithoutAudioModeUpdateOnForegroundCallChange() {
+        when(mFlags.ensureAudioModeUpdatesOnForegroundCallChange()).thenReturn(false);
+        testSingleOutgoingCall();
+    }
+
     @MediumTest
     @Test
     public void testRingbackStartStop() {
@@ -327,8 +354,14 @@
         verify(mCallAudioModeStateMachine, times(2)).sendMessageWithArgs(
                 eq(CallAudioModeStateMachine.NEW_ACTIVE_OR_DIALING_CALL), captor.capture());
         assertMessageArgEquality(expectedArgs, captor.getValue());
-        verify(mCallAudioModeStateMachine, times(2)).sendMessageWithArgs(
-                anyInt(), any(CallAudioModeStateMachine.MessageArgs.class));
+        if (mFlags.ensureAudioModeUpdatesOnForegroundCallChange()) {
+            // Expect an extra time due to audio mode change signal
+            verify(mCallAudioModeStateMachine, times(3)).sendMessageWithArgs(
+                    anyInt(), any(CallAudioModeStateMachine.MessageArgs.class));
+        } else {
+            verify(mCallAudioModeStateMachine, times(2)).sendMessageWithArgs(
+                    anyInt(), any(CallAudioModeStateMachine.MessageArgs.class));
+        }
 
         // Ensure we started ringback.
         verify(mRingbackPlayer).startRingbackForCall(any(Call.class));
@@ -350,6 +383,12 @@
         verify(mRingbackPlayer, times(1)).startRingbackForCall(any(Call.class));
     }
 
+    @Test
+    public void testRingbackStartStopWithoutAudioModeUpdateOnForegroundCallChange() {
+        when(mFlags.ensureAudioModeUpdatesOnForegroundCallChange()).thenReturn(false);
+        testRingbackStartStop();
+    }
+
     @SmallTest
     @Test
     public void testNewCallGoesToAudioProcessing() {
@@ -389,9 +428,12 @@
         Call call = mock(Call.class);
         ArgumentCaptor<CallAudioModeStateMachine.MessageArgs> captor = makeNewCaptor();
         when(call.getState()).thenReturn(CallState.RINGING);
+        handleWaitForBtIcsBinding(call);
 
         // Make sure appropriate messages are sent when we add a RINGING call
         mCallAudioManager.onCallAdded(call);
+        mCallAudioManager.getCallRingingFuture().join();
+        waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT);
 
         assertEquals(call, mCallAudioManager.getForegroundCall());
         verify(mCallAudioRouteStateMachine).sendMessageWithSessionInfo(
@@ -495,7 +537,7 @@
 
         mCallAudioManager.onCallStateChanged(call, CallState.AUDIO_PROCESSING,
                 CallState.DISCONNECTED);
-        verify(mPlayerFactory, never()).createPlayer(anyInt());
+        verify(mPlayerFactory, never()).createPlayer(any(Call.class), anyInt());
         CallAudioModeStateMachine.MessageArgs expectedArgs2 = new Builder()
                 .setHasActiveOrDialingCalls(false)
                 .setHasRingingCalls(false)
@@ -522,11 +564,15 @@
 
         Call call = createAudioProcessingCall();
 
+
         when(call.getState()).thenReturn(CallState.SIMULATED_RINGING);
+        handleWaitForBtIcsBinding(call);
 
         mCallAudioManager.onCallStateChanged(call, CallState.AUDIO_PROCESSING,
                 CallState.SIMULATED_RINGING);
-        verify(mPlayerFactory, never()).createPlayer(anyInt());
+        mCallAudioManager.getCallRingingFuture().join();
+        waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT);
+        verify(mPlayerFactory, never()).createPlayer(any(Call.class), anyInt());
         CallAudioModeStateMachine.MessageArgs expectedArgs = new Builder()
                 .setHasActiveOrDialingCalls(false)
                 .setHasRingingCalls(true)
@@ -555,7 +601,7 @@
 
         mCallAudioManager.onCallStateChanged(call, CallState.AUDIO_PROCESSING,
                 CallState.ACTIVE);
-        verify(mPlayerFactory, never()).createPlayer(anyInt());
+        verify(mPlayerFactory, never()).createPlayer(any(Call.class), anyInt());
         CallAudioModeStateMachine.MessageArgs expectedArgs = new Builder()
                 .setHasActiveOrDialingCalls(true)
                 .setHasRingingCalls(false)
@@ -584,7 +630,7 @@
 
         mCallAudioManager.onCallStateChanged(call, CallState.SIMULATED_RINGING,
                 CallState.ACTIVE);
-        verify(mPlayerFactory, never()).createPlayer(anyInt());
+        verify(mPlayerFactory, never()).createPlayer(any(Call.class), anyInt());
         CallAudioModeStateMachine.MessageArgs expectedArgs = new Builder()
                 .setHasActiveOrDialingCalls(true)
                 .setHasRingingCalls(false)
@@ -643,7 +689,7 @@
 
         mCallAudioManager.onCallStateChanged(call, CallState.SIMULATED_RINGING,
                 CallState.DISCONNECTED);
-        verify(mPlayerFactory, never()).createPlayer(anyInt());
+        verify(mPlayerFactory, never()).createPlayer(any(Call.class), anyInt());
         CallAudioModeStateMachine.MessageArgs expectedArgs2 = new Builder()
                 .setHasActiveOrDialingCalls(false)
                 .setHasRingingCalls(false)
@@ -702,12 +748,86 @@
         assertFalse(captor.getValue().isStreaming);
     }
 
+    @SmallTest
+    @Test
+    public void testTriggerAudioManagerModeChange() {
+        if (!mFlags.ensureAudioModeUpdatesOnForegroundCallChange()) {
+            // Skip if the new behavior isn't in use.
+            return;
+        }
+        // Start with an incoming PSTN call
+        Call pstnCall = mock(Call.class);
+        when(pstnCall.getState()).thenReturn(CallState.RINGING);
+        when(pstnCall.getIsVoipAudioMode()).thenReturn(false);
+        ArgumentCaptor<CallAudioModeStateMachine.MessageArgs> captor = makeNewCaptor();
+
+        // Add the call
+        mCallAudioManager.onCallAdded(pstnCall);
+        verify(mCallAudioModeStateMachine).sendMessageWithArgs(
+                eq(CallAudioModeStateMachine.FOREGROUND_VOIP_MODE_CHANGE), captor.capture());
+        CallAudioModeStateMachine.MessageArgs expectedArgs =
+                new Builder()
+                        .setHasActiveOrDialingCalls(false)
+                        .setHasRingingCalls(true)
+                        .setHasHoldingCalls(false)
+                        .setIsTonePlaying(false)
+                        .setHasAudioProcessingCalls(false)
+                        .setForegroundCallIsVoip(false)
+                        .setSession(null)
+                        .setForegroundCallIsVoip(false)
+                        .build();
+        assertMessageArgEquality(expectedArgs, captor.getValue());
+        clearInvocations(mCallAudioModeStateMachine); // Avoid verifying for previous calls
+
+        // Make call active; don't expect there to be an audio mode transition.
+        when(pstnCall.getState()).thenReturn(CallState.ACTIVE);
+        mCallAudioManager.onCallStateChanged(pstnCall, CallState.RINGING, CallState.ACTIVE);
+        verify(mCallAudioModeStateMachine, never()).sendMessageWithArgs(
+                eq(CallAudioModeStateMachine.FOREGROUND_VOIP_MODE_CHANGE),
+                any(CallAudioModeStateMachine.MessageArgs.class));
+        clearInvocations(mCallAudioModeStateMachine); // Avoid verifying for previous calls
+
+        // Add a new Voip call in ringing state; this should not result in a direct audio mode
+        // change.
+        Call voipCall = mock(Call.class);
+        when(voipCall.getState()).thenReturn(CallState.RINGING);
+        when(voipCall.getIsVoipAudioMode()).thenReturn(true);
+        mCallAudioManager.onCallAdded(voipCall);
+        verify(mCallAudioModeStateMachine, never()).sendMessageWithArgs(
+                eq(CallAudioModeStateMachine.FOREGROUND_VOIP_MODE_CHANGE),
+                any(CallAudioModeStateMachine.MessageArgs.class));
+        clearInvocations(mCallAudioModeStateMachine); // Avoid verifying for previous calls
+
+        // Make voip call active and set the PSTN call to locally disconnecting; the new foreground
+        // call will be the voip call.
+        when(pstnCall.isLocallyDisconnecting()).thenReturn(true);
+        when(voipCall.getState()).thenReturn(CallState.ACTIVE);
+        mCallAudioManager.onCallStateChanged(voipCall, CallState.RINGING, CallState.ACTIVE);
+        verify(mCallAudioModeStateMachine).sendMessageWithArgs(
+                eq(CallAudioModeStateMachine.FOREGROUND_VOIP_MODE_CHANGE), captor.capture());
+        CallAudioModeStateMachine.MessageArgs expectedArgs2 =
+                new Builder()
+                        .setHasActiveOrDialingCalls(true)
+                        .setHasRingingCalls(false)
+                        .setHasHoldingCalls(false)
+                        .setIsTonePlaying(false)
+                        .setHasAudioProcessingCalls(false)
+                        .setForegroundCallIsVoip(false)
+                        .setSession(null)
+                        .setForegroundCallIsVoip(true)
+                        .build();
+        assertMessageArgEquality(expectedArgs2, captor.getValue());
+    }
+
     private Call createSimulatedRingingCall() {
         Call call = mock(Call.class);
         when(call.getState()).thenReturn(CallState.SIMULATED_RINGING);
+        handleWaitForBtIcsBinding(call);
         ArgumentCaptor<CallAudioModeStateMachine.MessageArgs> captor = makeNewCaptor();
 
         mCallAudioManager.onCallAdded(call);
+        mCallAudioManager.getCallRingingFuture().join();
+        waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT);
 
         assertEquals(call, mCallAudioManager.getForegroundCall());
 
@@ -733,8 +853,11 @@
     private Call createIncomingCall() {
         Call call = mock(Call.class);
         when(call.getState()).thenReturn(CallState.RINGING);
+        handleWaitForBtIcsBinding(call);
 
         mCallAudioManager.onCallAdded(call);
+        mCallAudioManager.getCallRingingFuture().join();
+        waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT);
         assertEquals(call, mCallAudioManager.getForegroundCall());
         ArgumentCaptor<CallAudioModeStateMachine.MessageArgs> captor =
                 ArgumentCaptor.forClass(CallAudioModeStateMachine.MessageArgs.class);
@@ -765,7 +888,7 @@
                 "", "", "", ToneGenerator.TONE_PROP_PROMPT));
 
         mCallAudioManager.onCallStateChanged(call, CallState.ACTIVE, CallState.DISCONNECTED);
-        verify(mPlayerFactory).createPlayer(InCallTonePlayer.TONE_CALL_ENDED);
+        verify(mPlayerFactory).createPlayer(any(Call.class), eq(InCallTonePlayer.TONE_CALL_ENDED));
         correctArgs = new Builder()
                 .setHasActiveOrDialingCalls(false)
                 .setHasRingingCalls(false)
@@ -782,10 +905,10 @@
         assertMessageArgEquality(correctArgs, captor.getValue());
     }
 
-    private void stopTone() {
+    private void stopTone(Call call) {
         ArgumentCaptor<CallAudioModeStateMachine.MessageArgs> captor =
                 ArgumentCaptor.forClass(CallAudioModeStateMachine.MessageArgs.class);
-        mCallAudioManager.setIsTonePlaying(false);
+        mCallAudioManager.setIsTonePlaying(call, false);
         CallAudioModeStateMachine.MessageArgs correctArgs = new Builder()
                         .setHasActiveOrDialingCalls(false)
                         .setHasRingingCalls(false)
@@ -819,4 +942,10 @@
         assertEquals(expected.isTonePlaying, actual.isTonePlaying);
         assertEquals(expected.foregroundCallIsVoip, actual.foregroundCallIsVoip);
     }
+
+    private void handleWaitForBtIcsBinding(Call call) {
+        when(mFlags.separatelyBindToBtIncallService()).thenReturn(true);
+        CompletableFuture<Boolean> btBindingFuture = CompletableFuture.completedFuture(true);
+        when(call.getBtIcsFuture()).thenReturn(btBindingFuture);
+    }
 }
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioModeStateMachineTest.java b/tests/src/com/android/server/telecom/tests/CallAudioModeStateMachineTest.java
index d7854a5..4513c65 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioModeStateMachineTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioModeStateMachineTest.java
@@ -16,14 +16,32 @@
 
 package com.android.server.telecom.tests;
 
+import static com.android.server.telecom.CallAudioModeStateMachine.CALL_AUDIO_FOCUS_REQUEST;
+import static com.android.server.telecom.CallAudioModeStateMachine.RING_AUDIO_FOCUS_REQUEST;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.media.AudioFocusRequest;
 import android.media.AudioManager;
 import android.os.HandlerThread;
-import android.test.suitebuilder.annotation.SmallTest;
 
+import androidx.test.filters.SmallTest;
+
+import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
 import com.android.server.telecom.CallAudioManager;
 import com.android.server.telecom.CallAudioModeStateMachine;
-import com.android.server.telecom.CallAudioRouteStateMachine;
 import com.android.server.telecom.CallAudioModeStateMachine.MessageArgs.Builder;
+import com.android.server.telecom.CallAudioRouteStateMachine;
 import com.android.server.telecom.SystemStateHelper;
 
 import org.junit.After;
@@ -31,18 +49,9 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 
-import static org.junit.Assert.assertEquals;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Mockito.clearInvocations;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
 @RunWith(JUnit4.class)
 public class CallAudioModeStateMachineTest extends TelecomTestCase {
     private static final int TEST_TIMEOUT = 1000;
@@ -51,6 +60,7 @@
     @Mock private AudioManager mAudioManager;
     @Mock private CallAudioManager mCallAudioManager;
     @Mock private CallAudioRouteStateMachine mCallAudioRouteStateMachine;
+    @Mock private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
 
     private HandlerThread mTestThread;
 
@@ -60,8 +70,9 @@
         mTestThread = new HandlerThread("CallAudioModeStateMachineTest");
         mTestThread.start();
         super.setUp();
-        when(mCallAudioManager.getCallAudioRouteStateMachine())
+        when(mCallAudioManager.getCallAudioRouteAdapter())
                 .thenReturn(mCallAudioRouteStateMachine);
+        when(mFeatureFlags.telecomResolveHiddenDependencies()).thenReturn(false);
     }
 
     @Override
@@ -76,7 +87,7 @@
     @Test
     public void testNoFocusWhenRingerSilenced() throws Throwable {
         CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mSystemStateHelper,
-                mAudioManager, mTestThread.getLooper());
+                mAudioManager, mTestThread.getLooper(), mFeatureFlags, mCommunicationDeviceTracker);
         sm.setCallAudioManager(mCallAudioManager);
         sm.sendMessage(CallAudioModeStateMachine.ABANDON_FOCUS_FOR_TESTING);
         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
@@ -108,7 +119,7 @@
     @Test
     public void testSwitchToStreamingMode() {
         CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mSystemStateHelper,
-                mAudioManager, mTestThread.getLooper());
+                mAudioManager, mTestThread.getLooper(), mFeatureFlags, mCommunicationDeviceTracker);
         sm.setCallAudioManager(mCallAudioManager);
         sm.sendMessage(CallAudioModeStateMachine.ABANDON_FOCUS_FOR_TESTING);
         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
@@ -138,7 +149,7 @@
     @Test
     public void testExitStreamingMode() {
         CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mSystemStateHelper,
-                mAudioManager, mTestThread.getLooper());
+                mAudioManager, mTestThread.getLooper(), mFeatureFlags, mCommunicationDeviceTracker);
         sm.setCallAudioManager(mCallAudioManager);
         sm.sendMessage(CallAudioModeStateMachine.ENTER_STREAMING_FOCUS_FOR_TESTING);
         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
@@ -166,7 +177,7 @@
     @Test
     public void testNoRingWhenDeviceIsAtEar() {
         CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mSystemStateHelper,
-                mAudioManager, mTestThread.getLooper());
+                mAudioManager, mTestThread.getLooper(), mFeatureFlags, mCommunicationDeviceTracker);
         sm.setCallAudioManager(mCallAudioManager);
         sm.sendMessage(CallAudioModeStateMachine.ABANDON_FOCUS_FOR_TESTING);
         sm.sendMessage(CallAudioModeStateMachine.NEW_HOLDING_CALL, new Builder()
@@ -202,7 +213,7 @@
     @Test
     public void testRegainFocusWhenHfpIsConnectedSilenced() throws Throwable {
         CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mSystemStateHelper,
-                mAudioManager, mTestThread.getLooper());
+                mAudioManager, mTestThread.getLooper(), mFeatureFlags, mCommunicationDeviceTracker);
         sm.setCallAudioManager(mCallAudioManager);
         sm.sendMessage(CallAudioModeStateMachine.ABANDON_FOCUS_FOR_TESTING);
         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
@@ -246,7 +257,7 @@
     @Test
     public void testDoNotRingTwiceWhenHfpConnected() {
         CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mSystemStateHelper,
-                mAudioManager, mTestThread.getLooper());
+                mAudioManager, mTestThread.getLooper(), mFeatureFlags, mCommunicationDeviceTracker);
         sm.setCallAudioManager(mCallAudioManager);
         sm.sendMessage(CallAudioModeStateMachine.ABANDON_FOCUS_FOR_TESTING);
         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
@@ -284,7 +295,7 @@
     @Test
     public void testStartRingingAfterHfpConnectedIfNotAlreadyPlaying() {
         CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mSystemStateHelper,
-                mAudioManager, mTestThread.getLooper());
+                mAudioManager, mTestThread.getLooper(), mFeatureFlags, mCommunicationDeviceTracker);
         sm.setCallAudioManager(mCallAudioManager);
         sm.sendMessage(CallAudioModeStateMachine.ABANDON_FOCUS_FOR_TESTING);
         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
@@ -318,7 +329,46 @@
         verify(mCallAudioManager, times(2)).startRinging();
     }
 
+    @SmallTest
+    @Test
+    public void testAudioFocusRequestWithResolveHiddenDependencies() {
+        CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mSystemStateHelper,
+                mAudioManager, mTestThread.getLooper(), mFeatureFlags, mCommunicationDeviceTracker);
+        when(mFeatureFlags.telecomResolveHiddenDependencies()).thenReturn(true);
+        ArgumentCaptor<AudioFocusRequest> captor = ArgumentCaptor.forClass(AudioFocusRequest.class);
+        sm.setCallAudioManager(mCallAudioManager);
+
+        resetMocks();
+        when(mCallAudioManager.startRinging()).thenReturn(true);
+        when(mCallAudioManager.isRingtonePlaying()).thenReturn(false);
+
+        sm.sendMessage(CallAudioModeStateMachine.ENTER_RING_FOCUS_FOR_TESTING);
+        waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
+        verify(mAudioManager).requestAudioFocus(captor.capture());
+        assertTrue(areAudioFocusRequestsMatch(captor.getValue(), RING_AUDIO_FOCUS_REQUEST));
+
+        sm.sendMessage(CallAudioModeStateMachine.ENTER_CALL_FOCUS_FOR_TESTING);
+        waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
+        verify(mAudioManager, atLeast(1)).requestAudioFocus(captor.capture());
+        AudioFocusRequest request = captor.getValue();
+        assertTrue(areAudioFocusRequestsMatch(request, CALL_AUDIO_FOCUS_REQUEST));
+
+        sm.sendMessage(CallAudioModeStateMachine.ABANDON_FOCUS_FOR_TESTING);
+    }
+
     private void resetMocks() {
         clearInvocations(mCallAudioManager, mAudioManager);
     }
+
+    private boolean areAudioFocusRequestsMatch(AudioFocusRequest r1, AudioFocusRequest r2) {
+        if ((r1 == null) || (r2 == null)) {
+            return false;
+        }
+
+        if (r1.getFocusGain() != r2.getFocusGain()) {
+            return false;
+        }
+
+        return r1.getAudioAttributes().equals(r2.getAudioAttributes());
+    }
 }
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioModeTransitionTests.java b/tests/src/com/android/server/telecom/tests/CallAudioModeTransitionTests.java
index c7e5aa9..844a216 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioModeTransitionTests.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioModeTransitionTests.java
@@ -16,10 +16,22 @@
 
 package com.android.server.telecom.tests;
 
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
 import android.media.AudioManager;
 import android.os.HandlerThread;
-import android.test.suitebuilder.annotation.SmallTest;
 
+import androidx.test.filters.SmallTest;
+
+import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
 import com.android.server.telecom.CallAudioManager;
 import com.android.server.telecom.CallAudioModeStateMachine;
 import com.android.server.telecom.CallAudioModeStateMachine.MessageArgs;
@@ -36,15 +48,6 @@
 import java.util.Collection;
 import java.util.List;
 
-import static org.junit.Assert.assertEquals;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
 @RunWith(Parameterized.class)
 public class CallAudioModeTransitionTests extends TelecomTestCase {
     private static class ModeTestParameters {
@@ -103,6 +106,7 @@
     @Mock private SystemStateHelper mSystemStateHelper;
     @Mock private AudioManager mAudioManager;
     @Mock private CallAudioManager mCallAudioManager;
+    @Mock private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
     private final ModeTestParameters mParams;
     private HandlerThread mTestThread;
 
@@ -130,13 +134,14 @@
     @SmallTest
     public void modeTransitionTest() {
         CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mSystemStateHelper,
-                mAudioManager, mTestThread.getLooper());
+                mAudioManager, mTestThread.getLooper(), mFeatureFlags, mCommunicationDeviceTracker);
         sm.setCallAudioManager(mCallAudioManager);
         sm.sendMessage(mParams.initialAudioState);
         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
 
         resetMocks();
         when(mCallAudioManager.startRinging()).thenReturn(true);
+        when(mFeatureFlags.telecomResolveHiddenDependencies()).thenReturn(false);
         if (mParams.initialAudioState
                 == CallAudioModeStateMachine.ENTER_AUDIO_PROCESSING_FOCUS_FOR_TESTING) {
             when(mAudioManager.getMode())
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..59473bd
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
@@ -0,0 +1,776 @@
+/*
+ * 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.ACTIVE_FOCUS;
+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 com.android.server.telecom.CallAudioRouteAdapter.CONNECT_DOCK;
+import static com.android.server.telecom.CallAudioRouteAdapter.CONNECT_WIRED_HEADSET;
+import static com.android.server.telecom.CallAudioRouteAdapter.DISCONNECT_DOCK;
+import static com.android.server.telecom.CallAudioRouteAdapter.DISCONNECT_WIRED_HEADSET;
+import static com.android.server.telecom.CallAudioRouteAdapter.MUTE_OFF;
+import static com.android.server.telecom.CallAudioRouteAdapter.MUTE_ON;
+import static com.android.server.telecom.CallAudioRouteAdapter.NO_FOCUS;
+import static com.android.server.telecom.CallAudioRouteAdapter.RINGING_FOCUS;
+import static com.android.server.telecom.CallAudioRouteAdapter.SPEAKER_OFF;
+import static com.android.server.telecom.CallAudioRouteAdapter.SPEAKER_ON;
+import static com.android.server.telecom.CallAudioRouteAdapter.STREAMING_FORCE_DISABLED;
+import static com.android.server.telecom.CallAudioRouteAdapter.STREAMING_FORCE_ENABLED;
+import static com.android.server.telecom.CallAudioRouteAdapter.SWITCH_BASELINE_ROUTE;
+import static com.android.server.telecom.CallAudioRouteAdapter.SWITCH_FOCUS;
+import static com.android.server.telecom.CallAudioRouteAdapter.USER_SWITCH_BLUETOOTH;
+import static com.android.server.telecom.CallAudioRouteAdapter.USER_SWITCH_EARPIECE;
+import static com.android.server.telecom.CallAudioRouteAdapter.USER_SWITCH_HEADSET;
+import static com.android.server.telecom.CallAudioRouteAdapter.USER_SWITCH_SPEAKER;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+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.mock;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeAudio;
+import android.media.AudioDeviceInfo;
+import android.media.AudioManager;
+import android.media.IAudioService;
+import android.media.audiopolicy.AudioProductStrategy;
+import android.os.UserHandle;
+import android.telecom.CallAudioState;
+import android.telecom.VideoProfile;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.server.telecom.AudioRoute;
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallAudioManager;
+import com.android.server.telecom.CallAudioRouteController;
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.PendingAudioRoute;
+import com.android.server.telecom.StatusBarNotifier;
+import com.android.server.telecom.TelecomSystem;
+import com.android.server.telecom.WiredHeadsetManager;
+import com.android.server.telecom.bluetooth.BluetoothDeviceManager;
+import com.android.server.telecom.bluetooth.BluetoothRouteManager;
+
+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.List;
+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;
+    @Mock CallAudioManager.AudioServiceFactory mAudioServiceFactory;
+    @Mock IAudioService mAudioService;
+    @Mock BluetoothRouteManager mBluetoothRouteManager;
+    @Mock BluetoothDeviceManager mBluetoothDeviceManager;
+    @Mock BluetoothAdapter mBluetoothAdapter;
+    @Mock StatusBarNotifier mockStatusBarNotifier;
+    @Mock AudioDeviceInfo mAudioDeviceInfo;
+    @Mock BluetoothLeAudio mBluetoothLeAudio;
+    @Mock CallAudioManager mCallAudioManager;
+    @Mock Call mCall;
+    @Mock private TelecomSystem.SyncRoot mLock;
+    private AudioRoute mEarpieceRoute;
+    private AudioRoute mSpeakerRoute;
+    private static final String BT_ADDRESS_1 = "00:00:00:00:00:01";
+    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, mAudioDeviceInfo);
+        }
+    };
+
+    @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
+                });
+        when(mAudioManager.getPreferredDeviceForStrategy(nullable(AudioProductStrategy.class)))
+                .thenReturn(null);
+        when(mAudioManager.getAvailableCommunicationDevices())
+                .thenReturn(List.of(mAudioDeviceInfo));
+        when(mAudioManager.getCommunicationDevice()).thenReturn(mAudioDeviceInfo);
+        when(mAudioManager.setCommunicationDevice(any(AudioDeviceInfo.class)))
+                .thenReturn(true);
+        when(mAudioServiceFactory.getAudioService()).thenReturn(mAudioService);
+        when(mContext.getAttributionTag()).thenReturn("");
+        doNothing().when(mCallsManager).onCallAudioStateChanged(any(CallAudioState.class),
+                any(CallAudioState.class));
+        when(mCallsManager.getCurrentUserHandle()).thenReturn(
+                new UserHandle(UserHandle.USER_SYSTEM));
+        when(mCallsManager.getLock()).thenReturn(mLock);
+        when(mBluetoothRouteManager.getDeviceManager()).thenReturn(mBluetoothDeviceManager);
+        when(mBluetoothDeviceManager.connectAudio(any(BluetoothDevice.class), anyInt()))
+                .thenReturn(true);
+        when(mBluetoothDeviceManager.getBluetoothAdapter()).thenReturn(mBluetoothAdapter);
+        when(mBluetoothAdapter.getActiveDevices(anyInt())).thenReturn(List.of(BLUETOOTH_DEVICE_1));
+        when(mBluetoothDeviceManager.getLeAudioService()).thenReturn(mBluetoothLeAudio);
+        when(mBluetoothLeAudio.getGroupId(any(BluetoothDevice.class))).thenReturn(1);
+        when(mBluetoothLeAudio.getConnectedGroupLeadDevice(anyInt()))
+                .thenReturn(BLUETOOTH_DEVICE_1);
+        when(mAudioDeviceInfo.getAddress()).thenReturn(BT_ADDRESS_1);
+        mController = new CallAudioRouteController(mContext, mCallsManager, mAudioServiceFactory,
+                mAudioRouteFactory, mWiredHeadsetManager,
+                mBluetoothRouteManager, mockStatusBarNotifier, mFeatureFlags);
+        mController.setAudioRouteFactory(mAudioRouteFactory);
+        mController.setAudioManager(mAudioManager);
+        mEarpieceRoute = new AudioRoute(AudioRoute.TYPE_EARPIECE, null, null);
+        mSpeakerRoute = new AudioRoute(AudioRoute.TYPE_SPEAKER, null, null);
+        mController.setCallAudioManager(mCallAudioManager);
+        when(mCallAudioManager.getForegroundCall()).thenReturn(mCall);
+        when(mCall.getVideoState()).thenReturn(VideoProfile.STATE_AUDIO_ONLY);
+        when(mFeatureFlags.ignoreAutoRouteToWatchDevice()).thenReturn(false);
+    }
+
+    @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 testInitializeWithWiredHeadset() {
+        AudioRoute wiredHeadsetRoute = new AudioRoute(AudioRoute.TYPE_WIRED, null, null);
+        when(mWiredHeadsetManager.isPluggedIn()).thenReturn(true);
+        mController.initialize();
+        assertEquals(wiredHeadsetRoute, mController.getCurrentRoute());
+        assertEquals(2, mController.getAvailableRoutes().size());
+        assertTrue(mController.getAvailableRoutes().contains(mSpeakerRoute));
+    }
+
+    @SmallTest
+    @Test
+    public void testNormalCallRouteToEarpiece() {
+        mController.initialize();
+        mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS);
+        // Verify that pending audio destination route is set to speaker. This will trigger pending
+        // message to wait for SPEAKER_ON message once communication device is set before routing.
+        waitForHandlerAction(mController.getAdapterHandler(), TEST_TIMEOUT);
+        PendingAudioRoute pendingRoute = mController.getPendingAudioRoute();
+        assertEquals(AudioRoute.TYPE_EARPIECE, pendingRoute.getDestRoute().getType());
+
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+    }
+
+    @SmallTest
+    @Test
+    public void testVideoCallHoldRouteToEarpiece() {
+        mController.initialize();
+        mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS);
+        // Verify that pending audio destination route is not defaulted to speaker when a video call
+        // is not the foreground call.
+        waitForHandlerAction(mController.getAdapterHandler(), TEST_TIMEOUT);
+        PendingAudioRoute pendingRoute = mController.getPendingAudioRoute();
+        assertEquals(AudioRoute.TYPE_EARPIECE, pendingRoute.getDestRoute().getType());
+    }
+
+    @SmallTest
+    @Test
+    public void testVideoCallRouteToSpeaker() {
+        when(mCall.getVideoState()).thenReturn(VideoProfile.STATE_BIDIRECTIONAL);
+        mController.initialize();
+        mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS);
+        // Verify that pending audio destination route is set to speaker. This will trigger pending
+        // message to wait for SPEAKER_ON message once communication device is set before routing.
+        waitForHandlerAction(mController.getAdapterHandler(), TEST_TIMEOUT);
+        PendingAudioRoute pendingRoute = mController.getPendingAudioRoute();
+        assertEquals(AudioRoute.TYPE_SPEAKER, pendingRoute.getDestRoute().getType());
+
+        // Mock SPEAKER_ON message received by controller.
+        mController.sendMessageWithSessionInfo(SPEAKER_ON);
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        // Verify that audio is routed to wired headset if it's present.
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_WIRED_HEADSET,
+                CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        mController.sendMessageWithSessionInfo(CONNECT_WIRED_HEADSET);
+        waitForHandlerAction(mController.getAdapterHandler(), TEST_TIMEOUT);
+        mController.sendMessageWithSessionInfo(SPEAKER_OFF);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+    }
+
+    @SmallTest
+    @Test
+    public void testActiveDeactivateBluetoothDevice() {
+        mController.initialize();
+        mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
+                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 testSwitchFocusForBluetoothDeviceSupportInbandRinging() {
+        when(mBluetoothRouteManager.isInbandRingEnabled(eq(BLUETOOTH_DEVICE_1))).thenReturn(true);
+
+        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));
+        assertFalse(mController.isActive());
+
+        mController.sendMessageWithSessionInfo(SWITCH_FOCUS, RINGING_FOCUS);
+        verify(mBluetoothDeviceManager, timeout(TEST_TIMEOUT))
+                .connectAudio(BLUETOOTH_DEVICE_1, AudioRoute.TYPE_BLUETOOTH_SCO);
+        assertTrue(mController.isActive());
+
+        mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS);
+        assertTrue(mController.isActive());
+
+        mController.sendMessageWithSessionInfo(SWITCH_FOCUS, NO_FOCUS);
+        verify(mBluetoothDeviceManager, timeout(TEST_TIMEOUT).atLeastOnce()).disconnectSco();
+        assertFalse(mController.isActive());
+    }
+
+    @SmallTest
+    @Test
+    public void testConnectAndDisconnectWiredHeadset() {
+        mController.initialize();
+        mController.sendMessageWithSessionInfo(CONNECT_WIRED_HEADSET);
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_WIRED_HEADSET,
+                CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        mController.sendMessageWithSessionInfo(DISCONNECT_WIRED_HEADSET);
+        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 testConnectAndDisconnectDock() {
+        mController.initialize();
+        mController.sendMessageWithSessionInfo(CONNECT_DOCK);
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        mController.sendMessageWithSessionInfo(DISCONNECT_DOCK);
+        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 testSpeakerToggle() {
+        mController.initialize();
+        mController.setActive(true);
+        mController.sendMessageWithSessionInfo(SPEAKER_ON);
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        mController.sendMessageWithSessionInfo(SPEAKER_OFF);
+        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 testSpeakerToggleWhenDockConnected() {
+        mController.initialize();
+        mController.setActive(true);
+        mController.sendMessageWithSessionInfo(CONNECT_DOCK);
+        mController.sendMessageWithSessionInfo(SPEAKER_ON);
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        mController.sendMessageWithSessionInfo(SPEAKER_ON);
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        mController.sendMessageWithSessionInfo(SPEAKER_OFF);
+        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 testSwitchEarpiece() {
+        mController.initialize();
+        mController.sendMessageWithSessionInfo(SPEAKER_ON);
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        mController.sendMessageWithSessionInfo(USER_SWITCH_EARPIECE);
+        mController.sendMessageWithSessionInfo(SPEAKER_OFF);
+        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 testSwitchBluetooth() {
+        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(USER_SWITCH_BLUETOOTH, 0,
+                BLUETOOTH_DEVICE_1.getAddress());
+        mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0, BLUETOOTH_DEVICE_1);
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+    }
+
+    @SmallTest
+    @Test
+    public void tesetSwitchSpeakerAndHeadset() {
+        mController.initialize();
+        mController.sendMessageWithSessionInfo(CONNECT_WIRED_HEADSET);
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_WIRED_HEADSET,
+                CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        mController.sendMessageWithSessionInfo(USER_SWITCH_SPEAKER);
+        mController.sendMessageWithSessionInfo(SPEAKER_ON);
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+                CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        mController.sendMessageWithSessionInfo(USER_SWITCH_HEADSET);
+        mController.sendMessageWithSessionInfo(SPEAKER_OFF);
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_WIRED_HEADSET,
+                CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+    }
+
+    @SmallTest
+    @Test
+    public void testEnableAndDisableStreaming() {
+        mController.initialize();
+        mController.sendMessageWithSessionInfo(STREAMING_FORCE_ENABLED);
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_STREAMING,
+                CallAudioState.ROUTE_STREAMING, null, new HashSet<>());
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        mController.sendMessageWithSessionInfo(SPEAKER_ON);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        mController.sendMessageWithSessionInfo(CONNECT_WIRED_HEADSET);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        mController.sendMessageWithSessionInfo(STREAMING_FORCE_DISABLED);
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_WIRED_HEADSET,
+                CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+    }
+
+    @SmallTest
+    @Test
+    public void testToggleMute() throws Exception {
+        when(mAudioManager.isMicrophoneMute()).thenReturn(false);
+        mController.initialize();
+        mController.setActive(true);
+
+        mController.sendMessageWithSessionInfo(MUTE_ON);
+        CallAudioState expectedState = new CallAudioState(true, CallAudioState.ROUTE_EARPIECE,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        verify(mAudioService, timeout(TEST_TIMEOUT)).setMicrophoneMute(eq(true), anyString(),
+                anyInt(), anyString());
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        when(mAudioManager.isMicrophoneMute()).thenReturn(true);
+        mController.sendMessageWithSessionInfo(MUTE_OFF);
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        verify(mAudioService, timeout(TEST_TIMEOUT)).setMicrophoneMute(eq(false), anyString(),
+                anyInt(), anyString());
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+    }
+
+    @SmallTest
+    @Test
+    public void testMuteOffAfterCallEnds() throws Exception {
+        when(mAudioManager.isMicrophoneMute()).thenReturn(false);
+        mController.initialize();
+        mController.setActive(true);
+
+        mController.sendMessageWithSessionInfo(MUTE_ON);
+        CallAudioState expectedState = new CallAudioState(true, CallAudioState.ROUTE_EARPIECE,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        verify(mAudioService, timeout(TEST_TIMEOUT)).setMicrophoneMute(eq(true), anyString(),
+                anyInt(), anyString());
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        // Switch to NO_FOCUS to indicate call termination and verify mute is reset.
+        when(mAudioManager.isMicrophoneMute()).thenReturn(true);
+        mController.sendMessageWithSessionInfo(SWITCH_FOCUS, NO_FOCUS);
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        verify(mAudioService, timeout(TEST_TIMEOUT)).setMicrophoneMute(eq(false), anyString(),
+                anyInt(), anyString());
+        verify(mCallsManager, timeout(TEST_TIMEOUT).atLeastOnce()).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+    }
+
+    @SmallTest
+    @Test
+    public void testIgnoreAutoRouteToWatch() {
+        when(mFeatureFlags.ignoreAutoRouteToWatchDevice()).thenReturn(true);
+        when(mBluetoothRouteManager.isWatch(any(BluetoothDevice.class))).thenReturn(true);
+
+        mController.initialize();
+        mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
+                BLUETOOTH_DEVICE_1);
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, null, BLUETOOTH_DEVICES);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        // Connect wired headset.
+        mController.sendMessageWithSessionInfo(CONNECT_WIRED_HEADSET);
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_WIRED_HEADSET,
+                CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER
+                        | CallAudioState.ROUTE_BLUETOOTH, null, BLUETOOTH_DEVICES);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        // Disconnect wired headset and ensure Telecom routes to earpiece instead of the BT route.
+        mController.sendMessageWithSessionInfo(DISCONNECT_WIRED_HEADSET);
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER
+                        | CallAudioState.ROUTE_BLUETOOTH, null , BLUETOOTH_DEVICES);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+    }
+
+    @SmallTest
+    @Test
+    public void testConnectDisconnectScoDuringCall() {
+        verifyConnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_SCO);
+        verifyDisconnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_SCO);
+    }
+
+    @SmallTest
+    @Test
+    public void testConnectAndDisconnectLeDeviceDuringCall() {
+        when(mBluetoothLeAudio.getConnectedGroupLeadDevice(anyInt()))
+                .thenReturn(BLUETOOTH_DEVICE_1);
+        verifyConnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_LE);
+        verifyDisconnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_LE);
+    }
+
+    @SmallTest
+    @Test
+    public void testConnectAndDisconnectHearingAidDuringCall() {
+        verifyConnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_HA);
+        verifyDisconnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_HA);
+    }
+
+    @SmallTest
+    @Test
+    public void testSwitchBetweenLeAndScoDevices() {
+        when(mBluetoothLeAudio.getConnectedGroupLeadDevice(anyInt()))
+                .thenReturn(BLUETOOTH_DEVICE_1);
+        verifyConnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_LE);
+        BluetoothDevice scoDevice =
+                BluetoothRouteManagerTest.makeBluetoothDevice("00:00:00:00:00:03");
+        BLUETOOTH_DEVICES.add(scoDevice);
+
+        // Add SCO device.
+        mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
+                scoDevice);
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        // Switch to SCO and verify active device is updated.
+        mController.sendMessageWithSessionInfo(USER_SWITCH_BLUETOOTH, 0, scoDevice.getAddress());
+        mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0, scoDevice);
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, scoDevice, BLUETOOTH_DEVICES);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        // Disconnect SCO and verify audio routed back to LE audio.
+        BLUETOOTH_DEVICES.remove(scoDevice);
+        mController.sendMessageWithSessionInfo(BT_DEVICE_REMOVED, AudioRoute.TYPE_BLUETOOTH_SCO,
+                scoDevice);
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+    }
+
+    @SmallTest
+    @Test
+    public void testFallbackWhenBluetoothConnectionFails() {
+        when(mBluetoothDeviceManager.connectAudio(any(BluetoothDevice.class), anyInt()))
+                .thenReturn(false);
+
+        AudioDeviceInfo mockAudioDeviceInfo = mock(AudioDeviceInfo.class);
+        when(mAudioManager.getCommunicationDevice()).thenReturn(mockAudioDeviceInfo);
+        verifyConnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_LE);
+        BluetoothDevice scoDevice =
+                BluetoothRouteManagerTest.makeBluetoothDevice("00:00:00:00:00:03");
+        BLUETOOTH_DEVICES.add(scoDevice);
+
+        // Add SCO device.
+        mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
+                scoDevice);
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        // Switch to SCO but reject connection and make sure audio is routed back to LE device.
+        mController.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
+                AudioRoute.TYPE_BLUETOOTH_SCO, scoDevice.getAddress());
+        verify(mBluetoothDeviceManager, timeout(TEST_TIMEOUT))
+                .connectAudio(scoDevice, AudioRoute.TYPE_BLUETOOTH_SCO);
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+        verify(mCallsManager, timeout(TEST_TIMEOUT).atLeastOnce()).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        // Cleanup supported devices for next test
+        BLUETOOTH_DEVICES.remove(scoDevice);
+    }
+
+    @SmallTest
+    @Test
+    public void testIgnoreLeRouteWhenServiceUnavailable() {
+        when(mBluetoothLeAudio.getConnectedGroupLeadDevice(anyInt()))
+                .thenReturn(BLUETOOTH_DEVICE_1);
+        verifyConnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_LE);
+
+        when(mBluetoothDeviceManager.getLeAudioService()).thenReturn(null);
+        // Switch baseline to verify that we don't route back to LE audio this time.
+        mController.sendMessageWithSessionInfo(SWITCH_BASELINE_ROUTE, 0, (String) null);
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, null, BLUETOOTH_DEVICES);
+        verify(mCallsManager, timeout(TEST_TIMEOUT).atLeastOnce()).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+    }
+
+    private void verifyConnectBluetoothDevice(int audioType) {
+        mController.initialize();
+        mController.setActive(true);
+
+        mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, audioType, BLUETOOTH_DEVICE_1);
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, null, BLUETOOTH_DEVICES);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        mController.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT, audioType, BT_ADDRESS_1);
+        if (audioType == AudioRoute.TYPE_BLUETOOTH_SCO) {
+            verify(mBluetoothDeviceManager, timeout(TEST_TIMEOUT))
+                    .connectAudio(BLUETOOTH_DEVICE_1, AudioRoute.TYPE_BLUETOOTH_SCO);
+            mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED,
+                    0, BLUETOOTH_DEVICE_1);
+        } else {
+            verify(mAudioManager, timeout(TEST_TIMEOUT))
+                    .setCommunicationDevice(nullable(AudioDeviceInfo.class));
+        }
+
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        // Test hearing aid pair and ensure second device isn't added as a route
+        if (audioType == AudioRoute.TYPE_BLUETOOTH_HA) {
+            BluetoothDevice hearingAidDevice2 =
+                    BluetoothRouteManagerTest.makeBluetoothDevice("00:00:00:00:00:02");
+            mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, audioType, hearingAidDevice2);
+            expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+                    CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                            | CallAudioState.ROUTE_SPEAKER, null, BLUETOOTH_DEVICES);
+            // Verify that supported BT devices only shows the first connected hearing aid device.
+            verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                    any(CallAudioState.class), eq(expectedState));
+        }
+    }
+
+    private void verifyDisconnectBluetoothDevice(int audioType) {
+        mController.sendMessageWithSessionInfo(BT_DEVICE_REMOVED, audioType, BLUETOOTH_DEVICE_1);
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        if (audioType == AudioRoute.TYPE_BLUETOOTH_SCO) {
+            verify(mBluetoothDeviceManager, timeout(TEST_TIMEOUT)).disconnectSco();
+        } else {
+            verify(mAudioManager, timeout(TEST_TIMEOUT)).clearCommunicationDevice();
+        }
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+    }
+}
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRoutePeripheralAdapterTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRoutePeripheralAdapterTest.java
index dfe1483..79247be 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRoutePeripheralAdapterTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRoutePeripheralAdapterTest.java
@@ -24,8 +24,9 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import android.test.suitebuilder.annotation.SmallTest;
+import androidx.test.filters.SmallTest;
 
+import com.android.server.telecom.AsyncRingtonePlayer;
 import com.android.server.telecom.CallAudioRoutePeripheralAdapter;
 import com.android.server.telecom.CallAudioRouteStateMachine;
 import com.android.server.telecom.DockManager;
@@ -47,6 +48,7 @@
     @Mock private BluetoothRouteManager mBluetoothRouteManager;
     @Mock private WiredHeadsetManager mWiredHeadsetManager;
     @Mock private DockManager mDockManager;
+    @Mock private AsyncRingtonePlayer mRingtonePlayer;
 
     @Override
     @Before
@@ -57,7 +59,8 @@
                 mCallAudioRouteStateMachine,
                 mBluetoothRouteManager,
                 mWiredHeadsetManager,
-                mDockManager);
+                mDockManager,
+                mRingtonePlayer);
     }
 
     @Override
@@ -126,6 +129,16 @@
         mAdapter.onBluetoothAudioConnected();
         verify(mCallAudioRouteStateMachine).sendMessageWithSessionInfo(
                 CallAudioRouteStateMachine.BT_AUDIO_CONNECTED);
+        verify(mRingtonePlayer).updateBtActiveState(true);
+    }
+
+    @SmallTest
+    @Test
+    public void testOnBluetoothAudioConnecting() {
+        mAdapter.onBluetoothAudioConnecting();
+        verify(mCallAudioRouteStateMachine).sendMessageWithSessionInfo(
+                CallAudioRouteStateMachine.BT_AUDIO_CONNECTED);
+        verify(mRingtonePlayer).updateBtActiveState(false);
     }
 
     @SmallTest
@@ -134,6 +147,7 @@
         mAdapter.onBluetoothAudioDisconnected();
         verify(mCallAudioRouteStateMachine).sendMessageWithSessionInfo(
                 CallAudioRouteStateMachine.BT_AUDIO_DISCONNECTED);
+        verify(mRingtonePlayer).updateBtActiveState(false);
     }
 
     @SmallTest
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
index 569c487..e97de2e 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
@@ -16,6 +16,27 @@
 
 package com.android.server.telecom.tests;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
 import android.bluetooth.BluetoothDevice;
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -26,18 +47,20 @@
 import android.media.IAudioService;
 import android.os.HandlerThread;
 import android.telecom.CallAudioState;
-import android.test.suitebuilder.annotation.MediumTest;
-import android.test.suitebuilder.annotation.SmallTest;
 
-import com.android.server.telecom.bluetooth.BluetoothRouteManager;
+import androidx.test.filters.MediumTest;
+import androidx.test.filters.SmallTest;
+
 import com.android.server.telecom.Call;
+import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
+import com.android.server.telecom.CallAudioManager;
 import com.android.server.telecom.CallAudioRouteStateMachine;
 import com.android.server.telecom.CallsManager;
 import com.android.server.telecom.ConnectionServiceWrapper;
-import com.android.server.telecom.CallAudioManager;
 import com.android.server.telecom.StatusBarNotifier;
 import com.android.server.telecom.TelecomSystem;
 import com.android.server.telecom.WiredHeadsetManager;
+import com.android.server.telecom.bluetooth.BluetoothRouteManager;
 
 import org.junit.After;
 import org.junit.Before;
@@ -50,6 +73,7 @@
 import org.mockito.invocation.InvocationOnMock;
 import org.mockito.stubbing.Answer;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
@@ -57,24 +81,6 @@
 import java.util.List;
 import java.util.Set;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.same;
-import static org.mockito.Mockito.atLeastOnce;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.timeout;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
 @RunWith(JUnit4.class)
 public class CallAudioRouteStateMachineTest extends TelecomTestCase {
 
@@ -94,12 +100,14 @@
     @Mock Call fakeSelfManagedCall;
     @Mock Call fakeCall;
     @Mock CallAudioManager mockCallAudioManager;
+    @Mock BluetoothDevice mockWatchDevice;
 
     private CallAudioManager.AudioServiceFactory mAudioServiceFactory;
     private static final int TEST_TIMEOUT = 500;
     private AudioManager mockAudioManager;
     private final TelecomSystem.SyncRoot mLock = new TelecomSystem.SyncRoot() { };
     private HandlerThread mThreadHandler;
+    CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
 
     @Override
     @Before
@@ -110,6 +118,8 @@
         mThreadHandler.start();
         mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
         mockAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+        mCommunicationDeviceTracker = new CallAudioCommunicationDeviceTracker(mContext);
+        mCommunicationDeviceTracker.setBluetoothRouteManager(mockBluetoothRouteManager);
 
         mAudioServiceFactory = new CallAudioManager.AudioServiceFactory() {
             @Override
@@ -129,9 +139,12 @@
         when(fakeSelfManagedCall.isAlive()).thenReturn(true);
         when(fakeSelfManagedCall.getSupportedAudioRoutes()).thenReturn(CallAudioState.ROUTE_ALL);
         when(fakeSelfManagedCall.isSelfManaged()).thenReturn(true);
+        when(mFeatureFlags.transitRouteBeforeAudioDisconnectBt()).thenReturn(false);
 
         doNothing().when(mockConnectionServiceWrapper).onCallAudioStateChanged(any(Call.class),
                 any(CallAudioState.class));
+        when(mFeatureFlags.ignoreAutoRouteToWatchDevice()).thenReturn(false);
+        when(mFeatureFlags.callAudioCommunicationDeviceRefactor()).thenReturn(false);
     }
 
     @Override
@@ -154,7 +167,9 @@
                 mAudioServiceFactory,
                 CallAudioRouteStateMachine.EARPIECE_AUTO_DETECT,
                 mThreadHandler.getLooper(),
-                Runnable::run /** do async stuff sync for test purposes */);
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker,
+                mFeatureFlags);
 
         // Since we don't know if we're on a platform with an earpiece or not, all we can do
         // is ensure the stateMachine construction didn't fail.  But at least we exercised the
@@ -174,7 +189,9 @@
                 mAudioServiceFactory,
                 CallAudioRouteStateMachine.EARPIECE_AUTO_DETECT,
                 mThreadHandler.getLooper(),
-                Runnable::run /** do async stuff sync for test purposes */);
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker,
+                mFeatureFlags);
         stateMachine.setCallAudioManager(mockCallAudioManager);
 
         Set<Call> trackedCalls = new HashSet<>(Arrays.asList(fakeCall, fakeSelfManagedCall));
@@ -197,8 +214,8 @@
         stateMachine.sendMessageWithSessionInfo(
                 CallAudioRouteStateMachine.UPDATE_SYSTEM_AUDIO_ROUTE);
 
-        waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
-        waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
 
         // assert expected end state
         assertEquals(stateMachine.getCurrentCallAudioState().getRoute(),
@@ -208,6 +225,112 @@
                 .onCallAudioStateChanged(any(), any());
     }
 
+    @SmallTest
+    @Test
+    public void testSystemAudioStateIsNotUpdatedFlagOff() {
+        CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
+                mContext,
+                mockCallsManager,
+                mockBluetoothRouteManager,
+                mockWiredHeadsetManager,
+                mockStatusBarNotifier,
+                mAudioServiceFactory,
+                CallAudioRouteStateMachine.EARPIECE_AUTO_DETECT,
+                mThreadHandler.getLooper(),
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker,
+                mFeatureFlags);
+        stateMachine.setCallAudioManager(mockCallAudioManager);
+
+        Set<Call> trackedCalls = new HashSet<>(Arrays.asList(fakeCall, fakeSelfManagedCall));
+        when(mockCallsManager.getTrackedCalls()).thenReturn(trackedCalls);
+        when(mFeatureFlags.availableRoutesNeverUpdatedAfterSetSystemAudioState()).thenReturn(false);
+
+        // start state --> ROUTE_EARPIECE
+        CallAudioState initState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER);
+        stateMachine.initialize(initState);
+
+        stateMachine.setCallAudioManager(mockCallAudioManager);
+
+        assertEquals(stateMachine.getCurrentCallAudioState().getRoute(),
+                CallAudioRouteStateMachine.ROUTE_EARPIECE);
+
+        // ROUTE_EARPIECE  --> ROUTE_SPEAKER
+        stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_SPEAKER,
+                CallAudioRouteStateMachine.SPEAKER_ON);
+
+        stateMachine.sendMessageWithSessionInfo(
+                CallAudioRouteStateMachine.UPDATE_SYSTEM_AUDIO_ROUTE);
+
+        waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+        waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+
+        CallAudioState expectedCallAudioState = stateMachine.getLastKnownCallAudioState();
+
+        // assert expected end state
+        assertEquals(stateMachine.getCurrentCallAudioState().getRoute(),
+                CallAudioRouteStateMachine.ROUTE_SPEAKER);
+        // should update the audio route on all tracked calls ...
+        verify(mockConnectionServiceWrapper, times(trackedCalls.size()))
+                .onCallAudioStateChanged(any(), any());
+
+        assertNotEquals(expectedCallAudioState, stateMachine.getCurrentCallAudioState());
+    }
+
+    @SmallTest
+    @Test
+    public void testSystemAudioStateIsUpdatedFlagOn() {
+        CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
+                mContext,
+                mockCallsManager,
+                mockBluetoothRouteManager,
+                mockWiredHeadsetManager,
+                mockStatusBarNotifier,
+                mAudioServiceFactory,
+                CallAudioRouteStateMachine.EARPIECE_AUTO_DETECT,
+                mThreadHandler.getLooper(),
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker,
+                mFeatureFlags);
+        stateMachine.setCallAudioManager(mockCallAudioManager);
+
+        Set<Call> trackedCalls = new HashSet<>(Arrays.asList(fakeCall, fakeSelfManagedCall));
+        when(mockCallsManager.getTrackedCalls()).thenReturn(trackedCalls);
+        when(mFeatureFlags.availableRoutesNeverUpdatedAfterSetSystemAudioState()).thenReturn(true);
+
+        // start state --> ROUTE_EARPIECE
+        CallAudioState initState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER);
+        stateMachine.initialize(initState);
+
+        stateMachine.setCallAudioManager(mockCallAudioManager);
+
+        assertEquals(stateMachine.getCurrentCallAudioState().getRoute(),
+                CallAudioRouteStateMachine.ROUTE_EARPIECE);
+
+        // ROUTE_EARPIECE  --> ROUTE_SPEAKER
+        stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_SPEAKER,
+                CallAudioRouteStateMachine.SPEAKER_ON);
+
+        stateMachine.sendMessageWithSessionInfo(
+                CallAudioRouteStateMachine.UPDATE_SYSTEM_AUDIO_ROUTE);
+
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
+
+        CallAudioState expectedCallAudioState = stateMachine.getLastKnownCallAudioState();
+
+        // assert expected end state
+        assertEquals(stateMachine.getCurrentCallAudioState().getRoute(),
+                CallAudioRouteStateMachine.ROUTE_SPEAKER);
+        // should update the audio route on all tracked calls ...
+        verify(mockConnectionServiceWrapper, times(trackedCalls.size()))
+                .onCallAudioStateChanged(any(), any());
+
+        assertEquals(expectedCallAudioState, stateMachine.getCurrentCallAudioState());
+    }
+
     @MediumTest
     @Test
     public void testStreamRingMuteChange() {
@@ -220,7 +343,9 @@
                 mAudioServiceFactory,
                 CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
                 mThreadHandler.getLooper(),
-                Runnable::run /** do async stuff sync for test purposes */);
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker,
+                mFeatureFlags);
         stateMachine.setCallAudioManager(mockCallAudioManager);
         CallAudioState initState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
                 CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER);
@@ -250,6 +375,7 @@
             foundValid = true;
         }
         assertTrue(foundValid);
+        verify(mockBluetoothRouteManager, timeout(1000L)).getBluetoothAudioConnectedDevice();
     }
 
     @MediumTest
@@ -264,7 +390,9 @@
                 mAudioServiceFactory,
                 CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
                 mThreadHandler.getLooper(),
-                Runnable::run /** do async stuff sync for test purposes */);
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker,
+                mFeatureFlags);
 
         when(mockBluetoothRouteManager.isBluetoothAudioConnectedOrPending()).thenReturn(false);
         when(mockBluetoothRouteManager.isBluetoothAvailable()).thenReturn(true);
@@ -287,14 +415,14 @@
         CallAudioState expectedMiddleState = new CallAudioState(false,
                 CallAudioState.ROUTE_WIRED_HEADSET,
                 CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER);
-        waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
-        waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
         verifyNewSystemCallAudioState(initState, expectedMiddleState);
         resetMocks();
 
         stateMachine.sendMessageWithSessionInfo(
                 CallAudioRouteStateMachine.DISCONNECT_WIRED_HEADSET);
-        waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
         verifyNewSystemCallAudioState(expectedMiddleState, initState);
     }
 
@@ -310,7 +438,9 @@
                 mAudioServiceFactory,
                 CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
                 mThreadHandler.getLooper(),
-                Runnable::run /** do async stuff sync for test purposes */);
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker,
+                mFeatureFlags);
         stateMachine.setCallAudioManager(mockCallAudioManager);
 
         when(mockBluetoothRouteManager.isBluetoothAudioConnectedOrPending()).thenReturn(false);
@@ -331,7 +461,7 @@
                 CallAudioState.ROUTE_EARPIECE,
                 CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH);
 
-        waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
         verifyNewSystemCallAudioState(initState, expectedEndState);
         resetMocks();
         stateMachine.sendMessageWithSessionInfo(
@@ -339,7 +469,7 @@
         stateMachine.sendMessageWithSessionInfo(
                 CallAudioRouteStateMachine.BT_ACTIVE_DEVICE_PRESENT);
 
-        waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
         assertEquals(expectedEndState, stateMachine.getCurrentCallAudioState());
     }
 
@@ -355,7 +485,9 @@
                 mAudioServiceFactory,
                 CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
                 mThreadHandler.getLooper(),
-                Runnable::run /** do async stuff sync for test purposes */);
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker,
+                mFeatureFlags);
         stateMachine.setCallAudioManager(mockCallAudioManager);
         Collection<BluetoothDevice> availableDevices = Collections.singleton(bluetoothDevice1);
 
@@ -380,12 +512,12 @@
                 CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH,
                 null, availableDevices);
 
-        waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
         verifyNewSystemCallAudioState(initState, expectedMidState);
         // clear out the handler state before resetting mocks in order to avoid introducing a
         // CallAudioState that has a null list of supported BT devices
-        waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
-        waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
         resetMocks();
 
         // Now, switch back to BT explicitly
@@ -403,9 +535,9 @@
                 CallAudioState.ROUTE_BLUETOOTH,
                 CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH,
                 bluetoothDevice1, availableDevices);
-        waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
         // second wait needed for the BT_AUDIO_CONNECTED message
-        waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
         verifyNewSystemCallAudioState(expectedMidState, expectedEndState);
 
         stateMachine.sendMessageWithSessionInfo(
@@ -415,9 +547,9 @@
         stateMachine.sendMessageWithSessionInfo(
                 CallAudioRouteStateMachine.BT_ACTIVE_DEVICE_PRESENT);
 
-        waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
         // second wait needed for the BT_AUDIO_CONNECTED message
-        waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
         // Verify that we're still on bluetooth.
         assertEquals(expectedEndState, stateMachine.getCurrentCallAudioState());
     }
@@ -434,7 +566,9 @@
                 mAudioServiceFactory,
                 CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
                 mThreadHandler.getLooper(),
-                Runnable::run /** do async stuff sync for test purposes */);
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker,
+                mFeatureFlags);
         stateMachine.setCallAudioManager(mockCallAudioManager);
 
         when(mockBluetoothRouteManager.isBluetoothAudioConnectedOrPending()).thenReturn(false);
@@ -449,13 +583,13 @@
 
         stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
                 CallAudioRouteStateMachine.RINGING_FOCUS);
-        waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
 
         verify(mockBluetoothRouteManager, never()).connectBluetoothAudio(nullable(String.class));
 
         stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
                 CallAudioRouteStateMachine.ACTIVE_FOCUS);
-        waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
         verify(mockBluetoothRouteManager, times(1)).connectBluetoothAudio(nullable(String.class));
     }
 
@@ -471,7 +605,9 @@
                 mAudioServiceFactory,
                 CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
                 mThreadHandler.getLooper(),
-                Runnable::run /** do async stuff sync for test purposes */);
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker,
+                mFeatureFlags);
         stateMachine.setCallAudioManager(mockCallAudioManager);
         setInBandRing(false);
         when(mockBluetoothRouteManager.isBluetoothAudioConnectedOrPending()).thenReturn(false);
@@ -485,7 +621,7 @@
                 CallAudioRouteStateMachine.RINGING_FOCUS);
         // Wait for the state machine to finish transiting to ActiveEarpiece before hooking up
         // bluetooth mocks
-        waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
 
         when(mockBluetoothRouteManager.isBluetoothAvailable()).thenReturn(true);
         when(mockBluetoothRouteManager.getConnectedDevices())
@@ -494,7 +630,7 @@
                 CallAudioRouteStateMachine.BLUETOOTH_DEVICE_LIST_CHANGED);
         stateMachine.sendMessageWithSessionInfo(
                 CallAudioRouteStateMachine.BT_ACTIVE_DEVICE_PRESENT);
-        waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
 
         verify(mockBluetoothRouteManager, never()).connectBluetoothAudio(null);
         CallAudioState expectedEndState = new CallAudioState(false,
@@ -505,14 +641,17 @@
 
         stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
                 CallAudioRouteStateMachine.ACTIVE_FOCUS);
-        waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
         verify(mockBluetoothRouteManager, times(1)).connectBluetoothAudio(null);
 
         when(mockBluetoothRouteManager.getBluetoothAudioConnectedDevice())
                 .thenReturn(bluetoothDevice1);
         stateMachine.sendMessage(CallAudioRouteStateMachine.BT_AUDIO_CONNECTED);
-        waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
-        verify(mockCallAudioManager, times(1)).onRingerModeChange();
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
+        // It is possible that this will be called twice from ActiveBluetoothRoute#enter. The extra
+        // call to setBluetoothOn will trigger BT_AUDIO_CONNECTED, which also ends up invoking
+        // CallAudioManager#onRingerModeChange.
+        verify(mockCallAudioManager, atLeastOnce()).onRingerModeChange();
     }
 
     @SmallTest
@@ -527,7 +666,9 @@
                 mAudioServiceFactory,
                 CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
                 mThreadHandler.getLooper(),
-                Runnable::run /** do async stuff sync for test purposes */);
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker,
+                mFeatureFlags);
         stateMachine.setCallAudioManager(mockCallAudioManager);
         List<BluetoothDevice> availableDevices =
                 Arrays.asList(bluetoothDevice1, bluetoothDevice2, bluetoothDevice3);
@@ -553,17 +694,80 @@
 
         stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.USER_SWITCH_BLUETOOTH,
                 0, bluetoothDevice2.getAddress());
-        waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
 
         verify(mockBluetoothRouteManager).connectBluetoothAudio(bluetoothDevice2.getAddress());
-        waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
         CallAudioState expectedEndState = new CallAudioState(false,
                 CallAudioState.ROUTE_BLUETOOTH,
                 CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH,
                 bluetoothDevice2,
                 availableDevices);
 
-        verifyNewSystemCallAudioState(initState, expectedEndState);
+        assertEquals(expectedEndState, stateMachine.getCurrentCallAudioState());
+    }
+
+    @SmallTest
+    @Test
+    public void testCallDisconnectedWhenAudioRoutedToBluetooth() {
+        when(mFeatureFlags.callAudioCommunicationDeviceRefactor()).thenReturn(true);
+        CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
+                mContext,
+                mockCallsManager,
+                mockBluetoothRouteManager,
+                mockWiredHeadsetManager,
+                mockStatusBarNotifier,
+                mAudioServiceFactory,
+                CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
+                mThreadHandler.getLooper(),
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker,
+                mFeatureFlags);
+        stateMachine.setCallAudioManager(mockCallAudioManager);
+        List<BluetoothDevice> availableDevices = Arrays.asList(bluetoothDevice1);
+
+        when(mockAudioManager.isSpeakerphoneOn()).thenReturn(false);
+        when(mockBluetoothRouteManager.isBluetoothAudioConnectedOrPending()).thenReturn(false);
+        when(mockBluetoothRouteManager.isBluetoothAvailable()).thenReturn(true);
+        when(mockBluetoothRouteManager.getConnectedDevices()).thenReturn(availableDevices);
+        when(mockBluetoothRouteManager.isInbandRingingEnabled()).thenReturn(true);
+        when(mFeatureFlags.transitRouteBeforeAudioDisconnectBt()).thenReturn(true);
+        doAnswer(invocation -> {
+            when(mockBluetoothRouteManager.getBluetoothAudioConnectedDevice())
+                    .thenReturn(bluetoothDevice1);
+            stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.BT_AUDIO_CONNECTED);
+            return null;
+        }).when(mockBluetoothRouteManager).connectBluetoothAudio(bluetoothDevice1.getAddress());
+        doAnswer(invocation -> {
+            when(mockBluetoothRouteManager.getBluetoothAudioConnectedDevice())
+                    .thenReturn(bluetoothDevice1);
+            stateMachine.sendMessageWithSessionInfo(
+                    CallAudioRouteStateMachine.BT_AUDIO_DISCONNECTED);
+            return null;
+        }).when(mockBluetoothRouteManager).disconnectAudio();
+
+        CallAudioState initState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, null,
+                availableDevices);
+        stateMachine.initialize(initState);
+
+        stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
+                CallAudioRouteStateMachine.ACTIVE_FOCUS);
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
+
+        stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
+                CallAudioRouteStateMachine.NO_FOCUS, bluetoothDevice1.getAddress());
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
+
+        verify(mockBluetoothRouteManager).disconnectAudio();
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
+        CallAudioState expectedEndState = new CallAudioState(false,
+                CallAudioState.ROUTE_BLUETOOTH,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH,
+                bluetoothDevice1,
+                availableDevices);
+
+        assertEquals(expectedEndState, stateMachine.getCurrentCallAudioState());
     }
 
     @SmallTest
@@ -578,7 +782,9 @@
                 mAudioServiceFactory,
                 CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
                 mThreadHandler.getLooper(),
-                Runnable::run /** do async stuff sync for test purposes */);
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker,
+                mFeatureFlags);
         stateMachine.setCallAudioManager(mockCallAudioManager);
         when(mockAudioManager.isSpeakerphoneOn()).thenReturn(false);
         CallAudioState initState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
@@ -587,13 +793,13 @@
 
         // Raise a dock connect event.
         stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.CONNECT_DOCK);
-        waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
         assertTrue(!stateMachine.isInActiveState());
         verify(mockAudioManager, never()).setSpeakerphoneOn(eq(true));
 
         // Raise a dock disconnect event.
         stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.DISCONNECT_DOCK);
-        waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
         assertTrue(!stateMachine.isInActiveState());
         verify(mockAudioManager, never()).setSpeakerphoneOn(eq(false));
     }
@@ -610,7 +816,9 @@
                 mAudioServiceFactory,
                 CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
                 mThreadHandler.getLooper(),
-                Runnable::run /** do async stuff sync for test purposes */);
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker,
+                mFeatureFlags);
         stateMachine.setCallAudioManager(mockCallAudioManager);
 
         when(mockAudioManager.isSpeakerphoneOn()).thenReturn(false);
@@ -622,7 +830,7 @@
         // Switch to active, pretending that a call came in.
         stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
                 CallAudioRouteStateMachine.ACTIVE_FOCUS);
-        waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
 
         // Make sure that we've successfully switched to the active speaker route and that we've
         // called setSpeakerOn
@@ -645,7 +853,9 @@
                 mAudioServiceFactory,
                 CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
                 mThreadHandler.getLooper(),
-                Runnable::run /** do async stuff sync for test purposes */);
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker,
+                mFeatureFlags);
         stateMachine.setCallAudioManager(mockCallAudioManager);
         List<BluetoothDevice> availableDevices =
                 Arrays.asList(bluetoothDevice1, bluetoothDevice2);
@@ -667,7 +877,7 @@
         // Switch to active, pretending that a call came in.
         stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
                 CallAudioRouteStateMachine.ACTIVE_FOCUS);
-        waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
 
         // Make sure that we've successfully switched to the active BT route and that we've
         // called connectAudio on the right device.
@@ -678,6 +888,112 @@
 
     @SmallTest
     @Test
+    public void testSetAndClearEarpieceCommunicationDevice() {
+        when(mFeatureFlags.callAudioCommunicationDeviceRefactor()).thenReturn(true);
+        CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
+                mContext,
+                mockCallsManager,
+                mockBluetoothRouteManager,
+                mockWiredHeadsetManager,
+                mockStatusBarNotifier,
+                mAudioServiceFactory,
+                CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
+                mThreadHandler.getLooper(),
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker,
+                mFeatureFlags);
+        stateMachine.setCallAudioManager(mockCallAudioManager);
+
+        AudioDeviceInfo earpiece = mock(AudioDeviceInfo.class);
+        when(earpiece.getType()).thenReturn(AudioDeviceInfo.TYPE_BUILTIN_EARPIECE);
+        when(earpiece.getAddress()).thenReturn("");
+        List<AudioDeviceInfo> devices = new ArrayList<>();
+        devices.add(earpiece);
+
+        when(mockAudioManager.getAvailableCommunicationDevices())
+                .thenReturn(devices);
+        when(mockAudioManager.setCommunicationDevice(eq(earpiece)))
+                .thenReturn(true);
+        when(mockAudioManager.getCommunicationDevice()).thenReturn(earpiece);
+
+        CallAudioState initState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER |
+                        CallAudioState.ROUTE_WIRED_HEADSET);
+        stateMachine.initialize(initState);
+
+        // Switch to active
+        stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
+                CallAudioRouteStateMachine.ACTIVE_FOCUS);
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
+
+        // Make sure that we've successfully switched to the active earpiece and that we set the
+        // communication device.
+        assertTrue(stateMachine.isInActiveState());
+        ArgumentCaptor<AudioDeviceInfo> infoArgumentCaptor = ArgumentCaptor.forClass(
+                AudioDeviceInfo.class);
+        verify(mockAudioManager).setCommunicationDevice(infoArgumentCaptor.capture());
+        assertEquals(AudioDeviceInfo.TYPE_BUILTIN_EARPIECE,
+                infoArgumentCaptor.getValue().getType());
+
+        // Route earpiece to speaker
+        stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_SPEAKER,
+                CallAudioRouteStateMachine.SPEAKER_ON);
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
+
+        // Assert that communication device was cleared
+        verify(mockAudioManager).clearCommunicationDevice();
+    }
+
+    @SmallTest
+    @Test
+    public void testSetAndClearWiredHeadsetCommunicationDevice() {
+        when(mFeatureFlags.callAudioCommunicationDeviceRefactor()).thenReturn(true);
+        verifySetAndClearHeadsetCommunicationDevice(AudioDeviceInfo.TYPE_WIRED_HEADSET);
+    }
+
+    @SmallTest
+    @Test
+    public void testSetAndClearUsbHeadsetCommunicationDevice() {
+        when(mFeatureFlags.callAudioCommunicationDeviceRefactor()).thenReturn(true);
+        verifySetAndClearHeadsetCommunicationDevice(AudioDeviceInfo.TYPE_USB_HEADSET);
+    }
+
+    @SmallTest
+    @Test
+    public void testActiveFocusRouteSwitchFromQuiescentBluetooth() {
+        when(mFeatureFlags.callAudioCommunicationDeviceRefactor()).thenReturn(true);
+        CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
+                mContext,
+                mockCallsManager,
+                mockBluetoothRouteManager,
+                mockWiredHeadsetManager,
+                mockStatusBarNotifier,
+                mAudioServiceFactory,
+                CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
+                mThreadHandler.getLooper(),
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker,
+                mFeatureFlags);
+        stateMachine.setCallAudioManager(mockCallAudioManager);
+
+        // Start the route in quiescent and ensure that a switch to ACTIVE_FOCUS transitions to
+        // the corresponding active route even when there aren't any active BT devices available.
+        CallAudioState initState = new CallAudioState(false,
+                CallAudioState.ROUTE_BLUETOOTH,
+                CallAudioState.ROUTE_BLUETOOTH | CallAudioState.ROUTE_EARPIECE);
+        stateMachine.initialize(initState);
+
+        // Switch to active
+        stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
+                CallAudioRouteStateMachine.ACTIVE_FOCUS);
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
+
+        // Make sure that we've successfully switched to the active route on BT
+        assertTrue(stateMachine.isInActiveState());
+    }
+
+    @SmallTest
+    @Test
     public void testInitializationWithEarpieceNoHeadsetNoBluetooth() {
         CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
                 CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER);
@@ -761,7 +1077,9 @@
                 mAudioServiceFactory,
                 CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
                 mThreadHandler.getLooper(),
-                Runnable::run /** do async stuff sync for test purposes */);
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker,
+                mFeatureFlags);
         stateMachine.initialize();
         assertEquals(expectedState, stateMachine.getCurrentCallAudioState());
     }
@@ -778,7 +1096,9 @@
                 mAudioServiceFactory,
                 CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
                 mThreadHandler.getLooper(),
-                Runnable::run /** do async stuff sync for test purposes */);
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker,
+                mFeatureFlags);
         stateMachine.setCallAudioManager(mockCallAudioManager);
 
         CallAudioState initState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
@@ -789,15 +1109,207 @@
         CallAudioState expectedEndState = new CallAudioState(false,
                 CallAudioState.ROUTE_STREAMING, CallAudioState.ROUTE_STREAMING);
 
-        waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
         verifyNewSystemCallAudioState(initState, expectedEndState);
         resetMocks();
         stateMachine.sendMessageWithSessionInfo(
                 CallAudioRouteStateMachine.STREAMING_FORCE_DISABLED);
-        waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
         assertEquals(initState, stateMachine.getCurrentCallAudioState());
     }
 
+    @SmallTest
+    @Test
+    public void testIgnoreSpeakerOffMessage() {
+        when(mockBluetoothRouteManager.isInbandRingingEnabled()).thenReturn(true);
+        when(mockBluetoothRouteManager.getBluetoothAudioConnectedDevice())
+                .thenReturn(bluetoothDevice1);
+        when(mockBluetoothRouteManager.isBluetoothAudioConnectedOrPending()).thenReturn(true);
+        CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
+                mContext,
+                mockCallsManager,
+                mockBluetoothRouteManager,
+                mockWiredHeadsetManager,
+                mockStatusBarNotifier,
+                mAudioServiceFactory,
+                CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
+                mThreadHandler.getLooper(),
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker,
+                mFeatureFlags);
+        stateMachine.setCallAudioManager(mockCallAudioManager);
+
+        CallAudioState initState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER
+                        | CallAudioState.ROUTE_BLUETOOTH);
+        stateMachine.initialize(initState);
+
+        doAnswer(
+                (address) -> {
+                    stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SPEAKER_OFF);
+                    stateMachine.sendMessageDelayed(CallAudioRouteStateMachine.BT_AUDIO_CONNECTED,
+                            5000L);
+                    return null;
+        }).when(mockBluetoothRouteManager).connectBluetoothAudio(anyString());
+        stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
+                CallAudioRouteStateMachine.ACTIVE_FOCUS);
+        stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.USER_SWITCH_BLUETOOTH);
+
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+                CallAudioState.ROUTE_SPEAKER | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_EARPIECE);
+        assertEquals(expectedState, stateMachine.getCurrentCallAudioState());
+    }
+
+    @MediumTest
+    @Test
+    public void testIgnoreImplicitBTSwitchWhenDeviceIsWatch() {
+        when(mFeatureFlags.ignoreAutoRouteToWatchDevice()).thenReturn(true);
+        when(mFeatureFlags.callAudioCommunicationDeviceRefactor()).thenReturn(true);
+        CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
+                mContext,
+                mockCallsManager,
+                mockBluetoothRouteManager,
+                mockWiredHeadsetManager,
+                mockStatusBarNotifier,
+                mAudioServiceFactory,
+                CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
+                mThreadHandler.getLooper(),
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker,
+                mFeatureFlags);
+        stateMachine.setCallAudioManager(mockCallAudioManager);
+
+        AudioDeviceInfo headset = mock(AudioDeviceInfo.class);
+        when(headset.getType()).thenReturn(AudioDeviceInfo.TYPE_WIRED_HEADSET);
+        when(headset.getAddress()).thenReturn("");
+        List<AudioDeviceInfo> devices = new ArrayList<>();
+        devices.add(headset);
+
+        when(mockAudioManager.getAvailableCommunicationDevices())
+                .thenReturn(devices);
+        when(mockAudioManager.setCommunicationDevice(eq(headset)))
+                .thenReturn(true);
+        when(mockAudioManager.getCommunicationDevice()).thenReturn(headset);
+
+        CallAudioState initState = new CallAudioState(false,
+                CallAudioState.ROUTE_WIRED_HEADSET, CallAudioState.ROUTE_WIRED_HEADSET
+                | CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH);
+        stateMachine.initialize(initState);
+
+        // Switch to active
+        stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
+                CallAudioRouteStateMachine.ACTIVE_FOCUS);
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
+
+        // Make sure that we've successfully switched to the active headset.
+        assertTrue(stateMachine.isInActiveState());
+        ArgumentCaptor<AudioDeviceInfo> infoArgumentCaptor = ArgumentCaptor.forClass(
+                AudioDeviceInfo.class);
+        verify(mockAudioManager).setCommunicationDevice(infoArgumentCaptor.capture());
+        assertEquals(AudioDeviceInfo.TYPE_WIRED_HEADSET, infoArgumentCaptor.getValue().getType());
+
+        // Set up watch device as only available BT device.
+        Collection<BluetoothDevice> availableDevices = Collections.singleton(mockWatchDevice);
+
+        when(mockBluetoothRouteManager.isBluetoothAudioConnectedOrPending()).thenReturn(false);
+        when(mockBluetoothRouteManager.isBluetoothAvailable()).thenReturn(true);
+        when(mockBluetoothRouteManager.getConnectedDevices()).thenReturn(availableDevices);
+        when(mockBluetoothRouteManager.isWatch(any(BluetoothDevice.class))).thenReturn(true);
+
+        // Disconnect wired headset to force switch to BT (verify that we ignore the implicit switch
+        // to BT when the watch is the only connected device and that we move into the next
+        // available route.
+        stateMachine.sendMessageWithSessionInfo(
+                CallAudioRouteStateMachine.DISCONNECT_WIRED_HEADSET);
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH,
+                null, availableDevices);
+        assertEquals(expectedState, stateMachine.getCurrentCallAudioState());
+    }
+
+    @SmallTest
+    @Test
+    public void testQuiescentBluetoothRouteResetMute() throws Exception {
+        when(mFeatureFlags.resetMuteWhenEnteringQuiescentBtRoute()).thenReturn(true);
+        when(mFeatureFlags.transitRouteBeforeAudioDisconnectBt()).thenReturn(true);
+        CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
+                mContext,
+                mockCallsManager,
+                mockBluetoothRouteManager,
+                mockWiredHeadsetManager,
+                mockStatusBarNotifier,
+                mAudioServiceFactory,
+                CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
+                mThreadHandler.getLooper(),
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker,
+                mFeatureFlags);
+        stateMachine.setCallAudioManager(mockCallAudioManager);
+
+        CallAudioState initState = new CallAudioState(false,
+                CallAudioState.ROUTE_BLUETOOTH, CallAudioState.ROUTE_SPEAKER
+                | CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH);
+        stateMachine.initialize(initState);
+
+        // Switch to active and mute
+        stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
+                CallAudioRouteStateMachine.ACTIVE_FOCUS);
+        stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.BT_AUDIO_CONNECTED);
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
+        assertTrue(stateMachine.isInActiveState());
+
+        stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.MUTE_ON);
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
+        CallAudioState expectedState = new CallAudioState(true,
+                CallAudioState.ROUTE_BLUETOOTH, CallAudioState.ROUTE_SPEAKER
+                | CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH);
+        assertEquals(expectedState, stateMachine.getCurrentCallAudioState());
+        when(mockAudioManager.isMicrophoneMute()).thenReturn(true);
+
+        stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
+                CallAudioRouteStateMachine.NO_FOCUS);
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
+
+        expectedState = new CallAudioState(false,
+                CallAudioState.ROUTE_BLUETOOTH, CallAudioState.ROUTE_SPEAKER
+                | CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH);
+        assertEquals(expectedState, stateMachine.getCurrentCallAudioState());
+        verify(mockAudioService).setMicrophoneMute(eq(false), anyString(), anyInt(), eq(null));
+    }
+
+    @SmallTest
+    @Test
+    public void testSupportRouteMaskUpdateWhenBtAudioConnected() {
+        when(mFeatureFlags.updateRouteMaskWhenBtConnected()).thenReturn(true);
+        CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
+                mContext,
+                mockCallsManager,
+                mockBluetoothRouteManager,
+                mockWiredHeadsetManager,
+                mockStatusBarNotifier,
+                mAudioServiceFactory,
+                CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
+                mThreadHandler.getLooper(),
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker,
+                mFeatureFlags);
+        stateMachine.setCallAudioManager(mockCallAudioManager);
+
+        CallAudioState initState = new CallAudioState(false,
+                CallAudioState.ROUTE_EARPIECE,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER);
+        stateMachine.initialize(initState);
+
+        stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.BT_AUDIO_CONNECTED);
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER
+                        | CallAudioState.ROUTE_BLUETOOTH);
+        assertEquals(expectedState, stateMachine.getCurrentCallAudioState());
+    }
+
     private void initializationTestHelper(CallAudioState expectedState,
             int earpieceControl) {
         when(mockWiredHeadsetManager.isPluggedIn()).thenReturn(
@@ -816,7 +1328,9 @@
                 mAudioServiceFactory,
                 earpieceControl,
                 mThreadHandler.getLooper(),
-                Runnable::run /** do async stuff sync for test purposes */);
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker,
+                mFeatureFlags);
         stateMachine.initialize();
         assertEquals(expectedState, stateMachine.getCurrentCallAudioState());
     }
@@ -856,4 +1370,59 @@
         doNothing().when(mockConnectionServiceWrapper).onCallAudioStateChanged(any(Call.class),
                 any(CallAudioState.class));
     }
+
+    private void verifySetAndClearHeadsetCommunicationDevice(int audioType) {
+        CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
+                mContext,
+                mockCallsManager,
+                mockBluetoothRouteManager,
+                mockWiredHeadsetManager,
+                mockStatusBarNotifier,
+                mAudioServiceFactory,
+                CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
+                mThreadHandler.getLooper(),
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker,
+                mFeatureFlags);
+        stateMachine.setCallAudioManager(mockCallAudioManager);
+
+        AudioDeviceInfo headset = mock(AudioDeviceInfo.class);
+        when(headset.getType()).thenReturn(audioType);
+        when(headset.getAddress()).thenReturn("");
+        List<AudioDeviceInfo> devices = new ArrayList<>();
+        devices.add(headset);
+
+        when(mockAudioManager.getAvailableCommunicationDevices())
+                .thenReturn(devices);
+        when(mockAudioManager.setCommunicationDevice(eq(headset)))
+                .thenReturn(true);
+        when(mockAudioManager.getCommunicationDevice()).thenReturn(headset);
+
+        CallAudioState initState = new CallAudioState(false,
+                CallAudioState.ROUTE_WIRED_HEADSET,
+                CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_EARPIECE);
+        stateMachine.initialize(initState);
+
+        // Switch to active
+        stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
+                CallAudioRouteStateMachine.ACTIVE_FOCUS);
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
+
+        // Make sure that we've successfully switched to the active headset and that we set the
+        // communication device.
+        assertTrue(stateMachine.isInActiveState());
+        ArgumentCaptor<AudioDeviceInfo> infoArgumentCaptor = ArgumentCaptor.forClass(
+                AudioDeviceInfo.class);
+        verify(mockAudioManager).setCommunicationDevice(infoArgumentCaptor.capture());
+        assertEquals(audioType, infoArgumentCaptor.getValue().getType());
+
+        // Route out of headset route
+        stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
+                CallAudioRouteStateMachine.ACTIVE_FOCUS);
+        stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.USER_SWITCH_EARPIECE);
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
+
+        // Assert that communication device was cleared
+        verify(mockAudioManager).clearCommunicationDevice();
+    }
 }
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteTransitionTests.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteTransitionTests.java
index cf684de..6b9b5c8 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRouteTransitionTests.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteTransitionTests.java
@@ -20,10 +20,10 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.nullable;
 import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.verify;
@@ -37,9 +37,11 @@
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.telecom.CallAudioState;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.filters.SmallTest;
 
 import com.android.server.telecom.Call;
+import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
 import com.android.server.telecom.CallAudioManager;
 import com.android.server.telecom.CallAudioRouteStateMachine;
 import com.android.server.telecom.CallsManager;
@@ -155,6 +157,7 @@
     @Mock StatusBarNotifier mockStatusBarNotifier;
     @Mock Call fakeCall;
     @Mock CallAudioManager mockCallAudioManager;
+    private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
     private CallAudioManager.AudioServiceFactory mAudioServiceFactory;
     private static final int TEST_TIMEOUT = 500;
     private AudioManager mockAudioManager;
@@ -174,6 +177,8 @@
         mHandlerThread.start();
         mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
         mockAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+        mCommunicationDeviceTracker = new CallAudioCommunicationDeviceTracker(mContext);
+        mCommunicationDeviceTracker.setBluetoothRouteManager(mockBluetoothRouteManager);
 
         mAudioServiceFactory = new CallAudioManager.AudioServiceFactory() {
             @Override
@@ -270,7 +275,9 @@
                 mAudioServiceFactory,
                 mParams.earpieceControl,
                 mHandlerThread.getLooper(),
-                Runnable::run /** do async stuff sync for test purposes */);
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker,
+                mFeatureFlags);
         stateMachine.setCallAudioManager(mockCallAudioManager);
 
         setupMocksForParams(stateMachine, mParams);
@@ -288,17 +295,17 @@
         if (mParams.initialRoute == CallAudioState.ROUTE_BLUETOOTH) {
             stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.BT_AUDIO_CONNECTED);
         }
-        waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
 
         // Clear invocations on mocks to discard stuff from initialization
         clearInvocations();
 
         sendActionToStateMachine(stateMachine);
 
-        waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
-        waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
 
-        Handler h = stateMachine.getHandler();
+        Handler h = stateMachine.getAdapterHandler();
         waitForHandlerAction(h, TEST_TIMEOUT);
         stateMachine.quitStateMachine();
 
@@ -311,7 +318,7 @@
                 break;
             case ON:
                 if (mParams.expectedBluetoothDevice == null) {
-                    verify(mockBluetoothRouteManager).connectBluetoothAudio(null);
+                    verify(mockBluetoothRouteManager, atLeastOnce()).connectBluetoothAudio(null);
                 } else {
                     verify(mockBluetoothRouteManager).connectBluetoothAudio(
                             mParams.expectedBluetoothDevice.getAddress());
@@ -367,7 +374,9 @@
                 mAudioServiceFactory,
                 mParams.earpieceControl,
                 mHandlerThread.getLooper(),
-                Runnable::run /** do async stuff sync for test purposes */);
+                Runnable::run /** do async stuff sync for test purposes */,
+                mCommunicationDeviceTracker,
+                mFeatureFlags);
         stateMachine.setCallAudioManager(mockCallAudioManager);
 
         // Set up bluetooth and speakerphone state
@@ -388,8 +397,8 @@
         // Omit the focus-getting statement
         sendActionToStateMachine(stateMachine);
 
-        waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
-        waitForHandlerAction(stateMachine.getHandler(), TEST_TIMEOUT);
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
+        waitForHandlerAction(stateMachine.getAdapterHandler(), TEST_TIMEOUT);
 
         stateMachine.quitStateMachine();
 
diff --git a/tests/src/com/android/server/telecom/tests/CallControlTest.java b/tests/src/com/android/server/telecom/tests/CallControlTest.java
index 2613206..c69521a 100644
--- a/tests/src/com/android/server/telecom/tests/CallControlTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallControlTest.java
@@ -39,14 +39,7 @@
 import java.util.UUID;
 
 public class CallControlTest extends TelecomTestCase {
-
-    private static final PhoneAccountHandle mHandle = new PhoneAccountHandle(
-            new ComponentName("foo", "bar"), "1");
-
-    @Mock
-    private ICallControl mICallControl;
-    @Mock
-    private ClientTransactionalServiceRepository mRepository;
+    @Mock private ICallControl mICallControl;
     private static final String CALL_ID_1 = UUID.randomUUID().toString();
 
     @Override
@@ -64,15 +57,7 @@
 
     @Test
     public void testGetCallId() {
-        CallControl control = new CallControl(CALL_ID_1, mICallControl, mRepository, mHandle);
+        CallControl control = new CallControl(CALL_ID_1, mICallControl);
         assertEquals(CALL_ID_1, control.getCallId().toString());
     }
-
-    @Test
-    public void testCallControlHitsIllegalStateException() {
-        CallControl control = new CallControl(CALL_ID_1, null, mRepository, mHandle);
-        assertThrows(IllegalStateException.class, () ->
-                control.setInactive(Runnable::run, result -> {
-                }));
-    }
 }
diff --git a/tests/src/com/android/server/telecom/tests/CallEndpointControllerTest.java b/tests/src/com/android/server/telecom/tests/CallEndpointControllerTest.java
index f4008aa..b8b9560 100644
--- a/tests/src/com/android/server/telecom/tests/CallEndpointControllerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallEndpointControllerTest.java
@@ -17,6 +17,7 @@
 package com.android.server.telecom.tests;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
@@ -39,6 +40,7 @@
 import com.android.server.telecom.CallEndpointController;
 import com.android.server.telecom.CallsManager;
 import com.android.server.telecom.ConnectionServiceWrapper;
+import com.android.server.telecom.flags.FeatureFlags;
 
 import org.junit.Before;
 import org.junit.After;
@@ -50,7 +52,9 @@
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 @RunWith(JUnit4.class)
 public class CallEndpointControllerTest extends TelecomTestCase {
@@ -81,6 +85,9 @@
             availableBluetooth1);
     private static final CallAudioState audioState7 = new CallAudioState(false,
             CallAudioState.ROUTE_STREAMING, CallAudioState.ROUTE_ALL, null, availableBluetooth1);
+    private static final CallAudioState audioState8 = new CallAudioState(false,
+            CallAudioState.ROUTE_EARPIECE, CallAudioState.ROUTE_ALL, bluetoothDevice1,
+            availableBluetooth2);
 
     private CallEndpointController mCallEndpointController;
 
@@ -95,7 +102,10 @@
     @Before
     public void setUp() throws Exception {
         super.setUp();
-        mCallEndpointController = new CallEndpointController(mMockContext, mCallsManager);
+        mCallEndpointController = new CallEndpointController(
+                mMockContext,
+                mCallsManager,
+                mFeatureFlags);
         doReturn(new HashSet<>(Arrays.asList(mCall))).when(mCallsManager).getTrackedCalls();
         doReturn(mConnectionService).when(mCall).getConnectionService();
         doReturn(mCallAudioManager).when(mCallsManager).getCallAudioManager();
@@ -177,6 +187,74 @@
         verify(mConnectionService, never()).onMuteStateChanged(any(), anyBoolean());
     }
 
+    /**
+     * Ensure that {@link CallAudioManager#setAudioRoute(int, String)} is invoked when the user
+     * requests to switch to a bluetooth CallEndpoint.  This is an edge case where bluetooth is not
+     * the current CallEndpoint but the CallAudioState shows the bluetooth device is
+     * active/available.
+     */
+    @Test
+    public void testSwitchFromEarpieceToBluetooth() {
+        // simulate an audio state where the EARPIECE is active but a bluetooth device is active.
+        mCallEndpointController.onCallAudioStateChanged(null, audioState8 /* Ear but BT active */);
+        CallEndpoint btEndpoint = mCallEndpointController.getAvailableEndpoints().stream()
+                .filter(e -> e.getEndpointType() == CallEndpoint.TYPE_BLUETOOTH)
+                .toList().get(0); // get the only available BT endpoint
+
+        // verify the CallEndpointController shows EARPIECE active + BT endpoint is active device
+        assertEquals(CallEndpoint.TYPE_EARPIECE,
+                mCallEndpointController.getCurrentCallEndpoint().getEndpointType());
+        assertNotNull(btEndpoint);
+
+        // request an endpoint change from earpiece to the bluetooth
+        doReturn(audioState8).when(mCallAudioManager).getCallAudioState();
+        mCallEndpointController.requestCallEndpointChange(btEndpoint, mResultReceiver);
+
+        // verify the transaction was successful and CallAudioManager#setAudioRoute was called
+        verify(mResultReceiver, never()).send(eq(CallEndpoint.ENDPOINT_OPERATION_FAILED), any());
+        verify(mCallAudioManager, times(1)).setAudioRoute(eq(CallAudioState.ROUTE_BLUETOOTH),
+                eq(bluetoothDevice1.getAddress()));
+    }
+
+
+    /**
+     * Ensure that {@link CallAudioManager#setAudioRoute(int, String)} is invoked when the user
+     * requests to switch to from one bluetooth device to another.
+     */
+    @Test
+    public void testBtDeviceSwitch() {
+        // bluetoothDevice1 should start as active and bluetoothDevice2 is available
+        mCallEndpointController.onCallAudioStateChanged(null, audioState2 /* BT active D1 */);
+        CallEndpoint currentEndpoint = mCallEndpointController.getCurrentCallEndpoint();
+        List<CallEndpoint> btEndpoints = mCallEndpointController.getAvailableEndpoints().stream()
+                .filter(e -> e.getEndpointType() == CallEndpoint.TYPE_BLUETOOTH)
+                .toList(); // get the only available BT endpoint
+
+        // verify the initial state of the test
+        assertEquals(2, btEndpoints.size());
+        assertEquals(CallEndpoint.TYPE_BLUETOOTH, currentEndpoint.getEndpointType());
+
+        CallEndpoint otherBluetoothEndpoint = null;
+        for (CallEndpoint e : btEndpoints) {
+            if (!e.equals(currentEndpoint)) {
+                otherBluetoothEndpoint = e;
+            }
+        }
+
+        assertNotNull(otherBluetoothEndpoint);
+        assertNotEquals(currentEndpoint, otherBluetoothEndpoint);
+
+        // request an endpoint change from BT D1 --> BT D2
+        doReturn(audioState2).when(mCallAudioManager).getCallAudioState();
+        mCallEndpointController.requestCallEndpointChange(otherBluetoothEndpoint, mResultReceiver);
+
+        // verify the transaction was successful and CallAudioManager#setAudioRoute was called
+        verify(mResultReceiver, never()).send(eq(CallEndpoint.ENDPOINT_OPERATION_FAILED), any());
+        verify(mCallAudioManager, times(1))
+                .setAudioRoute(eq(CallAudioState.ROUTE_BLUETOOTH),
+                        eq(bluetoothDevice2.getAddress()));
+    }
+
     @Test
     public void testAvailableEndpointChanged() throws Exception {
         mCallEndpointController.onCallAudioStateChanged(audioState1, audioState6);
diff --git a/tests/src/com/android/server/telecom/tests/CallExtrasTest.java b/tests/src/com/android/server/telecom/tests/CallExtrasTest.java
index cf44cfe..be8e6fb 100644
--- a/tests/src/com/android/server/telecom/tests/CallExtrasTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallExtrasTest.java
@@ -27,10 +27,10 @@
 import android.telecom.Connection;
 import android.telecom.InCallService;
 import android.telecom.ParcelableCall;
-import android.test.suitebuilder.annotation.LargeTest;
-import android.test.suitebuilder.annotation.MediumTest;
 
 import androidx.test.filters.FlakyTest;
+import androidx.test.filters.LargeTest;
+import androidx.test.filters.MediumTest;
 
 import org.junit.After;
 import org.junit.Before;
diff --git a/tests/src/com/android/server/telecom/tests/CallIntentProcessorTest.java b/tests/src/com/android/server/telecom/tests/CallIntentProcessorTest.java
new file mode 100644
index 0000000..6deade4
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/CallIntentProcessorTest.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2024 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 org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ComponentInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.platform.test.flag.junit.SetFlagsRule;
+
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.internal.app.IntentForwarderActivity;
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallIntentProcessor;
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.DefaultDialerCache;
+import com.android.server.telecom.PhoneNumberUtilsAdapter;
+import com.android.server.telecom.TelephonyUtil;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoSession;
+
+import java.util.concurrent.CompletableFuture;
+
+/** Unit tests for CollIntentProcessor class. */
+@RunWith(JUnit4.class)
+public class CallIntentProcessorTest extends TelecomTestCase {
+
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+    @Mock
+    private CallsManager mCallsManager;
+    @Mock
+    private DefaultDialerCache mDefaultDialerCache;
+    @Mock
+    private Context mMockCreateContextAsUser;
+    @Mock
+    private UserManager mMockCurrentUserManager;
+    @Mock
+    private PhoneNumberUtilsAdapter mPhoneNumberUtilsAdapter;
+    @Mock
+    private PackageManager mPackageManager;
+    @Mock
+    private ResolveInfo mResolveInfo;
+    @Mock
+    private ComponentName mComponentName;
+    @Mock
+    private ComponentInfo mComponentInfo;
+    @Mock
+    private CompletableFuture<Call> mCall;
+    private CallIntentProcessor mCallIntentProcessor;
+    private static final UserHandle PRIVATE_SPACE_USERHANDLE = new UserHandle(12);
+    private static final String TEST_PACKAGE_NAME = "testPackageName";
+    private static final Uri TEST_PHONE_NUMBER = Uri.parse("tel:1234567890");
+    private static final Uri TEST_EMERGENCY_PHONE_NUMBER = Uri.parse("tel:911");
+
+    @Override
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
+        when(mContext.createContextAsUser(any(UserHandle.class), eq(0))).thenReturn(
+                mMockCreateContextAsUser);
+        when(mMockCreateContextAsUser.getSystemService(UserManager.class)).thenReturn(
+                mMockCurrentUserManager);
+        mCallIntentProcessor = new CallIntentProcessor(mContext, mCallsManager, mDefaultDialerCache,
+                mFeatureFlags);
+        when(mFeatureFlags.telecomResolveHiddenDependencies()).thenReturn(false);
+        when(mCallsManager.getPhoneNumberUtilsAdapter()).thenReturn(mPhoneNumberUtilsAdapter);
+        when(mPhoneNumberUtilsAdapter.isUriNumber(anyString())).thenReturn(true);
+        when(mCallsManager.startOutgoingCall(any(Uri.class), any(), any(Bundle.class),
+                any(UserHandle.class), any(Intent.class), anyString())).thenReturn(mCall);
+        when(mCall.thenAccept(any())).thenReturn(null);
+    }
+
+    @Test
+    public void testNonPrivateSpaceCall_noConsentDialogShown() {
+        setPrivateSpaceFlagsEnabled();
+
+        Intent intent = new Intent(Intent.ACTION_CALL);
+        intent.setData(TEST_PHONE_NUMBER);
+        intent.putExtra(CallIntentProcessor.KEY_INITIATING_USER, UserHandle.CURRENT);
+        when(mCallsManager.isSelfManaged(any(), eq(UserHandle.CURRENT))).thenReturn(false);
+
+        mCallIntentProcessor.processIntent(intent, TEST_PACKAGE_NAME);
+
+        verify(mContext, never()).startActivityAsUser(any(Intent.class), any(UserHandle.class));
+
+        // Verify that the call proceeds as normal since the dialog was not shown
+        verify(mCallsManager).startOutgoingCall(any(Uri.class), any(), any(Bundle.class),
+                eq(UserHandle.CURRENT), eq(intent), eq(TEST_PACKAGE_NAME));
+    }
+
+    @Test
+    public void testPrivateSpaceCall_isSelfManaged_noDialogShown() {
+        setPrivateSpaceFlagsEnabled();
+        markInitiatingUserAsPrivateProfile();
+        resolveAsIntentForwarderActivity();
+
+        Intent intent = new Intent(Intent.ACTION_CALL);
+        intent.setData(TEST_PHONE_NUMBER);
+        intent.putExtra(CallIntentProcessor.KEY_INITIATING_USER, PRIVATE_SPACE_USERHANDLE);
+        when(mCallsManager.isSelfManaged(any(), eq(PRIVATE_SPACE_USERHANDLE))).thenReturn(true);
+
+        mCallIntentProcessor.processIntent(intent, TEST_PACKAGE_NAME);
+
+        verify(mContext, never()).startActivityAsUser(any(Intent.class),
+                eq(PRIVATE_SPACE_USERHANDLE));
+
+        // Verify that the call proceeds as normal since the dialog was not shown
+        verify(mCallsManager).startOutgoingCall(any(Uri.class), any(), any(Bundle.class),
+                eq(PRIVATE_SPACE_USERHANDLE), eq(intent), eq(TEST_PACKAGE_NAME));
+    }
+
+    @Test
+    public void testPrivateSpaceCall_isEmergency_noDialogShown() {
+        MockitoSession session = ExtendedMockito.mockitoSession().mockStatic(
+                TelephonyUtil.class).startMocking();
+        ExtendedMockito.doReturn(true).when(
+                () -> TelephonyUtil.shouldProcessAsEmergency(any(), any()));
+
+        setPrivateSpaceFlagsEnabled();
+        markInitiatingUserAsPrivateProfile();
+        resolveAsIntentForwarderActivity();
+
+        Intent intent = new Intent(Intent.ACTION_CALL);
+        intent.setData(TEST_EMERGENCY_PHONE_NUMBER);
+        intent.putExtra(CallIntentProcessor.KEY_INITIATING_USER, PRIVATE_SPACE_USERHANDLE);
+        when(mCallsManager.isSelfManaged(any(), eq(PRIVATE_SPACE_USERHANDLE))).thenReturn(false);
+
+        mCallIntentProcessor.processIntent(intent, TEST_PACKAGE_NAME);
+
+        verify(mContext, never()).startActivityAsUser(any(Intent.class),
+                eq(PRIVATE_SPACE_USERHANDLE));
+        session.finishMocking();
+    }
+
+    @Test
+    public void testPrivateSpaceCall_showConsentDialog() {
+        setPrivateSpaceFlagsEnabled();
+        markInitiatingUserAsPrivateProfile();
+        resolveAsIntentForwarderActivity();
+
+        Intent intent = new Intent(Intent.ACTION_CALL);
+        intent.setData(TEST_PHONE_NUMBER);
+        intent.putExtra(CallIntentProcessor.KEY_INITIATING_USER, PRIVATE_SPACE_USERHANDLE);
+        when(mCallsManager.isSelfManaged(any(), eq(PRIVATE_SPACE_USERHANDLE))).thenReturn(false);
+
+        mCallIntentProcessor.processIntent(intent, TEST_PACKAGE_NAME);
+
+        // Consent dialog should be shown
+        verify(mContext).startActivityAsUser(any(Intent.class), eq(PRIVATE_SPACE_USERHANDLE));
+
+        /// Verify that the call does not proceeds as normal since the dialog was shown
+        verify(mCallsManager, never()).startOutgoingCall(any(), any(), any(), any(), any(),
+                anyString());
+    }
+
+    private void setPrivateSpaceFlagsEnabled() {
+        mSetFlagsRule.enableFlags(android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES,
+                android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_INTENT_REDIRECTION);
+    }
+
+    private void markInitiatingUserAsPrivateProfile() {
+        when(mMockCurrentUserManager.isPrivateProfile()).thenReturn(true);
+    }
+
+    private void resolveAsIntentForwarderActivity() {
+        when(mComponentName.getShortClassName()).thenReturn(
+                IntentForwarderActivity.FORWARD_INTENT_TO_PARENT);
+        when(mComponentInfo.getComponentName()).thenReturn(mComponentName);
+        when(mResolveInfo.getComponentInfo()).thenReturn(mComponentInfo);
+
+        when(mContext.getPackageManager()).thenReturn(mPackageManager);
+
+        when(mPackageManager.resolveActivityAsUser(any(Intent.class),
+                any(PackageManager.ResolveInfoFlags.class),
+                eq(PRIVATE_SPACE_USERHANDLE.getIdentifier()))).thenReturn(mResolveInfo);
+    }
+}
\ No newline at end of file
diff --git a/tests/src/com/android/server/telecom/tests/CallLogManagerTest.java b/tests/src/com/android/server/telecom/tests/CallLogManagerTest.java
index 9466220..cb04dc3 100644
--- a/tests/src/com/android/server/telecom/tests/CallLogManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallLogManagerTest.java
@@ -16,12 +16,15 @@
 
 package com.android.server.telecom.tests;
 
+import static com.android.server.telecom.tests.TelecomSystemTest.TEST_TIMEOUT;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.doAnswer;
@@ -36,6 +39,7 @@
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.content.pm.UserInfo;
 import android.content.res.Resources;
 import android.location.Country;
@@ -44,6 +48,7 @@
 import android.location.Location;
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.Handler;
 import android.os.Looper;
 import android.os.PersistableBundle;
 import android.os.SystemClock;
@@ -59,12 +64,13 @@
 import android.telecom.VideoProfile;
 import android.telephony.CarrierConfigManager;
 import android.telephony.PhoneNumberUtils;
-import android.test.suitebuilder.annotation.MediumTest;
-import android.test.suitebuilder.annotation.SmallTest;
 
 import androidx.test.filters.FlakyTest;
+import androidx.test.filters.MediumTest;
+import androidx.test.filters.SmallTest;
 
 import com.android.server.telecom.Analytics;
+import com.android.server.telecom.AnomalyReporterAdapter;
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallLogManager;
 import com.android.server.telecom.CallState;
@@ -72,6 +78,7 @@
 import com.android.server.telecom.MissedCallNotifier;
 import com.android.server.telecom.PhoneAccountRegistrar;
 import com.android.server.telecom.TelephonyUtil;
+import com.android.server.telecom.flags.FeatureFlags;
 
 import org.junit.After;
 import org.junit.Before;
@@ -85,6 +92,9 @@
 import org.mockito.stubbing.Answer;
 
 import java.util.Arrays;
+import java.util.Locale;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
 
 @RunWith(JUnit4.class)
 public class CallLogManagerTest extends TelecomTestCase {
@@ -113,6 +123,7 @@
     private static final int CURRENT_USER_ID = 0;
     private static final int OTHER_USER_ID = 10;
     private static final int MANAGED_USER_ID = 11;
+    private static final int PRIVATE_USER_ID = 12;
 
     private static final String TEST_ISO = "KR";
     private static final String TEST_ISO_2 = "JP";
@@ -123,6 +134,11 @@
     PhoneAccountRegistrar mMockPhoneAccountRegistrar;
     @Mock
     MissedCallNotifier mMissedCallNotifier;
+    @Mock
+    AnomalyReporterAdapter mAnomalyReporterAdapter;
+
+    @Mock
+    FeatureFlags mFeatureFlags;
 
     @Override
     @Before
@@ -130,7 +146,7 @@
         super.setUp();
         mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
         mCallLogManager = new CallLogManager(mContext, mMockPhoneAccountRegistrar,
-                mMissedCallNotifier);
+                mMissedCallNotifier, mAnomalyReporterAdapter, mFeatureFlags);
         mDefaultAccountHandle = new PhoneAccountHandle(
                 new ComponentName("com.android.server.telecom.tests", "CallLogManagerTest"),
                 TEST_PHONE_ACCOUNT_ID,
@@ -161,9 +177,22 @@
 
         UserManager userManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
         UserInfo userInfo = new UserInfo(CURRENT_USER_ID, "test", 0);
+        userInfo.profileGroupId = UserInfo.NO_PROFILE_GROUP_ID;
+        userInfo.userType = UserManager.USER_TYPE_FULL_SYSTEM;
+
         UserInfo otherUserInfo = new UserInfo(OTHER_USER_ID, "test2", 0);
+        otherUserInfo.profileGroupId = UserInfo.NO_PROFILE_GROUP_ID;
+        otherUserInfo.userType = UserManager.USER_TYPE_FULL_SECONDARY;
+
         UserInfo managedProfileUserInfo = new UserInfo(MANAGED_USER_ID, "test3",
-                UserInfo.FLAG_MANAGED_PROFILE);
+                UserInfo.FLAG_MANAGED_PROFILE | userInfo.FLAG_PROFILE);
+        managedProfileUserInfo.profileGroupId = 90210;
+        managedProfileUserInfo.userType = UserManager.USER_TYPE_PROFILE_MANAGED;
+
+        UserInfo privateProfileUserInfo = new UserInfo(PRIVATE_USER_ID, "private",
+                UserInfo.FLAG_PROFILE);
+        privateProfileUserInfo.profileGroupId = 90210;
+        privateProfileUserInfo.userType = UserManager.USER_TYPE_PROFILE_PRIVATE;
 
         doAnswer(new Answer<Uri>() {
             @Override
@@ -174,13 +203,44 @@
 
         when(userManager.isUserRunning(any(UserHandle.class))).thenReturn(true);
         when(userManager.isUserUnlocked(any(UserHandle.class))).thenReturn(true);
-        when(userManager.hasUserRestriction(any(String.class), any(UserHandle.class)))
+        when(userManager.hasUserRestrictionForUser(any(String.class), any(UserHandle.class)))
                 .thenReturn(false);
         when(userManager.getAliveUsers())
                 .thenReturn(Arrays.asList(userInfo, otherUserInfo, managedProfileUserInfo));
+        configureContextForUser(CURRENT_USER_ID, userInfo);
         when(userManager.getUserInfo(eq(CURRENT_USER_ID))).thenReturn(userInfo);
+
+        configureContextForUser(OTHER_USER_ID, otherUserInfo);
         when(userManager.getUserInfo(eq(OTHER_USER_ID))).thenReturn(otherUserInfo);
+
+        configureContextForUser(MANAGED_USER_ID, managedProfileUserInfo);
         when(userManager.getUserInfo(eq(MANAGED_USER_ID))).thenReturn(managedProfileUserInfo);
+
+        configureContextForUser(PRIVATE_USER_ID, privateProfileUserInfo);
+        when(userManager.getUserInfo(eq(PRIVATE_USER_ID))).thenReturn(privateProfileUserInfo);
+
+        PackageManager packageManager = mContext.getPackageManager();
+        when(packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(false);
+        when(mFeatureFlags.telecomLogExternalWearableCalls()).thenReturn(false);
+        when(mFeatureFlags.telecomResolveHiddenDependencies()).thenReturn(true);
+    }
+
+    /**
+     * Yuck; this is absolutely wretched that we have to mock things out in this way.
+     * Because the preferred way to get info about a user is to first user
+     * {@link Context#createContextAsUser(UserHandle, int)} to first get a user-specific context,
+     * and to then query the {@link UserManager} instance to see if it's a profile, we need to do
+     * all of this really gross mocking.
+     * @param userId The userid.
+     * @param info The associated userinfo.
+     */
+    private void configureContextForUser(int userId, UserInfo info) {
+        Context mockContext = mock(Context.class);
+        mComponentContextFixture.addContextForUser(UserHandle.of(userId), mockContext);
+        UserManager mockUserManager = mock(UserManager.class);
+        when(mockUserManager.getUserInfo(eq(userId))).thenReturn(info);
+        when(mockUserManager.isProfile()).thenReturn(info.isProfile());
+        when(mockContext.getSystemService(eq(UserManager.class))).thenReturn(mockUserManager);
     }
 
     @Override
@@ -215,7 +275,7 @@
     @Test
     public void testDontLogChoosingAccountCall() {
         when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
-                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
         Call fakeCall = makeFakeCall(
                 DisconnectCause.OTHER, // disconnectCauseCode
                 false, // isConference
@@ -318,7 +378,7 @@
     @Test
     public void testLogCallDirectionOutgoing() {
         when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
-                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
         Call fakeOutgoingCall = makeFakeCall(
                 DisconnectCause.OTHER, // disconnectCauseCode
                 false, // isConference
@@ -343,7 +403,7 @@
     @Test
     public void testLogCallDirectionIncoming() {
         when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
-                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
         Call fakeIncomingCall = makeFakeCall(
                 DisconnectCause.OTHER, // disconnectCauseCode
                 false, // isConference
@@ -357,6 +417,7 @@
                 VIA_NUMBER_STRING, // viaNumber
                 null
         );
+        when(mFeatureFlags.addCallUriForMissedCalls()).thenReturn(true);
         mCallLogManager.onCallStateChanged(fakeIncomingCall, CallState.ACTIVE,
                 CallState.DISCONNECTED);
         ContentValues insertedValues = verifyInsertionWithCapture(CURRENT_USER_ID);
@@ -366,9 +427,9 @@
 
     @MediumTest
     @Test
-    public void testLogCallDirectionMissed() {
+    public void testLogCallDirectionMissedAddCallUriForMissedCallsFlagOff() {
         when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
-                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
         Call fakeMissedCall = makeFakeCall(
                 DisconnectCause.MISSED, // disconnectCauseCode
                 false, // isConference
@@ -382,6 +443,7 @@
                 VIA_NUMBER_STRING, // viaNumber
                 null
         );
+        when(mFeatureFlags.addCallUriForMissedCalls()).thenReturn(false);
 
         mCallLogManager.onCallStateChanged(fakeMissedCall, CallState.ACTIVE,
                 CallState.DISCONNECTED);
@@ -390,14 +452,46 @@
                 Integer.valueOf(CallLog.Calls.MISSED_TYPE));
         // Timeout needed because showMissedCallNotification is called from onPostExecute.
         verify(mMissedCallNotifier, timeout(TEST_TIMEOUT_MILLIS))
-                .showMissedCallNotification(any(MissedCallNotifier.CallInfo.class));
+                .showMissedCallNotification(any(MissedCallNotifier.CallInfo.class),
+                        /* uri= */ eq(null));
+    }
+
+    @MediumTest
+    @Test
+    public void testLogCallDirectionMissedAddCallUriForMissedCallsFlagOn() {
+        when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
+                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
+        Call fakeMissedCall = makeFakeCall(
+                DisconnectCause.MISSED, // disconnectCauseCode
+                false, // isConference
+                true, // isIncoming
+                1L, // creationTimeMillis
+                1000L, // ageMillis
+                TEL_PHONEHANDLE, // callHandle
+                mDefaultAccountHandle, // phoneAccountHandle
+                NO_VIDEO_STATE, // callVideoState
+                POST_DIAL_STRING, // postDialDigits
+                VIA_NUMBER_STRING, // viaNumber
+                null
+        );
+        when(mFeatureFlags.addCallUriForMissedCalls()).thenReturn(true);
+
+        mCallLogManager.onCallStateChanged(fakeMissedCall, CallState.ACTIVE,
+                CallState.DISCONNECTED);
+        ContentValues insertedValues = verifyInsertionWithCapture(CURRENT_USER_ID);
+        assertEquals(insertedValues.getAsInteger(CallLog.Calls.TYPE),
+                Integer.valueOf(CallLog.Calls.MISSED_TYPE));
+        // Timeout needed because showMissedCallNotification is called from onPostExecute.
+        verify(mMissedCallNotifier, timeout(TEST_TIMEOUT_MILLIS))
+                .showMissedCallNotification(any(MissedCallNotifier.CallInfo.class),
+                        /* uri= */ any(Uri.class));
     }
 
     @MediumTest
     @Test
     public void testLogCallDirectionRejected() {
         when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
-                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
         Call fakeMissedCall = makeFakeCall(
                 DisconnectCause.REJECTED, // disconnectCauseCode
                 false, // isConference
@@ -423,7 +517,7 @@
     @Test
     public void testCreationTimeAndAge() {
         when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
-                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
         long currentTime = System.currentTimeMillis();
         long duration = 1000L;
         Call fakeCall = makeFakeCall(
@@ -451,7 +545,7 @@
     @Test
     public void testLogPhoneAccountId() {
         when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
-                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
         Call fakeCall = makeFakeCall(
                 DisconnectCause.OTHER, // disconnectCauseCode
                 false, // isConference
@@ -475,7 +569,7 @@
     @Test
     public void testLogCorrectPhoneNumber() {
         when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
-                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
         Call fakeCall = makeFakeCall(
                 DisconnectCause.OTHER, // disconnectCauseCode
                 false, // isConference
@@ -502,7 +596,7 @@
     @Test
     public void testLogCallVideoFeatures() {
         when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
-                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
         Call fakeVideoCall = makeFakeCall(
                 DisconnectCause.OTHER, // disconnectCauseCode
                 false, // isConference
@@ -560,7 +654,8 @@
                 ContentProvider.maybeAddUserId(CallLog.Calls.CONTENT_URI, OTHER_USER_ID)));
         assertFalse(uris.getAllValues().contains(
                 ContentProvider.maybeAddUserId(CallLog.Calls.CONTENT_URI, MANAGED_USER_ID)));
-
+        assertFalse(uris.getAllValues().contains(
+                ContentProvider.maybeAddUserId(CallLog.Calls.CONTENT_URI, PRIVATE_USER_ID)));
         for (ContentValues v : values.getAllValues()) {
             assertEquals(v.getAsInteger(CallLog.Calls.TYPE),
                     Integer.valueOf(CallLog.Calls.OUTGOING_TYPE));
@@ -605,7 +700,8 @@
                 ContentProvider.maybeAddUserId(CallLog.Calls.CONTENT_URI, OTHER_USER_ID)));
         assertFalse(uris.getAllValues().contains(
                 ContentProvider.maybeAddUserId(CallLog.Calls.CONTENT_URI, MANAGED_USER_ID)));
-
+        assertFalse(uris.getAllValues().contains(
+                ContentProvider.maybeAddUserId(CallLog.Calls.CONTENT_URI, PRIVATE_USER_ID)));
         for (ContentValues v : values.getAllValues()) {
             assertEquals(v.getAsInteger(CallLog.Calls.TYPE),
                     Integer.valueOf(CallLog.Calls.INCOMING_TYPE));
@@ -615,6 +711,8 @@
     @MediumTest
     @Test
     public void testLogCallDirectionOutgoingWithMultiUserCapabilityFromManagedProfile() {
+        UserManager userManager = mContext.getSystemService(UserManager.class);
+        when(userManager.isManagedProfile()).thenReturn(true);
         when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
                 .thenReturn(makeFakePhoneAccount(mManagedProfileAccountHandle,
                         PhoneAccount.CAPABILITY_MULTI_USER));
@@ -643,6 +741,72 @@
     }
 
     @MediumTest
+    @Test
+    public void testLogCallDirectionOutgoingWithMultiUserCapabilityFromPrivateProfile() {
+        when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
+                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle,
+                        PhoneAccount.CAPABILITY_MULTI_USER));
+        Call fakeOutgoingCall = makeFakeCall(
+                DisconnectCause.OTHER, // disconnectCauseCode
+                false, // isConference
+                false, // isIncoming
+                1L, // creationTimeMillis
+                1000L, // ageMillis
+                TEL_PHONEHANDLE, // callHandle
+                mDefaultAccountHandle, // phoneAccountHandle
+                NO_VIDEO_STATE, // callVideoState
+                POST_DIAL_STRING, // postDialDigits
+                VIA_NUMBER_STRING, // viaNumber
+                UserHandle.of(PRIVATE_USER_ID)
+        );
+        mCallLogManager.onCallStateChanged(fakeOutgoingCall, CallState.ACTIVE,
+                CallState.DISCONNECTED);
+
+        // Outgoing call placed through private space should only show up in the private space
+        // call logs.
+        verifyNoInsertionInUser(CURRENT_USER_ID);
+        verifyNoInsertionInUser(OTHER_USER_ID);
+        verifyNoInsertionInUser(MANAGED_USER_ID);
+        ContentValues insertedValues = verifyInsertionWithCapture(PRIVATE_USER_ID);
+        assertEquals(insertedValues.getAsInteger(CallLog.Calls.TYPE),
+                Integer.valueOf(CallLog.Calls.OUTGOING_TYPE));
+    }
+
+    @MediumTest
+    @Test
+    public void testLogCallDirectionOutgoingWithMultiUserCapabilityFromPrivateProfileNoRefactor() {
+        // Same as the above test, but turns off the hidden deps refactor; there are some minor
+        // differences in how we detect profiles, so we want to ensure this works both ways.
+        when(mFeatureFlags.telecomResolveHiddenDependencies()).thenReturn(false);
+        when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
+                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle,
+                        PhoneAccount.CAPABILITY_MULTI_USER));
+        Call fakeOutgoingCall = makeFakeCall(
+                DisconnectCause.OTHER, // disconnectCauseCode
+                false, // isConference
+                false, // isIncoming
+                1L, // creationTimeMillis
+                1000L, // ageMillis
+                TEL_PHONEHANDLE, // callHandle
+                mDefaultAccountHandle, // phoneAccountHandle
+                NO_VIDEO_STATE, // callVideoState
+                POST_DIAL_STRING, // postDialDigits
+                VIA_NUMBER_STRING, // viaNumber
+                UserHandle.of(PRIVATE_USER_ID)
+        );
+        mCallLogManager.onCallStateChanged(fakeOutgoingCall, CallState.ACTIVE,
+                CallState.DISCONNECTED);
+
+        // Outgoing call placed through work dialer should be inserted to managed profile only.
+        verifyNoInsertionInUser(CURRENT_USER_ID);
+        verifyNoInsertionInUser(OTHER_USER_ID);
+        verifyNoInsertionInUser(MANAGED_USER_ID);
+        ContentValues insertedValues = verifyInsertionWithCapture(PRIVATE_USER_ID);
+        assertEquals(insertedValues.getAsInteger(CallLog.Calls.TYPE),
+                Integer.valueOf(CallLog.Calls.OUTGOING_TYPE));
+    }
+
+    @MediumTest
     @FlakyTest
     @Test
     public void testLogCallDirectionOutgoingFromManagedProfile() {
@@ -668,6 +832,7 @@
         // profile only.
         verifyNoInsertionInUser(CURRENT_USER_ID);
         verifyNoInsertionInUser(OTHER_USER_ID);
+        verifyNoInsertionInUser(PRIVATE_USER_ID);
         ContentValues insertedValues = verifyInsertionWithCapture(MANAGED_USER_ID);
         assertEquals(insertedValues.getAsInteger(CallLog.Calls.TYPE),
                 Integer.valueOf(CallLog.Calls.OUTGOING_TYPE));
@@ -698,6 +863,7 @@
         // profile only.
         verifyNoInsertionInUser(CURRENT_USER_ID);
         verifyNoInsertionInUser(OTHER_USER_ID);
+        verifyNoInsertionInUser(PRIVATE_USER_ID);
         ContentValues insertedValues = verifyInsertionWithCapture(MANAGED_USER_ID);
         assertEquals(insertedValues.getAsInteger(CallLog.Calls.TYPE),
                 Integer.valueOf(Calls.INCOMING_TYPE));
@@ -710,7 +876,7 @@
     @Test
     public void testLogCallDataUsageSet() {
         when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
-                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
         Call fakeVideoCall = makeFakeCall(
                 DisconnectCause.OTHER, // disconnectCauseCode
                 false, // isConference
@@ -737,7 +903,7 @@
     @Test
     public void testLogCallDataUsageNotSet() {
         when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
-                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
         Call fakeVideoCall = makeFakeCall(
                 DisconnectCause.OTHER, // disconnectCauseCode
                 false, // isConference
@@ -788,19 +954,34 @@
         assertEquals(1, insertedValues.getAsInteger(Calls.IS_READ).intValue());
     }
 
-    @SmallTest
     @Test
-    public void testCountryIso_setCache() {
-        Country testCountry = new Country(TEST_ISO, Country.COUNTRY_SOURCE_LOCALE);
-        CountryDetector mockDetector = (CountryDetector) mContext.getSystemService(
-                Context.COUNTRY_DETECTOR);
-        when(mockDetector.detectCountry()).thenReturn(testCountry);
+    public void testLogCallWhenExternalCallOnWatch() {
+        when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
+                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
+        PackageManager packageManager = mContext.getPackageManager();
+        when(packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(true);
+        when(mFeatureFlags.telecomLogExternalWearableCalls()).thenReturn(true);
+        Call fakeMissedCall = makeFakeCall(
+                DisconnectCause.REJECTED, // disconnectCauseCode
+                false, // isConference
+                true, // isIncoming
+                1L, // creationTimeMillis
+                1000L, // ageMillis
+                TEL_PHONEHANDLE, // callHandle
+                mDefaultAccountHandle, // phoneAccountHandle
+                NO_VIDEO_STATE, // callVideoState
+                POST_DIAL_STRING, // postDialDigits
+                VIA_NUMBER_STRING, // viaNumber
+                null
+        );
+        when(fakeMissedCall.isExternalCall()).thenReturn(true);
 
-        String resultIso = mCallLogManager.getCountryIso();
-
-        verifyCountryIso(mockDetector, resultIso);
+        mCallLogManager.onCallStateChanged(fakeMissedCall, CallState.ACTIVE,
+                CallState.DISCONNECTED);
+        verifyInsertionWithCapture(CURRENT_USER_ID);
     }
 
+
     @SmallTest
     @Test
     public void testCountryIso_newCountryDetected() {
@@ -808,17 +989,28 @@
         Country testCountry2 = new Country(TEST_ISO_2, Country.COUNTRY_SOURCE_LOCALE);
         CountryDetector mockDetector = (CountryDetector) mContext.getSystemService(
                 Context.COUNTRY_DETECTOR);
-        when(mockDetector.detectCountry()).thenReturn(testCountry);
-        // Put TEST_ISO in the Cache
+        Handler handler = new Handler(Looper.getMainLooper());
+
+        String initialIso = mCallLogManager.getCountryIso();
+        assertEquals(Locale.getDefault().getCountry(), initialIso);
+
+        ArgumentCaptor<Consumer<Country>> capture = ArgumentCaptor.forClass(Consumer.class);
+        verify(mockDetector).registerCountryDetectorCallback(
+                any(Executor.class), capture.capture());
+        Consumer<Country> countryConsumer = capture.getValue();
+
+        countryConsumer.accept(testCountry);
+        waitForHandlerAction(handler, TEST_TIMEOUT);
         String resultIso = mCallLogManager.getCountryIso();
-        ArgumentCaptor<CountryListener> captor = verifyCountryIso(mockDetector, resultIso);
+        assertEquals(TEST_ISO, resultIso);
 
-        // Change ISO to TEST_ISO_2
-        CountryListener listener = captor.getValue();
-        listener.onCountryDetected(testCountry2);
-
-        String resultIso2 = mCallLogManager.getCountryIso();
-        assertEquals(TEST_ISO_2, resultIso2);
+        // If default locale is equal to TEST_ISO, test another ISO to assure working functionality.
+        if (initialIso.equals(TEST_ISO)) {
+            countryConsumer.accept(testCountry2);
+            waitForHandlerAction(handler, TEST_TIMEOUT);
+            resultIso = mCallLogManager.getCountryIso();
+            assertEquals(TEST_ISO_2, resultIso);
+        }
     }
 
     @SmallTest
@@ -893,6 +1085,56 @@
 
     @SmallTest
     @Test
+    public void testDoNotLogCallExtra() {
+        when(mFeatureFlags.telecomSkipLogBasedOnExtra()).thenReturn(true);
+        Call fakeCall = makeFakeCall(
+                DisconnectCause.LOCAL, // disconnectCauseCode
+                false, // isConference
+                true, // isIncoming
+                1L, // creationTimeMillis
+                1000L, // ageMillis
+                TEL_PHONEHANDLE, // callHandle
+                mDefaultAccountHandle, // phoneAccountHandle
+                NO_VIDEO_STATE, // callVideoState
+                POST_DIAL_STRING, // postDialDigits
+                VIA_NUMBER_STRING, // viaNumber
+                UserHandle.of(CURRENT_USER_ID)
+        );
+        Bundle extras = new Bundle();
+        extras.putBoolean(TelecomManager.EXTRA_DO_NOT_LOG_CALL, true);
+        when(fakeCall.getExtras()).thenReturn(extras);
+
+        assertFalse(mCallLogManager.shouldLogDisconnectedCall(fakeCall, CallState.DISCONNECTED,
+                false /* isCanceled */));
+    }
+
+    @SmallTest
+    @Test
+    public void testIgnoresDoNotLogCallExtra_whenFlagDisabled() {
+        when(mFeatureFlags.telecomSkipLogBasedOnExtra()).thenReturn(false);
+        Call fakeCall = makeFakeCall(
+                DisconnectCause.LOCAL, // disconnectCauseCode
+                false, // isConference
+                true, // isIncoming
+                1L, // creationTimeMillis
+                1000L, // ageMillis
+                TEL_PHONEHANDLE, // callHandle
+                mDefaultAccountHandle, // phoneAccountHandle
+                NO_VIDEO_STATE, // callVideoState
+                POST_DIAL_STRING, // postDialDigits
+                VIA_NUMBER_STRING, // viaNumber
+                UserHandle.of(CURRENT_USER_ID)
+        );
+        Bundle extras = new Bundle();
+        extras.putBoolean(TelecomManager.EXTRA_DO_NOT_LOG_CALL, true);
+        when(fakeCall.getExtras()).thenReturn(extras);
+
+        assertTrue(mCallLogManager.shouldLogDisconnectedCall(fakeCall, CallState.DISCONNECTED,
+                false /* isCanceled */));
+    }
+
+    @SmallTest
+    @Test
     public void testDoNotLogConferenceWithChildren() {
         Call fakeCall = makeFakeCall(
                 DisconnectCause.LOCAL, // disconnectCauseCode
diff --git a/tests/src/com/android/server/telecom/tests/CallRecordingTonePlayerTest.java b/tests/src/com/android/server/telecom/tests/CallRecordingTonePlayerTest.java
index b5c6468..60952d3 100644
--- a/tests/src/com/android/server/telecom/tests/CallRecordingTonePlayerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallRecordingTonePlayerTest.java
@@ -28,7 +28,6 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -41,11 +40,11 @@
 import android.media.AudioRecordingConfiguration;
 import android.media.MediaPlayer;
 import android.media.MediaRecorder;
-import android.net.Uri;
 import android.os.Handler;
 import android.os.Looper;
 import android.telecom.PhoneAccountHandle;
-import android.test.suitebuilder.annotation.MediumTest;
+
+import androidx.test.filters.MediumTest;
 
 import com.android.dx.mockito.inline.extended.ExtendedMockito;
 import com.android.server.telecom.Call;
diff --git a/tests/src/com/android/server/telecom/tests/CallRedirectionProcessorTest.java b/tests/src/com/android/server/telecom/tests/CallRedirectionProcessorTest.java
index 01446d1..241216a 100644
--- a/tests/src/com/android/server/telecom/tests/CallRedirectionProcessorTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallRedirectionProcessorTest.java
@@ -55,10 +55,10 @@
 
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Matchers.anyString;
-import static org.mockito.Matchers.eq;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.times;
@@ -378,4 +378,22 @@
         verify(mContext, times(1)).
                 unbindService(any(ServiceConnection.class));
     }
+
+    /**
+     * Verifies that calling formatNumberToE164 will not crash when Telephony is not present and
+     * we can't ascertain the network country ISO.
+     */
+    @Test
+    public void testFormatNumberToE164WhenNoTelephony() {
+        // Need to do this even though we're just testing the helper
+        startProcessWithNoGateWayInfo();
+
+        CallRedirectionProcessorHelper helper = new CallRedirectionProcessorHelper(mContext,
+                mCallsManager, mPhoneAccountRegistrar);
+        when(mComponentContextFixture.getTelephonyManager().getNetworkCountryIso())
+                .thenThrow(new UnsupportedOperationException("Bee boop"));
+        assertEquals(Uri.fromParts("tel", "6505551212", null),
+                helper.formatNumberToE164(
+                        Uri.fromParts("tel", "6505551212", null)));
+    }
 }
diff --git a/tests/src/com/android/server/telecom/tests/CallScreeningServiceFilterTest.java b/tests/src/com/android/server/telecom/tests/CallScreeningServiceFilterTest.java
index 4d8d497..d1427db 100644
--- a/tests/src/com/android/server/telecom/tests/CallScreeningServiceFilterTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallScreeningServiceFilterTest.java
@@ -43,7 +43,8 @@
 import android.telecom.ParcelableCall;
 import android.telecom.PhoneAccountHandle;
 import android.telecom.TelecomManager;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.filters.SmallTest;
 
 import com.android.internal.telecom.ICallScreeningAdapter;
 import com.android.internal.telecom.ICallScreeningService;
diff --git a/tests/src/com/android/server/telecom/tests/CallTest.java b/tests/src/com/android/server/telecom/tests/CallTest.java
index 997e7dd..a22d2ca 100644
--- a/tests/src/com/android/server/telecom/tests/CallTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallTest.java
@@ -21,24 +21,30 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.content.ComponentName;
 import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
 import android.graphics.Bitmap;
 import android.graphics.drawable.ColorDrawable;
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.UserHandle;
 import android.telecom.CallAttributes;
+import android.telecom.CallEndpoint;
 import android.telecom.CallerInfo;
 import android.telecom.Connection;
 import android.telecom.DisconnectCause;
@@ -50,11 +56,14 @@
 import android.telecom.TelecomManager;
 import android.telecom.VideoProfile;
 import android.telephony.CallQuality;
-import android.test.suitebuilder.annotation.SmallTest;
 import android.widget.Toast;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
 
+import com.android.server.telecom.CachedAvailableEndpointsChange;
+import com.android.server.telecom.CachedCurrentEndpointChange;
+import com.android.server.telecom.CachedMuteStateChange;
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallIdMapper;
 import com.android.server.telecom.CallState;
@@ -62,6 +71,7 @@
 import com.android.server.telecom.CallsManager;
 import com.android.server.telecom.ClockProxy;
 import com.android.server.telecom.ConnectionServiceWrapper;
+import com.android.server.telecom.EmergencyCallHelper;
 import com.android.server.telecom.PhoneAccountRegistrar;
 import com.android.server.telecom.PhoneNumberUtilsAdapter;
 import com.android.server.telecom.TelecomSystem;
@@ -77,6 +87,7 @@
 import org.mockito.Mockito;
 
 import java.util.Collections;
+import java.util.Set;
 
 @RunWith(AndroidJUnit4.class)
 public class CallTest extends TelecomTestCase {
@@ -99,7 +110,6 @@
     @Mock private PhoneAccountRegistrar mMockPhoneAccountRegistrar;
     @Mock private ClockProxy mMockClockProxy;
     @Mock private ToastFactory mMockToastProxy;
-    @Mock private Toast mMockToast;
     @Mock private PhoneNumberUtilsAdapter mMockPhoneNumberUtilsAdapter;
     @Mock private ConnectionServiceWrapper mMockConnectionService;
     @Mock private TransactionalServiceWrapper mMockTransactionalService;
@@ -116,7 +126,14 @@
                 eq(SIM_1_HANDLE));
         doReturn(new ComponentName(mContext, CallTest.class))
                 .when(mMockConnectionService).getComponentName();
-        doReturn(mMockToast).when(mMockToastProxy).makeText(any(), anyInt(), anyInt());
+        doReturn(UserHandle.CURRENT).when(mMockCallsManager).getCurrentUserHandle();
+        Resources mockResources = mContext.getResources();
+        when(mockResources.getBoolean(R.bool.skip_loading_canned_text_response))
+                .thenReturn(false);
+        when(mockResources.getBoolean(R.bool.skip_incoming_caller_info_query))
+                .thenReturn(false);
+        EmergencyCallHelper helper = mock(EmergencyCallHelper.class);
+        doReturn(helper).when(mMockCallsManager).getEmergencyCallHelper();
     }
 
     @After
@@ -136,6 +153,211 @@
     }
 
     /**
+     * Verify Call#setVideoState will only upgrade to video if the PhoneAccount supports video
+     * state capabilities
+     */
+    @Test
+    @SmallTest
+    public void testSetVideoStateForTransactionalCalls() {
+        Call call = createCall("1", Call.CALL_DIRECTION_INCOMING);
+        TransactionalServiceWrapper tsw = Mockito.mock(TransactionalServiceWrapper.class);
+        call.setIsTransactionalCall(true);
+        call.setTransactionServiceWrapper(tsw);
+        assertTrue(call.isTransactionalCall());
+        assertNotNull(call.getTransactionServiceWrapper());
+        when(mFeatureFlags.transactionalVideoState()).thenReturn(true);
+
+        // VoIP apps using transactional APIs must register a PhoneAccount that supports
+        // video calling capabilities or the video state will be defaulted to audio
+        assertFalse(call.isVideoCallingSupportedByPhoneAccount());
+        call.setVideoState(VideoProfile.STATE_BIDIRECTIONAL);
+        assertEquals(VideoProfile.STATE_AUDIO_ONLY, call.getVideoState());
+
+        call.setVideoCallingSupportedByPhoneAccount(true);
+        assertTrue(call.isVideoCallingSupportedByPhoneAccount());
+
+        // After the PhoneAccount signals it supports video calling, video state changes can occur
+        call.setVideoState(VideoProfile.STATE_BIDIRECTIONAL);
+        assertEquals(VideoProfile.STATE_BIDIRECTIONAL, call.getVideoState());
+        verify(tsw, times(1)).onVideoStateChanged(call, CallAttributes.VIDEO_CALL);
+    }
+
+    /**
+     * Verify all video state changes are echoed out to the TransactionalServiceWrapper
+     */
+    @Test
+    @SmallTest
+    public void testToggleTransactionalVideoState() {
+        Call call = createCall("1", Call.CALL_DIRECTION_INCOMING);
+        TransactionalServiceWrapper tsw = Mockito.mock(TransactionalServiceWrapper.class);
+        call.setIsTransactionalCall(true);
+        call.setTransactionServiceWrapper(tsw);
+        call.setVideoCallingSupportedByPhoneAccount(true);
+        assertTrue(call.isTransactionalCall());
+        assertNotNull(call.getTransactionServiceWrapper());
+        assertTrue(call.isVideoCallingSupportedByPhoneAccount());
+        when(mFeatureFlags.transactionalVideoState()).thenReturn(true);
+
+        call.setVideoState(VideoProfile.STATE_BIDIRECTIONAL);
+        assertEquals(VideoProfile.STATE_BIDIRECTIONAL, call.getVideoState());
+        verify(tsw, times(1)).onVideoStateChanged(call, CallAttributes.VIDEO_CALL);
+
+        call.setVideoState(VideoProfile.STATE_BIDIRECTIONAL);
+        assertEquals(VideoProfile.STATE_BIDIRECTIONAL, call.getVideoState());
+        verify(tsw, times(2)).onVideoStateChanged(call, CallAttributes.VIDEO_CALL);
+
+        call.setVideoState(VideoProfile.STATE_AUDIO_ONLY);
+        assertEquals(VideoProfile.STATE_AUDIO_ONLY, call.getVideoState());
+        verify(tsw, times(1)).onVideoStateChanged(call, CallAttributes.AUDIO_CALL);
+
+        call.setVideoState(VideoProfile.STATE_BIDIRECTIONAL);
+        assertEquals(VideoProfile.STATE_BIDIRECTIONAL, call.getVideoState());
+        verify(tsw, times(3)).onVideoStateChanged(call, CallAttributes.VIDEO_CALL);
+    }
+
+    @Test
+    public void testMultipleCachedMuteStateChanges() {
+        when(mFeatureFlags.cacheCallAudioCallbacks()).thenReturn(true);
+        TransactionalServiceWrapper tsw = Mockito.mock(TransactionalServiceWrapper.class);
+        Call call = createCall("1", Call.CALL_DIRECTION_INCOMING);
+
+        assertNull(call.getTransactionServiceWrapper());
+
+        call.cacheServiceCallback(new CachedMuteStateChange(true));
+        assertEquals(1, call.getCachedServiceCallbacks().size());
+
+        call.cacheServiceCallback(new CachedMuteStateChange(false));
+        assertEquals(1, call.getCachedServiceCallbacks().size());
+
+        CachedMuteStateChange currentCacheMuteState = (CachedMuteStateChange) call
+                .getCachedServiceCallbacks()
+                .get(CachedMuteStateChange.ID);
+
+        assertFalse(currentCacheMuteState.isMuted());
+
+        call.setTransactionServiceWrapper(tsw);
+        verify(tsw, times(1)).onMuteStateChanged(any(), eq(false));
+        assertEquals(0, call.getCachedServiceCallbacks().size());
+    }
+
+    @Test
+    public void testMultipleCachedCurrentEndpointChanges() {
+        when(mFeatureFlags.cacheCallAudioCallbacks()).thenReturn(true);
+        TransactionalServiceWrapper tsw = Mockito.mock(TransactionalServiceWrapper.class);
+        CallEndpoint earpiece = Mockito.mock(CallEndpoint.class);
+        CallEndpoint speaker = Mockito.mock(CallEndpoint.class);
+        when(earpiece.getEndpointType()).thenReturn(CallEndpoint.TYPE_EARPIECE);
+        when(speaker.getEndpointType()).thenReturn(CallEndpoint.TYPE_SPEAKER);
+
+        Call call = createCall("1", Call.CALL_DIRECTION_INCOMING);
+
+        assertNull(call.getTransactionServiceWrapper());
+
+        call.cacheServiceCallback(new CachedCurrentEndpointChange(earpiece));
+        assertEquals(1, call.getCachedServiceCallbacks().size());
+
+        call.cacheServiceCallback(new CachedCurrentEndpointChange(speaker));
+        assertEquals(1, call.getCachedServiceCallbacks().size());
+
+        CachedCurrentEndpointChange currentEndpointChange = (CachedCurrentEndpointChange) call
+                .getCachedServiceCallbacks()
+                .get(CachedCurrentEndpointChange.ID);
+
+        assertEquals(CallEndpoint.TYPE_SPEAKER,
+                currentEndpointChange.getCurrentCallEndpoint().getEndpointType());
+
+        call.setTransactionServiceWrapper(tsw);
+        verify(tsw, times(1)).onCallEndpointChanged(any(), any());
+        assertEquals(0, call.getCachedServiceCallbacks().size());
+    }
+
+    @Test
+    public void testMultipleCachedAvailableEndpointChanges() {
+        when(mFeatureFlags.cacheCallAudioCallbacks()).thenReturn(true);
+        TransactionalServiceWrapper tsw = Mockito.mock(TransactionalServiceWrapper.class);
+        CallEndpoint earpiece = Mockito.mock(CallEndpoint.class);
+        CallEndpoint bluetooth = Mockito.mock(CallEndpoint.class);
+        Set<CallEndpoint> initialSet = Set.of(earpiece);
+        Set<CallEndpoint> finalSet = Set.of(earpiece, bluetooth);
+        when(earpiece.getEndpointType()).thenReturn(CallEndpoint.TYPE_EARPIECE);
+        when(bluetooth.getEndpointType()).thenReturn(CallEndpoint.TYPE_BLUETOOTH);
+
+        Call call = createCall("1", Call.CALL_DIRECTION_INCOMING);
+
+        assertNull(call.getTransactionServiceWrapper());
+
+        call.cacheServiceCallback(new CachedAvailableEndpointsChange(initialSet));
+        assertEquals(1, call.getCachedServiceCallbacks().size());
+
+        call.cacheServiceCallback(new CachedAvailableEndpointsChange(finalSet));
+        assertEquals(1, call.getCachedServiceCallbacks().size());
+
+        CachedAvailableEndpointsChange availableEndpoints = (CachedAvailableEndpointsChange) call
+                .getCachedServiceCallbacks()
+                .get(CachedAvailableEndpointsChange.ID);
+
+        assertEquals(2, availableEndpoints.getAvailableEndpoints().size());
+
+        call.setTransactionServiceWrapper(tsw);
+        verify(tsw, times(1)).onAvailableCallEndpointsChanged(any(), any());
+        assertEquals(0, call.getCachedServiceCallbacks().size());
+    }
+
+    /**
+     * verify that if multiple types of cached callbacks are added to the call, the call executes
+     * all the callbacks once the service is set.
+     */
+    @Test
+    public void testAllCachedCallbacks() {
+        when(mFeatureFlags.cacheCallAudioCallbacks()).thenReturn(true);
+        TransactionalServiceWrapper tsw = Mockito.mock(TransactionalServiceWrapper.class);
+        CallEndpoint earpiece = Mockito.mock(CallEndpoint.class);
+        CallEndpoint bluetooth = Mockito.mock(CallEndpoint.class);
+        Set<CallEndpoint> availableEndpointsSet = Set.of(earpiece, bluetooth);
+        when(earpiece.getEndpointType()).thenReturn(CallEndpoint.TYPE_EARPIECE);
+        when(bluetooth.getEndpointType()).thenReturn(CallEndpoint.TYPE_BLUETOOTH);
+        Call call = createCall("1", Call.CALL_DIRECTION_INCOMING);
+
+        // The call should have a null service so that callbacks are cached
+        assertNull(call.getTransactionServiceWrapper());
+
+        // add cached callbacks
+        call.cacheServiceCallback(new CachedMuteStateChange(false));
+        assertEquals(1, call.getCachedServiceCallbacks().size());
+        call.cacheServiceCallback(new CachedCurrentEndpointChange(earpiece));
+        assertEquals(2, call.getCachedServiceCallbacks().size());
+        call.cacheServiceCallback(new CachedAvailableEndpointsChange(availableEndpointsSet));
+        assertEquals(3, call.getCachedServiceCallbacks().size());
+
+        // verify the cached callbacks are stored properly within the cache map and the values
+        // can be evaluated
+        CachedMuteStateChange currentCacheMuteState = (CachedMuteStateChange) call
+                .getCachedServiceCallbacks()
+                .get(CachedMuteStateChange.ID);
+        CachedCurrentEndpointChange currentEndpointChange = (CachedCurrentEndpointChange) call
+                .getCachedServiceCallbacks()
+                .get(CachedCurrentEndpointChange.ID);
+        CachedAvailableEndpointsChange availableEndpoints = (CachedAvailableEndpointsChange) call
+                .getCachedServiceCallbacks()
+                .get(CachedAvailableEndpointsChange.ID);
+        assertFalse(currentCacheMuteState.isMuted());
+        assertEquals(CallEndpoint.TYPE_EARPIECE,
+                currentEndpointChange.getCurrentCallEndpoint().getEndpointType());
+        assertEquals(2, availableEndpoints.getAvailableEndpoints().size());
+
+        // set the service to a non-null value
+        call.setTransactionServiceWrapper(tsw);
+
+        // ensure the cached callbacks were executed
+        verify(tsw, times(1)).onMuteStateChanged(any(), anyBoolean());
+        verify(tsw, times(1)).onCallEndpointChanged(any(), any());
+        verify(tsw, times(1)).onAvailableCallEndpointsChanged(any(), any());
+
+        // the cache map should be cleared
+        assertEquals(0, call.getCachedServiceCallbacks().size());
+    }
+
+    /**
      * Basic tests to check which call states are considered transitory.
      */
     @Test
@@ -200,7 +422,8 @@
                 false /* shouldAttachToExistingConnection*/,
                 false /* isConference */,
                 mMockClockProxy,
-                mMockToastProxy);
+                mMockToastProxy,
+                mFeatureFlags);
 
         // To start with connection creation isn't complete.
         assertFalse(call.isCreateConnectionComplete());
@@ -301,7 +524,6 @@
         doReturn(true).when(mMockCallsManager).isInEmergencyCall();
         call.pullExternalCall();
         verify(mMockConnectionService, never()).pullExternalCall(any());
-        verify(mMockToast).show();
     }
 
     @Test
@@ -338,7 +560,8 @@
                 false /* shouldAttachToExistingConnection*/,
                 true /* isConference */,
                 mMockClockProxy,
-                mMockToastProxy);
+                mMockToastProxy,
+                mFeatureFlags);
 
         assertFalse(call.wasDndCheckComputedForCall());
         assertFalse(call.isCallSuppressedByDoNotDisturb());
@@ -364,7 +587,8 @@
                 false /* shouldAttachToExistingConnection*/,
                 true /* isConference */,
                 mMockClockProxy,
-                mMockToastProxy);
+                mMockToastProxy,
+                mFeatureFlags);
 
         assertNull(call.getConnectionServiceWrapper());
         assertFalse(call.isTransactionalCall());
@@ -394,7 +618,8 @@
                 false /* shouldAttachToExistingConnection*/,
                 true /* isConference */,
                 mMockClockProxy,
-                mMockToastProxy);
+                mMockToastProxy,
+                mFeatureFlags);
 
         // setup
         call.setIsTransactionalCall(true);
@@ -467,6 +692,18 @@
 
     @Test
     @SmallTest
+    public void testGetFromCallerInfo_skipLookup() {
+        Resources mockResources = mContext.getResources();
+        when(mockResources.getBoolean(R.bool.skip_incoming_caller_info_query))
+                .thenReturn(true);
+
+        createCall("1");
+
+        verify(mMockCallerInfoLookupHelper, never()).startLookup(any(), any());
+    }
+
+    @Test
+    @SmallTest
     public void testOriginalCallIntent() {
         Call call = createCall("1");
 
@@ -728,11 +965,91 @@
                 }));
     }
 
+    @Test
+    @SmallTest
+    public void testExcludesInCallServiceFromDoNotLogCallExtra() {
+        Call call = createCall("any");
+        Bundle extra = new Bundle();
+        extra.putBoolean(TelecomManager.EXTRA_DO_NOT_LOG_CALL, true);
+
+        call.putInCallServiceExtras(extra, "packageName");
+
+        assertFalse(call.getExtras().containsKey(TelecomManager.EXTRA_DO_NOT_LOG_CALL));
+    }
+
+    /**
+     * Verify that a Call can handle a case where no telephony stack is present to detect emergency
+     * numbers.
+     */
+    @Test
+    @SmallTest
+    public void testNoTelephonyEmergencyBehavior() {
+        when(mComponentContextFixture.getTelephonyManager().isEmergencyNumber(any()))
+                .thenReturn(true);
+        Call testCall = createCall("1", Call.CALL_DIRECTION_OUTGOING, Uri.parse("tel:911"));
+        assertTrue(testCall.isEmergencyCall());
+
+        when(mComponentContextFixture.getTelephonyManager().isEmergencyNumber(any()))
+                .thenThrow(new UnsupportedOperationException("Bee-boop"));
+        Call testCall2 = createCall("2", Call.CALL_DIRECTION_OUTGOING, Uri.parse("tel:911"));
+        assertTrue(!testCall2.isEmergencyCall());
+    }
+
+    @Test
+    @SmallTest
+    public void testExcludesConnectionServiceWithoutModifyStatePermissionFromDoNotLogCallExtra() {
+        PackageManager packageManager = mContext.getPackageManager();
+        Bundle extra = new Bundle();
+        extra.putBoolean(TelecomManager.EXTRA_DO_NOT_LOG_CALL, true);
+        String packageName = SIM_1_HANDLE.getComponentName().getPackageName();
+        doReturn(PackageManager.PERMISSION_DENIED)
+                .when(packageManager)
+                .checkPermission(android.Manifest.permission.MODIFY_PHONE_STATE, packageName);
+        Call call = createCall("any");
+
+        call.putConnectionServiceExtras(extra);
+
+        assertFalse(call.getExtras().containsKey(TelecomManager.EXTRA_DO_NOT_LOG_CALL));
+    }
+
+    @Test
+    @SmallTest
+    public void testDoesNotExcludeConnectionServiceWithModifyStatePermissionFromDoNotLogCallExtra() {
+        String packageName = SIM_1_HANDLE.getComponentName().getPackageName();
+        Bundle extra = new Bundle();
+        extra.putBoolean(TelecomManager.EXTRA_DO_NOT_LOG_CALL, true);
+        PackageManager packageManager = mContext.getPackageManager();
+        doReturn(PackageManager.PERMISSION_GRANTED)
+                .when(packageManager)
+                .checkPermission(android.Manifest.permission.MODIFY_PHONE_STATE, packageName);
+        Call call = createCall("any");
+
+        call.putConnectionServiceExtras(extra);
+
+        assertTrue(call.getExtras().containsKey(TelecomManager.EXTRA_DO_NOT_LOG_CALL));
+    }
+
+    @Test
+    @SmallTest
+    public void testSkipLoadingCannedTextResponse() {
+        Call call = createCall("any");
+        Resources mockResources = mContext.getResources();
+        when(mockResources.getBoolean(R.bool.skip_loading_canned_text_response))
+                .thenReturn(true);
+
+
+        assertFalse(call.isRespondViaSmsCapable());
+    }
+
     private Call createCall(String id) {
         return createCall(id, Call.CALL_DIRECTION_UNDEFINED);
     }
 
     private Call createCall(String id, int callDirection) {
+        return createCall(id, callDirection, TEST_ADDRESS);
+    }
+
+    private Call createCall(String id, int callDirection, Uri address) {
         return new Call(
                 id,
                 mContext,
@@ -740,7 +1057,7 @@
                 mLock,
                 null,
                 mMockPhoneNumberUtilsAdapter,
-                TEST_ADDRESS,
+                address,
                 null /* GatewayInfo */,
                 null,
                 SIM_1_HANDLE,
@@ -748,6 +1065,7 @@
                 false,
                 false,
                 mMockClockProxy,
-                mMockToastProxy);
+                mMockToastProxy,
+                mFeatureFlags);
     }
 }
diff --git a/tests/src/com/android/server/telecom/tests/CallerInfoLookupHelperTest.java b/tests/src/com/android/server/telecom/tests/CallerInfoLookupHelperTest.java
index 7c001c0..614ef71 100644
--- a/tests/src/com/android/server/telecom/tests/CallerInfoLookupHelperTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallerInfoLookupHelperTest.java
@@ -17,11 +17,11 @@
 package com.android.server.telecom.tests;
 
 import static org.junit.Assert.assertEquals;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Matchers.anyString;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Matchers.isNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -32,13 +32,13 @@
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
-import android.telecom.Logging.Session;
-import android.test.suitebuilder.annotation.SmallTest;
-
-import androidx.test.InstrumentationRegistry;
-
 import android.telecom.CallerInfo;
 import android.telecom.CallerInfoAsyncQuery;
+import android.telecom.Logging.Session;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+
 import com.android.server.telecom.CallerInfoAsyncQueryFactory;
 import com.android.server.telecom.CallerInfoLookupHelper;
 import com.android.server.telecom.ContactsAsyncHelper;
diff --git a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
index 9f46336..641aed0 100644
--- a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
@@ -33,9 +33,12 @@
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
@@ -44,6 +47,8 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import static java.lang.Thread.sleep;
+
 import android.Manifest;
 import android.content.ComponentName;
 import android.content.ContentResolver;
@@ -54,6 +59,7 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Handler;
+import android.os.IBinder;
 import android.os.Looper;
 import android.os.OutcomeReceiver;
 import android.os.Process;
@@ -61,7 +67,7 @@
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.os.UserManager;
-import android.provider.BlockedNumberContract;
+import android.provider.BlockedNumbersManager;
 import android.telecom.CallException;
 import android.telecom.CallScreeningService;
 import android.telecom.CallerInfo;
@@ -75,15 +81,19 @@
 import android.telephony.CarrierConfigManager;
 import android.telephony.PhoneCapability;
 import android.telephony.TelephonyManager;
-import android.test.suitebuilder.annotation.MediumTest;
-import android.test.suitebuilder.annotation.SmallTest;
+import android.util.ArraySet;
 import android.util.Pair;
 import android.widget.Toast;
 
+import androidx.test.filters.MediumTest;
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.telecom.IConnectionService;
 import com.android.server.telecom.AnomalyReporterAdapter;
 import com.android.server.telecom.AsyncRingtonePlayer;
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallAnomalyWatchdog;
+import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
 import com.android.server.telecom.CallAudioManager;
 import com.android.server.telecom.CallAudioModeStateMachine;
 import com.android.server.telecom.CallAudioRouteStateMachine;
@@ -97,6 +107,7 @@
 import com.android.server.telecom.ConnectionServiceFocusManager;
 import com.android.server.telecom.ConnectionServiceFocusManager.ConnectionServiceFocusManagerFactory;
 import com.android.server.telecom.ConnectionServiceWrapper;
+import com.android.server.telecom.CreateConnectionResponse;
 import com.android.server.telecom.DefaultDialerCache;
 import com.android.server.telecom.EmergencyCallDiagnosticLogger;
 import com.android.server.telecom.EmergencyCallHelper;
@@ -119,23 +130,29 @@
 import com.android.server.telecom.TelecomSystem;
 import com.android.server.telecom.Timeouts;
 import com.android.server.telecom.WiredHeadsetManager;
+import com.android.server.telecom.bluetooth.BluetoothDeviceManager;
 import com.android.server.telecom.bluetooth.BluetoothRouteManager;
 import com.android.server.telecom.bluetooth.BluetoothStateReceiver;
 import com.android.server.telecom.callfiltering.BlockedNumbersAdapter;
 import com.android.server.telecom.callfiltering.CallFilteringResult;
+import com.android.server.telecom.flags.FeatureFlags;
+import com.android.server.telecom.callfiltering.IncomingCallFilterGraph;
 import com.android.server.telecom.ui.AudioProcessingNotification;
 import com.android.server.telecom.ui.CallStreamingNotification;
 import com.android.server.telecom.ui.DisconnectedCallNotifier;
 import com.android.server.telecom.ui.ToastFactory;
 import com.android.server.telecom.voip.TransactionManager;
 
+import com.google.common.base.Objects;
+
 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.ArgumentCaptor;
-import org.mockito.Matchers;
+import org.mockito.ArgumentMatchers;
+import org.mockito.InOrder;
 import org.mockito.Mock;
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
@@ -145,10 +162,12 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
 
 @RunWith(JUnit4.class)
 public class CallsManagerTest extends TelecomTestCase {
@@ -164,6 +183,10 @@
             new UserHandle(SECONDARY_USER_ID));
     private static final PhoneAccountHandle SIM_2_HANDLE = new PhoneAccountHandle(
             ComponentName.unflattenFromString("com.foo/.Blah"), "Sim2");
+    private static final PhoneAccountHandle SIM_3_HANDLE = new PhoneAccountHandle(
+            ComponentName.unflattenFromString("com.foo/.Blah"), "Sim3");
+    private static final PhoneAccountHandle CALL_PROVIDER_HANDLE = new PhoneAccountHandle(
+            ComponentName.unflattenFromString("com.sip.foo/.Blah"), "sip1");
     private static final PhoneAccountHandle CONNECTION_MGR_1_HANDLE = new PhoneAccountHandle(
             ComponentName.unflattenFromString("com.bar/.Conn"), "Cm1");
     private static final PhoneAccountHandle CONNECTION_MGR_2_HANDLE = new PhoneAccountHandle(
@@ -197,6 +220,18 @@
                     | PhoneAccount.CAPABILITY_SUPPORTS_VIDEO_CALLING)
             .setIsEnabled(true)
             .build();
+    private static final PhoneAccount SIM_3_ACCOUNT = new PhoneAccount.Builder(SIM_3_HANDLE, "Sim3")
+            .setCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION
+                    | PhoneAccount.CAPABILITY_CALL_PROVIDER
+                    | PhoneAccount.CAPABILITY_SUPPORTS_VIDEO_CALLING)
+            .setIsEnabled(true)
+            .build();
+    private static final PhoneAccount CALL_PROVIDER_ACCOUNT =
+            new PhoneAccount.Builder(CALL_PROVIDER_HANDLE, "sip1")
+            .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER
+                    | PhoneAccount.CAPABILITY_SUPPORTS_VIDEO_CALLING)
+            .setIsEnabled(true)
+            .build();
     private static final PhoneAccount SELF_MANAGED_ACCOUNT = new PhoneAccount.Builder(
             SELF_MANAGED_HANDLE, "Self")
             .setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)
@@ -268,7 +303,6 @@
     @Mock private BluetoothStateReceiver mBluetoothStateReceiver;
     @Mock private RoleManagerAdapter mRoleManagerAdapter;
     @Mock private ToastFactory mToastFactory;
-    @Mock private Toast mToast;
     @Mock private CallAnomalyWatchdog mCallAnomalyWatchdog;
 
     @Mock private EmergencyCallDiagnosticLogger mEmergencyCallDiagnosticLogger;
@@ -276,8 +310,15 @@
     @Mock private Ringer.AccessibilityManagerAdapter mAccessibilityManagerAdapter;
     @Mock private BlockedNumbersAdapter mBlockedNumbersAdapter;
     @Mock private PhoneCapability mPhoneCapability;
+    @Mock private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
     @Mock private CallStreamingNotification mCallStreamingNotification;
-
+    @Mock private BluetoothDeviceManager mBluetoothDeviceManager;
+    @Mock private FeatureFlags mFeatureFlags;
+    @Mock private com.android.internal.telephony.flags.FeatureFlags mTelephonyFlags;
+    @Mock private IncomingCallFilterGraph mIncomingCallFilterGraph;
+    @Mock private Context mMockCreateContextAsUser;
+    @Mock private UserManager mMockCurrentUserManager;
+    @Mock private IConnectionService mIConnectionService;
     private CallsManager mCallsManager;
 
     @Override
@@ -296,8 +337,8 @@
         when(mCallEndpointControllerFactory.create(any(), any(), any())).thenReturn(
                 mCallEndpointController);
         when(mCallAudioRouteStateMachineFactory.create(any(), any(), any(), any(), any(), any(),
-                anyInt(), any())).thenReturn(mCallAudioRouteStateMachine);
-        when(mCallAudioModeStateMachineFactory.create(any(), any()))
+                anyInt(), any(), any(), any())).thenReturn(mCallAudioRouteStateMachine);
+        when(mCallAudioModeStateMachineFactory.create(any(), any(), any(), any()))
                 .thenReturn(mCallAudioModeStateMachine);
         when(mClockProxy.currentTimeMillis()).thenReturn(System.currentTimeMillis());
         when(mClockProxy.elapsedRealtime()).thenReturn(SystemClock.elapsedRealtime());
@@ -345,10 +386,17 @@
                 mAccessibilityManagerAdapter,
                 // Just do async tasks synchronously to support testing.
                 command -> command.run(),
+                // For call audio tasks
+                command -> command.run(),
                 mBlockedNumbersAdapter,
                 TransactionManager.getTestInstance(),
                 mEmergencyCallDiagnosticLogger,
-                mCallStreamingNotification);
+                mCommunicationDeviceTracker,
+                mCallStreamingNotification,
+                mBluetoothDeviceManager,
+                mFeatureFlags,
+                mTelephonyFlags,
+                (call, listener, context, timeoutsAdapter, lock) -> mIncomingCallFilterGraph);
 
         when(mPhoneAccountRegistrar.getPhoneAccount(
                 eq(SELF_MANAGED_HANDLE), any())).thenReturn(SELF_MANAGED_ACCOUNT);
@@ -357,22 +405,49 @@
         when(mPhoneAccountRegistrar.getPhoneAccount(
                 eq(SIM_2_HANDLE), any())).thenReturn(SIM_2_ACCOUNT);
         when(mPhoneAccountRegistrar.getPhoneAccount(
+                eq(SIM_3_HANDLE), any())).thenReturn(SIM_3_ACCOUNT);
+        when(mPhoneAccountRegistrar.getPhoneAccount(
+                eq(CALL_PROVIDER_HANDLE), any())).thenReturn(CALL_PROVIDER_ACCOUNT);
+        when(mPhoneAccountRegistrar.getPhoneAccount(
                 eq(WORK_HANDLE), any())).thenReturn(WORK_ACCOUNT);
-        when(mToastFactory.makeText(any(), anyInt(), anyInt())).thenReturn(mToast);
-        when(mToastFactory.makeText(any(), any(), anyInt())).thenReturn(mToast);
+        when(mFeatureFlags.separatelyBindToBtIncallService()).thenReturn(false);
+        when(mFeatureFlags.telecomResolveHiddenDependencies()).thenReturn(true);
+        when(mContext.createContextAsUser(any(UserHandle.class), eq(0)))
+                .thenReturn(mMockCreateContextAsUser);
+        when(mMockCreateContextAsUser.getSystemService(UserManager.class))
+                .thenReturn(mMockCurrentUserManager);
+        when(mIConnectionService.asBinder()).thenReturn(mock(IBinder.class));
+
+        mComponentContextFixture.addConnectionService(
+                SIM_1_ACCOUNT.getAccountHandle().getComponentName(), mIConnectionService);
     }
 
     @Override
     @After
     public void tearDown() throws Exception {
+        mComponentContextFixture.removeConnectionService(
+                SIM_1_ACCOUNT.getAccountHandle().getComponentName(), mIConnectionService);
         super.tearDown();
     }
 
     @MediumTest
     @Test
     public void testConstructPossiblePhoneAccounts() throws Exception {
+        when(mTelephonyFlags.simultaneousCallingIndications()).thenReturn(false);
+        setupMsimAccounts();
         // Should be empty since the URI is null.
-        assertEquals(0, mCallsManager.constructPossiblePhoneAccounts(null, null, false, false).size());
+        assertEquals(0, mCallsManager.constructPossiblePhoneAccounts(null, null,
+                false, false, false).size());
+    }
+
+    @MediumTest
+    @Test
+    public void testConstructPossiblePhoneAccounts_simulCalling() {
+        when(mTelephonyFlags.simultaneousCallingIndications()).thenReturn(true);
+        setupMsimAccounts();
+        // Should be empty since the URI is null.
+        assertEquals(0, mCallsManager.constructPossiblePhoneAccounts(null, null,
+                false, false, false).size());
     }
 
     private Call constructOngoingCall(String callId, PhoneAccountHandle phoneAccountHandle) {
@@ -391,10 +466,12 @@
                 false /* shouldAttachToExistingConnection*/,
                 false /* isConference */,
                 mClockProxy,
-                mToastFactory);
+                mToastFactory,
+                mFeatureFlags);
         ongoingCall.setState(CallState.ACTIVE, "just cuz");
         return ongoingCall;
     }
+
     /**
      * Verify behavior for multisim devices where we want to ensure that the active sim is used for
      * placing a new call.
@@ -403,32 +480,127 @@
     @MediumTest
     @Test
     public void testConstructPossiblePhoneAccountsMultiSimActive() throws Exception {
+        when(mTelephonyFlags.simultaneousCallingIndications()).thenReturn(false);
         setupMsimAccounts();
 
         Call ongoingCall = constructOngoingCall("1", SIM_2_HANDLE);
         mCallsManager.addCall(ongoingCall);
 
         List<PhoneAccountHandle> phoneAccountHandles = mCallsManager.constructPossiblePhoneAccounts(
-                TEST_ADDRESS, null, false, false);
+                TEST_ADDRESS, null, false, false, false);
         assertEquals(1, phoneAccountHandles.size());
         assertEquals(SIM_2_HANDLE, phoneAccountHandles.get(0));
     }
 
     /**
+     * Verify behavior for multisim devices where we want to ensure that the active sim is used for
+     * placing a new call when a restriction is set as well as other call providers from different
+     * apps.
+     */
+    @MediumTest
+    @Test
+    public void testConstructPossiblePhoneAccountsMultiSimActive_simulCallingRestriction() {
+        when(mTelephonyFlags.simultaneousCallingIndications()).thenReturn(true);
+        setupAccountsWithCallingRestriction(Arrays.asList(SIM_1_HANDLE, SIM_2_HANDLE,
+                CALL_PROVIDER_HANDLE), Collections.emptySet());
+
+        Call ongoingCall = constructOngoingCall("1", SIM_2_HANDLE);
+        mCallsManager.addCall(ongoingCall);
+
+        List<PhoneAccountHandle> phoneAccountHandles = mCallsManager.constructPossiblePhoneAccounts(
+                TEST_ADDRESS, null, false, false, false);
+        assertEquals(Arrays.asList(SIM_2_HANDLE, CALL_PROVIDER_HANDLE), phoneAccountHandles);
+    }
+
+    /**
+     * When we have 3 SIM PhoneAccounts on a device, but only 2 allow simultaneous calling, place a
+     * call on a SIM that allows simultaneous calling and verify that the subset of PhoneAccounts
+     * are available when in a call as well as the call provider.
+     */
+    @MediumTest
+    @Test
+    public void testConstructPossiblePhoneAccountsMultiSimActive_simulCallingRestrictionSubset() {
+        when(mTelephonyFlags.simultaneousCallingIndications()).thenReturn(true);
+        setupAccountsWithCallingRestriction(Arrays.asList(SIM_1_HANDLE, SIM_2_HANDLE, SIM_3_HANDLE,
+                CALL_PROVIDER_HANDLE), new ArraySet<>(Arrays.asList(SIM_1_HANDLE, SIM_2_HANDLE)));
+
+        Call ongoingCall = constructOngoingCall("1", SIM_2_HANDLE);
+        mCallsManager.addCall(ongoingCall);
+
+        List<PhoneAccountHandle> phoneAccountHandles = mCallsManager.constructPossiblePhoneAccounts(
+                TEST_ADDRESS, null, false, false, false);
+        assertEquals(Arrays.asList(SIM_1_HANDLE, SIM_2_HANDLE, CALL_PROVIDER_HANDLE),
+                phoneAccountHandles);
+    }
+
+    /**
+     * When we have 3 SIM PhoneAccounts on a device, but only 2 allow simultaneous calling, place a
+     * call on the SIM that does not allow simultaneous calling and verify  that only that SIM and
+     * the separate call provider are allowed to place a second call.
+     */
+    @MediumTest
+    @Test
+    public void testConstructPossiblePhoneAccountsMultiSimActive_simulCallingRestrictionSubset2() {
+        when(mTelephonyFlags.simultaneousCallingIndications()).thenReturn(true);
+        setupAccountsWithCallingRestriction(Arrays.asList(SIM_1_HANDLE, SIM_2_HANDLE, SIM_3_HANDLE,
+                CALL_PROVIDER_HANDLE), new ArraySet<>(Arrays.asList(SIM_1_HANDLE, SIM_2_HANDLE)));
+
+        Call ongoingCall = constructOngoingCall("1", SIM_3_HANDLE);
+        mCallsManager.addCall(ongoingCall);
+
+        List<PhoneAccountHandle> phoneAccountHandles = mCallsManager.constructPossiblePhoneAccounts(
+                TEST_ADDRESS, null, false, false, false);
+        assertEquals(Arrays.asList(SIM_3_HANDLE, CALL_PROVIDER_HANDLE),
+                phoneAccountHandles);
+    }
+
+    /**
      * Verify behavior for multisim devices when there are no calls active; expect both accounts.
      * @throws Exception
      */
     @MediumTest
     @Test
     public void testConstructPossiblePhoneAccountsMultiSimIdle() throws Exception {
+        when(mTelephonyFlags.simultaneousCallingIndications()).thenReturn(false);
         setupMsimAccounts();
 
         List<PhoneAccountHandle> phoneAccountHandles = mCallsManager.constructPossiblePhoneAccounts(
-                TEST_ADDRESS, null, false, false);
+                TEST_ADDRESS, null, false, false, false);
         assertEquals(2, phoneAccountHandles.size());
     }
 
     /**
+     * Verify behavior for multisim devices when there are no calls active and there are no calling
+     * restrictions set; expect both accounts.
+     */
+    @MediumTest
+    @Test
+    public void testConstructPossiblePhoneAccountsMultiSimIdle_noSimulCallingRestriction() {
+        when(mTelephonyFlags.simultaneousCallingIndications()).thenReturn(true);
+        setupAccountsNoSimultaneousCallingRestriction();
+
+        List<PhoneAccountHandle> phoneAccountHandles = mCallsManager.constructPossiblePhoneAccounts(
+                TEST_ADDRESS, null, false, false, false);
+        assertEquals(3, phoneAccountHandles.size());
+    }
+
+    /**
+     * Verify behavior for multisim devices when there are no calls active and there are no calling
+     * restrictions set; expect both accounts.
+     */
+    @MediumTest
+    @Test
+    public void testConstructPossiblePhoneAccountsMultiSimIdle_withSimulCallingRestriction() {
+        when(mTelephonyFlags.simultaneousCallingIndications()).thenReturn(true);
+        setupAccountsWithCallingRestriction(Arrays.asList(SIM_1_HANDLE, SIM_2_HANDLE,
+                CALL_PROVIDER_HANDLE), Collections.emptySet());
+
+        List<PhoneAccountHandle> phoneAccountHandles = mCallsManager.constructPossiblePhoneAccounts(
+                TEST_ADDRESS, null, false, false, false);
+        assertEquals(3, phoneAccountHandles.size());
+    }
+
+    /**
      * For DSDA-enabled multisim devices with an ongoing call, verify that both SIMs'
      * PhoneAccountHandles are constructed while placing a new call.
      * @throws Exception
@@ -437,6 +609,7 @@
     @Test
     public void testConstructPossiblePhoneAccountsMultiSimActive_dsdaCallingPossible() throws
             Exception {
+        when(mTelephonyFlags.simultaneousCallingIndications()).thenReturn(false);
         setupMsimAccounts();
         setMaxActiveVoiceSubscriptions(2);
 
@@ -444,11 +617,29 @@
         mCallsManager.addCall(ongoingCall);
 
         List<PhoneAccountHandle> phoneAccountHandles = mCallsManager.constructPossiblePhoneAccounts(
-                TEST_ADDRESS, null, false, false);
+                TEST_ADDRESS, null, false, false, false);
         assertEquals(2, phoneAccountHandles.size());
     }
 
     /**
+     * For multisim devices with an ongoing call, verify that all call capable PhoneAccounts are
+     * available when creating a second call.
+     */
+    @MediumTest
+    @Test
+    public void testConstructPossiblePhoneAccountsMultiSimActive_simulCalling_dsdaPossible() {
+        when(mTelephonyFlags.simultaneousCallingIndications()).thenReturn(true);
+        setupAccountsNoSimultaneousCallingRestriction();
+
+        Call ongoingCall = constructOngoingCall("1", SIM_2_HANDLE);
+        mCallsManager.addCall(ongoingCall);
+
+        List<PhoneAccountHandle> phoneAccountHandles = mCallsManager.constructPossiblePhoneAccounts(
+                TEST_ADDRESS, null, false, false, false);
+        assertEquals(3, phoneAccountHandles.size());
+    }
+
+    /**
      * For DSDA-enabled multisim devices with an ongoing call, verify that only the active SIMs'
      * PhoneAccountHandle is constructed while placing an emergency call.
      * @throws Exception
@@ -457,6 +648,7 @@
     @Test
     public void testConstructPossiblePhoneAccountsMultiSimActive_dsdaCallingPossible_emergencyCall()
             throws Exception {
+        when(mTelephonyFlags.simultaneousCallingIndications()).thenReturn(false);
         setupMsimAccounts();
         setMaxActiveVoiceSubscriptions(2);
 
@@ -464,11 +656,96 @@
         mCallsManager.addCall(ongoingCall);
 
         List<PhoneAccountHandle> phoneAccountHandles = mCallsManager.constructPossiblePhoneAccounts(
-                TEST_ADDRESS, null, false, true /* isEmergency */);
+                TEST_ADDRESS, null, false, true /* isEmergency */, false);
         assertEquals(1, phoneAccountHandles.size());
         assertEquals(SIM_2_HANDLE, phoneAccountHandles.get(0));
     }
 
+    /**
+     * For multisim devices with an ongoing call, verify that only the active SIM's
+     * PhoneAccountHandle is available if we have a calling restriction where only one SIM is
+     * active at a time.
+     */
+    @MediumTest
+    @Test
+    public void testConstructPossiblePhoneAccountsMultiSimActive_simulCalling_emergencyCall() {
+        when(mTelephonyFlags.simultaneousCallingIndications()).thenReturn(true);
+        setupAccountsWithCallingRestriction(Arrays.asList(SIM_1_HANDLE, SIM_2_HANDLE,
+                CALL_PROVIDER_HANDLE), Collections.emptySet());
+
+        Call ongoingCall = constructOngoingCall("1", SIM_2_HANDLE);
+        mCallsManager.addCall(ongoingCall);
+
+        List<PhoneAccountHandle> phoneAccountHandles = mCallsManager.constructPossiblePhoneAccounts(
+                TEST_ADDRESS, null, false, true /* isEmergency */, false);
+        assertEquals(2, phoneAccountHandles.size());
+        assertTrue("Candidate PAHs must contain the SIM account hosting the emergency call",
+                phoneAccountHandles.contains(SIM_2_HANDLE));
+        assertFalse("Candidate PAHs must not contain other SIM accounts",
+                phoneAccountHandles.contains(SIM_1_HANDLE));
+    }
+
+    /**
+     * For devices with an ongoing call on a call provider, verify that when an emergency
+     * call is placed, all SIM accounts are still available for SIM selection.
+     */
+    @MediumTest
+    @Test
+    public void testConstructPossiblePhoneAccounts_callProvider_emergencyCall() {
+        when(mTelephonyFlags.simultaneousCallingIndications()).thenReturn(true);
+        setupAccountsWithCallingRestriction(Arrays.asList(SIM_1_HANDLE, SIM_2_HANDLE,
+                CALL_PROVIDER_HANDLE), Collections.emptySet());
+
+        Call ongoingCall = constructOngoingCall("1", CALL_PROVIDER_HANDLE);
+        mCallsManager.addCall(ongoingCall);
+
+        List<PhoneAccountHandle> phoneAccountHandles = mCallsManager.constructPossiblePhoneAccounts(
+                TEST_ADDRESS, null, false, true /* isEmergency */, false);
+        assertEquals(3, phoneAccountHandles.size());
+    }
+
+    /**
+     * For multisim devices with an ongoing call, for backwards compatibility, only allow the
+     * SIM with the active call to be chosen to place an emergency call, even if there is no
+     * calling restriction.
+     */
+    @MediumTest
+    @Test
+    public void testConstructPossiblePhoneAccountsMultiSimActive_simulCallingRest_emergencyCall() {
+        when(mTelephonyFlags.simultaneousCallingIndications()).thenReturn(true);
+        setupAccountsNoSimultaneousCallingRestriction();
+
+        Call ongoingCall = constructOngoingCall("1", SIM_2_HANDLE);
+        mCallsManager.addCall(ongoingCall);
+
+        List<PhoneAccountHandle> phoneAccountHandles = mCallsManager.constructPossiblePhoneAccounts(
+                TEST_ADDRESS, null, false, true /* isEmergency */, false);
+        assertEquals(2, phoneAccountHandles.size());
+        assertTrue("Candidate PAHs must contain the SIM account hosting the emergency call",
+                phoneAccountHandles.contains(SIM_2_HANDLE));
+        assertFalse("Candidate PAHs must not contain other SIM accounts",
+                phoneAccountHandles.contains(SIM_1_HANDLE));
+    }
+
+    /**
+     * For multisim devices with an ongoing call on a call provider, it is still possible to place
+     * a SIM call on any SIM account.
+     */
+    @MediumTest
+    @Test
+    public void testConstructPossiblePhoneAccounts_crossAccount_simulCalling() {
+        when(mTelephonyFlags.simultaneousCallingIndications()).thenReturn(true);
+        setupAccountsWithCallingRestriction(Arrays.asList(SIM_1_HANDLE, SIM_2_HANDLE,
+                CALL_PROVIDER_HANDLE), Collections.emptySet());
+
+        Call ongoingCall = constructOngoingCall("1", CALL_PROVIDER_HANDLE);
+        mCallsManager.addCall(ongoingCall);
+
+        List<PhoneAccountHandle> phoneAccountHandles = mCallsManager.constructPossiblePhoneAccounts(
+                TEST_ADDRESS, null, false, false /* isEmergency */, false);
+        assertEquals(3, phoneAccountHandles.size());
+    }
+
     private void setupCallerInfoLookupHelper() {
         doAnswer(invocation -> {
             Uri handle = invocation.getArgument(0);
@@ -1175,6 +1452,36 @@
         verify(incomingCall).setIsUsingCallFiltering(eq(false));
     }
 
+    /**
+     * Verify the ability to skip call filtering when Telephony reports we are in emergency SMS mode
+     * and also verify that when Telephony is not available we will not try to skip filtering.
+     */
+    @SmallTest
+    @Test
+    public void testFilteringWhenEmergencySmsCheckFails() {
+        // First see if it works when Telephony is present.
+        Call incomingCall = addSpyCall(CallState.NEW);
+        doReturn(true).when(mComponentContextFixture.getTelephonyManager()).isInEmergencySmsMode();
+        mCallsManager.onSuccessfulIncomingCall(incomingCall);
+        verify(incomingCall).setIsUsingCallFiltering(eq(false));
+
+        // Ensure when there is no telephony it doesn't try to skip filtering.
+        Call incomingCall2 = addSpyCall(CallState.NEW);
+        doThrow(new UnsupportedOperationException("Bee-boop")).when(
+                mComponentContextFixture.getTelephonyManager()).isInEmergencySmsMode();
+        mCallsManager.onSuccessfulIncomingCall(incomingCall2);
+        verify(incomingCall2).setIsUsingCallFiltering(eq(true));
+    }
+
+    @SmallTest
+    @Test
+    public void testDsdaAvailableCheckWhenNoTelephony() {
+        doThrow(new UnsupportedOperationException("Bee-boop")).when(
+                mComponentContextFixture.getTelephonyManager())
+                        .getMaxNumberOfSimultaneouslyActiveSims();
+        assertFalse(mCallsManager.isDsdaCallingPossible());
+    }
+
     @SmallTest
     @Test
     public void testNoFilteringOfNetworkIdentifiedEmergencyCalls() {
@@ -1318,8 +1625,9 @@
 
     @SmallTest
     @Test
-    public void testNoFilteringOfCallsWhenPhoneAccountRequestsSkipped() {
+    public void testDndFilterAppliesOfCallsWhenPhoneAccountRequestsSkipped() {
         // GIVEN an incoming call which is from a PhoneAccount that requested to skip filtering.
+        when(mFeatureFlags.skipFilterPhoneAccountPerformDndFilter()).thenReturn(true);
         Call incomingCall = addSpyCall(SIM_1_HANDLE, CallState.NEW);
         Bundle extras = new Bundle();
         extras.putBoolean(PhoneAccount.EXTRA_SKIP_CALL_FILTERING, true);
@@ -1339,7 +1647,35 @@
         // WHEN the incoming call is successfully added.
         mCallsManager.onSuccessfulIncomingCall(incomingCall);
 
-        // THEN the incoming call is not using call filtering
+        // THEN the incoming call is still applying Dnd filter.
+        verify(incomingCall).setIsUsingCallFiltering(eq(true));
+    }
+
+    @SmallTest
+    @Test
+    public void testNoFilterAppliesOfCallsWhenFlagNotEnabled() {
+        // Flag is not enabled.
+        when(mFeatureFlags.skipFilterPhoneAccountPerformDndFilter()).thenReturn(false);
+        Call incomingCall = addSpyCall(SIM_1_HANDLE, CallState.NEW);
+        Bundle extras = new Bundle();
+        extras.putBoolean(PhoneAccount.EXTRA_SKIP_CALL_FILTERING, true);
+        PhoneAccount skipRequestedAccount = new PhoneAccount.Builder(SIM_2_HANDLE, "Skipper")
+                .setCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION
+                        | PhoneAccount.CAPABILITY_CALL_PROVIDER)
+                .setExtras(extras)
+                .setIsEnabled(true)
+                .build();
+        when(mPhoneAccountRegistrar.getPhoneAccountUnchecked(SIM_1_HANDLE))
+                .thenReturn(skipRequestedAccount);
+        doReturn(false).when(incomingCall).can(Connection.CAPABILITY_HOLD);
+        doReturn(false).when(incomingCall).can(Connection.CAPABILITY_SUPPORT_HOLD);
+        doReturn(false).when(incomingCall).isSelfManaged();
+        doReturn(true).when(incomingCall).setState(anyInt(), any());
+
+        // WHEN the incoming call is successfully added.
+        mCallsManager.onSuccessfulIncomingCall(incomingCall);
+
+        // THEN the incoming call is not applying filter.
         verify(incomingCall).setIsUsingCallFiltering(eq(false));
     }
 
@@ -2396,7 +2732,7 @@
 
         mCallsManager.onCallFilteringComplete(callSpy, result, false /* timeout */);
         verify(mMissedCallNotifier).showMissedCallNotification(
-                any(MissedCallNotifier.CallInfo.class));
+                any(MissedCallNotifier.CallInfo.class), /* uri= */ eq(null));
     }
 
     @Test
@@ -2433,6 +2769,24 @@
     }
 
     /**
+     * Verify when Telephony is not available we don't try to block redirection due to the failed
+     * isEmergency check.
+     */
+    @SmallTest
+    @Test
+    public void testEmergencyCheckFailsOnRedirectionCheckCompleteDueToNoTelephony() {
+        when(mComponentContextFixture.getTelephonyManager().isEmergencyNumber(anyString()))
+                .thenThrow(new UnsupportedOperationException("Bee boop"));
+
+        Call callSpy = addSpyCall(CallState.NEW);
+        mCallsManager.onCallRedirectionComplete(callSpy, Uri.parse("tel:911"),
+                SIM_1_HANDLE_SECONDARY,
+                new GatewayInfo("foo", TEST_ADDRESS2, TEST_ADDRESS), true /* speakerphoneOn */,
+                VideoProfile.STATE_AUDIO_ONLY, false /* shouldCancelCall */, "" /* uiAction */);
+        verify(callSpy, never()).disconnect(anyString());
+    }
+
+    /**
      * Verifies that target phone account is set in startOutgoingCall. The multi-user functionality
      * is dependent on the call's phone account handle being present so this test ensures that
      * existing outgoing call flow does not break from future updates.
@@ -2500,6 +2854,32 @@
         assertEquals(DEFAULT_CALL_SCREENING_APP, outgoingCall.getPostCallPackageName());
     }
 
+    /**
+     * Verify the only call state set from calling onSuccessfulOutgoingCall is CallState.DIALING.
+     */
+    @SmallTest
+    @Test
+    public void testOutgoingCallStateIsSetToAPreviousStateAndIgnored() {
+        when(mFeatureFlags.fixAudioFlickerForOutgoingCalls()).thenReturn(true);
+        Call outgoingCall = addSpyCall(CallState.CONNECTING);
+        mCallsManager.onSuccessfulOutgoingCall(outgoingCall, CallState.NEW);
+        verify(outgoingCall, never()).setState(eq(CallState.NEW), any());
+        verify(outgoingCall, times(1)).setState(eq(CallState.DIALING), any());
+    }
+
+    /**
+     * Verify a ConnectionService can start the call in the active state and avoid the dialing state
+     */
+    @SmallTest
+    @Test
+    public void testOutgoingCallStateCanAvoidDialingAndGoStraightToActive() {
+        when(mFeatureFlags.fixAudioFlickerForOutgoingCalls()).thenReturn(true);
+        Call outgoingCall = addSpyCall(CallState.CONNECTING);
+        mCallsManager.onSuccessfulOutgoingCall(outgoingCall, CallState.ACTIVE);
+        verify(outgoingCall, never()).setState(eq(CallState.DIALING), any());
+        verify(outgoingCall, times(1)).setState(eq(CallState.ACTIVE), any());
+    }
+
     @SmallTest
     @Test
     public void testRejectIncomingCallOnPAHInactive_SecondaryUser() throws Exception {
@@ -2508,11 +2888,9 @@
         mCallsManager.addConnectionServiceRepositoryCache(WORK_HANDLE.getComponentName(),
                 WORK_HANDLE.getUserHandle(), service);
 
-        UserManager um = mContext.getSystemService(UserManager.class);
-        UserHandle newUser = new UserHandle(11);
-        when(mCallsManager.getCurrentUserHandle()).thenReturn(newUser);
-        when(um.isUserAdmin(eq(newUser.getIdentifier()))).thenReturn(false);
-        when(um.isQuietModeEnabled(eq(WORK_HANDLE.getUserHandle()))).thenReturn(false);
+        when(mMockCurrentUserManager.isAdminUser()).thenReturn(false);
+        when(mMockCurrentUserManager.isQuietModeEnabled(eq(WORK_HANDLE.getUserHandle())))
+                .thenReturn(false);
         when(mPhoneAccountRegistrar.getPhoneAccountUnchecked(eq(WORK_HANDLE)))
                 .thenReturn(WORK_ACCOUNT);
         Call newCall = mCallsManager.processIncomingCallIntent(
@@ -2527,14 +2905,17 @@
     @Test
     public void testRejectIncomingCallOnPAHInactive_ProfilePaused() throws Exception {
         ConnectionServiceWrapper service = mock(ConnectionServiceWrapper.class);
-        doReturn(SIM_2_HANDLE.getComponentName()).when(service).getComponentName();
-        mCallsManager.addConnectionServiceRepositoryCache(SIM_2_HANDLE.getComponentName(),
-                SIM_2_HANDLE.getUserHandle(), service);
+        doReturn(WORK_HANDLE.getComponentName()).when(service).getComponentName();
+        mCallsManager.addConnectionServiceRepositoryCache(WORK_HANDLE.getComponentName(),
+                WORK_HANDLE.getUserHandle(), service);
 
-        UserManager um = mContext.getSystemService(UserManager.class);
-        when(um.isQuietModeEnabled(eq(SIM_2_HANDLE.getUserHandle()))).thenReturn(true);
+        when(mMockCurrentUserManager.isAdminUser()).thenReturn(true);
+        when(mMockCurrentUserManager.isQuietModeEnabled(any(UserHandle.class)))
+                .thenReturn(true);
+        when(mPhoneAccountRegistrar.getPhoneAccountUnchecked(eq(WORK_HANDLE)))
+                .thenReturn(WORK_ACCOUNT);
         Call newCall = mCallsManager.processIncomingCallIntent(
-                SIM_2_HANDLE, new Bundle(), false);
+                WORK_HANDLE, new Bundle(), false);
 
         verify(service, timeout(TEST_TIMEOUT)).createConnectionFailed(any());
         assertFalse(newCall.isInECBM());
@@ -2551,8 +2932,8 @@
 
         when(mEmergencyCallHelper.isLastOutgoingEmergencyCallPAH(eq(SIM_2_HANDLE)))
                 .thenReturn(true);
-        UserManager um = mContext.getSystemService(UserManager.class);
-        when(um.isQuietModeEnabled(eq(SIM_2_HANDLE.getUserHandle()))).thenReturn(true);
+        when(mMockCurrentUserManager.isQuietModeEnabled(eq(SIM_2_HANDLE.getUserHandle())))
+                .thenReturn(true);
         Call newCall = mCallsManager.processIncomingCallIntent(
                 SIM_2_HANDLE, new Bundle(), false);
 
@@ -2570,11 +2951,9 @@
 
         when(mEmergencyCallHelper.isLastOutgoingEmergencyCallPAH(eq(WORK_HANDLE)))
                 .thenReturn(true);
-        UserManager um = mContext.getSystemService(UserManager.class);
-        UserHandle newUser = new UserHandle(11);
-        when(mCallsManager.getCurrentUserHandle()).thenReturn(newUser);
-        when(um.isUserAdmin(eq(newUser.getIdentifier()))).thenReturn(false);
-        when(um.isQuietModeEnabled(eq(WORK_HANDLE.getUserHandle()))).thenReturn(false);
+        when(mMockCurrentUserManager.isAdminUser()).thenReturn(false);
+        when(mMockCurrentUserManager.isQuietModeEnabled(eq(WORK_HANDLE.getUserHandle())))
+                .thenReturn(false);
         when(mPhoneAccountRegistrar.getPhoneAccountUnchecked(eq(WORK_HANDLE)))
                 .thenReturn(WORK_ACCOUNT);
         Call newCall = mCallsManager.processIncomingCallIntent(
@@ -2595,8 +2974,8 @@
         Bundle extras = new Bundle();
         extras.putParcelable(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS, TEST_ADDRESS);
         TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
-        UserManager um = mContext.getSystemService(UserManager.class);
-        when(um.isQuietModeEnabled(eq(SIM_2_HANDLE.getUserHandle()))).thenReturn(true);
+        when(mMockCurrentUserManager.isQuietModeEnabled(eq(SIM_2_HANDLE.getUserHandle())))
+                .thenReturn(true);
         when(tm.isEmergencyNumber(any(String.class))).thenReturn(true);
         Call newCall = mCallsManager.processIncomingCallIntent(
                 SIM_2_HANDLE, extras, false);
@@ -2659,42 +3038,152 @@
         assertFalse(mCallsManager.getCalls().contains(call));
     }
 
+    /**
+     * Verify that
+     * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
+     * OutcomeReceiver)}s OutcomeReceiver returns onResult when there is no active call to place
+     * on hold.
+     */
     @MediumTest
     @Test
-    public void testHoldTransactional() throws Exception {
-        CountDownLatch latch = new CountDownLatch(1);
+    public void testHoldWhenActiveCallIsNullOrSame() throws Exception {
         Call newCall = addSpyCall();
-
         // case 1: no active call, no need to put the call on hold
-        when(mConnectionSvrFocusMgr.getCurrentFocusCall()).thenReturn(null);
-        mCallsManager.transactionHoldPotentialActiveCallForNewCall(newCall,
-                new LatchedOutcomeReceiver(latch, true));
-        waitForCountDownLatch(latch);
-
+        assertHoldActiveCallForNewCall(
+                newCall,
+                null  /* activeCall */,
+                false /* isCallControlRequest */,
+                true  /* expectOnResult */);
         // case 2: active call == new call, no need to put the call on hold
-        latch = new CountDownLatch(1);
-        when(mConnectionSvrFocusMgr.getCurrentFocusCall()).thenReturn(newCall);
-        mCallsManager.transactionHoldPotentialActiveCallForNewCall(newCall,
-                new LatchedOutcomeReceiver(latch, true));
-        waitForCountDownLatch(latch);
+        assertHoldActiveCallForNewCall(
+                newCall,
+                newCall /* activeCall */,
+                false /* isCallControlRequest */,
+                true  /* expectOnResult */);
+    }
 
-        // case 3: cannot hold current active call early check
+    /**
+     * Verify that
+     * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
+     * OutcomeReceiver)}s OutcomeReceiver returns onError when there is an active call that
+     * cannot be held, and it's a CallControlRequest.
+     */
+    @MediumTest
+    @Test
+    public void testHoldFailsWithUnholdableCallAndCallControlRequest() throws Exception {
         Call cannotHoldCall = addSpyCall(SIM_1_HANDLE, null,
                 CallState.ACTIVE, 0, 0);
-        latch = new CountDownLatch(1);
-        when(mConnectionSvrFocusMgr.getCurrentFocusCall()).thenReturn(cannotHoldCall);
-        mCallsManager.transactionHoldPotentialActiveCallForNewCall(newCall,
-                new LatchedOutcomeReceiver(latch, false));
-        waitForCountDownLatch(latch);
+        assertHoldActiveCallForNewCall(
+                addSpyCall(),
+                cannotHoldCall /* activeCall */,
+                true /* isCallControlRequest */,
+                false  /* expectOnResult */);
+    }
 
-        // case 4: activeCall != newCall && canHold(activeCall)
+    /**
+     * Verify that
+     * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
+     * OutcomeReceiver)}s OutcomeReceiver returns onResult when there is a holdable call and
+     * it's a CallControlRequest.
+     */
+    @MediumTest
+    @Test
+    public void testHoldSuccessWithHoldableActiveCall() throws Exception {
+        Call newCall = addSpyCall(VOIP_1_HANDLE, CallState.CONNECTING);
         Call canHoldCall = addSpyCall(SIM_1_HANDLE, null,
                 CallState.ACTIVE, Connection.CAPABILITY_HOLD, 0);
-        latch = new CountDownLatch(1);
-        when(mConnectionSvrFocusMgr.getCurrentFocusCall()).thenReturn(canHoldCall);
-        mCallsManager.transactionHoldPotentialActiveCallForNewCall(newCall,
-                new LatchedOutcomeReceiver(latch, true));
-        waitForCountDownLatch(latch);
+        assertHoldActiveCallForNewCall(
+                newCall,
+                canHoldCall /* activeCall */,
+                true /* isCallControlRequest */,
+                true  /* expectOnResult */);
+    }
+
+    /**
+     * Verify that
+     * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
+     * OutcomeReceiver)}s OutcomeReceiver returns onResult when there is an active call that
+     * supports hold, and it's a CallControlRequest.
+     */
+    @MediumTest
+    @Test
+    public void testHoldWhenTheActiveCallSupportsHold() throws Exception {
+        Call newCall = addSpyCall();
+        Call supportsHold = addSpyCall(SIM_1_HANDLE, null,
+                CallState.ACTIVE, Connection.CAPABILITY_SUPPORT_HOLD, 0);
+        assertHoldActiveCallForNewCall(
+                newCall,
+                supportsHold /* activeCall */,
+                true /* isCallControlRequest */,
+                true  /* expectOnResult */);
+    }
+
+    /**
+     * Verify that
+     * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
+     * OutcomeReceiver)}s OutcomeReceiver returns onResult when there is an active call that
+     * supports hold + can hold, and it's a CallControlRequest.
+     */
+    @MediumTest
+    @Test
+    public void testHoldWhenTheActiveCallSupportsAndCanHold() throws Exception {
+        Call newCall = addSpyCall();
+        Call supportsHold = addSpyCall(SIM_1_HANDLE, null,
+                CallState.ACTIVE,
+                Connection.CAPABILITY_HOLD | Connection.CAPABILITY_SUPPORT_HOLD,
+                0);
+        assertHoldActiveCallForNewCall(
+                newCall,
+                supportsHold /* activeCall */,
+                true /* isCallControlRequest */,
+                true  /* expectOnResult */);
+    }
+
+    /**
+     * Verify that
+     * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
+     * OutcomeReceiver)}s OutcomeReceiver returns onResult when there is an active call that
+     * supports hold + can hold, and it's a CallControlCallbackRequest.
+     */
+    @MediumTest
+    @Test
+    public void testHoldForCallControlCallbackRequestWithActiveCallThatCanHold() throws Exception {
+        Call newCall = addSpyCall();
+        Call supportsHold = addSpyCall(SIM_1_HANDLE, null,
+                CallState.ACTIVE, Connection.CAPABILITY_HOLD | Connection.CAPABILITY_SUPPORT_HOLD,
+                0);
+        assertHoldActiveCallForNewCall(
+                newCall,
+                supportsHold /* activeCall */,
+                false /* isCallControlRequest */,
+                true  /* expectOnResult */);
+    }
+
+    /**
+     * Verify that
+     * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
+     * OutcomeReceiver)}s OutcomeReceiver returns onResult when there is an active unholdable call,
+     * and it's a CallControlCallbackRequest.
+     */
+    @MediumTest
+    @Test
+    public void testHoldDisconnectsTheActiveCall() throws Exception {
+        Call newCall = addSpyCall(VOIP_1_HANDLE, CallState.CONNECTING);
+        Call activeUnholdableCall = addSpyCall(SIM_1_HANDLE, null,
+                CallState.ACTIVE, 0, 0);
+
+        doAnswer(invocation -> {
+            doReturn(true).when(activeUnholdableCall).isLocallyDisconnecting();
+            return null;
+        }).when(activeUnholdableCall).disconnect();
+
+        assertHoldActiveCallForNewCall(
+                newCall,
+                activeUnholdableCall /* activeCall */,
+                false /* isCallControlRequest */,
+                true  /* expectOnResult */);
+
+        verify(activeUnholdableCall, atLeast(1)).disconnect();
     }
 
     @SmallTest
@@ -2736,8 +3225,9 @@
     public void testQueryCurrentLocationCheckOnReceiveResult() throws Exception {
         ConnectionServiceWrapper service = new ConnectionServiceWrapper(
                 new ComponentName(mContext.getPackageName(),
-                        mContext.getPackageName().getClass().getName()),
-                null, mPhoneAccountRegistrar, mCallsManager, mContext, mLock, null);
+                        mContext.getPackageName().getClass().getName()), null,
+                        mPhoneAccountRegistrar, mCallsManager, mContext, mLock, null,
+                        mFeatureFlags);
 
         CompletableFuture<String> resultFuture = new CompletableFuture<>();
         try {
@@ -2757,6 +3247,35 @@
         assertTrue(result.contains("onReceiveResult"));
     }
 
+    @Test
+    public void testConnectionServiceCreateConnectionTimeout() throws Exception {
+        ConnectionServiceWrapper service = new ConnectionServiceWrapper(
+                SIM_1_ACCOUNT.getAccountHandle().getComponentName(), null,
+                mPhoneAccountRegistrar, mCallsManager, mContext, mLock, null, mFeatureFlags);
+        TestScheduledExecutorService scheduledExecutorService = new TestScheduledExecutorService();
+        service.setScheduledExecutorService(scheduledExecutorService);
+        Call call = addSpyCall();
+        service.addCall(call);
+        when(call.isCreateConnectionComplete()).thenReturn(false);
+        CreateConnectionResponse response = mock(CreateConnectionResponse.class);
+
+        service.createConnection(call, response);
+        waitUntilConditionIsTrueOrTimeout(new Condition() {
+            @Override
+            public Object expected() {
+                return true;
+            }
+
+            @Override
+            public Object actual() {
+                return scheduledExecutorService.isRunnableScheduledAtTime(15000L);
+            }
+        }, 5000L, "Expected job failed to schedule");
+        scheduledExecutorService.advanceTime(15000L);
+        verify(response).handleCreateConnectionFailure(
+                eq(new DisconnectCause(DisconnectCause.ERROR)));
+    }
+
     @SmallTest
     @Test
     public void testOnFailedOutgoingCallUnholdsCallAfterLocallyDisconnect() {
@@ -2806,10 +3325,10 @@
         Bundle extras = mock(Bundle.class);
         when(call.getIntentExtras()).thenReturn(extras);
 
-        final int attachmentDisabledMask = ~0
-                ^ CallScreeningService.CallResponse.CALL_COMPOSER_ATTACHMENT_LOCATION
-                ^ CallScreeningService.CallResponse.CALL_COMPOSER_ATTACHMENT_SUBJECT
-                ^ CallScreeningService.CallResponse.CALL_COMPOSER_ATTACHMENT_PRIORITY;
+        final int attachmentDisabledMask = ~(
+                CallScreeningService.CallResponse.CALL_COMPOSER_ATTACHMENT_LOCATION |
+                CallScreeningService.CallResponse.CALL_COMPOSER_ATTACHMENT_SUBJECT |
+                CallScreeningService.CallResponse.CALL_COMPOSER_ATTACHMENT_PRIORITY);
         CallScreeningService.ParcelableCallResponse response =
                 mock(CallScreeningService.ParcelableCallResponse.class);
         when(response.getCallComposerAttachmentsToShow()).thenReturn(attachmentDisabledMask);
@@ -2966,11 +3485,9 @@
         mCallsManager.createActionSetCallStateAndPerformAction(
                 call, CallState.DISCONNECTED, "");
 
-        verify(sourceCall).onConnectionEvent(eq(Connection.EVENT_HANDOVER_FAILED), any());
         verify(sourceCall).onHandoverFailed(
                     android.telecom.Call.Callback.HANDOVER_FAILURE_USER_REJECTED);
 
-        verify(call).sendCallEvent(eq(android.telecom.Call.EVENT_HANDOVER_FAILED), any());
         verify(call).markFinishedHandoverStateAndCleanup(HandoverState.HANDOVER_FAILED);
     }
 
@@ -2981,12 +3498,8 @@
         Call call = addSpyCall(CONNECTION_MGR_1_HANDLE, CallState.NEW);
         when(call.getHandoverDestinationCall()).thenReturn(destinationCall);
         when(call.getHandoverState()).thenReturn(HandoverState.HANDOVER_FROM_STARTED);
-
         mCallsManager.createActionSetCallStateAndPerformAction(
                 call, CallState.DISCONNECTED, "");
-
-        verify(destinationCall).sendCallEvent(
-                eq(android.telecom.Call.EVENT_HANDOVER_SOURCE_DISCONNECTED), any());
     }
 
     @SmallTest
@@ -3000,11 +3513,8 @@
         mCallsManager.createActionSetCallStateAndPerformAction(
                 call, CallState.DISCONNECTED, "");
 
-        verify(call).onConnectionEvent(eq(Connection.EVENT_HANDOVER_COMPLETE), any());
         verify(call).onHandoverComplete();
         verify(call).markFinishedHandoverStateAndCleanup(HandoverState.HANDOVER_COMPLETE);
-        verify(destinationCall).sendCallEvent(
-                eq(android.telecom.Call.EVENT_HANDOVER_COMPLETE), any());
         verify(destinationCall).onHandoverComplete();
     }
 
@@ -3021,11 +3531,8 @@
         mCallsManager.createActionSetCallStateAndPerformAction(
                 call, CallState.DISCONNECTED, "");
 
-        verify(call).onConnectionEvent(eq(Connection.EVENT_HANDOVER_COMPLETE), any());
         verify(call).onHandoverComplete();
         verify(call).markFinishedHandoverStateAndCleanup(HandoverState.HANDOVER_COMPLETE);
-        verify(destinationCall).sendCallEvent(
-                eq(android.telecom.Call.EVENT_HANDOVER_COMPLETE), any());
         verify(destinationCall).onHandoverComplete();
         verify(otherCall).disconnect();
     }
@@ -3129,18 +3636,14 @@
         when(mBlockedNumbersAdapter.shouldShowEmergencyCallNotification(any(Context.class)))
                 .thenReturn(true);
         mComponentContextFixture.getBroadcastReceivers().forEach(c -> c.onReceive(mContext,
-                new Intent(
-                        BlockedNumberContract.SystemContract
-                                .ACTION_BLOCK_SUPPRESSION_STATE_CHANGED)));
+                new Intent(BlockedNumbersManager.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED)));
         verify(mBlockedNumbersAdapter).updateEmergencyCallNotification(any(Context.class),
                 eq(true));
 
         when(mBlockedNumbersAdapter.shouldShowEmergencyCallNotification(any(Context.class)))
                 .thenReturn(false);
         mComponentContextFixture.getBroadcastReceivers().forEach(c -> c.onReceive(mContext,
-                new Intent(
-                        BlockedNumberContract.SystemContract
-                                .ACTION_BLOCK_SUPPRESSION_STATE_CHANGED)));
+                new Intent(BlockedNumbersManager.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED)));
         verify(mBlockedNumbersAdapter).updateEmergencyCallNotification(any(Context.class),
                 eq(false));
     }
@@ -3159,9 +3662,7 @@
         // WHEN
         when(mPhoneAccountRegistrar.getPhoneAccountUnchecked(any()))
                 .thenReturn(SM_W_DIFFERENT_PACKAGE_AND_USER);
-        UserManager um = mContext.getSystemService(UserManager.class);
-        when(um.isUserAdmin(eq(mCallsManager.getCurrentUserHandle().getIdentifier())))
-                .thenReturn(true);
+        when(mMockCurrentUserManager.isAdminUser()).thenReturn(true);
 
         // THEN
         mCallsManager.processIncomingCallIntent(SELF_MANAGED_W_CUSTOM_HANDLE, new Bundle(), false);
@@ -3250,6 +3751,26 @@
         assertTrue(mCallsManager.isInSelfManagedCall(TEST_PACKAGE_NAME, TEST_USER_HANDLE));
     }
 
+    @SmallTest
+    @Test
+    public void testBindToBtServiceSeparately() {
+        when(mFeatureFlags.separatelyBindToBtIncallService()).thenReturn(true);
+        Call call = addSpyCall(CallState.NEW);
+        CallFilteringResult result = new CallFilteringResult.Builder()
+                .setShouldAllowCall(true)
+                .setShouldReject(false)
+                .build();
+        when(mInCallController.isBoundAndConnectedToBTService(any(UserHandle.class)))
+                .thenReturn(false);
+
+        mCallsManager.onCallFilteringComplete(call, result, false);
+
+        InOrder inOrder = inOrder(mInCallController, call, mInCallController);
+
+        inOrder.verify(mInCallController).bindToBTService(eq(call), eq(null));
+        inOrder.verify(call).setState(eq(CallState.RINGING), anyString());
+    }
+
 
     private Call addSpyCall() {
         return addSpyCall(SIM_2_HANDLE, CallState.ACTIVE);
@@ -3280,8 +3801,8 @@
         // Mocks some methods to not call the real method.
         doNothing().when(callSpy).unhold();
         doNothing().when(callSpy).hold();
-        doNothing().when(callSpy).answer(Matchers.anyInt());
-        doNothing().when(callSpy).setStartWithSpeakerphoneOn(Matchers.anyBoolean());
+        doNothing().when(callSpy).answer(ArgumentMatchers.anyInt());
+        doNothing().when(callSpy).setStartWithSpeakerphoneOn(ArgumentMatchers.anyBoolean());
 
         mCallsManager.addCall(callSpy);
         return callSpy;
@@ -3295,8 +3816,8 @@
         doNothing().when(callSpy).unhold();
         doNothing().when(callSpy).hold();
         doNothing().when(callSpy).disconnect();
-        doNothing().when(callSpy).answer(Matchers.anyInt());
-        doNothing().when(callSpy).setStartWithSpeakerphoneOn(Matchers.anyBoolean());
+        doNothing().when(callSpy).answer(ArgumentMatchers.anyInt());
+        doNothing().when(callSpy).setStartWithSpeakerphoneOn(ArgumentMatchers.anyBoolean());
 
         return callSpy;
     }
@@ -3321,7 +3842,8 @@
                 false /* shouldAttachToExistingConnection*/,
                 false /* isConference */,
                 mClockProxy,
-                mToastFactory);
+                mToastFactory,
+                mFeatureFlags);
         ongoingCall.setState(initialState, "just cuz");
         if (targetPhoneAccount == SELF_MANAGED_HANDLE
                 || targetPhoneAccount == SELF_MANAGED_2_HANDLE) {
@@ -3338,6 +3860,9 @@
         callback.onRequestFocusDone(call);
     }
 
+    /**
+     * Set up 2 SIM accounts in DSDS mode, where only one SIM can be active at a time for calls.
+     */
     private void setupMsimAccounts() {
         TelephonyManager mockTelephonyManager = mComponentContextFixture.getTelephonyManager();
         when(mockTelephonyManager.getMaxNumberOfSimultaneouslyActiveSims()).thenReturn(1);
@@ -3348,9 +3873,88 @@
                 new ArrayList<>(Arrays.asList(SIM_1_HANDLE, SIM_2_HANDLE)));
     }
 
+    /**
+     * Set up 2 SIM accounts in DSDS mode and one call provider, where there is no restriction on
+     * simultaneous calls across accounts.
+     */
+    private void setupAccountsNoSimultaneousCallingRestriction() {
+        when(mPhoneAccountRegistrar.getCallCapablePhoneAccounts(any(), anyBoolean(),
+                any(), anyInt(), anyInt(), anyBoolean())).thenReturn(
+                new ArrayList<>(Arrays.asList(SIM_1_HANDLE, SIM_2_HANDLE, CALL_PROVIDER_HANDLE)));
+        when(mPhoneAccountRegistrar.getSimPhoneAccountsOfCurrentUser()).thenReturn(
+                new ArrayList<>(Arrays.asList(SIM_1_HANDLE, SIM_2_HANDLE)));
+    }
+
+    /**
+     * Set up the call capable PhoneAccounts passed in here to have simultaneous calling
+     * restrictions. If the callCapableHandle is a SIM account and it is in the restriction set, we
+     * will set that callCapableHandle's restriction to the Set. If not, we will set the restriction
+     * to allow no other simultaneous calls.
+     */
+    private void setupAccountsWithCallingRestriction(List<PhoneAccountHandle> callCapableHandles,
+            Set<PhoneAccountHandle> restrictions) {
+        when(mPhoneAccountRegistrar.getCallCapablePhoneAccounts(any(), anyBoolean(),
+                any(), anyInt(), anyInt(), anyBoolean())).thenReturn(
+                new ArrayList<>(callCapableHandles));
+        List<PhoneAccountHandle> allSims = Arrays.asList(SIM_1_HANDLE, SIM_2_HANDLE,
+                SIM_3_HANDLE);
+        List<PhoneAccountHandle> simsToTest = callCapableHandles.stream()
+                .filter(allSims::contains).toList();
+        when(mPhoneAccountRegistrar.getSimPhoneAccountsOfCurrentUser()).thenReturn(simsToTest);
+        // Remap the PhoneAccounts to inherit the restriction set
+        for (PhoneAccountHandle callCapableHandle : callCapableHandles) {
+            PhoneAccount pa = mPhoneAccountRegistrar.getPhoneAccount(
+                    callCapableHandle, callCapableHandle.getUserHandle());
+            assertNotNull("test setup error: could not find PA for PAH:" + callCapableHandle,
+                    pa);
+            // For simplicity, for testing only apply restrictions to SIM accounts
+            if (!allSims.contains(callCapableHandle)) continue;
+            if (restrictions.contains(callCapableHandle)) {
+                pa = new PhoneAccount.Builder(pa)
+                        .setSimultaneousCallingRestriction(restrictions).build();
+            } else {
+                pa = new PhoneAccount.Builder(pa)
+                        .setSimultaneousCallingRestriction(Collections.emptySet()).build();
+            }
+            when(mPhoneAccountRegistrar.getPhoneAccount(eq(callCapableHandle),
+                    any())).thenReturn(pa);
+        }
+    }
+
     private void setMaxActiveVoiceSubscriptions(int num) {
         TelephonyManager mockTelephonyManager = mComponentContextFixture.getTelephonyManager();
         when(mockTelephonyManager.getPhoneCapability()).thenReturn(mPhoneCapability);
         when(mPhoneCapability.getMaxActiveVoiceSubscriptions()).thenReturn(num);
     }
+
+   private void assertHoldActiveCallForNewCall(
+            Call newCall,
+            Call activeCall,
+            boolean isCallControlRequest,
+            boolean expectOnResult)
+            throws InterruptedException {
+        CountDownLatch latch = new CountDownLatch(1);
+        when(mFeatureFlags.transactionalHoldDisconnectsUnholdable()).thenReturn(true);
+        when(mConnectionSvrFocusMgr.getCurrentFocusCall()).thenReturn(activeCall);
+        mCallsManager.transactionHoldPotentialActiveCallForNewCall(
+                newCall,
+                isCallControlRequest,
+                new LatchedOutcomeReceiver(latch, expectOnResult));
+        waitForCountDownLatch(latch);
+    }
+
+    private void waitUntilConditionIsTrueOrTimeout(Condition condition, long timeout,
+            String description) throws InterruptedException {
+        final long start = System.currentTimeMillis();
+        while (!condition.expected().equals(condition.actual())
+                && System.currentTimeMillis() - start < timeout) {
+            sleep(50);
+        }
+        assertEquals(description, condition.expected(), condition.actual());
+    }
+
+    protected interface Condition {
+        Object expected();
+        Object actual();
+    }
 }
diff --git a/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java b/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
index cc22de2..25f94c6 100644
--- a/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
+++ b/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
@@ -16,6 +16,7 @@
 
 package com.android.server.telecom.tests;
 
+import com.android.server.telecom.flags.FeatureFlags;
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.Multimap;
 
@@ -27,6 +28,8 @@
 import org.mockito.stubbing.Answer;
 
 import android.Manifest;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
 import android.app.AppOpsManager;
 import android.app.NotificationManager;
 import android.app.StatusBarManager;
@@ -55,6 +58,7 @@
 import android.location.LocationManager;
 import android.media.AudioDeviceInfo;
 import android.media.AudioManager;
+import android.net.Uri;
 import android.os.BugreportManager;
 import android.os.Bundle;
 import android.os.DropBoxManager;
@@ -69,6 +73,7 @@
 import android.os.Vibrator;
 import android.os.VibratorManager;
 import android.permission.PermissionCheckerManager;
+import android.provider.BlockedNumbersManager;
 import android.telecom.ConnectionService;
 import android.telecom.Log;
 import android.telecom.InCallService;
@@ -81,8 +86,10 @@
 import android.util.DisplayMetrics;
 import android.view.accessibility.AccessibilityManager;
 
+import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
@@ -96,7 +103,7 @@
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.matches;
 import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Matchers.anyString;
+import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.doAnswer;
@@ -115,6 +122,7 @@
  */
 public class ComponentContextFixture implements TestFixture<Context> {
     private HandlerThread mHandlerThread;
+    private Map<UserHandle, Context> mContextsByUser = new HashMap<>();
 
     public class FakeApplicationContext extends MockContext {
         @Override
@@ -131,6 +139,9 @@
 
         @Override
         public Context createContextAsUser(UserHandle userHandle, int flags) {
+            if (mContextsByUser.containsKey(userHandle)) {
+                return mContextsByUser.get(userHandle);
+            }
             return this;
         }
 
@@ -245,6 +256,8 @@
                     return mSensorPrivacyManager;
                 case Context.ACCESSIBILITY_SERVICE:
                     return mAccessibilityManager;
+                case Context.BLOCKED_NUMBERS_SERVICE:
+                    return mBlockedNumbersManager;
                 default:
                     return null;
             }
@@ -284,6 +297,10 @@
                 return Context.DROPBOX_SERVICE;
             } else if (svcClass == BugreportManager.class) {
                 return Context.BUGREPORT_SERVICE;
+            } else if (svcClass == TelecomManager.class) {
+                return Context.TELECOM_SERVICE;
+            } else if (svcClass == BlockedNumbersManager.class) {
+                return Context.BLOCKED_NUMBERS_SERVICE;
             }
             throw new UnsupportedOperationException(svcClass.getName());
         }
@@ -409,12 +426,23 @@
         }
 
         @Override
+        public void sendBroadcastAsUser(Intent intent, UserHandle user, String receiverPermission) {
+            // Override so that this can be verified via spy.
+        }
+
+        @Override
         public void sendBroadcastAsUser(Intent intent, UserHandle user, String receiverPermission,
                 Bundle options) {
             // Override so that this can be verified via spy.
         }
 
         @Override
+        public void sendBroadcastAsUser(Intent intent, UserHandle user, String receiverPermission,
+                int appOp) {
+            // Override so that this can be verified via spy.
+        }
+
+        @Override
         public void sendOrderedBroadcastAsUser(Intent intent, UserHandle user,
                 String receiverPermission, BroadcastReceiver resultReceiver, Handler scheduler,
                 int initialCode, String initialData, Bundle initialExtras) {
@@ -616,9 +644,11 @@
     private final List<BroadcastReceiver> mBroadcastReceivers = new ArrayList<>();
 
     private TelecomManager mTelecomManager = mock(TelecomManager.class);
+    private BlockedNumbersManager mBlockedNumbersManager = mock(BlockedNumbersManager.class);
 
-    public ComponentContextFixture() {
+    public ComponentContextFixture(FeatureFlags featureFlags) {
         MockitoAnnotations.initMocks(this);
+        when(featureFlags.telecomResolveHiddenDependencies()).thenReturn(true);
         when(mResources.getConfiguration()).thenReturn(mResourceConfiguration);
         when(mResources.getString(anyInt())).thenReturn("");
         when(mResources.getStringArray(anyInt())).thenReturn(new String[0]);
@@ -701,7 +731,7 @@
             }
         }).when(mAppOpsManager).checkPackage(anyInt(), anyString());
 
-        when(mNotificationManager.matchesCallFilter(any(Bundle.class))).thenReturn(true);
+        when(mNotificationManager.matchesCallFilter(any(Uri.class))).thenReturn(true);
 
         when(mCarrierConfigManager.getConfig()).thenReturn(new PersistableBundle());
         when(mCarrierConfigManager.getConfigForSubId(anyInt())).thenReturn(new PersistableBundle());
@@ -735,6 +765,14 @@
         mServiceInfoByComponentName.put(componentName, serviceInfo);
     }
 
+    public void removeConnectionService(
+            ComponentName componentName,
+            IConnectionService service)
+            throws Exception {
+        removeService(ConnectionService.SERVICE_INTERFACE, componentName, service);
+        mServiceInfoByComponentName.remove(componentName);
+    }
+
     public void addInCallService(
             ComponentName componentName,
             IInCallService service,
@@ -756,6 +794,8 @@
                 componentName.getPackageName() });
         when(mPackageManager.checkPermission(eq(Manifest.permission.CONTROL_INCALL_EXPERIENCE),
                 eq(componentName.getPackageName()))).thenReturn(PackageManager.PERMISSION_GRANTED);
+        when(mPackageManager.checkPermission(eq(Manifest.permission.INTERACT_ACROSS_USERS),
+                eq(componentName.getPackageName()))).thenReturn(PackageManager.PERMISSION_GRANTED);
         when(mPermissionCheckerManager.checkPermission(
                 eq(Manifest.permission.CONTROL_INCALL_EXPERIENCE),
                 any(AttributionSourceState.class), anyString(), anyBoolean(), anyBoolean(),
@@ -794,6 +834,11 @@
         when(mResources.getStringArray(eq(id))).thenReturn(value);
     }
 
+    public void putRawResource(int id, String content) {
+        when(mResources.openRawResource(id))
+                .thenReturn(new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)));
+    }
+
     public void setTelecomManager(TelecomManager telecomManager) {
         mTelecomManager = telecomManager;
     }
@@ -802,6 +847,10 @@
         mSubscriptionManager = subscriptionManager;
     }
 
+    public SubscriptionManager getSubscriptionManager() {
+        return mSubscriptionManager;
+    }
+
     public TelephonyManager getTelephonyManager() {
         return mTelephonyManager;
     }
@@ -822,12 +871,31 @@
         return mBroadcastReceivers;
     }
 
+    public TelephonyRegistryManager getTelephonyRegistryManager() {
+        return mTelephonyRegistryManager;
+    }
+
+    /**
+     * For testing purposes, add a context for a specific user.
+     * @param userHandle the userhandle
+     * @param context the context
+     */
+    public void addContextForUser(UserHandle userHandle, Context context) {
+        mContextsByUser.put(userHandle, context);
+    }
+
     private void addService(String action, ComponentName name, IInterface service) {
         mComponentNamesByAction.put(action, name);
         mServiceByComponentName.put(name, service);
         mComponentNameByService.put(service, name);
     }
 
+    private void removeService(String action, ComponentName name, IInterface service) {
+        mComponentNamesByAction.remove(action, name);
+        mServiceByComponentName.remove(name);
+        mComponentNameByService.remove(service);
+    }
+
     private List<ResolveInfo> doQueryIntentServices(Intent intent, int flags) {
         List<ResolveInfo> result = new ArrayList<>();
         for (ComponentName componentName : mComponentNamesByAction.get(intent.getAction())) {
diff --git a/tests/src/com/android/server/telecom/tests/ConnectionServiceFocusManagerTest.java b/tests/src/com/android/server/telecom/tests/ConnectionServiceFocusManagerTest.java
index 0d6ceba..ab2c679 100644
--- a/tests/src/com/android/server/telecom/tests/ConnectionServiceFocusManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/ConnectionServiceFocusManagerTest.java
@@ -16,7 +16,19 @@
 
 package com.android.server.telecom.tests;
 
-import android.test.suitebuilder.annotation.SmallTest;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import androidx.test.filters.SmallTest;
+
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallState;
 import com.android.server.telecom.CallsManager;
@@ -32,17 +44,6 @@
 import org.mockito.Mock;
 import org.mockito.Mockito;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.Matchers.any;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
 @RunWith(JUnit4.class)
 public class ConnectionServiceFocusManagerTest extends TelecomTestCase {
 
diff --git a/tests/src/com/android/server/telecom/tests/ConnectionServiceWrapperTest.java b/tests/src/com/android/server/telecom/tests/ConnectionServiceWrapperTest.java
new file mode 100644
index 0000000..c815e8e
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/ConnectionServiceWrapperTest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2024 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 org.junit.Assert.assertNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.content.ComponentName;
+import android.os.UserHandle;
+
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.ConnectionServiceRepository;
+import com.android.server.telecom.ConnectionServiceWrapper;
+import com.android.server.telecom.PhoneAccountRegistrar;
+import com.android.server.telecom.TelecomSystem;
+import com.android.server.telecom.flags.FeatureFlags;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class ConnectionServiceWrapperTest extends TelecomTestCase {
+    @Override
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+    }
+
+    @Override
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    /**
+     * Verify we don't crash when getting the last known cell id and there is no telephony.
+     */
+    @Test
+    public void testGetLastKnownCellIdWhenNoTelephony() {
+        ConnectionServiceWrapper wrapper = new ConnectionServiceWrapper(
+                ComponentName.unflattenFromString("foo/baz"),
+                mock(ConnectionServiceRepository.class),
+                mock(PhoneAccountRegistrar.class),
+                mock(CallsManager.class),
+                mContext,
+                new TelecomSystem.SyncRoot() {},
+                UserHandle.CURRENT,
+                mock(FeatureFlags.class));
+        when(mComponentContextFixture.getTelephonyManager().getLastKnownCellIdentity())
+                .thenThrow(new UnsupportedOperationException("Bee boop"));
+        assertNull(wrapper.getLastKnownCellIdentity());
+   }
+}
diff --git a/tests/src/com/android/server/telecom/tests/ContactsAsyncHelperTest.java b/tests/src/com/android/server/telecom/tests/ContactsAsyncHelperTest.java
index 10cac93..7adb32c 100644
--- a/tests/src/com/android/server/telecom/tests/ContactsAsyncHelperTest.java
+++ b/tests/src/com/android/server/telecom/tests/ContactsAsyncHelperTest.java
@@ -17,11 +17,11 @@
 package com.android.server.telecom.tests;
 
 import static org.junit.Assert.assertTrue;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Matchers.anyObject;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Matchers.isNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyObject;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.timeout;
@@ -32,11 +32,10 @@
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
-import android.os.Handler;
 import android.os.Looper;
-import android.test.suitebuilder.annotation.SmallTest;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
 
 import com.android.server.telecom.ContactsAsyncHelper;
 
@@ -49,7 +48,6 @@
 
 import java.io.FileNotFoundException;
 import java.io.InputStream;
-import java.util.concurrent.Executor;
 
 @RunWith(JUnit4.class)
 public class ContactsAsyncHelperTest extends TelecomTestCase {
diff --git a/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java b/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java
index 8a85a87..ddbc250 100644
--- a/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java
+++ b/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java
@@ -16,53 +16,16 @@
 
 package com.android.server.telecom.tests;
 
-import android.Manifest;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.graphics.drawable.Icon;
-import android.net.Uri;
-import android.os.Binder;
-import android.os.UserHandle;
-import android.telecom.DisconnectCause;
-import android.telecom.PhoneAccount;
-import android.telecom.PhoneAccountHandle;
-import android.telephony.SubscriptionManager;
-import android.test.suitebuilder.annotation.SmallTest;
-
-import com.android.server.telecom.Call;
-import com.android.server.telecom.CallIdMapper;
-import com.android.server.telecom.ConnectionServiceFocusManager;
-import com.android.server.telecom.ConnectionServiceRepository;
-import com.android.server.telecom.ConnectionServiceWrapper;
-import com.android.server.telecom.CreateConnectionProcessor;
-import com.android.server.telecom.CreateConnectionResponse;
-import com.android.server.telecom.PhoneAccountRegistrar;
-
-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.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.stubbing.Answer;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Random;
-import java.util.UUID;
-
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyList;
 import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.eq;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.after;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
@@ -70,11 +33,57 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.content.ComponentName;
+import android.content.Context;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.UserHandle;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.telecom.DisconnectCause;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.SubscriptionManager;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.telephony.flags.Flags;
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallIdMapper;
+import com.android.server.telecom.CallState;
+import com.android.server.telecom.ConnectionServiceFocusManager;
+import com.android.server.telecom.ConnectionServiceRepository;
+import com.android.server.telecom.ConnectionServiceWrapper;
+import com.android.server.telecom.CreateConnectionProcessor;
+import com.android.server.telecom.CreateConnectionResponse;
+import com.android.server.telecom.CreateConnectionTimeout;
+import com.android.server.telecom.PhoneAccountRegistrar;
+import com.android.server.telecom.Timeouts;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Random;
+import java.util.UUID;
+
 /**
  * Unit testing for CreateConnectionProcessor as well as CreateConnectionTimeout classes.
  */
 @RunWith(JUnit4.class)
 public class CreateConnectionProcessorTest extends TelecomTestCase {
+    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
 
     private static final String TEST_PACKAGE = "com.android.server.telecom.tests";
     private static final String TEST_CLASS =
@@ -91,12 +100,14 @@
     Call mMockCall;
     @Mock
     ConnectionServiceFocusManager mConnectionServiceFocusManager;
+    @Mock Timeouts.Adapter mTimeoutsAdapter;
 
     CreateConnectionProcessor mTestCreateConnectionProcessor;
+    CreateConnectionTimeout mTestCreateConnectionTimeout;
 
     private ArrayList<PhoneAccount> phoneAccounts;
-    private HashMap<Integer,Integer> mSubToSlot;
-    private HashMap<PhoneAccount,Integer> mAccountToSub;
+    private HashMap<Integer, Integer> mSubToSlot;
+    private HashMap<PhoneAccount, Integer> mAccountToSub;
 
     @Override
     @Before
@@ -119,11 +130,11 @@
                          return null;
                      }
                  }
-                ).when(mConnectionServiceFocusManager).requestFocus(any(), any());
+        ).when(mConnectionServiceFocusManager).requestFocus(any(), any());
 
         mTestCreateConnectionProcessor = new CreateConnectionProcessor(mMockCall,
                 mMockConnectionServiceRepository, mMockCreateConnectionResponse,
-                mMockAccountRegistrar, mContext);
+                mMockAccountRegistrar, mContext, mFeatureFlags, mTimeoutsAdapter);
 
         mAccountToSub = new HashMap<>();
         phoneAccounts = new ArrayList<>();
@@ -146,6 +157,11 @@
                 .thenReturn(phoneAccounts);
         when(mMockCall.getAssociatedUser()).
                 thenReturn(Binder.getCallingUserHandle());
+
+        mTestCreateConnectionTimeout = new CreateConnectionTimeout(mContext, mMockAccountRegistrar,
+                makeConnectionServiceWrapper(), mMockCall, mTimeoutsAdapter);
+
+        mSetFlagsRule.enableFlags(Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG);
     }
 
     @Override
@@ -302,7 +318,8 @@
 
     /**
      * Ensure that when a test emergency number is being dialed and we restrict the usable
-     * PhoneAccounts using {@link PhoneAccountRegistrar#filterRestrictedPhoneAccounts(List)}, the
+     * PhoneAccounts using {@link PhoneAccountRegistrar#filterRestrictedPhoneAccounts(List)},
+     * the
      * test emergency call is sent on the filtered PhoneAccount.
      */
     @SmallTest
@@ -340,7 +357,8 @@
     }
 
     /**
-     * Ensure that when no phone accounts (visible to the user) are available for the call, we use
+     * Ensure that when no phone accounts (visible to the user) are available for the call, we
+     * use
      * an available sim from other another user (on the condition that the user has the
      * INTERACT_ACROSS_USERS permission).
      */
@@ -370,7 +388,8 @@
     }
 
     /**
-     * Ensure that the non-emergency capable PhoneAccount and the SIM manager is not chosen to place
+     * Ensure that the non-emergency capable PhoneAccount and the SIM manager is not chosen to
+     * place
      * the emergency call if there is an emergency capable PhoneAccount available as well.
      */
     @SmallTest
@@ -407,7 +426,8 @@
     }
 
     /**
-     * 1) Ensure that if there is a non-SIM PhoneAccount, it is not chosen as the Phone Account to
+     * 1) Ensure that if there is a non-SIM PhoneAccount, it is not chosen as the Phone Account
+     * to
      * dial the emergency call.
      * 2) Register multiple emergency capable PhoneAccounts. Since there is not preference, we
      * default to sending on the lowest slot.
@@ -447,8 +467,10 @@
     }
 
     /**
-     * Ensure that the call goes out on the PhoneAccount that has the CAPABILITY_EMERGENCY_PREFERRED
-     * capability, even if the user specifically chose the other emergency capable PhoneAccount.
+     * Ensure that the call goes out on the PhoneAccount that has the
+     * CAPABILITY_EMERGENCY_PREFERRED
+     * capability, even if the user specifically chose the other emergency capable
+     * PhoneAccount.
      */
     @SmallTest
     @Test
@@ -480,7 +502,8 @@
     }
 
     /**
-     * If there is no phone account with CAPABILITY_EMERGENCY_PREFERRED capability, choose the user
+     * If there is no phone account with CAPABILITY_EMERGENCY_PREFERRED capability, choose the
+     * user
      * chosen target account.
      */
     @SmallTest
@@ -554,7 +577,8 @@
     }
 
     /**
-     * If the user preferred PhoneAccount is associated with an invalid slot, place on the other,
+     * If the user preferred PhoneAccount is associated with an invalid slot, place on the
+     * other,
      * valid slot.
      */
     @SmallTest
@@ -692,7 +716,8 @@
 
     /**
      * Tests to verify that the
-     * {@link CreateConnectionProcessor#sortSimPhoneAccountsForEmergency(List, PhoneAccount)} can
+     * {@link CreateConnectionProcessor#sortSimPhoneAccountsForEmergency(List, PhoneAccount)}
+     * can
      * successfully sort without running into sort issues related to the hashcodes of the
      * PhoneAccounts.
      */
@@ -709,6 +734,176 @@
         }
     }
 
+    @Test
+    public void testIsTimeoutNeededForCall_nonEmergencyCall() {
+        when(mMockCall.isEmergencyCall()).thenReturn(false);
+
+        assertFalse(mTestCreateConnectionTimeout.isTimeoutNeededForCall(null, null));
+    }
+
+    @Test
+    public void testIsTimeoutNeededForCall_noConnectionManager() {
+        when(mMockCall.isEmergencyCall()).thenReturn(true);
+        List<PhoneAccountHandle> phoneAccountHandles = new ArrayList<>();
+        // Put in a regular phone account handle
+        PhoneAccount regularAccount = makePhoneAccount("tel_acct1",
+                PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION);
+        phoneAccountHandles.add(regularAccount.getAccountHandle());
+        // Create a connection manager for the call and do not include in phoneAccountHandles
+        createNewConnectionManagerPhoneAccountForCall(mMockCall, "cm_acct", 0);
+
+        assertFalse(mTestCreateConnectionTimeout.isTimeoutNeededForCall(phoneAccountHandles, null));
+    }
+
+    @Test
+    public void testIsTimeoutNeededForCall_usingConnectionManager() {
+        when(mMockCall.isEmergencyCall()).thenReturn(true);
+        List<PhoneAccountHandle> phoneAccountHandles = new ArrayList<>();
+        // Put in a regular phone account handle
+        PhoneAccount regularAccount = makePhoneAccount("tel_acct1",
+                PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION);
+        phoneAccountHandles.add(regularAccount.getAccountHandle());
+        // Create a connection manager for the call and include it in phoneAccountHandles
+        PhoneAccount callManagerPA = createNewConnectionManagerPhoneAccountForCall(mMockCall,
+                "cm_acct", 0);
+        phoneAccountHandles.add(callManagerPA.getAccountHandle());
+
+        assertFalse(mTestCreateConnectionTimeout.isTimeoutNeededForCall(
+                phoneAccountHandles, callManagerPA.getAccountHandle()));
+    }
+
+    @Test
+    public void testIsTimeoutNeededForCall_NotSystemSimCallManager() {
+        when(mMockCall.isEmergencyCall()).thenReturn(true);
+        List<PhoneAccountHandle> phoneAccountHandles = new ArrayList<>();
+        // Put in a regular phone account handle
+        PhoneAccount regularAccount = makePhoneAccount("tel_acct1",
+                PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION);
+        phoneAccountHandles.add(regularAccount.getAccountHandle());
+        // Create a connection manager for the call and include it in phoneAccountHandles
+        PhoneAccount callManagerPA = createNewConnectionManagerPhoneAccountForCall(mMockCall,
+                "cm_acct", 0);
+        phoneAccountHandles.add(callManagerPA.getAccountHandle());
+
+        assertFalse(mTestCreateConnectionTimeout.isTimeoutNeededForCall(phoneAccountHandles, null));
+    }
+
+    @Test
+    public void testIsTimeoutNeededForCall() {
+        when(mMockCall.isEmergencyCall()).thenReturn(true);
+        when(mMockAccountRegistrar.getSystemSimCallManagerComponent()).thenReturn(
+                new ComponentName(TEST_PACKAGE, TEST_CLASS));
+
+        List<PhoneAccountHandle> phoneAccountHandles = new ArrayList<>();
+        // Put in a regular phone account handle
+        PhoneAccount regularAccount = makePhoneAccount("tel_acct1",
+                PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION);
+        phoneAccountHandles.add(regularAccount.getAccountHandle());
+        // Create a connection manager for the call and include it in phoneAccountHandles
+        int capability = PhoneAccount.CAPABILITY_SUPPORTS_VOICE_CALLING_INDICATIONS
+                | PhoneAccount.CAPABILITY_VOICE_CALLING_AVAILABLE;
+        PhoneAccount callManagerPA = createNewConnectionManagerPhoneAccountForCall(mMockCall,
+                "cm_acct", capability);
+        PhoneAccount phoneAccountWithService = makeQuickAccount("cm_acct", capability, null);
+        when(mMockAccountRegistrar.getPhoneAccount(callManagerPA.getAccountHandle(),
+                callManagerPA.getAccountHandle().getUserHandle()))
+                .thenReturn(phoneAccountWithService);
+        phoneAccountHandles.add(callManagerPA.getAccountHandle());
+
+        assertTrue(mTestCreateConnectionTimeout.isTimeoutNeededForCall(phoneAccountHandles, null));
+    }
+
+    @Test
+    public void testConnTimeout_carrierSatelliteEnabled_noInServiceConnManager_callNeverTimesOut() {
+        when(mMockCall.isEmergencyCall()).thenReturn(true);
+        when(mMockCall.isTestEmergencyCall()).thenReturn(false);
+        when(mMockCall.getHandle()).thenReturn(Uri.parse(""));
+        when(mMockCall.getState()).thenReturn(CallState.DIALING);
+        // Primary phone account, meant to fail
+        PhoneAccount regularAccount = makePhoneAccount("tel_acct1",
+                PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION
+                        | PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS);
+        phoneAccounts.add(regularAccount);
+        when(mMockAccountRegistrar.getOutgoingPhoneAccountForSchemeOfCurrentUser(
+                nullable(String.class))).thenReturn(regularAccount.getAccountHandle());
+        when(mMockAccountRegistrar.getSystemSimCallManagerComponent()).thenReturn(
+                new ComponentName(TEST_PACKAGE, TEST_CLASS));
+        PhoneAccount callManagerPA = getNewEmergencyConnectionManagerPhoneAccount(
+                "cm_acct", PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS
+                        | PhoneAccount.CAPABILITY_SUPPORTS_VOICE_CALLING_INDICATIONS);
+        phoneAccounts.add(callManagerPA);
+        ConnectionServiceWrapper service = makeConnectionServiceWrapper();
+        when(mMockAccountRegistrar.getSimCallManagerFromCall(mMockCall)).thenReturn(
+                callManagerPA.getAccountHandle());
+        when(mMockAccountRegistrar.getPhoneAccount(eq(callManagerPA.getAccountHandle()),
+                any())).thenReturn(callManagerPA);
+        Duration timeout = Duration.ofMillis(10);
+        when(mTimeoutsAdapter.getEmergencyCallTimeoutMillis(any())).thenReturn(timeout.toMillis());
+        when(mTimeoutsAdapter.getEmergencyCallTimeoutRadioOffMillis(any())).thenReturn(
+                timeout.toMillis());
+
+
+        mTestCreateConnectionProcessor.process();
+
+        // Validate the call is not disconnected after the timeout.
+        verify(service, after(timeout.toMillis() + 100).never()).disconnect(eq(mMockCall));
+    }
+
+    @Test
+    public void testConnTimeout_carrierSatelliteEnabled_inServiceConnManager_callTimesOut() {
+        when(mMockCall.isEmergencyCall()).thenReturn(true);
+        when(mMockCall.isTestEmergencyCall()).thenReturn(false);
+        when(mMockCall.getHandle()).thenReturn(Uri.parse(""));
+        when(mMockCall.getState()).thenReturn(CallState.DIALING);
+        // Primary phone account, meant to fail
+        PhoneAccount regularAccount = makePhoneAccount("tel_acct1",
+                PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION
+                        | PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS);
+        phoneAccounts.add(regularAccount);
+        when(mMockAccountRegistrar.getOutgoingPhoneAccountForSchemeOfCurrentUser(
+                nullable(String.class))).thenReturn(regularAccount.getAccountHandle());
+        when(mMockAccountRegistrar.getSystemSimCallManagerComponent()).thenReturn(
+                new ComponentName(TEST_PACKAGE, TEST_CLASS));
+        PhoneAccount callManagerPA = getNewEmergencyConnectionManagerPhoneAccount(
+                "cm_acct", PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS
+                        | PhoneAccount.CAPABILITY_SUPPORTS_VOICE_CALLING_INDICATIONS
+                        | PhoneAccount.CAPABILITY_VOICE_CALLING_AVAILABLE
+        );
+        phoneAccounts.add(callManagerPA);
+        ConnectionServiceWrapper service = makeConnectionServiceWrapper();
+        when(mMockAccountRegistrar.getSimCallManagerFromCall(mMockCall)).thenReturn(
+                callManagerPA.getAccountHandle());
+        when(mMockAccountRegistrar.getPhoneAccount(eq(callManagerPA.getAccountHandle()),
+                any())).thenReturn(callManagerPA);
+        Duration timeout = Duration.ofMillis(10);
+        when(mTimeoutsAdapter.getEmergencyCallTimeoutMillis(any())).thenReturn(timeout.toMillis());
+        when(mTimeoutsAdapter.getEmergencyCallTimeoutRadioOffMillis(any())).thenReturn(
+                timeout.toMillis());
+
+        mTestCreateConnectionProcessor.process();
+
+        // Validate the call was disconnected after the timeout.
+        verify(service, after(timeout.toMillis() + 100)).disconnect(eq(mMockCall));
+    }
+
+    /**
+     * Verifies when telephony is not available that we just get invalid sub id for a phone acct.
+     */
+    @SmallTest
+    @Test
+    public void testTelephonyAdapterWhenNoTelephony() {
+        PhoneAccount telephonyAcct = makePhoneAccount("test-acct",
+                PhoneAccount.CAPABILITY_CALL_PROVIDER
+                        | PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS);
+
+        CreateConnectionProcessor.ITelephonyManagerAdapterImpl impl
+                = new CreateConnectionProcessor.ITelephonyManagerAdapterImpl();
+        when(mComponentContextFixture.getTelephonyManager().
+                getSubscriptionId(any(PhoneAccountHandle.class)))
+                .thenThrow(new UnsupportedOperationException("Bee boop"));
+        assertEquals(-1, impl.getSubIdForPhoneAccount(mContext, telephonyAcct));
+    }
+
     /**
      * Generates random phone accounts.
      * @param seed random seed to use for random UUIDs; passed in for determinism.
@@ -840,9 +1035,10 @@
 
     private ConnectionServiceWrapper makeConnectionServiceWrapper() {
         ConnectionServiceWrapper wrapper = mock(ConnectionServiceWrapper.class);
+
         when(mMockConnectionServiceRepository.getService(
-                eq(makeQuickConnectionServiceComponentName()),
-                any(UserHandle.class))).thenReturn(wrapper);
+                eq(makeQuickConnectionServiceComponentName()), any(UserHandle.class)))
+                .thenReturn(wrapper);
         return wrapper;
     }
 
diff --git a/tests/src/com/android/server/telecom/tests/DefaultDialerCacheTest.java b/tests/src/com/android/server/telecom/tests/DefaultDialerCacheTest.java
index e733465..18f2eb0 100644
--- a/tests/src/com/android/server/telecom/tests/DefaultDialerCacheTest.java
+++ b/tests/src/com/android/server/telecom/tests/DefaultDialerCacheTest.java
@@ -16,6 +16,14 @@
 
 package com.android.server.telecom.tests;
 
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -24,7 +32,8 @@
 import android.net.Uri;
 import android.os.Handler;
 import android.os.UserHandle;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.filters.SmallTest;
 
 import com.android.server.telecom.DefaultDialerCache;
 import com.android.server.telecom.RoleManagerAdapter;
@@ -38,14 +47,6 @@
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 
-import static org.junit.Assert.assertEquals;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Matchers.isNull;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
 @RunWith(JUnit4.class)
 public class DefaultDialerCacheTest extends TelecomTestCase {
 
diff --git a/tests/src/com/android/server/telecom/tests/DirectToVoicemailFilterTest.java b/tests/src/com/android/server/telecom/tests/DirectToVoicemailFilterTest.java
index 2ab4e78..097061b 100644
--- a/tests/src/com/android/server/telecom/tests/DirectToVoicemailFilterTest.java
+++ b/tests/src/com/android/server/telecom/tests/DirectToVoicemailFilterTest.java
@@ -26,11 +26,11 @@
 import android.net.Uri;
 import android.provider.CallLog;
 import android.telecom.CallerInfo;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.filters.SmallTest;
 
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallerInfoLookupHelper;
-import com.android.server.telecom.callfiltering.CallFilter;
 import com.android.server.telecom.callfiltering.CallFilteringResult;
 import com.android.server.telecom.callfiltering.DirectToVoicemailFilter;
 
@@ -44,7 +44,6 @@
 import java.util.concurrent.CompletionStage;
 import java.util.concurrent.TimeUnit;
 
-
 @RunWith(JUnit4.class)
 public class DirectToVoicemailFilterTest extends TelecomTestCase {
     @Mock private CallerInfoLookupHelper mCallerInfoLookupHelper;
diff --git a/tests/src/com/android/server/telecom/tests/DisconnectedCallNotifierTest.java b/tests/src/com/android/server/telecom/tests/DisconnectedCallNotifierTest.java
index 05c5071..3eacc54 100644
--- a/tests/src/com/android/server/telecom/tests/DisconnectedCallNotifierTest.java
+++ b/tests/src/com/android/server/telecom/tests/DisconnectedCallNotifierTest.java
@@ -146,6 +146,19 @@
         verify(mNotificationManager).cancelAsUser(anyString(), anyInt(), any());
     }
 
+    /**
+     * Verifies when there is no telephony available, that we'll still be able to determine a
+     * country iso.
+     */
+    @Test
+    @SmallTest
+    public void testGetCountryIsoWithNoTelephony() {
+        DisconnectedCallNotifier notifier = new DisconnectedCallNotifier(mContext, mCallsManager);
+        when(mComponentContextFixture.getTelephonyManager().getNetworkCountryIso())
+                .thenThrow(new UnsupportedOperationException("Bee boop"));
+        assertNotNull(notifier.getCurrentCountryIso(mContext));
+    }
+
     private Call createCall(DisconnectCause cause) {
         Call call = mock(Call.class);
         when(call.getDisconnectCause()).thenReturn(cause);
diff --git a/tests/src/com/android/server/telecom/tests/DtmfLocalTonePlayerTest.java b/tests/src/com/android/server/telecom/tests/DtmfLocalTonePlayerTest.java
index 85a5278..5ccfc38 100644
--- a/tests/src/com/android/server/telecom/tests/DtmfLocalTonePlayerTest.java
+++ b/tests/src/com/android/server/telecom/tests/DtmfLocalTonePlayerTest.java
@@ -15,8 +15,15 @@
 
 package com.android.server.telecom.tests;
 
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
 import android.media.ToneGenerator;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.filters.SmallTest;
 
 import com.android.server.telecom.Call;
 import com.android.server.telecom.DtmfLocalTonePlayer;
@@ -29,12 +36,6 @@
 import org.junit.runners.JUnit4;
 import org.mockito.Mock;
 
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
 @RunWith(JUnit4.class)
 public class DtmfLocalTonePlayerTest extends TelecomTestCase {
     private static final int TIMEOUT = 2000;
diff --git a/tests/src/com/android/server/telecom/tests/EmergencyCallDiagnosticLoggerTest.java b/tests/src/com/android/server/telecom/tests/EmergencyCallDiagnosticLoggerTest.java
index 3cb8196..41426c0 100644
--- a/tests/src/com/android/server/telecom/tests/EmergencyCallDiagnosticLoggerTest.java
+++ b/tests/src/com/android/server/telecom/tests/EmergencyCallDiagnosticLoggerTest.java
@@ -17,7 +17,7 @@
 package com.android.server.telecom.tests;
 
 
-import static android.telephony.TelephonyManager.EmergencyCallDiagnosticParams;
+import static android.telephony.TelephonyManager.EmergencyCallDiagnosticData;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
@@ -35,6 +35,7 @@
 import android.net.Uri;
 import android.os.BugreportManager;
 import android.os.DropBoxManager;
+import android.os.UserHandle;
 import android.telecom.DisconnectCause;
 import android.telecom.PhoneAccount;
 import android.telecom.PhoneAccountHandle;
@@ -128,6 +129,7 @@
         when(mTimeouts.getDaysBackToSearchEmergencyDiagnosticEntries()).
                 thenReturn(DAYS_BACK_TO_SEARCH_EMERGENCY_DIAGNOSTIC_ENTRIES);
         when(mClockProxy.currentTimeMillis()).thenReturn(System.currentTimeMillis());
+        when(mMockCallsManager.getCurrentUserHandle()).thenReturn(UserHandle.CURRENT);
 
         mEmergencyCallDiagnosticLogger = new EmergencyCallDiagnosticLogger(mTm, mBrm,
                 mTimeouts, mDbm, Runnable::run, mClockProxy);
@@ -171,7 +173,8 @@
                 false /* shouldAttachToExistingConnection*/,
                 false /* isConference */,
                 mMockClockProxy,
-                mMockToastProxy);
+                mMockToastProxy,
+                mFeatureFlags);
     }
 
     /**
@@ -235,16 +238,16 @@
         mEmergencyCallDiagnosticLogger.reportStuckCall(call);
 
         //for stuck calls, we should always be persisting some data
-        ArgumentCaptor<EmergencyCallDiagnosticParams> captor =
-                ArgumentCaptor.forClass(EmergencyCallDiagnosticParams.class);
+        ArgumentCaptor<EmergencyCallDiagnosticData> captor =
+                ArgumentCaptor.forClass(EmergencyCallDiagnosticData.class);
         verify(mTm, times(1)).persistEmergencyCallDiagnosticData(eq(DROP_BOX_TAG),
                 captor.capture());
-        EmergencyCallDiagnosticParams dp = captor.getValue();
+        EmergencyCallDiagnosticData ecdData = captor.getValue();
 
-        assertNotNull(dp);
+        assertNotNull(ecdData);
         assertTrue(
-                dp.isLogcatCollectionEnabled() || dp.isTelecomDumpSysCollectionEnabled()
-                        || dp.isTelephonyDumpSysCollectionEnabled());
+                ecdData.isLogcatCollectionEnabled() || ecdData.isTelecomDumpsysCollectionEnabled()
+                        || ecdData.isTelephonyDumpsysCollectionEnabled());
 
         //tracking should end
         assertEquals(0, mEmergencyCallDiagnosticLogger.getEmergencyCallsMap().size());
@@ -262,17 +265,16 @@
         mEmergencyCallDiagnosticLogger.onCallRemoved(call);
 
         //for non-local disconnect of non-active call,  we should always be persisting some data
-        ArgumentCaptor<TelephonyManager.EmergencyCallDiagnosticParams> captor =
-                ArgumentCaptor.forClass(
-                        TelephonyManager.EmergencyCallDiagnosticParams.class);
+        ArgumentCaptor<EmergencyCallDiagnosticData> captor =
+                ArgumentCaptor.forClass(EmergencyCallDiagnosticData.class);
         verify(mTm, times(1)).persistEmergencyCallDiagnosticData(eq(DROP_BOX_TAG),
                 captor.capture());
-        TelephonyManager.EmergencyCallDiagnosticParams dp = captor.getValue();
+        EmergencyCallDiagnosticData ecdData = captor.getValue();
 
-        assertNotNull(dp);
+        assertNotNull(ecdData);
         assertTrue(
-                dp.isLogcatCollectionEnabled() || dp.isTelecomDumpSysCollectionEnabled()
-                        || dp.isTelephonyDumpSysCollectionEnabled());
+                ecdData.isLogcatCollectionEnabled() || ecdData.isTelecomDumpsysCollectionEnabled()
+                        || ecdData.isTelephonyDumpsysCollectionEnabled());
 
         //tracking should end
         assertEquals(0, mEmergencyCallDiagnosticLogger.getEmergencyCallsMap().size());
diff --git a/tests/src/com/android/server/telecom/tests/EmergencyCallHelperTest.java b/tests/src/com/android/server/telecom/tests/EmergencyCallHelperTest.java
index 692d720..cc1c38a 100644
--- a/tests/src/com/android/server/telecom/tests/EmergencyCallHelperTest.java
+++ b/tests/src/com/android/server/telecom/tests/EmergencyCallHelperTest.java
@@ -19,12 +19,24 @@
 import static android.Manifest.permission.ACCESS_BACKGROUND_LOCATION;
 import static android.Manifest.permission.ACCESS_FINE_LOCATION;
 
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
 import android.content.ComponentName;
 import android.content.ContentResolver;
 import android.content.pm.PackageManager;
 import android.os.UserHandle;
 import android.telecom.PhoneAccountHandle;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.filters.SmallTest;
 
 import com.android.server.telecom.Call;
 import com.android.server.telecom.DefaultDialerCache;
@@ -39,18 +51,6 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.assertFalse;
-
-import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-import static org.mockito.Mockito.any;
-
 @RunWith(JUnit4.class)
 public class EmergencyCallHelperTest extends TelecomTestCase {
   private static final String SYSTEM_DIALER_PACKAGE = "abc.xyz";
@@ -75,7 +75,7 @@
     mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
     when(mContext.getPackageManager()).thenReturn(mPackageManager);
     mEmergencyCallHelper = new EmergencyCallHelper(mContext, mDefaultDialerCache,
-        mTimeoutsAdapter);
+        mTimeoutsAdapter, mFeatureFlags);
     when(mDefaultDialerCache.getSystemDialerApplication()).thenReturn(SYSTEM_DIALER_PACKAGE);
 
     //start with no perms
@@ -185,6 +185,61 @@
 
   @SmallTest
   @Test
+  public void testPermGrantAndRevokeForEmergencyCall() {
+
+    when(mFeatureFlags.preventRedundantLocationPermissionGrantAndRevoke()).thenReturn(true);
+
+    mEmergencyCallHelper.maybeGrantTemporaryLocationPermission(mCall, mUserHandle);
+    mEmergencyCallHelper.maybeRevokeTemporaryLocationPermission();
+
+    //permissions should be granted then revoked
+    verifyGrantInvokedFor(ACCESS_BACKGROUND_LOCATION);
+    verifyGrantInvokedFor(ACCESS_FINE_LOCATION);
+    verifyRevokeInvokedFor(ACCESS_BACKGROUND_LOCATION);
+    verifyRevokeInvokedFor(ACCESS_FINE_LOCATION);
+  }
+
+  @SmallTest
+  @Test
+  public void testPermGrantAndRevokeForMultiEmergencyCall() {
+
+    when(mFeatureFlags.preventRedundantLocationPermissionGrantAndRevoke()).thenReturn(true);
+
+    //first call is emergency call
+    mEmergencyCallHelper.maybeGrantTemporaryLocationPermission(mCall, mUserHandle);
+    //second call is emergency call
+    mEmergencyCallHelper.maybeGrantTemporaryLocationPermission(mCall, mUserHandle);
+    mEmergencyCallHelper.maybeRevokeTemporaryLocationPermission();
+
+    //permissions should be granted then revoked
+    verifyGrantInvokedFor(ACCESS_BACKGROUND_LOCATION);
+    verifyGrantInvokedFor(ACCESS_FINE_LOCATION);
+    verifyRevokeInvokedFor(ACCESS_BACKGROUND_LOCATION);
+    verifyRevokeInvokedFor(ACCESS_FINE_LOCATION);
+  }
+
+  @SmallTest
+  @Test
+  public void testPermGrantAndRevokeForEmergencyCallAndNormalCall() {
+
+    when(mFeatureFlags.preventRedundantLocationPermissionGrantAndRevoke()).thenReturn(true);
+
+    //first call is emergency call
+    mEmergencyCallHelper.maybeGrantTemporaryLocationPermission(mCall, mUserHandle);
+    //second call is normal call
+    when(mCall.isEmergencyCall()).thenReturn(false);
+    mEmergencyCallHelper.maybeGrantTemporaryLocationPermission(mCall, mUserHandle);
+    mEmergencyCallHelper.maybeRevokeTemporaryLocationPermission();
+
+    //permissions should be granted then revoked
+    verifyGrantInvokedFor(ACCESS_BACKGROUND_LOCATION);
+    verifyGrantInvokedFor(ACCESS_FINE_LOCATION);
+    verifyRevokeInvokedFor(ACCESS_BACKGROUND_LOCATION);
+    verifyRevokeInvokedFor(ACCESS_FINE_LOCATION);
+  }
+
+  @SmallTest
+  @Test
   public void testNoPermGrantForNonEmergencyCall() {
 
     when(mCall.isEmergencyCall()).thenReturn(false);
diff --git a/tests/src/com/android/server/telecom/tests/EventManagerTest.java b/tests/src/com/android/server/telecom/tests/EventManagerTest.java
index c7d3541..cee0f39 100644
--- a/tests/src/com/android/server/telecom/tests/EventManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/EventManagerTest.java
@@ -16,11 +16,17 @@
 
 package com.android.server.telecom.tests;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
 import android.net.Uri;
 import android.os.Build;
 import android.telecom.Log;
 import android.telecom.Logging.EventManager;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.filters.SmallTest;
 
 import org.junit.After;
 import org.junit.Before;
@@ -32,11 +38,6 @@
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.stream.Collectors;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-
 /**
  * Unit tests for android.telecom.Logging.EventManager.
  */
diff --git a/tests/src/com/android/server/telecom/tests/HeadsetMediaButtonTest.java b/tests/src/com/android/server/telecom/tests/HeadsetMediaButtonTest.java
index 0bfa987..b7e5921 100644
--- a/tests/src/com/android/server/telecom/tests/HeadsetMediaButtonTest.java
+++ b/tests/src/com/android/server/telecom/tests/HeadsetMediaButtonTest.java
@@ -16,17 +16,26 @@
 
 package com.android.server.telecom.tests;
 
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
 import android.content.Intent;
 import android.media.session.MediaSession;
-import android.test.suitebuilder.annotation.MediumTest;
-import android.test.suitebuilder.annotation.SmallTest;
 import android.view.KeyEvent;
 
+import androidx.test.filters.MediumTest;
+import androidx.test.filters.SmallTest;
+
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallsManager;
 import com.android.server.telecom.HeadsetMediaButton;
-import com.android.server.telecom.TelecomSystem;
 import com.android.server.telecom.HeadsetMediaButton.MediaSessionWrapper;
+import com.android.server.telecom.TelecomSystem;
 
 import org.junit.After;
 import org.junit.Before;
@@ -37,15 +46,6 @@
 import org.mockito.Mock;
 import org.mockito.Mockito;
 
-import static org.junit.Assert.assertEquals;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.eq;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
 @RunWith(JUnit4.class)
 public class HeadsetMediaButtonTest extends TelecomTestCase {
     private static final int TEST_TIMEOUT_MILLIS = 1000;
diff --git a/tests/src/com/android/server/telecom/tests/InCallControllerTests.java b/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
index 683a5e2..6a515fd 100644
--- a/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
+++ b/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
@@ -19,7 +19,6 @@
 import static com.android.server.telecom.InCallController.IN_CALL_SERVICE_NOTIFICATION_ID;
 import static com.android.server.telecom.InCallController.NOTIFICATION_TAG;
 import static com.android.server.telecom.tests.TelecomSystemTest.TEST_TIMEOUT;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -28,15 +27,13 @@
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.ArgumentMatchers.anyObject;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.matches;
 import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Matchers.anyString;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
@@ -53,6 +50,7 @@
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.UiModeManager;
+import android.compat.testing.PlatformCompatChangeRule;
 import android.content.AttributionSource;
 import android.content.AttributionSourceState;
 import android.content.BroadcastReceiver;
@@ -70,7 +68,6 @@
 import android.content.pm.ServiceInfo;
 import android.content.pm.UserInfo;
 import android.content.res.Resources;
-import android.compat.testing.PlatformCompatChangeRule;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
@@ -87,10 +84,11 @@
 import android.telecom.PhoneAccountHandle;
 import android.telecom.TelecomManager;
 import android.test.mock.MockContext;
-import android.test.suitebuilder.annotation.MediumTest;
-import android.test.suitebuilder.annotation.SmallTest;
 import android.text.TextUtils;
 
+import androidx.test.filters.MediumTest;
+import androidx.test.filters.SmallTest;
+
 import com.android.dx.mockito.inline.extended.ExtendedMockito;
 import com.android.internal.telecom.IInCallAdapter;
 import com.android.internal.telecom.IInCallService;
@@ -143,6 +141,8 @@
     @Mock PackageManager mMockPackageManager;
     @Mock PermissionCheckerManager mMockPermissionCheckerManager;
     @Mock Call mMockCall;
+    @Mock Call mMockSystemCall1;
+    @Mock Call mMockSystemCall2;
     @Mock Resources mMockResources;
     @Mock AppOpsManager mMockAppOpsManager;
     @Mock MockContext mMockContext;
@@ -154,10 +154,9 @@
     @Mock NotificationManager mNotificationManager;
     @Mock PermissionInfo mMockPermissionInfo;
     @Mock InCallController.InCallServiceInfo mInCallServiceInfo;
-    @Mock private AnomalyReporterAdapter mAnomalyReporterAdapter;
     @Mock UserManager mMockUserManager;
-    @Mock UserInfo mMockUserInfo;
-    @Mock UserInfo mMockChildUserInfo; //work profile
+    @Mock Context mMockCreateContextAsUser;
+    @Mock UserManager mMockCurrentUserManager;
 
     @Rule
     public TestRule compatChangeRule = new PlatformCompatChangeRule();
@@ -184,6 +183,9 @@
     private static final String APPOP_NONUI_PKG = "appop_nonui_pkg";
     private static final String APPOP_NONUI_CLASS = "appop_nonui_cls";
     private static final int APPOP_NONUI_UID = 7;
+    private static final String BT_PKG = "btpkg";
+    private static final String BT_CLS = "btcls";
+    private static final int BT_UID = 900974;
 
     private static final PhoneAccountHandle PA_HANDLE =
             new PhoneAccountHandle(new ComponentName("pa_pkg", "pa_cls"),
@@ -223,8 +225,9 @@
         when(mDefaultDialerCache.getSystemDialerApplication()).thenReturn(SYS_PKG);
         when(mDefaultDialerCache.getSystemDialerComponent()).thenReturn(
                 new ComponentName(SYS_PKG, SYS_CLASS));
+        when(mDefaultDialerCache.getBTInCallServicePackages()).thenReturn(new String[] {BT_PKG});
         mEmergencyCallHelper = new EmergencyCallHelper(mMockContext, mDefaultDialerCache,
-                mTimeoutsAdapter);
+                mTimeoutsAdapter, mFeatureFlags);
         when(mMockCallsManager.getRoleManagerAdapter()).thenReturn(mMockRoleManagerAdapter);
         when(mMockContext.getSystemService(eq(Context.NOTIFICATION_SERVICE)))
                 .thenReturn(mNotificationManager);
@@ -236,7 +239,7 @@
                 "com.android.server.telecom.tests", null));
         mInCallController = new InCallController(mMockContext, mLock, mMockCallsManager,
                 mMockSystemStateHelper, mDefaultDialerCache, mTimeoutsAdapter,
-                mEmergencyCallHelper, mCarModeTracker, mClockProxy);
+                mEmergencyCallHelper, mCarModeTracker, mClockProxy, mFeatureFlags);
         // Capture the broadcast receiver registered.
         doAnswer(invocation -> {
             mRegisteredReceiver = invocation.getArgument(0);
@@ -267,6 +270,8 @@
                     return new String[] { NONUI_PKG };
                 case APPOP_NONUI_UID:
                     return new String[] { APPOP_NONUI_PKG };
+                case BT_UID:
+                    return new String[] { BT_PKG };
             }
             return null;
         }).when(mMockPackageManager).getPackagesForUid(anyInt());
@@ -306,9 +311,15 @@
         when(mMockContext.getSystemService(eq(Context.USER_SERVICE))).thenReturn(mMockUserManager);
         when(mMockContext.getSystemService(eq(UserManager.class)))
                 .thenReturn(mMockUserManager);
+        when(mMockContext.createContextAsUser(any(UserHandle.class), eq(0)))
+                .thenReturn(mMockCreateContextAsUser);
+        when(mMockCreateContextAsUser.getSystemService(eq(UserManager.class)))
+                .thenReturn(mMockCurrentUserManager);
         // Mock user info to allow binding on user stored in the phone account (mUserHandle).
-        when(mMockUserManager.getUserInfo(anyInt())).thenReturn(mMockUserInfo);
-        when(mMockUserInfo.isManagedProfile()).thenReturn(true);
+        when(mFeatureFlags.separatelyBindToBtIncallService()).thenReturn(false);
+        when(mFeatureFlags.telecomResolveHiddenDependencies()).thenReturn(true);
+        when(mMockCurrentUserManager.isManagedProfile()).thenReturn(true);
+        when(mFeatureFlags.profileUserSupport()).thenReturn(false);
     }
 
     @Override
@@ -517,7 +528,8 @@
         when(mMockCall.isEmergencyCall()).thenReturn(true);
         when(mMockContext.getSystemService(eq(UserManager.class)))
             .thenReturn(mMockUserManager);
-        when(mMockUserManager.isQuietModeEnabled(any(UserHandle.class))).thenReturn(false);
+        when(mMockCurrentUserManager.isQuietModeEnabled(any(UserHandle.class)))
+                .thenReturn(false);
         when(mMockCall.isIncoming()).thenReturn(false);
         when(mMockCall.getAssociatedUser()).thenReturn(mUserHandle);
         when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
@@ -588,9 +600,11 @@
         when(mMockCall.isEmergencyCall()).thenReturn(true);
         when(mMockCall.isIncoming()).thenReturn(true);
         when(mMockCall.getAssociatedUser()).thenReturn(DUMMY_USER_HANDLE);
+        when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
         when(mMockContext.getSystemService(eq(UserManager.class)))
             .thenReturn(mMockUserManager);
-        when(mMockUserManager.isQuietModeEnabled(any(UserHandle.class))).thenReturn(true);
+        when(mMockCurrentUserManager.isQuietModeEnabled(any(UserHandle.class)))
+                .thenReturn(true);
         setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
         setupMockPackageManagerLocationPermission(SYS_PKG, false /* granted */);
 
@@ -617,9 +631,11 @@
         when(mMockCall.isInECBM()).thenReturn(true);
         when(mMockCall.isIncoming()).thenReturn(true);
         when(mMockCall.getAssociatedUser()).thenReturn(DUMMY_USER_HANDLE);
+        when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
         when(mMockContext.getSystemService(eq(UserManager.class)))
                 .thenReturn(mMockUserManager);
-        when(mMockUserManager.isQuietModeEnabled(any(UserHandle.class))).thenReturn(true);
+        when(mMockCurrentUserManager.isQuietModeEnabled(any(UserHandle.class)))
+                .thenReturn(true);
         setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
         setupMockPackageManagerLocationPermission(SYS_PKG, false /* granted */);
 
@@ -647,10 +663,12 @@
         when(mMockCall.isInECBM()).thenReturn(true);
         when(mMockCall.isIncoming()).thenReturn(true);
         when(mMockCall.getAssociatedUser()).thenReturn(DUMMY_USER_HANDLE);
+        when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
         when(mMockContext.getSystemService(eq(UserManager.class)))
                 .thenReturn(mMockUserManager);
-        when(mMockUserManager.isQuietModeEnabled(any(UserHandle.class))).thenReturn(false);
-        when(mMockUserManager.isUserAdmin(anyInt())).thenReturn(false);
+        when(mMockCurrentUserManager.isQuietModeEnabled(any(UserHandle.class)))
+                .thenReturn(false);
+        when(mMockCurrentUserManager.isAdminUser()).thenReturn(false);
         setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
         setupMockPackageManagerLocationPermission(SYS_PKG, false /* granted */);
 
@@ -677,8 +695,9 @@
         when(mMockCall.getAssociatedUser()).thenReturn(DUMMY_USER_HANDLE);
         when(mMockContext.getSystemService(eq(UserManager.class)))
             .thenReturn(mMockUserManager);
-        when(mMockUserManager.isQuietModeEnabled(any(UserHandle.class))).thenReturn(false);
-        when(mMockUserManager.isUserAdmin(anyInt())).thenReturn(true);
+        when(mMockCurrentUserManager.isQuietModeEnabled(any(UserHandle.class)))
+                .thenReturn(false);
+        when(mMockCurrentUserManager.isAdminUser()).thenReturn(true);
         setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
         setupMockPackageManagerLocationPermission(SYS_PKG, false /* granted */);
 
@@ -712,7 +731,8 @@
         when(mMockCall.isEmergencyCall()).thenReturn(true);
         when(mMockContext.getSystemService(eq(UserManager.class)))
             .thenReturn(mMockUserManager);
-        when(mMockUserManager.isQuietModeEnabled(any(UserHandle.class))).thenReturn(false);
+        when(mMockCurrentUserManager.isQuietModeEnabled(any(UserHandle.class)))
+                .thenReturn(false);
         when(mMockCall.isIncoming()).thenReturn(false);
         when(mMockCall.getAssociatedUser()).thenReturn(mUserHandle);
         when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
@@ -990,7 +1010,8 @@
         when(mMockCall.isEmergencyCall()).thenReturn(true);
         when(mMockContext.getSystemService(eq(UserManager.class)))
                 .thenReturn(mMockUserManager);
-        when(mMockUserManager.isQuietModeEnabled(any(UserHandle.class))).thenReturn(false);
+        when(mMockCurrentUserManager.isQuietModeEnabled(any(UserHandle.class)))
+                .thenReturn(false);
         when(mMockCall.isIncoming()).thenReturn(false);
         when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
         when(mMockCall.getIntentExtras()).thenReturn(callExtras);
@@ -1100,7 +1121,8 @@
         when(mMockCall.isEmergencyCall()).thenReturn(true);
         when(mMockContext.getSystemService(eq(UserManager.class)))
                 .thenReturn(mMockUserManager);
-        when(mMockUserManager.isQuietModeEnabled(any(UserHandle.class))).thenReturn(false);
+        when(mMockCurrentUserManager.isQuietModeEnabled(any(UserHandle.class)))
+                .thenReturn(false);
         when(mMockCall.isIncoming()).thenReturn(false);
         when(mMockCall.getAssociatedUser()).thenReturn(mUserHandle);
         when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
@@ -1776,7 +1798,7 @@
         // Force the difference between the phone account user and current user. This is supposed to
         // simulate a secondary user placing a call over an unassociated sim.
         assertFalse(mUserHandle.equals(UserHandle.USER_CURRENT));
-        when(mMockUserInfo.isManagedProfile()).thenReturn(false);
+        when(mMockCurrentUserManager.isManagedProfile()).thenReturn(false);
 
         mInCallController.bindToServices(mMockCall);
 
@@ -1839,7 +1861,66 @@
         assertNull(mInCallController.getInCallServiceConnections().get(testUser));
     }
 
-    private void setupMocksForWorkProfileTest() {
+    @Test
+    public void testRemoveAllServiceConnections_MultiUser() throws Exception {
+        when(mFeatureFlags.associatedUserRefactorForWorkProfile()).thenReturn(true);
+        setupMocks(false /* isExternalCall */);
+        setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
+        UserHandle workUser = new UserHandle(12);
+        when(mMockCurrentUserManager.isManagedProfile()).thenReturn(false);
+        when(mMockCall.getAssociatedUser()).thenReturn(workUser);
+        setupFakeSystemCall(mMockSystemCall1, 1);
+        setupFakeSystemCall(mMockSystemCall2, 2);
+
+        // Add "work" call to service. The mapping should've been inserted
+        // with the workUser as the key.
+        mInCallController.onCallAdded(mMockCall);
+        // Add system call to service. The mapping should've been
+        // inserted with the system user as the key.
+        mInCallController.onCallAdded(mMockSystemCall1);
+
+        ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
+        // Make sure we bound to the system call as well as the work call.
+        verify(mMockContext, times(2)).bindServiceAsUser(
+                bindIntentCaptor.capture(),
+                any(ServiceConnection.class),
+                eq(serviceBindingFlags),
+                eq(UserHandle.CURRENT));
+        assertTrue(mInCallController.getInCallServiceConnections().containsKey(workUser));
+        assertTrue(mInCallController.getInCallServiceConnections().containsKey(UserHandle.SYSTEM));
+
+        // Remove the work call. This leverages getUserFromCall to remove the ICS mapping.
+        when(mMockCallsManager.getCalls()).thenReturn(Collections.singletonList(mMockSystemCall1));
+        mInCallController.onCallRemoved(mMockCall);
+        waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT);
+        // Verify that the mapping was properly removed.
+        assertNull(mInCallController.getInCallServiceConnections().get(workUser));
+        // Verify mapping for system user is still present.
+        assertNotNull(mInCallController.getInCallServiceConnections().get(UserHandle.SYSTEM));
+
+        // Add another system call
+        mInCallController.onCallAdded(mMockSystemCall2);
+        when(mMockCallsManager.getCalls()).thenReturn(Collections.singletonList(mMockSystemCall2));
+        // Remove first system call and verify that mapping is present
+        mInCallController.onCallRemoved(mMockSystemCall1);
+        waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT);
+        // Verify mapping for system user is still present.
+        assertNotNull(mInCallController.getInCallServiceConnections().get(UserHandle.SYSTEM));
+        // Remove last system call and verify that connection isn't present in ICS mapping.
+        when(mMockCallsManager.getCalls()).thenReturn(Collections.emptyList());
+        mInCallController.onCallRemoved(mMockSystemCall2);
+        waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT);
+        assertNull(mInCallController.getInCallServiceConnections().get(UserHandle.SYSTEM));
+    }
+
+    private void setupFakeSystemCall(@Mock Call call, int id) {
+        when(call.getAssociatedUser()).thenReturn(UserHandle.SYSTEM);
+        when(call.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
+        when(call.getAnalytics()).thenReturn(new Analytics.CallInfo());
+        when(call.getId()).thenReturn("TC@" + id);
+    }
+
+    private void setupMocksForProfileTest() {
         when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
         when(mMockCallsManager.isInEmergencyCall()).thenReturn(false);
         when(mMockChildUserCall.isIncoming()).thenReturn(false);
@@ -1852,27 +1933,15 @@
         when(mMockChildUserCall.visibleToInCallService()).thenReturn(true);
 
         //Setup up parent and child/work profile relation
-        when(mMockUserInfo.getUserHandle()).thenReturn(mParentUserHandle);
-        when(mMockChildUserInfo.getUserHandle()).thenReturn(mChildUserHandle);
-        when(mMockUserInfo.isManagedProfile()).thenReturn(false);
-        when(mMockChildUserInfo.isManagedProfile()).thenReturn(true);
         when(mMockChildUserCall.getAssociatedUser()).thenReturn(mChildUserHandle);
-        when(mMockCallsManager.getCurrentUserHandle()).thenReturn(mChildUserHandle);
-        when(mMockUserManager.getProfileParent(mChildUserHandle.getIdentifier())).thenReturn(
-                mMockUserInfo);
+        when(mMockCallsManager.getCurrentUserHandle()).thenReturn(mParentUserHandle);
         when(mMockUserManager.getProfileParent(mChildUserHandle)).thenReturn(mParentUserHandle);
-        when(mMockUserManager.getUserInfo(eq(mParentUserHandle.getIdentifier()))).thenReturn(
-                mMockUserInfo);
-        when(mMockUserManager.getUserInfo(eq(mChildUserHandle.getIdentifier()))).thenReturn(
-                mMockChildUserInfo);
-        when(mMockUserManager.isManagedProfile(mChildUserHandle.getIdentifier())).thenReturn(true);
-        when(mMockUserManager.isManagedProfile(mParentUserHandle.getIdentifier())).thenReturn(
-                false);
+        when(mFeatureFlags.profileUserSupport()).thenReturn(true);
     }
 
     @Test
-    public void testManagedProfileCallQueriesIcsUsingParentUserToo() throws Exception {
-        setupMocksForWorkProfileTest();
+    public void testProfileCallQueriesIcsUsingParentUserToo() throws Exception {
+        setupMocksForProfileTest();
         setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
         setupMockPackageManager(true /* default */,
                 true /*useNonUiInCalls*/, true /*useAppOpNonUiInCalls*/,
@@ -1881,9 +1950,8 @@
                 true /*includeSelfManagedCallsInCarModeDialer*/,
                 true /*includeSelfManagedCallsInNonUi*/);
 
-        //pass in call by child/work-profileuser
+        //pass in call by child/profile user
         mInCallController.bindToServices(mMockChildUserCall);
-
         // Verify that queryIntentServicesAsUser is also called with parent handle
         // Query for the different InCallServices
         ArgumentCaptor<Integer> userIdCaptor = ArgumentCaptor.forClass(Integer.class);
@@ -1901,6 +1969,34 @@
                 userIds.contains(mParentUserHandle.getIdentifier()));
     }
 
+    @Test
+    public void testSeparatelyBluetoothService() {
+        setupMocks(false /* isExternalCall */);
+        setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
+        Intent expectedIntent = new Intent(InCallService.SERVICE_INTERFACE);
+        expectedIntent.setPackage(mDefaultDialerCache.getBTInCallServicePackages()[0]);
+        LinkedList<ResolveInfo> resolveInfo = new LinkedList<ResolveInfo>();
+        resolveInfo.add(getBluetoothResolveinfo());
+        when(mFeatureFlags.separatelyBindToBtIncallService()).thenReturn(true);
+        when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
+        doAnswer(invocation -> {
+            Object[] args = invocation.getArguments();
+            LinkedList<ResolveInfo> resolveInfo1 = new LinkedList<ResolveInfo>();
+            Intent intent = (Intent) args[0];
+            if (intent.getAction().equals(InCallService.SERVICE_INTERFACE)) {
+                resolveInfo1.add(getBluetoothResolveinfo());
+            }
+            return resolveInfo1;
+        }).when(mMockPackageManager).queryIntentServicesAsUser(any(Intent.class), anyInt(),
+                anyInt());
+
+        mInCallController.bindToBTService(mMockCall, null);
+
+        ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
+        verify(mMockContext).bindServiceAsUser(captor.capture(), any(ServiceConnection.class),
+                anyInt(), any(UserHandle.class));
+    }
+
     private void setupMocks(boolean isExternalCall) {
         setupMocks(isExternalCall, false /* isSelfManagedCall */);
     }
@@ -2025,6 +2121,18 @@
         }};
     }
 
+    private ResolveInfo getBluetoothResolveinfo() {
+        return new ResolveInfo() {{
+            serviceInfo = new ServiceInfo();
+            serviceInfo.packageName = BT_PKG;
+            serviceInfo.name = BT_CLS;
+            serviceInfo.applicationInfo = new ApplicationInfo();
+            serviceInfo.applicationInfo.uid = BT_UID;
+            serviceInfo.enabled = true;
+            serviceInfo.permission = Manifest.permission.BIND_INCALL_SERVICE;
+        }};
+    }
+
     private void setupMockPackageManager(final boolean useDefaultDialer,
             final boolean useSystemDialer, final boolean includeExternalCalls) {
         setupMockPackageManager(useDefaultDialer, false, false, useSystemDialer, includeExternalCalls,
diff --git a/tests/src/com/android/server/telecom/tests/InCallServiceFixture.java b/tests/src/com/android/server/telecom/tests/InCallServiceFixture.java
index 88b5bb5..39381e6 100644
--- a/tests/src/com/android/server/telecom/tests/InCallServiceFixture.java
+++ b/tests/src/com/android/server/telecom/tests/InCallServiceFixture.java
@@ -16,6 +16,7 @@
 
 package com.android.server.telecom.tests;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.telecom.IInCallAdapter;
 import com.android.internal.telecom.IInCallService;
 
@@ -39,7 +40,7 @@
  * Controls a test {@link IInCallService} as would be provided by an InCall UI on a system.
  */
 public class InCallServiceFixture implements TestFixture<IInCallService> {
-
+    public static boolean sIgnoreOverrideAdapterFlag = false;
     public String mLatestCallId;
     public IInCallAdapter mInCallAdapter;
     public CallAudioState mCallAudioState;
@@ -53,10 +54,17 @@
     public CountDownLatch mUpdateCallLock = new CountDownLatch(1);
     public CountDownLatch mAddCallLock = new CountDownLatch(1);
 
+    @VisibleForTesting
+    public static void setIgnoreOverrideAdapterFlag(boolean flag) {
+        sIgnoreOverrideAdapterFlag = flag;
+    }
+
     public class FakeInCallService extends IInCallService.Stub {
         @Override
         public void setInCallAdapter(IInCallAdapter inCallAdapter) throws RemoteException {
-            if (mInCallAdapter != null && inCallAdapter != null) {
+            // sIgnoreOverrideAdapterFlag is being used to verify a scenario where the InCallAdapter
+            // gets set twice (secondary user places MO/MT call).
+            if (mInCallAdapter != null && inCallAdapter != null && !sIgnoreOverrideAdapterFlag) {
                 throw new RuntimeException("Adapter is already set");
             }
             if (mInCallAdapter == null && inCallAdapter == null) {
diff --git a/tests/src/com/android/server/telecom/tests/InCallTonePlayerTest.java b/tests/src/com/android/server/telecom/tests/InCallTonePlayerTest.java
index f11afc1..c9faa52 100644
--- a/tests/src/com/android/server/telecom/tests/InCallTonePlayerTest.java
+++ b/tests/src/com/android/server/telecom/tests/InCallTonePlayerTest.java
@@ -23,7 +23,6 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -33,8 +32,11 @@
 import android.media.AudioManager;
 import android.media.MediaPlayer;
 import android.media.ToneGenerator;
-import android.test.suitebuilder.annotation.SmallTest;
 
+import androidx.test.filters.SmallTest;
+
+import com.android.server.telecom.AsyncRingtonePlayer;
+import com.android.server.telecom.Call;
 import com.android.server.telecom.CallAudioManager;
 import com.android.server.telecom.CallAudioRoutePeripheralAdapter;
 import com.android.server.telecom.CallAudioRouteStateMachine;
@@ -69,6 +71,7 @@
     @Mock private InCallTonePlayer.ToneGeneratorFactory mToneGeneratorFactory;
     @Mock private WiredHeadsetManager mWiredHeadsetManager;
     @Mock private DockManager mDockManager;
+    @Mock private AsyncRingtonePlayer mRingtonePlayer;
     @Mock private BluetoothDevice mDevice;
     @Mock private BluetoothAdapter mBluetoothAdapter;
 
@@ -110,6 +113,8 @@
 
     @Mock
     private CallAudioManager mCallAudioManager;
+    @Mock
+    private Call mCall;
 
     private InCallTonePlayer mInCallTonePlayer;
 
@@ -120,15 +125,15 @@
 
         when(mToneGeneratorFactory.get(anyInt(), anyInt())).thenReturn(mToneGenerator);
         when(mMediaPlayerFactory.get(anyInt(), any())).thenReturn(mMediaPlayerAdapter);
-        doNothing().when(mCallAudioManager).setIsTonePlaying(anyBoolean());
+        doNothing().when(mCallAudioManager).setIsTonePlaying(any(Call.class), anyBoolean());
 
         mCallAudioRoutePeripheralAdapter = new CallAudioRoutePeripheralAdapter(
                 mCallAudioRouteStateMachine, mBluetoothRouteManager, mWiredHeadsetManager,
-                mDockManager);
+                mDockManager, mRingtonePlayer);
         mFactory = new InCallTonePlayer.Factory(mCallAudioRoutePeripheralAdapter, mLock,
                 mToneGeneratorFactory, mMediaPlayerFactory, mAudioManagerAdapter);
         mFactory.setCallAudioManager(mCallAudioManager);
-        mInCallTonePlayer = mFactory.createPlayer(InCallTonePlayer.TONE_CALL_ENDED);
+        mInCallTonePlayer = mFactory.createPlayer(mCall, InCallTonePlayer.TONE_CALL_ENDED);
     }
 
     @Override
@@ -145,11 +150,12 @@
         assertTrue(mInCallTonePlayer.startTone());
         // Verify we did play a tone.
         verify(mMediaPlayerFactory, timeout(TEST_TIMEOUT)).get(anyInt(), any());
-        verify(mCallAudioManager).setIsTonePlaying(eq(true));
+        verify(mCallAudioManager).setIsTonePlaying(any(Call.class), eq(true));
 
         mInCallTonePlayer.stopTone();
         // Timeouts due to threads!
-        verify(mCallAudioManager, timeout(TEST_TIMEOUT)).setIsTonePlaying(eq(false));
+        verify(mCallAudioManager, timeout(TEST_TIMEOUT)).setIsTonePlaying(any(Call.class),
+                eq(false));
     }
 
     @SmallTest
@@ -159,11 +165,12 @@
         assertTrue(mInCallTonePlayer.startTone());
         // Verify we did play a tone.
         verify(mMediaPlayerFactory, timeout(TEST_TIMEOUT)).get(anyInt(), any());
-        verify(mCallAudioManager).setIsTonePlaying(eq(true));
+        verify(mCallAudioManager).setIsTonePlaying(any(Call.class), eq(true));
 
         mInCallTonePlayer.stopTone();
         // Timeouts due to threads!
-        verify(mCallAudioManager, timeout(TEST_TIMEOUT)).setIsTonePlaying(eq(false));
+        verify(mCallAudioManager, timeout(TEST_TIMEOUT)).setIsTonePlaying(any(Call.class),
+                eq(false));
 
         // Correctness check: ensure we can't start the tone again.
         assertFalse(mInCallTonePlayer.startTone());
@@ -172,15 +179,16 @@
     @SmallTest
     @Test
     public void testInterruptToneGenerator() {
-        mInCallTonePlayer = mFactory.createPlayer(InCallTonePlayer.TONE_RING_BACK);
+        mInCallTonePlayer = mFactory.createPlayer(mCall, InCallTonePlayer.TONE_RING_BACK);
         when(mAudioManagerAdapter.isVolumeOverZero()).thenReturn(true);
         assertTrue(mInCallTonePlayer.startTone());
         verify(mToneGenerator, timeout(TEST_TIMEOUT)).startTone(anyInt());
-        verify(mCallAudioManager).setIsTonePlaying(eq(true));
+        verify(mCallAudioManager).setIsTonePlaying(any(Call.class), eq(true));
 
         mInCallTonePlayer.stopTone();
         // Timeouts due to threads!
-        verify(mCallAudioManager, timeout(TEST_TIMEOUT)).setIsTonePlaying(eq(false));
+        verify(mCallAudioManager, timeout(TEST_TIMEOUT)).setIsTonePlaying(any(Call.class),
+                eq(false));
         // Ideally it would be nice to verify this, however release is a native method so appears to
         // cause flakiness when testing on Cuttlefish.
         // verify(mToneGenerator, timeout(TEST_TIMEOUT)).release();
@@ -197,7 +205,8 @@
 
         // Verify we did play a tone.
         verify(mMediaPlayerFactory, timeout(TEST_TIMEOUT)).get(anyInt(), any());
-        verify(mCallAudioManager, timeout(TEST_TIMEOUT)).setIsTonePlaying(eq(true));
+        verify(mCallAudioManager, timeout(TEST_TIMEOUT)).setIsTonePlaying(any(Call.class),
+                eq(true));
     }
 
     @SmallTest
@@ -211,11 +220,11 @@
         when(mBluetoothRouteManager.isCachedLeAudioDevice(mDevice)).thenReturn(false);
         when(mBluetoothRouteManager.isCachedHearingAidDevice(mDevice)).thenReturn(false);
 
-        mInCallTonePlayer = mFactory.createPlayer(InCallTonePlayer.TONE_RING_BACK);
+        mInCallTonePlayer = mFactory.createPlayer(mCall, InCallTonePlayer.TONE_RING_BACK);
         assertTrue(mInCallTonePlayer.startTone());
         verify(mToneGeneratorFactory, timeout(TEST_TIMEOUT))
                 .get(eq(AudioManager.STREAM_BLUETOOTH_SCO), anyInt());
-        verify(mCallAudioManager).setIsTonePlaying(eq(true));
+        verify(mCallAudioManager).setIsTonePlaying(any(Call.class), eq(true));
     }
 
     @SmallTest
@@ -229,11 +238,11 @@
         when(mBluetoothRouteManager.isCachedLeAudioDevice(mDevice)).thenReturn(false);
         when(mBluetoothRouteManager.isCachedHearingAidDevice(mDevice)).thenReturn(false);
 
-        mInCallTonePlayer = mFactory.createPlayer(InCallTonePlayer.TONE_CALL_WAITING);
+        mInCallTonePlayer = mFactory.createPlayer(mCall, InCallTonePlayer.TONE_CALL_WAITING);
         assertTrue(mInCallTonePlayer.startTone());
         verify(mToneGeneratorFactory, timeout(TEST_TIMEOUT))
                 .get(eq(AudioManager.STREAM_BLUETOOTH_SCO), anyInt());
-        verify(mCallAudioManager).setIsTonePlaying(eq(true));
+        verify(mCallAudioManager).setIsTonePlaying(any(Call.class), eq(true));
     }
 
     @SmallTest
@@ -247,11 +256,11 @@
         when(mBluetoothRouteManager.isCachedLeAudioDevice(mDevice)).thenReturn(false);
         when(mBluetoothRouteManager.isCachedHearingAidDevice(mDevice)).thenReturn(true);
 
-        mInCallTonePlayer = mFactory.createPlayer(InCallTonePlayer.TONE_RING_BACK);
+        mInCallTonePlayer = mFactory.createPlayer(mCall, InCallTonePlayer.TONE_RING_BACK);
         assertTrue(mInCallTonePlayer.startTone());
         verify(mToneGeneratorFactory, timeout(TEST_TIMEOUT))
                 .get(eq(AudioManager.STREAM_VOICE_CALL), anyInt());
-        verify(mCallAudioManager).setIsTonePlaying(eq(true));
+        verify(mCallAudioManager).setIsTonePlaying(any(Call.class), eq(true));
     }
 
     @SmallTest
@@ -265,10 +274,10 @@
         when(mBluetoothRouteManager.isCachedLeAudioDevice(mDevice)).thenReturn(false);
         when(mBluetoothRouteManager.isCachedHearingAidDevice(mDevice)).thenReturn(true);
 
-        mInCallTonePlayer = mFactory.createPlayer(InCallTonePlayer.TONE_CALL_WAITING);
+        mInCallTonePlayer = mFactory.createPlayer(mCall, InCallTonePlayer.TONE_CALL_WAITING);
         assertTrue(mInCallTonePlayer.startTone());
         verify(mToneGeneratorFactory, timeout(TEST_TIMEOUT))
                 .get(eq(AudioManager.STREAM_VOICE_CALL), anyInt());
-        verify(mCallAudioManager).setIsTonePlaying(eq(true));
+        verify(mCallAudioManager).setIsTonePlaying(any(Call.class), eq(true));
     }
 }
diff --git a/tests/src/com/android/server/telecom/tests/InCallWakeLockControllerTest.java b/tests/src/com/android/server/telecom/tests/InCallWakeLockControllerTest.java
index f935908..cdf2542 100644
--- a/tests/src/com/android/server/telecom/tests/InCallWakeLockControllerTest.java
+++ b/tests/src/com/android/server/telecom/tests/InCallWakeLockControllerTest.java
@@ -17,13 +17,14 @@
 package com.android.server.telecom.tests;
 
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
-import static org.mockito.Mockito.never;
 
 import android.os.PowerManager;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.filters.SmallTest;
 
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallState;
diff --git a/tests/src/com/android/server/telecom/tests/IncomingCallFilterGraphTest.java b/tests/src/com/android/server/telecom/tests/IncomingCallFilterGraphTest.java
index 9269836..66ac553 100644
--- a/tests/src/com/android/server/telecom/tests/IncomingCallFilterGraphTest.java
+++ b/tests/src/com/android/server/telecom/tests/IncomingCallFilterGraphTest.java
@@ -16,11 +16,16 @@
 
 package com.android.server.telecom.tests;
 
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.when;
+
 import android.content.ContentResolver;
 import android.content.Context;
 import android.os.Handler;
 import android.os.HandlerThread;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.filters.SmallTest;
 
 import com.android.server.telecom.Call;
 import com.android.server.telecom.TelecomSystem;
@@ -30,15 +35,11 @@
 import com.android.server.telecom.callfiltering.CallFilteringResult;
 import com.android.server.telecom.callfiltering.IncomingCallFilterGraph;
 
-import static org.junit.Assert.assertEquals;
-import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Mockito.when;
-
 import org.junit.Before;
+import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 import org.mockito.Mock;
-import org.junit.Test;
 
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionStage;
diff --git a/tests/src/com/android/server/telecom/tests/IncomingCallNotifierTest.java b/tests/src/com/android/server/telecom/tests/IncomingCallNotifierTest.java
index 914fdc5..2d81bb3 100644
--- a/tests/src/com/android/server/telecom/tests/IncomingCallNotifierTest.java
+++ b/tests/src/com/android/server/telecom/tests/IncomingCallNotifierTest.java
@@ -16,15 +16,21 @@
 
 package com.android.server.telecom.tests;
 
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
 import android.app.NotificationManager;
-import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.os.Build;
 import android.os.UserHandle;
-import android.telecom.PhoneAccountHandle;
 import android.telecom.VideoProfile;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.filters.SmallTest;
 
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallState;
@@ -38,14 +44,6 @@
 import org.junit.runners.JUnit4;
 import org.mockito.Mock;
 
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Matchers.isNull;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
 /**
  * Tests for the {@link com.android.server.telecom.ui.IncomingCallNotifier} class.
  */
diff --git a/tests/src/com/android/server/telecom/tests/LogUtilsTest.java b/tests/src/com/android/server/telecom/tests/LogUtilsTest.java
index 637dfbc..4393d90 100644
--- a/tests/src/com/android/server/telecom/tests/LogUtilsTest.java
+++ b/tests/src/com/android/server/telecom/tests/LogUtilsTest.java
@@ -18,7 +18,7 @@
 
 import static org.junit.Assert.assertTrue;
 
-import android.test.suitebuilder.annotation.SmallTest;
+import androidx.test.filters.SmallTest;
 
 import com.android.server.telecom.LogUtils;
 
diff --git a/tests/src/com/android/server/telecom/tests/MissedCallNotifierImplTest.java b/tests/src/com/android/server/telecom/tests/MissedCallNotifierImplTest.java
index 2b05430..1776411 100644
--- a/tests/src/com/android/server/telecom/tests/MissedCallNotifierImplTest.java
+++ b/tests/src/com/android/server/telecom/tests/MissedCallNotifierImplTest.java
@@ -16,9 +16,30 @@
 
 package com.android.server.telecom.tests;
 
+import static com.android.server.telecom.ui.MissedCallNotifierImpl.CALL_LOG_COLUMN_ID;
+
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertTrue;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
 import android.app.BroadcastOptions;
 import android.app.Notification;
 import android.app.NotificationManager;
@@ -29,8 +50,6 @@
 import android.content.IContentProvider;
 import android.content.Intent;
 import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.Bundle;
@@ -39,13 +58,14 @@
 import android.os.Looper;
 import android.os.UserHandle;
 import android.provider.CallLog;
+import android.telecom.CallerInfo;
 import android.telecom.PhoneAccount;
 import android.telecom.PhoneAccountHandle;
 import android.telecom.TelecomManager;
 import android.telephony.TelephonyManager;
-import android.test.suitebuilder.annotation.SmallTest;
 
-import android.telecom.CallerInfo;
+import androidx.test.filters.SmallTest;
+
 import com.android.server.telecom.CallerInfoLookupHelper;
 import com.android.server.telecom.Constants;
 import com.android.server.telecom.DefaultDialerCache;
@@ -67,32 +87,11 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.List;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyString;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Matchers.isNull;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.timeout;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
 @RunWith(JUnit4.class)
 public class MissedCallNotifierImplTest extends TelecomTestCase {
 
@@ -241,7 +240,7 @@
         MissedCallNotifier.CallInfo fakeCall = makeFakeCallInfo(TEL_CALL_HANDLE, CALLER_NAME,
                 CALL_TIMESTAMP, phoneAccount.getAccountHandle());
 
-        missedCallNotifier.showMissedCallNotification(fakeCall);
+        missedCallNotifier.showMissedCallNotification(fakeCall, /* uri= */ null);
         ArgumentCaptor<Intent> intentArgumentCaptor = ArgumentCaptor.forClass(Intent.class);
         verify(mContext).sendBroadcastAsUser(intentArgumentCaptor.capture(), any(),
                 anyString(), any());
@@ -250,6 +249,31 @@
         assertEquals(1, sentIntent.getIntExtra(TelecomManager.EXTRA_NOTIFICATION_COUNT, -1));
     }
 
+    @SmallTest
+    @Test
+    public void testCallLogUriSentToNotifier(){
+        MissedCallNotifier missedCallNotifier = setupMissedCallNotificationThroughDefaultDialer();
+        PhoneAccount phoneAccount = makePhoneAccount(PRIMARY_USER, NO_CAPABILITY);
+        Cursor mockMissedCallsCursor = new MockMissedCallCursorBuilder()
+                .addEntry(TEL_CALL_HANDLE.getSchemeSpecificPart(),
+                        CallLog.Calls.PRESENTATION_ALLOWED, CALL_TIMESTAMP)
+                .build();
+        MissedCallNotifier.CallInfo fakeCall = makeFakeCallInfo(TEL_CALL_HANDLE, CALLER_NAME,
+                CALL_TIMESTAMP, phoneAccount.getAccountHandle());
+        when(mFeatureFlags.addCallUriForMissedCalls()).thenReturn(true);
+
+        missedCallNotifier.showMissedCallNotification(fakeCall,
+                CallLog.Calls.CONTENT_URI.buildUpon().appendPath(Long.toString(
+                        mockMissedCallsCursor.getInt(CALL_LOG_COLUMN_ID))).build());
+        ArgumentCaptor<Intent> intentArgumentCaptor = ArgumentCaptor.forClass(Intent.class);
+        verify(mContext).sendBroadcastAsUser(intentArgumentCaptor.capture(), any(),
+                anyString(), any());
+
+        Intent sentIntent = intentArgumentCaptor.getValue();
+        Uri actualCallUri = sentIntent.getParcelableExtra(TelecomManager.EXTRA_CALL_LOG_URI);
+        assertTrue(actualCallUri.isPathPrefixMatch(CallLog.Calls.CONTENT_URI));
+    }
+
     private MissedCallNotifier setupMissedCallNotificationThroughDefaultDialer() {
         mComponentContextFixture.addIntentReceiver(
                 TelecomManager.ACTION_SHOW_MISSED_CALLS_NOTIFICATION, COMPONENT_NAME);
@@ -275,9 +299,9 @@
         MissedCallNotifier.CallInfo fakeCall = makeFakeCallInfo(TEL_CALL_HANDLE, CALLER_NAME,
                 CALL_TIMESTAMP, phoneAccount.getAccountHandle());
 
-        missedCallNotifier.showMissedCallNotification(fakeCall);
+        missedCallNotifier.showMissedCallNotification(fakeCall, /* uri= */null);
         missedCallNotifier.clearMissedCalls(userHandle);
-        missedCallNotifier.showMissedCallNotification(fakeCall);
+        missedCallNotifier.showMissedCallNotification(fakeCall, /* uri= */null);
 
         ArgumentCaptor<Integer> requestIdCaptor = ArgumentCaptor.forClass(
                 Integer.class);
@@ -308,10 +332,10 @@
 
         MissedCallNotifier missedCallNotifier = new MissedCallNotifierImpl(mContext,
                 mPhoneAccountRegistrar, mDefaultDialerCache, fakeBuilderFactory,
-                mDeviceIdleControllerAdapter);
+                mDeviceIdleControllerAdapter, mFeatureFlags);
 
-        missedCallNotifier.showMissedCallNotification(fakeCall);
-        missedCallNotifier.showMissedCallNotification(fakeCall);
+        missedCallNotifier.showMissedCallNotification(fakeCall, /* uri= */ null);
+        missedCallNotifier.showMissedCallNotification(fakeCall, /* uri= */ null);
 
         // The following captor is to capture the two notifications that got passed into
         // notifyAsUser. This distinguishes between the builders used for the full notification
@@ -402,7 +426,7 @@
 
         MissedCallNotifier.CallInfo fakeCall = makeFakeCallInfo(TEL_CALL_HANDLE, CALLER_NAME,
                 CALL_TIMESTAMP, phoneAccount.getAccountHandle());
-        missedCallNotifier.showMissedCallNotification(fakeCall);
+        missedCallNotifier.showMissedCallNotification(fakeCall, /* uri= */ null);
 
         ArgumentCaptor<Notification> notificationArgumentCaptor = ArgumentCaptor.forClass(
                 Notification.class);
@@ -464,13 +488,13 @@
 
         MissedCallNotifier missedCallNotifier = new MissedCallNotifierImpl(mContext,
                 mPhoneAccountRegistrar, mDefaultDialerCache, fakeBuilderFactory,
-                mDeviceIdleControllerAdapter);
+                mDeviceIdleControllerAdapter, mFeatureFlags);
         PhoneAccount phoneAccount = makePhoneAccount(PRIMARY_USER, NO_CAPABILITY);
 
         MissedCallNotifier.CallInfo fakeCall =
                 makeFakeCallInfo(SIP_CALL_HANDLE, CALLER_NAME, CALL_TIMESTAMP,
                 phoneAccount.getAccountHandle());
-        missedCallNotifier.showMissedCallNotification(fakeCall);
+        missedCallNotifier.showMissedCallNotification(fakeCall, /* uri= */ null);
 
         // Create two intents that correspond to call-back and respond back with SMS, and assert
         // that in the case of a SIP call, no SMS intent is generated.
@@ -525,7 +549,7 @@
 
         MissedCallNotifier missedCallNotifier = new MissedCallNotifierImpl(mContext,
                 mPhoneAccountRegistrar, mDefaultDialerCache, fakeBuilderFactory,
-                mDeviceIdleControllerAdapter);
+                mDeviceIdleControllerAdapter, mFeatureFlags);
 
         // AsyncQueryHandler used in reloadFromDatabase interacts poorly with the below
         // timeout-verify, so run this in a new handler to mitigate that.
@@ -595,7 +619,7 @@
 
         MissedCallNotifier missedCallNotifier = new MissedCallNotifierImpl(mContext,
                 mPhoneAccountRegistrar, mDefaultDialerCache, fakeBuilderFactory,
-                mDeviceIdleControllerAdapter);
+                mDeviceIdleControllerAdapter, mFeatureFlags);
 
         // AsyncQueryHandler used in reloadFromDatabase interacts poorly with the below
         // timeout-verify, so run this in a new handler to mitigate that.
@@ -637,13 +661,13 @@
 
         MissedCallNotifier missedCallNotifier = new MissedCallNotifierImpl(mContext,
                 mPhoneAccountRegistrar, mDefaultDialerCache, fakeBuilderFactory,
-                mDeviceIdleControllerAdapter);
+                mDeviceIdleControllerAdapter, mFeatureFlags);
         PhoneAccount phoneAccount = makePhoneAccount(PRIMARY_USER, NO_CAPABILITY);
 
         MissedCallNotifier.CallInfo fakeCall =
                 makeFakeCallInfo(SIP_CALL_HANDLE, CALLER_NAME, CALL_TIMESTAMP,
                         phoneAccount.getAccountHandle());
-        missedCallNotifier.showMissedCallNotification(fakeCall);
+        missedCallNotifier.showMissedCallNotification(fakeCall, /* uri= */ null);
 
         ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
         ArgumentCaptor<Bundle> bundleCaptor =
@@ -668,6 +692,25 @@
                 nullable(Notification.class), eq(PRIMARY_USER));
     }
 
+    /**
+     * Ensure when Telephony is not present on a device and getNetworkCountryIso throws an
+     * unsupported operation exception that we will still fallback to the device locale.
+     */
+    @SmallTest
+    @Test
+    public void testGetCountryIsoWithNoTelephony() {
+        Notification.Builder builder1 = makeNotificationBuilder("builder1");
+        MissedCallNotifierImpl.NotificationBuilderFactory fakeBuilderFactory =
+                makeNotificationBuilderFactory(builder1);
+        MissedCallNotifierImpl missedCallNotifier = new MissedCallNotifierImpl(mContext,
+                mPhoneAccountRegistrar, mDefaultDialerCache, fakeBuilderFactory,
+                mDeviceIdleControllerAdapter, mFeatureFlags);
+
+        when(mComponentContextFixture.getTelephonyManager().getNetworkCountryIso())
+                .thenThrow(new UnsupportedOperationException("Bee boop"));
+        assertNotNull(missedCallNotifier.getCurrentCountryIso(mContext));
+    }
+
     private Notification.Builder makeNotificationBuilder(String label) {
         Notification.Builder builder = spy(new Notification.Builder(mContext));
         Notification notification = mock(Notification.class);
@@ -701,7 +744,7 @@
             NotificationBuilderFactory fakeBuilderFactory, UserHandle currentUser) {
         MissedCallNotifier missedCallNotifier = new MissedCallNotifierImpl(mContext,
                 mPhoneAccountRegistrar, mDefaultDialerCache, fakeBuilderFactory,
-                mDeviceIdleControllerAdapter);
+                mDeviceIdleControllerAdapter, mFeatureFlags);
         missedCallNotifier.setCurrentUserHandle(currentUser);
         return missedCallNotifier;
     }
diff --git a/tests/src/com/android/server/telecom/tests/MissedCallNotifierTest.java b/tests/src/com/android/server/telecom/tests/MissedCallNotifierTest.java
index e441835..c0e3435 100644
--- a/tests/src/com/android/server/telecom/tests/MissedCallNotifierTest.java
+++ b/tests/src/com/android/server/telecom/tests/MissedCallNotifierTest.java
@@ -16,14 +16,17 @@
 
 package com.android.server.telecom.tests;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
 import android.content.ComponentName;
 import android.net.Uri;
-import android.telecom.PhoneAccountHandle;
-import android.test.suitebuilder.annotation.SmallTest;
 import android.telecom.CallerInfo;
+import android.telecom.PhoneAccountHandle;
+
+import androidx.test.filters.SmallTest;
 
 import com.android.server.telecom.MissedCallNotifier;
-import com.android.server.telecom.MissedCallNotifier.CallInfo;
 
 import org.junit.After;
 import org.junit.Before;
@@ -31,9 +34,6 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
-
 @RunWith(JUnit4.class)
 public class MissedCallNotifierTest extends TelecomTestCase {
     private static final ComponentName COMPONENT_NAME =
diff --git a/tests/src/com/android/server/telecom/tests/MissedInformationTest.java b/tests/src/com/android/server/telecom/tests/MissedInformationTest.java
index 4af3de3..0c3588e 100644
--- a/tests/src/com/android/server/telecom/tests/MissedInformationTest.java
+++ b/tests/src/com/android/server/telecom/tests/MissedInformationTest.java
@@ -32,9 +32,9 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Matchers.anyString;
-import static org.mockito.Matchers.eq;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.spy;
@@ -109,6 +109,7 @@
         mAdapter = new CallIntentProcessor.AdapterImpl(mCallsManager.getDefaultDialerCache());
         mNotificationManager = spy((NotificationManager) mContext.getSystemService(
                 Context.NOTIFICATION_SERVICE));
+        when(mFeatureFlags.telecomResolveHiddenDependencies()).thenReturn(true);
         when(mContentResolver.getPackageName()).thenReturn(PACKAGE_NAME);
         when(mContentResolver.acquireProvider(any(String.class))).thenReturn(mContentProvider);
         when(mContentProvider.call(any(String.class), any(String.class),
@@ -152,6 +153,8 @@
         setUpEmergencyCall();
         when(mEmergencyCall.getAssociatedUser()).
                 thenReturn(mPhoneAccountA0.getAccountHandle().getUserHandle());
+        when(mEmergencyCall.getTargetPhoneAccount())
+                .thenReturn(mPhoneAccountA0.getAccountHandle());
         mCallsManager.addCall(mEmergencyCall);
         assertTrue(mCallsManager.isInEmergencyCall());
 
@@ -417,7 +420,7 @@
                 null, mCallsManager.getPhoneNumberUtilsAdapter(), null,
                 null, null, mPhoneAccountA0.getAccountHandle(),
                 Call.CALL_DIRECTION_INCOMING, false, false,
-                mClockProxy, null));
+                mClockProxy, null, mFeatureFlags));
         doReturn(1L).when(mIncomingCall).getStartRingTime();
         doAnswer((x) -> {
             mCountDownLatch.countDown();
diff --git a/tests/src/com/android/server/telecom/tests/MmiUtilsTest.java b/tests/src/com/android/server/telecom/tests/MmiUtilsTest.java
index ed74637..3f4b5f6 100644
--- a/tests/src/com/android/server/telecom/tests/MmiUtilsTest.java
+++ b/tests/src/com/android/server/telecom/tests/MmiUtilsTest.java
@@ -20,7 +20,8 @@
 import static org.junit.Assert.assertTrue;
 
 import android.net.Uri;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.filters.SmallTest;
 
 import com.android.server.telecom.MmiUtils;
 
diff --git a/tests/src/com/android/server/telecom/tests/NewOutgoingCallIntentBroadcasterTest.java b/tests/src/com/android/server/telecom/tests/NewOutgoingCallIntentBroadcasterTest.java
index 33acd98..e75ad97 100644
--- a/tests/src/com/android/server/telecom/tests/NewOutgoingCallIntentBroadcasterTest.java
+++ b/tests/src/com/android/server/telecom/tests/NewOutgoingCallIntentBroadcasterTest.java
@@ -20,17 +20,16 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyBoolean;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Matchers.anyString;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Matchers.isNotNull;
-import static org.mockito.Matchers.isNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNotNull;
+import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -51,7 +50,8 @@
 import android.telecom.VideoProfile;
 import android.telephony.DisconnectCause;
 import android.telephony.TelephonyManager;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.filters.SmallTest;
 
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallsManager;
@@ -64,6 +64,7 @@
 import com.android.server.telecom.RoleManagerAdapter;
 import com.android.server.telecom.SystemStateHelper;
 import com.android.server.telecom.TelecomSystem;
+import com.android.server.telecom.flags.FeatureFlags;
 
 import org.junit.After;
 import org.junit.Before;
@@ -75,6 +76,8 @@
 
 @RunWith(JUnit4.class)
 public class NewOutgoingCallIntentBroadcasterTest extends TelecomTestCase {
+    private static final Uri TEST_URI = Uri.parse("tel:16505551212");
+
     private static class ReceiverIntentPair {
         public BroadcastReceiver receiver;
         public Intent intent;
@@ -93,6 +96,7 @@
     @Mock private PhoneAccountRegistrar mPhoneAccountRegistrar;
     @Mock private RoleManagerAdapter mRoleManagerAdapter;
     @Mock private DefaultDialerCache mDefaultDialerCache;
+    @Mock private FeatureFlags mFeatureFlags;
 
     @Mock private MmiUtils mMmiUtils;
     private PhoneNumberUtilsAdapter mPhoneNumberUtilsAdapter = new PhoneNumberUtilsAdapterImpl();
@@ -113,6 +117,7 @@
             any(PhoneAccountHandle.class))).thenReturn(mPhoneAccount);
         when(mPhoneAccount.isSelfManaged()).thenReturn(true);
         when(mSystemStateHelper.isCarModeOrProjectionActive()).thenReturn(false);
+        when(mFeatureFlags.isNewOutgoingCallBroadcastUnblocking()).thenReturn(false);
     }
 
     @Override
@@ -510,6 +515,84 @@
         testUnmodifiedRegularCall();
     }
 
+    /**
+     * Where the flag `isNewOutgoingCallBroadcastUnblocking` is off, verify that we sent an ordered
+     * broadcast and did not try to start the call immediately (legacy behavior).
+     */
+    @SmallTest
+    @Test
+    public void testSendBroadcastBlocking() {
+        when(mFeatureFlags.isNewOutgoingCallBroadcastUnblocking()).thenReturn(false);
+        Intent intent = new Intent(Intent.ACTION_CALL, TEST_URI);
+        NewOutgoingCallIntentBroadcaster nocib = new NewOutgoingCallIntentBroadcaster(
+                mContext, mCallsManager, intent, mPhoneNumberUtilsAdapter,
+                true /* isDefaultPhoneApp */, mDefaultDialerCache, mMmiUtils, mFeatureFlags);
+
+        NewOutgoingCallIntentBroadcaster.CallDisposition disposition = nocib.evaluateCall();
+        nocib.processCall(mCall, disposition);
+
+        // We should not have not short-circuited to place the outgoing call directly.
+        verify(mCall, never()).setNewOutgoingCallIntentBroadcastIsDone();
+        verify(mCallsManager, never()).placeOutgoingCall(any(Call.class), any(Uri.class),
+                any(GatewayInfo.class), anyBoolean(), anyInt());
+
+        // Ensure we did send the broadcast ordered
+        verifyBroadcastSent(TEST_URI.getSchemeSpecificPart(),
+                createNumberExtras(TEST_URI.getSchemeSpecificPart()));
+
+        // Ensure we did not try to directly send the broadcast unordered.
+        verify(mContext, never()).sendBroadcastAsUser(
+                any(Intent.class),
+                eq(UserHandle.CURRENT),
+                eq(android.Manifest.permission.PROCESS_OUTGOING_CALLS));
+    }
+
+    /**
+     * Where the flag `isNewOutgoingCallBroadcastUnblocking` is off, verify that we sent an ordered
+     * broadcast and did not try to start the call immediately.  Also ensure that the broadcast
+     * flags are correct.
+     */
+    @SmallTest
+    @Test
+    public void testSendBroadcastNonBlocking() {
+        when(mFeatureFlags.isNewOutgoingCallBroadcastUnblocking()).thenReturn(true);
+        Intent intent = new Intent(Intent.ACTION_CALL, TEST_URI);
+        NewOutgoingCallIntentBroadcaster nocib = new NewOutgoingCallIntentBroadcaster(
+                mContext, mCallsManager, intent, mPhoneNumberUtilsAdapter,
+                true /* isDefaultPhoneApp */, mDefaultDialerCache, mMmiUtils, mFeatureFlags);
+
+        NewOutgoingCallIntentBroadcaster.CallDisposition disposition = nocib.evaluateCall();
+        nocib.processCall(mCall, disposition);
+
+        // We should have started the outgoing call flow immediately.
+        verify(mCall).setNewOutgoingCallIntentBroadcastIsDone();
+        verify(mCallsManager).placeOutgoingCall(any(Call.class), any(Uri.class),
+                nullable(GatewayInfo.class), anyBoolean(), anyInt());
+
+        // Ensure we didn't send an ordered broadcast.
+        verify(mContext, never()).sendOrderedBroadcastAsUser(
+                any(Intent.class),
+                any(UserHandle.class),
+                anyString(),
+                anyInt(),
+                any(Bundle.class),
+                any(BroadcastReceiver.class),
+                any(Handler.class),
+                eq(Activity.RESULT_OK),
+                anyString(),
+                any(Bundle.class));
+
+        // But that we did send a regular broadcast.
+        ArgumentCaptor<Intent> intentArgumentCaptor = ArgumentCaptor.forClass(Intent.class);
+        verify(mContext).sendBroadcastAsUser(
+                intentArgumentCaptor.capture(),
+                eq(UserHandle.CURRENT),
+                eq(android.Manifest.permission.PROCESS_OUTGOING_CALLS),
+                eq(AppOpsManager.OP_PROCESS_OUTGOING_CALLS));
+        Intent capturedIntent = intentArgumentCaptor.getValue();
+        assertEquals(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND, capturedIntent.getFlags());
+    }
+
     private ReceiverIntentPair regularCallTestHelper(Intent intent,
             Bundle expectedAdditionalExtras) {
         Uri handle = intent.getData();
@@ -542,7 +625,7 @@
             boolean isDefaultPhoneApp) {
         NewOutgoingCallIntentBroadcaster b = new NewOutgoingCallIntentBroadcaster(
                 mContext, mCallsManager, intent, mPhoneNumberUtilsAdapter,
-                isDefaultPhoneApp, mDefaultDialerCache, mMmiUtils);
+                isDefaultPhoneApp, mDefaultDialerCache, mMmiUtils, mFeatureFlags);
         NewOutgoingCallIntentBroadcaster.CallDisposition cd = b.evaluateCall();
         if (cd.disconnectCause == DisconnectCause.NOT_DISCONNECTED) {
             b.processCall(mCall, cd);
diff --git a/tests/src/com/android/server/telecom/tests/ParcelableCallUtilsTest.java b/tests/src/com/android/server/telecom/tests/ParcelableCallUtilsTest.java
index fed8084..8eefd96 100644
--- a/tests/src/com/android/server/telecom/tests/ParcelableCallUtilsTest.java
+++ b/tests/src/com/android/server/telecom/tests/ParcelableCallUtilsTest.java
@@ -13,11 +13,13 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.SystemClock;
+import android.os.UserHandle;
 import android.telecom.Connection;
 import android.telecom.ParcelableCall;
 import android.telecom.PhoneAccountHandle;
 import android.telephony.ims.ImsCallProfile;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.filters.SmallTest;
 
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallerInfoLookupHelper;
@@ -57,6 +59,7 @@
         when(mClockProxy.elapsedRealtime()).thenReturn(SystemClock.elapsedRealtime());
         when(mCallsManager.getCallerInfoLookupHelper()).thenReturn(mCallerInfoLookupHelper);
         when(mCallsManager.getPhoneAccountRegistrar()).thenReturn(mPhoneAccountRegistrar);
+        when(mCallsManager.getCurrentUserHandle()).thenReturn(UserHandle.CURRENT);
         when(mPhoneAccountRegistrar.getPhoneAccountUnchecked(any())).thenReturn(null);
         when(mComponentContextFixture.getTelephonyManager().isEmergencyNumber(any()))
                 .thenReturn(false);
@@ -75,7 +78,8 @@
                 false /* shouldAttachToExistingConnection */,
                 false /* isConference */,
                 mClockProxy /* ClockProxy */,
-                mToastProxy);
+                mToastProxy,
+                mFeatureFlags);
     }
 
     @Override
diff --git a/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java b/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java
index e573bb8..45b4ed1 100644
--- a/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java
+++ b/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java
@@ -23,14 +23,15 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Matchers.anyBoolean;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Matchers.anyString;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -57,13 +58,14 @@
 import android.telephony.CarrierConfigManager;
 import android.telephony.SubscriptionInfo;
 import android.telephony.SubscriptionManager;
-import android.test.suitebuilder.annotation.SmallTest;
-import android.test.suitebuilder.annotation.MediumTest;
 import android.util.Xml;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.filters.SmallTest;
 
 import com.android.internal.telecom.IConnectionService;
+import com.android.internal.telephony.flags.FeatureFlags;
 import com.android.internal.util.FastXmlSerializer;
 import com.android.server.telecom.AppLabelProxy;
 import com.android.server.telecom.DefaultDialerCache;
@@ -92,6 +94,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -117,6 +120,7 @@
     @Mock private TelecomManager mTelecomManager;
     @Mock private DefaultDialerCache mDefaultDialerCache;
     @Mock private AppLabelProxy mAppLabelProxy;
+    @Mock private FeatureFlags mTelephonyFeatureFlags;
 
     @Override
     @Before
@@ -134,8 +138,11 @@
         when(mAppLabelProxy.getAppLabel(anyString()))
                 .thenReturn(TEST_LABEL);
         mRegistrar = new PhoneAccountRegistrar(
-                mComponentContextFixture.getTestDouble().getApplicationContext(),
-                mLock, FILE_NAME, mDefaultDialerCache, mAppLabelProxy);
+                mComponentContextFixture.getTestDouble().getApplicationContext(), mLock, FILE_NAME,
+                mDefaultDialerCache, mAppLabelProxy, mTelephonyFeatureFlags, mFeatureFlags);
+        when(mFeatureFlags.onlyUpdateTelephonyOnValidSubIds()).thenReturn(false);
+        when(mFeatureFlags.unregisterUnresolvableAccounts()).thenReturn(true);
+        when(mTelephonyFeatureFlags.workProfileApiSplit()).thenReturn(false);
     }
 
     @Override
@@ -154,12 +161,14 @@
     public void testPhoneAccountHandle() throws Exception {
         PhoneAccountHandle input = new PhoneAccountHandle(new ComponentName("pkg0", "cls0"), "id0");
         PhoneAccountHandle result = roundTripXml(this, input,
-                PhoneAccountRegistrar.sPhoneAccountHandleXml, mContext);
+                PhoneAccountRegistrar.sPhoneAccountHandleXml, mContext,
+                mTelephonyFeatureFlags, mFeatureFlags);
         assertPhoneAccountHandleEquals(input, result);
 
         PhoneAccountHandle inputN = new PhoneAccountHandle(new ComponentName("pkg0", "cls0"), null);
         PhoneAccountHandle resultN = roundTripXml(this, inputN,
-                PhoneAccountRegistrar.sPhoneAccountHandleXml, mContext);
+                PhoneAccountRegistrar.sPhoneAccountHandleXml, mContext,
+                mTelephonyFeatureFlags, mFeatureFlags);
         Log.i(this, "inputN = %s, resultN = %s", inputN, resultN);
         assertPhoneAccountHandleEquals(inputN, resultN);
     }
@@ -182,7 +191,112 @@
                 .setIsEnabled(true)
                 .build();
         PhoneAccount result = roundTripXml(this, input, PhoneAccountRegistrar.sPhoneAccountXml,
-                mContext);
+                mContext, mTelephonyFeatureFlags, mFeatureFlags);
+
+        assertPhoneAccountEquals(input, result);
+    }
+
+    @MediumTest
+    @Test
+    public void testPhoneAccountParsing_simultaneousCallingRestriction() throws Exception {
+        doReturn(true).when(mTelephonyFeatureFlags).simultaneousCallingIndications();
+        // workaround: UserManager converts the user to a serial and back, we need to mock this
+        // behavior, unfortunately: USER_HANDLE_10 <-> 10L
+        UserManager userManager = mContext.getSystemService(UserManager.class);
+        doReturn(10L).when(userManager).getSerialNumberForUser(eq(USER_HANDLE_10));
+        doReturn(USER_HANDLE_10).when(userManager).getUserForSerialNumber(eq(10L));
+        Bundle testBundle = new Bundle();
+        testBundle.putInt("EXTRA_INT_1", 1);
+        testBundle.putInt("EXTRA_INT_100", 100);
+        testBundle.putBoolean("EXTRA_BOOL_TRUE", true);
+        testBundle.putBoolean("EXTRA_BOOL_FALSE", false);
+        testBundle.putString("EXTRA_STR1", "Hello");
+        testBundle.putString("EXTRA_STR2", "There");
+
+        Set<PhoneAccountHandle> restriction = new HashSet<>(10);
+        for (int i = 0; i < 10; i++) {
+            restriction.add(makeQuickAccountHandleForUser("id" + i, USER_HANDLE_10));
+        }
+
+        PhoneAccount input = makeQuickAccountBuilder("id0", 0, USER_HANDLE_10)
+                .addSupportedUriScheme(PhoneAccount.SCHEME_TEL)
+                .addSupportedUriScheme(PhoneAccount.SCHEME_VOICEMAIL)
+                .setExtras(testBundle)
+                .setIsEnabled(true)
+                .setSimultaneousCallingRestriction(restriction)
+                .build();
+        PhoneAccount result = roundTripXml(this, input, PhoneAccountRegistrar.sPhoneAccountXml,
+                mContext, mTelephonyFeatureFlags, mFeatureFlags);
+
+        assertPhoneAccountEquals(input, result);
+    }
+
+    @MediumTest
+    @Test
+    public void testPhoneAccountParsing_simultaneousCallingRestrictionOnOffFlag() throws Exception {
+        // Start the test with the flag on
+        doReturn(true).when(mTelephonyFeatureFlags).simultaneousCallingIndications();
+        // workaround: UserManager converts the user to a serial and back, we need to mock this
+        // behavior, unfortunately: USER_HANDLE_10 <-> 10L
+        UserManager userManager = mContext.getSystemService(UserManager.class);
+        doReturn(10L).when(userManager).getSerialNumberForUser(eq(USER_HANDLE_10));
+        doReturn(USER_HANDLE_10).when(userManager).getUserForSerialNumber(eq(10L));
+        Bundle testBundle = new Bundle();
+        testBundle.putInt("EXTRA_INT_1", 1);
+        testBundle.putInt("EXTRA_INT_100", 100);
+        testBundle.putBoolean("EXTRA_BOOL_TRUE", true);
+        testBundle.putBoolean("EXTRA_BOOL_FALSE", false);
+        testBundle.putString("EXTRA_STR1", "Hello");
+        testBundle.putString("EXTRA_STR2", "There");
+
+        Set<PhoneAccountHandle> restriction = new HashSet<>(10);
+        for (int i = 0; i < 10; i++) {
+            restriction.add(makeQuickAccountHandleForUser("id" + i, USER_HANDLE_10));
+        }
+
+        PhoneAccount input = makeQuickAccountBuilder("id0", 0, USER_HANDLE_10)
+                .addSupportedUriScheme(PhoneAccount.SCHEME_TEL)
+                .addSupportedUriScheme(PhoneAccount.SCHEME_VOICEMAIL)
+                .setExtras(testBundle)
+                .setIsEnabled(true)
+                .setSimultaneousCallingRestriction(restriction)
+                .build();
+        byte[] xmlData = toXml(input, PhoneAccountRegistrar.sPhoneAccountXml, mContext,
+                mTelephonyFeatureFlags);
+        // Simulate turning off the flag after reboot
+        doReturn(false).when(mTelephonyFeatureFlags).simultaneousCallingIndications();
+        PhoneAccount result = fromXml(xmlData, PhoneAccountRegistrar.sPhoneAccountXml, mContext,
+                mTelephonyFeatureFlags, mFeatureFlags);
+
+        assertNotNull(result);
+        assertFalse(result.hasSimultaneousCallingRestriction());
+    }
+
+    @MediumTest
+    @Test
+    public void testPhoneAccountParsing_simultaneousCallingRestrictionOffOnFlag() throws Exception {
+        // Start the test with the flag on
+        doReturn(false).when(mTelephonyFeatureFlags).simultaneousCallingIndications();
+        Bundle testBundle = new Bundle();
+        testBundle.putInt("EXTRA_INT_1", 1);
+        testBundle.putInt("EXTRA_INT_100", 100);
+        testBundle.putBoolean("EXTRA_BOOL_TRUE", true);
+        testBundle.putBoolean("EXTRA_BOOL_FALSE", false);
+        testBundle.putString("EXTRA_STR1", "Hello");
+        testBundle.putString("EXTRA_STR2", "There");
+
+        PhoneAccount input = makeQuickAccountBuilder("id0", 0, USER_HANDLE_10)
+                .addSupportedUriScheme(PhoneAccount.SCHEME_TEL)
+                .addSupportedUriScheme(PhoneAccount.SCHEME_VOICEMAIL)
+                .setExtras(testBundle)
+                .setIsEnabled(true)
+                .build();
+        byte[] xmlData = toXml(input, PhoneAccountRegistrar.sPhoneAccountXml, mContext,
+                mTelephonyFeatureFlags);
+        // Simulate turning on the flag after reboot
+        doReturn(true).when(mTelephonyFeatureFlags).simultaneousCallingIndications();
+        PhoneAccount result = fromXml(xmlData, PhoneAccountRegistrar.sPhoneAccountXml, mContext,
+                mTelephonyFeatureFlags, mFeatureFlags);
 
         assertPhoneAccountEquals(input, result);
     }
@@ -254,12 +368,14 @@
     public void testDefaultPhoneAccountHandleEmptyGroup() throws Exception {
         DefaultPhoneAccountHandle input = new DefaultPhoneAccountHandle(Process.myUserHandle(),
                 makeQuickAccountHandle("i1"), "");
-        when(UserManager.get(mContext).getSerialNumberForUser(input.userHandle))
+        UserManager userManager = mContext.getSystemService(UserManager.class);
+        when(userManager.getSerialNumberForUser(input.userHandle))
                 .thenReturn(0L);
-        when(UserManager.get(mContext).getUserForSerialNumber(0L))
+        when(userManager.getUserForSerialNumber(0L))
                 .thenReturn(input.userHandle);
         DefaultPhoneAccountHandle result = roundTripXml(this, input,
-                PhoneAccountRegistrar.sDefaultPhoneAcountHandleXml, mContext);
+                PhoneAccountRegistrar.sDefaultPhoneAccountHandleXml, mContext,
+                mTelephonyFeatureFlags, mFeatureFlags);
 
         assertDefaultPhoneAccountHandleEquals(input, result);
     }
@@ -289,7 +405,7 @@
                 .setExtras(testBundle)
                 .build();
         PhoneAccount result = roundTripXml(this, input, PhoneAccountRegistrar.sPhoneAccountXml,
-                mContext);
+                mContext, mTelephonyFeatureFlags, mFeatureFlags);
 
         Bundle extras = result.getExtras();
         assertFalse(extras.keySet().contains("EXTRA_STR2"));
@@ -303,8 +419,7 @@
     public void testState() throws Exception {
         PhoneAccountRegistrar.State input = makeQuickState();
         PhoneAccountRegistrar.State result = roundTripXml(this, input,
-                PhoneAccountRegistrar.sStateXml,
-                mContext);
+                PhoneAccountRegistrar.sStateXml, mContext, mTelephonyFeatureFlags, mFeatureFlags);
         assertStateEquals(input, result);
     }
 
@@ -353,6 +468,60 @@
                 PhoneAccount.SCHEME_TEL));
     }
 
+    /**
+     * Verify when a {@link android.telecom.ConnectionService} is disabled or cannot be resolved,
+     * all phone accounts are unregistered when calling
+     * {@link  PhoneAccountRegistrar#cleanupAndGetVerifiedAccounts(PhoneAccount)}.
+     */
+    @Test
+    public void testCannotResolveServiceUnregistersAccounts() throws Exception {
+        ComponentName componentName = makeQuickConnectionServiceComponentName();
+        PhoneAccount account = makeQuickAccountBuilder("0", 0, USER_HANDLE_10)
+                .setCapabilities(PhoneAccount.CAPABILITY_CONNECTION_MANAGER
+                        | PhoneAccount.CAPABILITY_CALL_PROVIDER).build();
+        // add the ConnectionService and register a single phone account for it
+        mComponentContextFixture.addConnectionService(componentName,
+                Mockito.mock(IConnectionService.class));
+        registerAndEnableAccount(account);
+        // verify the start state
+        assertEquals(1,
+                mRegistrar.getRegisteredAccountsForPackageName(componentName.getPackageName(),
+                        USER_HANDLE_10).size());
+        // remove the ConnectionService so that the account cannot be resolved anymore
+        mComponentContextFixture.removeConnectionService(componentName,
+                Mockito.mock(IConnectionService.class));
+        // verify the account is unregistered when fetching the phone accounts for the package
+        assertEquals(1,
+                mRegistrar.getRegisteredAccountsForPackageName(componentName.getPackageName(),
+                        USER_HANDLE_10).size());
+        assertEquals(0,
+                mRegistrar.cleanupAndGetVerifiedAccounts(account).size());
+        assertEquals(0,
+                mRegistrar.getRegisteredAccountsForPackageName(componentName.getPackageName(),
+                        USER_HANDLE_10).size());
+    }
+
+    /**
+     * Verify that if a client adds both the {@link
+     * PhoneAccount#CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS} capability AND is backed by a
+     * {@link android.telecom.ConnectionService}, a {@link IllegalArgumentException} is thrown.
+     */
+    @Test
+    public void testConnectionServiceAndTransactionalAccount() throws Exception {
+        PhoneAccount account = makeQuickAccountBuilder("0", 0, USER_HANDLE_10)
+                .setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED
+                        | PhoneAccount.CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS).build();
+        mComponentContextFixture.addConnectionService(
+                makeQuickConnectionServiceComponentName(),
+                Mockito.mock(IConnectionService.class));
+        try {
+            registerAndEnableAccount(account);
+            fail("failed to throw IllegalArgumentException");
+        } catch (IllegalArgumentException e) {
+            // test passed, ignore Exception.
+        }
+    }
+
     @MediumTest
     @Test
     public void testSimCallManager() throws Exception {
@@ -1135,6 +1304,7 @@
         // GIVEN
         mComponentContextFixture.addConnectionService(makeQuickConnectionServiceComponentName(),
                 Mockito.mock(IConnectionService.class));
+        UserManager userManager = mContext.getSystemService(UserManager.class);
 
         List<UserHandle> users = Arrays.asList(new UserHandle(0),
                 new UserHandle(1000));
@@ -1161,10 +1331,10 @@
         when(mContext.getPackageManager().getPackageInfo(PACKAGE_2, 0))
                 .thenThrow(new PackageManager.NameNotFoundException());
 
-        when(UserManager.get(mContext).getSerialNumberForUser(users.get(0)))
+        when(userManager.getSerialNumberForUser(users.get(0)))
                 .thenReturn(0L);
 
-        when(UserManager.get(mContext).getSerialNumberForUser(users.get(1)))
+        when(userManager.getSerialNumberForUser(users.get(1)))
                 .thenReturn(-1L);
 
         // THEN
@@ -1624,6 +1794,107 @@
     }
 
     /**
+     * Ensure an IllegalArgumentException is thrown when adding too many PhoneAccountHandles to
+     * a PhoneAccount.
+     */
+    @Test
+    public void testLimitOnSimultaneousCallingRestriction_tooManyElements() throws Exception {
+        doReturn(true).when(mTelephonyFeatureFlags).simultaneousCallingIndications();
+        mComponentContextFixture.addConnectionService(makeQuickConnectionServiceComponentName(),
+                Mockito.mock(IConnectionService.class));
+        Set<PhoneAccountHandle> tooManyElements = new HashSet<>(11);
+        for (int i = 0; i < 11; i++) {
+            tooManyElements.add(makeQuickAccountHandle(TEST_ID + i));
+        }
+        PhoneAccount tooManyRestrictionsPA = new PhoneAccount.Builder(
+                makeQuickAccountHandle(TEST_ID), TEST_LABEL)
+                .setSimultaneousCallingRestriction(tooManyElements)
+                .build();
+        try {
+            mRegistrar.registerPhoneAccount(tooManyRestrictionsPA);
+            fail("should have hit registrations exception in "
+                    + "enforceSimultaneousCallingRestrictionLimit");
+        } catch (IllegalArgumentException e) {
+            // pass test
+        }
+    }
+
+    /**
+     * Ensure an IllegalArgumentException is thrown when adding a PhoneAccountHandle where the
+     * package name field is too large.
+     */
+    @Test
+    public void testLimitOnSimultaneousCallingRestriction_InvalidPackageName() throws Exception {
+        doReturn(true).when(mTelephonyFeatureFlags).simultaneousCallingIndications();
+        mComponentContextFixture.addConnectionService(makeQuickConnectionServiceComponentName(),
+                Mockito.mock(IConnectionService.class));
+        Set<PhoneAccountHandle> invalidElement = new HashSet<>(1);
+        invalidElement.add(new PhoneAccountHandle(new ComponentName(INVALID_STR, "Class"),
+                TEST_ID));
+        PhoneAccount invalidRestrictionPA = new PhoneAccount.Builder(
+                makeQuickAccountHandle(TEST_ID), TEST_LABEL)
+                .setSimultaneousCallingRestriction(invalidElement)
+                .build();
+        try {
+            mRegistrar.registerPhoneAccount(invalidRestrictionPA);
+            fail("should have hit package name size limit exception in "
+                    + "enforceSimultaneousCallingRestrictionLimit");
+        } catch (IllegalArgumentException e) {
+            // pass test
+        }
+    }
+
+    /**
+     * Ensure an IllegalArgumentException is thrown when adding a PhoneAccountHandle where the
+     * class name field is too large.
+     */
+    @Test
+    public void testLimitOnSimultaneousCallingRestriction_InvalidClassName() throws Exception {
+        doReturn(true).when(mTelephonyFeatureFlags).simultaneousCallingIndications();
+        mComponentContextFixture.addConnectionService(makeQuickConnectionServiceComponentName(),
+                Mockito.mock(IConnectionService.class));
+        Set<PhoneAccountHandle> invalidElement = new HashSet<>(1);
+        invalidElement.add(new PhoneAccountHandle(new ComponentName("pkg", INVALID_STR),
+                TEST_ID));
+        PhoneAccount invalidRestrictionPA = new PhoneAccount.Builder(
+                makeQuickAccountHandle(TEST_ID), TEST_LABEL)
+                .setSimultaneousCallingRestriction(invalidElement)
+                .build();
+        try {
+            mRegistrar.registerPhoneAccount(invalidRestrictionPA);
+            fail("should have hit class name size limit exception in "
+                    + "enforceSimultaneousCallingRestrictionLimit");
+        } catch (IllegalArgumentException e) {
+            // pass test
+        }
+    }
+
+    /**
+     * Ensure an IllegalArgumentException is thrown when adding a PhoneAccountHandle where the
+     * ID field is too large.
+     */
+    @Test
+    public void testLimitOnSimultaneousCallingRestriction_InvalidIdSize() throws Exception {
+        doReturn(true).when(mTelephonyFeatureFlags).simultaneousCallingIndications();
+        mComponentContextFixture.addConnectionService(makeQuickConnectionServiceComponentName(),
+                Mockito.mock(IConnectionService.class));
+        Set<PhoneAccountHandle> invalidIdElement = new HashSet<>(1);
+        invalidIdElement.add(new PhoneAccountHandle(makeQuickConnectionServiceComponentName(),
+                INVALID_STR));
+        PhoneAccount invalidRestrictionPA = new PhoneAccount.Builder(
+                makeQuickAccountHandle(TEST_ID), TEST_LABEL)
+                .setSimultaneousCallingRestriction(invalidIdElement)
+                .build();
+        try {
+            mRegistrar.registerPhoneAccount(invalidRestrictionPA);
+            fail("should have hit ID size limit exception in "
+                    + "enforceSimultaneousCallingRestrictionLimit");
+        } catch (IllegalArgumentException e) {
+            // pass test
+        }
+    }
+
+    /**
      * Ensure an IllegalArgumentException is thrown when adding an address over the limit
      */
     @Test
@@ -1654,7 +1925,7 @@
                 makeQuickAccountHandle(TEST_ID)).setIcon(mockIcon);
         try {
             // WHEN
-            Mockito.doThrow(new IOException())
+            doThrow(new IOException())
                     .when(mockIcon).writeToStream(any(OutputStream.class));
             //THEN
             mRegistrar.enforceIconSizeLimit(builder.build());
@@ -1700,6 +1971,86 @@
         }
     }
 
+    @Test
+    public void testGetPhoneAccountAcrossUsers() throws Exception {
+        when(mTelephonyFeatureFlags.workProfileApiSplit()).thenReturn(true);
+        mComponentContextFixture.addConnectionService(makeQuickConnectionServiceComponentName(),
+                Mockito.mock(IConnectionService.class));
+
+        PhoneAccount accountForCurrent = makeQuickAccountBuilder("id_0", 0, UserHandle.CURRENT)
+                .setCapabilities(PhoneAccount.CAPABILITY_CONNECTION_MANAGER
+                        | PhoneAccount.CAPABILITY_CALL_PROVIDER).build();
+        PhoneAccount accountForAll = makeQuickAccountBuilder("id_0", 0, UserHandle.ALL)
+                .setCapabilities(PhoneAccount.CAPABILITY_CONNECTION_MANAGER
+                        | PhoneAccount.CAPABILITY_CALL_PROVIDER
+                        | PhoneAccount.CAPABILITY_MULTI_USER).build();
+        PhoneAccount accountForWorkProfile = makeQuickAccountBuilder("id_1", 1, USER_HANDLE_10)
+                .setCapabilities(PhoneAccount.CAPABILITY_CONNECTION_MANAGER
+                        | PhoneAccount.CAPABILITY_CALL_PROVIDER).build();
+
+        registerAndEnableAccount(accountForCurrent);
+        registerAndEnableAccount(accountForAll);
+        registerAndEnableAccount(accountForWorkProfile);
+
+        List<PhoneAccount> accountsForUser = mRegistrar.getPhoneAccounts(0, 0,
+                null, null, false, USER_HANDLE_10, false, false);
+        List<PhoneAccount> accountsVisibleUser = mRegistrar.getPhoneAccounts(0, 0,
+                null, null, false, USER_HANDLE_10, false, true);
+        List<PhoneAccount> accountsAcrossUser = mRegistrar.getPhoneAccounts(0, 0,
+                null, null, false, USER_HANDLE_10, true, false);
+
+        // Return the account exactly matching the user if it exists
+        assertEquals(1, accountsForUser.size());
+        assertTrue(accountsForUser.contains(accountForWorkProfile));
+        // The accounts visible to the user without across user permission
+        assertEquals(2, accountsVisibleUser.size());
+        assertTrue(accountsVisibleUser.containsAll(accountsForUser));
+        assertTrue(accountsVisibleUser.contains(accountForAll));
+        // The accounts visible to the user with across user permission
+        assertEquals(3, accountsAcrossUser.size());
+        assertTrue(accountsAcrossUser.containsAll(accountsVisibleUser));
+        assertTrue(accountsAcrossUser.contains(accountForCurrent));
+
+        mRegistrar.unregisterPhoneAccount(accountForWorkProfile.getAccountHandle());
+
+        accountsForUser = mRegistrar.getPhoneAccounts(0, 0,
+                null, null, false, USER_HANDLE_10, false, false);
+
+        // Return the account visible for the user if no account exactly matches the user
+        assertEquals(1, accountsForUser.size());
+        assertTrue(accountsForUser.contains(accountForAll));
+    }
+
+    @SmallTest
+    @Test
+    public void testGetSubscriptionIdForPhoneAccountWhenNoTelephony() throws Exception {
+        mComponentContextFixture.addConnectionService(makeQuickConnectionServiceComponentName(),
+                Mockito.mock(IConnectionService.class));
+
+        PhoneAccount simAccount =
+                makeQuickAccountBuilder("simzor", 1, null)
+                        .setCapabilities(
+                                PhoneAccount.CAPABILITY_CALL_PROVIDER
+                                        | PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)
+                        .setIsEnabled(true)
+                        .build();
+        registerAndEnableAccount(simAccount);
+        when(mComponentContextFixture.getTelephonyManager()
+                .getSubscriptionId(any(PhoneAccountHandle.class)))
+                .thenThrow(new UnsupportedOperationException("Bee-boop"));
+        assertEquals(SubscriptionManager.INVALID_SUBSCRIPTION_ID,
+                mRegistrar.getSubscriptionIdForPhoneAccount(simAccount.getAccountHandle()));
+
+        // One more thing; we'll test
+        doThrow(new UnsupportedOperationException("Bee boop!"))
+                .when(mComponentContextFixture.getSubscriptionManager())
+                .setDefaultVoiceSubscriptionId(anyInt());
+        mRegistrar.setUserSelectedOutgoingPhoneAccount(simAccount.getAccountHandle(),
+                simAccount.getAccountHandle().getUserHandle());
+
+        // There is nothing to verify, we just want to ensure that we didn't crash.
+    }
+
     private static PhoneAccount.Builder makeBuilderWithBindCapabilities(PhoneAccountHandle handle) {
         return new PhoneAccount.Builder(handle, TEST_LABEL)
                 .setCapabilities(PhoneAccount.CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS);
@@ -1818,35 +2169,44 @@
             Object self,
             T input,
             PhoneAccountRegistrar.XmlSerialization<T> xml,
-            Context context)
+            Context context,
+            FeatureFlags telephonyFeatureFlags,
+            com.android.server.telecom.flags.FeatureFlags telecomFeatureFlags)
             throws Exception {
         Log.d(self, "Input = %s", input);
 
-        byte[] data;
-        {
-            XmlSerializer serializer = new FastXmlSerializer();
-            ByteArrayOutputStream baos = new ByteArrayOutputStream();
-            serializer.setOutput(new BufferedOutputStream(baos), "utf-8");
-            xml.writeToXml(input, serializer, context);
-            serializer.flush();
-            data = baos.toByteArray();
-        }
+        byte[] data = toXml(input, xml, context, telephonyFeatureFlags);
 
         Log.i(self, "====== XML data ======\n%s", new String(data));
 
-        T result = null;
-        {
-            XmlPullParser parser = Xml.newPullParser();
-            parser.setInput(new BufferedInputStream(new ByteArrayInputStream(data)), null);
-            parser.nextTag();
-            result = xml.readFromXml(parser, MAX_VERSION, context);
-        }
+        T result = fromXml(data, xml, context, telephonyFeatureFlags, telecomFeatureFlags);
 
         Log.i(self, "result = " + result);
 
         return result;
     }
 
+    private static <T> byte[] toXml(T input, PhoneAccountRegistrar.XmlSerialization<T> xml,
+            Context context, FeatureFlags telephonyFeatureFlags) throws Exception {
+        XmlSerializer serializer = new FastXmlSerializer();
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        serializer.setOutput(new BufferedOutputStream(baos), "utf-8");
+        xml.writeToXml(input, serializer, context, telephonyFeatureFlags);
+        serializer.flush();
+        return baos.toByteArray();
+    }
+
+    private static <T> T fromXml(byte[] data, PhoneAccountRegistrar.XmlSerialization<T> xml,
+            Context context, FeatureFlags telephonyFeatureFlags,
+            com.android.server.telecom.flags.FeatureFlags telecomFeatureFlags) throws Exception {
+        XmlPullParser parser = Xml.newPullParser();
+        parser.setInput(new BufferedInputStream(new ByteArrayInputStream(data)), null);
+        parser.nextTag();
+        return xml.readFromXml(parser, MAX_VERSION, context,
+                telephonyFeatureFlags, telecomFeatureFlags);
+
+    }
+
     private static void assertPhoneAccountHandleEquals(PhoneAccountHandle a, PhoneAccountHandle b) {
         if (a != b) {
             assertEquals(
@@ -1895,6 +2255,12 @@
                 assertEquals(a.getSupportedUriSchemes(), b.getSupportedUriSchemes());
                 assertBundlesEqual(a.getExtras(), b.getExtras());
                 assertEquals(a.isEnabled(), b.isEnabled());
+                assertEquals(a.hasSimultaneousCallingRestriction(),
+                        b.hasSimultaneousCallingRestriction());
+                if (a.hasSimultaneousCallingRestriction()) {
+                    assertEquals(a.getSimultaneousCallingRestriction(),
+                            b.getSimultaneousCallingRestriction());
+                }
             } else {
                 fail("Phone accounts not equal: " + a + ", " + b);
             }
@@ -1935,6 +2301,7 @@
     }
 
     private PhoneAccountRegistrar.State makeQuickStateWithTelephonyPhoneAccountHandle() {
+        UserManager userManager = mContext.getSystemService(UserManager.class);
         PhoneAccountRegistrar.State s = new PhoneAccountRegistrar.State();
         s.accounts.add(makeQuickAccount("id0", 0));
         s.accounts.add(makeQuickAccount("id1", 1));
@@ -1943,9 +2310,9 @@
                 "com.android.phone",
                         "com.android.services.telephony.TelephonyConnectionService"), "id0");
         UserHandle userHandle = phoneAccountHandle.getUserHandle();
-        when(UserManager.get(mContext).getSerialNumberForUser(userHandle))
+        when(userManager.getSerialNumberForUser(userHandle))
             .thenReturn(0L);
-        when(UserManager.get(mContext).getUserForSerialNumber(0L))
+        when(userManager.getUserForSerialNumber(0L))
             .thenReturn(userHandle);
         s.defaultOutgoingAccountHandles
             .put(userHandle, new DefaultPhoneAccountHandle(userHandle, phoneAccountHandle,
@@ -1954,6 +2321,7 @@
     }
 
     private PhoneAccountRegistrar.State makeQuickState() {
+        UserManager userManager = mContext.getSystemService(UserManager.class);
         PhoneAccountRegistrar.State s = new PhoneAccountRegistrar.State();
         s.accounts.add(makeQuickAccount("id0", 0));
         s.accounts.add(makeQuickAccount("id1", 1));
@@ -1961,9 +2329,9 @@
         PhoneAccountHandle phoneAccountHandle = new PhoneAccountHandle(
                 new ComponentName("pkg0", "cls0"), "id0");
         UserHandle userHandle = phoneAccountHandle.getUserHandle();
-        when(UserManager.get(mContext).getSerialNumberForUser(userHandle))
+        when(userManager.getSerialNumberForUser(userHandle))
                 .thenReturn(0L);
-        when(UserManager.get(mContext).getUserForSerialNumber(0L))
+        when(userManager.getUserForSerialNumber(0L))
                 .thenReturn(userHandle);
         s.defaultOutgoingAccountHandles
                 .put(userHandle, new DefaultPhoneAccountHandle(userHandle, phoneAccountHandle,
diff --git a/tests/src/com/android/server/telecom/tests/PhoneStateBroadcasterTest.java b/tests/src/com/android/server/telecom/tests/PhoneStateBroadcasterTest.java
new file mode 100644
index 0000000..b18c2ce
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/PhoneStateBroadcasterTest.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2024 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 org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.ComponentName;
+import android.net.Uri;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.SubscriptionManager;
+import android.telephony.emergency.EmergencyNumber;
+import android.util.ArrayMap;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.PhoneStateBroadcaster;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+@RunWith(JUnit4.class)
+public class PhoneStateBroadcasterTest extends TelecomTestCase {
+    @Override
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+    }
+
+    @Override
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    /**
+     * Tests behavior where FEATURE_TELEPHONY_CALLING is not available, but
+     * FEATURE_TELEPHONY_SUBSCRIPTION is; in this case we can't detect that the number is emergency
+     * so we will not bother sending out anything.
+     */
+    @Test
+    public void testNotifyOutgoingEmergencyCallWithNoTelephonyCalling() {
+        CallsManager cm = mock(CallsManager.class);
+        when(cm.getContext()).thenReturn(mContext);
+        when(mComponentContextFixture.getTelephonyManager().isEmergencyNumber(anyString()))
+                .thenThrow(new UnsupportedOperationException("Bee boop"));
+        PhoneStateBroadcaster psb = new PhoneStateBroadcaster(cm);
+
+        Call call = mock(Call.class);
+        when(call.isExternalCall()).thenReturn(false);
+        when(call.isEmergencyCall()).thenReturn(true);
+        when(call.isIncoming()).thenReturn(false);
+        when(call.getHandle()).thenReturn(Uri.parse("tel:911"));
+
+        psb.onCallAdded(call);
+        verify(mComponentContextFixture.getTelephonyRegistryManager(), never())
+                .notifyOutgoingEmergencyCall(anyInt(), anyInt(), any(EmergencyNumber.class));
+    }
+
+    /**
+     * Tests behavior where FEATURE_TELEPHONY_CALLING is available, but
+     * FEATURE_TELEPHONY_SUBSCRIPTION is; in this case we can detect that this is an emergency
+     * call, but we can't figure out any of the subscription parameters.  It is doubtful we'd ever
+     * see this in practice since technically FEATURE_TELEPHONY_CALLING needs
+     * FEATURE_TELEPHONY_SUBSCRIPTION.
+     */
+    @Test
+    public void testNotifyOutgoingEmergencyCallWithNoTelephonySubscription() {
+        CallsManager cm = mock(CallsManager.class);
+        when(cm.getContext()).thenReturn(mContext);
+        Map<Integer, List<EmergencyNumber>> nums = new ArrayMap<Integer, List<EmergencyNumber>>();
+        nums.put(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID,
+                Arrays.asList(new EmergencyNumber("911", "US", null, 0, Collections.EMPTY_LIST,
+                        0, 0)));
+        when(mComponentContextFixture.getTelephonyManager().getEmergencyNumberList())
+                .thenReturn(nums);
+        when(mComponentContextFixture.getTelephonyManager().getSubscriptionId(any(
+                        PhoneAccountHandle.class)))
+                .thenThrow(new UnsupportedOperationException("Bee boop"));
+        PhoneStateBroadcaster psb = new PhoneStateBroadcaster(cm);
+
+        Call call = mock(Call.class);
+        when(call.isExternalCall()).thenReturn(false);
+        when(call.isEmergencyCall()).thenReturn(true);
+        when(call.isIncoming()).thenReturn(false);
+        when(call.getHandle()).thenReturn(Uri.parse("tel:911"));
+        when(call.getTargetPhoneAccount()).thenReturn(new PhoneAccountHandle(
+                ComponentName.unflattenFromString("foo/bar"), "90210"));
+
+        psb.onCallAdded(call);
+        verify(mComponentContextFixture.getTelephonyRegistryManager())
+                .notifyOutgoingEmergencyCall(eq(SubscriptionManager.INVALID_SIM_SLOT_INDEX),
+                        eq(SubscriptionManager.INVALID_SUBSCRIPTION_ID),
+                        any(EmergencyNumber.class));
+    }
+}
diff --git a/tests/src/com/android/server/telecom/tests/ProximitySensorManagerTest.java b/tests/src/com/android/server/telecom/tests/ProximitySensorManagerTest.java
index 807b7cf..310f4cb 100644
--- a/tests/src/com/android/server/telecom/tests/ProximitySensorManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/ProximitySensorManagerTest.java
@@ -16,8 +16,14 @@
 
 package com.android.server.telecom.tests;
 
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
 import android.os.PowerManager;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.filters.SmallTest;
 
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallsManager;
@@ -33,11 +39,6 @@
 
 import java.util.List;
 
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
 @RunWith(JUnit4.class)
 public class ProximitySensorManagerTest extends TelecomTestCase{
 
diff --git a/tests/src/com/android/server/telecom/tests/RingbackPlayerTest.java b/tests/src/com/android/server/telecom/tests/RingbackPlayerTest.java
new file mode 100644
index 0000000..e8d39c8
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/RingbackPlayerTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2022 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 org.junit.Assert.assertFalse;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.when;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallState;
+import com.android.server.telecom.InCallTonePlayer;
+import com.android.server.telecom.RingbackPlayer;
+
+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.concurrent.CountDownLatch;
+
+@RunWith(JUnit4.class)
+public class RingbackPlayerTest extends TelecomTestCase {
+    @Mock InCallTonePlayer.Factory mFactory;
+    @Mock Call mCall;
+    @Mock InCallTonePlayer mTonePlayer;
+
+    private RingbackPlayer mRingbackPlayer;
+
+    @Before
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        when(mFactory.createPlayer(any(Call.class), anyInt())).thenReturn(mTonePlayer);
+        mRingbackPlayer = new RingbackPlayer(mFactory);
+    }
+
+    @SmallTest
+    @Test
+    public void testPlayerSync() {
+        // make sure InCallTonePlayer try to start playing the tone after RingbackPlayer receives
+        // stop tone request.
+        CountDownLatch latch = new CountDownLatch(1);
+        doReturn(CallState.DIALING).when(mCall).getState();
+        doAnswer(x -> {
+            new Thread(() -> {
+                try {
+                    latch.await();
+                } catch (InterruptedException e) {
+                    // Ignore
+                }
+            }).start();
+            return true;
+        }).when(mTonePlayer).startTone();
+
+        mRingbackPlayer.startRingbackForCall(mCall);
+        mRingbackPlayer.stopRingbackForCall(mCall);
+        assertFalse(mRingbackPlayer.isRingbackPlaying());
+        latch.countDown();
+    }
+}
diff --git a/tests/src/com/android/server/telecom/tests/RingerTest.java b/tests/src/com/android/server/telecom/tests/RingerTest.java
index a4adf77..1215fd3 100644
--- a/tests/src/com/android/server/telecom/tests/RingerTest.java
+++ b/tests/src/com/android/server/telecom/tests/RingerTest.java
@@ -16,8 +16,10 @@
 
 package com.android.server.telecom.tests;
 
+import static android.os.VibrationEffect.EFFECT_CLICK;
 import static android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
@@ -30,6 +32,8 @@
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
@@ -50,9 +54,14 @@
 import android.os.VibrationAttributes;
 import android.os.VibrationEffect;
 import android.os.Vibrator;
+import android.os.VibratorInfo;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.telecom.PhoneAccountHandle;
 import android.telecom.TelecomManager;
-import android.test.suitebuilder.annotation.SmallTest;
+import android.util.Pair;
+
+import androidx.test.filters.SmallTest;
 
 import com.android.server.telecom.AsyncRingtonePlayer;
 import com.android.server.telecom.Call;
@@ -62,33 +71,48 @@
 import com.android.server.telecom.Ringer;
 import com.android.server.telecom.RingtoneFactory;
 import com.android.server.telecom.SystemSettingsUtil;
+import com.android.server.telecom.flags.FeatureFlags;
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 import org.mockito.Mock;
 import org.mockito.Spy;
 
+import java.time.Duration;
 import java.util.concurrent.CompletableFuture;
+import java.util.function.Supplier;
 
 @RunWith(JUnit4.class)
 public class RingerTest extends TelecomTestCase {
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
     private static final Uri FAKE_RINGTONE_URI = Uri.parse("content://media/fake/audio/1729");
     // Returned when the a URI-based VibrationEffect is attempted, to avoid depending on actual
     // device configuration for ringtone URIs. The actual Uri can be verified via the
     // VibrationEffectProxy mock invocation.
     private static final VibrationEffect URI_VIBRATION_EFFECT =
             VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK);
+    private static final VibrationEffect EXPECTED_SIMPLE_VIBRATION_PATTERN =
+            VibrationEffect.createWaveform(
+                    new long[] {0, 1000, 1000}, new int[] {0, 255, 0}, 1);
+    private static final VibrationEffect EXPECTED_PULSE_VIBRATION_PATTERN =
+            VibrationEffect.createWaveform(
+                    Ringer.PULSE_PATTERN, Ringer.PULSE_AMPLITUDE, 5);
 
     @Mock InCallTonePlayer.Factory mockPlayerFactory;
     @Mock SystemSettingsUtil mockSystemSettingsUtil;
     @Mock RingtoneFactory mockRingtoneFactory;
     @Mock Vibrator mockVibrator;
+    @Mock VibratorInfo mockVibratorInfo;
     @Mock InCallController mockInCallController;
     @Mock NotificationManager mockNotificationManager;
     @Mock Ringer.AccessibilityManagerAdapter mockAccessibilityManagerAdapter;
+    @Mock private FeatureFlags mFeatureFlags;
 
     @Spy Ringer.VibrationEffectProxy spyVibrationEffectProxy;
 
@@ -110,21 +134,27 @@
     @Before
     public void setUp() throws Exception {
         super.setUp();
-        mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
+        mContext = spy(mComponentContextFixture.getTestDouble().getApplicationContext());
+        when(mFeatureFlags.telecomResolveHiddenDependencies()).thenReturn(true);
         doReturn(URI_VIBRATION_EFFECT).when(spyVibrationEffectProxy).get(any(), any());
-        when(mockPlayerFactory.createPlayer(anyInt())).thenReturn(mockTonePlayer);
+        when(mockPlayerFactory.createPlayer(any(Call.class), anyInt())).thenReturn(mockTonePlayer);
         mockAudioManager = mContext.getSystemService(AudioManager.class);
         when(mockAudioManager.getRingerMode()).thenReturn(AudioManager.RINGER_MODE_NORMAL);
+        when(mockVibrator.getInfo()).thenReturn(mockVibratorInfo);
         when(mockSystemSettingsUtil.isHapticPlaybackSupported(any(Context.class)))
                 .thenAnswer((invocation) -> mIsHapticPlaybackSupported);
         mockNotificationManager =mContext.getSystemService(NotificationManager.class);
         when(mockTonePlayer.startTone()).thenReturn(true);
-        when(mockNotificationManager.matchesCallFilter(any(Bundle.class))).thenReturn(true);
-        when(mockRingtoneFactory.hasHapticChannels(any(Ringtone.class))).thenReturn(false);
+        when(mockNotificationManager.matchesCallFilter(any(Uri.class))).thenReturn(true);
         when(mockCall1.getState()).thenReturn(CallState.RINGING);
         when(mockCall2.getState()).thenReturn(CallState.RINGING);
         when(mockCall1.getAssociatedUser()).thenReturn(PA_HANDLE.getUserHandle());
         when(mockCall2.getAssociatedUser()).thenReturn(PA_HANDLE.getUserHandle());
+        when(mockCall1.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
+        when(mockCall2.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
+        // Set BT active state in tests to ensure that we do not end up blocking tests for 1 sec
+        // waiting for BT to connect in unit tests by default.
+        asyncRingtonePlayer.updateBtActiveState(true);
 
         createRingerUnderTest();
     }
@@ -136,7 +166,8 @@
     private void createRingerUnderTest() {
         mRingerUnderTest = new Ringer(mockPlayerFactory, mContext, mockSystemSettingsUtil,
                 asyncRingtonePlayer, mockRingtoneFactory, mockVibrator, spyVibrationEffectProxy,
-                mockInCallController, mockNotificationManager, mockAccessibilityManagerAdapter);
+                mockInCallController, mockNotificationManager, mockAccessibilityManagerAdapter,
+                mFeatureFlags);
         // This future is used to wait for AsyncRingtonePlayer to finish its part.
         mRingerUnderTest.setBlockOnRingingFuture(mRingCompletionFuture);
     }
@@ -149,6 +180,151 @@
 
     @SmallTest
     @Test
+    public void testSimpleVibrationPrecedesValidSupportedDefaultRingVibrationOverride()
+            throws Exception {
+        when(mFeatureFlags.useDeviceProvidedSerializedRingerVibration()).thenReturn(true);
+        mockVibrationResourceValues(
+                """
+                    <vibration-effect>
+                        <predefined-effect name="click"/>
+                    </vibration-effect>
+                """,
+                /* useSimpleVibration= */ true);
+        when(mockVibratorInfo.areVibrationFeaturesSupported(any())).thenReturn(true);
+
+        createRingerUnderTest();
+
+        assertEquals(EXPECTED_SIMPLE_VIBRATION_PATTERN, mRingerUnderTest.mDefaultVibrationEffect);
+    }
+
+    @SmallTest
+    @Test
+    public void testDefaultRingVibrationOverrideNotUsedWhenFeatureIsDisabled()
+            throws Exception {
+        when(mFeatureFlags.useDeviceProvidedSerializedRingerVibration()).thenReturn(false);
+        mockVibrationResourceValues(
+                """
+                    <vibration-effect>
+                        <waveform-effect>
+                            <waveform-entry durationMs="100" amplitude="0"/>
+                            <repeating>
+                                <waveform-entry durationMs="500" amplitude="default"/>
+                                <waveform-entry durationMs="700" amplitude="0"/>
+                            </repeating>
+                        </waveform-effect>
+                    </vibration-effect>
+                """,
+                /* useSimpleVibration= */ false);
+        when(mockVibratorInfo.areVibrationFeaturesSupported(any())).thenReturn(true);
+
+        createRingerUnderTest();
+
+        assertEquals(EXPECTED_PULSE_VIBRATION_PATTERN, mRingerUnderTest.mDefaultVibrationEffect);
+    }
+
+    @SmallTest
+    @Test
+    public void testValidSupportedRepeatingDefaultRingVibrationOverride() throws Exception {
+        when(mFeatureFlags.useDeviceProvidedSerializedRingerVibration()).thenReturn(true);
+        mockVibrationResourceValues(
+                """
+                    <vibration-effect>
+                        <waveform-effect>
+                            <waveform-entry durationMs="100" amplitude="0"/>
+                            <repeating>
+                                <waveform-entry durationMs="500" amplitude="default"/>
+                                <waveform-entry durationMs="700" amplitude="0"/>
+                            </repeating>
+                        </waveform-effect>
+                    </vibration-effect>
+                """,
+                /* useSimpleVibration= */ false);
+        when(mockVibratorInfo.areVibrationFeaturesSupported(any())).thenReturn(true);
+
+        createRingerUnderTest();
+
+        assertEquals(
+                VibrationEffect.createWaveform(new long[]{100, 500, 700}, /* repeat= */ 1),
+                mRingerUnderTest.mDefaultVibrationEffect);
+    }
+
+    @SmallTest
+    @Test
+    public void testValidSupportedNonRepeatingDefaultRingVibrationOverride() throws Exception {
+        when(mFeatureFlags.useDeviceProvidedSerializedRingerVibration()).thenReturn(true);
+        mockVibrationResourceValues(
+                """
+                    <vibration-effect>
+                        <predefined-effect name="click"/>
+                    </vibration-effect>
+                """,
+                /* useSimpleVibration= */ false);
+        when(mockVibratorInfo.areVibrationFeaturesSupported(any())).thenReturn(true);
+
+        createRingerUnderTest();
+
+        assertEquals(
+                VibrationEffect
+                        .startComposition()
+                        .repeatEffectIndefinitely(
+                                VibrationEffect
+                                        .startComposition()
+                                        .addEffect(VibrationEffect.createPredefined(EFFECT_CLICK))
+                                        .addOffDuration(Duration.ofSeconds(1))
+                                        .compose()
+                        )
+                        .compose(),
+                mRingerUnderTest.mDefaultVibrationEffect);
+    }
+
+    @SmallTest
+    @Test
+    public void testValidButUnsupportedDefaultRingVibrationOverride() throws Exception {
+        when(mFeatureFlags.useDeviceProvidedSerializedRingerVibration()).thenReturn(true);
+        mockVibrationResourceValues(
+                """
+                    <vibration-effect>
+                        <predefined-effect name="click"/>
+                    </vibration-effect>
+                """,
+                /* useSimpleVibration= */ false);
+        when(mockVibratorInfo.areVibrationFeaturesSupported(
+                eq(VibrationEffect.createPredefined(EFFECT_CLICK)))).thenReturn(false);
+
+        createRingerUnderTest();
+
+        assertEquals(EXPECTED_SIMPLE_VIBRATION_PATTERN, mRingerUnderTest.mDefaultVibrationEffect);
+    }
+
+    @SmallTest
+    @Test
+    public void testInvalidDefaultRingVibrationOverride() throws Exception {
+        when(mFeatureFlags.useDeviceProvidedSerializedRingerVibration()).thenReturn(true);
+        mockVibrationResourceValues(
+                /* defaultVibrationContent= */ "bad serialization",
+                /* useSimpleVibration= */ false);
+        when(mockVibratorInfo.areVibrationFeaturesSupported(any())).thenReturn(true);
+
+        createRingerUnderTest();
+
+        assertEquals(EXPECTED_SIMPLE_VIBRATION_PATTERN, mRingerUnderTest.mDefaultVibrationEffect);
+    }
+
+    @SmallTest
+    @Test
+    public void testEmptyDefaultRingVibrationOverride() throws Exception {
+        when(mFeatureFlags.useDeviceProvidedSerializedRingerVibration()).thenReturn(true);
+        mockVibrationResourceValues(
+                /* defaultVibrationContent= */ "", /* useSimpleVibration= */ false);
+        when(mockVibratorInfo.areVibrationFeaturesSupported(any())).thenReturn(true);
+
+        createRingerUnderTest();
+
+        assertEquals(EXPECTED_SIMPLE_VIBRATION_PATTERN, mRingerUnderTest.mDefaultVibrationEffect);
+    }
+
+    @SmallTest
+    @Test
     public void testNoActionInTheaterMode() throws Exception {
         // Start call waiting to make sure that it doesn't stop when we start ringing
         mRingerUnderTest.startCallWaiting(mockCall1);
@@ -227,7 +403,7 @@
     @SmallTest
     @Test
     public void testCallWaitingButNoRingForSpecificContacts() throws Exception {
-        when(mockNotificationManager.matchesCallFilter(any(Bundle.class))).thenReturn(false);
+        when(mockNotificationManager.matchesCallFilter(any(Uri.class))).thenReturn(false);
         // Start call waiting to make sure that it does stop when we start ringing
         mRingerUnderTest.startCallWaiting(mockCall1);
         verify(mockTonePlayer).startTone();
@@ -251,7 +427,7 @@
         // Pretend we're using audio coupled haptics.
         setIsUsingHaptics(mockRingtone, true);
         assertTrue(startRingingAndWaitForAsync(mockCall1, false));
-        verify(mockRingtoneFactory, times(1))
+        verify(mockRingtoneFactory, atLeastOnce())
             .getRingtone(any(Call.class), nullable(VolumeShaper.Configuration.class), anyBoolean());
         verifyNoMoreInteractions(mockRingtoneFactory);
         verify(mockTonePlayer).stopTone();
@@ -293,14 +469,14 @@
 
         mRingerUnderTest.startCallWaiting(mockCall1);
         when(mockRingtoneFactory.getRingtone(any(Call.class), eq(null), anyBoolean()))
-            .thenReturn(mockRingtone);
+            .thenReturn(new Pair(FAKE_RINGTONE_URI, mockRingtone));
         when(mockAudioManager.getRingerMode()).thenReturn(AudioManager.RINGER_MODE_VIBRATE);
         when(mockAudioManager.getStreamVolume(AudioManager.STREAM_RING)).thenReturn(0);
         enableVibrationWhenRinging();
         assertFalse(startRingingAndWaitForAsync(mockCall2, false));
         verify(mockTonePlayer).stopTone();
         // Try to play a silent haptics ringtone
-        verify(mockRingtoneFactory, times(1)).getHapticOnlyRingtone();
+        verify(mockRingtoneFactory, atLeastOnce()).getHapticOnlyRingtone();
         verifyNoMoreInteractions(mockRingtoneFactory);
         verify(mockRingtone).play();
 
@@ -339,7 +515,7 @@
         enableVibrationWhenRinging();
         assertFalse(startRingingAndWaitForAsync(mockCall2, false));
 
-        verify(mockRingtoneFactory, times(1)).getHapticOnlyRingtone();
+        verify(mockRingtoneFactory, atLeastOnce()).getHapticOnlyRingtone();
         verifyNoMoreInteractions(mockRingtoneFactory);
         verify(mockTonePlayer).stopTone();
         // Try to play a silent haptics ringtone
@@ -359,7 +535,7 @@
         enableVibrationWhenRinging();
         assertTrue(startRingingAndWaitForAsync(mockCall2, false));
         verify(mockTonePlayer).stopTone();
-        verify(mockRingtoneFactory, times(1))
+        verify(mockRingtoneFactory, atLeastOnce())
             .getRingtone(any(Call.class), isNull(), anyBoolean());
         verifyNoMoreInteractions(mockRingtoneFactory);
         verify(mockRingtone).play();
@@ -376,7 +552,7 @@
         ensureRingerIsAudible();
         enableVibrationOnlyWhenNotRinging();
         assertTrue(startRingingAndWaitForAsync(mockCall2, false));
-        verify(mockRingtoneFactory, times(1))
+        verify(mockRingtoneFactory, atLeastOnce())
             .getRingtone(any(Call.class), nullable(VolumeShaper.Configuration.class), anyBoolean());
         verifyNoMoreInteractions(mockRingtoneFactory);
         verify(mockTonePlayer).stopTone();
@@ -395,7 +571,7 @@
         enableRampingRinger();
         enableVibrationWhenRinging();
         assertTrue(startRingingAndWaitForAsync(mockCall2, false));
-        verify(mockRingtoneFactory, times(1))
+        verify(mockRingtoneFactory, atLeastOnce())
             .getRingtone(any(Call.class), nullable(VolumeShaper.Configuration.class), anyBoolean());
         verifyNoMoreInteractions(mockRingtoneFactory);
         verify(mockTonePlayer).stopTone();
@@ -427,13 +603,55 @@
         when(mockAudioManager.getStreamVolume(AudioManager.STREAM_RING)).thenReturn(100);
         enableVibrationWhenRinging();
         assertTrue(startRingingAndWaitForAsync(mockCall2, true));
-        verify(mockRingtoneFactory, times(1))
+        verify(mockRingtoneFactory, atLeastOnce())
             .getRingtone(any(Call.class), isNull(), anyBoolean());
         verifyNoMoreInteractions(mockRingtoneFactory);
         verify(mockTonePlayer).stopTone();
         verify(mockRingtone).play();
     }
 
+    @SmallTest
+    @Test
+    public void testDelayRingerForBtHfpDevices() throws Exception {
+        asyncRingtonePlayer.updateBtActiveState(false);
+        Ringtone mockRingtone = ensureRingtoneMocked();
+
+        ensureRingerIsAudible();
+        assertTrue(mRingerUnderTest.startRinging(mockCall1, true));
+        assertTrue(mRingerUnderTest.isRinging());
+        // We should not have the ringtone play until BT moves active
+        verify(mockRingtone, never()).play();
+
+        asyncRingtonePlayer.updateBtActiveState(true);
+        mRingCompletionFuture.get();
+        verify(mockRingtoneFactory, atLeastOnce())
+                .getRingtone(any(Call.class), nullable(VolumeShaper.Configuration.class),
+                        anyBoolean());
+        verifyNoMoreInteractions(mockRingtoneFactory);
+        verify(mockRingtone).play();
+
+        mRingerUnderTest.stopRinging();
+        verify(mockRingtone, timeout(1000/*ms*/)).stop();
+        assertFalse(mRingerUnderTest.isRinging());
+    }
+
+    @SmallTest
+    @Test
+    public void testUnblockRingerForStopCommand() throws Exception {
+        asyncRingtonePlayer.updateBtActiveState(false);
+        Ringtone mockRingtone = ensureRingtoneMocked();
+
+        ensureRingerIsAudible();
+        assertTrue(mRingerUnderTest.startRinging(mockCall1, true));
+        // We should not have the ringtone play until BT moves active
+        verify(mockRingtone, never()).play();
+
+        // We are not setting BT active, but calling stop ringing while the other thread is waiting
+        // for BT active should also unblock it.
+        mRingerUnderTest.stopRinging();
+        verify(mockRingtone, timeout(1000/*ms*/)).stop();
+    }
+
     /**
      * test shouldRingForContact will suppress the incoming call if matchesCallFilter returns
      * false (meaning DND is ON and the caller cannot bypass the settings)
@@ -446,7 +664,7 @@
         when(mContext.getSystemService(NotificationManager.class)).thenReturn(
                 mockNotificationManager);
         // suppress the call
-        when(mockNotificationManager.matchesCallFilter(any(Bundle.class))).thenReturn(false);
+        when(mockNotificationManager.matchesCallFilter(any(Uri.class))).thenReturn(false);
 
         // run the method under test
         assertFalse(mRingerUnderTest.shouldRingForContact(mockCall1));
@@ -455,7 +673,7 @@
         // verify we never set the call object and matchesCallFilter is called
         verify(mockCall1, never()).setCallIsSuppressedByDoNotDisturb(true);
         verify(mockNotificationManager, times(1))
-                .matchesCallFilter(any(Bundle.class));
+                .matchesCallFilter(any(Uri.class));
     }
 
     /**
@@ -468,7 +686,6 @@
         when(mockCall1.wasDndCheckComputedForCall()).thenReturn(false);
         when(mockCall1.getHandle()).thenReturn(Uri.parse(""));
         // alert the user of the call
-        when(mockNotificationManager.matchesCallFilter(any(Bundle.class))).thenReturn(true);
 
         // run the method under test
         assertTrue(mRingerUnderTest.shouldRingForContact(mockCall1));
@@ -477,7 +694,7 @@
         // verify we never set the call object and matchesCallFilter is called
         verify(mockCall1, never()).setCallIsSuppressedByDoNotDisturb(false);
         verify(mockNotificationManager, times(1))
-                .matchesCallFilter(any(Bundle.class));
+                .matchesCallFilter(any(Uri.class));
     }
 
     /**
@@ -493,7 +710,7 @@
         // THEN
         assertFalse(mRingerUnderTest.shouldRingForContact(mockCall1));
         verify(mockCall1, never()).setCallIsSuppressedByDoNotDisturb(false);
-        verify(mockNotificationManager, never()).matchesCallFilter(any(Bundle.class));
+        verify(mockNotificationManager, never()).matchesCallFilter(any(Uri.class));
     }
 
     @Test
@@ -503,7 +720,7 @@
         mRingerUnderTest.startCallWaiting(mockCall1);
         when(mockCall2.wasDndCheckComputedForCall()).thenReturn(false);
         when(mockCall2.getHandle()).thenReturn(Uri.parse(""));
-        when(mockNotificationManager.matchesCallFilter(any(Bundle.class))).thenReturn(false);
+        when(mockNotificationManager.matchesCallFilter(any(Uri.class))).thenReturn(false);
 
         assertFalse(mRingerUnderTest.shouldRingForContact(mockCall2));
         assertFalse(startRingingAndWaitForAsync(mockCall2, false));
@@ -518,7 +735,6 @@
         mRingerUnderTest.startCallWaiting(mockCall1);
         when(mockCall2.wasDndCheckComputedForCall()).thenReturn(false);
         when(mockCall2.getHandle()).thenReturn(Uri.parse(""));
-        when(mockNotificationManager.matchesCallFilter(any(Bundle.class))).thenReturn(true);
 
         assertTrue(mRingerUnderTest.shouldRingForContact(mockCall2));
         assertTrue(startRingingAndWaitForAsync(mockCall2, false));
@@ -538,14 +754,13 @@
                     } catch (InterruptedException e) {
                         Thread.currentThread().interrupt();
                     }
-                    return mockRingtone;
+                    return new Pair(FAKE_RINGTONE_URI, mockRingtone);
                 });
         // Start call waiting to make sure that it doesn't stop when we start ringing
         enableVibrationWhenRinging();
         mRingerUnderTest.startCallWaiting(mockCall1);
         when(mockCall2.wasDndCheckComputedForCall()).thenReturn(false);
         when(mockCall2.getHandle()).thenReturn(Uri.parse(""));
-        when(mockNotificationManager.matchesCallFilter(any(Bundle.class))).thenReturn(true);
 
         assertTrue(mRingerUnderTest.shouldRingForContact(mockCall2));
         assertTrue(mRingerUnderTest.startRinging(mockCall2, false));
@@ -618,10 +833,21 @@
 
     private Ringtone ensureRingtoneMocked() {
         Ringtone mockRingtone = mock(Ringtone.class);
+        Pair<Uri, Ringtone> ringtoneInfo = new Pair(
+                FAKE_RINGTONE_URI, mockRingtone);
         when(mockRingtoneFactory.getRingtone(
                 any(Call.class), nullable(VolumeShaper.Configuration.class), anyBoolean()))
-                .thenReturn(mockRingtone);
-        when(mockRingtoneFactory.getHapticOnlyRingtone()).thenReturn(mockRingtone);
+                .thenReturn(ringtoneInfo);
+        when(mockRingtoneFactory.getHapticOnlyRingtone()).thenReturn(ringtoneInfo);
         return mockRingtone;
     }
+
+    private void mockVibrationResourceValues(
+            String defaultVibrationContent, boolean useSimpleVibration) {
+        mComponentContextFixture.putRawResource(
+                com.android.internal.R.raw.default_ringtone_vibration_effect,
+                defaultVibrationContent);
+        mComponentContextFixture.putBooleanResource(
+                R.bool.use_simple_vibration_pattern, useSimpleVibration);
+    }
 }
diff --git a/tests/src/com/android/server/telecom/tests/SessionManagerTest.java b/tests/src/com/android/server/telecom/tests/SessionManagerTest.java
index cf84b7c..3e82eac 100644
--- a/tests/src/com/android/server/telecom/tests/SessionManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/SessionManagerTest.java
@@ -24,7 +24,8 @@
 
 import android.telecom.Logging.Session;
 import android.telecom.Logging.SessionManager;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.filters.SmallTest;
 
 import org.junit.After;
 import org.junit.Before;
diff --git a/tests/src/com/android/server/telecom/tests/SessionTest.java b/tests/src/com/android/server/telecom/tests/SessionTest.java
index f38618c..5378596 100644
--- a/tests/src/com/android/server/telecom/tests/SessionTest.java
+++ b/tests/src/com/android/server/telecom/tests/SessionTest.java
@@ -20,7 +20,8 @@
 
 import android.telecom.Log;
 import android.telecom.Logging.Session;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.filters.SmallTest;
 
 import org.junit.After;
 import org.junit.Before;
diff --git a/tests/src/com/android/server/telecom/tests/SystemStateHelperTest.java b/tests/src/com/android/server/telecom/tests/SystemStateHelperTest.java
index dc7d1fd..169d580 100644
--- a/tests/src/com/android/server/telecom/tests/SystemStateHelperTest.java
+++ b/tests/src/com/android/server/telecom/tests/SystemStateHelperTest.java
@@ -24,7 +24,7 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.isNull;
-import static org.mockito.Matchers.any;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
@@ -44,7 +44,8 @@
 import android.hardware.SensorEventListener;
 import android.hardware.SensorManager;
 import android.net.Uri;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.filters.SmallTest;
 
 import com.android.server.telecom.SystemStateHelper;
 import com.android.server.telecom.SystemStateHelper.SystemStateListener;
diff --git a/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java b/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java
index 8bc1f2a..dc5f325 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java
@@ -23,6 +23,36 @@
 import static android.Manifest.permission.READ_PHONE_NUMBERS;
 import static android.Manifest.permission.READ_PHONE_STATE;
 import static android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE;
+import static android.Manifest.permission.READ_SMS;
+import static android.Manifest.permission.REGISTER_SIM_SUBSCRIPTION;
+import static android.Manifest.permission.WRITE_SECURE_SETTINGS;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.isA;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.Manifest;
 import android.app.ActivityManager;
@@ -48,7 +78,8 @@
 import android.telecom.TelecomManager;
 import android.telecom.VideoProfile;
 import android.telephony.TelephonyManager;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.filters.SmallTest;
 
 import com.android.internal.telecom.ICallEventCallback;
 import com.android.internal.telecom.ITelecomService;
@@ -58,11 +89,13 @@
 import com.android.server.telecom.CallState;
 import com.android.server.telecom.CallsManager;
 import com.android.server.telecom.DefaultDialerCache;
+import com.android.server.telecom.InCallController;
 import com.android.server.telecom.PhoneAccountRegistrar;
 import com.android.server.telecom.TelecomServiceImpl;
 import com.android.server.telecom.TelecomSystem;
 import com.android.server.telecom.components.UserCallIntentProcessor;
 import com.android.server.telecom.components.UserCallIntentProcessorFactory;
+import com.android.server.telecom.flags.FeatureFlags;
 import com.android.server.telecom.voip.IncomingCallTransaction;
 import com.android.server.telecom.voip.OutgoingCallTransaction;
 import com.android.server.telecom.voip.TransactionManager;
@@ -76,40 +109,15 @@
 import org.mockito.ArgumentMatcher;
 import org.mockito.Mock;
 
+import java.lang.reflect.Method;
 import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 import java.util.concurrent.Executor;
 import java.util.function.IntConsumer;
 
-import static android.Manifest.permission.READ_SMS;
-import static android.Manifest.permission.REGISTER_SIM_SUBSCRIPTION;
-import static android.Manifest.permission.WRITE_SECURE_SETTINGS;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertThrows;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyBoolean;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Matchers.anyString;
-import static org.mockito.Matchers.argThat;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Matchers.isNull;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.isA;
-import static org.mockito.Mockito.when;
-
 @RunWith(JUnit4.class)
 public class TelecomServiceImplTest extends TelecomTestCase {
 
@@ -122,7 +130,7 @@
     public static class CallIntentProcessAdapterFake implements CallIntentProcessor.Adapter {
         @Override
         public void processOutgoingCallIntent(Context context, CallsManager callsManager,
-                Intent intent, String callingPackage) {
+                Intent intent, String callingPackage, FeatureFlags flags) {
 
         }
 
@@ -192,6 +200,10 @@
     @Mock private ICallEventCallback mICallEventCallback;
     @Mock private TransactionManager mTransactionManager;
     @Mock private AnomalyReporterAdapter mAnomalyReporterAdapter;
+    @Mock private FeatureFlags mFeatureFlags;
+    @Mock private com.android.internal.telephony.flags.FeatureFlags mTelephonyFeatureFlags;
+
+    @Mock private InCallController mInCallController;
 
     private final TelecomSystem.SyncRoot mLock = new TelecomSystem.SyncRoot() { };
 
@@ -219,6 +231,7 @@
 
         doReturn(mContext).when(mContext).getApplicationContext();
         doReturn(mContext).when(mContext).createContextAsUser(any(UserHandle.class), anyInt());
+        when(mFakeCallsManager.getInCallController()).thenReturn(mInCallController);
         doNothing().when(mContext).sendBroadcastAsUser(any(Intent.class), any(UserHandle.class),
                 anyString());
         when(mContext.checkCallingOrSelfPermission(Manifest.permission.INTERACT_ACROSS_USERS))
@@ -242,6 +255,8 @@
                 mDefaultDialerCache,
                 mSubscriptionManagerAdapter,
                 mSettingsSecureAdapter,
+                mFeatureFlags,
+                mTelephonyFeatureFlags,
                 mLock);
         telecomServiceImpl.setTransactionManager(mTransactionManager);
         telecomServiceImpl.setAnomalyReporterAdapter(mAnomalyReporterAdapter);
@@ -260,6 +275,8 @@
 
         mPackageManager = mContext.getPackageManager();
         when(mPackageManager.getPackageUid(anyString(), eq(0))).thenReturn(Binder.getCallingUid());
+        when(mFeatureFlags.earlyBindingToIncallService()).thenReturn(true);
+        when(mTelephonyFeatureFlags.workProfileApiSplit()).thenReturn(false);
     }
 
     @Override
@@ -525,10 +542,64 @@
 
         assertEquals(fullPHList,
                 mTSIBinder.getCallCapablePhoneAccounts(
-                        true, DEFAULT_DIALER_PACKAGE, null).getList());
+                        true, DEFAULT_DIALER_PACKAGE, null, false).getList());
         assertEquals(smallPHList,
                 mTSIBinder.getCallCapablePhoneAccounts(
-                        false, DEFAULT_DIALER_PACKAGE, null).getList());
+                        false, DEFAULT_DIALER_PACKAGE, null, false).getList());
+    }
+
+    @SmallTest
+    @Test
+    public void testGetCallCapablePhoneAccountsAcrossProfiles() throws RemoteException {
+        List<PhoneAccountHandle> fullPHList = List.of(TEL_PA_HANDLE_16, SIP_PA_HANDLE_17);
+        List<PhoneAccountHandle> smallPHList = List.of(SIP_PA_HANDLE_17);
+
+        // Returns all accounts when getCallCapablePhoneAccounts is called with across user.
+        doReturn(fullPHList).when(mFakePhoneAccountRegistrar).getCallCapablePhoneAccounts(
+                nullable(String.class), anyBoolean(), nullable(UserHandle.class), eq(true));
+        // Returns one account when getCallCapablePhoneAccounts is called without across user.
+        doReturn(smallPHList).when(mFakePhoneAccountRegistrar).getCallCapablePhoneAccounts(
+                nullable(String.class), anyBoolean(), nullable(UserHandle.class), eq(false));
+        // With across user permission
+        doReturn(PackageManager.PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(
+                eq(Manifest.permission.INTERACT_ACROSS_USERS));
+
+        assertEquals(fullPHList,
+                mTSIBinder.getCallCapablePhoneAccounts(
+                        true, DEFAULT_DIALER_PACKAGE, null, false).getList());
+
+        // Without across user permission
+        doReturn(PackageManager.PERMISSION_DENIED).when(mContext).checkCallingOrSelfPermission(
+                eq(Manifest.permission.INTERACT_ACROSS_USERS));
+
+        assertEquals(smallPHList,
+                mTSIBinder.getCallCapablePhoneAccounts(
+                        true, DEFAULT_DIALER_PACKAGE, null, false).getList());
+
+        // Enabled the feature flag of the work profile split mode
+        when(mTelephonyFeatureFlags.workProfileApiSplit()).thenReturn(true);
+
+        // With across user permission
+        doReturn(PackageManager.PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(
+                eq(Manifest.permission.INTERACT_ACROSS_PROFILES));
+
+        assertEquals(fullPHList,
+                mTSIBinder.getCallCapablePhoneAccounts(
+                        true, DEFAULT_DIALER_PACKAGE, null, true).getList());
+        assertEquals(smallPHList,
+                mTSIBinder.getCallCapablePhoneAccounts(
+                        true, DEFAULT_DIALER_PACKAGE, null, false).getList());
+
+        // Without across user permission
+        doReturn(PackageManager.PERMISSION_DENIED).when(mContext).checkCallingOrSelfPermission(
+                eq(Manifest.permission.INTERACT_ACROSS_PROFILES));
+
+        assertEquals(smallPHList,
+                mTSIBinder.getCallCapablePhoneAccounts(
+                        true, DEFAULT_DIALER_PACKAGE, null, true).getList());
+        assertEquals(smallPHList,
+                mTSIBinder.getCallCapablePhoneAccounts(
+                        true, DEFAULT_DIALER_PACKAGE, null, false).getList());
     }
 
     @SmallTest
@@ -540,7 +611,7 @@
                 argThat(new AnyStringIn(enforcedPermissions)), anyString());
 
         assertThrows(SecurityException.class,
-                () -> mTSIBinder.getCallCapablePhoneAccounts(true, "", null));
+                () -> mTSIBinder.getCallCapablePhoneAccounts(true, "", null, false));
     }
 
     @SmallTest
@@ -778,6 +849,62 @@
 
     @SmallTest
     @Test
+    public void testRegisterPhoneAccountSimultaneousCallingVerification() throws RemoteException {
+        doReturn(true).when(mTelephonyFeatureFlags).simultaneousCallingIndications();
+        doReturn(PackageManager.PERMISSION_GRANTED)
+                .when(mContext).checkCallingOrSelfPermission(MODIFY_PHONE_STATE);
+        String packageNameToUse = "com.android.officialpackage";
+        PhoneAccountHandle phHandle = new PhoneAccountHandle(new ComponentName(
+                packageNameToUse, "cs"), "test", Binder.getCallingUserHandle());
+        PhoneAccountHandle phAllowedRestriction = new PhoneAccountHandle(new ComponentName(
+                packageNameToUse, "cs"), "test2", Binder.getCallingUserHandle());
+
+        PhoneAccount phoneAccountEmptyRestriction = makePhoneAccount(phHandle)
+                .setSimultaneousCallingRestriction(Collections.emptySet())
+                .build();
+        try {
+            mTSIBinder.registerPhoneAccount(phoneAccountEmptyRestriction, CALLING_PACKAGE);
+            verify(mFakePhoneAccountRegistrar).registerPhoneAccount(phoneAccountEmptyRestriction);
+        } catch (SecurityException e) {
+            fail("registerPhoneAccount must not throw a SecurityException if there is a "
+                    + " restriction registered with the same package name.");
+        }
+
+        Set<PhoneAccountHandle> restriction = new HashSet<>(3);
+        restriction.add(phAllowedRestriction);
+        PhoneAccount phoneAccount = makePhoneAccount(phHandle)
+                .setSimultaneousCallingRestriction(restriction)
+                .build();
+
+        try {
+            mTSIBinder.registerPhoneAccount(phoneAccount, CALLING_PACKAGE);
+            verify(mFakePhoneAccountRegistrar).registerPhoneAccount(phoneAccount);
+        } catch (SecurityException e) {
+            fail("registerPhoneAccount must not throw a SecurityException if there is a "
+                    + " restriction registered with the same package name.");
+        }
+
+        String anotherPackageName = "com.android.anotherpackage";
+        PhoneAccountHandle phDisallowedRestriction = new PhoneAccountHandle(new ComponentName(
+                anotherPackageName, "cs"), "test", Binder.getCallingUserHandle());
+        restriction.add(phDisallowedRestriction);
+        phoneAccount = makePhoneAccount(phHandle)
+                .setSimultaneousCallingRestriction(restriction)
+                .build();
+
+        try {
+            mTSIBinder.registerPhoneAccount(phoneAccount, CALLING_PACKAGE);
+            // there should not be another call to registerPhoneAccount
+            verify(mFakePhoneAccountRegistrar, times(1)).registerPhoneAccount(phoneAccount);
+            fail("registerPhoneAccount must throw a SecurityException if there is a "
+                    + " restriction registered with a different package name.");
+        } catch (SecurityException e) {
+            //expected
+        }
+    }
+
+    @SmallTest
+    @Test
     public void testRegisterPhoneAccountWithoutPermissionAnomalyReported() throws RemoteException {
         PhoneAccountHandle handle = new PhoneAccountHandle(
                 new ComponentName("package", "cs"), "test", Binder.getCallingUserHandle());
@@ -1040,6 +1167,7 @@
 
         verify(mFakePhoneAccountRegistrar).getPhoneAccount(
                 TEL_PA_HANDLE_16, TEL_PA_HANDLE_16.getUserHandle());
+        verify(mInCallController, never()).bindToServices(any());
         addCallTestHelper(TelecomManager.ACTION_INCOMING_CALL,
                 CallIntentProcessor.KEY_IS_INCOMING_CALL, extras,
                 TEL_PA_HANDLE_16, false);
@@ -1047,6 +1175,81 @@
 
     @SmallTest
     @Test
+    public void testAddNewIncomingFlagDisabledNoEarlyBinding() throws Exception {
+        when(mFeatureFlags.earlyBindingToIncallService()).thenReturn(false);
+        PhoneAccount phoneAccount = makeSkipCallFilteringPhoneAccount(TEL_PA_HANDLE_16).build();
+        phoneAccount.setIsEnabled(true);
+        doReturn(phoneAccount).when(mFakePhoneAccountRegistrar).getPhoneAccount(
+                eq(TEL_PA_HANDLE_16), any(UserHandle.class));
+        doReturn(phoneAccount).when(mFakePhoneAccountRegistrar).getPhoneAccountUnchecked(
+                eq(TEL_PA_HANDLE_16));
+        doNothing().when(mAppOpsManager).checkPackage(anyInt(), anyString());
+        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(true);
+        Bundle extras = createSampleExtras();
+
+        mTSIBinder.addNewIncomingCall(TEL_PA_HANDLE_16, extras, CALLING_PACKAGE);
+
+        verify(mInCallController, never()).bindToServices(eq(null));
+    }
+
+    @SmallTest
+    @Test
+    public void testAddNewIncomingCallEarlyBindingForNoCallFilterCalls() throws Exception {
+        PhoneAccount phoneAccount = makeSkipCallFilteringPhoneAccount(TEL_PA_HANDLE_16).build();
+        phoneAccount.setIsEnabled(true);
+        doReturn(phoneAccount).when(mFakePhoneAccountRegistrar).getPhoneAccount(
+                eq(TEL_PA_HANDLE_16), any(UserHandle.class));
+        doReturn(phoneAccount).when(mFakePhoneAccountRegistrar).getPhoneAccountUnchecked(
+                eq(TEL_PA_HANDLE_16));
+        doNothing().when(mAppOpsManager).checkPackage(anyInt(), anyString());
+        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(true);
+        Bundle extras = createSampleExtras();
+
+        mTSIBinder.addNewIncomingCall(TEL_PA_HANDLE_16, extras, CALLING_PACKAGE);
+
+        verify(mInCallController).bindToServices(eq(null));
+    }
+
+    @SmallTest
+    @Test
+    public void testAddNewIncomingCallEarlyBindingNotEnableForNonWatchDevices() throws Exception {
+        PhoneAccount phoneAccount = makeSkipCallFilteringPhoneAccount(TEL_PA_HANDLE_16).build();
+        phoneAccount.setIsEnabled(true);
+        doReturn(phoneAccount).when(mFakePhoneAccountRegistrar).getPhoneAccount(
+                eq(TEL_PA_HANDLE_16), any(UserHandle.class));
+        doReturn(phoneAccount).when(mFakePhoneAccountRegistrar).getPhoneAccountUnchecked(
+                eq(TEL_PA_HANDLE_16));
+        doNothing().when(mAppOpsManager).checkPackage(anyInt(), anyString());
+        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(false);
+        Bundle extras = createSampleExtras();
+
+        mTSIBinder.addNewIncomingCall(TEL_PA_HANDLE_16, extras, CALLING_PACKAGE);
+
+        verify(mInCallController, never()).bindToServices(eq(null));
+    }
+
+    @SmallTest
+    @Test
+    public void testAddNewIncomingCallEarlyBindingNotEnableForPhoneAccountHasCallFilters()
+            throws Exception {
+        PhoneAccount phoneAccount = makePhoneAccount(TEL_PA_HANDLE_16).build();
+        phoneAccount.setIsEnabled(true);
+        doReturn(phoneAccount).when(mFakePhoneAccountRegistrar).getPhoneAccount(
+                eq(TEL_PA_HANDLE_16), any(UserHandle.class));
+        doReturn(phoneAccount).when(mFakePhoneAccountRegistrar).getPhoneAccountUnchecked(
+                eq(TEL_PA_HANDLE_16));
+        doNothing().when(mAppOpsManager).checkPackage(anyInt(), anyString());
+        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(true);
+        Bundle extras = createSampleExtras();
+
+        mTSIBinder.addNewIncomingCall(TEL_PA_HANDLE_16, extras, CALLING_PACKAGE);
+
+        verify(mInCallController, never()).bindToServices(eq(null));
+    }
+
+
+    @SmallTest
+    @Test
     public void testAddNewIncomingCallFailure() throws Exception {
         try {
             mTSIBinder.addNewIncomingCall(TEL_PA_HANDLE_16, null, CALLING_PACKAGE);
@@ -1703,6 +1906,28 @@
         verify(mContext, never()).sendBroadcastAsUser(any(Intent.class), any(UserHandle.class));
     }
 
+    /**
+     * FeatureFlags is autogenerated code, so there could be a situation where something changes
+     * outside of Telecom control that breaks reflection. This test attempts to ensure that changes
+     * to auto-generated FeatureFlags code that breaks reflection are caught early.
+     */
+    @SmallTest
+    @Test
+    public void testFlagConfigReflectionWorks() {
+        try {
+            Method[] methods = FeatureFlags.class.getMethods();
+            for (Method m : methods) {
+                // test getting the name and invoking the flag code
+                String name = m.getName();
+                Object val = m.invoke(mFeatureFlags);
+                assertNotNull(name);
+                assertNotNull(val);
+            }
+        } catch (Exception e) {
+            fail("Reflection failed for FeatureFlags with error: " + e);
+        }
+    }
+
     @SmallTest
     @Test
     public void testIsVoicemailNumber() throws Exception {
@@ -1867,6 +2092,23 @@
                 mTSIBinder.getLine1Number(TEL_PA_HANDLE_CURRENT, DEFAULT_DIALER_PACKAGE, null));
     }
 
+    /**
+     * Verify that when Telephony is not present that getLine1Number returns null as expected.
+     * @throws Exception
+     */
+    @SmallTest
+    @Test
+    public void testGetLine1NumberWithNoTelephony() throws Exception {
+        setupGetLine1NumberTest();
+        grantPermissionAndAppOp(READ_PHONE_NUMBERS, AppOpsManager.OPSTR_READ_PHONE_NUMBERS);
+        TelephonyManager mockTelephonyManager =
+                (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
+        when(mockTelephonyManager.getLine1Number()).thenThrow(
+                new UnsupportedOperationException("Bee-boop"));
+
+        assertNull(mTSIBinder.getLine1Number(TEL_PA_HANDLE_CURRENT, DEFAULT_DIALER_PACKAGE, null));
+    }
+
     private String setupGetLine1NumberTest() throws Exception {
         int subId = 58374;
         String line1Number = "9482752023479";
@@ -2144,6 +2386,12 @@
         return new PhoneAccount.Builder(paHandle, "testLabel");
     }
 
+    private PhoneAccount.Builder makeSkipCallFilteringPhoneAccount(PhoneAccountHandle paHandle) {
+        Bundle extras = new Bundle();
+        extras.putBoolean(PhoneAccount.EXTRA_SKIP_CALL_FILTERING, true);
+        return new PhoneAccount.Builder(paHandle, "testLabel").setExtras(extras);
+    }
+
     private Bundle createSampleExtras() {
         Bundle extras = new Bundle();
         extras.putString("test_key", "test_value");
diff --git a/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java b/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
index b962b2a..4463d65 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
@@ -22,11 +22,11 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyBoolean;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Matchers.anyString;
-import static org.mockito.Matchers.eq;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
@@ -38,6 +38,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.annotation.Nullable;
 import android.app.AppOpsManager;
 import android.bluetooth.BluetoothManager;
 import android.content.BroadcastReceiver;
@@ -69,6 +70,7 @@
 
 import com.android.internal.telecom.IInCallAdapter;
 import com.android.server.telecom.AsyncRingtonePlayer;
+import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
 import com.android.server.telecom.CallAudioManager;
 import com.android.server.telecom.CallAudioModeStateMachine;
 import com.android.server.telecom.CallAudioRouteStateMachine;
@@ -98,6 +100,7 @@
 import com.android.server.telecom.bluetooth.BluetoothRouteManager;
 import com.android.server.telecom.callfiltering.BlockedNumbersAdapter;
 import com.android.server.telecom.components.UserCallIntentProcessor;
+import com.android.server.telecom.flags.FeatureFlags;
 import com.android.server.telecom.ui.IncomingCallNotifier;
 
 import com.google.common.base.Predicate;
@@ -119,7 +122,7 @@
 /**
  * Implements mocks and functionality required to implement telecom system tests.
  */
-public class TelecomSystemTest extends TelecomTestCase {
+public class TelecomSystemTest extends TelecomTestCase{
 
     private static final String CALLING_PACKAGE = TelecomSystemTest.class.getPackageName();
     static final int TEST_POLL_INTERVAL = 10;  // milliseconds
@@ -167,7 +170,7 @@
         }
 
         @Override
-        public void showMissedCallNotification(CallInfo call) {
+        public void showMissedCallNotification(CallInfo call, @Nullable Uri uri) {
             missedCallsNotified.add(call);
         }
 
@@ -214,6 +217,12 @@
     @Mock Ringer.AccessibilityManagerAdapter mAccessibilityManagerAdapter;
     @Mock
     BlockedNumbersAdapter mBlockedNumbersAdapter;
+    @Mock
+    CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
+    @Mock
+    FeatureFlags mFeatureFlags;
+    @Mock
+    com.android.internal.telephony.flags.FeatureFlags mTelephonyFlags;
 
     final ComponentName mInCallServiceComponentNameX =
             new ComponentName(
@@ -320,6 +329,20 @@
                                     PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS)
                     .build();
 
+    final PhoneAccount mPhoneAccountMultiUser =
+            PhoneAccount.builder(
+                            new PhoneAccountHandle(
+                                    mConnectionServiceComponentNameA,
+                                    "id MU", UserHandle.of(12)),
+                            "Phone account service MU")
+                    .addSupportedUriScheme("tel")
+                    .setCapabilities(
+                            PhoneAccount.CAPABILITY_CALL_PROVIDER |
+                                    PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION |
+                                    PhoneAccount.CAPABILITY_VIDEO_CALLING |
+                                    PhoneAccount.CAPABILITY_MULTI_USER)
+                    .build();
+
     ConnectionServiceFixture mConnectionServiceFixtureA;
     ConnectionServiceFixture mConnectionServiceFixtureB;
     Timeouts.Adapter mTimeoutsAdapter;
@@ -493,9 +516,12 @@
         when(mRoleManagerAdapter.getCallCompanionApps()).thenReturn(Collections.emptyList());
         when(mRoleManagerAdapter.getDefaultCallScreeningApp(any(UserHandle.class)))
                 .thenReturn(null);
+        when(mRoleManagerAdapter.getBTInCallService()).thenReturn(new String[] {"bt_pkg"});
+        when(mFeatureFlags.useRefactoredAudioRouteSwitching()).thenReturn(false);
         mTelecomSystem = new TelecomSystem(
                 mComponentContextFixture.getTestDouble(),
-                (context, phoneAccountRegistrar, defaultDialerCache, mDeviceIdleControllerAdapter)
+                (context, phoneAccountRegistrar, defaultDialerCache, mDeviceIdleControllerAdapter,
+                        mFeatureFlag)
                         -> mMissedCallNotifier,
                 mCallerInfoAsyncQueryFactoryFixture.getTestDouble(),
                 headsetMediaButtonFactory,
@@ -518,7 +544,9 @@
                             StatusBarNotifier statusBarNotifier,
                             CallAudioManager.AudioServiceFactory audioServiceFactory,
                             int earpieceControl,
-                            Executor asyncTaskExecutor) {
+                            Executor asyncTaskExecutor,
+                            CallAudioCommunicationDeviceTracker communicationDeviceTracker,
+                            FeatureFlags featureFlags) {
                         return new CallAudioRouteStateMachine(context,
                                 callsManager,
                                 bluetoothManager,
@@ -528,15 +556,20 @@
                                 // Force enable an earpiece for the end-to-end tests
                                 CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED,
                                 mHandlerThread.getLooper(),
-                                Runnable::run /* async tasks as now sync for testing! */);
+                                Runnable::run /* async tasks as now sync for testing! */,
+                                communicationDeviceTracker,
+                                featureFlags);
                     }
                 },
                 new CallAudioModeStateMachine.Factory() {
                     @Override
                     public CallAudioModeStateMachine create(SystemStateHelper systemStateHelper,
-                            AudioManager am) {
+                            AudioManager am, FeatureFlags featureFlags,
+                            CallAudioCommunicationDeviceTracker callAudioCommunicationDeviceTracker
+                    ) {
                         return new CallAudioModeStateMachine(systemStateHelper, am,
-                                mHandlerThread.getLooper());
+                                mHandlerThread.getLooper(), featureFlags,
+                                callAudioCommunicationDeviceTracker);
                     }
                 },
                 mClockProxy,
@@ -549,7 +582,10 @@
                     }
                 }, mDeviceIdleControllerAdapter, mAccessibilityManagerAdapter,
                 Runnable::run,
-                mBlockedNumbersAdapter);
+                Runnable::run,
+                mBlockedNumbersAdapter,
+                mFeatureFlags,
+                mTelephonyFlags);
 
         mComponentContextFixture.setTelecomManager(new TelecomManager(
                 mComponentContextFixture.getTestDouble(),
@@ -583,6 +619,7 @@
         mTelecomSystem.getPhoneAccountRegistrar().registerPhoneAccount(mPhoneAccountB0);
         mTelecomSystem.getPhoneAccountRegistrar().registerPhoneAccount(mPhoneAccountE0);
         mTelecomSystem.getPhoneAccountRegistrar().registerPhoneAccount(mPhoneAccountE1);
+        mTelecomSystem.getPhoneAccountRegistrar().registerPhoneAccount(mPhoneAccountMultiUser);
 
         mTelecomSystem.getPhoneAccountRegistrar().setUserSelectedOutgoingPhoneAccount(
                 mPhoneAccountA0.getAccountHandle(), Process.myUserHandle());
@@ -767,7 +804,7 @@
 
         final UserHandle userHandle = initiatingUser;
         Context localAppContext = mComponentContextFixture.getTestDouble().getApplicationContext();
-        new UserCallIntentProcessor(localAppContext, userHandle).processIntent(
+        new UserCallIntentProcessor(localAppContext, userHandle, mFeatureFlags).processIntent(
                 actionCallIntent, null, false, true /* hasCallAppOp*/, false /* isLocal */);
         // Wait for handler to start CallerInfo lookup.
         waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT);
diff --git a/tests/src/com/android/server/telecom/tests/TelecomTestCase.java b/tests/src/com/android/server/telecom/tests/TelecomTestCase.java
index 5353bc6..e8389a0 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomTestCase.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomTestCase.java
@@ -22,6 +22,9 @@
 
 import androidx.test.InstrumentationRegistry;
 
+import com.android.server.telecom.flags.FeatureFlags;
+
+import org.mockito.Mock;
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
@@ -33,6 +36,8 @@
 public abstract class TelecomTestCase {
     protected static final String TESTING_TAG = "Telecom-TEST";
     protected Context mContext;
+    @Mock
+    FeatureFlags mFeatureFlags;
 
     MockitoHelper mMockitoHelper = new MockitoHelper();
     ComponentContextFixture mComponentContextFixture;
@@ -42,11 +47,12 @@
         Log.setIsExtendedLoggingEnabled(true);
         Log.setUnitTestingEnabled(true);
         mMockitoHelper.setUp(InstrumentationRegistry.getContext(), getClass());
-        mComponentContextFixture = new ComponentContextFixture();
+        MockitoAnnotations.initMocks(this);
+
+        mComponentContextFixture = new ComponentContextFixture(mFeatureFlags);
         mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
         Log.setSessionContext(mComponentContextFixture.getTestDouble().getApplicationContext());
         Log.getSessionManager().mCleanStaleSessions = null;
-        MockitoAnnotations.initMocks(this);
     }
 
     public void tearDown() throws Exception {
diff --git a/tests/src/com/android/server/telecom/tests/TelephonyUtilTest.java b/tests/src/com/android/server/telecom/tests/TelephonyUtilTest.java
new file mode 100644
index 0000000..207da71
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/TelephonyUtilTest.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2024 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 org.junit.Assert.assertFalse;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.when;
+
+import com.android.server.telecom.TelephonyUtil;
+
+import android.net.Uri;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class TelephonyUtilTest extends TelecomTestCase {
+    @Override
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+    }
+
+    @Override
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    /**
+     * Verifies that the helper method shouldProcessAsEmergency does not crash when telephony is not
+     * present and returns "false" instead.
+     */
+    @Test
+    public void testShouldProcessAsEmergencyWithNoTelephonyCalling() {
+        when(mComponentContextFixture.getTelephonyManager().isEmergencyNumber(anyString()))
+                .thenThrow(new UnsupportedOperationException("Bee boop"));
+        assertFalse(TelephonyUtil.shouldProcessAsEmergency(mContext, Uri.parse("tel:911")));
+    }
+}
diff --git a/tests/src/com/android/server/telecom/tests/TransactionTests.java b/tests/src/com/android/server/telecom/tests/TransactionTests.java
index 3fc87a9..5876474 100644
--- a/tests/src/com/android/server/telecom/tests/TransactionTests.java
+++ b/tests/src/com/android/server/telecom/tests/TransactionTests.java
@@ -16,8 +16,17 @@
 
 package com.android.server.telecom.tests;
 
+import static com.android.server.telecom.voip.VideoStateTranslation.TransactionalVideoStateToVideoProfileState;
+import static com.android.server.telecom.voip.VideoStateTranslation.VideoProfileStateToTransactionalVideoState;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.isA;
@@ -31,6 +40,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
+import android.content.res.Resources;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.OutcomeReceiver;
@@ -38,12 +48,18 @@
 import android.telecom.CallAttributes;
 import android.telecom.DisconnectCause;
 import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.telecom.VideoProfile;
+
+import androidx.test.filters.SmallTest;
 
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallState;
 import com.android.server.telecom.CallerInfoLookupHelper;
 import com.android.server.telecom.CallsManager;
 import com.android.server.telecom.ClockProxy;
+import com.android.server.telecom.ConnectionServiceWrapper;
+import com.android.server.telecom.flags.FeatureFlags;
 import com.android.server.telecom.PhoneNumberUtilsAdapter;
 import com.android.server.telecom.TelecomSystem;
 import com.android.server.telecom.ui.ToastFactory;
@@ -53,6 +69,10 @@
 import com.android.server.telecom.voip.OutgoingCallTransaction;
 import com.android.server.telecom.voip.MaybeHoldCallForNewCallTransaction;
 import com.android.server.telecom.voip.RequestNewActiveCallTransaction;
+import com.android.server.telecom.voip.TransactionManager;
+import com.android.server.telecom.voip.VerifyCallStateChangeTransaction;
+import com.android.server.telecom.voip.VideoStateTranslation;
+import com.android.server.telecom.voip.VoipCallTransactionResult;
 
 import org.junit.After;
 import org.junit.Before;
@@ -62,6 +82,11 @@
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
 
 public class TransactionTests extends TelecomTestCase {
 
@@ -90,6 +115,7 @@
         super.setUp();
         MockitoAnnotations.initMocks(this);
         Mockito.when(mMockCall1.getId()).thenReturn(CALL_ID_1);
+        Mockito.when(mMockContext.getResources()).thenReturn(Mockito.mock(Resources.class));
     }
 
     @Override
@@ -191,14 +217,14 @@
     public void testTransactionalHoldActiveCallForNewCall() throws Exception {
         // GIVEN
         MaybeHoldCallForNewCallTransaction transaction =
-                new MaybeHoldCallForNewCallTransaction(mCallsManager, mMockCall1);
+                new MaybeHoldCallForNewCallTransaction(mCallsManager, mMockCall1, false);
 
         // WHEN
         transaction.processTransaction(null);
 
         // THEN
         verify(mCallsManager, times(1))
-                .transactionHoldPotentialActiveCallForNewCall(eq(mMockCall1),
+                .transactionHoldPotentialActiveCallForNewCall(eq(mMockCall1), eq(false),
                         isA(OutcomeReceiver.class));
     }
 
@@ -209,7 +235,8 @@
                 CallAttributes.DIRECTION_OUTGOING, TEST_NAME, TEST_URI).build();
 
         OutgoingCallTransaction transaction =
-                new OutgoingCallTransaction(CALL_ID_1, mMockContext, callAttributes, mCallsManager);
+                new OutgoingCallTransaction(CALL_ID_1, mMockContext, callAttributes, mCallsManager,
+                        mFeatureFlags);
 
         // WHEN
         when(mMockContext.getOpPackageName()).thenReturn("testPackage");
@@ -236,7 +263,8 @@
                 CallAttributes.DIRECTION_INCOMING, TEST_NAME, TEST_URI).build();
 
         IncomingCallTransaction transaction =
-                new IncomingCallTransaction(CALL_ID_1, callAttributes, mCallsManager);
+                new IncomingCallTransaction(CALL_ID_1, callAttributes, mCallsManager,
+                        mFeatureFlags);
 
         // WHEN
         when(mCallsManager.isIncomingCallPermitted(callAttributes.getPhoneAccountHandle()))
@@ -250,6 +278,154 @@
                         isA(Boolean.class));
     }
 
+    /**
+     * Verify that transactional OUTGOING calls are re-mapping the CallAttributes video state to
+     * VideoProfile states when starting the call via CallsManager#startOugoingCall.
+     */
+    @Test
+    public void testOutgoingCallTransactionRemapsVideoState() {
+        // GIVEN
+        CallAttributes audioOnlyAttributes = new CallAttributes.Builder(mHandle,
+                CallAttributes.DIRECTION_OUTGOING, TEST_NAME, TEST_URI)
+                .setCallType(CallAttributes.AUDIO_CALL)
+                .build();
+
+        CallAttributes videoAttributes = new CallAttributes.Builder(mHandle,
+                CallAttributes.DIRECTION_OUTGOING, TEST_NAME, TEST_URI)
+                .setCallType(CallAttributes.VIDEO_CALL)
+                .build();
+
+        OutgoingCallTransaction t = new OutgoingCallTransaction(null,
+                mContext, null, mCallsManager, new Bundle(), mFeatureFlags);
+
+        // WHEN
+        when(mFeatureFlags.transactionalVideoState()).thenReturn(true);
+        t.setFeatureFlags(mFeatureFlags);
+
+        // THEN
+        assertEquals(VideoProfile.STATE_AUDIO_ONLY, t
+                .generateExtras(audioOnlyAttributes)
+                .getInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE));
+
+        assertEquals(VideoProfile.STATE_BIDIRECTIONAL, t
+                .generateExtras(videoAttributes)
+                .getInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE));
+    }
+
+    /**
+     * Verify that transactional INCOMING calls are re-mapping the CallAttributes video state to
+     * VideoProfile states when starting the call in CallsManager#processIncomingCallIntent.
+     */
+    @Test
+    public void testIncomingCallTransactionRemapsVideoState() {
+        // GIVEN
+        CallAttributes audioOnlyAttributes = new CallAttributes.Builder(mHandle,
+                CallAttributes.DIRECTION_INCOMING, TEST_NAME, TEST_URI)
+                .setCallType(CallAttributes.AUDIO_CALL)
+                .build();
+
+        CallAttributes videoAttributes = new CallAttributes.Builder(mHandle,
+                CallAttributes.DIRECTION_INCOMING, TEST_NAME, TEST_URI)
+                .setCallType(CallAttributes.VIDEO_CALL)
+                .build();
+
+        IncomingCallTransaction t = new IncomingCallTransaction(null, null,
+                mCallsManager, new Bundle(), mFeatureFlags);
+
+        // WHEN
+        when(mFeatureFlags.transactionalVideoState()).thenReturn(true);
+        t.setFeatureFlags(mFeatureFlags);
+
+        // THEN
+        assertEquals(VideoProfile.STATE_AUDIO_ONLY, t
+                .generateExtras(audioOnlyAttributes)
+                .getInt(TelecomManager.EXTRA_INCOMING_VIDEO_STATE));
+
+        assertEquals(VideoProfile.STATE_BIDIRECTIONAL, t
+                .generateExtras(videoAttributes)
+                .getInt(TelecomManager.EXTRA_INCOMING_VIDEO_STATE));
+    }
+
+    @Test
+    public void testTransactionalVideoStateToVideoProfileState() {
+        assertEquals(VideoProfile.STATE_AUDIO_ONLY,
+                TransactionalVideoStateToVideoProfileState(CallAttributes.AUDIO_CALL));
+        assertEquals(VideoProfile.STATE_BIDIRECTIONAL,
+                TransactionalVideoStateToVideoProfileState(CallAttributes.VIDEO_CALL));
+        // ensure non-defined values default to audio
+        assertEquals(VideoProfile.STATE_AUDIO_ONLY,
+                TransactionalVideoStateToVideoProfileState(-1));
+    }
+
+    @Test
+    public void testVideoProfileStateToTransactionalVideoState() {
+        assertEquals(CallAttributes.AUDIO_CALL,
+                VideoProfileStateToTransactionalVideoState(VideoProfile.STATE_AUDIO_ONLY));
+        assertEquals(CallAttributes.VIDEO_CALL,
+                VideoProfileStateToTransactionalVideoState(VideoProfile.STATE_RX_ENABLED));
+        assertEquals(CallAttributes.VIDEO_CALL,
+                VideoProfileStateToTransactionalVideoState(VideoProfile.STATE_TX_ENABLED));
+        assertEquals(CallAttributes.VIDEO_CALL,
+                VideoProfileStateToTransactionalVideoState(VideoProfile.STATE_BIDIRECTIONAL));
+        // ensure non-defined values default to audio
+        assertEquals(CallAttributes.AUDIO_CALL,
+                VideoProfileStateToTransactionalVideoState(-1));
+    }
+
+    /**
+     * This test verifies if the ConnectionService call is NOT transitioned to the desired call
+     * state (within timeout period), Telecom will disconnect the call.
+     */
+    @SmallTest
+    @Test
+    public void testCallStateChangeTimesOut() {
+        when(mFeatureFlags.transactionalCsVerifier()).thenReturn(true);
+        VerifyCallStateChangeTransaction t = new VerifyCallStateChangeTransaction(
+                mLock, mMockCall1, CallState.ON_HOLD);
+        TransactionManager.TransactionCompleteListener listener =
+                mock(TransactionManager.TransactionCompleteListener.class);
+        t.setCompleteListener(listener);
+        // WHEN
+        setupHoldableCall();
+
+        // simulate the transaction being processed and the CompletableFuture timing out
+        t.processTransaction(null);
+        t.timeout();
+
+        // THEN
+        verify(mMockCall1, times(1)).addCallStateListener(t.getCallStateListenerImpl());
+        verify(listener).onTransactionTimeout(anyString());
+        verify(mMockCall1, atLeastOnce()).removeCallStateListener(any());
+    }
+
+    /**
+     * This test verifies that when an application transitions a call to the requested state,
+     * Telecom does not disconnect the call and transaction completes successfully.
+     */
+    @SmallTest
+    @Test
+    public void testCallStateIsSuccessfullyChanged()
+            throws ExecutionException, InterruptedException, TimeoutException {
+        when(mFeatureFlags.transactionalCsVerifier()).thenReturn(true);
+        VerifyCallStateChangeTransaction t = new VerifyCallStateChangeTransaction(
+                mLock, mMockCall1, CallState.ON_HOLD);
+        // WHEN
+        setupHoldableCall();
+
+        // simulate the transaction being processed and the setOnHold() being called / state change
+        t.processTransaction(null);
+        doReturn(CallState.ON_HOLD).when(mMockCall1).getState();
+        t.getCallStateListenerImpl().onCallStateChanged(CallState.ON_HOLD);
+        t.finish(null);
+
+
+        // THEN
+        verify(mMockCall1, times(1)).addCallStateListener(t.getCallStateListenerImpl());
+        assertEquals(VoipCallTransactionResult.RESULT_SUCCEED,
+                t.getTransactionResult().get(2, TimeUnit.SECONDS).getResult());
+        verify(mMockCall1, atLeastOnce()).removeCallStateListener(any());
+    }
+
     private Call createSpyCall(PhoneAccountHandle targetPhoneAccount, int initialState, String id) {
         when(mCallsManager.getCallerInfoLookupHelper()).thenReturn(mCallerInfoLookupHelper);
 
@@ -267,7 +443,8 @@
                 false /* shouldAttachToExistingConnection*/,
                 false /* isConference */,
                 mClockProxy,
-                mToastFactory);
+                mToastFactory,
+                mFeatureFlags);
 
         Call callSpy = Mockito.spy(call);
 
@@ -280,4 +457,12 @@
 
         return callSpy;
     }
+
+    private void setupHoldableCall(){
+        when(mMockCall1.getState()).thenReturn(CallState.ACTIVE);
+        when(mMockCall1.getConnectionServiceWrapper()).thenReturn(
+                mock(ConnectionServiceWrapper.class));
+        doNothing().when(mMockCall1).addCallStateListener(any());
+        doReturn(true).when(mMockCall1).removeCallStateListener(any());
+    }
 }
\ No newline at end of file
diff --git a/tests/src/com/android/server/telecom/tests/VideoCallTests.java b/tests/src/com/android/server/telecom/tests/VideoCallTests.java
index 84beedc..0ce70af 100644
--- a/tests/src/com/android/server/telecom/tests/VideoCallTests.java
+++ b/tests/src/com/android/server/telecom/tests/VideoCallTests.java
@@ -16,6 +16,26 @@
 
 package com.android.server.telecom.tests;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.verify;
+
+import android.os.Process;
+import android.os.RemoteException;
+import android.telecom.CallAudioState;
+import android.telecom.DisconnectCause;
+import android.telecom.VideoProfile;
+
+import androidx.test.filters.LargeTest;
+import androidx.test.filters.MediumTest;
+
+import com.android.server.telecom.CallAudioModeStateMachine;
+import com.android.server.telecom.CallAudioRouteAdapter;
+import com.android.server.telecom.CallAudioRouteStateMachine;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -23,26 +43,8 @@
 import org.junit.runners.JUnit4;
 import org.mockito.ArgumentCaptor;
 
-import android.os.Process;
-import android.os.RemoteException;
-import android.telecom.CallAudioState;
-import android.telecom.DisconnectCause;
-import android.telecom.VideoProfile;
-import android.test.suitebuilder.annotation.LargeTest;
-import android.test.suitebuilder.annotation.MediumTest;
-
-import com.android.server.telecom.CallAudioModeStateMachine;
-import com.android.server.telecom.CallAudioRouteStateMachine;
-
 import java.util.List;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-import static org.mockito.Mockito.atLeastOnce;
-import static org.mockito.Mockito.verify;
-
 /**
  * System tests for video-specific behavior in telecom.
  * TODO: Add unit tests which ensure that auto-speakerphone does not occur when using a wired
@@ -258,13 +260,13 @@
      */
     private void verifyAudioRoute(int expectedRoute) throws Exception {
         // Capture all onCallAudioStateChanged callbacks to InCall.
-        CallAudioRouteStateMachine carsm = mTelecomSystem.getCallsManager()
-                .getCallAudioManager().getCallAudioRouteStateMachine();
+        CallAudioRouteAdapter cara = mTelecomSystem.getCallsManager()
+                .getCallAudioManager().getCallAudioRouteAdapter();
         CallAudioModeStateMachine camsm = mTelecomSystem.getCallsManager()
                 .getCallAudioManager().getCallAudioModeStateMachine();
         waitForHandlerAction(camsm.getHandler(), TEST_TIMEOUT);
         final boolean[] success = {true};
-        carsm.sendMessage(CallAudioRouteStateMachine.RUN_RUNNABLE, (Runnable) () -> {
+        cara.sendMessage(CallAudioRouteStateMachine.RUN_RUNNABLE, (Runnable) () -> {
             ArgumentCaptor<CallAudioState> callAudioStateArgumentCaptor = ArgumentCaptor.forClass(
                     CallAudioState.class);
             try {
@@ -277,7 +279,7 @@
             assertEquals(expectedRoute, changes.get(changes.size() - 1).getRoute());
             success[0] = true;
         });
-        waitForHandlerAction(carsm.getHandler(), TEST_TIMEOUT);
+        waitForHandlerAction(cara.getAdapterHandler(), TEST_TIMEOUT);
         assertTrue(success[0]);
     }
 }
diff --git a/tests/src/com/android/server/telecom/tests/VideoProfileTest.java b/tests/src/com/android/server/telecom/tests/VideoProfileTest.java
index 5ee0414..b2a1c81 100644
--- a/tests/src/com/android/server/telecom/tests/VideoProfileTest.java
+++ b/tests/src/com/android/server/telecom/tests/VideoProfileTest.java
@@ -21,7 +21,8 @@
 import static org.junit.Assert.assertTrue;
 
 import android.telecom.VideoProfile;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.filters.SmallTest;
 
 import org.junit.After;
 import org.junit.Before;
diff --git a/tests/src/com/android/server/telecom/tests/VideoProviderProxyTest.java b/tests/src/com/android/server/telecom/tests/VideoProviderProxyTest.java
index 2b6c260..060e3ae 100644
--- a/tests/src/com/android/server/telecom/tests/VideoProviderProxyTest.java
+++ b/tests/src/com/android/server/telecom/tests/VideoProviderProxyTest.java
@@ -26,7 +26,8 @@
 
 import android.os.IBinder;
 import android.telecom.VideoProfile;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.filters.SmallTest;
 
 import com.android.internal.telecom.IVideoProvider;
 import com.android.server.telecom.Analytics;
diff --git a/tests/src/com/android/server/telecom/tests/VideoProviderTest.java b/tests/src/com/android/server/telecom/tests/VideoProviderTest.java
index 597924d..56fbf72 100644
--- a/tests/src/com/android/server/telecom/tests/VideoProviderTest.java
+++ b/tests/src/com/android/server/telecom/tests/VideoProviderTest.java
@@ -16,6 +16,41 @@
 
 package com.android.server.telecom.tests;
 
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.graphics.SurfaceTexture;
+import android.net.Uri;
+import android.os.Build;
+import android.os.UserHandle;
+import android.telecom.Connection;
+import android.telecom.Connection.VideoProvider;
+import android.telecom.InCallService;
+import android.telecom.InCallService.VideoCall;
+import android.telecom.VideoCallImpl;
+import android.telecom.VideoProfile;
+import android.telecom.VideoProfile.CameraCapabilities;
+import android.view.Surface;
+
+import androidx.test.filters.MediumTest;
+
+import com.android.server.telecom.CallsManager;
+
+import com.google.common.base.Predicate;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -26,48 +61,10 @@
 import org.mockito.invocation.InvocationOnMock;
 import org.mockito.stubbing.Answer;
 
-import android.app.AppOpsManager;
-import android.content.Context;
-import android.graphics.SurfaceTexture;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.UserHandle;
-import android.telecom.Call;
-import android.telecom.Connection;
-import android.telecom.Connection.VideoProvider;
-import android.telecom.DisconnectCause;
-import android.telecom.InCallService;
-import android.telecom.InCallService.VideoCall;
-import android.telecom.VideoCallImpl;
-import android.telecom.VideoProfile;
-import android.telecom.VideoProfile.CameraCapabilities;
-import android.test.suitebuilder.annotation.MediumTest;
-import android.view.Surface;
-
-import com.google.common.base.Predicate;
-
 import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
-import static org.junit.Assert.assertEquals;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Matchers.anyLong;
-import static org.mockito.Matchers.anyString;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.timeout;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-
-import com.android.server.telecom.CallsManager;
-
 /**
  * Performs tests of the {@link VideoProvider} and {@link VideoCall} APIs.  Ensures that requests
  * sent from an InCallService are routed through Telecom to a VideoProvider, and that callbacks are
diff --git a/tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java b/tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java
index c66b0f7..7f7399c 100644
--- a/tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java
+++ b/tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java
@@ -16,6 +16,11 @@
 
 package com.android.server.telecom.tests;
 
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL;
+
 import static org.junit.Assert.assertEquals;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
@@ -38,7 +43,8 @@
 import android.os.UserHandle;
 import android.service.notification.StatusBarNotification;
 import android.telecom.PhoneAccountHandle;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.filters.SmallTest;
 
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallState;
@@ -86,6 +92,31 @@
                 .thenReturn(true);
     }
 
+    /**
+     * This test ensures VoipCallMonitor is passing the correct foregroundServiceTypes when starting
+     * foreground service delegation on behalf of a client.
+     */
+    @SmallTest
+    @Test
+    public void testVerifyForegroundServiceTypesBeingPassedToActivityManager() {
+        Call call = createTestCall("testCall", mHandle1User1);
+        ArgumentCaptor<ForegroundServiceDelegationOptions> optionsCaptor =
+                ArgumentCaptor.forClass(ForegroundServiceDelegationOptions.class);
+
+        mMonitor.onCallAdded(call);
+
+        verify(mActivityManagerInternal, timeout(TIMEOUT)).startForegroundServiceDelegate(
+                 optionsCaptor.capture(), any(ServiceConnection.class));
+
+        assertEquals( FOREGROUND_SERVICE_TYPE_PHONE_CALL |
+                FOREGROUND_SERVICE_TYPE_MICROPHONE |
+                FOREGROUND_SERVICE_TYPE_CAMERA |
+                FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE,
+                optionsCaptor.getValue().mForegroundServiceTypes);
+
+        mMonitor.onCallRemoved(call);
+    }
+
     @SmallTest
     @Test
     public void testStartMonitorForOneCall() {
diff --git a/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java b/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java
index 0a7e27d..c5be130 100644
--- a/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java
+++ b/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java
@@ -21,7 +21,6 @@
 
 import android.os.OutcomeReceiver;
 import android.telecom.CallException;
-import android.util.Log;
 
 import androidx.test.filters.SmallTest;
 
@@ -56,10 +55,12 @@
         public static final int SUCCESS = 0;
         public static final int FAILED = 1;
         public static final int TIMEOUT = 2;
+        public static final int EXCEPTION = 3;
 
         private long mSleepTime;
         private String mName;
         private int mType;
+        public boolean isFinished = false;
 
         public TestVoipCallTransaction(String name, long sleepTime, int type) {
             super(VoipCallTransactionTest.this.mLock);
@@ -70,6 +71,10 @@
 
         @Override
         public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+            if (mType == EXCEPTION) {
+                mLog.append(mName).append(" exception;\n");
+                throw new IllegalStateException("TEST EXCEPTION");
+            }
             CompletableFuture<VoipCallTransactionResult> resultFuture = new CompletableFuture<>();
             mHandler.postDelayed(() -> {
                 if (mType == SUCCESS) {
@@ -80,17 +85,22 @@
                 } else if (mType == FAILED) {
                     mLog.append(mName).append(" failed;\n");
                     resultFuture.complete(
-                            new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_FAILED,
+                            new VoipCallTransactionResult(CallException.CODE_ERROR_UNKNOWN,
                                     null));
                 } else {
                     mLog.append(mName).append(" timeout;\n");
                     resultFuture.complete(
-                            new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_FAILED,
+                            new VoipCallTransactionResult(CallException.CODE_ERROR_UNKNOWN,
                                     "timeout"));
                 }
             }, mSleepTime);
             return resultFuture;
         }
+
+        @Override
+        public void finishTransaction() {
+            isFinished = true;
+        }
     }
 
     @Override
@@ -104,7 +114,6 @@
     @Override
     @After
     public void tearDown() throws Exception {
-        Log.i("Grace", mLog.toString());
         mTransactionManager.clear();
         super.tearDown();
     }
@@ -114,11 +123,11 @@
     public void testSerialTransactionSuccess()
             throws ExecutionException, InterruptedException, TimeoutException {
         List<VoipCallTransaction> subTransactions = new ArrayList<>();
-        VoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
+        TestVoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
                 TestVoipCallTransaction.SUCCESS);
-        VoipCallTransaction t2 = new TestVoipCallTransaction("t2", 1000L,
+        TestVoipCallTransaction t2 = new TestVoipCallTransaction("t2", 1000L,
                 TestVoipCallTransaction.SUCCESS);
-        VoipCallTransaction t3 = new TestVoipCallTransaction("t3", 1000L,
+        TestVoipCallTransaction t3 = new TestVoipCallTransaction("t3", 1000L,
                 TestVoipCallTransaction.SUCCESS);
         subTransactions.add(t1);
         subTransactions.add(t2);
@@ -132,6 +141,7 @@
         assertEquals(VoipCallTransactionResult.RESULT_SUCCEED,
                 resultFuture.get(5000L, TimeUnit.MILLISECONDS).getResult());
         assertEquals(expectedLog, mLog.toString());
+        verifyTransactionsFinished(t1, t2, t3);
     }
 
     @SmallTest
@@ -139,11 +149,11 @@
     public void testSerialTransactionFailed()
             throws ExecutionException, InterruptedException, TimeoutException {
         List<VoipCallTransaction> subTransactions = new ArrayList<>();
-        VoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
+        TestVoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
                 TestVoipCallTransaction.SUCCESS);
-        VoipCallTransaction t2 = new TestVoipCallTransaction("t2", 1000L,
+        TestVoipCallTransaction t2 = new TestVoipCallTransaction("t2", 1000L,
                 TestVoipCallTransaction.FAILED);
-        VoipCallTransaction t3 = new TestVoipCallTransaction("t3", 1000L,
+        TestVoipCallTransaction t3 = new TestVoipCallTransaction("t3", 1000L,
                 TestVoipCallTransaction.SUCCESS);
         subTransactions.add(t1);
         subTransactions.add(t2);
@@ -166,6 +176,7 @@
         exceptionFuture.get(5000L, TimeUnit.MILLISECONDS);
         String expectedLog = "t1 success;\nt2 failed;\n";
         assertEquals(expectedLog, mLog.toString());
+        verifyTransactionsFinished(t1, t2, t3);
     }
 
     @SmallTest
@@ -173,11 +184,11 @@
     public void testParallelTransactionSuccess()
             throws ExecutionException, InterruptedException, TimeoutException {
         List<VoipCallTransaction> subTransactions = new ArrayList<>();
-        VoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
+        TestVoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
                 TestVoipCallTransaction.SUCCESS);
-        VoipCallTransaction t2 = new TestVoipCallTransaction("t2", 500L,
+        TestVoipCallTransaction t2 = new TestVoipCallTransaction("t2", 500L,
                 TestVoipCallTransaction.SUCCESS);
-        VoipCallTransaction t3 = new TestVoipCallTransaction("t3", 200L,
+        TestVoipCallTransaction t3 = new TestVoipCallTransaction("t3", 200L,
                 TestVoipCallTransaction.SUCCESS);
         subTransactions.add(t1);
         subTransactions.add(t2);
@@ -193,6 +204,7 @@
         assertTrue(log.contains("t1 success;\n"));
         assertTrue(log.contains("t2 success;\n"));
         assertTrue(log.contains("t3 success;\n"));
+        verifyTransactionsFinished(t1, t2, t3);
     }
 
     @SmallTest
@@ -200,11 +212,11 @@
     public void testParallelTransactionFailed()
             throws ExecutionException, InterruptedException, TimeoutException {
         List<VoipCallTransaction> subTransactions = new ArrayList<>();
-        VoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
+        TestVoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
                 TestVoipCallTransaction.SUCCESS);
-        VoipCallTransaction t2 = new TestVoipCallTransaction("t2", 500L,
+        TestVoipCallTransaction t2 = new TestVoipCallTransaction("t2", 500L,
                 TestVoipCallTransaction.FAILED);
-        VoipCallTransaction t3 = new TestVoipCallTransaction("t3", 200L,
+        TestVoipCallTransaction t3 = new TestVoipCallTransaction("t3", 200L,
                 TestVoipCallTransaction.SUCCESS);
         subTransactions.add(t1);
         subTransactions.add(t2);
@@ -226,13 +238,14 @@
                 outcomeReceiver);
         exceptionFuture.get(5000L, TimeUnit.MILLISECONDS);
         assertTrue(mLog.toString().contains("t2 failed;\n"));
+        verifyTransactionsFinished(t1, t2, t3);
     }
 
     @SmallTest
     @Test
     public void testTransactionTimeout()
             throws ExecutionException, InterruptedException, TimeoutException {
-        VoipCallTransaction t = new TestVoipCallTransaction("t", 10000L,
+        TestVoipCallTransaction t = new TestVoipCallTransaction("t", 10000L,
                 TestVoipCallTransaction.SUCCESS);
         CompletableFuture<String> exceptionFuture = new CompletableFuture<>();
         OutcomeReceiver<VoipCallTransactionResult, CallException> outcomeReceiver =
@@ -246,8 +259,133 @@
                     public void onError(CallException e) {
                         exceptionFuture.complete(e.getMessage());
                     }
-                };        mTransactionManager.addTransaction(t, outcomeReceiver);
+                };
+        mTransactionManager.addTransaction(t, outcomeReceiver);
         String message = exceptionFuture.get(7000L, TimeUnit.MILLISECONDS);
         assertTrue(message.contains("timeout"));
+        verifyTransactionsFinished(t);
+    }
+
+    @SmallTest
+    @Test
+    public void testTransactionException()
+            throws ExecutionException, InterruptedException, TimeoutException {
+        TestVoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
+                TestVoipCallTransaction.EXCEPTION);
+        TestVoipCallTransaction t2 = new TestVoipCallTransaction("t2", 1000L,
+                TestVoipCallTransaction.SUCCESS);
+        CompletableFuture<String> exceptionFuture = new CompletableFuture<>();
+        OutcomeReceiver<VoipCallTransactionResult, CallException> outcomeExceptionReceiver =
+                new OutcomeReceiver<>() {
+                    @Override
+                    public void onResult(VoipCallTransactionResult result) {
+                    }
+
+                    @Override
+                    public void onError(CallException e) {
+                        exceptionFuture.complete(e.getMessage());
+                    }
+                };
+        mTransactionManager.addTransaction(t1, outcomeExceptionReceiver);
+        // Transaction will timeout because the Exception caused the transaction to stop processing.
+        exceptionFuture.get(7000L, TimeUnit.MILLISECONDS);
+        assertTrue(mLog.toString().contains("t1 exception;\n"));
+        // Verify an exception in a processing a previous transaction does not stall the next one.
+        CompletableFuture<VoipCallTransactionResult> resultFuture = new CompletableFuture<>();
+        OutcomeReceiver<VoipCallTransactionResult, CallException> outcomeReceiver =
+                resultFuture::complete;
+        mTransactionManager.addTransaction(t2, outcomeReceiver);
+        String expectedLog = "t1 exception;\nt2 success;\n";
+        assertEquals(VoipCallTransactionResult.RESULT_SUCCEED,
+                resultFuture.get(5000L, TimeUnit.MILLISECONDS).getResult());
+        assertEquals(expectedLog, mLog.toString());
+        verifyTransactionsFinished(t1, t2);
+    }
+
+    /**
+     * This test verifies that if a transaction encounters an exception while processing it,
+     * the exception finishes the transaction immediately instead of waiting for the timeout.
+     */
+    @SmallTest
+    @Test
+    public void testTransactionHitsException()
+            throws ExecutionException, InterruptedException, TimeoutException {
+        // GIVEN - a transaction that throws an exception when processing
+        TestVoipCallTransaction t1 = new TestVoipCallTransaction(
+                "t1",
+                100L,
+                TestVoipCallTransaction.EXCEPTION);
+        // verify the TransactionManager informs the client of the failed transaction
+        CompletableFuture<String> exceptionFuture = new CompletableFuture<>();
+        OutcomeReceiver<VoipCallTransactionResult, CallException> outcomeExceptionReceiver =
+                new OutcomeReceiver<>() {
+                    @Override
+                    public void onResult(VoipCallTransactionResult result) {
+                    }
+
+                    @Override
+                    public void onError(CallException e) {
+                        exceptionFuture.complete(e.getMessage());
+                    }
+                };
+        // WHEN - add and process the transaction
+        mTransactionManager.addTransaction(t1, outcomeExceptionReceiver);
+        exceptionFuture.get(200L, TimeUnit.MILLISECONDS);
+        // THEN - assert the transaction finished and failed
+        assertTrue(mLog.toString().contains("t1 exception;\n"));
+        verifyTransactionsFinished(t1);
+    }
+
+    @SmallTest
+    @Test
+    public void testTransactionResultException()
+            throws ExecutionException, InterruptedException, TimeoutException {
+        TestVoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
+                TestVoipCallTransaction.SUCCESS);
+        TestVoipCallTransaction t2 = new TestVoipCallTransaction("t2", 1000L,
+                TestVoipCallTransaction.SUCCESS);
+        TestVoipCallTransaction t3 = new TestVoipCallTransaction("t3", 1000L,
+                TestVoipCallTransaction.SUCCESS);
+        OutcomeReceiver<VoipCallTransactionResult, CallException> outcomeExceptionReceiver =
+                new OutcomeReceiver<>() {
+                    @Override
+                    public void onResult(VoipCallTransactionResult result) {
+                        throw new IllegalStateException("RESULT EXCEPTION");
+                    }
+
+                    @Override
+                    public void onError(CallException e) {
+                    }
+                };
+        mTransactionManager.addTransaction(t1, outcomeExceptionReceiver);
+        OutcomeReceiver<VoipCallTransactionResult, CallException> outcomeException2Receiver =
+                new OutcomeReceiver<>() {
+                    @Override
+                    public void onResult(VoipCallTransactionResult result) {
+                    }
+
+                    @Override
+                    public void onError(CallException e) {
+                        throw new IllegalStateException("RESULT EXCEPTION");
+                    }
+                };
+        mTransactionManager.addTransaction(t2, outcomeException2Receiver);
+        // Verify an exception in a previous transaction result does not stall the next one.
+        CompletableFuture<VoipCallTransactionResult> resultFuture = new CompletableFuture<>();
+        OutcomeReceiver<VoipCallTransactionResult, CallException> outcomeReceiver =
+                resultFuture::complete;
+        mTransactionManager.addTransaction(t3, outcomeReceiver);
+        String expectedLog = "t1 success;\nt2 success;\nt3 success;\n";
+        assertEquals(VoipCallTransactionResult.RESULT_SUCCEED,
+                resultFuture.get(5000L, TimeUnit.MILLISECONDS).getResult());
+        assertEquals(expectedLog, mLog.toString());
+        verifyTransactionsFinished(t1, t2, t3);
+    }
+
+    public void verifyTransactionsFinished(TestVoipCallTransaction... transactions) {
+        for (TestVoipCallTransaction t : transactions) {
+            assertTrue("TestVoipCallTransaction[" + t.mName + "] never called finishTransaction",
+                    t.isFinished);
+        }
     }
 }