Merge Android 14 QPR3 to AOSP main

Bug: 346855327
Merged-In: I215273a9286cd8725c9e46bbd28aee06e4d53eff
Change-Id: Ia762d6b83027f8f63b75580265d5f7c6bd0f0820
diff --git a/Android.bp b/Android.bp
index 7a20f08..68a8e39 100644
--- a/Android.bp
+++ b/Android.bp
@@ -11,19 +11,12 @@
     out: ["com/android/server/telecom/TelecomStatsLog.java"],
 }
 
-filegroup {
-    name: "Telecom-srcs",
+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: [
@@ -31,9 +24,6 @@
         "androidx.core_core",
         "telecom_flags_core_java_lib",
     ],
-    libs: [
-        "services",
-    ],
     resource_dirs: ["res"],
     proto: {
         type: "nano",
@@ -41,6 +31,22 @@
         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: {
@@ -51,6 +57,7 @@
 android_test {
     name: "TelecomUnitTests",
     static_libs: [
+        "TelecomLib",
         "android-ex-camera2",
         "flag-junit",
         "guava",
@@ -59,25 +66,15 @@
         "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",
-        "telecom_flags_core_java_lib",
     ],
     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",
@@ -90,11 +87,6 @@
         "libstaticjvmtiagent",
     ],
 
-    aaptflags: [
-        "--auto-add-overlay",
-        "--extra-packages",
-        "com.android.server.telecom",
-    ],
     manifest: "tests/AndroidManifest.xml",
     optimize: {
         enabled: false,
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index c6f5e9c..a9b6154 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -64,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"
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/flags/Android.bp b/flags/Android.bp
index 807344e..b089796 100644
--- a/flags/Android.bp
+++ b/flags/Android.bp
@@ -23,22 +23,22 @@
     package: "com.android.server.telecom.flags",
     container: "system",
     srcs: [
-      "telecom_broadcast_flags.aconfig",
-      "telecom_ringer_flag_declarations.aconfig",
-      "telecom_api_flags.aconfig",
-      "telecom_call_filtering_flags.aconfig",
-      "telecom_incallservice_flags.aconfig",
-      "telecom_default_phone_account_flags.aconfig",
-      "telecom_callaudioroutestatemachine_flags.aconfig",
-      "telecom_call_flags.aconfig",
-      "telecom_calls_manager_flags.aconfig",
-      "telecom_anomaly_report_flags.aconfig",
-      "telecom_callaudiomodestatemachine_flags.aconfig",
-      "telecom_calllog_flags.aconfig",
-      "telecom_resolve_hidden_dependencies.aconfig",
-      "telecom_bluetoothroutemanager_flags.aconfig",
-      "telecom_work_profile_flags.aconfig",
-      "telecom_connection_service_wrapper_flags.aconfig",
+        "telecom_broadcast_flags.aconfig",
+        "telecom_ringer_flag_declarations.aconfig",
+        "telecom_api_flags.aconfig",
+        "telecom_call_filtering_flags.aconfig",
+        "telecom_incallservice_flags.aconfig",
+        "telecom_default_phone_account_flags.aconfig",
+        "telecom_callaudioroutestatemachine_flags.aconfig",
+        "telecom_call_flags.aconfig",
+        "telecom_calls_manager_flags.aconfig",
+        "telecom_anomaly_report_flags.aconfig",
+        "telecom_callaudiomodestatemachine_flags.aconfig",
+        "telecom_calllog_flags.aconfig",
+        "telecom_resolve_hidden_dependencies.aconfig",
+        "telecom_bluetoothroutemanager_flags.aconfig",
+        "telecom_work_profile_flags.aconfig",
+        "telecom_connection_service_wrapper_flags.aconfig",
+        "telecom_profile_user_flags.aconfig",
     ],
 }
-
diff --git a/flags/telecom_api_flags.aconfig b/flags/telecom_api_flags.aconfig
index a211c7c..c0f4cba 100644
--- a/flags/telecom_api_flags.aconfig
+++ b/flags/telecom_api_flags.aconfig
@@ -15,16 +15,45 @@
   bug: "301713560"
 }
 
-flag {
-  name: "unbind_timeout_connections"
-  namespace: "telecom"
-  description: "When set, Telecom will auto-unbind if a ConnectionService returns no connections after some time."
-  bug: "293458004"
-}
-
 flag{
   name: "add_call_uri_for_missed_calls"
   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"
 }
+
+flag{
+  name: "set_mute_state"
+  namespace: "telecom"
+  description: "transactional calls need the ability to mute the call audio input"
+  bug: "310669304"
+}
+
+flag{
+  name: "get_registered_phone_accounts"
+  namespace: "telecom"
+  description: "When set, self-managed clients can get their own phone accounts"
+  bug: "317132586"
+}
+
+flag{
+  name: "transactional_video_state"
+  namespace: "telecom"
+  description: "when set, clients using transactional implementations will be able to set & get the video state"
+  bug: "311265260"
+}
+
+flag{
+  name: "business_call_composer"
+  namespace: "telecom"
+  description: "Enables enriched calling features (e.g. Business name will show for a call)"
+  bug: "311688497"
+  is_exported: true
+}
+
+flag{
+  name: "get_last_known_cell_identity"
+  namespace: "telecom"
+  description: "Formalizes the getLastKnownCellIdentity API that Telecom reliees on as a system api"
+  bug: "327454165"
+}
diff --git a/flags/telecom_callaudioroutestatemachine_flags.aconfig b/flags/telecom_callaudioroutestatemachine_flags.aconfig
index fadaa13..f5da045 100644
--- a/flags/telecom_callaudioroutestatemachine_flags.aconfig
+++ b/flags/telecom_callaudioroutestatemachine_flags.aconfig
@@ -63,3 +63,10 @@
   description: "Update supported route mask when Bluetooth devices audio connected."
   bug: "301695370"
 }
+
+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"
+}
diff --git a/flags/telecom_calls_manager_flags.aconfig b/flags/telecom_calls_manager_flags.aconfig
index cdfcc30..de17eee 100644
--- a/flags/telecom_calls_manager_flags.aconfig
+++ b/flags/telecom_calls_manager_flags.aconfig
@@ -14,3 +14,10 @@
   description: "This fix ensures the MO calls won't switch from Active to Quite b/c setDialing was not called"
   bug: "309540769"
 }
+
+flag {
+  name: "enable_call_sequencing"
+  namespace: "telecom"
+  description: "Enables simultaneous call sequencing for SIM PhoneAccounts"
+  bug: "297446980"
+}
diff --git a/flags/telecom_incallservice_flags.aconfig b/flags/telecom_incallservice_flags.aconfig
index efb1df6..08a82ba 100644
--- a/flags/telecom_incallservice_flags.aconfig
+++ b/flags/telecom_incallservice_flags.aconfig
@@ -13,4 +13,11 @@
   namespace: "telecom"
   description: "Ensure that users are able to return to call from keyguard UI for ECC"
   bug: "306582821"
-}
\ No newline at end of file
+}
+
+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_profile_user_flags.aconfig b/flags/telecom_profile_user_flags.aconfig
new file mode 100644
index 0000000..c046de8
--- /dev/null
+++ b/flags/telecom_profile_user_flags.aconfig
@@ -0,0 +1,12 @@
+package: "com.android.server.telecom.flags"
+container: "system"
+
+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_resolve_hidden_dependencies.aconfig b/flags/telecom_resolve_hidden_dependencies.aconfig
index cde952e..674a968 100644
--- a/flags/telecom_resolve_hidden_dependencies.aconfig
+++ b/flags/telecom_resolve_hidden_dependencies.aconfig
@@ -3,7 +3,7 @@
 
 flag {
     name: "telecom_resolve_hidden_dependencies"
-    namespace: "android_platform_telecom"
+    namespace: "telecom"
     description: "Mainland cleanup for hidden dependencies"
-    bug: "b/303440370"
+    bug: "323414215"
 }
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-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-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-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/config.xml b/res/values/config.xml
index c38a6ec..bf30720 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -79,4 +79,7 @@
     <!-- 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>
+
+    <!-- System bluetooth stack package name -->
+    <string name="system_bluetooth_stack">com.android.bluetooth</string>
 </resources>
diff --git a/src/com/android/server/telecom/Analytics.java b/src/com/android/server/telecom/Analytics.java
index bbcf858..45e3340 100644
--- a/src/com/android/server/telecom/Analytics.java
+++ b/src/com/android/server/telecom/Analytics.java
@@ -721,7 +721,7 @@
 
     private static int getCarrierId(Context context) {
         SubscriptionManager subscriptionManager =
-                context.getSystemService(SubscriptionManager.class);
+                context.getSystemService(SubscriptionManager.class).createForAllUserProfiles();
         List<SubscriptionInfo> subInfos = subscriptionManager.getActiveSubscriptionInfoList();
         if (subInfos == null) {
             return -1;
diff --git a/src/com/android/server/telecom/AsyncRingtonePlayer.java b/src/com/android/server/telecom/AsyncRingtonePlayer.java
index 912305b..3b5e342 100644
--- a/src/com/android/server/telecom/AsyncRingtonePlayer.java
+++ b/src/com/android/server/telecom/AsyncRingtonePlayer.java
@@ -26,6 +26,8 @@
 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;
@@ -81,16 +83,17 @@
      * 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,  boolean isHfpDeviceConnected) {
+    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);
@@ -209,8 +212,10 @@
      * 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();
@@ -226,6 +231,7 @@
                 return;
             }
             Ringtone ringtone = null;
+            Uri ringtoneUri = null;
             boolean hasStopped = false;
             try {
                 try {
@@ -236,7 +242,11 @@
                 } catch (InterruptedException e) {
                     Log.w(this, "handlePlay: latch exception: " + e);
                 }
-                ringtone = ringtoneSupplier.get();
+                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)) {
@@ -253,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()) {
@@ -265,7 +274,7 @@
                 Log.i(this, "Play ringtone, looping.");
             } finally {
                 removePendingRingingReadyLatch(ringingReadyLatch);
-                ringtoneConsumer.accept(ringtone, hasStopped);
+                ringtoneConsumer.accept(new Pair(ringtoneUri, ringtone), hasStopped);
             }
         } finally {
             Log.cancelSubsession(session);
diff --git a/src/com/android/server/telecom/AudioRoute.java b/src/com/android/server/telecom/AudioRoute.java
new file mode 100644
index 0000000..cdf44a8
--- /dev/null
+++ b/src/com/android/server/telecom/AudioRoute.java
@@ -0,0 +1,285 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.telecom;
+
+import static com.android.server.telecom.CallAudioRouteAdapter.BT_AUDIO_CONNECTED;
+import static com.android.server.telecom.CallAudioRouteAdapter.BT_AUDIO_DISCONNECTED;
+import static com.android.server.telecom.CallAudioRouteAdapter.PENDING_ROUTE_FAILED;
+import static com.android.server.telecom.CallAudioRouteAdapter.SPEAKER_OFF;
+import static com.android.server.telecom.CallAudioRouteAdapter.SPEAKER_ON;
+
+import android.annotation.IntDef;
+import android.media.AudioDeviceInfo;
+import android.media.AudioManager;
+import android.telecom.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+public class AudioRoute {
+    public static class Factory {
+        private final ScheduledExecutorService mScheduledExecutorService =
+                new ScheduledThreadPoolExecutor(1);
+        private final CompletableFuture<AudioRoute> mAudioRouteFuture = new CompletableFuture<>();
+        public AudioRoute create(@AudioRouteType int type, String bluetoothAddress,
+                                 AudioManager audioManager) throws RuntimeException {
+            createRetry(type, bluetoothAddress, audioManager, MAX_CONNECTION_RETRIES);
+            try {
+                return mAudioRouteFuture.get();
+            } catch (InterruptedException | ExecutionException e) {
+                throw new RuntimeException("Error when creating requested audio route");
+            }
+        }
+        private void createRetry(@AudioRouteType int type, String bluetoothAddress,
+                                       AudioManager audioManager, int retryCount) {
+            if (retryCount == 0) {
+                mAudioRouteFuture.complete(null);
+            }
+
+            Log.i(this, "creating AudioRoute with type %s and address %s, retry count %d",
+                    DEVICE_TYPE_STRINGS.get(type), bluetoothAddress, retryCount);
+            AudioDeviceInfo routeInfo = null;
+            List<AudioDeviceInfo> infos = audioManager.getAvailableCommunicationDevices();
+            List<Integer> possibleInfoTypes = AUDIO_ROUTE_TYPE_TO_DEVICE_INFO_TYPE.get(type);
+            for (AudioDeviceInfo info : infos) {
+                Log.i(this, "type: " + info.getType());
+                if (possibleInfoTypes != null && possibleInfoTypes.contains(info.getType())) {
+                    if (BT_AUDIO_ROUTE_TYPES.contains(type)) {
+                        if (bluetoothAddress.equals(info.getAddress())) {
+                            routeInfo = info;
+                            break;
+                        }
+                    } else {
+                        routeInfo = info;
+                        break;
+                    }
+                }
+            }
+            if (routeInfo == null) {
+                mScheduledExecutorService.schedule(
+                        () -> createRetry(type, bluetoothAddress, audioManager, retryCount - 1),
+                        RETRY_TIME_DELAY, TimeUnit.MILLISECONDS);
+            } else {
+                mAudioRouteFuture.complete(new AudioRoute(type, bluetoothAddress, routeInfo));
+            }
+        }
+    }
+
+    private static final long RETRY_TIME_DELAY = 500L;
+    private static final int MAX_CONNECTION_RETRIES = 2;
+    public static final int TYPE_INVALID = 0;
+    public static final int TYPE_EARPIECE = 1;
+    public static final int TYPE_WIRED = 2;
+    public static final int TYPE_SPEAKER = 3;
+    public static final int TYPE_DOCK = 4;
+    public static final int TYPE_BLUETOOTH_SCO = 5;
+    public static final int TYPE_BLUETOOTH_HA = 6;
+    public static final int TYPE_BLUETOOTH_LE = 7;
+    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);
+    }
+
+    int getType() {
+        return mAudioRouteType;
+    }
+
+    String getBluetoothAddress() {
+        return mBluetoothAddress;
+    }
+
+    // Invoked when entered pending route whose dest route is this route
+    void onDestRouteAsPendingRoute(boolean active, PendingAudioRoute pendingAudioRoute,
+                                   AudioManager audioManager) {
+        if (pendingAudioRoute.isActive() && !active) {
+            Log.i(this, "clearCommunicationDevice");
+            audioManager.clearCommunicationDevice();
+        } else if (active) {
+            if (mAudioRouteType == TYPE_BLUETOOTH_SCO) {
+                pendingAudioRoute.addMessage(BT_AUDIO_CONNECTED);
+            } else if (mAudioRouteType == TYPE_SPEAKER) {
+                pendingAudioRoute.addMessage(SPEAKER_ON);
+            }
+            if (!audioManager.setCommunicationDevice(mInfo)) {
+                pendingAudioRoute.onMessageReceived(PENDING_ROUTE_FAILED);
+            }
+        }
+    }
+
+    void onOrigRouteAsPendingRoute(boolean active, PendingAudioRoute pendingAudioRoute,
+                                   AudioManager audioManager) {
+        if (active) {
+            if (mAudioRouteType == TYPE_BLUETOOTH_SCO) {
+                pendingAudioRoute.addMessage(BT_AUDIO_DISCONNECTED);
+            } else if (mAudioRouteType == TYPE_SPEAKER) {
+                pendingAudioRoute.addMessage(SPEAKER_OFF);
+            }
+        }
+    }
+
+    @VisibleForTesting
+    public AudioRoute(@AudioRouteType int type, String bluetoothAddress, AudioDeviceInfo info) {
+        mAudioRouteType = type;
+        mBluetoothAddress = bluetoothAddress;
+        mInfo = info;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (!(obj instanceof AudioRoute otherRoute)) {
+            return false;
+        }
+        if (mAudioRouteType != otherRoute.getType()) {
+            return false;
+        }
+        return !BT_AUDIO_ROUTE_TYPES.contains(mAudioRouteType) || mBluetoothAddress.equals(
+                otherRoute.getBluetoothAddress());
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mAudioRouteType, mBluetoothAddress);
+    }
+
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() + "[Type=" + DEVICE_TYPE_STRINGS.get(mAudioRouteType)
+                + ", Address=" + ((mBluetoothAddress != null) ? mBluetoothAddress : "invalid")
+                + "]";
+    }
+}
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index 1ec113c..f7ad93f 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -19,6 +19,8 @@
 import static android.provider.CallLog.Calls.MISSED_REASON_NOT_MISSED;
 import static android.telephony.TelephonyManager.EVENT_DISPLAY_EMERGENCY_MESSAGE;
 
+import static com.android.server.telecom.voip.VideoStateTranslation.VideoProfileStateToTransactionalVideoState;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
@@ -27,13 +29,11 @@
 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;
@@ -58,7 +58,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;
@@ -74,7 +73,6 @@
 import com.android.internal.telecom.IVideoProvider;
 import com.android.internal.util.Preconditions;
 import com.android.server.telecom.flags.FeatureFlags;
-import com.android.server.telecom.flags.Flags;
 import com.android.server.telecom.stats.CallFailureCause;
 import com.android.server.telecom.stats.CallStateChangedAtomWriter;
 import com.android.server.telecom.ui.ToastFactory;
@@ -96,6 +94,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;
 
@@ -655,6 +654,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.
@@ -797,6 +826,13 @@
      */
     private CompletableFuture<Boolean> mDisconnectFuture;
 
+    /**
+     * {@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;
+
     private FeatureFlags mFlags;
 
     /**
@@ -3172,8 +3208,7 @@
             } 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());
             }
         }
     }
@@ -3427,62 +3462,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());
@@ -3824,7 +3813,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) {
@@ -4065,6 +4054,15 @@
             videoState = VideoProfile.STATE_AUDIO_ONLY;
         }
 
+        // Transactional calls have the ability to change video calling capabilities on a per-call
+        // basis as opposed to ConnectionService calls which are only based on the PhoneAccount.
+        if (mFlags.transactionalVideoState()
+                && mIsTransactionalCall && !mTransactionalCallSupportsVideoCalling) {
+            Log.i(this, "setVideoState: The transactional does NOT support video calling."
+                    + " defaulted to audio (video not supported)");
+            videoState = VideoProfile.STATE_AUDIO_ONLY;
+        }
+
         // 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:
@@ -4087,6 +4085,12 @@
             }
         }
 
+        if (mFlags.transactionalVideoState()
+                && mIsTransactionalCall && mTransactionalService != null) {
+            int transactionalVS = VideoProfileStateToTransactionalVideoState(mVideoState);
+            mTransactionalService.onVideoStateChanged(this, transactionalVS);
+        }
+
         if (VideoProfile.isVideo(videoState)) {
             mAnalytics.setCallIsVideo(true);
         }
@@ -4740,6 +4744,30 @@
     }
 
     /**
+     * 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;
+    }
+
+    /**
+     * 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 {
+                mBtIcsFuture.get();
+            } catch (InterruptedException | ExecutionException e) {
+                // ignore
+            }
+        }
+    }
+
+    /**
      * @return {@code true} if the connection has been created by the underlying
      * {@link ConnectionService}, {@code false} otherwise.
      */
diff --git a/src/com/android/server/telecom/CallAudioCommunicationDeviceTracker.java b/src/com/android/server/telecom/CallAudioCommunicationDeviceTracker.java
index 5fc2414..3a05eb5 100644
--- a/src/com/android/server/telecom/CallAudioCommunicationDeviceTracker.java
+++ b/src/com/android/server/telecom/CallAudioCommunicationDeviceTracker.java
@@ -16,6 +16,8 @@
 
 package com.android.server.telecom;
 
+import static com.android.server.telecom.AudioRoute.BT_AUDIO_DEVICE_INFO_TYPES;
+
 import android.bluetooth.BluetoothDevice;
 import android.content.Context;
 import android.media.AudioDeviceInfo;
@@ -28,7 +30,6 @@
 
 import java.util.Arrays;
 import java.util.List;
