Make java_sdk_library dependencies explicit am: f9754e2dd6

Original change: https://android-review.googlesource.com/c/platform/packages/services/Telecomm/+/3250874

Change-Id: Icf8041fdaf99ccd4d700dd09aeaa8c0dcd9efbf4
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/Android.bp b/Android.bp
index b2c228d..0d1c81d 100644
--- a/Android.bp
+++ b/Android.bp
@@ -11,6 +11,14 @@
     out: ["com/android/server/telecom/TelecomStatsLog.java"],
 }
 
+filegroup {
+    name: "telecom-shell-commands-src",
+    srcs: [
+        "src/com/android/server/telecom/TelecomShellCommand.java",
+    ],
+    path: "src",
+}
+
 android_library {
     name: "TelecomLib",
     manifest: "AndroidManifestLib.xml",
@@ -33,7 +41,6 @@
     platform_apis: true,
 }
 
-
 // Build the Telecom service.
 android_app {
     name: "Telecom",
diff --git a/flags/Android.bp b/flags/Android.bp
index b089796..45acacf 100644
--- a/flags/Android.bp
+++ b/flags/Android.bp
@@ -39,6 +39,9 @@
         "telecom_bluetoothroutemanager_flags.aconfig",
         "telecom_work_profile_flags.aconfig",
         "telecom_connection_service_wrapper_flags.aconfig",
+        "telecom_remote_connection_service.aconfig",
         "telecom_profile_user_flags.aconfig",
+        "telecom_bluetoothdevicemanager_flags.aconfig",
+        "telecom_non_critical_security_flags.aconfig",
     ],
 }
diff --git a/flags/telecom_anomaly_report_flags.aconfig b/flags/telecom_anomaly_report_flags.aconfig
index 6879d86..296b300 100644
--- a/flags/telecom_anomaly_report_flags.aconfig
+++ b/flags/telecom_anomaly_report_flags.aconfig
@@ -1,6 +1,7 @@
 package: "com.android.server.telecom.flags"
 container: "system"
 
+# OWNER=tjstuart TARGET=24Q3
 flag {
   name: "gen_anom_report_on_focus_timeout"
   namespace: "telecom"
diff --git a/flags/telecom_api_flags.aconfig b/flags/telecom_api_flags.aconfig
index c0f4cba..75efdfa 100644
--- a/flags/telecom_api_flags.aconfig
+++ b/flags/telecom_api_flags.aconfig
@@ -1,58 +1,74 @@
 package: "com.android.server.telecom.flags"
 container: "system"
 
+# OWNER=grantmenke TARGET=24Q3
 flag {
   name: "voip_app_actions_support"
+  is_exported: true
   namespace: "telecom"
   description: "When set, Telecom support for additional VOIP application actions is active."
   bug: "296934278"
 }
 
+# OWNER=grantmenke TARGET=24Q3
 flag {
   name: "call_details_id_changes"
+  is_exported: true
   namespace: "telecom"
   description: "When set, call details/extras id updates to Telecom APIs for Android V are active."
   bug: "301713560"
 }
 
-flag{
+# OWNER=kunduz TARGET=24Q2
+flag {
   name: "add_call_uri_for_missed_calls"
+  is_exported: true
   namespace: "telecom"
   description: "The key is used for dialer apps to mark missed calls as read when it gets the notification on reboot."
   bug: "292597423"
 }
 
-flag{
+# OWNER=tjstuart TARGET=24Q3
+flag {
   name: "set_mute_state"
+  is_exported: true
   namespace: "telecom"
   description: "transactional calls need the ability to mute the call audio input"
   bug: "310669304"
 }
 
-flag{
+# OWNER=tjstuart TARGET=24Q3
+flag {
   name: "get_registered_phone_accounts"
+  is_exported: true
   namespace: "telecom"
   description: "When set, self-managed clients can get their own phone accounts"
   bug: "317132586"
 }
 
-flag{
+# OWNER=tjstuart TARGET=24Q3
+flag {
   name: "transactional_video_state"
+  is_exported: true
   namespace: "telecom"
   description: "when set, clients using transactional implementations will be able to set & get the video state"
   bug: "311265260"
 }
 
-flag{
+# OWNER=tjstuart TARGET=24Q3
+flag {
   name: "business_call_composer"
+  is_exported: true
   namespace: "telecom"
   description: "Enables enriched calling features (e.g. Business name will show for a call)"
   bug: "311688497"
   is_exported: true
 }
 
-flag{
+# OWNER=tgunn TARGET=25Q3
+flag {
   name: "get_last_known_cell_identity"
+  is_exported: true
   namespace: "telecom"
   description: "Formalizes the getLastKnownCellIdentity API that Telecom reliees on as a system api"
   bug: "327454165"
diff --git a/flags/telecom_bluetoothdevicemanager_flags.aconfig b/flags/telecom_bluetoothdevicemanager_flags.aconfig
new file mode 100644
index 0000000..4c91491
--- /dev/null
+++ b/flags/telecom_bluetoothdevicemanager_flags.aconfig
@@ -0,0 +1,10 @@
+package: "com.android.server.telecom.flags"
+container: "system"
+
+# OWNER=tgunn TARGET=24Q4
+flag {
+  name: "postpone_register_to_leaudio"
+  namespace: "telecom"
+  description: "Fix for Log.wtf in the BinderProxy"
+  bug: "333417369"
+}
diff --git a/flags/telecom_bluetoothroutemanager_flags.aconfig b/flags/telecom_bluetoothroutemanager_flags.aconfig
index 1df1e9b..dc69dd5 100644
--- a/flags/telecom_bluetoothroutemanager_flags.aconfig
+++ b/flags/telecom_bluetoothroutemanager_flags.aconfig
@@ -1,10 +1,10 @@
 package: "com.android.server.telecom.flags"
 container: "system"
 
+# OWNER=tgunn TARGET=24Q3
 flag {
   name: "use_actual_address_to_enter_connecting_state"
   namespace: "telecom"
   description: "Fix bugs that may add bluetooth device with null address."
   bug: "306113816"
 }
-
diff --git a/flags/telecom_broadcast_flags.aconfig b/flags/telecom_broadcast_flags.aconfig
index de8dd27..8314376 100644
--- a/flags/telecom_broadcast_flags.aconfig
+++ b/flags/telecom_broadcast_flags.aconfig
@@ -1,6 +1,7 @@
 package: "com.android.server.telecom.flags"
 container: "system"
 
+# OWNER=tgunn TARGET=24Q3
 flag {
   name: "is_new_outgoing_call_broadcast_unblocking"
   namespace: "telecom"
diff --git a/flags/telecom_call_filtering_flags.aconfig b/flags/telecom_call_filtering_flags.aconfig
index 72f9db3..d80cfa3 100644
--- a/flags/telecom_call_filtering_flags.aconfig
+++ b/flags/telecom_call_filtering_flags.aconfig
@@ -1,6 +1,7 @@
 package: "com.android.server.telecom.flags"
 container: "system"
 
+# OWNER=qingzhong TARGET=24Q2
 flag {
   name: "skip_filter_phone_account_perform_dnd_filter"
   namespace: "telecom"
diff --git a/flags/telecom_call_flags.aconfig b/flags/telecom_call_flags.aconfig
index 27a4b22..ed75f14 100644
--- a/flags/telecom_call_flags.aconfig
+++ b/flags/telecom_call_flags.aconfig
@@ -1,9 +1,28 @@
 package: "com.android.server.telecom.flags"
 container: "system"
 
+# OWNER=tjstuart TARGET=24Q3
 flag {
   name: "transactional_cs_verifier"
   namespace: "telecom"
   description: "verify connection service callbacks via a transaction"
   bug: "309541257"
-}
\ No newline at end of file
+}
+
+flag {
+  name: "cache_call_audio_callbacks"
+  namespace: "telecom"
+  description: "cache call audio callbacks if the service is not available and execute when set"
+  bug: "321369729"
+}
+
+# OWNER = breadley TARGET=24Q3
+flag {
+  name: "cancel_removal_on_emergency_redial"
+  namespace: "telecom"
+  description: "When redialing an emergency call on another connection service, ensure any pending removal operation is cancelled"
+  bug: "341157874"
+  metadata {
+      purpose: PURPOSE_BUGFIX
+    }
+}
diff --git a/flags/telecom_callaudiomodestatemachine_flags.aconfig b/flags/telecom_callaudiomodestatemachine_flags.aconfig
index 1d81535..63761ec 100644
--- a/flags/telecom_callaudiomodestatemachine_flags.aconfig
+++ b/flags/telecom_callaudiomodestatemachine_flags.aconfig
@@ -1,6 +1,7 @@
 package: "com.android.server.telecom.flags"
 container: "system"
 
+# OWNER=pmadapurmath TARGET=24Q3
 flag {
   name: "set_audio_mode_before_abandon_focus"
   namespace: "telecom"
diff --git a/flags/telecom_callaudioroutestatemachine_flags.aconfig b/flags/telecom_callaudioroutestatemachine_flags.aconfig
index f5da045..33bccba 100644
--- a/flags/telecom_callaudioroutestatemachine_flags.aconfig
+++ b/flags/telecom_callaudioroutestatemachine_flags.aconfig
@@ -1,6 +1,7 @@
 package: "com.android.server.telecom.flags"
 container: "system"
 
+# OWNER=kunduz TARGET=24Q2
 flag {
   name: "available_routes_never_updated_after_set_system_audio_state"
   namespace: "telecom"
@@ -8,6 +9,7 @@
   bug: "292599751"
 }
 
+# OWNER=pmadapurmath TARGET=24Q3
 flag {
   name: "use_refactored_audio_route_switching"
   namespace: "telecom"
@@ -15,6 +17,7 @@
   bug: "306395598"
 }
 
+# OWNER=tgunn TARGET=24Q3
 flag {
   name: "ensure_audio_mode_updates_on_foreground_call_change"
   namespace: "telecom"
@@ -22,6 +25,7 @@
   bug: "289861657"
 }
 
+# OWNER=pmadapurmath TARGET=24Q1
 flag {
   name: "ignore_auto_route_to_watch_device"
   namespace: "telecom"
@@ -29,6 +33,7 @@
   bug: "294378768"
 }
 
+# OWNER=pmadapurmath TARGET=24Q3
 flag {
   name: "transit_route_before_audio_disconnect_bt"
   namespace: "telecom"
@@ -36,6 +41,7 @@
   bug: "306113816"
 }
 
+# OWNER=pmadapurmath TARGET=24Q3
 flag {
   name: "call_audio_communication_device_refactor"
   namespace: "telecom"
@@ -43,6 +49,7 @@
   bug: "308968392"
 }
 
+# OWNER=pmadapurmath TARGET=24Q3
 flag {
   name: "communication_device_protected_by_lock"
   namespace: "telecom"
@@ -50,6 +57,7 @@
   bug: "303001133"
 }
 
+# OWNER=pmadapurmath TARGET=24Q3
 flag {
   name: "reset_mute_when_entering_quiescent_bt_route"
   namespace: "telecom"
@@ -57,6 +65,7 @@
   bug: "311313250"
 }
 
+# OWNER=pmadapurmath TARGET=24Q3
 flag {
   name: "update_route_mask_when_bt_connected"
   namespace: "telecom"
@@ -64,9 +73,29 @@
   bug: "301695370"
 }
 
+# OWNER=pmadapurmath TARGET=24Q3
 flag {
   name: "clear_communication_device_after_audio_ops_complete"
   namespace: "telecom"
   description: "Clear the requested communication device after the audio operations are completed."
   bug: "315865533"
 }
+
+# OWNER=pmadapurmath TARGET=24Q3
+flag {
+  name: "resolve_switching_bt_devices_computation"
+  namespace: "telecom"
+  description: "Update switching bt devices based on arbitrary device chosen if no device is specified."
+  bug: "333751408"
+}
+
+# OWNER=pmadapurmath TARGET=24Q3
+flag {
+  name: "early_update_internal_call_audio_state"
+  namespace: "telecom"
+  description: "Update internal call audio state before sending updated state to ICS"
+  bug: "335538831"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
diff --git a/flags/telecom_calllog_flags.aconfig b/flags/telecom_calllog_flags.aconfig
index 593b7e5..c0eebf1 100644
--- a/flags/telecom_calllog_flags.aconfig
+++ b/flags/telecom_calllog_flags.aconfig
@@ -1,6 +1,7 @@
 package: "com.android.server.telecom.flags"
 container: "system"
 
+# OWNER=qingzhong TARGET=24Q2
 flag {
   name: "telecom_log_external_wearable_calls"
   namespace: "telecom"
@@ -8,6 +9,7 @@
   bug: "292600751"
 }
 
+# OWNER=ranamouawi TARGET=24Q2
 flag {
   name: "telecom_skip_log_based_on_extra"
   namespace: "telecom"
diff --git a/flags/telecom_calls_manager_flags.aconfig b/flags/telecom_calls_manager_flags.aconfig
index de17eee..f46e844 100644
--- a/flags/telecom_calls_manager_flags.aconfig
+++ b/flags/telecom_calls_manager_flags.aconfig
@@ -1,6 +1,7 @@
 package: "com.android.server.telecom.flags"
 container: "system"
 
+# OWNER=pmadapurmath TARGET=24Q3
 flag {
   name: "use_improved_listener_order"
   namespace: "telecom"
@@ -8,6 +9,7 @@
   bug: "24244713"
 }
 
+# OWNER=tjstuart TARGET=24Q3
 flag {
   name: "fix_audio_flicker_for_outgoing_calls"
   namespace: "telecom"
@@ -15,9 +17,21 @@
   bug: "309540769"
 }
 
+# OWNER=breadley TARGET=24Q3
 flag {
   name: "enable_call_sequencing"
   namespace: "telecom"
   description: "Enables simultaneous call sequencing for SIM PhoneAccounts"
-  bug: "297446980"
+  bug: "327038818"
+}
+
+# OWNER=tjstuart TARGET=24Q4
+flag {
+  name: "transactional_hold_disconnects_unholdable"
+  namespace: "telecom"
+  description: "Disconnect ongoing unholdable calls for CallControlCallbacks"
+  bug: "340621152"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
 }
diff --git a/flags/telecom_connection_service_wrapper_flags.aconfig b/flags/telecom_connection_service_wrapper_flags.aconfig
index 80a8dfe..38e5e13 100644
--- a/flags/telecom_connection_service_wrapper_flags.aconfig
+++ b/flags/telecom_connection_service_wrapper_flags.aconfig
@@ -1,6 +1,7 @@
 package: "com.android.server.telecom.flags"
 container: "system"
 
+# OWNER=grantmenke TARGET=24Q2
 flag {
   name: "updated_rcs_call_count_tracking"
   namespace: "telecom"
diff --git a/flags/telecom_default_phone_account_flags.aconfig b/flags/telecom_default_phone_account_flags.aconfig
index e6badde..161b674 100644
--- a/flags/telecom_default_phone_account_flags.aconfig
+++ b/flags/telecom_default_phone_account_flags.aconfig
@@ -1,6 +1,7 @@
 package: "com.android.server.telecom.flags"
 container: "system"
 
+# OWNER=tjstuart TARGET=24Q3
 flag {
   name: "only_update_telephony_on_valid_sub_ids"
   namespace: "telecom"
@@ -8,6 +9,7 @@
   bug: "234846282"
 }
 
+# OWNER=tjstuart TARGET=24Q3
 flag {
   name: "telephony_has_default_but_telecom_does_not"
   namespace: "telecom"
diff --git a/flags/telecom_incallservice_flags.aconfig b/flags/telecom_incallservice_flags.aconfig
index 08a82ba..ea842ac 100644
--- a/flags/telecom_incallservice_flags.aconfig
+++ b/flags/telecom_incallservice_flags.aconfig
@@ -1,6 +1,7 @@
 package: "com.android.server.telecom.flags"
 container: "system"
 
+# OWNER=qingzhong TARGET=24Q2
 flag {
   name: "early_binding_to_incall_service"
   namespace: "telecom"
@@ -8,6 +9,7 @@
   bug: "282113261"
 }
 
+# OWNER=pmadapurmath TARGET=24Q2
 flag {
   name: "ecc_keyguard"
   namespace: "telecom"
@@ -15,6 +17,7 @@
   bug: "306582821"
 }
 
+# OWNER=pmadapurmath TARGET=24Q3
 flag {
   name: "separately_bind_to_bt_incall_service"
   namespace: "telecom"
diff --git a/flags/telecom_non_critical_security_flags.aconfig b/flags/telecom_non_critical_security_flags.aconfig
new file mode 100644
index 0000000..37929a8
--- /dev/null
+++ b/flags/telecom_non_critical_security_flags.aconfig
@@ -0,0 +1,10 @@
+package: "com.android.server.telecom.flags"
+container: "system"
+
+# OWNER=tjstuart TARGET=24Q4
+flag {
+  name: "unregister_unresolvable_accounts"
+  namespace: "telecom"
+  description: "When set, Telecom will unregister accounts if the service is not resolvable"
+  bug: "281061708"
+}
\ No newline at end of file
diff --git a/flags/telecom_profile_user_flags.aconfig b/flags/telecom_profile_user_flags.aconfig
index c046de8..feee07d 100644
--- a/flags/telecom_profile_user_flags.aconfig
+++ b/flags/telecom_profile_user_flags.aconfig
@@ -1,6 +1,7 @@
 package: "com.android.server.telecom.flags"
 container: "system"
 
+# OWNER=huiwang TARGET=24Q3
 flag {
   name: "profile_user_support"
   namespace: "telecom"
diff --git a/flags/telecom_remote_connection_service.aconfig b/flags/telecom_remote_connection_service.aconfig
new file mode 100644
index 0000000..a30f0b2
--- /dev/null
+++ b/flags/telecom_remote_connection_service.aconfig
@@ -0,0 +1,10 @@
+package: "com.android.server.telecom.flags"
+container: "system"
+
+# OWNER=pmadapurmath TARGET=24Q3
+flag {
+  name: "set_remote_connection_call_id"
+  namespace: "telecom"
+  description: "Sets the telecom call id for remote connections/ conferences."
+  bug: "320242200"
+}
diff --git a/flags/telecom_resolve_hidden_dependencies.aconfig b/flags/telecom_resolve_hidden_dependencies.aconfig
index 674a968..a120b85 100644
--- a/flags/telecom_resolve_hidden_dependencies.aconfig
+++ b/flags/telecom_resolve_hidden_dependencies.aconfig
@@ -1,9 +1,19 @@
 package: "com.android.server.telecom.flags"
 container: "system"
 
+# OWNER=tgunn TARGET=24Q3
 flag {
     name: "telecom_resolve_hidden_dependencies"
+    is_exported: true
     namespace: "telecom"
     description: "Mainland cleanup for hidden dependencies"
     bug: "323414215"
 }
+
+flag {
+    name: "telecom_mainline_blocked_numbers_manager"
+    namespace: "telecom"
+    description: "Fixed read only flag used for setting up BlockedNumbersManager to be retrieved via context"
+    bug: "325049252"
+    is_fixed_read_only: true
+}
diff --git a/flags/telecom_ringer_flag_declarations.aconfig b/flags/telecom_ringer_flag_declarations.aconfig
index 13577bb..f126bf3 100644
--- a/flags/telecom_ringer_flag_declarations.aconfig
+++ b/flags/telecom_ringer_flag_declarations.aconfig
@@ -1,6 +1,7 @@
 package: "com.android.server.telecom.flags"
 container: "system"
 
+# OWNER=yeabkal TARGET=24Q2
 flag {
   name: "use_device_provided_serialized_ringer_vibration"
   namespace: "telecom"
diff --git a/flags/telecom_work_profile_flags.aconfig b/flags/telecom_work_profile_flags.aconfig
index 854568b..1891423 100644
--- a/flags/telecom_work_profile_flags.aconfig
+++ b/flags/telecom_work_profile_flags.aconfig
@@ -1,6 +1,7 @@
 package: "com.android.server.telecom.flags"
 container: "system"
 
+# OWNER=pmadapurmath TARGET=24Q3
 flag {
   name: "associated_user_refactor_for_work_profile"
   namespace: "telecom"
diff --git a/res/values-fr-rCA/strings.xml b/res/values-fr-rCA/strings.xml
index aaf651f..031b25d 100644
--- a/res/values-fr-rCA/strings.xml
+++ b/res/values-fr-rCA/strings.xml
@@ -30,9 +30,9 @@
     <string name="notification_disconnectedCall_body" msgid="600491714584417536">"L\'appel à <xliff:g id="CALLER">%s</xliff:g> a été déconnecté en raison d\'un appel d\'urgence qui a été passé."</string>
     <string name="notification_disconnectedCall_generic_body" msgid="5282765206349184853">"Votre appel a été déconnecté en raison d\'un appel d\'urgence en cours de lancement."</string>
     <string name="notification_audioProcessing_title" msgid="1619035039880584575">"Appel en arrière-plan"</string>
-    <string name="notification_audioProcessing_body" msgid="8811420157964118913">"<xliff:g id="AUDIO_PROCESSING_APP_NAME">%s</xliff:g> traite un appel en arrière-plan. Cette application peut accéder à l\'audio de l\'appel et faire jouer un contenu audio par l\'intermédiaire de l\'appel."</string>
+    <string name="notification_audioProcessing_body" msgid="8811420157964118913">"<xliff:g id="AUDIO_PROCESSING_APP_NAME">%s</xliff:g> traite un appel en arrière-plan. Cette appli peut accéder à l\'audio de l\'appel et faire jouer un contenu audio par l\'intermédiaire de l\'appel."</string>
     <string name="notification_incallservice_not_responding_title" msgid="5347557574288598548">"<xliff:g id="IN_CALL_SERVICE_APP_NAME">%s</xliff:g> a arrêté de répondre"</string>
-    <string name="notification_incallservice_not_responding_body" msgid="9209308270131968623">"Votre appel a utilisé l\'application Téléphone intégrée à votre appareil"</string>
+    <string name="notification_incallservice_not_responding_body" msgid="9209308270131968623">"Votre appel a utilisé l\'appli Téléphone intégrée à votre appareil"</string>
     <string name="accessibility_call_muted" msgid="2968461092554300779">"Son coupé"</string>
     <string name="accessibility_speakerphone_enabled" msgid="555386652061614267">"Haut-parleur activé"</string>
     <string name="respond_via_sms_canned_response_1" msgid="6332561460870382561">"Peux pas parler. Quoi de neuf?"</string>
@@ -47,20 +47,20 @@
     <string name="respond_via_sms_failure_format" msgid="5198680980054596391">"Échec de l\'envoi du message au <xliff:g id="PHONE_NUMBER">%s</xliff:g>."</string>
     <string name="enable_account_preference_title" msgid="6949224486748457976">"Comptes d\'appel"</string>
     <string name="outgoing_call_not_allowed_user_restriction" msgid="3424338207838851646">"Seuls les appels d\'urgence sont autorisés."</string>
-    <string name="outgoing_call_not_allowed_no_permission" msgid="8590468836581488679">"Cette application ne peut pas faire d\'appels sans l\'autorisation de l\'application Téléphone."</string>
+    <string name="outgoing_call_not_allowed_no_permission" msgid="8590468836581488679">"Cette appli ne peut pas faire d\'appels sans l\'autorisation de l\'appli Téléphone."</string>
     <string name="outgoing_call_error_no_phone_number_supplied" msgid="7665135102566099778">"Pour faire un appel, entrez un numéro valide."</string>
     <string name="duplicate_video_call_not_allowed" msgid="5754746140185781159">"Impossible d\'ajouter l\'appel pour le moment."</string>
     <string name="no_vm_number" msgid="2179959110602180844">"Numéro de messagerie vocale manquant"</string>
     <string name="no_vm_number_msg" msgid="1339245731058529388">"Aucun numéro de messagerie vocale n\'est enregistré sur la carte SIM."</string>
     <string name="add_vm_number_str" msgid="5179510133063168998">"Ajouter un numéro"</string>
-    <string name="change_default_dialer_dialog_title" msgid="5861469279421508060">"Définir <xliff:g id="NEW_APP">%s</xliff:g> comme votre application de téléphonie par défaut?"</string>
-    <string name="change_default_dialer_dialog_affirmative" msgid="8604665314757739550">"Définir comme application de téléphonie par défaut"</string>
+    <string name="change_default_dialer_dialog_title" msgid="5861469279421508060">"Définir <xliff:g id="NEW_APP">%s</xliff:g> comme votre appli de téléphonie par défaut?"</string>
+    <string name="change_default_dialer_dialog_affirmative" msgid="8604665314757739550">"Définir comme appli de téléphonie par défaut"</string>
     <string name="change_default_dialer_dialog_negative" msgid="8648669840052697821">"Annuler"</string>
-    <string name="change_default_dialer_warning_message" msgid="8461963987376916114">"<xliff:g id="NEW_APP">%s</xliff:g> sera en mesure de passer des appels et de contrôler tous les aspects des appels. Seules les applications auxquelles vous avez confiance doivent être définies comme l\'application de téléphonie par défaut."</string>
+    <string name="change_default_dialer_warning_message" msgid="8461963987376916114">"<xliff:g id="NEW_APP">%s</xliff:g> sera en mesure de passer des appels et de contrôler tous les aspects des appels. Seules les applis auxquelles vous avez confiance doivent être définies comme appli de téléphonie par défaut."</string>
     <string name="change_default_call_screening_dialog_title" msgid="5365787219927262408">"Définir <xliff:g id="NEW_APP">%s</xliff:g> comme appli de filtrage d\'appels par défaut?"</string>
     <string name="change_default_call_screening_warning_message_for_disable_old_app" msgid="2039830033533243164">"<xliff:g id="OLD_APP">%s</xliff:g> ne pourra plus filtrer les appels."</string>
-    <string name="change_default_call_screening_warning_message" msgid="9020537562292754269">"<xliff:g id="NEW_APP">%s</xliff:g> pourra accéder aux renseignements sur les appelants qui ne figurent pas dans vos contacts et pourra bloquer ces appels. L\'application de filtrage d\'appels par défaut doit être une application de confiance."</string>
-    <string name="change_default_call_screening_dialog_affirmative" msgid="7162433828280058647">"Définir comme application de filtrage d\'appels par défaut"</string>
+    <string name="change_default_call_screening_warning_message" msgid="9020537562292754269">"<xliff:g id="NEW_APP">%s</xliff:g> pourra accéder aux renseignements sur les appelants qui ne figurent pas dans vos contacts et pourra bloquer ces appels. L\'appli de filtrage d\'appels par défaut doit être une appli de confiance."</string>
+    <string name="change_default_call_screening_dialog_affirmative" msgid="7162433828280058647">"Définir comme appli de filtrage d\'appels par défaut"</string>
     <string name="change_default_call_screening_dialog_negative" msgid="1839266125623106342">"Annuler"</string>
     <string name="blocked_numbers" msgid="8322134197039865180">"Numéros bloqués"</string>
     <string name="blocked_numbers_msg" msgid="2797422132329662697">"Vous ne recevrez pas d\'appels ni de messages texte provenant des numéros bloqués."</string>
@@ -93,19 +93,19 @@
     <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Impossible de passer cet appel, car aucun compte d\'appel ne prend en charge les appels de ce type."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Impossible de faire l\'appel en raison de votre appel <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Impossible de faire l\'appel en raison de vos appels <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
-    <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Impossible de faire l\'appel en raison d\'un appel dans une autre application."</string>
+    <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Impossible de faire l\'appel en raison d\'un appel dans une autre appli."</string>
     <string name="notification_channel_incoming_call" msgid="5245550964701715662">"Appels entrants"</string>
     <string name="notification_channel_missed_call" msgid="7168893015283909012">"Appels manqués"</string>
     <string name="notification_channel_call_blocking" msgid="2028807677868598710">"Blocage des appels"</string>
     <string name="notification_channel_background_calls" msgid="7785659903711350506">"Appels en arrière-plan"</string>
     <string name="notification_channel_disconnected_calls" msgid="8228636543997645757">"Appels déconnectés"</string>
-    <string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Applications téléphoniques qui ont planté"</string>
+    <string name="notification_channel_in_call_service_crash" msgid="7313237519166984267">"Applis téléphoniques qui ont planté"</string>
     <string name="notification_channel_call_streaming" msgid="5100510699787538991">"Diffusion en continu d\'appels"</string>
     <string name="alert_outgoing_call" msgid="5319895109298927431">"Si vous passez cet appel, vous mettrez fin à l\'appel <xliff:g id="OTHER_APP">%1$s</xliff:g>."</string>
     <string name="alert_redirect_outgoing_call_or_not" msgid="665409645789521636">"Choisissez comment passer cet appel"</string>
     <string name="alert_place_outgoing_call_with_redirection" msgid="5221065030959024121">"Rediriger l\'appel en utilisant <xliff:g id="OTHER_APP">%1$s</xliff:g>"</string>
     <string name="alert_place_unredirect_outgoing_call" msgid="2467608535225764006">"Appeler en utilisant mon numéro de téléphone"</string>
-    <string name="alert_redirect_outgoing_call_timeout" msgid="5568101425637373060">"Impossible de passer l\'appel au moyen de l\'application <xliff:g id="OTHER_APP">%1$s</xliff:g>. Essayez d\'utiliser une autre application de redirection d\'appels ou de communiquer avec le développeur de l\'application pour obtenir de l\'aide."</string>
+    <string name="alert_redirect_outgoing_call_timeout" msgid="5568101425637373060">"Impossible de passer l\'appel au moyen de l\'appli <xliff:g id="OTHER_APP">%1$s</xliff:g>. Essayez d\'utiliser une autre appli de redirection d\'appels ou de communiquer avec le développeur de l\'appli pour obtenir de l\'aide."</string>
     <string name="phone_settings_call_blocking_txt" msgid="7311523114822507178">"Blocage des appels"</string>
     <string name="phone_settings_number_not_in_contact_txt" msgid="2602249106007265757">"Numéros non répertoriés dans les contacts"</string>
     <string name="phone_settings_number_not_in_contact_summary_txt" msgid="963327038085718969">"Bloquer les numéros non répertoriés dans vos contacts"</string>
diff --git a/res/values-kk/strings.xml b/res/values-kk/strings.xml
index 399da20..e53631b 100644
--- a/res/values-kk/strings.xml
+++ b/res/values-kk/strings.xml
@@ -62,7 +62,7 @@
     <string name="change_default_call_screening_warning_message" msgid="9020537562292754269">"<xliff:g id="NEW_APP">%s</xliff:g> контактілер тізімінде жоқ қоңырау шалушылар туралы ақпаратты көріп, бұндай қоңырауларды бөгей алады. Әдепкі қоңырауды тексеру қолданбасы ретінде тек өзіңіз сенетін қолданбаларды ғана орнатқан дұрыс."</string>
     <string name="change_default_call_screening_dialog_affirmative" msgid="7162433828280058647">"Әдепкі ретінде орнату"</string>
     <string name="change_default_call_screening_dialog_negative" msgid="1839266125623106342">"Жабу"</string>
-    <string name="blocked_numbers" msgid="8322134197039865180">"Бөгелген нөмірлер"</string>
+    <string name="blocked_numbers" msgid="8322134197039865180">"Блокталған нөмірлер"</string>
     <string name="blocked_numbers_msg" msgid="2797422132329662697">"Тыйым салынған нөмірлерден қоңыраулар немесе мәтіндік хабарлар алмайсыз."</string>
     <string name="block_number" msgid="3784343046852802722">"Нөмір қосу"</string>
     <string name="unblock_dialog_body" msgid="2723393535797217261">"<xliff:g id="NUMBER_TO_BLOCK">%1$s</xliff:g> бөгеуден шығарылсын ба?"</string>
@@ -70,7 +70,7 @@
     <string name="add_blocked_dialog_body" msgid="8599974422407139255">"Қоңыраулары мен мәтіндік хабарлары бөгелетін нөмір"</string>
     <string name="add_blocked_number_hint" msgid="8769422085658041097">"Телефон нөмірі"</string>
     <string name="block_button" msgid="485080149164258770">"Блоктау"</string>
-    <string name="non_primary_user" msgid="315564589279622098">"Бөгелген нөмірлерді тек құрылғы иесі көре және басқара алады."</string>
+    <string name="non_primary_user" msgid="315564589279622098">"Блокталған нөмірлерді тек құрылғы иесі көре және басқара алады."</string>
     <string name="delete_icon_description" msgid="5335959254954774373">"Бөгеуді алу"</string>
     <string name="blocked_numbers_butter_bar_title" msgid="582982373755950791">"Тыйым уақытша алынды"</string>
     <string name="blocked_numbers_butter_bar_body" msgid="1261213114919301485">"Төтенше жағдай нөмірін терген немесе мәтіндік хабар жіберген соң, төтенше жағдай қызметтері сізге хабарласа алуы үшін тыйым алынады."</string>
diff --git a/res/values-ne/strings.xml b/res/values-ne/strings.xml
index 44645dc..df2c70c 100644
--- a/res/values-ne/strings.xml
+++ b/res/values-ne/strings.xml
@@ -27,7 +27,7 @@
     <string name="notification_missedCall_call_back" msgid="7900333283939789732">"फेरि कल गर्नुहोस्"</string>
     <string name="notification_missedCall_message" msgid="4054698824390076431">"सन्देश"</string>
     <string name="notification_disconnectedCall_title" msgid="1790131923692416928">"विच्छेद गरिएको कल"</string>
-    <string name="notification_disconnectedCall_body" msgid="600491714584417536">"आपत्‌कालीन कल गरिएको हुनाले <xliff:g id="CALLER">%s</xliff:g> लाई गरिएको कल विच्छेद गरियो।"</string>
+    <string name="notification_disconnectedCall_body" msgid="600491714584417536">"आपत्‌कालीन कल गरिएको हुनाले <xliff:g id="CALLER">%s</xliff:g> लाई गरिएको कल डिस्कनेक्ट गरियो।"</string>
     <string name="notification_disconnectedCall_generic_body" msgid="5282765206349184853">"आपत्‌कालीन कल जारी रहेको हुनाले तपाईंको कल विच्छेद गरिएको छ।"</string>
     <string name="notification_audioProcessing_title" msgid="1619035039880584575">"पृष्ठभूमिको कल"</string>
     <string name="notification_audioProcessing_body" msgid="8811420157964118913">"<xliff:g id="AUDIO_PROCESSING_APP_NAME">%s</xliff:g> ले ब्याकग्राउन्डमा कुनै कल प्रोसेस गर्दै छ। यो एपले तपाईंको कलको अडियो प्रयोग गरिरहेको र सोही अडियो प्ले गरिरहेको हुन सक्छ।"</string>
diff --git a/res/values-night/styles.xml b/res/values-night/styles.xml
index 5b81fac..b94b9e4 100644
--- a/res/values-night/styles.xml
+++ b/res/values-night/styles.xml
@@ -23,6 +23,11 @@
         <item name="android:actionOverflowButtonStyle">@style/TelecomDialerSettingsActionOverflowButtonStyle</item>
         <item name="android:windowLightNavigationBar">true</item>
         <item name="android:windowContentOverlay">@null</item>
+
+        <!--
+            TODO(b/309578419): Make activities handle insets properly and then remove this.
+        -->
+        <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
     </style>
 
     <style name="Theme.Telecom.BlockedNumbers" parent="@android:style/Theme.DeviceDefault.Light">
@@ -31,6 +36,11 @@
         <item name="android:windowLightNavigationBar">true</item>
         <item name="android:windowContentOverlay">@null</item>
         <item name="android:listDivider">@null</item>
+
+        <!--
+            TODO(b/309578419): Make activities handle insets properly and then remove this.
+        -->
+        <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
     </style>
 
 </resources>
\ No newline at end of file
diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml
index 8dfee81..48c7957 100644
--- a/res/values-nl/strings.xml
+++ b/res/values-nl/strings.xml
@@ -16,7 +16,7 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="telecommAppLabel" product="default" msgid="1825598513414129827">"Telefoongesprekken"</string>
+    <string name="telecommAppLabel" product="default" msgid="1825598513414129827">"Telefoon­gesprekken"</string>
     <string name="userCallActivityLabel" product="default" msgid="3605391260292846248">"Telefoon"</string>
     <string name="unknown" msgid="6993977514360123431">"Onbekend"</string>
     <string name="notification_missedCallTitle" msgid="5060387047205532974">"Gemist gesprek"</string>
diff --git a/res/values-vi/strings.xml b/res/values-vi/strings.xml
index 5ae2e79..70f9bfc 100644
--- a/res/values-vi/strings.xml
+++ b/res/values-vi/strings.xml
@@ -133,5 +133,5 @@
     <string name="callendpoint_name_unknown" msgid="2199074708477193852">"Không xác định"</string>
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Đang truyền trực tuyến âm thanh tới thiết bị khác"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Kết thúc"</string>
-    <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Chuyển đổi tại đây"</string>
+    <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Chuyển qua thiết bị này"</string>
 </resources>
diff --git a/res/values/config.xml b/res/values/config.xml
index bf30720..ae5d88e 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -80,6 +80,16 @@
      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>
+    <!-- When true, skip fetching quick reply response -->
+    <bool name="skip_loading_canned_text_response">false</bool>
+
+    <!-- When true, skip fetching incoming caller info -->
+    <bool name="skip_incoming_caller_info_query">false</bool>
+
+    <string-array name="system_bluetooth_stack_package_name" translatable="false">
+        <!-- AOSP -->
+        <item>com.android.bluetooth</item>
+        <!-- Used for internal targets -->
+        <item>com.google.android.bluetooth</item>
+    </string-array>
 </resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index cd608f5..0624082 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -32,6 +32,11 @@
         <item name="android:navigationBarColor">@android:color/transparent</item>
         <item name="android:windowLightStatusBar">true</item>
         <item name="android:windowLightNavigationBar">true</item>
+
+        <!--
+            TODO(b/309578419): Make activities handle insets properly and then remove this.
+        -->
+        <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
     </style>
 
     <style name="Theme.Telecom.EnableAccount" parent="Theme.Telecom.DialerSettings">
diff --git a/src/com/android/server/telecom/Analytics.java b/src/com/android/server/telecom/Analytics.java
index 45e3340..4cf54ed 100644
--- a/src/com/android/server/telecom/Analytics.java
+++ b/src/com/android/server/telecom/Analytics.java
@@ -720,15 +720,19 @@
     }
 
     private static int getCarrierId(Context context) {
-        SubscriptionManager subscriptionManager =
-                context.getSystemService(SubscriptionManager.class).createForAllUserProfiles();
-        List<SubscriptionInfo> subInfos = subscriptionManager.getActiveSubscriptionInfoList();
-        if (subInfos == null) {
+        try {
+            SubscriptionManager subscriptionManager =
+                    context.getSystemService(SubscriptionManager.class).createForAllUserProfiles();
+            List<SubscriptionInfo> subInfos = subscriptionManager.getActiveSubscriptionInfoList();
+            if (subInfos == null) {
+                return -1;
+            }
+            return subInfos.stream()
+                    .max(Comparator.comparing(Analytics::scoreSubscriptionInfo))
+                    .map(SubscriptionInfo::getCarrierId).orElse(-1);
+        } catch (UnsupportedOperationException ignored) {
             return -1;
         }
-        return subInfos.stream()
-                .max(Comparator.comparing(Analytics::scoreSubscriptionInfo))
-                .map(SubscriptionInfo::getCarrierId).orElse(-1);
     }
 
     // Copied over from Telephony's server-side logic for consistency
diff --git a/src/com/android/server/telecom/AudioRoute.java b/src/com/android/server/telecom/AudioRoute.java
index cdf44a8..8a5e858 100644
--- a/src/com/android/server/telecom/AudioRoute.java
+++ b/src/com/android/server/telecom/AudioRoute.java
@@ -23,11 +23,15 @@
 import static com.android.server.telecom.CallAudioRouteAdapter.SPEAKER_ON;
 
 import android.annotation.IntDef;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothStatusCodes;
 import android.media.AudioDeviceInfo;
 import android.media.AudioManager;
 import android.telecom.Log;
+import android.util.Pair;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.telecom.bluetooth.BluetoothRouteManager;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -38,6 +42,7 @@
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
+import java.util.concurrent.RejectedExecutionException;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
@@ -46,9 +51,10 @@
     public static class Factory {
         private final ScheduledExecutorService mScheduledExecutorService =
                 new ScheduledThreadPoolExecutor(1);
-        private final CompletableFuture<AudioRoute> mAudioRouteFuture = new CompletableFuture<>();
+        private CompletableFuture<AudioRoute> mAudioRouteFuture;
         public AudioRoute create(@AudioRouteType int type, String bluetoothAddress,
                                  AudioManager audioManager) throws RuntimeException {
+            mAudioRouteFuture = new CompletableFuture();
             createRetry(type, bluetoothAddress, audioManager, MAX_CONNECTION_RETRIES);
             try {
                 return mAudioRouteFuture.get();
@@ -58,8 +64,10 @@
         }
         private void createRetry(@AudioRouteType int type, String bluetoothAddress,
                                        AudioManager audioManager, int retryCount) {
+            // Early exit if exceeded max number of retries (and complete the future).
             if (retryCount == 0) {
                 mAudioRouteFuture.complete(null);
+                return;
             }
 
             Log.i(this, "creating AudioRoute with type %s and address %s, retry count %d",
@@ -81,10 +89,17 @@
                     }
                 }
             }
-            if (routeInfo == null) {
-                mScheduledExecutorService.schedule(
-                        () -> createRetry(type, bluetoothAddress, audioManager, retryCount - 1),
-                        RETRY_TIME_DELAY, TimeUnit.MILLISECONDS);
+            // Try connecting BT device anyway (to handle wearables not showing as available
+            // communication device or LE device not showing up since it may not be the lead
+            // device).
+            if (routeInfo == null && bluetoothAddress == null) {
+                try {
+                    mScheduledExecutorService.schedule(
+                            () -> createRetry(type, bluetoothAddress, audioManager, retryCount - 1),
+                            RETRY_TIME_DELAY, TimeUnit.MILLISECONDS);
+                } catch (RejectedExecutionException e) {
+                    Log.e(this, e, "Could not schedule retry for audio routing.");
+                }
             } else {
                 mAudioRouteFuture.complete(new AudioRoute(type, bluetoothAddress, routeInfo));
             }
@@ -212,7 +227,7 @@
         AUDIO_ROUTE_TYPE_TO_DEVICE_INFO_TYPE.put(TYPE_BLUETOOTH_LE, bluetoothLeDeviceInfoTypes);
     }
 
-    int getType() {
+    public int getType() {
         return mAudioRouteType;
     }
 
@@ -222,29 +237,79 @@
 
     // Invoked when entered pending route whose dest route is this route
     void onDestRouteAsPendingRoute(boolean active, PendingAudioRoute pendingAudioRoute,
-                                   AudioManager audioManager) {
+            BluetoothDevice device, AudioManager audioManager,
+            BluetoothRouteManager bluetoothRouteManager, boolean isScoAudioConnected) {
+        Log.i(this, "onDestRouteAsPendingRoute: active (%b), type (%d)", active, mAudioRouteType);
         if (pendingAudioRoute.isActive() && !active) {
-            Log.i(this, "clearCommunicationDevice");
-            audioManager.clearCommunicationDevice();
+            clearCommunicationDevice(pendingAudioRoute, bluetoothRouteManager, audioManager);
         } else if (active) {
-            if (mAudioRouteType == TYPE_BLUETOOTH_SCO) {
-                pendingAudioRoute.addMessage(BT_AUDIO_CONNECTED);
+            // Handle BT routing case.
+            if (BT_AUDIO_ROUTE_TYPES.contains(mAudioRouteType)) {
+                boolean connectedBtAudio = connectBtAudio(pendingAudioRoute, device,
+                        audioManager, bluetoothRouteManager);
+                // Special handling for SCO case.
+                if (mAudioRouteType == TYPE_BLUETOOTH_SCO) {
+                    // Check if the communication device was set for the device, even if
+                    // BluetoothHeadset#connectAudio reports that the SCO connection wasn't
+                    // successfully established.
+                    if (connectedBtAudio || isScoAudioConnected) {
+                        pendingAudioRoute.setCommunicationDeviceType(mAudioRouteType);
+                        if (!isScoAudioConnected) {
+                            pendingAudioRoute.addMessage(BT_AUDIO_CONNECTED, mBluetoothAddress);
+                        }
+                    } else {
+                        pendingAudioRoute.onMessageReceived(new Pair<>(PENDING_ROUTE_FAILED,
+                                mBluetoothAddress), mBluetoothAddress);
+                    }
+                    return;
+                }
             } else if (mAudioRouteType == TYPE_SPEAKER) {
-                pendingAudioRoute.addMessage(SPEAKER_ON);
+                pendingAudioRoute.addMessage(SPEAKER_ON, null);
             }
-            if (!audioManager.setCommunicationDevice(mInfo)) {
-                pendingAudioRoute.onMessageReceived(PENDING_ROUTE_FAILED);
+
+            boolean result = false;
+            List<AudioDeviceInfo> devices = audioManager.getAvailableCommunicationDevices();
+            for (AudioDeviceInfo deviceInfo : devices) {
+                // It's possible for the AudioDeviceInfo to be updated for the BT device so adjust
+                // mInfo accordingly.
+                if (BT_AUDIO_ROUTE_TYPES.contains(mAudioRouteType) && mBluetoothAddress
+                        .equals(deviceInfo.getAddress())) {
+                    mInfo = deviceInfo;
+                }
+                if (deviceInfo.equals(mInfo)) {
+                    result = audioManager.setCommunicationDevice(mInfo);
+                    if (result) {
+                        pendingAudioRoute.setCommunicationDeviceType(mAudioRouteType);
+                    }
+                    Log.i(this, "Result of setting communication device for audio "
+                            + "route (%s) - %b", this, result);
+                    break;
+                }
+            }
+
+            // It's possible that BluetoothStateReceiver needs to report that the device is active
+            // before being able to successfully set the communication device. Refrain from sending
+            // pending route failed message for BT route until the second attempt fails.
+            if (!result && !BT_AUDIO_ROUTE_TYPES.contains(mAudioRouteType)) {
+                pendingAudioRoute.onMessageReceived(new Pair<>(PENDING_ROUTE_FAILED, null), null);
             }
         }
     }
 
+    // Takes care of cleaning up original audio route (i.e. clearCommunicationDevice,
+    // sending SPEAKER_OFF, or disconnecting SCO).
     void onOrigRouteAsPendingRoute(boolean active, PendingAudioRoute pendingAudioRoute,
-                                   AudioManager audioManager) {
+            AudioManager audioManager, BluetoothRouteManager bluetoothRouteManager) {
+        Log.i(this, "onOrigRouteAsPendingRoute: active (%b), type (%d)", active, mAudioRouteType);
         if (active) {
-            if (mAudioRouteType == TYPE_BLUETOOTH_SCO) {
-                pendingAudioRoute.addMessage(BT_AUDIO_DISCONNECTED);
-            } else if (mAudioRouteType == TYPE_SPEAKER) {
-                pendingAudioRoute.addMessage(SPEAKER_OFF);
+            if (mAudioRouteType == TYPE_SPEAKER) {
+                pendingAudioRoute.addMessage(SPEAKER_OFF, null);
+            }
+            int result = clearCommunicationDevice(pendingAudioRoute, bluetoothRouteManager,
+                    audioManager);
+            // Only send BT_AUDIO_DISCONNECTED for SCO if disconnect was successful.
+            if (mAudioRouteType == TYPE_BLUETOOTH_SCO && result == BluetoothStatusCodes.SUCCESS) {
+                pendingAudioRoute.addMessage(BT_AUDIO_DISCONNECTED, mBluetoothAddress);
             }
         }
     }
@@ -282,4 +347,50 @@
                 + ", Address=" + ((mBluetoothAddress != null) ? mBluetoothAddress : "invalid")
                 + "]";
     }
+
+    private boolean connectBtAudio(PendingAudioRoute pendingAudioRoute, BluetoothDevice device,
+            AudioManager audioManager, BluetoothRouteManager bluetoothRouteManager) {
+        // Ensure that if another BT device was set, it is disconnected before connecting
+        // the new one.
+        AudioRoute currentRoute = pendingAudioRoute.getOrigRoute();
+        if (currentRoute.getBluetoothAddress() != null &&
+                !currentRoute.getBluetoothAddress().equals(device.getAddress())) {
+            clearCommunicationDevice(pendingAudioRoute, bluetoothRouteManager, audioManager);
+        }
+
+        // Connect to the device (explicit handling for HFP devices).
+        boolean success = false;
+        if (device != null) {
+            success = bluetoothRouteManager.getDeviceManager()
+                    .connectAudio(device, mAudioRouteType);
+        }
+
+        Log.i(this, "connectBtAudio: routeToConnectTo = %s, successful = %b",
+                this, success);
+        return success;
+    }
+
+    int clearCommunicationDevice(PendingAudioRoute pendingAudioRoute,
+            BluetoothRouteManager bluetoothRouteManager, AudioManager audioManager) {
+        // Try to see if there's a previously set device for communication that should be cleared.
+        // This only serves to help in the SCO case to ensure that we disconnect the headset.
+        if (pendingAudioRoute.getCommunicationDeviceType() == AudioRoute.TYPE_INVALID) {
+            return -1;
+        }
+
+        int result = BluetoothStatusCodes.SUCCESS;
+        if (pendingAudioRoute.getCommunicationDeviceType() == TYPE_BLUETOOTH_SCO) {
+            Log.i(this, "Disconnecting SCO device.");
+            result = bluetoothRouteManager.getDeviceManager().disconnectSco();
+        } else {
+            Log.i(this, "Clearing communication device for audio type %d.",
+                    pendingAudioRoute.getCommunicationDeviceType());
+            audioManager.clearCommunicationDevice();
+        }
+
+        if (result == BluetoothStatusCodes.SUCCESS) {
+            pendingAudioRoute.setCommunicationDeviceType(AudioRoute.TYPE_INVALID);
+        }
+        return result;
+    }
 }
diff --git a/src/com/android/server/telecom/CachedAvailableEndpointsChange.java b/src/com/android/server/telecom/CachedAvailableEndpointsChange.java
new file mode 100644
index 0000000..232f00d
--- /dev/null
+++ b/src/com/android/server/telecom/CachedAvailableEndpointsChange.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+import android.telecom.CallEndpoint;
+
+import java.util.Objects;
+import java.util.Set;
+
+public class CachedAvailableEndpointsChange implements CachedCallback {
+    public static final String ID = CachedAvailableEndpointsChange.class.getSimpleName();
+    Set<CallEndpoint> mAvailableEndpoints;
+
+    public Set<CallEndpoint> getAvailableEndpoints() {
+        return mAvailableEndpoints;
+    }
+
+    public CachedAvailableEndpointsChange(Set<CallEndpoint> endpoints) {
+        mAvailableEndpoints = endpoints;
+    }
+
+    @Override
+    public void executeCallback(CallSourceService service, Call call) {
+        service.onAvailableCallEndpointsChanged(call, mAvailableEndpoints);
+    }
+
+    @Override
+    public String getCallbackId() {
+        return ID;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(mAvailableEndpoints);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (!(obj instanceof CachedAvailableEndpointsChange other)) {
+            return false;
+        }
+        if (mAvailableEndpoints.size() != other.mAvailableEndpoints.size()) {
+            return false;
+        }
+        for (CallEndpoint e : mAvailableEndpoints) {
+            if (!other.getAvailableEndpoints().contains(e)) {
+                return false;
+            }
+        }
+        return true;
+    }
+}
+
diff --git a/src/com/android/server/telecom/CachedCallback.java b/src/com/android/server/telecom/CachedCallback.java
new file mode 100644
index 0000000..88dad07
--- /dev/null
+++ b/src/com/android/server/telecom/CachedCallback.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+/**
+ * Any android.telecom.Call service (e.g. ConnectionService, TransactionalService) that declares
+ * a {@link CallSourceService} should implement this interface in order to cache the callback.
+ * The callback will be executed once the service is set.
+ */
+public interface CachedCallback {
+    /**
+     * This method executes the callback that was cached because the service was not available
+     * at the time the callback was ready.
+     *
+     * @param service that was recently set (e.g. ConnectionService)
+     * @param call    that had a null service at the time the callback was ready. The service is now
+     *                non-null in the call and can be executed/
+     */
+    void executeCallback(CallSourceService service, Call call);
+
+    /**
+     * This method is helpful for caching the callbacks.  If the callback is called multiple times
+     * while the service is not set, ONLY the last callback should be sent to the client since the
+     * last callback is the most relevant
+     *
+     * @return the callback id that is used in a map to only store the last callback value
+     */
+    String getCallbackId();
+}
diff --git a/src/com/android/server/telecom/CachedCurrentEndpointChange.java b/src/com/android/server/telecom/CachedCurrentEndpointChange.java
new file mode 100644
index 0000000..0d5bac9
--- /dev/null
+++ b/src/com/android/server/telecom/CachedCurrentEndpointChange.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+import android.telecom.CallEndpoint;
+
+import java.util.Objects;
+
+public class CachedCurrentEndpointChange implements CachedCallback {
+    public static final String ID = CachedCurrentEndpointChange.class.getSimpleName();
+    CallEndpoint mCurrentCallEndpoint;
+
+    public CallEndpoint getCurrentCallEndpoint() {
+        return mCurrentCallEndpoint;
+    }
+
+    public CachedCurrentEndpointChange(CallEndpoint callEndpoint) {
+        mCurrentCallEndpoint = callEndpoint;
+    }
+
+    @Override
+    public void executeCallback(CallSourceService service, Call call) {
+        service.onCallEndpointChanged(call, mCurrentCallEndpoint);
+    }
+
+    @Override
+    public String getCallbackId() {
+        return ID;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(mCurrentCallEndpoint);
+    }
+
+    @Override
+    public boolean equals(Object obj){
+        if (obj == null) {
+            return false;
+        }
+        if (!(obj instanceof CachedCurrentEndpointChange other)) {
+            return false;
+        }
+        return mCurrentCallEndpoint.equals(other.mCurrentCallEndpoint);
+    }
+}
+
diff --git a/src/com/android/server/telecom/CachedMuteStateChange.java b/src/com/android/server/telecom/CachedMuteStateChange.java
new file mode 100644
index 0000000..45cbfaa
--- /dev/null
+++ b/src/com/android/server/telecom/CachedMuteStateChange.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+public class CachedMuteStateChange implements CachedCallback {
+    public static final String ID = CachedMuteStateChange.class.getSimpleName();
+    boolean mIsMuted;
+
+    public boolean isMuted() {
+        return mIsMuted;
+    }
+
+    public CachedMuteStateChange(boolean isMuted) {
+        mIsMuted = isMuted;
+    }
+
+    @Override
+    public void executeCallback(CallSourceService service, Call call) {
+        service.onMuteStateChanged(call, mIsMuted);
+    }
+
+    @Override
+    public String getCallbackId() {
+        return ID;
+    }
+
+    @Override
+    public int hashCode() {
+        return Boolean.hashCode(mIsMuted);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (!(obj instanceof CachedMuteStateChange other)) {
+            return false;
+        }
+        return mIsMuted == other.mIsMuted;
+    }
+}
+
diff --git a/src/com/android/server/telecom/CachedVideoStateChange.java b/src/com/android/server/telecom/CachedVideoStateChange.java
new file mode 100644
index 0000000..0892c33
--- /dev/null
+++ b/src/com/android/server/telecom/CachedVideoStateChange.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+import static com.android.server.telecom.voip.VideoStateTranslation.TransactionalVideoStateToString;
+
+import android.telecom.Log;
+
+public class CachedVideoStateChange implements CachedCallback {
+    public static final String ID = CachedVideoStateChange.class.getSimpleName();
+    int mCurrentVideoState;
+
+    public int getCurrentCallEndpoint() {
+        return mCurrentVideoState;
+    }
+
+    public CachedVideoStateChange(int videoState) {
+        mCurrentVideoState = videoState;
+    }
+
+    @Override
+    public void executeCallback(CallSourceService service, Call call) {
+        service.onVideoStateChanged(call, mCurrentVideoState);
+        Log.addEvent(call, LogUtils.Events.VIDEO_STATE_CHANGED,
+                TransactionalVideoStateToString(mCurrentVideoState));
+    }
+
+    @Override
+    public String getCallbackId() {
+        return ID;
+    }
+
+    @Override
+    public int hashCode() {
+        return mCurrentVideoState;
+    }
+
+    @Override
+    public boolean equals(Object obj){
+        if (obj == null) {
+            return false;
+        }
+        if (!(obj instanceof CachedVideoStateChange other)) {
+            return false;
+        }
+        return mCurrentVideoState == other.mCurrentVideoState;
+    }
+}
+
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index f7ad93f..760028d 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -19,6 +19,7 @@
 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.TransactionalVideoStateToString;
 import static com.android.server.telecom.voip.VideoStateTranslation.VideoProfileStateToTransactionalVideoState;
 
 import android.annotation.NonNull;
@@ -86,6 +87,7 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Date;
+import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Locale;
@@ -824,7 +826,22 @@
      * disconnect message via {@link CallDiagnostics#onCallDisconnected(ImsReasonInfo)} or
      * {@link CallDiagnostics#onCallDisconnected(int, int)}.
      */
-    private CompletableFuture<Boolean> mDisconnectFuture;
+    private CompletableFuture<Boolean> mDiagnosticCompleteFuture;
+
+    /**
+     * {@link CompletableFuture} used to perform disconnect operations after
+     * {@link #mDiagnosticCompleteFuture} has completed.
+     */
+    private CompletableFuture<Void> mDisconnectFuture;
+
+    /**
+     * {@link CompletableFuture} used to perform call removal operations after the
+     * {@link #mDisconnectFuture} has completed.
+     * <p>
+     * Note: It is possible for this future to be cancelled in the case that an internal operation
+     * will be handling clean up. (See {@link #setState}.)
+     */
+    private CompletableFuture<Void> mRemovalFuture;
 
     /**
      * {@link CompletableFuture} used to delay audio routing change for a ringing call until the
@@ -833,6 +850,16 @@
      */
     private CompletableFuture<Boolean> mBtIcsFuture;
 
+    Map<String, CachedCallback> mCachedServiceCallbacks = new HashMap<>();
+
+    public void cacheServiceCallback(CachedCallback callback) {
+        mCachedServiceCallbacks.put(callback.getCallbackId(), callback);
+    }
+
+    public Map<String, CachedCallback> getCachedServiceCallbacks() {
+        return mCachedServiceCallbacks;
+    }
+
     private FeatureFlags mFlags;
 
     /**
@@ -1304,7 +1331,7 @@
                         message, null));
             }
 
-            mDisconnectFuture.complete(true);
+            mDiagnosticCompleteFuture.complete(true);
         } else {
             Log.w(this, "handleOverrideDisconnectMessage; callid=%s - got override when unbound",
                     getId());
@@ -1326,6 +1353,12 @@
 
             if (newState == CallState.DISCONNECTED && shouldContinueProcessingAfterDisconnect()) {
                 Log.w(this, "continuing processing disconnected call with another service");
+                if (mFlags.cancelRemovalOnEmergencyRedial() && isDisconnectHandledViaFuture()
+                        && isRemovalPending()) {
+                    Log.i(this, "cancelling removal future in favor of "
+                            + "CreateConnectionProcessor handling removal");
+                    mRemovalFuture.cancel(true);
+                }
                 mCreateConnectionProcessor.continueProcessingIfPossible(this, mDisconnectCause);
                 return false;
             } else if (newState == CallState.ANSWERED && mState == CallState.ACTIVE) {
@@ -1562,6 +1595,9 @@
                     mIsEmergencyCall = mHandle != null &&
                             getTelephonyManager().isEmergencyNumber(
                                     mHandle.getSchemeSpecificPart());
+                } catch (UnsupportedOperationException use) {
+                    Log.i(this, "setHandle: no FEATURE_TELEPHONY; emergency state unknown.");
+                    mIsEmergencyCall = false;
                 } catch (IllegalStateException ise) {
                     Log.e(this, ise, "setHandle: can't determine if number is emergency");
                     mIsEmergencyCall = false;
@@ -1575,7 +1611,11 @@
                 mIsTestEmergencyCall = mHandle != null &&
                         isTestEmergencyCall(mHandle.getSchemeSpecificPart());
             }
-            startCallerInfoLookup();
+            if (!mContext.getResources().getBoolean(R.bool.skip_incoming_caller_info_query)) {
+                startCallerInfoLookup();
+            } else  {
+                Log.i(this, "skip incoming caller info lookup");
+            }
             for (Listener l : mListeners) {
                 l.onHandleChanged(this);
             }
@@ -1590,6 +1630,9 @@
                     .anyMatch(eNumber ->
                             eNumber.isFromSources(EmergencyNumber.EMERGENCY_NUMBER_SOURCE_TEST) &&
                                     number.equals(eNumber.getNumber()));
+        } catch (UnsupportedOperationException uoe) {
+            // No Telephony feature, so unable to determine.
+            return false;
         } catch (IllegalStateException ise) {
             return false;
         } catch (RuntimeException r) {
@@ -2001,7 +2044,27 @@
     }
 
     public void setTransactionServiceWrapper(TransactionalServiceWrapper service) {
+        Log.i(this, "setTransactionServiceWrapper: service=[%s]", service);
         mTransactionalService = service;
+        processCachedCallbacks(service);
+    }
+
+    private void processCachedCallbacks(CallSourceService service) {
+        if(mFlags.cacheCallAudioCallbacks()) {
+            for (CachedCallback callback : mCachedServiceCallbacks.values()) {
+                callback.executeCallback(service, this);
+            }
+            // clear list for memory cleanup purposes. The Service should never be reset
+            mCachedServiceCallbacks.clear();
+        }
+    }
+
+    public CallSourceService getService() {
+        if (isTransactionalCall()) {
+            return mTransactionalService;
+        } else {
+            return mConnectionService;
+        }
     }
 
     public TransactionalServiceWrapper getTransactionServiceWrapper() {
@@ -2083,7 +2146,7 @@
                 userHandle = mTargetPhoneAccountHandle.getUserHandle();
             }
             if (userHandle != null) {
-                isWorkCall = UserUtil.isManagedProfile(mContext, userHandle);
+                isWorkCall = UserUtil.isManagedProfile(mContext, userHandle, mFlags);
             }
 
             isCallRecordingToneSupported = (phoneAccount.hasCapabilities(
@@ -2408,6 +2471,7 @@
 
     @VisibleForTesting
     public void setConnectionService(ConnectionServiceWrapper service) {
+        Log.i(this, "setConnectionService: service=[%s]", service);
         setConnectionService(service, null);
     }
 
@@ -2430,6 +2494,7 @@
         mConnectionService = service;
         mAnalytics.setCallConnectionService(service.getComponentName().flattenToShortString());
         mConnectionService.addCall(this);
+        processCachedCallbacks(service);
     }
 
     /**
@@ -2511,7 +2576,7 @@
             return;
         }
         mCreateConnectionProcessor = new CreateConnectionProcessor(this, mRepository, this,
-                phoneAccountRegistrar, mContext, mFlags);
+                phoneAccountRegistrar, mContext, mFlags, new Timeouts.Adapter());
         mCreateConnectionProcessor.process();
     }
 
@@ -3053,16 +3118,24 @@
     public void awaitCallStateChangeAndMaybeDisconnectCall(int targetCallState,
             boolean shouldDisconnectUponTimeout, String callingMethod) {
         TransactionManager tm = TransactionManager.getInstance();
-        tm.addTransaction(new VerifyCallStateChangeTransaction(mCallsManager,
-                this, targetCallState, shouldDisconnectUponTimeout), new OutcomeReceiver<>() {
+        tm.addTransaction(new VerifyCallStateChangeTransaction(mCallsManager.getLock(),
+                this, targetCallState), new OutcomeReceiver<>() {
             @Override
             public void onResult(VoipCallTransactionResult result) {
+                Log.i(this, "awaitCallStateChangeAndMaybeDisconnectCall: %s: onResult:"
+                        + " due to CallException=[%s]", callingMethod, result);
             }
 
             @Override
             public void onError(CallException e) {
                 Log.i(this, "awaitCallStateChangeAndMaybeDisconnectCall: %s: onError"
                         + " due to CallException=[%s]", callingMethod, e);
+                if (shouldDisconnectUponTimeout) {
+                    mCallsManager.markCallAsDisconnected(Call.this,
+                            new DisconnectCause(DisconnectCause.ERROR,
+                                    "did not hold in timeout window"));
+                    mCallsManager.markCallAsRemoved(Call.this);
+                }
             }
         });
     }
@@ -3432,7 +3505,7 @@
             Log.w(this, "pullExternalCall = pullExternalCall - call %s is external but can not be"
                     + " pulled while an emergency call is in progress.", mId);
             mToastFactory.makeText(mContext, R.string.toast_emergency_can_not_pull_call,
-                    Toast.LENGTH_LONG).show();
+                    Toast.LENGTH_LONG);
             return;
         }
 
@@ -3679,6 +3752,11 @@
      * SMSes to that number will silently fail.
      */
     public boolean isRespondViaSmsCapable() {
+        if (mContext.getResources().getBoolean(R.bool.skip_loading_canned_text_response)) {
+            Log.d(this, "maybeLoadCannedSmsResponses: skip loading due to setting");
+            return false;
+        }
+
         if (mState != CallState.RINGING) {
             return false;
         }
@@ -3699,8 +3777,12 @@
         }
 
         // Is there a valid SMS application on the phone?
-        if (mContext.getSystemService(TelephonyManager.class)
-                .getAndUpdateDefaultRespondViaMessageApplication() == null) {
+        try {
+            if (mContext.getSystemService(TelephonyManager.class)
+                    .getAndUpdateDefaultRespondViaMessageApplication() == null) {
+                return false;
+            }
+        } catch (UnsupportedOperationException uoe) {
             return false;
         }
 
@@ -4054,14 +4136,8 @@
             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;
-        }
+        // TODO:: b/338280297. If a transactional call does not have the
+        //   CallAttributes.SUPPORTS_VIDEO_CALLING capability, the videoState should be set to audio
 
         // Track Video State history during the duration of the call.
         // Only update the history when the call is active or disconnected. This ensures we do
@@ -4078,17 +4154,24 @@
         int previousVideoState = mVideoState;
         mVideoState = videoState;
         if (mVideoState != previousVideoState) {
-            Log.addEvent(this, LogUtils.Events.VIDEO_STATE_CHANGED,
-                    VideoProfile.videoStateToString(videoState));
+            if (!mIsTransactionalCall) {
+                Log.addEvent(this, LogUtils.Events.VIDEO_STATE_CHANGED,
+                        VideoProfile.videoStateToString(videoState));
+            }
             for (Listener l : mListeners) {
                 l.onVideoStateChanged(this, previousVideoState, mVideoState);
             }
         }
 
-        if (mFlags.transactionalVideoState()
-                && mIsTransactionalCall && mTransactionalService != null) {
+        if (mFlags.transactionalVideoState() && mIsTransactionalCall) {
             int transactionalVS = VideoProfileStateToTransactionalVideoState(mVideoState);
-            mTransactionalService.onVideoStateChanged(this, transactionalVS);
+            if (mTransactionalService != null) {
+                Log.addEvent(this, LogUtils.Events.VIDEO_STATE_CHANGED,
+                        TransactionalVideoStateToString(transactionalVS));
+                mTransactionalService.onVideoStateChanged(this, transactionalVS);
+            } else {
+                cacheServiceCallback(new CachedVideoStateChange(transactionalVS));
+            }
         }
 
         if (VideoProfile.isVideo(videoState)) {
@@ -4696,17 +4779,17 @@
      * @param timeoutMillis Timeout we use for waiting for the response.
      * @return the {@link CompletableFuture}.
      */
-    public CompletableFuture<Boolean> initializeDisconnectFuture(long timeoutMillis) {
-        if (mDisconnectFuture == null) {
-            mDisconnectFuture = new CompletableFuture<Boolean>()
+    public CompletableFuture<Boolean> initializeDiagnosticCompleteFuture(long timeoutMillis) {
+        if (mDiagnosticCompleteFuture == null) {
+            mDiagnosticCompleteFuture = new CompletableFuture<Boolean>()
                     .completeOnTimeout(false, timeoutMillis, TimeUnit.MILLISECONDS);
             // After all the chained stuff we will report where the CDS timed out.
-            mDisconnectFuture.thenRunAsync(() -> {
+            mDiagnosticCompleteFuture.thenRunAsync(() -> {
                 if (!mReceivedCallDiagnosticPostCallResponse) {
                     Log.addEvent(this, LogUtils.Events.CALL_DIAGNOSTIC_SERVICE_TIMEOUT);
                 }
                 // Clear the future as a final step.
-                mDisconnectFuture = null;
+                mDiagnosticCompleteFuture = null;
                 },
                 new LoggedHandlerExecutor(mHandler, "C.iDF", mLock))
                     .exceptionally((throwable) -> {
@@ -4714,14 +4797,14 @@
                         return null;
                     });
         }
-        return mDisconnectFuture;
+        return mDiagnosticCompleteFuture;
     }
 
     /**
      * @return the disconnect future, if initialized.  Used for chaining operations after creation.
      */
-    public CompletableFuture<Boolean> getDisconnectFuture() {
-        return mDisconnectFuture;
+    public CompletableFuture<Boolean> getDiagnosticCompleteFuture() {
+        return mDiagnosticCompleteFuture;
     }
 
     /**
@@ -4729,7 +4812,7 @@
      * if this is handled immediately.
      */
     public boolean isDisconnectHandledViaFuture() {
-        return mDisconnectFuture != null;
+        return mDiagnosticCompleteFuture != null;
     }
 
     /**
@@ -4737,13 +4820,42 @@
      * {@code cleanupStuckCalls} request.
      */
     public void cleanup() {
-        if (mDisconnectFuture != null) {
-            mDisconnectFuture.complete(false);
-            mDisconnectFuture = null;
+        if (mDiagnosticCompleteFuture != null) {
+            mDiagnosticCompleteFuture.complete(false);
+            mDiagnosticCompleteFuture = null;
         }
     }
 
     /**
+     * Set the pending future to use when the call is disconnected.
+     */
+    public void setDisconnectFuture(CompletableFuture<Void> future) {
+        mDisconnectFuture = future;
+    }
+
+    /**
+     * @return The future that will be executed when the call is disconnected.
+     */
+    public CompletableFuture<Void> getDisconnectFuture() {
+        return mDisconnectFuture;
+    }
+
+    /**
+     * Set the future that will be used when call removal is taking place.
+     */
+    public void setRemovalFuture(CompletableFuture<Void> future) {
+        mRemovalFuture = future;
+    }
+
+    /**
+     * @return {@code true} if there is a pending removal operation that hasn't taken place yet, or
+     * {@code false} if there is no removal pending.
+     */
+    public boolean isRemovalPending() {
+        return mRemovalFuture != null && !mRemovalFuture.isDone();
+    }
+
+    /**
      * Set the bluetooth {@link android.telecom.InCallService} binding completion or timeout future
      * which is used to delay the audio routing change after the bluetooth stack get notified about
      * the ringing calls.
@@ -4754,12 +4866,20 @@
     }
 
     /**
+     * @return The binding {@link CompletableFuture} for the BT ICS.
+     */
+    public CompletableFuture<Boolean> getBtIcsFuture() {
+        return mBtIcsFuture;
+    }
+
+    /**
      * Wait for bluetooth {@link android.telecom.InCallService} binding completion or timeout. Used
      * for audio routing operations for a ringing call.
      */
     public void waitForBtIcs() {
         if (mBtIcsFuture != null) {
             try {
+                Log.i(this, "waitForBtIcs: waiting for BT service to bind");
                 mBtIcsFuture.get();
             } catch (InterruptedException | ExecutionException e) {
                 // ignore
diff --git a/src/com/android/server/telecom/CallAudioCommunicationDeviceTracker.java b/src/com/android/server/telecom/CallAudioCommunicationDeviceTracker.java
index 3a05eb5..8130685 100644
--- a/src/com/android/server/telecom/CallAudioCommunicationDeviceTracker.java
+++ b/src/com/android/server/telecom/CallAudioCommunicationDeviceTracker.java
@@ -31,6 +31,8 @@
 import java.util.Arrays;
 import java.util.List;
 import java.util.concurrent.Semaphore;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 
 /**
  * Helper class used to keep track of the requested communication device within Telecom for audio
@@ -47,7 +49,7 @@
     private int mAudioDeviceType = sAUDIO_DEVICE_TYPE_INVALID;
     // Keep track of the locally requested BT audio device if set
     private String mBtAudioDevice = null;
-    private final Semaphore mLock =  new Semaphore(1);
+    private final Lock mLock = new ReentrantLock();
 
     public CallAudioCommunicationDeviceTracker(Context context) {
         mAudioManager = context.getSystemService(AudioManager.class);
@@ -58,11 +60,29 @@
     }
 
     public boolean isAudioDeviceSetForType(int audioDeviceType) {
-        return mAudioDeviceType == audioDeviceType;
+        if (Flags.communicationDeviceProtectedByLock()) {
+            mLock.lock();
+        }
+        try {
+            return mAudioDeviceType == audioDeviceType;
+        } finally {
+            if (Flags.communicationDeviceProtectedByLock()) {
+                mLock.unlock();
+            }
+        }
     }
 
     public int getCurrentLocallyRequestedCommunicationDevice() {
-       return mAudioDeviceType;
+        if (Flags.communicationDeviceProtectedByLock()) {
+            mLock.lock();
+        }
+        try {
+            return mAudioDeviceType;
+        } finally {
+            if (Flags.communicationDeviceProtectedByLock()) {
+                mLock.unlock();
+            }
+        }
     }
 
     @VisibleForTesting
@@ -71,13 +91,22 @@
     }
 
     public void clearBtCommunicationDevice() {
-        if (mBtAudioDevice == null) {
-            Log.i(this, "No bluetooth device was set for communication that can be cleared.");
-            return;
+        if (Flags.communicationDeviceProtectedByLock()) {
+            mLock.lock();
         }
-        // If mBtAudioDevice is set, we know a BT audio device was set for communication so
-        // mAudioDeviceType corresponds to a BT device type (e.g. hearing aid, SCO, LE).
-        clearCommunicationDevice(mAudioDeviceType);
+        try {
+            if (mBtAudioDevice == null) {
+                Log.i(this, "No bluetooth device was set for communication that can be cleared.");
+            } else {
+                // If mBtAudioDevice is set, we know a BT audio device was set for communication so
+                // mAudioDeviceType corresponds to a BT device type (e.g. hearing aid, SCO, LE).
+                processClearCommunicationDevice(mAudioDeviceType);
+            }
+        } finally {
+            if (Flags.communicationDeviceProtectedByLock()) {
+                mLock.unlock();
+            }
+        }
     }
 
     /*
@@ -93,8 +122,19 @@
     public boolean setCommunicationDevice(int audioDeviceType,
             BluetoothDevice btDevice) {
         if (Flags.communicationDeviceProtectedByLock()) {
-            mLock.tryAcquire();
+            mLock.lock();
         }
+        try {
+            return processSetCommunicationDevice(audioDeviceType, btDevice);
+        } finally {
+            if (Flags.communicationDeviceProtectedByLock()) {
+                mLock.unlock();
+            }
+        }
+    }
+
+    private boolean processSetCommunicationDevice(int audioDeviceType,
+            BluetoothDevice btDevice) {
         // There is only one audio device type associated with each type of BT device.
         boolean isBtDevice = BT_AUDIO_DEVICE_INFO_TYPES.contains(audioDeviceType);
         Log.i(this, "setCommunicationDevice: type = %s, isBtDevice = %s, btDevice = %s",
@@ -132,14 +172,14 @@
             Log.i(this, "No active device of type(s) %s available",
                     audioDeviceType == AudioDeviceInfo.TYPE_WIRED_HEADSET
                             ? Arrays.asList(AudioDeviceInfo.TYPE_WIRED_HEADSET,
-                                    AudioDeviceInfo.TYPE_USB_HEADSET)
+                            AudioDeviceInfo.TYPE_USB_HEADSET)
                             : audioDeviceType);
             return false;
         }
 
         // Force clear previous communication device, if one was set, before setting the new device.
         if (mAudioDeviceType != sAUDIO_DEVICE_TYPE_INVALID) {
-            clearCommunicationDevice(mAudioDeviceType);
+            processClearCommunicationDevice(mAudioDeviceType);
         }
 
         // Turn activeDevice ON.
@@ -161,12 +201,8 @@
                 mBtAudioDevice = null;
             }
         }
-        if (Flags.communicationDeviceProtectedByLock()) {
-            mLock.release();
-        }
         return result;
     }
-
     /*
      * Clears the communication device for the passed in audio device types, given that the device
      * has previously been set for communication.
@@ -174,8 +210,23 @@
      */
     public void clearCommunicationDevice(int audioDeviceType) {
         if (Flags.communicationDeviceProtectedByLock()) {
-            mLock.tryAcquire();
+            mLock.lock();
         }
+        try {
+            processClearCommunicationDevice(audioDeviceType);
+        } finally {
+            if (Flags.communicationDeviceProtectedByLock()) {
+                mLock.unlock();
+            }
+        }
+    }
+
+    public void processClearCommunicationDevice(int audioDeviceType) {
+        if (audioDeviceType == sAUDIO_DEVICE_TYPE_INVALID) {
+            Log.i(this, "clearCommunicationDevice: Skip clearing communication device"
+                    + "for invalid audio type (-1).");
+        }
+
         // There is only one audio device type associated with each type of BT device.
         boolean isBtDevice = BT_AUDIO_DEVICE_INFO_TYPES.contains(audioDeviceType);
         Log.i(this, "clearCommunicationDevice: type = %s, isBtDevice = %s",
@@ -184,10 +235,10 @@
         if (audioDeviceType != mAudioDeviceType
                 && !isUsbHeadsetType(audioDeviceType, mAudioDeviceType)) {
             Log.i(this, "Unable to clear communication device of type(s), %s. "
-                    + "Device does not correspond to the locally requested device type.",
+                            + "Device does not correspond to the locally requested device type.",
                     audioDeviceType == AudioDeviceInfo.TYPE_WIRED_HEADSET
                             ? Arrays.asList(AudioDeviceInfo.TYPE_WIRED_HEADSET,
-                                    AudioDeviceInfo.TYPE_USB_HEADSET)
+                            AudioDeviceInfo.TYPE_USB_HEADSET)
                             : audioDeviceType
             );
             return;
@@ -207,13 +258,10 @@
             mBluetoothRouteManager.onAudioLost(mBtAudioDevice);
             mBtAudioDevice = null;
         }
-        if (Flags.communicationDeviceProtectedByLock()) {
-            mLock.release();
-        }
     }
 
     private boolean isUsbHeadsetType(int audioDeviceType, int sourceType) {
-        return audioDeviceType != AudioDeviceInfo.TYPE_WIRED_HEADSET
-                ? false : sourceType == AudioDeviceInfo.TYPE_USB_HEADSET;
+        return audioDeviceType == AudioDeviceInfo.TYPE_WIRED_HEADSET
+                && sourceType == AudioDeviceInfo.TYPE_USB_HEADSET;
     }
 }
diff --git a/src/com/android/server/telecom/CallAudioManager.java b/src/com/android/server/telecom/CallAudioManager.java
index e5678a0..1f1ca9d 100644
--- a/src/com/android/server/telecom/CallAudioManager.java
+++ b/src/com/android/server/telecom/CallAudioManager.java
@@ -20,6 +20,8 @@
 import android.content.Context;
 import android.media.IAudioService;
 import android.media.ToneGenerator;
+import android.os.Handler;
+import android.os.HandlerThread;
 import android.os.UserHandle;
 import android.telecom.CallAudioState;
 import android.telecom.Log;
@@ -36,6 +38,7 @@
 import java.util.HashSet;
 import java.util.Set;
 import java.util.LinkedHashSet;
+import java.util.concurrent.CompletableFuture;
 import java.util.stream.Collectors;
 
 
@@ -66,9 +69,13 @@
 
     private Call mStreamingCall;
     private Call mForegroundCall;
+    private CompletableFuture<Boolean> mCallRingingFuture;
+    private Thread mBtIcsBindingThread;
     private boolean mIsTonePlaying = false;
     private boolean mIsDisconnectedTonePlaying = false;
     private InCallTonePlayer mHoldTonePlayer;
+    private final HandlerThread mHandlerThread;
+    private final Handler mHandler;
 
     public CallAudioManager(CallAudioRouteAdapter callAudioRouteAdapter,
             CallsManager callsManager,
@@ -105,6 +112,9 @@
         mBluetoothStateReceiver = bluetoothStateReceiver;
         mDtmfLocalTonePlayer = dtmfLocalTonePlayer;
         mFeatureFlags = featureFlags;
+        mHandlerThread = new HandlerThread(this.getClass().getSimpleName());
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper());
 
         mPlayerFactory.setCallAudioManager(this);
         mCallAudioModeStateMachine.setCallAudioManager(this);
@@ -750,14 +760,42 @@
 
     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();
+            Log.i(this, "onCallEnteringRinging: mFeatureFlags.separatelyBindToBtIncallService() ? %s",
+                    mFeatureFlags.separatelyBindToBtIncallService());
+            Log.i(this, "onCallEnteringRinging: mRingingCalls.getFirst().getBtIcsFuture() = %s",
+                    mRingingCalls.getFirst().getBtIcsFuture());
+            if (mFeatureFlags.separatelyBindToBtIncallService()
+                    && mRingingCalls.getFirst().getBtIcsFuture() != null) {
+                mCallRingingFuture  = mRingingCalls.getFirst().getBtIcsFuture()
+                        .thenComposeAsync((completed) -> {
+                            mCallAudioModeStateMachine.sendMessageWithArgs(
+                                    CallAudioModeStateMachine.NEW_RINGING_CALL,
+                                    makeArgsForModeStateMachine());
+                            return CompletableFuture.completedFuture(completed);
+                        }, new LoggedHandlerExecutor(mHandler, "CAM.oCER", mCallsManager.getLock()))
+                        .exceptionally((throwable) -> {
+                            Log.e(this, throwable, "Error while executing BT ICS future");
+                            // Fallback on performing computation on a separate thread.
+                            handleBtBindingWaitFallback();
+                            return null;
+                        });
+            } else {
+                mCallAudioModeStateMachine.sendMessageWithArgs(
+                        CallAudioModeStateMachine.NEW_RINGING_CALL,
+                        makeArgsForModeStateMachine());
             }
+        }
+    }
+
+    private void handleBtBindingWaitFallback() {
+        // Wait until the BT ICS binding completed to request further audio route change
+        mBtIcsBindingThread = new Thread(() -> {
+            mRingingCalls.getFirst().waitForBtIcs();
             mCallAudioModeStateMachine.sendMessageWithArgs(
                     CallAudioModeStateMachine.NEW_RINGING_CALL,
                     makeArgsForModeStateMachine());
-        }
+        });
+        mBtIcsBindingThread.start();
     }
 
     private void onCallEnteringHold() {
@@ -889,12 +927,14 @@
         // we will not play a disconnect tone.
         if (call.isHandoverInProgress()) {
             Log.i(LOG_TAG, "Omitting tone because %s is being handed over.", call);
+            completeDisconnectToneFuture(call);
             return;
         }
 
         if (mForegroundCall != null && call != mForegroundCall && mCalls.size() > 1) {
             Log.v(LOG_TAG, "Omitting tone because we are not foreground" +
                     " and there is another call.");
+            completeDisconnectToneFuture(call);
             return;
         }
 
@@ -935,6 +975,8 @@
                     mCallsManager.onDisconnectedTonePlaying(call, true);
                     mIsDisconnectedTonePlaying = true;
                 }
+            } else {
+                completeDisconnectToneFuture(call);
             }
         }
     }
@@ -1022,6 +1064,14 @@
                 oldState == CallState.ON_HOLD;
     }
 
+    private void completeDisconnectToneFuture(Call call) {
+        CompletableFuture<Void> disconnectedToneFuture = mCallsManager.getInCallController()
+                .getDisconnectedToneBtFutures().get(call.getId());
+        if (disconnectedToneFuture != null) {
+            disconnectedToneFuture.complete(null);
+        }
+    }
+
     @VisibleForTesting
     public Set<Call> getTrackedCalls() {
         return mCalls;
@@ -1031,4 +1081,9 @@
     public SparseArray<LinkedHashSet<Call>> getCallStateToCalls() {
         return mCallStateToCalls;
     }
+
+    @VisibleForTesting
+    public CompletableFuture<Boolean> getCallRingingFuture() {
+        return mCallRingingFuture;
+    }
 }
diff --git a/src/com/android/server/telecom/CallAudioRouteAdapter.java b/src/com/android/server/telecom/CallAudioRouteAdapter.java
index 5585d09..9927c22 100644
--- a/src/com/android/server/telecom/CallAudioRouteAdapter.java
+++ b/src/com/android/server/telecom/CallAudioRouteAdapter.java
@@ -134,5 +134,6 @@
     CallAudioState getCurrentCallAudioState();
     boolean isHfpDeviceAvailable();
     Handler getAdapterHandler();
+    PendingAudioRoute getPendingAudioRoute();
     void dump(IndentingPrintWriter pw);
 }
diff --git a/src/com/android/server/telecom/CallAudioRouteController.java b/src/com/android/server/telecom/CallAudioRouteController.java
index d38577d..76555c3 100644
--- a/src/com/android/server/telecom/CallAudioRouteController.java
+++ b/src/com/android/server/telecom/CallAudioRouteController.java
@@ -18,30 +18,32 @@
 
 import static com.android.server.telecom.AudioRoute.BT_AUDIO_ROUTE_TYPES;
 import static com.android.server.telecom.AudioRoute.TYPE_INVALID;
+import static com.android.server.telecom.AudioRoute.TYPE_SPEAKER;
 
-import android.app.ActivityManager;
+import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeAudio;
+import android.bluetooth.BluetoothProfile;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
-import android.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.Message;
 import android.os.RemoteException;
-import android.os.UserHandle;
 import android.telecom.CallAudioState;
 import android.telecom.Log;
 import android.telecom.Logging.Session;
+import android.telecom.VideoProfile;
 import android.util.ArrayMap;
+import android.util.Pair;
 
 import androidx.annotation.NonNull;
 
@@ -49,8 +51,11 @@
 import com.android.internal.os.SomeArgs;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.server.telecom.bluetooth.BluetoothRouteManager;
+import com.android.server.telecom.flags.FeatureFlags;
 
+import java.util.HashMap;
 import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -72,6 +77,9 @@
         ROUTE_MAP.put(AudioRoute.TYPE_STREAMING, CallAudioState.ROUTE_STREAMING);
     }
 
+    /** Valid values for the first argument for SWITCH_BASELINE_ROUTE */
+    public static final int INCLUDE_BLUETOOTH_IN_BASELINE = 1;
+
     private final CallsManager mCallsManager;
     private final Context mContext;
     private AudioManager mAudioManager;
@@ -87,11 +95,17 @@
     private AudioRoute mStreamingRoute;
     private Set<AudioRoute> mStreamingRoutes;
     private Map<AudioRoute, BluetoothDevice> mBluetoothRoutes;
+    private Pair<Integer, String> mActiveBluetoothDevice;
+    private Map<Integer, String> mActiveDeviceCache;
     private Map<Integer, AudioRoute> mTypeRoutes;
     private PendingAudioRoute mPendingAudioRoute;
     private AudioRoute.Factory mAudioRouteFactory;
+    private StatusBarNotifier mStatusBarNotifier;
+    private FeatureFlags mFeatureFlags;
     private int mFocusType;
+    private boolean mIsScoAudioConnected;
     private final Object mLock = new Object();
+    private final TelecomSystem.SyncRoot mTelecomLock;
     private final BroadcastReceiver mSpeakerPhoneChangeReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
@@ -102,7 +116,9 @@
                         AudioDeviceInfo info = mAudioManager.getCommunicationDevice();
                         if ((info != null) &&
                                 (info.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER)) {
-                            sendMessageWithSessionInfo(SPEAKER_ON);
+                            if (mCurrentRoute.getType() != AudioRoute.TYPE_SPEAKER) {
+                                sendMessageWithSessionInfo(SPEAKER_ON);
+                            }
                         } else {
                             sendMessageWithSessionInfo(SPEAKER_OFF);
                         }
@@ -152,12 +168,11 @@
     private boolean mIsActive;
 
     public CallAudioRouteController(
-            Context context,
-            CallsManager callsManager,
+            Context context, CallsManager callsManager,
             CallAudioManager.AudioServiceFactory audioServiceFactory,
-            AudioRoute.Factory audioRouteFactory,
-            WiredHeadsetManager wiredHeadsetManager,
-            BluetoothRouteManager bluetoothRouteManager) {
+            AudioRoute.Factory audioRouteFactory, WiredHeadsetManager wiredHeadsetManager,
+            BluetoothRouteManager bluetoothRouteManager, StatusBarNotifier statusBarNotifier,
+            FeatureFlags featureFlags) {
         mContext = context;
         mCallsManager = callsManager;
         mAudioManager = context.getSystemService(AudioManager.class);
@@ -166,7 +181,11 @@
         mWiredHeadsetManager = wiredHeadsetManager;
         mIsMute = false;
         mBluetoothRouteManager = bluetoothRouteManager;
+        mStatusBarNotifier = statusBarNotifier;
+        mFeatureFlags = featureFlags;
         mFocusType = NO_FOCUS;
+        mIsScoAudioConnected = false;
+        mTelecomLock = callsManager.getLock();
         HandlerThread handlerThread = new HandlerThread(this.getClass().getSimpleName());
         handlerThread.start();
 
@@ -246,8 +265,14 @@
                         case USER_SWITCH_SPEAKER:
                             handleSwitchSpeaker();
                             break;
+                        case SWITCH_BASELINE_ROUTE:
+                            address = (String) ((SomeArgs) msg.obj).arg2;
+                            handleSwitchBaselineRoute(msg.arg1 == INCLUDE_BLUETOOTH_IN_BASELINE,
+                                    address);
+                            break;
                         case USER_SWITCH_BASELINE_ROUTE:
-                            handleSwitchBaselineRoute();
+                            handleSwitchBaselineRoute(msg.arg1 == INCLUDE_BLUETOOTH_IN_BASELINE,
+                                    null);
                             break;
                         case SPEAKER_ON:
                             handleSpeakerOn();
@@ -276,7 +301,7 @@
                             handleMuteChanged(false);
                             break;
                         case MUTE_EXTERNALLY_CHANGED:
-                            handleMuteChanged(mAudioManager.isMasterMute());
+                            handleMuteChanged(mAudioManager.isMicrophoneMute());
                             break;
                         case SWITCH_FOCUS:
                             focus = msg.arg1;
@@ -296,16 +321,21 @@
     @Override
     public void initialize() {
         mAvailableRoutes = new HashSet<>();
-        mBluetoothRoutes = new ArrayMap<>();
+        mBluetoothRoutes = new LinkedHashMap<>();
+        mActiveDeviceCache = new HashMap<>();
+        mActiveDeviceCache.put(AudioRoute.TYPE_BLUETOOTH_SCO, null);
+        mActiveDeviceCache.put(AudioRoute.TYPE_BLUETOOTH_HA, null);
+        mActiveDeviceCache.put(AudioRoute.TYPE_BLUETOOTH_LE, null);
+        mActiveBluetoothDevice = null;
         mTypeRoutes = new ArrayMap<>();
         mStreamingRoutes = new HashSet<>();
-        mPendingAudioRoute = new PendingAudioRoute(this, mAudioManager);
+        mPendingAudioRoute = new PendingAudioRoute(this, mAudioManager, mBluetoothRouteManager);
         mStreamingRoute = new AudioRoute(AudioRoute.TYPE_STREAMING, null, null);
         mStreamingRoutes.add(mStreamingRoute);
 
-        int supportMask = calculateSupportedRouteMask();
+        int supportMask = calculateSupportedRouteMaskInit();
         if ((supportMask & CallAudioState.ROUTE_SPEAKER) != 0) {
-            // Create spekaer routes
+            // Create speaker routes
             mSpeakerDockRoute = mAudioRouteFactory.create(AudioRoute.TYPE_SPEAKER, null,
                     mAudioManager);
             if (mSpeakerDockRoute == null) {
@@ -391,7 +421,7 @@
 
     @Override
     public CallAudioState getCurrentCallAudioState() {
-        return null;
+        return mCallAudioState;
     }
 
     @Override
@@ -405,6 +435,11 @@
     }
 
     @Override
+    public PendingAudioRoute getPendingAudioRoute() {
+        return mPendingAudioRoute;
+    }
+
+    @Override
     public void dump(IndentingPrintWriter pw) {
     }
 
@@ -434,6 +469,7 @@
 
     private void routeTo(boolean active, AudioRoute destRoute) {
         if (!destRoute.equals(mStreamingRoute) && !getAvailableRoutes().contains(destRoute)) {
+            Log.i(this, "Ignore routing to unavailable route: %s", destRoute);
             return;
         }
         if (mIsPending) {
@@ -443,11 +479,13 @@
             Log.i(this, "Override current pending route destination from %s(active=%b) to "
                             + "%s(active=%b)",
                     mPendingAudioRoute.getDestRoute(), mIsActive, destRoute, active);
+            // Ensure we don't keep waiting for SPEAKER_ON if dest route gets overridden.
+            if (active && mPendingAudioRoute.getDestRoute().getType() == TYPE_SPEAKER) {
+                mPendingAudioRoute.clearPendingMessage(new Pair<>(SPEAKER_ON, null));
+            }
             // override pending route while keep waiting for still pending messages for the
             // previous pending route
-            mIsActive = active;
             mPendingAudioRoute.setOrigRoute(mIsActive, mPendingAudioRoute.getDestRoute());
-            mPendingAudioRoute.setDestRoute(active, destRoute);
         } else {
             if (mCurrentRoute.equals(destRoute) && (mIsActive == active)) {
                 return;
@@ -461,10 +499,11 @@
                 // Avoid waiting for pending messages for an unavailable route
                 mPendingAudioRoute.setOrigRoute(mIsActive, DUMMY_ROUTE);
             }
-            mPendingAudioRoute.setDestRoute(active, destRoute);
-            mIsActive = active;
             mIsPending = true;
         }
+        mPendingAudioRoute.setDestRoute(active, destRoute, mBluetoothRoutes.get(destRoute),
+                mIsScoAudioConnected);
+        mIsActive = active;
         mPendingAudioRoute.evaluatePendingState();
         postTimeoutMessage();
     }
@@ -512,7 +551,7 @@
 
         // Route to expected state
         if (mCurrentRoute.equals(wiredHeadsetRoute)) {
-            routeTo(mIsActive, getBaseRoute(true));
+            routeTo(mIsActive, getBaseRoute(true, null));
         }
     }
 
@@ -551,7 +590,7 @@
 
         // Route to expected state
         if (mCurrentRoute.equals(dockRoute)) {
-            routeTo(mIsActive, getBaseRoute(true));
+            routeTo(mIsActive, getBaseRoute(true, null));
         }
     }
 
@@ -567,38 +606,71 @@
         if (mCurrentRoute.equals(mStreamingRoute)) {
             mCurrentRoute = DUMMY_ROUTE;
             onAvailableRoutesChanged();
-            routeTo(mIsActive, getBaseRoute(true));
+            routeTo(mIsActive, getBaseRoute(true, null));
         } else {
             Log.i(this, "ignore disable streaming, not in streaming");
         }
     }
 
+    /**
+     * Handles the case when SCO audio is connected for the BT headset. This follows shortly after
+     * the BT device has been established as an active device (BT_ACTIVE_DEVICE_PRESENT) and doesn't
+     * apply to other BT device types. In this case, the pending audio route will process the
+     * BT_AUDIO_CONNECTED message that will trigger routing to the pending destination audio route;
+     * otherwise, routing will be ignored if there aren't pending routes to be processed.
+     *
+     * Message being handled: BT_AUDIO_CONNECTED
+     */
     private void handleBtAudioActive(BluetoothDevice bluetoothDevice) {
         if (mIsPending) {
+            Log.i(this, "handleBtAudioActive: is pending path");
             if (Objects.equals(mPendingAudioRoute.getDestRoute().getBluetoothAddress(),
                     bluetoothDevice.getAddress())) {
-                mPendingAudioRoute.onMessageReceived(BT_AUDIO_CONNECTED);
+                mPendingAudioRoute.onMessageReceived(new Pair<>(BT_AUDIO_CONNECTED,
+                        bluetoothDevice.getAddress()), null);
             }
         } else {
             // ignore, not triggered by telecom
+            Log.i(this, "handleBtAudioActive: ignoring handling bt audio active.");
         }
     }
 
+    /**
+     * Handles the case when SCO audio is disconnected for the BT headset. In this case, the pending
+     * audio route will process the BT_AUDIO_DISCONNECTED message which will trigger routing to the
+     * pending destination audio route; otherwise, routing will be ignored if there aren't any
+     * pending routes to be processed.
+     *
+     * Message being handled: BT_AUDIO_DISCONNECTED
+     */
     private void handleBtAudioInactive(BluetoothDevice bluetoothDevice) {
         if (mIsPending) {
+            Log.i(this, "handleBtAudioInactive: is pending path");
             if (Objects.equals(mPendingAudioRoute.getOrigRoute().getBluetoothAddress(),
                     bluetoothDevice.getAddress())) {
-                mPendingAudioRoute.onMessageReceived(BT_AUDIO_DISCONNECTED);
+                mPendingAudioRoute.onMessageReceived(new Pair<>(BT_AUDIO_DISCONNECTED,
+                        bluetoothDevice.getAddress()), null);
             }
         } else {
             // ignore, not triggered by telecom
+            Log.i(this, "handleBtAudioInactive: ignoring handling bt audio inactive.");
         }
     }
 
+    /**
+     * This particular routing occurs when the BT device is trying to establish itself as a
+     * connected device (refer to BluetoothStateReceiver#handleConnectionStateChanged). The device
+     * is included as an available route and cached into the current BT routes.
+     *
+     * Message being handled: BT_DEVICE_ADDED
+     */
     private void handleBtConnected(@AudioRoute.AudioRouteType int type,
                                    BluetoothDevice bluetoothDevice) {
-        AudioRoute bluetoothRoute = null;
-        bluetoothRoute = mAudioRouteFactory.create(type, bluetoothDevice.getAddress(),
+        if (containsHearingAidPair(type, bluetoothDevice)) {
+            return;
+        }
+
+        AudioRoute bluetoothRoute = mAudioRouteFactory.create(type, bluetoothDevice.getAddress(),
                 mAudioManager);
         if (bluetoothRoute == null) {
             Log.w(this, "Can't find available audio device info for route type:"
@@ -611,6 +683,14 @@
         }
     }
 
+    /**
+     * Handles the case when the BT device is in a disconnecting/disconnected state. In this case,
+     * the audio route for the specified device is removed from the available BT routes and the
+     * audio is routed to an available route if the current route is pointing to the device which
+     * got disconnected.
+     *
+     * Message being handled: BT_DEVICE_REMOVED
+     */
     private void handleBtDisconnected(@AudioRoute.AudioRouteType int type,
                                       BluetoothDevice bluetoothDevice) {
         // Clean up unavailable routes
@@ -624,31 +704,51 @@
 
         // Fallback to an available route
         if (Objects.equals(mCurrentRoute, bluetoothRoute)) {
-            routeTo(mIsActive, getBaseRoute(false));
+            routeTo(mIsActive, getBaseRoute(true, null));
         }
     }
 
+    /**
+     * This particular routing occurs when the specified bluetooth device is marked as the active
+     * device (refer to BluetoothStateReceiver#handleActiveDeviceChanged). This takes care of
+     * moving the call audio route to the bluetooth route.
+     *
+     * Message being handled: BT_ACTIVE_DEVICE_PRESENT
+     */
     private void handleBtActiveDevicePresent(@AudioRoute.AudioRouteType int type,
                                              String deviceAddress) {
         AudioRoute bluetoothRoute = getBluetoothRoute(type, deviceAddress);
         if (bluetoothRoute != null) {
-            Log.i(this, "request to route to bluetooth route: %s(active=%b)", bluetoothRoute,
+            Log.i(this, "request to route to bluetooth route: %s (active=%b)", bluetoothRoute,
                     mIsActive);
             routeTo(mIsActive, bluetoothRoute);
+        } else {
+            Log.i(this, "request to route to unavailable bluetooth route - type (%s), address (%s)",
+                    type, deviceAddress);
         }
     }
 
+    /**
+     * Handles routing for when the active BT device is removed for a given audio route type. In
+     * this case, the audio is routed to another available route if the current route hasn't been
+     * adjusted yet or there is a pending destination route associated with the device type that
+     * went inactive. Note that BT_DEVICE_REMOVED will be processed first in this case, which will
+     * handle removing the BT route for the device that went inactive as well as falling back to
+     * an available route.
+     *
+     * Message being handled: BT_ACTIVE_DEVICE_GONE
+     */
     private void handleBtActiveDeviceGone(@AudioRoute.AudioRouteType int type) {
         if ((mIsPending && mPendingAudioRoute.getDestRoute().getType() == type)
                 || (!mIsPending && mCurrentRoute.getType() == type)) {
             // Fallback to an available route
-            routeTo(mIsActive, getBaseRoute(true));
+            routeTo(mIsActive, getBaseRoute(true, null));
         }
     }
 
     private void handleMuteChanged(boolean mute) {
         mIsMute = mute;
-        if (mIsMute != mAudioManager.isMasterMute() && mIsActive) {
+        if (mIsMute != mAudioManager.isMicrophoneMute() && mIsActive) {
             IAudioService audioService = mAudioServiceFactory.getAudioService();
             Log.i(this, "changing microphone mute state to: %b [serviceIsNull=%b]", mute,
                     audioService == null);
@@ -667,30 +767,37 @@
     }
 
     private void handleSwitchFocus(int focus) {
+        Log.i(this, "handleSwitchFocus: focus (%s)", focus);
         mFocusType = focus;
         switch (focus) {
             case NO_FOCUS -> {
                 if (mIsActive) {
+                    // Reset mute state after call ends.
                     handleMuteChanged(false);
+                    // Route back to inactive route.
                     routeTo(false, mCurrentRoute);
+                    // Clear pending messages
+                    mPendingAudioRoute.clearPendingMessages();
                 }
             }
             case ACTIVE_FOCUS -> {
-                if (!mIsActive) {
-                    routeTo(true, getBaseRoute(true));
-                }
+                // Route to active baseline route (we may need to change audio route in the case
+                // when a video call is put on hold).
+                routeTo(true, getBaseRoute(true, null));
             }
             case RINGING_FOCUS -> {
                 if (!mIsActive) {
-                    AudioRoute route = getBaseRoute(true);
+                    AudioRoute route = getBaseRoute(true, null);
                     BluetoothDevice device = mBluetoothRoutes.get(route);
+                    // Check if in-band ringtone is enabled for the device; if it isn't, move to
+                    // inactive route.
                     if (device != null && !mBluetoothRouteManager.isInbandRingEnabled(device)) {
                         routeTo(false, route);
                     } else {
                         routeTo(true, route);
                     }
                 } else {
-                    // active
+                    // Route is already active.
                     BluetoothDevice device = mBluetoothRoutes.get(mCurrentRoute);
                     if (device != null && !mBluetoothRouteManager.isInbandRingEnabled(device)) {
                         routeTo(false, mCurrentRoute);
@@ -713,11 +820,16 @@
         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 (address == null) {
+            bluetoothRoute = getArbitraryBluetoothDevice();
+            bluetoothDevice = mBluetoothRoutes.get(bluetoothRoute);
+        } else {
+            for (AudioRoute route : getAvailableRoutes()) {
+                if (Objects.equals(address, route.getBluetoothAddress())) {
+                    bluetoothRoute = route;
+                    bluetoothDevice = mBluetoothRoutes.get(route);
+                    break;
+                }
             }
         }
 
@@ -729,16 +841,30 @@
                 routeTo(mIsActive, bluetoothRoute);
             }
         } else {
-            Log.i(this, "ignore switch bluetooth request");
+            Log.i(this, "ignore switch bluetooth request to unavailable address");
         }
     }
 
+    /**
+     * Retrieve the active BT device, if available, otherwise return the most recently tracked
+     * active device, or null if none are available.
+     * @return {@link AudioRoute} of the BT device.
+     */
+    private AudioRoute getArbitraryBluetoothDevice() {
+        if (mActiveBluetoothDevice != null) {
+            return getBluetoothRoute(mActiveBluetoothDevice.first, mActiveBluetoothDevice.second);
+        } else if (!mBluetoothRoutes.isEmpty()) {
+            return mBluetoothRoutes.keySet().stream().toList().get(mBluetoothRoutes.size() - 1);
+        }
+        return null;
+    }
+
     private void handleSwitchHeadset() {
         AudioRoute headsetRoute = mTypeRoutes.get(AudioRoute.TYPE_WIRED);
         if (headsetRoute != null && getAvailableRoutes().contains(headsetRoute)) {
             routeTo(mIsActive, headsetRoute);
         } else {
-            Log.i(this, "ignore switch speaker request");
+            Log.i(this, "ignore switch headset request");
         }
     }
 
@@ -750,13 +876,16 @@
         }
     }
 
-    private void handleSwitchBaselineRoute() {
-        routeTo(mIsActive, getBaseRoute(true));
+    private void handleSwitchBaselineRoute(boolean includeBluetooth, String btAddressToExclude) {
+        routeTo(mIsActive, getBaseRoute(includeBluetooth, btAddressToExclude));
     }
 
     private void handleSpeakerOn() {
         if (isPending()) {
-            mPendingAudioRoute.onMessageReceived(SPEAKER_ON);
+            Log.i(this, "handleSpeakerOn: sending SPEAKER_ON to pending audio route");
+            mPendingAudioRoute.onMessageReceived(new Pair<>(SPEAKER_ON, null), null);
+            // Update status bar notification if we are in a call.
+            mStatusBarNotifier.notifySpeakerphone(mCallsManager.hasAnyCalls());
         } else {
             if (mSpeakerDockRoute != null && getAvailableRoutes().contains(mSpeakerDockRoute)) {
                 routeTo(mIsActive, mSpeakerDockRoute);
@@ -771,9 +900,12 @@
 
     private void handleSpeakerOff() {
         if (isPending()) {
-            mPendingAudioRoute.onMessageReceived(SPEAKER_OFF);
+            Log.i(this, "handleSpeakerOff - sending SPEAKER_OFF to pending audio route");
+            mPendingAudioRoute.onMessageReceived(new Pair<>(SPEAKER_OFF, null), null);
+            // Update status bar notification
+            mStatusBarNotifier.notifySpeakerphone(false);
         } else if (mCurrentRoute.getType() == AudioRoute.TYPE_SPEAKER) {
-            routeTo(mIsActive, getBaseRoute(true));
+            routeTo(mIsActive, getBaseRoute(true, null));
             // Since the route switching triggered by this message, we need to manually send it
             // again so that we won't stuck in the pending route
             if (mIsActive) {
@@ -783,12 +915,17 @@
         }
     }
 
+    /**
+     * This is invoked when there are no more pending audio routes to be processed, which signals
+     * a change for the current audio route and the call audio state to be updated accordingly.
+     */
     public void handleExitPendingRoute() {
         if (mIsPending) {
-            Log.i(this, "Exit pending route and enter %s(active=%b)",
-                    mPendingAudioRoute.getDestRoute(), mIsActive);
             mCurrentRoute = mPendingAudioRoute.getDestRoute();
+            Log.addEvent(mCallsManager.getForegroundCall(), LogUtils.Events.AUDIO_ROUTE,
+                    "Entering audio route: " + mCurrentRoute + " (active=" + mIsActive + ")");
             mIsPending = false;
+            mPendingAudioRoute.clearPendingMessages();
             onCurrentRouteChanged();
         }
     }
@@ -817,7 +954,21 @@
             for (AudioRoute route : getAvailableRoutes()) {
                 routeMask |= ROUTE_MAP.get(route.getType());
                 if (BT_AUDIO_ROUTE_TYPES.contains(route.getType())) {
-                    availableBluetoothDevices.add(mBluetoothRoutes.get(route));
+                    BluetoothDevice deviceToAdd = mBluetoothRoutes.get(route);
+                    // Only include the lead device for LE audio (otherwise, the routes will show
+                    // two separate devices in the UI).
+                    if (route.getType() == AudioRoute.TYPE_BLUETOOTH_LE
+                            && getLeAudioService() != null) {
+                        int groupId = getLeAudioService().getGroupId(deviceToAdd);
+                        if (groupId != BluetoothLeAudio.GROUP_ID_INVALID) {
+                            deviceToAdd = getLeAudioService().getConnectedGroupLeadDevice(groupId);
+                        }
+                    }
+                    // This will only ever be null when the lead device (LE) is disconnected and
+                    // try to obtain the lead device for the 2nd bud.
+                    if (deviceToAdd != null) {
+                        availableBluetoothDevices.add(deviceToAdd);
+                    }
                 }
             }
             updateCallAudioState(new CallAudioState(mIsMute, mCallAudioState.getRoute(), routeMask,
@@ -831,10 +982,12 @@
                 mCallAudioState.getSupportedBluetoothDevices()));
     }
 
-    private void updateCallAudioState(CallAudioState callAudioState) {
-        Log.i(this, "updateCallAudioState: " + callAudioState);
+    private void updateCallAudioState(CallAudioState newCallAudioState) {
+        Log.i(this, "updateCallAudioState: updating call audio state to %s", newCallAudioState);
         CallAudioState oldState = mCallAudioState;
-        mCallAudioState = callAudioState;
+        mCallAudioState = newCallAudioState;
+        // Update status bar notification
+        mStatusBarNotifier.notifyMute(newCallAudioState.isMuted());
         mCallsManager.onCallAudioStateChanged(oldState, mCallAudioState);
         updateAudioStateForTrackedCalls(mCallAudioState);
     }
@@ -866,6 +1019,7 @@
 
         // Get preferred device
         AudioDeviceAttributes deviceAttr = mAudioManager.getPreferredDeviceForStrategy(strategy);
+        Log.i(this, "getPreferredAudioRouteFromStrategy: preferred device is %s", deviceAttr);
         if (deviceAttr == null) {
             return null;
         }
@@ -881,16 +1035,48 @@
         }
     }
 
-    private AudioRoute getPreferredAudioRouteFromDefault(boolean includeBluetooth) {
-        if (mBluetoothRoutes.isEmpty() || !includeBluetooth) {
-            return mEarpieceWiredRoute != null ? mEarpieceWiredRoute : mSpeakerDockRoute;
+    private AudioRoute getPreferredAudioRouteFromDefault(boolean includeBluetooth,
+            String btAddressToExclude) {
+        boolean skipEarpiece;
+        Call foregroundCall = mCallAudioManager.getForegroundCall();
+        synchronized (mTelecomLock) {
+            skipEarpiece = foregroundCall != null
+                    && VideoProfile.isVideo(foregroundCall.getVideoState());
+        }
+        // Route to earpiece, wired, or speaker route if there are not bluetooth routes or if there
+        // are only wearables available.
+        AudioRoute activeWatchOrNonWatchDeviceRoute =
+                getActiveWatchOrNonWatchDeviceRoute(btAddressToExclude);
+        if (mBluetoothRoutes.isEmpty() || !includeBluetooth
+                || activeWatchOrNonWatchDeviceRoute == null) {
+            Log.i(this, "getPreferredAudioRouteFromDefault: Audio routing defaulting to "
+                    + "available non-BT route.");
+            AudioRoute defaultRoute = mEarpieceWiredRoute != null
+                    ? mEarpieceWiredRoute
+                    : mSpeakerDockRoute;
+            // Ensure that we default to speaker route if we're in a video call, but disregard it if
+            // a wired headset is plugged in.
+            if (skipEarpiece && defaultRoute.getType() == AudioRoute.TYPE_EARPIECE) {
+                Log.i(this, "getPreferredAudioRouteFromDefault: Audio routing defaulting to "
+                        + "speaker route for video call.");
+                defaultRoute = mSpeakerDockRoute;
+            }
+            return defaultRoute;
         } else {
-            // Most recent active route will always be the last in the array
-            return mBluetoothRoutes.keySet().stream().toList().get(mBluetoothRoutes.size() - 1);
+            // Most recent active route will always be the last in the array (ensure that we don't
+            // auto route to a wearable device unless it's already active).
+            String autoRoutingToWatchExcerpt = mFeatureFlags.ignoreAutoRouteToWatchDevice()
+                    ? " (except watch)"
+                    : "";
+            Log.i(this, "getPreferredAudioRouteFromDefault: Audio routing defaulting to "
+                    + "most recently active BT route" + autoRoutingToWatchExcerpt + ".");
+            return activeWatchOrNonWatchDeviceRoute;
         }
     }
 
-    private int calculateSupportedRouteMask() {
+    private int calculateSupportedRouteMaskInit() {
+        Log.i(this, "calculateSupportedRouteMaskInit: is wired headset plugged in - %s",
+                mWiredHeadsetManager.isPluggedIn());
         int routeMask = CallAudioState.ROUTE_SPEAKER;
 
         if (mWiredHeadsetManager.isPluggedIn()) {
@@ -921,8 +1107,8 @@
         return mCurrentRoute;
     }
 
-    private AudioRoute getBluetoothRoute(@AudioRoute.AudioRouteType int audioRouteType,
-                                         String address) {
+    public AudioRoute getBluetoothRoute(@AudioRoute.AudioRouteType int audioRouteType,
+            String address) {
         for (AudioRoute route : mBluetoothRoutes.keySet()) {
             if (route.getType() == audioRouteType && route.getBluetoothAddress().equals(address)) {
                 return route;
@@ -931,17 +1117,144 @@
         return null;
     }
 
-    public AudioRoute getBaseRoute(boolean includeBluetooth) {
+    public AudioRoute getBaseRoute(boolean includeBluetooth, String btAddressToExclude) {
         AudioRoute destRoute = getPreferredAudioRouteFromStrategy();
-        if (destRoute == null) {
-            destRoute = getPreferredAudioRouteFromDefault(includeBluetooth);
+        if (destRoute == null || (destRoute.getBluetoothAddress() != null && !includeBluetooth)) {
+            destRoute = getPreferredAudioRouteFromDefault(includeBluetooth, btAddressToExclude);
         }
         if (destRoute != null && !getAvailableRoutes().contains(destRoute)) {
             destRoute = null;
         }
+        Log.i(this, "getBaseRoute - audio routing to %s", destRoute);
         return destRoute;
     }
 
+    /**
+     * Don't add additional AudioRoute when a hearing aid pair is detected. The devices have
+     * separate addresses, so we need to perform explicit handling to ensure we don't
+     * treat them as two separate devices.
+     */
+    private boolean containsHearingAidPair(@AudioRoute.AudioRouteType int type,
+            BluetoothDevice bluetoothDevice) {
+        // Check if it is a hearing aid pair and skip connecting to the other device in this case.
+        // Traverse mBluetoothRoutes backwards as the most recently active device will be inserted
+        // last.
+        String existingHearingAidAddress = null;
+        List<AudioRoute> bluetoothRoutes = mBluetoothRoutes.keySet().stream().toList();
+        for (int i = bluetoothRoutes.size() - 1; i >= 0; i--) {
+            AudioRoute audioRoute = bluetoothRoutes.get(i);
+            if (audioRoute.getType() == AudioRoute.TYPE_BLUETOOTH_HA) {
+                existingHearingAidAddress = audioRoute.getBluetoothAddress();
+                break;
+            }
+        }
+
+        // Check that route is for hearing aid and that there exists another hearing aid route
+        // created for the first device (of the pair) that was connected.
+        if (type == AudioRoute.TYPE_BLUETOOTH_HA && existingHearingAidAddress != null) {
+            BluetoothAdapter bluetoothAdapter = mBluetoothRouteManager.getDeviceManager()
+                    .getBluetoothAdapter();
+            if (bluetoothAdapter != null) {
+                List<BluetoothDevice> activeHearingAids =
+                        bluetoothAdapter.getActiveDevices(BluetoothProfile.HEARING_AID);
+                for (BluetoothDevice hearingAid : activeHearingAids) {
+                    if (hearingAid != null && hearingAid.getAddress() != null) {
+                        String address = hearingAid.getAddress();
+                        if (address.equals(bluetoothDevice.getAddress())
+                                || address.equals(existingHearingAidAddress)) {
+                            Log.i(this, "containsHearingAidPair: Detected a hearing aid "
+                                    + "pair, ignoring creating a new AudioRoute");
+                            return true;
+                        }
+                    }
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Prevent auto routing to a wearable device when calculating the default bluetooth audio route
+     * to move to. This function ensures that the most recently active non-wearable device is
+     * selected for routing unless a wearable device has already been identified as an active
+     * device.
+     */
+    private AudioRoute getActiveWatchOrNonWatchDeviceRoute(String btAddressToExclude) {
+        if (!mFeatureFlags.ignoreAutoRouteToWatchDevice()) {
+            Log.i(this, "getActiveWatchOrNonWatchDeviceRoute: ignore_auto_route_to_watch_device "
+                    + "flag is disabled. Routing to most recently reported active device.");
+            return getMostRecentlyActiveBtRoute(btAddressToExclude);
+        }
+
+        List<AudioRoute> bluetoothRoutes = mBluetoothRoutes.keySet().stream().toList();
+        // Traverse the routes from the most recently active recorded devices first.
+        AudioRoute nonWatchDeviceRoute = null;
+        for (int i = bluetoothRoutes.size() - 1; i >= 0; i--) {
+            AudioRoute route = bluetoothRoutes.get(i);
+            BluetoothDevice device = mBluetoothRoutes.get(route);
+            // Skip excluded BT address and LE audio if it's not the lead device.
+            if (route.getBluetoothAddress().equals(btAddressToExclude)
+                    || isLeAudioNonLeadDeviceOrServiceUnavailable(route.getType(), device)) {
+                continue;
+            }
+            // Check if the most recently active device is a watch device.
+            if (i == (bluetoothRoutes.size() - 1) && device.equals(mCallAudioState
+                    .getActiveBluetoothDevice()) && mBluetoothRouteManager.isWatch(device)) {
+                Log.i(this, "getActiveWatchOrNonWatchDeviceRoute: Routing to active watch - %s",
+                        bluetoothRoutes.get(0));
+                return bluetoothRoutes.get(0);
+            }
+            // Record the first occurrence of a non-watch device route if found.
+            if (!mBluetoothRouteManager.isWatch(device) && nonWatchDeviceRoute == null) {
+                nonWatchDeviceRoute = route;
+                break;
+            }
+        }
+
+        Log.i(this, "Routing to a non-watch device - %s", nonWatchDeviceRoute);
+        return nonWatchDeviceRoute;
+    }
+
+    /**
+     * Returns the most actively reported bluetooth route excluding the passed in route.
+     */
+    private AudioRoute getMostRecentlyActiveBtRoute(String btAddressToExclude) {
+        List<AudioRoute> bluetoothRoutes = mBluetoothRoutes.keySet().stream().toList();
+        for (int i = bluetoothRoutes.size() - 1; i >= 0; i--) {
+            AudioRoute route = bluetoothRoutes.get(i);
+            // Skip LE route if it's not the lead device.
+            if (isLeAudioNonLeadDeviceOrServiceUnavailable(
+                    route.getType(), mBluetoothRoutes.get(route))) {
+                continue;
+            }
+            if (!route.getBluetoothAddress().equals(btAddressToExclude)) {
+                return route;
+            }
+        }
+        return null;
+    }
+
+    private boolean isLeAudioNonLeadDeviceOrServiceUnavailable(@AudioRoute.AudioRouteType int type,
+            BluetoothDevice device) {
+        if (type != AudioRoute.TYPE_BLUETOOTH_LE) {
+            return false;
+        } else if (getLeAudioService() == null) {
+            return true;
+        }
+
+        int groupId = getLeAudioService().getGroupId(device);
+        if (groupId != BluetoothLeAudio.GROUP_ID_INVALID) {
+            BluetoothDevice leadDevice = getLeAudioService().getConnectedGroupLeadDevice(groupId);
+            Log.i(this, "Lead device for device (%s) is %s.", device, leadDevice);
+            return leadDevice == null || !device.getAddress().equals(leadDevice.getAddress());
+        }
+        return false;
+    }
+
+    private BluetoothLeAudio getLeAudioService() {
+        return mBluetoothRouteManager.getDeviceManager().getLeAudioService();
+    }
+
     @VisibleForTesting
     public void setAudioManager(AudioManager audioManager) {
         mAudioManager = audioManager;
@@ -952,6 +1265,51 @@
         mAudioRouteFactory = audioRouteFactory;
     }
 
+    public Map<AudioRoute, BluetoothDevice> getBluetoothRoutes() {
+        return mBluetoothRoutes;
+    }
+
+    public void overrideIsPending(boolean isPending) {
+        mIsPending = isPending;
+    }
+
+    public void setIsScoAudioConnected(boolean value) {
+        mIsScoAudioConnected = value;
+    }
+
+    /**
+     * Update the active bluetooth device being tracked (as well as for individual profiles).
+     * We need to keep track of active devices for individual profiles because of potential
+     * inconsistencies found in BluetoothStateReceiver#handleActiveDeviceChanged. When multiple
+     * profiles are paired, we could have a scenario where an active device A is replaced
+     * with an active device B (from a different profile), which is then removed as an active
+     * device shortly after, causing device A to be reactive. It's possible that the active device
+     * changed intent is never received again for device A so an active device cache is necessary
+     * to track these devices at a profile level.
+     * @param device {@link Pair} containing the BT audio route type (i.e. SCO/HA/LE) and the
+     *                           address of the device.
+     */
+    public void updateActiveBluetoothDevice(Pair<Integer, String> device) {
+        mActiveDeviceCache.put(device.first, device.second);
+        // Update most recently active device if address isn't null (meaning some device is active).
+        if (device.second != null) {
+            mActiveBluetoothDevice = device;
+        } else {
+            // If a device was removed, check to ensure that no other device is still considered
+            // active.
+            boolean hasActiveDevice = false;
+            for (String address : mActiveDeviceCache.values()) {
+                if (address != null) {
+                    hasActiveDevice = true;
+                    break;
+                }
+            }
+            if (!hasActiveDevice) {
+                mActiveBluetoothDevice = null;
+            }
+        }
+    }
+
     @VisibleForTesting
     public void setActive(boolean active) {
         if (active) {
diff --git a/src/com/android/server/telecom/CallAudioRouteStateMachine.java b/src/com/android/server/telecom/CallAudioRouteStateMachine.java
index 26c25e8..74d23a9 100644
--- a/src/com/android/server/telecom/CallAudioRouteStateMachine.java
+++ b/src/com/android/server/telecom/CallAudioRouteStateMachine.java
@@ -288,8 +288,13 @@
             CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_EARPIECE,
                     mAvailableRoutes, null,
                     mBluetoothRouteManager.getConnectedDevices());
-            setSystemAudioState(newState, true);
-            updateInternalCallAudioState();
+            if (mFeatureFlags.earlyUpdateInternalCallAudioState()) {
+                updateInternalCallAudioState();
+                setSystemAudioState(newState, true);
+            } else {
+                setSystemAudioState(newState, true);
+                updateInternalCallAudioState();
+            }
         }
 
         @Override
@@ -511,8 +516,13 @@
             }
             CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_WIRED_HEADSET,
                     mAvailableRoutes, null, mBluetoothRouteManager.getConnectedDevices());
-            setSystemAudioState(newState, true);
-            updateInternalCallAudioState();
+            if (mFeatureFlags.earlyUpdateInternalCallAudioState()) {
+                updateInternalCallAudioState();
+                setSystemAudioState(newState, true);
+            } else {
+                setSystemAudioState(newState, true);
+                updateInternalCallAudioState();
+            }
         }
 
         @Override
@@ -749,8 +759,13 @@
             CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_BLUETOOTH,
                     mAvailableRoutes, mBluetoothRouteManager.getBluetoothAudioConnectedDevice(),
                     mBluetoothRouteManager.getConnectedDevices());
-            setSystemAudioState(newState, true);
-            updateInternalCallAudioState();
+            if (mFeatureFlags.earlyUpdateInternalCallAudioState()) {
+                updateInternalCallAudioState();
+                setSystemAudioState(newState, true);
+            } else {
+                setSystemAudioState(newState, true);
+                updateInternalCallAudioState();
+            }
             // Do not send RINGER_MODE_CHANGE if no Bluetooth SCO audio device is available
             if (mBluetoothRouteManager.getBluetoothAudioConnectedDevice() != null) {
                 mCallAudioManager.onRingerModeChange();
@@ -847,6 +862,14 @@
                     if (msg.arg1 == NO_FOCUS) {
                         // Only disconnect audio here instead of routing away from BT entirely.
                         if (mFeatureFlags.transitRouteBeforeAudioDisconnectBt()) {
+                            // Note: We have to turn off mute here rather than when entering the
+                            // QuiescentBluetooth route because setMuteOn will only work when there the
+                            // current state is active.
+                            // We don't need to do this in the unflagged path since reinitialize
+                            // will turn off mute.
+                            if (mFeatureFlags.resetMuteWhenEnteringQuiescentBtRoute()) {
+                                setMuteOn(false);
+                            }
                             transitionTo(mQuiescentBluetoothRoute);
                             mBluetoothRouteManager.disconnectAudio();
                         } else {
@@ -890,8 +913,13 @@
             CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_BLUETOOTH,
                     mAvailableRoutes, mBluetoothRouteManager.getBluetoothAudioConnectedDevice(),
                     mBluetoothRouteManager.getConnectedDevices());
-            setSystemAudioState(newState);
-            updateInternalCallAudioState();
+            if (mFeatureFlags.earlyUpdateInternalCallAudioState()) {
+                updateInternalCallAudioState();
+                setSystemAudioState(newState, true);
+            } else {
+                setSystemAudioState(newState, true);
+                updateInternalCallAudioState();
+            }
         }
 
         @Override
@@ -977,9 +1005,6 @@
         public void enter() {
             super.enter();
             mHasUserExplicitlyLeftBluetooth = false;
-            if (mFeatureFlags.resetMuteWhenEnteringQuiescentBtRoute()) {
-                setMuteOn(false);
-            }
             updateInternalCallAudioState();
         }
 
@@ -1117,8 +1142,13 @@
             mWasOnSpeaker = true;
             CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_SPEAKER,
                     mAvailableRoutes, null, mBluetoothRouteManager.getConnectedDevices());
-            setSystemAudioState(newState, true);
-            updateInternalCallAudioState();
+            if (mFeatureFlags.earlyUpdateInternalCallAudioState()) {
+                updateInternalCallAudioState();
+                setSystemAudioState(newState, true);
+            } else {
+                setSystemAudioState(newState, true);
+                updateInternalCallAudioState();
+            }
         }
 
         @Override
@@ -2095,7 +2125,14 @@
         return base;
     }
 
+    @Override
     public Handler getAdapterHandler() {
         return getHandler();
     }
+
+    @Override
+    public PendingAudioRoute getPendingAudioRoute() {
+        // Only used by CallAudioRouteController.
+        return null;
+    }
 }
diff --git a/src/com/android/server/telecom/CallEndpointController.java b/src/com/android/server/telecom/CallEndpointController.java
index 4738cd4..49c0d51 100644
--- a/src/com/android/server/telecom/CallEndpointController.java
+++ b/src/com/android/server/telecom/CallEndpointController.java
@@ -27,6 +27,7 @@
 import android.telecom.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.telecom.flags.FeatureFlags;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -49,6 +50,7 @@
 
     private final Context mContext;
     private final CallsManager mCallsManager;
+    private final FeatureFlags mFeatureFlags;
     private final HashMap<Integer, Integer> mRouteToTypeMap;
     private final HashMap<Integer, Integer> mTypeToRouteMap;
     private final Map<ParcelUuid, String> mBluetoothAddressMap = new HashMap<>();
@@ -57,10 +59,10 @@
     private ParcelUuid mRequestedEndpointId;
     private CompletableFuture<Integer> mPendingChangeRequest;
 
-    public CallEndpointController(Context context, CallsManager callsManager) {
+    public CallEndpointController(Context context, CallsManager callsManager, FeatureFlags flags) {
         mContext = context;
         mCallsManager = callsManager;
-
+        mFeatureFlags = flags;
         mRouteToTypeMap = new HashMap<>(5);
         mRouteToTypeMap.put(CallAudioState.ROUTE_EARPIECE, CallEndpoint.TYPE_EARPIECE);
         mRouteToTypeMap.put(CallAudioState.ROUTE_BLUETOOTH, CallEndpoint.TYPE_BLUETOOTH);
@@ -197,43 +199,91 @@
 
         Set<Call> calls = mCallsManager.getTrackedCalls();
         for (Call call : calls) {
-            if (call != null && call.getConnectionService() != null) {
-                call.getConnectionService().onCallEndpointChanged(call, mActiveCallEndpoint);
-            } else if (call != null && call.getTransactionServiceWrapper() != null) {
-                call.getTransactionServiceWrapper()
-                        .onCallEndpointChanged(call, mActiveCallEndpoint);
+            if (mFeatureFlags.cacheCallAudioCallbacks()) {
+                onCallEndpointChangedOrCache(call);
+            } else {
+                if (call != null && call.getConnectionService() != null) {
+                    call.getConnectionService().onCallEndpointChanged(call, mActiveCallEndpoint);
+                } else if (call != null && call.getTransactionServiceWrapper() != null) {
+                    call.getTransactionServiceWrapper()
+                            .onCallEndpointChanged(call, mActiveCallEndpoint);
+                }
             }
         }
     }
 
+    private void onCallEndpointChangedOrCache(Call call) {
+        if (call == null) {
+            return;
+        }
+        CallSourceService service = call.getService();
+        if (service != null) {
+            service.onCallEndpointChanged(call, mActiveCallEndpoint);
+        } else {
+            call.cacheServiceCallback(new CachedCurrentEndpointChange(mActiveCallEndpoint));
+        }
+    }
+
     private void notifyAvailableCallEndpointsChange() {
         mCallsManager.updateAvailableCallEndpoints(mAvailableCallEndpoints);
 
         Set<Call> calls = mCallsManager.getTrackedCalls();
         for (Call call : calls) {
-            if (call != null && call.getConnectionService() != null) {
-                call.getConnectionService().onAvailableCallEndpointsChanged(call,
-                        mAvailableCallEndpoints);
-            } else if (call != null && call.getTransactionServiceWrapper() != null) {
-                call.getTransactionServiceWrapper()
-                        .onAvailableCallEndpointsChanged(call, mAvailableCallEndpoints);
+            if (mFeatureFlags.cacheCallAudioCallbacks()) {
+                onAvailableEndpointsChangedOrCache(call);
+            } else {
+                if (call != null && call.getConnectionService() != null) {
+                    call.getConnectionService().onAvailableCallEndpointsChanged(call,
+                            mAvailableCallEndpoints);
+                } else if (call != null && call.getTransactionServiceWrapper() != null) {
+                    call.getTransactionServiceWrapper().onAvailableCallEndpointsChanged(call,
+                            mAvailableCallEndpoints);
+                }
             }
         }
     }
 
+    private void onAvailableEndpointsChangedOrCache(Call call) {
+        if (call == null) {
+            return;
+        }
+        CallSourceService service = call.getService();
+        if (service != null) {
+            service.onAvailableCallEndpointsChanged(call, mAvailableCallEndpoints);
+        } else {
+            call.cacheServiceCallback(new CachedAvailableEndpointsChange(mAvailableCallEndpoints));
+        }
+    }
+
     private void notifyMuteStateChange(boolean isMuted) {
         mCallsManager.updateMuteState(isMuted);
 
         Set<Call> calls = mCallsManager.getTrackedCalls();
         for (Call call : calls) {
-            if (call != null && call.getConnectionService() != null) {
-                call.getConnectionService().onMuteStateChanged(call, isMuted);
-            } else if (call != null && call.getTransactionServiceWrapper() != null) {
-                call.getTransactionServiceWrapper().onMuteStateChanged(call, isMuted);
+            if (mFeatureFlags.cacheCallAudioCallbacks()) {
+                onMuteStateChangedOrCache(call, isMuted);
+            } else {
+                if (call != null && call.getConnectionService() != null) {
+                    call.getConnectionService().onMuteStateChanged(call, isMuted);
+                } else if (call != null && call.getTransactionServiceWrapper() != null) {
+                    call.getTransactionServiceWrapper().onMuteStateChanged(call, isMuted);
+                }
             }
         }
     }
 
+    private void onMuteStateChangedOrCache(Call call, boolean isMuted){
+        if (call == null) {
+            return;
+        }
+        CallSourceService service = call.getService();
+        if (service != null) {
+            service.onMuteStateChanged(call, isMuted);
+        } else {
+            call.cacheServiceCallback(new CachedMuteStateChange(isMuted));
+        }
+    }
+
     private void createAvailableCallEndpoints(CallAudioState state) {
         Set<CallEndpoint> newAvailableEndpoints = new HashSet<>();
         Map<ParcelUuid, String> newBluetoothDevices = new HashMap<>();
diff --git a/src/com/android/server/telecom/CallIntentProcessor.java b/src/com/android/server/telecom/CallIntentProcessor.java
index c02d20d..8e1f754 100644
--- a/src/com/android/server/telecom/CallIntentProcessor.java
+++ b/src/com/android/server/telecom/CallIntentProcessor.java
@@ -1,10 +1,16 @@
 package com.android.server.telecom;
 
+import static android.content.pm.PackageManager.MATCH_DEFAULT_ONLY;
+
+import com.android.internal.app.IntentForwarderActivity;
 import com.android.server.telecom.components.ErrorDialogActivity;
 import com.android.server.telecom.flags.FeatureFlags;
 
+import android.content.ActivityNotFoundException;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Looper;
@@ -20,6 +26,7 @@
 import android.telecom.VideoProfile;
 import android.telephony.DisconnectCause;
 import android.telephony.PhoneNumberUtils;
+import android.telephony.TelephonyManager;
 import android.widget.Toast;
 
 import java.util.concurrent.CompletableFuture;
@@ -168,13 +175,15 @@
 
         if (!callsManager.isSelfManaged(phoneAccountHandle,
                 (UserHandle) intent.getParcelableExtra(KEY_INITIATING_USER))) {
-            boolean fixedInitiatingUser = fixInitiatingUserIfNecessary(context, intent);
+            boolean fixedInitiatingUser = fixInitiatingUserIfNecessary(
+                    context, intent, featureFlags);
             // Show the toast to warn user that it is a personal call though initiated in work
             // profile.
             if (fixedInitiatingUser) {
                 if (featureFlags.telecomResolveHiddenDependencies()) {
-                    Toast.makeText(context, context.getString(R.string.toast_personal_call_msg),
-                            Toast.LENGTH_LONG).show();
+                    context.getMainExecutor().execute(() ->
+                            Toast.makeText(context, context.getString(
+                                    R.string.toast_personal_call_msg), Toast.LENGTH_LONG).show());
                 } else {
                     Toast.makeText(context, Looper.getMainLooper(),
                             context.getString(R.string.toast_personal_call_msg),
@@ -191,6 +200,18 @@
         boolean isPrivilegedDialer = defaultDialerCache.isDefaultOrSystemDialer(callingPackage,
                 initiatingUser.getIdentifier());
 
+        if (privateSpaceFlagsEnabled()) {
+            if (!callsManager.isSelfManaged(phoneAccountHandle, initiatingUser)
+                    && !TelephonyUtil.shouldProcessAsEmergency(context, handle)
+                    && UserUtil.isPrivateProfile(initiatingUser, context)) {
+                boolean dialogShown = maybeRedirectToIntentForwarderForPrivate(context, intent,
+                        initiatingUser);
+                if (dialogShown) {
+                    return;
+                }
+            }
+        }
+
         NewOutgoingCallIntentBroadcaster broadcaster = new NewOutgoingCallIntentBroadcaster(
                 context, callsManager, intent, callsManager.getPhoneNumberUtilsAdapter(),
                 isPrivilegedDialer, defaultDialerCache, new MmiUtils(), featureFlags);
@@ -226,16 +247,18 @@
      *
      * @return whether the initiating user is fixed.
      */
-    static boolean fixInitiatingUserIfNecessary(Context context, Intent intent) {
+    static boolean fixInitiatingUserIfNecessary(Context context, Intent intent,
+            FeatureFlags featureFlags) {
         final UserHandle initiatingUser = intent.getParcelableExtra(KEY_INITIATING_USER);
-        if (UserUtil.isManagedProfile(context, initiatingUser)) {
+        if (UserUtil.isManagedProfile(context, initiatingUser, featureFlags)) {
             boolean noDialerInstalled = DefaultDialerManager.getInstalledDialerApplications(context,
                     initiatingUser.getIdentifier()).size() == 0;
             if (noDialerInstalled) {
-                final UserManager userManager = UserManager.get(context);
-                UserHandle parentUserHandle =
-                        userManager.getProfileParent(
-                                initiatingUser.getIdentifier()).getUserHandle();
+                final UserManager userManager = context.getSystemService(UserManager.class);
+                UserHandle parentUserHandle = featureFlags.telecomResolveHiddenDependencies()
+                        ? userManager.getProfileParent(initiatingUser)
+                        : userManager.getProfileParent(initiatingUser.getIdentifier())
+                                .getUserHandle();
                 intent.putExtra(KEY_INITIATING_USER, parentUserHandle);
 
                 Log.i(CallIntentProcessor.class, "fixInitiatingUserIfNecessary: no dialer installed"
@@ -306,4 +329,43 @@
             context.startActivityAsUser(errorIntent, UserHandle.CURRENT);
         }
     }
+
+    private static boolean privateSpaceFlagsEnabled() {
+        return android.multiuser.Flags.enablePrivateSpaceFeatures()
+                && android.multiuser.Flags.enablePrivateSpaceIntentRedirection();
+    }
+
+    private static boolean maybeRedirectToIntentForwarderForPrivate(
+            Context context,
+            Intent forwardCallIntent,
+            UserHandle initiatingUser) {
+
+        // If CALL intent filters are set to SKIP_CURRENT_PROFILE, PM will resolve this to an
+        // intent forwarder activity.
+        forwardCallIntent.setComponent(null);
+        forwardCallIntent.setPackage(null);
+        ResolveInfo resolveInfos =
+                context.getPackageManager()
+                        .resolveActivityAsUser(
+                                forwardCallIntent,
+                                PackageManager.ResolveInfoFlags.of(MATCH_DEFAULT_ONLY),
+                                initiatingUser.getIdentifier());
+
+        if (resolveInfos == null
+                || !resolveInfos
+                .getComponentInfo()
+                .getComponentName()
+                .getShortClassName()
+                .equals(IntentForwarderActivity.FORWARD_INTENT_TO_PARENT)) {
+            return false;
+        }
+
+        try {
+            context.startActivityAsUser(forwardCallIntent, initiatingUser);
+            return true;
+        } catch (ActivityNotFoundException e) {
+            Log.e(CallIntentProcessor.class, e, "Unable to start call intent in the main user");
+            return false;
+        }
+    }
 }
diff --git a/src/com/android/server/telecom/CallLogManager.java b/src/com/android/server/telecom/CallLogManager.java
index 1f87593..4484e23 100644
--- a/src/com/android/server/telecom/CallLogManager.java
+++ b/src/com/android/server/telecom/CallLogManager.java
@@ -254,12 +254,11 @@
                 return false;
             }
 
-            PersistableBundle b = mCarrierConfigManager.getConfigForSubId(subscriptionId);
-            if (b == null) {
+            if (mCarrierConfigManager == null) {
                 return false;
             }
-
-            if (b.getBoolean(KEY_SUPPORT_IMS_CONFERENCE_EVENT_PACKAGE_BOOL, true)) {
+            PersistableBundle b = mCarrierConfigManager.getConfigForSubId(subscriptionId);
+            if (b == null || b.getBoolean(KEY_SUPPORT_IMS_CONFERENCE_EVENT_PACKAGE_BOOL, true)) {
                 return false;
             }
         }
@@ -369,7 +368,7 @@
         if (phoneAccount != null &&
                 phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_MULTI_USER)) {
             if (initiatingUser != null &&
-                    UserUtil.isManagedProfile(mContext, initiatingUser)) {
+                    UserUtil.isProfile(mContext, initiatingUser, mFeatureFlags)) {
                 paramBuilder.setUserToBeInsertedTo(initiatingUser);
                 paramBuilder.setAddForAllUsers(false);
             } else {
@@ -575,17 +574,10 @@
                 AddCallArgs c = callList[i];
                 mListeners[i] = c.logCallCompletedListener;
                 try {
-                    Pair<Integer, Integer> startStats = getCallLogStats(c.call);
-                    Log.i(TAG, "LogCall; about to log callId=%s, "
-                                    + "startCount=%d, startMaxId=%d",
-                            c.call.getId(), startStats.first, startStats.second);
-
                     result[i] = Calls.addCall(c.context, c.params);
-                    Pair<Integer, Integer> endStats = getCallLogStats(c.call);
-                    Log.i(TAG, "LogCall; logged callId=%s, uri=%s, "
-                                    + "endCount=%d, endMaxId=%s",
-                            c.call.getId(), result, endStats.first, endStats.second);
-                    if ((endStats.second - startStats.second) <= 0) {
+                    Log.i(TAG, "LogCall; logged callId=%s, uri=%s",
+                            c.call.getId(), result[i]);
+                    if (result[i] == null) {
                         // No call was added or even worse we lost a call in the log.  Trigger an
                         // anomaly report.  Note: it technically possible that an app modified the
                         // call log while we were writing to it here; that is pretty unlikely, and
@@ -686,52 +678,6 @@
         }
     }
 
-    /**
-     * Returns a pair containing the number of rows in the call log, as well as the maximum call log
-     * ID.  There is a limit of 500 entries in the call log for a phone account, so once we hit 500
-     * we can reasonably expect that number to not change before and after logging a call.
-     * We determine the maximum ID in the call log since this is a way we can objectively check if
-     * the provider did record a call log entry or not.  Ideally there should be more call log
-     * entries after logging than before, and certainly not less.
-     * @return pair with number of rows in the call log and max id.
-     */
-    private Pair<Integer, Integer> getCallLogStats(@NonNull Call call) {
-        try {
-            // Ensure we query the call log based on the current user.
-            final Context currentUserContext = mContext.createContextAsUser(
-                    call.getAssociatedUser(), /* flags= */ 0);
-            final ContentResolver currentUserResolver = currentUserContext.getContentResolver();
-            final UserManager userManager = currentUserContext.getSystemService(UserManager.class);
-            final int currentUserId = userManager.getProcessUserId();
-
-            // Use shadow provider based on current user unlock state.
-            Uri providerUri;
-            if (userManager.isUserUnlocked(currentUserId)) {
-                providerUri = Calls.CONTENT_URI;
-            } else {
-                providerUri = Calls.SHADOW_CONTENT_URI;
-            }
-            int maxCallId = -1;
-            int numFound;
-            try (Cursor countCursor = currentUserResolver.query(providerUri,
-                    new String[]{Calls._ID},
-                    null,
-                    null,
-                    Calls._ID + " DESC")) {
-                numFound = countCursor.getCount();
-                if (numFound > 0) {
-                    countCursor.moveToFirst();
-                    maxCallId = countCursor.getInt(0);
-                }
-            }
-            return new Pair<>(numFound, maxCallId);
-        } catch (Exception e) {
-            // Oh jeepers, we crashed getting the call count.
-            Log.e(TAG, e, "getCountOfCallLogRows: failed");
-            return new Pair<>(-1, -1);
-        }
-    }
-
     @VisibleForTesting
     public void setAnomalyReporterAdapter(AnomalyReporterAdapter anomalyReporterAdapter){
         mAnomalyReporterAdapter = anomalyReporterAdapter;
diff --git a/src/com/android/server/telecom/CallScreeningServiceHelper.java b/src/com/android/server/telecom/CallScreeningServiceHelper.java
index 9426100..fa436d4 100644
--- a/src/com/android/server/telecom/CallScreeningServiceHelper.java
+++ b/src/com/android/server/telecom/CallScreeningServiceHelper.java
@@ -176,6 +176,10 @@
                             Log.w(TAG, "Cancelling call id process due to timeout");
                         }
                         mFuture.complete(null);
+                        mContext.unbindService(serviceConnection);
+                    } catch (IllegalArgumentException e) {
+                        Log.i(this, "Exception when unbinding service %s : %s", serviceConnection,
+                                e.getMessage());
                     } finally {
                         Log.endSession();
                     }
diff --git a/src/com/android/server/telecom/CallSourceService.java b/src/com/android/server/telecom/CallSourceService.java
new file mode 100644
index 0000000..d579542
--- /dev/null
+++ b/src/com/android/server/telecom/CallSourceService.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+import android.telecom.CallEndpoint;
+
+import java.util.Set;
+
+/**
+ * android.telecom.Call backed Services (i.e. ConnectionService, TransactionalService, etc.) that
+ * have callbacks that can be executed before the service is set (within the Call object) should
+ * implement this interface in order for clients to receive the callback.
+ *
+ * It has been shown that clients can miss important callback information (e.g. available audio
+ * endpoints) if the service is null within the call at the time the callback is sent.  This is a
+ * way to eliminate the timing issue and for clients to receive all callbacks.
+ */
+public interface CallSourceService {
+    void onMuteStateChanged(Call activeCall, boolean isMuted);
+
+    void onCallEndpointChanged(Call activeCall, CallEndpoint callEndpoint);
+
+    void onAvailableCallEndpointsChanged(Call activeCall, Set<CallEndpoint> availableCallEndpoints);
+
+    void onVideoStateChanged(Call activeCall, int videoState);
+}
diff --git a/src/com/android/server/telecom/CallStreamingController.java b/src/com/android/server/telecom/CallStreamingController.java
index 1323633..efd458e 100644
--- a/src/com/android/server/telecom/CallStreamingController.java
+++ b/src/com/android/server/telecom/CallStreamingController.java
@@ -127,7 +127,7 @@
 
             if (mCallsManager.getCallStreamingController().isStreaming()) {
                 future.complete(new VoipCallTransactionResult(
-                        VoipCallTransactionResult.RESULT_FAILED,
+                        CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
                         "STREAMING_FAILED_ALREADY_STREAMING"));
             } else {
                 future.complete(new VoipCallTransactionResult(
@@ -196,7 +196,8 @@
             if (roleManager == null || packageManager == null) {
                 Log.w(this, "processTransaction: Can't find system service");
                 future.complete(new VoipCallTransactionResult(
-                        VoipCallTransactionResult.RESULT_FAILED, MESSAGE));
+                        CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
+                        MESSAGE));
                 return future;
             }
 
@@ -205,7 +206,8 @@
             if (holders.isEmpty()) {
                 Log.w(this, "processTransaction: Can't find streaming app");
                 future.complete(new VoipCallTransactionResult(
-                        VoipCallTransactionResult.RESULT_FAILED, MESSAGE));
+                        CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
+                        MESSAGE));
                 return future;
             }
             Log.i(this, "processTransaction: servicePackage=%s", holders.get(0));
@@ -216,7 +218,8 @@
             if (infos.isEmpty()) {
                 Log.w(this, "processTransaction: Can't find streaming service");
                 future.complete(new VoipCallTransactionResult(
-                        VoipCallTransactionResult.RESULT_FAILED, MESSAGE));
+                        CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
+                        MESSAGE));
                 return future;
             }
 
@@ -227,7 +230,8 @@
                 Log.w(this, "Must require BIND_CALL_STREAMING_SERVICE: " +
                         serviceInfo.packageName);
                 future.complete(new VoipCallTransactionResult(
-                        VoipCallTransactionResult.RESULT_FAILED, MESSAGE));
+                        CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
+                        MESSAGE));
                 return future;
             }
             Intent intent = new Intent(CallStreamingService.SERVICE_INTERFACE);
@@ -239,7 +243,7 @@
                     | Context.BIND_SCHEDULE_LIKE_TOP_APP, mUserHandle)) {
                 Log.w(this, "Can't bind to streaming service");
                 future.complete(new VoipCallTransactionResult(
-                        VoipCallTransactionResult.RESULT_FAILED,
+                        CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
                         "STREAMING_FAILED_SENDER_BINDING_ERROR"));
             }
             return future;
@@ -379,7 +383,8 @@
                         VoipCallTransactionResult.RESULT_SUCCEED, null));
             } catch (RemoteException e) {
                 future.complete(new VoipCallTransactionResult(
-                        VoipCallTransactionResult.RESULT_FAILED, "Exception when request "
+                        CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
+                        "Exception when request "
                         + "setting state to streaming app."));
             }
             return future;
@@ -409,7 +414,7 @@
             } catch (RemoteException e) {
                 resetController();
                 mFuture.complete(new VoipCallTransactionResult(
-                        VoipCallTransactionResult.RESULT_FAILED,
+                        CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
                         StreamingServiceTransaction.MESSAGE));
             }
         }
@@ -433,7 +438,7 @@
             resetController();
             if (!mFuture.isDone()) {
                 mFuture.complete(new VoipCallTransactionResult(
-                        VoipCallTransactionResult.RESULT_FAILED,
+                        CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
                         "STREAMING_FAILED_SENDER_BINDING_ERROR"));
             } else {
                 mWrapper.onCallStreamingFailed(mCall, STREAMING_FAILED_SENDER_BINDING_ERROR);
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index afe201b..600f847 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -79,7 +79,7 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.provider.BlockedNumberContract;
-import android.provider.BlockedNumberContract.BlockedNumbers;
+import android.provider.BlockedNumbersManager;
 import android.provider.CallLog.Calls;
 import android.provider.Settings;
 import android.telecom.CallAttributes;
@@ -488,6 +488,7 @@
     private final TransactionManager mTransactionManager;
     private final UserManager mUserManager;
     private final CallStreamingNotification mCallStreamingNotification;
+    private final BlockedNumbersManager mBlockedNumbersManager;
     private final FeatureFlags mFeatureFlags;
     private final com.android.internal.telephony.flags.FeatureFlags mTelephonyFeatureFlags;
 
@@ -560,7 +561,8 @@
         public void onReceive(Context context, Intent intent) {
             String action = intent.getAction();
             if (CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED.equals(action)
-                    || BlockedNumbers.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED.equals(action)) {
+                    || BlockedNumbersManager
+                    .ACTION_BLOCK_SUPPRESSION_STATE_CHANGED.equals(action)) {
                 updateEmergencyCallNotificationAsync(context);
             }
         }
@@ -656,10 +658,12 @@
             );
         } else {
             callAudioRouteAdapter = new CallAudioRouteController(context, this, audioServiceFactory,
-                    new AudioRoute.Factory(), wiredHeadsetManager, mBluetoothRouteManager);
+                    new AudioRoute.Factory(), wiredHeadsetManager, mBluetoothRouteManager,
+                    statusBarNotifier, featureFlags);
         }
         callAudioRouteAdapter.initialize();
         bluetoothStateReceiver.setCallAudioRouteAdapter(callAudioRouteAdapter);
+        bluetoothDeviceManager.setCallAudioRouteAdapter(callAudioRouteAdapter);
 
         CallAudioRoutePeripheralAdapter callAudioRoutePeripheralAdapter =
                 new CallAudioRoutePeripheralAdapter(
@@ -679,7 +683,7 @@
                 () -> audioManager.getStreamVolume(AudioManager.STREAM_RING) > 0);
 
         SystemSettingsUtil systemSettingsUtil = new SystemSettingsUtil();
-        RingtoneFactory ringtoneFactory = new RingtoneFactory(this, context);
+        RingtoneFactory ringtoneFactory = new RingtoneFactory(this, context, featureFlags);
         SystemVibrator systemVibrator = new SystemVibrator(context);
         mInCallController = inCallControllerFactory.create(context, mLock, this,
                 systemStateHelper, defaultDialerCache, mTimeoutsAdapter,
@@ -722,6 +726,9 @@
         mCallStreamingNotification = callStreamingNotification;
         mFeatureFlags = featureFlags;
         mTelephonyFeatureFlags = telephonyFlags;
+        mBlockedNumbersManager = mFeatureFlags.telecomMainlineBlockedNumbersManager()
+                ? mContext.getSystemService(BlockedNumbersManager.class)
+                : null;
 
         if (mFeatureFlags.useImprovedListenerOrder()) {
             mListeners.add(mInCallController);
@@ -753,7 +760,7 @@
         mVoipCallMonitor.startMonitor();
 
         // There is no USER_SWITCHED broadcast for user 0, handle it here explicitly.
-        final UserManager userManager = UserManager.get(mContext);
+        final UserManager userManager = mContext.getSystemService(UserManager.class);
         // Don't load missed call if it is run in split user model.
         if (userManager.isPrimaryUser()) {
             onUserSwitch(Process.myUserHandle());
@@ -762,7 +769,7 @@
         IntentFilter intentFilter = new IntentFilter(
                 CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED);
         intentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
-        intentFilter.addAction(BlockedNumbers.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED);
+        intentFilter.addAction(BlockedNumbersManager.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED);
         context.registerReceiver(mReceiver, intentFilter, Context.RECEIVER_EXPORTED);
         mGraphHandlerThreads = new LinkedList<>();
 
@@ -855,10 +862,16 @@
                 ? new Bundle()
                 : phoneAccount.getExtras();
         TelephonyManager telephonyManager = getTelephonyManager();
+        boolean isInEmergencySmsMode;
+        try {
+            isInEmergencySmsMode = telephonyManager.isInEmergencySmsMode();
+        } catch (UnsupportedOperationException uoe) {
+            isInEmergencySmsMode = false;
+        }
         boolean performDndFilter = mFeatureFlags.skipFilterPhoneAccountPerformDndFilter();
         if (incomingCall.hasProperty(Connection.PROPERTY_EMERGENCY_CALLBACK_MODE) ||
                 incomingCall.hasProperty(Connection.PROPERTY_NETWORK_IDENTIFIED_EMERGENCY_CALL) ||
-                telephonyManager.isInEmergencySmsMode() ||
+                isInEmergencySmsMode ||
                 incomingCall.isSelfManaged() ||
                 (!performDndFilter && extras.getBoolean(PhoneAccount.EXTRA_SKIP_CALL_FILTERING))) {
             Log.i(this, "Skipping call filtering for %s (ecm=%b, "
@@ -867,7 +880,7 @@
                     incomingCall.getId(),
                     incomingCall.hasProperty(Connection.PROPERTY_EMERGENCY_CALLBACK_MODE),
                     incomingCall.hasProperty(Connection.PROPERTY_NETWORK_IDENTIFIED_EMERGENCY_CALL),
-                    telephonyManager.isInEmergencySmsMode(),
+                    isInEmergencySmsMode,
                     incomingCall.isSelfManaged(),
                     extras.getBoolean(PhoneAccount.EXTRA_SKIP_CALL_FILTERING));
             onCallFilteringComplete(incomingCall, new Builder()
@@ -916,7 +929,7 @@
         DirectToVoicemailFilter voicemailFilter = new DirectToVoicemailFilter(incomingCall,
                 mCallerInfoLookupHelper);
         BlockCheckerFilter blockCheckerFilter = new BlockCheckerFilter(mContext, incomingCall,
-                mCallerInfoLookupHelper, new BlockCheckerAdapter());
+                mCallerInfoLookupHelper, new BlockCheckerAdapter(mFeatureFlags));
         DndCallFilter dndCallFilter = new DndCallFilter(incomingCall, getRinger());
         CallScreeningServiceFilter carrierCallScreeningServiceFilter =
                 new CallScreeningServiceFilter(incomingCall, carrierPackageName,
@@ -951,6 +964,7 @@
         ComponentName componentName = null;
         CarrierConfigManager configManager = (CarrierConfigManager) mContext.getSystemService
                 (Context.CARRIER_CONFIG_SERVICE);
+        if (configManager == null) return null;
         PersistableBundle configBundle = configManager.getConfig();
         if (configBundle != null) {
             componentName = ComponentName.unflattenFromString(configBundle.getString
@@ -1025,7 +1039,8 @@
 
         if (result.shouldAllowCall) {
             if (mFeatureFlags.separatelyBindToBtIncallService()) {
-                incomingCall.setBtIcsFuture(mInCallController.bindToBTService(incomingCall));
+                mInCallController.bindToBTService(incomingCall, null);
+                incomingCall.setBtIcsFuture(mInCallController.getBtBindingFuture(incomingCall));
                 setCallState(incomingCall, CallState.RINGING, "successful incoming call");
             }
             incomingCall.setPostCallPackageName(
@@ -1404,7 +1419,7 @@
         return mCallEndpointController;
     }
 
-    EmergencyCallHelper getEmergencyCallHelper() {
+    public EmergencyCallHelper getEmergencyCallHelper() {
         return mEmergencyCallHelper;
     }
 
@@ -1681,9 +1696,15 @@
         boolean isCallHiddenFromProfile = !isCallVisibleForUser(call, mCurrentUserHandle);
         // For admins, we should check if the work profile is paused in order to reject
         // the call.
-        if (mUserManager.isUserAdmin(mCurrentUserHandle.getIdentifier())) {
-            isCallHiddenFromProfile &= mUserManager.isQuietModeEnabled(
-                call.getAssociatedUser());
+        UserManager currentUserManager = mContext.createContextAsUser(mCurrentUserHandle, 0)
+                .getSystemService(UserManager.class);
+        boolean isCurrentUserAdmin = mFeatureFlags.telecomResolveHiddenDependencies()
+                ? currentUserManager.isAdminUser()
+                : mUserManager.isUserAdmin(mCurrentUserHandle.getIdentifier());
+        if (isCurrentUserAdmin) {
+            isCallHiddenFromProfile &= mFeatureFlags.telecomResolveHiddenDependencies()
+                    ? currentUserManager.isQuietModeEnabled(call.getAssociatedUser())
+                    : mUserManager.isQuietModeEnabled(call.getAssociatedUser());
         }
 
         // We should always allow emergency calls and also allow non-emergency calls when ECBM
@@ -1906,19 +1927,25 @@
 
             // Log info for emergency call
             if (call.isEmergencyCall()) {
-                String simNumeric = "";
-                String networkNumeric = "";
-                int defaultVoiceSubId = SubscriptionManager.getDefaultVoiceSubscriptionId();
-                if (defaultVoiceSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
-                    TelephonyManager tm = getTelephonyManager().createForSubscriptionId(
-                            defaultVoiceSubId);
-                    CellIdentity cellIdentity = tm.getLastKnownCellIdentity();
-                    simNumeric = tm.getSimOperatorNumeric();
-                    networkNumeric = (cellIdentity != null) ? cellIdentity.getPlmn() : "";
-                }
-                TelecomStatsLog.write(TelecomStatsLog.EMERGENCY_NUMBER_DIALED,
+                try {
+                    String simNumeric = "";
+                    String networkNumeric = "";
+                    int defaultVoiceSubId = SubscriptionManager.getDefaultVoiceSubscriptionId();
+                    if (defaultVoiceSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+                        TelephonyManager tm = getTelephonyManager().createForSubscriptionId(
+                                defaultVoiceSubId);
+                        CellIdentity cellIdentity = tm.getLastKnownCellIdentity();
+                        simNumeric = tm.getSimOperatorNumeric();
+                        networkNumeric = (cellIdentity != null) ? cellIdentity.getPlmn() : "";
+                    }
+                    TelecomStatsLog.write(TelecomStatsLog.EMERGENCY_NUMBER_DIALED,
                             handle.getSchemeSpecificPart(),
                             callingPackage, simNumeric, networkNumeric);
+                } catch (UnsupportedOperationException uoe) {
+                    // Ignore; likely we should not be able to get here since emergency calls
+                    // require Telephony at the current time, however that could change in the
+                    // future, so we best be safe.
+                }
             }
 
             // Ensure new calls related to self-managed calls/connections are set as such.  This
@@ -2140,7 +2167,7 @@
                                 Uri callUri = callToPlace.getHandle();
                                 if (PhoneAccount.SCHEME_TEL.equals(callUri.getScheme())) {
                                     int managedProfileUserId = getManagedProfileUserId(mContext,
-                                            initiatingUser.getIdentifier());
+                                            initiatingUser.getIdentifier(), mFeatureFlags);
                                     if (managedProfileUserId != UserHandle.USER_NULL
                                             &&
                                             mPhoneAccountRegistrar.getCallCapablePhoneAccounts(
@@ -2180,11 +2207,16 @@
                             // At some point, Telecom and Telephony are out of sync with the default
                             // outgoing calling account.
                             if(mFeatureFlags.telephonyHasDefaultButTelecomDoesNot()) {
-                                if (SubscriptionManager.getDefaultVoiceSubscriptionId() !=
-                                        SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
-                                    mAnomalyReporter.reportAnomaly(
-                                            TELEPHONY_HAS_DEFAULT_BUT_TELECOM_DOES_NOT_UUID,
-                                            TELEPHONY_HAS_DEFAULT_BUT_TELECOM_DOES_NOT_MSG);
+                                // SubscriptionManager will throw if FEATURE_TELEPHONY_SUBSCRIPTION
+                                // is not present.
+                                if (mContext.getPackageManager().hasSystemFeature(
+                                        PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION)) {
+                                    if (SubscriptionManager.getDefaultVoiceSubscriptionId() !=
+                                            SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+                                        mAnomalyReporter.reportAnomaly(
+                                                TELEPHONY_HAS_DEFAULT_BUT_TELECOM_DOES_NOT_UUID,
+                                                TELEPHONY_HAS_DEFAULT_BUT_TELECOM_DOES_NOT_MSG);
+                                    }
                                 }
                             }
 
@@ -2315,15 +2347,35 @@
         return mLatestPostSelectionProcessingFuture;
     }
 
-    private static int getManagedProfileUserId(Context context, int userId) {
-        UserManager um = context.getSystemService(UserManager.class);
-        List<UserInfo> userProfiles = um.getProfiles(userId);
-        for (UserInfo uInfo : userProfiles) {
-            if (uInfo.id == userId) {
-                continue;
+    private static int getManagedProfileUserId(Context context, int userId,
+            FeatureFlags featureFlags) {
+        UserManager um;
+        UserHandle userHandle = UserHandle.of(userId);
+        um = featureFlags.telecomResolveHiddenDependencies()
+                ? context.createContextAsUser(userHandle, 0).getSystemService(UserManager.class)
+                : context.getSystemService(UserManager.class);
+
+        if (featureFlags.telecomResolveHiddenDependencies()) {
+            List<UserHandle> userProfiles = um.getAllProfiles();
+            for (UserHandle userProfile : userProfiles) {
+                UserManager profileUserManager = context.createContextAsUser(userProfile, 0)
+                        .getSystemService(UserManager.class);
+                if (userProfile.getIdentifier() == userId) {
+                    continue;
+                }
+                if (profileUserManager.isManagedProfile()) {
+                    return userProfile.getIdentifier();
+                }
             }
-            if (uInfo.isManagedProfile()) {
-                return uInfo.id;
+        } else {
+            List<UserInfo> userInfoProfiles = um.getProfiles(userId);
+            for (UserInfo uInfo : userInfoProfiles) {
+                if (uInfo.id == userId) {
+                    continue;
+                }
+                if (uInfo.isManagedProfile()) {
+                    return uInfo.id;
+                }
             }
         }
         return UserHandle.USER_NULL;
@@ -2436,8 +2488,8 @@
          boolean isSelfManaged = account != null && account.isSelfManaged();
          // Enforce outgoing call restriction for conference calls. This is handled via
          // UserCallIntentProcessor for normal MO calls.
-         if (UserUtil.hasOutgoingCallsUserRestriction(mContext, initiatingUser,
-                 null, isSelfManaged, CallsManager.class.getCanonicalName())) {
+         if (UserUtil.hasOutgoingCallsUserRestriction(mContext, initiatingUser, null,
+                 isSelfManaged, CallsManager.class.getCanonicalName(), mFeatureFlags)) {
              return;
          }
          CompletableFuture<Call> callFuture = startOutgoingCall(participants, phoneAccountHandle,
@@ -2648,6 +2700,9 @@
             isEmergencyNumber =
                     handle != null && getTelephonyManager().isEmergencyNumber(
                             handle.getSchemeSpecificPart());
+        } catch (UnsupportedOperationException uoe) {
+            // If device has no telephony, we can't check if it is an emergency call.
+            isEmergencyNumber = false;
         } catch (IllegalStateException ise) {
             isEmergencyNumber = false;
         } catch (RuntimeException r) {
@@ -2936,9 +2991,13 @@
         }
 
         if (call.isEmergencyCall()) {
-            Executors.defaultThreadFactory().newThread(() ->
-                    BlockedNumberContract.BlockedNumbers.notifyEmergencyContact(mContext))
-                    .start();
+            Executors.defaultThreadFactory().newThread(() -> {
+                if (mBlockedNumbersManager != null) {
+                    mBlockedNumbersManager.notifyEmergencyContact();
+                } else {
+                    BlockedNumberContract.SystemContract.notifyEmergencyContact(mContext);
+                }
+            }).start();
         }
 
         final boolean requireCallCapableAccountByHandle = mContext.getResources().getBoolean(
@@ -3444,11 +3503,15 @@
     }
 
     // Returns whether the device is capable of 2 simultaneous active voice calls on different subs.
-    private boolean isDsdaCallingPossible() {
+    @VisibleForTesting
+    public boolean isDsdaCallingPossible() {
         try {
             return getTelephonyManager().getMaxNumberOfSimultaneouslyActiveSims() > 1
                     || getTelephonyManager().getPhoneCapability()
                            .getMaxActiveVoiceSubscriptions() > 1;
+        } catch(UnsupportedOperationException uoe) {
+            Log.w(this, "Telephony not supported");
+            return false;
         } catch (Exception e) {
             Log.w(this, "exception in isDsdaCallingPossible(): ", e);
             return false;
@@ -3674,6 +3737,7 @@
         int subscriptionId = mPhoneAccountRegistrar.getSubscriptionIdForPhoneAccount(handle);
         CarrierConfigManager carrierConfigManager =
                 mContext.getSystemService(CarrierConfigManager.class);
+        if (carrierConfigManager == null) return new PersistableBundle();
         PersistableBundle result = carrierConfigManager.getConfigForSubId(subscriptionId);
         return result == null ? new PersistableBundle() : result;
     }
@@ -3772,8 +3836,7 @@
             if (canHold(activeCall)) {
                 activeCall.hold("swap to " + call.getId());
                 return true;
-            } else if (supportsHold(activeCall)
-                    && areFromSameSource(activeCall, call)) {
+            } else if (sameSourceHoldCase(activeCall, call)) {
 
                 // Handle the case where the active call and the new call are from the same CS or
                 // connection manager, and the currently active call supports hold but cannot
@@ -3822,43 +3885,85 @@
         return false;
     }
 
-    // attempt to hold the requested call and complete the callback on the result
+    /**
+     * attempt to hold or swap the current active call in favor of a new call request. The
+     * OutcomeReceiver will return onResult if the current active call is held or disconnected.
+     * Otherwise, the OutcomeReceiver will fail.
+     */
     public void transactionHoldPotentialActiveCallForNewCall(Call newCall,
-            OutcomeReceiver<Boolean, CallException> callback) {
+            boolean isCallControlRequest, OutcomeReceiver<Boolean, CallException> callback) {
+        String mTag = "transactionHoldPotentialActiveCallForNewCall: ";
         Call activeCall = (Call) mConnectionSvrFocusMgr.getCurrentFocusCall();
-        Log.i(this, "transactionHoldPotentialActiveCallForNewCall: "
-                + "newCall=[%s], activeCall=[%s]", newCall, activeCall);
+        Log.i(this, mTag + "newCall=[%s], activeCall=[%s]", newCall, activeCall);
 
-        // early exit if there is no need to hold an active call
         if (activeCall == null || activeCall == newCall) {
-            Log.i(this, "transactionHoldPotentialActiveCallForNewCall:"
-                    + " no need to hold activeCall");
+            Log.i(this, mTag + "no need to hold activeCall");
             callback.onResult(true);
             return;
         }
 
-        // before attempting CallsManager#holdActiveCallForNewCall(Call), check if it'll fail early
-        if (!canHold(activeCall) &&
-                !(supportsHold(activeCall) && areFromSameSource(activeCall, newCall))) {
-            Log.i(this, "transactionHoldPotentialActiveCallForNewCall: "
-                    + "conditions show the call cannot be held.");
-            callback.onError(new CallException("call does not support hold",
-                    CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
-            return;
-        }
+        if (mFeatureFlags.transactionalHoldDisconnectsUnholdable()) {
+            // prevent bad actors from disconnecting the activeCall. Instead, clients will need to
+            // notify the user that they need to disconnect the ongoing call before making the
+            // new call ACTIVE.
+            if (isCallControlRequest && !canHoldOrSwapActiveCall(activeCall, newCall)) {
+                Log.i(this, mTag + "CallControlRequest exit");
+                callback.onError(new CallException("activeCall is NOT holdable or swappable, please"
+                        + " request the user disconnect the call.",
+                        CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
+                return;
+            }
 
-        // attempt to hold the active call
-        if (!holdActiveCallForNewCall(newCall)) {
-            Log.i(this, "transactionHoldPotentialActiveCallForNewCall: "
-                    + "attempted to hold call but failed.");
-            callback.onError(new CallException("cannot hold active call failed",
-                    CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
-            return;
-        }
+            if (holdActiveCallForNewCall(newCall)) {
+                // Transactional clients do not call setHold but the request was sent to set the
+                // call as inactive and it has already been acked by this point.
+                markCallAsOnHold(activeCall);
+                callback.onResult(true);
+            } else {
+                // It's possible that holdActiveCallForNewCall disconnected the activeCall.
+                // Therefore, the activeCalls state should be checked before failing.
+                if (activeCall.isLocallyDisconnecting()) {
+                    callback.onResult(true);
+                } else {
+                    Log.i(this, mTag + "active call could not be held or disconnected");
+                    callback.onError(
+                            new CallException("activeCall could not be held or disconnected",
+                                    CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
+                }
+            }
+        } else {
+            // before attempting CallsManager#holdActiveCallForNewCall(Call), check if it'll fail
+            // early
+            if (!canHold(activeCall) &&
+                    !(supportsHold(activeCall) && areFromSameSource(activeCall, newCall))) {
+                Log.i(this, "transactionHoldPotentialActiveCallForNewCall: "
+                        + "conditions show the call cannot be held.");
+                callback.onError(new CallException("call does not support hold",
+                        CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
+                return;
+            }
 
-        // officially mark the activeCall as held
-        markCallAsOnHold(activeCall);
-        callback.onResult(true);
+            // attempt to hold the active call
+            if (!holdActiveCallForNewCall(newCall)) {
+                Log.i(this, "transactionHoldPotentialActiveCallForNewCall: "
+                        + "attempted to hold call but failed.");
+                callback.onError(new CallException("cannot hold active call failed",
+                        CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
+                return;
+            }
+
+            // officially mark the activeCall as held
+            markCallAsOnHold(activeCall);
+            callback.onResult(true);
+        }
+    }
+
+    private boolean canHoldOrSwapActiveCall(Call activeCall, Call newCall) {
+        return canHold(activeCall) || sameSourceHoldCase(activeCall, newCall);
+    }
+
+    private boolean sameSourceHoldCase(Call activeCall, Call call) {
+        return supportsHold(activeCall) && areFromSameSource(activeCall, call);
     }
 
     @VisibleForTesting
@@ -3952,20 +4057,21 @@
             Log.addEvent(call, LogUtils.Events.SET_DISCONNECTED_ORIG, disconnectCause);
 
             // Setup the future with a timeout so that the CDS is time boxed.
-            CompletableFuture<Boolean> future = call.initializeDisconnectFuture(
+            CompletableFuture<Boolean> future = call.initializeDiagnosticCompleteFuture(
                     mTimeoutsAdapter.getCallDiagnosticServiceTimeoutMillis(
                             mContext.getContentResolver()));
 
             // Post the disconnection updates to the future for completion once the CDS returns
             // with it's overridden disconnect message.
-            future.thenRunAsync(() -> {
+            CompletableFuture<Void> disconnectFuture = future.thenRunAsync(() -> {
                 call.setDisconnectCause(disconnectCause);
                 setCallState(call, CallState.DISCONNECTED, "disconnected set explicitly");
-            }, new LoggedHandlerExecutor(mHandler, "CM.mCAD", mLock))
-                    .exceptionally((throwable) -> {
-                        Log.e(TAG, throwable, "Error while executing disconnect future.");
-                        return null;
-                    });
+            }, new LoggedHandlerExecutor(mHandler, "CM.mCAD", mLock));
+            disconnectFuture.exceptionally((throwable) -> {
+                Log.e(TAG, throwable, "Error while executing disconnect future.");
+                return null;
+            });
+            call.setDisconnectFuture(disconnectFuture);
         } else {
             // No CallDiagnosticService, or it doesn't handle this call, so just do this
             // synchronously as always.
@@ -3985,16 +4091,7 @@
     public void markCallAsRemoved(Call call) {
         if (call.isDisconnectHandledViaFuture()) {
             Log.i(this, "markCallAsRemoved; callid=%s, postingToFuture.", call.getId());
-            // A future is being used due to a CallDiagnosticService handling the call.  We will
-            // chain the removal operation to the end of any outstanding disconnect work.
-            call.getDisconnectFuture().thenRunAsync(() -> {
-                performRemoval(call);
-            }, new LoggedHandlerExecutor(mHandler, "CM.mCAR", mLock))
-                    .exceptionally((throwable) -> {
-                        Log.e(TAG, throwable, "Error while executing disconnect future");
-                        return null;
-                    });
-
+            configureRemovalFuture(call);
         } else {
             Log.i(this, "markCallAsRemoved; callid=%s, immediate.", call.getId());
             performRemoval(call);
@@ -4002,8 +4099,52 @@
     }
 
     /**
+     * Configure the removal as a dependent stage after the disconnect future completes, which could
+     * be cancelled as part of {@link Call#setState(int, String)} when need to retry dial on another
+     * ConnectionService.
+     * <p>
+     * We can not remove the call yet, we need to wait for the DisconnectCause to be processed and
+     * potentially re-written via the {@link android.telecom.CallDiagnosticService} first.
+     *
+     * @param call The call to configure the removal future for.
+     */
+    private void configureRemovalFuture(Call call) {
+        if (!mFeatureFlags.cancelRemovalOnEmergencyRedial()) {
+            call.getDiagnosticCompleteFuture().thenRunAsync(() -> performRemoval(call),
+                            new LoggedHandlerExecutor(mHandler, "CM.cRF-O", mLock))
+                    .exceptionally((throwable) -> {
+                        Log.e(TAG, throwable, "Error while executing disconnect future");
+                        return null;
+                    });
+        } else {
+            // A future is being used due to a CallDiagnosticService handling the call.  We will
+            // chain the removal operation to the end of any outstanding disconnect work.
+            CompletableFuture<Void> removalFuture;
+            if (call.getDisconnectFuture() == null) {
+                // Unexpected - can not get the disconnect future, attach to the diagnostic complete
+                // future in this case.
+                removalFuture = call.getDiagnosticCompleteFuture().thenRun(() ->
+                        Log.w(this, "configureRemovalFuture: remove called without disconnecting"
+                                + " first."));
+            } else {
+                removalFuture = call.getDisconnectFuture();
+            }
+            removalFuture = removalFuture.thenRunAsync(() -> performRemoval(call),
+                    new LoggedHandlerExecutor(mHandler, "CM.cRF-N", mLock));
+            removalFuture.exceptionally((throwable) -> {
+                Log.e(TAG, throwable, "Error while executing disconnect future");
+                return null;
+            });
+            // Cache the future to remove the call initiated by the ConnectionService in case we
+            // need to cancel it in favor of removing the call internally as part of creating a
+            // new connection (CreateConnectionProcessor#continueProcessingIfPossible)
+            call.setRemovalFuture(removalFuture);
+        }
+    }
+
+    /**
      * Work which is completed when a call is to be removed. Can either be be run synchronously or
-     * posted to a {@link Call#getDisconnectFuture()}.
+     * posted to a {@link Call#getDiagnosticCompleteFuture()}.
      * @param call The call.
      */
     private void performRemoval(Call call) {
@@ -4778,19 +4919,7 @@
      * @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) {
+        boolean hasCrossUserAccess = userHandle.equals(UserHandle.ALL);
         return mSelfManagedCallsBeingSetup.stream().anyMatch(c -> c.isSelfManaged()
                 && c.getTargetPhoneAccount().getComponentName().getPackageName().equals(packageName)
                 && (!hasCrossUserAccess
@@ -5541,10 +5670,21 @@
         mCurrentUserHandle = userHandle;
         mMissedCallNotifier.setCurrentUserHandle(userHandle);
         mRoleManagerAdapter.setCurrentUserHandle(userHandle);
-        final UserManager userManager = UserManager.get(mContext);
-        List<UserInfo> profiles = userManager.getEnabledProfiles(userHandle.getIdentifier());
-        for (UserInfo profile : profiles) {
-            reloadMissedCallsOfUser(profile.getUserHandle());
+        final UserManager userManager = mFeatureFlags.telecomResolveHiddenDependencies()
+                ? mContext.createContextAsUser(userHandle, 0).getSystemService(
+                        UserManager.class)
+                : mContext.getSystemService(UserManager.class);
+        List<UserHandle> profiles = userManager.getUserProfiles();
+        List<UserInfo> userInfoProfiles = userManager.getEnabledProfiles(
+                userHandle.getIdentifier());
+        if (mFeatureFlags.telecomResolveHiddenDependencies()) {
+            for (UserHandle profileUser : profiles) {
+                reloadMissedCallsOfUser(profileUser);
+            }
+        } else {
+            for (UserInfo profile : userInfoProfiles) {
+                reloadMissedCallsOfUser(profile.getUserHandle());
+            }
         }
     }
 
@@ -5553,7 +5693,7 @@
      * switched, we reload missed calls of profile that are just started here.
      */
     void onUserStarting(UserHandle userHandle) {
-        if (UserUtil.isProfile(mContext, userHandle)) {
+        if (UserUtil.isProfile(mContext, userHandle, mFeatureFlags)) {
             reloadMissedCallsOfUser(userHandle);
         }
     }
@@ -5662,8 +5802,10 @@
         UserManager userManager = mContext.getSystemService(UserManager.class);
         KeyguardManager keyguardManager = mContext.getSystemService(KeyguardManager.class);
 
-        boolean isUserRestricted = userManager != null
-                && userManager.hasUserRestriction(UserManager.DISALLOW_SMS, callingUser);
+        boolean hasUserRestriction = mFeatureFlags.telecomResolveHiddenDependencies()
+                ? userManager.hasUserRestrictionForUser(UserManager.DISALLOW_SMS, callingUser)
+                : userManager.hasUserRestriction(UserManager.DISALLOW_SMS, callingUser);
+        boolean isUserRestricted = userManager != null && hasUserRestriction;
         boolean isLockscreenRestricted = keyguardManager != null
                 && keyguardManager.isDeviceLocked();
         Log.d(this, "isReplyWithSmsAllowed: isUserRestricted: %s, isLockscreenRestricted: %s",
diff --git a/src/com/android/server/telecom/ConnectionServiceWrapper.java b/src/com/android/server/telecom/ConnectionServiceWrapper.java
index 2ed416d..c3c0c1c 100644
--- a/src/com/android/server/telecom/ConnectionServiceWrapper.java
+++ b/src/com/android/server/telecom/ConnectionServiceWrapper.java
@@ -33,7 +33,6 @@
 import android.os.CancellationSignal;
 import android.os.IBinder;
 import android.os.ParcelFileDescriptor;
-import android.os.Process;
 import android.os.RemoteException;
 import android.os.ResultReceiver;
 import android.os.UserHandle;
@@ -45,6 +44,7 @@
 import android.telecom.DisconnectCause;
 import android.telecom.GatewayInfo;
 import android.telecom.Log;
+import android.telecom.Logging.Runnable;
 import android.telecom.Logging.Session;
 import android.telecom.ParcelableConference;
 import android.telecom.ParcelableConnection;
@@ -66,7 +66,6 @@
 import com.android.internal.telecom.RemoteServiceCallback;
 import com.android.internal.util.Preconditions;
 import com.android.server.telecom.flags.FeatureFlags;
-import com.android.server.telecom.flags.Flags;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -75,10 +74,13 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.UUID;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
 import java.util.Objects;
 
@@ -90,13 +92,30 @@
  */
 @VisibleForTesting
 public class ConnectionServiceWrapper extends ServiceBinder implements
-        ConnectionServiceFocusManager.ConnectionServiceFocus {
+        ConnectionServiceFocusManager.ConnectionServiceFocus, CallSourceService {
+
+    /**
+     * Anomaly Report UUIDs and corresponding error descriptions specific to CallsManager.
+     */
+    public static final UUID CREATE_CONNECTION_TIMEOUT_ERROR_UUID =
+            UUID.fromString("54b7203d-a79f-4cbd-b639-85cd93a39cbb");
+    public static final String CREATE_CONNECTION_TIMEOUT_ERROR_MSG =
+            "Timeout expired before Telecom connection was created.";
+    public static final UUID CREATE_CONFERENCE_TIMEOUT_ERROR_UUID =
+            UUID.fromString("caafe5ea-2472-4c61-b2d8-acb9d47e13dd");
+    public static final String CREATE_CONFERENCE_TIMEOUT_ERROR_MSG =
+            "Timeout expired before Telecom conference was created.";
 
     private static final String TELECOM_ABBREVIATION = "cast";
+    private static final long SERVICE_BINDING_TIMEOUT = 15000L;
     private CompletableFuture<Pair<Integer, Location>> mQueryLocationFuture = null;
     private @Nullable CancellationSignal mOngoingQueryLocationRequest = null;
     private final ExecutorService mQueryLocationExecutor = Executors.newSingleThreadExecutor();
-
+    private ScheduledExecutorService mScheduledExecutor =
+            Executors.newSingleThreadScheduledExecutor();
+    // Pre-allocate space for 2 calls; realistically thats all we should ever need (tm)
+    private final Map<Call, ScheduledFuture<?>> mScheduledFutureMap = new ConcurrentHashMap<>(2);
+    private AnomalyReporterAdapter mAnomalyReporter = new AnomalyReporterAdapterImpl();
     private final class Adapter extends IConnectionServiceAdapter.Stub {
 
         @Override
@@ -109,6 +128,12 @@
             try {
                 synchronized (mLock) {
                     logIncoming("handleCreateConnectionComplete %s", callId);
+                    Call call = mCallIdMapper.getCall(callId);
+                    if (mScheduledFutureMap.containsKey(call)) {
+                        ScheduledFuture<?> existingTimeout = mScheduledFutureMap.get(call);
+                        existingTimeout.cancel(false /* cancelIfRunning */);
+                        mScheduledFutureMap.remove(call);
+                    }
                     // Check status hints image for cross user access
                     if (connection.getStatusHints() != null) {
                         Icon icon = connection.getStatusHints().getIcon();
@@ -142,10 +167,23 @@
                 ParcelableConference conference, Session.Info sessionInfo) {
             Log.startSession(sessionInfo, LogUtils.Sessions.CSW_HANDLE_CREATE_CONNECTION_COMPLETE,
                     mPackageAbbreviation);
+            UserHandle callingUserHandle = Binder.getCallingUserHandle();
             long token = Binder.clearCallingIdentity();
             try {
                 synchronized (mLock) {
                     logIncoming("handleCreateConferenceComplete %s", callId);
+                    // Check status hints image for cross user access
+                    if (conference.getStatusHints() != null) {
+                        Icon icon = conference.getStatusHints().getIcon();
+                        conference.getStatusHints().setIcon(StatusHints.
+                                validateAccountIconUserBoundary(icon, callingUserHandle));
+                    }
+                    Call call = mCallIdMapper.getCall(callId);
+                    if (mScheduledFutureMap.containsKey(call)) {
+                        ScheduledFuture<?> existingTimeout = mScheduledFutureMap.get(call);
+                        existingTimeout.cancel(false /* cancelIfRunning */);
+                        mScheduledFutureMap.remove(call);
+                    }
                     ConnectionServiceWrapper.this
                             .handleCreateConferenceComplete(callId, request, conference);
 
@@ -383,7 +421,12 @@
                     logIncoming("removeCall %s", callId);
                     Call call = mCallIdMapper.getCall(callId);
                     if (call != null) {
-                        if (call.isAlive() && !call.isDisconnectHandledViaFuture()) {
+                        boolean isRemovalPending = mFlags.cancelRemovalOnEmergencyRedial()
+                                && call.isRemovalPending();
+                        if (call.isAlive() && !call.isDisconnectHandledViaFuture()
+                                && !isRemovalPending) {
+                            Log.w(this, "call not disconnected when removeCall"
+                                    + " called, marking disconnected first.");
                             mCallsManager.markCallAsDisconnected(
                                     call, new DisconnectCause(DisconnectCause.REMOTE));
                         }
@@ -1409,20 +1452,23 @@
         }
     }
 
-    private CellIdentity getLastKnownCellIdentity() {
+    @VisibleForTesting
+    public CellIdentity getLastKnownCellIdentity() {
         TelephonyManager telephonyManager = mContext.getSystemService(TelephonyManager.class);
         if (telephonyManager != null) {
-            CellIdentity lastKnownCellIdentity = telephonyManager.getLastKnownCellIdentity();
             try {
+                CellIdentity lastKnownCellIdentity = telephonyManager.getLastKnownCellIdentity();
                 mAppOpsManager.noteOp(AppOpsManager.OP_FINE_LOCATION,
                         mContext.getPackageManager().getPackageUid(
                                 getComponentName().getPackageName(), 0),
                         getComponentName().getPackageName());
+                return lastKnownCellIdentity;
+            } catch (UnsupportedOperationException ignored) {
+                Log.w(this, "getLastKnownCellIdentity - no telephony on this device");
             } catch (PackageManager.NameNotFoundException nameNotFoundException) {
                 Log.e(this, nameNotFoundException, "could not find the package -- %s",
                         getComponentName().getPackageName());
             }
-            return lastKnownCellIdentity;
         }
         return null;
     }
@@ -1598,6 +1644,29 @@
                         .setParticipants(call.getParticipants())
                         .setIsAdhocConferenceCall(call.isAdhocConferenceCall())
                         .build();
+                Runnable r = new Runnable("CSW.cC", mLock) {
+                            @Override
+                            public void loggedRun() {
+                                if (!call.isCreateConnectionComplete()) {
+                                    Log.e(this, new Exception(),
+                                            "Conference %s creation timeout",
+                                            getComponentName());
+                                    Log.addEvent(call, LogUtils.Events.CREATE_CONFERENCE_TIMEOUT,
+                                            Log.piiHandle(call.getHandle()) + " via:" +
+                                                    getComponentName().getPackageName());
+                                    mAnomalyReporter.reportAnomaly(
+                                            CREATE_CONFERENCE_TIMEOUT_ERROR_UUID,
+                                            CREATE_CONFERENCE_TIMEOUT_ERROR_MSG);
+                                    response.handleCreateConferenceFailure(
+                                            new DisconnectCause(DisconnectCause.ERROR));
+                                }
+                            }
+                        };
+                // Post cleanup to the executor service and cache the future, so we can cancel it if
+                // needed.
+                ScheduledFuture<?> future = mScheduledExecutor.schedule(r.getRunnableToCancel(),
+                        SERVICE_BINDING_TIMEOUT, TimeUnit.MILLISECONDS);
+                mScheduledFutureMap.put(call, future);
                 try {
                     mServiceInterface.createConference(
                             call.getConnectionManagerPhoneAccount(),
@@ -1698,6 +1767,29 @@
                         .setRttPipeFromInCall(call.getInCallToCsRttPipeForCs())
                         .setRttPipeToInCall(call.getCsToInCallRttPipeForCs())
                         .build();
+                Runnable r = new Runnable("CSW.cC", mLock) {
+                            @Override
+                            public void loggedRun() {
+                                if (!call.isCreateConnectionComplete()) {
+                                    Log.e(this, new Exception(),
+                                            "Connection %s creation timeout",
+                                            getComponentName());
+                                    Log.addEvent(call, LogUtils.Events.CREATE_CONNECTION_TIMEOUT,
+                                            Log.piiHandle(call.getHandle()) + " via:" +
+                                                    getComponentName().getPackageName());
+                                    mAnomalyReporter.reportAnomaly(
+                                            CREATE_CONNECTION_TIMEOUT_ERROR_UUID,
+                                            CREATE_CONNECTION_TIMEOUT_ERROR_MSG);
+                                    response.handleCreateConnectionFailure(
+                                            new DisconnectCause(DisconnectCause.ERROR));
+                                }
+                            }
+                        };
+                // Post cleanup to the executor service and cache the future, so we can cancel it if
+                // needed.
+                ScheduledFuture<?> future = mScheduledExecutor.schedule(r.getRunnableToCancel(),
+                        SERVICE_BINDING_TIMEOUT, TimeUnit.MILLISECONDS);
+                mScheduledFutureMap.put(call, future);
                 try {
                     mServiceInterface.createConnection(
                             call.getConnectionManagerPhoneAccount(),
@@ -1953,6 +2045,7 @@
 
     /** @see IConnectionService#onCallEndpointChanged(String, CallEndpoint, Session.Info) */
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    @Override
     public void onCallEndpointChanged(Call activeCall, CallEndpoint callEndpoint) {
         final String callId = mCallIdMapper.getCallId(activeCall);
         if (callId != null && isServiceValid("onCallEndpointChanged")) {
@@ -1968,6 +2061,7 @@
 
     /** @see IConnectionService#onAvailableCallEndpointsChanged(String, List, Session.Info) */
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    @Override
     public void onAvailableCallEndpointsChanged(Call activeCall,
             Set<CallEndpoint> availableCallEndpoints) {
         final String callId = mCallIdMapper.getCallId(activeCall);
@@ -1984,8 +2078,14 @@
         }
     }
 
+    @Override
+    public void onVideoStateChanged(Call call, int videoState){
+        // pass through. ConnectionService does not implement this method.
+    }
+
     /** @see IConnectionService#onMuteStateChanged(String, boolean, Session.Info) */
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    @Override
     public void onMuteStateChanged(Call activeCall, boolean isMuted) {
         final String callId = mCallIdMapper.getCallId(activeCall);
         if (callId != null && isServiceValid("onMuteStateChanged")) {
@@ -2028,7 +2128,8 @@
     }
 
     /** @see IConnectionService#disconnect(String, Session.Info) */
-    void disconnect(Call call) {
+    @VisibleForTesting
+    public void disconnect(Call call) {
         final String callId = mCallIdMapper.getCallId(call);
         if (callId != null && isServiceValid("disconnect")) {
             try {
@@ -2155,7 +2256,8 @@
         }
     }
 
-    void addCall(Call call) {
+    @VisibleForTesting
+    public void addCall(Call call) {
         if (mCallIdMapper.getCallId(call) == null) {
             mCallIdMapper.addCall(call);
         }
@@ -2623,4 +2725,14 @@
         sb.append("]");
         return sb.toString();
     }
+
+    @VisibleForTesting
+    public void setScheduledExecutorService(ScheduledExecutorService service) {
+        mScheduledExecutor = service;
+    }
+
+    @VisibleForTesting
+    public void setAnomalyReporterAdapter(AnomalyReporterAdapter mAnomalyReporterAdapter){
+        mAnomalyReporter = mAnomalyReporterAdapter;
+    }
 }
diff --git a/src/com/android/server/telecom/CreateConnectionProcessor.java b/src/com/android/server/telecom/CreateConnectionProcessor.java
index bcb2d2f..a2c742d 100644
--- a/src/com/android/server/telecom/CreateConnectionProcessor.java
+++ b/src/com/android/server/telecom/CreateConnectionProcessor.java
@@ -103,22 +103,32 @@
         int getSlotIndex(int subId);
     }
 
-    private ITelephonyManagerAdapter mTelephonyAdapter = new ITelephonyManagerAdapter() {
+    public static class ITelephonyManagerAdapterImpl implements ITelephonyManagerAdapter {
         @Override
         public int getSubIdForPhoneAccount(Context context, PhoneAccount account) {
             TelephonyManager manager = context.getSystemService(TelephonyManager.class);
             if (manager == null) {
                 return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
             }
-            return manager.getSubscriptionId(account.getAccountHandle());
+            try {
+                return manager.getSubscriptionId(account.getAccountHandle());
+            } catch (UnsupportedOperationException uoe) {
+                return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+            }
         }
 
         @Override
         public int getSlotIndex(int subId) {
-            return SubscriptionManager.getSlotIndex(subId);
+            try {
+                return SubscriptionManager.getSlotIndex(subId);
+            } catch (UnsupportedOperationException uoe) {
+                return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+            }
         }
     };
 
+    private ITelephonyManagerAdapter mTelephonyAdapter = new ITelephonyManagerAdapterImpl();
+
     private final Call mCall;
     private final ConnectionServiceRepository mRepository;
     private List<CallAttemptRecord> mAttemptRecords;
@@ -128,15 +138,19 @@
     private final PhoneAccountRegistrar mPhoneAccountRegistrar;
     private final Context mContext;
     private final FeatureFlags mFlags;
+    private final Timeouts.Adapter mTimeoutsAdapter;
     private CreateConnectionTimeout mTimeout;
     private ConnectionServiceWrapper mService;
     private int mConnectionAttempt;
 
     @VisibleForTesting
-    public CreateConnectionProcessor(
-            Call call, ConnectionServiceRepository repository, CreateConnectionResponse response,
-            PhoneAccountRegistrar phoneAccountRegistrar, Context context,
-            FeatureFlags featureFlags) {
+    public CreateConnectionProcessor(Call call,
+            ConnectionServiceRepository repository,
+            CreateConnectionResponse response,
+            PhoneAccountRegistrar phoneAccountRegistrar,
+            Context context,
+            FeatureFlags featureFlags,
+            Timeouts.Adapter timeoutsAdapter) {
         Log.v(this, "CreateConnectionProcessor created for Call = %s", call);
         mCall = call;
         mRepository = repository;
@@ -145,6 +159,7 @@
         mContext = context;
         mConnectionAttempt = 0;
         mFlags = featureFlags;
+        mTimeoutsAdapter = timeoutsAdapter;
     }
 
     boolean isProcessingComplete() {
@@ -317,7 +332,7 @@
         clearTimeout();
 
         CreateConnectionTimeout timeout = new CreateConnectionTimeout(
-                mContext, mPhoneAccountRegistrar, service, mCall);
+                mContext, mPhoneAccountRegistrar, service, mCall, mTimeoutsAdapter);
         if (timeout.isTimeoutNeededForCall(getConnectionServices(mAttemptRecords),
                 attempt.connectionManagerPhoneAccount)) {
             mTimeout = timeout;
diff --git a/src/com/android/server/telecom/CreateConnectionTimeout.java b/src/com/android/server/telecom/CreateConnectionTimeout.java
index 14e5bf0..3046ca4 100644
--- a/src/com/android/server/telecom/CreateConnectionTimeout.java
+++ b/src/com/android/server/telecom/CreateConnectionTimeout.java
@@ -16,21 +16,27 @@
 
 package com.android.server.telecom;
 
+import static com.android.internal.telephony.flags.Flags.carrierEnabledSatelliteFlag;
+
 import android.content.Context;
 import android.os.Handler;
 import android.os.Looper;
 import android.telecom.Log;
 import android.telecom.Logging.Runnable;
+import android.telecom.PhoneAccount;
 import android.telecom.PhoneAccountHandle;
 import android.telephony.TelephonyManager;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 import java.util.Collection;
 import java.util.Objects;
 
 /**
  * Registers a timeout for a call and disconnects the call when the timeout expires.
  */
-final class CreateConnectionTimeout extends Runnable {
+@VisibleForTesting
+public final class CreateConnectionTimeout extends Runnable {
     private final Context mContext;
     private final PhoneAccountRegistrar mPhoneAccountRegistrar;
     private final ConnectionServiceWrapper mConnectionService;
@@ -38,20 +44,25 @@
     private final Handler mHandler = new Handler(Looper.getMainLooper());
     private boolean mIsRegistered;
     private boolean mIsCallTimedOut;
+    private final Timeouts.Adapter mTimeoutsAdapter;
 
-    CreateConnectionTimeout(Context context, PhoneAccountRegistrar phoneAccountRegistrar,
-            ConnectionServiceWrapper service, Call call) {
+    @VisibleForTesting
+    public CreateConnectionTimeout(Context context, PhoneAccountRegistrar phoneAccountRegistrar,
+            ConnectionServiceWrapper service, Call call, Timeouts.Adapter timeoutsAdapter) {
         super("CCT", null /*lock*/);
         mContext = context;
         mPhoneAccountRegistrar = phoneAccountRegistrar;
         mConnectionService = service;
         mCall = call;
+        mTimeoutsAdapter = timeoutsAdapter;
     }
 
-    boolean isTimeoutNeededForCall(Collection<PhoneAccountHandle> accounts,
+    @VisibleForTesting
+    public boolean isTimeoutNeededForCall(Collection<PhoneAccountHandle> accounts,
             PhoneAccountHandle currentAccount) {
         // Non-emergency calls timeout automatically at the radio layer. No need for a timeout here.
         if (!mCall.isEmergencyCall()) {
+            Log.d(this, "isTimeoutNeededForCall, not an emergency call");
             return false;
         }
 
@@ -60,11 +71,13 @@
         PhoneAccountHandle connectionManager =
                 mPhoneAccountRegistrar.getSimCallManagerFromCall(mCall);
         if (!accounts.contains(connectionManager)) {
+            Log.d(this, "isTimeoutNeededForCall, no connection manager");
             return false;
         }
 
         // No need to add a timeout if the current attempt is over the connection manager.
         if (Objects.equals(connectionManager, currentAccount)) {
+            Log.d(this, "isTimeoutNeededForCall, already attempting over connection manager");
             return false;
         }
 
@@ -104,8 +117,34 @@
 
     @Override
     public void loggedRun() {
+        if (!carrierEnabledSatelliteFlag()) {
+            timeoutCallIfNeeded();
+            return;
+        }
+
+        PhoneAccountHandle connectionManager =
+                mPhoneAccountRegistrar.getSimCallManagerFromCall(mCall);
+        if (connectionManager != null) {
+            PhoneAccount account = mPhoneAccountRegistrar.getPhoneAccount(connectionManager,
+                    connectionManager.getUserHandle());
+            if (account != null && account.hasCapabilities(
+                    (PhoneAccount.CAPABILITY_SUPPORTS_VOICE_CALLING_INDICATIONS
+                            | PhoneAccount.CAPABILITY_VOICE_CALLING_AVAILABLE))) {
+                // If we have encountered the timeout and there is an in service
+                // ConnectionManager, disconnect the call so that it can be attempted over
+                // the ConnectionManager.
+                timeoutCallIfNeeded();
+                return;
+            }
+            Log.i(
+                this,
+               "loggedRun, no PhoneAccount with voice calling capabilities, not timing out call");
+        }
+    }
+
+    private void timeoutCallIfNeeded() {
         if (mIsRegistered && isCallBeingPlaced(mCall)) {
-            Log.i(this, "run, call timed out, calling disconnect");
+            Log.i(this, "timeoutCallIfNeeded, call timed out, calling disconnect");
             mIsCallTimedOut = true;
             mConnectionService.disconnect(mCall);
         }
@@ -122,13 +161,19 @@
     private long getTimeoutLengthMillis() {
         // If the radio is off then use a longer timeout. This gives us more time to power on the
         // radio.
-        TelephonyManager telephonyManager =
-            (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
-        if (telephonyManager.isRadioOn()) {
-            return Timeouts.getEmergencyCallTimeoutMillis(mContext.getContentResolver());
-        } else {
-            return Timeouts.getEmergencyCallTimeoutRadioOffMillis(
-                    mContext.getContentResolver());
+        try {
+            TelephonyManager telephonyManager =
+                    (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
+            if (telephonyManager.isRadioOn()) {
+                return mTimeoutsAdapter.getEmergencyCallTimeoutMillis(
+                        mContext.getContentResolver());
+            } else {
+                return mTimeoutsAdapter.getEmergencyCallTimeoutRadioOffMillis(
+                        mContext.getContentResolver());
+            }
+        } catch (UnsupportedOperationException uoe) {
+            Log.e(this, uoe, "getTimeoutLengthMillis - telephony is not supported");
+            return mTimeoutsAdapter.getEmergencyCallTimeoutMillis(mContext.getContentResolver());
         }
     }
 }
diff --git a/src/com/android/server/telecom/DefaultDialerCache.java b/src/com/android/server/telecom/DefaultDialerCache.java
index d819780..44b426a 100644
--- a/src/com/android/server/telecom/DefaultDialerCache.java
+++ b/src/com/android/server/telecom/DefaultDialerCache.java
@@ -176,7 +176,7 @@
                         UserHandle.USER_ALL);
     }
 
-    public String getBTInCallServicePackage() {
+    public String[] getBTInCallServicePackages() {
         return mRoleManagerAdapter.getBTInCallService();
     }
 
diff --git a/src/com/android/server/telecom/InCallController.java b/src/com/android/server/telecom/InCallController.java
index f464d04..f3c84ba 100644
--- a/src/com/android/server/telecom/InCallController.java
+++ b/src/com/android/server/telecom/InCallController.java
@@ -361,7 +361,8 @@
             Log.i(this, "Attempting to bind to InCall %s, with %s", mInCallServiceInfo, intent);
             mIsConnected = true;
             mInCallServiceInfo.setBindingStartTime(mClockProxy.elapsedRealtime());
-            boolean isManagedProfile = UserUtil.isManagedProfile(mContext, userFromCall);
+            boolean isManagedProfile = UserUtil.isManagedProfile(mContext,
+                    userFromCall, mFeatureFlags);
             // Note that UserHandle.CURRENT fails to capture the work profile, so we need to handle
             // it separately to ensure that the ICS is bound to the appropriate user. If ECBM is
             // active, we know that a work sim was previously used to place a MO emergency call. We
@@ -369,7 +370,8 @@
             // not be running (handled in getUserFromCall).
             UserHandle userToBind = isManagedProfile ? userFromCall : UserHandle.CURRENT;
             if ((mInCallServiceInfo.mType == IN_CALL_SERVICE_TYPE_NON_UI
-                    || mInCallServiceInfo.mType == IN_CALL_SERVICE_TYPE_CAR_MODE_UI) && (
+                    || mInCallServiceInfo.mType == IN_CALL_SERVICE_TYPE_CAR_MODE_UI
+                    || mInCallServiceInfo.mType == IN_CALL_SERVICE_TYPE_BLUETOOTH) && (
                     mUserHandleToUseForBinding != null)) {
                 //guarding change for non-UI/carmode-UI services which may not be present for
                 // work profile.
@@ -1079,6 +1081,7 @@
                 if (Intent.ACTION_PACKAGE_CHANGED.equals(intent.getAction())) {
                     synchronized (mLock) {
                         int uid = intent.getIntExtra(Intent.EXTRA_UID, 0);
+                        String changedPackage = intent.getData().getSchemeSpecificPart();
                         UserHandle userHandle = UserHandle.getUserHandleForUid(uid);
                         boolean isManagedProfile = um.isManagedProfile(userHandle.getIdentifier());
 
@@ -1108,12 +1111,36 @@
                                         childManagedProfileUser);
                         List<InCallServiceBindingConnection> componentsToBindForUser = null;
                         List<InCallServiceBindingConnection> componentsToBindForChild = null;
+                        // Separate binding for BT logic.
+                        boolean isBluetoothPkg = isBluetoothPackage(changedPackage);
+                        Call callToConnectWith = mCallIdMapper.getCalls().isEmpty()
+                                ? null
+                                : mCallIdMapper.getCalls().iterator().next();
+
+                        // Bind to BT service if there's an available call. When the flag isn't
+                        // enabled, the service will be included as part of
+                        // getNonUiInCallServiceBindingConnectionList.
+                        if (mFeatureFlags.separatelyBindToBtIncallService()
+                                && isBluetoothPkg && callToConnectWith != null) {
+                            // mNonUIInCallServiceConnections will always contain a key for
+                            // userHandle and/or the child user if there is an ongoing call with
+                            // that user, regardless if there aren't any non-UI ICS bound.
+                            if (isUserKeyPresent) {
+                                bindToBTService(callToConnectWith, userHandle);
+                            }
+                            if (isChildUserKeyPresent) {
+                                // This will try to use the ICS found in the parent if one isn't
+                                // available for the child.
+                                bindToBTService(callToConnectWith, childManagedProfileUser);
+                            }
+                        }
 
                         if(isUserKeyPresent) {
                             componentsToBindForUser =
                                     getNonUiInCallServiceBindingConnectionList(intent,
                                             userHandle, null);
                         }
+
                         if (isChildUserKeyPresent) {
                             componentsToBindForChild =
                                     getNonUiInCallServiceBindingConnectionList(intent,
@@ -1126,11 +1153,11 @@
                                 isUserKeyPresent, isChildUserKeyPresent, isManagedProfile,
                                 userHandle.getIdentifier());
 
-                        if (isUserKeyPresent && componentsToBindForUser != null) {
+                        if (isUserKeyPresent && !componentsToBindForUser.isEmpty()) {
                             mNonUIInCallServiceConnections.get(userHandle).
                                     addConnections(componentsToBindForUser);
                         }
-                        if (isChildUserKeyPresent && componentsToBindForChild != null) {
+                        if (isChildUserKeyPresent && !componentsToBindForChild.isEmpty()) {
                             mNonUIInCallServiceConnections.get(childManagedProfileUser).
                                     addConnections(componentsToBindForChild);
                         }
@@ -1185,6 +1212,10 @@
     private static final int IN_CALL_SERVICE_TYPE_COMPANION = 5;
     private static final int IN_CALL_SERVICE_TYPE_BLUETOOTH = 6;
 
+    // Timeout value to be used to ensure future completion for mDisconnectedToneBtFutures. This is
+    // set to 4 seconds to account for the exceptional case (TONE_CONGESTION).
+    private static final int DISCONNECTED_TONE_TIMEOUT = 4000;
+
     private static final int[] LIVE_CALL_STATES = { CallState.ACTIVE, CallState.PULLING,
             CallState.DISCONNECTING };
 
@@ -1232,7 +1263,10 @@
     // 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 Map<UserHandle, CompletableFuture<Boolean>> mBtBindingFuture = new ArrayMap<>();
+    // Future used to delay terminating the BT InCallService before the call disconnect tone
+    // finishes playing.
+    private Map<String, CompletableFuture<Void>> mDisconnectedToneBtFutures = new ArrayMap<>();
 
     private final CarModeTracker mCarModeTracker;
 
@@ -1255,6 +1289,8 @@
 
     private boolean mIsStartCallDelayScheduled = false;
 
+    private boolean mDisconnectedToneStartedPlaying = false;
+
     /**
      * A list of call IDs which are currently using the camera.
      */
@@ -1374,27 +1410,29 @@
         addCall(call);
 
         if (mFeatureFlags.separatelyBindToBtIncallService()) {
-            boolean bindBTService = false;
-            boolean bindOtherServices = false;
+            boolean bindingToBtRequired = false;
+            boolean bindingToOtherServicesRequired = false;
             if (!isBoundAndConnectedToBTService(userFromCall)) {
                 Log.i(this, "onCallAdded: %s; not bound or connected to BT ICS.", call);
-                bindBTService = true;
-                bindToBTService(call);
+                bindingToBtRequired = true;
+                bindToBTService(call, null);
             }
             if (!isBoundAndConnectedToServices(userFromCall)) {
                 Log.i(this, "onCallAdded: %s; not bound or connected to other ICS.", call);
                 // We are not bound, or we're not connected.
-                bindOtherServices = true;
-                bindToOtherServices(call);
+                bindingToOtherServicesRequired = true;
+                bindToServices(call);
             }
-            if (!bindBTService || !bindOtherServices) {
+            // If either BT service are already bound or other services are already bound, attempt
+            // to add the new call to the connected incall services.
+            if (!bindingToBtRequired || !bindingToOtherServicesRequired) {
                 addCallToConnectedServices(call, userFromCall);
             }
         } else {
             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);
+                bindToServices(call);
             } else {
                 addCallToConnectedServices(call, userFromCall);
             }
@@ -1506,17 +1544,31 @@
     @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);
+                if (isTonePlaying) {
+                    mDisconnectedToneStartedPlaying = true;
+                } else if (mDisconnectedToneStartedPlaying) {
+                    mDisconnectedToneStartedPlaying = false;
+                    if (mDisconnectedToneBtFutures.containsKey(call.getId())) {
+                        Log.i(this, "onDisconnectedTonePlaying: completing BT "
+                                + "disconnected tone future");
+                        mDisconnectedToneBtFutures.get(call.getId()).complete(null);
+                    }
+                    mPendingEndToneCall.remove(call);
+                    if (!mPendingEndToneCall.isEmpty()) {
+                        return;
+                    }
+                    UserHandle userHandle = getUserFromCall(call);
+                    if (mBTInCallServiceConnections.containsKey(userHandle)) {
+                        Log.i(this, "onDisconnectedTonePlaying: Unbinding BT service");
+                        mBTInCallServiceConnections.get(userHandle).disconnect();
+                        mBTInCallServiceConnections.remove(userHandle);
+                    }
+                    // Ensure that BT ICS instance is cleaned up
+                    if (mBTInCallServices.remove(userHandle) != null) {
+                        updateCombinedInCallServiceMap(userHandle);
+                    }
                 }
             }
         }
@@ -1968,6 +2020,8 @@
         }
         getCombinedInCallServiceMap().remove(userHandle);
         if (mFeatureFlags.separatelyBindToBtIncallService()) {
+            // Note that the BT ICS will be repopulated as part of the combined map if the
+            // BT ICS is still bound (disconnected tone hasn't finished playing).
             updateCombinedInCallServiceMap(userHandle);
         }
     }
@@ -1978,34 +2032,49 @@
      *
      * @param call The newly added call that triggered the binding to the in-call services.
      */
-    public CompletableFuture<Boolean> bindToBTService(Call call) {
+    public void bindToBTService(Call call, UserHandle userHandle) {
+        Log.i(this, "bindToBtService");
+        UserHandle userToBind = userHandle == null
+                ? getUserFromCall(call)
+                : userHandle;
+        UserManager um = mContext.getSystemService(UserManager.class);
+        UserHandle parentUser = mFeatureFlags.profileUserSupport()
+                ? um.getProfileParent(userToBind) : null;
+
+        if (!mFeatureFlags.profileUserSupport()
+                && um.isManagedProfile(userToBind.getIdentifier())) {
+            parentUser = um.getProfileParent(userToBind);
+        }
+
         // Track the call if we don't already know about it.
         addCall(call);
-        UserHandle userFromCall = getUserFromCall(call);
-
-        List<InCallServiceInfo> infos = getInCallServiceComponents(userFromCall,
+        List<InCallServiceInfo> infos = getInCallServiceComponents(userToBind,
                 IN_CALL_SERVICE_TYPE_BLUETOOTH);
+        boolean serviceUnavailableForUser = false;
         if (infos.size() == 0 || infos.get(0) == null) {
-            Log.w(this, "No available BT service");
-            mBtBindingFuture = CompletableFuture.completedFuture(false);
-            return mBtBindingFuture;
+            Log.i(this, "No available BT ICS for user (%s). Trying with parent instead.",
+                    userToBind);
+            serviceUnavailableForUser = true;
+            // Check if the service is available under the parent user instead.
+            if (parentUser != null) {
+                infos = getInCallServiceComponents(parentUser, IN_CALL_SERVICE_TYPE_BLUETOOTH);
+            }
+            if (infos.size() == 0 || infos.get(0) == null) {
+                Log.w(this, "No available BT ICS to bind to for user %s or its parent %s.",
+                        userToBind, parentUser);
+                mBtBindingFuture.put(userToBind, CompletableFuture.completedFuture(false));
+                return;
+            }
         }
-        mBtBindingFuture = 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 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.
-     */
-    public void bindToOtherServices(Call call) {
-        bindToServices(call, true);
+        mBtBindingFuture.put(userToBind, new CompletableFuture<Boolean>().completeOnTimeout(false,
+                mTimeoutsAdapter.getCallBindBluetoothInCallServicesDelay(
+                        mContext.getContentResolver()), TimeUnit.MILLISECONDS));
+        InCallServiceBindingConnection btIcsBindingConnection =
+                new InCallServiceBindingConnection(infos.get(0),
+                        serviceUnavailableForUser ? parentUser : userToBind);
+        mBTInCallServiceConnections.put(userToBind, btIcsBindingConnection);
+        btIcsBindingConnection.connect(call);
     }
 
     /**
@@ -2015,11 +2084,9 @@
      *
      * @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) {
+    public void bindToServices(Call call) {
         UserHandle userFromCall = getUserFromCall(call);
         UserManager um = mContext.getSystemService(UserManager.class);
         UserHandle parentUser = mFeatureFlags.profileUserSupport()
@@ -2084,7 +2151,7 @@
             // 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, skipBTServices);
+            connectToNonUiInCallServices(call);
             mBindingFuture = new CompletableFuture<Boolean>().completeOnTimeout(false,
                     mTimeoutsAdapter.getCallRemoveUnbindInCallServicesDelay(
                             mContext.getContentResolver()),
@@ -2099,7 +2166,7 @@
                 packageChangedFilter, null, null);
     }
 
-    private void updateNonUiInCallServices(Call call, boolean skipBTService) {
+    private void updateNonUiInCallServices(Call call) {
         UserHandle userFromCall = getUserFromCall(call);
 
         UserManager um = mContext.getSystemService(UserManager.class);
@@ -2154,10 +2221,10 @@
                 nonUIInCalls));
     }
 
-    private void connectToNonUiInCallServices(Call call, boolean skipBTService) {
+    private void connectToNonUiInCallServices(Call call) {
         UserHandle userFromCall = getUserFromCall(call);
         if (!mNonUIInCallServiceConnections.containsKey(userFromCall)) {
-            updateNonUiInCallServices(call, skipBTService);
+            updateNonUiInCallServices(call);
         }
         mNonUIInCallServiceConnections.get(userFromCall).connect(call);
     }
@@ -2426,10 +2493,8 @@
             return IN_CALL_SERVICE_TYPE_DEFAULT_DIALER_UI;
         }
 
-        String bluetoothPackage = mDefaultDialerCache.getBTInCallServicePackage();
-        if (mFeatureFlags.separatelyBindToBtIncallService()
-                && serviceInfo.packageName != null
-                && serviceInfo.packageName.equals(bluetoothPackage)
+        boolean processingBluetoothPackage = isBluetoothPackage(serviceInfo.packageName);
+        if (mFeatureFlags.separatelyBindToBtIncallService() && processingBluetoothPackage
                 && (hasControlInCallPermission || hasAppOpsPermittedManageOngoingCalls)) {
             return IN_CALL_SERVICE_TYPE_BLUETOOTH;
         }
@@ -2476,11 +2541,13 @@
         IInCallService inCallService = IInCallService.Stub.asInterface(service);
         if (mFeatureFlags.separatelyBindToBtIncallService()
                 && info.getType() == IN_CALL_SERVICE_TYPE_BLUETOOTH) {
-            if (mBtBindingFuture.isDone()) {
+            if (!mBtBindingFuture.containsKey(userHandle)
+                    || mBtBindingFuture.get(userHandle).isDone()) {
+                Log.i(this, "onConnected: BT binding future timed out.");
                 // Binding completed after the timeout. Clean up this binding
                 return false;
             } else {
-                mBtBindingFuture.complete(true);
+                mBtBindingFuture.get(userHandle).complete(true);
             }
             mBTInCallServices.put(userHandle, new Pair<>(info, inCallService));
         } else {
@@ -2651,12 +2718,20 @@
                 IInCallService inCallService = entry.getValue();
                 componentsUpdated.add(componentName);
 
-                try {
-                    inCallService.updateCall(
-                            sanitizeParcelableCallForService(info, parcelableCall));
-                } catch (RemoteException exception) {
-                    Log.w(this, "Call status update did not send to: "
-                                + componentName +" successfully with error " + exception);
+                if (info.getType() == IN_CALL_SERVICE_TYPE_BLUETOOTH
+                        && call.getState() == CallState.DISCONNECTED
+                        && !mDisconnectedToneBtFutures.containsKey(call.getId())) {
+                    CompletableFuture<Void> disconnectedToneFuture = new CompletableFuture<Void>()
+                            .completeOnTimeout(null, DISCONNECTED_TONE_TIMEOUT,
+                                    TimeUnit.MILLISECONDS);
+                    mDisconnectedToneBtFutures.put(call.getId(), disconnectedToneFuture);
+                    mDisconnectedToneBtFutures.get(call.getId()).thenRunAsync(() -> {
+                        Log.i(this, "updateCall: Sending call disconnected update to BT ICS.");
+                        updateCallToIcs(inCallService, info, parcelableCall, componentName);
+                        mDisconnectedToneBtFutures.remove(call.getId());
+                    }, new LoggedHandlerExecutor(mHandler, "ICC.uC", mLock));
+                } else {
+                    updateCallToIcs(inCallService, info, parcelableCall, componentName);
                 }
             }
             Log.i(this, "Components updated: %s", componentsUpdated);
@@ -2666,12 +2741,27 @@
         }
     }
 
+    private void updateCallToIcs(IInCallService inCallService, InCallServiceInfo info,
+            ParcelableCall parcelableCall, ComponentName componentName) {
+        try {
+            inCallService.updateCall(
+                    sanitizeParcelableCallForService(info, parcelableCall));
+        } catch (RemoteException exception) {
+            Log.w(this, "Call status update did not send to: "
+                    + componentName + " successfully with error " + exception);
+        }
+    }
+
     /**
      * Adds the call to the list of calls tracked by the {@link InCallController}.
      * @param call The call to add.
      */
     @VisibleForTesting
     public void addCall(Call call) {
+        if (call == null) {
+            return;
+        }
+
         if (mCallIdMapper.getCalls().size() == 0) {
             mAppOpsManager.startWatchingActive(new String[] { OPSTR_RECORD_AUDIO },
                     java.lang.Runnable::run, this);
@@ -2682,12 +2772,12 @@
         if (mCallIdMapper.getCallId(call) == null) {
             mCallIdMapper.addCall(call);
             call.addListener(mCallListener);
+            if (mFeatureFlags.separatelyBindToBtIncallService()) {
+                mPendingEndToneCall.add(call);
+            }
         }
 
         maybeTrackMicrophoneUse(isMuted());
-        if (mFeatureFlags.separatelyBindToBtIncallService()) {
-            mPendingEndToneCall.add(call);
-        }
     }
 
     /**
@@ -2717,6 +2807,23 @@
     }
 
     /**
+     * @return A future that is pending whenever we are in the middle of binding to the BT
+     *         incall service.
+     */
+    public CompletableFuture<Boolean> getBtBindingFuture(Call call) {
+        UserHandle userHandle = getUserFromCall(call);
+        return mBtBindingFuture.get(userHandle);
+    }
+
+    /**
+     * @return A future that is pending whenever we are in the process of sending the call
+     *         disconnected state to the BT ICS so that the disconnect tone can finish playing.
+     */
+    public Map<String, CompletableFuture<Void>> getDisconnectedToneBtFutures() {
+        return mDisconnectedToneBtFutures;
+    }
+
+    /**
      * Dumps the state of the {@link InCallController}.
      *
      * @param pw The {@code IndentingPrintWriter} to write the state to.
@@ -3021,19 +3128,38 @@
         mIsCallUsingMicrophone = mIsTrackingManagedAliveCall && !isMuted
                 && !isCarrierPrivilegedUsingMicDuringVoipCall();
         if (wasUsingMicrophone != mIsCallUsingMicrophone) {
+            int opPackageUid = getOpPackageUid();
             if (mIsCallUsingMicrophone) {
                 // Note, not checking return value, as this op call is merely for tracing use
-                mAppOpsManager.startOp(AppOpsManager.OP_PHONE_CALL_MICROPHONE, myUid(),
+                mAppOpsManager.startOp(AppOpsManager.OP_PHONE_CALL_MICROPHONE, opPackageUid,
                         mContext.getOpPackageName(), false, null, null);
                 mSensorPrivacyManager.showSensorUseDialog(SensorPrivacyManager.Sensors.MICROPHONE);
             } else {
-                mAppOpsManager.finishOp(AppOpsManager.OP_PHONE_CALL_MICROPHONE, myUid(),
+                mAppOpsManager.finishOp(AppOpsManager.OP_PHONE_CALL_MICROPHONE, opPackageUid,
                         mContext.getOpPackageName(), null);
             }
         }
     }
 
     /**
+     * Returns the uid of the package in the current user to be used for app ops attribution.
+     */
+    private int getOpPackageUid() {
+        UserHandle user = mCallsManager.getCurrentUserHandle();
+
+        try {
+            PackageManager pkgManager = mContext.getPackageManager();
+            return pkgManager.getPackageUidAsUser(mContext.getOpPackageName(),
+                    user.getIdentifier());
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.e(this, e, "getPackageForAssociatedUser: could not find package %s"
+                    + " for user %s", mContext.getOpPackageName(), user);
+            // fallback to current process id - this should not happen
+            return myUid();
+        }
+    }
+
+    /**
      * @return {@code true} if InCallController is tracking a managed call (i.e. not self managed
      * and not external) that is active.
      */
@@ -3113,7 +3239,13 @@
             return mCallsManager.getCurrentUserHandle();
         } else {
             UserHandle userFromCall = call.getAssociatedUser();
-            UserManager userManager = mContext.getSystemService(UserManager.class);
+            UserManager userManager = mFeatureFlags.telecomResolveHiddenDependencies()
+                    ? mContext.createContextAsUser(mCallsManager.getCurrentUserHandle(), 0)
+                            .getSystemService(UserManager.class)
+                    : mContext.getSystemService(UserManager.class);
+            boolean isCurrentUserAdmin = mFeatureFlags.telecomResolveHiddenDependencies()
+                    ? userManager.isAdminUser()
+                    : userManager.isUserAdmin(mCallsManager.getCurrentUserHandle().getIdentifier());
             // Emergency call should never be blocked, so if the user associated with the target
             // phone account handle user is in quiet mode, use the current user for the ecall.
             // Note, that this only applies to incoming calls that are received on assigned
@@ -3123,8 +3255,7 @@
                     && (userManager.isQuietModeEnabled(userFromCall)
                     // We should also account for secondary/guest users where the profile may not
                     // necessarily be paused.
-                    || !userManager.isUserAdmin(mCallsManager.getCurrentUserHandle()
-                    .getIdentifier()))) {
+                    || !isCurrentUserAdmin)) {
                 return mCallsManager.getCurrentUserHandle();
             }
             return userFromCall;
@@ -3149,7 +3280,10 @@
                 }
             }
         }
-        return false;
+        // If early binding for BT ICS is enabled, ensure that it is included into consideration as
+        // a bound non-UI ICS.
+        return mFeatureFlags.separatelyBindToBtIncallService() && !mBTInCallServices.isEmpty()
+                && isBluetoothPackage(packageName);
     }
 
     private void updateCombinedInCallServiceMap(UserHandle user) {
@@ -3183,4 +3317,13 @@
             }
         }
     }
+
+    private boolean isBluetoothPackage(String packageName) {
+        for (String pkgName : mDefaultDialerCache.getBTInCallServicePackages()) {
+            if (pkgName.equals(packageName)) {
+                return true;
+            }
+        }
+        return false;
+    }
 }
diff --git a/src/com/android/server/telecom/LogUtils.java b/src/com/android/server/telecom/LogUtils.java
index 0d6acd5..d98ebfe 100644
--- a/src/com/android/server/telecom/LogUtils.java
+++ b/src/com/android/server/telecom/LogUtils.java
@@ -139,8 +139,10 @@
         public static final String STOP_CALL_WAITING_TONE = "STOP_CALL_WAITING_TONE";
         public static final String START_CONNECTION = "START_CONNECTION";
         public static final String CREATE_CONNECTION_FAILED = "CREATE_CONNECTION_FAILED";
+        public static final String CREATE_CONNECTION_TIMEOUT = "CREATE_CONNECTION_TIMEOUT";
         public static final String START_CONFERENCE = "START_CONFERENCE";
         public static final String CREATE_CONFERENCE_FAILED = "CREATE_CONFERENCE_FAILED";
+        public static final String CREATE_CONFERENCE_TIMEOUT = "CREATE_CONFERENCE_TIMEOUT";
         public static final String BIND_CS = "BIND_CS";
         public static final String CS_BOUND = "CS_BOUND";
         public static final String CONFERENCE_WITH = "CONF_WITH";
diff --git a/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java b/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java
index 6070baa..c24ac97 100644
--- a/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java
+++ b/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java
@@ -623,6 +623,9 @@
         try {
             return mContext.getSystemService(TelephonyManager.class).isEmergencyNumber(
                     number);
+        } catch (UnsupportedOperationException uoe) {
+            Log.w(this, "isEmergencyNumber: Telephony not supported");
+            return false;
         } catch (Exception e) {
             Log.e(this, e, "isEmergencyNumber: Telephony threw an exception.");
             return false;
diff --git a/src/com/android/server/telecom/PendingAudioRoute.java b/src/com/android/server/telecom/PendingAudioRoute.java
index 8de62ed..396aca0 100644
--- a/src/com/android/server/telecom/PendingAudioRoute.java
+++ b/src/com/android/server/telecom/PendingAudioRoute.java
@@ -17,10 +17,18 @@
 package com.android.server.telecom;
 
 import static com.android.server.telecom.CallAudioRouteAdapter.PENDING_ROUTE_FAILED;
+import static com.android.server.telecom.CallAudioRouteAdapter.SWITCH_BASELINE_ROUTE;
+import static com.android.server.telecom.CallAudioRouteController.INCLUDE_BLUETOOTH_IN_BASELINE;
 
+import android.bluetooth.BluetoothDevice;
 import android.media.AudioManager;
+import android.telecom.Log;
+import android.util.ArraySet;
+import android.util.Pair;
 
-import java.util.ArrayList;
+import com.android.server.telecom.bluetooth.BluetoothRouteManager;
+
+import java.util.Set;
 
 /**
  * Used to represent the intermediate state during audio route switching.
@@ -32,6 +40,7 @@
 public class PendingAudioRoute {
     private CallAudioRouteController mCallAudioRouteController;
     private AudioManager mAudioManager;
+    private BluetoothRouteManager mBluetoothRouteManager;
     /**
      * The {@link AudioRoute} that this pending audio switching started with
      */
@@ -41,17 +50,25 @@
      * by new switching request during the ongoing switching
      */
     private AudioRoute mDestRoute;
-    private ArrayList<Integer> mPendingMessages;
+    private Set<Pair<Integer, String>> mPendingMessages;
     private boolean mActive;
-    PendingAudioRoute(CallAudioRouteController controller, AudioManager audioManager) {
+    /**
+     * The device that has been set for communication by Telecom
+     */
+    private @AudioRoute.AudioRouteType int mCommunicationDeviceType = AudioRoute.TYPE_INVALID;
+
+    PendingAudioRoute(CallAudioRouteController controller, AudioManager audioManager,
+            BluetoothRouteManager bluetoothRouteManager) {
         mCallAudioRouteController = controller;
         mAudioManager = audioManager;
-        mPendingMessages = new ArrayList<>();
+        mBluetoothRouteManager = bluetoothRouteManager;
+        mPendingMessages = new ArraySet<>();
         mActive = false;
+        mCommunicationDeviceType = AudioRoute.TYPE_INVALID;
     }
 
     void setOrigRoute(boolean active, AudioRoute origRoute) {
-        origRoute.onOrigRouteAsPendingRoute(active, this, mAudioManager);
+        origRoute.onOrigRouteAsPendingRoute(active, this, mAudioManager, mBluetoothRouteManager);
         mOrigRoute = origRoute;
     }
 
@@ -59,30 +76,33 @@
         return mOrigRoute;
     }
 
-    void setDestRoute(boolean active, AudioRoute destRoute) {
-        destRoute.onDestRouteAsPendingRoute(active, this, mAudioManager);
+    void setDestRoute(boolean active, AudioRoute destRoute, BluetoothDevice device,
+            boolean isScoAudioConnected) {
+        destRoute.onDestRouteAsPendingRoute(active, this, device,
+                mAudioManager, mBluetoothRouteManager, isScoAudioConnected);
         mActive = active;
         mDestRoute = destRoute;
     }
 
-    AudioRoute getDestRoute() {
+    public AudioRoute getDestRoute() {
         return mDestRoute;
     }
 
-    public void addMessage(int message) {
-        mPendingMessages.add(message);
+    public void addMessage(int message, String bluetoothDevice) {
+        mPendingMessages.add(new Pair<>(message, bluetoothDevice));
     }
 
-    public void onMessageReceived(int message) {
-        if (message == PENDING_ROUTE_FAILED) {
+    public void onMessageReceived(Pair<Integer, String> message, String btAddressToExclude) {
+        Log.i(this, "onMessageReceived: message - %s", message);
+        if (message.first == PENDING_ROUTE_FAILED) {
             // Fallback to base route
-            mDestRoute = mCallAudioRouteController.getBaseRoute(true);
             mCallAudioRouteController.sendMessageWithSessionInfo(
-                    CallAudioRouteAdapter.EXIT_PENDING_ROUTE);
+                    SWITCH_BASELINE_ROUTE, INCLUDE_BLUETOOTH_IN_BASELINE, btAddressToExclude);
+            return;
         }
 
         // Removes the first occurrence of the specified message from this list, if it is present.
-        mPendingMessages.remove((Object) message);
+        mPendingMessages.remove(message);
         evaluatePendingState();
     }
 
@@ -90,10 +110,33 @@
         if (mPendingMessages.isEmpty()) {
             mCallAudioRouteController.sendMessageWithSessionInfo(
                     CallAudioRouteAdapter.EXIT_PENDING_ROUTE);
+        } else {
+            Log.i(this, "evaluatePendingState: mPendingMessages - %s", mPendingMessages);
         }
     }
 
+    public void clearPendingMessages() {
+        mPendingMessages.clear();
+    }
+
+    public void clearPendingMessage(Pair<Integer, String> message) {
+        mPendingMessages.remove(message);
+    }
+
     public boolean isActive() {
         return mActive;
     }
+
+    public @AudioRoute.AudioRouteType int getCommunicationDeviceType() {
+        return mCommunicationDeviceType;
+    }
+
+    public void setCommunicationDeviceType(
+            @AudioRoute.AudioRouteType int communicationDeviceType) {
+        mCommunicationDeviceType = communicationDeviceType;
+    }
+
+    public void overrideDestRoute(AudioRoute route) {
+        mDestRoute = route;
+    }
 }
diff --git a/src/com/android/server/telecom/PhoneAccountRegistrar.java b/src/com/android/server/telecom/PhoneAccountRegistrar.java
index fc90edd..f0423c3 100644
--- a/src/com/android/server/telecom/PhoneAccountRegistrar.java
+++ b/src/com/android/server/telecom/PhoneAccountRegistrar.java
@@ -185,30 +185,35 @@
     private final PhoneAccountRegistrarWriteLock mWriteLock =
             new PhoneAccountRegistrarWriteLock() {};
     private final FeatureFlags mTelephonyFeatureFlags;
+    private final com.android.server.telecom.flags.FeatureFlags mTelecomFeatureFlags;
 
     @VisibleForTesting
     public PhoneAccountRegistrar(Context context, TelecomSystem.SyncRoot lock,
             DefaultDialerCache defaultDialerCache, AppLabelProxy appLabelProxy,
-            FeatureFlags telephonyFeatureFlags) {
-        this(context, lock, FILE_NAME, defaultDialerCache, appLabelProxy, telephonyFeatureFlags);
+            FeatureFlags telephonyFeatureFlags,
+            com.android.server.telecom.flags.FeatureFlags telecomFeatureFlags) {
+        this(context, lock, FILE_NAME, defaultDialerCache, appLabelProxy,
+                telephonyFeatureFlags, telecomFeatureFlags);
     }
 
     @VisibleForTesting
     public PhoneAccountRegistrar(Context context, TelecomSystem.SyncRoot lock, String fileName,
             DefaultDialerCache defaultDialerCache, AppLabelProxy appLabelProxy,
-            FeatureFlags telephonyFeatureFlags) {
+            FeatureFlags telephonyFeatureFlags,
+            com.android.server.telecom.flags.FeatureFlags telecomFeatureFlags) {
 
         mAtomicFile = new AtomicFile(new File(context.getFilesDir(), fileName));
 
         mState = new State();
         mContext = context;
         mLock = lock;
-        mUserManager = UserManager.get(context);
+        mUserManager = context.getSystemService(UserManager.class);
         mDefaultDialerCache = defaultDialerCache;
         mSubscriptionManager = SubscriptionManager.from(mContext);
         mTelephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
         mAppLabelProxy = appLabelProxy;
         mCurrentUserHandle = Process.myUserHandle();
+        mTelecomFeatureFlags = telecomFeatureFlags;
 
         if (telephonyFeatureFlags != null) {
             mTelephonyFeatureFlags = telephonyFeatureFlags;
@@ -237,7 +242,11 @@
         PhoneAccount account = getPhoneAccountUnchecked(accountHandle);
 
         if (account != null && account.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) {
-            return mTelephonyManager.getSubscriptionId(accountHandle);
+            try {
+                return mTelephonyManager.getSubscriptionId(accountHandle);
+            } catch (UnsupportedOperationException ignored) {
+                // Ignore; fall back to invalid below.
+            }
         }
         return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
     }
@@ -420,18 +429,23 @@
                 Log.i(this, "setUserSelectedOutgoingPhoneAccount: %s is not a sub", accountHandle);
             }
         }
+
         write();
         fireDefaultOutgoingChanged();
     }
 
     private void updateDefaultVoiceSubId(int newSubId, PhoneAccountHandle accountHandle){
-        int currentVoiceSubId = mSubscriptionManager.getDefaultVoiceSubscriptionId();
-        if (newSubId != currentVoiceSubId) {
-            Log.i(this, "setUserSelectedOutgoingPhoneAccount: update voice sub; "
-                    + "account=%s, subId=%d", accountHandle, newSubId);
-            mSubscriptionManager.setDefaultVoiceSubscriptionId(newSubId);
-        } else {
-            Log.i(this, "setUserSelectedOutgoingPhoneAccount: no change to voice sub");
+        try {
+            int currentVoiceSubId = mSubscriptionManager.getDefaultVoiceSubscriptionId();
+            if (newSubId != currentVoiceSubId) {
+                Log.i(this, "setUserSelectedOutgoingPhoneAccount: update voice sub; "
+                        + "account=%s, subId=%d", accountHandle, newSubId);
+                mSubscriptionManager.setDefaultVoiceSubscriptionId(newSubId);
+            } else {
+                Log.i(this, "setUserSelectedOutgoingPhoneAccount: no change to voice sub");
+            }
+        } catch (UnsupportedOperationException uoe) {
+            Log.w(this, "setUserSelectedOutgoingPhoneAccount: no telephony");
         }
     }
 
@@ -460,8 +474,13 @@
     }
 
     boolean isUserSelectedSmsPhoneAccount(PhoneAccountHandle accountHandle) {
-        return getSubscriptionIdForPhoneAccount(accountHandle) ==
-                SubscriptionManager.getDefaultSmsSubscriptionId();
+        try {
+            return getSubscriptionIdForPhoneAccount(accountHandle) ==
+                    SubscriptionManager.getDefaultSmsSubscriptionId();
+        } catch (UnsupportedOperationException uoe) {
+            Log.w(this, "isUserSelectedSmsPhoneAccount: no telephony");
+            return false;
+        }
     }
 
     public ComponentName getSystemSimCallManagerComponent() {
@@ -470,12 +489,18 @@
 
     public ComponentName getSystemSimCallManagerComponent(int subId) {
         String defaultSimCallManager = null;
-        CarrierConfigManager configManager = (CarrierConfigManager) mContext.getSystemService(
-                Context.CARRIER_CONFIG_SERVICE);
-        PersistableBundle configBundle = configManager.getConfigForSubId(subId);
-        if (configBundle != null) {
-            defaultSimCallManager = configBundle.getString(
-                    CarrierConfigManager.KEY_DEFAULT_SIM_CALL_MANAGER_STRING);
+        try {
+            CarrierConfigManager configManager = (CarrierConfigManager) mContext.getSystemService(
+                    Context.CARRIER_CONFIG_SERVICE);
+            if (configManager == null) return null;
+            PersistableBundle configBundle = configManager.getConfigForSubId(subId);
+            if (configBundle != null) {
+                defaultSimCallManager = configBundle.getString(
+                        CarrierConfigManager.KEY_DEFAULT_SIM_CALL_MANAGER_STRING);
+            }
+        } catch (UnsupportedOperationException ignored) {
+            Log.w(this, "getSystemSimCallManagerComponent: no telephony");
+            // Fall through to empty below.
         }
         return TextUtils.isEmpty(defaultSimCallManager)
                 ?  null : ComponentName.unflattenFromString(defaultSimCallManager);
@@ -757,8 +782,11 @@
         }
 
         if (acrossProfiles) {
-            return UserManager.get(mContext).isSameProfileGroup(userHandle.getIdentifier(),
-                    phoneAccountUserHandle.getIdentifier());
+            UserManager um = mContext.getSystemService(UserManager.class);
+            return mTelecomFeatureFlags.telecomResolveHiddenDependencies()
+                    ? um.isSameProfileGroup(userHandle, phoneAccountUserHandle)
+                    : um.isSameProfileGroup(userHandle.getIdentifier(),
+                            phoneAccountUserHandle.getIdentifier());
         } else {
             return phoneAccountUserHandle.equals(userHandle);
         }
@@ -953,6 +981,9 @@
         }
         enforceCharacterLimit(account);
         enforceIconSizeLimit(account);
+        if (mTelecomFeatureFlags.unregisterUnresolvableAccounts()) {
+            enforcePhoneAccountTargetService(account);
+        }
         enforceMaxPhoneAccountLimit(account);
         if (mTelephonyFeatureFlags.simultaneousCallingIndications()) {
             enforceSimultaneousCallingRestrictionLimit(account);
@@ -961,6 +992,25 @@
     }
 
     /**
+     * This method ensures that {@link PhoneAccount}s that have the {@link
+     * PhoneAccount#CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS} capability are not
+     * backed by a {@link ConnectionService}
+     *
+     * @param account enforce the check on
+     */
+    private void enforcePhoneAccountTargetService(PhoneAccount account) {
+        if (phoneAccountRequiresBindPermission(account.getAccountHandle()) &&
+                hasTransactionalCallCapabilities(account)) {
+            throw new IllegalArgumentException(
+                    "Error, the PhoneAccount you are registering has"
+                            + " CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS and the"
+                            + " PhoneAccountHandle's ComponentName#ClassName points to a"
+                            + " ConnectionService class.  Either remove the capability or use a"
+                            + " different ClassName in the PhoneAccountHandle.");
+        }
+    }
+
+    /**
      * Enforce an upper bound on the number of PhoneAccount's a package can register.
      * Most apps should only require 1-2.  * Include disabled accounts.
      *
@@ -968,13 +1018,17 @@
      * @throws IllegalArgumentException if MAX_PHONE_ACCOUNT_REGISTRATIONS are reached
      */
     private void enforceMaxPhoneAccountLimit(@NonNull PhoneAccount account) {
-        final PhoneAccountHandle accountHandle = account.getAccountHandle();
-        final UserHandle user = accountHandle.getUserHandle();
-        final ComponentName componentName = accountHandle.getComponentName();
-
-        if (getPhoneAccountHandles(0, null, componentName.getPackageName(),
-                true /* includeDisabled */, user, false /* crossUserAccess */).size()
-                >= MAX_PHONE_ACCOUNT_REGISTRATIONS) {
+        int numOfAcctsRegisteredForPackage = mTelecomFeatureFlags.unregisterUnresolvableAccounts()
+                ? cleanupAndGetVerifiedAccounts(account).size()
+                : getPhoneAccountHandles(
+                        0/* capabilities */,
+                        null /* uriScheme */,
+                        account.getAccountHandle().getComponentName().getPackageName(),
+                        true /* includeDisabled */,
+                        account.getAccountHandle().getUserHandle(),
+                        false /* crossUserAccess */).size();
+        // enforce the max phone account limit for the application registering accounts
+        if (numOfAcctsRegisteredForPackage >= MAX_PHONE_ACCOUNT_REGISTRATIONS) {
             EventLog.writeEvent(0x534e4554, "259064622", Binder.getCallingUid(),
                     "enforceMaxPhoneAccountLimit");
             throw new IllegalArgumentException(
@@ -984,6 +1038,64 @@
         }
     }
 
+    @VisibleForTesting
+    public List<PhoneAccount> getRegisteredAccountsForPackageName(String packageName,
+            UserHandle userHandle) {
+        if (packageName == null) {
+            return new ArrayList<>();
+        }
+        List<PhoneAccount> accounts = new ArrayList<>(mState.accounts.size());
+        for (PhoneAccount m : mState.accounts) {
+            PhoneAccountHandle handle = m.getAccountHandle();
+            if (!packageName.equals(handle.getComponentName().getPackageName())) {
+                // Not the right package name; skip this one.
+                continue;
+            }
+            // Do not count accounts registered under different users on the device. Otherwise, an
+            // application can only have MAX_PHONE_ACCOUNT_REGISTRATIONS across all users. If the
+            // DUT has multiple users, they should each get to register 10 accounts. Also, 3rd
+            // party applications cannot create new UserHandles without highly privileged
+            // permissions.
+            if (!isVisibleForUser(m, userHandle, false)) {
+                // Account is not visible for the current user; skip this one.
+                continue;
+            }
+            accounts.add(m);
+        }
+        return accounts;
+    }
+
+    /**
+     * Unregister {@link ConnectionService} accounts that no longer have a resolvable Service. This
+     * means the Service has been disabled or died.  Skip the verification for transactional
+     * accounts.
+     *
+     * @param newAccount being registered
+     * @return all the verified accounts. These accounts are now guaranteed to be backed by a
+     * {@link ConnectionService} or do not need one (transactional accounts).
+     */
+    @VisibleForTesting
+    public List<PhoneAccount> cleanupAndGetVerifiedAccounts(PhoneAccount newAccount) {
+        ArrayList<PhoneAccount> verifiedAccounts = new ArrayList<>();
+        List<PhoneAccount> unverifiedAccounts = getRegisteredAccountsForPackageName(
+                newAccount.getAccountHandle().getComponentName().getPackageName(),
+                newAccount.getAccountHandle().getUserHandle());
+        for (PhoneAccount account : unverifiedAccounts) {
+            PhoneAccountHandle handle = account.getAccountHandle();
+            if (/* skip for transactional accounts since they don't require a ConnectionService */
+                    !hasTransactionalCallCapabilities(account) &&
+                    /* check if the {@link ConnectionService} has been disabled or can longer be
+                       found */ resolveComponent(handle).isEmpty()) {
+                Log.i(this, " cAGVA: Cannot resolve the ConnectionService for"
+                        + " handle=[%s]; unregistering account", handle);
+                unregisterPhoneAccount(handle);
+            } else {
+                verifiedAccounts.add(account);
+            }
+        }
+        return verifiedAccounts;
+    }
+
     /**
      * determine if there will be an issue writing the icon to memory
      *
@@ -1456,16 +1568,22 @@
                 "Notifying telephony of voice service override change for %d SIMs, hasService = %b",
                 simHandlesToNotify.size(),
                 hasService);
-        for (PhoneAccountHandle simHandle : simHandlesToNotify) {
-            // This may be null if there are no active SIMs but the device is still camped for
-            // emergency calls and registered a SIM_SUBSCRIPTION for that purpose.
-            TelephonyManager simTm = mTelephonyManager.createForPhoneAccountHandle(simHandle);
-            if (simTm == null) {
-                Log.i(this, "maybeNotifyTelephonyForVoiceServiceState: "
-                        + "simTm is null.");
-                continue;
+        try {
+            for (PhoneAccountHandle simHandle : simHandlesToNotify) {
+                // This may be null if there are no active SIMs but the device is still camped for
+                // emergency calls and registered a SIM_SUBSCRIPTION for that purpose.
+                TelephonyManager simTm = mTelephonyManager.createForPhoneAccountHandle(simHandle);
+                if (simTm == null) {
+                    Log.i(this, "maybeNotifyTelephonyForVoiceServiceState: "
+                            + "simTm is null.");
+                    continue;
+                }
+                simTm.setVoiceServiceStateOverride(hasService);
             }
-            simTm.setVoiceServiceStateOverride(hasService);
+        } catch (UnsupportedOperationException ignored) {
+            // No telephony, so we can't override the sim service state.
+            // Realistically we shouldn't get here because there should be no sim subs in this case.
+            Log.w(this, "maybeNotifyTelephonyForVoiceServiceState: no telephony");
         }
     }
 
@@ -1832,7 +1950,12 @@
             } else {
                 pw.println(defaultOutgoing);
             }
-            pw.println("defaultVoiceSubId: " + SubscriptionManager.getDefaultVoiceSubscriptionId());
+            // SubscriptionManager will throw if FEATURE_TELEPHONY_SUBSCRIPTION is not present.
+            if (mContext.getPackageManager().hasSystemFeature(
+                    PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION)) {
+                pw.println("defaultVoiceSubId: "
+                        + SubscriptionManager.getDefaultVoiceSubscriptionId());
+            }
             pw.println("simCallManager: " + getSimCallManager(mCurrentUserHandle));
             pw.println("phoneAccounts:");
             pw.increaseIndent();
@@ -1942,7 +2065,7 @@
         try {
             XmlPullParser parser = Xml.resolvePullParser(is);
             parser.nextTag();
-            mState = readFromXml(parser, mContext, mTelephonyFeatureFlags);
+            mState = readFromXml(parser, mContext, mTelephonyFeatureFlags, mTelecomFeatureFlags);
             migratePhoneAccountHandle(mState);
             versionChanged = mState.versionNumber < EXPECTED_STATE_VERSION;
 
@@ -1983,8 +2106,11 @@
     }
 
     private static State readFromXml(XmlPullParser parser, Context context,
-            FeatureFlags telephonyFeatureFlags) throws IOException, XmlPullParserException {
-        State s = sStateXml.readFromXml(parser, 0, context, telephonyFeatureFlags);
+            FeatureFlags telephonyFeatureFlags,
+            com.android.server.telecom.flags.FeatureFlags telecomFeatureFlags)
+            throws IOException, XmlPullParserException {
+        State s = sStateXml.readFromXml(parser, 0, context,
+                telephonyFeatureFlags, telecomFeatureFlags);
         return s != null ? s : new State();
     }
 
@@ -2061,7 +2187,9 @@
          * 'parser' if it does not recognize the data it sees.
          */
         public abstract T readFromXml(XmlPullParser parser, int version, Context context,
-                FeatureFlags telephonyFeatureFlags) throws IOException, XmlPullParserException;
+                FeatureFlags telephonyFeatureFlags,
+                com.android.server.telecom.flags.FeatureFlags featureFlags)
+                throws IOException, XmlPullParserException;
 
         protected void writeTextIfNonNull(String tagName, Object value, XmlSerializer serializer)
                 throws IOException {
@@ -2190,7 +2318,8 @@
         }
 
         protected Set<PhoneAccountHandle> readPhoneAccountHandleSet(XmlPullParser parser,
-                int version, Context context, FeatureFlags telephonyFeatureFlags)
+                int version, Context context, FeatureFlags telephonyFeatureFlags,
+                com.android.server.telecom.flags.FeatureFlags telecomFeatureFlags)
                 throws IOException, XmlPullParserException {
             int length = Integer.parseInt(parser.getAttributeValue(null, ATTRIBUTE_LENGTH));
             Set<PhoneAccountHandle> handles = new HashSet<>(length);
@@ -2199,7 +2328,7 @@
             int outerDepth = parser.getDepth();
             while (XmlUtils.nextElementWithin(parser, outerDepth)) {
                 handles.add(sPhoneAccountHandleXml.readFromXml(parser, version, context,
-                        telephonyFeatureFlags));
+                        telephonyFeatureFlags, telecomFeatureFlags));
             }
             return handles;
         }
@@ -2338,7 +2467,9 @@
 
         @Override
         public State readFromXml(XmlPullParser parser, int version, Context context,
-                FeatureFlags telephonyFeatureFlags) throws IOException, XmlPullParserException {
+                FeatureFlags telephonyFeatureFlags,
+                com.android.server.telecom.flags.FeatureFlags telecomFeatureFlags)
+                throws IOException, XmlPullParserException {
             if (parser.getName().equals(CLASS_STATE)) {
                 State s = new State();
 
@@ -2355,16 +2486,23 @@
                             parser.nextTag();
                             PhoneAccountHandle phoneAccountHandle = sPhoneAccountHandleXml
                                     .readFromXml(parser, s.versionNumber, context,
-                                            telephonyFeatureFlags);
-                            UserManager userManager = UserManager.get(context);
-                            UserInfo primaryUser = userManager.getPrimaryUser();
+                                            telephonyFeatureFlags, telecomFeatureFlags);
+                            UserManager userManager = context.getSystemService(UserManager.class);
+                            // UserManager#getMainUser requires either the MANAGE_USERS,
+                            // CREATE_USERS, or QUERY_USERS permission.
+                            UserHandle primaryUser = userManager.getMainUser();
+                            UserInfo primaryUserInfo = userManager.getPrimaryUser();
+                            if (!telecomFeatureFlags.telecomResolveHiddenDependencies()) {
+                                primaryUser = primaryUserInfo != null
+                                        ? primaryUserInfo.getUserHandle()
+                                        : null;
+                            }
                             if (primaryUser != null) {
-                                UserHandle userHandle = primaryUser.getUserHandle();
                                 DefaultPhoneAccountHandle defaultPhoneAccountHandle
-                                        = new DefaultPhoneAccountHandle(userHandle,
+                                        = new DefaultPhoneAccountHandle(primaryUser,
                                         phoneAccountHandle, "" /* groupId */);
                                 s.defaultOutgoingAccountHandles
-                                        .put(userHandle, defaultPhoneAccountHandle);
+                                        .put(primaryUser, defaultPhoneAccountHandle);
                             }
                         } else {
                             int defaultAccountHandlesDepth = parser.getDepth();
@@ -2372,7 +2510,7 @@
                                 DefaultPhoneAccountHandle accountHandle
                                         = sDefaultPhoneAccountHandleXml
                                         .readFromXml(parser, s.versionNumber, context,
-                                                telephonyFeatureFlags);
+                                                telephonyFeatureFlags, telecomFeatureFlags);
                                 if (accountHandle != null && s.accounts != null) {
                                     s.defaultOutgoingAccountHandles
                                             .put(accountHandle.userHandle, accountHandle);
@@ -2383,7 +2521,8 @@
                         int accountsDepth = parser.getDepth();
                         while (XmlUtils.nextElementWithin(parser, accountsDepth)) {
                             PhoneAccount account = sPhoneAccountXml.readFromXml(parser,
-                                    s.versionNumber, context, telephonyFeatureFlags);
+                                    s.versionNumber, context, telephonyFeatureFlags,
+                                    telecomFeatureFlags);
 
                             if (account != null && s.accounts != null) {
                                 s.accounts.add(account);
@@ -2410,7 +2549,7 @@
                 public void writeToXml(DefaultPhoneAccountHandle o, XmlSerializer serializer,
                         Context context, FeatureFlags telephonyFeatureFlags) throws IOException {
                     if (o != null) {
-                        final UserManager userManager = UserManager.get(context);
+                        final UserManager userManager = context.getSystemService(UserManager.class);
                         final long serialNumber = userManager.getSerialNumberForUser(o.userHandle);
                         if (serialNumber != -1) {
                             serializer.startTag(null, CLASS_DEFAULT_OUTGOING_PHONE_ACCOUNT_HANDLE);
@@ -2427,7 +2566,8 @@
 
                 @Override
                 public DefaultPhoneAccountHandle readFromXml(XmlPullParser parser, int version,
-                        Context context, FeatureFlags telephonyFeatureFlags)
+                        Context context, FeatureFlags telephonyFeatureFlags,
+                        com.android.server.telecom.flags.FeatureFlags telecomFeatureFlags)
                         throws IOException, XmlPullParserException {
                     if (parser.getName().equals(CLASS_DEFAULT_OUTGOING_PHONE_ACCOUNT_HANDLE)) {
                         int outerDepth = parser.getDepth();
@@ -2438,7 +2578,7 @@
                             if (parser.getName().equals(ACCOUNT_HANDLE)) {
                                 parser.nextTag();
                                 accountHandle = sPhoneAccountHandleXml.readFromXml(parser, version,
-                                        context, telephonyFeatureFlags);
+                                        context, telephonyFeatureFlags, telecomFeatureFlags);
                             } else if (parser.getName().equals(USER_SERIAL_NUMBER)) {
                                 parser.next();
                                 userSerialNumberString = parser.getText();
@@ -2452,7 +2592,7 @@
                         if (userSerialNumberString != null) {
                             try {
                                 long serialNumber = Long.parseLong(userSerialNumberString);
-                                userHandle = UserManager.get(context)
+                                userHandle = context.getSystemService(UserManager.class)
                                         .getUserForSerialNumber(serialNumber);
                             } catch (NumberFormatException e) {
                                 Log.e(this, e,
@@ -2530,7 +2670,8 @@
         }
 
         public PhoneAccount readFromXml(XmlPullParser parser, int version, Context context,
-                FeatureFlags telephonyFeatureFlags) throws IOException, XmlPullParserException {
+                FeatureFlags telephonyFeatureFlags,
+                com.android.server.telecom.flags.FeatureFlags telecomFeatureFlags) throws IOException, XmlPullParserException {
             if (parser.getName().equals(CLASS_PHONE_ACCOUNT)) {
                 int outerDepth = parser.getDepth();
                 PhoneAccountHandle accountHandle = null;
@@ -2555,7 +2696,7 @@
                     if (parser.getName().equals(ACCOUNT_HANDLE)) {
                         parser.nextTag();
                         accountHandle = sPhoneAccountHandleXml.readFromXml(parser, version,
-                                context, telephonyFeatureFlags);
+                                context, telephonyFeatureFlags, telecomFeatureFlags);
                     } else if (parser.getName().equals(ADDRESS)) {
                         parser.next();
                         address = Uri.parse(parser.getText());
@@ -2605,7 +2746,7 @@
                         // 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);
+                                context, telephonyFeatureFlags, telecomFeatureFlags);
                     }
                 }
 
@@ -2734,7 +2875,7 @@
                 writeTextIfNonNull(ID, o.getId(), serializer);
 
                 if (o.getUserHandle() != null && context != null) {
-                    UserManager userManager = UserManager.get(context);
+                    UserManager userManager = context.getSystemService(UserManager.class);
                     writeLong(USER_SERIAL_NUMBER,
                             userManager.getSerialNumberForUser(o.getUserHandle()), serializer);
                 }
@@ -2745,14 +2886,16 @@
 
         @Override
         public PhoneAccountHandle readFromXml(XmlPullParser parser, int version, Context context,
-                FeatureFlags telephonyFeatureFlags) throws IOException, XmlPullParserException {
+                FeatureFlags telephonyFeatureFlags,
+                com.android.server.telecom.flags.FeatureFlags telecomFeatureFlags)
+                throws IOException, XmlPullParserException {
             if (parser.getName().equals(CLASS_PHONE_ACCOUNT_HANDLE)) {
                 String componentNameString = null;
                 String idString = null;
                 String userSerialNumberString = null;
                 int outerDepth = parser.getDepth();
 
-                UserManager userManager = UserManager.get(context);
+                UserManager userManager = context.getSystemService(UserManager.class);
 
                 while (XmlUtils.nextElementWithin(parser, outerDepth)) {
                     if (parser.getName().equals(COMPONENT_NAME)) {
diff --git a/src/com/android/server/telecom/PhoneStateBroadcaster.java b/src/com/android/server/telecom/PhoneStateBroadcaster.java
index 490db85..11f5e02 100644
--- a/src/com/android/server/telecom/PhoneStateBroadcaster.java
+++ b/src/com/android/server/telecom/PhoneStateBroadcaster.java
@@ -16,6 +16,7 @@
 
 package com.android.server.telecom;
 
+import android.content.pm.PackageManager;
 import android.telecom.Log;
 import android.telephony.PhoneNumberUtils;
 import android.telephony.SubscriptionInfo;
@@ -24,6 +25,8 @@
 import android.telephony.TelephonyRegistryManager;
 import android.telephony.emergency.EmergencyNumber;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 import java.util.List;
 import java.util.Objects;
 import java.util.Optional;
@@ -32,7 +35,7 @@
  * Send a {@link TelephonyManager#ACTION_PHONE_STATE_CHANGED} broadcast when the call state
  * changes.
  */
-final class PhoneStateBroadcaster extends CallsManagerListenerBase {
+public final class PhoneStateBroadcaster extends CallsManagerListenerBase {
 
     private final CallsManager mCallsManager;
     private final TelephonyRegistryManager mRegistry;
@@ -136,25 +139,34 @@
                     .flatMap(List::stream)
                     .filter(numberObj -> Objects.equals(numberObj.getNumber(), strippedNumber))
                     .findFirst();
+        } catch (UnsupportedOperationException ignored) {
+            emergencyNumber = Optional.empty();
         } catch (IllegalStateException ie) {
             emergencyNumber = Optional.empty();
         } catch (RuntimeException r) {
             emergencyNumber = Optional.empty();
         }
 
-        int subscriptionId = tm.getSubscriptionId(call.getTargetPhoneAccount());
-        SubscriptionManager subscriptionManager =
-                mCallsManager.getContext().getSystemService(SubscriptionManager.class);
-        int simSlotIndex = SubscriptionManager.DEFAULT_PHONE_INDEX;
-        if (subscriptionManager != null) {
-            SubscriptionInfo subInfo =
-                    subscriptionManager.getActiveSubscriptionInfo(subscriptionId);
-            if (subInfo != null) {
-                simSlotIndex = subInfo.getSimSlotIndex();
-            }
-        }
-
         if (emergencyNumber.isPresent()) {
+            int subscriptionId;
+            int simSlotIndex;
+            try {
+                subscriptionId = tm.getSubscriptionId(call.getTargetPhoneAccount());
+                SubscriptionManager subscriptionManager =
+                        mCallsManager.getContext().getSystemService(SubscriptionManager.class);
+                simSlotIndex = SubscriptionManager.DEFAULT_PHONE_INDEX;
+                if (subscriptionManager != null) {
+                    SubscriptionInfo subInfo =
+                            subscriptionManager.getActiveSubscriptionInfo(subscriptionId);
+                    if (subInfo != null) {
+                        simSlotIndex = subInfo.getSimSlotIndex();
+                    }
+                }
+            } catch (UnsupportedOperationException ignored) {
+                subscriptionId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+                simSlotIndex = SubscriptionManager.INVALID_SIM_SLOT_INDEX;
+            }
+
             mRegistry.notifyOutgoingEmergencyCall(
                     simSlotIndex, subscriptionId, emergencyNumber.get());
         }
diff --git a/src/com/android/server/telecom/RingtoneFactory.java b/src/com/android/server/telecom/RingtoneFactory.java
index 0e0b99f..16fa0c4 100644
--- a/src/com/android/server/telecom/RingtoneFactory.java
+++ b/src/com/android/server/telecom/RingtoneFactory.java
@@ -33,6 +33,8 @@
 import android.text.TextUtils;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.telecom.flags.FeatureFlags;
+
 import android.telecom.CallerInfo;
 import android.util.Pair;
 
@@ -48,10 +50,12 @@
 
     private final Context mContext;
     private final CallsManager mCallsManager;
+    private FeatureFlags mFeatureFlags;
 
-    public RingtoneFactory(CallsManager callsManager, Context context) {
+    public RingtoneFactory(CallsManager callsManager, Context context, FeatureFlags featureFlags) {
         mContext = context;
         mCallsManager = callsManager;
+        mFeatureFlags = featureFlags;
     }
 
     public Pair<Uri, Ringtone> getRingtone(Call incomingCall,
@@ -82,8 +86,12 @@
             // Contact didn't specify ringtone or custom Ringtone creation failed. Get default
             // ringtone for user or profile.
             Context contextToUse = hasDefaultRingtoneForUser(userContext) ? userContext : mContext;
+            UserManager um = contextToUse.getSystemService(UserManager.class);
+            boolean isUserUnlocked = mFeatureFlags.telecomResolveHiddenDependencies()
+                    ? um.isUserUnlocked(contextToUse.getUser())
+                    : um.isUserUnlocked(contextToUse.getUserId());
             Uri defaultRingtoneUri;
-            if (UserManager.get(contextToUse).isUserUnlocked(contextToUse.getUserId())) {
+            if (isUserUnlocked) {
                 defaultRingtoneUri = RingtoneManager.getActualDefaultRingtoneUri(contextToUse,
                         RingtoneManager.TYPE_RINGTONE);
                 if (defaultRingtoneUri == null) {
@@ -138,22 +146,38 @@
     }
 
     private Context getWorkProfileContextForUser(UserHandle userHandle) {
-        // UserManager.getEnabledProfiles returns the enabled profiles along with the user's handle
-        // itself (so we must filter out the user).
-        List<UserInfo> profiles = UserManager.get(mContext).getEnabledProfiles(
-                userHandle.getIdentifier());
-        UserInfo workprofile = null;
+        // UserManager.getUserProfiles returns the enabled profiles along with the context user's
+        // handle itself (so we must filter out the user).
+        Context userContext = mContext.createContextAsUser(userHandle, 0);
+        UserManager um = mFeatureFlags.telecomResolveHiddenDependencies()
+                ? userContext.getSystemService(UserManager.class)
+                : mContext.getSystemService(UserManager.class);
+        List<UserHandle> profiles = um.getUserProfiles();
+        List<UserInfo> userInfoProfiles = um.getEnabledProfiles(userHandle.getIdentifier());
+        UserHandle workProfileUser = null;
         int managedProfileCount = 0;
-        for (UserInfo profile : profiles) {
-            UserHandle profileUserHandle = profile.getUserHandle();
-            if (profileUserHandle != userHandle && profile.isManagedProfile()) {
-                managedProfileCount++;
-                workprofile = profile;
+
+        if (mFeatureFlags.telecomResolveHiddenDependencies()) {
+            for (UserHandle profileUser : profiles) {
+                UserManager userManager = mContext.createContextAsUser(profileUser, 0)
+                        .getSystemService(UserManager.class);
+                if (!userHandle.equals(profileUser) && userManager.isManagedProfile()) {
+                    managedProfileCount++;
+                    workProfileUser = profileUser;
+                }
+            }
+        } else {
+            for(UserInfo profile: userInfoProfiles) {
+                UserHandle profileUserHandle = profile.getUserHandle();
+                if (!profileUserHandle.equals(userHandle) && profile.isManagedProfile()) {
+                    managedProfileCount++;
+                    workProfileUser = profileUserHandle;
+                }
             }
         }
         // There may be many different types of profiles, so only count Managed (Work) Profiles.
         if(managedProfileCount == 1) {
-            return getContextForUserHandle(workprofile.getUserHandle());
+            return getContextForUserHandle(workProfileUser);
         }
         // There are multiple managed profiles for the associated user and we do not have enough
         // info to determine which profile is the work profile. Just use the default.
diff --git a/src/com/android/server/telecom/RoleManagerAdapter.java b/src/com/android/server/telecom/RoleManagerAdapter.java
index 9f515e6..1b5c71b 100644
--- a/src/com/android/server/telecom/RoleManagerAdapter.java
+++ b/src/com/android/server/telecom/RoleManagerAdapter.java
@@ -71,7 +71,7 @@
      * bt in-call service role.
      * @return the package name of the package filling the role, {@code null} otherwise.
      */
-    String getBTInCallService();
+    String[] getBTInCallService();
 
     /**
      * Override the {@link android.app.role.RoleManager} bt in-call service package with another
diff --git a/src/com/android/server/telecom/RoleManagerAdapterImpl.java b/src/com/android/server/telecom/RoleManagerAdapterImpl.java
index 33ec466..ded4d9c 100644
--- a/src/com/android/server/telecom/RoleManagerAdapterImpl.java
+++ b/src/com/android/server/telecom/RoleManagerAdapterImpl.java
@@ -78,9 +78,9 @@
     }
 
     @Override
-    public String getBTInCallService() {
+    public String[] getBTInCallService() {
         if (mOverrideBTInCallService != null) {
-            return mOverrideBTInCallService;
+            return new String [] {mOverrideBTInCallService};
         }
         return getBluetoothInCallServicePackageName();
     }
@@ -166,8 +166,8 @@
         return roleHolders.get(0);
     }
 
-    private String getBluetoothInCallServicePackageName() {
-        return mContext.getResources().getString(R.string.system_bluetooth_stack);
+    private String[] getBluetoothInCallServicePackageName() {
+        return mContext.getResources().getStringArray(R.array.system_bluetooth_stack_package_name);
     }
 
     /**
diff --git a/src/com/android/server/telecom/ServiceBinder.java b/src/com/android/server/telecom/ServiceBinder.java
index 77f7b2e..a18042b 100644
--- a/src/com/android/server/telecom/ServiceBinder.java
+++ b/src/com/android/server/telecom/ServiceBinder.java
@@ -241,7 +241,7 @@
      * Abbreviated form of the package name from {@link #mComponentName}; used for session logging.
      */
     protected final String mPackageAbbreviation;
-    private final FeatureFlags mFlags;
+    protected final FeatureFlags mFlags;
 
 
     /** The set of callbacks waiting for notification of the binding's success or failure. */
diff --git a/src/com/android/server/telecom/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java
index 9314d7e..20320f2 100644
--- a/src/com/android/server/telecom/TelecomServiceImpl.java
+++ b/src/com/android/server/telecom/TelecomServiceImpl.java
@@ -53,13 +53,18 @@
 import android.os.Build;
 import android.os.Bundle;
 import android.os.OutcomeReceiver;
+import android.os.ParcelFileDescriptor;
 import android.os.Process;
 import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.os.ShellCallback;
 import android.os.UserHandle;
 import android.provider.BlockedNumberContract;
+import android.provider.BlockedNumbersManager;
 import android.provider.Settings;
 import android.telecom.CallAttributes;
 import android.telecom.CallException;
+import android.telecom.DisconnectCause;
 import android.telecom.Log;
 import android.telecom.PhoneAccount;
 import android.telecom.PhoneAccountHandle;
@@ -72,12 +77,14 @@
 import android.util.EventLog;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.telecom.ICallControl;
 import com.android.internal.telecom.ICallEventCallback;
 import com.android.internal.telecom.ITelecomService;
 import com.android.internal.util.IndentingPrintWriter;
+import com.android.modules.utils.BasicShellCommandHandler;
 import com.android.server.telecom.components.UserCallIntentProcessorFactory;
 import com.android.server.telecom.flags.FeatureFlags;
 import com.android.server.telecom.settings.BlockedNumbersActivity;
@@ -90,8 +97,10 @@
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.lang.reflect.Method;
+import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Locale;
 import java.util.Objects;
 import java.util.Set;
 import java.util.UUID;
@@ -206,11 +215,11 @@
                 switch (callAttributes.getDirection()) {
                     case DIRECTION_OUTGOING:
                         transaction = new OutgoingCallTransaction(callId, mContext, callAttributes,
-                                mCallsManager, extras);
+                                mCallsManager, extras, mFeatureFlags);
                         break;
                     case DIRECTION_INCOMING:
                         transaction = new IncomingCallTransaction(callId, callAttributes,
-                                mCallsManager, extras);
+                                mCallsManager, extras, mFeatureFlags);
                         break;
                     default:
                         throw new IllegalArgumentException(String.format("Invalid Call Direction. "
@@ -238,6 +247,7 @@
                                                 callEventCallback, mCallsManager, call);
 
                         call.setTransactionServiceWrapper(serviceWrapper);
+
                         if (mFeatureFlags.transactionalVideoState()) {
                             call.setTransactionalCallSupportsVideoCalling(callAttributes);
                         }
@@ -988,6 +998,9 @@
                         }
                     }
                     return getTelephonyManager(subId).getVoiceMailNumber();
+                } catch (UnsupportedOperationException ignored) {
+                    Log.w(this, "getVoiceMailNumber: no Telephony");
+                    return null;
                 } catch (Exception e) {
                     Log.e(this, e, "getSubscriptionIdForPhoneAccount");
                     throw e;
@@ -1024,6 +1037,9 @@
                                 accountHandle);
                     }
                     return getTelephonyManager(subId).getLine1Number();
+                } catch (UnsupportedOperationException ignored) {
+                    Log.w(this, "getLine1Number: no telephony");
+                    return null;
                 } catch (Exception e) {
                     Log.e(this, e, "getSubscriptionIdForPhoneAccount");
                     throw e;
@@ -1502,8 +1518,13 @@
                         subId = mPhoneAccountRegistrar.getSubscriptionIdForPhoneAccount(
                                 accountHandle);
                     }
-                    retval = getTelephonyManager(subId)
-                            .handlePinMmiForSubscriber(subId, dialString);
+                    try {
+                        retval = getTelephonyManager(subId)
+                                .handlePinMmiForSubscriber(subId, dialString);
+                    } catch (UnsupportedOperationException uoe) {
+                        Log.w(this, "handlePinMmiForPhoneAccount: no telephony");
+                        retval = false;
+                    }
                 } finally {
                     Binder.restoreCallingIdentity(token);
                 }
@@ -1651,7 +1672,13 @@
                                         && accountExtra != null && accountExtra.getBoolean(
                                         PhoneAccount.EXTRA_SKIP_CALL_FILTERING,
                                         false)) {
-                                    mCallsManager.getInCallController().bindToServices(null, false);
+                                    if (mFeatureFlags.separatelyBindToBtIncallService()) {
+                                        mCallsManager.getInCallController().bindToBTService(
+                                                null, null);
+                                    }
+                                    // Should be able to run this as is even if above flag is
+                                    // enabled (BT binding should be skipped automatically).
+                                    mCallsManager.getInCallController().bindToServices(null);
                                 }
                             }
                         } finally {
@@ -2004,7 +2031,11 @@
                 synchronized (mLock) {
                     long token = Binder.clearCallingIdentity();
                     try {
-                        BlockedNumberContract.BlockedNumbers.endBlockSuppression(mContext);
+                        if (mBlockedNumbersManager != null) {
+                            mBlockedNumbersManager.endBlockSuppression();
+                        } else {
+                            BlockedNumberContract.SystemContract.endBlockSuppression(mContext);
+                        }
                     } finally {
                         Binder.restoreCallingIdentity(token);
                     }
@@ -2093,6 +2124,14 @@
             }
         }
 
+        @Override
+        public int handleShellCommand(@NonNull ParcelFileDescriptor in,
+                @NonNull ParcelFileDescriptor out, @NonNull ParcelFileDescriptor err,
+                @NonNull String[] args) {
+            return new TelecomShellCommand(this, mContext).exec(this,
+                    in.getFileDescriptor(), out.getFileDescriptor(), err.getFileDescriptor(), args);
+        }
+
         /**
          * Print all feature flag configurations that Telecom is using for debugging purposes.
          */
@@ -2102,12 +2141,24 @@
                 // Look away, a forbidden technique (reflection) is being used to allow us to get
                 // all flag configs without having to add them manually to this method.
                 Method[] methods = FeatureFlags.class.getMethods();
+                int maxLength = Arrays.stream(methods)
+                        .map(Method::getName)
+                        .map(String::length)
+                        .max(Integer::compare)
+                        .get();
+                String format = "\t%s: %-" + maxLength + "s %s";
+
                 if (methods.length == 0) {
                     pw.println("NONE");
                     return;
                 }
+
                 for (Method m : methods) {
-                    pw.println(m.getName() + "-> " + m.invoke(mFeatureFlags));
+                    String flagEnabled = (Boolean) m.invoke(mFeatureFlags) ? "[✅]": "[❌]";
+                    String methodName = m.getName();
+                    String camelCaseName = methodName.replaceAll("([a-z])([A-Z]+)", "$1_$2")
+                            .toLowerCase(Locale.US);
+                    pw.println(String.format(format, flagEnabled, methodName, camelCaseName));
                 }
             } catch (Exception e) {
                 pw.println("[ERROR]");
@@ -2289,14 +2340,10 @@
         }
 
         /**
-         * A method intended for use in testing to clean up any calls that get stuck in the
-         * {@link CallState#DISCONNECTED} or {@link CallState#DISCONNECTING} states. Stuck
-         * calls
-         * during CTS cause cascading failures, so if the CTS test detects such a state, it
-         * should
-         * call this method via a shell command to clean up before moving on to the next
-         * test.
-         * Also cleans up any pending futures related to
+         * A method intended for use in testing to clean up any calls are ongoing. Stuck
+         * calls during CTS cause cascading failures, so if the CTS test detects such a state, it
+         * should call this method via a shell command to clean up before moving on to the next
+         * test. Also cleans up any pending futures related to
          * {@link android.telecom.CallDiagnosticService}s.
          */
         @Override
@@ -2309,11 +2356,19 @@
                     try {
                         Set<UserHandle> userHandles = new HashSet<>();
                         for (Call call : mCallsManager.getCalls()) {
-                            call.cleanup();
-                            if (call.getState() == CallState.DISCONNECTED
-                                    || call.getState() == CallState.DISCONNECTING) {
-                                mCallsManager.markCallAsRemoved(call);
+                            // Any call that is not in a disconnect* state should be moved to the
+                            // disconnected state
+                            if (!isDisconnectingOrDisconnected(call)) {
+                                mCallsManager.markCallAsDisconnected(
+                                        call,
+                                        new DisconnectCause(DisconnectCause.OTHER,
+                                                "cleaning up stuck calls"));
                             }
+                            // ensure the call is immediately removed from CallsManager instead of
+                            // using a Future to do the work.
+                            call.cleanup();
+                            // finally, officially remove the call from CallsManager tracking
+                            mCallsManager.markCallAsRemoved(call);
                             userHandles.add(call.getAssociatedUser());
                         }
                         for (UserHandle userHandle : userHandles) {
@@ -2328,6 +2383,11 @@
             }
         }
 
+        private boolean isDisconnectingOrDisconnected(Call call){
+            return call.getState() == CallState.DISCONNECTED
+                    || call.getState() == CallState.DISCONNECTING;
+        }
+
         /**
          * A method intended for test to clean up orphan {@link PhoneAccount}. An orphan
          * {@link PhoneAccount} is a phone account belongs to an invalid {@link UserHandle}
@@ -2564,34 +2624,26 @@
          * @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, boolean detectForAllUsers) {
+                String callingPackage) {
             try {
                 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))) {
+                if (!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.isInSelfManagedCallCrossUsers(
-                                packageName, userHandle, detectForAllUsers);
+                        return mCallsManager.isInSelfManagedCall(
+                                packageName, userHandle);
                     } finally {
                         Binder.restoreCallingIdentity(token);
                     }
@@ -2668,10 +2720,10 @@
     private final TelecomSystem.SyncRoot mLock;
     private TransactionManager mTransactionManager;
     private final TransactionalServiceRepository mTransactionalServiceRepository;
+    private final BlockedNumbersManager mBlockedNumbersManager;
     private final FeatureFlags mFeatureFlags;
     private final com.android.internal.telephony.flags.FeatureFlags mTelephonyFeatureFlags;
 
-
     public TelecomServiceImpl(
             Context context,
             CallsManager callsManager,
@@ -2719,6 +2771,9 @@
 
         mTransactionManager = TransactionManager.getInstance();
         mTransactionalServiceRepository = new TransactionalServiceRepository();
+        mBlockedNumbersManager = mFeatureFlags.telecomMainlineBlockedNumbersManager()
+                ? mContext.getSystemService(BlockedNumbersManager.class)
+                : null;
     }
 
     @VisibleForTesting
diff --git a/src/com/android/server/telecom/TelecomShellCommand.java b/src/com/android/server/telecom/TelecomShellCommand.java
new file mode 100644
index 0000000..557002c
--- /dev/null
+++ b/src/com/android/server/telecom/TelecomShellCommand.java
@@ -0,0 +1,513 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.sysprop.TelephonyProperties;
+import android.telecom.Log;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+
+import com.android.internal.telecom.ITelecomService;
+import com.android.modules.utils.BasicShellCommandHandler;
+
+import java.util.Arrays;
+import java.util.stream.Collectors;
+
+/**
+ * Implements shell commands sent to telecom using the "adb shell cmd telecom..." command from shell
+ * or CTS.
+ */
+public class TelecomShellCommand extends BasicShellCommandHandler {
+    private static final String CALLING_PACKAGE = TelecomShellCommand.class.getPackageName();
+    private static final String COMMAND_SET_PHONE_ACCOUNT_ENABLED = "set-phone-account-enabled";
+    private static final String COMMAND_SET_PHONE_ACCOUNT_DISABLED = "set-phone-account-disabled";
+    private static final String COMMAND_REGISTER_PHONE_ACCOUNT = "register-phone-account";
+    private static final String COMMAND_SET_USER_SELECTED_OUTGOING_PHONE_ACCOUNT =
+            "set-user-selected-outgoing-phone-account";
+    private static final String COMMAND_REGISTER_SIM_PHONE_ACCOUNT = "register-sim-phone-account";
+    private static final String COMMAND_SET_TEST_CALL_REDIRECTION_APP =
+            "set-test-call-redirection-app";
+    private static final String COMMAND_SET_TEST_CALL_SCREENING_APP = "set-test-call-screening-app";
+    private static final String COMMAND_ADD_OR_REMOVE_CALL_COMPANION_APP =
+            "add-or-remove-call-companion-app";
+    private static final String COMMAND_SET_PHONE_ACCOUNT_SUGGESTION_COMPONENT =
+            "set-phone-acct-suggestion-component";
+    private static final String COMMAND_UNREGISTER_PHONE_ACCOUNT = "unregister-phone-account";
+    private static final String COMMAND_SET_CALL_DIAGNOSTIC_SERVICE = "set-call-diagnostic-service";
+    private static final String COMMAND_SET_DEFAULT_DIALER = "set-default-dialer";
+    private static final String COMMAND_GET_DEFAULT_DIALER = "get-default-dialer";
+    private static final String COMMAND_STOP_BLOCK_SUPPRESSION = "stop-block-suppression";
+    private static final String COMMAND_CLEANUP_STUCK_CALLS = "cleanup-stuck-calls";
+    private static final String COMMAND_CLEANUP_ORPHAN_PHONE_ACCOUNTS =
+            "cleanup-orphan-phone-accounts";
+    private static final String COMMAND_RESET_CAR_MODE = "reset-car-mode";
+    private static final String COMMAND_IS_NON_IN_CALL_SERVICE_BOUND =
+            "is-non-ui-in-call-service-bound";
+
+    /**
+     * Change the system dialer package name if a package name was specified,
+     * Example: adb shell telecom set-system-dialer <PACKAGE>
+     *
+     * Restore it to the default if if argument is "default" or no argument is passed.
+     * Example: adb shell telecom set-system-dialer default
+     */
+    private static final String COMMAND_SET_SYSTEM_DIALER = "set-system-dialer";
+    private static final String COMMAND_GET_SYSTEM_DIALER = "get-system-dialer";
+    private static final String COMMAND_WAIT_ON_HANDLERS = "wait-on-handlers";
+    private static final String COMMAND_SET_SIM_COUNT = "set-sim-count";
+    private static final String COMMAND_GET_SIM_CONFIG = "get-sim-config";
+    private static final String COMMAND_GET_MAX_PHONES = "get-max-phones";
+    private static final String COMMAND_SET_TEST_EMERGENCY_PHONE_ACCOUNT_PACKAGE_FILTER =
+            "set-test-emergency-phone-account-package-filter";
+    /**
+     * Command used to emit a distinct "mark" in the logs.
+     */
+    private static final String COMMAND_LOG_MARK = "log-mark";
+
+    private final Context mContext;
+    private final ITelecomService mTelecomService;
+    private TelephonyManager mTelephonyManager;
+    private UserManager mUserManager;
+
+    public TelecomShellCommand(ITelecomService binder, Context context) {
+        mTelecomService = binder;
+        mContext = context;
+    }
+
+    @Override
+    public int onCommand(String command) {
+        if (command == null || command.isEmpty()) {
+            onHelp();
+            return 0;
+        }
+        try {
+            switch (command) {
+                case COMMAND_SET_PHONE_ACCOUNT_ENABLED:
+                    runSetPhoneAccountEnabled(true);
+                    break;
+                case COMMAND_SET_PHONE_ACCOUNT_DISABLED:
+                    runSetPhoneAccountEnabled(false);
+                    break;
+                case COMMAND_REGISTER_PHONE_ACCOUNT:
+                    runRegisterPhoneAccount();
+                    break;
+                case COMMAND_SET_TEST_CALL_REDIRECTION_APP:
+                    runSetTestCallRedirectionApp();
+                    break;
+                case COMMAND_SET_TEST_CALL_SCREENING_APP:
+                    runSetTestCallScreeningApp();
+                    break;
+                case COMMAND_ADD_OR_REMOVE_CALL_COMPANION_APP:
+                    runAddOrRemoveCallCompanionApp();
+                    break;
+                case COMMAND_SET_PHONE_ACCOUNT_SUGGESTION_COMPONENT:
+                    runSetTestPhoneAcctSuggestionComponent();
+                    break;
+                case COMMAND_SET_CALL_DIAGNOSTIC_SERVICE:
+                    runSetCallDiagnosticService();
+                    break;
+                case COMMAND_REGISTER_SIM_PHONE_ACCOUNT:
+                    runRegisterSimPhoneAccount();
+                    break;
+                case COMMAND_SET_USER_SELECTED_OUTGOING_PHONE_ACCOUNT:
+                    runSetUserSelectedOutgoingPhoneAccount();
+                    break;
+                case COMMAND_UNREGISTER_PHONE_ACCOUNT:
+                    runUnregisterPhoneAccount();
+                    break;
+                case COMMAND_STOP_BLOCK_SUPPRESSION:
+                    runStopBlockSuppression();
+                    break;
+                case COMMAND_CLEANUP_STUCK_CALLS:
+                    runCleanupStuckCalls();
+                    break;
+                case COMMAND_CLEANUP_ORPHAN_PHONE_ACCOUNTS:
+                    runCleanupOrphanPhoneAccounts();
+                    break;
+                case COMMAND_RESET_CAR_MODE:
+                    runResetCarMode();
+                    break;
+                case COMMAND_SET_DEFAULT_DIALER:
+                    runSetDefaultDialer();
+                    break;
+                case COMMAND_GET_DEFAULT_DIALER:
+                    runGetDefaultDialer();
+                    break;
+                case COMMAND_SET_SYSTEM_DIALER:
+                    runSetSystemDialer();
+                    break;
+                case COMMAND_GET_SYSTEM_DIALER:
+                    runGetSystemDialer();
+                    break;
+                case COMMAND_WAIT_ON_HANDLERS:
+                    runWaitOnHandler();
+                    break;
+                case COMMAND_SET_SIM_COUNT:
+                    runSetSimCount();
+                    break;
+                case COMMAND_GET_SIM_CONFIG:
+                    runGetSimConfig();
+                    break;
+                case COMMAND_GET_MAX_PHONES:
+                    runGetMaxPhones();
+                    break;
+                case COMMAND_IS_NON_IN_CALL_SERVICE_BOUND:
+                    runIsNonUiInCallServiceBound();
+                    break;
+                case COMMAND_SET_TEST_EMERGENCY_PHONE_ACCOUNT_PACKAGE_FILTER:
+                    runSetEmergencyPhoneAccountPackageFilter();
+                    break;
+                case COMMAND_LOG_MARK:
+                    runLogMark();
+                    break;
+                default:
+                    return handleDefaultCommands(command);
+            }
+        } catch (Exception e) {
+            getErrPrintWriter().println("Command["+ command + "]: Error: " + e);
+            return -1;
+        }
+        return 0;
+    }
+
+    @Override
+    public void onHelp() {
+        getOutPrintWriter().println("usage: telecom [subcommand] [options]\n"
+                + "usage: telecom set-phone-account-enabled <COMPONENT> <ID> <USER_SN>\n"
+                + "usage: telecom set-phone-account-disabled <COMPONENT> <ID> <USER_SN>\n"
+                + "usage: telecom register-phone-account <COMPONENT> <ID> <USER_SN> <LABEL>\n"
+                + "usage: telecom register-sim-phone-account [-e] <COMPONENT> <ID> <USER_SN>"
+                + " <LABEL>: registers a PhoneAccount with CAPABILITY_SIM_SUBSCRIPTION"
+                + " and optionally CAPABILITY_PLACE_EMERGENCY_CALLS if \"-e\" is provided\n"
+                + "usage: telecom set-user-selected-outgoing-phone-account [-e] <COMPONENT> <ID> "
+                + "<USER_SN>\n"
+                + "usage: telecom set-test-call-redirection-app <PACKAGE>\n"
+                + "usage: telecom set-test-call-screening-app <PACKAGE>\n"
+                + "usage: telecom set-phone-acct-suggestion-component <COMPONENT>\n"
+                + "usage: telecom add-or-remove-call-companion-app <PACKAGE> <1/0>\n"
+                + "usage: telecom register-sim-phone-account <COMPONENT> <ID> <USER_SN>"
+                + " <LABEL> <ADDRESS>\n"
+                + "usage: telecom unregister-phone-account <COMPONENT> <ID> <USER_SN>\n"
+                + "usage: telecom set-call-diagnostic-service <PACKAGE>\n"
+                + "usage: telecom set-default-dialer <PACKAGE>\n"
+                + "usage: telecom get-default-dialer\n"
+                + "usage: telecom get-system-dialer\n"
+                + "usage: telecom wait-on-handlers\n"
+                + "usage: telecom set-sim-count <COUNT>\n"
+                + "usage: telecom get-sim-config\n"
+                + "usage: telecom get-max-phones\n"
+                + "usage: telecom stop-block-suppression: Stop suppressing the blocked number"
+                + " provider after a call to emergency services.\n"
+                + "usage: telecom cleanup-stuck-calls: Clear any disconnected calls that have"
+                + " gotten wedged in Telecom.\n"
+                + "usage: telecom cleanup-orphan-phone-accounts: remove any phone accounts that"
+                + " no longer have a valid UserHandle or accounts that no longer belongs to an"
+                + " installed package.\n"
+                + "usage: telecom set-emer-phone-account-filter <PACKAGE>\n"
+                + "\n"
+                + "telecom set-phone-account-enabled: Enables the given phone account, if it has"
+                + " already been registered with Telecom.\n"
+                + "\n"
+                + "telecom set-phone-account-disabled: Disables the given phone account, if it"
+                + " has already been registered with telecom.\n"
+                + "\n"
+                + "telecom set-call-diagnostic-service: overrides call diagnostic service.\n"
+                + "telecom set-default-dialer: Sets the override default dialer to the given"
+                + " component; this will override whatever the dialer role is set to.\n"
+                + "\n"
+                + "telecom get-default-dialer: Displays the current default dialer.\n"
+                + "\n"
+                + "telecom get-system-dialer: Displays the current system dialer.\n"
+                + "telecom set-system-dialer: Set the override system dialer to the given"
+                + " component. To remove the override, send \"default\"\n"
+                + "\n"
+                + "telecom wait-on-handlers: Wait until all handlers finish their work.\n"
+                + "\n"
+                + "telecom set-sim-count: Set num SIMs (2 for DSDS, 1 for single SIM."
+                + " This may restart the device.\n"
+                + "\n"
+                + "telecom get-sim-config: Get the mSIM config string. \"DSDS\" for DSDS mode,"
+                + " or \"\" for single SIM\n"
+                + "\n"
+                + "telecom get-max-phones: Get the max supported phones from the modem.\n"
+                + "telecom set-test-emergency-phone-account-package-filter <PACKAGE>: sets a"
+                + " package name that will be used for test emergency calls. To clear,"
+                + " send an empty package name. Real emergency calls will still be placed"
+                + " over Telephony.\n"
+                + "telecom log-mark <MESSAGE>: emits a message into the telecom logs.  Useful for "
+                + "testers to indicate where in the logs various test steps take place.\n"
+                + "telecom is-non-ui-in-call-service-bound <PACKAGE>: queries a particular "
+                + "non-ui-InCallService in InCallController to determine if it is bound \n"
+        );
+    }
+    private void runSetPhoneAccountEnabled(boolean enabled) throws RemoteException {
+        final PhoneAccountHandle handle = getPhoneAccountHandleFromArgs();
+        final boolean success =  mTelecomService.enablePhoneAccount(handle, enabled);
+        if (success) {
+            getOutPrintWriter().println("Success - " + handle
+                    + (enabled ? " enabled." : " disabled."));
+        } else {
+            getOutPrintWriter().println("Error - is " + handle + " a valid PhoneAccount?");
+        }
+    }
+
+    private void runRegisterPhoneAccount() throws RemoteException {
+        final PhoneAccountHandle handle = getPhoneAccountHandleFromArgs();
+        final String label = getNextArgRequired();
+        PhoneAccount account = PhoneAccount.builder(handle, label)
+                .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER).build();
+        mTelecomService.registerPhoneAccount(account, CALLING_PACKAGE);
+        getOutPrintWriter().println("Success - " + handle + " registered.");
+    }
+
+    private void runRegisterSimPhoneAccount() throws RemoteException {
+        boolean isEmergencyAccount = false;
+        String opt;
+        while ((opt = getNextOption()) != null) {
+            switch (opt) {
+                case "-e": {
+                    isEmergencyAccount = true;
+                    break;
+                }
+            }
+        }
+        final PhoneAccountHandle handle = getPhoneAccountHandleFromArgs();
+        final String label = getNextArgRequired();
+        final String address = getNextArgRequired();
+        int capabilities = PhoneAccount.CAPABILITY_CALL_PROVIDER
+                | PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION
+                | (isEmergencyAccount ? PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS : 0);
+        PhoneAccount account = PhoneAccount.builder(
+                        handle, label)
+                .setAddress(Uri.parse(address))
+                .setSubscriptionAddress(Uri.parse(address))
+                .setCapabilities(capabilities)
+                .setShortDescription(label)
+                .addSupportedUriScheme(PhoneAccount.SCHEME_TEL)
+                .addSupportedUriScheme(PhoneAccount.SCHEME_VOICEMAIL)
+                .build();
+        mTelecomService.registerPhoneAccount(account, CALLING_PACKAGE);
+        getOutPrintWriter().println("Success - " + handle + " registered.");
+    }
+
+    private void runSetTestCallRedirectionApp() throws RemoteException {
+        final String packageName = getNextArg();
+        mTelecomService.setTestDefaultCallRedirectionApp(packageName);
+    }
+
+    private void runSetTestCallScreeningApp() throws RemoteException {
+        final String packageName = getNextArg();
+        mTelecomService.setTestDefaultCallScreeningApp(packageName);
+    }
+
+    private void runAddOrRemoveCallCompanionApp() throws RemoteException {
+        final String packageName = getNextArgRequired();
+        String isAdded = getNextArgRequired();
+        boolean isAddedBool = "1".equals(isAdded);
+        mTelecomService.addOrRemoveTestCallCompanionApp(packageName, isAddedBool);
+    }
+
+    private void runSetCallDiagnosticService() throws RemoteException {
+        String packageName = getNextArg();
+        if ("default".equals(packageName)) packageName = null;
+        mTelecomService.setTestCallDiagnosticService(packageName);
+        getOutPrintWriter().println("Success - " + packageName
+                + " set as call diagnostic service.");
+    }
+
+    private void runSetTestPhoneAcctSuggestionComponent() throws RemoteException {
+        final String componentName = getNextArg();
+        mTelecomService.setTestPhoneAcctSuggestionComponent(componentName);
+    }
+
+    private void runSetUserSelectedOutgoingPhoneAccount() throws RemoteException {
+        Log.i(this, "runSetUserSelectedOutgoingPhoneAccount");
+        final PhoneAccountHandle handle = getPhoneAccountHandleFromArgs();
+        mTelecomService.setUserSelectedOutgoingPhoneAccount(handle);
+        getOutPrintWriter().println("Success - " + handle + " set as default outgoing account.");
+    }
+
+    private void runUnregisterPhoneAccount() throws RemoteException {
+        final PhoneAccountHandle handle = getPhoneAccountHandleFromArgs();
+        mTelecomService.unregisterPhoneAccount(handle, CALLING_PACKAGE);
+        getOutPrintWriter().println("Success - " + handle + " unregistered.");
+    }
+
+    private void runStopBlockSuppression() throws RemoteException {
+        mTelecomService.stopBlockSuppression();
+    }
+
+    private void runCleanupStuckCalls() throws RemoteException {
+        mTelecomService.cleanupStuckCalls();
+    }
+
+    private void runCleanupOrphanPhoneAccounts() throws RemoteException {
+        getOutPrintWriter().println("Success - cleaned up "
+                + mTelecomService.cleanupOrphanPhoneAccounts()
+                + "  phone accounts.");
+    }
+
+    private void runResetCarMode() throws RemoteException {
+        mTelecomService.resetCarMode();
+    }
+
+    private void runSetDefaultDialer() throws RemoteException {
+        String packageName = getNextArg();
+        if ("default".equals(packageName)) packageName = null;
+        mTelecomService.setTestDefaultDialer(packageName);
+        getOutPrintWriter().println("Success - " + packageName
+                + " set as override default dialer.");
+    }
+
+    private void runSetSystemDialer() throws RemoteException {
+        final String flatComponentName = getNextArg();
+        final ComponentName componentName = (flatComponentName.equals("default")
+                ? null : parseComponentName(flatComponentName));
+        mTelecomService.setSystemDialer(componentName);
+        getOutPrintWriter().println("Success - " + componentName + " set as override system dialer.");
+    }
+
+    private void runGetDefaultDialer() throws RemoteException {
+        getOutPrintWriter().println(mTelecomService.getDefaultDialerPackage(CALLING_PACKAGE));
+    }
+
+    private void runGetSystemDialer() throws RemoteException {
+        getOutPrintWriter().println(mTelecomService.getSystemDialerPackage(CALLING_PACKAGE));
+    }
+
+    private void runWaitOnHandler() throws RemoteException {
+
+    }
+
+    private void runSetSimCount() throws RemoteException {
+        if (!callerIsRoot()) {
+            getOutPrintWriter().println("set-sim-count requires adb root");
+            return;
+        }
+        int numSims = Integer.parseInt(getNextArgRequired());
+        getOutPrintWriter().println("Setting sim count to " + numSims + ". Device may reboot");
+        getTelephonyManager().switchMultiSimConfig(numSims);
+    }
+
+    /**
+     * prints out whether a particular non-ui InCallServices is bound in a call
+     */
+    public void runIsNonUiInCallServiceBound() throws RemoteException {
+        if (TextUtils.isEmpty(peekNextArg())) {
+            getOutPrintWriter().println("No Argument passed. Please pass a <PACKAGE_NAME> to "
+                    + "lookup.");
+        } else {
+            getOutPrintWriter().println(
+                    String.valueOf(mTelecomService.isNonUiInCallServiceBound(getNextArg())));
+        }
+    }
+
+    /**
+     * Prints the mSIM config to the console.
+     * "DSDS" for a phone in DSDS mode
+     * "" (empty string) for a phone in SS mode
+     */
+    private void runGetSimConfig() throws RemoteException {
+        getOutPrintWriter().println(TelephonyProperties.multi_sim_config().orElse(""));
+    }
+
+    private void runGetMaxPhones() throws RemoteException {
+        // how many logical modems can be potentially active simultaneously
+        getOutPrintWriter().println(getTelephonyManager().getSupportedModemCount());
+    }
+
+    private void runSetEmergencyPhoneAccountPackageFilter() throws RemoteException {
+        String packageName = getNextArg();
+        if (TextUtils.isEmpty(packageName)) {
+            mTelecomService.setTestEmergencyPhoneAccountPackageNameFilter(null);
+            getOutPrintWriter().println("Success - filter cleared");
+        } else {
+            mTelecomService.setTestEmergencyPhoneAccountPackageNameFilter(packageName);
+            getOutPrintWriter().println("Success = filter set to " + packageName);
+        }
+
+    }
+
+    private void runLogMark() throws RemoteException {
+        String message = Arrays.stream(peekRemainingArgs()).collect(Collectors.joining(" "));
+        mTelecomService.requestLogMark(message);
+    }
+
+    private PhoneAccountHandle getPhoneAccountHandleFromArgs() throws RemoteException {
+        if (TextUtils.isEmpty(peekNextArg())) {
+            return null;
+        }
+        final ComponentName component = parseComponentName(getNextArgRequired());
+        final String accountId = getNextArgRequired();
+        final String userSnInStr = getNextArgRequired();
+        UserHandle userHandle;
+        try {
+            final int userSn = Integer.parseInt(userSnInStr);
+            userHandle = UserHandle.of(getUserManager().getUserHandle(userSn));
+        } catch (NumberFormatException ex) {
+            Log.w(this, "getPhoneAccountHandleFromArgs - invalid user %s", userSnInStr);
+            throw new IllegalArgumentException ("Invalid user serial number " + userSnInStr);
+        }
+        return new PhoneAccountHandle(component, accountId, userHandle);
+    }
+
+    private boolean callerIsRoot() {
+        return Process.ROOT_UID == Process.myUid();
+    }
+
+    private ComponentName parseComponentName(String component) {
+        ComponentName cn = ComponentName.unflattenFromString(component);
+        if (cn == null) {
+            throw new IllegalArgumentException ("Invalid component " + component);
+        }
+        return cn;
+    }
+
+    private TelephonyManager getTelephonyManager() throws IllegalStateException {
+        if (mTelephonyManager == null) {
+            mTelephonyManager = mContext.getSystemService(TelephonyManager.class);
+        }
+        if (mTelephonyManager == null) {
+            Log.w(this, "getTelephonyManager: Can't access telephony service.");
+            throw new IllegalStateException("Could not access the Telephony Service. Is the system "
+                    + "running?");
+        }
+        return mTelephonyManager;
+    }
+
+    private UserManager getUserManager() throws IllegalStateException {
+        if (mUserManager == null) {
+            mUserManager = mContext.getSystemService(UserManager.class);
+        }
+        if (mUserManager == null) {
+            Log.w(this, "getUserManager: Can't access UserManager service.");
+            throw new IllegalStateException("Could not access the UserManager Service. Is the "
+                    + "system running?");
+        }
+        return mUserManager;
+    }
+}
diff --git a/src/com/android/server/telecom/TelecomSystem.java b/src/com/android/server/telecom/TelecomSystem.java
index 1d0300d..d7dcf38 100644
--- a/src/com/android/server/telecom/TelecomSystem.java
+++ b/src/com/android/server/telecom/TelecomSystem.java
@@ -29,7 +29,6 @@
 import android.os.BugreportManager;
 import android.os.DropBoxManager;
 import android.os.UserHandle;
-import android.os.UserManager;
 import android.telecom.Log;
 import android.telecom.PhoneAccountHandle;
 import android.telephony.AnomalyReporter;
@@ -45,9 +44,7 @@
 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.CallFilterResultCallback;
 import com.android.server.telecom.callfiltering.IncomingCallFilterGraph;
-import com.android.server.telecom.callfiltering.IncomingCallFilterGraphProvider;
 import com.android.server.telecom.components.UserCallIntentProcessor;
 import com.android.server.telecom.components.UserCallIntentProcessorFactory;
 import com.android.server.telecom.flags.FeatureFlags;
@@ -248,7 +245,7 @@
         try {
             mPhoneAccountRegistrar = new PhoneAccountRegistrar(mContext, mLock, defaultDialerCache,
                     packageName -> AppLabelProxy.Util.getAppLabel(
-                            mContext.getPackageManager(), packageName), null);
+                            mContext.getPackageManager(), packageName), null, mFeatureFlags);
 
             mContactsAsyncHelper = contactsAsyncHelperFactory.create(
                     new ContactsAsyncHelper.ContentResolverAdapter() {
@@ -307,7 +304,7 @@
                 @Override
                 public CallEndpointController create(Context context, SyncRoot lock,
                         CallsManager callsManager) {
-                    return new CallEndpointController(context, callsManager);
+                    return new CallEndpointController(context, callsManager, featureFlags);
                 }
             };
 
@@ -349,22 +346,23 @@
 
             ToastFactory toastFactory = new ToastFactory() {
                 @Override
-                public Toast makeText(Context context, int resId, int duration) {
+                public void makeText(Context context, int resId, int duration) {
                     if (mFeatureFlags.telecomResolveHiddenDependencies()) {
-                        return Toast.makeText(context, resId, duration);
+                        context.getMainExecutor().execute(() ->
+                                Toast.makeText(context, resId, duration).show());
                     } else {
-                        return Toast.makeText(context, context.getMainLooper(),
-                                context.getString(resId),
-                                duration);
+                        Toast.makeText(context, context.getMainLooper(),
+                                context.getString(resId), duration).show();
                     }
                 }
 
                 @Override
-                public Toast makeText(Context context, CharSequence text, int duration) {
+                public void makeText(Context context, CharSequence text, int duration) {
                     if (mFeatureFlags.telecomResolveHiddenDependencies()) {
-                        return Toast.makeText(context, text, duration);
+                        context.getMainExecutor().execute(() ->
+                                Toast.makeText(context, text, duration).show());
                     } else {
-                        return Toast.makeText(context, context.getMainLooper(), text, duration);
+                        Toast.makeText(context, context.getMainLooper(), text, duration).show();
                     }
                 }
             };
@@ -483,7 +481,6 @@
                     Manifest.permission.CONTROL_INCALL_EXPERIENCE, null);
 
             // There is no USER_SWITCHED broadcast for user 0, handle it here explicitly.
-            final UserManager userManager = UserManager.get(mContext);
             mTelecomServiceImpl = new TelecomServiceImpl(
                     mContext, mCallsManager, mPhoneAccountRegistrar,
                     new CallIntentProcessor.AdapterImpl(defaultDialerCache),
@@ -491,7 +488,7 @@
                         @Override
                         public UserCallIntentProcessor create(Context context,
                                 UserHandle userHandle) {
-                            return new UserCallIntentProcessor(context, userHandle);
+                            return new UserCallIntentProcessor(context, userHandle, featureFlags);
                         }
                     },
                     defaultDialerCache,
@@ -534,4 +531,8 @@
     public boolean isBootComplete() {
         return mIsBootComplete;
     }
+
+    public FeatureFlags getFeatureFlags() {
+        return mFeatureFlags;
+    }
 }
diff --git a/src/com/android/server/telecom/TelephonyUtil.java b/src/com/android/server/telecom/TelephonyUtil.java
index 7eb08d7..0a7dc19 100644
--- a/src/com/android/server/telecom/TelephonyUtil.java
+++ b/src/com/android/server/telecom/TelephonyUtil.java
@@ -74,7 +74,7 @@
             TelephonyManager tm = (TelephonyManager) context.getSystemService(
                     Context.TELEPHONY_SERVICE);
             return handle != null && tm.isEmergencyNumber(handle.getSchemeSpecificPart());
-        } catch (IllegalStateException ise) {
+        } catch (UnsupportedOperationException | IllegalStateException ignored) {
             return false;
         }
     }
diff --git a/src/com/android/server/telecom/Timeouts.java b/src/com/android/server/telecom/Timeouts.java
index abc7ff6..0ed71df 100644
--- a/src/com/android/server/telecom/Timeouts.java
+++ b/src/com/android/server/telecom/Timeouts.java
@@ -61,6 +61,14 @@
             return Timeouts.getEmergencyCallbackWindowMillis(cr);
         }
 
+        public long getEmergencyCallTimeoutMillis(ContentResolver cr) {
+            return Timeouts.getEmergencyCallTimeoutMillis(cr);
+        }
+
+        public long getEmergencyCallTimeoutRadioOffMillis(ContentResolver cr) {
+            return Timeouts.getEmergencyCallTimeoutRadioOffMillis(cr);
+        }
+
         public long getUserDefinedCallRedirectionTimeoutMillis(ContentResolver cr) {
             return Timeouts.getUserDefinedCallRedirectionTimeoutMillis(cr);
         }
@@ -127,7 +135,6 @@
 
         public int getDaysBackToSearchEmergencyDiagnosticEntries(){
             return Timeouts.getDaysBackToSearchEmergencyDiagnosticEntries();
-
         }
     }
 
diff --git a/src/com/android/server/telecom/TransactionalServiceWrapper.java b/src/com/android/server/telecom/TransactionalServiceWrapper.java
index d497c6a..50ef2e8 100644
--- a/src/com/android/server/telecom/TransactionalServiceWrapper.java
+++ b/src/com/android/server/telecom/TransactionalServiceWrapper.java
@@ -21,6 +21,7 @@
 import static android.telecom.TelecomManager.TELECOM_TRANSACTION_SUCCESS;
 
 import android.content.ComponentName;
+import android.os.Binder;
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.OutcomeReceiver;
@@ -62,7 +63,7 @@
  * on a per-client basis which is tied to a {@link PhoneAccountHandle}
  */
 public class TransactionalServiceWrapper implements
-        ConnectionServiceFocusManager.ConnectionServiceFocus {
+        ConnectionServiceFocusManager.ConnectionServiceFocus, CallSourceService {
     private static final String TAG = TransactionalServiceWrapper.class.getSimpleName();
 
     // CallControl : Client (ex. voip app) --> Telecom
@@ -185,10 +186,12 @@
         @Override
         public void setActive(String callId, android.os.ResultReceiver callback)
                 throws RemoteException {
+            long token = Binder.clearCallingIdentity();
             try {
                 Log.startSession("TSW.sA");
                 createTransactions(callId, callback, SET_ACTIVE);
             } finally {
+                Binder.restoreCallingIdentity(token);
                 Log.endSession();
             }
         }
@@ -196,10 +199,12 @@
         @Override
         public void answer(int videoState, String callId, android.os.ResultReceiver callback)
                 throws RemoteException {
+            long token = Binder.clearCallingIdentity();
             try {
                 Log.startSession("TSW.a");
                 createTransactions(callId, callback, ANSWER, videoState);
             } finally {
+                Binder.restoreCallingIdentity(token);
                 Log.endSession();
             }
         }
@@ -207,10 +212,12 @@
         @Override
         public void setInactive(String callId, android.os.ResultReceiver callback)
                 throws RemoteException {
+            long token = Binder.clearCallingIdentity();
             try {
                 Log.startSession("TSW.sI");
                 createTransactions(callId, callback, SET_INACTIVE);
             } finally {
+                Binder.restoreCallingIdentity(token);
                 Log.endSession();
             }
         }
@@ -219,10 +226,12 @@
         public void disconnect(String callId, DisconnectCause disconnectCause,
                 android.os.ResultReceiver callback)
                 throws RemoteException {
+            long token = Binder.clearCallingIdentity();
             try {
                 Log.startSession("TSW.d");
                 createTransactions(callId, callback, DISCONNECT, disconnectCause);
             } finally {
+                Binder.restoreCallingIdentity(token);
                 Log.endSession();
             }
         }
@@ -230,11 +239,13 @@
         @Override
         public void setMuteState(boolean isMuted, android.os.ResultReceiver callback)
                 throws RemoteException {
+            long token = Binder.clearCallingIdentity();
             try {
                 Log.startSession("TSW.sMS");
                 addTransactionsToManager(
                         new SetMuteStateTransaction(mCallsManager, isMuted), callback);
             } finally {
+                Binder.restoreCallingIdentity(token);
                 Log.endSession();
             }
         }
@@ -242,10 +253,12 @@
         @Override
         public void startCallStreaming(String callId, android.os.ResultReceiver callback)
                 throws RemoteException {
+            long token = Binder.clearCallingIdentity();
             try {
                 Log.startSession("TSW.sCS");
                 createTransactions(callId, callback, START_STREAMING);
             } finally {
+                Binder.restoreCallingIdentity(token);
                 Log.endSession();
             }
         }
@@ -253,10 +266,12 @@
         @Override
         public void requestVideoState(int videoState, String callId, ResultReceiver callback)
                 throws RemoteException {
+            long token = Binder.clearCallingIdentity();
             try {
                 Log.startSession("TSW.rVS");
                 createTransactions(callId, callback, REQUEST_VIDEO_STATE, videoState);
             } finally {
+                Binder.restoreCallingIdentity(token);
                 Log.endSession();
             }
         }
@@ -310,7 +325,8 @@
         // This request is originating from the VoIP application.
         private void handleCallControlNewCallFocusTransactions(Call call, String action,
                 boolean isAnswer, int potentiallyNewVideoState, ResultReceiver callback) {
-            mTransactionManager.addTransaction(createSetActiveTransactions(call),
+            mTransactionManager.addTransaction(
+                    createSetActiveTransactions(call, true /* isCallControlRequest */),
                     new OutcomeReceiver<>() {
                         @Override
                         public void onResult(VoipCallTransactionResult result) {
@@ -334,11 +350,13 @@
 
         @Override
         public void requestCallEndpointChange(CallEndpoint endpoint, ResultReceiver callback) {
+            long token = Binder.clearCallingIdentity();
             try {
                 Log.startSession("TSW.rCEC");
                 addTransactionsToManager(new EndpointChangeTransaction(endpoint, mCallsManager),
                         callback);
             } finally {
+                Binder.restoreCallingIdentity(token);
                 Log.endSession();
             }
         }
@@ -348,6 +366,7 @@
          */
         @Override
         public void sendEvent(String callId, String event, Bundle extras) {
+            long token = Binder.clearCallingIdentity();
             try {
                 Log.startSession("TSW.sE");
                 Call call = mTrackedCalls.get(callId);
@@ -359,6 +378,7 @@
                                     + "found. Most likely the call has been disconnected");
                 }
             } finally {
+                Binder.restoreCallingIdentity(token);
                 Log.endSession();
             }
         }
@@ -426,7 +446,8 @@
         Call foregroundCallBeforeSwap = mCallsManager.getForegroundCall();
         boolean wasActive = foregroundCallBeforeSwap != null && foregroundCallBeforeSwap.isActive();
 
-        SerialTransaction serialTransactions = createSetActiveTransactions(call);
+        SerialTransaction serialTransactions = createSetActiveTransactions(call,
+                false /* isCallControlRequest */);
         // 3. get ack from client (that the requested call can go active)
         if (isAnswerRequest) {
             serialTransactions.appendTransaction(
@@ -552,6 +573,7 @@
         }
     }
 
+    @Override
     public void onCallEndpointChanged(Call call, CallEndpoint endpoint) {
         if (call != null) {
             try {
@@ -561,6 +583,7 @@
         }
     }
 
+    @Override
     public void onAvailableCallEndpointsChanged(Call call, Set<CallEndpoint> endpoints) {
         if (call != null) {
             try {
@@ -571,6 +594,7 @@
         }
     }
 
+    @Override
     public void onMuteStateChanged(Call call, boolean isMuted) {
         if (call != null) {
             try {
@@ -580,6 +604,7 @@
         }
     }
 
+    @Override
     public void onVideoStateChanged(Call call, int videoState) {
         if (call != null) {
             try {
@@ -631,12 +656,13 @@
         mCallsManager.removeCall(call);
     }
 
-    private SerialTransaction createSetActiveTransactions(Call call) {
+    private SerialTransaction createSetActiveTransactions(Call call, boolean isCallControlRequest) {
         // create list for multiple transactions
         List<VoipCallTransaction> transactions = new ArrayList<>();
 
         // potentially hold the current active call in order to set a new call (active/answered)
-        transactions.add(new MaybeHoldCallForNewCallTransaction(mCallsManager, call));
+        transactions.add(
+                new MaybeHoldCallForNewCallTransaction(mCallsManager, call, isCallControlRequest));
         // And request a new focus call update
         transactions.add(new RequestNewActiveCallTransaction(mCallsManager, call));
 
diff --git a/src/com/android/server/telecom/UserUtil.java b/src/com/android/server/telecom/UserUtil.java
index 670ad34..57906d4 100644
--- a/src/com/android/server/telecom/UserUtil.java
+++ b/src/com/android/server/telecom/UserUtil.java
@@ -36,18 +36,35 @@
     }
 
     private static UserInfo getUserInfoFromUserHandle(Context context, UserHandle userHandle) {
-        UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
+        UserManager userManager = context.getSystemService(UserManager.class);
         return userManager.getUserInfo(userHandle.getIdentifier());
     }
 
-    public static boolean isManagedProfile(Context context, UserHandle userHandle) {
+    public static boolean isManagedProfile(Context context, UserHandle userHandle,
+            FeatureFlags featureFlags) {
+        UserManager userManager = context.createContextAsUser(userHandle, 0)
+                .getSystemService(UserManager.class);
         UserInfo userInfo = getUserInfoFromUserHandle(context, userHandle);
-        return userInfo != null && userInfo.isManagedProfile();
+        return featureFlags.telecomResolveHiddenDependencies()
+                ? userManager != null && userManager.isManagedProfile()
+                : userInfo != null && userInfo.isManagedProfile();
     }
 
-    public static boolean isProfile(Context context, UserHandle userHandle) {
+    public static boolean isPrivateProfile(UserHandle userHandle, Context context) {
+        UserManager um = context.createContextAsUser(userHandle, 0).getSystemService(
+                UserManager.class);
+        return um != null && um.isPrivateProfile();
+    }
+
+    public static boolean isProfile(Context context, UserHandle userHandle,
+            FeatureFlags featureFlags) {
+        UserManager userManager = context.createContextAsUser(userHandle, 0)
+                .getSystemService(UserManager.class);
         UserInfo userInfo = getUserInfoFromUserHandle(context, userHandle);
-        return userInfo != null && userInfo.profileGroupId != userInfo.id;
+        return featureFlags.telecomResolveHiddenDependencies()
+                ? userManager != null && userManager.isProfile()
+                : userInfo != null && userInfo.profileGroupId != userInfo.id
+                        && userInfo.profileGroupId != UserInfo.NO_PROFILE_GROUP_ID;
     }
 
     public static void showErrorDialogForRestrictedOutgoingCall(Context context,
@@ -61,7 +78,8 @@
     }
 
     public static boolean hasOutgoingCallsUserRestriction(Context context,
-            UserHandle userHandle, Uri handle, boolean isSelfManaged, String tag) {
+            UserHandle userHandle, Uri handle, boolean isSelfManaged, String tag,
+            FeatureFlags featureFlags) {
         // Set handle for conference calls. Refer to {@link Connection#ADHOC_CONFERENCE_ADDRESS}.
         if (handle == null) {
             handle = Uri.parse("tel:conf-factory");
@@ -71,20 +89,23 @@
             // Check DISALLOW_OUTGOING_CALLS restriction. Note: We are skipping this
             // check in a managed profile user because this check can always be bypassed
             // by copying and pasting the phone number into the personal dialer.
-            if (!UserUtil.isManagedProfile(context, userHandle)) {
+            if (!UserUtil.isManagedProfile(context, userHandle, featureFlags)) {
+                final UserManager userManager = context.getSystemService(UserManager.class);
+                boolean hasUserRestriction = featureFlags.telecomResolveHiddenDependencies()
+                        ? userManager.hasUserRestrictionForUser(
+                                UserManager.DISALLOW_OUTGOING_CALLS, userHandle)
+                        : userManager.hasUserRestriction(
+                                UserManager.DISALLOW_OUTGOING_CALLS, userHandle);
                 // Only emergency calls are allowed for users with the DISALLOW_OUTGOING_CALLS
                 // restriction.
                 if (!TelephonyUtil.shouldProcessAsEmergency(context, handle)) {
-                    final UserManager userManager =
-                            (UserManager) context.getSystemService(Context.USER_SERVICE);
                     if (userManager.hasBaseUserRestriction(UserManager.DISALLOW_OUTGOING_CALLS,
                             userHandle)) {
                         String reason = "of DISALLOW_OUTGOING_CALLS restriction";
                         showErrorDialogForRestrictedOutgoingCall(context,
                                 R.string.outgoing_call_not_allowed_user_restriction, tag, reason);
                         return true;
-                    } else if (userManager.hasUserRestriction(UserManager.DISALLOW_OUTGOING_CALLS,
-                            userHandle)) {
+                    } else if (hasUserRestriction) {
                         final DevicePolicyManager dpm =
                                 context.getSystemService(DevicePolicyManager.class);
                         if (dpm == null) {
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
index a0ffe63..3c97d41 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
@@ -16,6 +16,12 @@
 
 package com.android.server.telecom.bluetooth;
 
+import static com.android.server.telecom.AudioRoute.TYPE_BLUETOOTH_HA;
+import static com.android.server.telecom.AudioRoute.TYPE_BLUETOOTH_SCO;
+import static com.android.server.telecom.CallAudioRouteAdapter.BT_DEVICE_REMOVED;
+import static com.android.server.telecom.CallAudioRouteAdapter.SWITCH_BASELINE_ROUTE;
+import static com.android.server.telecom.CallAudioRouteController.INCLUDE_BLUETOOTH_IN_BASELINE;
+
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothHeadset;
@@ -31,19 +37,25 @@
 import android.telecom.Log;
 import android.util.ArraySet;
 import android.util.LocalLog;
+import android.util.Pair;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.telecom.AudioRoute;
 import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
+import com.android.server.telecom.CallAudioRouteAdapter;
+import com.android.server.telecom.CallAudioRouteController;
 import com.android.server.telecom.flags.FeatureFlags;
 
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
@@ -58,6 +70,16 @@
     public static final int DEVICE_TYPE_HEARING_AID = 1;
     public static final int DEVICE_TYPE_LE_AUDIO = 2;
 
+    private static final Map<Integer, Integer> PROFILE_TO_AUDIO_ROUTE_MAP = new HashMap<>();
+    static {
+        PROFILE_TO_AUDIO_ROUTE_MAP.put(BluetoothProfile.HEADSET,
+                AudioRoute.TYPE_BLUETOOTH_SCO);
+        PROFILE_TO_AUDIO_ROUTE_MAP.put(BluetoothProfile.LE_AUDIO,
+                AudioRoute.TYPE_BLUETOOTH_LE);
+        PROFILE_TO_AUDIO_ROUTE_MAP.put(BluetoothProfile.HEARING_AID,
+                TYPE_BLUETOOTH_HA);
+    }
+
     private BluetoothLeAudio.Callback mLeAudioCallbacks =
         new BluetoothLeAudio.Callback() {
             @Override
@@ -112,15 +134,14 @@
                                         + mBluetoothHearingAid;
                             } else if (profile == BluetoothProfile.LE_AUDIO) {
                                 mBluetoothLeAudioService = (BluetoothLeAudio) proxy;
-                                logString = "Got BluetoothLeAudio: "
-                                        + mBluetoothLeAudioService;
+                                logString = ("Got BluetoothLeAudio: " + mBluetoothLeAudioService )
+                                        + (", mLeAudioCallbackRegistered: "
+                                        + mLeAudioCallbackRegistered);
                                 if (!mLeAudioCallbackRegistered) {
-                                    try {
-                                        mBluetoothLeAudioService.registerCallback(
-                                                    mExecutor, mLeAudioCallbacks);
-                                        mLeAudioCallbackRegistered = true;
-                                    } catch (IllegalStateException e) {
-                                        logString += ", but Bluetooth is down";
+                                    if (mFeatureFlags.postponeRegisterToLeaudio()) {
+                                        mExecutor.execute(this::registerToLeAudio);
+                                    } else {
+                                        registerToLeAudio();
                                     }
                                 }
                             } else {
@@ -135,6 +156,29 @@
                     }
                 }
 
+                private void registerToLeAudio() {
+                    synchronized (mLock) {
+                        String logString = "Register to leAudio";
+
+                        if (mLeAudioCallbackRegistered) {
+                            logString +=  ", but already registered";
+                            Log.i(BluetoothDeviceManager.this, logString);
+                            mLocalLog.log(logString);
+                            return;
+                        }
+                        try {
+                            mLeAudioCallbackRegistered = true;
+                            mBluetoothLeAudioService.registerCallback(
+                                            mExecutor, mLeAudioCallbacks);
+                        } catch (IllegalStateException e) {
+                            mLeAudioCallbackRegistered = false;
+                            logString += ", but failed: " + e;
+                        }
+                        Log.i(BluetoothDeviceManager.this, logString);
+                        mLocalLog.log(logString);
+                    }
+                }
+
                 @Override
                 public void onServiceDisconnected(int profile) {
                     Log.startSession("BPSL.oSD");
@@ -172,11 +216,15 @@
                             Log.i(BluetoothDeviceManager.this, logString);
                             mLocalLog.log(logString);
 
-                            List<BluetoothDevice> devicesToRemove = new LinkedList<>(
-                                    lostServiceDevices.values());
-                            lostServiceDevices.clear();
-                            for (BluetoothDevice device : devicesToRemove) {
-                                mBluetoothRouteManager.onDeviceLost(device.getAddress());
+                            if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+                                handleAudioRefactoringServiceDisconnected(profile);
+                            } else {
+                                List<BluetoothDevice> devicesToRemove = new LinkedList<>(
+                                        lostServiceDevices.values());
+                                lostServiceDevices.clear();
+                                for (BluetoothDevice device : devicesToRemove) {
+                                    mBluetoothRouteManager.onDeviceLost(device.getAddress());
+                                }
                             }
                         }
                     } finally {
@@ -185,6 +233,34 @@
                 }
            };
 
+    private void handleAudioRefactoringServiceDisconnected(int profile) {
+        CallAudioRouteController controller = (CallAudioRouteController)
+                mCallAudioRouteAdapter;
+        Map<AudioRoute, BluetoothDevice> btRoutes = controller
+                .getBluetoothRoutes();
+        List<Pair<AudioRoute, BluetoothDevice>> btRoutesToRemove =
+                new ArrayList<>();
+        for (AudioRoute route: btRoutes.keySet()) {
+            if (route.getType() != PROFILE_TO_AUDIO_ROUTE_MAP.get(profile)) {
+                continue;
+            }
+            BluetoothDevice device = btRoutes.get(route);
+            // Prevent concurrent modification exception by just iterating through keys instead of
+            // simultaneously removing them.
+            btRoutesToRemove.add(new Pair<>(route, device));
+        }
+
+        for (Pair<AudioRoute, BluetoothDevice> routeToRemove:
+                btRoutesToRemove) {
+            AudioRoute route = routeToRemove.first;
+            BluetoothDevice device = routeToRemove.second;
+            mCallAudioRouteAdapter.sendMessageWithSessionInfo(
+                    BT_DEVICE_REMOVED, route.getType(), device);
+        }
+        mCallAudioRouteAdapter.sendMessageWithSessionInfo(
+                SWITCH_BASELINE_ROUTE, INCLUDE_BLUETOOTH_IN_BASELINE, (String) null);
+    }
+
     private final LinkedHashMap<String, BluetoothDevice> mHfpDevicesByAddress =
             new LinkedHashMap<>();
     private final LinkedHashMap<String, BluetoothDevice> mHearingAidDevicesByAddress =
@@ -223,6 +299,7 @@
     private AudioManager mAudioManager;
     private Executor mExecutor;
     private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
+    private CallAudioRouteAdapter mCallAudioRouteAdapter;
     private FeatureFlags mFeatureFlags;
 
     public BluetoothDeviceManager(Context context, BluetoothAdapter bluetoothAdapter,
@@ -489,12 +566,14 @@
         }
     }
 
-    public void disconnectSco() {
+    public int disconnectSco() {
+        int result = BluetoothStatusCodes.ERROR_UNKNOWN;
         if (getBluetoothHeadset() == null) {
             Log.w(this, "Trying to disconnect audio but no headset service exists.");
         } else {
-            mBluetoothHeadset.disconnectAudio();
+            result = mBluetoothHeadset.disconnectAudio();
         }
+        return result;
     }
 
     public boolean isLeAudioCommunicationDevice() {
@@ -651,6 +730,28 @@
         return result;
     }
 
+    public boolean setCommunicationDeviceForAddress(String address) {
+        AudioDeviceInfo deviceInfo = null;
+        List<AudioDeviceInfo> devices = mAudioManager.getAvailableCommunicationDevices();
+        if (devices.size() == 0) {
+            Log.w(this, " No communication devices available.");
+            return false;
+        }
+
+        for (AudioDeviceInfo device : devices) {
+            Log.i(this, " Available device type:  " + device.getType());
+            if (device.getAddress().equals(address)) {
+                deviceInfo = device;
+                break;
+            }
+        }
+
+        if (!mAudioManager.getCommunicationDevice().equals(deviceInfo)) {
+            return mAudioManager.setCommunicationDevice(deviceInfo);
+        }
+        return true;
+    }
+
     // Connect audio to the bluetooth device at address, checking to see whether it's
     // le audio, hearing aid or a HFP device, and using the proper BT API.
     public boolean connectAudio(String address, boolean switchingBtDevices) {
@@ -747,6 +848,54 @@
         }
     }
 
+    /**
+     * Used by CallAudioRouteController in order to connect the BT device.
+     * @param device {@link BluetoothDevice} to connect to.
+     * @param type {@link AudioRoute.AudioRouteType} associated with the device.
+     * @return {@code true} if device was successfully connected, {@code false} otherwise.
+     */
+    public boolean connectAudio(BluetoothDevice device, @AudioRoute.AudioRouteType int type) {
+        String address = device.getAddress();
+        int callProfile = BluetoothProfile.LE_AUDIO;
+        if (type == TYPE_BLUETOOTH_SCO) {
+            callProfile = BluetoothProfile.HEADSET;
+        } else if (type == TYPE_BLUETOOTH_HA) {
+            callProfile = BluetoothProfile.HEARING_AID;
+        }
+
+        Bundle preferredAudioProfiles = mBluetoothAdapter.getPreferredAudioProfiles(device);
+        if (preferredAudioProfiles != null && !preferredAudioProfiles.isEmpty()
+                && preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX) != 0) {
+            Log.i(this, "Preferred duplex profile for device=" + address + " is "
+                    + preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX));
+            callProfile = preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX);
+        }
+
+        if (callProfile == BluetoothProfile.LE_AUDIO
+                || callProfile == BluetoothProfile.HEARING_AID) {
+            return mBluetoothAdapter.setActiveDevice(device, BluetoothAdapter.ACTIVE_DEVICE_ALL);
+        } else if (callProfile == BluetoothProfile.HEADSET) {
+            boolean success = mBluetoothAdapter.setActiveDevice(device,
+                    BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL);
+            if (!success) {
+                Log.w(this, "Couldn't set active device to %s", address);
+                return false;
+            }
+            if (getBluetoothHeadset() != null) {
+                int scoConnectionRequest = mBluetoothHeadset.connectAudio();
+                return scoConnectionRequest == BluetoothStatusCodes.SUCCESS ||
+                        scoConnectionRequest
+                                == BluetoothStatusCodes.ERROR_AUDIO_DEVICE_ALREADY_CONNECTED;
+            } else {
+                Log.w(this, "Couldn't find bluetooth headset service");
+                return false;
+            }
+        } else {
+            Log.w(this, "Attempting to turn on audio for a disconnected device");
+            return false;
+        }
+    }
+
     public void cacheHearingAidDevice() {
         if (mBluetoothAdapter != null) {
             for (BluetoothDevice device : mBluetoothAdapter.getActiveDevices(
@@ -791,6 +940,10 @@
         }
     }
 
+    public void setCallAudioRouteAdapter(CallAudioRouteAdapter adapter) {
+        mCallAudioRouteAdapter = adapter;
+    }
+
     public void dump(IndentingPrintWriter pw) {
         mLocalLog.dump(pw);
     }
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java b/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
index 7da5339..5a44041 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
@@ -28,6 +28,7 @@
 import android.os.Message;
 import android.telecom.Log;
 import android.telecom.Logging.Session;
+import android.util.Pair;
 import android.util.SparseArray;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -40,14 +41,12 @@
 import com.android.server.telecom.Timeouts;
 import com.android.server.telecom.flags.FeatureFlags;
 
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.List;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
@@ -167,10 +166,23 @@
                         removeDevice((String) args.arg2);
                         break;
                     case CONNECT_BT:
-                        String actualAddress = connectBtAudio((String) args.arg2,
-                            false /* switchingBtDevices*/);
+                        String actualAddress;
+                        boolean connected;
+                        if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+                            Pair<String, Boolean> addressInfo = computeAddressToConnectTo(
+                                    (String) args.arg2, false, null);
+                            // See if we need to transition route if the device is already
+                            // connected. If connected, another connection will not occur.
+                            addressInfo = handleDeviceAlreadyConnected(addressInfo);
+                            actualAddress = addressInfo.first;
+                            connected = connectBtAudio(actualAddress, 0,
+                                    false /* switchingBtDevices*/);
+                        } else {
+                            actualAddress = connectBtAudioLegacy((String) args.arg2, false);
+                            connected = actualAddress != null;
+                        }
 
-                        if (actualAddress != null) {
+                        if (connected) {
                             transitionTo(getConnectingStateForAddress(actualAddress,
                                     "AudioOff/CONNECT_BT"));
                         } else {
@@ -183,10 +195,24 @@
                         break;
                     case RETRY_BT_CONNECTION:
                         Log.i(LOG_TAG, "Retrying BT connection to %s", (String) args.arg2);
-                        String retryAddress = connectBtAudio((String) args.arg2, args.argi1,
-                            false /* switchingBtDevices*/);
+                        String retryAddress;
+                        boolean retrySuccessful;
+                        if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+                            Pair<String, Boolean> retryAddressInfo = computeAddressToConnectTo(
+                                    (String) args.arg2, false, null);
+                            // See if we need to transition route if the device is already
+                            // connected. If connected, another connection will not occur.
+                            retryAddressInfo = handleDeviceAlreadyConnected(retryAddressInfo);
+                            retryAddress = retryAddressInfo.first;
+                            retrySuccessful = connectBtAudio(retryAddress, args.argi1,
+                                    false /* switchingBtDevices*/);
+                        } else {
+                            retryAddress = connectBtAudioLegacy((String) args.arg2, args.argi1,
+                                    false /* switchingBtDevices*/);
+                            retrySuccessful = retryAddress != null;
+                        }
 
-                        if (retryAddress != null) {
+                        if (retrySuccessful) {
                             transitionTo(getConnectingStateForAddress(retryAddress,
                                     "AudioOff/RETRY_BT_CONNECTION"));
                         } else {
@@ -257,7 +283,7 @@
             String address = (String) args.arg2;
             boolean switchingBtDevices = !Objects.equals(mDeviceAddress, address);
 
-            if (switchingBtDevices == true) { // check if it is an hearing aid pair
+            if (switchingBtDevices) { // check if it is an hearing aid pair
                 BluetoothAdapter bluetoothAdapter = mDeviceManager.getBluetoothAdapter();
                 if (bluetoothAdapter != null) {
                     List<BluetoothDevice> activeHearingAids =
@@ -274,7 +300,9 @@
                             }
                         }
                     }
-
+                }
+                if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+                    switchingBtDevices &= (mDeviceAddress != null);
                 }
             }
             try {
@@ -290,14 +318,30 @@
                         }
                         break;
                     case CONNECT_BT:
+                        String actualAddress = null;
+                        if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+                            Pair<String, Boolean> addressInfo = computeAddressToConnectTo(address,
+                                    switchingBtDevices, mDeviceAddress);
+                            // See if we need to transition route if the device is already
+                            // connected. If connected, another connection will not occur.
+                            addressInfo = handleDeviceAlreadyConnected(addressInfo);
+                            actualAddress = addressInfo.first;
+                            switchingBtDevices = addressInfo.second;
+                        }
+
                         if (!switchingBtDevices) {
                             // Ignore repeated connection attempts to the same device
                             break;
                         }
 
-                        String actualAddress = connectBtAudio(address,
-                            true /* switchingBtDevices*/);
-                        if (actualAddress != null) {
+                        if (!mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+                            actualAddress = connectBtAudioLegacy(address,
+                                    true /* switchingBtDevices*/);
+                        }
+                        boolean connected = mFeatureFlags.resolveSwitchingBtDevicesComputation()
+                                ? connectBtAudio(actualAddress, 0, true /* switchingBtDevices*/)
+                                : actualAddress != null;
+                        if (connected) {
                             transitionTo(getConnectingStateForAddress(actualAddress,
                                     "AudioConnecting/CONNECT_BT"));
                         } else {
@@ -309,14 +353,32 @@
                         mDeviceManager.disconnectAudio();
                         break;
                     case RETRY_BT_CONNECTION:
+                        String retryAddress = null;
+                        if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+                            Pair<String, Boolean> retryAddressInfo = computeAddressToConnectTo(
+                                    address, switchingBtDevices, mDeviceAddress);
+                            // See if we need to transition route if the device is already
+                            // connected. If connected, another connection will not occur.
+                            retryAddressInfo = handleDeviceAlreadyConnected(retryAddressInfo);
+                            retryAddress = retryAddressInfo.first;
+                            switchingBtDevices = retryAddressInfo.second;
+                        }
+
                         if (!switchingBtDevices) {
                             Log.d(LOG_TAG, "Retry message came through while connecting.");
                             break;
                         }
 
-                        String retryAddress = connectBtAudio(address, args.argi1,
-                            true /* switchingBtDevices*/);
-                        if (retryAddress != null) {
+                        if (!mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+                            retryAddress = connectBtAudioLegacy(address, args.argi1,
+                                    true /* switchingBtDevices*/);
+                        }
+                        boolean retrySuccessful = mFeatureFlags
+                                .resolveSwitchingBtDevicesComputation()
+                                ? connectBtAudio(retryAddress, args.argi1,
+                                        true /* switchingBtDevices*/)
+                                : retryAddress != null;
+                        if (retrySuccessful) {
                             transitionTo(getConnectingStateForAddress(retryAddress,
                                     "AudioConnecting/RETRY_BT_CONNECTION"));
                         } else {
@@ -395,6 +457,10 @@
             SomeArgs args = (SomeArgs) msg.obj;
             String address = (String) args.arg2;
             boolean switchingBtDevices = !Objects.equals(mDeviceAddress, address);
+            if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+                switchingBtDevices &= (mDeviceAddress != null);
+            }
+
             try {
                 switch (msg.what) {
                     case NEW_DEVICE_CONNECTED:
@@ -407,6 +473,17 @@
                         }
                         break;
                     case CONNECT_BT:
+                        String actualAddress = null;
+                        if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+                            Pair<String, Boolean> addressInfo = computeAddressToConnectTo(address,
+                                    switchingBtDevices, mDeviceAddress);
+                            // See if we need to transition route if the device is already
+                            // connected. If connected, another connection will not occur.
+                            addressInfo = handleDeviceAlreadyConnected(addressInfo);
+                            actualAddress = addressInfo.first;
+                            switchingBtDevices = addressInfo.second;
+                        }
+
                         if (!switchingBtDevices) {
                             // Ignore connection to already connected device but still notify
                             // CallAudioRouteStateMachine since this might be a switch from other
@@ -415,9 +492,14 @@
                             break;
                         }
 
-                        String actualAddress = connectBtAudio(address,
-                            true /* switchingBtDevices*/);
-                        if (actualAddress != null) {
+                        if (!mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+                            actualAddress = connectBtAudioLegacy(address,
+                                    true /* switchingBtDevices*/);
+                        }
+                        boolean connected = mFeatureFlags.resolveSwitchingBtDevicesComputation()
+                                ? connectBtAudio(actualAddress, 0, true /* switchingBtDevices*/)
+                                : actualAddress != null;
+                        if (connected) {
                             if (mFeatureFlags.useActualAddressToEnterConnectingState()) {
                                 transitionTo(getConnectingStateForAddress(actualAddress,
                                         "AudioConnected/CONNECT_BT"));
@@ -434,14 +516,32 @@
                         mDeviceManager.disconnectAudio();
                         break;
                     case RETRY_BT_CONNECTION:
+                        String retryAddress = null;
+                        if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+                            Pair<String, Boolean> retryAddressInfo = computeAddressToConnectTo(
+                                    address, switchingBtDevices, mDeviceAddress);
+                            // See if we need to transition route if the device is already
+                            // connected. If connected, another connection will not occur.
+                            retryAddressInfo = handleDeviceAlreadyConnected(retryAddressInfo);
+                            retryAddress = retryAddressInfo.first;
+                            switchingBtDevices = retryAddressInfo.second;
+                        }
+
                         if (!switchingBtDevices) {
                             Log.d(LOG_TAG, "Retry message came through while connected.");
                             break;
                         }
 
-                        String retryAddress = connectBtAudio(address, args.argi1,
-                            true /* switchingBtDevices*/);
-                        if (retryAddress != null) {
+                        if (!mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+                            retryAddress = connectBtAudioLegacy(address, args.argi1,
+                                    true /* switchingBtDevices*/);
+                        }
+                        boolean retrySuccessful = mFeatureFlags
+                                .resolveSwitchingBtDevicesComputation()
+                                ? connectBtAudio(retryAddress, args.argi1,
+                                        true /* switchingBtDevices*/)
+                                : retryAddress != null;
+                        if (retrySuccessful) {
                             transitionTo(getConnectingStateForAddress(retryAddress,
                                     "AudioConnected/RETRY_BT_CONNECTION"));
                         } else {
@@ -742,8 +842,124 @@
         return false;
     }
 
-    private String connectBtAudio(String address, boolean switchingBtDevices) {
-        return connectBtAudio(address, 0, switchingBtDevices);
+    /**
+     * Determines the address that should be used for the connection attempt. In the case that the
+     * specified address to be used is null, Telecom will try to find an arbitrary address to
+     * connect instead.
+     *
+     * @param address The address that should be prioritized for the connection attempt
+     * @param switchingBtDevices Used when there is existing audio connection to other Bt device.
+     * @param stateAddress The address stored in the state that indicates the connecting/connected
+     *                     device.
+     * @return {@link Pair} containing the address to connect to and whether an existing BT audio
+     *                      connection for a different device exists.
+     */
+    private Pair<String, Boolean> computeAddressToConnectTo(
+            String address, boolean switchingBtDevices, String stateAddress) {
+        Collection<BluetoothDevice> deviceList = mDeviceManager.getConnectedDevices();
+        Optional<BluetoothDevice> matchingDevice = deviceList.stream()
+                .filter(d -> Objects.equals(d.getAddress(), address))
+                .findAny();
+
+        String actualAddress = matchingDevice.isPresent()
+                ? address : getActiveDeviceAddress();
+        if (actualAddress == null) {
+            Log.i(this, "No device specified and BT stack has no active device."
+                    + " Using arbitrary device - except watch");
+            if (deviceList.size() > 0) {
+                for (BluetoothDevice device : deviceList) {
+                    if (mFeatureFlags.ignoreAutoRouteToWatchDevice() && isWatch(device)) {
+                        Log.i(this, "Skipping a watch device: " + device);
+                        continue;
+                    }
+                    actualAddress = device.getAddress();
+                    break;
+                }
+            }
+
+            if (actualAddress == null) {
+                Log.i(this, "No devices available at all. Not connecting.");
+                return new Pair<>(null, false);
+            }
+            if (switchingBtDevices && actualAddress.equals(stateAddress)) {
+                switchingBtDevices = false;
+            }
+        }
+        if (!matchingDevice.isPresent()) {
+            Log.i(this, "No device with address %s available. Using %s instead.",
+                    address, actualAddress);
+        }
+        return new Pair<>(actualAddress, switchingBtDevices);
+    }
+
+    /**
+     * Handles route switching to the connected state for a device. This currently handles the case
+     * for hearing aids when the route manager reports AudioOff since Telecom doesn't treat HA as
+     * the active device outside of a call.
+     *
+     * @param addressInfo A {@link Pair} containing the BT address to connect to as well as if we're
+     *                    handling a switch of BT devices.
+     * @return {@link Pair} indicating the address to connect to as well as if we're handling a
+     *                      switch of BT devices. If the device is already connected, then the
+     *                      return value will be {null, false} to indicate that a connection attempt
+     *                      is not required.
+     */
+    private Pair<String, Boolean> handleDeviceAlreadyConnected(Pair<String, Boolean> addressInfo) {
+        String address = addressInfo.first;
+        BluetoothDevice alreadyConnectedDevice = getBluetoothAudioConnectedDevice();
+        if (alreadyConnectedDevice != null && alreadyConnectedDevice.getAddress().equals(
+                address)) {
+            Log.i(this, "trying to connect to already connected device -- skipping connection"
+                    + " and going into the actual connected state.");
+            transitionToActualState();
+            return new Pair<>(null, false);
+        }
+        return addressInfo;
+    }
+
+    /**
+     * Initiates a connection to the BT address specified.
+     * Note: This method is not synchronized on the Telecom lock, so don't try and call back into
+     * Telecom from within it.
+     * @param address The address that should be tried first. May be null.
+     * @param retryCount The number of times this connection attempt has been retried.
+     * @param switchingBtDevices Used when there is existing audio connection to other Bt device.
+     * @return {@code true} if the connection to the address was successful, otherwise {@code false}
+     *          if the connection fails.
+     *
+     * Note: This should only be used in par with the resolveSwitchingBtDevicesComputation flag.
+     */
+    private boolean connectBtAudio(String address, int retryCount, boolean switchingBtDevices) {
+        if (address == null) {
+            return false;
+        }
+
+        if (switchingBtDevices) {
+            /* When new Bluetooth connects audio, make sure previous one has disconnected audio. */
+            mDeviceManager.disconnectAudio();
+        }
+
+        if (!mDeviceManager.connectAudio(address, switchingBtDevices)) {
+            boolean shouldRetry = retryCount < MAX_CONNECTION_RETRIES;
+            Log.w(LOG_TAG, "Could not connect to %s. Will %s", address,
+                    shouldRetry ? "retry" : "not retry");
+            if (shouldRetry) {
+                SomeArgs args = SomeArgs.obtain();
+                args.arg1 = Log.createSubsession();
+                args.arg2 = address;
+                args.argi1 = retryCount + 1;
+                sendMessageDelayed(RETRY_BT_CONNECTION, args,
+                        mTimeoutsAdapter.getRetryBluetoothConnectAudioBackoffMillis(
+                                mContext.getContentResolver()));
+            }
+            return false;
+        }
+
+        return true;
+    }
+
+    private String connectBtAudioLegacy(String address, boolean switchingBtDevices) {
+        return connectBtAudioLegacy(address, 0, switchingBtDevices);
     }
 
     /**
@@ -756,7 +972,8 @@
      * @return The address of the device that's actually being connected to, or null if no
      * connection was successful.
      */
-    private String connectBtAudio(String address, int retryCount, boolean switchingBtDevices) {
+    private String connectBtAudioLegacy(String address, int retryCount,
+            boolean switchingBtDevices) {
         Collection<BluetoothDevice> deviceList = mDeviceManager.getConnectedDevices();
         Optional<BluetoothDevice> matchingDevice = deviceList.stream()
                 .filter(d -> Objects.equals(d.getAddress(), address))
@@ -1048,4 +1265,8 @@
             mHfpActiveDeviceCache = device;
         }
     }
+
+    public BluetoothDeviceManager getDeviceManager() {
+        return mDeviceManager;
+    }
 }
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java b/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
index 6d80cd5..f76391c 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
@@ -22,6 +22,7 @@
 import static com.android.server.telecom.CallAudioRouteAdapter.BT_AUDIO_DISCONNECTED;
 import static com.android.server.telecom.CallAudioRouteAdapter.BT_DEVICE_ADDED;
 import static com.android.server.telecom.CallAudioRouteAdapter.BT_DEVICE_REMOVED;
+import static com.android.server.telecom.CallAudioRouteAdapter.PENDING_ROUTE_FAILED;
 import static com.android.server.telecom.bluetooth.BluetoothRouteManager.BT_AUDIO_IS_ON;
 import static com.android.server.telecom.bluetooth.BluetoothRouteManager.BT_AUDIO_LOST;
 
@@ -39,15 +40,16 @@
 import android.os.Bundle;
 import android.telecom.Log;
 import android.telecom.Logging.Session;
+import android.util.Pair;
 
 import com.android.internal.os.SomeArgs;
 import com.android.server.telecom.AudioRoute;
 import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
 import com.android.server.telecom.CallAudioRouteAdapter;
+import com.android.server.telecom.CallAudioRouteController;
 import com.android.server.telecom.flags.FeatureFlags;
 import com.android.server.telecom.flags.Flags;
 
-
 public class BluetoothStateReceiver extends BroadcastReceiver {
     private static final String LOG_TAG = BluetoothStateReceiver.class.getSimpleName();
     public static final IntentFilter INTENT_FILTER;
@@ -116,9 +118,28 @@
         args.arg2 = device.getAddress();
         switch (bluetoothHeadsetAudioState) {
             case BluetoothHeadset.STATE_AUDIO_CONNECTED:
-                if (Flags.useRefactoredAudioRouteSwitching()) {
-                    mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0,
-                            device);
+                if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+                    CallAudioRouteController audioRouteController =
+                            (CallAudioRouteController) mCallAudioRouteAdapter;
+                    audioRouteController.setIsScoAudioConnected(true);
+                    if (audioRouteController.isPending()) {
+                        mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0,
+                                device);
+                    } else {
+                        // It's possible that the initial BT connection fails but BT_AUDIO_CONNECTED
+                        // is sent later, indicating that SCO audio is on. We should route
+                        // appropriately in order for the UI to reflect this state.
+                        AudioRoute btRoute = audioRouteController.getBluetoothRoute(
+                                AudioRoute.TYPE_BLUETOOTH_SCO, device.getAddress());
+                        if (btRoute != null) {
+                            audioRouteController.getPendingAudioRoute().overrideDestRoute(btRoute);
+                            audioRouteController.overrideIsPending(true);
+                            audioRouteController.getPendingAudioRoute()
+                                    .setCommunicationDeviceType(AudioRoute.TYPE_BLUETOOTH_SCO);
+                            mCallAudioRouteAdapter.sendMessageWithSessionInfo(
+                                    CallAudioRouteAdapter.EXIT_PENDING_ROUTE);
+                        }
+                    }
                 } else {
                     if (!mIsInCall) {
                         Log.i(LOG_TAG, "Ignoring BT audio on since we're not in a call");
@@ -129,6 +150,9 @@
                 break;
             case BluetoothHeadset.STATE_AUDIO_DISCONNECTED:
                 if (Flags.useRefactoredAudioRouteSwitching()) {
+                    CallAudioRouteController audioRouteController =
+                            (CallAudioRouteController) mCallAudioRouteAdapter;
+                    audioRouteController.setIsScoAudioConnected(false);
                     mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_AUDIO_DISCONNECTED, 0,
                             device);
                 }  else {
@@ -212,12 +236,36 @@
                 BluetoothDeviceManager.getDeviceTypeString(deviceType));
 
         if (Flags.useRefactoredAudioRouteSwitching()) {
+            CallAudioRouteController audioRouteController = (CallAudioRouteController)
+                    mCallAudioRouteAdapter;
             if (device == null) {
+                audioRouteController.updateActiveBluetoothDevice(new Pair(audioRouteType, null));
                 mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_GONE,
                         audioRouteType);
             } else {
+                audioRouteController.updateActiveBluetoothDevice(
+                        new Pair(audioRouteType, device.getAddress()));
                 mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
                         audioRouteType, device.getAddress());
+                if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID
+                        || deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO) {
+                    if (!mBluetoothDeviceManager.setCommunicationDeviceForAddress(
+                            device.getAddress())) {
+                        Log.i(this, "handleActiveDeviceChanged: Failed to set "
+                                + "communication device for %s. Sending PENDING_ROUTE_FAILED to "
+                                + "pending audio route.", device);
+                        mCallAudioRouteAdapter.getPendingAudioRoute()
+                                .onMessageReceived(new Pair<>(PENDING_ROUTE_FAILED,
+                                        device.getAddress()), device.getAddress());
+                    } else {
+                        // Track the currently set communication device.
+                        int routeType = deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO
+                                ? AudioRoute.TYPE_BLUETOOTH_LE
+                                : AudioRoute.TYPE_BLUETOOTH_HA;
+                        mCallAudioRouteAdapter.getPendingAudioRoute()
+                                .setCommunicationDeviceType(routeType);
+                    }
+                }
             }
         } else {
             mBluetoothRouteManager.onActiveDeviceChanged(device, deviceType);
diff --git a/src/com/android/server/telecom/callfiltering/BlockCheckerAdapter.java b/src/com/android/server/telecom/callfiltering/BlockCheckerAdapter.java
index 1fda542..55540de 100644
--- a/src/com/android/server/telecom/callfiltering/BlockCheckerAdapter.java
+++ b/src/com/android/server/telecom/callfiltering/BlockCheckerAdapter.java
@@ -19,12 +19,19 @@
 import android.content.Context;
 import android.os.Bundle;
 import android.provider.BlockedNumberContract;
+import android.provider.BlockedNumbersManager;
 import android.telecom.Log;
 
+import com.android.server.telecom.flags.FeatureFlags;
+
 public class BlockCheckerAdapter {
     private static final String TAG = BlockCheckerAdapter.class.getSimpleName();
 
-    public BlockCheckerAdapter() { }
+    private FeatureFlags mFeatureFlags;
+
+    public BlockCheckerAdapter(FeatureFlags featureFlags) {
+        mFeatureFlags = featureFlags;
+    }
 
     /**
      * Returns the call blocking status for the {@code phoneNumber}.
@@ -32,7 +39,6 @@
      * This method catches all underlying exceptions to ensure that this method never throws any
      * exception.
      *
-     * @param context the context of the caller.
      * @param phoneNumber the number to check.
      * @param numberPresentation the presentation code associated with the call.
      * @param isNumberInContacts indicates if the provided number exists as a contact.
@@ -48,10 +54,20 @@
             int numberPresentation, boolean isNumberInContacts) {
         int blockStatus = BlockedNumberContract.STATUS_NOT_BLOCKED;
         long startTimeNano = System.nanoTime();
+        BlockedNumbersManager blockedNumbersManager = mFeatureFlags
+                .telecomMainlineBlockedNumbersManager()
+                ? context.getSystemService(BlockedNumbersManager.class)
+                : null;
 
         try {
-            blockStatus = BlockedNumberContract.BlockedNumbers.shouldSystemBlockNumber(
-                    context, phoneNumber, numberPresentation, isNumberInContacts);
+            Bundle extras = new Bundle();
+            extras.putInt(BlockedNumberContract.EXTRA_CALL_PRESENTATION, numberPresentation);
+            extras.putBoolean(BlockedNumberContract.EXTRA_CONTACT_EXIST, isNumberInContacts);
+            blockStatus = blockedNumbersManager != null
+                    ? blockedNumbersManager.shouldSystemBlockNumber(phoneNumber,
+                    numberPresentation, isNumberInContacts)
+                    : BlockedNumberContract.SystemContract.shouldSystemBlockNumber(context,
+                            phoneNumber, extras);
             if (blockStatus != BlockedNumberContract.STATUS_NOT_BLOCKED) {
                 Log.d(TAG, phoneNumber + " is blocked.");
             }
diff --git a/src/com/android/server/telecom/callfiltering/BlockedNumbersAdapter.java b/src/com/android/server/telecom/callfiltering/BlockedNumbersAdapter.java
index f640826..66137d5 100644
--- a/src/com/android/server/telecom/callfiltering/BlockedNumbersAdapter.java
+++ b/src/com/android/server/telecom/callfiltering/BlockedNumbersAdapter.java
@@ -20,10 +20,10 @@
 
 /**
  * Adapter interface that wraps methods from
- * {@link android.provider.BlockedNumberContract.BlockedNumbers} and
+ * {@link android.provider.BlockedNumbersManager} and
  * {@link com.android.server.telecom.settings.BlockedNumbersUtil} to make things testable.
  */
 public interface BlockedNumbersAdapter {
-    boolean shouldShowEmergencyCallNotification (Context context);
+    boolean shouldShowEmergencyCallNotification(Context context);
     void updateEmergencyCallNotification(Context context, boolean showNotification);
 }
diff --git a/src/com/android/server/telecom/callredirection/CallRedirectionProcessorHelper.java b/src/com/android/server/telecom/callredirection/CallRedirectionProcessorHelper.java
index 7a2d415..a6f089f 100644
--- a/src/com/android/server/telecom/callredirection/CallRedirectionProcessorHelper.java
+++ b/src/com/android/server/telecom/callredirection/CallRedirectionProcessorHelper.java
@@ -143,15 +143,25 @@
         return PhoneNumberUtils.extractPostDialPortion(handle.getSchemeSpecificPart());
     }
 
-    protected Uri formatNumberToE164(Uri handle) {
+    @VisibleForTesting
+    public Uri formatNumberToE164(Uri handle) {
         String number = handle.getSchemeSpecificPart();
 
         // Format number to E164
         TelephonyManager tm = (TelephonyManager) mContext.getSystemService(
                 Context.TELEPHONY_SERVICE);
         Log.i(this, "formatNumberToE164, original number: " + Log.pii(number));
-        number = PhoneNumberUtils.formatNumberToE164(number, tm.getNetworkCountryIso());
-        Log.i(this, "formatNumberToE164, formatted E164 number: " + Log.pii(number));
+        String networkCountryIso;
+        try {
+            number = PhoneNumberUtils.formatNumberToE164(number, tm.getNetworkCountryIso());
+            Log.i(this, "formatNumberToE164, formatted E164 number: " + Log.pii(number));
+        } catch (UnsupportedOperationException ignored) {
+            // Note: Yes, this could default back to the system locale, however redirection when
+            // there is no telephony is NOT expected.  Hence in reality we shouldn't really hit this
+            // code path in practice; this is a "just in case" to ensure we don't crash.
+            Log.w(this, "formatNumberToE164: no telephony; use original format");
+            number = null;
+        }
         // if there is a problem with parsing the phone number, formatNumberToE164 will return null;
         // and should just use the original number in that case.
         if (number == null) {
diff --git a/src/com/android/server/telecom/components/AppUninstallBroadcastReceiver.java b/src/com/android/server/telecom/components/AppUninstallBroadcastReceiver.java
index b7e5880..b89fe94 100644
--- a/src/com/android/server/telecom/components/AppUninstallBroadcastReceiver.java
+++ b/src/com/android/server/telecom/components/AppUninstallBroadcastReceiver.java
@@ -61,9 +61,14 @@
                 return;
             }
 
-            String packageName = uri.getSchemeSpecificPart();
-            handlePackageRemoved(context, packageName);
-            handleUninstallOfCallScreeningService(context, packageName);
+            final PendingResult result = goAsync();
+            // Move computation off into a separate thread to prevent ANR.
+            new Thread(() -> {
+                String packageName = uri.getSchemeSpecificPart();
+                handlePackageRemoved(context, packageName);
+                handleUninstallOfCallScreeningService(context, packageName);
+                result.finish();
+            }).start();
         }
     }
 
diff --git a/src/com/android/server/telecom/components/TelecomService.java b/src/com/android/server/telecom/components/TelecomService.java
index 845f788..2d8c78e 100644
--- a/src/com/android/server/telecom/components/TelecomService.java
+++ b/src/com/android/server/telecom/components/TelecomService.java
@@ -27,6 +27,7 @@
 import android.os.ServiceManager;
 import android.os.SystemClock;
 import android.provider.BlockedNumberContract;
+import android.provider.BlockedNumbersManager;
 import android.telecom.Log;
 
 import android.telecom.CallerInfoAsyncQuery;
@@ -104,6 +105,7 @@
     static void initializeTelecomSystem(Context context,
             InternalServiceRetrieverAdapter internalServiceRetriever) {
         if (TelecomSystem.getInstance() == null) {
+            FeatureFlags featureFlags = new FeatureFlagsImpl();
             NotificationChannelManager notificationChannelManager =
                     new NotificationChannelManager();
             notificationChannelManager.createChannels(context);
@@ -223,8 +225,11 @@
                                 @Override
                                 public boolean shouldShowEmergencyCallNotification(Context
                                         context) {
-                                    return BlockedNumberContract.BlockedNumbers
-                                            .shouldShowEmergencyCallNotification(context);
+                                    return featureFlags.telecomMainlineBlockedNumbersManager()
+                                            ? context.getSystemService(BlockedNumbersManager.class)
+                                            .shouldShowEmergencyCallNotification()
+                                            : BlockedNumberContract.SystemContract
+                                                    .shouldShowEmergencyCallNotification(context);
                                 }
 
                                 @Override
@@ -234,7 +239,7 @@
                                             showNotification);
                                 }
                             },
-                            new FeatureFlagsImpl(),
+                            featureFlags,
                             new com.android.internal.telephony.flags.FeatureFlagsImpl()));
         }
     }
diff --git a/src/com/android/server/telecom/components/UserCallActivity.java b/src/com/android/server/telecom/components/UserCallActivity.java
index d7b2001..6f5dfea 100644
--- a/src/com/android/server/telecom/components/UserCallActivity.java
+++ b/src/com/android/server/telecom/components/UserCallActivity.java
@@ -18,6 +18,8 @@
 
 import com.android.server.telecom.CallIntentProcessor;
 import com.android.server.telecom.TelecomSystem;
+import com.android.server.telecom.flags.FeatureFlags;
+import com.android.server.telecom.flags.FeatureFlagsImpl;
 
 import android.app.Activity;
 import android.content.Context;
@@ -64,8 +66,13 @@
             // See OutgoingCallBroadcaster in services/Telephony for more.
             Intent intent = getIntent();
             verifyCallAction(intent);
-            final UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE);
-            final UserHandle userHandle = new UserHandle(userManager.getProcessUserId());
+            FeatureFlags featureFlags = getTelecomSystem() != null
+                    ? getTelecomSystem().getFeatureFlags()
+                    : new FeatureFlagsImpl();
+            final UserManager userManager = getSystemService(UserManager.class);
+            final UserHandle userHandle = new UserHandle(
+                    featureFlags.telecomResolveHiddenDependencies()
+                            ? UserHandle.myUserId() : userManager.getProcessUserId());
             // Once control flow has passed to this activity, it is no longer guaranteed that we can
             // accurately determine whether the calling package has the CALL_PHONE runtime permission.
             // At this point in time we trust that the ActivityManager has already performed this
@@ -73,9 +80,9 @@
             // Create a new instance of intent to avoid modifying the
             // ActivityThread.ActivityClientRecord#intent directly.
             // Modifying directly may be a potential risk when relaunching this activity.
-            new UserCallIntentProcessor(this, userHandle).processIntent(new Intent(intent),
-                    getCallingPackage(), false, true /* hasCallAppOp*/,
-                    false /* isLocalInvocation */);
+            new UserCallIntentProcessor(this, userHandle, featureFlags)
+                    .processIntent(new Intent(intent), getCallingPackage(), false,
+                            true /* hasCallAppOp*/, false /* isLocalInvocation */);
         } finally {
             Log.endSession();
             wakelock.release();
diff --git a/src/com/android/server/telecom/components/UserCallIntentProcessor.java b/src/com/android/server/telecom/components/UserCallIntentProcessor.java
index 41232c2..c3dc963 100755
--- a/src/com/android/server/telecom/components/UserCallIntentProcessor.java
+++ b/src/com/android/server/telecom/components/UserCallIntentProcessor.java
@@ -34,6 +34,7 @@
 import com.android.server.telecom.TelecomSystem;
 import com.android.server.telecom.TelephonyUtil;
 import com.android.server.telecom.UserUtil;
+import com.android.server.telecom.flags.FeatureFlags;
 
 // TODO: Needed for move to system service: import com.android.internal.R;
 
@@ -58,10 +59,13 @@
 
     private final Context mContext;
     private final UserHandle mUserHandle;
+    private FeatureFlags mFeatureFlags;
 
-    public UserCallIntentProcessor(Context context, UserHandle userHandle) {
+    public UserCallIntentProcessor(Context context, UserHandle userHandle,
+            FeatureFlags featureFlags) {
         mContext = context;
         mUserHandle = userHandle;
+        mFeatureFlags = featureFlags;
     }
 
     /**
@@ -105,8 +109,8 @@
             handle = Uri.fromParts(PhoneAccount.SCHEME_SIP, uriString, null);
         }
 
-       if (UserUtil.hasOutgoingCallsUserRestriction(mContext, mUserHandle,
-               handle, isSelfManaged, UserCallIntentProcessor.class.getCanonicalName())) {
+       if (UserUtil.hasOutgoingCallsUserRestriction(mContext, mUserHandle, handle, isSelfManaged,
+               UserCallIntentProcessor.class.getCanonicalName(), mFeatureFlags)) {
            return;
        }
 
diff --git a/src/com/android/server/telecom/settings/BlockNumberTaskFragment.java b/src/com/android/server/telecom/settings/BlockNumberTaskFragment.java
index d96b3e1..6ca4d2a 100644
--- a/src/com/android/server/telecom/settings/BlockNumberTaskFragment.java
+++ b/src/com/android/server/telecom/settings/BlockNumberTaskFragment.java
@@ -23,7 +23,6 @@
 import android.os.AsyncTask;
 import android.os.Bundle;
 import android.provider.BlockedNumberContract;
-import com.android.server.telecom.R;
 
 /**
  * Retained fragment that runs an async task to add a blocked number.
diff --git a/src/com/android/server/telecom/settings/BlockedNumbersActivity.java b/src/com/android/server/telecom/settings/BlockedNumbersActivity.java
index 819b270..edc8da6 100644
--- a/src/com/android/server/telecom/settings/BlockedNumbersActivity.java
+++ b/src/com/android/server/telecom/settings/BlockedNumbersActivity.java
@@ -34,6 +34,7 @@
 import android.database.Cursor;
 import android.os.Bundle;
 import android.provider.BlockedNumberContract;
+import android.provider.BlockedNumbersManager;
 import android.telephony.PhoneNumberFormattingTextWatcher;
 import android.telephony.PhoneNumberUtils;
 import android.telephony.TelephonyManager;
@@ -53,7 +54,10 @@
 import android.widget.TextView;
 import android.widget.Toast;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.telecom.R;
+import com.android.server.telecom.flags.FeatureFlags;
+import com.android.server.telecom.flags.FeatureFlagsImpl;
 
 
 /**
@@ -75,8 +79,10 @@
             BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER + " NOTNULL) AND (" +
             BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER + " != '' ))";
 
+    private BlockedNumbersManager mBlockedNumbersManager;
     private BlockNumberTaskFragment mBlockNumberTaskFragment;
     private BlockedNumbersAdapter mAdapter;
+    private FeatureFlags mFeatureFlags;
     private TextView mAddButton;
     private ProgressBar mProgressBar;
     private RelativeLayout mButterBar;
@@ -114,6 +120,7 @@
             return;
         }
 
+        mFeatureFlags = new FeatureFlagsImpl();
         FragmentManager fm = getFragmentManager();
         mBlockNumberTaskFragment =
                 (BlockNumberTaskFragment) fm.findFragmentByTag(TAG_BLOCK_NUMBER_TASK_FRAGMENT);
@@ -155,12 +162,15 @@
             }
         };
         IntentFilter blockStatusIntentFilter = new IntentFilter(
-                BlockedNumberContract.BlockedNumbers.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED);
+                BlockedNumbersManager.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED);
         blockStatusIntentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
         registerReceiver(mBlockingStatusReceiver, blockStatusIntentFilter,
                 Context.RECEIVER_EXPORTED);
 
         getLoaderManager().initLoader(0, null, this);
+        mBlockedNumbersManager = mFeatureFlags.telecomMainlineBlockedNumbersManager()
+                ? getSystemService(BlockedNumbersManager.class)
+                : null;
     }
 
     @Override
@@ -183,8 +193,10 @@
     }
 
     private void updateButterBar() {
-        if (BlockedNumberContract.BlockedNumbers
-                .getBlockSuppressionStatus(this).getIsSuppressed()) {
+        boolean isBlockSuppressionEnabled = mBlockedNumbersManager != null
+                ? mBlockedNumbersManager.getBlockSuppressionStatus().getIsSuppressed()
+                : BlockedNumberContract.SystemContract.getBlockSuppressionStatus(this).isSuppressed;
+        if (isBlockSuppressionEnabled) {
             mButterBar.setVisibility(View.VISIBLE);
         } else {
             mButterBar.setVisibility(View.GONE);
@@ -239,7 +251,11 @@
         if (view == mAddButton) {
             showAddBlockedNumberDialog();
         } else if (view == mReEnableButton) {
-            BlockedNumberContract.BlockedNumbers.endBlockSuppression(this);
+            if (mBlockedNumbersManager != null) {
+                mBlockedNumbersManager.endBlockSuppression();
+            } else {
+                BlockedNumberContract.SystemContract.endBlockSuppression(this);
+            }
             mButterBar.setVisibility(View.GONE);
         }
     }
@@ -302,12 +318,13 @@
         }
     }
 
-    private boolean isEmergencyNumber(Context context, String number) {
+    @VisibleForTesting
+    public static boolean isEmergencyNumber(Context context, String number) {
         try {
             TelephonyManager tm = (TelephonyManager) context.getSystemService(
                     Context.TELEPHONY_SERVICE);
             return tm.isEmergencyNumber(number);
-        } catch (IllegalStateException ise) {
+        } catch (UnsupportedOperationException | IllegalStateException ignored) {
             return false;
         }
     }
diff --git a/src/com/android/server/telecom/settings/BlockedNumbersUtil.java b/src/com/android/server/telecom/settings/BlockedNumbersUtil.java
index e0fe81e..3e1da17 100644
--- a/src/com/android/server/telecom/settings/BlockedNumbersUtil.java
+++ b/src/com/android/server/telecom/settings/BlockedNumbersUtil.java
@@ -23,7 +23,8 @@
 import android.content.Intent;
 import android.os.PersistableBundle;
 import android.os.UserHandle;
-import android.provider.BlockedNumberContract.BlockedNumbers;
+import android.provider.BlockedNumberContract;
+import android.provider.BlockedNumbersManager;
 import android.telephony.CarrierConfigManager;
 import android.telephony.PhoneNumberUtils;
 import android.text.BidiFormatter;
@@ -34,6 +35,7 @@
 
 import com.android.server.telecom.R;
 import com.android.server.telecom.SystemSettingsUtil;
+import com.android.server.telecom.flags.FeatureFlags;
 import com.android.server.telecom.ui.NotificationChannelManager;
 
 import java.util.Locale;
@@ -148,8 +150,11 @@
      * @return If {@code true} means the key enabled in the SharedPreferences,
      *            {@code false} otherwise.
      */
-    public static boolean getBlockedNumberSetting(Context context, String key) {
-        return BlockedNumbers.getBlockedNumberSetting(context, key);
+    public static boolean getBlockedNumberSetting(Context context, String key,
+            FeatureFlags featureFlags) {
+        return featureFlags.telecomMainlineBlockedNumbersManager()
+                ? context.getSystemService(BlockedNumbersManager.class).getBlockedNumberSetting(key)
+                : BlockedNumberContract.SystemContract.getEnhancedBlockSetting(context, key);
     }
 
     /**
@@ -159,7 +164,13 @@
      * @param key preference key of SharedPreferences.
      * @param value the register value to the SharedPreferences.
      */
-    public static void setBlockedNumberSetting(Context context, String key, boolean value) {
-        BlockedNumbers.setBlockedNumberSetting(context, key, value);
+    public static void setBlockedNumberSetting(Context context, String key, boolean value,
+            FeatureFlags featureFlags) {
+        if (featureFlags.telecomMainlineBlockedNumbersManager()) {
+            context.getSystemService(BlockedNumbersManager.class).setBlockedNumberSetting(key,
+                    value);
+        } else {
+            BlockedNumberContract.SystemContract.setEnhancedBlockSetting(context, key, value);
+        }
     }
 }
diff --git a/src/com/android/server/telecom/settings/CallBlockDisabledActivity.java b/src/com/android/server/telecom/settings/CallBlockDisabledActivity.java
index 35b7f70..cc66a2d 100644
--- a/src/com/android/server/telecom/settings/CallBlockDisabledActivity.java
+++ b/src/com/android/server/telecom/settings/CallBlockDisabledActivity.java
@@ -20,19 +20,23 @@
 import android.app.AlertDialog;
 import android.content.DialogInterface;
 import android.os.Bundle;
-import android.provider.BlockedNumberContract;
+import android.provider.BlockedNumbersManager;
 
 import com.android.server.telecom.R;
+import com.android.server.telecom.flags.FeatureFlags;
+import com.android.server.telecom.flags.FeatureFlagsImpl;
 
 /**
  * Shows a dialog when user taps an notification in notification tray.
  */
 public class CallBlockDisabledActivity extends Activity {
     private AlertDialog mDialog;
+    private FeatureFlags mFeatureFlags;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        mFeatureFlags = new FeatureFlagsImpl();
         showCallBlockingOffDialog();
     }
 
@@ -60,9 +64,9 @@
                     public void onClick(DialogInterface dialog, int which) {
                         BlockedNumbersUtil.setBlockedNumberSetting(
                                 CallBlockDisabledActivity.this,
-                                BlockedNumberContract.BlockedNumbers
+                                BlockedNumbersManager
                                         .ENHANCED_SETTING_KEY_SHOW_EMERGENCY_CALL_NOTIFICATION,
-                                false);
+                                false, mFeatureFlags);
                         BlockedNumbersUtil.updateEmergencyCallNotification(
                                 CallBlockDisabledActivity.this, false);
                         finish();
diff --git a/src/com/android/server/telecom/settings/EnhancedCallBlockingFragment.java b/src/com/android/server/telecom/settings/EnhancedCallBlockingFragment.java
index 7ea8926..b54e273 100644
--- a/src/com/android/server/telecom/settings/EnhancedCallBlockingFragment.java
+++ b/src/com/android/server/telecom/settings/EnhancedCallBlockingFragment.java
@@ -23,7 +23,7 @@
 import android.preference.PreferenceFragment;
 import android.preference.PreferenceScreen;
 import android.preference.SwitchPreference;
-import android.provider.BlockedNumberContract.BlockedNumbers;
+import android.provider.BlockedNumbersManager;
 import android.telephony.CarrierConfigManager;
 import android.telephony.SubscriptionManager;
 import android.telecom.Log;
@@ -32,6 +32,8 @@
 import android.view.ViewGroup;
 
 import com.android.server.telecom.R;
+import com.android.server.telecom.flags.FeatureFlags;
+import com.android.server.telecom.flags.FeatureFlagsImpl;
 
 public class EnhancedCallBlockingFragment extends PreferenceFragment
         implements Preference.OnPreferenceChangeListener {
@@ -45,22 +47,25 @@
             "block_unavailable_calls_setting";
     private boolean mIsCombiningRestrictedAndUnknownOption = false;
     private boolean mIsCombiningUnavailableAndUnknownOption = false;
+    private FeatureFlags mFeatureFlags;
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         addPreferencesFromResource(R.xml.enhanced_call_blocking_settings);
+        mFeatureFlags = new FeatureFlagsImpl();
 
         maybeConfigureCallBlockingOptions();
 
-        setOnPreferenceChangeListener(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);
+        setOnPreferenceChangeListener(
+                BlockedNumbersManager.ENHANCED_SETTING_KEY_BLOCK_UNREGISTERED);
+        setOnPreferenceChangeListener(BlockedNumbersManager.ENHANCED_SETTING_KEY_BLOCK_PRIVATE);
+        setOnPreferenceChangeListener(BlockedNumbersManager.ENHANCED_SETTING_KEY_BLOCK_PAYPHONE);
+        setOnPreferenceChangeListener(BlockedNumbersManager.ENHANCED_SETTING_KEY_BLOCK_UNKNOWN);
+        setOnPreferenceChangeListener(BlockedNumbersManager.ENHANCED_SETTING_KEY_BLOCK_UNAVAILABLE);
         if (!showPayPhoneBlocking()) {
             Preference payPhoneOption = getPreferenceScreen()
-                    .findPreference(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_PAYPHONE);
+                    .findPreference(BlockedNumbersManager.ENHANCED_SETTING_KEY_BLOCK_PAYPHONE);
             getPreferenceScreen().removePreference(payPhoneOption);
         }
     }
@@ -122,13 +127,13 @@
     public void onResume() {
         super.onResume();
 
-        updateEnhancedBlockPref(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_UNREGISTERED);
-        updateEnhancedBlockPref(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_PRIVATE);
+        updateEnhancedBlockPref(BlockedNumbersManager.ENHANCED_SETTING_KEY_BLOCK_UNREGISTERED);
+        updateEnhancedBlockPref(BlockedNumbersManager.ENHANCED_SETTING_KEY_BLOCK_PRIVATE);
         if (showPayPhoneBlocking()) {
-            updateEnhancedBlockPref(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_PAYPHONE);
+            updateEnhancedBlockPref(BlockedNumbersManager.ENHANCED_SETTING_KEY_BLOCK_PAYPHONE);
         }
-        updateEnhancedBlockPref(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_UNKNOWN);
-        updateEnhancedBlockPref(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_UNAVAILABLE);
+        updateEnhancedBlockPref(BlockedNumbersManager.ENHANCED_SETTING_KEY_BLOCK_UNKNOWN);
+        updateEnhancedBlockPref(BlockedNumbersManager.ENHANCED_SETTING_KEY_BLOCK_UNAVAILABLE);
     }
 
     /**
@@ -137,7 +142,8 @@
     private void updateEnhancedBlockPref(String key) {
         SwitchPreference pref = (SwitchPreference) findPreference(key);
         if (pref != null) {
-            pref.setChecked(BlockedNumbersUtil.getBlockedNumberSetting(getActivity(), key));
+            pref.setChecked(BlockedNumbersUtil.getBlockedNumberSetting(
+                    getActivity(), key, mFeatureFlags));
         }
     }
 
@@ -148,18 +154,18 @@
                 Log.i(this, "onPreferenceChange: changing %s and %s to %b",
                         preference.getKey(), BLOCK_RESTRICTED_NUMBERS_KEY, (boolean) objValue);
                 BlockedNumbersUtil.setBlockedNumberSetting(getActivity(),
-                        BLOCK_RESTRICTED_NUMBERS_KEY, (boolean) objValue);
+                        BLOCK_RESTRICTED_NUMBERS_KEY, (boolean) objValue, mFeatureFlags);
             }
 
             if (mIsCombiningUnavailableAndUnknownOption) {
                 Log.i(this, "onPreferenceChange: changing %s and %s to %b",
                         preference.getKey(), BLOCK_UNAVAILABLE_NUMBERS_KEY, (boolean) objValue);
                 BlockedNumbersUtil.setBlockedNumberSetting(getActivity(),
-                        BLOCK_UNAVAILABLE_NUMBERS_KEY, (boolean) objValue);
+                        BLOCK_UNAVAILABLE_NUMBERS_KEY, (boolean) objValue, mFeatureFlags);
             }
         }
         BlockedNumbersUtil.setBlockedNumberSetting(getActivity(), preference.getKey(),
-                (boolean) objValue);
+                (boolean) objValue, mFeatureFlags);
         return true;
     }
 
diff --git a/src/com/android/server/telecom/ui/DisconnectedCallNotifier.java b/src/com/android/server/telecom/ui/DisconnectedCallNotifier.java
index 1604285..04228c1 100644
--- a/src/com/android/server/telecom/ui/DisconnectedCallNotifier.java
+++ b/src/com/android/server/telecom/ui/DisconnectedCallNotifier.java
@@ -41,6 +41,7 @@
 import android.text.TextDirectionHeuristics;
 import android.text.TextUtils;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallState;
 import com.android.server.telecom.CallsManager;
@@ -285,12 +286,18 @@
      *      network location.  If the network location does not exist, fall back to the locale
      *      setting.
      */
-    private String getCurrentCountryIso(Context context) {
+    @VisibleForTesting
+    public String getCurrentCountryIso(Context context) {
         // Without framework function calls, this seems to be the most accurate location service
         // we can rely on.
         final TelephonyManager telephonyManager =
                 (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
-        String countryIso = telephonyManager.getNetworkCountryIso().toUpperCase();
+        String countryIso;
+        try {
+            countryIso = telephonyManager.getNetworkCountryIso().toUpperCase();
+        } catch (UnsupportedOperationException ignored) {
+            countryIso = null;
+        }
 
         if (countryIso == null) {
             countryIso = Locale.getDefault().getCountry();
diff --git a/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java b/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java
index 25ce0ca..220b44e 100644
--- a/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java
+++ b/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java
@@ -59,6 +59,7 @@
 import android.util.ArrayMap;
 import android.util.ArraySet;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.telecom.CallerInfoLookupHelper;
 import com.android.server.telecom.CallsManagerListenerBase;
 import com.android.server.telecom.Constants;
@@ -506,12 +507,18 @@
      *      network location.  If the network location does not exist, fall back to the locale
      *      setting.
      */
-    private String getCurrentCountryIso(Context context) {
+    @VisibleForTesting
+    public String getCurrentCountryIso(Context context) {
         // Without framework function calls, this seems to be the most accurate location service
         // we can rely on.
         final TelephonyManager telephonyManager =
                 (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
-        String countryIso = telephonyManager.getNetworkCountryIso().toUpperCase();
+        String countryIso;
+        try {
+            countryIso = telephonyManager.getNetworkCountryIso().toUpperCase();
+        } catch (UnsupportedOperationException ignored) {
+            countryIso = null;
+        }
 
         if (countryIso == null) {
             countryIso = Locale.getDefault().getCountry();
diff --git a/src/com/android/server/telecom/ui/ToastFactory.java b/src/com/android/server/telecom/ui/ToastFactory.java
index 75b69da..1561f0b 100644
--- a/src/com/android/server/telecom/ui/ToastFactory.java
+++ b/src/com/android/server/telecom/ui/ToastFactory.java
@@ -21,6 +21,6 @@
 import android.widget.Toast;
 
 public interface ToastFactory {
-    Toast makeText(Context context, @StringRes int resId, int duration);
-    Toast makeText(Context context, CharSequence text, int duration);
+    void makeText(Context context, @StringRes int resId, int duration);
+    void makeText(Context context, CharSequence text, int duration);
 }
diff --git a/src/com/android/server/telecom/voip/CallEventCallbackAckTransaction.java b/src/com/android/server/telecom/voip/CallEventCallbackAckTransaction.java
index 93d9836..9e140a7 100644
--- a/src/com/android/server/telecom/voip/CallEventCallbackAckTransaction.java
+++ b/src/com/android/server/telecom/voip/CallEventCallbackAckTransaction.java
@@ -125,7 +125,7 @@
 
         try {
             // wait for the client to ack that CallEventCallback
-            boolean success = latch.await(VoipCallTransaction.TIMEOUT_LIMIT, TimeUnit.MILLISECONDS);
+            boolean success = latch.await(mTransactionTimeoutMs, TimeUnit.MILLISECONDS);
             if (!success) {
                 // client send onError and failed to complete transaction
                 Log.i(TAG, String.format("CallEventCallbackAckTransaction:"
diff --git a/src/com/android/server/telecom/voip/EndpointChangeTransaction.java b/src/com/android/server/telecom/voip/EndpointChangeTransaction.java
index e037a79..6841fcf 100644
--- a/src/com/android/server/telecom/voip/EndpointChangeTransaction.java
+++ b/src/com/android/server/telecom/voip/EndpointChangeTransaction.java
@@ -19,6 +19,7 @@
 import android.os.Bundle;
 import android.os.ResultReceiver;
 import android.telecom.CallEndpoint;
+import android.telecom.CallException;
 import android.util.Log;
 
 import com.android.server.telecom.CallsManager;
@@ -49,8 +50,9 @@
                     future.complete(new VoipCallTransactionResult(
                             VoipCallTransactionResult.RESULT_SUCCEED, null));
                 } else {
+                    // TODO:: define errors in CallException class. b/335703584
                     future.complete(new VoipCallTransactionResult(
-                            VoipCallTransactionResult.RESULT_FAILED, null));
+                            CallException.CODE_ERROR_UNKNOWN, null));
                 }
             }
         });
diff --git a/src/com/android/server/telecom/voip/IncomingCallTransaction.java b/src/com/android/server/telecom/voip/IncomingCallTransaction.java
index d35030c..ed0c7d6 100644
--- a/src/com/android/server/telecom/voip/IncomingCallTransaction.java
+++ b/src/com/android/server/telecom/voip/IncomingCallTransaction.java
@@ -19,14 +19,18 @@
 import static android.telecom.CallAttributes.CALL_CAPABILITIES_KEY;
 import static android.telecom.CallAttributes.DISPLAY_NAME_KEY;
 
+import static com.android.server.telecom.voip.VideoStateTranslation.TransactionalVideoStateToVideoProfileState;
+
 import android.os.Bundle;
 import android.telecom.CallAttributes;
 import android.telecom.CallException;
 import android.telecom.TelecomManager;
 import android.util.Log;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.flags.FeatureFlags;
 
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionStage;
@@ -38,19 +42,25 @@
     private final CallAttributes mCallAttributes;
     private final CallsManager mCallsManager;
     private final Bundle mExtras;
+    private FeatureFlags mFeatureFlags;
+
+    public void setFeatureFlags(FeatureFlags featureFlags) {
+        mFeatureFlags = featureFlags;
+    }
 
     public IncomingCallTransaction(String callId, CallAttributes callAttributes,
-            CallsManager callsManager, Bundle extras) {
+            CallsManager callsManager, Bundle extras, FeatureFlags featureFlags) {
         super(callsManager.getLock());
         mExtras = extras;
         mCallId = callId;
         mCallAttributes = callAttributes;
         mCallsManager = callsManager;
+        mFeatureFlags = featureFlags;
     }
 
     public IncomingCallTransaction(String callId, CallAttributes callAttributes,
-            CallsManager callsManager) {
-        this(callId, callAttributes, callsManager, new Bundle());
+            CallsManager callsManager, FeatureFlags featureFlags) {
+        this(callId, callAttributes, callsManager, new Bundle(), featureFlags);
     }
 
     @Override
@@ -77,10 +87,19 @@
         }
     }
 
-    private Bundle generateExtras(CallAttributes callAttributes) {
+    @VisibleForTesting
+    public Bundle generateExtras(CallAttributes callAttributes) {
         mExtras.putString(TelecomManager.TRANSACTION_CALL_ID_KEY, mCallId);
         mExtras.putInt(CALL_CAPABILITIES_KEY, callAttributes.getCallCapabilities());
-        mExtras.putInt(TelecomManager.EXTRA_INCOMING_VIDEO_STATE, callAttributes.getCallType());
+        if (mFeatureFlags.transactionalVideoState()) {
+            // Transactional calls need to remap the CallAttributes video state to the existing
+            // VideoProfile for consistency.
+            mExtras.putInt(TelecomManager.EXTRA_INCOMING_VIDEO_STATE,
+                    TransactionalVideoStateToVideoProfileState(callAttributes.getCallType()));
+        } else {
+            mExtras.putInt(TelecomManager.EXTRA_INCOMING_VIDEO_STATE,
+                    callAttributes.getCallType());
+        }
         mExtras.putCharSequence(DISPLAY_NAME_KEY, callAttributes.getDisplayName());
         mExtras.putParcelable(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS,
                 callAttributes.getAddress());
diff --git a/src/com/android/server/telecom/voip/MaybeHoldCallForNewCallTransaction.java b/src/com/android/server/telecom/voip/MaybeHoldCallForNewCallTransaction.java
index a245c1c..3bed088 100644
--- a/src/com/android/server/telecom/voip/MaybeHoldCallForNewCallTransaction.java
+++ b/src/com/android/server/telecom/voip/MaybeHoldCallForNewCallTransaction.java
@@ -26,16 +26,23 @@
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionStage;
 
+/**
+ * This VoipCallTransaction is responsible for holding any active call in favor of a new call
+ * request. If the active call cannot be held or disconnected, the transaction will fail.
+ */
 public class MaybeHoldCallForNewCallTransaction extends VoipCallTransaction {
 
     private static final String TAG = MaybeHoldCallForNewCallTransaction.class.getSimpleName();
     private final CallsManager mCallsManager;
     private final Call mCall;
+    private final boolean mIsCallControlRequest;
 
-    public MaybeHoldCallForNewCallTransaction(CallsManager callsManager, Call call) {
+    public MaybeHoldCallForNewCallTransaction(CallsManager callsManager, Call call,
+            boolean isCallControlRequest) {
         super(callsManager.getLock());
         mCallsManager = callsManager;
         mCall = call;
+        mIsCallControlRequest = isCallControlRequest;
     }
 
     @Override
@@ -43,7 +50,8 @@
         Log.d(TAG, "processTransaction");
         CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
 
-        mCallsManager.transactionHoldPotentialActiveCallForNewCall(mCall, new OutcomeReceiver<>() {
+        mCallsManager.transactionHoldPotentialActiveCallForNewCall(mCall, mIsCallControlRequest,
+                new OutcomeReceiver<>() {
             @Override
             public void onResult(Boolean result) {
                 Log.d(TAG, "processTransaction: onResult");
diff --git a/src/com/android/server/telecom/voip/OutgoingCallTransaction.java b/src/com/android/server/telecom/voip/OutgoingCallTransaction.java
index b2625e6..8c970db 100644
--- a/src/com/android/server/telecom/voip/OutgoingCallTransaction.java
+++ b/src/com/android/server/telecom/voip/OutgoingCallTransaction.java
@@ -21,6 +21,8 @@
 import static android.telecom.CallAttributes.DISPLAY_NAME_KEY;
 import static android.telecom.CallException.CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME;
 
+import static com.android.server.telecom.voip.VideoStateTranslation.TransactionalVideoStateToVideoProfileState;
+
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
@@ -29,9 +31,11 @@
 import android.telecom.TelecomManager;
 import android.util.Log;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallsManager;
 import com.android.server.telecom.LoggedHandlerExecutor;
+import com.android.server.telecom.flags.FeatureFlags;
 
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionStage;
@@ -45,9 +49,14 @@
     private final CallAttributes mCallAttributes;
     private final CallsManager mCallsManager;
     private final Bundle mExtras;
+    private FeatureFlags mFeatureFlags;
+
+    public void setFeatureFlags(FeatureFlags featureFlags) {
+        mFeatureFlags = featureFlags;
+    }
 
     public OutgoingCallTransaction(String callId, Context context, CallAttributes callAttributes,
-            CallsManager callsManager, Bundle extras) {
+            CallsManager callsManager, Bundle extras, FeatureFlags featureFlags) {
         super(callsManager.getLock());
         mCallId = callId;
         mContext = context;
@@ -55,11 +64,12 @@
         mCallsManager = callsManager;
         mExtras = extras;
         mCallingPackage = mContext.getOpPackageName();
+        mFeatureFlags = featureFlags;
     }
 
     public OutgoingCallTransaction(String callId, Context context, CallAttributes callAttributes,
-            CallsManager callsManager) {
-        this(callId, context, callAttributes, callsManager, new Bundle());
+            CallsManager callsManager, FeatureFlags featureFlags) {
+        this(callId, context, callAttributes, callsManager, new Bundle(), featureFlags);
     }
 
     @Override
@@ -121,12 +131,20 @@
         }
     }
 
-    private Bundle generateExtras(CallAttributes callAttributes) {
+    @VisibleForTesting
+    public Bundle generateExtras(CallAttributes callAttributes) {
         mExtras.setDefusable(true);
         mExtras.putString(TelecomManager.TRANSACTION_CALL_ID_KEY, mCallId);
         mExtras.putInt(CALL_CAPABILITIES_KEY, callAttributes.getCallCapabilities());
-        mExtras.putInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
-                callAttributes.getCallType());
+        if (mFeatureFlags.transactionalVideoState()) {
+            // Transactional calls need to remap the CallAttributes video state to the existing
+            // VideoProfile for consistency.
+            mExtras.putInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
+                    TransactionalVideoStateToVideoProfileState(callAttributes.getCallType()));
+        } else {
+            mExtras.putInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
+                    callAttributes.getCallType());
+        }
         mExtras.putCharSequence(DISPLAY_NAME_KEY, callAttributes.getDisplayName());
         return mExtras;
     }
diff --git a/src/com/android/server/telecom/voip/ParallelTransaction.java b/src/com/android/server/telecom/voip/ParallelTransaction.java
index 621892a..e235ead 100644
--- a/src/com/android/server/telecom/voip/ParallelTransaction.java
+++ b/src/com/android/server/telecom/voip/ParallelTransaction.java
@@ -16,6 +16,8 @@
 
 package com.android.server.telecom.voip;
 
+import android.telecom.CallException;
+
 import com.android.server.telecom.LoggedHandlerExecutor;
 import com.android.server.telecom.TelecomSystem;
 
@@ -33,78 +35,56 @@
     }
 
     @Override
-    public void start() {
-        if (mStats != null) mStats.markStarted();
-        // post timeout work
-        CompletableFuture<Void> future = new CompletableFuture<>();
-        mHandler.postDelayed(() -> future.complete(null), TIMEOUT_LIMIT);
-        future.thenApplyAsync((x) -> {
-            if (mCompleted.getAndSet(true)) {
-                return null;
-            }
-            if (mCompleteListener != null) {
-                mCompleteListener.onTransactionTimeout(mTransactionName);
-            }
-            timeout();
-            return null;
-        }, new LoggedHandlerExecutor(mHandler, mTransactionName + "@" + hashCode()
-                + ".s", mLock));
+    public void processTransactions() {
+        if (mSubTransactions == null || mSubTransactions.isEmpty()) {
+            scheduleTransaction();
+            return;
+        }
+        TransactionManager.TransactionCompleteListener subTransactionListener =
+                new TransactionManager.TransactionCompleteListener() {
+                    private final AtomicInteger mCount = new AtomicInteger(mSubTransactions.size());
 
-        if (mSubTransactions != null && mSubTransactions.size() > 0) {
-            TransactionManager.TransactionCompleteListener subTransactionListener =
-                    new TransactionManager.TransactionCompleteListener() {
-                        private final AtomicInteger mCount = new AtomicInteger(mSubTransactions.size());
-
-                        @Override
-                        public void onTransactionCompleted(VoipCallTransactionResult result,
-                                String transactionName) {
-                            if (result.getResult() != VoipCallTransactionResult.RESULT_SUCCEED) {
-                                CompletableFuture.completedFuture(null).thenApplyAsync(
-                                        (x) -> {
-                                            VoipCallTransactionResult mainResult =
-                                                    new VoipCallTransactionResult(
-                                                            VoipCallTransactionResult.RESULT_FAILED,
-                                                            String.format(
-                                                                    "sub transaction %s failed",
-                                                                    transactionName));
-                                            mCompleteListener.onTransactionCompleted(mainResult,
-                                                    mTransactionName);
-                                            finish(mainResult);
-                                            return null;
-                                        }, new LoggedHandlerExecutor(mHandler,
-                                                mTransactionName + "@" + hashCode()
-                                                        + ".oTC", mLock));
-                            } else {
-                                if (mCount.decrementAndGet() == 0) {
-                                    scheduleTransaction();
-                                }
-                            }
-                        }
-
-                        @Override
-                        public void onTransactionTimeout(String transactionName) {
+                    @Override
+                    public void onTransactionCompleted(VoipCallTransactionResult result,
+                            String transactionName) {
+                        if (result.getResult() != VoipCallTransactionResult.RESULT_SUCCEED) {
                             CompletableFuture.completedFuture(null).thenApplyAsync(
                                     (x) -> {
-                                        VoipCallTransactionResult mainResult =
-                                                new VoipCallTransactionResult(
-                                                VoipCallTransactionResult.RESULT_FAILED,
-                                                String.format("sub transaction %s timed out",
-                                                        transactionName));
-                                        mCompleteListener.onTransactionCompleted(mainResult,
+                                        finish(result);
+                                        mCompleteListener.onTransactionCompleted(result,
                                                 mTransactionName);
-                                        finish(mainResult);
                                         return null;
                                     }, new LoggedHandlerExecutor(mHandler,
                                             mTransactionName + "@" + hashCode()
-                                                    + ".oTT", mLock));
+                                                    + ".oTC", mLock));
+                        } else {
+                            if (mCount.decrementAndGet() == 0) {
+                                scheduleTransaction();
+                            }
                         }
-                    };
-            for (VoipCallTransaction transaction : mSubTransactions) {
-                transaction.setCompleteListener(subTransactionListener);
-                transaction.start();
-            }
-        } else {
-            scheduleTransaction();
+                    }
+
+                    @Override
+                    public void onTransactionTimeout(String transactionName) {
+                        CompletableFuture.completedFuture(null).thenApplyAsync(
+                                (x) -> {
+                                    VoipCallTransactionResult mainResult =
+                                            new VoipCallTransactionResult(
+                                                    CallException.CODE_OPERATION_TIMED_OUT,
+                                            String.format("sub transaction %s timed out",
+                                                    transactionName));
+                                    finish(mainResult);
+                                    mCompleteListener.onTransactionCompleted(mainResult,
+                                            mTransactionName);
+                                    return null;
+                                }, new LoggedHandlerExecutor(mHandler,
+                                        mTransactionName + "@" + hashCode()
+                                                + ".oTT", mLock));
+                    }
+                };
+        for (VoipCallTransaction transaction : mSubTransactions) {
+            transaction.setCompleteListener(subTransactionListener);
+            transaction.start();
         }
     }
 }
diff --git a/src/com/android/server/telecom/voip/RequestNewActiveCallTransaction.java b/src/com/android/server/telecom/voip/RequestNewActiveCallTransaction.java
index f586cc3..e3aed8e 100644
--- a/src/com/android/server/telecom/voip/RequestNewActiveCallTransaction.java
+++ b/src/com/android/server/telecom/voip/RequestNewActiveCallTransaction.java
@@ -17,7 +17,6 @@
 package com.android.server.telecom.voip;
 
 import android.os.OutcomeReceiver;
-import android.telecom.CallAttributes;
 import android.telecom.CallException;
 import android.util.Log;
 
@@ -25,6 +24,7 @@
 import com.android.server.telecom.CallState;
 import com.android.server.telecom.CallsManager;
 import com.android.server.telecom.ConnectionServiceFocusManager;
+import com.android.server.telecom.flags.Flags;
 
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionStage;
@@ -69,7 +69,8 @@
             return future;
         }
 
-        if (mCallsManager.getActiveCall() != null) {
+        if (!Flags.transactionalHoldDisconnectsUnholdable() &&
+                mCallsManager.getActiveCall() != null) {
             future.complete(new VoipCallTransactionResult(
                     CallException.CODE_CALL_CANNOT_BE_SET_TO_ACTIVE,
                     "Already an active call. Request hold on current active call."));
diff --git a/src/com/android/server/telecom/voip/RequestVideoStateTransaction.java b/src/com/android/server/telecom/voip/RequestVideoStateTransaction.java
index 64596b1..c1bc343 100644
--- a/src/com/android/server/telecom/voip/RequestVideoStateTransaction.java
+++ b/src/com/android/server/telecom/voip/RequestVideoStateTransaction.java
@@ -18,6 +18,7 @@
 
 import static com.android.server.telecom.voip.VideoStateTranslation.TransactionalVideoStateToVideoProfileState;
 
+import android.telecom.CallException;
 import android.telecom.VideoProfile;
 import android.util.Log;
 
@@ -48,13 +49,8 @@
         if (isRequestingVideoTransmission(mVideoProfileState) &&
                 !mCall.isVideoCallingSupportedByPhoneAccount()) {
             future.complete(new VoipCallTransactionResult(
-                    VoipCallTransactionResult.RESULT_FAILED,
+                    CallException.CODE_ERROR_UNKNOWN /*TODO:: define error code. b/335703584 */,
                     "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(
diff --git a/src/com/android/server/telecom/voip/SerialTransaction.java b/src/com/android/server/telecom/voip/SerialTransaction.java
index 7d5a178..748f285 100644
--- a/src/com/android/server/telecom/voip/SerialTransaction.java
+++ b/src/com/android/server/telecom/voip/SerialTransaction.java
@@ -16,6 +16,8 @@
 
 package com.android.server.telecom.voip;
 
+import android.telecom.CallException;
+
 import com.android.server.telecom.LoggedHandlerExecutor;
 import com.android.server.telecom.TelecomSystem;
 
@@ -37,86 +39,65 @@
     }
 
     @Override
-    public void start() {
-        if (mStats != null) mStats.markStarted();
-        // post timeout work
-        CompletableFuture<Void> future = new CompletableFuture<>();
-        mHandler.postDelayed(() -> future.complete(null), TIMEOUT_LIMIT);
-        future.thenApplyAsync((x) -> {
-            if (mCompleted.getAndSet(true)) {
-                return null;
-            }
-            if (mCompleteListener != null) {
-                mCompleteListener.onTransactionTimeout(mTransactionName);
-            }
-            timeout();
-            return null;
-        }, new LoggedHandlerExecutor(mHandler, mTransactionName + "@" + hashCode()
-                + ".s", mLock));
+    public void processTransactions() {
+        if (mSubTransactions == null || mSubTransactions.isEmpty()) {
+            scheduleTransaction();
+            return;
+        }
+        TransactionManager.TransactionCompleteListener subTransactionListener =
+                new TransactionManager.TransactionCompleteListener() {
+                    private final AtomicInteger mTransactionIndex = new AtomicInteger(0);
 
-        if (mSubTransactions != null && mSubTransactions.size() > 0) {
-            TransactionManager.TransactionCompleteListener subTransactionListener =
-                    new TransactionManager.TransactionCompleteListener() {
-                        private final AtomicInteger mTransactionIndex = new AtomicInteger(0);
-
-                        @Override
-                        public void onTransactionCompleted(VoipCallTransactionResult result,
-                                String transactionName) {
-                            if (result.getResult() != VoipCallTransactionResult.RESULT_SUCCEED) {
-                                handleTransactionFailure();
-                                CompletableFuture.completedFuture(null).thenApplyAsync(
-                                        (x) -> {
-                                            VoipCallTransactionResult mainResult =
-                                                    new VoipCallTransactionResult(
-                                                            VoipCallTransactionResult.RESULT_FAILED,
-                                                            String.format(
-                                                                    "sub transaction %s failed",
-                                                                    transactionName));
-                                            mCompleteListener.onTransactionCompleted(mainResult,
-                                                    mTransactionName);
-                                            finish(mainResult);
-                                            return null;
-                                        }, new LoggedHandlerExecutor(mHandler,
-                                                mTransactionName + "@" + hashCode()
-                                                        + ".oTC", mLock));
-                            } else {
-                                int currTransactionIndex = mTransactionIndex.incrementAndGet();
-                                if (currTransactionIndex < mSubTransactions.size()) {
-                                    VoipCallTransaction transaction = mSubTransactions.get(
-                                            currTransactionIndex);
-                                    transaction.setCompleteListener(this);
-                                    transaction.start();
-                                } else {
-                                    scheduleTransaction();
-                                }
-                            }
-                        }
-
-                        @Override
-                        public void onTransactionTimeout(String transactionName) {
+                    @Override
+                    public void onTransactionCompleted(VoipCallTransactionResult result,
+                            String transactionName) {
+                        if (result.getResult() != VoipCallTransactionResult.RESULT_SUCCEED) {
                             handleTransactionFailure();
                             CompletableFuture.completedFuture(null).thenApplyAsync(
                                     (x) -> {
-                                        VoipCallTransactionResult mainResult =
-                                                new VoipCallTransactionResult(
-                                                VoipCallTransactionResult.RESULT_FAILED,
-                                                String.format("sub transaction %s timed out",
-                                                        transactionName));
-                                        mCompleteListener.onTransactionCompleted(mainResult,
+                                        finish(result);
+                                        mCompleteListener.onTransactionCompleted(result,
                                                 mTransactionName);
-                                        finish(mainResult);
                                         return null;
                                     }, new LoggedHandlerExecutor(mHandler,
                                             mTransactionName + "@" + hashCode()
-                                                    + ".oTT", mLock));
+                                                    + ".oTC", mLock));
+                        } else {
+                            int currTransactionIndex = mTransactionIndex.incrementAndGet();
+                            if (currTransactionIndex < mSubTransactions.size()) {
+                                VoipCallTransaction transaction = mSubTransactions.get(
+                                        currTransactionIndex);
+                                transaction.setCompleteListener(this);
+                                transaction.start();
+                            } else {
+                                scheduleTransaction();
+                            }
                         }
-                    };
-            VoipCallTransaction transaction = mSubTransactions.get(0);
-            transaction.setCompleteListener(subTransactionListener);
-            transaction.start();
-        } else {
-            scheduleTransaction();
-        }
+                    }
+
+                    @Override
+                    public void onTransactionTimeout(String transactionName) {
+                        handleTransactionFailure();
+                        CompletableFuture.completedFuture(null).thenApplyAsync(
+                                (x) -> {
+                                    VoipCallTransactionResult mainResult =
+                                            new VoipCallTransactionResult(
+                                                    CallException.CODE_OPERATION_TIMED_OUT,
+                                            String.format("sub transaction %s timed out",
+                                                    transactionName));
+                                    finish(mainResult);
+                                    mCompleteListener.onTransactionCompleted(mainResult,
+                                            mTransactionName);
+                                    return null;
+                                }, new LoggedHandlerExecutor(mHandler,
+                                        mTransactionName + "@" + hashCode()
+                                                + ".oTT", mLock));
+                    }
+                };
+        VoipCallTransaction transaction = mSubTransactions.get(0);
+        transaction.setCompleteListener(subTransactionListener);
+        transaction.start();
+
     }
 
     public void handleTransactionFailure() {}
diff --git a/src/com/android/server/telecom/voip/TransactionManager.java b/src/com/android/server/telecom/voip/TransactionManager.java
index 299bcc3..0086d07 100644
--- a/src/com/android/server/telecom/voip/TransactionManager.java
+++ b/src/com/android/server/telecom/voip/TransactionManager.java
@@ -146,8 +146,8 @@
             pendingTransactions = new ArrayList<>(mTransactions);
         }
         for (VoipCallTransaction t : pendingTransactions) {
-            t.finish(new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_FAILED,
-                    "clear called"));
+            t.finish(new VoipCallTransactionResult(CallException.CODE_ERROR_UNKNOWN
+                    /* TODO:: define error b/335703584 */, "clear called"));
         }
     }
 
diff --git a/src/com/android/server/telecom/voip/VerifyCallStateChangeTransaction.java b/src/com/android/server/telecom/voip/VerifyCallStateChangeTransaction.java
index b17dedd..5de4b1d 100644
--- a/src/com/android/server/telecom/voip/VerifyCallStateChangeTransaction.java
+++ b/src/com/android/server/telecom/voip/VerifyCallStateChangeTransaction.java
@@ -18,14 +18,12 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.telecom.Call;
-import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.TelecomSystem;
 
-import android.telecom.DisconnectCause;
 import android.telecom.Log;
 
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionStage;
-import java.util.concurrent.TimeUnit;
 
 /**
  * VerifyCallStateChangeTransaction is a transaction that verifies a CallState change and has
@@ -35,37 +33,30 @@
  */
 public class VerifyCallStateChangeTransaction extends VoipCallTransaction {
     private static final String TAG = VerifyCallStateChangeTransaction.class.getSimpleName();
-    public static final int FAILURE_CODE = 0;
-    public static final int SUCCESS_CODE = 1;
-    public static final int TIMEOUT_SECONDS = 2;
+    private static final long CALL_STATE_TIMEOUT_MILLISECONDS = 2000L;
     private final Call mCall;
-    private final CallsManager mCallsManager;
     private final int mTargetCallState;
-    private final boolean mShouldDisconnectUponFailure;
-    private final CompletableFuture<Integer> mCallStateOrTimeoutResult = new CompletableFuture<>();
     private final CompletableFuture<VoipCallTransactionResult> mTransactionResult =
             new CompletableFuture<>();
 
-    @VisibleForTesting
-    public Call.CallStateListener mCallStateListenerImpl = new Call.CallStateListener() {
+    private final Call.CallStateListener mCallStateListenerImpl = new Call.CallStateListener() {
         @Override
         public void onCallStateChanged(int newCallState) {
             Log.d(TAG, "newState=[%d], expectedState=[%d]", newCallState, mTargetCallState);
             if (newCallState == mTargetCallState) {
-                mCallStateOrTimeoutResult.complete(SUCCESS_CODE);
+                mTransactionResult.complete(new VoipCallTransactionResult(
+                        VoipCallTransactionResult.RESULT_SUCCEED, TAG));
             }
             // NOTE:: keep listening to the call state until the timeout is reached. It's possible
             // another call state is reached in between...
         }
     };
 
-    public VerifyCallStateChangeTransaction(CallsManager callsManager, Call call,
-            int targetCallState, boolean shouldDisconnectUponFailure) {
-        super(callsManager.getLock());
-        mCallsManager = callsManager;
+    public VerifyCallStateChangeTransaction(TelecomSystem.SyncRoot lock,  Call call,
+            int targetCallState) {
+        super(lock, CALL_STATE_TIMEOUT_MILLISECONDS);
         mCall = call;
         mTargetCallState = targetCallState;
-        mShouldDisconnectUponFailure = shouldDisconnectUponFailure;
     }
 
     @Override
@@ -73,68 +64,23 @@
         Log.d(TAG, "processTransaction:");
         // It's possible the Call is already in the expected call state
         if (isNewCallStateTargetCallState()) {
-            mTransactionResult.complete(
-                    new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_SUCCEED,
-                            TAG));
+            mTransactionResult.complete(new VoipCallTransactionResult(
+                    VoipCallTransactionResult.RESULT_SUCCEED, TAG));
             return mTransactionResult;
         }
-        initCallStateListenerOnTimeout();
-        // At this point, the mCallStateOrTimeoutResult has been completed. There are 2 scenarios:
-        // (1) newCallState == targetCallState --> the transaction is successful
-        // (2) timeout is reached --> evaluate the current call state and complete the t accordingly
-        // also need to do cleanup for the transaction
-        evaluateCallStateUponChangeOrTimeout();
-
+        mCall.addCallStateListener(mCallStateListenerImpl);
         return mTransactionResult;
     }
 
+    @Override
+    public void finishTransaction() {
+        mCall.removeCallStateListener(mCallStateListenerImpl);
+    }
+
     private boolean isNewCallStateTargetCallState() {
         return mCall.getState() == mTargetCallState;
     }
 
-    private void initCallStateListenerOnTimeout() {
-        mCall.addCallStateListener(mCallStateListenerImpl);
-        mCallStateOrTimeoutResult.completeOnTimeout(FAILURE_CODE, TIMEOUT_SECONDS,
-                TimeUnit.SECONDS);
-    }
-
-    private void evaluateCallStateUponChangeOrTimeout() {
-        mCallStateOrTimeoutResult.thenAcceptAsync((result) -> {
-            Log.i(TAG, "processTransaction: thenAcceptAsync: result=[%s]", result);
-            mCall.removeCallStateListener(mCallStateListenerImpl);
-            if (isNewCallStateTargetCallState()) {
-                mTransactionResult.complete(
-                        new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_SUCCEED,
-                                TAG));
-            } else {
-                maybeDisconnectCall();
-                mTransactionResult.complete(
-                        new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_FAILED,
-                                TAG));
-            }
-        }).exceptionally(exception -> {
-            Log.i(TAG, "hit exception=[%s] while completing future", exception);
-            mTransactionResult.complete(
-                    new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_FAILED,
-                            TAG));
-            return null;
-        });
-    }
-
-    private void maybeDisconnectCall() {
-        if (mShouldDisconnectUponFailure) {
-            mCallsManager.markCallAsDisconnected(mCall,
-                    new DisconnectCause(DisconnectCause.ERROR,
-                            "did not hold in timeout window"));
-            mCallsManager.markCallAsRemoved(mCall);
-        }
-    }
-
-    @VisibleForTesting
-    public CompletableFuture<Integer> getCallStateOrTimeoutResult() {
-        return mCallStateOrTimeoutResult;
-    }
-
     @VisibleForTesting
     public CompletableFuture<VoipCallTransactionResult> getTransactionResult() {
         return mTransactionResult;
diff --git a/src/com/android/server/telecom/voip/VideoStateTranslation.java b/src/com/android/server/telecom/voip/VideoStateTranslation.java
index 615e4bc..3812d15 100644
--- a/src/com/android/server/telecom/voip/VideoStateTranslation.java
+++ b/src/com/android/server/telecom/voip/VideoStateTranslation.java
@@ -20,6 +20,11 @@
 import android.telecom.Log;
 import android.telecom.VideoProfile;
 
+import com.android.server.telecom.AnomalyReporterAdapter;
+import com.android.server.telecom.AnomalyReporterAdapterImpl;
+
+import java.util.UUID;
+
 /**
  * This remapping class is needed because {@link VideoProfile} has more fine grain levels of video
  * states as apposed to Transactional video states (defined in  {@link CallAttributes.CallType}.
@@ -41,15 +46,16 @@
      * 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));
+    public static int TransactionalVideoStateToVideoProfileState(int callType) {
+        if (callType == CallAttributes.AUDIO_CALL) {
+            Log.i(TAG, "CallAttributes.AUDIO_CALL --> VideoProfile.STATE_AUDIO_ONLY");
             return VideoProfile.STATE_AUDIO_ONLY;
-        } else {
-            Log.i(TAG, "%s --> VideoProfile.STATE_BIDIRECTIONAL",
-                    TransactionalVideoState_toString(transactionalVideo));
+        } else if (callType == CallAttributes.VIDEO_CALL) {
+            Log.i(TAG, "CallAttributes.VIDEO_CALL--> VideoProfile.STATE_BIDIRECTIONAL");
             return VideoProfile.STATE_BIDIRECTIONAL;
+        } else {
+            Log.w(TAG, "CallType=[%d] does not have a VideoProfile mapping", callType);
+            return VideoProfile.STATE_AUDIO_ONLY;
         }
     }
 
@@ -58,26 +64,36 @@
      * 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;
+        switch (videoProfileState) {
+            case VideoProfile.STATE_AUDIO_ONLY -> {
+                Log.i(TAG, "%s --> CallAttributes.AUDIO_CALL",
+                        VideoProfileStateToString(videoProfileState));
+                return CallAttributes.AUDIO_CALL;
+            }
+            case VideoProfile.STATE_BIDIRECTIONAL, VideoProfile.STATE_TX_ENABLED,
+                    VideoProfile.STATE_RX_ENABLED -> {
+                Log.i(TAG, "%s --> CallAttributes.VIDEO_CALL",
+                        VideoProfileStateToString(videoProfileState));
+                return CallAttributes.VIDEO_CALL;
+            }
+            default -> {
+                Log.w(TAG, "VideoProfile=[%d] does not have a CallType mapping", videoProfileState);
+                return CallAttributes.AUDIO_CALL;
+            }
         }
     }
 
-    private static String TransactionalVideoState_toString(int transactionalVideoState) {
+    public static String TransactionalVideoStateToString(int transactionalVideoState) {
         if (transactionalVideoState == CallAttributes.AUDIO_CALL) {
             return "CallAttributes.AUDIO_CALL";
-        } else {
+        } else if (transactionalVideoState == CallAttributes.VIDEO_CALL) {
             return "CallAttributes.VIDEO_CALL";
+        } else {
+            return "CallAttributes.UNKNOWN";
         }
     }
 
-    private static String VideoProfileState_toString(int videoProfileState) {
+    private static String VideoProfileStateToString(int videoProfileState) {
         switch (videoProfileState) {
             case VideoProfile.STATE_BIDIRECTIONAL -> {
                 return "VideoProfile.STATE_BIDIRECTIONAL";
@@ -88,7 +104,12 @@
             case VideoProfile.STATE_TX_ENABLED -> {
                 return "VideoProfile.STATE_TX_ENABLED";
             }
+            case VideoProfile.STATE_AUDIO_ONLY -> {
+                return "VideoProfile.STATE_AUDIO_ONLY";
+            }
+            default -> {
+                return "VideoProfile.UNKNOWN";
+            }
         }
-        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 3c91158..a589a6d 100644
--- a/src/com/android/server/telecom/voip/VoipCallTransaction.java
+++ b/src/com/android/server/telecom/voip/VoipCallTransaction.java
@@ -18,8 +18,10 @@
 
 import android.os.Handler;
 import android.os.HandlerThread;
+import android.telecom.CallException;
 import android.telecom.Log;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.telecom.LoggedHandlerExecutor;
 import com.android.server.telecom.TelecomSystem;
 import com.android.server.telecom.flags.Flags;
@@ -34,7 +36,7 @@
 
 public class VoipCallTransaction {
     //TODO: add log events
-    protected static final long TIMEOUT_LIMIT = 5000L;
+    private static final long DEFAULT_TRANSACTION_TIMEOUT_MS = 5000L;
 
     /**
      * Tracks stats about a transaction for logging purposes.
@@ -129,96 +131,141 @@
 
     protected final AtomicBoolean mCompleted = new AtomicBoolean(false);
     protected final String mTransactionName = this.getClass().getSimpleName();
-    private HandlerThread mHandlerThread;
-    protected Handler mHandler;
+    private final HandlerThread mHandlerThread;
+    protected final Handler mHandler;
     protected TransactionManager.TransactionCompleteListener mCompleteListener;
-    protected List<VoipCallTransaction> mSubTransactions;
-    protected TelecomSystem.SyncRoot mLock;
+    protected final List<VoipCallTransaction> mSubTransactions;
+    protected final TelecomSystem.SyncRoot mLock;
+    protected final long mTransactionTimeoutMs;
     protected final Stats mStats;
 
     public VoipCallTransaction(
-            List<VoipCallTransaction> subTransactions, TelecomSystem.SyncRoot lock) {
+            List<VoipCallTransaction> subTransactions, TelecomSystem.SyncRoot lock,
+            long timeoutMs) {
         mSubTransactions = subTransactions;
         mHandlerThread = new HandlerThread(this.toString());
         mHandlerThread.start();
         mHandler = new Handler(mHandlerThread.getLooper());
         mLock = lock;
+        mTransactionTimeoutMs = timeoutMs;
         mStats = Flags.enableCallSequencing() ? new Stats() : null;
     }
 
-    public VoipCallTransaction(TelecomSystem.SyncRoot lock) {
-        this(null /** mSubTransactions */, lock);
+    public VoipCallTransaction(List<VoipCallTransaction> subTransactions,
+            TelecomSystem.SyncRoot lock) {
+        this(subTransactions, lock, DEFAULT_TRANSACTION_TIMEOUT_MS);
+    }
+    public VoipCallTransaction(TelecomSystem.SyncRoot lock, long timeoutMs) {
+        this(null /* mSubTransactions */, lock, timeoutMs);
     }
 
-    public void start() {
+    public VoipCallTransaction(TelecomSystem.SyncRoot lock) {
+        this(null /* mSubTransactions */, lock);
+    }
+
+    public final void start() {
         if (mStats != null) mStats.markStarted();
         // post timeout work
         CompletableFuture<Void> future = new CompletableFuture<>();
-        mHandler.postDelayed(() -> future.complete(null), TIMEOUT_LIMIT);
+        mHandler.postDelayed(() -> future.complete(null), mTransactionTimeoutMs);
         future.thenApplyAsync((x) -> {
-            if (mCompleted.getAndSet(true)) {
-                return null;
-            }
-            if (mCompleteListener != null) {
-                mCompleteListener.onTransactionTimeout(mTransactionName);
-            }
             timeout();
             return null;
         }, new LoggedHandlerExecutor(mHandler, mTransactionName + "@" + hashCode()
                 + ".s", mLock));
 
+        processTransactions();
+    }
+
+    /**
+     * By default, this processes this transaction. For VoipCallTransactions with sub-transactions,
+     * this implementation should be overwritten to handle also processing sub-transactions.
+     */
+    protected void processTransactions() {
         scheduleTransaction();
     }
 
-    protected void scheduleTransaction() {
+    /**
+     * This method is called when the transaction has finished either successfully or exceptionally.
+     * VoipCallTransactions that are extending this class should override this method to clean up
+     * any leftover state.
+     */
+    protected void finishTransaction() {
+
+    }
+
+    protected final void scheduleTransaction() {
         LoggedHandlerExecutor executor = new LoggedHandlerExecutor(mHandler,
-                mTransactionName + "@" + hashCode() + ".pT", mLock);
+                mTransactionName + "@" + hashCode() + ".sT", mLock);
         CompletableFuture<Void> future = CompletableFuture.completedFuture(null);
         future.thenComposeAsync(this::processTransaction, executor)
                 .thenApplyAsync((Function<VoipCallTransactionResult, Void>) result -> {
-                    mCompleted.set(true);
-                    if (mCompleteListener != null) {
-                        mCompleteListener.onTransactionCompleted(result, mTransactionName);
-                    }
-                    finish(result);
+                    notifyListenersOfResult(result);
                     return null;
-                    }, executor)
-                .exceptionallyAsync((throwable -> {
+                }, executor)
+                .exceptionally((throwable -> {
+                    // Do NOT wait for the timeout in order to finish this failed transaction.
+                    // Instead, propagate the failure to the other transactions immediately!
+                    String errorMessage = throwable != null ? throwable.getMessage() :
+                            "encountered an exception while processing " + mTransactionName;
+                    notifyListenersOfResult(new VoipCallTransactionResult(
+                            CallException.CODE_ERROR_UNKNOWN, errorMessage));
                     Log.e(this, throwable, "Error while executing transaction.");
                     return null;
-                }), executor);
+                }));
     }
 
-    public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+    protected void notifyListenersOfResult(VoipCallTransactionResult result){
+        mCompleted.set(true);
+        finish(result);
+        if (mCompleteListener != null) {
+            mCompleteListener.onTransactionCompleted(result, mTransactionName);
+        }
+    }
+
+    protected CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
         return CompletableFuture.completedFuture(
                 new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_SUCCEED, null));
     }
 
-    public void setCompleteListener(TransactionManager.TransactionCompleteListener listener) {
+    public final void setCompleteListener(TransactionManager.TransactionCompleteListener listener) {
         mCompleteListener = listener;
     }
 
-    public void timeout() {
+    @VisibleForTesting
+    public final void timeout() {
+        if (mCompleted.getAndSet(true)) {
+            return;
+        }
         finish(true, null);
+        if (mCompleteListener != null) {
+            mCompleteListener.onTransactionTimeout(mTransactionName);
+        }
     }
 
-    public void finish(VoipCallTransactionResult result) {
+    @VisibleForTesting
+    public final Handler getHandler() {
+        return mHandler;
+    }
+
+    public final void finish(VoipCallTransactionResult result) {
         finish(false, result);
     }
 
-    public void finish(boolean isTimedOut, VoipCallTransactionResult result) {
+    private void finish(boolean isTimedOut, VoipCallTransactionResult result) {
         if (mStats != null) mStats.markComplete(isTimedOut, result);
+        finishTransaction();
         // finish all sub transactions
         if (mSubTransactions != null && !mSubTransactions.isEmpty()) {
             mSubTransactions.forEach( t -> t.finish(isTimedOut, result));
         }
-        mHandlerThread.quit();
+        mHandlerThread.quitSafely();
     }
 
     /**
      * @return Stats related to this transaction if stats are enabled, null otherwise.
      */
-    public Stats getStats() {
+    public final Stats getStats() {
         return mStats;
     }
 }
diff --git a/src/com/android/server/telecom/voip/VoipCallTransactionResult.java b/src/com/android/server/telecom/voip/VoipCallTransactionResult.java
index ffc0255..50871f2 100644
--- a/src/com/android/server/telecom/voip/VoipCallTransactionResult.java
+++ b/src/com/android/server/telecom/voip/VoipCallTransactionResult.java
@@ -22,7 +22,10 @@
 
 public class VoipCallTransactionResult {
     public static final int RESULT_SUCCEED = 0;
-    public static final int RESULT_FAILED = 1;
+
+    // NOTE: if the VoipCallTransactionResult should not use the RESULT_SUCCEED to represent a
+    // successful transaction, use an error code defined in the
+    // {@link android.telecom.CallException} class
 
     private final int mResult;
     private final String mMessage;
diff --git a/testapps/transactionalVoipApp/res/values-ca/strings.xml b/testapps/transactionalVoipApp/res/values-ca/strings.xml
index 5500444..00e028e 100644
--- a/testapps/transactionalVoipApp/res/values-ca/strings.xml
+++ b/testapps/transactionalVoipApp/res/values-ca/strings.xml
@@ -31,7 +31,7 @@
     <string name="request_earpiece_endpoint" msgid="6649571985089296573">"Auricular"</string>
     <string name="request_speaker_endpoint" msgid="1033259535289845405">"Altaveu"</string>
     <string name="request_bluetooth_endpoint" msgid="5933254250623451836">"Bluetooth"</string>
-    <string name="start_stream" msgid="3567634786280097431">"inicia la reproducció en línia"</string>
+    <string name="start_stream" msgid="3567634786280097431">"inicia l\'estríming"</string>
     <string name="crash_app" msgid="2548690390730057704">"llança una excepció"</string>
     <string name="update_notification" msgid="8677916482672588779">"actualitza la notificació a l\'estil de trucada en curs"</string>
 </resources>
diff --git a/testapps/transactionalVoipApp/res/values-fr-rCA/strings.xml b/testapps/transactionalVoipApp/res/values-fr-rCA/strings.xml
index d58aa13..414caa8 100644
--- a/testapps/transactionalVoipApp/res/values-fr-rCA/strings.xml
+++ b/testapps/transactionalVoipApp/res/values-fr-rCA/strings.xml
@@ -20,7 +20,7 @@
     <string name="app_name" msgid="2907804426411305091">"Activité de test de l\'API transactionnelle"</string>
     <string name="in_call_activity_name" msgid="7545884666442897585">"Activité transactionnelle durant l\'appel"</string>
     <string name="register_phone_account" msgid="1920315963082350332">"Inscrire un compte téléphonique"</string>
-    <string name="start_foreground_service" msgid="8968755699895128574">"Démarrer FGS (simuler TA + application en arrière-plan)"</string>
+    <string name="start_foreground_service" msgid="8968755699895128574">"Démarrer FGS (simuler TA + appli en arrière-plan)"</string>
     <string name="start_outgoing" msgid="1441644037370361864">"Démarrer un appel sortant"</string>
     <string name="start_incoming" msgid="6444983300186361271">"Démarrer un appel entrant"</string>
     <string name="get_call_id" msgid="5513943242738347108">"identifiant de l\'appel non défini"</string>
diff --git a/tests/src/com/android/server/telecom/tests/BasicCallTests.java b/tests/src/com/android/server/telecom/tests/BasicCallTests.java
index 4bca30d..7646c2d 100644
--- a/tests/src/com/android/server/telecom/tests/BasicCallTests.java
+++ b/tests/src/com/android/server/telecom/tests/BasicCallTests.java
@@ -1036,6 +1036,7 @@
         call.setTargetPhoneAccount(mPhoneAccountA1.getAccountHandle());
         assert(call.isVideoCallingSupportedByPhoneAccount());
         assertEquals(VideoProfile.STATE_BIDIRECTIONAL, call.getVideoState());
+        call.setIsCreateConnectionComplete(true);
     }
 
     /**
@@ -1059,6 +1060,7 @@
         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/BlockedNumbersUtilTests.java b/tests/src/com/android/server/telecom/tests/BlockedNumbersUtilTests.java
index 696867e..57aee62 100644
--- a/tests/src/com/android/server/telecom/tests/BlockedNumbersUtilTests.java
+++ b/tests/src/com/android/server/telecom/tests/BlockedNumbersUtilTests.java
@@ -16,10 +16,13 @@
 
 package com.android.server.telecom.tests;
 
+import static org.junit.Assert.assertFalse;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.app.Notification;
 import android.app.NotificationManager;
@@ -27,6 +30,7 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.server.telecom.settings.BlockedNumbersActivity;
 import com.android.server.telecom.settings.BlockedNumbersUtil;
 
 import org.junit.Before;
@@ -58,4 +62,16 @@
         NotificationManager mgr = mComponentContextFixture.getNotificationManager();
         verify(mgr).cancelAsUser(isNull(), anyInt(), any(UserHandle.class));
     }
+
+    /**
+     * Verify that when Telephony isn't present we can still check if a number is an emergency
+     * number in the {@link BlockedNumbersActivity} and not crash.
+     */
+    @SmallTest
+    @Test
+    public void testBlockedNumbersActivityEmergencyCheckWithNoTelephony() {
+        when(mComponentContextFixture.getTelephonyManager().isEmergencyNumber(anyString()))
+                .thenThrow(new UnsupportedOperationException("Bee boop"));
+        assertFalse(BlockedNumbersActivity.isEmergencyNumber(mContext, "911"));
+    }
 }
diff --git a/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java b/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java
index c516c8e..d5e903b 100644
--- a/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java
@@ -60,9 +60,11 @@
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 
+import static org.mockito.Mockito.reset;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.concurrent.Executor;
 
 @RunWith(JUnit4.class)
 public class BluetoothDeviceManagerTest extends TelecomTestCase {
@@ -75,6 +77,7 @@
     @Mock BluetoothLeAudio mBluetoothLeAudio;
     @Mock AudioManager mockAudioManager;
     @Mock AudioDeviceInfo mSpeakerInfo;
+    @Mock Executor mExecutor;
 
     BluetoothDeviceManager mBluetoothDeviceManager;
     BluetoothProfile.ServiceListener serviceListenerUnderTest;
@@ -114,6 +117,7 @@
         mCommunicationDeviceTracker.setBluetoothRouteManager(mRouteManager);
 
         mockAudioManager = mContext.getSystemService(AudioManager.class);
+        mExecutor = mContext.getMainExecutor();
 
         ArgumentCaptor<BluetoothProfile.ServiceListener> serviceCaptor =
                 ArgumentCaptor.forClass(BluetoothProfile.ServiceListener.class);
@@ -750,6 +754,31 @@
         assertTrue(mBluetoothDeviceManager.isInbandRingingEnabled());
     }
 
+    @SmallTest
+    @Test
+    public void testRegisterLeAudioCallbackNoPostpone() {
+        reset(mBluetoothLeAudio);
+        when(mFeatureFlags.postponeRegisterToLeaudio()).thenReturn(false);
+        serviceListenerUnderTest.onServiceConnected(BluetoothProfile.LE_AUDIO,
+                        (BluetoothProfile) mBluetoothLeAudio);
+        // Second time on purpose
+        serviceListenerUnderTest.onServiceConnected(BluetoothProfile.LE_AUDIO,
+                        (BluetoothProfile) mBluetoothLeAudio);
+        verify(mExecutor, times(0)).execute(any());
+        verify(mBluetoothLeAudio, times(1)).registerCallback(any(Executor.class),
+                        any(BluetoothLeAudio.Callback.class));
+    }
+
+    @SmallTest
+    @Test
+    public void testRegisterLeAudioCallbackWithPostpone() {
+        reset(mBluetoothLeAudio);
+        when(mFeatureFlags.postponeRegisterToLeaudio()).thenReturn(true);
+        serviceListenerUnderTest.onServiceConnected(BluetoothProfile.LE_AUDIO,
+                        (BluetoothProfile) mBluetoothLeAudio);
+        verify(mExecutor, times(1)).execute(any());
+    }
+
     private void assertClearHearingAidOrLeCommunicationDevice(
             boolean flagEnabled, int device_type
     ) {
diff --git a/tests/src/com/android/server/telecom/tests/BluetoothRouteManagerTest.java b/tests/src/com/android/server/telecom/tests/BluetoothRouteManagerTest.java
index 07dd350..1c885c1 100644
--- a/tests/src/com/android/server/telecom/tests/BluetoothRouteManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/BluetoothRouteManagerTest.java
@@ -21,6 +21,7 @@
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -249,7 +250,18 @@
 
     @SmallTest
     @Test
-    public void testConnectBtWithoutAddress() {
+    public void testConnectBtWithoutAddress_SwitchingBtDeviceFlag() {
+        when(mFeatureFlags.resolveSwitchingBtDevicesComputation()).thenReturn(true);
+        verifyConnectBtWithoutAddress();
+    }
+
+    @SmallTest
+    @Test
+    public void testConnectBtWithoutAddress_SwitchingBtDeviceFlagDisabled() {
+        verifyConnectBtWithoutAddress();
+    }
+
+    private void verifyConnectBtWithoutAddress() {
         when(mFeatureFlags.useActualAddressToEnterConnectingState()).thenReturn(true);
         BluetoothRouteManager sm = setupStateMachine(
                 BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX, DEVICE1);
@@ -266,7 +278,15 @@
         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
-        verifyConnectionAttempt(DEVICE1, 1);
+        // We should not expect explicit connection attempt (BluetoothDeviceManager#connectAudio)
+        // as the device is already "connected" as per how the state machine was initialized.
+        if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
+            verify(mDeviceManager, never()).disconnectAudio();
+        } else {
+            // Legacy behavior
+            verifyConnectionAttempt(DEVICE1, 1);
+            verify(mDeviceManager, times(1)).disconnectAudio();
+        }
         assertEquals(BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX
                         + ":" + DEVICE1.getAddress(),
                 sm.getCurrentState().getName());
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioManagerTest.java b/tests/src/com/android/server/telecom/tests/CallAudioManagerTest.java
index 97405a3..1d641ba 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioManagerTest.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.assertNull;
@@ -34,6 +36,8 @@
 import static org.mockito.Mockito.when;
 
 import android.media.ToneGenerator;
+import android.os.Handler;
+import android.os.Looper;
 import android.telecom.DisconnectCause;
 import android.util.SparseArray;
 
@@ -67,6 +71,7 @@
 import java.util.Arrays;
 import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.concurrent.CompletableFuture;
 import java.util.stream.Collectors;
 
 @RunWith(JUnit4.class)
@@ -423,9 +428,12 @@
         Call call = mock(Call.class);
         ArgumentCaptor<CallAudioModeStateMachine.MessageArgs> captor = makeNewCaptor();
         when(call.getState()).thenReturn(CallState.RINGING);
+        handleWaitForBtIcsBinding(call);
 
         // Make sure appropriate messages are sent when we add a RINGING call
         mCallAudioManager.onCallAdded(call);
+        mCallAudioManager.getCallRingingFuture().join();
+        waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT);
 
         assertEquals(call, mCallAudioManager.getForegroundCall());
         verify(mCallAudioRouteStateMachine).sendMessageWithSessionInfo(
@@ -556,10 +564,14 @@
 
         Call call = createAudioProcessingCall();
 
+
         when(call.getState()).thenReturn(CallState.SIMULATED_RINGING);
+        handleWaitForBtIcsBinding(call);
 
         mCallAudioManager.onCallStateChanged(call, CallState.AUDIO_PROCESSING,
                 CallState.SIMULATED_RINGING);
+        mCallAudioManager.getCallRingingFuture().join();
+        waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT);
         verify(mPlayerFactory, never()).createPlayer(any(Call.class), anyInt());
         CallAudioModeStateMachine.MessageArgs expectedArgs = new Builder()
                 .setHasActiveOrDialingCalls(false)
@@ -810,9 +822,12 @@
     private Call createSimulatedRingingCall() {
         Call call = mock(Call.class);
         when(call.getState()).thenReturn(CallState.SIMULATED_RINGING);
+        handleWaitForBtIcsBinding(call);
         ArgumentCaptor<CallAudioModeStateMachine.MessageArgs> captor = makeNewCaptor();
 
         mCallAudioManager.onCallAdded(call);
+        mCallAudioManager.getCallRingingFuture().join();
+        waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT);
 
         assertEquals(call, mCallAudioManager.getForegroundCall());
 
@@ -838,8 +853,11 @@
     private Call createIncomingCall() {
         Call call = mock(Call.class);
         when(call.getState()).thenReturn(CallState.RINGING);
+        handleWaitForBtIcsBinding(call);
 
         mCallAudioManager.onCallAdded(call);
+        mCallAudioManager.getCallRingingFuture().join();
+        waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT);
         assertEquals(call, mCallAudioManager.getForegroundCall());
         ArgumentCaptor<CallAudioModeStateMachine.MessageArgs> captor =
                 ArgumentCaptor.forClass(CallAudioModeStateMachine.MessageArgs.class);
@@ -924,4 +942,10 @@
         assertEquals(expected.isTonePlaying, actual.isTonePlaying);
         assertEquals(expected.foregroundCallIsVoip, actual.foregroundCallIsVoip);
     }
+
+    private void handleWaitForBtIcsBinding(Call call) {
+        when(mFlags.separatelyBindToBtIncallService()).thenReturn(true);
+        CompletableFuture<Boolean> btBindingFuture = CompletableFuture.completedFuture(true);
+        when(call.getBtIcsFuture()).thenReturn(btBindingFuture);
+    }
 }
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
index 0a53eb0..59473bd 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
@@ -20,7 +20,6 @@
 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;
@@ -35,7 +34,7 @@
 import static com.android.server.telecom.CallAudioRouteAdapter.SPEAKER_ON;
 import static com.android.server.telecom.CallAudioRouteAdapter.STREAMING_FORCE_DISABLED;
 import static com.android.server.telecom.CallAudioRouteAdapter.STREAMING_FORCE_ENABLED;
-import static com.android.server.telecom.CallAudioRouteAdapter.SWITCH_EARPIECE;
+import static com.android.server.telecom.CallAudioRouteAdapter.SWITCH_BASELINE_ROUTE;
 import static com.android.server.telecom.CallAudioRouteAdapter.SWITCH_FOCUS;
 import static com.android.server.telecom.CallAudioRouteAdapter.USER_SWITCH_BLUETOOTH;
 import static com.android.server.telecom.CallAudioRouteAdapter.USER_SWITCH_EARPIECE;
@@ -48,30 +47,37 @@
 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.mock;
 import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeAudio;
 import android.media.AudioDeviceInfo;
 import android.media.AudioManager;
 import android.media.IAudioService;
 import android.media.audiopolicy.AudioProductStrategy;
 import android.os.UserHandle;
 import android.telecom.CallAudioState;
+import android.telecom.VideoProfile;
 
 import androidx.test.filters.SmallTest;
 
 import com.android.server.telecom.AudioRoute;
+import com.android.server.telecom.Call;
 import com.android.server.telecom.CallAudioManager;
 import com.android.server.telecom.CallAudioRouteController;
 import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.PendingAudioRoute;
+import com.android.server.telecom.StatusBarNotifier;
+import com.android.server.telecom.TelecomSystem;
 import com.android.server.telecom.WiredHeadsetManager;
+import com.android.server.telecom.bluetooth.BluetoothDeviceManager;
 import com.android.server.telecom.bluetooth.BluetoothRouteManager;
 
 import org.junit.After;
@@ -82,6 +88,7 @@
 import org.mockito.Mock;
 
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 
 @RunWith(JUnit4.class)
@@ -94,6 +101,14 @@
     @Mock CallAudioManager.AudioServiceFactory mAudioServiceFactory;
     @Mock IAudioService mAudioService;
     @Mock BluetoothRouteManager mBluetoothRouteManager;
+    @Mock BluetoothDeviceManager mBluetoothDeviceManager;
+    @Mock BluetoothAdapter mBluetoothAdapter;
+    @Mock StatusBarNotifier mockStatusBarNotifier;
+    @Mock AudioDeviceInfo mAudioDeviceInfo;
+    @Mock BluetoothLeAudio mBluetoothLeAudio;
+    @Mock CallAudioManager mCallAudioManager;
+    @Mock Call mCall;
+    @Mock private TelecomSystem.SyncRoot mLock;
     private AudioRoute mEarpieceRoute;
     private AudioRoute mSpeakerRoute;
     private static final String BT_ADDRESS_1 = "00:00:00:00:00:01";
@@ -109,7 +124,7 @@
         @Override
         public AudioRoute create(@AudioRoute.AudioRouteType int type, String bluetoothAddress,
                                  AudioManager audioManager) {
-            return new AudioRoute(type, bluetoothAddress, null);
+            return new AudioRoute(type, bluetoothAddress, mAudioDeviceInfo);
         }
     };
 
@@ -124,18 +139,39 @@
                 });
         when(mAudioManager.getPreferredDeviceForStrategy(nullable(AudioProductStrategy.class)))
                 .thenReturn(null);
+        when(mAudioManager.getAvailableCommunicationDevices())
+                .thenReturn(List.of(mAudioDeviceInfo));
+        when(mAudioManager.getCommunicationDevice()).thenReturn(mAudioDeviceInfo);
+        when(mAudioManager.setCommunicationDevice(any(AudioDeviceInfo.class)))
+                .thenReturn(true);
         when(mAudioServiceFactory.getAudioService()).thenReturn(mAudioService);
         when(mContext.getAttributionTag()).thenReturn("");
         doNothing().when(mCallsManager).onCallAudioStateChanged(any(CallAudioState.class),
                 any(CallAudioState.class));
         when(mCallsManager.getCurrentUserHandle()).thenReturn(
                 new UserHandle(UserHandle.USER_SYSTEM));
+        when(mCallsManager.getLock()).thenReturn(mLock);
+        when(mBluetoothRouteManager.getDeviceManager()).thenReturn(mBluetoothDeviceManager);
+        when(mBluetoothDeviceManager.connectAudio(any(BluetoothDevice.class), anyInt()))
+                .thenReturn(true);
+        when(mBluetoothDeviceManager.getBluetoothAdapter()).thenReturn(mBluetoothAdapter);
+        when(mBluetoothAdapter.getActiveDevices(anyInt())).thenReturn(List.of(BLUETOOTH_DEVICE_1));
+        when(mBluetoothDeviceManager.getLeAudioService()).thenReturn(mBluetoothLeAudio);
+        when(mBluetoothLeAudio.getGroupId(any(BluetoothDevice.class))).thenReturn(1);
+        when(mBluetoothLeAudio.getConnectedGroupLeadDevice(anyInt()))
+                .thenReturn(BLUETOOTH_DEVICE_1);
+        when(mAudioDeviceInfo.getAddress()).thenReturn(BT_ADDRESS_1);
         mController = new CallAudioRouteController(mContext, mCallsManager, mAudioServiceFactory,
-                mAudioRouteFactory, mWiredHeadsetManager, mBluetoothRouteManager);
+                mAudioRouteFactory, mWiredHeadsetManager,
+                mBluetoothRouteManager, mockStatusBarNotifier, mFeatureFlags);
         mController.setAudioRouteFactory(mAudioRouteFactory);
         mController.setAudioManager(mAudioManager);
         mEarpieceRoute = new AudioRoute(AudioRoute.TYPE_EARPIECE, null, null);
         mSpeakerRoute = new AudioRoute(AudioRoute.TYPE_SPEAKER, null, null);
+        mController.setCallAudioManager(mCallAudioManager);
+        when(mCallAudioManager.getForegroundCall()).thenReturn(mCall);
+        when(mCall.getVideoState()).thenReturn(VideoProfile.STATE_AUDIO_ONLY);
+        when(mFeatureFlags.ignoreAutoRouteToWatchDevice()).thenReturn(false);
     }
 
     @After
@@ -166,36 +202,27 @@
 
     @SmallTest
     @Test
-    public void testActivateAndRemoveBluetoothDeviceDuringCall() {
-        doAnswer(invocation -> {
-            mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0, BLUETOOTH_DEVICE_1);
-            return true;
-        }).when(mAudioManager).setCommunicationDevice(nullable(AudioDeviceInfo.class));
-
+    public void testInitializeWithWiredHeadset() {
+        AudioRoute wiredHeadsetRoute = new AudioRoute(AudioRoute.TYPE_WIRED, null, null);
+        when(mWiredHeadsetManager.isPluggedIn()).thenReturn(true);
         mController.initialize();
-        mController.setActive(true);
-        mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
-                BLUETOOTH_DEVICE_1);
+        assertEquals(wiredHeadsetRoute, mController.getCurrentRoute());
+        assertEquals(2, mController.getAvailableRoutes().size());
+        assertTrue(mController.getAvailableRoutes().contains(mSpeakerRoute));
+    }
+
+    @SmallTest
+    @Test
+    public void testNormalCallRouteToEarpiece() {
+        mController.initialize();
+        mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS);
+        // Verify that pending audio destination route is set to speaker. This will trigger pending
+        // message to wait for SPEAKER_ON message once communication device is set before routing.
+        waitForHandlerAction(mController.getAdapterHandler(), TEST_TIMEOUT);
+        PendingAudioRoute pendingRoute = mController.getPendingAudioRoute();
+        assertEquals(AudioRoute.TYPE_EARPIECE, pendingRoute.getDestRoute().getType());
+
         CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
-                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_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(
@@ -204,6 +231,49 @@
 
     @SmallTest
     @Test
+    public void testVideoCallHoldRouteToEarpiece() {
+        mController.initialize();
+        mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS);
+        // Verify that pending audio destination route is not defaulted to speaker when a video call
+        // is not the foreground call.
+        waitForHandlerAction(mController.getAdapterHandler(), TEST_TIMEOUT);
+        PendingAudioRoute pendingRoute = mController.getPendingAudioRoute();
+        assertEquals(AudioRoute.TYPE_EARPIECE, pendingRoute.getDestRoute().getType());
+    }
+
+    @SmallTest
+    @Test
+    public void testVideoCallRouteToSpeaker() {
+        when(mCall.getVideoState()).thenReturn(VideoProfile.STATE_BIDIRECTIONAL);
+        mController.initialize();
+        mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS);
+        // Verify that pending audio destination route is set to speaker. This will trigger pending
+        // message to wait for SPEAKER_ON message once communication device is set before routing.
+        waitForHandlerAction(mController.getAdapterHandler(), TEST_TIMEOUT);
+        PendingAudioRoute pendingRoute = mController.getPendingAudioRoute();
+        assertEquals(AudioRoute.TYPE_SPEAKER, pendingRoute.getDestRoute().getType());
+
+        // Mock SPEAKER_ON message received by controller.
+        mController.sendMessageWithSessionInfo(SPEAKER_ON);
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        // Verify that audio is routed to wired headset if it's present.
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_WIRED_HEADSET,
+                CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        mController.sendMessageWithSessionInfo(CONNECT_WIRED_HEADSET);
+        waitForHandlerAction(mController.getAdapterHandler(), TEST_TIMEOUT);
+        mController.sendMessageWithSessionInfo(SPEAKER_OFF);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+    }
+
+    @SmallTest
+    @Test
     public void testActiveDeactivateBluetoothDevice() {
         mController.initialize();
         mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
@@ -229,14 +299,6 @@
     @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();
@@ -253,15 +315,15 @@
         assertFalse(mController.isActive());
 
         mController.sendMessageWithSessionInfo(SWITCH_FOCUS, RINGING_FOCUS);
-        verify(mAudioManager, timeout(TEST_TIMEOUT)).setCommunicationDevice(
-                nullable(AudioDeviceInfo.class));
+        verify(mBluetoothDeviceManager, timeout(TEST_TIMEOUT))
+                .connectAudio(BLUETOOTH_DEVICE_1, AudioRoute.TYPE_BLUETOOTH_SCO);
         assertTrue(mController.isActive());
 
         mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS);
         assertTrue(mController.isActive());
 
         mController.sendMessageWithSessionInfo(SWITCH_FOCUS, NO_FOCUS);
-        verify(mAudioManager, timeout(TEST_TIMEOUT)).clearCommunicationDevice();
+        verify(mBluetoothDeviceManager, timeout(TEST_TIMEOUT).atLeastOnce()).disconnectSco();
         assertFalse(mController.isActive());
     }
 
@@ -456,8 +518,7 @@
     @SmallTest
     @Test
     public void testToggleMute() throws Exception {
-        when(mAudioManager.isMasterMute()).thenReturn(false);
-
+        when(mAudioManager.isMicrophoneMute()).thenReturn(false);
         mController.initialize();
         mController.setActive(true);
 
@@ -470,7 +531,7 @@
         verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
                 any(CallAudioState.class), eq(expectedState));
 
-        when(mAudioManager.isMasterMute()).thenReturn(true);
+        when(mAudioManager.isMicrophoneMute()).thenReturn(true);
         mController.sendMessageWithSessionInfo(MUTE_OFF);
         expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
                 CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
@@ -480,4 +541,236 @@
         verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
                 any(CallAudioState.class), eq(expectedState));
     }
+
+    @SmallTest
+    @Test
+    public void testMuteOffAfterCallEnds() throws Exception {
+        when(mAudioManager.isMicrophoneMute()).thenReturn(false);
+        mController.initialize();
+        mController.setActive(true);
+
+        mController.sendMessageWithSessionInfo(MUTE_ON);
+        CallAudioState expectedState = new CallAudioState(true, CallAudioState.ROUTE_EARPIECE,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        verify(mAudioService, timeout(TEST_TIMEOUT)).setMicrophoneMute(eq(true), anyString(),
+                anyInt(), anyString());
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        // Switch to NO_FOCUS to indicate call termination and verify mute is reset.
+        when(mAudioManager.isMicrophoneMute()).thenReturn(true);
+        mController.sendMessageWithSessionInfo(SWITCH_FOCUS, NO_FOCUS);
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        verify(mAudioService, timeout(TEST_TIMEOUT)).setMicrophoneMute(eq(false), anyString(),
+                anyInt(), anyString());
+        verify(mCallsManager, timeout(TEST_TIMEOUT).atLeastOnce()).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+    }
+
+    @SmallTest
+    @Test
+    public void testIgnoreAutoRouteToWatch() {
+        when(mFeatureFlags.ignoreAutoRouteToWatchDevice()).thenReturn(true);
+        when(mBluetoothRouteManager.isWatch(any(BluetoothDevice.class))).thenReturn(true);
+
+        mController.initialize();
+        mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
+                BLUETOOTH_DEVICE_1);
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, null, BLUETOOTH_DEVICES);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        // Connect wired headset.
+        mController.sendMessageWithSessionInfo(CONNECT_WIRED_HEADSET);
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_WIRED_HEADSET,
+                CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER
+                        | CallAudioState.ROUTE_BLUETOOTH, null, BLUETOOTH_DEVICES);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        // Disconnect wired headset and ensure Telecom routes to earpiece instead of the BT route.
+        mController.sendMessageWithSessionInfo(DISCONNECT_WIRED_HEADSET);
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER
+                        | CallAudioState.ROUTE_BLUETOOTH, null , BLUETOOTH_DEVICES);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+    }
+
+    @SmallTest
+    @Test
+    public void testConnectDisconnectScoDuringCall() {
+        verifyConnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_SCO);
+        verifyDisconnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_SCO);
+    }
+
+    @SmallTest
+    @Test
+    public void testConnectAndDisconnectLeDeviceDuringCall() {
+        when(mBluetoothLeAudio.getConnectedGroupLeadDevice(anyInt()))
+                .thenReturn(BLUETOOTH_DEVICE_1);
+        verifyConnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_LE);
+        verifyDisconnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_LE);
+    }
+
+    @SmallTest
+    @Test
+    public void testConnectAndDisconnectHearingAidDuringCall() {
+        verifyConnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_HA);
+        verifyDisconnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_HA);
+    }
+
+    @SmallTest
+    @Test
+    public void testSwitchBetweenLeAndScoDevices() {
+        when(mBluetoothLeAudio.getConnectedGroupLeadDevice(anyInt()))
+                .thenReturn(BLUETOOTH_DEVICE_1);
+        verifyConnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_LE);
+        BluetoothDevice scoDevice =
+                BluetoothRouteManagerTest.makeBluetoothDevice("00:00:00:00:00:03");
+        BLUETOOTH_DEVICES.add(scoDevice);
+
+        // Add SCO device.
+        mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
+                scoDevice);
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        // Switch to SCO and verify active device is updated.
+        mController.sendMessageWithSessionInfo(USER_SWITCH_BLUETOOTH, 0, scoDevice.getAddress());
+        mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0, scoDevice);
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, scoDevice, BLUETOOTH_DEVICES);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        // Disconnect SCO and verify audio routed back to LE audio.
+        BLUETOOTH_DEVICES.remove(scoDevice);
+        mController.sendMessageWithSessionInfo(BT_DEVICE_REMOVED, AudioRoute.TYPE_BLUETOOTH_SCO,
+                scoDevice);
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+    }
+
+    @SmallTest
+    @Test
+    public void testFallbackWhenBluetoothConnectionFails() {
+        when(mBluetoothDeviceManager.connectAudio(any(BluetoothDevice.class), anyInt()))
+                .thenReturn(false);
+
+        AudioDeviceInfo mockAudioDeviceInfo = mock(AudioDeviceInfo.class);
+        when(mAudioManager.getCommunicationDevice()).thenReturn(mockAudioDeviceInfo);
+        verifyConnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_LE);
+        BluetoothDevice scoDevice =
+                BluetoothRouteManagerTest.makeBluetoothDevice("00:00:00:00:00:03");
+        BLUETOOTH_DEVICES.add(scoDevice);
+
+        // Add SCO device.
+        mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
+                scoDevice);
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        // Switch to SCO but reject connection and make sure audio is routed back to LE device.
+        mController.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
+                AudioRoute.TYPE_BLUETOOTH_SCO, scoDevice.getAddress());
+        verify(mBluetoothDeviceManager, timeout(TEST_TIMEOUT))
+                .connectAudio(scoDevice, AudioRoute.TYPE_BLUETOOTH_SCO);
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+        verify(mCallsManager, timeout(TEST_TIMEOUT).atLeastOnce()).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        // Cleanup supported devices for next test
+        BLUETOOTH_DEVICES.remove(scoDevice);
+    }
+
+    @SmallTest
+    @Test
+    public void testIgnoreLeRouteWhenServiceUnavailable() {
+        when(mBluetoothLeAudio.getConnectedGroupLeadDevice(anyInt()))
+                .thenReturn(BLUETOOTH_DEVICE_1);
+        verifyConnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_LE);
+
+        when(mBluetoothDeviceManager.getLeAudioService()).thenReturn(null);
+        // Switch baseline to verify that we don't route back to LE audio this time.
+        mController.sendMessageWithSessionInfo(SWITCH_BASELINE_ROUTE, 0, (String) null);
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, null, BLUETOOTH_DEVICES);
+        verify(mCallsManager, timeout(TEST_TIMEOUT).atLeastOnce()).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+    }
+
+    private void verifyConnectBluetoothDevice(int audioType) {
+        mController.initialize();
+        mController.setActive(true);
+
+        mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, audioType, BLUETOOTH_DEVICE_1);
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, null, BLUETOOTH_DEVICES);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        mController.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT, audioType, BT_ADDRESS_1);
+        if (audioType == AudioRoute.TYPE_BLUETOOTH_SCO) {
+            verify(mBluetoothDeviceManager, timeout(TEST_TIMEOUT))
+                    .connectAudio(BLUETOOTH_DEVICE_1, AudioRoute.TYPE_BLUETOOTH_SCO);
+            mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED,
+                    0, BLUETOOTH_DEVICE_1);
+        } else {
+            verify(mAudioManager, timeout(TEST_TIMEOUT))
+                    .setCommunicationDevice(nullable(AudioDeviceInfo.class));
+        }
+
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        // Test hearing aid pair and ensure second device isn't added as a route
+        if (audioType == AudioRoute.TYPE_BLUETOOTH_HA) {
+            BluetoothDevice hearingAidDevice2 =
+                    BluetoothRouteManagerTest.makeBluetoothDevice("00:00:00:00:00:02");
+            mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, audioType, hearingAidDevice2);
+            expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+                    CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                            | CallAudioState.ROUTE_SPEAKER, null, BLUETOOTH_DEVICES);
+            // Verify that supported BT devices only shows the first connected hearing aid device.
+            verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                    any(CallAudioState.class), eq(expectedState));
+        }
+    }
+
+    private void verifyDisconnectBluetoothDevice(int audioType) {
+        mController.sendMessageWithSessionInfo(BT_DEVICE_REMOVED, audioType, BLUETOOTH_DEVICE_1);
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        if (audioType == AudioRoute.TYPE_BLUETOOTH_SCO) {
+            verify(mBluetoothDeviceManager, timeout(TEST_TIMEOUT)).disconnectSco();
+        } else {
+            verify(mAudioManager, timeout(TEST_TIMEOUT)).clearCommunicationDevice();
+        }
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+    }
 }
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
index d2da505..e97de2e 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
@@ -20,6 +20,7 @@
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.nullable;
@@ -1230,8 +1231,9 @@
 
     @SmallTest
     @Test
-    public void testQuiescentBluetoothRouteResetMute() {
+    public void testQuiescentBluetoothRouteResetMute() throws Exception {
         when(mFeatureFlags.resetMuteWhenEnteringQuiescentBtRoute()).thenReturn(true);
+        when(mFeatureFlags.transitRouteBeforeAudioDisconnectBt()).thenReturn(true);
         CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
                 mContext,
                 mockCallsManager,
@@ -1264,6 +1266,7 @@
                 CallAudioState.ROUTE_BLUETOOTH, CallAudioState.ROUTE_SPEAKER
                 | CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH);
         assertEquals(expectedState, stateMachine.getCurrentCallAudioState());
+        when(mockAudioManager.isMicrophoneMute()).thenReturn(true);
 
         stateMachine.sendMessageWithSessionInfo(CallAudioRouteStateMachine.SWITCH_FOCUS,
                 CallAudioRouteStateMachine.NO_FOCUS);
@@ -1272,9 +1275,8 @@
         expectedState = new CallAudioState(false,
                 CallAudioState.ROUTE_BLUETOOTH, CallAudioState.ROUTE_SPEAKER
                 | CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH);
-        // TODO: Re-enable this part of the test; this is now failing because we have to
-        // revert ag/23783145.
-        // assertEquals(expectedState, stateMachine.getCurrentCallAudioState());
+        assertEquals(expectedState, stateMachine.getCurrentCallAudioState());
+        verify(mockAudioService).setMicrophoneMute(eq(false), anyString(), anyInt(), eq(null));
     }
 
     @SmallTest
diff --git a/tests/src/com/android/server/telecom/tests/CallEndpointControllerTest.java b/tests/src/com/android/server/telecom/tests/CallEndpointControllerTest.java
index 9101a19..b8b9560 100644
--- a/tests/src/com/android/server/telecom/tests/CallEndpointControllerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallEndpointControllerTest.java
@@ -40,6 +40,7 @@
 import com.android.server.telecom.CallEndpointController;
 import com.android.server.telecom.CallsManager;
 import com.android.server.telecom.ConnectionServiceWrapper;
+import com.android.server.telecom.flags.FeatureFlags;
 
 import org.junit.Before;
 import org.junit.After;
@@ -101,7 +102,10 @@
     @Before
     public void setUp() throws Exception {
         super.setUp();
-        mCallEndpointController = new CallEndpointController(mMockContext, mCallsManager);
+        mCallEndpointController = new CallEndpointController(
+                mMockContext,
+                mCallsManager,
+                mFeatureFlags);
         doReturn(new HashSet<>(Arrays.asList(mCall))).when(mCallsManager).getTrackedCalls();
         doReturn(mConnectionService).when(mCall).getConnectionService();
         doReturn(mCallAudioManager).when(mCallsManager).getCallAudioManager();
diff --git a/tests/src/com/android/server/telecom/tests/CallIntentProcessorTest.java b/tests/src/com/android/server/telecom/tests/CallIntentProcessorTest.java
new file mode 100644
index 0000000..6deade4
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/CallIntentProcessorTest.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom.tests;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ComponentInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.platform.test.flag.junit.SetFlagsRule;
+
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.internal.app.IntentForwarderActivity;
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallIntentProcessor;
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.DefaultDialerCache;
+import com.android.server.telecom.PhoneNumberUtilsAdapter;
+import com.android.server.telecom.TelephonyUtil;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoSession;
+
+import java.util.concurrent.CompletableFuture;
+
+/** Unit tests for CollIntentProcessor class. */
+@RunWith(JUnit4.class)
+public class CallIntentProcessorTest extends TelecomTestCase {
+
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+    @Mock
+    private CallsManager mCallsManager;
+    @Mock
+    private DefaultDialerCache mDefaultDialerCache;
+    @Mock
+    private Context mMockCreateContextAsUser;
+    @Mock
+    private UserManager mMockCurrentUserManager;
+    @Mock
+    private PhoneNumberUtilsAdapter mPhoneNumberUtilsAdapter;
+    @Mock
+    private PackageManager mPackageManager;
+    @Mock
+    private ResolveInfo mResolveInfo;
+    @Mock
+    private ComponentName mComponentName;
+    @Mock
+    private ComponentInfo mComponentInfo;
+    @Mock
+    private CompletableFuture<Call> mCall;
+    private CallIntentProcessor mCallIntentProcessor;
+    private static final UserHandle PRIVATE_SPACE_USERHANDLE = new UserHandle(12);
+    private static final String TEST_PACKAGE_NAME = "testPackageName";
+    private static final Uri TEST_PHONE_NUMBER = Uri.parse("tel:1234567890");
+    private static final Uri TEST_EMERGENCY_PHONE_NUMBER = Uri.parse("tel:911");
+
+    @Override
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
+        when(mContext.createContextAsUser(any(UserHandle.class), eq(0))).thenReturn(
+                mMockCreateContextAsUser);
+        when(mMockCreateContextAsUser.getSystemService(UserManager.class)).thenReturn(
+                mMockCurrentUserManager);
+        mCallIntentProcessor = new CallIntentProcessor(mContext, mCallsManager, mDefaultDialerCache,
+                mFeatureFlags);
+        when(mFeatureFlags.telecomResolveHiddenDependencies()).thenReturn(false);
+        when(mCallsManager.getPhoneNumberUtilsAdapter()).thenReturn(mPhoneNumberUtilsAdapter);
+        when(mPhoneNumberUtilsAdapter.isUriNumber(anyString())).thenReturn(true);
+        when(mCallsManager.startOutgoingCall(any(Uri.class), any(), any(Bundle.class),
+                any(UserHandle.class), any(Intent.class), anyString())).thenReturn(mCall);
+        when(mCall.thenAccept(any())).thenReturn(null);
+    }
+
+    @Test
+    public void testNonPrivateSpaceCall_noConsentDialogShown() {
+        setPrivateSpaceFlagsEnabled();
+
+        Intent intent = new Intent(Intent.ACTION_CALL);
+        intent.setData(TEST_PHONE_NUMBER);
+        intent.putExtra(CallIntentProcessor.KEY_INITIATING_USER, UserHandle.CURRENT);
+        when(mCallsManager.isSelfManaged(any(), eq(UserHandle.CURRENT))).thenReturn(false);
+
+        mCallIntentProcessor.processIntent(intent, TEST_PACKAGE_NAME);
+
+        verify(mContext, never()).startActivityAsUser(any(Intent.class), any(UserHandle.class));
+
+        // Verify that the call proceeds as normal since the dialog was not shown
+        verify(mCallsManager).startOutgoingCall(any(Uri.class), any(), any(Bundle.class),
+                eq(UserHandle.CURRENT), eq(intent), eq(TEST_PACKAGE_NAME));
+    }
+
+    @Test
+    public void testPrivateSpaceCall_isSelfManaged_noDialogShown() {
+        setPrivateSpaceFlagsEnabled();
+        markInitiatingUserAsPrivateProfile();
+        resolveAsIntentForwarderActivity();
+
+        Intent intent = new Intent(Intent.ACTION_CALL);
+        intent.setData(TEST_PHONE_NUMBER);
+        intent.putExtra(CallIntentProcessor.KEY_INITIATING_USER, PRIVATE_SPACE_USERHANDLE);
+        when(mCallsManager.isSelfManaged(any(), eq(PRIVATE_SPACE_USERHANDLE))).thenReturn(true);
+
+        mCallIntentProcessor.processIntent(intent, TEST_PACKAGE_NAME);
+
+        verify(mContext, never()).startActivityAsUser(any(Intent.class),
+                eq(PRIVATE_SPACE_USERHANDLE));
+
+        // Verify that the call proceeds as normal since the dialog was not shown
+        verify(mCallsManager).startOutgoingCall(any(Uri.class), any(), any(Bundle.class),
+                eq(PRIVATE_SPACE_USERHANDLE), eq(intent), eq(TEST_PACKAGE_NAME));
+    }
+
+    @Test
+    public void testPrivateSpaceCall_isEmergency_noDialogShown() {
+        MockitoSession session = ExtendedMockito.mockitoSession().mockStatic(
+                TelephonyUtil.class).startMocking();
+        ExtendedMockito.doReturn(true).when(
+                () -> TelephonyUtil.shouldProcessAsEmergency(any(), any()));
+
+        setPrivateSpaceFlagsEnabled();
+        markInitiatingUserAsPrivateProfile();
+        resolveAsIntentForwarderActivity();
+
+        Intent intent = new Intent(Intent.ACTION_CALL);
+        intent.setData(TEST_EMERGENCY_PHONE_NUMBER);
+        intent.putExtra(CallIntentProcessor.KEY_INITIATING_USER, PRIVATE_SPACE_USERHANDLE);
+        when(mCallsManager.isSelfManaged(any(), eq(PRIVATE_SPACE_USERHANDLE))).thenReturn(false);
+
+        mCallIntentProcessor.processIntent(intent, TEST_PACKAGE_NAME);
+
+        verify(mContext, never()).startActivityAsUser(any(Intent.class),
+                eq(PRIVATE_SPACE_USERHANDLE));
+        session.finishMocking();
+    }
+
+    @Test
+    public void testPrivateSpaceCall_showConsentDialog() {
+        setPrivateSpaceFlagsEnabled();
+        markInitiatingUserAsPrivateProfile();
+        resolveAsIntentForwarderActivity();
+
+        Intent intent = new Intent(Intent.ACTION_CALL);
+        intent.setData(TEST_PHONE_NUMBER);
+        intent.putExtra(CallIntentProcessor.KEY_INITIATING_USER, PRIVATE_SPACE_USERHANDLE);
+        when(mCallsManager.isSelfManaged(any(), eq(PRIVATE_SPACE_USERHANDLE))).thenReturn(false);
+
+        mCallIntentProcessor.processIntent(intent, TEST_PACKAGE_NAME);
+
+        // Consent dialog should be shown
+        verify(mContext).startActivityAsUser(any(Intent.class), eq(PRIVATE_SPACE_USERHANDLE));
+
+        /// Verify that the call does not proceeds as normal since the dialog was shown
+        verify(mCallsManager, never()).startOutgoingCall(any(), any(), any(), any(), any(),
+                anyString());
+    }
+
+    private void setPrivateSpaceFlagsEnabled() {
+        mSetFlagsRule.enableFlags(android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES,
+                android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_INTENT_REDIRECTION);
+    }
+
+    private void markInitiatingUserAsPrivateProfile() {
+        when(mMockCurrentUserManager.isPrivateProfile()).thenReturn(true);
+    }
+
+    private void resolveAsIntentForwarderActivity() {
+        when(mComponentName.getShortClassName()).thenReturn(
+                IntentForwarderActivity.FORWARD_INTENT_TO_PARENT);
+        when(mComponentInfo.getComponentName()).thenReturn(mComponentName);
+        when(mResolveInfo.getComponentInfo()).thenReturn(mComponentInfo);
+
+        when(mContext.getPackageManager()).thenReturn(mPackageManager);
+
+        when(mPackageManager.resolveActivityAsUser(any(Intent.class),
+                any(PackageManager.ResolveInfoFlags.class),
+                eq(PRIVATE_SPACE_USERHANDLE.getIdentifier()))).thenReturn(mResolveInfo);
+    }
+}
\ No newline at end of file
diff --git a/tests/src/com/android/server/telecom/tests/CallLogManagerTest.java b/tests/src/com/android/server/telecom/tests/CallLogManagerTest.java
index fa35f25..cb04dc3 100644
--- a/tests/src/com/android/server/telecom/tests/CallLogManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallLogManagerTest.java
@@ -24,6 +24,7 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.doAnswer;
@@ -122,6 +123,7 @@
     private static final int CURRENT_USER_ID = 0;
     private static final int OTHER_USER_ID = 10;
     private static final int MANAGED_USER_ID = 11;
+    private static final int PRIVATE_USER_ID = 12;
 
     private static final String TEST_ISO = "KR";
     private static final String TEST_ISO_2 = "JP";
@@ -175,9 +177,22 @@
 
         UserManager userManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
         UserInfo userInfo = new UserInfo(CURRENT_USER_ID, "test", 0);
+        userInfo.profileGroupId = UserInfo.NO_PROFILE_GROUP_ID;
+        userInfo.userType = UserManager.USER_TYPE_FULL_SYSTEM;
+
         UserInfo otherUserInfo = new UserInfo(OTHER_USER_ID, "test2", 0);
+        otherUserInfo.profileGroupId = UserInfo.NO_PROFILE_GROUP_ID;
+        otherUserInfo.userType = UserManager.USER_TYPE_FULL_SECONDARY;
+
         UserInfo managedProfileUserInfo = new UserInfo(MANAGED_USER_ID, "test3",
-                UserInfo.FLAG_MANAGED_PROFILE);
+                UserInfo.FLAG_MANAGED_PROFILE | userInfo.FLAG_PROFILE);
+        managedProfileUserInfo.profileGroupId = 90210;
+        managedProfileUserInfo.userType = UserManager.USER_TYPE_PROFILE_MANAGED;
+
+        UserInfo privateProfileUserInfo = new UserInfo(PRIVATE_USER_ID, "private",
+                UserInfo.FLAG_PROFILE);
+        privateProfileUserInfo.profileGroupId = 90210;
+        privateProfileUserInfo.userType = UserManager.USER_TYPE_PROFILE_PRIVATE;
 
         doAnswer(new Answer<Uri>() {
             @Override
@@ -188,16 +203,44 @@
 
         when(userManager.isUserRunning(any(UserHandle.class))).thenReturn(true);
         when(userManager.isUserUnlocked(any(UserHandle.class))).thenReturn(true);
-        when(userManager.hasUserRestriction(any(String.class), any(UserHandle.class)))
+        when(userManager.hasUserRestrictionForUser(any(String.class), any(UserHandle.class)))
                 .thenReturn(false);
         when(userManager.getAliveUsers())
                 .thenReturn(Arrays.asList(userInfo, otherUserInfo, managedProfileUserInfo));
+        configureContextForUser(CURRENT_USER_ID, userInfo);
         when(userManager.getUserInfo(eq(CURRENT_USER_ID))).thenReturn(userInfo);
+
+        configureContextForUser(OTHER_USER_ID, otherUserInfo);
         when(userManager.getUserInfo(eq(OTHER_USER_ID))).thenReturn(otherUserInfo);
+
+        configureContextForUser(MANAGED_USER_ID, managedProfileUserInfo);
         when(userManager.getUserInfo(eq(MANAGED_USER_ID))).thenReturn(managedProfileUserInfo);
+
+        configureContextForUser(PRIVATE_USER_ID, privateProfileUserInfo);
+        when(userManager.getUserInfo(eq(PRIVATE_USER_ID))).thenReturn(privateProfileUserInfo);
+
         PackageManager packageManager = mContext.getPackageManager();
         when(packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(false);
         when(mFeatureFlags.telecomLogExternalWearableCalls()).thenReturn(false);
+        when(mFeatureFlags.telecomResolveHiddenDependencies()).thenReturn(true);
+    }
+
+    /**
+     * Yuck; this is absolutely wretched that we have to mock things out in this way.
+     * Because the preferred way to get info about a user is to first user
+     * {@link Context#createContextAsUser(UserHandle, int)} to first get a user-specific context,
+     * and to then query the {@link UserManager} instance to see if it's a profile, we need to do
+     * all of this really gross mocking.
+     * @param userId The userid.
+     * @param info The associated userinfo.
+     */
+    private void configureContextForUser(int userId, UserInfo info) {
+        Context mockContext = mock(Context.class);
+        mComponentContextFixture.addContextForUser(UserHandle.of(userId), mockContext);
+        UserManager mockUserManager = mock(UserManager.class);
+        when(mockUserManager.getUserInfo(eq(userId))).thenReturn(info);
+        when(mockUserManager.isProfile()).thenReturn(info.isProfile());
+        when(mockContext.getSystemService(eq(UserManager.class))).thenReturn(mockUserManager);
     }
 
     @Override
@@ -232,7 +275,7 @@
     @Test
     public void testDontLogChoosingAccountCall() {
         when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
-                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
         Call fakeCall = makeFakeCall(
                 DisconnectCause.OTHER, // disconnectCauseCode
                 false, // isConference
@@ -335,7 +378,7 @@
     @Test
     public void testLogCallDirectionOutgoing() {
         when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
-                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
         Call fakeOutgoingCall = makeFakeCall(
                 DisconnectCause.OTHER, // disconnectCauseCode
                 false, // isConference
@@ -360,7 +403,7 @@
     @Test
     public void testLogCallDirectionIncoming() {
         when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
-                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
         Call fakeIncomingCall = makeFakeCall(
                 DisconnectCause.OTHER, // disconnectCauseCode
                 false, // isConference
@@ -386,7 +429,7 @@
     @Test
     public void testLogCallDirectionMissedAddCallUriForMissedCallsFlagOff() {
         when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
-                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
         Call fakeMissedCall = makeFakeCall(
                 DisconnectCause.MISSED, // disconnectCauseCode
                 false, // isConference
@@ -417,7 +460,7 @@
     @Test
     public void testLogCallDirectionMissedAddCallUriForMissedCallsFlagOn() {
         when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
-                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
         Call fakeMissedCall = makeFakeCall(
                 DisconnectCause.MISSED, // disconnectCauseCode
                 false, // isConference
@@ -448,7 +491,7 @@
     @Test
     public void testLogCallDirectionRejected() {
         when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
-                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
         Call fakeMissedCall = makeFakeCall(
                 DisconnectCause.REJECTED, // disconnectCauseCode
                 false, // isConference
@@ -474,7 +517,7 @@
     @Test
     public void testCreationTimeAndAge() {
         when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
-                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
         long currentTime = System.currentTimeMillis();
         long duration = 1000L;
         Call fakeCall = makeFakeCall(
@@ -502,7 +545,7 @@
     @Test
     public void testLogPhoneAccountId() {
         when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
-                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
         Call fakeCall = makeFakeCall(
                 DisconnectCause.OTHER, // disconnectCauseCode
                 false, // isConference
@@ -526,7 +569,7 @@
     @Test
     public void testLogCorrectPhoneNumber() {
         when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
-                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
         Call fakeCall = makeFakeCall(
                 DisconnectCause.OTHER, // disconnectCauseCode
                 false, // isConference
@@ -553,7 +596,7 @@
     @Test
     public void testLogCallVideoFeatures() {
         when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
-                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
         Call fakeVideoCall = makeFakeCall(
                 DisconnectCause.OTHER, // disconnectCauseCode
                 false, // isConference
@@ -611,7 +654,8 @@
                 ContentProvider.maybeAddUserId(CallLog.Calls.CONTENT_URI, OTHER_USER_ID)));
         assertFalse(uris.getAllValues().contains(
                 ContentProvider.maybeAddUserId(CallLog.Calls.CONTENT_URI, MANAGED_USER_ID)));
-
+        assertFalse(uris.getAllValues().contains(
+                ContentProvider.maybeAddUserId(CallLog.Calls.CONTENT_URI, PRIVATE_USER_ID)));
         for (ContentValues v : values.getAllValues()) {
             assertEquals(v.getAsInteger(CallLog.Calls.TYPE),
                     Integer.valueOf(CallLog.Calls.OUTGOING_TYPE));
@@ -656,7 +700,8 @@
                 ContentProvider.maybeAddUserId(CallLog.Calls.CONTENT_URI, OTHER_USER_ID)));
         assertFalse(uris.getAllValues().contains(
                 ContentProvider.maybeAddUserId(CallLog.Calls.CONTENT_URI, MANAGED_USER_ID)));
-
+        assertFalse(uris.getAllValues().contains(
+                ContentProvider.maybeAddUserId(CallLog.Calls.CONTENT_URI, PRIVATE_USER_ID)));
         for (ContentValues v : values.getAllValues()) {
             assertEquals(v.getAsInteger(CallLog.Calls.TYPE),
                     Integer.valueOf(CallLog.Calls.INCOMING_TYPE));
@@ -666,6 +711,8 @@
     @MediumTest
     @Test
     public void testLogCallDirectionOutgoingWithMultiUserCapabilityFromManagedProfile() {
+        UserManager userManager = mContext.getSystemService(UserManager.class);
+        when(userManager.isManagedProfile()).thenReturn(true);
         when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
                 .thenReturn(makeFakePhoneAccount(mManagedProfileAccountHandle,
                         PhoneAccount.CAPABILITY_MULTI_USER));
@@ -694,6 +741,72 @@
     }
 
     @MediumTest
+    @Test
+    public void testLogCallDirectionOutgoingWithMultiUserCapabilityFromPrivateProfile() {
+        when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
+                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle,
+                        PhoneAccount.CAPABILITY_MULTI_USER));
+        Call fakeOutgoingCall = makeFakeCall(
+                DisconnectCause.OTHER, // disconnectCauseCode
+                false, // isConference
+                false, // isIncoming
+                1L, // creationTimeMillis
+                1000L, // ageMillis
+                TEL_PHONEHANDLE, // callHandle
+                mDefaultAccountHandle, // phoneAccountHandle
+                NO_VIDEO_STATE, // callVideoState
+                POST_DIAL_STRING, // postDialDigits
+                VIA_NUMBER_STRING, // viaNumber
+                UserHandle.of(PRIVATE_USER_ID)
+        );
+        mCallLogManager.onCallStateChanged(fakeOutgoingCall, CallState.ACTIVE,
+                CallState.DISCONNECTED);
+
+        // Outgoing call placed through private space should only show up in the private space
+        // call logs.
+        verifyNoInsertionInUser(CURRENT_USER_ID);
+        verifyNoInsertionInUser(OTHER_USER_ID);
+        verifyNoInsertionInUser(MANAGED_USER_ID);
+        ContentValues insertedValues = verifyInsertionWithCapture(PRIVATE_USER_ID);
+        assertEquals(insertedValues.getAsInteger(CallLog.Calls.TYPE),
+                Integer.valueOf(CallLog.Calls.OUTGOING_TYPE));
+    }
+
+    @MediumTest
+    @Test
+    public void testLogCallDirectionOutgoingWithMultiUserCapabilityFromPrivateProfileNoRefactor() {
+        // Same as the above test, but turns off the hidden deps refactor; there are some minor
+        // differences in how we detect profiles, so we want to ensure this works both ways.
+        when(mFeatureFlags.telecomResolveHiddenDependencies()).thenReturn(false);
+        when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
+                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle,
+                        PhoneAccount.CAPABILITY_MULTI_USER));
+        Call fakeOutgoingCall = makeFakeCall(
+                DisconnectCause.OTHER, // disconnectCauseCode
+                false, // isConference
+                false, // isIncoming
+                1L, // creationTimeMillis
+                1000L, // ageMillis
+                TEL_PHONEHANDLE, // callHandle
+                mDefaultAccountHandle, // phoneAccountHandle
+                NO_VIDEO_STATE, // callVideoState
+                POST_DIAL_STRING, // postDialDigits
+                VIA_NUMBER_STRING, // viaNumber
+                UserHandle.of(PRIVATE_USER_ID)
+        );
+        mCallLogManager.onCallStateChanged(fakeOutgoingCall, CallState.ACTIVE,
+                CallState.DISCONNECTED);
+
+        // Outgoing call placed through work dialer should be inserted to managed profile only.
+        verifyNoInsertionInUser(CURRENT_USER_ID);
+        verifyNoInsertionInUser(OTHER_USER_ID);
+        verifyNoInsertionInUser(MANAGED_USER_ID);
+        ContentValues insertedValues = verifyInsertionWithCapture(PRIVATE_USER_ID);
+        assertEquals(insertedValues.getAsInteger(CallLog.Calls.TYPE),
+                Integer.valueOf(CallLog.Calls.OUTGOING_TYPE));
+    }
+
+    @MediumTest
     @FlakyTest
     @Test
     public void testLogCallDirectionOutgoingFromManagedProfile() {
@@ -719,6 +832,7 @@
         // profile only.
         verifyNoInsertionInUser(CURRENT_USER_ID);
         verifyNoInsertionInUser(OTHER_USER_ID);
+        verifyNoInsertionInUser(PRIVATE_USER_ID);
         ContentValues insertedValues = verifyInsertionWithCapture(MANAGED_USER_ID);
         assertEquals(insertedValues.getAsInteger(CallLog.Calls.TYPE),
                 Integer.valueOf(CallLog.Calls.OUTGOING_TYPE));
@@ -749,6 +863,7 @@
         // profile only.
         verifyNoInsertionInUser(CURRENT_USER_ID);
         verifyNoInsertionInUser(OTHER_USER_ID);
+        verifyNoInsertionInUser(PRIVATE_USER_ID);
         ContentValues insertedValues = verifyInsertionWithCapture(MANAGED_USER_ID);
         assertEquals(insertedValues.getAsInteger(CallLog.Calls.TYPE),
                 Integer.valueOf(Calls.INCOMING_TYPE));
@@ -761,7 +876,7 @@
     @Test
     public void testLogCallDataUsageSet() {
         when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
-                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
         Call fakeVideoCall = makeFakeCall(
                 DisconnectCause.OTHER, // disconnectCauseCode
                 false, // isConference
@@ -788,7 +903,7 @@
     @Test
     public void testLogCallDataUsageNotSet() {
         when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
-                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
         Call fakeVideoCall = makeFakeCall(
                 DisconnectCause.OTHER, // disconnectCauseCode
                 false, // isConference
@@ -842,7 +957,7 @@
     @Test
     public void testLogCallWhenExternalCallOnWatch() {
         when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class)))
-                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, CURRENT_USER_ID));
+                .thenReturn(makeFakePhoneAccount(mDefaultAccountHandle, 0 /* capabilities */));
         PackageManager packageManager = mContext.getPackageManager();
         when(packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(true);
         when(mFeatureFlags.telecomLogExternalWearableCalls()).thenReturn(true);
diff --git a/tests/src/com/android/server/telecom/tests/CallRedirectionProcessorTest.java b/tests/src/com/android/server/telecom/tests/CallRedirectionProcessorTest.java
index 8210686..241216a 100644
--- a/tests/src/com/android/server/telecom/tests/CallRedirectionProcessorTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallRedirectionProcessorTest.java
@@ -378,4 +378,22 @@
         verify(mContext, times(1)).
                 unbindService(any(ServiceConnection.class));
     }
+
+    /**
+     * Verifies that calling formatNumberToE164 will not crash when Telephony is not present and
+     * we can't ascertain the network country ISO.
+     */
+    @Test
+    public void testFormatNumberToE164WhenNoTelephony() {
+        // Need to do this even though we're just testing the helper
+        startProcessWithNoGateWayInfo();
+
+        CallRedirectionProcessorHelper helper = new CallRedirectionProcessorHelper(mContext,
+                mCallsManager, mPhoneAccountRegistrar);
+        when(mComponentContextFixture.getTelephonyManager().getNetworkCountryIso())
+                .thenThrow(new UnsupportedOperationException("Bee boop"));
+        assertEquals(Uri.fromParts("tel", "6505551212", null),
+                helper.formatNumberToE164(
+                        Uri.fromParts("tel", "6505551212", null)));
+    }
 }
diff --git a/tests/src/com/android/server/telecom/tests/CallTest.java b/tests/src/com/android/server/telecom/tests/CallTest.java
index e06938d..a22d2ca 100644
--- a/tests/src/com/android/server/telecom/tests/CallTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallTest.java
@@ -22,24 +22,29 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.content.ComponentName;
 import android.content.Intent;
 import android.content.pm.PackageManager;
+import android.content.res.Resources;
 import android.graphics.Bitmap;
 import android.graphics.drawable.ColorDrawable;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.UserHandle;
 import android.telecom.CallAttributes;
+import android.telecom.CallEndpoint;
 import android.telecom.CallerInfo;
 import android.telecom.Connection;
 import android.telecom.DisconnectCause;
@@ -56,6 +61,9 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
+import com.android.server.telecom.CachedAvailableEndpointsChange;
+import com.android.server.telecom.CachedCurrentEndpointChange;
+import com.android.server.telecom.CachedMuteStateChange;
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallIdMapper;
 import com.android.server.telecom.CallState;
@@ -63,6 +71,7 @@
 import com.android.server.telecom.CallsManager;
 import com.android.server.telecom.ClockProxy;
 import com.android.server.telecom.ConnectionServiceWrapper;
+import com.android.server.telecom.EmergencyCallHelper;
 import com.android.server.telecom.PhoneAccountRegistrar;
 import com.android.server.telecom.PhoneNumberUtilsAdapter;
 import com.android.server.telecom.TelecomSystem;
@@ -78,6 +87,7 @@
 import org.mockito.Mockito;
 
 import java.util.Collections;
+import java.util.Set;
 
 @RunWith(AndroidJUnit4.class)
 public class CallTest extends TelecomTestCase {
@@ -100,7 +110,6 @@
     @Mock private PhoneAccountRegistrar mMockPhoneAccountRegistrar;
     @Mock private ClockProxy mMockClockProxy;
     @Mock private ToastFactory mMockToastProxy;
-    @Mock private Toast mMockToast;
     @Mock private PhoneNumberUtilsAdapter mMockPhoneNumberUtilsAdapter;
     @Mock private ConnectionServiceWrapper mMockConnectionService;
     @Mock private TransactionalServiceWrapper mMockTransactionalService;
@@ -117,8 +126,14 @@
                 eq(SIM_1_HANDLE));
         doReturn(new ComponentName(mContext, CallTest.class))
                 .when(mMockConnectionService).getComponentName();
-        doReturn(mMockToast).when(mMockToastProxy).makeText(any(), anyInt(), anyInt());
         doReturn(UserHandle.CURRENT).when(mMockCallsManager).getCurrentUserHandle();
+        Resources mockResources = mContext.getResources();
+        when(mockResources.getBoolean(R.bool.skip_loading_canned_text_response))
+                .thenReturn(false);
+        when(mockResources.getBoolean(R.bool.skip_incoming_caller_info_query))
+                .thenReturn(false);
+        EmergencyCallHelper helper = mock(EmergencyCallHelper.class);
+        doReturn(helper).when(mMockCallsManager).getEmergencyCallHelper();
     }
 
     @After
@@ -138,6 +153,211 @@
     }
 
     /**
+     * Verify Call#setVideoState will only upgrade to video if the PhoneAccount supports video
+     * state capabilities
+     */
+    @Test
+    @SmallTest
+    public void testSetVideoStateForTransactionalCalls() {
+        Call call = createCall("1", Call.CALL_DIRECTION_INCOMING);
+        TransactionalServiceWrapper tsw = Mockito.mock(TransactionalServiceWrapper.class);
+        call.setIsTransactionalCall(true);
+        call.setTransactionServiceWrapper(tsw);
+        assertTrue(call.isTransactionalCall());
+        assertNotNull(call.getTransactionServiceWrapper());
+        when(mFeatureFlags.transactionalVideoState()).thenReturn(true);
+
+        // VoIP apps using transactional APIs must register a PhoneAccount that supports
+        // video calling capabilities or the video state will be defaulted to audio
+        assertFalse(call.isVideoCallingSupportedByPhoneAccount());
+        call.setVideoState(VideoProfile.STATE_BIDIRECTIONAL);
+        assertEquals(VideoProfile.STATE_AUDIO_ONLY, call.getVideoState());
+
+        call.setVideoCallingSupportedByPhoneAccount(true);
+        assertTrue(call.isVideoCallingSupportedByPhoneAccount());
+
+        // After the PhoneAccount signals it supports video calling, video state changes can occur
+        call.setVideoState(VideoProfile.STATE_BIDIRECTIONAL);
+        assertEquals(VideoProfile.STATE_BIDIRECTIONAL, call.getVideoState());
+        verify(tsw, times(1)).onVideoStateChanged(call, CallAttributes.VIDEO_CALL);
+    }
+
+    /**
+     * Verify all video state changes are echoed out to the TransactionalServiceWrapper
+     */
+    @Test
+    @SmallTest
+    public void testToggleTransactionalVideoState() {
+        Call call = createCall("1", Call.CALL_DIRECTION_INCOMING);
+        TransactionalServiceWrapper tsw = Mockito.mock(TransactionalServiceWrapper.class);
+        call.setIsTransactionalCall(true);
+        call.setTransactionServiceWrapper(tsw);
+        call.setVideoCallingSupportedByPhoneAccount(true);
+        assertTrue(call.isTransactionalCall());
+        assertNotNull(call.getTransactionServiceWrapper());
+        assertTrue(call.isVideoCallingSupportedByPhoneAccount());
+        when(mFeatureFlags.transactionalVideoState()).thenReturn(true);
+
+        call.setVideoState(VideoProfile.STATE_BIDIRECTIONAL);
+        assertEquals(VideoProfile.STATE_BIDIRECTIONAL, call.getVideoState());
+        verify(tsw, times(1)).onVideoStateChanged(call, CallAttributes.VIDEO_CALL);
+
+        call.setVideoState(VideoProfile.STATE_BIDIRECTIONAL);
+        assertEquals(VideoProfile.STATE_BIDIRECTIONAL, call.getVideoState());
+        verify(tsw, times(2)).onVideoStateChanged(call, CallAttributes.VIDEO_CALL);
+
+        call.setVideoState(VideoProfile.STATE_AUDIO_ONLY);
+        assertEquals(VideoProfile.STATE_AUDIO_ONLY, call.getVideoState());
+        verify(tsw, times(1)).onVideoStateChanged(call, CallAttributes.AUDIO_CALL);
+
+        call.setVideoState(VideoProfile.STATE_BIDIRECTIONAL);
+        assertEquals(VideoProfile.STATE_BIDIRECTIONAL, call.getVideoState());
+        verify(tsw, times(3)).onVideoStateChanged(call, CallAttributes.VIDEO_CALL);
+    }
+
+    @Test
+    public void testMultipleCachedMuteStateChanges() {
+        when(mFeatureFlags.cacheCallAudioCallbacks()).thenReturn(true);
+        TransactionalServiceWrapper tsw = Mockito.mock(TransactionalServiceWrapper.class);
+        Call call = createCall("1", Call.CALL_DIRECTION_INCOMING);
+
+        assertNull(call.getTransactionServiceWrapper());
+
+        call.cacheServiceCallback(new CachedMuteStateChange(true));
+        assertEquals(1, call.getCachedServiceCallbacks().size());
+
+        call.cacheServiceCallback(new CachedMuteStateChange(false));
+        assertEquals(1, call.getCachedServiceCallbacks().size());
+
+        CachedMuteStateChange currentCacheMuteState = (CachedMuteStateChange) call
+                .getCachedServiceCallbacks()
+                .get(CachedMuteStateChange.ID);
+
+        assertFalse(currentCacheMuteState.isMuted());
+
+        call.setTransactionServiceWrapper(tsw);
+        verify(tsw, times(1)).onMuteStateChanged(any(), eq(false));
+        assertEquals(0, call.getCachedServiceCallbacks().size());
+    }
+
+    @Test
+    public void testMultipleCachedCurrentEndpointChanges() {
+        when(mFeatureFlags.cacheCallAudioCallbacks()).thenReturn(true);
+        TransactionalServiceWrapper tsw = Mockito.mock(TransactionalServiceWrapper.class);
+        CallEndpoint earpiece = Mockito.mock(CallEndpoint.class);
+        CallEndpoint speaker = Mockito.mock(CallEndpoint.class);
+        when(earpiece.getEndpointType()).thenReturn(CallEndpoint.TYPE_EARPIECE);
+        when(speaker.getEndpointType()).thenReturn(CallEndpoint.TYPE_SPEAKER);
+
+        Call call = createCall("1", Call.CALL_DIRECTION_INCOMING);
+
+        assertNull(call.getTransactionServiceWrapper());
+
+        call.cacheServiceCallback(new CachedCurrentEndpointChange(earpiece));
+        assertEquals(1, call.getCachedServiceCallbacks().size());
+
+        call.cacheServiceCallback(new CachedCurrentEndpointChange(speaker));
+        assertEquals(1, call.getCachedServiceCallbacks().size());
+
+        CachedCurrentEndpointChange currentEndpointChange = (CachedCurrentEndpointChange) call
+                .getCachedServiceCallbacks()
+                .get(CachedCurrentEndpointChange.ID);
+
+        assertEquals(CallEndpoint.TYPE_SPEAKER,
+                currentEndpointChange.getCurrentCallEndpoint().getEndpointType());
+
+        call.setTransactionServiceWrapper(tsw);
+        verify(tsw, times(1)).onCallEndpointChanged(any(), any());
+        assertEquals(0, call.getCachedServiceCallbacks().size());
+    }
+
+    @Test
+    public void testMultipleCachedAvailableEndpointChanges() {
+        when(mFeatureFlags.cacheCallAudioCallbacks()).thenReturn(true);
+        TransactionalServiceWrapper tsw = Mockito.mock(TransactionalServiceWrapper.class);
+        CallEndpoint earpiece = Mockito.mock(CallEndpoint.class);
+        CallEndpoint bluetooth = Mockito.mock(CallEndpoint.class);
+        Set<CallEndpoint> initialSet = Set.of(earpiece);
+        Set<CallEndpoint> finalSet = Set.of(earpiece, bluetooth);
+        when(earpiece.getEndpointType()).thenReturn(CallEndpoint.TYPE_EARPIECE);
+        when(bluetooth.getEndpointType()).thenReturn(CallEndpoint.TYPE_BLUETOOTH);
+
+        Call call = createCall("1", Call.CALL_DIRECTION_INCOMING);
+
+        assertNull(call.getTransactionServiceWrapper());
+
+        call.cacheServiceCallback(new CachedAvailableEndpointsChange(initialSet));
+        assertEquals(1, call.getCachedServiceCallbacks().size());
+
+        call.cacheServiceCallback(new CachedAvailableEndpointsChange(finalSet));
+        assertEquals(1, call.getCachedServiceCallbacks().size());
+
+        CachedAvailableEndpointsChange availableEndpoints = (CachedAvailableEndpointsChange) call
+                .getCachedServiceCallbacks()
+                .get(CachedAvailableEndpointsChange.ID);
+
+        assertEquals(2, availableEndpoints.getAvailableEndpoints().size());
+
+        call.setTransactionServiceWrapper(tsw);
+        verify(tsw, times(1)).onAvailableCallEndpointsChanged(any(), any());
+        assertEquals(0, call.getCachedServiceCallbacks().size());
+    }
+
+    /**
+     * verify that if multiple types of cached callbacks are added to the call, the call executes
+     * all the callbacks once the service is set.
+     */
+    @Test
+    public void testAllCachedCallbacks() {
+        when(mFeatureFlags.cacheCallAudioCallbacks()).thenReturn(true);
+        TransactionalServiceWrapper tsw = Mockito.mock(TransactionalServiceWrapper.class);
+        CallEndpoint earpiece = Mockito.mock(CallEndpoint.class);
+        CallEndpoint bluetooth = Mockito.mock(CallEndpoint.class);
+        Set<CallEndpoint> availableEndpointsSet = Set.of(earpiece, bluetooth);
+        when(earpiece.getEndpointType()).thenReturn(CallEndpoint.TYPE_EARPIECE);
+        when(bluetooth.getEndpointType()).thenReturn(CallEndpoint.TYPE_BLUETOOTH);
+        Call call = createCall("1", Call.CALL_DIRECTION_INCOMING);
+
+        // The call should have a null service so that callbacks are cached
+        assertNull(call.getTransactionServiceWrapper());
+
+        // add cached callbacks
+        call.cacheServiceCallback(new CachedMuteStateChange(false));
+        assertEquals(1, call.getCachedServiceCallbacks().size());
+        call.cacheServiceCallback(new CachedCurrentEndpointChange(earpiece));
+        assertEquals(2, call.getCachedServiceCallbacks().size());
+        call.cacheServiceCallback(new CachedAvailableEndpointsChange(availableEndpointsSet));
+        assertEquals(3, call.getCachedServiceCallbacks().size());
+
+        // verify the cached callbacks are stored properly within the cache map and the values
+        // can be evaluated
+        CachedMuteStateChange currentCacheMuteState = (CachedMuteStateChange) call
+                .getCachedServiceCallbacks()
+                .get(CachedMuteStateChange.ID);
+        CachedCurrentEndpointChange currentEndpointChange = (CachedCurrentEndpointChange) call
+                .getCachedServiceCallbacks()
+                .get(CachedCurrentEndpointChange.ID);
+        CachedAvailableEndpointsChange availableEndpoints = (CachedAvailableEndpointsChange) call
+                .getCachedServiceCallbacks()
+                .get(CachedAvailableEndpointsChange.ID);
+        assertFalse(currentCacheMuteState.isMuted());
+        assertEquals(CallEndpoint.TYPE_EARPIECE,
+                currentEndpointChange.getCurrentCallEndpoint().getEndpointType());
+        assertEquals(2, availableEndpoints.getAvailableEndpoints().size());
+
+        // set the service to a non-null value
+        call.setTransactionServiceWrapper(tsw);
+
+        // ensure the cached callbacks were executed
+        verify(tsw, times(1)).onMuteStateChanged(any(), anyBoolean());
+        verify(tsw, times(1)).onCallEndpointChanged(any(), any());
+        verify(tsw, times(1)).onAvailableCallEndpointsChanged(any(), any());
+
+        // the cache map should be cleared
+        assertEquals(0, call.getCachedServiceCallbacks().size());
+    }
+
+    /**
      * Basic tests to check which call states are considered transitory.
      */
     @Test
@@ -304,7 +524,6 @@
         doReturn(true).when(mMockCallsManager).isInEmergencyCall();
         call.pullExternalCall();
         verify(mMockConnectionService, never()).pullExternalCall(any());
-        verify(mMockToast).show();
     }
 
     @Test
@@ -473,6 +692,18 @@
 
     @Test
     @SmallTest
+    public void testGetFromCallerInfo_skipLookup() {
+        Resources mockResources = mContext.getResources();
+        when(mockResources.getBoolean(R.bool.skip_incoming_caller_info_query))
+                .thenReturn(true);
+
+        createCall("1");
+
+        verify(mMockCallerInfoLookupHelper, never()).startLookup(any(), any());
+    }
+
+    @Test
+    @SmallTest
     public void testOriginalCallIntent() {
         Call call = createCall("1");
 
@@ -746,6 +977,24 @@
         assertFalse(call.getExtras().containsKey(TelecomManager.EXTRA_DO_NOT_LOG_CALL));
     }
 
+    /**
+     * Verify that a Call can handle a case where no telephony stack is present to detect emergency
+     * numbers.
+     */
+    @Test
+    @SmallTest
+    public void testNoTelephonyEmergencyBehavior() {
+        when(mComponentContextFixture.getTelephonyManager().isEmergencyNumber(any()))
+                .thenReturn(true);
+        Call testCall = createCall("1", Call.CALL_DIRECTION_OUTGOING, Uri.parse("tel:911"));
+        assertTrue(testCall.isEmergencyCall());
+
+        when(mComponentContextFixture.getTelephonyManager().isEmergencyNumber(any()))
+                .thenThrow(new UnsupportedOperationException("Bee-boop"));
+        Call testCall2 = createCall("2", Call.CALL_DIRECTION_OUTGOING, Uri.parse("tel:911"));
+        assertTrue(!testCall2.isEmergencyCall());
+    }
+
     @Test
     @SmallTest
     public void testExcludesConnectionServiceWithoutModifyStatePermissionFromDoNotLogCallExtra() {
@@ -780,11 +1029,27 @@
         assertTrue(call.getExtras().containsKey(TelecomManager.EXTRA_DO_NOT_LOG_CALL));
     }
 
+    @Test
+    @SmallTest
+    public void testSkipLoadingCannedTextResponse() {
+        Call call = createCall("any");
+        Resources mockResources = mContext.getResources();
+        when(mockResources.getBoolean(R.bool.skip_loading_canned_text_response))
+                .thenReturn(true);
+
+
+        assertFalse(call.isRespondViaSmsCapable());
+    }
+
     private Call createCall(String id) {
         return createCall(id, Call.CALL_DIRECTION_UNDEFINED);
     }
 
     private Call createCall(String id, int callDirection) {
+        return createCall(id, callDirection, TEST_ADDRESS);
+    }
+
+    private Call createCall(String id, int callDirection, Uri address) {
         return new Call(
                 id,
                 mContext,
@@ -792,7 +1057,7 @@
                 mLock,
                 null,
                 mMockPhoneNumberUtilsAdapter,
-                TEST_ADDRESS,
+                address,
                 null /* GatewayInfo */,
                 null,
                 SIM_1_HANDLE,
diff --git a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
index cb9aba9..ae5e6c1 100644
--- a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
@@ -33,9 +33,11 @@
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
@@ -57,6 +59,7 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Handler;
+import android.os.IBinder;
 import android.os.Looper;
 import android.os.OutcomeReceiver;
 import android.os.Process;
@@ -64,7 +67,7 @@
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.os.UserManager;
-import android.provider.BlockedNumberContract;
+import android.provider.BlockedNumbersManager;
 import android.telecom.CallException;
 import android.telecom.CallScreeningService;
 import android.telecom.CallerInfo;
@@ -85,6 +88,7 @@
 import androidx.test.filters.MediumTest;
 import androidx.test.filters.SmallTest;
 
+import com.android.internal.telecom.IConnectionService;
 import com.android.server.telecom.AnomalyReporterAdapter;
 import com.android.server.telecom.AsyncRingtonePlayer;
 import com.android.server.telecom.Call;
@@ -103,6 +107,7 @@
 import com.android.server.telecom.ConnectionServiceFocusManager;
 import com.android.server.telecom.ConnectionServiceFocusManager.ConnectionServiceFocusManagerFactory;
 import com.android.server.telecom.ConnectionServiceWrapper;
+import com.android.server.telecom.CreateConnectionResponse;
 import com.android.server.telecom.DefaultDialerCache;
 import com.android.server.telecom.EmergencyCallDiagnosticLogger;
 import com.android.server.telecom.EmergencyCallHelper;
@@ -298,7 +303,6 @@
     @Mock private BluetoothStateReceiver mBluetoothStateReceiver;
     @Mock private RoleManagerAdapter mRoleManagerAdapter;
     @Mock private ToastFactory mToastFactory;
-    @Mock private Toast mToast;
     @Mock private CallAnomalyWatchdog mCallAnomalyWatchdog;
 
     @Mock private EmergencyCallDiagnosticLogger mEmergencyCallDiagnosticLogger;
@@ -312,6 +316,9 @@
     @Mock private FeatureFlags mFeatureFlags;
     @Mock private com.android.internal.telephony.flags.FeatureFlags mTelephonyFlags;
     @Mock private IncomingCallFilterGraph mIncomingCallFilterGraph;
+    @Mock private Context mMockCreateContextAsUser;
+    @Mock private UserManager mMockCurrentUserManager;
+    @Mock private IConnectionService mIConnectionService;
     private CallsManager mCallsManager;
 
     @Override
@@ -403,14 +410,23 @@
                 eq(CALL_PROVIDER_HANDLE), any())).thenReturn(CALL_PROVIDER_ACCOUNT);
         when(mPhoneAccountRegistrar.getPhoneAccount(
                 eq(WORK_HANDLE), any())).thenReturn(WORK_ACCOUNT);
-        when(mToastFactory.makeText(any(), anyInt(), anyInt())).thenReturn(mToast);
-        when(mToastFactory.makeText(any(), any(), anyInt())).thenReturn(mToast);
         when(mFeatureFlags.separatelyBindToBtIncallService()).thenReturn(false);
+        when(mFeatureFlags.telecomResolveHiddenDependencies()).thenReturn(true);
+        when(mContext.createContextAsUser(any(UserHandle.class), eq(0)))
+                .thenReturn(mMockCreateContextAsUser);
+        when(mMockCreateContextAsUser.getSystemService(UserManager.class))
+                .thenReturn(mMockCurrentUserManager);
+        when(mIConnectionService.asBinder()).thenReturn(mock(IBinder.class));
+
+        mComponentContextFixture.addConnectionService(
+                SIM_1_ACCOUNT.getAccountHandle().getComponentName(), mIConnectionService);
     }
 
     @Override
     @After
     public void tearDown() throws Exception {
+        mComponentContextFixture.removeConnectionService(
+                SIM_1_ACCOUNT.getAccountHandle().getComponentName(), mIConnectionService);
         super.tearDown();
     }
 
@@ -1436,6 +1452,36 @@
         verify(incomingCall).setIsUsingCallFiltering(eq(false));
     }
 
+    /**
+     * Verify the ability to skip call filtering when Telephony reports we are in emergency SMS mode
+     * and also verify that when Telephony is not available we will not try to skip filtering.
+     */
+    @SmallTest
+    @Test
+    public void testFilteringWhenEmergencySmsCheckFails() {
+        // First see if it works when Telephony is present.
+        Call incomingCall = addSpyCall(CallState.NEW);
+        doReturn(true).when(mComponentContextFixture.getTelephonyManager()).isInEmergencySmsMode();
+        mCallsManager.onSuccessfulIncomingCall(incomingCall);
+        verify(incomingCall).setIsUsingCallFiltering(eq(false));
+
+        // Ensure when there is no telephony it doesn't try to skip filtering.
+        Call incomingCall2 = addSpyCall(CallState.NEW);
+        doThrow(new UnsupportedOperationException("Bee-boop")).when(
+                mComponentContextFixture.getTelephonyManager()).isInEmergencySmsMode();
+        mCallsManager.onSuccessfulIncomingCall(incomingCall2);
+        verify(incomingCall2).setIsUsingCallFiltering(eq(true));
+    }
+
+    @SmallTest
+    @Test
+    public void testDsdaAvailableCheckWhenNoTelephony() {
+        doThrow(new UnsupportedOperationException("Bee-boop")).when(
+                mComponentContextFixture.getTelephonyManager())
+                        .getMaxNumberOfSimultaneouslyActiveSims();
+        assertFalse(mCallsManager.isDsdaCallingPossible());
+    }
+
     @SmallTest
     @Test
     public void testNoFilteringOfNetworkIdentifiedEmergencyCalls() {
@@ -2723,6 +2769,24 @@
     }
 
     /**
+     * Verify when Telephony is not available we don't try to block redirection due to the failed
+     * isEmergency check.
+     */
+    @SmallTest
+    @Test
+    public void testEmergencyCheckFailsOnRedirectionCheckCompleteDueToNoTelephony() {
+        when(mComponentContextFixture.getTelephonyManager().isEmergencyNumber(anyString()))
+                .thenThrow(new UnsupportedOperationException("Bee boop"));
+
+        Call callSpy = addSpyCall(CallState.NEW);
+        mCallsManager.onCallRedirectionComplete(callSpy, Uri.parse("tel:911"),
+                SIM_1_HANDLE_SECONDARY,
+                new GatewayInfo("foo", TEST_ADDRESS2, TEST_ADDRESS), true /* speakerphoneOn */,
+                VideoProfile.STATE_AUDIO_ONLY, false /* shouldCancelCall */, "" /* uiAction */);
+        verify(callSpy, never()).disconnect(anyString());
+    }
+
+    /**
      * Verifies that target phone account is set in startOutgoingCall. The multi-user functionality
      * is dependent on the call's phone account handle being present so this test ensures that
      * existing outgoing call flow does not break from future updates.
@@ -2824,9 +2888,9 @@
         mCallsManager.addConnectionServiceRepositoryCache(WORK_HANDLE.getComponentName(),
                 WORK_HANDLE.getUserHandle(), service);
 
-        UserManager um = mContext.getSystemService(UserManager.class);
-        when(um.isUserAdmin(anyInt())).thenReturn(false);
-        when(um.isQuietModeEnabled(eq(WORK_HANDLE.getUserHandle()))).thenReturn(false);
+        when(mMockCurrentUserManager.isAdminUser()).thenReturn(false);
+        when(mMockCurrentUserManager.isQuietModeEnabled(eq(WORK_HANDLE.getUserHandle())))
+                .thenReturn(false);
         when(mPhoneAccountRegistrar.getPhoneAccountUnchecked(eq(WORK_HANDLE)))
                 .thenReturn(WORK_ACCOUNT);
         Call newCall = mCallsManager.processIncomingCallIntent(
@@ -2845,9 +2909,9 @@
         mCallsManager.addConnectionServiceRepositoryCache(WORK_HANDLE.getComponentName(),
                 WORK_HANDLE.getUserHandle(), service);
 
-        UserManager um = mContext.getSystemService(UserManager.class);
-        when(um.isUserAdmin(anyInt())).thenReturn(true);
-        when(um.isQuietModeEnabled(eq(WORK_HANDLE.getUserHandle()))).thenReturn(true);
+        when(mMockCurrentUserManager.isAdminUser()).thenReturn(true);
+        when(mMockCurrentUserManager.isQuietModeEnabled(any(UserHandle.class)))
+                .thenReturn(true);
         when(mPhoneAccountRegistrar.getPhoneAccountUnchecked(eq(WORK_HANDLE)))
                 .thenReturn(WORK_ACCOUNT);
         Call newCall = mCallsManager.processIncomingCallIntent(
@@ -2868,8 +2932,8 @@
 
         when(mEmergencyCallHelper.isLastOutgoingEmergencyCallPAH(eq(SIM_2_HANDLE)))
                 .thenReturn(true);
-        UserManager um = mContext.getSystemService(UserManager.class);
-        when(um.isQuietModeEnabled(eq(SIM_2_HANDLE.getUserHandle()))).thenReturn(true);
+        when(mMockCurrentUserManager.isQuietModeEnabled(eq(SIM_2_HANDLE.getUserHandle())))
+                .thenReturn(true);
         Call newCall = mCallsManager.processIncomingCallIntent(
                 SIM_2_HANDLE, new Bundle(), false);
 
@@ -2887,9 +2951,9 @@
 
         when(mEmergencyCallHelper.isLastOutgoingEmergencyCallPAH(eq(WORK_HANDLE)))
                 .thenReturn(true);
-        UserManager um = mContext.getSystemService(UserManager.class);
-        when(um.isUserAdmin(anyInt())).thenReturn(false);
-        when(um.isQuietModeEnabled(eq(WORK_HANDLE.getUserHandle()))).thenReturn(false);
+        when(mMockCurrentUserManager.isAdminUser()).thenReturn(false);
+        when(mMockCurrentUserManager.isQuietModeEnabled(eq(WORK_HANDLE.getUserHandle())))
+                .thenReturn(false);
         when(mPhoneAccountRegistrar.getPhoneAccountUnchecked(eq(WORK_HANDLE)))
                 .thenReturn(WORK_ACCOUNT);
         Call newCall = mCallsManager.processIncomingCallIntent(
@@ -2910,8 +2974,8 @@
         Bundle extras = new Bundle();
         extras.putParcelable(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS, TEST_ADDRESS);
         TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
-        UserManager um = mContext.getSystemService(UserManager.class);
-        when(um.isQuietModeEnabled(eq(SIM_2_HANDLE.getUserHandle()))).thenReturn(true);
+        when(mMockCurrentUserManager.isQuietModeEnabled(eq(SIM_2_HANDLE.getUserHandle())))
+                .thenReturn(true);
         when(tm.isEmergencyNumber(any(String.class))).thenReturn(true);
         Call newCall = mCallsManager.processIncomingCallIntent(
                 SIM_2_HANDLE, extras, false);
@@ -2974,42 +3038,152 @@
         assertFalse(mCallsManager.getCalls().contains(call));
     }
 
+    /**
+     * Verify that
+     * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
+     * OutcomeReceiver)}s OutcomeReceiver returns onResult when there is no active call to place
+     * on hold.
+     */
     @MediumTest
     @Test
-    public void testHoldTransactional() throws Exception {
-        CountDownLatch latch = new CountDownLatch(1);
+    public void testHoldWhenActiveCallIsNullOrSame() throws Exception {
         Call newCall = addSpyCall();
-
         // case 1: no active call, no need to put the call on hold
-        when(mConnectionSvrFocusMgr.getCurrentFocusCall()).thenReturn(null);
-        mCallsManager.transactionHoldPotentialActiveCallForNewCall(newCall,
-                new LatchedOutcomeReceiver(latch, true));
-        waitForCountDownLatch(latch);
-
+        assertHoldActiveCallForNewCall(
+                newCall,
+                null  /* activeCall */,
+                false /* isCallControlRequest */,
+                true  /* expectOnResult */);
         // case 2: active call == new call, no need to put the call on hold
-        latch = new CountDownLatch(1);
-        when(mConnectionSvrFocusMgr.getCurrentFocusCall()).thenReturn(newCall);
-        mCallsManager.transactionHoldPotentialActiveCallForNewCall(newCall,
-                new LatchedOutcomeReceiver(latch, true));
-        waitForCountDownLatch(latch);
+        assertHoldActiveCallForNewCall(
+                newCall,
+                newCall /* activeCall */,
+                false /* isCallControlRequest */,
+                true  /* expectOnResult */);
+    }
 
-        // case 3: cannot hold current active call early check
+    /**
+     * Verify that
+     * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
+     * OutcomeReceiver)}s OutcomeReceiver returns onError when there is an active call that
+     * cannot be held, and it's a CallControlRequest.
+     */
+    @MediumTest
+    @Test
+    public void testHoldFailsWithUnholdableCallAndCallControlRequest() throws Exception {
         Call cannotHoldCall = addSpyCall(SIM_1_HANDLE, null,
                 CallState.ACTIVE, 0, 0);
-        latch = new CountDownLatch(1);
-        when(mConnectionSvrFocusMgr.getCurrentFocusCall()).thenReturn(cannotHoldCall);
-        mCallsManager.transactionHoldPotentialActiveCallForNewCall(newCall,
-                new LatchedOutcomeReceiver(latch, false));
-        waitForCountDownLatch(latch);
+        assertHoldActiveCallForNewCall(
+                addSpyCall(),
+                cannotHoldCall /* activeCall */,
+                true /* isCallControlRequest */,
+                false  /* expectOnResult */);
+    }
 
-        // case 4: activeCall != newCall && canHold(activeCall)
+    /**
+     * Verify that
+     * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
+     * OutcomeReceiver)}s OutcomeReceiver returns onResult when there is a holdable call and
+     * it's a CallControlRequest.
+     */
+    @MediumTest
+    @Test
+    public void testHoldSuccessWithHoldableActiveCall() throws Exception {
+        Call newCall = addSpyCall(VOIP_1_HANDLE, CallState.CONNECTING);
         Call canHoldCall = addSpyCall(SIM_1_HANDLE, null,
                 CallState.ACTIVE, Connection.CAPABILITY_HOLD, 0);
-        latch = new CountDownLatch(1);
-        when(mConnectionSvrFocusMgr.getCurrentFocusCall()).thenReturn(canHoldCall);
-        mCallsManager.transactionHoldPotentialActiveCallForNewCall(newCall,
-                new LatchedOutcomeReceiver(latch, true));
-        waitForCountDownLatch(latch);
+        assertHoldActiveCallForNewCall(
+                newCall,
+                canHoldCall /* activeCall */,
+                true /* isCallControlRequest */,
+                true  /* expectOnResult */);
+    }
+
+    /**
+     * Verify that
+     * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
+     * OutcomeReceiver)}s OutcomeReceiver returns onResult when there is an active call that
+     * supports hold, and it's a CallControlRequest.
+     */
+    @MediumTest
+    @Test
+    public void testHoldWhenTheActiveCallSupportsHold() throws Exception {
+        Call newCall = addSpyCall();
+        Call supportsHold = addSpyCall(SIM_1_HANDLE, null,
+                CallState.ACTIVE, Connection.CAPABILITY_SUPPORT_HOLD, 0);
+        assertHoldActiveCallForNewCall(
+                newCall,
+                supportsHold /* activeCall */,
+                true /* isCallControlRequest */,
+                true  /* expectOnResult */);
+    }
+
+    /**
+     * Verify that
+     * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
+     * OutcomeReceiver)}s OutcomeReceiver returns onResult when there is an active call that
+     * supports hold + can hold, and it's a CallControlRequest.
+     */
+    @MediumTest
+    @Test
+    public void testHoldWhenTheActiveCallSupportsAndCanHold() throws Exception {
+        Call newCall = addSpyCall();
+        Call supportsHold = addSpyCall(SIM_1_HANDLE, null,
+                CallState.ACTIVE,
+                Connection.CAPABILITY_HOLD | Connection.CAPABILITY_SUPPORT_HOLD,
+                0);
+        assertHoldActiveCallForNewCall(
+                newCall,
+                supportsHold /* activeCall */,
+                true /* isCallControlRequest */,
+                true  /* expectOnResult */);
+    }
+
+    /**
+     * Verify that
+     * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
+     * OutcomeReceiver)}s OutcomeReceiver returns onResult when there is an active call that
+     * supports hold + can hold, and it's a CallControlCallbackRequest.
+     */
+    @MediumTest
+    @Test
+    public void testHoldForCallControlCallbackRequestWithActiveCallThatCanHold() throws Exception {
+        Call newCall = addSpyCall();
+        Call supportsHold = addSpyCall(SIM_1_HANDLE, null,
+                CallState.ACTIVE, Connection.CAPABILITY_HOLD | Connection.CAPABILITY_SUPPORT_HOLD,
+                0);
+        assertHoldActiveCallForNewCall(
+                newCall,
+                supportsHold /* activeCall */,
+                false /* isCallControlRequest */,
+                true  /* expectOnResult */);
+    }
+
+    /**
+     * Verify that
+     * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
+     * OutcomeReceiver)}s OutcomeReceiver returns onResult when there is an active unholdable call,
+     * and it's a CallControlCallbackRequest.
+     */
+    @MediumTest
+    @Test
+    public void testHoldDisconnectsTheActiveCall() throws Exception {
+        Call newCall = addSpyCall(VOIP_1_HANDLE, CallState.CONNECTING);
+        Call activeUnholdableCall = addSpyCall(SIM_1_HANDLE, null,
+                CallState.ACTIVE, 0, 0);
+
+        doAnswer(invocation -> {
+            doReturn(true).when(activeUnholdableCall).isLocallyDisconnecting();
+            return null;
+        }).when(activeUnholdableCall).disconnect();
+
+        assertHoldActiveCallForNewCall(
+                newCall,
+                activeUnholdableCall /* activeCall */,
+                false /* isCallControlRequest */,
+                true  /* expectOnResult */);
+
+        verify(activeUnholdableCall, atLeast(1)).disconnect();
     }
 
     @SmallTest
@@ -3073,6 +3247,35 @@
         assertTrue(result.contains("onReceiveResult"));
     }
 
+    @Test
+    public void testConnectionServiceCreateConnectionTimeout() throws Exception {
+        ConnectionServiceWrapper service = new ConnectionServiceWrapper(
+                SIM_1_ACCOUNT.getAccountHandle().getComponentName(), null,
+                mPhoneAccountRegistrar, mCallsManager, mContext, mLock, null, mFeatureFlags);
+        TestScheduledExecutorService scheduledExecutorService = new TestScheduledExecutorService();
+        service.setScheduledExecutorService(scheduledExecutorService);
+        Call call = addSpyCall();
+        service.addCall(call);
+        when(call.isCreateConnectionComplete()).thenReturn(false);
+        CreateConnectionResponse response = mock(CreateConnectionResponse.class);
+
+        service.createConnection(call, response);
+        waitUntilConditionIsTrueOrTimeout(new Condition() {
+            @Override
+            public Object expected() {
+                return true;
+            }
+
+            @Override
+            public Object actual() {
+                return scheduledExecutorService.isRunnableScheduledAtTime(15000L);
+            }
+        }, 5000L, "Expected job failed to schedule");
+        scheduledExecutorService.advanceTime(15000L);
+        verify(response).handleCreateConnectionFailure(
+                eq(new DisconnectCause(DisconnectCause.ERROR)));
+    }
+
     @SmallTest
     @Test
     public void testOnFailedOutgoingCallUnholdsCallAfterLocallyDisconnect() {
@@ -3433,18 +3636,14 @@
         when(mBlockedNumbersAdapter.shouldShowEmergencyCallNotification(any(Context.class)))
                 .thenReturn(true);
         mComponentContextFixture.getBroadcastReceivers().forEach(c -> c.onReceive(mContext,
-                new Intent(
-                        BlockedNumberContract.BlockedNumbers
-                                .ACTION_BLOCK_SUPPRESSION_STATE_CHANGED)));
+                new Intent(BlockedNumbersManager.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED)));
         verify(mBlockedNumbersAdapter).updateEmergencyCallNotification(any(Context.class),
                 eq(true));
 
         when(mBlockedNumbersAdapter.shouldShowEmergencyCallNotification(any(Context.class)))
                 .thenReturn(false);
         mComponentContextFixture.getBroadcastReceivers().forEach(c -> c.onReceive(mContext,
-                new Intent(
-                        BlockedNumberContract.BlockedNumbers
-                                .ACTION_BLOCK_SUPPRESSION_STATE_CHANGED)));
+                new Intent(BlockedNumbersManager.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED)));
         verify(mBlockedNumbersAdapter).updateEmergencyCallNotification(any(Context.class),
                 eq(false));
     }
@@ -3463,9 +3662,7 @@
         // WHEN
         when(mPhoneAccountRegistrar.getPhoneAccountUnchecked(any()))
                 .thenReturn(SM_W_DIFFERENT_PACKAGE_AND_USER);
-        UserManager um = mContext.getSystemService(UserManager.class);
-        when(um.isUserAdmin(eq(mCallsManager.getCurrentUserHandle().getIdentifier())))
-                .thenReturn(true);
+        when(mMockCurrentUserManager.isAdminUser()).thenReturn(true);
 
         // THEN
         mCallsManager.processIncomingCallIntent(SELF_MANAGED_W_CUSTOM_HANDLE, new Bundle(), false);
@@ -3563,8 +3760,6 @@
                 .setShouldAllowCall(true)
                 .setShouldReject(false)
                 .build();
-        when(mInCallController.bindToBTService(eq(call))).thenReturn(
-                CompletableFuture.completedFuture(true));
         when(mInCallController.isBoundAndConnectedToBTService(any(UserHandle.class)))
                 .thenReturn(false);
 
@@ -3572,7 +3767,7 @@
 
         InOrder inOrder = inOrder(mInCallController, call, mInCallController);
 
-        inOrder.verify(mInCallController).bindToBTService(eq(call));
+        inOrder.verify(mInCallController).bindToBTService(eq(call), eq(null));
         inOrder.verify(call).setState(eq(CallState.RINGING), anyString());
     }
 
@@ -3731,4 +3926,35 @@
         when(mockTelephonyManager.getPhoneCapability()).thenReturn(mPhoneCapability);
         when(mPhoneCapability.getMaxActiveVoiceSubscriptions()).thenReturn(num);
     }
+
+   private void assertHoldActiveCallForNewCall(
+            Call newCall,
+            Call activeCall,
+            boolean isCallControlRequest,
+            boolean expectOnResult)
+            throws InterruptedException {
+        CountDownLatch latch = new CountDownLatch(1);
+        when(mFeatureFlags.transactionalHoldDisconnectsUnholdable()).thenReturn(true);
+        when(mConnectionSvrFocusMgr.getCurrentFocusCall()).thenReturn(activeCall);
+        mCallsManager.transactionHoldPotentialActiveCallForNewCall(
+                newCall,
+                isCallControlRequest,
+                new LatchedOutcomeReceiver(latch, expectOnResult));
+        waitForCountDownLatch(latch);
+    }
+
+    private void waitUntilConditionIsTrueOrTimeout(Condition condition, long timeout,
+            String description) throws InterruptedException {
+        final long start = System.currentTimeMillis();
+        while (!condition.expected().equals(condition.actual())
+                && System.currentTimeMillis() - start < timeout) {
+            sleep(50);
+        }
+        assertEquals(description, condition.expected(), condition.actual());
+    }
+
+    protected interface Condition {
+        Object expected();
+        Object actual();
+    }
 }
diff --git a/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java b/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
index 54aaa4c..25f94c6 100644
--- a/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
+++ b/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
@@ -73,6 +73,7 @@
 import android.os.Vibrator;
 import android.os.VibratorManager;
 import android.permission.PermissionCheckerManager;
+import android.provider.BlockedNumbersManager;
 import android.telecom.ConnectionService;
 import android.telecom.Log;
 import android.telecom.InCallService;
@@ -121,6 +122,7 @@
  */
 public class ComponentContextFixture implements TestFixture<Context> {
     private HandlerThread mHandlerThread;
+    private Map<UserHandle, Context> mContextsByUser = new HashMap<>();
 
     public class FakeApplicationContext extends MockContext {
         @Override
@@ -137,6 +139,9 @@
 
         @Override
         public Context createContextAsUser(UserHandle userHandle, int flags) {
+            if (mContextsByUser.containsKey(userHandle)) {
+                return mContextsByUser.get(userHandle);
+            }
             return this;
         }
 
@@ -251,6 +256,8 @@
                     return mSensorPrivacyManager;
                 case Context.ACCESSIBILITY_SERVICE:
                     return mAccessibilityManager;
+                case Context.BLOCKED_NUMBERS_SERVICE:
+                    return mBlockedNumbersManager;
                 default:
                     return null;
             }
@@ -292,6 +299,8 @@
                 return Context.BUGREPORT_SERVICE;
             } else if (svcClass == TelecomManager.class) {
                 return Context.TELECOM_SERVICE;
+            } else if (svcClass == BlockedNumbersManager.class) {
+                return Context.BLOCKED_NUMBERS_SERVICE;
             }
             throw new UnsupportedOperationException(svcClass.getName());
         }
@@ -635,6 +644,7 @@
     private final List<BroadcastReceiver> mBroadcastReceivers = new ArrayList<>();
 
     private TelecomManager mTelecomManager = mock(TelecomManager.class);
+    private BlockedNumbersManager mBlockedNumbersManager = mock(BlockedNumbersManager.class);
 
     public ComponentContextFixture(FeatureFlags featureFlags) {
         MockitoAnnotations.initMocks(this);
@@ -837,6 +847,10 @@
         mSubscriptionManager = subscriptionManager;
     }
 
+    public SubscriptionManager getSubscriptionManager() {
+        return mSubscriptionManager;
+    }
+
     public TelephonyManager getTelephonyManager() {
         return mTelephonyManager;
     }
@@ -857,6 +871,19 @@
         return mBroadcastReceivers;
     }
 
+    public TelephonyRegistryManager getTelephonyRegistryManager() {
+        return mTelephonyRegistryManager;
+    }
+
+    /**
+     * For testing purposes, add a context for a specific user.
+     * @param userHandle the userhandle
+     * @param context the context
+     */
+    public void addContextForUser(UserHandle userHandle, Context context) {
+        mContextsByUser.put(userHandle, context);
+    }
+
     private void addService(String action, ComponentName name, IInterface service) {
         mComponentNamesByAction.put(action, name);
         mServiceByComponentName.put(name, service);
diff --git a/tests/src/com/android/server/telecom/tests/ConnectionServiceWrapperTest.java b/tests/src/com/android/server/telecom/tests/ConnectionServiceWrapperTest.java
new file mode 100644
index 0000000..c815e8e
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/ConnectionServiceWrapperTest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom.tests;
+
+import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.content.ComponentName;
+import android.os.UserHandle;
+
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.ConnectionServiceRepository;
+import com.android.server.telecom.ConnectionServiceWrapper;
+import com.android.server.telecom.PhoneAccountRegistrar;
+import com.android.server.telecom.TelecomSystem;
+import com.android.server.telecom.flags.FeatureFlags;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class ConnectionServiceWrapperTest extends TelecomTestCase {
+    @Override
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+    }
+
+    @Override
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    /**
+     * Verify we don't crash when getting the last known cell id and there is no telephony.
+     */
+    @Test
+    public void testGetLastKnownCellIdWhenNoTelephony() {
+        ConnectionServiceWrapper wrapper = new ConnectionServiceWrapper(
+                ComponentName.unflattenFromString("foo/baz"),
+                mock(ConnectionServiceRepository.class),
+                mock(PhoneAccountRegistrar.class),
+                mock(CallsManager.class),
+                mContext,
+                new TelecomSystem.SyncRoot() {},
+                UserHandle.CURRENT,
+                mock(FeatureFlags.class));
+        when(mComponentContextFixture.getTelephonyManager().getLastKnownCellIdentity())
+                .thenThrow(new UnsupportedOperationException("Bee boop"));
+        assertNull(wrapper.getLastKnownCellIdentity());
+   }
+}
diff --git a/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java b/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java
index 2f27bb5..ddbc250 100644
--- a/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java
+++ b/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java
@@ -16,12 +16,16 @@
 
 package com.android.server.telecom.tests;
 
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyList;
 import static org.mockito.ArgumentMatchers.nullable;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.after;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
@@ -35,6 +39,7 @@
 import android.net.Uri;
 import android.os.Binder;
 import android.os.UserHandle;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.telecom.DisconnectCause;
 import android.telecom.PhoneAccount;
 import android.telecom.PhoneAccountHandle;
@@ -42,18 +47,22 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.internal.telephony.flags.Flags;
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallIdMapper;
+import com.android.server.telecom.CallState;
 import com.android.server.telecom.ConnectionServiceFocusManager;
 import com.android.server.telecom.ConnectionServiceRepository;
 import com.android.server.telecom.ConnectionServiceWrapper;
 import com.android.server.telecom.CreateConnectionProcessor;
 import com.android.server.telecom.CreateConnectionResponse;
+import com.android.server.telecom.CreateConnectionTimeout;
 import com.android.server.telecom.PhoneAccountRegistrar;
-import com.android.server.telecom.flags.FeatureFlags;
+import com.android.server.telecom.Timeouts;
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -62,6 +71,7 @@
 import org.mockito.invocation.InvocationOnMock;
 import org.mockito.stubbing.Answer;
 
+import java.time.Duration;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -73,6 +83,7 @@
  */
 @RunWith(JUnit4.class)
 public class CreateConnectionProcessorTest extends TelecomTestCase {
+    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
 
     private static final String TEST_PACKAGE = "com.android.server.telecom.tests";
     private static final String TEST_CLASS =
@@ -89,12 +100,14 @@
     Call mMockCall;
     @Mock
     ConnectionServiceFocusManager mConnectionServiceFocusManager;
+    @Mock Timeouts.Adapter mTimeoutsAdapter;
 
     CreateConnectionProcessor mTestCreateConnectionProcessor;
+    CreateConnectionTimeout mTestCreateConnectionTimeout;
 
     private ArrayList<PhoneAccount> phoneAccounts;
-    private HashMap<Integer,Integer> mSubToSlot;
-    private HashMap<PhoneAccount,Integer> mAccountToSub;
+    private HashMap<Integer, Integer> mSubToSlot;
+    private HashMap<PhoneAccount, Integer> mAccountToSub;
 
     @Override
     @Before
@@ -117,11 +130,11 @@
                          return null;
                      }
                  }
-                ).when(mConnectionServiceFocusManager).requestFocus(any(), any());
+        ).when(mConnectionServiceFocusManager).requestFocus(any(), any());
 
         mTestCreateConnectionProcessor = new CreateConnectionProcessor(mMockCall,
                 mMockConnectionServiceRepository, mMockCreateConnectionResponse,
-                mMockAccountRegistrar, mContext, mFeatureFlags);
+                mMockAccountRegistrar, mContext, mFeatureFlags, mTimeoutsAdapter);
 
         mAccountToSub = new HashMap<>();
         phoneAccounts = new ArrayList<>();
@@ -144,6 +157,11 @@
                 .thenReturn(phoneAccounts);
         when(mMockCall.getAssociatedUser()).
                 thenReturn(Binder.getCallingUserHandle());
+
+        mTestCreateConnectionTimeout = new CreateConnectionTimeout(mContext, mMockAccountRegistrar,
+                makeConnectionServiceWrapper(), mMockCall, mTimeoutsAdapter);
+
+        mSetFlagsRule.enableFlags(Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG);
     }
 
     @Override
@@ -300,7 +318,8 @@
 
     /**
      * Ensure that when a test emergency number is being dialed and we restrict the usable
-     * PhoneAccounts using {@link PhoneAccountRegistrar#filterRestrictedPhoneAccounts(List)}, the
+     * PhoneAccounts using {@link PhoneAccountRegistrar#filterRestrictedPhoneAccounts(List)},
+     * the
      * test emergency call is sent on the filtered PhoneAccount.
      */
     @SmallTest
@@ -338,7 +357,8 @@
     }
 
     /**
-     * Ensure that when no phone accounts (visible to the user) are available for the call, we use
+     * Ensure that when no phone accounts (visible to the user) are available for the call, we
+     * use
      * an available sim from other another user (on the condition that the user has the
      * INTERACT_ACROSS_USERS permission).
      */
@@ -368,7 +388,8 @@
     }
 
     /**
-     * Ensure that the non-emergency capable PhoneAccount and the SIM manager is not chosen to place
+     * Ensure that the non-emergency capable PhoneAccount and the SIM manager is not chosen to
+     * place
      * the emergency call if there is an emergency capable PhoneAccount available as well.
      */
     @SmallTest
@@ -405,7 +426,8 @@
     }
 
     /**
-     * 1) Ensure that if there is a non-SIM PhoneAccount, it is not chosen as the Phone Account to
+     * 1) Ensure that if there is a non-SIM PhoneAccount, it is not chosen as the Phone Account
+     * to
      * dial the emergency call.
      * 2) Register multiple emergency capable PhoneAccounts. Since there is not preference, we
      * default to sending on the lowest slot.
@@ -445,8 +467,10 @@
     }
 
     /**
-     * Ensure that the call goes out on the PhoneAccount that has the CAPABILITY_EMERGENCY_PREFERRED
-     * capability, even if the user specifically chose the other emergency capable PhoneAccount.
+     * Ensure that the call goes out on the PhoneAccount that has the
+     * CAPABILITY_EMERGENCY_PREFERRED
+     * capability, even if the user specifically chose the other emergency capable
+     * PhoneAccount.
      */
     @SmallTest
     @Test
@@ -478,7 +502,8 @@
     }
 
     /**
-     * If there is no phone account with CAPABILITY_EMERGENCY_PREFERRED capability, choose the user
+     * If there is no phone account with CAPABILITY_EMERGENCY_PREFERRED capability, choose the
+     * user
      * chosen target account.
      */
     @SmallTest
@@ -552,7 +577,8 @@
     }
 
     /**
-     * If the user preferred PhoneAccount is associated with an invalid slot, place on the other,
+     * If the user preferred PhoneAccount is associated with an invalid slot, place on the
+     * other,
      * valid slot.
      */
     @SmallTest
@@ -690,7 +716,8 @@
 
     /**
      * Tests to verify that the
-     * {@link CreateConnectionProcessor#sortSimPhoneAccountsForEmergency(List, PhoneAccount)} can
+     * {@link CreateConnectionProcessor#sortSimPhoneAccountsForEmergency(List, PhoneAccount)}
+     * can
      * successfully sort without running into sort issues related to the hashcodes of the
      * PhoneAccounts.
      */
@@ -707,6 +734,176 @@
         }
     }
 
+    @Test
+    public void testIsTimeoutNeededForCall_nonEmergencyCall() {
+        when(mMockCall.isEmergencyCall()).thenReturn(false);
+
+        assertFalse(mTestCreateConnectionTimeout.isTimeoutNeededForCall(null, null));
+    }
+
+    @Test
+    public void testIsTimeoutNeededForCall_noConnectionManager() {
+        when(mMockCall.isEmergencyCall()).thenReturn(true);
+        List<PhoneAccountHandle> phoneAccountHandles = new ArrayList<>();
+        // Put in a regular phone account handle
+        PhoneAccount regularAccount = makePhoneAccount("tel_acct1",
+                PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION);
+        phoneAccountHandles.add(regularAccount.getAccountHandle());
+        // Create a connection manager for the call and do not include in phoneAccountHandles
+        createNewConnectionManagerPhoneAccountForCall(mMockCall, "cm_acct", 0);
+
+        assertFalse(mTestCreateConnectionTimeout.isTimeoutNeededForCall(phoneAccountHandles, null));
+    }
+
+    @Test
+    public void testIsTimeoutNeededForCall_usingConnectionManager() {
+        when(mMockCall.isEmergencyCall()).thenReturn(true);
+        List<PhoneAccountHandle> phoneAccountHandles = new ArrayList<>();
+        // Put in a regular phone account handle
+        PhoneAccount regularAccount = makePhoneAccount("tel_acct1",
+                PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION);
+        phoneAccountHandles.add(regularAccount.getAccountHandle());
+        // Create a connection manager for the call and include it in phoneAccountHandles
+        PhoneAccount callManagerPA = createNewConnectionManagerPhoneAccountForCall(mMockCall,
+                "cm_acct", 0);
+        phoneAccountHandles.add(callManagerPA.getAccountHandle());
+
+        assertFalse(mTestCreateConnectionTimeout.isTimeoutNeededForCall(
+                phoneAccountHandles, callManagerPA.getAccountHandle()));
+    }
+
+    @Test
+    public void testIsTimeoutNeededForCall_NotSystemSimCallManager() {
+        when(mMockCall.isEmergencyCall()).thenReturn(true);
+        List<PhoneAccountHandle> phoneAccountHandles = new ArrayList<>();
+        // Put in a regular phone account handle
+        PhoneAccount regularAccount = makePhoneAccount("tel_acct1",
+                PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION);
+        phoneAccountHandles.add(regularAccount.getAccountHandle());
+        // Create a connection manager for the call and include it in phoneAccountHandles
+        PhoneAccount callManagerPA = createNewConnectionManagerPhoneAccountForCall(mMockCall,
+                "cm_acct", 0);
+        phoneAccountHandles.add(callManagerPA.getAccountHandle());
+
+        assertFalse(mTestCreateConnectionTimeout.isTimeoutNeededForCall(phoneAccountHandles, null));
+    }
+
+    @Test
+    public void testIsTimeoutNeededForCall() {
+        when(mMockCall.isEmergencyCall()).thenReturn(true);
+        when(mMockAccountRegistrar.getSystemSimCallManagerComponent()).thenReturn(
+                new ComponentName(TEST_PACKAGE, TEST_CLASS));
+
+        List<PhoneAccountHandle> phoneAccountHandles = new ArrayList<>();
+        // Put in a regular phone account handle
+        PhoneAccount regularAccount = makePhoneAccount("tel_acct1",
+                PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION);
+        phoneAccountHandles.add(regularAccount.getAccountHandle());
+        // Create a connection manager for the call and include it in phoneAccountHandles
+        int capability = PhoneAccount.CAPABILITY_SUPPORTS_VOICE_CALLING_INDICATIONS
+                | PhoneAccount.CAPABILITY_VOICE_CALLING_AVAILABLE;
+        PhoneAccount callManagerPA = createNewConnectionManagerPhoneAccountForCall(mMockCall,
+                "cm_acct", capability);
+        PhoneAccount phoneAccountWithService = makeQuickAccount("cm_acct", capability, null);
+        when(mMockAccountRegistrar.getPhoneAccount(callManagerPA.getAccountHandle(),
+                callManagerPA.getAccountHandle().getUserHandle()))
+                .thenReturn(phoneAccountWithService);
+        phoneAccountHandles.add(callManagerPA.getAccountHandle());
+
+        assertTrue(mTestCreateConnectionTimeout.isTimeoutNeededForCall(phoneAccountHandles, null));
+    }
+
+    @Test
+    public void testConnTimeout_carrierSatelliteEnabled_noInServiceConnManager_callNeverTimesOut() {
+        when(mMockCall.isEmergencyCall()).thenReturn(true);
+        when(mMockCall.isTestEmergencyCall()).thenReturn(false);
+        when(mMockCall.getHandle()).thenReturn(Uri.parse(""));
+        when(mMockCall.getState()).thenReturn(CallState.DIALING);
+        // Primary phone account, meant to fail
+        PhoneAccount regularAccount = makePhoneAccount("tel_acct1",
+                PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION
+                        | PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS);
+        phoneAccounts.add(regularAccount);
+        when(mMockAccountRegistrar.getOutgoingPhoneAccountForSchemeOfCurrentUser(
+                nullable(String.class))).thenReturn(regularAccount.getAccountHandle());
+        when(mMockAccountRegistrar.getSystemSimCallManagerComponent()).thenReturn(
+                new ComponentName(TEST_PACKAGE, TEST_CLASS));
+        PhoneAccount callManagerPA = getNewEmergencyConnectionManagerPhoneAccount(
+                "cm_acct", PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS
+                        | PhoneAccount.CAPABILITY_SUPPORTS_VOICE_CALLING_INDICATIONS);
+        phoneAccounts.add(callManagerPA);
+        ConnectionServiceWrapper service = makeConnectionServiceWrapper();
+        when(mMockAccountRegistrar.getSimCallManagerFromCall(mMockCall)).thenReturn(
+                callManagerPA.getAccountHandle());
+        when(mMockAccountRegistrar.getPhoneAccount(eq(callManagerPA.getAccountHandle()),
+                any())).thenReturn(callManagerPA);
+        Duration timeout = Duration.ofMillis(10);
+        when(mTimeoutsAdapter.getEmergencyCallTimeoutMillis(any())).thenReturn(timeout.toMillis());
+        when(mTimeoutsAdapter.getEmergencyCallTimeoutRadioOffMillis(any())).thenReturn(
+                timeout.toMillis());
+
+
+        mTestCreateConnectionProcessor.process();
+
+        // Validate the call is not disconnected after the timeout.
+        verify(service, after(timeout.toMillis() + 100).never()).disconnect(eq(mMockCall));
+    }
+
+    @Test
+    public void testConnTimeout_carrierSatelliteEnabled_inServiceConnManager_callTimesOut() {
+        when(mMockCall.isEmergencyCall()).thenReturn(true);
+        when(mMockCall.isTestEmergencyCall()).thenReturn(false);
+        when(mMockCall.getHandle()).thenReturn(Uri.parse(""));
+        when(mMockCall.getState()).thenReturn(CallState.DIALING);
+        // Primary phone account, meant to fail
+        PhoneAccount regularAccount = makePhoneAccount("tel_acct1",
+                PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION
+                        | PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS);
+        phoneAccounts.add(regularAccount);
+        when(mMockAccountRegistrar.getOutgoingPhoneAccountForSchemeOfCurrentUser(
+                nullable(String.class))).thenReturn(regularAccount.getAccountHandle());
+        when(mMockAccountRegistrar.getSystemSimCallManagerComponent()).thenReturn(
+                new ComponentName(TEST_PACKAGE, TEST_CLASS));
+        PhoneAccount callManagerPA = getNewEmergencyConnectionManagerPhoneAccount(
+                "cm_acct", PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS
+                        | PhoneAccount.CAPABILITY_SUPPORTS_VOICE_CALLING_INDICATIONS
+                        | PhoneAccount.CAPABILITY_VOICE_CALLING_AVAILABLE
+        );
+        phoneAccounts.add(callManagerPA);
+        ConnectionServiceWrapper service = makeConnectionServiceWrapper();
+        when(mMockAccountRegistrar.getSimCallManagerFromCall(mMockCall)).thenReturn(
+                callManagerPA.getAccountHandle());
+        when(mMockAccountRegistrar.getPhoneAccount(eq(callManagerPA.getAccountHandle()),
+                any())).thenReturn(callManagerPA);
+        Duration timeout = Duration.ofMillis(10);
+        when(mTimeoutsAdapter.getEmergencyCallTimeoutMillis(any())).thenReturn(timeout.toMillis());
+        when(mTimeoutsAdapter.getEmergencyCallTimeoutRadioOffMillis(any())).thenReturn(
+                timeout.toMillis());
+
+        mTestCreateConnectionProcessor.process();
+
+        // Validate the call was disconnected after the timeout.
+        verify(service, after(timeout.toMillis() + 100)).disconnect(eq(mMockCall));
+    }
+
+    /**
+     * Verifies when telephony is not available that we just get invalid sub id for a phone acct.
+     */
+    @SmallTest
+    @Test
+    public void testTelephonyAdapterWhenNoTelephony() {
+        PhoneAccount telephonyAcct = makePhoneAccount("test-acct",
+                PhoneAccount.CAPABILITY_CALL_PROVIDER
+                        | PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS);
+
+        CreateConnectionProcessor.ITelephonyManagerAdapterImpl impl
+                = new CreateConnectionProcessor.ITelephonyManagerAdapterImpl();
+        when(mComponentContextFixture.getTelephonyManager().
+                getSubscriptionId(any(PhoneAccountHandle.class)))
+                .thenThrow(new UnsupportedOperationException("Bee boop"));
+        assertEquals(-1, impl.getSubIdForPhoneAccount(mContext, telephonyAcct));
+    }
+
     /**
      * Generates random phone accounts.
      * @param seed random seed to use for random UUIDs; passed in for determinism.
diff --git a/tests/src/com/android/server/telecom/tests/DisconnectedCallNotifierTest.java b/tests/src/com/android/server/telecom/tests/DisconnectedCallNotifierTest.java
index 05c5071..3eacc54 100644
--- a/tests/src/com/android/server/telecom/tests/DisconnectedCallNotifierTest.java
+++ b/tests/src/com/android/server/telecom/tests/DisconnectedCallNotifierTest.java
@@ -146,6 +146,19 @@
         verify(mNotificationManager).cancelAsUser(anyString(), anyInt(), any());
     }
 
+    /**
+     * Verifies when there is no telephony available, that we'll still be able to determine a
+     * country iso.
+     */
+    @Test
+    @SmallTest
+    public void testGetCountryIsoWithNoTelephony() {
+        DisconnectedCallNotifier notifier = new DisconnectedCallNotifier(mContext, mCallsManager);
+        when(mComponentContextFixture.getTelephonyManager().getNetworkCountryIso())
+                .thenThrow(new UnsupportedOperationException("Bee boop"));
+        assertNotNull(notifier.getCurrentCountryIso(mContext));
+    }
+
     private Call createCall(DisconnectCause cause) {
         Call call = mock(Call.class);
         when(call.getDisconnectCause()).thenReturn(cause);
diff --git a/tests/src/com/android/server/telecom/tests/InCallControllerTests.java b/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
index 3d99d07..6af31ae 100644
--- a/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
+++ b/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
@@ -154,10 +154,9 @@
     @Mock NotificationManager mNotificationManager;
     @Mock PermissionInfo mMockPermissionInfo;
     @Mock InCallController.InCallServiceInfo mInCallServiceInfo;
-    @Mock private AnomalyReporterAdapter mAnomalyReporterAdapter;
     @Mock UserManager mMockUserManager;
-    @Mock UserInfo mMockUserInfo;
-    @Mock UserInfo mMockChildUserInfo; //work profile
+    @Mock Context mMockCreateContextAsUser;
+    @Mock UserManager mMockCurrentUserManager;
 
     @Rule
     public TestRule compatChangeRule = new PlatformCompatChangeRule();
@@ -226,7 +225,7 @@
         when(mDefaultDialerCache.getSystemDialerApplication()).thenReturn(SYS_PKG);
         when(mDefaultDialerCache.getSystemDialerComponent()).thenReturn(
                 new ComponentName(SYS_PKG, SYS_CLASS));
-        when(mDefaultDialerCache.getBTInCallServicePackage()).thenReturn(BT_PKG);
+        when(mDefaultDialerCache.getBTInCallServicePackages()).thenReturn(new String[] {BT_PKG});
         mEmergencyCallHelper = new EmergencyCallHelper(mMockContext, mDefaultDialerCache,
                 mTimeoutsAdapter);
         when(mMockCallsManager.getRoleManagerAdapter()).thenReturn(mMockRoleManagerAdapter);
@@ -312,10 +311,14 @@
         when(mMockContext.getSystemService(eq(Context.USER_SERVICE))).thenReturn(mMockUserManager);
         when(mMockContext.getSystemService(eq(UserManager.class)))
                 .thenReturn(mMockUserManager);
+        when(mMockContext.createContextAsUser(any(UserHandle.class), eq(0)))
+                .thenReturn(mMockCreateContextAsUser);
+        when(mMockCreateContextAsUser.getSystemService(eq(UserManager.class)))
+                .thenReturn(mMockCurrentUserManager);
         // Mock user info to allow binding on user stored in the phone account (mUserHandle).
-        when(mMockUserManager.getUserInfo(anyInt())).thenReturn(mMockUserInfo);
-        when(mMockUserInfo.isManagedProfile()).thenReturn(true);
         when(mFeatureFlags.separatelyBindToBtIncallService()).thenReturn(false);
+        when(mFeatureFlags.telecomResolveHiddenDependencies()).thenReturn(true);
+        when(mMockCurrentUserManager.isManagedProfile()).thenReturn(true);
         when(mFeatureFlags.profileUserSupport()).thenReturn(false);
     }
 
@@ -406,7 +409,7 @@
                 .thenReturn(300_000L);
 
         setupMockPackageManager(false /* default */, true /* system */, false /* external calls */);
-        mInCallController.bindToServices(mMockCall, false);
+        mInCallController.bindToServices(mMockCall);
 
         ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
         verify(mMockContext).bindServiceAsUser(
@@ -441,7 +444,7 @@
 
         Intent queryIntent = new Intent(InCallService.SERVICE_INTERFACE);
         setupMockPackageManager(false /* default */, true /* system */, false /* external calls */);
-        mInCallController.bindToServices(mMockCall, false);
+        mInCallController.bindToServices(mMockCall);
 
         ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
         verify(mMockContext).bindServiceAsUser(
@@ -480,7 +483,7 @@
                 anyInt(), eq(mUserHandle))).thenReturn(true);
 
         setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
-        mInCallController.bindToServices(mMockCall, false);
+        mInCallController.bindToServices(mMockCall);
 
         // Query for the different InCallServices
         ArgumentCaptor<Intent> queryIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -525,7 +528,8 @@
         when(mMockCall.isEmergencyCall()).thenReturn(true);
         when(mMockContext.getSystemService(eq(UserManager.class)))
             .thenReturn(mMockUserManager);
-        when(mMockUserManager.isQuietModeEnabled(any(UserHandle.class))).thenReturn(false);
+        when(mMockCurrentUserManager.isQuietModeEnabled(any(UserHandle.class)))
+                .thenReturn(false);
         when(mMockCall.isIncoming()).thenReturn(false);
         when(mMockCall.getAssociatedUser()).thenReturn(mUserHandle);
         when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
@@ -542,7 +546,7 @@
         setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
         setupMockPackageManagerLocationPermission(SYS_PKG, false /* granted */);
 
-        mInCallController.bindToServices(mMockCall, false);
+        mInCallController.bindToServices(mMockCall);
 
         // Query for the different InCallServices
         ArgumentCaptor<Intent> queryIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -599,11 +603,12 @@
         when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
         when(mMockContext.getSystemService(eq(UserManager.class)))
             .thenReturn(mMockUserManager);
-        when(mMockUserManager.isQuietModeEnabled(any(UserHandle.class))).thenReturn(true);
+        when(mMockCurrentUserManager.isQuietModeEnabled(any(UserHandle.class)))
+                .thenReturn(true);
         setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
         setupMockPackageManagerLocationPermission(SYS_PKG, false /* granted */);
 
-        mInCallController.bindToServices(mMockCall, false);
+        mInCallController.bindToServices(mMockCall);
 
         ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
         verify(mMockContext, times(1)).bindServiceAsUser(
@@ -629,11 +634,12 @@
         when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
         when(mMockContext.getSystemService(eq(UserManager.class)))
                 .thenReturn(mMockUserManager);
-        when(mMockUserManager.isQuietModeEnabled(any(UserHandle.class))).thenReturn(true);
+        when(mMockCurrentUserManager.isQuietModeEnabled(any(UserHandle.class)))
+                .thenReturn(true);
         setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
         setupMockPackageManagerLocationPermission(SYS_PKG, false /* granted */);
 
-        mInCallController.bindToServices(mMockCall, false);
+        mInCallController.bindToServices(mMockCall);
 
         ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
         verify(mMockContext, times(1)).bindServiceAsUser(
@@ -660,12 +666,13 @@
         when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
         when(mMockContext.getSystemService(eq(UserManager.class)))
                 .thenReturn(mMockUserManager);
-        when(mMockUserManager.isQuietModeEnabled(any(UserHandle.class))).thenReturn(false);
-        when(mMockUserManager.isUserAdmin(anyInt())).thenReturn(false);
+        when(mMockCurrentUserManager.isQuietModeEnabled(any(UserHandle.class)))
+                .thenReturn(false);
+        when(mMockCurrentUserManager.isAdminUser()).thenReturn(false);
         setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
         setupMockPackageManagerLocationPermission(SYS_PKG, false /* granted */);
 
-        mInCallController.bindToServices(mMockCall, false);
+        mInCallController.bindToServices(mMockCall);
 
         ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
         verify(mMockContext, times(1)).bindServiceAsUser(
@@ -688,12 +695,13 @@
         when(mMockCall.getAssociatedUser()).thenReturn(DUMMY_USER_HANDLE);
         when(mMockContext.getSystemService(eq(UserManager.class)))
             .thenReturn(mMockUserManager);
-        when(mMockUserManager.isQuietModeEnabled(any(UserHandle.class))).thenReturn(false);
-        when(mMockUserManager.isUserAdmin(anyInt())).thenReturn(true);
+        when(mMockCurrentUserManager.isQuietModeEnabled(any(UserHandle.class)))
+                .thenReturn(false);
+        when(mMockCurrentUserManager.isAdminUser()).thenReturn(true);
         setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
         setupMockPackageManagerLocationPermission(SYS_PKG, false /* granted */);
 
-        mInCallController.bindToServices(mMockCall, false);
+        mInCallController.bindToServices(mMockCall);
 
         ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
         verify(mMockContext, times(1)).bindServiceAsUser(
@@ -723,7 +731,8 @@
         when(mMockCall.isEmergencyCall()).thenReturn(true);
         when(mMockContext.getSystemService(eq(UserManager.class)))
             .thenReturn(mMockUserManager);
-        when(mMockUserManager.isQuietModeEnabled(any(UserHandle.class))).thenReturn(false);
+        when(mMockCurrentUserManager.isQuietModeEnabled(any(UserHandle.class)))
+                .thenReturn(false);
         when(mMockCall.isIncoming()).thenReturn(false);
         when(mMockCall.getAssociatedUser()).thenReturn(mUserHandle);
         when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
@@ -742,7 +751,7 @@
         setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
         setupMockPackageManagerLocationPermission(SYS_PKG, false /* granted */);
 
-        mInCallController.bindToServices(mMockCall, false);
+        mInCallController.bindToServices(mMockCall);
 
         // Query for the different InCallServices
         ArgumentCaptor<Intent> queryIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -824,7 +833,7 @@
                 .thenReturn(true);
 
         setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
-        mInCallController.bindToServices(mMockCall, false);
+        mInCallController.bindToServices(mMockCall);
 
         // Query for the different InCallServices
         ArgumentCaptor<Intent> queryIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -905,7 +914,7 @@
         when(mDefaultDialerCache.getDefaultDialerApplication(CURRENT_USER_ID)).thenReturn(DEF_PKG);
 
         setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
-        mInCallController.bindToServices(mMockCall, false);
+        mInCallController.bindToServices(mMockCall);
 
         ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
         ArgumentCaptor<ServiceConnection> serviceConnectionCaptor =
@@ -952,7 +961,7 @@
         mInCallController.handleCarModeChange(UiModeManager.DEFAULT_PRIORITY, CAR_PKG, true);
 
         // Now bind; we should only bind to one app.
-        mInCallController.bindToServices(mMockCall, false);
+        mInCallController.bindToServices(mMockCall);
 
         // Bind InCallServices
         ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -1001,7 +1010,8 @@
         when(mMockCall.isEmergencyCall()).thenReturn(true);
         when(mMockContext.getSystemService(eq(UserManager.class)))
                 .thenReturn(mMockUserManager);
-        when(mMockUserManager.isQuietModeEnabled(any(UserHandle.class))).thenReturn(false);
+        when(mMockCurrentUserManager.isQuietModeEnabled(any(UserHandle.class)))
+                .thenReturn(false);
         when(mMockCall.isIncoming()).thenReturn(false);
         when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
         when(mMockCall.getIntentExtras()).thenReturn(callExtras);
@@ -1034,7 +1044,7 @@
                 .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_DISABLED);
 
         mInCallController.addCall(mMockCall);
-        mInCallController.bindToServices(mMockCall, false);
+        mInCallController.bindToServices(mMockCall);
 
         // There will be 4 calls for the various types of ICS.
         verify(mMockPackageManager, times(4)).queryIntentServicesAsUser(
@@ -1111,7 +1121,8 @@
         when(mMockCall.isEmergencyCall()).thenReturn(true);
         when(mMockContext.getSystemService(eq(UserManager.class)))
                 .thenReturn(mMockUserManager);
-        when(mMockUserManager.isQuietModeEnabled(any(UserHandle.class))).thenReturn(false);
+        when(mMockCurrentUserManager.isQuietModeEnabled(any(UserHandle.class)))
+                .thenReturn(false);
         when(mMockCall.isIncoming()).thenReturn(false);
         when(mMockCall.getAssociatedUser()).thenReturn(mUserHandle);
         when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
@@ -1202,7 +1213,7 @@
     public void testBindToService_IncludeExternal() throws Exception {
         setupMocks(true /* isExternalCall */);
         setupMockPackageManager(true /* default */, true /* system */, true /* external calls */);
-        mInCallController.bindToServices(mMockCall, false);
+        mInCallController.bindToServices(mMockCall);
 
         // Query for the different InCallServices
         ArgumentCaptor<Intent> queryIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -1253,7 +1264,7 @@
 
         when(mMockCallsManager.getCalls()).thenReturn(Collections.singletonList(mMockCall));
         setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
-        mInCallController.bindToServices(mMockCall, false);
+        mInCallController.bindToServices(mMockCall);
 
         ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
         ArgumentCaptor<ServiceConnection> serviceConnectionCaptor =
@@ -1302,7 +1313,7 @@
         mInCallController.handleCarModeChange(UiModeManager.DEFAULT_PRIORITY, CAR_PKG, true);
 
         // Now bind; we should only bind to one app.
-        mInCallController.bindToServices(mMockCall, false);
+        mInCallController.bindToServices(mMockCall);
 
         // Bind InCallServices
         ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -1321,7 +1332,7 @@
     public void testNoBindToInvalidService_CarModeUI() throws Exception {
         setupMocks(true /* isExternalCall */);
         setupMockPackageManager(true /* default */, true /* system */, true /* external calls */);
-        mInCallController.bindToServices(mMockCall, false);
+        mInCallController.bindToServices(mMockCall);
 
         when(mMockPackageManager.checkPermission(
                 matches(Manifest.permission.CONTROL_INCALL_EXPERIENCE),
@@ -1373,7 +1384,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, false);
+            mInCallController.bindToServices(mMockCall);
 
             // Bind InCallServices
             ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -1417,7 +1428,7 @@
         when(mDefaultDialerCache.getDefaultDialerApplication(CURRENT_USER_ID)).thenReturn(null);
 
         // we should bind to only the non ui app.
-        mInCallController.bindToServices(mMockCall, false);
+        mInCallController.bindToServices(mMockCall);
 
         // Bind InCallServices
         ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -1450,7 +1461,7 @@
                 matches(DEF_PKG))).thenReturn(PackageManager.PERMISSION_DENIED);
         when(mMockCall.getName()).thenReturn("evil");
 
-        mInCallController.bindToServices(mMockCall, false);
+        mInCallController.bindToServices(mMockCall);
 
         // Bind InCallServices
         ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -1487,7 +1498,7 @@
         setupMocks(true /* isExternalCall */);
         setupMockPackageManager(true /* default */, true /* system */, true /* external calls */);
         // Bind to default dialer.
-        mInCallController.bindToServices(mMockCall, false);
+        mInCallController.bindToServices(mMockCall);
 
         // Uninstall an unrelated app.
         mSystemStateListener.onPackageUninstalled("com.joe.stuff");
@@ -1511,7 +1522,7 @@
         setupMocks(true /* isExternalCall */);
         setupMockPackageManager(true /* default */, true /* system */, true /* external calls */);
         // Bind to default dialer.
-        mInCallController.bindToServices(mMockCall, false);
+        mInCallController.bindToServices(mMockCall);
 
         // Enable car mode and enter car mode at default priority.
         when(mMockSystemStateHelper.isCarModeOrProjectionActive()).thenReturn(true);
@@ -1579,7 +1590,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, false);
+        mInCallController.bindToServices(mMockCall);
 
         ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
         ArgumentCaptor<ServiceConnection> serviceConnectionCaptor =
@@ -1648,7 +1659,7 @@
 
         // Bind; we should not bind to anything right now; the dialer does not support self
         // managed calls.
-        mInCallController.bindToServices(mMockCall, false);
+        mInCallController.bindToServices(mMockCall);
 
         // Bind InCallServices; make sure no binding took place.  InCallController handles not
         // binding initially, but the rebind (see next test case) will always happen.
@@ -1687,7 +1698,7 @@
 
         // Bind; we should not bind to anything right now; the dialer does not support self
         // managed calls.
-        mInCallController.bindToServices(mMockCall, false);
+        mInCallController.bindToServices(mMockCall);
 
         // Bind InCallServices; make sure no binding took place.
         verify(mMockContext, never()).bindServiceAsUser(
@@ -1787,9 +1798,9 @@
         // Force the difference between the phone account user and current user. This is supposed to
         // simulate a secondary user placing a call over an unassociated sim.
         assertFalse(mUserHandle.equals(UserHandle.USER_CURRENT));
-        when(mMockUserInfo.isManagedProfile()).thenReturn(false);
+        when(mMockCurrentUserManager.isManagedProfile()).thenReturn(false);
 
-        mInCallController.bindToServices(mMockCall, false);
+        mInCallController.bindToServices(mMockCall);
 
         // Bind InCallService on UserHandle.CURRENT and not the user from the call (mUserHandle)
         ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -1810,7 +1821,7 @@
         when(mMockCall.getAssociatedUser()).thenReturn(testUser);
 
         // Bind to ICS. The mapping should've been inserted with the testUser as the key.
-        mInCallController.bindToServices(mMockCall, false);
+        mInCallController.bindToServices(mMockCall);
         assertTrue(mInCallController.getInCallServiceConnections().containsKey(testUser));
 
         // Set the target phone account. Simulates the flow when the user has chosen which sim to
@@ -1838,7 +1849,7 @@
         when(mMockCall.isIncoming()).thenReturn(true);
 
         // Bind to ICS. The mapping should've been inserted with the testUser as the key.
-        mInCallController.bindToServices(mMockCall, false);
+        mInCallController.bindToServices(mMockCall);
         assertTrue(mInCallController.getInCallServiceConnections().containsKey(testUser));
 
         // Remove the call. This invokes getUserFromCall to remove the ICS mapping.
@@ -1856,9 +1867,7 @@
         setupMocks(false /* isExternalCall */);
         setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
         UserHandle workUser = new UserHandle(12);
-        UserManager um = mContext.getSystemService(UserManager.class);
-        when(um.getUserInfo(anyInt())).thenReturn(mMockUserInfo);
-        when(mMockUserInfo.isManagedProfile()).thenReturn(false);
+        when(mMockCurrentUserManager.isManagedProfile()).thenReturn(false);
         when(mMockCall.getAssociatedUser()).thenReturn(workUser);
         setupFakeSystemCall(mMockSystemCall1, 1);
         setupFakeSystemCall(mMockSystemCall2, 2);
@@ -1924,21 +1933,9 @@
         when(mMockChildUserCall.visibleToInCallService()).thenReturn(true);
 
         //Setup up parent and child/work profile relation
-        when(mMockUserInfo.getUserHandle()).thenReturn(mParentUserHandle);
-        when(mMockChildUserInfo.getUserHandle()).thenReturn(mChildUserHandle);
-        when(mMockUserInfo.isManagedProfile()).thenReturn(false);
-        when(mMockChildUserInfo.isManagedProfile()).thenReturn(false);
         when(mMockChildUserCall.getAssociatedUser()).thenReturn(mChildUserHandle);
         when(mMockCallsManager.getCurrentUserHandle()).thenReturn(mParentUserHandle);
-        when(mMockUserManager.getProfileParent(mChildUserHandle.getIdentifier())).thenReturn(
-                mMockUserInfo);
         when(mMockUserManager.getProfileParent(mChildUserHandle)).thenReturn(mParentUserHandle);
-        when(mMockUserManager.getUserInfo(eq(mParentUserHandle.getIdentifier()))).thenReturn(
-                mMockUserInfo);
-        when(mMockUserManager.getUserInfo(eq(mChildUserHandle.getIdentifier()))).thenReturn(
-                mMockChildUserInfo);
-        when(mMockUserManager.isManagedProfile(mParentUserHandle.getIdentifier())).thenReturn(
-                false);
         when(mFeatureFlags.profileUserSupport()).thenReturn(true);
     }
 
@@ -1954,7 +1951,7 @@
                 true /*includeSelfManagedCallsInNonUi*/);
 
         //pass in call by child/profile user
-        mInCallController.bindToServices(mMockChildUserCall, false);
+        mInCallController.bindToServices(mMockChildUserCall);
         // Verify that queryIntentServicesAsUser is also called with parent handle
         // Query for the different InCallServices
         ArgumentCaptor<Integer> userIdCaptor = ArgumentCaptor.forClass(Integer.class);
@@ -1974,8 +1971,10 @@
 
     @Test
     public void testSeparatelyBluetoothService() {
+        setupMocks(false /* isExternalCall */);
+        setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
         Intent expectedIntent = new Intent(InCallService.SERVICE_INTERFACE);
-        expectedIntent.setPackage(mDefaultDialerCache.getBTInCallServicePackage());
+        expectedIntent.setPackage(mDefaultDialerCache.getBTInCallServicePackages()[0]);
         LinkedList<ResolveInfo> resolveInfo = new LinkedList<ResolveInfo>();
         resolveInfo.add(getBluetoothResolveinfo());
         when(mFeatureFlags.separatelyBindToBtIncallService()).thenReturn(true);
@@ -1991,7 +1990,7 @@
         }).when(mMockPackageManager).queryIntentServicesAsUser(any(Intent.class), anyInt(),
                 anyInt());
 
-        mInCallController.bindToBTService(mMockCall);
+        mInCallController.bindToBTService(mMockCall, null);
 
         ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
         verify(mMockContext).bindServiceAsUser(captor.capture(), any(ServiceConnection.class),
diff --git a/tests/src/com/android/server/telecom/tests/MissedCallNotifierImplTest.java b/tests/src/com/android/server/telecom/tests/MissedCallNotifierImplTest.java
index 61e8347..1776411 100644
--- a/tests/src/com/android/server/telecom/tests/MissedCallNotifierImplTest.java
+++ b/tests/src/com/android/server/telecom/tests/MissedCallNotifierImplTest.java
@@ -692,6 +692,25 @@
                 nullable(Notification.class), eq(PRIMARY_USER));
     }
 
+    /**
+     * Ensure when Telephony is not present on a device and getNetworkCountryIso throws an
+     * unsupported operation exception that we will still fallback to the device locale.
+     */
+    @SmallTest
+    @Test
+    public void testGetCountryIsoWithNoTelephony() {
+        Notification.Builder builder1 = makeNotificationBuilder("builder1");
+        MissedCallNotifierImpl.NotificationBuilderFactory fakeBuilderFactory =
+                makeNotificationBuilderFactory(builder1);
+        MissedCallNotifierImpl missedCallNotifier = new MissedCallNotifierImpl(mContext,
+                mPhoneAccountRegistrar, mDefaultDialerCache, fakeBuilderFactory,
+                mDeviceIdleControllerAdapter, mFeatureFlags);
+
+        when(mComponentContextFixture.getTelephonyManager().getNetworkCountryIso())
+                .thenThrow(new UnsupportedOperationException("Bee boop"));
+        assertNotNull(missedCallNotifier.getCurrentCountryIso(mContext));
+    }
+
     private Notification.Builder makeNotificationBuilder(String label) {
         Notification.Builder builder = spy(new Notification.Builder(mContext));
         Notification notification = mock(Notification.class);
diff --git a/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java b/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java
index 0ce5836..45b4ed1 100644
--- a/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java
+++ b/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java
@@ -29,6 +29,7 @@
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
@@ -138,8 +139,9 @@
                 .thenReturn(TEST_LABEL);
         mRegistrar = new PhoneAccountRegistrar(
                 mComponentContextFixture.getTestDouble().getApplicationContext(), mLock, FILE_NAME,
-                mDefaultDialerCache, mAppLabelProxy, mTelephonyFeatureFlags);
+                mDefaultDialerCache, mAppLabelProxy, mTelephonyFeatureFlags, mFeatureFlags);
         when(mFeatureFlags.onlyUpdateTelephonyOnValidSubIds()).thenReturn(false);
+        when(mFeatureFlags.unregisterUnresolvableAccounts()).thenReturn(true);
         when(mTelephonyFeatureFlags.workProfileApiSplit()).thenReturn(false);
     }
 
@@ -159,12 +161,14 @@
     public void testPhoneAccountHandle() throws Exception {
         PhoneAccountHandle input = new PhoneAccountHandle(new ComponentName("pkg0", "cls0"), "id0");
         PhoneAccountHandle result = roundTripXml(this, input,
-                PhoneAccountRegistrar.sPhoneAccountHandleXml, mContext, mTelephonyFeatureFlags);
+                PhoneAccountRegistrar.sPhoneAccountHandleXml, mContext,
+                mTelephonyFeatureFlags, mFeatureFlags);
         assertPhoneAccountHandleEquals(input, result);
 
         PhoneAccountHandle inputN = new PhoneAccountHandle(new ComponentName("pkg0", "cls0"), null);
         PhoneAccountHandle resultN = roundTripXml(this, inputN,
-                PhoneAccountRegistrar.sPhoneAccountHandleXml, mContext, mTelephonyFeatureFlags);
+                PhoneAccountRegistrar.sPhoneAccountHandleXml, mContext,
+                mTelephonyFeatureFlags, mFeatureFlags);
         Log.i(this, "inputN = %s, resultN = %s", inputN, resultN);
         assertPhoneAccountHandleEquals(inputN, resultN);
     }
@@ -187,7 +191,7 @@
                 .setIsEnabled(true)
                 .build();
         PhoneAccount result = roundTripXml(this, input, PhoneAccountRegistrar.sPhoneAccountXml,
-                mContext, mTelephonyFeatureFlags);
+                mContext, mTelephonyFeatureFlags, mFeatureFlags);
 
         assertPhoneAccountEquals(input, result);
     }
@@ -198,7 +202,7 @@
         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);
+        UserManager userManager = mContext.getSystemService(UserManager.class);
         doReturn(10L).when(userManager).getSerialNumberForUser(eq(USER_HANDLE_10));
         doReturn(USER_HANDLE_10).when(userManager).getUserForSerialNumber(eq(10L));
         Bundle testBundle = new Bundle();
@@ -222,7 +226,7 @@
                 .setSimultaneousCallingRestriction(restriction)
                 .build();
         PhoneAccount result = roundTripXml(this, input, PhoneAccountRegistrar.sPhoneAccountXml,
-                mContext, mTelephonyFeatureFlags);
+                mContext, mTelephonyFeatureFlags, mFeatureFlags);
 
         assertPhoneAccountEquals(input, result);
     }
@@ -234,7 +238,7 @@
         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);
+        UserManager userManager = mContext.getSystemService(UserManager.class);
         doReturn(10L).when(userManager).getSerialNumberForUser(eq(USER_HANDLE_10));
         doReturn(USER_HANDLE_10).when(userManager).getUserForSerialNumber(eq(10L));
         Bundle testBundle = new Bundle();
@@ -262,7 +266,7 @@
         // Simulate turning off the flag after reboot
         doReturn(false).when(mTelephonyFeatureFlags).simultaneousCallingIndications();
         PhoneAccount result = fromXml(xmlData, PhoneAccountRegistrar.sPhoneAccountXml, mContext,
-                mTelephonyFeatureFlags);
+                mTelephonyFeatureFlags, mFeatureFlags);
 
         assertNotNull(result);
         assertFalse(result.hasSimultaneousCallingRestriction());
@@ -292,7 +296,7 @@
         // Simulate turning on the flag after reboot
         doReturn(true).when(mTelephonyFeatureFlags).simultaneousCallingIndications();
         PhoneAccount result = fromXml(xmlData, PhoneAccountRegistrar.sPhoneAccountXml, mContext,
-                mTelephonyFeatureFlags);
+                mTelephonyFeatureFlags, mFeatureFlags);
 
         assertPhoneAccountEquals(input, result);
     }
@@ -364,13 +368,14 @@
     public void testDefaultPhoneAccountHandleEmptyGroup() throws Exception {
         DefaultPhoneAccountHandle input = new DefaultPhoneAccountHandle(Process.myUserHandle(),
                 makeQuickAccountHandle("i1"), "");
-        when(UserManager.get(mContext).getSerialNumberForUser(input.userHandle))
+        UserManager userManager = mContext.getSystemService(UserManager.class);
+        when(userManager.getSerialNumberForUser(input.userHandle))
                 .thenReturn(0L);
-        when(UserManager.get(mContext).getUserForSerialNumber(0L))
+        when(userManager.getUserForSerialNumber(0L))
                 .thenReturn(input.userHandle);
         DefaultPhoneAccountHandle result = roundTripXml(this, input,
                 PhoneAccountRegistrar.sDefaultPhoneAccountHandleXml, mContext,
-                mTelephonyFeatureFlags);
+                mTelephonyFeatureFlags, mFeatureFlags);
 
         assertDefaultPhoneAccountHandleEquals(input, result);
     }
@@ -400,7 +405,7 @@
                 .setExtras(testBundle)
                 .build();
         PhoneAccount result = roundTripXml(this, input, PhoneAccountRegistrar.sPhoneAccountXml,
-                mContext, mTelephonyFeatureFlags);
+                mContext, mTelephonyFeatureFlags, mFeatureFlags);
 
         Bundle extras = result.getExtras();
         assertFalse(extras.keySet().contains("EXTRA_STR2"));
@@ -414,7 +419,7 @@
     public void testState() throws Exception {
         PhoneAccountRegistrar.State input = makeQuickState();
         PhoneAccountRegistrar.State result = roundTripXml(this, input,
-                PhoneAccountRegistrar.sStateXml, mContext, mTelephonyFeatureFlags);
+                PhoneAccountRegistrar.sStateXml, mContext, mTelephonyFeatureFlags, mFeatureFlags);
         assertStateEquals(input, result);
     }
 
@@ -463,6 +468,60 @@
                 PhoneAccount.SCHEME_TEL));
     }
 
+    /**
+     * Verify when a {@link android.telecom.ConnectionService} is disabled or cannot be resolved,
+     * all phone accounts are unregistered when calling
+     * {@link  PhoneAccountRegistrar#cleanupAndGetVerifiedAccounts(PhoneAccount)}.
+     */
+    @Test
+    public void testCannotResolveServiceUnregistersAccounts() throws Exception {
+        ComponentName componentName = makeQuickConnectionServiceComponentName();
+        PhoneAccount account = makeQuickAccountBuilder("0", 0, USER_HANDLE_10)
+                .setCapabilities(PhoneAccount.CAPABILITY_CONNECTION_MANAGER
+                        | PhoneAccount.CAPABILITY_CALL_PROVIDER).build();
+        // add the ConnectionService and register a single phone account for it
+        mComponentContextFixture.addConnectionService(componentName,
+                Mockito.mock(IConnectionService.class));
+        registerAndEnableAccount(account);
+        // verify the start state
+        assertEquals(1,
+                mRegistrar.getRegisteredAccountsForPackageName(componentName.getPackageName(),
+                        USER_HANDLE_10).size());
+        // remove the ConnectionService so that the account cannot be resolved anymore
+        mComponentContextFixture.removeConnectionService(componentName,
+                Mockito.mock(IConnectionService.class));
+        // verify the account is unregistered when fetching the phone accounts for the package
+        assertEquals(1,
+                mRegistrar.getRegisteredAccountsForPackageName(componentName.getPackageName(),
+                        USER_HANDLE_10).size());
+        assertEquals(0,
+                mRegistrar.cleanupAndGetVerifiedAccounts(account).size());
+        assertEquals(0,
+                mRegistrar.getRegisteredAccountsForPackageName(componentName.getPackageName(),
+                        USER_HANDLE_10).size());
+    }
+
+    /**
+     * Verify that if a client adds both the {@link
+     * PhoneAccount#CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS} capability AND is backed by a
+     * {@link android.telecom.ConnectionService}, a {@link IllegalArgumentException} is thrown.
+     */
+    @Test
+    public void testConnectionServiceAndTransactionalAccount() throws Exception {
+        PhoneAccount account = makeQuickAccountBuilder("0", 0, USER_HANDLE_10)
+                .setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED
+                        | PhoneAccount.CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS).build();
+        mComponentContextFixture.addConnectionService(
+                makeQuickConnectionServiceComponentName(),
+                Mockito.mock(IConnectionService.class));
+        try {
+            registerAndEnableAccount(account);
+            fail("failed to throw IllegalArgumentException");
+        } catch (IllegalArgumentException e) {
+            // test passed, ignore Exception.
+        }
+    }
+
     @MediumTest
     @Test
     public void testSimCallManager() throws Exception {
@@ -1245,6 +1304,7 @@
         // GIVEN
         mComponentContextFixture.addConnectionService(makeQuickConnectionServiceComponentName(),
                 Mockito.mock(IConnectionService.class));
+        UserManager userManager = mContext.getSystemService(UserManager.class);
 
         List<UserHandle> users = Arrays.asList(new UserHandle(0),
                 new UserHandle(1000));
@@ -1271,10 +1331,10 @@
         when(mContext.getPackageManager().getPackageInfo(PACKAGE_2, 0))
                 .thenThrow(new PackageManager.NameNotFoundException());
 
-        when(UserManager.get(mContext).getSerialNumberForUser(users.get(0)))
+        when(userManager.getSerialNumberForUser(users.get(0)))
                 .thenReturn(0L);
 
-        when(UserManager.get(mContext).getSerialNumberForUser(users.get(1)))
+        when(userManager.getSerialNumberForUser(users.get(1)))
                 .thenReturn(-1L);
 
         // THEN
@@ -1865,7 +1925,7 @@
                 makeQuickAccountHandle(TEST_ID)).setIcon(mockIcon);
         try {
             // WHEN
-            Mockito.doThrow(new IOException())
+            doThrow(new IOException())
                     .when(mockIcon).writeToStream(any(OutputStream.class));
             //THEN
             mRegistrar.enforceIconSizeLimit(builder.build());
@@ -1961,6 +2021,36 @@
         assertTrue(accountsForUser.contains(accountForAll));
     }
 
+    @SmallTest
+    @Test
+    public void testGetSubscriptionIdForPhoneAccountWhenNoTelephony() throws Exception {
+        mComponentContextFixture.addConnectionService(makeQuickConnectionServiceComponentName(),
+                Mockito.mock(IConnectionService.class));
+
+        PhoneAccount simAccount =
+                makeQuickAccountBuilder("simzor", 1, null)
+                        .setCapabilities(
+                                PhoneAccount.CAPABILITY_CALL_PROVIDER
+                                        | PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)
+                        .setIsEnabled(true)
+                        .build();
+        registerAndEnableAccount(simAccount);
+        when(mComponentContextFixture.getTelephonyManager()
+                .getSubscriptionId(any(PhoneAccountHandle.class)))
+                .thenThrow(new UnsupportedOperationException("Bee-boop"));
+        assertEquals(SubscriptionManager.INVALID_SUBSCRIPTION_ID,
+                mRegistrar.getSubscriptionIdForPhoneAccount(simAccount.getAccountHandle()));
+
+        // One more thing; we'll test
+        doThrow(new UnsupportedOperationException("Bee boop!"))
+                .when(mComponentContextFixture.getSubscriptionManager())
+                .setDefaultVoiceSubscriptionId(anyInt());
+        mRegistrar.setUserSelectedOutgoingPhoneAccount(simAccount.getAccountHandle(),
+                simAccount.getAccountHandle().getUserHandle());
+
+        // There is nothing to verify, we just want to ensure that we didn't crash.
+    }
+
     private static PhoneAccount.Builder makeBuilderWithBindCapabilities(PhoneAccountHandle handle) {
         return new PhoneAccount.Builder(handle, TEST_LABEL)
                 .setCapabilities(PhoneAccount.CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS);
@@ -2080,7 +2170,8 @@
             T input,
             PhoneAccountRegistrar.XmlSerialization<T> xml,
             Context context,
-            FeatureFlags telephonyFeatureFlags)
+            FeatureFlags telephonyFeatureFlags,
+            com.android.server.telecom.flags.FeatureFlags telecomFeatureFlags)
             throws Exception {
         Log.d(self, "Input = %s", input);
 
@@ -2088,7 +2179,7 @@
 
         Log.i(self, "====== XML data ======\n%s", new String(data));
 
-        T result = fromXml(data, xml, context, telephonyFeatureFlags);
+        T result = fromXml(data, xml, context, telephonyFeatureFlags, telecomFeatureFlags);
 
         Log.i(self, "result = " + result);
 
@@ -2106,11 +2197,13 @@
     }
 
     private static <T> T fromXml(byte[] data, PhoneAccountRegistrar.XmlSerialization<T> xml,
-            Context context, FeatureFlags telephonyFeatureFlags) throws Exception {
+            Context context, FeatureFlags telephonyFeatureFlags,
+            com.android.server.telecom.flags.FeatureFlags telecomFeatureFlags) throws Exception {
         XmlPullParser parser = Xml.newPullParser();
         parser.setInput(new BufferedInputStream(new ByteArrayInputStream(data)), null);
         parser.nextTag();
-        return xml.readFromXml(parser, MAX_VERSION, context, telephonyFeatureFlags);
+        return xml.readFromXml(parser, MAX_VERSION, context,
+                telephonyFeatureFlags, telecomFeatureFlags);
 
     }
 
@@ -2208,6 +2301,7 @@
     }
 
     private PhoneAccountRegistrar.State makeQuickStateWithTelephonyPhoneAccountHandle() {
+        UserManager userManager = mContext.getSystemService(UserManager.class);
         PhoneAccountRegistrar.State s = new PhoneAccountRegistrar.State();
         s.accounts.add(makeQuickAccount("id0", 0));
         s.accounts.add(makeQuickAccount("id1", 1));
@@ -2216,9 +2310,9 @@
                 "com.android.phone",
                         "com.android.services.telephony.TelephonyConnectionService"), "id0");
         UserHandle userHandle = phoneAccountHandle.getUserHandle();
-        when(UserManager.get(mContext).getSerialNumberForUser(userHandle))
+        when(userManager.getSerialNumberForUser(userHandle))
             .thenReturn(0L);
-        when(UserManager.get(mContext).getUserForSerialNumber(0L))
+        when(userManager.getUserForSerialNumber(0L))
             .thenReturn(userHandle);
         s.defaultOutgoingAccountHandles
             .put(userHandle, new DefaultPhoneAccountHandle(userHandle, phoneAccountHandle,
@@ -2227,6 +2321,7 @@
     }
 
     private PhoneAccountRegistrar.State makeQuickState() {
+        UserManager userManager = mContext.getSystemService(UserManager.class);
         PhoneAccountRegistrar.State s = new PhoneAccountRegistrar.State();
         s.accounts.add(makeQuickAccount("id0", 0));
         s.accounts.add(makeQuickAccount("id1", 1));
@@ -2234,9 +2329,9 @@
         PhoneAccountHandle phoneAccountHandle = new PhoneAccountHandle(
                 new ComponentName("pkg0", "cls0"), "id0");
         UserHandle userHandle = phoneAccountHandle.getUserHandle();
-        when(UserManager.get(mContext).getSerialNumberForUser(userHandle))
+        when(userManager.getSerialNumberForUser(userHandle))
                 .thenReturn(0L);
-        when(UserManager.get(mContext).getUserForSerialNumber(0L))
+        when(userManager.getUserForSerialNumber(0L))
                 .thenReturn(userHandle);
         s.defaultOutgoingAccountHandles
                 .put(userHandle, new DefaultPhoneAccountHandle(userHandle, phoneAccountHandle,
diff --git a/tests/src/com/android/server/telecom/tests/PhoneStateBroadcasterTest.java b/tests/src/com/android/server/telecom/tests/PhoneStateBroadcasterTest.java
new file mode 100644
index 0000000..b18c2ce
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/PhoneStateBroadcasterTest.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom.tests;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.ComponentName;
+import android.net.Uri;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.SubscriptionManager;
+import android.telephony.emergency.EmergencyNumber;
+import android.util.ArrayMap;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.PhoneStateBroadcaster;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+@RunWith(JUnit4.class)
+public class PhoneStateBroadcasterTest extends TelecomTestCase {
+    @Override
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+    }
+
+    @Override
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    /**
+     * Tests behavior where FEATURE_TELEPHONY_CALLING is not available, but
+     * FEATURE_TELEPHONY_SUBSCRIPTION is; in this case we can't detect that the number is emergency
+     * so we will not bother sending out anything.
+     */
+    @Test
+    public void testNotifyOutgoingEmergencyCallWithNoTelephonyCalling() {
+        CallsManager cm = mock(CallsManager.class);
+        when(cm.getContext()).thenReturn(mContext);
+        when(mComponentContextFixture.getTelephonyManager().isEmergencyNumber(anyString()))
+                .thenThrow(new UnsupportedOperationException("Bee boop"));
+        PhoneStateBroadcaster psb = new PhoneStateBroadcaster(cm);
+
+        Call call = mock(Call.class);
+        when(call.isExternalCall()).thenReturn(false);
+        when(call.isEmergencyCall()).thenReturn(true);
+        when(call.isIncoming()).thenReturn(false);
+        when(call.getHandle()).thenReturn(Uri.parse("tel:911"));
+
+        psb.onCallAdded(call);
+        verify(mComponentContextFixture.getTelephonyRegistryManager(), never())
+                .notifyOutgoingEmergencyCall(anyInt(), anyInt(), any(EmergencyNumber.class));
+    }
+
+    /**
+     * Tests behavior where FEATURE_TELEPHONY_CALLING is available, but
+     * FEATURE_TELEPHONY_SUBSCRIPTION is; in this case we can detect that this is an emergency
+     * call, but we can't figure out any of the subscription parameters.  It is doubtful we'd ever
+     * see this in practice since technically FEATURE_TELEPHONY_CALLING needs
+     * FEATURE_TELEPHONY_SUBSCRIPTION.
+     */
+    @Test
+    public void testNotifyOutgoingEmergencyCallWithNoTelephonySubscription() {
+        CallsManager cm = mock(CallsManager.class);
+        when(cm.getContext()).thenReturn(mContext);
+        Map<Integer, List<EmergencyNumber>> nums = new ArrayMap<Integer, List<EmergencyNumber>>();
+        nums.put(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID,
+                Arrays.asList(new EmergencyNumber("911", "US", null, 0, Collections.EMPTY_LIST,
+                        0, 0)));
+        when(mComponentContextFixture.getTelephonyManager().getEmergencyNumberList())
+                .thenReturn(nums);
+        when(mComponentContextFixture.getTelephonyManager().getSubscriptionId(any(
+                        PhoneAccountHandle.class)))
+                .thenThrow(new UnsupportedOperationException("Bee boop"));
+        PhoneStateBroadcaster psb = new PhoneStateBroadcaster(cm);
+
+        Call call = mock(Call.class);
+        when(call.isExternalCall()).thenReturn(false);
+        when(call.isEmergencyCall()).thenReturn(true);
+        when(call.isIncoming()).thenReturn(false);
+        when(call.getHandle()).thenReturn(Uri.parse("tel:911"));
+        when(call.getTargetPhoneAccount()).thenReturn(new PhoneAccountHandle(
+                ComponentName.unflattenFromString("foo/bar"), "90210"));
+
+        psb.onCallAdded(call);
+        verify(mComponentContextFixture.getTelephonyRegistryManager())
+                .notifyOutgoingEmergencyCall(eq(SubscriptionManager.INVALID_SIM_SLOT_INDEX),
+                        eq(SubscriptionManager.INVALID_SUBSCRIPTION_ID),
+                        any(EmergencyNumber.class));
+    }
+}
diff --git a/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java b/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java
index a36e8ea..dc5f325 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java
@@ -1167,7 +1167,7 @@
 
         verify(mFakePhoneAccountRegistrar).getPhoneAccount(
                 TEL_PA_HANDLE_16, TEL_PA_HANDLE_16.getUserHandle());
-        verify(mInCallController, never()).bindToServices(any(), anyBoolean());
+        verify(mInCallController, never()).bindToServices(any());
         addCallTestHelper(TelecomManager.ACTION_INCOMING_CALL,
                 CallIntentProcessor.KEY_IS_INCOMING_CALL, extras,
                 TEL_PA_HANDLE_16, false);
@@ -1189,7 +1189,7 @@
 
         mTSIBinder.addNewIncomingCall(TEL_PA_HANDLE_16, extras, CALLING_PACKAGE);
 
-        verify(mInCallController, never()).bindToServices(eq(null), anyBoolean());
+        verify(mInCallController, never()).bindToServices(eq(null));
     }
 
     @SmallTest
@@ -1207,7 +1207,7 @@
 
         mTSIBinder.addNewIncomingCall(TEL_PA_HANDLE_16, extras, CALLING_PACKAGE);
 
-        verify(mInCallController).bindToServices(eq(null), anyBoolean());
+        verify(mInCallController).bindToServices(eq(null));
     }
 
     @SmallTest
@@ -1225,7 +1225,7 @@
 
         mTSIBinder.addNewIncomingCall(TEL_PA_HANDLE_16, extras, CALLING_PACKAGE);
 
-        verify(mInCallController, never()).bindToServices(eq(null), anyBoolean());
+        verify(mInCallController, never()).bindToServices(eq(null));
     }
 
     @SmallTest
@@ -1244,7 +1244,7 @@
 
         mTSIBinder.addNewIncomingCall(TEL_PA_HANDLE_16, extras, CALLING_PACKAGE);
 
-        verify(mInCallController, never()).bindToServices(eq(null), anyBoolean());
+        verify(mInCallController, never()).bindToServices(eq(null));
     }
 
 
@@ -2092,6 +2092,23 @@
                 mTSIBinder.getLine1Number(TEL_PA_HANDLE_CURRENT, DEFAULT_DIALER_PACKAGE, null));
     }
 
+    /**
+     * Verify that when Telephony is not present that getLine1Number returns null as expected.
+     * @throws Exception
+     */
+    @SmallTest
+    @Test
+    public void testGetLine1NumberWithNoTelephony() throws Exception {
+        setupGetLine1NumberTest();
+        grantPermissionAndAppOp(READ_PHONE_NUMBERS, AppOpsManager.OPSTR_READ_PHONE_NUMBERS);
+        TelephonyManager mockTelephonyManager =
+                (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
+        when(mockTelephonyManager.getLine1Number()).thenThrow(
+                new UnsupportedOperationException("Bee-boop"));
+
+        assertNull(mTSIBinder.getLine1Number(TEL_PA_HANDLE_CURRENT, DEFAULT_DIALER_PACKAGE, null));
+    }
+
     private String setupGetLine1NumberTest() throws Exception {
         int subId = 58374;
         String line1Number = "9482752023479";
diff --git a/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java b/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
index 57802e3..4463d65 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
@@ -516,6 +516,7 @@
         when(mRoleManagerAdapter.getCallCompanionApps()).thenReturn(Collections.emptyList());
         when(mRoleManagerAdapter.getDefaultCallScreeningApp(any(UserHandle.class)))
                 .thenReturn(null);
+        when(mRoleManagerAdapter.getBTInCallService()).thenReturn(new String[] {"bt_pkg"});
         when(mFeatureFlags.useRefactoredAudioRouteSwitching()).thenReturn(false);
         mTelecomSystem = new TelecomSystem(
                 mComponentContextFixture.getTestDouble(),
@@ -803,7 +804,7 @@
 
         final UserHandle userHandle = initiatingUser;
         Context localAppContext = mComponentContextFixture.getTestDouble().getApplicationContext();
-        new UserCallIntentProcessor(localAppContext, userHandle).processIntent(
+        new UserCallIntentProcessor(localAppContext, userHandle, mFeatureFlags).processIntent(
                 actionCallIntent, null, false, true /* hasCallAppOp*/, false /* isLocal */);
         // Wait for handler to start CallerInfo lookup.
         waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT);
diff --git a/tests/src/com/android/server/telecom/tests/TelephonyUtilTest.java b/tests/src/com/android/server/telecom/tests/TelephonyUtilTest.java
new file mode 100644
index 0000000..207da71
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/TelephonyUtilTest.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom.tests;
+
+import static org.junit.Assert.assertFalse;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.when;
+
+import com.android.server.telecom.TelephonyUtil;
+
+import android.net.Uri;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class TelephonyUtilTest extends TelecomTestCase {
+    @Override
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+    }
+
+    @Override
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    /**
+     * Verifies that the helper method shouldProcessAsEmergency does not crash when telephony is not
+     * present and returns "false" instead.
+     */
+    @Test
+    public void testShouldProcessAsEmergencyWithNoTelephonyCalling() {
+        when(mComponentContextFixture.getTelephonyManager().isEmergencyNumber(anyString()))
+                .thenThrow(new UnsupportedOperationException("Bee boop"));
+        assertFalse(TelephonyUtil.shouldProcessAsEmergency(mContext, Uri.parse("tel:911")));
+    }
+}
diff --git a/tests/src/com/android/server/telecom/tests/TransactionTests.java b/tests/src/com/android/server/telecom/tests/TransactionTests.java
index 0f7fd48..5876474 100644
--- a/tests/src/com/android/server/telecom/tests/TransactionTests.java
+++ b/tests/src/com/android/server/telecom/tests/TransactionTests.java
@@ -16,12 +16,17 @@
 
 package com.android.server.telecom.tests;
 
+import static com.android.server.telecom.voip.VideoStateTranslation.TransactionalVideoStateToVideoProfileState;
+import static com.android.server.telecom.voip.VideoStateTranslation.VideoProfileStateToTransactionalVideoState;
+
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.isA;
@@ -35,6 +40,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
+import android.content.res.Resources;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.OutcomeReceiver;
@@ -42,6 +48,8 @@
 import android.telecom.CallAttributes;
 import android.telecom.DisconnectCause;
 import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.telecom.VideoProfile;
 
 import androidx.test.filters.SmallTest;
 
@@ -61,7 +69,9 @@
 import com.android.server.telecom.voip.OutgoingCallTransaction;
 import com.android.server.telecom.voip.MaybeHoldCallForNewCallTransaction;
 import com.android.server.telecom.voip.RequestNewActiveCallTransaction;
+import com.android.server.telecom.voip.TransactionManager;
 import com.android.server.telecom.voip.VerifyCallStateChangeTransaction;
+import com.android.server.telecom.voip.VideoStateTranslation;
 import com.android.server.telecom.voip.VoipCallTransactionResult;
 
 import org.junit.After;
@@ -105,6 +115,7 @@
         super.setUp();
         MockitoAnnotations.initMocks(this);
         Mockito.when(mMockCall1.getId()).thenReturn(CALL_ID_1);
+        Mockito.when(mMockContext.getResources()).thenReturn(Mockito.mock(Resources.class));
     }
 
     @Override
@@ -206,14 +217,14 @@
     public void testTransactionalHoldActiveCallForNewCall() throws Exception {
         // GIVEN
         MaybeHoldCallForNewCallTransaction transaction =
-                new MaybeHoldCallForNewCallTransaction(mCallsManager, mMockCall1);
+                new MaybeHoldCallForNewCallTransaction(mCallsManager, mMockCall1, false);
 
         // WHEN
         transaction.processTransaction(null);
 
         // THEN
         verify(mCallsManager, times(1))
-                .transactionHoldPotentialActiveCallForNewCall(eq(mMockCall1),
+                .transactionHoldPotentialActiveCallForNewCall(eq(mMockCall1), eq(false),
                         isA(OutcomeReceiver.class));
     }
 
@@ -224,7 +235,8 @@
                 CallAttributes.DIRECTION_OUTGOING, TEST_NAME, TEST_URI).build();
 
         OutgoingCallTransaction transaction =
-                new OutgoingCallTransaction(CALL_ID_1, mMockContext, callAttributes, mCallsManager);
+                new OutgoingCallTransaction(CALL_ID_1, mMockContext, callAttributes, mCallsManager,
+                        mFeatureFlags);
 
         // WHEN
         when(mMockContext.getOpPackageName()).thenReturn("testPackage");
@@ -251,7 +263,8 @@
                 CallAttributes.DIRECTION_INCOMING, TEST_NAME, TEST_URI).build();
 
         IncomingCallTransaction transaction =
-                new IncomingCallTransaction(CALL_ID_1, callAttributes, mCallsManager);
+                new IncomingCallTransaction(CALL_ID_1, callAttributes, mCallsManager,
+                        mFeatureFlags);
 
         // WHEN
         when(mCallsManager.isIncomingCallPermitted(callAttributes.getPhoneAccountHandle()))
@@ -266,32 +279,123 @@
     }
 
     /**
+     * Verify that transactional OUTGOING calls are re-mapping the CallAttributes video state to
+     * VideoProfile states when starting the call via CallsManager#startOugoingCall.
+     */
+    @Test
+    public void testOutgoingCallTransactionRemapsVideoState() {
+        // GIVEN
+        CallAttributes audioOnlyAttributes = new CallAttributes.Builder(mHandle,
+                CallAttributes.DIRECTION_OUTGOING, TEST_NAME, TEST_URI)
+                .setCallType(CallAttributes.AUDIO_CALL)
+                .build();
+
+        CallAttributes videoAttributes = new CallAttributes.Builder(mHandle,
+                CallAttributes.DIRECTION_OUTGOING, TEST_NAME, TEST_URI)
+                .setCallType(CallAttributes.VIDEO_CALL)
+                .build();
+
+        OutgoingCallTransaction t = new OutgoingCallTransaction(null,
+                mContext, null, mCallsManager, new Bundle(), mFeatureFlags);
+
+        // WHEN
+        when(mFeatureFlags.transactionalVideoState()).thenReturn(true);
+        t.setFeatureFlags(mFeatureFlags);
+
+        // THEN
+        assertEquals(VideoProfile.STATE_AUDIO_ONLY, t
+                .generateExtras(audioOnlyAttributes)
+                .getInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE));
+
+        assertEquals(VideoProfile.STATE_BIDIRECTIONAL, t
+                .generateExtras(videoAttributes)
+                .getInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE));
+    }
+
+    /**
+     * Verify that transactional INCOMING calls are re-mapping the CallAttributes video state to
+     * VideoProfile states when starting the call in CallsManager#processIncomingCallIntent.
+     */
+    @Test
+    public void testIncomingCallTransactionRemapsVideoState() {
+        // GIVEN
+        CallAttributes audioOnlyAttributes = new CallAttributes.Builder(mHandle,
+                CallAttributes.DIRECTION_INCOMING, TEST_NAME, TEST_URI)
+                .setCallType(CallAttributes.AUDIO_CALL)
+                .build();
+
+        CallAttributes videoAttributes = new CallAttributes.Builder(mHandle,
+                CallAttributes.DIRECTION_INCOMING, TEST_NAME, TEST_URI)
+                .setCallType(CallAttributes.VIDEO_CALL)
+                .build();
+
+        IncomingCallTransaction t = new IncomingCallTransaction(null, null,
+                mCallsManager, new Bundle(), mFeatureFlags);
+
+        // WHEN
+        when(mFeatureFlags.transactionalVideoState()).thenReturn(true);
+        t.setFeatureFlags(mFeatureFlags);
+
+        // THEN
+        assertEquals(VideoProfile.STATE_AUDIO_ONLY, t
+                .generateExtras(audioOnlyAttributes)
+                .getInt(TelecomManager.EXTRA_INCOMING_VIDEO_STATE));
+
+        assertEquals(VideoProfile.STATE_BIDIRECTIONAL, t
+                .generateExtras(videoAttributes)
+                .getInt(TelecomManager.EXTRA_INCOMING_VIDEO_STATE));
+    }
+
+    @Test
+    public void testTransactionalVideoStateToVideoProfileState() {
+        assertEquals(VideoProfile.STATE_AUDIO_ONLY,
+                TransactionalVideoStateToVideoProfileState(CallAttributes.AUDIO_CALL));
+        assertEquals(VideoProfile.STATE_BIDIRECTIONAL,
+                TransactionalVideoStateToVideoProfileState(CallAttributes.VIDEO_CALL));
+        // ensure non-defined values default to audio
+        assertEquals(VideoProfile.STATE_AUDIO_ONLY,
+                TransactionalVideoStateToVideoProfileState(-1));
+    }
+
+    @Test
+    public void testVideoProfileStateToTransactionalVideoState() {
+        assertEquals(CallAttributes.AUDIO_CALL,
+                VideoProfileStateToTransactionalVideoState(VideoProfile.STATE_AUDIO_ONLY));
+        assertEquals(CallAttributes.VIDEO_CALL,
+                VideoProfileStateToTransactionalVideoState(VideoProfile.STATE_RX_ENABLED));
+        assertEquals(CallAttributes.VIDEO_CALL,
+                VideoProfileStateToTransactionalVideoState(VideoProfile.STATE_TX_ENABLED));
+        assertEquals(CallAttributes.VIDEO_CALL,
+                VideoProfileStateToTransactionalVideoState(VideoProfile.STATE_BIDIRECTIONAL));
+        // ensure non-defined values default to audio
+        assertEquals(CallAttributes.AUDIO_CALL,
+                VideoProfileStateToTransactionalVideoState(-1));
+    }
+
+    /**
      * This test verifies if the ConnectionService call is NOT transitioned to the desired call
      * state (within timeout period), Telecom will disconnect the call.
      */
     @SmallTest
     @Test
-    public void testCallStateChangeTimesOut()
-            throws ExecutionException, InterruptedException, TimeoutException {
+    public void testCallStateChangeTimesOut() {
         when(mFeatureFlags.transactionalCsVerifier()).thenReturn(true);
-        VerifyCallStateChangeTransaction t = new VerifyCallStateChangeTransaction(mCallsManager,
-                mMockCall1, CallState.ON_HOLD, true);
+        VerifyCallStateChangeTransaction t = new VerifyCallStateChangeTransaction(
+                mLock, mMockCall1, CallState.ON_HOLD);
+        TransactionManager.TransactionCompleteListener listener =
+                mock(TransactionManager.TransactionCompleteListener.class);
+        t.setCompleteListener(listener);
         // WHEN
         setupHoldableCall();
 
         // simulate the transaction being processed and the CompletableFuture timing out
         t.processTransaction(null);
-        CompletableFuture<Integer> timeoutFuture = t.getCallStateOrTimeoutResult();
-        timeoutFuture.complete(VerifyCallStateChangeTransaction.FAILURE_CODE);
+        t.timeout();
 
         // THEN
         verify(mMockCall1, times(1)).addCallStateListener(t.getCallStateListenerImpl());
-        assertEquals(timeoutFuture.get().intValue(), VerifyCallStateChangeTransaction.FAILURE_CODE);
-        assertEquals(VoipCallTransactionResult.RESULT_FAILED,
-                t.getTransactionResult().get(2, TimeUnit.SECONDS).getResult());
+        verify(listener).onTransactionTimeout(anyString());
         verify(mMockCall1, atLeastOnce()).removeCallStateListener(any());
-        verify(mCallsManager, times(1)).markCallAsDisconnected(eq(mMockCall1), any());
-        verify(mCallsManager, times(1)).markCallAsRemoved(eq(mMockCall1));
     }
 
     /**
@@ -303,25 +407,23 @@
     public void testCallStateIsSuccessfullyChanged()
             throws ExecutionException, InterruptedException, TimeoutException {
         when(mFeatureFlags.transactionalCsVerifier()).thenReturn(true);
-        VerifyCallStateChangeTransaction t = new VerifyCallStateChangeTransaction(mCallsManager,
-                mMockCall1, CallState.ON_HOLD, true);
+        VerifyCallStateChangeTransaction t = new VerifyCallStateChangeTransaction(
+                mLock, mMockCall1, CallState.ON_HOLD);
         // WHEN
         setupHoldableCall();
 
         // simulate the transaction being processed and the setOnHold() being called / state change
         t.processTransaction(null);
-        when(mMockCall1.getState()).thenReturn(CallState.ON_HOLD);
+        doReturn(CallState.ON_HOLD).when(mMockCall1).getState();
         t.getCallStateListenerImpl().onCallStateChanged(CallState.ON_HOLD);
+        t.finish(null);
+
 
         // THEN
         verify(mMockCall1, times(1)).addCallStateListener(t.getCallStateListenerImpl());
-        assertEquals(t.getCallStateOrTimeoutResult().get().intValue(),
-                VerifyCallStateChangeTransaction.SUCCESS_CODE);
         assertEquals(VoipCallTransactionResult.RESULT_SUCCEED,
                 t.getTransactionResult().get(2, TimeUnit.SECONDS).getResult());
         verify(mMockCall1, atLeastOnce()).removeCallStateListener(any());
-        verify(mCallsManager, never()).markCallAsDisconnected(eq(mMockCall1), any());
-        verify(mCallsManager, never()).markCallAsRemoved(eq(mMockCall1));
     }
 
     private Call createSpyCall(PhoneAccountHandle targetPhoneAccount, int initialState, String id) {
diff --git a/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java b/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java
index b7848a2..c5be130 100644
--- a/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java
+++ b/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java
@@ -21,7 +21,6 @@
 
 import android.os.OutcomeReceiver;
 import android.telecom.CallException;
-import android.util.Log;
 
 import androidx.test.filters.SmallTest;
 
@@ -61,6 +60,7 @@
         private long mSleepTime;
         private String mName;
         private int mType;
+        public boolean isFinished = false;
 
         public TestVoipCallTransaction(String name, long sleepTime, int type) {
             super(VoipCallTransactionTest.this.mLock);
@@ -85,17 +85,22 @@
                 } else if (mType == FAILED) {
                     mLog.append(mName).append(" failed;\n");
                     resultFuture.complete(
-                            new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_FAILED,
+                            new VoipCallTransactionResult(CallException.CODE_ERROR_UNKNOWN,
                                     null));
                 } else {
                     mLog.append(mName).append(" timeout;\n");
                     resultFuture.complete(
-                            new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_FAILED,
+                            new VoipCallTransactionResult(CallException.CODE_ERROR_UNKNOWN,
                                     "timeout"));
                 }
             }, mSleepTime);
             return resultFuture;
         }
+
+        @Override
+        public void finishTransaction() {
+            isFinished = true;
+        }
     }
 
     @Override
@@ -109,7 +114,6 @@
     @Override
     @After
     public void tearDown() throws Exception {
-        Log.i("Grace", mLog.toString());
         mTransactionManager.clear();
         super.tearDown();
     }
@@ -119,11 +123,11 @@
     public void testSerialTransactionSuccess()
             throws ExecutionException, InterruptedException, TimeoutException {
         List<VoipCallTransaction> subTransactions = new ArrayList<>();
-        VoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
+        TestVoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
                 TestVoipCallTransaction.SUCCESS);
-        VoipCallTransaction t2 = new TestVoipCallTransaction("t2", 1000L,
+        TestVoipCallTransaction t2 = new TestVoipCallTransaction("t2", 1000L,
                 TestVoipCallTransaction.SUCCESS);
-        VoipCallTransaction t3 = new TestVoipCallTransaction("t3", 1000L,
+        TestVoipCallTransaction t3 = new TestVoipCallTransaction("t3", 1000L,
                 TestVoipCallTransaction.SUCCESS);
         subTransactions.add(t1);
         subTransactions.add(t2);
@@ -137,6 +141,7 @@
         assertEquals(VoipCallTransactionResult.RESULT_SUCCEED,
                 resultFuture.get(5000L, TimeUnit.MILLISECONDS).getResult());
         assertEquals(expectedLog, mLog.toString());
+        verifyTransactionsFinished(t1, t2, t3);
     }
 
     @SmallTest
@@ -144,11 +149,11 @@
     public void testSerialTransactionFailed()
             throws ExecutionException, InterruptedException, TimeoutException {
         List<VoipCallTransaction> subTransactions = new ArrayList<>();
-        VoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
+        TestVoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
                 TestVoipCallTransaction.SUCCESS);
-        VoipCallTransaction t2 = new TestVoipCallTransaction("t2", 1000L,
+        TestVoipCallTransaction t2 = new TestVoipCallTransaction("t2", 1000L,
                 TestVoipCallTransaction.FAILED);
-        VoipCallTransaction t3 = new TestVoipCallTransaction("t3", 1000L,
+        TestVoipCallTransaction t3 = new TestVoipCallTransaction("t3", 1000L,
                 TestVoipCallTransaction.SUCCESS);
         subTransactions.add(t1);
         subTransactions.add(t2);
@@ -171,6 +176,7 @@
         exceptionFuture.get(5000L, TimeUnit.MILLISECONDS);
         String expectedLog = "t1 success;\nt2 failed;\n";
         assertEquals(expectedLog, mLog.toString());
+        verifyTransactionsFinished(t1, t2, t3);
     }
 
     @SmallTest
@@ -178,11 +184,11 @@
     public void testParallelTransactionSuccess()
             throws ExecutionException, InterruptedException, TimeoutException {
         List<VoipCallTransaction> subTransactions = new ArrayList<>();
-        VoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
+        TestVoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
                 TestVoipCallTransaction.SUCCESS);
-        VoipCallTransaction t2 = new TestVoipCallTransaction("t2", 500L,
+        TestVoipCallTransaction t2 = new TestVoipCallTransaction("t2", 500L,
                 TestVoipCallTransaction.SUCCESS);
-        VoipCallTransaction t3 = new TestVoipCallTransaction("t3", 200L,
+        TestVoipCallTransaction t3 = new TestVoipCallTransaction("t3", 200L,
                 TestVoipCallTransaction.SUCCESS);
         subTransactions.add(t1);
         subTransactions.add(t2);
@@ -198,6 +204,7 @@
         assertTrue(log.contains("t1 success;\n"));
         assertTrue(log.contains("t2 success;\n"));
         assertTrue(log.contains("t3 success;\n"));
+        verifyTransactionsFinished(t1, t2, t3);
     }
 
     @SmallTest
@@ -205,11 +212,11 @@
     public void testParallelTransactionFailed()
             throws ExecutionException, InterruptedException, TimeoutException {
         List<VoipCallTransaction> subTransactions = new ArrayList<>();
-        VoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
+        TestVoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
                 TestVoipCallTransaction.SUCCESS);
-        VoipCallTransaction t2 = new TestVoipCallTransaction("t2", 500L,
+        TestVoipCallTransaction t2 = new TestVoipCallTransaction("t2", 500L,
                 TestVoipCallTransaction.FAILED);
-        VoipCallTransaction t3 = new TestVoipCallTransaction("t3", 200L,
+        TestVoipCallTransaction t3 = new TestVoipCallTransaction("t3", 200L,
                 TestVoipCallTransaction.SUCCESS);
         subTransactions.add(t1);
         subTransactions.add(t2);
@@ -231,13 +238,14 @@
                 outcomeReceiver);
         exceptionFuture.get(5000L, TimeUnit.MILLISECONDS);
         assertTrue(mLog.toString().contains("t2 failed;\n"));
+        verifyTransactionsFinished(t1, t2, t3);
     }
 
     @SmallTest
     @Test
     public void testTransactionTimeout()
             throws ExecutionException, InterruptedException, TimeoutException {
-        VoipCallTransaction t = new TestVoipCallTransaction("t", 10000L,
+        TestVoipCallTransaction t = new TestVoipCallTransaction("t", 10000L,
                 TestVoipCallTransaction.SUCCESS);
         CompletableFuture<String> exceptionFuture = new CompletableFuture<>();
         OutcomeReceiver<VoipCallTransactionResult, CallException> outcomeReceiver =
@@ -255,15 +263,16 @@
         mTransactionManager.addTransaction(t, outcomeReceiver);
         String message = exceptionFuture.get(7000L, TimeUnit.MILLISECONDS);
         assertTrue(message.contains("timeout"));
+        verifyTransactionsFinished(t);
     }
 
     @SmallTest
     @Test
     public void testTransactionException()
             throws ExecutionException, InterruptedException, TimeoutException {
-        VoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
+        TestVoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
                 TestVoipCallTransaction.EXCEPTION);
-        VoipCallTransaction t2 = new TestVoipCallTransaction("t2", 1000L,
+        TestVoipCallTransaction t2 = new TestVoipCallTransaction("t2", 1000L,
                 TestVoipCallTransaction.SUCCESS);
         CompletableFuture<String> exceptionFuture = new CompletableFuture<>();
         OutcomeReceiver<VoipCallTransactionResult, CallException> outcomeExceptionReceiver =
@@ -290,17 +299,52 @@
         assertEquals(VoipCallTransactionResult.RESULT_SUCCEED,
                 resultFuture.get(5000L, TimeUnit.MILLISECONDS).getResult());
         assertEquals(expectedLog, mLog.toString());
+        verifyTransactionsFinished(t1, t2);
+    }
+
+    /**
+     * This test verifies that if a transaction encounters an exception while processing it,
+     * the exception finishes the transaction immediately instead of waiting for the timeout.
+     */
+    @SmallTest
+    @Test
+    public void testTransactionHitsException()
+            throws ExecutionException, InterruptedException, TimeoutException {
+        // GIVEN - a transaction that throws an exception when processing
+        TestVoipCallTransaction t1 = new TestVoipCallTransaction(
+                "t1",
+                100L,
+                TestVoipCallTransaction.EXCEPTION);
+        // verify the TransactionManager informs the client of the failed transaction
+        CompletableFuture<String> exceptionFuture = new CompletableFuture<>();
+        OutcomeReceiver<VoipCallTransactionResult, CallException> outcomeExceptionReceiver =
+                new OutcomeReceiver<>() {
+                    @Override
+                    public void onResult(VoipCallTransactionResult result) {
+                    }
+
+                    @Override
+                    public void onError(CallException e) {
+                        exceptionFuture.complete(e.getMessage());
+                    }
+                };
+        // WHEN - add and process the transaction
+        mTransactionManager.addTransaction(t1, outcomeExceptionReceiver);
+        exceptionFuture.get(200L, TimeUnit.MILLISECONDS);
+        // THEN - assert the transaction finished and failed
+        assertTrue(mLog.toString().contains("t1 exception;\n"));
+        verifyTransactionsFinished(t1);
     }
 
     @SmallTest
     @Test
     public void testTransactionResultException()
             throws ExecutionException, InterruptedException, TimeoutException {
-        VoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
+        TestVoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
                 TestVoipCallTransaction.SUCCESS);
-        VoipCallTransaction t2 = new TestVoipCallTransaction("t2", 1000L,
+        TestVoipCallTransaction t2 = new TestVoipCallTransaction("t2", 1000L,
                 TestVoipCallTransaction.SUCCESS);
-        VoipCallTransaction t3 = new TestVoipCallTransaction("t3", 1000L,
+        TestVoipCallTransaction t3 = new TestVoipCallTransaction("t3", 1000L,
                 TestVoipCallTransaction.SUCCESS);
         OutcomeReceiver<VoipCallTransactionResult, CallException> outcomeExceptionReceiver =
                 new OutcomeReceiver<>() {
@@ -335,5 +379,13 @@
         assertEquals(VoipCallTransactionResult.RESULT_SUCCEED,
                 resultFuture.get(5000L, TimeUnit.MILLISECONDS).getResult());
         assertEquals(expectedLog, mLog.toString());
+        verifyTransactionsFinished(t1, t2, t3);
+    }
+
+    public void verifyTransactionsFinished(TestVoipCallTransaction... transactions) {
+        for (TestVoipCallTransaction t : transactions) {
+            assertTrue("TestVoipCallTransaction[" + t.mName + "] never called finishTransaction",
+                    t.isFinished);
+        }
     }
 }