-import java.util.Set;
 import java.util.concurrent.Semaphore;
 
 /**
@@ -41,12 +42,6 @@
 
     // Use -1 indicates device is not set for any communication use case
     private static final int sAUDIO_DEVICE_TYPE_INVALID = -1;
-    // Possible bluetooth audio device types
-    private static final Set<Integer> sBT_AUDIO_DEVICE_TYPES = Set.of(
-            AudioDeviceInfo.TYPE_BLE_HEADSET,
-            AudioDeviceInfo.TYPE_HEARING_AID,
-            AudioDeviceInfo.TYPE_BLUETOOTH_SCO
-    );
     private AudioManager mAudioManager;
     private BluetoothRouteManager mBluetoothRouteManager;
     private int mAudioDeviceType = sAUDIO_DEVICE_TYPE_INVALID;
@@ -101,7 +96,7 @@
             mLock.tryAcquire();
         }
         // There is only one audio device type associated with each type of BT device.
-        boolean isBtDevice = sBT_AUDIO_DEVICE_TYPES.contains(audioDeviceType);
+        boolean isBtDevice = BT_AUDIO_DEVICE_INFO_TYPES.contains(audioDeviceType);
         Log.i(this, "setCommunicationDevice: type = %s, isBtDevice = %s, btDevice = %s",
                 audioDeviceType, isBtDevice, btDevice);
 
@@ -182,7 +177,7 @@
             mLock.tryAcquire();
         }
         // There is only one audio device type associated with each type of BT device.
-        boolean isBtDevice = sBT_AUDIO_DEVICE_TYPES.contains(audioDeviceType);
+        boolean isBtDevice = BT_AUDIO_DEVICE_INFO_TYPES.contains(audioDeviceType);
         Log.i(this, "clearCommunicationDevice: type = %s, isBtDevice = %s",
                 audioDeviceType, isBtDevice);
 
diff --git a/src/com/android/server/telecom/CallAudioManager.java b/src/com/android/server/telecom/CallAudioManager.java
index 96bf2c6..e5678a0 100644
--- a/src/com/android/server/telecom/CallAudioManager.java
+++ b/src/com/android/server/telecom/CallAudioManager.java
@@ -750,6 +750,10 @@
 
     private void onCallEnteringRinging() {
         if (mRingingCalls.size() == 1) {
+            // Wait until the BT ICS binding completed to request further audio route change
+            for (Call ringingCall: mRingingCalls) {
+                ringingCall.waitForBtIcs();
+            }
             mCallAudioModeStateMachine.sendMessageWithArgs(
                     CallAudioModeStateMachine.NEW_RINGING_CALL,
                     makeArgsForModeStateMachine());
diff --git a/src/com/android/server/telecom/CallAudioModeStateMachine.java b/src/com/android/server/telecom/CallAudioModeStateMachine.java
index 71956a1..6420f2e 100644
--- a/src/com/android/server/telecom/CallAudioModeStateMachine.java
+++ b/src/com/android/server/telecom/CallAudioModeStateMachine.java
@@ -280,7 +280,10 @@
             mLocalLog.log("Enter UNFOCUSED");
             if (mIsInitialized) {
                 // Clear any communication device that was requested previously.
-                if (mFeatureFlags.callAudioCommunicationDeviceRefactor()) {
+                // Todo: Remove once clearCommunicationDeviceAfterAudioOpsComplete is
+                // completely rolled out.
+                if (mFeatureFlags.callAudioCommunicationDeviceRefactor()
+                        && !mFeatureFlags.clearCommunicationDeviceAfterAudioOpsComplete()) {
                     mCommunicationDeviceTracker.clearCommunicationDevice(mCommunicationDeviceTracker
                             .getCurrentLocallyRequestedCommunicationDevice());
                 }
@@ -353,6 +356,12 @@
                     } 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.
diff --git a/src/com/android/server/telecom/CallAudioRouteAdapter.java b/src/com/android/server/telecom/CallAudioRouteAdapter.java
index 7f7b43c..5585d09 100644
--- a/src/com/android/server/telecom/CallAudioRouteAdapter.java
+++ b/src/com/android/server/telecom/CallAudioRouteAdapter.java
@@ -1,15 +1,134 @@
 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();
diff --git a/src/com/android/server/telecom/CallAudioRouteController.java b/src/com/android/server/telecom/CallAudioRouteController.java
index f8c49bb..d38577d 100644
--- a/src/com/android/server/telecom/CallAudioRouteController.java
+++ b/src/com/android/server/telecom/CallAudioRouteController.java
@@ -1,45 +1,392 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
 package com.android.server.telecom;
 
+import static com.android.server.telecom.AudioRoute.BT_AUDIO_ROUTE_TYPES;
+import static com.android.server.telecom.AudioRoute.TYPE_INVALID;
+
+import android.app.ActivityManager;
+import android.bluetooth.BluetoothDevice;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.UserInfo;
+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.Binder;
 import android.os.Handler;
 import android.os.HandlerThread;
-import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.UserHandle;
 import android.telecom.CallAudioState;
+import android.telecom.Log;
+import android.telecom.Logging.Session;
+import android.util.ArrayMap;
 
+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 java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
 
 public class CallAudioRouteController implements CallAudioRouteAdapter {
-    private Handler mHandler;
+    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);
+    }
 
-    public CallAudioRouteController() {
+    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 Map<Integer, AudioRoute> mTypeRoutes;
+    private PendingAudioRoute mPendingAudioRoute;
+    private AudioRoute.Factory mAudioRouteFactory;
+    private int mFocusType;
+    private final Object mLock = new Object();
+    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)) {
+                            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) {
+        mContext = context;
+        mCallsManager = callsManager;
+        mAudioManager = context.getSystemService(AudioManager.class);
+        mAudioServiceFactory = audioServiceFactory;
+        mAudioRouteFactory = audioRouteFactory;
+        mWiredHeadsetManager = wiredHeadsetManager;
+        mIsMute = false;
+        mBluetoothRouteManager = bluetoothRouteManager;
+        mFocusType = NO_FOCUS;
         HandlerThread handlerThread = new HandlerThread(this.getClass().getSimpleName());
         handlerThread.start();
-        mHandler = new Handler(handlerThread.getLooper());
+
+        // 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 USER_SWITCH_BASELINE_ROUTE:
+                            handleSwitchBaselineRoute();
+                            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.isMasterMute());
+                            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 ArrayMap<>();
+        mTypeRoutes = new ArrayMap<>();
+        mStreamingRoutes = new HashSet<>();
+        mPendingAudioRoute = new PendingAudioRoute(this, mAudioManager);
+        mStreamingRoute = new AudioRoute(AudioRoute.TYPE_STREAMING, null, null);
+        mStreamingRoutes.add(mStreamingRoute);
+
+        int supportMask = calculateSupportedRouteMask();
+        if ((supportMask & CallAudioState.ROUTE_SPEAKER) != 0) {
+            // Create spekaer routes
+            mSpeakerDockRoute = mAudioRouteFactory.create(AudioRoute.TYPE_SPEAKER, null,
+                    mAudioManager);
+            if (mSpeakerDockRoute == null) {
+                Log.w(this, "Can't find available audio device info for route TYPE_SPEAKER");
+            } else {
+                mTypeRoutes.put(AudioRoute.TYPE_SPEAKER, mSpeakerDockRoute);
+                mAvailableRoutes.add(mSpeakerDockRoute);
+            }
+        }
+
+        if ((supportMask & CallAudioState.ROUTE_WIRED_HEADSET) != 0) {
+            // Create wired headset routes
+            mEarpieceWiredRoute = mAudioRouteFactory.create(AudioRoute.TYPE_WIRED, null,
+                    mAudioManager);
+            if (mEarpieceWiredRoute == null) {
+                Log.w(this, "Can't find available audio device info for route TYPE_WIRED_HEADSET");
+            } else {
+                mTypeRoutes.put(AudioRoute.TYPE_WIRED, mEarpieceWiredRoute);
+                mAvailableRoutes.add(mEarpieceWiredRoute);
+            }
+        } else if ((supportMask & CallAudioState.ROUTE_EARPIECE) != 0) {
+            // Create earpiece routes
+            mEarpieceWiredRoute = mAudioRouteFactory.create(AudioRoute.TYPE_EARPIECE, null,
+                    mAudioManager);
+            if (mEarpieceWiredRoute == null) {
+                Log.w(this, "Can't find available audio device info for route TYPE_EARPIECE");
+            } else {
+                mTypeRoutes.put(AudioRoute.TYPE_EARPIECE, mEarpieceWiredRoute);
+                mAvailableRoutes.add(mEarpieceWiredRoute);
+            }
+        }
+
+        // set current route
+        if (mEarpieceWiredRoute != null) {
+            mCurrentRoute = mEarpieceWiredRoute;
+        } else {
+            mCurrentRoute = mSpeakerDockRoute;
+        }
+        mIsActive = false;
+        mCallAudioState = new CallAudioState(mIsMute, ROUTE_MAP.get(mCurrentRoute.getType()),
+                supportMask, null, new HashSet<>());
     }
 
     @Override
     public void sendMessageWithSessionInfo(int message) {
+        sendMessageWithSessionInfo(message, 0, (String) null);
     }
 
     @Override
     public void sendMessageWithSessionInfo(int message, int arg) {
-
+        sendMessageWithSessionInfo(message, arg, (String) null);
     }
 
     @Override
     public void sendMessageWithSessionInfo(int message, int arg, String data) {
+        SomeArgs args = SomeArgs.obtain();
+        args.arg1 = Log.createSubsession();
+        args.arg2 = data;
+        sendMessage(message, arg, 0, args);
+    }
 
+    @Override
+    public void sendMessageWithSessionInfo(int message, int arg, BluetoothDevice bluetoothDevice) {
+        SomeArgs args = SomeArgs.obtain();
+        args.arg1 = Log.createSubsession();
+        args.arg2 = bluetoothDevice;
+        sendMessage(message, arg, 0, args);
     }
 
     @Override
     public void sendMessage(int message, Runnable r) {
+        r.run();
+    }
 
+    private void sendMessage(int what, int arg1, int arg2, Object obj) {
+        mHandler.sendMessage(Message.obtain(mHandler, what, arg1, arg2, obj));
     }
 
     @Override
     public void setCallAudioManager(CallAudioManager callAudioManager) {
+        mCallAudioManager = callAudioManager;
     }
 
     @Override
@@ -49,7 +396,7 @@
 
     @Override
     public boolean isHfpDeviceAvailable() {
-        return false;
+        return !mBluetoothRoutes.isEmpty();
     }
 
     @Override
@@ -59,6 +406,559 @@
 
     @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)) {
+            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);
+            // override pending route while keep waiting for still pending messages for the
+            // previous pending route
+            mIsActive = active;
+            mPendingAudioRoute.setOrigRoute(mIsActive, mPendingAudioRoute.getDestRoute());
+            mPendingAudioRoute.setDestRoute(active, destRoute);
+        } else {
+            if (mCurrentRoute.equals(destRoute) && (mIsActive == active)) {
+                return;
+            }
+            Log.i(this, "Enter pending route, orig%s(active=%b), dest%s(active=%b)", mCurrentRoute,
+                    mIsActive, destRoute, active);
+            // route to pending route
+            if (getAvailableRoutes().contains(mCurrentRoute)) {
+                mPendingAudioRoute.setOrigRoute(mIsActive, mCurrentRoute);
+            } else {
+                // Avoid waiting for pending messages for an unavailable route
+                mPendingAudioRoute.setOrigRoute(mIsActive, DUMMY_ROUTE);
+            }
+            mPendingAudioRoute.setDestRoute(active, destRoute);
+            mIsActive = active;
+            mIsPending = true;
+        }
+        mPendingAudioRoute.evaluatePendingState();
+        postTimeoutMessage();
+    }
+
+    private void postTimeoutMessage() {
+        // reset timeout handler
+        mHandler.removeMessages(PENDING_ROUTE_TIMEOUT);
+        mHandler.postDelayed(() -> mHandler.sendMessage(
+                Message.obtain(mHandler, PENDING_ROUTE_TIMEOUT)), TIMEOUT_LIMIT);
+    }
+
+    private void 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));
+        }
+    }
+
+    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));
+        }
+    }
+
+    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));
+        } else {
+            Log.i(this, "ignore disable streaming, not in streaming");
+        }
+    }
+
+    private void handleBtAudioActive(BluetoothDevice bluetoothDevice) {
+        if (mIsPending) {
+            if (Objects.equals(mPendingAudioRoute.getDestRoute().getBluetoothAddress(),
+                    bluetoothDevice.getAddress())) {
+                mPendingAudioRoute.onMessageReceived(BT_AUDIO_CONNECTED);
+            }
+        } else {
+            // ignore, not triggered by telecom
+        }
+    }
+
+    private void handleBtAudioInactive(BluetoothDevice bluetoothDevice) {
+        if (mIsPending) {
+            if (Objects.equals(mPendingAudioRoute.getOrigRoute().getBluetoothAddress(),
+                    bluetoothDevice.getAddress())) {
+                mPendingAudioRoute.onMessageReceived(BT_AUDIO_DISCONNECTED);
+            }
+        } else {
+            // ignore, not triggered by telecom
+        }
+    }
+
+    private void handleBtConnected(@AudioRoute.AudioRouteType int type,
+                                   BluetoothDevice bluetoothDevice) {
+        AudioRoute bluetoothRoute = null;
+        bluetoothRoute = mAudioRouteFactory.create(type, bluetoothDevice.getAddress(),
+                mAudioManager);
+        if (bluetoothRoute == null) {
+            Log.w(this, "Can't find available audio device info for route type:"
+                    + AudioRoute.DEVICE_TYPE_STRINGS.get(type));
+        } else {
+            Log.i(this, "bluetooth route added: " + bluetoothRoute);
+            mAvailableRoutes.add(bluetoothRoute);
+            mBluetoothRoutes.put(bluetoothRoute, bluetoothDevice);
+            onAvailableRoutesChanged();
+        }
+    }
+
+    private void handleBtDisconnected(@AudioRoute.AudioRouteType int type,
+                                      BluetoothDevice bluetoothDevice) {
+        // Clean up unavailable routes
+        AudioRoute bluetoothRoute = getBluetoothRoute(type, bluetoothDevice.getAddress());
+        if (bluetoothRoute != null) {
+            Log.i(this, "bluetooth route removed: " + bluetoothRoute);
+            mBluetoothRoutes.remove(bluetoothRoute);
+            mAvailableRoutes.remove(bluetoothRoute);
+            onAvailableRoutesChanged();
+        }
+
+        // Fallback to an available route
+        if (Objects.equals(mCurrentRoute, bluetoothRoute)) {
+            routeTo(mIsActive, getBaseRoute(false));
+        }
+    }
+
+    private void handleBtActiveDevicePresent(@AudioRoute.AudioRouteType int type,
+                                             String deviceAddress) {
+        AudioRoute bluetoothRoute = getBluetoothRoute(type, deviceAddress);
+        if (bluetoothRoute != null) {
+            Log.i(this, "request to route to bluetooth route: %s(active=%b)", bluetoothRoute,
+                    mIsActive);
+            routeTo(mIsActive, bluetoothRoute);
+        }
+    }
+
+    private void handleBtActiveDeviceGone(@AudioRoute.AudioRouteType int type) {
+        if ((mIsPending && mPendingAudioRoute.getDestRoute().getType() == type)
+                || (!mIsPending && mCurrentRoute.getType() == type)) {
+            // Fallback to an available route
+            routeTo(mIsActive, getBaseRoute(true));
+        }
+    }
+
+    private void handleMuteChanged(boolean mute) {
+        mIsMute = mute;
+        if (mIsMute != mAudioManager.isMasterMute() && 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) {
+        mFocusType = focus;
+        switch (focus) {
+            case NO_FOCUS -> {
+                if (mIsActive) {
+                    handleMuteChanged(false);
+                    routeTo(false, mCurrentRoute);
+                }
+            }
+            case ACTIVE_FOCUS -> {
+                if (!mIsActive) {
+                    routeTo(true, getBaseRoute(true));
+                }
+            }
+            case RINGING_FOCUS -> {
+                if (!mIsActive) {
+                    AudioRoute route = getBaseRoute(true);
+                    BluetoothDevice device = mBluetoothRoutes.get(route);
+                    if (device != null && !mBluetoothRouteManager.isInbandRingEnabled(device)) {
+                        routeTo(false, route);
+                    } else {
+                        routeTo(true, route);
+                    }
+                } else {
+                    // 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;
+        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");
+        }
+    }
+
+    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 speaker request");
+        }
+    }
+
+    private void handleSwitchSpeaker() {
+        if (mSpeakerDockRoute != null && getAvailableRoutes().contains(mSpeakerDockRoute)) {
+            routeTo(mIsActive, mSpeakerDockRoute);
+        } else {
+            Log.i(this, "ignore switch speaker request");
+        }
+    }
+
+    private void handleSwitchBaselineRoute() {
+        routeTo(mIsActive, getBaseRoute(true));
+    }
+
+    private void handleSpeakerOn() {
+        if (isPending()) {
+            mPendingAudioRoute.onMessageReceived(SPEAKER_ON);
+        } 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()) {
+            mPendingAudioRoute.onMessageReceived(SPEAKER_OFF);
+        } else if (mCurrentRoute.getType() == AudioRoute.TYPE_SPEAKER) {
+            routeTo(mIsActive, getBaseRoute(true));
+            // 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();
+        }
+    }
+
+    public void handleExitPendingRoute() {
+        if (mIsPending) {
+            Log.i(this, "Exit pending route and enter %s(active=%b)",
+                    mPendingAudioRoute.getDestRoute(), mIsActive);
+            mCurrentRoute = mPendingAudioRoute.getDestRoute();
+            mIsPending = false;
+            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())) {
+                    availableBluetoothDevices.add(mBluetoothRoutes.get(route));
+                }
+            }
+            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 callAudioState) {
+        Log.i(this, "updateCallAudioState: " + callAudioState);
+        CallAudioState oldState = mCallAudioState;
+        mCallAudioState = callAudioState;
+        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);
+        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) {
+        if (mBluetoothRoutes.isEmpty() || !includeBluetooth) {
+            return mEarpieceWiredRoute != null ? mEarpieceWiredRoute : mSpeakerDockRoute;
+        } else {
+            // Most recent active route will always be the last in the array
+            return mBluetoothRoutes.keySet().stream().toList().get(mBluetoothRoutes.size() - 1);
+        }
+    }
+
+    private int calculateSupportedRouteMask() {
+        int routeMask = CallAudioState.ROUTE_SPEAKER;
+
+        if (mWiredHeadsetManager.isPluggedIn()) {
+            routeMask |= CallAudioState.ROUTE_WIRED_HEADSET;
+        } else {
+            AudioDeviceInfo[] deviceList = mAudioManager.getDevices(
+                    AudioManager.GET_DEVICES_OUTPUTS);
+            for (AudioDeviceInfo device: deviceList) {
+                if (device.getType() == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE) {
+                    routeMask |= CallAudioState.ROUTE_EARPIECE;
+                    break;
+                }
+            }
+        }
+        return routeMask;
+    }
+
+    @VisibleForTesting
+    public Set<AudioRoute> getAvailableRoutes() {
+        if (mCurrentRoute.equals(mStreamingRoute)) {
+            return mStreamingRoutes;
+        } else {
+            return mAvailableRoutes;
+        }
+    }
+
+    public AudioRoute getCurrentRoute() {
+        return mCurrentRoute;
+    }
+
+    private AudioRoute getBluetoothRoute(@AudioRoute.AudioRouteType int audioRouteType,
+                                         String address) {
+        for (AudioRoute route : mBluetoothRoutes.keySet()) {
+            if (route.getType() == audioRouteType && route.getBluetoothAddress().equals(address)) {
+                return route;
+            }
+        }
+        return null;
+    }
+
+    public AudioRoute getBaseRoute(boolean includeBluetooth) {
+        AudioRoute destRoute = getPreferredAudioRouteFromStrategy();
+        if (destRoute == null) {
+            destRoute = getPreferredAudioRouteFromDefault(includeBluetooth);
+        }
+        if (destRoute != null && !getAvailableRoutes().contains(destRoute)) {
+            destRoute = null;
+        }
+        return destRoute;
+    }
+
+    @VisibleForTesting
+    public void setAudioManager(AudioManager audioManager) {
+        mAudioManager = audioManager;
+    }
+
+    @VisibleForTesting
+    public void setAudioRouteFactory(AudioRoute.Factory audioRouteFactory) {
+        mAudioRouteFactory = audioRouteFactory;
+    }
+
+    @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/CallAudioRouteStateMachine.java b/src/com/android/server/telecom/CallAudioRouteStateMachine.java
index c0bb50e..26c25e8 100644
--- a/src/com/android/server/telecom/CallAudioRouteStateMachine.java
+++ b/src/com/android/server/telecom/CallAudioRouteStateMachine.java
@@ -120,63 +120,6 @@
     /** Direct the audio stream through another device. */
     public static final int ROUTE_STREAMING     = CallAudioState.ROUTE_STREAMING;
 
-    /** Valid values for msg.what */
-    public static final int CONNECT_WIRED_HEADSET = 1;
-    public static final int DISCONNECT_WIRED_HEADSET = 2;
-    public static final int CONNECT_DOCK = 5;
-    public static final int DISCONNECT_DOCK = 6;
-    public static final int BLUETOOTH_DEVICE_LIST_CHANGED = 7;
-    public static final int BT_ACTIVE_DEVICE_PRESENT = 8;
-    public static final int BT_ACTIVE_DEVICE_GONE = 9;
-
-    public static final int SWITCH_EARPIECE = 1001;
-    public static final int SWITCH_BLUETOOTH = 1002;
-    public static final int SWITCH_HEADSET = 1003;
-    public static final int SWITCH_SPEAKER = 1004;
-    // Wired headset, earpiece, or speakerphone, in that order of precedence.
-    public static final int SWITCH_BASELINE_ROUTE = 1005;
-
-    // Messages denoting that the speakerphone was turned on/off. Used to update state when we
-    // weren't the ones who turned it on/off
-    public static final int SPEAKER_ON = 1006;
-    public static final int SPEAKER_OFF = 1007;
-
-    // Messages denoting that the streaming route switch request was sent.
-    public static final int STREAMING_FORCE_ENABLED = 1008;
-    public static final int STREAMING_FORCE_DISABLED = 1009;
-
-    public static final int USER_SWITCH_EARPIECE = 1101;
-    public static final int USER_SWITCH_BLUETOOTH = 1102;
-    public static final int USER_SWITCH_HEADSET = 1103;
-    public static final int USER_SWITCH_SPEAKER = 1104;
-    public static final int USER_SWITCH_BASELINE_ROUTE = 1105;
-
-    public static final int UPDATE_SYSTEM_AUDIO_ROUTE = 1201;
-
-    // These three messages indicate state changes that come from BluetoothRouteManager.
-    // They may be triggered by the BT stack doing something on its own or they may be sent after
-    // we request that the BT stack do something. Any logic for these messages should take into
-    // account the possibility that the event indicated has already been processed (i.e. handling
-    // should be idempotent).
-    public static final int BT_AUDIO_DISCONNECTED = 1301;
-    public static final int BT_AUDIO_CONNECTED = 1302;
-    public static final int BT_AUDIO_PENDING = 1303;
-
-    public static final int MUTE_ON = 3001;
-    public static final int MUTE_OFF = 3002;
-    public static final int TOGGLE_MUTE = 3003;
-    public static final int MUTE_EXTERNALLY_CHANGED = 3004;
-
-    public static final int SWITCH_FOCUS = 4001;
-
-    // Used in testing to execute verifications. Not compatible with subsessions.
-    public static final int RUN_RUNNABLE = 9001;
-
-    /** Valid values for mAudioFocusType */
-    public static final int NO_FOCUS = 1;
-    public static final int ACTIVE_FOCUS = 2;
-    public static final int RINGING_FOCUS = 3;
-
     /** Valid values for the first argument for SWITCH_BASELINE_ROUTE */
     public static final int NO_INCLUDE_BLUETOOTH_IN_BASELINE = 0;
     public static final int INCLUDE_BLUETOOTH_IN_BASELINE = 1;
@@ -189,45 +132,6 @@
         put(CallAudioState.ROUTE_WIRED_HEADSET, LogUtils.Events.AUDIO_ROUTE_HEADSET);
     }};
 
-    private static final SparseArray<String> MESSAGE_CODE_TO_NAME = new SparseArray<String>() {{
-        put(CONNECT_WIRED_HEADSET, "CONNECT_WIRED_HEADSET");
-        put(DISCONNECT_WIRED_HEADSET, "DISCONNECT_WIRED_HEADSET");
-        put(CONNECT_DOCK, "CONNECT_DOCK");
-        put(DISCONNECT_DOCK, "DISCONNECT_DOCK");
-        put(BLUETOOTH_DEVICE_LIST_CHANGED, "BLUETOOTH_DEVICE_LIST_CHANGED");
-        put(BT_ACTIVE_DEVICE_PRESENT, "BT_ACTIVE_DEVICE_PRESENT");
-        put(BT_ACTIVE_DEVICE_GONE, "BT_ACTIVE_DEVICE_GONE");
-
-        put(SWITCH_EARPIECE, "SWITCH_EARPIECE");
-        put(SWITCH_BLUETOOTH, "SWITCH_BLUETOOTH");
-        put(SWITCH_HEADSET, "SWITCH_HEADSET");
-        put(SWITCH_SPEAKER, "SWITCH_SPEAKER");
-        put(SWITCH_BASELINE_ROUTE, "SWITCH_BASELINE_ROUTE");
-        put(SPEAKER_ON, "SPEAKER_ON");
-        put(SPEAKER_OFF, "SPEAKER_OFF");
-
-        put(USER_SWITCH_EARPIECE, "USER_SWITCH_EARPIECE");
-        put(USER_SWITCH_BLUETOOTH, "USER_SWITCH_BLUETOOTH");
-        put(USER_SWITCH_HEADSET, "USER_SWITCH_HEADSET");
-        put(USER_SWITCH_SPEAKER, "USER_SWITCH_SPEAKER");
-        put(USER_SWITCH_BASELINE_ROUTE, "USER_SWITCH_BASELINE_ROUTE");
-
-        put(UPDATE_SYSTEM_AUDIO_ROUTE, "UPDATE_SYSTEM_AUDIO_ROUTE");
-
-        put(BT_AUDIO_DISCONNECTED, "BT_AUDIO_DISCONNECTED");
-        put(BT_AUDIO_CONNECTED, "BT_AUDIO_CONNECTED");
-        put(BT_AUDIO_PENDING, "BT_AUDIO_PENDING");
-
-        put(MUTE_ON, "MUTE_ON");
-        put(MUTE_OFF, "MUTE_OFF");
-        put(TOGGLE_MUTE, "TOGGLE_MUTE");
-        put(MUTE_EXTERNALLY_CHANGED, "MUTE_EXTERNALLY_CHANGED");
-
-        put(SWITCH_FOCUS, "SWITCH_FOCUS");
-
-        put(RUN_RUNNABLE, "RUN_RUNNABLE");
-    }};
-
     private static final String ACTIVE_EARPIECE_ROUTE_NAME = "ActiveEarpieceRoute";
     private static final String ACTIVE_BLUETOOTH_ROUTE_NAME = "ActiveBluetoothRoute";
     private static final String ACTIVE_SPEAKER_ROUTE_NAME = "ActiveSpeakerRoute";
@@ -1737,11 +1641,11 @@
     }
 
     public void sendMessageWithSessionInfo(int message, int arg) {
-        sendMessageWithSessionInfo(message, arg, null);
+        sendMessageWithSessionInfo(message, arg, (String) null);
     }
 
     public void sendMessageWithSessionInfo(int message) {
-        sendMessageWithSessionInfo(message, 0, null);
+        sendMessageWithSessionInfo(message, 0, (String) null);
     }
 
     public void sendMessageWithSessionInfo(int message, int arg, String data) {
@@ -1751,6 +1655,10 @@
         sendMessage(message, arg, 0, args);
     }
 
+    public void sendMessageWithSessionInfo(int message, int arg, BluetoothDevice bluetoothDevice) {
+        // ignore, only used in CallAudioRouteController
+    }
+
     @Override
     public void sendMessage(int message, Runnable r) {
         super.sendMessage(message, r);
diff --git a/src/com/android/server/telecom/CallIntentProcessor.java b/src/com/android/server/telecom/CallIntentProcessor.java
index 062c872..c02d20d 100644
--- a/src/com/android/server/telecom/CallIntentProcessor.java
+++ b/src/com/android/server/telecom/CallIntentProcessor.java
@@ -172,9 +172,14 @@
             // 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()) {
+                    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,
diff --git a/src/com/android/server/telecom/CallLogManager.java b/src/com/android/server/telecom/CallLogManager.java
index 93ea465..1f87593 100644
--- a/src/com/android/server/telecom/CallLogManager.java
+++ b/src/com/android/server/telecom/CallLogManager.java
@@ -16,6 +16,7 @@
 
 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;
 
@@ -31,6 +32,9 @@
 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;
@@ -52,6 +56,7 @@
 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;
@@ -116,8 +121,10 @@
     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;
 
@@ -130,7 +137,7 @@
         mPhoneAccountRegistrar = phoneAccountRegistrar;
         mMissedCallNotifier = missedCallNotifier;
         mAnomalyReporterAdapter = anomalyReporterAdapter;
-        mLock = new Object();
+        mCountryCodeExecutor = new HandlerExecutor(new Handler(Looper.getMainLooper()));
         mFeatureFlags = featureFlags;
     }
 
@@ -412,7 +419,25 @@
         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 =
@@ -620,7 +645,7 @@
             return Locale.getDefault().getCountry();
         }
 
-        return country.getCountryIso();
+        return country.getCountryCode();
     }
 
     /**
@@ -631,31 +656,35 @@
     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();
+        }
+    }
 
     /**
      * Returns a pair containing the number of rows in the call log, as well as the maximum call log
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
old mode 100755
new mode 100644
index 9f09047..afe201b
--- 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.BlockedNumberContract.BlockedNumbers;
 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;
@@ -224,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";
 
     /**
@@ -465,6 +489,7 @@
     private final UserManager mUserManager;
     private final CallStreamingNotification mCallStreamingNotification;
     private final FeatureFlags mFeatureFlags;
+    private final com.android.internal.telephony.flags.FeatureFlags mTelephonyFeatureFlags;
 
     private final IncomingCallFilterGraphProvider mIncomingCallFilterGraphProvider;
 
@@ -535,7 +560,7 @@
         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)) {
+                    || BlockedNumbers.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED.equals(action)) {
                 updateEmergencyCallNotificationAsync(context);
             }
         }
@@ -586,7 +611,9 @@
             EmergencyCallDiagnosticLogger emergencyCallDiagnosticLogger,
             CallAudioCommunicationDeviceTracker communicationDeviceTracker,
             CallStreamingNotification callStreamingNotification,
+            BluetoothDeviceManager bluetoothDeviceManager,
             FeatureFlags featureFlags,
+            com.android.internal.telephony.flags.FeatureFlags telephonyFlags,
             IncomingCallFilterGraphProvider incomingCallFilterGraphProvider) {
 
         mContext = context;
@@ -611,6 +638,9 @@
         mDtmfLocalTonePlayer =
                 new DtmfLocalTonePlayer(new DtmfLocalTonePlayer.ToneGeneratorProxy());
         CallAudioRouteAdapter callAudioRouteAdapter;
+        // TODO: add another flag check when
+        // bluetoothDeviceManager.getBluetoothHeadset().isScoManagedByAudio()
+        // available and return true
         if (!featureFlags.useRefactoredAudioRouteSwitching()) {
             callAudioRouteAdapter = callAudioRouteStateMachineFactory.create(
                     context,
@@ -625,9 +655,11 @@
                     featureFlags
             );
         } else {
-            callAudioRouteAdapter = new CallAudioRouteController();
+            callAudioRouteAdapter = new CallAudioRouteController(context, this, audioServiceFactory,
+                    new AudioRoute.Factory(), wiredHeadsetManager, mBluetoothRouteManager);
         }
         callAudioRouteAdapter.initialize();
+        bluetoothStateReceiver.setCallAudioRouteAdapter(callAudioRouteAdapter);
 
         CallAudioRoutePeripheralAdapter callAudioRoutePeripheralAdapter =
                 new CallAudioRoutePeripheralAdapter(
@@ -677,7 +709,8 @@
         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;
@@ -688,6 +721,7 @@
         mCallStreamingController = new CallStreamingController(mContext, mLock);
         mCallStreamingNotification = callStreamingNotification;
         mFeatureFlags = featureFlags;
+        mTelephonyFeatureFlags = telephonyFlags;
 
         if (mFeatureFlags.useImprovedListenerOrder()) {
             mListeners.add(mInCallController);
@@ -728,7 +762,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(BlockedNumbers.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED);
         context.registerReceiver(mReceiver, intentFilter, Context.RECEIVER_EXPORTED);
         mGraphHandlerThreads = new LinkedList<>();
 
@@ -866,11 +900,11 @@
     }
 
     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(
@@ -942,8 +976,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;
@@ -988,6 +1024,10 @@
         }
 
         if (result.shouldAllowCall) {
+            if (mFeatureFlags.separatelyBindToBtIncallService()) {
+                incomingCall.setBtIcsFuture(mInCallController.bindToBTService(incomingCall));
+                setCallState(incomingCall, CallState.RINGING, "successful incoming call");
+            }
             incomingCall.setPostCallPackageName(
                     getRoleManagerAdapter().getDefaultCallScreeningApp(
                             incomingCall.getAssociatedUser()
@@ -1002,7 +1042,6 @@
                             "Exceeds maximum number of ringing calls.");
                     incomingCall.setMissedReason(AUTO_MISSED_MAXIMUM_RINGING);
                     autoMissCallAndLog(incomingCall, result);
-                    return;
                 }
             } else if (hasMaximumManagedDialingCalls(incomingCall)) {
                 if (shouldSilenceInsteadOfReject(incomingCall)) {
@@ -1012,7 +1051,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");
@@ -1031,6 +1069,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) {
@@ -1302,9 +1343,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);
         }
     }
@@ -2898,7 +2937,7 @@
 
         if (call.isEmergencyCall()) {
             Executors.defaultThreadFactory().newThread(() ->
-                    BlockedNumberContract.SystemContract.notifyEmergencyContact(mContext))
+                    BlockedNumberContract.BlockedNumbers.notifyEmergencyContact(mContext))
                     .start();
         }
 
@@ -2982,10 +3021,6 @@
                 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.
-            Bundle bundle = new Bundle();
-            bundle.putLong(TelecomManager.EXTRA_CALL_ANSWERED_TIME_MILLIS,
-                     mClockProxy.currentTimeMillis());
-            call.putConnectionServiceExtras(bundle);
             holdActiveCallForNewCall(call);
             mConnectionSvrFocusMgr.requestFocus(
                     call,
@@ -3137,9 +3172,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;
     }
 
     /**
@@ -3399,8 +3433,14 @@
     // 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.
@@ -3415,7 +3455,7 @@
         }
     }
 
-    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) {
@@ -3456,6 +3496,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);
     }
@@ -3506,7 +3622,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;
@@ -4561,10 +4677,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);
@@ -4582,11 +4694,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);
@@ -4609,7 +4719,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
@@ -4618,7 +4727,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);
         }
@@ -4664,18 +4772,35 @@
     }
 
     /**
-     * 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) {
+        return isInSelfManagedCallCrossUsers(packageName, userHandle, false);
+    }
+
+    /**
+     * Determines if there are any ongoing self-managed calls for the given package/user (unless
+     * hasCrossUsers has been enabled).
+     * @param packageName The package name to check.
+     * @param userHandle The {@link UserHandle} to check.
+     * @param hasCrossUserAccess indicates if calls across all users should be returned.
+     * @return {@code true} if the app has ongoing calls, or {@code false} otherwise.
+     */
+    public boolean isInSelfManagedCallCrossUsers(
+            String packageName, UserHandle userHandle, boolean hasCrossUserAccess) {
         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
@@ -5849,8 +5974,7 @@
             return;
         }
         ConnectionServiceWrapper service = mConnectionServiceRepository.getService(
-                phoneAccountHandle.getComponentName(), phoneAccountHandle.getUserHandle(),
-                mFeatureFlags);
+                phoneAccountHandle.getComponentName(), phoneAccountHandle.getUserHandle());
         if (service == null) {
             Log.i(this, "Found no connection service.");
             return;
@@ -5875,8 +5999,7 @@
             return;
         }
         ConnectionServiceWrapper service = mConnectionServiceRepository.getService(
-                phoneAccountHandle.getComponentName(), phoneAccountHandle.getUserHandle(),
-                mFeatureFlags);
+                phoneAccountHandle.getComponentName(), phoneAccountHandle.getUserHandle());
         if (service == null) {
             Log.i(this, "Found no connection service.");
             return;
@@ -5913,28 +6036,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}.
diff --git a/src/com/android/server/telecom/ConnectionServiceRepository.java b/src/com/android/server/telecom/ConnectionServiceRepository.java
index d6a78d0..e4ed220 100644
--- a/src/com/android/server/telecom/ConnectionServiceRepository.java
+++ b/src/com/android/server/telecom/ConnectionServiceRepository.java
@@ -38,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>() {
@@ -54,18 +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,
-            FeatureFlags featureFlags) {
+            UserHandle userHandle) {
         Pair<ComponentName, UserHandle> cacheKey = Pair.create(componentName, userHandle);
         ConnectionServiceWrapper service = mServiceCache.get(cacheKey);
         if (service == null) {
@@ -77,7 +79,7 @@
                     mContext,
                     mLock,
                     userHandle,
-                    featureFlags);
+                    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 19e267c..2ed416d 100644
--- a/src/com/android/server/telecom/ConnectionServiceWrapper.java
+++ b/src/com/android/server/telecom/ConnectionServiceWrapper.java
@@ -33,6 +33,7 @@
 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;
@@ -73,14 +74,13 @@
 import java.util.HashMap;
 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.ConcurrentHashMap;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
+import java.util.Objects;
 
 /**
  * Wrapper for {@link IConnectionService}s, handles binding to {@link IConnectionService} and keeps
@@ -93,12 +93,9 @@
         ConnectionServiceFocusManager.ConnectionServiceFocus {
 
     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();
 
     private final class Adapter extends IConnectionServiceAdapter.Stub {
 
@@ -1354,7 +1351,6 @@
     private final CallsManager mCallsManager;
     private final AppOpsManager mAppOpsManager;
     private final Context mContext;
-    private final FeatureFlags mFlags;
 
     private ConnectionServiceFocusManager.ConnectionServiceFocusListener mConnSvrFocusListener;
 
@@ -1389,7 +1385,6 @@
         mCallsManager = callsManager;
         mAppOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
         mContext = context;
-        mFlags = featureFlags;
     }
 
     /** See {@link IConnectionService#addConnectionServiceAdapter}. */
@@ -1603,22 +1598,6 @@
                         .setParticipants(call.getParticipants())
                         .setIsAdhocConferenceCall(call.isAdhocConferenceCall())
                         .build();
-                if (Flags.unbindTimeoutConnections()) {
-                    android.telecom.Logging.Runnable r =
-                            new android.telecom.Logging.Runnable("CSW.cC", mLock) {
-                        @Override
-                        public void loggedRun() {
-                            if (!call.isCreateConnectionComplete()) {
-                                Log.e(this, new Exception(), "Conference %s creation timeout",
-                                        getComponentName());
-                                response.handleCreateConferenceFailure(
-                                        new DisconnectCause(DisconnectCause.ERROR));
-                            }
-                        }
-                    };
-                    mScheduledExecutor.schedule(r.getRunnableToCancel(), SERVICE_BINDING_TIMEOUT,
-                            TimeUnit.MILLISECONDS);
-                }
                 try {
                     mServiceInterface.createConference(
                             call.getConnectionManagerPhoneAccount(),
@@ -1659,7 +1638,6 @@
                     Log.i(ConnectionServiceWrapper.this, "Call not present"
                             + " in call id mapper, maybe it was aborted before the bind"
                             + " completed successfully?");
-
                     response.handleCreateConnectionFailure(
                             new DisconnectCause(DisconnectCause.CANCELED));
                     return;
@@ -1720,24 +1698,6 @@
                         .setRttPipeFromInCall(call.getInCallToCsRttPipeForCs())
                         .setRttPipeToInCall(call.getCsToInCallRttPipeForCs())
                         .build();
-
-                if (Flags.unbindTimeoutConnections()) {
-                    android.telecom.Logging.Runnable r =
-                            new android.telecom.Logging.Runnable("CSW.cC", mLock) {
-                                @Override
-                                public void loggedRun() {
-                                    if (!call.isCreateConnectionComplete()) {
-                                        Log.e(this, new Exception(),
-                                                "Connection %s creation timeout",
-                                                getComponentName());
-                                        response.handleCreateConnectionFailure(
-                                                new DisconnectCause(DisconnectCause.ERROR));
-                                    }
-                                }
-                            };
-                    mScheduledExecutor.schedule(r.getRunnableToCancel(), SERVICE_BINDING_TIMEOUT,
-                            TimeUnit.MILLISECONDS);
-                }
                 try {
                     mServiceInterface.createConnection(
                             call.getConnectionManagerPhoneAccount(),
@@ -1746,6 +1706,7 @@
                             call.shouldAttachToExistingConnection(),
                             call.isUnknown(),
                             Log.getExternalSession(TELECOM_ABBREVIATION));
+
                 } catch (RemoteException e) {
                     Log.e(this, e, "Failure to createConnection -- %s", getComponentName());
                     mPendingResponses.remove(callId).handleCreateConnectionFailure(
@@ -2194,8 +2155,7 @@
         }
     }
 
-    @VisibleForTesting
-    public void addCall(Call call) {
+    void addCall(Call call) {
         if (mCallIdMapper.getCallId(call) == null) {
             mCallIdMapper.addCall(call);
         }
@@ -2578,7 +2538,7 @@
                 isCallerConnectionManager = true;
             }
             ConnectionServiceWrapper service = mConnectionServiceRepository.getService(
-                    handle.getComponentName(), handle.getUserHandle(), mFlags);
+                    handle.getComponentName(), handle.getUserHandle());
             if (service != null && service != this) {
                 simServices.add(service);
             } else {
@@ -2663,9 +2623,4 @@
         sb.append("]");
         return sb.toString();
     }
-
-    @VisibleForTesting
-    public void setScheduledExecutorService(ScheduledExecutorService service) {
-        mScheduledExecutor = service;
-    }
 }
diff --git a/src/com/android/server/telecom/CreateConnectionProcessor.java b/src/com/android/server/telecom/CreateConnectionProcessor.java
index f5b257d..bcb2d2f 100644
--- a/src/com/android/server/telecom/CreateConnectionProcessor.java
+++ b/src/com/android/server/telecom/CreateConnectionProcessor.java
@@ -244,7 +244,7 @@
             Log.i(this, "Trying attempt %s", attempt);
             PhoneAccountHandle phoneAccount = attempt.connectionManagerPhoneAccount;
             mService = mRepository.getService(phoneAccount.getComponentName(),
-                    phoneAccount.getUserHandle(), mFlags);
+                    phoneAccount.getUserHandle());
             if (mService == null) {
                 Log.i(this, "Found no connection service for attempt %s", attempt);
                 attemptNextPhoneAccount();
@@ -260,7 +260,7 @@
                         PhoneAccountHandle remotePhoneAccount = attempt.targetPhoneAccount;
                         ConnectionServiceWrapper mRemoteService =
                                 mRepository.getService(remotePhoneAccount.getComponentName(),
-                                remotePhoneAccount.getUserHandle(), mFlags);
+                                remotePhoneAccount.getUserHandle());
                         if (mRemoteService == null) {
                             mCall.setConnectionService(mService);
                         } else {
diff --git a/src/com/android/server/telecom/DefaultDialerCache.java b/src/com/android/server/telecom/DefaultDialerCache.java
index dc79715..d819780 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 getBTInCallServicePackage() {
+        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/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 ad3d26a..f464d04 100644
--- a/src/com/android/server/telecom/InCallController.java
+++ b/src/com/android/server/telecom/InCallController.java
@@ -22,7 +22,6 @@
 import android.Manifest;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.app.ActivityManager;
 import android.app.AppOpsManager;
 import android.app.KeyguardManager;
 import android.app.Notification;
@@ -60,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;
@@ -73,6 +73,7 @@
 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;
@@ -327,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;
             }
@@ -1176,6 +1183,7 @@
     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;
 
     private static final int[] LIVE_CALL_STATES = { CallState.ACTIVE, CallState.PULLING,
             CallState.DISCONNECTING };
@@ -1183,8 +1191,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;
@@ -1200,8 +1213,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
@@ -1212,6 +1228,12 @@
     // 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 CompletableFuture<Boolean> mBtBindingFuture = CompletableFuture.completedFuture(true);
+
     private final CarModeTracker mCarModeTracker;
 
     /**
@@ -1240,12 +1262,21 @@
 
     private ArraySet<String> mAllCarrierPrivilegedApps = new ArraySet<>();
     private ArraySet<String> mActiveCarrierPrivilegedApps = new ArraySet<>();
-    private FeatureFlags mFeatureFlags;
 
     public InCallController(Context context, TelecomSystem.SyncRoot lock, CallsManager callsManager,
             SystemStateHelper systemStateHelper, DefaultDialerCache defaultDialerCache,
             Timeouts.Adapter timeoutsAdapter, EmergencyCallHelper emergencyCallHelper,
             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);
@@ -1342,65 +1373,89 @@
         // 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 bindBTService = false;
+            boolean bindOtherServices = false;
+            if (!isBoundAndConnectedToBTService(userFromCall)) {
+                Log.i(this, "onCallAdded: %s; not bound or connected to BT ICS.", call);
+                bindBTService = true;
+                bindToBTService(call);
+            }
+            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.
+                bindOtherServices = true;
+                bindToOtherServices(call);
+            }
+            if (!bindBTService || !bindOtherServices) {
+                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, false);
+            } 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);
         }
     }
 
@@ -1449,15 +1504,36 @@
     }
 
     @Override
+    public void onDisconnectedTonePlaying(Call call, boolean isTonePlaying) {
+        Log.i(this, "onDisconnectedTonePlaying: %s -> %b", call, isTonePlaying);
+
+        if (mFeatureFlags.separatelyBindToBtIncallService()) {
+            synchronized (mLock) {
+                mPendingEndToneCall.remove(call);
+                if (!mPendingEndToneCall.isEmpty()) {
+                    return;
+                }
+                UserHandle userHandle = getUserFromCall(call);
+                if (mBTInCallServiceConnections.containsKey(userHandle)) {
+                    mBTInCallServiceConnections.get(userHandle).disconnect();
+                    mBTInCallServiceConnections.remove(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();
 
@@ -1496,9 +1572,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
@@ -1534,6 +1610,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);
     }
@@ -1549,11 +1627,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);
@@ -1566,9 +1646,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);
@@ -1582,10 +1664,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);
@@ -1599,9 +1683,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);
@@ -1615,9 +1701,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);
@@ -1630,9 +1718,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) {
@@ -1713,12 +1803,14 @@
         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 && !mInCallServices.containsKey(callingUser)) {
+                && 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.
@@ -1732,8 +1824,8 @@
                 callingUser = currentUser;
             }
         }
-        if (mInCallServices.containsKey(callingUser)) {
-            for (IInCallService inCallService : mInCallServices.get(callingUser).values()) {
+        if (serviceMap.containsKey(callingUser)) {
+            for (IInCallService inCallService : serviceMap.get(callingUser).values()) {
                 try {
                     inCallService.bringToForeground(showDialpad);
                 } catch (RemoteException ignored) {
@@ -1746,7 +1838,7 @@
 
     @VisibleForTesting
     public Map<UserHandle, Map<InCallServiceInfo, IInCallService>> getInCallServices() {
-        return mInCallServices;
+        return getCombinedInCallServiceMap();
     }
 
     @VisibleForTesting
@@ -1755,9 +1847,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) {
@@ -1769,8 +1863,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"),
@@ -1785,8 +1881,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) -> {
@@ -1803,8 +1901,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) -> {
@@ -1821,8 +1921,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) {
@@ -1833,8 +1935,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) {
@@ -1862,26 +1966,69 @@
             mNonUIInCallServiceConnections.get(userHandle).disconnect();
             mNonUIInCallServiceConnections.remove(userHandle);
         }
-        mInCallServices.remove(userHandle);
+        getCombinedInCallServiceMap().remove(userHandle);
+        if (mFeatureFlags.separatelyBindToBtIncallService()) {
+            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 CompletableFuture<Boolean> bindToBTService(Call call) {
+        // Track the call if we don't already know about it.
+        addCall(call);
+        UserHandle userFromCall = getUserFromCall(call);
+
+        List<InCallServiceInfo> infos = getInCallServiceComponents(userFromCall,
+                IN_CALL_SERVICE_TYPE_BLUETOOTH);
+        if (infos.size() == 0 || infos.get(0) == null) {
+            Log.w(this, "No available BT service");
+            mBtBindingFuture = CompletableFuture.completedFuture(false);
+            return mBtBindingFuture;
+        }
+        mBtBindingFuture = new CompletableFuture<Boolean>().completeOnTimeout(false,
+                mTimeoutsAdapter.getCallBindBluetoothInCallServicesDelay(
+                        mContext.getContentResolver()), TimeUnit.MILLISECONDS);
+        new InCallServiceBindingConnection(infos.get(0)).connect(call);
+        return mBtBindingFuture;
     }
 
     /**
      * Binds to all the UI-providing InCallService as well as system-implemented non-UI
-     * InCallServices. Method-invoker must check {@link #isBoundAndConnectedToServices()}
-     * before invoking.
+     * InCallServices except BT 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.
      */
-    @VisibleForTesting
-    public void bindToServices(Call call) {
-        UserHandle userFromCall = getUserFromCall(call);
-        UserHandle parentUser = null;
-        UserManager um = mContext.getSystemService(UserManager.class);
+    public void bindToOtherServices(Call call) {
+        bindToServices(call, true);
+    }
 
-        if (um.isManagedProfile(userFromCall.getIdentifier())) {
+    /**
+     * Binds to all the UI-providing InCallService as well as system-implemented non-UI
+     * 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 skipBTServices Boolean variable to specify if the binding to BT InCallService should
+     *                      be skipped
+     */
+    @VisibleForTesting
+    public void bindToServices(Call call, boolean skipBTServices) {
+        UserHandle userFromCall = getUserFromCall(call);
+        UserManager um = mContext.getSystemService(UserManager.class);
+        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;
@@ -1932,11 +2079,12 @@
 
         // 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.
-            connectToNonUiInCallServices(call);
+            connectToNonUiInCallServices(call, skipBTServices);
             mBindingFuture = new CompletableFuture<Boolean>().completeOnTimeout(false,
                     mTimeoutsAdapter.getCallRemoveUnbindInCallServicesDelay(
                             mContext.getContentResolver()),
@@ -1951,21 +2099,22 @@
                 packageChangedFilter, null, null);
     }
 
-    private void updateNonUiInCallServices(Call call) {
+    private void updateNonUiInCallServices(Call call, boolean skipBTService) {
         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);
@@ -2005,10 +2154,10 @@
                 nonUIInCalls));
     }
 
-    private void connectToNonUiInCallServices(Call call) {
+    private void connectToNonUiInCallServices(Call call, boolean skipBTService) {
         UserHandle userFromCall = getUserFromCall(call);
         if (!mNonUIInCallServiceConnections.containsKey(userFromCall)) {
-            updateNonUiInCallServices(call);
+            updateNonUiInCallServices(call, skipBTService);
         }
         mNonUIInCallServiceConnections.get(userFromCall).connect(call);
     }
@@ -2277,6 +2426,14 @@
             return IN_CALL_SERVICE_TYPE_DEFAULT_DIALER_UI;
         }
 
+        String bluetoothPackage = mDefaultDialerCache.getBTInCallServicePackage();
+        if (mFeatureFlags.separatelyBindToBtIncallService()
+                && serviceInfo.packageName != null
+                && serviceInfo.packageName.equals(bluetoothPackage)
+                && (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 ||
@@ -2317,9 +2474,24 @@
             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.isDone()) {
+                // Binding completed after the timeout. Clean up this binding
+                return false;
+            } else {
+                mBtBindingFuture.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(
@@ -2408,6 +2580,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);
+        }
     }
 
     /**
@@ -2428,17 +2605,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();
@@ -2506,6 +2685,9 @@
         }
 
         maybeTrackMicrophoneUse(isMuted());
+        if (mFeatureFlags.separatelyBindToBtIncallService()) {
+            mPendingEndToneCall.add(call);
+        }
     }
 
     /**
@@ -2518,6 +2700,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.
@@ -2532,9 +2722,11 @@
      * @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);
             }
@@ -2959,4 +3151,36 @@
         }
         return false;
     }
+
+    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;
+            }
+        }
+    }
 }
diff --git a/src/com/android/server/telecom/ParcelableCallUtils.java b/src/com/android/server/telecom/ParcelableCallUtils.java
index c77e605..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 {
diff --git a/src/com/android/server/telecom/PendingAudioRoute.java b/src/com/android/server/telecom/PendingAudioRoute.java
new file mode 100644
index 0000000..8de62ed
--- /dev/null
+++ b/src/com/android/server/telecom/PendingAudioRoute.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.telecom;
+
+import static com.android.server.telecom.CallAudioRouteAdapter.PENDING_ROUTE_FAILED;
+
+import android.media.AudioManager;
+
+import java.util.ArrayList;
+
+/**
+ * Used to represent the intermediate state during audio route switching.
+ * Usually, audio route switching start with a communication device setting request to audio
+ * framework and will be completed with corresponding success broadcasts or messages. Instance of
+ * this class is responsible for tracking the pending success signals according to the original
+ * audio route and the destination audio route of this switching.
+ */
+public class PendingAudioRoute {
+    private CallAudioRouteController mCallAudioRouteController;
+    private AudioManager mAudioManager;
+    /**
+     * The {@link AudioRoute} that this pending audio switching started with
+     */
+    private AudioRoute mOrigRoute;
+    /**
+     * The expected destination {@link AudioRoute} of this pending audio switching, can be changed
+     * by new switching request during the ongoing switching
+     */
+    private AudioRoute mDestRoute;
+    private ArrayList<Integer> mPendingMessages;
+    private boolean mActive;
+    PendingAudioRoute(CallAudioRouteController controller, AudioManager audioManager) {
+        mCallAudioRouteController = controller;
+        mAudioManager = audioManager;
+        mPendingMessages = new ArrayList<>();
+        mActive = false;
+    }
+
+    void setOrigRoute(boolean active, AudioRoute origRoute) {
+        origRoute.onOrigRouteAsPendingRoute(active, this, mAudioManager);
+        mOrigRoute = origRoute;
+    }
+
+    AudioRoute getOrigRoute() {
+        return mOrigRoute;
+    }
+
+    void setDestRoute(boolean active, AudioRoute destRoute) {
+        destRoute.onDestRouteAsPendingRoute(active, this, mAudioManager);
+        mActive = active;
+        mDestRoute = destRoute;
+    }
+
+    AudioRoute getDestRoute() {
+        return mDestRoute;
+    }
+
+    public void addMessage(int message) {
+        mPendingMessages.add(message);
+    }
+
+    public void onMessageReceived(int message) {
+        if (message == PENDING_ROUTE_FAILED) {
+            // Fallback to base route
+            mDestRoute = mCallAudioRouteController.getBaseRoute(true);
+            mCallAudioRouteController.sendMessageWithSessionInfo(
+                    CallAudioRouteAdapter.EXIT_PENDING_ROUTE);
+        }
+
+        // Removes the first occurrence of the specified message from this list, if it is present.
+        mPendingMessages.remove((Object) message);
+        evaluatePendingState();
+    }
+
+    public void evaluatePendingState() {
+        if (mPendingMessages.isEmpty()) {
+            mCallAudioRouteController.sendMessageWithSessionInfo(
+                    CallAudioRouteAdapter.EXIT_PENDING_ROUTE);
+        }
+    }
+
+    public boolean isActive() {
+        return mActive;
+    }
+}
diff --git a/src/com/android/server/telecom/PhoneAccountRegistrar.java b/src/com/android/server/telecom/PhoneAccountRegistrar.java
index 5f23e4d..fc90edd 100644
--- a/src/com/android/server/telecom/PhoneAccountRegistrar.java
+++ b/src/com/android/server/telecom/PhoneAccountRegistrar.java
@@ -58,6 +58,7 @@
 
 // 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;
@@ -81,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;
@@ -181,16 +184,19 @@
     private interface PhoneAccountRegistrarWriteLock {}
     private final PhoneAccountRegistrarWriteLock mWriteLock =
             new PhoneAccountRegistrarWriteLock() {};
+    private final FeatureFlags mTelephonyFeatureFlags;
 
     @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) {
+        this(context, lock, FILE_NAME, defaultDialerCache, appLabelProxy, telephonyFeatureFlags);
     }
 
     @VisibleForTesting
     public PhoneAccountRegistrar(Context context, TelecomSystem.SyncRoot lock, String fileName,
-            DefaultDialerCache defaultDialerCache, AppLabelProxy appLabelProxy) {
+            DefaultDialerCache defaultDialerCache, AppLabelProxy appLabelProxy,
+            FeatureFlags telephonyFeatureFlags) {
 
         mAtomicFile = new AtomicFile(new File(context.getFilesDir(), fileName));
 
@@ -204,6 +210,13 @@
         mAppLabelProxy = appLabelProxy;
         mCurrentUserHandle = Process.myUserHandle();
 
+        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);
         intentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
@@ -697,6 +710,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) {
@@ -763,11 +794,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);
     }
 
     /**
@@ -858,7 +889,7 @@
     public List<PhoneAccountHandle> getAllPhoneAccountHandlesForPackage(UserHandle userHandle,
             String packageName) {
         return getPhoneAccountHandles(0, null, packageName, true /* includeDisabled */, userHandle,
-                true /* crossUserAccess */);
+                true /* crossUserAccess */, true);
     }
 
     /**
@@ -923,6 +954,9 @@
         enforceCharacterLimit(account);
         enforceIconSizeLimit(account);
         enforceMaxPhoneAccountLimit(account);
+        if (mTelephonyFeatureFlags.simultaneousCallingIndications()) {
+            enforceSimultaneousCallingRestrictionLimit(account);
+        }
         addOrReplacePhoneAccount(account);
     }
 
@@ -934,15 +968,13 @@
      * @throws IllegalArgumentException if MAX_PHONE_ACCOUNT_REGISTRATIONS are reached
      */
     private void enforceMaxPhoneAccountLimit(@NonNull PhoneAccount account) {
-        List<PhoneAccount> unverifiedAccounts = getAccountsForPackage_BypassResolveComp(
-                account.getAccountHandle().getComponentName().getPackageName(),
-                account.getAccountHandle().getUserHandle());
-        // verify each phone account is backed by a valid ConnectionService. If the
-        // ConnectionService has been disabled or cannot be resolved, unregister the accounts.
-        List<PhoneAccount> verifiedAccounts =
-                cleanupUnresolvableConnectionServiceAccounts(unverifiedAccounts);
-        // enforce the max phone account limit for the application registering accounts
-        if (verifiedAccounts.size() >= MAX_PHONE_ACCOUNT_REGISTRATIONS) {
+        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) {
             EventLog.writeEvent(0x534e4554, "259064622", Binder.getCallingUid(),
                     "enforceMaxPhoneAccountLimit");
             throw new IllegalArgumentException(
@@ -1053,6 +1085,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
@@ -1490,7 +1559,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);
     }
 
     /**
@@ -1505,11 +1586,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;
@@ -1523,7 +1617,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);
     }
 
     /**
@@ -1545,7 +1651,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.
@@ -1579,58 +1700,23 @@
                 // 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 accounts;
-    }
 
-    /**
-     * This getter should be used when you want to bypass the {@link
-     * PhoneAccountRegistrar#resolveComponent(PhoneAccountHandle)} check when fetching accounts
-     */
-    @VisibleForTesting
-    public List<PhoneAccount> getAccountsForPackage_BypassResolveComp(String packageName,
-            UserHandle userHandle) {
-        List<PhoneAccount> accounts = new ArrayList<>(mState.accounts.size());
-        for (PhoneAccount m : mState.accounts) {
-            PhoneAccountHandle handle = m.getAccountHandle();
-
-            if (packageName != null && !packageName.equals(
-                    handle.getComponentName().getPackageName())) {
-                // Not the right package name; skip this one.
-                continue;
-            }
-
-            if (!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;
-    }
 
-    @VisibleForTesting
-    public List<PhoneAccount> cleanupUnresolvableConnectionServiceAccounts(
-            List<PhoneAccount> accounts) {
-        ArrayList<PhoneAccount> verifiedAccounts = new ArrayList<>();
-        for (PhoneAccount account : accounts) {
-            PhoneAccountHandle handle = account.getAccountHandle();
-            // if the ConnectionService has been disabled or can longer be found, remove the handle
-            if (resolveComponent(handle).isEmpty()) {
-                Log.i(this,
-                        "Cannot resolve the ConnectionService for handle=[%s]; unregistering"
-                                + " account", handle);
-                unregisterPhoneAccount(handle);
-            } else {
-                verifiedAccounts.add(account);
-            }
-        }
-        return verifiedAccounts;
+        return accounts;
     }
 
     /**
@@ -1835,7 +1921,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) {
@@ -1856,7 +1942,7 @@
         try {
             XmlPullParser parser = Xml.resolvePullParser(is);
             parser.nextTag();
-            mState = readFromXml(parser, mContext);
+            mState = readFromXml(parser, mContext, mTelephonyFeatureFlags);
             migratePhoneAccountHandle(mState);
             versionChanged = mState.versionNumber < EXPECTED_STATE_VERSION;
 
@@ -1891,14 +1977,14 @@
         }
     }
 
-    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)
-            throws IOException, XmlPullParserException {
-        State s = sStateXml.readFromXml(parser, 0, context);
+    private static State readFromXml(XmlPullParser parser, Context context,
+            FeatureFlags telephonyFeatureFlags) throws IOException, XmlPullParserException {
+        State s = sStateXml.readFromXml(parser, 0, context, telephonyFeatureFlags);
         return s != null ? s : new State();
     }
 
@@ -1964,8 +2050,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
@@ -1974,8 +2060,8 @@
          * 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)
-                throws IOException, XmlPullParserException;
+        public abstract T readFromXml(XmlPullParser parser, int version, Context context,
+                FeatureFlags telephonyFeatureFlags) throws IOException, XmlPullParserException;
 
         protected void writeTextIfNonNull(String tagName, Object value, XmlSerializer serializer)
                 throws IOException {
@@ -1987,6 +2073,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.
@@ -2080,6 +2189,21 @@
             serializer.endTag(null, tagName);
         }
 
+        protected Set<PhoneAccountHandle> readPhoneAccountHandleSet(XmlPullParser parser,
+                int version, Context context, FeatureFlags telephonyFeatureFlags)
+                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));
+            }
+            return handles;
+        }
+
         /**
          * Reads a string array from the XML parser.
          *
@@ -2187,8 +2311,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));
@@ -2196,14 +2320,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);
 
@@ -2212,8 +2337,8 @@
         }
 
         @Override
-        public State readFromXml(XmlPullParser parser, int version, Context context)
-                throws IOException, XmlPullParserException {
+        public State readFromXml(XmlPullParser parser, int version, Context context,
+                FeatureFlags telephonyFeatureFlags) throws IOException, XmlPullParserException {
             if (parser.getName().equals(CLASS_STATE)) {
                 State s = new State();
 
@@ -2229,7 +2354,8 @@
                             // assume there are no groups.
                             parser.nextTag();
                             PhoneAccountHandle phoneAccountHandle = sPhoneAccountHandleXml
-                                    .readFromXml(parser, s.versionNumber, context);
+                                    .readFromXml(parser, s.versionNumber, context,
+                                            telephonyFeatureFlags);
                             UserManager userManager = UserManager.get(context);
                             UserInfo primaryUser = userManager.getPrimaryUser();
                             if (primaryUser != null) {
@@ -2244,8 +2370,9 @@
                             int defaultAccountHandlesDepth = parser.getDepth();
                             while (XmlUtils.nextElementWithin(parser, defaultAccountHandlesDepth)) {
                                 DefaultPhoneAccountHandle accountHandle
-                                        = sDefaultPhoneAcountHandleXml
-                                        .readFromXml(parser, s.versionNumber, context);
+                                        = sDefaultPhoneAccountHandleXml
+                                        .readFromXml(parser, s.versionNumber, context,
+                                                telephonyFeatureFlags);
                                 if (accountHandle != null && s.accounts != null) {
                                     s.defaultOutgoingAccountHandles
                                             .put(accountHandle.userHandle, accountHandle);
@@ -2256,7 +2383,7 @@
                         int accountsDepth = parser.getDepth();
                         while (XmlUtils.nextElementWithin(parser, accountsDepth)) {
                             PhoneAccount account = sPhoneAccountXml.readFromXml(parser,
-                                    s.versionNumber, context);
+                                    s.versionNumber, context, telephonyFeatureFlags);
 
                             if (account != null && s.accounts != null) {
                                 s.accounts.add(account);
@@ -2271,7 +2398,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";
@@ -2281,7 +2408,7 @@
 
                 @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 long serialNumber = userManager.getSerialNumberForUser(o.userHandle);
@@ -2291,7 +2418,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);
                         }
@@ -2300,7 +2427,7 @@
 
                 @Override
                 public DefaultPhoneAccountHandle readFromXml(XmlPullParser parser, int version,
-                        Context context)
+                        Context context, FeatureFlags telephonyFeatureFlags)
                         throws IOException, XmlPullParserException {
                     if (parser.getName().equals(CLASS_DEFAULT_OUTGOING_PHONE_ACCOUNT_HANDLE)) {
                         int outerDepth = parser.getDepth();
@@ -2311,7 +2438,7 @@
                             if (parser.getName().equals(ACCOUNT_HANDLE)) {
                                 parser.nextTag();
                                 accountHandle = sPhoneAccountHandleXml.readFromXml(parser, version,
-                                        context);
+                                        context, telephonyFeatureFlags);
                             } else if (parser.getName().equals(USER_SERIAL_NUMBER)) {
                                 parser.next();
                                 userSerialNumberString = parser.getText();
@@ -2362,16 +2489,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);
                 }
 
@@ -2388,13 +2518,19 @@
                 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) throws IOException, XmlPullParserException {
             if (parser.getName().equals(CLASS_PHONE_ACCOUNT)) {
                 int outerDepth = parser.getDepth();
                 PhoneAccountHandle accountHandle = null;
@@ -2413,12 +2549,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);
                     } else if (parser.getName().equals(ADDRESS)) {
                         parser.next();
                         address = Uri.parse(parser.getText());
@@ -2463,6 +2600,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);
                     }
                 }
 
@@ -2544,6 +2687,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();
@@ -2575,8 +2721,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);
 
@@ -2598,8 +2744,8 @@
         }
 
         @Override
-        public PhoneAccountHandle readFromXml(XmlPullParser parser, int version, Context context)
-                throws IOException, XmlPullParserException {
+        public PhoneAccountHandle readFromXml(XmlPullParser parser, int version, Context context,
+                FeatureFlags telephonyFeatureFlags) throws IOException, XmlPullParserException {
             if (parser.getName().equals(CLASS_PHONE_ACCOUNT_HANDLE)) {
                 String componentNameString = null;
                 String idString = null;
@@ -2639,8 +2785,4 @@
             return null;
         }
     };
-
-    private String nullToEmpty(String str) {
-        return str == null ? "" : str;
-    }
 }
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/Ringer.java b/src/com/android/server/telecom/Ringer.java
index 3ec4ebe..e148ef5 100644
--- a/src/com/android/server/telecom/Ringer.java
+++ b/src/com/android/server/telecom/Ringer.java
@@ -44,6 +44,7 @@
 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;
@@ -413,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);
             }
 
@@ -447,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.
@@ -460,7 +470,7 @@
                         if (DEBUG_RINGER) {
                             Log.d(this, "Using ringtone defined vibration effect.");
                         }
-                        vibrationEffect = getVibrationEffectForRingtone(ringtone);
+                        vibrationEffect = getVibrationEffectForRingtone(ringtoneUri);
                     } else {
                         vibrationEffect = mDefaultVibrationEffect;
                     }
@@ -477,10 +487,10 @@
                 }
             };
             deferBlockOnRingingFuture = true;  // Run in vibrationLogic.
-            if (ringtoneSupplier != null) {
-                mRingtonePlayer.play(ringtoneSupplier, afterRingtoneLogic, isHfpDeviceAttached);
+            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
@@ -542,8 +552,7 @@
         }
     }
 
-    private VibrationEffect getVibrationEffectForRingtone(@NonNull Ringtone ringtone) {
-        Uri ringtoneUri = ringtone.getUri();
+    private VibrationEffect getVibrationEffectForRingtone(Uri ringtoneUri) {
         if (ringtoneUri == null) {
             return mDefaultVibrationEffect;
         }
diff --git a/src/com/android/server/telecom/RingtoneFactory.java b/src/com/android/server/telecom/RingtoneFactory.java
index 6bcfb4c..0e0b99f 100644
--- a/src/com/android/server/telecom/RingtoneFactory.java
+++ b/src/com/android/server/telecom/RingtoneFactory.java
@@ -34,6 +34,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import android.telecom.CallerInfo;
+import android.util.Pair;
 
 import java.util.List;
 
@@ -53,18 +54,7 @@
         mCallsManager = callsManager;
     }
 
-    /**
-     * 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();
@@ -106,18 +96,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 +121,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,12 +129,12 @@
         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) {
diff --git a/src/com/android/server/telecom/RoleManagerAdapter.java b/src/com/android/server/telecom/RoleManagerAdapter.java
index 8fdfb11..9f515e6 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 ac35b3d..33ec466 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 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().getString(R.string.system_bluetooth_stack);
+    }
+
     /**
      * Returns the application label that corresponds to the given package name
      *
diff --git a/src/com/android/server/telecom/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java
index bd2a975..9314d7e 100644
--- a/src/com/android/server/telecom/TelecomServiceImpl.java
+++ b/src/com/android/server/telecom/TelecomServiceImpl.java
@@ -238,6 +238,9 @@
                                                 callEventCallback, mCallsManager, call);
 
                         call.setTransactionServiceWrapper(serviceWrapper);
+                        if (mFeatureFlags.transactionalVideoState()) {
+                            call.setTransactionalCallSupportsVideoCalling(callAttributes);
+                        }
                         ICallControl clientCallControl = serviceWrapper.getICallControl();
 
                         if (clientCallControl == null) {
@@ -356,9 +359,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")) {
@@ -370,7 +392,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<>(
@@ -576,6 +602,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");
@@ -638,6 +711,7 @@
         public ParceledListSlice<PhoneAccountHandle> getAllPhoneAccountHandles() {
             try {
                 Log.startSession("TSI.gAPAH");
+
                 try {
                     enforceModifyPermission(
                             "getAllPhoneAccountHandles requires MODIFY_PHONE_STATE permission.");
@@ -656,7 +730,7 @@
                                 .getAllPhoneAccountHandles(callingUserHandle,
                                         crossUserAccess));
                     } catch (Exception e) {
-                        Log.e(this, e, "getAllPhoneAccounts");
+                        Log.e(this, e, "getAllPhoneAccountsHandles");
                         throw e;
                     } finally {
                         Binder.restoreCallingIdentity(token);
@@ -762,6 +836,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();
@@ -783,6 +860,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",
@@ -1567,7 +1651,7 @@
                                         && accountExtra != null && accountExtra.getBoolean(
                                         PhoneAccount.EXTRA_SKIP_CALL_FILTERING,
                                         false)) {
-                                    mCallsManager.getInCallController().bindToServices(null);
+                                    mCallsManager.getInCallController().bindToServices(null, false);
                                 }
                             }
                         } finally {
@@ -1920,7 +2004,7 @@
                 synchronized (mLock) {
                     long token = Binder.clearCallingIdentity();
                     try {
-                        BlockedNumberContract.SystemContract.endBlockSuppression(mContext);
+                        BlockedNumberContract.BlockedNumbers.endBlockSuppression(mContext);
                     } finally {
                         Binder.restoreCallingIdentity(token);
                     }
@@ -1963,8 +2047,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;
             }
 
@@ -1992,6 +2080,11 @@
                 pw.increaseIndent();
                 reflectAndPrintFlagConfigs(pw);
                 pw.decreaseIndent();
+
+                pw.println("TransactionManager: ");
+                pw.increaseIndent();
+                TransactionManager.getInstance().dump(pw);
+                pw.decreaseIndent();
             }
             if (isTimeLineView) {
                 Log.dumpEventsTimeline(pw);
@@ -2212,7 +2305,8 @@
             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();
@@ -2225,7 +2319,9 @@
                         for (UserHandle userHandle : userHandles) {
                             mCallsManager.getInCallController().unbindFromServices(userHandle);
                         }
-                    });
+                    } finally {
+                        Binder.restoreCallingIdentity(token);
+                    }
                 }
             } finally {
                 Log.endSession();
@@ -2302,11 +2398,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();
@@ -2465,23 +2564,34 @@
          * @param packageName    the package name of the app to check calls for.
          * @param userHandle     the user handle on which to check for calls.
          * @param callingPackage The caller's package name.
+         * @param detectForAllUsers indicates if calls should be detected across all users. If the
+         *                          caller does not have the ability to interact across users, get
+         *                          managed calls for the caller instead.
          * @return {@code true} if there are ongoing calls, {@code false} otherwise.
          */
         @Override
         public boolean isInSelfManagedCall(String packageName, UserHandle userHandle,
-                String callingPackage) {
+                String callingPackage, boolean detectForAllUsers) {
             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 (detectForAllUsers || (userHandle != null
+                        && !Binder.getCallingUserHandle().equals(userHandle))) {
+                    enforceInAppCrossUserPermission();
+                } else {
+                    // If INTERACT_ACROSS_USERS doesn't need to be enforced, ensure that the user
+                    // being checked is the caller.
+                    userHandle = Binder.getCallingUserHandle();
+                }
 
                 Log.startSession("TSI.iISMC", Log.getPackageAbbreviation(callingPackage));
                 synchronized (mLock) {
                     long token = Binder.clearCallingIdentity();
                     try {
-                        return mCallsManager.isInSelfManagedCall(packageName, userHandle);
+                        return mCallsManager.isInSelfManagedCallCrossUsers(
+                                packageName, userHandle, detectForAllUsers);
                     } finally {
                         Binder.restoreCallingIdentity(token);
                     }
@@ -2559,6 +2669,8 @@
     private TransactionManager mTransactionManager;
     private final TransactionalServiceRepository mTransactionalServiceRepository;
     private final FeatureFlags mFeatureFlags;
+    private final com.android.internal.telephony.flags.FeatureFlags mTelephonyFeatureFlags;
+
 
     public TelecomServiceImpl(
             Context context,
@@ -2570,6 +2682,7 @@
             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);
@@ -2578,6 +2691,12 @@
 
         mCallsManager = callsManager;
         mFeatureFlags = featureFlags;
+        if (telephonyFeatureFlags != null) {
+            mTelephonyFeatureFlags = telephonyFeatureFlags;
+        } else {
+            mTelephonyFeatureFlags =
+                    new com.android.internal.telephony.flags.FeatureFlagsImpl();
+        }
         mLock = lock;
         mPhoneAccountRegistrar = phoneAccountRegistrar;
         mUserCallIntentProcessorFactory = userCallIntentProcessorFactory;
@@ -2948,12 +3067,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) {
@@ -3047,7 +3178,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.
@@ -3248,4 +3378,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/TelecomSystem.java b/src/com/android/server/telecom/TelecomSystem.java
index 101cd2d..1d0300d 100644
--- a/src/com/android/server/telecom/TelecomSystem.java
+++ b/src/com/android/server/telecom/TelecomSystem.java
@@ -141,6 +141,7 @@
     private final TelecomServiceImpl mTelecomServiceImpl;
     private final ContactsAsyncHelper mContactsAsyncHelper;
     private final DialerCodeReceiver mDialerCodeReceiver;
+    private final FeatureFlags mFeatureFlags;
 
     private boolean mIsBootComplete = false;
 
@@ -229,8 +230,10 @@
             Executor asyncTaskExecutor,
             Executor asyncCallAudioTaskExecutor,
             BlockedNumbersAdapter blockedNumbersAdapter,
-            FeatureFlags featureFlags) {
+            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);
@@ -245,7 +248,7 @@
         try {
             mPhoneAccountRegistrar = new PhoneAccountRegistrar(mContext, mLock, defaultDialerCache,
                     packageName -> AppLabelProxy.Util.getAppLabel(
-                            mContext.getPackageManager(), packageName));
+                            mContext.getPackageManager(), packageName), null);
 
             mContactsAsyncHelper = contactsAsyncHelperFactory.create(
                     new ContactsAsyncHelper.ContentResolverAdapter() {
@@ -347,14 +350,22 @@
             ToastFactory toastFactory = new ToastFactory() {
                 @Override
                 public Toast makeText(Context context, int resId, int duration) {
-                    return Toast.makeText(context, context.getMainLooper(),
-                            context.getString(resId),
-                            duration);
+                    if (mFeatureFlags.telecomResolveHiddenDependencies()) {
+                        return Toast.makeText(context, resId, duration);
+                    } else {
+                        return Toast.makeText(context, context.getMainLooper(),
+                                context.getString(resId),
+                                duration);
+                    }
                 }
 
                 @Override
                 public Toast makeText(Context context, CharSequence text, int duration) {
-                    return Toast.makeText(context, context.getMainLooper(), text, duration);
+                    if (mFeatureFlags.telecomResolveHiddenDependencies()) {
+                        return Toast.makeText(context, text, duration);
+                    } else {
+                        return Toast.makeText(context, context.getMainLooper(), text, duration);
+                    }
                 }
             };
 
@@ -415,7 +426,9 @@
                     emergencyCallDiagnosticLogger,
                     communicationDeviceTracker,
                     callStreamingNotification,
+                    bluetoothDeviceManager,
                     featureFlags,
+                    telephonyFlags,
                     IncomingCallFilterGraph::new);
 
             mIncomingCallNotifier = incomingCallNotifier;
@@ -485,6 +498,7 @@
                     new TelecomServiceImpl.SubscriptionManagerAdapterImpl(),
                     new TelecomServiceImpl.SettingsSecureAdapterImpl(),
                     featureFlags,
+                    null,
                     mLock);
         } finally {
             Log.endSession();
diff --git a/src/com/android/server/telecom/Timeouts.java b/src/com/android/server/telecom/Timeouts.java
index c5fdd4c..abc7ff6 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);
         }
@@ -270,6 +274,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 02ccef7..d497c6a 100644
--- a/src/com/android/server/telecom/TransactionalServiceWrapper.java
+++ b/src/com/android/server/telecom/TransactionalServiceWrapper.java
@@ -43,9 +43,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;
@@ -70,6 +71,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 +131,7 @@
         return mTransactionManager;
     }
 
+    @VisibleForTesting
     public PhoneAccountHandle getPhoneAccountHandle() {
         return mPhoneAccountHandle;
     }
@@ -165,7 +168,7 @@
         return callCount;
     }
 
-    public void cleanupTransactionalServiceWrapper() {
+    private void cleanupTransactionalServiceWrapper() {
         for (Call call : mTrackedCalls.values()) {
             mCallsManager.markCallAsDisconnected(call,
                     new DisconnectCause(DisconnectCause.ERROR, "process died"));
@@ -178,7 +181,7 @@
      **                        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 {
@@ -225,6 +228,18 @@
         }
 
         @Override
+        public void setMuteState(boolean isMuted, android.os.ResultReceiver callback)
+                throws RemoteException {
+            try {
+                Log.startSession("TSW.sMS");
+                addTransactionsToManager(
+                        new SetMuteStateTransaction(mCallsManager, isMuted), callback);
+            } finally {
+                Log.endSession();
+            }
+        }
+
+        @Override
         public void startCallStreaming(String callId, android.os.ResultReceiver callback)
                 throws RemoteException {
             try {
@@ -235,6 +250,17 @@
             }
         }
 
+        @Override
+        public void requestVideoState(int videoState, String callId, ResultReceiver callback)
+                throws RemoteException {
+            try {
+                Log.startSession("TSW.rVS");
+                createTransactions(callId, callback, REQUEST_VIDEO_STATE, videoState);
+            } finally {
+                Log.endSession();
+            }
+        }
+
         private void createTransactions(String callId, ResultReceiver callback, String action,
                 Object... objects) {
             Log.d(TAG, "createTransactions: callId=" + callId);
@@ -261,6 +287,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();
@@ -333,7 +364,7 @@
         }
     };
 
-    public void addTransactionsToManager(VoipCallTransaction transaction,
+    private void addTransactionsToManager(VoipCallTransaction transaction,
             ResultReceiver callback) {
         Log.d(TAG, "addTransactionsToManager");
 
@@ -549,6 +580,15 @@
         }
     }
 
+    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 {
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
index 27e5a7d..a0ffe63 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
@@ -25,9 +25,8 @@
 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;
@@ -41,13 +40,17 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.concurrent.Executor;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.LinkedList;
 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.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 
 public class BluetoothDeviceManager {
 
@@ -98,6 +101,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) {
@@ -137,6 +143,9 @@
                             LinkedHashMap<String, BluetoothDevice> lostServiceDevices;
                             String logString;
                             if (profile == BluetoothProfile.HEADSET) {
+                                if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+                                    mBluetoothHeadsetFuture.complete(null);
+                                }
                                 mBluetoothHeadset = null;
                                 lostServiceDevices = mHfpDevicesByAddress;
                                 mBluetoothRouteManager.onActiveDeviceChanged(null,
@@ -201,6 +210,7 @@
 
     private BluetoothRouteManager mBluetoothRouteManager;
     private BluetoothHeadset mBluetoothHeadset;
+    private CompletableFuture<BluetoothHeadset> mBluetoothHeadsetFuture;
     private BluetoothHearingAid mBluetoothHearingAid;
     private boolean mLeAudioCallbackRegistered = false;
     private BluetoothLeAudio mBluetoothLeAudioService;
@@ -218,8 +228,12 @@
     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,
@@ -229,7 +243,6 @@
             mAudioManager = context.getSystemService(AudioManager.class);
             mExecutor = context.getMainExecutor();
             mCommunicationDeviceTracker = communicationDeviceTracker;
-            mFeatureFlags = featureFlags;
         }
     }
 
@@ -333,7 +346,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() {
@@ -402,7 +427,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;
                 }
@@ -465,7 +490,7 @@
     }
 
     public void disconnectSco() {
-        if (mBluetoothHeadset == null) {
+        if (getBluetoothHeadset() == null) {
             Log.w(this, "Trying to disconnect audio but no headset service exists.");
         } else {
             mBluetoothHeadset.disconnectAudio();
@@ -650,7 +675,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;
             }
@@ -707,9 +732,15 @@
                 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;
@@ -739,16 +770,20 @@
         // 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();
-        Log.i(this, "isInbandRingingEnabled: activeDevice: " + activeDevice);
-        if (mBluetoothRouteManager.isCachedLeAudioDevice(activeDevice)) {
+        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;
             }
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java b/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
index 235ba56..7da5339 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
@@ -957,6 +957,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);
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java b/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
index d2521ac..6d80cd5 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
@@ -16,6 +16,15 @@
 
 package com.android.server.telecom.bluetooth;
 
+import static com.android.server.telecom.CallAudioRouteAdapter.BT_ACTIVE_DEVICE_GONE;
+import static com.android.server.telecom.CallAudioRouteAdapter.BT_ACTIVE_DEVICE_PRESENT;
+import static com.android.server.telecom.CallAudioRouteAdapter.BT_AUDIO_CONNECTED;
+import static com.android.server.telecom.CallAudioRouteAdapter.BT_AUDIO_DISCONNECTED;
+import static com.android.server.telecom.CallAudioRouteAdapter.BT_DEVICE_ADDED;
+import static com.android.server.telecom.CallAudioRouteAdapter.BT_DEVICE_REMOVED;
+import static com.android.server.telecom.bluetooth.BluetoothRouteManager.BT_AUDIO_IS_ON;
+import static com.android.server.telecom.bluetooth.BluetoothRouteManager.BT_AUDIO_LOST;
+
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothHeadset;
@@ -32,11 +41,11 @@
 import android.telecom.Logging.Session;
 
 import com.android.internal.os.SomeArgs;
+import com.android.server.telecom.AudioRoute;
 import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
+import com.android.server.telecom.CallAudioRouteAdapter;
 import com.android.server.telecom.flags.FeatureFlags;
-
-import static com.android.server.telecom.bluetooth.BluetoothRouteManager.BT_AUDIO_IS_ON;
-import static com.android.server.telecom.bluetooth.BluetoothRouteManager.BT_AUDIO_LOST;
+import com.android.server.telecom.flags.Flags;
 
 
 public class BluetoothStateReceiver extends BroadcastReceiver {
@@ -61,6 +70,7 @@
     private final BluetoothDeviceManager mBluetoothDeviceManager;
     private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
     private FeatureFlags mFeatureFlags;
+    private CallAudioRouteAdapter mCallAudioRouteAdapter;
 
     public void onReceive(Context context, Intent intent) {
         Log.startSession("BSR.oR");
@@ -106,14 +116,24 @@
         args.arg2 = device.getAddress();
         switch (bluetoothHeadsetAudioState) {
             case BluetoothHeadset.STATE_AUDIO_CONNECTED:
-                if (!mIsInCall) {
-                    Log.i(LOG_TAG, "Ignoring BT audio on since we're not in a call");
-                    return;
+                if (Flags.useRefactoredAudioRouteSwitching()) {
+                    mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0,
+                            device);
+                } else {
+                    if (!mIsInCall) {
+                        Log.i(LOG_TAG, "Ignoring BT audio on since we're not in a call");
+                        return;
+                    }
+                    mBluetoothRouteManager.sendMessage(BT_AUDIO_IS_ON, args);
                 }
-                mBluetoothRouteManager.sendMessage(BT_AUDIO_IS_ON, args);
                 break;
             case BluetoothHeadset.STATE_AUDIO_DISCONNECTED:
-                mBluetoothRouteManager.sendMessage(BT_AUDIO_LOST, args);
+                if (Flags.useRefactoredAudioRouteSwitching()) {
+                    mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_AUDIO_DISCONNECTED, 0,
+                            device);
+                }  else {
+                    mBluetoothRouteManager.sendMessage(BT_AUDIO_LOST, args);
+                }
                 break;
         }
     }
@@ -131,12 +151,16 @@
         }
 
         int deviceType;
+        @AudioRoute.AudioRouteType int audioRouteType;
         if (BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED.equals(intent.getAction())) {
             deviceType = BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO;
+            audioRouteType = AudioRoute.TYPE_BLUETOOTH_LE;
         } else if (BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED.equals(intent.getAction())) {
             deviceType = BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID;
+            audioRouteType = AudioRoute.TYPE_BLUETOOTH_HA;
         } else if (BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED.equals(intent.getAction())) {
             deviceType = BluetoothDeviceManager.DEVICE_TYPE_HEADSET;
+            audioRouteType = AudioRoute.TYPE_BLUETOOTH_SCO;
         } else {
             Log.w(LOG_TAG, "handleConnectionStateChanged: %s invalid device type", device);
             return;
@@ -147,10 +171,20 @@
                 device.getAddress(), bluetoothHeadsetState);
 
         if (bluetoothHeadsetState == BluetoothProfile.STATE_CONNECTED) {
-            mBluetoothDeviceManager.onDeviceConnected(device, deviceType);
+            if (Flags.useRefactoredAudioRouteSwitching()) {
+                mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_DEVICE_ADDED,
+                        audioRouteType, device);
+            } else {
+                mBluetoothDeviceManager.onDeviceConnected(device, deviceType);
+            }
         } else if (bluetoothHeadsetState == BluetoothProfile.STATE_DISCONNECTED
                 || bluetoothHeadsetState == BluetoothProfile.STATE_DISCONNECTING) {
-            mBluetoothDeviceManager.onDeviceDisconnected(device, deviceType);
+            if (Flags.useRefactoredAudioRouteSwitching()) {
+                mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_DEVICE_REMOVED,
+                        audioRouteType, device);
+            } else {
+                mBluetoothDeviceManager.onDeviceDisconnected(device, deviceType);
+            }
         }
     }
 
@@ -159,12 +193,16 @@
                 intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice.class);
 
         int deviceType;
+        @AudioRoute.AudioRouteType int audioRouteType;
         if (BluetoothLeAudio.ACTION_LE_AUDIO_ACTIVE_DEVICE_CHANGED.equals(intent.getAction())) {
             deviceType = BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO;
+            audioRouteType = AudioRoute.TYPE_BLUETOOTH_LE;
         } else if (BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED.equals(intent.getAction())) {
             deviceType = BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID;
+            audioRouteType = AudioRoute.TYPE_BLUETOOTH_HA;
         } else if (BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED.equals(intent.getAction())) {
             deviceType = BluetoothDeviceManager.DEVICE_TYPE_HEADSET;
+            audioRouteType = AudioRoute.TYPE_BLUETOOTH_SCO;
         } else {
             Log.w(LOG_TAG, "handleActiveDeviceChanged: %s invalid device type", device);
             return;
@@ -173,72 +211,84 @@
         Log.i(LOG_TAG, "Device %s is now the preferred BT device for %s", device,
                 BluetoothDeviceManager.getDeviceTypeString(deviceType));
 
-        mBluetoothRouteManager.onActiveDeviceChanged(device, deviceType);
-        if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID ||
-            deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO) {
-            Session session = Log.createSubsession();
-            SomeArgs args = SomeArgs.obtain();
-            args.arg1 = session;
+        if (Flags.useRefactoredAudioRouteSwitching()) {
             if (device == null) {
-                mBluetoothRouteManager.sendMessage(BT_AUDIO_LOST, args);
+                mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_GONE,
+                        audioRouteType);
             } else {
-                if (!mIsInCall) {
-                    Log.i(LOG_TAG, "Ignoring audio on since we're not in a call");
-                    return;
-                }
-                args.arg2 = device.getAddress();
-
-                boolean usePreferredAudioProfile = false;
-                BluetoothAdapter bluetoothAdapter = mBluetoothDeviceManager.getBluetoothAdapter();
-                int preferredDuplexProfile = BluetoothProfile.LE_AUDIO;
-                if (bluetoothAdapter != null) {
-                    Bundle preferredAudioProfiles = bluetoothAdapter.getPreferredAudioProfiles(
-                            device);
-                    if (preferredAudioProfiles != null && !preferredAudioProfiles.isEmpty()
-                            && preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX)
-                            != 0) {
-                        Log.i(this, "Preferred duplex profile for device=" + device + " is "
-                                + preferredAudioProfiles.getInt(
-                                BluetoothAdapter.AUDIO_MODE_DUPLEX));
-                        usePreferredAudioProfile = true;
-                        preferredDuplexProfile =
-                                preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX);
-                    }
-                }
-
-                if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO) {
-                    /* In Le Audio case, once device got Active, the Telecom needs to make sure it
-                     * is set as communication device before we can say that BT_AUDIO_IS_ON
-                     */
-                    boolean isLeAudioSetForCommunication =
-                            mFeatureFlags.callAudioCommunicationDeviceRefactor()
-                                    ? mCommunicationDeviceTracker.setCommunicationDevice(
-                                    AudioDeviceInfo.TYPE_BLE_HEADSET, device)
-                                    : mBluetoothDeviceManager.setLeAudioCommunicationDevice();
-                    if ((!usePreferredAudioProfile
-                            || preferredDuplexProfile == BluetoothProfile.LE_AUDIO)
-                            && !isLeAudioSetForCommunication) {
-                        Log.w(LOG_TAG,
-                                "Device %s cannot be use as LE audio communication device.",
-                                device);
+                mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
+                        audioRouteType, device.getAddress());
+            }
+        } else {
+            mBluetoothRouteManager.onActiveDeviceChanged(device, deviceType);
+            if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID ||
+                    deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO) {
+                Session session = Log.createSubsession();
+                SomeArgs args = SomeArgs.obtain();
+                args.arg1 = session;
+                if (device == null) {
+                    mBluetoothRouteManager.sendMessage(BT_AUDIO_LOST, args);
+                } else {
+                    if (!mIsInCall) {
+                        Log.i(LOG_TAG, "Ignoring audio on since we're not in a call");
                         return;
                     }
-                } else {
-                    boolean isHearingAidSetForCommunication =
-                            mFeatureFlags.callAudioCommunicationDeviceRefactor()
-                            ? mCommunicationDeviceTracker.setCommunicationDevice(
-                                    AudioDeviceInfo.TYPE_HEARING_AID, null)
-                            : mBluetoothDeviceManager.setHearingAidCommunicationDevice();
-                    /* deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID */
-                    if (!isHearingAidSetForCommunication) {
-                        Log.w(LOG_TAG,
-                                "Device %s cannot be use as hearing aid communication device.",
+                    args.arg2 = device.getAddress();
+
+                    boolean usePreferredAudioProfile = false;
+                    BluetoothAdapter bluetoothAdapter = mBluetoothDeviceManager
+                            .getBluetoothAdapter();
+                    int preferredDuplexProfile = BluetoothProfile.LE_AUDIO;
+                    if (bluetoothAdapter != null) {
+                        Bundle preferredAudioProfiles = bluetoothAdapter.getPreferredAudioProfiles(
                                 device);
+                        if (preferredAudioProfiles != null && !preferredAudioProfiles.isEmpty()
+                                && preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX)
+                                != 0) {
+                            Log.i(this, "Preferred duplex profile for device=" + device + " is "
+                                    + preferredAudioProfiles.getInt(
+                                    BluetoothAdapter.AUDIO_MODE_DUPLEX));
+                            usePreferredAudioProfile = true;
+                            preferredDuplexProfile =
+                                    preferredAudioProfiles.getInt(
+                                            BluetoothAdapter.AUDIO_MODE_DUPLEX);
+                        }
+                    }
+
+                    if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO) {
+                        /* In Le Audio case, once device got Active, the Telecom needs to make sure
+                         * it is set as communication device before we can say that BT_AUDIO_IS_ON
+                         */
+                        boolean isLeAudioSetForCommunication =
+                                mFeatureFlags.callAudioCommunicationDeviceRefactor()
+                                        ? mCommunicationDeviceTracker.setCommunicationDevice(
+                                        AudioDeviceInfo.TYPE_BLE_HEADSET, device)
+                                        : mBluetoothDeviceManager.setLeAudioCommunicationDevice();
+                        if ((!usePreferredAudioProfile
+                                || preferredDuplexProfile == BluetoothProfile.LE_AUDIO)
+                                && !isLeAudioSetForCommunication) {
+                            Log.w(LOG_TAG,
+                                    "Device %s cannot be use as LE audio communication device.",
+                                    device);
+                        }
                     } else {
-                        mBluetoothRouteManager.sendMessage(BT_AUDIO_IS_ON, args);
+                        boolean isHearingAidSetForCommunication =
+                                mFeatureFlags.callAudioCommunicationDeviceRefactor()
+                                        ? mCommunicationDeviceTracker.setCommunicationDevice(
+                                        AudioDeviceInfo.TYPE_HEARING_AID, null)
+                                        : mBluetoothDeviceManager
+                                        .setHearingAidCommunicationDevice();
+                        /* deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID */
+                        if (!isHearingAidSetForCommunication) {
+                            Log.w(LOG_TAG,
+                                    "Device %s cannot be use as hearing aid communication device.",
+                                    device);
+                        } else {
+                            mBluetoothRouteManager.sendMessage(BT_AUDIO_IS_ON, args);
+                        }
                     }
                 }
-           }
+            }
         }
     }
 
@@ -259,4 +309,8 @@
     public void setIsInCall(boolean isInCall) {
         mIsInCall = isInCall;
     }
+
+    public void setCallAudioRouteAdapter(CallAudioRouteAdapter adapter) {
+        mCallAudioRouteAdapter = adapter;
+    }
 }
diff --git a/src/com/android/server/telecom/callfiltering/BlockCheckerAdapter.java b/src/com/android/server/telecom/callfiltering/BlockCheckerAdapter.java
index a83f314..1fda542 100644
--- a/src/com/android/server/telecom/callfiltering/BlockCheckerAdapter.java
+++ b/src/com/android/server/telecom/callfiltering/BlockCheckerAdapter.java
@@ -34,22 +34,24 @@
      *
      * @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();
 
         try {
-            blockStatus = BlockedNumberContract.SystemContract.shouldSystemBlockNumber(
-                    context, phoneNumber, extras);
+            blockStatus = BlockedNumberContract.BlockedNumbers.shouldSystemBlockNumber(
+                    context, phoneNumber, numberPresentation, isNumberInContacts);
             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 64060c8..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,7 +183,7 @@
     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)
@@ -143,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;
@@ -152,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:
@@ -174,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..f640826 100644
--- a/src/com/android/server/telecom/callfiltering/BlockedNumbersAdapter.java
+++ b/src/com/android/server/telecom/callfiltering/BlockedNumbersAdapter.java
@@ -20,7 +20,7 @@
 
 /**
  * Adapter interface that wraps methods from
- * {@link android.provider.BlockedNumberContract.SystemContract} and
+ * {@link android.provider.BlockedNumberContract.BlockedNumbers} and
  * {@link com.android.server.telecom.settings.BlockedNumbersUtil} to make things testable.
  */
 public interface BlockedNumbersAdapter {
diff --git a/src/com/android/server/telecom/components/AppUninstallBroadcastReceiver.java b/src/com/android/server/telecom/components/AppUninstallBroadcastReceiver.java
index 3a0d517..b7e5880 100644
--- a/src/com/android/server/telecom/components/AppUninstallBroadcastReceiver.java
+++ b/src/com/android/server/telecom/components/AppUninstallBroadcastReceiver.java
@@ -74,7 +74,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 9287d33..845f788 100644
--- a/src/com/android/server/telecom/components/TelecomService.java
+++ b/src/com/android/server/telecom/components/TelecomService.java
@@ -223,7 +223,7 @@
                                 @Override
                                 public boolean shouldShowEmergencyCallNotification(Context
                                         context) {
-                                    return BlockedNumberContract.SystemContract
+                                    return BlockedNumberContract.BlockedNumbers
                                             .shouldShowEmergencyCallNotification(context);
                                 }
 
@@ -234,7 +234,8 @@
                                             showNotification);
                                 }
                             },
-                            new FeatureFlagsImpl()));
+                            new FeatureFlagsImpl(),
+                            new com.android.internal.telephony.flags.FeatureFlagsImpl()));
         }
     }
 
diff --git a/src/com/android/server/telecom/settings/BlockedNumbersActivity.java b/src/com/android/server/telecom/settings/BlockedNumbersActivity.java
index 5fa5f06..819b270 100644
--- a/src/com/android/server/telecom/settings/BlockedNumbersActivity.java
+++ b/src/com/android/server/telecom/settings/BlockedNumbersActivity.java
@@ -155,7 +155,7 @@
             }
         };
         IntentFilter blockStatusIntentFilter = new IntentFilter(
-                BlockedNumberContract.SystemContract.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED);
+                BlockedNumberContract.BlockedNumbers.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED);
         blockStatusIntentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
         registerReceiver(mBlockingStatusReceiver, blockStatusIntentFilter,
                 Context.RECEIVER_EXPORTED);
@@ -183,7 +183,8 @@
     }
 
     private void updateButterBar() {
-        if (BlockedNumberContract.SystemContract.getBlockSuppressionStatus(this).isSuppressed) {
+        if (BlockedNumberContract.BlockedNumbers
+                .getBlockSuppressionStatus(this).getIsSuppressed()) {
             mButterBar.setVisibility(View.VISIBLE);
         } else {
             mButterBar.setVisibility(View.GONE);
@@ -238,7 +239,7 @@
         if (view == mAddButton) {
             showAddBlockedNumberDialog();
         } else if (view == mReEnableButton) {
-            BlockedNumberContract.SystemContract.endBlockSuppression(this);
+            BlockedNumberContract.BlockedNumbers.endBlockSuppression(this);
             mButterBar.setVisibility(View.GONE);
         }
     }
diff --git a/src/com/android/server/telecom/settings/BlockedNumbersUtil.java b/src/com/android/server/telecom/settings/BlockedNumbersUtil.java
index 4be75f8..e0fe81e 100644
--- a/src/com/android/server/telecom/settings/BlockedNumbersUtil.java
+++ b/src/com/android/server/telecom/settings/BlockedNumbersUtil.java
@@ -23,7 +23,7 @@
 import android.content.Intent;
 import android.os.PersistableBundle;
 import android.os.UserHandle;
-import android.provider.BlockedNumberContract.SystemContract;
+import android.provider.BlockedNumberContract.BlockedNumbers;
 import android.telephony.CarrierConfigManager;
 import android.telephony.PhoneNumberUtils;
 import android.text.BidiFormatter;
@@ -148,8 +148,8 @@
      * @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) {
+        return BlockedNumbers.getBlockedNumberSetting(context, key);
     }
 
     /**
@@ -159,7 +159,7 @@
      * @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) {
+        BlockedNumbers.setBlockedNumberSetting(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..35b7f70 100644
--- a/src/com/android/server/telecom/settings/CallBlockDisabledActivity.java
+++ b/src/com/android/server/telecom/settings/CallBlockDisabledActivity.java
@@ -58,9 +58,9 @@
                 .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
                     @Override
                     public void onClick(DialogInterface dialog, int which) {
-                        BlockedNumbersUtil.setEnhancedBlockSetting(
+                        BlockedNumbersUtil.setBlockedNumberSetting(
                                 CallBlockDisabledActivity.this,
-                                BlockedNumberContract.SystemContract
+                                BlockedNumberContract.BlockedNumbers
                                         .ENHANCED_SETTING_KEY_SHOW_EMERGENCY_CALL_NOTIFICATION,
                                 false);
                         BlockedNumbersUtil.updateEmergencyCallNotification(
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..7ea8926 100644
--- a/src/com/android/server/telecom/settings/EnhancedCallBlockingFragment.java
+++ b/src/com/android/server/telecom/settings/EnhancedCallBlockingFragment.java
@@ -23,12 +23,11 @@
 import android.preference.PreferenceFragment;
 import android.preference.PreferenceScreen;
 import android.preference.SwitchPreference;
-import android.provider.BlockedNumberContract.SystemContract;
+import android.provider.BlockedNumberContract.BlockedNumbers;
 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;
 
@@ -54,13 +53,14 @@
 
         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(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_UNREGISTERED);
+        setOnPreferenceChangeListener(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_PRIVATE);
+        setOnPreferenceChangeListener(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_PAYPHONE);
+        setOnPreferenceChangeListener(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_UNKNOWN);
+        setOnPreferenceChangeListener(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_UNAVAILABLE);
         if (!showPayPhoneBlocking()) {
-            Preference payPhoneOption = getPreferenceScreen().findPreference(SystemContract.ENHANCED_SETTING_KEY_BLOCK_PAYPHONE);
+            Preference payPhoneOption = getPreferenceScreen()
+                    .findPreference(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_PAYPHONE);
             getPreferenceScreen().removePreference(payPhoneOption);
         }
     }
@@ -122,13 +122,13 @@
     public void onResume() {
         super.onResume();
 
-        updateEnhancedBlockPref(SystemContract.ENHANCED_SETTING_KEY_BLOCK_UNREGISTERED);
-        updateEnhancedBlockPref(SystemContract.ENHANCED_SETTING_KEY_BLOCK_PRIVATE);
+        updateEnhancedBlockPref(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_UNREGISTERED);
+        updateEnhancedBlockPref(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_PRIVATE);
         if (showPayPhoneBlocking()) {
-            updateEnhancedBlockPref(SystemContract.ENHANCED_SETTING_KEY_BLOCK_PAYPHONE);
+            updateEnhancedBlockPref(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_PAYPHONE);
         }
-        updateEnhancedBlockPref(SystemContract.ENHANCED_SETTING_KEY_BLOCK_UNKNOWN);
-        updateEnhancedBlockPref(SystemContract.ENHANCED_SETTING_KEY_BLOCK_UNAVAILABLE);
+        updateEnhancedBlockPref(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_UNKNOWN);
+        updateEnhancedBlockPref(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_UNAVAILABLE);
     }
 
     /**
@@ -137,7 +137,7 @@
     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));
         }
     }
 
@@ -147,18 +147,18 @@
             if (mIsCombiningRestrictedAndUnknownOption) {
                 Log.i(this, "onPreferenceChange: changing %s and %s to %b",
                         preference.getKey(), BLOCK_RESTRICTED_NUMBERS_KEY, (boolean) objValue);
-                BlockedNumbersUtil.setEnhancedBlockSetting(getActivity(),
+                BlockedNumbersUtil.setBlockedNumberSetting(getActivity(),
                         BLOCK_RESTRICTED_NUMBERS_KEY, (boolean) objValue);
             }
 
             if (mIsCombiningUnavailableAndUnknownOption) {
                 Log.i(this, "onPreferenceChange: changing %s and %s to %b",
                         preference.getKey(), BLOCK_UNAVAILABLE_NUMBERS_KEY, (boolean) objValue);
-                BlockedNumbersUtil.setEnhancedBlockSetting(getActivity(),
+                BlockedNumbersUtil.setBlockedNumberSetting(getActivity(),
                         BLOCK_UNAVAILABLE_NUMBERS_KEY, (boolean) objValue);
             }
         }
-        BlockedNumbersUtil.setEnhancedBlockSetting(getActivity(), preference.getKey(),
+        BlockedNumbersUtil.setBlockedNumberSetting(getActivity(), preference.getKey(),
                 (boolean) objValue);
         return true;
     }
diff --git a/src/com/android/server/telecom/voip/ParallelTransaction.java b/src/com/android/server/telecom/voip/ParallelTransaction.java
index 6176087..621892a 100644
--- a/src/com/android/server/telecom/voip/ParallelTransaction.java
+++ b/src/com/android/server/telecom/voip/ParallelTransaction.java
@@ -34,6 +34,7 @@
 
     @Override
     public void start() {
+        if (mStats != null) mStats.markStarted();
         // post timeout work
         CompletableFuture<Void> future = new CompletableFuture<>();
         mHandler.postDelayed(() -> future.complete(null), TIMEOUT_LIMIT);
@@ -44,7 +45,7 @@
             if (mCompleteListener != null) {
                 mCompleteListener.onTransactionTimeout(mTransactionName);
             }
-            finish();
+            timeout();
             return null;
         }, new LoggedHandlerExecutor(mHandler, mTransactionName + "@" + hashCode()
                 + ".s", mLock));
@@ -68,7 +69,7 @@
                                                                     transactionName));
                                             mCompleteListener.onTransactionCompleted(mainResult,
                                                     mTransactionName);
-                                            finish();
+                                            finish(mainResult);
                                             return null;
                                         }, new LoggedHandlerExecutor(mHandler,
                                                 mTransactionName + "@" + hashCode()
@@ -91,7 +92,7 @@
                                                         transactionName));
                                         mCompleteListener.onTransactionCompleted(mainResult,
                                                 mTransactionName);
-                                        finish();
+                                        finish(mainResult);
                                         return null;
                                     }, new LoggedHandlerExecutor(mHandler,
                                             mTransactionName + "@" + hashCode()
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..64596b1
--- /dev/null
+++ b/src/com/android/server/telecom/voip/RequestVideoStateTransaction.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.voip;
+
+import static com.android.server.telecom.voip.VideoStateTranslation.TransactionalVideoStateToVideoProfileState;
+
+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(
+                    VoipCallTransactionResult.RESULT_FAILED,
+                    "Video calling is not supported by the target account"));
+        } else if (isRequestingVideoTransmission(mVideoProfileState) &&
+                !mCall.isTransactionalCallSupportsVideoCalling()) {
+            future.complete(new VoipCallTransactionResult(
+                    VoipCallTransactionResult.RESULT_FAILED,
+                    "Video calling is not supported according to the callAttributes"));
+        } 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..7d5a178 100644
--- a/src/com/android/server/telecom/voip/SerialTransaction.java
+++ b/src/com/android/server/telecom/voip/SerialTransaction.java
@@ -21,6 +21,7 @@
 
 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
@@ -37,6 +38,7 @@
 
     @Override
     public void start() {
+        if (mStats != null) mStats.markStarted();
         // post timeout work
         CompletableFuture<Void> future = new CompletableFuture<>();
         mHandler.postDelayed(() -> future.complete(null), TIMEOUT_LIMIT);
@@ -47,7 +49,7 @@
             if (mCompleteListener != null) {
                 mCompleteListener.onTransactionTimeout(mTransactionName);
             }
-            finish();
+            timeout();
             return null;
         }, new LoggedHandlerExecutor(mHandler, mTransactionName + "@" + hashCode()
                 + ".s", mLock));
@@ -55,6 +57,7 @@
         if (mSubTransactions != null && mSubTransactions.size() > 0) {
             TransactionManager.TransactionCompleteListener subTransactionListener =
                     new TransactionManager.TransactionCompleteListener() {
+                        private final AtomicInteger mTransactionIndex = new AtomicInteger(0);
 
                         @Override
                         public void onTransactionCompleted(VoipCallTransactionResult result,
@@ -71,14 +74,16 @@
                                                                     transactionName));
                                             mCompleteListener.onTransactionCompleted(mainResult,
                                                     mTransactionName);
-                                            finish();
+                                            finish(mainResult);
                                             return null;
                                         }, new LoggedHandlerExecutor(mHandler,
                                                 mTransactionName + "@" + hashCode()
                                                         + ".oTC", mLock));
                             } else {
-                                if (mSubTransactions.size() > 0) {
-                                    VoipCallTransaction transaction = mSubTransactions.remove(0);
+                                int currTransactionIndex = mTransactionIndex.incrementAndGet();
+                                if (currTransactionIndex < mSubTransactions.size()) {
+                                    VoipCallTransaction transaction = mSubTransactions.get(
+                                            currTransactionIndex);
                                     transaction.setCompleteListener(this);
                                     transaction.start();
                                 } else {
@@ -99,14 +104,14 @@
                                                         transactionName));
                                         mCompleteListener.onTransactionCompleted(mainResult,
                                                 mTransactionName);
-                                        finish();
+                                        finish(mainResult);
                                         return null;
                                     }, new LoggedHandlerExecutor(mHandler,
                                             mTransactionName + "@" + hashCode()
                                                     + ".oTT", mLock));
                         }
                     };
-            VoipCallTransaction transaction = mSubTransactions.remove(0);
+            VoipCallTransaction transaction = mSubTransactions.get(0);
             transaction.setCompleteListener(subTransactionListener);
             transaction.start();
         } else {
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..299bcc3 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(VoipCallTransactionResult.RESULT_FAILED,
+                    "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/VideoStateTranslation.java b/src/com/android/server/telecom/voip/VideoStateTranslation.java
new file mode 100644
index 0000000..615e4bc
--- /dev/null
+++ b/src/com/android/server/telecom/voip/VideoStateTranslation.java
@@ -0,0 +1,94 @@
+/*
+ * 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;
+
+/**
+ * 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 transactionalVideo) {
+        if (transactionalVideo == CallAttributes.AUDIO_CALL) {
+            Log.i(TAG, "%s --> VideoProfile.STATE_AUDIO_ONLY",
+                    TransactionalVideoState_toString(transactionalVideo));
+            return VideoProfile.STATE_AUDIO_ONLY;
+        } else {
+            Log.i(TAG, "%s --> VideoProfile.STATE_BIDIRECTIONAL",
+                    TransactionalVideoState_toString(transactionalVideo));
+            return VideoProfile.STATE_BIDIRECTIONAL;
+        }
+    }
+
+    /**
+     * Telecom --> Client
+     * This should be used when Telecom is informing the client of a video state change.
+     */
+    public static int VideoProfileStateToTransactionalVideoState(int videoProfileState) {
+        if (videoProfileState == VideoProfile.STATE_AUDIO_ONLY) {
+            Log.i(TAG, "%s --> CallAttributes.AUDIO_CALL",
+                    VideoProfileState_toString(videoProfileState));
+            return CallAttributes.AUDIO_CALL;
+        } else {
+            Log.i(TAG, "%s --> CallAttributes.VIDEO_CALL",
+                    VideoProfileState_toString(videoProfileState));
+            return CallAttributes.VIDEO_CALL;
+        }
+    }
+
+    private static String TransactionalVideoState_toString(int transactionalVideoState) {
+        if (transactionalVideoState == CallAttributes.AUDIO_CALL) {
+            return "CallAttributes.AUDIO_CALL";
+        } else {
+            return "CallAttributes.VIDEO_CALL";
+        }
+    }
+
+    private static String VideoProfileState_toString(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";
+            }
+        }
+        return "VideoProfile.STATE_AUDIO_ONLY";
+    }
+}
diff --git a/src/com/android/server/telecom/voip/VoipCallTransaction.java b/src/com/android/server/telecom/voip/VoipCallTransaction.java
index a952eb1..3c91158 100644
--- a/src/com/android/server/telecom/voip/VoipCallTransaction.java
+++ b/src/com/android/server/telecom/voip/VoipCallTransaction.java
@@ -22,23 +22,119 @@
 
 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;
+
+    /**
+     * 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();
+    protected final String mTransactionName = this.getClass().getSimpleName();
     private HandlerThread mHandlerThread;
     protected Handler mHandler;
     protected TransactionManager.TransactionCompleteListener mCompleteListener;
     protected List<VoipCallTransaction> mSubTransactions;
     protected TelecomSystem.SyncRoot mLock;
+    protected final Stats mStats;
 
     public VoipCallTransaction(
             List<VoipCallTransaction> subTransactions, TelecomSystem.SyncRoot lock) {
@@ -47,6 +143,7 @@
         mHandlerThread.start();
         mHandler = new Handler(mHandlerThread.getLooper());
         mLock = lock;
+        mStats = Flags.enableCallSequencing() ? new Stats() : null;
     }
 
     public VoipCallTransaction(TelecomSystem.SyncRoot lock) {
@@ -54,6 +151,7 @@
     }
 
     public void start() {
+        if (mStats != null) mStats.markStarted();
         // post timeout work
         CompletableFuture<Void> future = new CompletableFuture<>();
         mHandler.postDelayed(() -> future.complete(null), TIMEOUT_LIMIT);
@@ -64,7 +162,7 @@
             if (mCompleteListener != null) {
                 mCompleteListener.onTransactionTimeout(mTransactionName);
             }
-            finish();
+            timeout();
             return null;
         }, new LoggedHandlerExecutor(mHandler, mTransactionName + "@" + hashCode()
                 + ".s", mLock));
@@ -82,7 +180,7 @@
                     if (mCompleteListener != null) {
                         mCompleteListener.onTransactionCompleted(result, mTransactionName);
                     }
-                    finish();
+                    finish(result);
                     return null;
                     }, executor)
                 .exceptionallyAsync((throwable -> {
@@ -100,11 +198,27 @@
         mCompleteListener = listener;
     }
 
-    public void finish() {
+    public void timeout() {
+        finish(true, null);
+    }
+
+    public void finish(VoipCallTransactionResult result) {
+        finish(false, result);
+    }
+
+    public void finish(boolean isTimedOut, VoipCallTransactionResult result) {
+        if (mStats != null) mStats.markComplete(isTimedOut, result);
         // finish all sub transactions
-        if (mSubTransactions != null && mSubTransactions.size() > 0) {
-            mSubTransactions.forEach(VoipCallTransaction::finish);
+        if (mSubTransactions != null && !mSubTransactions.isEmpty()) {
+            mSubTransactions.forEach( t -> t.finish(isTimedOut, result));
         }
         mHandlerThread.quit();
     }
+
+    /**
+     * @return Stats related to this transaction if stats are enabled, null otherwise.
+     */
+    public Stats getStats() {
+        return mStats;
+    }
 }
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/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/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 54aedc4..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());
     }
diff --git a/tests/src/com/android/server/telecom/tests/BasicCallTests.java b/tests/src/com/android/server/telecom/tests/BasicCallTests.java
index 0b9ee65..4bca30d 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;
@@ -52,6 +54,7 @@
 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;
@@ -62,16 +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 com.android.server.telecom.InCallController;
-
-import android.telecom.CallerInfo;
 
 import com.google.common.base.Predicate;
 
@@ -889,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;
             }
         });
@@ -1037,7 +1036,6 @@
         call.setTargetPhoneAccount(mPhoneAccountA1.getAccountHandle());
         assert(call.isVideoCallingSupportedByPhoneAccount());
         assertEquals(VideoProfile.STATE_BIDIRECTIONAL, call.getVideoState());
-        call.setIsCreateConnectionComplete(true);
     }
 
     /**
@@ -1061,7 +1059,6 @@
         call.setTargetPhoneAccount(mPhoneAccountA2.getAccountHandle());
         assert(!call.isVideoCallingSupportedByPhoneAccount());
         assertEquals(VideoProfile.STATE_AUDIO_ONLY, call.getVideoState());
-        call.setIsCreateConnectionComplete(true);
     }
 
     /**
diff --git a/tests/src/com/android/server/telecom/tests/BlockCheckerFilterTest.java b/tests/src/com/android/server/telecom/tests/BlockCheckerFilterTest.java
index a98c1ee..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;
 
@@ -95,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);
@@ -107,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);
@@ -121,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);
@@ -133,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..696867e 100644
--- a/tests/src/com/android/server/telecom/tests/BlockedNumbersUtilTests.java
+++ b/tests/src/com/android/server/telecom/tests/BlockedNumbersUtilTests.java
@@ -24,7 +24,8 @@
 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.BlockedNumbersUtil;
 
diff --git a/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java b/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java
index e3d4ec2..c516c8e 100644
--- a/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java
@@ -18,6 +18,21 @@
 
 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;
@@ -29,7 +44,8 @@
 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;
@@ -44,23 +60,8 @@
 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.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 java.util.Arrays;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 
 @RunWith(JUnit4.class)
@@ -402,7 +403,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();
diff --git a/tests/src/com/android/server/telecom/tests/BluetoothRouteManagerTest.java b/tests/src/com/android/server/telecom/tests/BluetoothRouteManagerTest.java
index e1ef08a..07dd350 100644
--- a/tests/src/com/android/server/telecom/tests/BluetoothRouteManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/BluetoothRouteManagerTest.java
@@ -18,9 +18,9 @@
 
 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.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -36,9 +36,8 @@
 import android.content.ContentResolver;
 import android.os.Parcel;
 import android.telecom.Log;
-import android.test.suitebuilder.annotation.SmallTest;
 
-import android.media.AudioDeviceInfo;
+import androidx.test.filters.SmallTest;
 
 import com.android.internal.os.SomeArgs;
 import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
diff --git a/tests/src/com/android/server/telecom/tests/BluetoothRouteTransitionTests.java b/tests/src/com/android/server/telecom/tests/BluetoothRouteTransitionTests.java
index 65854af..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,7 +40,8 @@
 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;
@@ -46,23 +62,8 @@
 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 {
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioManagerTest.java b/tests/src/com/android/server/telecom/tests/CallAudioManagerTest.java
index 0a75c3a..97405a3 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioManagerTest.java
@@ -16,22 +16,40 @@
 
 package com.android.server.telecom.tests;
 
+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.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;
@@ -51,23 +69,6 @@
 import java.util.List;
 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.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;
-
 @RunWith(JUnit4.class)
 public class CallAudioManagerTest extends TelecomTestCase {
     @Mock private CallAudioRouteStateMachine mCallAudioRouteStateMachine;
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioModeStateMachineTest.java b/tests/src/com/android/server/telecom/tests/CallAudioModeStateMachineTest.java
index cddf2ad..4513c65 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioModeStateMachineTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioModeStateMachineTest.java
@@ -18,6 +18,7 @@
 
 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;
@@ -33,7 +34,8 @@
 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;
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioModeTransitionTests.java b/tests/src/com/android/server/telecom/tests/CallAudioModeTransitionTests.java
index 3690d5f..844a216 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioModeTransitionTests.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioModeTransitionTests.java
@@ -16,13 +16,20 @@
 
 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 android.media.AudioFocusRequest;
+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;
@@ -41,16 +48,6 @@
 import java.util.Collection;
 import java.util.List;
 
-import static org.junit.Assert.assertEquals;
-import static org.mockito.ArgumentMatchers.any;
-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 {
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..0a53eb0
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
@@ -0,0 +1,483 @@
+/*
+ * 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_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.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_EARPIECE;
+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.atLeast;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.bluetooth.BluetoothDevice;
+import android.media.AudioDeviceInfo;
+import android.media.AudioManager;
+import android.media.IAudioService;
+import android.media.audiopolicy.AudioProductStrategy;
+import android.os.UserHandle;
+import android.telecom.CallAudioState;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.server.telecom.AudioRoute;
+import com.android.server.telecom.CallAudioManager;
+import com.android.server.telecom.CallAudioRouteController;
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.WiredHeadsetManager;
+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.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;
+    private AudioRoute mEarpieceRoute;
+    private AudioRoute mSpeakerRoute;
+    private static final String BT_ADDRESS_1 = "00:00:00:00:00:01";
+    private static final BluetoothDevice BLUETOOTH_DEVICE_1 =
+            BluetoothRouteManagerTest.makeBluetoothDevice("00:00:00:00:00:01");
+    private static final Set<BluetoothDevice> BLUETOOTH_DEVICES;
+    static {
+        BLUETOOTH_DEVICES = new HashSet<>();
+        BLUETOOTH_DEVICES.add(BLUETOOTH_DEVICE_1);
+    }
+    private static final int TEST_TIMEOUT = 500;
+    AudioRoute.Factory mAudioRouteFactory = new AudioRoute.Factory() {
+        @Override
+        public AudioRoute create(@AudioRoute.AudioRouteType int type, String bluetoothAddress,
+                                 AudioManager audioManager) {
+            return new AudioRoute(type, bluetoothAddress, null);
+        }
+    };
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        when(mWiredHeadsetManager.isPluggedIn()).thenReturn(false);
+        when(mEarpieceDeviceInfo.getType()).thenReturn(AudioDeviceInfo.TYPE_BUILTIN_EARPIECE);
+        when(mAudioManager.getDevices(eq(AudioManager.GET_DEVICES_OUTPUTS))).thenReturn(
+                new AudioDeviceInfo[] {
+                        mEarpieceDeviceInfo
+                });
+        when(mAudioManager.getPreferredDeviceForStrategy(nullable(AudioProductStrategy.class)))
+                .thenReturn(null);
+        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));
+        mController = new CallAudioRouteController(mContext, mCallsManager, mAudioServiceFactory,
+                mAudioRouteFactory, mWiredHeadsetManager, mBluetoothRouteManager);
+        mController.setAudioRouteFactory(mAudioRouteFactory);
+        mController.setAudioManager(mAudioManager);
+        mEarpieceRoute = new AudioRoute(AudioRoute.TYPE_EARPIECE, null, null);
+        mSpeakerRoute = new AudioRoute(AudioRoute.TYPE_SPEAKER, null, null);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mController.getAdapterHandler().getLooper().quit();
+        mController.getAdapterHandler().getLooper().getThread().join();
+        super.tearDown();
+    }
+
+    @SmallTest
+    @Test
+    public void testInitializeWithEarpiece() {
+        mController.initialize();
+        assertEquals(mEarpieceRoute, mController.getCurrentRoute());
+        assertEquals(2, mController.getAvailableRoutes().size());
+        assertTrue(mController.getAvailableRoutes().contains(mSpeakerRoute));
+    }
+
+    @SmallTest
+    @Test
+    public void testInitializeWithoutEarpiece() {
+        when(mAudioManager.getDevices(eq(AudioManager.GET_DEVICES_OUTPUTS))).thenReturn(
+                new AudioDeviceInfo[] {});
+
+        mController.initialize();
+        assertEquals(mSpeakerRoute, mController.getCurrentRoute());
+    }
+
+    @SmallTest
+    @Test
+    public void testActivateAndRemoveBluetoothDeviceDuringCall() {
+        doAnswer(invocation -> {
+            mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0, BLUETOOTH_DEVICE_1);
+            return true;
+        }).when(mAudioManager).setCommunicationDevice(nullable(AudioDeviceInfo.class));
+
+        mController.initialize();
+        mController.setActive(true);
+        mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
+                BLUETOOTH_DEVICE_1);
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, null, BLUETOOTH_DEVICES);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        mController.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
+                AudioRoute.TYPE_BLUETOOTH_SCO, BT_ADDRESS_1);
+        verify(mAudioManager, timeout(TEST_TIMEOUT)).setCommunicationDevice(
+                nullable(AudioDeviceInfo.class));
+
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        mController.sendMessageWithSessionInfo(BT_DEVICE_REMOVED, AudioRoute.TYPE_BLUETOOTH_SCO,
+                BLUETOOTH_DEVICE_1);
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+    }
+
+    @SmallTest
+    @Test
+    public void testActiveDeactivateBluetoothDevice() {
+        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() {
+        doAnswer(invocation -> {
+            mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0, BLUETOOTH_DEVICE_1);
+            return true;
+        }).when(mAudioManager).setCommunicationDevice(nullable(AudioDeviceInfo.class));
+        doAnswer(invocation -> {
+            mController.sendMessageWithSessionInfo(BT_AUDIO_DISCONNECTED, 0, BLUETOOTH_DEVICE_1);
+            return true;
+        }).when(mAudioManager).clearCommunicationDevice();
+        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(mAudioManager, timeout(TEST_TIMEOUT)).setCommunicationDevice(
+                nullable(AudioDeviceInfo.class));
+        assertTrue(mController.isActive());
+
+        mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS);
+        assertTrue(mController.isActive());
+
+        mController.sendMessageWithSessionInfo(SWITCH_FOCUS, NO_FOCUS);
+        verify(mAudioManager, timeout(TEST_TIMEOUT)).clearCommunicationDevice();
+        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.isMasterMute()).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.isMasterMute()).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));
+    }
+}
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRoutePeripheralAdapterTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRoutePeripheralAdapterTest.java
index 2fc6ec6..79247be 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRoutePeripheralAdapterTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRoutePeripheralAdapterTest.java
@@ -24,7 +24,7 @@
 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;
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
index 1fa14a5..d2da505 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
@@ -17,15 +17,14 @@
 package com.android.server.telecom.tests;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.anyString;
 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.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;
@@ -47,8 +46,9 @@
 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 androidx.test.filters.MediumTest;
+import androidx.test.filters.SmallTest;
 
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteTransitionTests.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteTransitionTests.java
index 25c4e9f..6b9b5c8 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRouteTransitionTests.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteTransitionTests.java
@@ -24,7 +24,6 @@
 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;
@@ -38,7 +37,8 @@
 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;
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/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/CallLogManagerTest.java b/tests/src/com/android/server/telecom/tests/CallLogManagerTest.java
index c09d138..fa35f25 100644
--- a/tests/src/com/android/server/telecom/tests/CallLogManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallLogManagerTest.java
@@ -16,6 +16,8 @@
 
 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;
@@ -23,7 +25,6 @@
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 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.mock;
@@ -46,6 +47,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;
@@ -61,10 +63,10 @@
 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;
@@ -89,6 +91,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 {
@@ -864,35 +869,33 @@
 
     @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);
-
-        String resultIso = mCallLogManager.getCountryIso();
-
-        verifyCountryIso(mockDetector, resultIso);
-    }
-
-    @SmallTest
-    @Test
     public void testCountryIso_newCountryDetected() {
         Country testCountry = new Country(TEST_ISO, Country.COUNTRY_SOURCE_LOCALE);
         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
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..8210686 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;
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 7a77374..e06938d 100644
--- a/tests/src/com/android/server/telecom/tests/CallTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallTest.java
@@ -51,10 +51,10 @@
 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.Call;
 import com.android.server.telecom.CallIdMapper;
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 6e0e660..cb9aba9 100644
--- a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
@@ -17,8 +17,10 @@
 package com.android.server.telecom.tests;
 
 import static android.provider.CallLog.Calls.USER_MISSED_NOT_RUNNING;
+
 import static junit.framework.Assert.assertNotNull;
 import static junit.framework.TestCase.fail;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
@@ -34,6 +36,7 @@
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
@@ -41,6 +44,7 @@
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
+
 import static java.lang.Thread.sleep;
 
 import android.Manifest;
@@ -53,7 +57,6 @@
 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 +64,6 @@
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.os.UserManager;
-import android.platform.test.flag.junit.SetFlagsRule;
 import android.provider.BlockedNumberContract;
 import android.telecom.CallException;
 import android.telecom.CallScreeningService;
@@ -76,12 +78,13 @@
 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 com.android.internal.telecom.IConnectionService;
+import androidx.test.filters.MediumTest;
+import androidx.test.filters.SmallTest;
+
 import com.android.server.telecom.AnomalyReporterAdapter;
 import com.android.server.telecom.AsyncRingtonePlayer;
 import com.android.server.telecom.Call;
@@ -100,7 +103,6 @@
 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;
@@ -123,27 +125,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.callfiltering.IncomingCallFilterGraph;
 import com.android.server.telecom.flags.FeatureFlags;
-import com.android.server.telecom.flags.Flags;
+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.Rule;
 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;
@@ -153,10 +157,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 {
@@ -172,6 +178,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(
@@ -205,6 +215,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)
@@ -286,11 +308,10 @@
     @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 IConnectionService mIConnectionService;
-    @Rule public SetFlagsRule mSetRlagsRule = new SetFlagsRule();
     private CallsManager mCallsManager;
 
     @Override
@@ -365,7 +386,9 @@
                 mEmergencyCallDiagnosticLogger,
                 mCommunicationDeviceTracker,
                 mCallStreamingNotification,
+                mBluetoothDeviceManager,
                 mFeatureFlags,
+                mTelephonyFlags,
                 (call, listener, context, timeoutsAdapter, lock) -> mIncomingCallFilterGraph);
 
         when(mPhoneAccountRegistrar.getPhoneAccount(
@@ -375,28 +398,40 @@
         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(mIConnectionService.asBinder()).thenReturn(mock(IBinder.class));
-
-        mComponentContextFixture.addConnectionService(
-                SIM_1_ACCOUNT.getAccountHandle().getComponentName(), mIConnectionService);
+        when(mFeatureFlags.separatelyBindToBtIncallService()).thenReturn(false);
     }
 
     @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) {
@@ -420,6 +455,7 @@
         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.
@@ -428,32 +464,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
@@ -462,6 +593,7 @@
     @Test
     public void testConstructPossiblePhoneAccountsMultiSimActive_dsdaCallingPossible() throws
             Exception {
+        when(mTelephonyFlags.simultaneousCallingIndications()).thenReturn(false);
         setupMsimAccounts();
         setMaxActiveVoiceSubscriptions(2);
 
@@ -469,11 +601,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
@@ -482,6 +632,7 @@
     @Test
     public void testConstructPossiblePhoneAccountsMultiSimActive_dsdaCallingPossible_emergencyCall()
             throws Exception {
+        when(mTelephonyFlags.simultaneousCallingIndications()).thenReturn(false);
         setupMsimAccounts();
         setMaxActiveVoiceSubscriptions(2);
 
@@ -489,11 +640,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);
@@ -2837,37 +3073,6 @@
         assertTrue(result.contains("onReceiveResult"));
     }
 
-    @Test
-    public void testConnectionServiceCreateConnectionTimeout() throws Exception {
-        mSetRlagsRule.enableFlags(Flags.FLAG_UNBIND_TIMEOUT_CONNECTIONS);
-        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() {
@@ -3077,11 +3282,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);
     }
 
@@ -3094,9 +3297,6 @@
         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
@@ -3110,11 +3310,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();
     }
 
@@ -3131,11 +3328,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();
     }
@@ -3240,7 +3434,7 @@
                 .thenReturn(true);
         mComponentContextFixture.getBroadcastReceivers().forEach(c -> c.onReceive(mContext,
                 new Intent(
-                        BlockedNumberContract.SystemContract
+                        BlockedNumberContract.BlockedNumbers
                                 .ACTION_BLOCK_SUPPRESSION_STATE_CHANGED)));
         verify(mBlockedNumbersAdapter).updateEmergencyCallNotification(any(Context.class),
                 eq(true));
@@ -3249,7 +3443,7 @@
                 .thenReturn(false);
         mComponentContextFixture.getBroadcastReceivers().forEach(c -> c.onReceive(mContext,
                 new Intent(
-                        BlockedNumberContract.SystemContract
+                        BlockedNumberContract.BlockedNumbers
                                 .ACTION_BLOCK_SUPPRESSION_STATE_CHANGED)));
         verify(mBlockedNumbersAdapter).updateEmergencyCallNotification(any(Context.class),
                 eq(false));
@@ -3360,6 +3554,28 @@
         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.bindToBTService(eq(call))).thenReturn(
+                CompletableFuture.completedFuture(true));
+        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));
+        inOrder.verify(call).setState(eq(CallState.RINGING), anyString());
+    }
+
 
     private Call addSpyCall() {
         return addSpyCall(SIM_2_HANDLE, CallState.ACTIVE);
@@ -3390,8 +3606,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;
@@ -3405,8 +3621,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;
     }
@@ -3449,6 +3665,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);
@@ -3459,24 +3678,57 @@
                 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 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 c732720..54aaa4c 100644
--- a/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
+++ b/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
@@ -102,7 +102,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;
@@ -290,6 +290,8 @@
                 return Context.DROPBOX_SERVICE;
             } else if (svcClass == BugreportManager.class) {
                 return Context.BUGREPORT_SERVICE;
+            } else if (svcClass == TelecomManager.class) {
+                return Context.TELECOM_SERVICE;
             }
             throw new UnsupportedOperationException(svcClass.getName());
         }
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/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 c356b8f..2f27bb5 100644
--- a/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java
+++ b/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java
@@ -16,10 +16,21 @@
 
 package com.android.server.telecom.tests;
 
-import android.Manifest;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+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 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;
@@ -28,7 +39,8 @@
 import android.telecom.PhoneAccount;
 import android.telecom.PhoneAccountHandle;
 import android.telephony.SubscriptionManager;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.filters.SmallTest;
 
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallIdMapper;
@@ -45,7 +57,6 @@
 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;
@@ -57,20 +68,6 @@
 import java.util.Random;
 import java.util.UUID;
 
-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.Mockito.doAnswer;
-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;
-
 /**
  * Unit testing for CreateConnectionProcessor as well as CreateConnectionTimeout classes.
  */
@@ -841,9 +838,10 @@
 
     private ConnectionServiceWrapper makeConnectionServiceWrapper() {
         ConnectionServiceWrapper wrapper = mock(ConnectionServiceWrapper.class);
+
         when(mMockConnectionServiceRepository.getService(
-                eq(makeQuickConnectionServiceComponentName()),
-                any(UserHandle.class), any(FeatureFlags.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/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 c63a3d5..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;
@@ -238,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());
@@ -265,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..f2ad2f7 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";
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 faae148..3d99d07 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;
@@ -31,10 +30,10 @@
 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.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;
@@ -51,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;
@@ -68,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;
@@ -85,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;
@@ -184,6 +184,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,6 +226,7 @@
         when(mDefaultDialerCache.getSystemDialerApplication()).thenReturn(SYS_PKG);
         when(mDefaultDialerCache.getSystemDialerComponent()).thenReturn(
                 new ComponentName(SYS_PKG, SYS_CLASS));
+        when(mDefaultDialerCache.getBTInCallServicePackage()).thenReturn(BT_PKG);
         mEmergencyCallHelper = new EmergencyCallHelper(mMockContext, mDefaultDialerCache,
                 mTimeoutsAdapter);
         when(mMockCallsManager.getRoleManagerAdapter()).thenReturn(mMockRoleManagerAdapter);
@@ -267,6 +271,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());
@@ -309,6 +315,8 @@
         // 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.profileUserSupport()).thenReturn(false);
     }
 
     @Override
@@ -398,7 +406,7 @@
                 .thenReturn(300_000L);
 
         setupMockPackageManager(false /* default */, true /* system */, false /* external calls */);
-        mInCallController.bindToServices(mMockCall);
+        mInCallController.bindToServices(mMockCall, false);
 
         ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
         verify(mMockContext).bindServiceAsUser(
@@ -433,7 +441,7 @@
 
         Intent queryIntent = new Intent(InCallService.SERVICE_INTERFACE);
         setupMockPackageManager(false /* default */, true /* system */, false /* external calls */);
-        mInCallController.bindToServices(mMockCall);
+        mInCallController.bindToServices(mMockCall, false);
 
         ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
         verify(mMockContext).bindServiceAsUser(
@@ -472,7 +480,7 @@
                 anyInt(), eq(mUserHandle))).thenReturn(true);
 
         setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
-        mInCallController.bindToServices(mMockCall);
+        mInCallController.bindToServices(mMockCall, false);
 
         // Query for the different InCallServices
         ArgumentCaptor<Intent> queryIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -534,7 +542,7 @@
         setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
         setupMockPackageManagerLocationPermission(SYS_PKG, false /* granted */);
 
-        mInCallController.bindToServices(mMockCall);
+        mInCallController.bindToServices(mMockCall, false);
 
         // Query for the different InCallServices
         ArgumentCaptor<Intent> queryIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -595,7 +603,7 @@
         setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
         setupMockPackageManagerLocationPermission(SYS_PKG, false /* granted */);
 
-        mInCallController.bindToServices(mMockCall);
+        mInCallController.bindToServices(mMockCall, false);
 
         ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
         verify(mMockContext, times(1)).bindServiceAsUser(
@@ -625,7 +633,7 @@
         setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
         setupMockPackageManagerLocationPermission(SYS_PKG, false /* granted */);
 
-        mInCallController.bindToServices(mMockCall);
+        mInCallController.bindToServices(mMockCall, false);
 
         ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
         verify(mMockContext, times(1)).bindServiceAsUser(
@@ -657,7 +665,7 @@
         setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
         setupMockPackageManagerLocationPermission(SYS_PKG, false /* granted */);
 
-        mInCallController.bindToServices(mMockCall);
+        mInCallController.bindToServices(mMockCall, false);
 
         ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
         verify(mMockContext, times(1)).bindServiceAsUser(
@@ -685,7 +693,7 @@
         setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
         setupMockPackageManagerLocationPermission(SYS_PKG, false /* granted */);
 
-        mInCallController.bindToServices(mMockCall);
+        mInCallController.bindToServices(mMockCall, false);
 
         ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
         verify(mMockContext, times(1)).bindServiceAsUser(
@@ -734,7 +742,7 @@
         setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
         setupMockPackageManagerLocationPermission(SYS_PKG, false /* granted */);
 
-        mInCallController.bindToServices(mMockCall);
+        mInCallController.bindToServices(mMockCall, false);
 
         // Query for the different InCallServices
         ArgumentCaptor<Intent> queryIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -816,7 +824,7 @@
                 .thenReturn(true);
 
         setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
-        mInCallController.bindToServices(mMockCall);
+        mInCallController.bindToServices(mMockCall, false);
 
         // Query for the different InCallServices
         ArgumentCaptor<Intent> queryIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -897,7 +905,7 @@
         when(mDefaultDialerCache.getDefaultDialerApplication(CURRENT_USER_ID)).thenReturn(DEF_PKG);
 
         setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
-        mInCallController.bindToServices(mMockCall);
+        mInCallController.bindToServices(mMockCall, false);
 
         ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
         ArgumentCaptor<ServiceConnection> serviceConnectionCaptor =
@@ -944,7 +952,7 @@
         mInCallController.handleCarModeChange(UiModeManager.DEFAULT_PRIORITY, CAR_PKG, true);
 
         // Now bind; we should only bind to one app.
-        mInCallController.bindToServices(mMockCall);
+        mInCallController.bindToServices(mMockCall, false);
 
         // Bind InCallServices
         ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -1026,7 +1034,7 @@
                 .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_DISABLED);
 
         mInCallController.addCall(mMockCall);
-        mInCallController.bindToServices(mMockCall);
+        mInCallController.bindToServices(mMockCall, false);
 
         // There will be 4 calls for the various types of ICS.
         verify(mMockPackageManager, times(4)).queryIntentServicesAsUser(
@@ -1194,7 +1202,7 @@
     public void testBindToService_IncludeExternal() throws Exception {
         setupMocks(true /* isExternalCall */);
         setupMockPackageManager(true /* default */, true /* system */, true /* external calls */);
-        mInCallController.bindToServices(mMockCall);
+        mInCallController.bindToServices(mMockCall, false);
 
         // Query for the different InCallServices
         ArgumentCaptor<Intent> queryIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -1245,7 +1253,7 @@
 
         when(mMockCallsManager.getCalls()).thenReturn(Collections.singletonList(mMockCall));
         setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
-        mInCallController.bindToServices(mMockCall);
+        mInCallController.bindToServices(mMockCall, false);
 
         ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
         ArgumentCaptor<ServiceConnection> serviceConnectionCaptor =
@@ -1294,7 +1302,7 @@
         mInCallController.handleCarModeChange(UiModeManager.DEFAULT_PRIORITY, CAR_PKG, true);
 
         // Now bind; we should only bind to one app.
-        mInCallController.bindToServices(mMockCall);
+        mInCallController.bindToServices(mMockCall, false);
 
         // Bind InCallServices
         ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -1313,7 +1321,7 @@
     public void testNoBindToInvalidService_CarModeUI() throws Exception {
         setupMocks(true /* isExternalCall */);
         setupMockPackageManager(true /* default */, true /* system */, true /* external calls */);
-        mInCallController.bindToServices(mMockCall);
+        mInCallController.bindToServices(mMockCall, false);
 
         when(mMockPackageManager.checkPermission(
                 matches(Manifest.permission.CONTROL_INCALL_EXPERIENCE),
@@ -1365,7 +1373,7 @@
                             anyInt(), any(AttributionSource.class), nullable(String.class)));
 
             // Now bind; we should bind to the system dialer and app op non ui app.
-            mInCallController.bindToServices(mMockCall);
+            mInCallController.bindToServices(mMockCall, false);
 
             // Bind InCallServices
             ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -1409,7 +1417,7 @@
         when(mDefaultDialerCache.getDefaultDialerApplication(CURRENT_USER_ID)).thenReturn(null);
 
         // we should bind to only the non ui app.
-        mInCallController.bindToServices(mMockCall);
+        mInCallController.bindToServices(mMockCall, false);
 
         // Bind InCallServices
         ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -1442,7 +1450,7 @@
                 matches(DEF_PKG))).thenReturn(PackageManager.PERMISSION_DENIED);
         when(mMockCall.getName()).thenReturn("evil");
 
-        mInCallController.bindToServices(mMockCall);
+        mInCallController.bindToServices(mMockCall, false);
 
         // Bind InCallServices
         ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -1479,7 +1487,7 @@
         setupMocks(true /* isExternalCall */);
         setupMockPackageManager(true /* default */, true /* system */, true /* external calls */);
         // Bind to default dialer.
-        mInCallController.bindToServices(mMockCall);
+        mInCallController.bindToServices(mMockCall, false);
 
         // Uninstall an unrelated app.
         mSystemStateListener.onPackageUninstalled("com.joe.stuff");
@@ -1503,7 +1511,7 @@
         setupMocks(true /* isExternalCall */);
         setupMockPackageManager(true /* default */, true /* system */, true /* external calls */);
         // Bind to default dialer.
-        mInCallController.bindToServices(mMockCall);
+        mInCallController.bindToServices(mMockCall, false);
 
         // Enable car mode and enter car mode at default priority.
         when(mMockSystemStateHelper.isCarModeOrProjectionActive()).thenReturn(true);
@@ -1571,7 +1579,7 @@
         setupMockPackageManager(true /* default */, true /* nonui */, false /* appop_nonui */ ,
                 true /* system */, false /* external calls */,
                 false /* self mgd in default*/, false /* self mgd in car*/);
-        mInCallController.bindToServices(mMockCall);
+        mInCallController.bindToServices(mMockCall, false);
 
         ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
         ArgumentCaptor<ServiceConnection> serviceConnectionCaptor =
@@ -1640,7 +1648,7 @@
 
         // Bind; we should not bind to anything right now; the dialer does not support self
         // managed calls.
-        mInCallController.bindToServices(mMockCall);
+        mInCallController.bindToServices(mMockCall, false);
 
         // Bind InCallServices; make sure no binding took place.  InCallController handles not
         // binding initially, but the rebind (see next test case) will always happen.
@@ -1679,7 +1687,7 @@
 
         // Bind; we should not bind to anything right now; the dialer does not support self
         // managed calls.
-        mInCallController.bindToServices(mMockCall);
+        mInCallController.bindToServices(mMockCall, false);
 
         // Bind InCallServices; make sure no binding took place.
         verify(mMockContext, never()).bindServiceAsUser(
@@ -1781,7 +1789,7 @@
         assertFalse(mUserHandle.equals(UserHandle.USER_CURRENT));
         when(mMockUserInfo.isManagedProfile()).thenReturn(false);
 
-        mInCallController.bindToServices(mMockCall);
+        mInCallController.bindToServices(mMockCall, false);
 
         // Bind InCallService on UserHandle.CURRENT and not the user from the call (mUserHandle)
         ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -1802,7 +1810,7 @@
         when(mMockCall.getAssociatedUser()).thenReturn(testUser);
 
         // Bind to ICS. The mapping should've been inserted with the testUser as the key.
-        mInCallController.bindToServices(mMockCall);
+        mInCallController.bindToServices(mMockCall, false);
         assertTrue(mInCallController.getInCallServiceConnections().containsKey(testUser));
 
         // Set the target phone account. Simulates the flow when the user has chosen which sim to
@@ -1830,7 +1838,7 @@
         when(mMockCall.isIncoming()).thenReturn(true);
 
         // Bind to ICS. The mapping should've been inserted with the testUser as the key.
-        mInCallController.bindToServices(mMockCall);
+        mInCallController.bindToServices(mMockCall, false);
         assertTrue(mInCallController.getInCallServiceConnections().containsKey(testUser));
 
         // Remove the call. This invokes getUserFromCall to remove the ICS mapping.
@@ -1903,7 +1911,7 @@
         when(call.getId()).thenReturn("TC@" + id);
     }
 
-    private void setupMocksForWorkProfileTest() {
+    private void setupMocksForProfileTest() {
         when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
         when(mMockCallsManager.isInEmergencyCall()).thenReturn(false);
         when(mMockChildUserCall.isIncoming()).thenReturn(false);
@@ -1919,9 +1927,9 @@
         when(mMockUserInfo.getUserHandle()).thenReturn(mParentUserHandle);
         when(mMockChildUserInfo.getUserHandle()).thenReturn(mChildUserHandle);
         when(mMockUserInfo.isManagedProfile()).thenReturn(false);
-        when(mMockChildUserInfo.isManagedProfile()).thenReturn(true);
+        when(mMockChildUserInfo.isManagedProfile()).thenReturn(false);
         when(mMockChildUserCall.getAssociatedUser()).thenReturn(mChildUserHandle);
-        when(mMockCallsManager.getCurrentUserHandle()).thenReturn(mChildUserHandle);
+        when(mMockCallsManager.getCurrentUserHandle()).thenReturn(mParentUserHandle);
         when(mMockUserManager.getProfileParent(mChildUserHandle.getIdentifier())).thenReturn(
                 mMockUserInfo);
         when(mMockUserManager.getProfileParent(mChildUserHandle)).thenReturn(mParentUserHandle);
@@ -1929,14 +1937,14 @@
                 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*/,
@@ -1945,9 +1953,8 @@
                 true /*includeSelfManagedCallsInCarModeDialer*/,
                 true /*includeSelfManagedCallsInNonUi*/);
 
-        //pass in call by child/work-profileuser
-        mInCallController.bindToServices(mMockChildUserCall);
-
+        //pass in call by child/profile user
+        mInCallController.bindToServices(mMockChildUserCall, false);
         // Verify that queryIntentServicesAsUser is also called with parent handle
         // Query for the different InCallServices
         ArgumentCaptor<Integer> userIdCaptor = ArgumentCaptor.forClass(Integer.class);
@@ -1965,6 +1972,32 @@
                 userIds.contains(mParentUserHandle.getIdentifier()));
     }
 
+    @Test
+    public void testSeparatelyBluetoothService() {
+        Intent expectedIntent = new Intent(InCallService.SERVICE_INTERFACE);
+        expectedIntent.setPackage(mDefaultDialerCache.getBTInCallServicePackage());
+        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);
+
+        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 */);
     }
@@ -2089,6 +2122,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/InCallTonePlayerTest.java b/tests/src/com/android/server/telecom/tests/InCallTonePlayerTest.java
index 004aa8e..c9faa52 100644
--- a/tests/src/com/android/server/telecom/tests/InCallTonePlayerTest.java
+++ b/tests/src/com/android/server/telecom/tests/InCallTonePlayerTest.java
@@ -32,7 +32,8 @@
 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;
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 ac2f1f1..61e8347 100644
--- a/tests/src/com/android/server/telecom/tests/MissedCallNotifierImplTest.java
+++ b/tests/src/com/android/server/telecom/tests/MissedCallNotifierImplTest.java
@@ -21,6 +21,25 @@
 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;
@@ -31,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;
@@ -41,21 +58,18 @@
 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.CallLogManager;
 import com.android.server.telecom.CallerInfoLookupHelper;
-import com.android.server.telecom.CallsManager;
 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.TelecomBroadcastIntentProcessor;
@@ -73,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 {
 
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 5bba742..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;
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 1ffcb76..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,9 +50,9 @@
 import android.telecom.VideoProfile;
 import android.telephony.DisconnectCause;
 import android.telephony.TelephonyManager;
-import android.test.suitebuilder.annotation.SmallTest;
 
-import com.android.server.telecom.flags.FeatureFlags;
+import androidx.test.filters.SmallTest;
+
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallsManager;
 import com.android.server.telecom.DefaultDialerCache;
@@ -65,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;
diff --git a/tests/src/com/android/server/telecom/tests/ParcelableCallUtilsTest.java b/tests/src/com/android/server/telecom/tests/ParcelableCallUtilsTest.java
index 57c6191..8eefd96 100644
--- a/tests/src/com/android/server/telecom/tests/ParcelableCallUtilsTest.java
+++ b/tests/src/com/android/server/telecom/tests/ParcelableCallUtilsTest.java
@@ -18,7 +18,8 @@
 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;
diff --git a/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java b/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java
index 9fcb87a..0ce5836 100644
--- a/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java
+++ b/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java
@@ -23,14 +23,14 @@
 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.doThrow;
+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.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -57,13 +57,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 +93,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 +119,7 @@
     @Mock private TelecomManager mTelecomManager;
     @Mock private DefaultDialerCache mDefaultDialerCache;
     @Mock private AppLabelProxy mAppLabelProxy;
+    @Mock private FeatureFlags mTelephonyFeatureFlags;
 
     @Override
     @Before
@@ -134,8 +137,10 @@
         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);
+        when(mFeatureFlags.onlyUpdateTelephonyOnValidSubIds()).thenReturn(false);
+        when(mTelephonyFeatureFlags.workProfileApiSplit()).thenReturn(false);
     }
 
     @Override
@@ -154,12 +159,12 @@
     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);
         assertPhoneAccountHandleEquals(input, result);
 
         PhoneAccountHandle inputN = new PhoneAccountHandle(new ComponentName("pkg0", "cls0"), null);
         PhoneAccountHandle resultN = roundTripXml(this, inputN,
-                PhoneAccountRegistrar.sPhoneAccountHandleXml, mContext);
+                PhoneAccountRegistrar.sPhoneAccountHandleXml, mContext, mTelephonyFeatureFlags);
         Log.i(this, "inputN = %s, resultN = %s", inputN, resultN);
         assertPhoneAccountHandleEquals(inputN, resultN);
     }
@@ -182,7 +187,112 @@
                 .setIsEnabled(true)
                 .build();
         PhoneAccount result = roundTripXml(this, input, PhoneAccountRegistrar.sPhoneAccountXml,
-                mContext);
+                mContext, mTelephonyFeatureFlags);
+
+        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 = UserManager.get(mContext);
+        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);
+
+        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 = UserManager.get(mContext);
+        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);
+
+        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);
 
         assertPhoneAccountEquals(input, result);
     }
@@ -259,7 +369,8 @@
         when(UserManager.get(mContext).getUserForSerialNumber(0L))
                 .thenReturn(input.userHandle);
         DefaultPhoneAccountHandle result = roundTripXml(this, input,
-                PhoneAccountRegistrar.sDefaultPhoneAcountHandleXml, mContext);
+                PhoneAccountRegistrar.sDefaultPhoneAccountHandleXml, mContext,
+                mTelephonyFeatureFlags);
 
         assertDefaultPhoneAccountHandleEquals(input, result);
     }
@@ -289,7 +400,7 @@
                 .setExtras(testBundle)
                 .build();
         PhoneAccount result = roundTripXml(this, input, PhoneAccountRegistrar.sPhoneAccountXml,
-                mContext);
+                mContext, mTelephonyFeatureFlags);
 
         Bundle extras = result.getExtras();
         assertFalse(extras.keySet().contains("EXTRA_STR2"));
@@ -303,8 +414,7 @@
     public void testState() throws Exception {
         PhoneAccountRegistrar.State input = makeQuickState();
         PhoneAccountRegistrar.State result = roundTripXml(this, input,
-                PhoneAccountRegistrar.sStateXml,
-                mContext);
+                PhoneAccountRegistrar.sStateXml, mContext, mTelephonyFeatureFlags);
         assertStateEquals(input, result);
     }
 
@@ -353,40 +463,6 @@
                 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#getAccountsForPackage_BypassResolveComp(String, UserHandle)}.
-     */
-    @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.getAccountsForPackage_BypassResolveComp(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.getAccountsForPackage_BypassResolveComp(componentName.getPackageName(),
-                        USER_HANDLE_10).size());
-        assertEquals(0,mRegistrar.cleanupUnresolvableConnectionServiceAccounts(
-                mRegistrar.getAccountsForPackage_BypassResolveComp(componentName.getPackageName(),
-                USER_HANDLE_10)).size());
-        assertEquals(0,
-                mRegistrar.getAccountsForPackage_BypassResolveComp(componentName.getPackageName(),
-                        USER_HANDLE_10).size());
-    }
-
     @MediumTest
     @Test
     public void testSimCallManager() throws Exception {
@@ -1658,6 +1734,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
@@ -1734,6 +1911,56 @@
         }
     }
 
+    @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));
+    }
+
     private static PhoneAccount.Builder makeBuilderWithBindCapabilities(PhoneAccountHandle handle) {
         return new PhoneAccount.Builder(handle, TEST_LABEL)
                 .setCapabilities(PhoneAccount.CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS);
@@ -1852,35 +2079,41 @@
             Object self,
             T input,
             PhoneAccountRegistrar.XmlSerialization<T> xml,
-            Context context)
+            Context context,
+            FeatureFlags telephonyFeatureFlags)
             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);
 
         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) throws Exception {
+        XmlPullParser parser = Xml.newPullParser();
+        parser.setInput(new BufferedInputStream(new ByteArrayInputStream(data)), null);
+        parser.nextTag();
+        return xml.readFromXml(parser, MAX_VERSION, context, telephonyFeatureFlags);
+
+    }
+
     private static void assertPhoneAccountHandleEquals(PhoneAccountHandle a, PhoneAccountHandle b) {
         if (a != b) {
             assertEquals(
@@ -1929,6 +2162,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);
             }
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
index e851944..e8d39c8 100644
--- a/tests/src/com/android/server/telecom/tests/RingbackPlayerTest.java
+++ b/tests/src/com/android/server/telecom/tests/RingbackPlayerTest.java
@@ -23,7 +23,7 @@
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.when;
 
-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/RingerTest.java b/tests/src/com/android/server/telecom/tests/RingerTest.java
index 771e736..1215fd3 100644
--- a/tests/src/com/android/server/telecom/tests/RingerTest.java
+++ b/tests/src/com/android/server/telecom/tests/RingerTest.java
@@ -18,6 +18,7 @@
 
 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;
@@ -58,7 +59,9 @@
 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;
@@ -81,6 +84,7 @@
 
 import java.time.Duration;
 import java.util.concurrent.CompletableFuture;
+import java.util.function.Supplier;
 
 @RunWith(JUnit4.class)
 public class RingerTest extends TelecomTestCase {
@@ -142,7 +146,6 @@
         mockNotificationManager =mContext.getSystemService(NotificationManager.class);
         when(mockTonePlayer.startTone()).thenReturn(true);
         when(mockNotificationManager.matchesCallFilter(any(Uri.class))).thenReturn(true);
-        when(mockRingtoneFactory.hasHapticChannels(any(Ringtone.class))).thenReturn(false);
         when(mockCall1.getState()).thenReturn(CallState.RINGING);
         when(mockCall2.getState()).thenReturn(CallState.RINGING);
         when(mockCall1.getAssociatedUser()).thenReturn(PA_HANDLE.getUserHandle());
@@ -424,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();
@@ -466,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();
 
@@ -512,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
@@ -532,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();
@@ -549,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();
@@ -568,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();
@@ -600,7 +603,7 @@
         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();
@@ -621,7 +624,7 @@
 
         asyncRingtonePlayer.updateBtActiveState(true);
         mRingCompletionFuture.get();
-        verify(mockRingtoneFactory, times(1))
+        verify(mockRingtoneFactory, atLeastOnce())
                 .getRingtone(any(Call.class), nullable(VolumeShaper.Configuration.class),
                         anyBoolean());
         verifyNoMoreInteractions(mockRingtoneFactory);
@@ -751,7 +754,7 @@
                     } 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();
@@ -830,10 +833,12 @@
 
     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;
     }
 
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 e9466ee..a36e8ea 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;
@@ -80,40 +111,13 @@
 
 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.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.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 {
 
@@ -197,6 +201,7 @@
     @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;
 
@@ -251,6 +256,7 @@
                 mSubscriptionManagerAdapter,
                 mSettingsSecureAdapter,
                 mFeatureFlags,
+                mTelephonyFeatureFlags,
                 mLock);
         telecomServiceImpl.setTransactionManager(mTransactionManager);
         telecomServiceImpl.setAnomalyReporterAdapter(mAnomalyReporterAdapter);
@@ -270,6 +276,7 @@
         mPackageManager = mContext.getPackageManager();
         when(mPackageManager.getPackageUid(anyString(), eq(0))).thenReturn(Binder.getCallingUid());
         when(mFeatureFlags.earlyBindingToIncallService()).thenReturn(true);
+        when(mTelephonyFeatureFlags.workProfileApiSplit()).thenReturn(false);
     }
 
     @Override
@@ -535,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
@@ -550,7 +611,7 @@
                 argThat(new AnyStringIn(enforcedPermissions)), anyString());
 
         assertThrows(SecurityException.class,
-                () -> mTSIBinder.getCallCapablePhoneAccounts(true, "", null));
+                () -> mTSIBinder.getCallCapablePhoneAccounts(true, "", null, false));
     }
 
     @SmallTest
@@ -788,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());
@@ -1050,7 +1167,7 @@
 
         verify(mFakePhoneAccountRegistrar).getPhoneAccount(
                 TEL_PA_HANDLE_16, TEL_PA_HANDLE_16.getUserHandle());
-        verify(mInCallController, never()).bindToServices(any());
+        verify(mInCallController, never()).bindToServices(any(), anyBoolean());
         addCallTestHelper(TelecomManager.ACTION_INCOMING_CALL,
                 CallIntentProcessor.KEY_IS_INCOMING_CALL, extras,
                 TEL_PA_HANDLE_16, false);
@@ -1072,7 +1189,7 @@
 
         mTSIBinder.addNewIncomingCall(TEL_PA_HANDLE_16, extras, CALLING_PACKAGE);
 
-        verify(mInCallController, never()).bindToServices(null);
+        verify(mInCallController, never()).bindToServices(eq(null), anyBoolean());
     }
 
     @SmallTest
@@ -1090,7 +1207,7 @@
 
         mTSIBinder.addNewIncomingCall(TEL_PA_HANDLE_16, extras, CALLING_PACKAGE);
 
-        verify(mInCallController).bindToServices(null);
+        verify(mInCallController).bindToServices(eq(null), anyBoolean());
     }
 
     @SmallTest
@@ -1108,7 +1225,7 @@
 
         mTSIBinder.addNewIncomingCall(TEL_PA_HANDLE_16, extras, CALLING_PACKAGE);
 
-        verify(mInCallController, never()).bindToServices(null);
+        verify(mInCallController, never()).bindToServices(eq(null), anyBoolean());
     }
 
     @SmallTest
@@ -1127,7 +1244,7 @@
 
         mTSIBinder.addNewIncomingCall(TEL_PA_HANDLE_16, extras, CALLING_PACKAGE);
 
-        verify(mInCallController, never()).bindToServices(null);
+        verify(mInCallController, never()).bindToServices(eq(null), anyBoolean());
     }
 
 
diff --git a/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java b/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
index aa2cf56..57802e3 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;
@@ -221,6 +221,8 @@
     CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
     @Mock
     FeatureFlags mFeatureFlags;
+    @Mock
+    com.android.internal.telephony.flags.FeatureFlags mTelephonyFlags;
 
     final ComponentName mInCallServiceComponentNameX =
             new ComponentName(
@@ -581,7 +583,8 @@
                 Runnable::run,
                 Runnable::run,
                 mBlockedNumbersAdapter,
-                mFeatureFlags);
+                mFeatureFlags,
+                mTelephonyFlags);
 
         mComponentContextFixture.setTelecomManager(new TelecomManager(
                 mComponentContextFixture.getTestDouble(),
diff --git a/tests/src/com/android/server/telecom/tests/VideoCallTests.java b/tests/src/com/android/server/telecom/tests/VideoCallTests.java
index c77a614..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,27 +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.CallAudioRouteAdapter;
-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
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 ddea231..7f7399c 100644
--- a/tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java
+++ b/tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java
@@ -16,7 +16,6 @@
 
 package com.android.server.telecom.tests;
 
-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;
@@ -44,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;
diff --git a/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java b/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java
index 0a7e27d..b7848a2 100644
--- a/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java
+++ b/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java
@@ -56,6 +56,7 @@
         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;
@@ -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) {
@@ -246,8 +251,89 @@
                     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"));
     }
+
+    @SmallTest
+    @Test
+    public void testTransactionException()
+            throws ExecutionException, InterruptedException, TimeoutException {
+        VoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
+                TestVoipCallTransaction.EXCEPTION);
+        VoipCallTransaction 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());
+    }
+
+    @SmallTest
+    @Test
+    public void testTransactionResultException()
+            throws ExecutionException, InterruptedException, TimeoutException {
+        VoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
+                TestVoipCallTransaction.SUCCESS);
+        VoipCallTransaction t2 = new TestVoipCallTransaction("t2", 1000L,
+                TestVoipCallTransaction.SUCCESS);
+        VoipCallTransaction 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());
+    }
 }