[automerger skipped] Import translations. DO NOT MERGE ANYWHERE am: b44b91b578 -s ours

am skip reason: contains skip directive

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

Change-Id: I0c302e6b4891a0470083410a6cc1c7a51dd18dd3
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/Android.bp b/Android.bp
index 68a8e39..94654a6 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/AndroidManifest.xml b/AndroidManifest.xml
index a9b6154..941bd5e 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -45,6 +45,8 @@
     <!-- Required to determine source of ongoing audio recordings. -->
     <uses-permission android:name="android.permission.MODIFY_AUDIO_ROUTING"/>
     <uses-permission android:name="android.permission.MODIFY_PHONE_STATE"/>
+    <!-- Required to query the audio framework to determine if a notification sound should play. -->
+    <uses-permission android:name="android.permission.QUERY_AUDIO_STATE"/>
     <uses-permission android:name="android.permission.READ_CALL_LOG"/>
     <!-- Required to check for direct to voicemail, to load custom ringtones for incoming calls
         which are specified on a per contact basis, and also to determine user preferred
@@ -66,7 +68,6 @@
     <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
     <uses-permission android:name="android.permission.ACCESS_LAST_KNOWN_CELL_ID"/>
     <uses-permission android:name="android.permission.STATUS_BAR_SERVICE" />
-    <uses-permission android:name="android.permission.MODIFY_AUDIO_ROUTING" />
 
     <permission android:name="android.permission.BROADCAST_CALLLOG_INFO"
          android:label="Broadcast the call type/duration information"
diff --git a/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..c95816a 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,9 +17,26 @@
   bug: "306582821"
 }
 
+# OWNER=pmadapurmath TARGET=24Q3
 flag {
   name: "separately_bind_to_bt_incall_service"
   namespace: "telecom"
   description: "Binding/Unbinding to BluetoothInCallServices in proper time to improve call audio"
   bug: "306395598"
 }
+
+# OWNER=pmadapurmath TARGET=24Q4
+flag {
+  name: "on_call_endpoint_changed_ics_on_connected"
+  namespace: "telecom"
+  description: "Ensure onCallEndpointChanged is sent to ICS when it connects."
+  bug: "348297436"
+}
+
+# OWNER=tjstuart TARGET=24Q4
+flag {
+  name: "do_not_send_call_to_null_ics"
+  namespace: "telecom"
+  description: "Only send calls to the InCallService if the binding is not null"
+  bug: "345473659"
+}
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..6517e0f 100644
--- a/flags/telecom_ringer_flag_declarations.aconfig
+++ b/flags/telecom_ringer_flag_declarations.aconfig
@@ -1,9 +1,18 @@
 package: "com.android.server.telecom.flags"
 container: "system"
 
+# OWNER=yeabkal TARGET=24Q2
 flag {
   name: "use_device_provided_serialized_ringer_vibration"
   namespace: "telecom"
   description: "Gates whether to use a serialized, device-specific ring vibration."
   bug: "282113261"
+}
+
+# OWNER=grantmenke TARGET=24Q4
+flag {
+  name: "ensure_in_car_ringing"
+  namespace: "telecom"
+  description: "Gates whether to ensure that when a user is in their car, they are able to hear ringing for an incoming call."
+  bug: "348708398"
 }
\ No newline at end of file
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-af/strings.xml b/res/values-af/strings.xml
index 50bead5..71564e8 100644
--- a/res/values-af/strings.xml
+++ b/res/values-af/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"As jy antwoord, sal dit jou huidige video-oproep beëindig"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Antwoord"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Wys af"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Oproep kan nie geplaas word nie, want daar is geen oproeprekeninge wat hierdie tipe oproepe ondersteun nie."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Kan nie oproep maak nie. Gaan jou toestel se verbinding na."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Oproep kan nie gemaak word nie weens jou <xliff:g id="OTHER_CALL">%1$s</xliff:g>-oproep."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Oproep kan nie gemaak word nie weens jou <xliff:g id="OTHER_CALL">%1$s</xliff:g>-oproepe."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Oproep kan nie gemaak word nie weens \'n oproep in \'n ander program."</string>
diff --git a/res/values-am/strings.xml b/res/values-am/strings.xml
index f0923d5..dafbe6e 100644
--- a/res/values-am/strings.xml
+++ b/res/values-am/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"መመለስ እየተካሄደ ያለ የቪዲዮ ጥሪዎን ይጨርሳል"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"ይመልሱ"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"አትቀበል"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"የዚህን ዓይነት ጥሪዎች የሚደግፉ መደወያ መለያዎች ስለሌሉ ጥሪ መደረግ አይችልም።"</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"ጥሪ ማድረግ አልተቻለም። የመሣሪያዎን ግንኙነት ይፈትሹ።"</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"በ<xliff:g id="OTHER_CALL">%1$s</xliff:g> ጥሪዎ ምክንያት ጥሪ መደረግ አይችልም።"</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"በ<xliff:g id="OTHER_CALL">%1$s</xliff:g> ጥሪዎችዎ ምክንያት ጥሪዎች መደረግ አይችሉም።"</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"በሌላ መተግበሪያ ውስጥ ባለ ጥሪ ምክንያት ጥሪ መደረግ አይችልም።"</string>
diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml
index 2a56809..9eb3a35 100644
--- a/res/values-ar/strings.xml
+++ b/res/values-ar/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"سيؤدي الرد إلى إنهاء مكالمات الفيديو"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"رد"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"رفض"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"يتعذَّر إجراء المكالمة بسبب عدم وجود حسابات اتصال يمكن استخدامها مع المكالمات من هذا النوع."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"لا يمكن إجراء المكالمة. يُرجى التأكّد من إمكانية الاتصال على جهازك."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"يتعذر إجراء المكالمة نتيجة لمكالمة <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"يتعذر إجراء المكالمة نتيجة لمكالمات <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"يتعذر إجراء المكالمة نتيجة لوجود مكالمة في تطبيق آخر."</string>
diff --git a/res/values-as/strings.xml b/res/values-as/strings.xml
index 72ac4db..668f5e5 100644
--- a/res/values-as/strings.xml
+++ b/res/values-as/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"উত্তৰ দিলে আপোনাৰ বৰ্তমান চলি থকা ভিডিঅ\' কলটোৰ অন্ত পৰিব"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"উত্তৰ"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"প্ৰত্যাখ্যান কৰক"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"এইধৰণৰ কল কৰিব পৰা কলিং একাউণ্ট নোহোৱাৰ কাৰণে কল কৰিব নোৱাৰি।"</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"কল কৰিব নোৱাৰি। আপোনাৰ ডিভাইচৰ সংযোগ পৰীক্ষা কৰক।"</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"আপোনাৰ <xliff:g id="OTHER_CALL">%1$s</xliff:g> কল চলি থকাৰ কাৰণে বেলেগ কল কৰিব নোৱাৰি।"</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"আপোনাৰ <xliff:g id="OTHER_CALL">%1$s</xliff:g> কলকেইটা চলি থকাৰ কাৰণে বেলেগ কল কৰিব নোৱাৰি।"</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"অইন এটা এপত কল চলি থকাৰ কাৰণে বেলেগ কল কৰিব নোৱাৰি।"</string>
diff --git a/res/values-az/strings.xml b/res/values-az/strings.xml
index ead7f54..c975159 100644
--- a/res/values-az/strings.xml
+++ b/res/values-az/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Cavab versəniz, davam edən video zəng sonlandırılacaq"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Cavab"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Rədd edin"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Bu növ zəngləri dəstəkləyən hesablar olmadığına görə zəng etmək mümkün deyil."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Zəng etmək olmur. Cihazınızın bağlantısını yoxlayın."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"<xliff:g id="OTHER_CALL">%1$s</xliff:g> zəngi səbəbilə çağrı edilə bilməz."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"<xliff:g id="OTHER_CALL">%1$s</xliff:g> zəngləri səbəbilə çağrı edilə bilməz."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Başqa bir tətbiqdəki zəng səbəbilə çağrı edilə bilməz."</string>
diff --git a/res/values-b+sr+Latn/strings.xml b/res/values-b+sr+Latn/strings.xml
index d527842..3709c25 100644
--- a/res/values-b+sr+Latn/strings.xml
+++ b/res/values-b+sr+Latn/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Ako odgovorite, završićete video poziv koji je u toku"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Odgovori"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Odbij"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Upućivanje poziva nije moguće jer nemate nijedan nalog za pozivanje koji podržava pozive ovog tipa."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Pozivanje nije uspelo. Proverite vezu uređaja."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Ne možete da uputite poziv zbog <xliff:g id="OTHER_CALL">%1$s</xliff:g> poziva."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Ne možete da uputite poziv zbog <xliff:g id="OTHER_CALL">%1$s</xliff:g> poziva."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Ne možete da uputite poziv zbog poziva u drugoj aplikaciji."</string>
diff --git a/res/values-be/strings.xml b/res/values-be/strings.xml
index c5b59bd..c3c6e2f 100644
--- a/res/values-be/strings.xml
+++ b/res/values-be/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Адказ на гэты выклік завершыць ваш бягучы відэавыклік"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Адказаць"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Адхіліць"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Не ўдалося зрабіць выклік, бо на прыладзе няма ўліковых запісаў для гэтага тыпу выклікаў."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Не ўдаецца зрабіць выклік. Праверце падключэнне прылады."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Выклік немагчыма выканаць, бо ідзе выклік <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Выклік немагчыма выканаць, бо ідуць выклікі <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Выклік немагчыма выканаць, бо ідзе выклік у іншай праграме."</string>
diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml
index fe5d70f..116c884 100644
--- a/res/values-bg/strings.xml
+++ b/res/values-bg/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Ако отговорите, текущото ви видеообаждане ще прекъсне"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Отговаряне"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Отхвърляне"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Обаждането не може да бъде извършено, защото няма профили за обаждане, които поддържат обаждания от този тип."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Не може да се извърши обаждане. Проверете връзката на устройството си."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Не можете да се обадите заради обаждането си през <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Не можете да се обадите заради обажданията си през <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Не можете да се обадите заради обаждане в друго приложение."</string>
diff --git a/res/values-bn/strings.xml b/res/values-bn/strings.xml
index 49e6ba3..4f4fea6 100644
--- a/res/values-bn/strings.xml
+++ b/res/values-bn/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"উত্তর দেওয়া হলে আপনার চালু থাকা ভিডিও কলটি কেটে যাবে"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"উত্তর দিন"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"প্রত্যাখ্যান করুন"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"এই ধরনের কল করার জন্য যে কলিং অ্যাকাউন্টের প্রয়োজন সেটি না থাকার জন্য এই কলটি করা যাবে না।"</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"কল করা যাবে না। আপনার ডিভাইসের কানেকশন চেক করুন।"</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"আপনার <xliff:g id="OTHER_CALL">%1$s</xliff:g> কলটির কারণে কলটি করা যাবে না।"</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"আপনার <xliff:g id="OTHER_CALL">%1$s</xliff:g> কলগুলির কারণে কলটি করা যাবে না।"</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"অন্য একটি অ্যাপের কলের কারণে কলটি করা যাবে না।"</string>
diff --git a/res/values-bs/strings.xml b/res/values-bs/strings.xml
index 61b86db..d2ac13a 100644
--- a/res/values-bs/strings.xml
+++ b/res/values-bs/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Odgovaranje će prekinuti video poziv koji je u toku"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Odgovori"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Odbij"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Ne može se uputiti poziv zato što ne postoji nijedan račun za pozivanje koji podržava ovu vrstu poziva."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Nije moguće uputiti poziv. Provjerite vezu uređaja."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Pozivanje nije moguće zbog poziva: <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Pozivanje nije moguće zbog poziva: <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Pozivanje nije moguće zbog poziva u drugoj aplikaciji."</string>
diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml
index 113d144..5793449 100644
--- a/res/values-ca/strings.xml
+++ b/res/values-ca/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"En respondre, finalitzarà la videotrucada en curs"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Respon"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Rebutja"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"No es pot trucar perquè, en aquest moment, no hi ha cap compte de trucades compatible amb les trucades d\'aquest tipus."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"No es pot fer la trucada. Comprova la connexió del dispositiu."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"No es pot trucar perquè ja hi ha una trucada en curs a <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"No es pot trucar perquè ja hi ha trucades en curs a <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"No es pot trucar perquè ja hi ha una trucada en curs en una altra aplicació."</string>
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
index 0adb30c..0619308 100644
--- a/res/values-cs/strings.xml
+++ b/res/values-cs/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Přijetím hovoru ukončíte probíhající videohovor"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Přijmout"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Odmítnout"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Hovor není možné provést, protože není k dispozici žádný účet, který by tento typ hovoru podporoval."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Nelze volat. Zkontrolujte připojení zařízení."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Hovor není možné provést kvůli hovoru <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Hovor není možné provést kvůli hovorům <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Hovor není možné provést kvůli hovoru v jiné aplikaci."</string>
diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml
index 4eead66..0eb69ff 100644
--- a/res/values-da/strings.xml
+++ b/res/values-da/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Hvis du besvarer, afsluttes dit igangværende videoopkald"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Besvar"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Afvis"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Opkaldet kan ikke foretages, fordi der ikke er nogen opkaldskonti, der understøtter opkald af denne type."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Det er ikke muligt at foretage opkald. Tjek din enheds forbindelse."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Opkaldet kan ikke foretages på grund af dit opkald i <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Opkaldet kan ikke foretages på grund af dine opkald i <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Opkaldet kan ikke foretages på grund et opkald i en anden app."</string>
diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml
index dccdb87..665124a 100644
--- a/res/values-de/strings.xml
+++ b/res/values-de/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Wenn du den Anruf annimmst, wird der Videoanruf beendet"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Annehmen"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Ablehnen"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Der Anruf kann nicht ausgehen, da es keine Anrufkonten gibt, die Anrufe dieses Typs unterstützen."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Keine Anrufe möglich. Prüfe die Verbindung deines Geräts."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Dieser Anruf kann aufgrund des Anrufs in <xliff:g id="OTHER_CALL">%1$s</xliff:g> nicht getätigt werden."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Dieser Anruf kann aufgrund deiner Anrufe in <xliff:g id="OTHER_CALL">%1$s</xliff:g> nicht getätigt werden."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Dieser Anruf kann aufgrund eines Anrufs in einer anderen App nicht getätigt werden."</string>
diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml
index 6b58863..ba504d7 100644
--- a/res/values-el/strings.xml
+++ b/res/values-el/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Εάν απαντήσετε, η τρέχουσα βιντεοκλήση σας θα τερματιστεί"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Απάντηση"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Απόρριψη"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Δεν είναι δυνατή η πραγματοποίηση της κλήσης, επειδή δεν υπάρχουν λογαριασμοί κλήσεων που υποστηρίζουν κλήσεις αυτού του τύπου."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Δεν είναι δυνατή η πραγματοποίηση της κλήσης. Ελέγξτε τη σύνδεση της συσκευής σας."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Δεν είναι δυνατή η πραγματοποίηση της κλήσης, λόγω της κλήσης σας μέσω <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Δεν είναι δυνατή η πραγματοποίηση της κλήσης, λόγω των κλήσεών σας μέσω <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Δεν είναι δυνατή η πραγματοποίηση της κλήσης, λόγω κάποιας κλήσης μέσω άλλης εφαρμογής."</string>
diff --git a/res/values-en-rAU/strings.xml b/res/values-en-rAU/strings.xml
index 250ab62..1ce62df 100644
--- a/res/values-en-rAU/strings.xml
+++ b/res/values-en-rAU/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Answering will end your ongoing video call"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Answer"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Decline"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Call cannot be placed because there are no calling accounts that support calls of this type."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Can\'t make call. Check your device\'s connection."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Call cannot be placed due to your <xliff:g id="OTHER_CALL">%1$s</xliff:g> call."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Call cannot be placed due to your <xliff:g id="OTHER_CALL">%1$s</xliff:g> calls."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Call cannot be placed due to a call in another app."</string>
diff --git a/res/values-en-rCA/strings.xml b/res/values-en-rCA/strings.xml
index e6291f4..8ae9c0a 100644
--- a/res/values-en-rCA/strings.xml
+++ b/res/values-en-rCA/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Answering will end your ongoing video call"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Answer"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Decline"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Call cannot be placed because there are no calling accounts which support calls of this type."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Can\'t make call. Check your device\'s connection."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Call cannot be placed due to your <xliff:g id="OTHER_CALL">%1$s</xliff:g> call."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Call cannot be placed due to your <xliff:g id="OTHER_CALL">%1$s</xliff:g> calls."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Call cannot be placed due to a call in another app."</string>
diff --git a/res/values-en-rGB/strings.xml b/res/values-en-rGB/strings.xml
index 250ab62..1ce62df 100644
--- a/res/values-en-rGB/strings.xml
+++ b/res/values-en-rGB/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Answering will end your ongoing video call"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Answer"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Decline"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Call cannot be placed because there are no calling accounts that support calls of this type."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Can\'t make call. Check your device\'s connection."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Call cannot be placed due to your <xliff:g id="OTHER_CALL">%1$s</xliff:g> call."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Call cannot be placed due to your <xliff:g id="OTHER_CALL">%1$s</xliff:g> calls."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Call cannot be placed due to a call in another app."</string>
diff --git a/res/values-en-rIN/strings.xml b/res/values-en-rIN/strings.xml
index 250ab62..1ce62df 100644
--- a/res/values-en-rIN/strings.xml
+++ b/res/values-en-rIN/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Answering will end your ongoing video call"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Answer"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Decline"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Call cannot be placed because there are no calling accounts that support calls of this type."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Can\'t make call. Check your device\'s connection."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Call cannot be placed due to your <xliff:g id="OTHER_CALL">%1$s</xliff:g> call."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Call cannot be placed due to your <xliff:g id="OTHER_CALL">%1$s</xliff:g> calls."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Call cannot be placed due to a call in another app."</string>
diff --git a/res/values-en-rXC/strings.xml b/res/values-en-rXC/strings.xml
index 5bd0e25..6849084 100644
--- a/res/values-en-rXC/strings.xml
+++ b/res/values-en-rXC/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‎‏‏‏‏‎‏‏‏‎‏‏‏‎‎‏‏‎‎‎‏‎‎‏‏‎‎‏‏‎‏‏‎‏‎‏‏‏‎‎‎‎‏‏‎‏‏‏‏‏‏‎‎‎‏‏‎‎‎‎‎‏‎‏‏‏‏‏‎Answering will end your ongoing video call‎‏‎‎‏‎"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‎‏‏‏‏‎‏‏‏‏‎‎‎‏‏‎‎‏‎‎‎‏‏‏‎‏‎‏‎‎‎‎‏‎‎‏‏‎‎‏‎‏‏‏‎‎‎‎‏‎‎‏‏‏‏‏‎‏‎‎‏‎‎‏‏‏‏‎‎Answer‎‏‎‎‏‎"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‎‏‏‏‏‎‎‏‏‎‎‏‏‎‎‏‏‎‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‎‎‎‏‎‏‏‎‏‏‎‏‎‎‎‏‏‎‎‏‎‏‏‎‏‏‏‏‏‎‏‏‏‎‎Decline‎‏‎‎‏‎"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‎‏‏‏‏‎‏‏‎‏‏‎‏‎‏‏‎‎‏‎‏‏‏‎‏‎‎‏‏‎‎‎‎‏‏‏‏‏‎‎‏‏‎‎‏‎‎‎‎‎‏‏‏‎‏‎‎‎‏‎‏‏‎‎‎‏‎‏‎Call cannot be placed because there are no calling accounts which support calls of this type.‎‏‎‎‏‎"</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‎‏‏‏‏‏‏‏‎‏‏‏‎‏‎‏‎‎‎‏‎‏‎‎‏‎‎‎‏‏‏‏‎‎‏‎‎‏‏‎‏‏‎‎‏‎‏‏‏‎‎‎‎‎‏‏‏‏‏‎‎‎‎‎‏‏‏‎‏‏‎Can\'t make call. Check your device\'s connection.‎‏‎‎‏‎"</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‏‏‎‎‎‏‎‏‎‎‏‏‎‎‎‎‎‏‏‎‎‏‏‏‏‏‎‏‏‏‏‎‏‎‏‏‏‏‏‎‏‎‎‎‎‏‏‎‎‎‏‎‎‏‎‏‎‏‎Call cannot be placed due to your ‎‏‎‎‏‏‎<xliff:g id="OTHER_CALL">%1$s</xliff:g>‎‏‎‎‏‏‏‎ call.‎‏‎‎‏‎"</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‎‏‏‏‏‏‏‏‎‏‏‎‎‎‏‎‎‎‎‏‏‏‎‏‎‏‎‏‏‏‏‎‏‎‏‏‎‎‎‏‏‎‏‏‎‏‏‎‎‏‎‎‎‏‎‏‎‎‎‎‏‏‎‏‏‎‎‏‎‎‎Call cannot be placed due to your ‎‏‎‎‏‏‎<xliff:g id="OTHER_CALL">%1$s</xliff:g>‎‏‎‎‏‏‏‎ calls.‎‏‎‎‏‎"</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‎‏‏‏‏‏‏‏‏‏‎‎‏‎‎‏‏‎‎‏‏‎‏‏‏‎‏‎‎‎‏‏‏‏‏‎‏‎‎‎‏‎‎‎‏‎‏‎‏‎‏‎‏‏‏‏‏‏‎‎‎‎‎‏‎‏‎‏‎‎‎Call cannot be placed due to a call in another app.‎‏‎‎‏‎"</string>
diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml
index c0f4e17..668a696 100644
--- a/res/values-es-rUS/strings.xml
+++ b/res/values-es-rUS/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Si respondes, finalizará tu videollamada en curso"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Responder"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Rechazar"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"No se puede realizar la llamada porque no hay ninguna cuenta compatible con este tipo de llamadas."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"No se puede realizar la llamada. Comprueba la conexión del dispositivo."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"No se puede realizar la llamada porque hay una llamada en curso en <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"No se puede realizar la llamada porque hay otras llamadas en curso en <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"No se puede realizar la llamada porque hay una llamada en curso en otra app."</string>
diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml
index 20b80a5..96163b3 100644
--- a/res/values-es/strings.xml
+++ b/res/values-es/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Al responder, finalizará la videollamada en curso"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Responder"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Rechazar"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"No puedes llamar porque no hay cuentas de llamada que admitan este tipo de llamadas."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"No se puede hacer la llamada. Comprueba la conexión de tu dispositivo."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"No puedes llamar porque tienes una llamada de <xliff:g id="OTHER_CALL">%1$s</xliff:g> en curso."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"No puedes llamar porque tienes varias llamadas de <xliff:g id="OTHER_CALL">%1$s</xliff:g> en curso."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"No puedes llamar porque tienes una llamada en curso en otra aplicación."</string>
diff --git a/res/values-et/strings.xml b/res/values-et/strings.xml
index cac1fd6..6fd5592 100644
--- a/res/values-et/strings.xml
+++ b/res/values-et/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Vastamisel lõpetatakse pooleliolev videokõne"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Vasta"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Keeldu"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Helistada ei saa, kuna pole ühtegi kõnekontot, mis toetaks seda tüüpi kõnesid."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Kõnet ei saa teha. Kontrollige seadme ühendust."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Kõnet ei saa teenuse <xliff:g id="OTHER_CALL">%1$s</xliff:g> kõne tõttu teha."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Kõnet ei saa teenuse <xliff:g id="OTHER_CALL">%1$s</xliff:g> kõnede tõttu teha."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Kõnet ei saa teise rakenduse kõne tõttu teha."</string>
diff --git a/res/values-eu/strings.xml b/res/values-eu/strings.xml
index d1aa545..3efbc07 100644
--- a/res/values-eu/strings.xml
+++ b/res/values-eu/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Erantzuten baduzu, amaitu egingo da oraingo bideodeia"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Erantzun"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Baztertu"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Ezin da egin deia, ez dagoelako mota honetako deiak onartzen duen deiak egiteko konturik."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Ezin da egin deia. Egiaztatu gailua konektatuta dagoela."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Ezin da egin deia, beste dei bat abian delako <xliff:g id="OTHER_CALL">%1$s</xliff:g> zerbitzuan."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Ezin da egin deia, beste dei batzuk abian direlako <xliff:g id="OTHER_CALL">%1$s</xliff:g> zerbitzuan."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Ezin da egin deia, beste dei bat abian delako beste aplikazio batean."</string>
diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml
index 8d562ec..6bd2ff6 100644
--- a/res/values-fa/strings.xml
+++ b/res/values-fa/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"پاسخ‌گویی به تماس تصویری درحال انجامتان پایان می‌دهد"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"پاسخ‌گویی"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"نپذیرفتن"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"به‌دلیل اینکه هیچ حساب تماسی وجود ندارد که از این نوع تماس پشتیبانی کند، تماس برقرار نشد."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"تماس برقرار نشد. اتصال دستگاهتان را بررسی کنید."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"به دلیل تماس <xliff:g id="OTHER_CALL">%1$s</xliff:g>، نمی‌توان تماسی برقرار کرد."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"به دلیل تماس‌های <xliff:g id="OTHER_CALL">%1$s</xliff:g>، نمی‌توان تماسی برقرار کرد."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"به دلیل تماسی در برنامه دیگر، نمی‌توان تماسی برقرار کرد."</string>
diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml
index 338e429..0d5fdbb 100644
--- a/res/values-fi/strings.xml
+++ b/res/values-fi/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Vastaaminen päättää käynnissä olevan videopuhelun."</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Vastaa"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Hylkää"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Puhelua ei voi soittaa, koska laitteella ei ole puhelutiliä, joka tukisi tätä puhelutyyppiä."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Soittaminen epäonnistui. Tarkista laitteen yhteys."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Puhelua ei voi soittaa, koska toisessa sovelluksessa (<xliff:g id="OTHER_CALL">%1$s</xliff:g>) on puhelu käynnissä."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Puhelua ei voi soittaa, koska toisessa sovelluksessa (<xliff:g id="OTHER_CALL">%1$s</xliff:g>) on puheluja käynnissä."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Puhelua ei voi soittaa, koska toisessa sovelluksessa on puhelu käynnissä."</string>
diff --git a/res/values-fr-rCA/strings.xml b/res/values-fr-rCA/strings.xml
index 031b25d..cfd153b 100644
--- a/res/values-fr-rCA/strings.xml
+++ b/res/values-fr-rCA/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Si vous répondez, vous mettrez fin à l\'appel vidéo en cours"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Répondre"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Refuser"</string>
-    <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_no_supported_service" msgid="6720817368116820027">"Impossible de passer l\'appel. Vérifiez la connexion de votre appareil."</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 appli."</string>
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
index a14cbb1..9dbca8f 100644
--- a/res/values-fr/strings.xml
+++ b/res/values-fr/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Si vous répondez, vous mettrez fin à l\'appel vidéo en cours"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Répondre"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Refuser"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Impossible de passer cet appel, car aucun compte téléphonique ne prend en charge ce type d\'appel."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Impossible de passer l\'appel. Vérifiez la connexion de votre appareil."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Vous ne pouvez pas passer cet appel, car vous avez une communication en cours dans <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Vous ne pouvez pas passer cet appel, car vous avez des communications en cours dans <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Vous ne pouvez pas passer cet appel, car vous avez une communication en cours dans une autre application."</string>
diff --git a/res/values-gl/strings.xml b/res/values-gl/strings.xml
index 8e82fce..f8eb32c 100644
--- a/res/values-gl/strings.xml
+++ b/res/values-gl/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Ao responder, finalizarán as túas videochamadas en curso"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Contestar"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Rexeitar"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Non se pode realizar a chamada porque non hai ningunha conta de chamadas que admita chamadas deste tipo."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Non se puido facer a chamada. Revisa a conexión do dispositivo."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Non se pode realizar a chamada porque hai unha chamada en curso en <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Non se pode realizar a chamada porque hai chamadas en curso en <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Non se pode realizar a chamada porque hai chamadas en curso noutra aplicación."</string>
diff --git a/res/values-gu/strings.xml b/res/values-gu/strings.xml
index 1b5c5ce..3d69e13 100644
--- a/res/values-gu/strings.xml
+++ b/res/values-gu/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"જવાબ આપવાથી તમારો ચાલુ વિડિઓ કૉલ સમાપ્ત થશે"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"જવાબ આપો"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"નકારો"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"કૉલ કરી શકાતો નથી કારણ કે આ પ્રકારના કૉલની સુવિધા આપતા હોય એવા કોઈ કૉલિંગ એકાઉન્ટ નથી."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"કૉલ કરી શકતા નથી. તમારા ડિવાઇસનું કનેક્શન ચેક કરો."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"તમારા <xliff:g id="OTHER_CALL">%1$s</xliff:g> કૉલને કારણે કૉલ કરી શકતાં નથી."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"તમારા <xliff:g id="OTHER_CALL">%1$s</xliff:g> કૉલને કારણે કૉલ કરી શકતાં નથી."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"અન્ય ઍપ્લિકેશનમાં કૉલને કારણે કૉલ કરી શકતાં નથી."</string>
diff --git a/res/values-hi/strings.xml b/res/values-hi/strings.xml
index c32f582..683a5ab 100644
--- a/res/values-hi/strings.xml
+++ b/res/values-hi/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"उत्तर देने से आपका जारी वीडियो कॉल खत्म हो जाएगा"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"उत्तर दें"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"अस्वीकार करें"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"कॉल नहीं किया जा सकता क्योंकि कॉल करने के लिए ऐसा कोई खाता नहीं है जिस पर इस तरह के कॉल की सुविधा हो."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"कॉल नहीं किया जा सकता. अपने डिवाइस के कनेक्शन की जांच करें."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"आपके <xliff:g id="OTHER_CALL">%1$s</xliff:g> कॉल के कारण कॉल नहीं लगाया जा सकता."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"आपके <xliff:g id="OTHER_CALL">%1$s</xliff:g> कॉल के कारण कॉल नहीं लगाया जा सकता."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"किसी दूसरे ऐप्लिकेशन में कॉल के कारण कॉल नहीं लगाया जा सकता."</string>
diff --git a/res/values-hr/strings.xml b/res/values-hr/strings.xml
index d6b209e..b664e5c 100644
--- a/res/values-hr/strings.xml
+++ b/res/values-hr/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Ako odgovorite, prekinut ćete videopoziv u tijeku"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Odgovori"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Odbij"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Poziv se ne može uputiti jer nema računa za pozivanje koji podržavaju pozive te vrste."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Poziv se ne može uputiti. Provjerite vezu uređaja."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Poziv se ne može uspostaviti zbog poziva u aplikaciji <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Poziv se ne može uspostaviti zbog poziva u aplikaciji <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Poziv se ne može uspostaviti zbog poziva u drugoj aplikaciji."</string>
diff --git a/res/values-hu/strings.xml b/res/values-hu/strings.xml
index 63f04b6..0a0c377 100644
--- a/res/values-hu/strings.xml
+++ b/res/values-hu/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Ha válaszol a hívásra, megszakítja a meglévő videohívást"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Hívás fogadása"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Elutasítás"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"A hívás nem indítható el, mert nincs olyan hívásra alkalmas fiók, amely támogatná az ilyen típusú hívásokat."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Nem lehet hívást indítani. Ellenőrizze eszköze kapcsolatát."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"A(z) <xliff:g id="OTHER_CALL">%1$s</xliff:g>-hívás miatt nem indítható hívás."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"A(z) <xliff:g id="OTHER_CALL">%1$s</xliff:g>-hívások miatt nem indítható hívás."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Egy másik alkalmazásban folytatott hívás miatt nem indítható hívás."</string>
diff --git a/res/values-hy/strings.xml b/res/values-hy/strings.xml
index 169ea36..7f877c5 100644
--- a/res/values-hy/strings.xml
+++ b/res/values-hy/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Եթե պատասխանեք այս զանգին, ընթացիկ տեսազանգը կընդհատվի"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Պատասխանել"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Մերժել"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Զանգը հնարավոր չէ կատարել, քանի որ հաշիվներ չկան, որոնք աջակցում են այս տեսակի զանգեր:"</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Հնարավոր չէ զանգել։ Ստուգեք սարքի միացումը։"</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Զանգը հնարավոր չէ կատարել՝ <xliff:g id="OTHER_CALL">%1$s</xliff:g>-ի ընթացիկ զանգի պատճառով:"</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Զանգը հնարավոր չէ կատարել՝ <xliff:g id="OTHER_CALL">%1$s</xliff:g>-ի ընթացիկ զանգերի պատճառով:"</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Զանգը հնարավոր չէ կատարել՝ մեկ այլ հավելվածի ընթացիկ զանգի պատճառով:"</string>
diff --git a/res/values-in/strings.xml b/res/values-in/strings.xml
index 1e51f7a..34c0c66 100644
--- a/res/values-in/strings.xml
+++ b/res/values-in/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Menjawab panggilan akan mengakhiri panggilan video yang sedang berlangsung"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Jawab"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Tolak"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Panggilan tidak dapat dilakukan karena tidak ada akun panggilan yang mendukung jenis panggilan ini."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Tidak dapat menelepon. Periksa koneksi perangkat Anda."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Panggilan tidak dapat dilakukan karena panggilan <xliff:g id="OTHER_CALL">%1$s</xliff:g> Anda."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Panggilan tidak dapat dilakukan karena panggilan <xliff:g id="OTHER_CALL">%1$s</xliff:g> Anda."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Panggilan tidak dapat dilakukan karena adanya panggilan di aplikasi lain."</string>
diff --git a/res/values-is/strings.xml b/res/values-is/strings.xml
index 7009b7c..c2fcf8f 100644
--- a/res/values-is/strings.xml
+++ b/res/values-is/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Ef þessu er svarað lýkur myndsímtalinu"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Svara"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Hafna"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Ekki er hægt að hringja vegna þess að engir símtalareikningar eru til staðar sem styðja svona símtöl."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Ekki er hægt að hringja. Athugaðu tengingu tækisins."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Ekki er hægt að hringja sökum símtalsins með <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Ekki er hægt að hringja sökum símtala með <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Ekki er hægt að hringja sökum símtals í öðru forriti."</string>
diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml
index 4a17d18..42cb0c8 100644
--- a/res/values-it/strings.xml
+++ b/res/values-it/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Se rispondi, la videochiamata in corso verrà terminata"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Rispondi"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Rifiuta"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Impossibile effettuare la chiamata perché non sono presenti account che supportano chiamate di questo tipo."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Impossibile effettuare la chiamata. Controlla la connessione del dispositivo."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Impossibile effettuare la chiamata a causa della chiamata <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Impossibile effettuare la chiamata a causa delle chiamate <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Impossibile effettuare la chiamata a causa di una chiamata in un\'altra app."</string>
diff --git a/res/values-iw/strings.xml b/res/values-iw/strings.xml
index 05ec712..98c5347 100644
--- a/res/values-iw/strings.xml
+++ b/res/values-iw/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"מענה יסיים את שיחת הווידאו הנוכחית"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"מענה"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"דחייה"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"אי אפשר להתקשר כי אין במכשיר חשבון שתומך בשיחות מהסוג הזה."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"אי אפשר להתקשר. כדאי לבדוק את החיבורים השונים של המכשיר."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"אי אפשר להתקשר בגלל שיש שיחה ב-<xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"אי אפשר להתקשר בגלל שיש שיחות ב-<xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"אי אפשר להתקשר בגלל שיש שיחה באפליקציה אחרת."</string>
diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml
index 19387ff..2df6736 100644
--- a/res/values-ja/strings.xml
+++ b/res/values-ja/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"応答すると、進行中のビデオ通話は終了します"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"応答"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"拒否"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"この種の通話に対応している通話アカウントがないため、通話を発信できません。"</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"発信できません。デバイスの接続状態を確認してください。"</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"<xliff:g id="OTHER_CALL">%1$s</xliff:g> で通話中のため、この通話を発信することはできません。"</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"<xliff:g id="OTHER_CALL">%1$s</xliff:g> で通話中のため、この通話を発信することはできません。"</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"別のアプリで通話中のため、この通話を発信することはできません。"</string>
diff --git a/res/values-ka/strings.xml b/res/values-ka/strings.xml
index d56873f..f2a3e90 100644
--- a/res/values-ka/strings.xml
+++ b/res/values-ka/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"პასუხის გაცემა თქვენს მიმდინარე ვიდეოზარს დაასრულებს"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"პასუხი"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"უარყოფა"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"ზარის განხორციელება შეუძლებელია, რადგან არ არის დარეკვის ის ანგარიშები, რომლებიც მხარს უჭერს ამ ტიპის ზარებს."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"ზარი ვერ ხორციელდება. შეამოწმეთ თქვენი მოწყობილობის კავშირი."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"ზარი ვერ ხორციელდება <xliff:g id="OTHER_CALL">%1$s</xliff:g> ზარის გამო."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"ზარი ვერ ხორციელდება <xliff:g id="OTHER_CALL">%1$s</xliff:g> ზარების გამო."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"ზარი ვერ ხორციელდება ზარის გამო სხვა აპში."</string>
diff --git a/res/values-kk/strings.xml b/res/values-kk/strings.xml
index e53631b..22ac1fc 100644
--- a/res/values-kk/strings.xml
+++ b/res/values-kk/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Жауап беру қазіргі бейне қоңырауды тоқтатады"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Жауап беру"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Қабылдамау"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Қоңырау шалу мүмкін емес, себебі бұндай қоңырауларға қолдау көрсететін аккаунт жоқ."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Қоңырау шалу мүмкін емес. Құрылғы байланысын тексеріңіз."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Қоңырау шалу мүмкін емес, себебі <xliff:g id="OTHER_CALL">%1$s</xliff:g> қоңырауы белсенді."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Қоңырау шалу мүмкін емес, себебі <xliff:g id="OTHER_CALL">%1$s</xliff:g> қоңыраулары белсенді."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Қоңырау шалу мүмкін емес, себебі басқа қолданбадан қоңырау шалынуда."</string>
diff --git a/res/values-km/strings.xml b/res/values-km/strings.xml
index 1c28d37..41b02f3 100644
--- a/res/values-km/strings.xml
+++ b/res/values-km/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"ការ​ឆ្លើយ​នឹង​បញ្ចប់​ការ​ហៅ​តាម​វីដេអូ​ដែល​កំពុង​តែ​ដំណើរការ​របស់​អ្នក"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"ឆ្លើយ"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"បដិសេធ"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"មិន​អាច​ធ្វើ​ការ​ហៅ​ទូរសព្ទ​បាន​ទេ ពីព្រោះ​មិនមាន​គណនី​ហៅ​ទូរសព្ទ​ដែល​អាច​ប្រើបាន​ជាមួយ​ការ​ហៅ​ប្រភេទ​នេះ​ទេ។"</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"មិនអាច​ហៅទូរសព្ទ​បានទេ។ សូម​ពិនិត្យមើល​ការតភ្ជាប់​របស់​ឧបករណ៍​អ្នក។"</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"ការ​ហៅ​មិន​អាចធ្វើ​បាន​ទេ ដោយ​សារ​ការហៅ​ <xliff:g id="OTHER_CALL">%1$s</xliff:g> របស់​អ្នក។"</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"ការ​ហៅ​មិន​អាច​ធ្វើ​បាន​ទេ ដោយ​សារ​ការ​ហៅ <xliff:g id="OTHER_CALL">%1$s</xliff:g> របស់​អ្នក។"</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"ការ​ហៅ​មិន​អាច​ធ្វើ​បាន​ទេ ដោយ​សារ​មាន​ការហៅ​មួយ​នៅ​ក្នុង​កម្មវិធី​ផ្សេង។"</string>
diff --git a/res/values-kn/strings.xml b/res/values-kn/strings.xml
index cbaa203..40151ec 100644
--- a/res/values-kn/strings.xml
+++ b/res/values-kn/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"ಕರೆಗೆ ಉತ್ತರಿಸುವುದರಿಂದ ನಿಮ್ಮ ಚಾಲ್ತಿಯಲ್ಲಿರುವ ವೀಡಿಯೊ ಕರೆಯು ಅಂತ್ಯಗೊಳ್ಳುತ್ತದೆ"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"ಉತ್ತರ"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"ನಿರಾಕರಿಸಿ"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"ಈ ಪ್ರಕಾರದ ಕರೆಗಳನ್ನು ಬೆಂಬಲಿಸುವ ಯಾವುದೇ ಕರೆಮಾಡುವಿಕೆ ಖಾತೆಗಳು ಇಲ್ಲದಿರುವ ಕಾರಣ ಕರೆಮಾಡಲು ಸಾಧ್ಯವಾಗುತ್ತಿಲ್ಲ."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"ಕರೆ ಮಾಡಲು ಸಾಧ್ಯವಿಲ್ಲ. ನಿಮ್ಮ ಸಾಧನದ ಕನೆಕ್ಷನ್ ಅನ್ನು ಪರಿಶೀಲಿಸಿ."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"ನಿಮ್ಮ <xliff:g id="OTHER_CALL">%1$s</xliff:g> ಕರೆ ಇರುವ ಕಾರಣ ಕರೆ ಮಾಡಲು ಸಾಧ್ಯವಾಗಿಲ್ಲ."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"ನಿಮ್ಮ <xliff:g id="OTHER_CALL">%1$s</xliff:g> ಕರೆಗಳ ಕಾರಣ ಕರೆ ಮಾಡಲು ಸಾಧ್ಯವಾಗಿಲ್ಲ."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"ಬೇರೊಂದು ಅಪ್ಲಿಕೇಶನ್‍ನಲ್ಲಿ ಕರೆಯಲ್ಲಿರುವುದರಿಂದ ಕರೆ ಮಾಡಲು ಸಾಧ್ಯವಾಗಲಿಲ್ಲ."</string>
diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml
index dc793e3..f0b95fd 100644
--- a/res/values-ko/strings.xml
+++ b/res/values-ko/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"전화를 받으면 진행 중인 화상 통화가 종료됩니다."</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"통화"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"거부"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"이 유형의 전화를 지원하는 전화 계정이 없으므로 전화를 걸 수 없습니다."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"전화를 걸 수 없습니다. 기기의 연결 상태를 확인하세요."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"<xliff:g id="OTHER_CALL">%1$s</xliff:g> 통화 중이므로 전화를 걸 수 없습니다."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"<xliff:g id="OTHER_CALL">%1$s</xliff:g> 통화 중이므로 전화를 걸 수 없습니다."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"다른 앱에서 통화 중이므로 전화를 걸 수 없습니다."</string>
diff --git a/res/values-ky/strings.xml b/res/values-ky/strings.xml
index 43def8b..ad19dd7 100644
--- a/res/values-ky/strings.xml
+++ b/res/values-ky/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Чалууга жооп берсеңиз, учурдагы видео чалууңуз бүтүп калат"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Жооп берүү"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Четке кагуу"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Бул түрдөгү чалуударды колдоого алган чалуу аккаунттары жок болгондуктан, чалуу аткарылбай койду."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Чалуу мүмкүн эмес. Түзмөгүңүздүн туташуусун текшериңиз."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Учурда <xliff:g id="OTHER_CALL">%1$s</xliff:g> чалууңуздан улам, башка жерге чала албайсыз."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Учурда <xliff:g id="OTHER_CALL">%1$s</xliff:g> чалууларыңуздан улам, башка жерге чала албайсыз."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Башка колдонмодо чалып жатасыз, ошондуктан чала албайсыз."</string>
diff --git a/res/values-lo/strings.xml b/res/values-lo/strings.xml
index ff79144..8e43935 100644
--- a/res/values-lo/strings.xml
+++ b/res/values-lo/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"ການຮັບສາຍຈະເປັນການວາງສາຍວິດີໂອທີ່ທ່ານກຳລັງໂທອອກ"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"ຮັບສາຍ"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"ປະຕິເສດ"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"ບໍ່ສາມາດໂທໄດ້ເນື່ອງຈາກບໍ່ມີບັນຊີການໂທທີ່ຮອງຮັບການໂທປະເພດນີ້."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"ບໍ່ສາມາດໂທອອກໄດ້. ກວດເບິ່ງການເຊື່ອມຕໍ່ຂອງອຸປະກອນຂອງທ່ານ."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"ບໍ່ສາມາດໂທອອກໄດ້ເນື່ອງຈາກການໂທ <xliff:g id="OTHER_CALL">%1$s</xliff:g> ຂອງທ່ານ"</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"ບໍ່ສາມາດໂທອອກໄດ້ເນື່ອງຈາກການໂທ <xliff:g id="OTHER_CALL">%1$s</xliff:g> ຂອງທ່ານ"</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"ບໍ່ສາມາດໂທອອກໄດ້ເນື່ອງຈາກສາຍໃນແອັບອື່ນ."</string>
diff --git a/res/values-lt/strings.xml b/res/values-lt/strings.xml
index 9454431..04f4c96 100644
--- a/res/values-lt/strings.xml
+++ b/res/values-lt/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Atsakius bus užbaigtas vykstantis vaizdo skambutis"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Atsakyti"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Atmesti"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Negalima skambinti, nes nėra jokių skambinimo paskyrų, kuriose palaikomi šio tipo skambučiai."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Nepavyko paskambinti. Patikrinkite įrenginio ryšį."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Negalima skambinti dėl „<xliff:g id="OTHER_CALL">%1$s</xliff:g>“ skambučio."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Negalima skambinti dėl „<xliff:g id="OTHER_CALL">%1$s</xliff:g>“ skambučių."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Negalima skambinti dėl skambučio kitoje programoje."</string>
diff --git a/res/values-lv/strings.xml b/res/values-lv/strings.xml
index 5ebdd8e..ee807da 100644
--- a/res/values-lv/strings.xml
+++ b/res/values-lv/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Atbildot uz zvanu, tiks beigts pašreizējais videozvans"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Atbildēt"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Noraidīt"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Nevar veikt zvanu, jo ierīcē nav neviena zvanu konta, kurā tiktu atbalstīti šī veida zvani."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Nevar veikt zvanu. Pārbaudiet ierīces savienojumu."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Nevar veikt zvanu notiekoša <xliff:g id="OTHER_CALL">%1$s</xliff:g> zvana dēļ."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Nevar veikt zvanu notiekošu <xliff:g id="OTHER_CALL">%1$s</xliff:g> zvanu dēļ."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Nevar veikt zvanu citā lietotnē notiekoša zvana dēļ."</string>
diff --git a/res/values-mk/strings.xml b/res/values-mk/strings.xml
index 57a3fce..c607df5 100644
--- a/res/values-mk/strings.xml
+++ b/res/values-mk/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Ако одговорите, ќе се прекине вашиот тековен видеоповик"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Одговорете"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Одбијте"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Повикот не може да се воспостави затоа што нема сметки за повикување што поддржуваат ваков тип повици."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Не може да се оствари повик. Проверете ја мрежата на уредот."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Не може да се воспостави повик поради вашиот повик на <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Не може да се воспостави повик поради вашите повици на <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Не може да се воспостави повик поради вашиот повик на друга апликација."</string>
diff --git a/res/values-ml/strings.xml b/res/values-ml/strings.xml
index a6d1626..1301b44 100644
--- a/res/values-ml/strings.xml
+++ b/res/values-ml/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"കോൾ സ്വീകരിക്കുന്നത് നിങ്ങളുടെ നിലവിലുള്ള വീഡിയോ കോൾ അവസാനിക്കാനിടയാക്കും"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"മറുപടി നൽകുക"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"നിരസിക്കുക"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"ഇത്തരം കോളുകൾക്ക് അനുയോജ്യമായ അക്കൗണ്ടുകളൊന്നും ഇല്ലാത്തതിനാൽ കോൾ ചെയ്യാനായില്ല."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"കോൾ ചെയ്യാനാകില്ല. നിങ്ങളുടെ ഉപകരണത്തിന്റെ കണക്ഷൻ പരിശോധിക്കുക."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"നിങ്ങളുടെ <xliff:g id="OTHER_CALL">%1$s</xliff:g> കോൾ കാരണം കോൾ ചെയ്യാനായില്ല."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"നിങ്ങളുടെ <xliff:g id="OTHER_CALL">%1$s</xliff:g> കോളുകൾ കാരണം കോൾ ചെയ്യാനായില്ല."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"മറ്റൊരു ആപ്പിലുള്ള കോൾ കാരണം കോൾ ചെയ്യാനായില്ല."</string>
diff --git a/res/values-mn/strings.xml b/res/values-mn/strings.xml
index 70dde8a..0b26e7e 100644
--- a/res/values-mn/strings.xml
+++ b/res/values-mn/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Хариулбал таны одоогийн видео дуудлагыг таслах болно"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Хариулах"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Татгалзах"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Энэ төрлийн дуудлага дэмждэг дуудлагын бүртгэл байхгүй тул дуудлага хийх боломжгүй байна."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Дуудлага хийх боломжгүй. Төхөөрөмжийнхөө холболтыг шалгана уу."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Таны <xliff:g id="OTHER_CALL">%1$s</xliff:g> дуудлагаас шалтгаалан дуудлага хийх боломжгүй байна."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Таны <xliff:g id="OTHER_CALL">%1$s</xliff:g> дуудлагаас шалтгаалан дуудлага хийх боломжгүй байна."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Өөр апп доторх дуудлагаас шалтгаалан дуудлага хийх боломжгүй байна."</string>
diff --git a/res/values-mr/strings.xml b/res/values-mr/strings.xml
index c4438ae..eca7b4d 100644
--- a/res/values-mr/strings.xml
+++ b/res/values-mr/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"उत्तर देण्यामुळे तुमचा सुरू असलेला व्हिडिओ कॉल समाप्त होईल"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"उत्तर द्या"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"नकार द्या"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"कॉल करू शकत नाही कारण अशाप्रकारच्या कॉलला सपोर्ट करतील अशी कोणतीही कॉलिंग खाती नाहीत."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"कॉल करू शकत नाही. तुमच्या डिव्हाइसचे कनेक्शन तपासणे."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"आपल्या <xliff:g id="OTHER_CALL">%1$s</xliff:g> कॉलमुळे कॉल केला जाऊ शकत नाही."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"आपल्या <xliff:g id="OTHER_CALL">%1$s</xliff:g> कॉलमुळे कॉल केला जाऊ शकत नाही."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"दुसर्‍या ॲपमधील कॉलमुळे कॉल केला जाऊ शकत नाही."</string>
diff --git a/res/values-ms/strings.xml b/res/values-ms/strings.xml
index 355502c..ebfffd0 100644
--- a/res/values-ms/strings.xml
+++ b/res/values-ms/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Menjawab akan menamatkan panggilan video semasa anda"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Jawab"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Tolak"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Panggilan tidak dapat dibuat kerana tiada akaun panggilan yang menyokong panggilan jenis ini."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Tidak dapat membuat panggilan. Semak sambungan peranti anda."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Panggilan tidak dapat dibuat disebabkan panggilan <xliff:g id="OTHER_CALL">%1$s</xliff:g> anda."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Panggilan tidak dapat dibuat disebabkan panggilan <xliff:g id="OTHER_CALL">%1$s</xliff:g> anda."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Panggilan tidak dapat dibuat disebabkan panggilan dalam apl lain."</string>
diff --git a/res/values-my/strings.xml b/res/values-my/strings.xml
index c9e5593..e7f0fd4 100644
--- a/res/values-my/strings.xml
+++ b/res/values-my/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"ဖုန်းကိုင်လိုက်လျှင် လက်ရှိဗီဒီယိုပြောနေခြင်းကိုဖြတ်ပစ်ပါမည်"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"ဖုန်းကိုင်ရန်"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"ဖုန်းမကိုင်ရန်"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"ဤဖုန်းခေါ်ဆိုမှု အမျိုးအစားကို ပံ့ပိုးပေးသည့် ခေါ်ဆိုမှုအကောင့်များ မရှိသဖြင့် ဖုန်းခေါ်၍ မရပါ။"</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"ဖုန်းမခေါ်ဆိုနိုင်ပါ။ သင့်စက်၏ ချိတ်ဆက်မှုကို စစ်ပါ။"</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"<xliff:g id="OTHER_CALL">%1$s</xliff:g> သုံးပြီးပြောနေသည့်အတွက် အထွက်ခေါ်ဆိုမှုကို မပြုလုပ်နိုင်ပါ။"</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"<xliff:g id="OTHER_CALL">%1$s</xliff:g> သုံးပြီးပြောနေသည့်အတွက် အထွက်ခေါ်ဆိုမှုများကို မပြုလုပ်နိုင်ပါ။"</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"အခြားအက်ပ်သုံးပြီးပြောနေသည့်အတွက် အထွက်ခေါ်ဆိုမှုကို မပြုလုပ်နိုင်ပါ။"</string>
diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml
index 8bebbff..66e6ffc 100644
--- a/res/values-nb/strings.xml
+++ b/res/values-nb/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Hvis du svarer, avsluttes videosamtalen du er i nå"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Svar"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Avvis"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Anropet kan ikke utføres fordi du ikke har noen ringekontoer som støtter denne typen anrop."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Kan ikke ringe. Sjekk tilkoblingen på enheten."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Kan ikke ringe ut på grunn av <xliff:g id="OTHER_CALL">%1$s</xliff:g>-samtalen din."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Kan ikke ringe ut på grunn av <xliff:g id="OTHER_CALL">%1$s</xliff:g>-samtalene dine."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Kan ikke ringe ut på grunn av en samtale i en annen app."</string>
diff --git a/res/values-ne/strings.xml b/res/values-ne/strings.xml
index df2c70c..d8fc473 100644
--- a/res/values-ne/strings.xml
+++ b/res/values-ne/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"जवाफ फर्काउनुले तपाईंको जारी भिडियो कल समाप्त हुनेछ"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"जवाफ दिनुहोस्"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"अस्वीकार गर्नुहोस्"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"यस प्रकारका कलहरूलाई समर्थन गर्ने कुनै पनि कल गर्ने खाता नभएकाले कल गर्न सकिँदैन।"</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"कल गर्न सकिएन। आफ्नो डिभाइसको इन्टरनेट जाँच्नुहोस्।"</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"तपाईंको <xliff:g id="OTHER_CALL">%1$s</xliff:g> कलका कारण कल गर्न सकिँदैन।"</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"तपाईंका <xliff:g id="OTHER_CALL">%1$s</xliff:g> कलहरूका कारण कल गर्न सकिँदैन।"</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"अर्को एपमा जारी कलका कारण कल गर्न सकिँदैन।"</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 48c7957..e395ef1 100644
--- a/res/values-nl/strings.xml
+++ b/res/values-nl/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Als je opneemt, wordt je actieve videogesprek beëindigd"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Beantwoorden"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Weigeren"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Gesprek kan niet worden geplaatst omdat er geen gespreksaccounts zijn die gesprekken van dit type ondersteunen."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Kan niet bellen. Check de verbinding van je apparaat."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Gesprek kan niet worden gestart vanwege je <xliff:g id="OTHER_CALL">%1$s</xliff:g>-gesprek."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Gesprek kan niet worden gestart vanwege je <xliff:g id="OTHER_CALL">%1$s</xliff:g>-gesprekken."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Gesprek kan niet worden gestart vanwege een gesprek in een andere app."</string>
diff --git a/res/values-or/strings.xml b/res/values-or/strings.xml
index 6f3ebe3..535583a 100644
--- a/res/values-or/strings.xml
+++ b/res/values-or/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"ଉତ୍ତର ଦେବାଦ୍ଵାରା ଆପଣଙ୍କର ଜାରି ରହିଥିବା ଭିଡିଓ କଲ୍ ସମାପ୍ତ ହୋ‌ଇଯିବ"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"ଉତ୍ତର ଦିଅନ୍ତୁ"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"ଅସ୍ୱୀକାର"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"ଏହି ପ୍ରକାରର କଲ୍ ସମର୍ଥନ କରୁଥିବା କଲିଂ ଆକାଉଣ୍ଟ ନଥିବା ଯୋଗୁଁ କଲ୍‌ କରାଯାଇପାରିବ ନାହିଁ।"</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"କଲ କରାଯାଇପାରିବ ନାହିଁ। ଆପଣଙ୍କ ଡିଭାଇସର କନେକ୍ସନ ଯାଞ୍ଚ କରନ୍ତୁ।"</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"ଆପଣଙ୍କର <xliff:g id="OTHER_CALL">%1$s</xliff:g> କଲ୍ ହେତୁ କଲ୍ କରାଯାଇପାରିବ ନାହିଁ।"</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"ଆପଣଙ୍କର <xliff:g id="OTHER_CALL">%1$s</xliff:g> କଲ୍ ହେତୁ କଲ୍ କରାଯାଇପାରିବ ନାହିଁ।"</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"ଅନ୍ୟ ଆପ୍‌ରେ କରାଯାଇଥିବା କଲ୍ ହେତୁ କଲ୍ କରାଯାଇପାରିବ ନାହିଁ।"</string>
diff --git a/res/values-pa/strings.xml b/res/values-pa/strings.xml
index b96a1db..831f260 100644
--- a/res/values-pa/strings.xml
+++ b/res/values-pa/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"ਜਵਾਬ ਦੇਣ ਨਾਲ ਤੁਹਾਡੀ ਜਾਰੀ ਵੀਡੀਓ ਕਾਲ ਸਮਾਪਤ ਹੋ ਜਾਵੇਗੀ"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"ਕਾਲ ਚੁੱਕੋ"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"ਕਾਲ ਕੱਟੋ"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"ਕਾਲ ਨਹੀਂ ਕੀਤੀ ਜਾ ਸਕਦੀ ਕਿਉਂਕਿ ਇੱਥੇ ਅਜਿਹੇ ਕੋਈ ਕਾਲਿੰਗ ਖਾਤੇ ਨਹੀਂ ਹਨ ਜਿਨ੍ਹਾਂ ਵਿੱਚ ਇਸ ਕਿਸਮ ਦੀਆਂ ਕਾਲਾਂ ਦੀ ਸੁਵਿਧਾ ਹੋਵੇ।"</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"ਕਾਲ ਨਹੀਂ ਕੀਤੀ ਜਾ ਸਕਦੀ। ਆਪਣੇ ਡੀਵਾਈਸ ਦੇ ਕਨੈਕਸ਼ਨ ਦੀ ਜਾਂਚ ਕਰੋ।"</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"ਤੁਹਾਡੀ <xliff:g id="OTHER_CALL">%1$s</xliff:g> ਕਾਲ ਦੇ ਕਾਰਨ ਕਾਲ ਨਹੀਂ ਕੀਤੀ ਜਾ ਸਕਦੀ।"</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"ਤੁਹਾਡੀਆਂ <xliff:g id="OTHER_CALL">%1$s</xliff:g> ਕਾਲਾਂ ਦੇ ਕਾਰਨ ਕਾਲ ਨਹੀਂ ਕੀਤੀ ਜਾ ਸਕਦੀ।"</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"ਕਿਸੇ ਹੋਰ ਐਪ ਵਿੱਚ ਇੱਕ ਕਾਲ ਹੋਣ ਦੇ ਕਾਰਨ ਕਾਲ ਨਹੀਂ ਕੀਤੀ ਜਾ ਸਕਦੀ।"</string>
diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml
index df5d29e..23776f5 100644
--- a/res/values-pl/strings.xml
+++ b/res/values-pl/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Jeśli odbierzesz połączenie, zakończysz rozmowę wideo"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Odbierz"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Odrzuć"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Nie można nawiązać połączenia, ponieważ nie ma żadnego konta, które obsługuje połączenia tego typu."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Nie udało się zadzwonić. Sprawdź połączenie urządzenia."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Nie możesz zadzwonić z powodu trwającej rozmowy w <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Nie możesz zadzwonić z powodu trwających rozmów w <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Nie możesz zadzwonić z powodu trwającej rozmowy w innej aplikacji."</string>
diff --git a/res/values-pt-rPT/strings.xml b/res/values-pt-rPT/strings.xml
index 5fbe1d3..122615a 100644
--- a/res/values-pt-rPT/strings.xml
+++ b/res/values-pt-rPT/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Ao atender, a sua videochamada em curso será terminada"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Atender"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Recusar"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Não é possível efetuar a chamada porque não existem contas de chamadas que suportem chamadas deste tipo."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Não é possível fazer a chamada. Verifique a ligação do seu dispositivo."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Não é possível efetuar a chamada devido à sua chamada do <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Não é possível efetuar a chamada devido às suas chamadas do <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Não é possível efetuar a chamada devido a uma chamada noutra app."</string>
diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml
index a7fc3c7..1e8b027 100644
--- a/res/values-pt/strings.xml
+++ b/res/values-pt/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Se você atender, a videochamada em andamento será encerrada"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Atender"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Recusar"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Não é possível ligar porque não há contas compatíveis com chamadas deste tipo."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Não foi possível fazer a chamada. Verifique a conexão do dispositivo."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Não é possível ligar com uma chamada em andamento no <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Não é possível ligar com chamadas em andamento no <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Não é possível ligar com uma chamada em andamento em outro aplicativo."</string>
diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml
index 8e485d0..fe5ad93 100644
--- a/res/values-ro/strings.xml
+++ b/res/values-ro/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Dacă răspunzi, apelul video în curs va fi încheiat."</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Răspunde"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Respinge"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Apelul nu poate fi inițiat deoarece nu există conturi pentru apelare compatibile cu apeluri de acest tip."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Apelul nu poate fi inițiat. Verifică conexiunea dispozitivului."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Apelul nu poate fi inițiat din cauza apelului <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Apelul nu poate fi inițiat din cauza apelurilor <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Apelul nu poate fi inițiat din cauza unui apel din altă aplicație."</string>
diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml
index 67ab2e9..cc69d40 100644
--- a/res/values-ru/strings.xml
+++ b/res/values-ru/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Если вы ответите, текущий видеовызов будет завершен."</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Ответить"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Отклонить"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Невозможно позвонить, так как нет аккаунтов, которые поддерживают вызовы этого типа."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Не удалось позвонить. Проверьте подключение устройства."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Вы не можете отправить вызов, пока не завершите другой в приложении <xliff:g id="OTHER_CALL">%1$s</xliff:g>"</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Вы не можете отправить вызов, пока не завершите другие в приложении <xliff:g id="OTHER_CALL">%1$s</xliff:g>"</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Вы не можете отправить новый вызов, пока не завершите текущий в другом приложении"</string>
diff --git a/res/values-si/strings.xml b/res/values-si/strings.xml
index 71442e0..2ea058f 100644
--- a/res/values-si/strings.xml
+++ b/res/values-si/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"පිළිතුරු දීම ඔබේ යන වීඩියෝ ඇමතුම අවසන් කරනු ඇත"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"පිළිතුරු දෙන්න"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"ප්‍රතික්ෂේප කරන්න"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"මෙම වර්ගයේ ඇමතුම්වලට සහාය දක්වන ඇමතීමේ ගිණුම් නොමැති නිසා ඇමතුම ගැනීමට නොහැකිය."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"ඇමතුම ගත නොහැක. ඔබේ‏‏‏ උපාංගයේ සම්බන්ධතාවය පරීක්ෂා කරන්න."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"ඔබේ <xliff:g id="OTHER_CALL">%1$s</xliff:g> ඇමතුම හේතුවෙන් ඇමතුම ගැනීමට නොහැකිය."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"ඔබේ <xliff:g id="OTHER_CALL">%1$s</xliff:g> ඇමතුම් හේතුවෙන් ඇමතුම ගැනීමට නොහැකිය."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"වෙනත් යෙදුමක ඇමතුමක් හේතුවෙන් ඇමතුම ගැනීමට නොහැකිය."</string>
diff --git a/res/values-sk/strings.xml b/res/values-sk/strings.xml
index a001130..fc7108a 100644
--- a/res/values-sk/strings.xml
+++ b/res/values-sk/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Prijatím hovoru ukončíte prebiehajúci videohovor"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Prijať"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Odmietnuť"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Hovor sa nedá uskutočniť, pretože nie je k dispozícii žiaden účet, ktorý by tento typ hovorov podporoval."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Nedá sa volať. Skontrolujte pripojenie zariadenia."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Hovor sa nedá uskutočniť, pretože prebieha hovor <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Hovor sa nedá uskutočniť, pretože prebiehajú hovory <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Hovor sa nedá uskutočniť, pretože prebieha hovor v inej aplikácii."</string>
diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml
index 994bc7e..7ee0b0b 100644
--- a/res/values-sl/strings.xml
+++ b/res/values-sl/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Če sprejmete, bo končan aktivni videoklic"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Sprejmi"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Zavrni"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Klica ni mogoče vzpostaviti, ker ni računov za klicanje, ki podpirajo tovrstne klice."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Klica ni mogoče vzpostaviti. Preverite povezavo naprave."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Klica ni mogoče vzpostaviti zaradi klica prek aplikacije <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Klica ni mogoče vzpostaviti zaradi klicev prek aplikacije <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Klica ni mogoče vzpostaviti zaradi klica prek druge aplikacije."</string>
diff --git a/res/values-sq/strings.xml b/res/values-sq/strings.xml
index 89ae852..876b049 100644
--- a/res/values-sq/strings.xml
+++ b/res/values-sq/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Përgjigjja do ta mbyllë telefonatën me video në vazhdim"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Përgjigju"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Refuzo"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Telefonata nuk mund të kryhet pasi nuk ka asnjë llogari telefonatash që i mbështet telefonatat e këtij lloji."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Telefonata nuk mund të kryhet. Kontrollo lidhjen e pajisjes sate."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Telefonata nuk mund të kryhet për shkak të telefonatës tënde të <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Telefonata nuk mund të kryhet për shkak të telefonatave të tua të <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Telefonata nuk mund të kryhet për shkak të një telefonate në një aplikacion tjetër."</string>
diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml
index 1134380..148cb14 100644
--- a/res/values-sr/strings.xml
+++ b/res/values-sr/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Ако одговорите, завршићете видео позив који је у току"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Одговори"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Одбиј"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Упућивање позива није могуће јер немате ниједан налог за позивање који подржава позиве овог типа."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Позивање није успело. Проверите везу уређаја."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Не можете да упутите позив због <xliff:g id="OTHER_CALL">%1$s</xliff:g> позива."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Не можете да упутите позив због <xliff:g id="OTHER_CALL">%1$s</xliff:g> позива."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Не можете да упутите позив због позива у другој апликацији."</string>
diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml
index c6f6ec9..d4a930c 100644
--- a/res/values-sv/strings.xml
+++ b/res/values-sv/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Det pågående videosamtalet avslutas om du svarar"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Svara"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Avvisa"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Det går inte att ringa på grund av att det inte finns uppringningskonton som stöder den här samtalstypen."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Det går inte att ringa samtalet. Kontrollera enhetens anslutning."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Det går inte att ringa på grund av samtalet via <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Det går inte att ringa på grund av samtalen via <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Det går inte att ringa på grund av ett samtal via en annan app."</string>
diff --git a/res/values-sw/strings.xml b/res/values-sw/strings.xml
index ef58c00..ac0518d 100644
--- a/res/values-sw/strings.xml
+++ b/res/values-sw/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Ukijibu utakata simu yako ya video inayoendelea"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Jibu"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Kataa"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Haiwezi kupiga simu kwa sababu hakuna akaunti za kupiga simu zinazoweza kupiga aina hii ya simu."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Imeshindwa kupiga simu. Kagua muunganisho wa kifaa chako."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Haiwezekani kupiga kwa sababu ya simu yako ya <xliff:g id="OTHER_CALL">%1$s</xliff:g> inayoendelea."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Haiwezekani kupiga kwa sababu ya simu zako za <xliff:g id="OTHER_CALL">%1$s</xliff:g> zinazoendelea."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Haiwezekani kwa sababu kuna simu inayoendelea kwenye programu nyingine."</string>
diff --git a/res/values-ta/strings.xml b/res/values-ta/strings.xml
index 9f37d87..57c70f4 100644
--- a/res/values-ta/strings.xml
+++ b/res/values-ta/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"பதிலளித்தால், செயலில் உள்ள வீடியோ அழைப்பு துண்டிக்கப்படும்"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"பதிலளி"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"நிராகரி"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"இந்த வகை அழைப்புகளை ஆதரிக்கும் அழைப்புக் கணக்குகள் இல்லாததால், அழைப்பை மேற்கொள்ள முடியாது."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"அழைப்பை மேற்கொள்ள முடியவில்லை. உங்கள் சாதனத்தின் இணைப்பைச் சரிபார்க்கவும்."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"<xliff:g id="OTHER_CALL">%1$s</xliff:g> அழைப்பு செயலில் உள்ளதால், புதிய அழைப்பைச் செய்ய முடியாது."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"<xliff:g id="OTHER_CALL">%1$s</xliff:g> அழைப்புகள் செயலில் உள்ளதால், புதிய அழைப்பைச் செய்ய முடியாது."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"மற்றொரு பயன்பாட்டில் அழைப்பு செயலில் உள்ளதால், புதிய அழைப்பைச் செய்ய முடியாது."</string>
diff --git a/res/values-te/strings.xml b/res/values-te/strings.xml
index 8f8a23e..22f4b8a 100644
--- a/res/values-te/strings.xml
+++ b/res/values-te/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"సమాధానమివ్వడం వలన మీ కొనసాగుతున్న వీడియో కాల్ ముగుస్తుంది"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"సమాధానమివ్వండి"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"తిరస్కరించు"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"కాల్ చేయడం సాధ్యపడదు ఎందుకంటే, ఈ రకమైన కాల్స్‌కు మద్దతిచ్చే కాల్ చేయడానికి ఉపయోగించే ఖాతాలు లేవు."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"కాల్ చేయడం సాధ్యపడదు. మీ పరికర కనెక్షన్‌ను చెక్ చేయండి."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"మీ <xliff:g id="OTHER_CALL">%1$s</xliff:g> కాల్ కొనసాగుతున్నందున కాల్ చేయడం సాధ్యపడదు."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"మీ <xliff:g id="OTHER_CALL">%1$s</xliff:g> కాల్స్‌ కొనసాగుతున్నందున కాల్ చేయడం సాధ్యపడదు."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"వేరొక యాప్‌లో కాల్ కొనసాగుతున్నందున కాల్ చేయడం సాధ్యపడదు."</string>
diff --git a/res/values-th/strings.xml b/res/values-th/strings.xml
index b8dc9f0..e3a20b1 100644
--- a/res/values-th/strings.xml
+++ b/res/values-th/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"การรับสายนี้จะวางสาย Hangouts วิดีโอที่สนทนาอยู่"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"รับสาย"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"ปฏิเสธ"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"การโทรไม่สำเร็จเนื่องจากไม่มีบัญชีการโทรที่รองรับการโทรประเภทนี้"</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"โทรออกไม่ได้ โปรดตรวจสอบการเชื่อมต่อของอุปกรณ์"</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"ไม่สามารถโทรออกได้เนื่องจากกำลังใช้สายอยู่ใน <xliff:g id="OTHER_CALL">%1$s</xliff:g>"</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"ไม่สามารถโทรออกได้เนื่องจากกำลังใช้สายอยู่ใน <xliff:g id="OTHER_CALL">%1$s</xliff:g>"</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"ไม่สามารถโทรออกได้เนื่องจากกำลังใช้สายอยู่ในแอปอื่น"</string>
diff --git a/res/values-tl/strings.xml b/res/values-tl/strings.xml
index 91e1b33..001a19a 100644
--- a/res/values-tl/strings.xml
+++ b/res/values-tl/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Kung sasagutin, matatapos ang iyong kasalukuyang video call"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Sagutin"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Tanggihan"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Hindi maisasagawa ang tawag dahil walang account sa pagtawag na sumusuporta sa ganitong uri ng mga tawag."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Hindi makatawag. Suriin ang koneksyon ng iyong device."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Hindi makakatawag dahil sa iyong tawag sa <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Hindi makakatawag dahil sa iyong mga tawag sa <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Hindi makakatawag dahil sa isang tawag sa isa pang app."</string>
diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml
index 0aa2e20..1924d92 100644
--- a/res/values-tr/strings.xml
+++ b/res/values-tr/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Cevapladığınızda, devam eden görüntülü görüşme sona erecek"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Cevapla"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Reddet"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Bu tür görüşmeleri destekleyen bir arama hesabı olmadığı için arama yapılamıyor."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Arama yapılamıyor. Cihazınızın bağlantısını kontrol edin."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Devam eden <xliff:g id="OTHER_CALL">%1$s</xliff:g> çağrınız nedeniyle telefon araması yapılamıyor."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Devam eden <xliff:g id="OTHER_CALL">%1$s</xliff:g> çağrılarınız nedeniyle telefon araması yapılamıyor."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Başka bir uygulamada devam eden çağrınız nedeniyle telefon araması yapılamıyor."</string>
diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml
index a4d01d1..2d4f5bc 100644
--- a/res/values-uk/strings.xml
+++ b/res/values-uk/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Якщо відповісти на виклик, поточний відеодзвінок завершиться"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Відповісти"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Відхилити"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Такі виклики не підтримуються. Немає потрібного облікового запису чи сервісу."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Не вдається здійснити виклик. Перевірте підключення пристрою."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Неможливо зателефонувати через поточний виклик у <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Неможливо зателефонувати через поточні виклики в <xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Неможливо зателефонувати через поточний виклик в іншому додатку."</string>
diff --git a/res/values-ur/strings.xml b/res/values-ur/strings.xml
index 6649f42..b09f244 100644
--- a/res/values-ur/strings.xml
+++ b/res/values-ur/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"جواب دینا آپ کی جاری ویڈیو کال کو ختم کر دے گا"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"جواب دیں"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"مسترد کریں"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"کال نہیں کی جا سکی کیونکہ اس قسم کی کالز کو سپورٹ کرنے والا کوئی کالنگ اکاؤنٹ نہیں ہے۔"</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"کال نہیں کر سکتے۔ اپنے آلے کا کنکشن چیک کریں۔"</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"آپ کی <xliff:g id="OTHER_CALL">%1$s</xliff:g> کال کی وجہ سے کال نہیں کی جاسکتی۔"</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"آپ کی <xliff:g id="OTHER_CALL">%1$s</xliff:g> کالز کی وجہ سے کالز نہیں کی جاسکتیں۔"</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"کسی دوسری ایپ میں موجود کال کی کی وجہ سے کال نہیں کی جا سکتی۔"</string>
diff --git a/res/values-uz/strings.xml b/res/values-uz/strings.xml
index c6805ea..ff04903 100644
--- a/res/values-uz/strings.xml
+++ b/res/values-uz/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Chaqiruvga javob berilsa, joriy video suhbat tugatiladi."</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Javob berish"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Rad etish"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Telefon qilish imkonsiz, chunki bunday turdagi chaqiruvni qo‘llab-quvvatlaydigan hisob yo‘q."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Telefon ishlamaydi. Qurilma aloqasini tekshiring."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Joriy <xliff:g id="OTHER_CALL">%1$s</xliff:g> qo‘ng‘ir. tufayli boshqa raqamni chaqirib bo‘lmaydi."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Joriy <xliff:g id="OTHER_CALL">%1$s</xliff:g> qo‘ng‘ir-r tufayli boshqa raqamni chaqirib bo‘lmaydi."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Boshqa ilovadagi joriy qo‘ng‘iroq tufayli boshqa raqamni chaqirib bo‘lmaydi."</string>
diff --git a/res/values-vi/strings.xml b/res/values-vi/strings.xml
index 70f9bfc..142026c 100644
--- a/res/values-vi/strings.xml
+++ b/res/values-vi/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Trả lời sẽ kết thúc cuộc gọi video đang diễn ra của bạn"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Trả lời"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Từ chối"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Không thể thực hiện cuộc gọi do không có tài khoản hỗ trợ loại cuộc gọi này."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Không thể gọi điện. Hãy kiểm tra kết nối của thiết bị."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Không thể thực hiện cuộc gọi do cuộc gọi <xliff:g id="OTHER_CALL">%1$s</xliff:g> của bạn."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Không thể thực hiện cuộc gọi do cuộc gọi <xliff:g id="OTHER_CALL">%1$s</xliff:g> của bạn."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Không thể thực hiện cuộc gọi do có cuộc gọi trong một ứng dụng khác."</string>
diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml
index 1ef0a55..7cb8a7a 100644
--- a/res/values-zh-rCN/strings.xml
+++ b/res/values-zh-rCN/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"如果接听此来电,您当前的视频通话会中断。"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"接听"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"拒接"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"无法拨出电话,因为没有通话账号支持拨打这类电话。"</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"无法拨打电话。请检查设备的连接情况。"</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"由于当前正在进行 <xliff:g id="OTHER_CALL">%1$s</xliff:g> 通话,因此无法拨打电话。"</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"由于当前正在进行 <xliff:g id="OTHER_CALL">%1$s</xliff:g> 通话,因此无法拨打电话。"</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"由于当前正在通过其他应用通话,因此无法拨打电话。"</string>
diff --git a/res/values-zh-rHK/strings.xml b/res/values-zh-rHK/strings.xml
index 0140f26..213255a 100644
--- a/res/values-zh-rHK/strings.xml
+++ b/res/values-zh-rHK/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"如果接聽,你進行中的視像通話將會結束"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"接聽"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"拒絕"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"沒有通話帳戶支援這類通話,因此無法撥打電話。"</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"無法撥打電話。請檢查裝置是否正確連接。"</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"由於你已在進行 <xliff:g id="OTHER_CALL">%1$s</xliff:g> 通話,因此無法撥打電話。"</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"由於你已在進行 <xliff:g id="OTHER_CALL">%1$s</xliff:g> 通話,因此無法撥打電話。"</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"由於已在另一個應用程式中進行通話,因此無法撥打電話。"</string>
diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml
index eeb98b5..287f627 100644
--- a/res/values-zh-rTW/strings.xml
+++ b/res/values-zh-rTW/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"接聽之後,你正在進行的視訊通話就會結束"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"接聽"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"拒接"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"你尚未設定支援這類通話的通話帳戶,因此無法撥打電話。"</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"無法撥打電話,請檢查裝置的藍牙連線。"</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"你正在進行 <xliff:g id="OTHER_CALL">%1$s</xliff:g> 通話,因此無法撥打電話。"</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"你正在進行 <xliff:g id="OTHER_CALL">%1$s</xliff:g> 通話,所以無法撥打電話。"</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"你正在使用其他應用程式進行通話,因此無法撥打電話。"</string>
diff --git a/res/values-zu/strings.xml b/res/values-zu/strings.xml
index faee0d9..8d0437d 100644
--- a/res/values-zu/strings.xml
+++ b/res/values-zu/strings.xml
@@ -90,7 +90,7 @@
     <string name="answering_ends_other_managed_video_call" msgid="1988508241432031327">"Ukuphendula kuzoqeda ikholi yakho yevidiyo eqhubekayo"</string>
     <string name="answer_incoming_call" msgid="2045888814782215326">"Phendula"</string>
     <string name="decline_incoming_call" msgid="922147089348451310">"Yenqaba"</string>
-    <string name="cant_call_due_to_no_supported_service" msgid="1635626384149947077">"Ikholi ayikwazi ukubekwa ngoba awasekho ama-akhawunti okushaya asekela amakholi walolu hlobo."</string>
+    <string name="cant_call_due_to_no_supported_service" msgid="6720817368116820027">"Ayikwazi ukwenza ikholi. Hlola ukuxhumeka kwedivayisi yakho."</string>
     <string name="cant_call_due_to_ongoing_call" msgid="8004235328451385493">"Ikholi ayikwazi ukwenziwa ngenxa yekholi yakho ye-<xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_calls" msgid="6379163795277824868">"Ikholi ayikwazi ukwenziwa ngenxa yamakholi akho e-<xliff:g id="OTHER_CALL">%1$s</xliff:g>."</string>
     <string name="cant_call_due_to_ongoing_unknown_call" msgid="8243532328969433172">"Ikholi ayikwazi ukwenziwa ngenxa yekholi kolunye uhlelo lokusebenza."</string>
diff --git a/res/values/config.xml b/res/values/config.xml
index bf30720..8ebbd86 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 set, telecom will skip fetching incoming caller info for this account -->
+    <string name="skip_incoming_caller_info_account_package"></string>
+
+    <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/strings.xml b/res/values/strings.xml
index ec278f0..aefd2e6 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -290,9 +290,11 @@
 
     <!-- Error message shown to the user when an outgoing call cannot be placed because there no
          calling service is present on the device which supports this call type.
-         This is typically encountered when the user tries to dial a SIP/VOIP call, but there are
-         no calling services present which support SIP calling. [CHAR LIMIT=none] -->
-    <string name="cant_call_due_to_no_supported_service">Call cannot be placed because there are no calling accounts which support calls of this type.</string>
+         This can happen on a device such as a watch or tablet which provides calling using a
+         service that may not be available all the time.  For example, a watch may rely on Bluetooth
+         to be enabled for calling to work; when Bluetooth is disabled calling would not work.
+         [CHAR LIMIT=none] -->
+    <string name="cant_call_due_to_no_supported_service">Can\'t make call. Check your device\'s connection.</string>
 
     <!-- Error message shown to the user when an outgoing call cannot be placed due to an ongoing
          phone call in a third-party app.  For example:
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..7ce9fcc 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,11 +64,13 @@
         }
         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",
+            Log.i(this, "createRetry; type=%s, address=%s, retryCount=%d",
                     DEVICE_TYPE_STRINGS.get(type), bluetoothAddress, retryCount);
             AudioDeviceInfo routeInfo = null;
             List<AudioDeviceInfo> infos = audioManager.getAvailableCommunicationDevices();
@@ -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,80 @@
 
     // 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 (%s)", active,
+                DEVICE_TYPE_STRINGS.get(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, "onDestRouteAsPendingRoute: route=%s, "
+                            + "AudioManager#setCommunicationDevice()=%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 +348,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, "clearCommunicationDevice: Disconnecting SCO device.");
+            result = bluetoothRouteManager.getDeviceManager().disconnectSco();
+        } else {
+            Log.i(this, "clearCommunicationDevice: AudioManager#clearCommunicationDevice, type=%s",
+                    DEVICE_TYPE_STRINGS.get(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 642a667..59cbdae 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;
 
     /**
@@ -904,7 +931,6 @@
         mLock = lock;
         mRepository = repository;
         mPhoneNumberUtilsAdapter = phoneNumberUtilsAdapter;
-        setHandle(handle);
         mParticipants = participants;
         mPostDialDigits = handle != null
                 ? PhoneNumberUtils.extractPostDialPortion(handle.getSchemeSpecificPart()) : "";
@@ -912,6 +938,7 @@
         setConnectionManagerPhoneAccount(connectionManagerPhoneAccountHandle);
         mCallDirection = callDirection;
         setTargetPhoneAccount(targetPhoneAccountHandle);
+        setHandle(handle);
         mIsConference = isConference;
         mShouldAttachToExistingConnection = shouldAttachToExistingConnection
                 || callDirection == CALL_DIRECTION_INCOMING;
@@ -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,13 @@
                 mIsTestEmergencyCall = mHandle != null &&
                         isTestEmergencyCall(mHandle.getSchemeSpecificPart());
             }
-            startCallerInfoLookup();
+            if (mTargetPhoneAccountHandle == null || !mContext.getResources().getString(
+                    R.string.skip_incoming_caller_info_account_package).equalsIgnoreCase(
+                    mTargetPhoneAccountHandle.getComponentName().getPackageName())) {
+                startCallerInfoLookup();
+            } else {
+                Log.i(this, "skip incoming caller info lookup");
+            }
             for (Listener l : mListeners) {
                 l.onHandleChanged(this);
             }
@@ -1590,6 +1632,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 +2046,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 +2148,7 @@
                 userHandle = mTargetPhoneAccountHandle.getUserHandle();
             }
             if (userHandle != null) {
-                isWorkCall = UserUtil.isManagedProfile(mContext, userHandle);
+                isWorkCall = UserUtil.isManagedProfile(mContext, userHandle, mFlags);
             }
 
             isCallRecordingToneSupported = (phoneAccount.hasCapabilities(
@@ -2408,6 +2473,7 @@
 
     @VisibleForTesting
     public void setConnectionService(ConnectionServiceWrapper service) {
+        Log.i(this, "setConnectionService: service=[%s]", service);
         setConnectionService(service, null);
     }
 
@@ -2430,6 +2496,7 @@
         mConnectionService = service;
         mAnalytics.setCallConnectionService(service.getComponentName().flattenToShortString());
         mConnectionService.addCall(this);
+        processCachedCallbacks(service);
     }
 
     /**
@@ -3053,16 +3120,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 +3507,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 +3754,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 +3779,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 +4138,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 +4156,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 +4781,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 +4799,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 +4814,7 @@
      * if this is handled immediately.
      */
     public boolean isDisconnectHandledViaFuture() {
-        return mDisconnectFuture != null;
+        return mDiagnosticCompleteFuture != null;
     }
 
     /**
@@ -4737,13 +4822,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 +4868,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..fafd87f 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);
@@ -428,6 +438,10 @@
 
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
     public void onRingerModeChange() {
+        if (mFeatureFlags.ensureInCarRinging()) {
+            // Stop the current ringtone before attempting to start the new ringtone:
+            stopRinging();
+        }
         mCallAudioModeStateMachine.sendMessageWithArgs(
                 CallAudioModeStateMachine.RINGER_MODE_CHANGE, makeArgsForModeStateMachine());
     }
@@ -566,8 +580,25 @@
 
     @VisibleForTesting
     public void setCallAudioRouteFocusState(int focusState) {
-        mCallAudioRouteAdapter.sendMessageWithSessionInfo(
-                CallAudioRouteStateMachine.SWITCH_FOCUS, focusState);
+        if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+            mCallAudioRouteAdapter.sendMessageWithSessionInfo(
+                    CallAudioRouteStateMachine.SWITCH_FOCUS, focusState, 0);
+        } else {
+            mCallAudioRouteAdapter.sendMessageWithSessionInfo(
+                    CallAudioRouteStateMachine.SWITCH_FOCUS, focusState);
+        }
+    }
+
+    public void setCallAudioRouteFocusStateForEndTone() {
+        if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+            mCallAudioRouteAdapter.sendMessageWithSessionInfo(
+                    CallAudioRouteStateMachine.SWITCH_FOCUS,
+                    CallAudioRouteStateMachine.ACTIVE_FOCUS, 1);
+        } else {
+            mCallAudioRouteAdapter.sendMessageWithSessionInfo(
+                    CallAudioRouteStateMachine.SWITCH_FOCUS,
+                    CallAudioRouteStateMachine.ACTIVE_FOCUS);
+        }
     }
 
     public void notifyAudioOperationsComplete() {
@@ -750,14 +781,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 +948,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 +996,8 @@
                     mCallsManager.onDisconnectedTonePlaying(call, true);
                     mIsDisconnectedTonePlaying = true;
                 }
+            } else {
+                completeDisconnectToneFuture(call);
             }
         }
     }
@@ -1022,6 +1085,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 +1102,9 @@
     public SparseArray<LinkedHashSet<Call>> getCallStateToCalls() {
         return mCallStateToCalls;
     }
+
+    @VisibleForTesting
+    public CompletableFuture<Boolean> getCallRingingFuture() {
+        return mCallRingingFuture;
+    }
 }
diff --git a/src/com/android/server/telecom/CallAudioModeStateMachine.java b/src/com/android/server/telecom/CallAudioModeStateMachine.java
index 6420f2e..cd1d6a3 100644
--- a/src/com/android/server/telecom/CallAudioModeStateMachine.java
+++ b/src/com/android/server/telecom/CallAudioModeStateMachine.java
@@ -288,12 +288,14 @@
                             .getCurrentLocallyRequestedCommunicationDevice());
                 }
                 if (mFeatureFlags.setAudioModeBeforeAbandonFocus()) {
+                    Log.i(this, "enter: AudioManager#setMode(MODE_NORMAL)");
                     mAudioManager.setMode(AudioManager.MODE_NORMAL);
                     mCallAudioManager.setCallAudioRouteFocusState(
                             CallAudioRouteStateMachine.NO_FOCUS);
                 } else {
                     mCallAudioManager.setCallAudioRouteFocusState(
                             CallAudioRouteStateMachine.NO_FOCUS);
+                    Log.i(this, "enter: AudioManager#setMode(MODE_NORMAL)");
                     mAudioManager.setMode(AudioManager.MODE_NORMAL);
                 }
                 mLocalLog.log("Mode MODE_NORMAL");
@@ -347,11 +349,14 @@
                             + args.toString());
                     return HANDLED;
                 case AUDIO_OPERATIONS_COMPLETE:
-                    Log.i(LOG_TAG, "Abandoning audio focus: now UNFOCUSED");
                     if (mFeatureFlags.telecomResolveHiddenDependencies()) {
                         if (mCurrentAudioFocusRequest != null) {
+                            Log.i(this, "AudioOperationsComplete: "
+                                    + "AudioManager#abandonAudioFocusRequest(); now unfocused");
                             mAudioManager.abandonAudioFocusRequest(mCurrentAudioFocusRequest);
                             mCurrentAudioFocusRequest = null;
+                        } else {
+                            Log.i(this, "AudioOperationsComplete: already unfocused");
                         }
                     } else {
                         mAudioManager.abandonAudioFocusForCall();
@@ -377,6 +382,7 @@
             mLocalLog.log("Enter AUDIO_PROCESSING");
             if (mIsInitialized) {
                 mCallAudioManager.setCallAudioRouteFocusState(CallAudioRouteStateMachine.NO_FOCUS);
+                Log.i(this, "enter: AudioManager#setMode(MODE_AUDIO_PROCESSING)");
                 mAudioManager.setMode(NEW_AUDIO_MODE_FOR_AUDIO_PROCESSING);
                 mLocalLog.log("Mode MODE_CALL_SCREENING");
                 mMostRecentMode = NEW_AUDIO_MODE_FOR_AUDIO_PROCESSING;
@@ -431,7 +437,8 @@
                     transitionTo(mStreamingFocusState);
                     return HANDLED;
                 case AUDIO_OPERATIONS_COMPLETE:
-                    Log.i(LOG_TAG, "Abandoning audio focus: now AUDIO_PROCESSING");
+                    Log.i(LOG_TAG, "AudioManager#abandonAudioFocusRequest: now "
+                            + "AUDIO_PROCESSING");
                     if (mFeatureFlags.telecomResolveHiddenDependencies()) {
                         if (mCurrentAudioFocusRequest != null) {
                             mAudioManager.abandonAudioFocusRequest(mCurrentAudioFocusRequest);
@@ -466,6 +473,7 @@
                 if (mCallAudioManager.startRinging()) {
                     if (mFeatureFlags.telecomResolveHiddenDependencies()) {
                         mCurrentAudioFocusRequest = RING_AUDIO_FOCUS_REQUEST;
+                        Log.i(this, "tryStartRinging: AudioManager#requestAudioFocus(RING)");
                         mAudioManager.requestAudioFocus(RING_AUDIO_FOCUS_REQUEST);
                     } else {
                         mAudioManager.requestAudioFocusForCall(
@@ -474,6 +482,7 @@
                     // Do not set MODE_RINGTONE if we were previously in the CALL_SCREENING mode --
                     // this trips up the audio system.
                     if (mAudioManager.getMode() != AudioManager.MODE_CALL_SCREENING) {
+                        Log.i(this, "enter: AudioManager#setMode(MODE_RINGTONE)");
                         mAudioManager.setMode(AudioManager.MODE_RINGTONE);
                         mLocalLog.log("Mode MODE_RINGTONE");
                     }
@@ -569,11 +578,13 @@
             mLocalLog.log("Enter SIM_CALL");
             if (mFeatureFlags.telecomResolveHiddenDependencies()) {
                 mCurrentAudioFocusRequest = CALL_AUDIO_FOCUS_REQUEST;
+                Log.i(this, "enter: AudioManager#requestAudioFocus(CALL)");
                 mAudioManager.requestAudioFocus(CALL_AUDIO_FOCUS_REQUEST);
             } else {
                 mAudioManager.requestAudioFocusForCall(AudioManager.STREAM_VOICE_CALL,
                         AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
             }
+            Log.i(this, "enter: AudioManager#setMode(MODE_IN_CALL)");
             mAudioManager.setMode(AudioManager.MODE_IN_CALL);
             mLocalLog.log("Mode MODE_IN_CALL");
             mMostRecentMode = AudioManager.MODE_IN_CALL;
@@ -657,11 +668,13 @@
             mLocalLog.log("Enter VOIP_CALL");
             if (mFeatureFlags.telecomResolveHiddenDependencies()) {
                 mCurrentAudioFocusRequest = CALL_AUDIO_FOCUS_REQUEST;
+                Log.i(this, "enter: AudioManager#requestAudioFocus(CALL)");
                 mAudioManager.requestAudioFocus(CALL_AUDIO_FOCUS_REQUEST);
             } else {
                 mAudioManager.requestAudioFocusForCall(AudioManager.STREAM_VOICE_CALL,
                         AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
             }
+            Log.i(this, "enter: AudioManager#setMode(MODE_IN_COMMUNICATION)");
             mAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
             mLocalLog.log("Mode MODE_IN_COMMUNICATION");
             mMostRecentMode = AudioManager.MODE_IN_COMMUNICATION;
@@ -740,6 +753,7 @@
             Log.i(LOG_TAG, "Audio focus entering streaming state");
             mLocalLog.log("Enter Streaming");
             mLocalLog.log("Mode MODE_COMMUNICATION_REDIRECT");
+            Log.i(this, "enter: AudioManager#setMode(MODE_COMMUNICATION_REDIRECT");
             mAudioManager.setMode(AudioManager.MODE_COMMUNICATION_REDIRECT);
             mMostRecentMode = AudioManager.MODE_NORMAL;
             mCallAudioManager.setCallAudioRouteFocusState(CallAudioRouteStateMachine.ACTIVE_FOCUS);
@@ -817,14 +831,16 @@
             mLocalLog.log("Enter TONE/HOLDING");
             if (mFeatureFlags.telecomResolveHiddenDependencies()) {
                 mCurrentAudioFocusRequest = CALL_AUDIO_FOCUS_REQUEST;
+                Log.i(this, "enter: AudioManager#requestAudioFocus(CALL)");
                 mAudioManager.requestAudioFocus(CALL_AUDIO_FOCUS_REQUEST);
             } else {
                 mAudioManager.requestAudioFocusForCall(AudioManager.STREAM_VOICE_CALL,
                         AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
             }
+            Log.i(this, "enter: AudioManager#setMode(%d)", mMostRecentMode);
             mAudioManager.setMode(mMostRecentMode);
             mLocalLog.log("Mode " + mMostRecentMode);
-            mCallAudioManager.setCallAudioRouteFocusState(CallAudioRouteStateMachine.ACTIVE_FOCUS);
+            mCallAudioManager.setCallAudioRouteFocusStateForEndTone();
         }
 
         @Override
diff --git a/src/com/android/server/telecom/CallAudioRouteAdapter.java b/src/com/android/server/telecom/CallAudioRouteAdapter.java
index 5585d09..b23851d 100644
--- a/src/com/android/server/telecom/CallAudioRouteAdapter.java
+++ b/src/com/android/server/telecom/CallAudioRouteAdapter.java
@@ -128,11 +128,13 @@
     void sendMessageWithSessionInfo(int message);
     void sendMessageWithSessionInfo(int message, int arg);
     void sendMessageWithSessionInfo(int message, int arg, String data);
+    void sendMessageWithSessionInfo(int message, int arg, int data);
     void sendMessageWithSessionInfo(int message, int arg, BluetoothDevice bluetoothDevice);
     void sendMessage(int message, Runnable r);
     void setCallAudioManager(CallAudioManager callAudioManager);
     CallAudioState getCurrentCallAudioState();
     boolean isHfpDeviceAvailable();
     Handler getAdapterHandler();
+    PendingAudioRoute getPendingAudioRoute();
     void dump(IndentingPrintWriter pw);
 }
diff --git a/src/com/android/server/telecom/CallAudioRouteController.java b/src/com/android/server/telecom/CallAudioRouteController.java
index d38577d..903bfac 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;
@@ -62,6 +67,7 @@
     private static final Map<Integer, Integer> ROUTE_MAP;
     static {
         ROUTE_MAP = new ArrayMap<>();
+        ROUTE_MAP.put(TYPE_INVALID, 0);
         ROUTE_MAP.put(AudioRoute.TYPE_EARPIECE, CallAudioState.ROUTE_EARPIECE);
         ROUTE_MAP.put(AudioRoute.TYPE_WIRED, CallAudioState.ROUTE_WIRED_HEADSET);
         ROUTE_MAP.put(AudioRoute.TYPE_SPEAKER, CallAudioState.ROUTE_SPEAKER);
@@ -72,6 +78,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;
@@ -81,17 +90,26 @@
     private final Handler mHandler;
     private final WiredHeadsetManager mWiredHeadsetManager;
     private Set<AudioRoute> mAvailableRoutes;
+    private Set<AudioRoute> mCallSupportedRoutes;
     private AudioRoute mCurrentRoute;
     private AudioRoute mEarpieceWiredRoute;
     private AudioRoute mSpeakerDockRoute;
     private AudioRoute mStreamingRoute;
     private Set<AudioRoute> mStreamingRoutes;
     private Map<AudioRoute, BluetoothDevice> mBluetoothRoutes;
+    private Pair<Integer, String> mActiveBluetoothDevice;
+    private Map<Integer, String> mActiveDeviceCache;
+    private String mBluetoothAddressForRinging;
     private Map<Integer, AudioRoute> mTypeRoutes;
     private PendingAudioRoute mPendingAudioRoute;
     private AudioRoute.Factory mAudioRouteFactory;
+    private StatusBarNotifier mStatusBarNotifier;
+    private FeatureFlags mFeatureFlags;
     private int mFocusType;
+    private int mCallSupportedRouteMask = -1;
+    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 +120,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 +172,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 +185,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();
 
@@ -194,6 +217,7 @@
                     String address;
                     BluetoothDevice bluetoothDevice;
                     int focus;
+                    int handleEndTone;
                     @AudioRoute.AudioRouteType int type;
                     switch (msg.what) {
                         case CONNECT_WIRED_HEADSET:
@@ -246,8 +270,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,15 +306,25 @@
                             handleMuteChanged(false);
                             break;
                         case MUTE_EXTERNALLY_CHANGED:
-                            handleMuteChanged(mAudioManager.isMasterMute());
+                            handleMuteChanged(mAudioManager.isMicrophoneMute());
                             break;
                         case SWITCH_FOCUS:
                             focus = msg.arg1;
-                            handleSwitchFocus(focus);
+                            handleEndTone = (int) ((SomeArgs) msg.obj).arg2;
+                            handleSwitchFocus(focus, handleEndTone);
                             break;
                         case EXIT_PENDING_ROUTE:
                             handleExitPendingRoute();
                             break;
+                        case UPDATE_SYSTEM_AUDIO_ROUTE:
+                            // Based on the available routes for foreground call, adjust routing.
+                            updateRouteForForeground();
+                            // Force update to notify all ICS/CS.
+                            updateCallAudioState(new CallAudioState(mIsMute,
+                                    mCallAudioState.getRoute(),
+                                    mCallAudioState.getSupportedRouteMask(),
+                                    mCallAudioState.getActiveBluetoothDevice(),
+                                    mCallAudioState.getSupportedBluetoothDevices()));
                         default:
                             break;
                     }
@@ -296,16 +336,22 @@
     @Override
     public void initialize() {
         mAvailableRoutes = new HashSet<>();
-        mBluetoothRoutes = new ArrayMap<>();
+        mCallSupportedRoutes = new HashSet<>();
+        mBluetoothRoutes = new LinkedHashMap<>();
+        mActiveDeviceCache = new HashMap<>();
+        mActiveDeviceCache.put(AudioRoute.TYPE_BLUETOOTH_SCO, null);
+        mActiveDeviceCache.put(AudioRoute.TYPE_BLUETOOTH_HA, null);
+        mActiveDeviceCache.put(AudioRoute.TYPE_BLUETOOTH_LE, null);
+        mActiveBluetoothDevice = null;
         mTypeRoutes = new ArrayMap<>();
         mStreamingRoutes = new HashSet<>();
-        mPendingAudioRoute = new PendingAudioRoute(this, mAudioManager);
+        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) {
@@ -341,8 +387,10 @@
         // set current route
         if (mEarpieceWiredRoute != null) {
             mCurrentRoute = mEarpieceWiredRoute;
-        } else {
+        } else if (mSpeakerDockRoute != null) {
             mCurrentRoute = mSpeakerDockRoute;
+        } else {
+            mCurrentRoute = DUMMY_ROUTE;
         }
         mIsActive = false;
         mCallAudioState = new CallAudioState(mIsMute, ROUTE_MAP.get(mCurrentRoute.getType()),
@@ -368,6 +416,14 @@
     }
 
     @Override
+    public void sendMessageWithSessionInfo(int message, int arg, int data) {
+        SomeArgs args = SomeArgs.obtain();
+        args.arg1 = Log.createSubsession();
+        args.arg2 = data;
+        sendMessage(message, arg, 0, args);
+    }
+
+    @Override
     public void sendMessageWithSessionInfo(int message, int arg, BluetoothDevice bluetoothDevice) {
         SomeArgs args = SomeArgs.obtain();
         args.arg1 = Log.createSubsession();
@@ -391,7 +447,7 @@
 
     @Override
     public CallAudioState getCurrentCallAudioState() {
-        return null;
+        return mCallAudioState;
     }
 
     @Override
@@ -405,6 +461,11 @@
     }
 
     @Override
+    public PendingAudioRoute getPendingAudioRoute() {
+        return mPendingAudioRoute;
+    }
+
+    @Override
     public void dump(IndentingPrintWriter pw) {
     }
 
@@ -433,7 +494,9 @@
     }
 
     private void routeTo(boolean active, AudioRoute destRoute) {
-        if (!destRoute.equals(mStreamingRoute) && !getAvailableRoutes().contains(destRoute)) {
+        if (destRoute == null || (!destRoute.equals(mStreamingRoute)
+                && !getCallSupportedRoutes().contains(destRoute))) {
+            Log.i(this, "Ignore routing to unavailable route: %s", destRoute);
             return;
         }
         if (mIsPending) {
@@ -443,11 +506,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;
@@ -455,16 +520,17 @@
             Log.i(this, "Enter pending route, orig%s(active=%b), dest%s(active=%b)", mCurrentRoute,
                     mIsActive, destRoute, active);
             // route to pending route
-            if (getAvailableRoutes().contains(mCurrentRoute)) {
+            if (getCallSupportedRoutes().contains(mCurrentRoute)) {
                 mPendingAudioRoute.setOrigRoute(mIsActive, mCurrentRoute);
             } else {
                 // Avoid waiting for pending messages for an unavailable route
                 mPendingAudioRoute.setOrigRoute(mIsActive, DUMMY_ROUTE);
             }
-            mPendingAudioRoute.setDestRoute(active, destRoute);
-            mIsActive = active;
             mIsPending = true;
         }
+        mPendingAudioRoute.setDestRoute(active, destRoute, mBluetoothRoutes.get(destRoute),
+                mIsScoAudioConnected);
+        mIsActive = active;
         mPendingAudioRoute.evaluatePendingState();
         postTimeoutMessage();
     }
@@ -512,7 +578,7 @@
 
         // Route to expected state
         if (mCurrentRoute.equals(wiredHeadsetRoute)) {
-            routeTo(mIsActive, getBaseRoute(true));
+            routeTo(mIsActive, getBaseRoute(true, null));
         }
     }
 
@@ -551,7 +617,7 @@
 
         // Route to expected state
         if (mCurrentRoute.equals(dockRoute)) {
-            routeTo(mIsActive, getBaseRoute(true));
+            routeTo(mIsActive, getBaseRoute(true, null));
         }
     }
 
@@ -567,38 +633,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 +710,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 +731,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);
@@ -666,31 +793,57 @@
         onMuteStateChanged(mIsMute);
     }
 
-    private void handleSwitchFocus(int focus) {
+    private void handleSwitchFocus(int focus, int handleEndTone) {
+        Log.i(this, "handleSwitchFocus: focus (%s)", focus);
         mFocusType = focus;
         switch (focus) {
             case NO_FOCUS -> {
                 if (mIsActive) {
+                    // Notify the CallAudioModeStateMachine that audio operations are complete so
+                    // that we can relinquish audio focus.
+                    mCallAudioManager.notifyAudioOperationsComplete();
+
+                    // Reset mute state after call ends.
                     handleMuteChanged(false);
+                    // Route back to inactive route.
                     routeTo(false, mCurrentRoute);
+                    // Clear pending messages
+                    mPendingAudioRoute.clearPendingMessages();
+                    clearRingingBluetoothAddress();
                 }
             }
             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). Ignore route changes if we're handling playing
+                // the end tone. Otherwise, it's possible that we'll override the route a client has
+                // previously requested.
+                if (handleEndTone == 0) {
+                    // Cache BT device switch in the case that inband ringing is disabled and audio
+                    // was routed to a watch. When active focus is received, this selection will be
+                    // honored provided that the current route is associated.
+                    Log.i(this, "handleSwitchFocus (ACTIVE_FOCUS): mBluetoothAddressForRinging = "
+                        + "%s, mCurrentRoute = %s", mBluetoothAddressForRinging, mCurrentRoute);
+                    AudioRoute audioRoute = mBluetoothAddressForRinging != null
+                        && mBluetoothAddressForRinging.equals(mCurrentRoute.getBluetoothAddress())
+                        ? mCurrentRoute
+                        : getBaseRoute(true, null);
+                    routeTo(true, audioRoute);
+                    clearRingingBluetoothAddress();
                 }
             }
             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);
@@ -702,7 +855,7 @@
 
     public void handleSwitchEarpiece() {
         AudioRoute earpieceRoute = mTypeRoutes.get(AudioRoute.TYPE_EARPIECE);
-        if (earpieceRoute != null && getAvailableRoutes().contains(earpieceRoute)) {
+        if (earpieceRoute != null && getCallSupportedRoutes().contains(earpieceRoute)) {
             routeTo(mIsActive, earpieceRoute);
         } else {
             Log.i(this, "ignore switch earpiece request");
@@ -713,11 +866,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 : getCallSupportedRoutes()) {
+                if (Objects.equals(address, route.getBluetoothAddress())) {
+                    bluetoothRoute = route;
+                    bluetoothDevice = mBluetoothRoutes.get(route);
+                    break;
+                }
             }
         }
 
@@ -725,40 +883,59 @@
             if (mFocusType == RINGING_FOCUS) {
                 routeTo(mBluetoothRouteManager.isInbandRingEnabled(bluetoothDevice) && mIsActive,
                         bluetoothRoute);
+                mBluetoothAddressForRinging = bluetoothDevice.getAddress();
             } else {
                 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)) {
+        if (headsetRoute != null && getCallSupportedRoutes().contains(headsetRoute)) {
             routeTo(mIsActive, headsetRoute);
         } else {
-            Log.i(this, "ignore switch speaker request");
+            Log.i(this, "ignore switch headset request");
         }
     }
 
     private void handleSwitchSpeaker() {
-        if (mSpeakerDockRoute != null && getAvailableRoutes().contains(mSpeakerDockRoute)) {
+        if (mSpeakerDockRoute != null && getCallSupportedRoutes().contains(mSpeakerDockRoute)) {
             routeTo(mIsActive, mSpeakerDockRoute);
         } else {
             Log.i(this, "ignore switch speaker request");
         }
     }
 
-    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)) {
+            if (mSpeakerDockRoute != null && getCallSupportedRoutes()
+                    .contains(mSpeakerDockRoute)) {
                 routeTo(mIsActive, mSpeakerDockRoute);
                 // Since the route switching triggered by this message, we need to manually send it
                 // again so that we won't stuck in the pending route
@@ -771,9 +948,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 +963,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();
         }
     }
@@ -814,12 +999,27 @@
         synchronized (mLock) {
             int routeMask = 0;
             Set<BluetoothDevice> availableBluetoothDevices = new HashSet<>();
-            for (AudioRoute route : getAvailableRoutes()) {
+            for (AudioRoute route : getCallSupportedRoutes()) {
                 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,
                     mCallAudioState.getActiveBluetoothDevice(), availableBluetoothDevices));
         }
@@ -831,12 +1031,58 @@
                 mCallAudioState.getSupportedBluetoothDevices()));
     }
 
-    private void updateCallAudioState(CallAudioState callAudioState) {
-        Log.i(this, "updateCallAudioState: " + callAudioState);
-        CallAudioState oldState = mCallAudioState;
-        mCallAudioState = callAudioState;
-        mCallsManager.onCallAudioStateChanged(oldState, mCallAudioState);
-        updateAudioStateForTrackedCalls(mCallAudioState);
+    /**
+     * Retrieves the current call's supported audio route and adjusts the audio routing if the
+     * current route isn't supported.
+     */
+    private void updateRouteForForeground() {
+        boolean updatedRouteForCall = updateCallSupportedAudioRoutes();
+        // Ensure that current call audio state has updated routes for current call.
+        if (updatedRouteForCall) {
+            mCallAudioState = new CallAudioState(mIsMute, mCallAudioState.getRoute(),
+                    mCallSupportedRouteMask, mCallAudioState.getActiveBluetoothDevice(),
+                    mCallAudioState.getSupportedBluetoothDevices());
+            // Update audio route if foreground call doesn't support the current route.
+            if ((mCallSupportedRouteMask & mCallAudioState.getRoute()) == 0) {
+                routeTo(mIsActive, getBaseRoute(true, null));
+            }
+        }
+    }
+
+    /**
+     * Update supported audio routes for the foreground call if present.
+     */
+    private boolean updateCallSupportedAudioRoutes() {
+        int availableRouteMask = 0;
+        Call foregroundCall = mCallsManager.getForegroundCall();
+        if (foregroundCall != null) {
+            int foregroundCallSupportedRouteMask = foregroundCall.getSupportedAudioRoutes();
+            for (AudioRoute route : getAvailableRoutes()) {
+                int routeType = ROUTE_MAP.get(route.getType());
+                availableRouteMask |= routeType;
+                if ((routeType & foregroundCallSupportedRouteMask) == routeType) {
+                    mCallSupportedRoutes.add(route);
+                }
+            }
+            mCallSupportedRouteMask = availableRouteMask & foregroundCallSupportedRouteMask;
+            return true;
+        } else {
+            mCallSupportedRoutes.clear();
+            mCallSupportedRouteMask = -1;
+            return false;
+        }
+    }
+
+    private void updateCallAudioState(CallAudioState newCallAudioState) {
+        synchronized (mTelecomLock) {
+            Log.i(this, "updateCallAudioState: updating call audio state to %s", newCallAudioState);
+            CallAudioState oldState = mCallAudioState;
+            mCallAudioState = newCallAudioState;
+            // Update status bar notification
+            mStatusBarNotifier.notifyMute(newCallAudioState.isMuted());
+            mCallsManager.onCallAudioStateChanged(oldState, mCallAudioState);
+            updateAudioStateForTrackedCalls(mCallAudioState);
+        }
     }
 
     private void updateAudioStateForTrackedCalls(CallAudioState newCallAudioState) {
@@ -866,6 +1112,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 +1128,54 @@
         }
     }
 
-    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 ((!mCallSupportedRoutes.isEmpty() && (mCallSupportedRouteMask
+                & CallAudioState.ROUTE_BLUETOOTH) == 0) || mBluetoothRoutes.isEmpty()
+                || !includeBluetooth || activeWatchOrNonWatchDeviceRoute == null) {
+            Log.i(this, "getPreferredAudioRouteFromDefault: Audio routing defaulting to "
+                    + "available non-BT route.");
+            boolean callSupportsEarpieceWiredRoute = mCallSupportedRoutes.isEmpty()
+                    || mCallSupportedRoutes.contains(mEarpieceWiredRoute);
+            // If call supported route doesn't contain earpiece/wired/BT, it should have speaker
+            // enabled. Otherwise, no routes would be supported for the call which should never be
+            // the case.
+            AudioRoute defaultRoute = mEarpieceWiredRoute != null && callSupportsEarpieceWiredRoute
+                    ? 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()) {
@@ -917,12 +1202,20 @@
         }
     }
 
+    public Set<AudioRoute> getCallSupportedRoutes() {
+        if (mCurrentRoute.equals(mStreamingRoute)) {
+            return mStreamingRoutes;
+        } else {
+            return mCallSupportedRoutes.isEmpty() ? mAvailableRoutes : mCallSupportedRoutes;
+        }
+    }
+
     public AudioRoute getCurrentRoute() {
         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 +1224,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)) {
+        if (destRoute != null && !getCallSupportedRoutes().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 +1372,55 @@
         mAudioRouteFactory = audioRouteFactory;
     }
 
+    public Map<AudioRoute, BluetoothDevice> getBluetoothRoutes() {
+        return mBluetoothRoutes;
+    }
+
+    public void overrideIsPending(boolean isPending) {
+        mIsPending = isPending;
+    }
+
+    public void setIsScoAudioConnected(boolean value) {
+        mIsScoAudioConnected = value;
+    }
+
+    private void clearRingingBluetoothAddress() {
+        mBluetoothAddressForRinging = null;
+    }
+
+    /**
+     * 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..0a99903 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
@@ -1655,6 +1685,10 @@
         sendMessage(message, arg, 0, args);
     }
 
+    public void sendMessageWithSessionInfo(int message, int arg, int data) {
+        // ignore, only used in CallAudioRouteController
+    }
+
     public void sendMessageWithSessionInfo(int message, int arg, BluetoothDevice bluetoothDevice) {
         // ignore, only used in CallAudioRouteController
     }
@@ -2095,7 +2129,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 56c568f..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 {
@@ -461,8 +460,8 @@
         boolean okToLogEmergencyNumber = false;
         CarrierConfigManager configManager = (CarrierConfigManager) mContext.getSystemService(
                 Context.CARRIER_CONFIG_SERVICE);
-        PersistableBundle configBundle = configManager.getConfigForSubId(
-                mPhoneAccountRegistrar.getSubscriptionIdForPhoneAccount(accountHandle));
+        PersistableBundle configBundle = (configManager != null) ? configManager.getConfigForSubId(
+                mPhoneAccountRegistrar.getSubscriptionIdForPhoneAccount(accountHandle)) : null;
         if (configBundle != null) {
             okToLogEmergencyNumber = configBundle.getBoolean(
                     CarrierConfigManager.KEY_ALLOW_EMERGENCY_NUMBERS_IN_CALL_LOG_BOOL);
@@ -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 f4b7840..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;
 
@@ -498,8 +499,12 @@
                 @Override
                 public void releaseConnectionService(
                         ConnectionServiceFocusManager.ConnectionServiceFocus connectionService) {
+                    if (connectionService == null) {
+                        Log.i(this, "releaseConnectionService: connectionService is null");
+                        return;
+                    }
                     mCalls.stream()
-                            .filter(c -> c.getConnectionServiceWrapper().equals(connectionService))
+                            .filter(c -> connectionService.equals(c.getConnectionServiceWrapper()))
                             .forEach(c -> c.disconnect("release " +
                                     connectionService.getComponentName().getPackageName()));
                 }
@@ -556,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);
             }
         }
@@ -652,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(
@@ -675,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,
@@ -718,6 +726,9 @@
         mCallStreamingNotification = callStreamingNotification;
         mFeatureFlags = featureFlags;
         mTelephonyFeatureFlags = telephonyFlags;
+        mBlockedNumbersManager = mFeatureFlags.telecomMainlineBlockedNumbersManager()
+                ? mContext.getSystemService(BlockedNumbersManager.class)
+                : null;
 
         if (mFeatureFlags.useImprovedListenerOrder()) {
             mListeners.add(mInCallController);
@@ -749,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());
@@ -758,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<>();
 
@@ -851,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, "
@@ -863,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()
@@ -912,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,
@@ -947,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
@@ -1021,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(
@@ -1400,7 +1419,7 @@
         return mCallEndpointController;
     }
 
-    EmergencyCallHelper getEmergencyCallHelper() {
+    public EmergencyCallHelper getEmergencyCallHelper() {
         return mEmergencyCallHelper;
     }
 
@@ -1677,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
@@ -1902,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
@@ -2136,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(
@@ -2176,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);
+                                    }
                                 }
                             }
 
@@ -2311,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;
@@ -2432,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,
@@ -2644,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) {
@@ -2932,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(
@@ -3440,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;
@@ -3670,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;
     }
@@ -3768,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
@@ -3818,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
@@ -3948,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.
@@ -3981,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);
@@ -3998,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) {
@@ -4774,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
@@ -4975,6 +5108,7 @@
                 // change what an "active call" is so that the call in SELECT_PHONE_ACCOUNT state
                 // will be properly cancelled.
                 call.getTargetPhoneAccount() != null
+                        && phoneAccountHandle != null
                         && !phoneAccountHandle.getComponentName().equals(
                                 call.getTargetPhoneAccount().getComponentName())
                         && call.getParentCall() == null
@@ -5536,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());
+            }
         }
     }
 
@@ -5548,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);
         }
     }
@@ -5657,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 4011207..bf25f38 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;
@@ -66,7 +65,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;
@@ -90,7 +88,7 @@
  */
 @VisibleForTesting
 public class ConnectionServiceWrapper extends ServiceBinder implements
-        ConnectionServiceFocusManager.ConnectionServiceFocus {
+        ConnectionServiceFocusManager.ConnectionServiceFocus, CallSourceService {
 
     private static final String TELECOM_ABBREVIATION = "cast";
     private CompletableFuture<Pair<Integer, Location>> mQueryLocationFuture = null;
@@ -142,10 +140,17 @@
                 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));
+                    }
                     ConnectionServiceWrapper.this
                             .handleCreateConferenceComplete(callId, request, conference);
 
@@ -383,7 +388,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 +1419,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;
     }
@@ -1953,6 +1966,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 +1982,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 +1999,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")) {
@@ -2549,9 +2570,11 @@
             }
         }
 
-        // Bail early if the caller isn't the sim connection mgr.
-        if (!isCallerConnectionManager) {
-            Log.d(this, "queryRemoteConnectionServices: none; not sim call mgr.");
+        Log.i(this, "queryRemoteConnectionServices, simServices = %s", simServices);
+        // Bail early if the caller isn't the sim connection mgr or no sim connection service
+        // other than caller available.
+        if (!isCallerConnectionManager || simServices.isEmpty()) {
+            Log.d(this, "queryRemoteConnectionServices: not sim call mgr or no simservices.");
             noRemoteServices(callback);
             return;
         }
@@ -2559,8 +2582,6 @@
         final List<ComponentName> simServiceComponentNames = new ArrayList<>();
         final List<IBinder> simServiceBinders = new ArrayList<>();
 
-        Log.i(this, "queryRemoteConnectionServices, simServices = %s", simServices);
-
         for (ConnectionServiceWrapper simService : simServices) {
             final ConnectionServiceWrapper currentSimService = simService;
 
diff --git a/src/com/android/server/telecom/CreateConnectionProcessor.java b/src/com/android/server/telecom/CreateConnectionProcessor.java
index 7164819..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;
diff --git a/src/com/android/server/telecom/CreateConnectionTimeout.java b/src/com/android/server/telecom/CreateConnectionTimeout.java
index 7615d21..3046ca4 100644
--- a/src/com/android/server/telecom/CreateConnectionTimeout.java
+++ b/src/com/android/server/telecom/CreateConnectionTimeout.java
@@ -136,6 +136,9 @@
                 timeoutCallIfNeeded();
                 return;
             }
+            Log.i(
+                this,
+               "loggedRun, no PhoneAccount with voice calling capabilities, not timing out call");
         }
     }
 
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..6164638 100644
--- a/src/com/android/server/telecom/InCallController.java
+++ b/src/com/android/server/telecom/InCallController.java
@@ -103,7 +103,10 @@
             UUID.fromString("0c2adf96-353a-433c-afe9-1e5564f304f9");
     public static final String SET_IN_CALL_ADAPTER_ERROR_MSG =
             "Exception thrown while setting the in-call adapter.";
-
+    public static final UUID NULL_IN_CALL_SERVICE_BINDING_UUID =
+            UUID.fromString("7d58dedf-b71d-4c18-9d23-47b434bde58b");
+    public static final String NULL_IN_CALL_SERVICE_BINDING_ERROR_MSG =
+            "InCallController#sendCallToInCallService with null InCallService binding";
     @VisibleForTesting
     public void setAnomalyReporterAdapter(AnomalyReporterAdapter mAnomalyReporterAdapter){
         mAnomalyReporter = mAnomalyReporterAdapter;
@@ -361,7 +364,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 +373,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 +1084,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 +1114,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 +1156,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 +1215,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 +1266,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 +1292,8 @@
 
     private boolean mIsStartCallDelayScheduled = false;
 
+    private boolean mDisconnectedToneStartedPlaying = false;
+
     /**
      * A list of call IDs which are currently using the camera.
      */
@@ -1374,27 +1413,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 +1547,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 +2023,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 +2035,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 +2087,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 +2154,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 +2169,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 +2224,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 +2496,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 +2544,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 {
@@ -2520,6 +2590,10 @@
         try {
             inCallService.onCallAudioStateChanged(mCallsManager.getAudioState());
             inCallService.onCanAddCallChanged(mCallsManager.canAddCall());
+            if (mFeatureFlags.onCallEndpointChangedIcsOnConnected()) {
+                inCallService.onCallEndpointChanged(mCallsManager.getCallEndpointController()
+                        .getCurrentCallEndpoint());
+            }
         } catch (RemoteException ignored) {
         }
         // Don't complete the binding future for non-ui incalls
@@ -2531,7 +2605,8 @@
         return true;
     }
 
-    private int sendCallToService(Call call, InCallServiceInfo info,
+    @VisibleForTesting
+    public int sendCallToService(Call call, InCallServiceInfo info,
             IInCallService inCallService) {
         try {
             if ((call.isSelfManaged() && (!info.isSelfManagedCallsSupported()
@@ -2557,7 +2632,20 @@
                     includeRttCall,
                     info.getType() == IN_CALL_SERVICE_TYPE_SYSTEM_UI ||
                             info.getType() == IN_CALL_SERVICE_TYPE_NON_UI);
-            inCallService.addCall(sanitizeParcelableCallForService(info, parcelableCall));
+            if (mFeatureFlags.doNotSendCallToNullIcs()) {
+                if (inCallService != null) {
+                    inCallService.addCall(sanitizeParcelableCallForService(info, parcelableCall));
+                } else {
+                    Log.w(this, "call=[%s], was not sent to InCallService"
+                                    + " with info=[%s] due to a null InCallService binding",
+                            call, info);
+                    mAnomalyReporter.reportAnomaly(NULL_IN_CALL_SERVICE_BINDING_UUID,
+                            NULL_IN_CALL_SERVICE_BINDING_ERROR_MSG);
+                    return 0;
+                }
+            } else {
+                inCallService.addCall(sanitizeParcelableCallForService(info, parcelableCall));
+            }
             updateCallTracking(call, info, true /* isAdd */);
             return 1;
         } catch (RemoteException ignored) {
@@ -2651,12 +2739,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 +2762,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 +2793,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 +2828,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 +3149,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 +3260,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 +3276,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 +3301,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 +3338,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/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/Ringer.java b/src/com/android/server/telecom/Ringer.java
index e148ef5..1c26017 100644
--- a/src/com/android/server/telecom/Ringer.java
+++ b/src/com/android/server/telecom/Ringer.java
@@ -28,6 +28,7 @@
 import android.app.Person;
 import android.content.Context;
 import android.content.res.Resources;
+import android.media.AudioAttributes;
 import android.media.AudioManager;
 import android.media.Ringtone;
 import android.media.VolumeShaper;
@@ -51,9 +52,9 @@
 import com.android.server.telecom.LogUtils.EventTimer;
 import com.android.server.telecom.flags.FeatureFlags;
 
+import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
-import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.concurrent.CompletableFuture;
@@ -414,17 +415,9 @@
                         isVibratorEnabled, mIsHapticPlaybackSupportedByDevice);
             }
             // Defer ringtone creation to the async player thread.
-            Supplier<Pair<Uri, Ringtone>> ringtoneInfoSupplier;
+            Supplier<Pair<Uri, Ringtone>> ringtoneInfoSupplier = null;
             final boolean finalHapticChannelsMuted = hapticChannelsMuted;
-            if (isHapticOnly) {
-                if (hapticChannelsMuted) {
-                    Log.i(this,
-                            "want haptic only ringtone but haptics are muted, skip ringtone play");
-                    ringtoneInfoSupplier = null;
-                } else {
-                    ringtoneInfoSupplier = mRingtoneFactory::getHapticOnlyRingtone;
-                }
-            } else {
+            if (!isHapticOnly) {
                 ringtoneInfoSupplier = () -> mRingtoneFactory.getRingtone(
                         foregroundCall, mVolumeShaperConfig, finalHapticChannelsMuted);
             }
@@ -578,10 +571,6 @@
     }
 
     public void startCallWaiting(Call call, String reason) {
-        if (mSystemSettingsUtil.isTheaterModeOn(mContext)) {
-            return;
-        }
-
         if (mInCallController.doesConnectedDialerSupportRinging(
                 call.getAssociatedUser())) {
             Log.addEvent(call, LogUtils.Events.SKIP_RINGING, "Dialer handles");
@@ -704,7 +693,16 @@
 
         LogUtils.EventTimer timer = new EventTimer();
 
-        boolean isVolumeOverZero = mAudioManager.getStreamVolume(AudioManager.STREAM_RING) > 0;
+        boolean isVolumeOverZero;
+
+        if (mFlags.ensureInCarRinging()) {
+            AudioAttributes aa = new AudioAttributes.Builder()
+                    .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
+                    .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION).build();
+            isVolumeOverZero = mAudioManager.shouldNotificationSoundPlay(aa);
+        } else {
+            isVolumeOverZero = mAudioManager.getStreamVolume(AudioManager.STREAM_RING) > 0;
+        }
         timer.record("isVolumeOverZero");
         boolean shouldRingForContact = shouldRingForContact(call);
         timer.record("shouldRingForContact");
@@ -724,8 +722,6 @@
         boolean hasExternalRinger = hasExternalRinger(call);
         timer.record("hasExternalRinger");
         // Don't do call waiting operations or vibration unless these are false.
-        boolean isTheaterModeOn = mSystemSettingsUtil.isTheaterModeOn(mContext);
-        timer.record("isTheaterModeOn");
         boolean letDialerHandleRinging = mInCallController.doesConnectedDialerSupportRinging(
                 call.getAssociatedUser());
         timer.record("letDialerHandleRinging");
@@ -734,15 +730,24 @@
         timer.record("isWorkProfileInQuietMode");
 
         Log.i(this, "startRinging timings: " + timer);
-        boolean endEarly = isTheaterModeOn || letDialerHandleRinging || isSelfManaged ||
-                hasExternalRinger || isSilentRingingRequested || isWorkProfileInQuietMode;
+        boolean endEarly =
+                letDialerHandleRinging
+                        || isSelfManaged
+                        || hasExternalRinger
+                        || isSilentRingingRequested
+                        || isWorkProfileInQuietMode;
 
         if (endEarly) {
-            Log.i(this, "Ending early -- isTheaterModeOn=%s, letDialerHandleRinging=%s, " +
-                            "isSelfManaged=%s, hasExternalRinger=%s, silentRingingRequested=%s, " +
-                            "isWorkProfileInQuietMode=%s",
-                    isTheaterModeOn, letDialerHandleRinging, isSelfManaged, hasExternalRinger,
-                    isSilentRingingRequested, isWorkProfileInQuietMode);
+            Log.i(
+                    this,
+                    "Ending early -- letDialerHandleRinging=%s, isSelfManaged=%s, "
+                            + "hasExternalRinger=%s, silentRingingRequested=%s, "
+                            + "isWorkProfileInQuietMode=%s",
+                    letDialerHandleRinging,
+                    isSelfManaged,
+                    hasExternalRinger,
+                    isSilentRingingRequested,
+                    isWorkProfileInQuietMode);
         }
 
         // Acquire audio focus under any of the following conditions:
diff --git a/src/com/android/server/telecom/RingtoneFactory.java b/src/com/android/server/telecom/RingtoneFactory.java
index 0e0b99f..c740c24 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) {
@@ -119,41 +127,39 @@
             .build();
     }
 
-    /** Returns a ringtone to be used when ringer is not audible for the incoming call. */
-    @Nullable
-    public Pair<Uri, Ringtone> getHapticOnlyRingtone() {
-        // Initializing ringtones on the main thread can deadlock
-        ThreadUtil.checkNotOnMainThread();
-        Uri ringtoneUri = Uri.parse("file://" + mContext.getString(
-                com.android.internal.R.string.config_defaultRingtoneVibrationSound));
-        AudioAttributes audioAttrs = getDefaultRingtoneAudioAttributes(
-            /* hapticChannelsMuted */ false);
-        Ringtone ringtone = RingtoneManager.getRingtone(
-                mContext, ringtoneUri, /* volumeShaperConfig */ null, audioAttrs);
-        if (ringtone != null) {
-            // Make sure the sound is muted.
-            ringtone.setVolume(0);
-        }
-        return new Pair(ringtoneUri, ringtone);
-    }
-
     private Context getWorkProfileContextForUser(UserHandle userHandle) {
-        // UserManager.getEnabledProfiles returns the enabled profiles along with the user's handle
-        // itself (so we must filter out the user).
-        List<UserInfo> profiles = UserManager.get(mContext).getEnabledProfiles(
-                userHandle.getIdentifier());
-        UserInfo workprofile = null;
+        // UserManager.getUserProfiles returns the enabled profiles along with the context user's
+        // handle itself (so we must filter out the user).
+        Context userContext = mContext.createContextAsUser(userHandle, 0);
+        UserManager um = mFeatureFlags.telecomResolveHiddenDependencies()
+                ? userContext.getSystemService(UserManager.class)
+                : mContext.getSystemService(UserManager.class);
+        List<UserHandle> profiles = um.getUserProfiles();
+        List<UserInfo> userInfoProfiles = um.getEnabledProfiles(userHandle.getIdentifier());
+        UserHandle workProfileUser = null;
         int managedProfileCount = 0;
-        for (UserInfo profile : profiles) {
-            UserHandle profileUserHandle = profile.getUserHandle();
-            if (profileUserHandle != userHandle && profile.isManagedProfile()) {
-                managedProfileCount++;
-                workprofile = profile;
+
+        if (mFeatureFlags.telecomResolveHiddenDependencies()) {
+            for (UserHandle profileUser : profiles) {
+                UserManager userManager = mContext.createContextAsUser(profileUser, 0)
+                        .getSystemService(UserManager.class);
+                if (!userHandle.equals(profileUser) && userManager.isManagedProfile()) {
+                    managedProfileCount++;
+                    workProfileUser = profileUser;
+                }
+            }
+        } else {
+            for(UserInfo profile: userInfoProfiles) {
+                UserHandle profileUserHandle = profile.getUserHandle();
+                if (!profileUserHandle.equals(userHandle) && profile.isManagedProfile()) {
+                    managedProfileCount++;
+                    workProfileUser = profileUserHandle;
+                }
             }
         }
         // There may be many different types of profiles, so only count Managed (Work) Profiles.
         if(managedProfileCount == 1) {
-            return getContextForUserHandle(workprofile.getUserHandle());
+            return getContextForUserHandle(workProfileUser);
         }
         // There are multiple managed profiles for the associated user and we do not have enough
         // info to determine which profile is the work profile. Just use the default.
diff --git a/src/com/android/server/telecom/RoleManagerAdapter.java b/src/com/android/server/telecom/RoleManagerAdapter.java
index 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/SystemSettingsUtil.java b/src/com/android/server/telecom/SystemSettingsUtil.java
index cdd14df..d846cce 100644
--- a/src/com/android/server/telecom/SystemSettingsUtil.java
+++ b/src/com/android/server/telecom/SystemSettingsUtil.java
@@ -35,11 +35,6 @@
     private static final String RAMPING_RINGER_AUDIO_COUPLED_VIBRATION_ENABLED =
             "ramping_ringer_audio_coupled_vibration_enabled";
 
-    public boolean isTheaterModeOn(Context context) {
-        return Settings.Global.getInt(context.getContentResolver(), Settings.Global.THEATER_MODE_ON,
-                0) == 1;
-    }
-
     public boolean isRingVibrationEnabled(Context context) {
         // VIBRATE_WHEN_RINGING setting was deprecated, only RING_VIBRATION_INTENSITY controls the
         // ringtone vibrations on/off state now. Ramping ringer should only be applied when ring
diff --git a/src/com/android/server/telecom/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java
index f3d91f1..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 {
@@ -1833,11 +1860,12 @@
                     throw new SecurityException("Package " + callingPackage + " is not allowed"
                             + " to start conference call");
                 }
-
+                // Binder is clearing the identity, so we need to keep the store the handle
+                UserHandle currentUserHandle = Binder.getCallingUserHandle();
                 long token = Binder.clearCallingIdentity();
                 try {
                     mCallsManager.startConference(participants, extras, callingPackage,
-                            Binder.getCallingUserHandle());
+                            currentUserHandle);
                 } finally {
                     Binder.restoreCallingIdentity(token);
                 }
@@ -2003,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);
                     }
@@ -2092,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.
          */
@@ -2101,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]");
@@ -2288,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
@@ -2308,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) {
@@ -2327,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}
@@ -2563,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);
                     }
@@ -2667,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,
@@ -2718,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/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..0f27dad 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,
@@ -353,7 +430,8 @@
                 return mBluetoothHeadset;
             } catch (TimeoutException | InterruptedException | ExecutionException e) {
                 // ignore
-                Log.w(this, "Acquire BluetoothHeadset service failed due to: " + e);
+                Log.w(this, "getBluetoothHeadset: Acquire BluetoothHeadset service failed due to: "
+                        + e);
                 return null;
             }
         } else {
@@ -406,7 +484,7 @@
             LinkedHashMap<String, BluetoothDevice> targetDeviceMap;
             if (deviceType == DEVICE_TYPE_LE_AUDIO) {
                 if (mBluetoothLeAudioService == null) {
-                    Log.w(this, "LE audio service null when receiving device added broadcast");
+                    Log.w(this, "onDeviceConnected: LE audio service null");
                     return;
                 }
                 /* Check if group is known. */
@@ -420,7 +498,7 @@
                 targetDeviceMap = mLeAudioDevicesByAddress;
             } else if (deviceType == DEVICE_TYPE_HEARING_AID) {
                 if (mBluetoothHearingAid == null) {
-                    Log.w(this, "Hearing aid service null when receiving device added broadcast");
+                    Log.w(this, "onDeviceConnected: Hearing aid service null");
                     return;
                 }
                 long hiSyncId = mBluetoothHearingAid.getHiSyncId(device);
@@ -428,18 +506,18 @@
                 targetDeviceMap = mHearingAidDevicesByAddress;
             } else if (deviceType == DEVICE_TYPE_HEADSET) {
                 if (getBluetoothHeadset() == null) {
-                    Log.w(this, "Headset service null when receiving device added broadcast");
+                    Log.w(this, "onDeviceConnected: Headset service null");
                     return;
                 }
                 targetDeviceMap = mHfpDevicesByAddress;
             } else {
-                Log.w(this, "Device: " + device.getAddress() + " with invalid type: "
-                            + getDeviceTypeString(deviceType));
+                Log.w(this, "onDeviceConnected: Device: %s; invalid type %s", device.getAddress(),
+                        getDeviceTypeString(deviceType));
                 return;
             }
             if (!targetDeviceMap.containsKey(device.getAddress())) {
-                Log.i(this, "Adding device with address: " + device + " and devicetype="
-                        + getDeviceTypeString(deviceType));
+                Log.i(this, "onDeviceConnected: Adding device with address: %s and devicetype=%s",
+                        device, getDeviceTypeString(deviceType));
                 targetDeviceMap.put(device.getAddress(), device);
                 mBluetoothRouteManager.onDeviceAdded(device.getAddress());
             }
@@ -465,13 +543,13 @@
             } else if (deviceType == DEVICE_TYPE_HEADSET) {
                 targetDeviceMap = mHfpDevicesByAddress;
             } else {
-                Log.w(this, "Device: " + device.getAddress() + " with invalid type: "
-                            + getDeviceTypeString(deviceType));
+                Log.w(this, "onDeviceDisconnected: Device: %s with invalid type: %s",
+                        device.getAddress(), getDeviceTypeString(deviceType));
                 return;
             }
             if (targetDeviceMap.containsKey(device.getAddress())) {
-                Log.i(this, "Removing device with address: " + device + " and devicetype="
-                        + getDeviceTypeString(deviceType));
+                Log.i(this, "onDeviceDisconnected: Removing device with address: %s, devicetype=%s",
+                        device, getDeviceTypeString(deviceType));
                 targetDeviceMap.remove(device.getAddress());
                 mBluetoothRouteManager.onDeviceLost(device.getAddress());
             }
@@ -489,12 +567,15 @@
         }
     }
 
-    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.");
+            Log.w(this, "disconnectSco: Trying to disconnect audio but no headset service exists.");
         } else {
-            mBluetoothHeadset.disconnectAudio();
+            result = mBluetoothHeadset.disconnectAudio();
+            Log.i(this, "disconnectSco: BluetoothHeadset#disconnectAudio()=%b", result);
         }
+        return result;
     }
 
     public boolean isLeAudioCommunicationDevice() {
@@ -526,6 +607,7 @@
         if (audioDeviceInfo != null && audioDeviceInfo.getType()
                 == AudioDeviceInfo.TYPE_BLE_HEADSET) {
             mBluetoothRouteManager.onAudioLost(audioDeviceInfo.getAddress());
+            Log.i(this, "clearLeAudioCommunicationDevice: audioManager#clearCommunicationDevice");
             mAudioManager.clearCommunicationDevice();
         }
     }
@@ -550,32 +632,33 @@
         AudioDeviceInfo audioDeviceInfo = mAudioManager.getCommunicationDevice();
         if (audioDeviceInfo != null && audioDeviceInfo.getType()
                 == AudioDeviceInfo.TYPE_HEARING_AID) {
+            Log.i(this, "clearHearingAidCommunicationDevice: "
+                    + "audioManager#clearCommunicationDevice");
             mAudioManager.clearCommunicationDevice();
         }
     }
 
     public boolean setLeAudioCommunicationDevice() {
-        Log.i(this, "setLeAudioCommunicationDevice");
-
         if (mLeAudioSetAsCommunicationDevice) {
-            Log.i(this, "setLeAudioCommunicationDevice already set");
+            Log.i(this, "setLeAudioCommunicationDevice: already set");
             return true;
         }
 
         if (mAudioManager == null) {
-            Log.w(this, " mAudioManager is null");
+            Log.w(this, "setLeAudioCommunicationDevice: mAudioManager is null");
             return false;
         }
 
         AudioDeviceInfo bleHeadset = null;
         List<AudioDeviceInfo> devices = mAudioManager.getAvailableCommunicationDevices();
         if (devices.size() == 0) {
-            Log.w(this, " No communication devices available.");
+            Log.w(this, "setLeAudioCommunicationDevice: No communication devices available.");
             return false;
         }
 
         for (AudioDeviceInfo device : devices) {
-            Log.i(this, " Available device type:  " + device.getType());
+            Log.d(this, "setLeAudioCommunicationDevice: Available device type:  "
+                    + device.getType());
             if (device.getType() == AudioDeviceInfo.TYPE_BLE_HEADSET) {
                 bleHeadset = device;
                 break;
@@ -583,7 +666,7 @@
         }
 
         if (bleHeadset == null) {
-            Log.w(this, " No bleHeadset device available");
+            Log.w(this, "setLeAudioCommunicationDevice: No bleHeadset device available");
             return false;
         }
 
@@ -593,9 +676,11 @@
         // Turn BLE_OUT_HEADSET ON.
         boolean result = mAudioManager.setCommunicationDevice(bleHeadset);
         if (!result) {
-            Log.w(this, " Could not set bleHeadset device");
+            Log.w(this, "setLeAudioCommunicationDevice: AudioManager#setCommunicationDevice(%s)=%b;"
+                    + " Could not set bleHeadset device", bleHeadset, result);
         } else {
-            Log.i(this, " bleHeadset device set");
+            Log.i(this, "setLeAudioCommunicationDevice: "
+                    + "AudioManager#setCommunicationDevice(%s)=%b", bleHeadset, result);
             mBluetoothRouteManager.onAudioOn(bleHeadset.getAddress());
             mLeAudioSetAsCommunicationDevice = true;
             mLeAudioDevice = bleHeadset.getAddress();
@@ -604,27 +689,26 @@
     }
 
     public boolean setHearingAidCommunicationDevice() {
-        Log.i(this, "setHearingAidCommunicationDevice");
-
         if (mHearingAidSetAsCommunicationDevice) {
-            Log.i(this, "mHearingAidSetAsCommunicationDevice already set");
+            Log.i(this, "setHearingAidCommunicationDevice: already set");
             return true;
         }
 
         if (mAudioManager == null) {
-            Log.w(this, " mAudioManager is null");
+            Log.w(this, "setHearingAidCommunicationDevice: mAudioManager is null");
             return false;
         }
 
         AudioDeviceInfo hearingAid = null;
         List<AudioDeviceInfo> devices = mAudioManager.getAvailableCommunicationDevices();
         if (devices.size() == 0) {
-            Log.w(this, " No communication devices available.");
+            Log.w(this, "setHearingAidCommunicationDevice: No communication devices available.");
             return false;
         }
 
         for (AudioDeviceInfo device : devices) {
-            Log.i(this, " Available device type:  " + device.getType());
+            Log.d(this, "setHearingAidCommunicationDevice: Available device type: "
+                    + device.getType());
             if (device.getType() == AudioDeviceInfo.TYPE_HEARING_AID) {
                 hearingAid = device;
                 break;
@@ -632,7 +716,7 @@
         }
 
         if (hearingAid == null) {
-            Log.w(this, " No hearingAid device available");
+            Log.w(this, "setHearingAidCommunicationDevice: No hearingAid device available");
             return false;
         }
 
@@ -642,57 +726,93 @@
         // Turn hearing aid ON.
         boolean result = mAudioManager.setCommunicationDevice(hearingAid);
         if (!result) {
-            Log.w(this, " Could not set hearingAid device");
+            Log.w(this, "setHearingAidCommunicationDevice: "
+                    + "AudioManager#setCommunicationDevice(%s)=%b; Could not set HA device",
+                    hearingAid, result);
         } else {
-            Log.i(this, " hearingAid device set");
+            Log.i(this, "setHearingAidCommunicationDevice: "
+                            + "AudioManager#setCommunicationDevice(%s)=%b", hearingAid, result);
             mHearingAidDevice = hearingAid.getAddress();
             mHearingAidSetAsCommunicationDevice = true;
         }
         return result;
     }
 
+    public boolean setCommunicationDeviceForAddress(String address) {
+        AudioDeviceInfo deviceInfo = null;
+        List<AudioDeviceInfo> devices = mAudioManager.getAvailableCommunicationDevices();
+        if (devices.size() == 0) {
+            Log.w(this, "setCommunicationDeviceForAddress: No communication devices available.");
+            return false;
+        }
+
+        for (AudioDeviceInfo device : devices) {
+            Log.d(this, "setCommunicationDeviceForAddress: Available device type: "
+                    + device.getType());
+            if (device.getAddress().equals(address)) {
+                deviceInfo = device;
+                break;
+            }
+        }
+
+        if (deviceInfo == null) {
+            Log.w(this, "setCommunicationDeviceForAddress: Device %s not found.", address);
+            return false;
+        }
+        if (mAudioManager.getCommunicationDevice().equals(deviceInfo)) {
+            Log.i(this, "setCommunicationDeviceForAddress: Device %s already active.", address);
+            return true;
+        }
+        boolean success = mAudioManager.setCommunicationDevice(deviceInfo);
+        Log.i(this, "setCommunicationDeviceForAddress: "
+                + "AudioManager#setCommunicationDevice(%s)=%b", deviceInfo, success);
+        return success;
+    }
+
     // 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) {
         int callProfile = BluetoothProfile.LE_AUDIO;
-        Log.i(this, "Telecomm connecting audio to device: " + address);
         BluetoothDevice device = null;
         if (mLeAudioDevicesByAddress.containsKey(address)) {
-            Log.i(this, "Telecomm found LE Audio device for address: " + address);
+            Log.i(this, "connectAudio: found LE Audio device for address: %s", address);
             if (mBluetoothLeAudioService == null) {
-                Log.w(this, "Attempting to turn on audio when the le audio service is null");
+                Log.w(this, "connectAudio: Attempting to turn on audio when the le audio service "
+                        + "is null");
                 return false;
             }
             device = mLeAudioDevicesByAddress.get(address);
             callProfile = BluetoothProfile.LE_AUDIO;
         } else if (mHearingAidDevicesByAddress.containsKey(address)) {
-            Log.i(this, "Telecomm found hearing aid device for address: " + address);
             if (mBluetoothHearingAid == null) {
-                Log.w(this, "Attempting to turn on audio when the hearing aid service is null");
+                Log.w(this, "connectAudio: Attempting to turn on audio when the hearing aid "
+                        + "service is null");
                 return false;
             }
+            Log.i(this, "connectAudio: found hearing aid device for address: %s", address);
             device = mHearingAidDevicesByAddress.get(address);
             callProfile = BluetoothProfile.HEARING_AID;
         } else if (mHfpDevicesByAddress.containsKey(address)) {
-            Log.i(this, "Telecomm found HFP device for address: " + address);
             if (getBluetoothHeadset() == null) {
-                Log.w(this, "Attempting to turn on audio when the headset service is null");
+                Log.w(this, "connectAudio: Attempting to turn on audio when the headset service "
+                        + "is null");
                 return false;
             }
+            Log.i(this, "connectAudio: found HFP device for address: %s", address);
             device = mHfpDevicesByAddress.get(address);
             callProfile = BluetoothProfile.HEADSET;
         }
 
         if (device == null) {
-            Log.w(this, "No active profiles for Bluetooth address=" + address);
+            Log.w(this, "No active profiles for Bluetooth address: %s", address);
             return false;
         }
 
         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));
+            Log.i(this, "connectAudio: Preferred duplex profile for device=% is %d", address,
+                preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX));
             callProfile = preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX);
         }
 
@@ -729,20 +849,75 @@
             boolean success = mBluetoothAdapter.setActiveDevice(device,
                 BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL);
             if (!success) {
-                Log.w(this, "Couldn't set active device to %s", address);
+                Log.w(this, "connectAudio: Couldn't set active device to %s", address);
                 return false;
             }
+            Log.i(this, "connectAudio: BluetoothAdapter#setActiveDevice(%s)", address);
             if (getBluetoothHeadset() != null) {
                 int scoConnectionRequest = mBluetoothHeadset.connectAudio();
+                Log.i(this, "connectAudio: BluetoothHeadset#connectAudio()=%d",
+                        scoConnectionRequest);
                 return scoConnectionRequest == BluetoothStatusCodes.SUCCESS ||
                         scoConnectionRequest
                                 == BluetoothStatusCodes.ERROR_AUDIO_DEVICE_ALREADY_CONNECTED;
             } else {
-                Log.w(this, "Couldn't find bluetooth headset service");
+                Log.w(this, "connectAudio: Couldn't find bluetooth headset service");
                 return false;
             }
         } else {
-            Log.w(this, "Attempting to turn on audio for a disconnected device");
+            Log.w(this, "connectAudio: Attempting to turn on audio for disconnected device %s",
+                    address);
+            return false;
+        }
+    }
+
+    /**
+     * Used by CallAudioRouteController in order to connect the BT device.
+     * @param device {@link BluetoothDevice} to connect to.
+     * @param type {@link AudioRoute.AudioRouteType} associated with the device.
+     * @return {@code true} if device was successfully connected, {@code false} otherwise.
+     */
+    public boolean connectAudio(BluetoothDevice device, @AudioRoute.AudioRouteType int type) {
+        String address = device.getAddress();
+        int callProfile = BluetoothProfile.LE_AUDIO;
+        if (type == TYPE_BLUETOOTH_SCO) {
+            callProfile = BluetoothProfile.HEADSET;
+        } else if (type == TYPE_BLUETOOTH_HA) {
+            callProfile = BluetoothProfile.HEARING_AID;
+        }
+
+        Bundle preferredAudioProfiles = mBluetoothAdapter.getPreferredAudioProfiles(device);
+        if (preferredAudioProfiles != null && !preferredAudioProfiles.isEmpty()
+                && preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX) != 0) {
+            Log.i(this, "connectAudio: Preferred duplex profile for device=%s is %d", address,
+                    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, "connectAudio: Couldn't set active device to %s", address);
+                return false;
+            }
+            if (getBluetoothHeadset() != null) {
+                int scoConnectionRequest = mBluetoothHeadset.connectAudio();
+                Log.i(this, "connectaudio: BluetoothHeadset#connectAudio()=%d",
+                        scoConnectionRequest);
+                return scoConnectionRequest == BluetoothStatusCodes.SUCCESS ||
+                        scoConnectionRequest
+                                == BluetoothStatusCodes.ERROR_AUDIO_DEVICE_ALREADY_CONNECTED;
+            } else {
+                Log.w(this, "connectAudio: Couldn't find bluetooth headset service");
+                return false;
+            }
+        } else {
+            Log.w(this, "connectAudio: Attempting to turn on audio for a disconnected device %s",
+                    address);
             return false;
         }
     }
@@ -762,6 +937,8 @@
         if (mBluetoothHearingAidActiveDeviceCache != null) {
             mBluetoothAdapter.setActiveDevice(mBluetoothHearingAidActiveDeviceCache,
                     BluetoothAdapter.ACTIVE_DEVICE_ALL);
+            Log.i(this, "restoreHearingAidDevice: BluetoothAdapter#setActiveDevice(%s)",
+                    mBluetoothHearingAidActiveDeviceCache.getAddress());
             mBluetoothHearingAidActiveDeviceCache = null;
         }
     }
@@ -774,7 +951,6 @@
     }
 
     public boolean isInbandRingEnabled(BluetoothDevice bluetoothDevice) {
-        Log.i(this, "isInbandRingEnabled: device: " + bluetoothDevice);
         if (mBluetoothRouteManager.isCachedLeAudioDevice(bluetoothDevice)) {
             if (mBluetoothLeAudioService == null) {
                 Log.i(this, "isInbandRingingEnabled: no leaudio service available.");
@@ -787,10 +963,17 @@
                 Log.i(this, "isInbandRingingEnabled: no headset service available.");
                 return false;
             }
-            return mBluetoothHeadset.isInbandRingingEnabled();
+            boolean isEnabled = mBluetoothHeadset.isInbandRingingEnabled();
+            Log.i(this, "isInbandRingEnabled: device: %s, isEnabled: %b", bluetoothDevice,
+                    isEnabled);
+            return isEnabled;
         }
     }
 
+    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..b4c3d8d 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");
@@ -128,7 +149,10 @@
                 }
                 break;
             case BluetoothHeadset.STATE_AUDIO_DISCONNECTED:
-                if (Flags.useRefactoredAudioRouteSwitching()) {
+                if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+                    CallAudioRouteController audioRouteController =
+                            (CallAudioRouteController) mCallAudioRouteAdapter;
+                    audioRouteController.setIsScoAudioConnected(false);
                     mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_AUDIO_DISCONNECTED, 0,
                             device);
                 }  else {
@@ -171,7 +195,7 @@
                 device.getAddress(), bluetoothHeadsetState);
 
         if (bluetoothHeadsetState == BluetoothProfile.STATE_CONNECTED) {
-            if (Flags.useRefactoredAudioRouteSwitching()) {
+            if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
                 mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_DEVICE_ADDED,
                         audioRouteType, device);
             } else {
@@ -179,7 +203,7 @@
             }
         } else if (bluetoothHeadsetState == BluetoothProfile.STATE_DISCONNECTED
                 || bluetoothHeadsetState == BluetoothProfile.STATE_DISCONNECTING) {
-            if (Flags.useRefactoredAudioRouteSwitching()) {
+            if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
                 mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_DEVICE_REMOVED,
                         audioRouteType, device);
             } else {
@@ -211,13 +235,37 @@
         Log.i(LOG_TAG, "Device %s is now the preferred BT device for %s", device,
                 BluetoothDeviceManager.getDeviceTypeString(deviceType));
 
-        if (Flags.useRefactoredAudioRouteSwitching()) {
+        if (mFeatureFlags.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/NotificationChannelManager.java b/src/com/android/server/telecom/ui/NotificationChannelManager.java
index b3cb2c3..987b6b3 100644
--- a/src/com/android/server/telecom/ui/NotificationChannelManager.java
+++ b/src/com/android/server/telecom/ui/NotificationChannelManager.java
@@ -83,61 +83,62 @@
         boolean vibration = false;
         Uri sound = silentRingtone;
         switch (channelId) {
-            case CHANNEL_ID_INCOMING_CALLS:
+            case CHANNEL_ID_INCOMING_CALLS -> {
                 name = context.getText(R.string.notification_channel_incoming_call);
                 importance = NotificationManager.IMPORTANCE_MAX;
                 canShowBadge = false;
                 lights = true;
                 vibration = false;
                 sound = silentRingtone;
-                break;
-            case CHANNEL_ID_MISSED_CALLS:
+            }
+            case CHANNEL_ID_MISSED_CALLS -> {
                 name = context.getText(R.string.notification_channel_missed_call);
                 importance = NotificationManager.IMPORTANCE_DEFAULT;
                 canShowBadge = true;
                 lights = true;
                 vibration = true;
                 sound = silentRingtone;
-                break;
-            case CHANNEL_ID_CALL_BLOCKING:
+            }
+            case CHANNEL_ID_CALL_BLOCKING -> {
                 name = context.getText(R.string.notification_channel_call_blocking);
                 importance = NotificationManager.IMPORTANCE_LOW;
                 canShowBadge = false;
                 lights = false;
                 vibration = false;
                 sound = null;
-                break;
-            case CHANNEL_ID_AUDIO_PROCESSING:
+            }
+            case CHANNEL_ID_AUDIO_PROCESSING -> {
                 name = context.getText(R.string.notification_channel_background_calls);
                 importance = NotificationManager.IMPORTANCE_LOW;
                 canShowBadge = false;
                 lights = false;
                 vibration = false;
                 sound = null;
-                break;
-            case CHANNEL_ID_DISCONNECTED_CALLS:
+            }
+            case CHANNEL_ID_DISCONNECTED_CALLS -> {
                 name = context.getText(R.string.notification_channel_disconnected_calls);
                 importance = NotificationManager.IMPORTANCE_DEFAULT;
                 canShowBadge = true;
                 lights = true;
                 vibration = true;
                 sound = silentRingtone;
-                break;
-            case CHANNEL_ID_IN_CALL_SERVICE_CRASH:
+            }
+            case CHANNEL_ID_IN_CALL_SERVICE_CRASH -> {
                 name = context.getText(R.string.notification_channel_in_call_service_crash);
                 importance = NotificationManager.IMPORTANCE_DEFAULT;
                 canShowBadge = true;
                 lights = true;
                 vibration = true;
                 sound = null;
-            case CHANNEL_ID_CALL_STREAMING:
+            }
+            case CHANNEL_ID_CALL_STREAMING -> {
                 name = context.getText(R.string.notification_channel_call_streaming);
                 importance = NotificationManager.IMPORTANCE_DEFAULT;
                 canShowBadge = false;
                 lights = false;
                 vibration = false;
                 sound = null;
-                break;
+            }
         }
 
         NotificationChannel channel = new NotificationChannel(channelId, name, importance);
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/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index 1c27b14..04dcef8 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -45,10 +45,13 @@
     <!-- Used to access PlatformCompat APIs -->
     <uses-permission android:name="android.permission.READ_COMPAT_CHANGE_CONFIG" />
     <uses-permission android:name="android.permission.LOG_COMPAT_CHANGE" />
-    
+
     <!-- Used to register NotificationListenerService -->
     <uses-permission android:name="android.permission.STATUS_BAR_SERVICE" />
 
+    <!-- Used to query the audio framework to determine if a notification sound should play. -->
+    <uses-permission android:name="android.permission.QUERY_AUDIO_STATE"/>
+
     <application android:label="@string/app_name"
                  android:debuggable="true">
         <uses-library android:name="android.test.runner" />
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..ac4a94e 100644
--- a/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java
@@ -48,6 +48,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
+import com.android.server.telecom.CallAudioRouteAdapter;
 import com.android.server.telecom.bluetooth.BluetoothDeviceManager;
 import com.android.server.telecom.bluetooth.BluetoothRouteManager;
 import com.android.server.telecom.bluetooth.BluetoothStateReceiver;
@@ -60,9 +61,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 +78,7 @@
     @Mock BluetoothLeAudio mBluetoothLeAudio;
     @Mock AudioManager mockAudioManager;
     @Mock AudioDeviceInfo mSpeakerInfo;
+    @Mock Executor mExecutor;
 
     BluetoothDeviceManager mBluetoothDeviceManager;
     BluetoothProfile.ServiceListener serviceListenerUnderTest;
@@ -114,6 +118,7 @@
         mCommunicationDeviceTracker.setBluetoothRouteManager(mRouteManager);
 
         mockAudioManager = mContext.getSystemService(AudioManager.class);
+        mExecutor = mContext.getMainExecutor();
 
         ArgumentCaptor<BluetoothProfile.ServiceListener> serviceCaptor =
                 ArgumentCaptor.forClass(BluetoothProfile.ServiceListener.class);
@@ -134,6 +139,7 @@
 
         when(mSpeakerInfo.getType()).thenReturn(TYPE_BUILTIN_SPEAKER);
         when(mFeatureFlags.callAudioCommunicationDeviceRefactor()).thenReturn(false);
+        when(mFeatureFlags.useRefactoredAudioRouteSwitching()).thenReturn(false);
     }
 
     @Override
@@ -750,6 +756,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..72e2111 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,40 +47,56 @@
 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.times;
 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.content.BroadcastReceiver;
+import android.content.Intent;
+import android.content.IntentFilter;
 import android.media.AudioDeviceInfo;
+import android.media.AudioFocusRequest;
 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.CallAudioRouteStateMachine;
 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 dalvik.annotation.TestTarget;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 
 @RunWith(JUnit4.class)
@@ -94,6 +109,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 +132,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 +147,42 @@
                 });
         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(mCallsManager.getForegroundCall()).thenReturn(mCall);
+        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(mCall.getSupportedAudioRoutes()).thenReturn(CallAudioState.ROUTE_ALL);
+        when(mFeatureFlags.ignoreAutoRouteToWatchDevice()).thenReturn(false);
+        when(mFeatureFlags.useRefactoredAudioRouteSwitching()).thenReturn(true);
     }
 
     @After
@@ -166,36 +213,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, 0);
+        // 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 +242,92 @@
 
     @SmallTest
     @Test
+    public void testActiveFocusAudioRouting() {
+        mController.initialize();
+        // Connect wired headset
+        mController.sendMessageWithSessionInfo(CONNECT_WIRED_HEADSET);
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_WIRED_HEADSET,
+                CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        // Explicitly switch to speaker
+        mController.sendMessageWithSessionInfo(USER_SWITCH_SPEAKER);
+        mController.sendMessageWithSessionInfo(SPEAKER_ON);
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+                CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+        // Expect that active focus received from a new active call will force route to baseline
+        // (in this case, this should be the wired headset).
+        mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS, 0);
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_WIRED_HEADSET,
+                CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        // Switch back to speaker and send active focus for end tone to confirm that audio routing
+        // doesn't fall back onto the baseline.
+        mController.sendMessageWithSessionInfo(USER_SWITCH_SPEAKER);
+        mController.sendMessageWithSessionInfo(SPEAKER_ON);
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+                CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+        mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS, 1);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+    }
+
+    @SmallTest
+    @Test
+    public void testVideoCallHoldRouteToEarpiece() {
+        mController.initialize();
+        mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS, 0);
+        // 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, 0);
+        // 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 +353,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();
@@ -252,16 +368,21 @@
                 any(CallAudioState.class), eq(expectedState));
         assertFalse(mController.isActive());
 
-        mController.sendMessageWithSessionInfo(SWITCH_FOCUS, RINGING_FOCUS);
-        verify(mAudioManager, timeout(TEST_TIMEOUT)).setCommunicationDevice(
-                nullable(AudioDeviceInfo.class));
+        mController.sendMessageWithSessionInfo(SWITCH_FOCUS, RINGING_FOCUS, 0);
+        verify(mBluetoothDeviceManager, timeout(TEST_TIMEOUT))
+                .connectAudio(BLUETOOTH_DEVICE_1, AudioRoute.TYPE_BLUETOOTH_SCO);
         assertTrue(mController.isActive());
 
-        mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS);
+        mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS, 0);
         assertTrue(mController.isActive());
 
-        mController.sendMessageWithSessionInfo(SWITCH_FOCUS, NO_FOCUS);
-        verify(mAudioManager, timeout(TEST_TIMEOUT)).clearCommunicationDevice();
+        mController.sendMessageWithSessionInfo(SWITCH_FOCUS, NO_FOCUS, 0);
+        // Ensure we tell the CallAudioManager that audio operations are done so that we can ensure
+        // audio focus is relinquished.
+        verify(mCallAudioManager, timeout(TEST_TIMEOUT)).notifyAudioOperationsComplete();
+
+        // Ensure the BT device is disconnected.
+        verify(mBluetoothDeviceManager, timeout(TEST_TIMEOUT).atLeastOnce()).disconnectSco();
         assertFalse(mController.isActive());
     }
 
@@ -401,7 +522,7 @@
 
     @SmallTest
     @Test
-    public void tesetSwitchSpeakerAndHeadset() {
+    public void testSwitchSpeakerAndHeadset() {
         mController.initialize();
         mController.sendMessageWithSessionInfo(CONNECT_WIRED_HEADSET);
         CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_WIRED_HEADSET,
@@ -455,9 +576,40 @@
 
     @SmallTest
     @Test
-    public void testToggleMute() throws Exception {
-        when(mAudioManager.isMasterMute()).thenReturn(false);
+    public void testStreamRingMuteChange() {
+        mController.initialize();
 
+        // Make sure we register a receiver for the STREAM_MUTE_CHANGED_ACTION so we can see if the
+        // ring stream unmutes.
+        ArgumentCaptor<BroadcastReceiver> brCaptor = ArgumentCaptor.forClass(
+                BroadcastReceiver.class);
+        ArgumentCaptor<IntentFilter> filterCaptor = ArgumentCaptor.forClass(IntentFilter.class);
+        verify(mContext, times(3)).registerReceiver(brCaptor.capture(), filterCaptor.capture());
+        boolean foundValid = false;
+        for (int ix = 0; ix < brCaptor.getAllValues().size(); ix++) {
+            BroadcastReceiver receiver = brCaptor.getAllValues().get(ix);
+            IntentFilter filter = filterCaptor.getAllValues().get(ix);
+            if (!filter.hasAction(AudioManager.STREAM_MUTE_CHANGED_ACTION)) {
+                continue;
+            }
+
+            // Fake out a call to the broadcast receiver and make sure we call into audio manager
+            // to trigger re-evaluation of ringing.
+            Intent intent = new Intent(AudioManager.STREAM_MUTE_CHANGED_ACTION);
+            intent.putExtra(AudioManager.EXTRA_STREAM_VOLUME_MUTED, false);
+            intent.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, AudioManager.STREAM_RING);
+            receiver.onReceive(mContext, intent);
+            verify(mCallAudioManager).onRingerModeChange();
+            foundValid = true;
+        }
+        assertTrue(foundValid);
+    }
+
+
+    @SmallTest
+    @Test
+    public void testToggleMute() throws Exception {
+        when(mAudioManager.isMicrophoneMute()).thenReturn(false);
         mController.initialize();
         mController.setActive(true);
 
@@ -470,7 +622,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 +632,304 @@
         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, 0);
+        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));
+        // Ensure we tell the CallAudioManager that audio operations are done so that we can ensure
+        // audio focus is relinquished.
+        verify(mCallAudioManager, timeout(TEST_TIMEOUT)).notifyAudioOperationsComplete();
+    }
+
+    @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));
+    }
+
+    @SmallTest
+    @Test
+    public void testRouteFromBtSwitchInRingingSelected() {
+        when(mFeatureFlags.ignoreAutoRouteToWatchDevice()).thenReturn(true);
+        when(mBluetoothRouteManager.isWatch(any(BluetoothDevice.class))).thenReturn(true);
+        when(mBluetoothRouteManager.isInbandRingEnabled(eq(BLUETOOTH_DEVICE_1))).thenReturn(false);
+
+        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));
+
+        mController.sendMessageWithSessionInfo(SWITCH_FOCUS, RINGING_FOCUS, 0);
+        assertFalse(mController.isActive());
+
+        // BT device should be cached. Verify routing into BT device once focus becomes active.
+        mController.sendMessageWithSessionInfo(USER_SWITCH_BLUETOOTH, 0,
+            BLUETOOTH_DEVICE_1.getAddress());
+        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(SWITCH_FOCUS, ACTIVE_FOCUS, 0);
+        mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0, BLUETOOTH_DEVICE_1);
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+            CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+            any(CallAudioState.class), eq(expectedState));
+    }
+
+    @SmallTest
+    @Test
+    public void testUpdateRouteForForeground() {
+        mController.initialize();
+        mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
+                BLUETOOTH_DEVICE_1);
+
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+        mController.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
+                AudioRoute.TYPE_BLUETOOTH_SCO, BT_ADDRESS_1);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        // Ensure that supported routes is updated along with the current route to reflect the
+        // foreground call's supported audio routes.
+        when(mCall.getSupportedAudioRoutes()).thenReturn(CallAudioState.ROUTE_SPEAKER);
+        mController.sendMessageWithSessionInfo(
+                CallAudioRouteStateMachine.UPDATE_SYSTEM_AUDIO_ROUTE);
+        mController.sendMessageWithSessionInfo(SPEAKER_ON);
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+                CallAudioState.ROUTE_SPEAKER, null, BLUETOOTH_DEVICES);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+        assertEquals(3, mController.getAvailableRoutes().size());
+        assertEquals(1, mController.getCallSupportedRoutes().size());
+    }
+
+    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..240e641 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.getString(R.string.skip_incoming_caller_info_account_package))
+                .thenReturn("");
+        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.getString(R.string.skip_incoming_caller_info_account_package))
+                .thenReturn("com.foo");
+
+        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..baf0208 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;
@@ -64,7 +66,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;
@@ -298,7 +300,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 +313,8 @@
     @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;
     private CallsManager mCallsManager;
 
     @Override
@@ -403,9 +406,12 @@
                 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);
     }
 
     @Override
@@ -1436,6 +1442,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 +2759,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 +2878,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 +2899,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 +2922,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 +2941,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 +2964,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 +3028,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
@@ -3433,18 +3597,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 +3623,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 +3721,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 +3728,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 +3887,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 a9802cb..e497f48 100644
--- a/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java
+++ b/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java
@@ -18,7 +18,7 @@
 
 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;
@@ -88,6 +88,8 @@
     private static final String TEST_PACKAGE = "com.android.server.telecom.tests";
     private static final String TEST_CLASS =
             "com.android.server.telecom.tests.MockConnectionService";
+    private static final String CONNECTION_MANAGER_TEST_CLASS =
+            "com.android.server.telecom.tests.ConnectionManagerConnectionService";
     private static final UserHandle USER_HANDLE_10 = new UserHandle(10);
 
     @Mock
@@ -106,8 +108,8 @@
     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
@@ -130,7 +132,7 @@
                          return null;
                      }
                  }
-                ).when(mConnectionServiceFocusManager).requestFocus(any(), any());
+        ).when(mConnectionServiceFocusManager).requestFocus(any(), any());
 
         mTestCreateConnectionProcessor = new CreateConnectionProcessor(mMockCall,
                 mMockConnectionServiceRepository, mMockCreateConnectionResponse,
@@ -195,7 +197,7 @@
 
     @SmallTest
     @Test
-    public void testbadPhoneAccount() throws Exception {
+    public void testBadPhoneAccount() throws Exception {
         PhoneAccountHandle pAHandle = null;
         when(mMockCall.isEmergencyCall()).thenReturn(false);
         when(mMockCall.getTargetPhoneAccount()).thenReturn(pAHandle);
@@ -219,9 +221,9 @@
         setTargetPhoneAccount(mMockCall, pAHandle);
         when(mMockCall.isEmergencyCall()).thenReturn(false);
         // Include a Connection Manager
-        PhoneAccountHandle callManagerPAHandle = getNewConnectionMangerHandleForCall(mMockCall,
+        PhoneAccountHandle callManagerPAHandle = getNewConnectionManagerHandleForCall(mMockCall,
                 "cm_acct");
-        ConnectionServiceWrapper service = makeConnectionServiceWrapper();
+        ConnectionServiceWrapper service = makeConnMgrConnectionServiceWrapper();
         // Make sure the target phone account has the correct permissions
         PhoneAccount mFakeTargetPhoneAccount = makeQuickAccount("cm_acct",
                 PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION, null);
@@ -243,14 +245,52 @@
 
     @SmallTest
     @Test
+    public void testConnectionManagerConnectionServiceSuccess() throws Exception {
+        when(mFeatureFlags.updatedRcsCallCountTracking()).thenReturn(true);
+
+        // Configure the target phone account as the remote connection service:
+        PhoneAccountHandle pAHandle = getNewTargetPhoneAccountHandle("tel_acct");
+        setTargetPhoneAccount(mMockCall, pAHandle);
+        when(mMockCall.isEmergencyCall()).thenReturn(false);
+        ConnectionServiceWrapper remoteService = makeConnectionServiceWrapper();
+
+        // Configure the connection manager phone account as the primary connection service:
+        PhoneAccountHandle callManagerPAHandle = getNewConnectionManagerHandleForCall(mMockCall,
+                "cm_acct");
+        ConnectionServiceWrapper service = makeConnMgrConnectionServiceWrapper();
+
+        // Make sure the target phone account has the correct permissions
+        PhoneAccount mFakeTargetPhoneAccount = makeQuickAccount("cm_acct",
+                PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION, null);
+        when(mMockAccountRegistrar.getPhoneAccountUnchecked(pAHandle)).thenReturn(
+                mFakeTargetPhoneAccount);
+
+        mTestCreateConnectionProcessor.process();
+
+        verify(mMockCall).setConnectionManagerPhoneAccount(eq(callManagerPAHandle));
+        verify(mMockCall).setTargetPhoneAccount(eq(pAHandle));
+        // Ensure the remote connection service and primary connection service are set properly:
+        verify(mMockCall).setConnectionService(eq(service), eq(remoteService));
+        verify(service).createConnection(eq(mMockCall),
+                any(CreateConnectionResponse.class));
+        // Notify successful connection to call:
+        CallIdMapper mockCallIdMapper = mock(CallIdMapper.class);
+        mTestCreateConnectionProcessor.handleCreateConnectionSuccess(mockCallIdMapper, null);
+        verify(mMockCreateConnectionResponse).handleCreateConnectionSuccess(mockCallIdMapper, null);
+    }
+
+    @SmallTest
+    @Test
     public void testConnectionManagerFailedFallToSim() throws Exception {
         PhoneAccountHandle pAHandle = getNewTargetPhoneAccountHandle("tel_acct");
         setTargetPhoneAccount(mMockCall, pAHandle);
         when(mMockCall.isEmergencyCall()).thenReturn(false);
+        ConnectionServiceWrapper remoteService = makeConnectionServiceWrapper();
+
         // Include a Connection Manager
-        PhoneAccountHandle callManagerPAHandle = getNewConnectionMangerHandleForCall(mMockCall,
+        PhoneAccountHandle callManagerPAHandle = getNewConnectionManagerHandleForCall(mMockCall,
                 "cm_acct");
-        ConnectionServiceWrapper service = makeConnectionServiceWrapper();
+        ConnectionServiceWrapper service = makeConnMgrConnectionServiceWrapper();
         when(mMockCall.getConnectionManagerPhoneAccount()).thenReturn(callManagerPAHandle);
         PhoneAccount mFakeTargetPhoneAccount = makeQuickAccount("cm_acct",
                 PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION, null);
@@ -273,8 +313,8 @@
         // Verify that the Sim Phone Account is used correctly
         verify(mMockCall).setConnectionManagerPhoneAccount(eq(pAHandle));
         verify(mMockCall).setTargetPhoneAccount(eq(pAHandle));
-        verify(mMockCall).setConnectionService(eq(service));
-        verify(service).createConnection(eq(mMockCall), any(CreateConnectionResponse.class));
+        verify(mMockCall).setConnectionService(eq(remoteService));
+        verify(remoteService).createConnection(eq(mMockCall), any(CreateConnectionResponse.class));
         // Notify successful connection to call
         CallIdMapper mockCallIdMapper = mock(CallIdMapper.class);
         mTestCreateConnectionProcessor.handleCreateConnectionSuccess(mockCallIdMapper, null);
@@ -288,7 +328,7 @@
         setTargetPhoneAccount(mMockCall, pAHandle);
         when(mMockCall.isEmergencyCall()).thenReturn(false);
         // Include a Connection Manager
-        PhoneAccountHandle callManagerPAHandle = getNewConnectionMangerHandleForCall(mMockCall,
+        PhoneAccountHandle callManagerPAHandle = getNewConnectionManagerHandleForCall(mMockCall,
                 "cm_acct");
         ConnectionServiceWrapper service = makeConnectionServiceWrapper();
         when(mMockCall.getConnectionManagerPhoneAccount()).thenReturn(callManagerPAHandle);
@@ -318,7 +358,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
@@ -356,7 +397,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).
      */
@@ -386,7 +428,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
@@ -423,7 +466,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.
@@ -463,8 +507,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
@@ -496,7 +542,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
@@ -570,7 +617,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
@@ -708,7 +756,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.
      */
@@ -878,6 +927,24 @@
     }
 
     /**
+     * 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.
      * @param count How many phone accounts to use.
@@ -963,8 +1030,8 @@
         when(mMockAccountRegistrar.phoneAccountRequiresBindPermission(eq(handle))).thenReturn(true);
     }
 
-    private PhoneAccountHandle getNewConnectionMangerHandleForCall(Call call, String id) {
-        PhoneAccountHandle callManagerPAHandle = makeQuickAccountHandle(id, null);
+    private PhoneAccountHandle getNewConnectionManagerHandleForCall(Call call, String id) {
+        PhoneAccountHandle callManagerPAHandle = makeQuickConnMgrAccountHandle(id, null);
         when(mMockAccountRegistrar.getSimCallManagerFromCall(eq(call))).thenReturn(
                 callManagerPAHandle);
         givePhoneAccountBindPermission(callManagerPAHandle);
@@ -1006,6 +1073,10 @@
         return new ComponentName(TEST_PACKAGE, TEST_CLASS);
     }
 
+    private static ComponentName makeQuickConnMgrConnectionServiceComponentName() {
+        return new ComponentName(TEST_PACKAGE, CONNECTION_MANAGER_TEST_CLASS);
+    }
+
     private ConnectionServiceWrapper makeConnectionServiceWrapper() {
         ConnectionServiceWrapper wrapper = mock(ConnectionServiceWrapper.class);
 
@@ -1015,6 +1086,24 @@
         return wrapper;
     }
 
+    private ConnectionServiceWrapper makeConnMgrConnectionServiceWrapper() {
+        ConnectionServiceWrapper wrapper = mock(ConnectionServiceWrapper.class);
+
+        when(mMockConnectionServiceRepository.getService(
+                eq(makeQuickConnMgrConnectionServiceComponentName()), any(UserHandle.class)))
+                .thenReturn(wrapper);
+        return wrapper;
+    }
+
+    private static PhoneAccountHandle makeQuickConnMgrAccountHandle(String id,
+            UserHandle userHandle) {
+        if (userHandle == null) {
+            userHandle = Binder.getCallingUserHandle();
+        }
+        return new PhoneAccountHandle(makeQuickConnMgrConnectionServiceComponentName(),
+                id, userHandle);
+    }
+
     private static PhoneAccountHandle makeQuickAccountHandle(String id, UserHandle userHandle) {
         if (userHandle == null) {
             userHandle = Binder.getCallingUserHandle();
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..449aa41 100644
--- a/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
+++ b/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
@@ -79,6 +79,7 @@
 import android.os.UserManager;
 import android.permission.PermissionCheckerManager;
 import android.telecom.CallAudioState;
+import android.telecom.CallEndpoint;
 import android.telecom.InCallService;
 import android.telecom.ParcelableCall;
 import android.telecom.PhoneAccountHandle;
@@ -95,6 +96,7 @@
 import com.android.server.telecom.Analytics;
 import com.android.server.telecom.AnomalyReporterAdapter;
 import com.android.server.telecom.Call;
+import com.android.server.telecom.CallEndpointController;
 import com.android.server.telecom.CallsManager;
 import com.android.server.telecom.CarModeTracker;
 import com.android.server.telecom.ClockProxy;
@@ -154,10 +156,10 @@
     @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;
+    @Mock CallEndpointController mMockCallEndpointController;
 
     @Rule
     public TestRule compatChangeRule = new PlatformCompatChangeRule();
@@ -226,7 +228,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);
@@ -308,14 +310,22 @@
                 .thenReturn(PackageManager.PERMISSION_DENIED);
 
         when(mMockCallsManager.getAudioState()).thenReturn(new CallAudioState(false, 0, 0));
+        when(mFeatureFlags.onCallEndpointChangedIcsOnConnected()).thenReturn(true);
+        when(mMockCallsManager.getCallEndpointController()).thenReturn(mMockCallEndpointController);
+        when(mMockCallEndpointController.getCurrentCallEndpoint())
+                .thenReturn(new CallEndpoint("Earpiece", 1));
 
         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 +416,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 +451,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 +490,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 +535,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 +553,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 +610,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 +641,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 +673,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 +702,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 +738,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 +758,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 +840,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 +921,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 +968,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 +1017,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 +1051,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 +1128,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 +1220,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 +1271,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 +1320,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 +1339,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 +1391,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 +1435,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 +1468,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 +1505,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 +1529,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 +1597,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 +1666,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 +1705,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 +1805,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 +1828,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 +1856,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 +1874,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,24 +1940,31 @@
         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);
     }
 
+    /**
+     * Verify that if a null inCallService object is passed to sendCallToInCallService, a
+     * NullPointerException is not thrown.
+     */
+    @Test
+    public void testSendCallToInCallServiceWithNullService() {
+        when(mFeatureFlags.doNotSendCallToNullIcs()).thenReturn(true);
+        //Setup up parent and child/work profile relation
+        when(mMockChildUserCall.getAssociatedUser()).thenReturn(mChildUserHandle);
+        when(mMockCallsManager.getCurrentUserHandle()).thenReturn(mParentUserHandle);
+        when(mMockUserManager.getProfileParent(mChildUserHandle)).thenReturn(mParentUserHandle);
+        when(mFeatureFlags.profileUserSupport()).thenReturn(true);
+        when(mMockContext.getSystemService(eq(UserManager.class)))
+                .thenReturn(mMockUserManager);
+        // verify a NullPointerException is not thrown
+        int res = mInCallController.sendCallToService(mMockCall, mInCallServiceInfo, null);
+        assertEquals(0, res);
+    }
+
     @Test
     public void testProfileCallQueriesIcsUsingParentUserToo() throws Exception {
         setupMocksForProfileTest();
@@ -1954,7 +1977,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 +1997,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 +2016,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/RingerTest.java b/tests/src/com/android/server/telecom/tests/RingerTest.java
index 1215fd3..1510e0c 100644
--- a/tests/src/com/android/server/telecom/tests/RingerTest.java
+++ b/tests/src/com/android/server/telecom/tests/RingerTest.java
@@ -136,6 +136,7 @@
         super.setUp();
         mContext = spy(mComponentContextFixture.getTestDouble().getApplicationContext());
         when(mFeatureFlags.telecomResolveHiddenDependencies()).thenReturn(true);
+        when(mFeatureFlags.ensureInCarRinging()).thenReturn(false);
         doReturn(URI_VIBRATION_EFFECT).when(spyVibrationEffectProxy).get(any(), any());
         when(mockPlayerFactory.createPlayer(any(Call.class), anyInt())).thenReturn(mockTonePlayer);
         mockAudioManager = mContext.getSystemService(AudioManager.class);
@@ -325,19 +326,6 @@
 
     @SmallTest
     @Test
-    public void testNoActionInTheaterMode() throws Exception {
-        // Start call waiting to make sure that it doesn't stop when we start ringing
-        mRingerUnderTest.startCallWaiting(mockCall1);
-        when(mockSystemSettingsUtil.isTheaterModeOn(any(Context.class))).thenReturn(true);
-        assertFalse(startRingingAndWaitForAsync(mockCall2, false));
-        verifyZeroInteractions(mockRingtoneFactory);
-        verify(mockTonePlayer, never()).stopTone();
-        verify(mockVibrator, never())
-                .vibrate(any(VibrationEffect.class), any(VibrationAttributes.class));
-    }
-
-    @SmallTest
-    @Test
     public void testNoActionWithExternalRinger() throws Exception {
         Bundle externalRingerExtra = new Bundle();
         externalRingerExtra.putBoolean(TelecomManager.EXTRA_CALL_HAS_IN_BAND_RINGTONE, true);
@@ -438,6 +426,62 @@
 
     @SmallTest
     @Test
+    public void testAudibleRingWhenNotificationSoundShouldPlay() throws Exception {
+        when(mFeatureFlags.ensureInCarRinging()).thenReturn(true);
+        Ringtone mockRingtone = ensureRingtoneMocked();
+
+        mRingerUnderTest.startCallWaiting(mockCall1);
+        AudioAttributes aa = new AudioAttributes.Builder()
+                .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
+                .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION).build();
+        // Set AudioManager#shouldNotificationSoundPlay to true:
+        when(mockAudioManager.shouldNotificationSoundPlay(aa)).thenReturn(true);
+        enableVibrationWhenRinging();
+
+        // This will set AudioManager#getStreamVolume to 0. This test ensures that whether a
+        // ringtone is audible is controlled by AudioManager#shouldNotificationSoundPlay instead:
+        ensureRingerIsNotAudible();
+
+        // Ensure an audible ringtone is played:
+        assertTrue(startRingingAndWaitForAsync(mockCall2, false));
+        verify(mockTonePlayer).stopTone();
+        verify(mockRingtoneFactory, atLeastOnce()).getRingtone(any(Call.class),
+                nullable(VolumeShaper.Configuration.class), anyBoolean());
+        verifyNoMoreInteractions(mockRingtoneFactory);
+        verify(mockRingtone).play();
+
+        // Ensure a vibration plays:
+        verify(mockVibrator).vibrate(any(VibrationEffect.class), any(VibrationAttributes.class));
+    }
+
+    @SmallTest
+    @Test
+    public void testNoAudibleRingWhenNotificationSoundShouldNotPlay() throws Exception {
+        when(mFeatureFlags.ensureInCarRinging()).thenReturn(true);
+        Ringtone mockRingtone = ensureRingtoneMocked();
+
+        mRingerUnderTest.startCallWaiting(mockCall1);
+        AudioAttributes aa = new AudioAttributes.Builder()
+                .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
+                .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION).build();
+        // Set AudioManager#shouldNotificationSoundPlay to false:
+        when(mockAudioManager.shouldNotificationSoundPlay(aa)).thenReturn(false);
+        enableVibrationWhenRinging();
+
+        // This will set AudioManager#getStreamVolume to 100. This test ensures that whether a
+        // ringtone is audible is controlled by AudioManager#shouldNotificationSoundPlay instead:
+        ensureRingerIsAudible();
+
+        // Ensure no audible ringtone is played:
+        assertFalse(startRingingAndWaitForAsync(mockCall2, false));
+        verify(mockTonePlayer).stopTone();
+
+        // Ensure a vibration plays:
+        verify(mockVibrator).vibrate(any(VibrationEffect.class), any(VibrationAttributes.class));
+    }
+
+    @SmallTest
+    @Test
     public void testVibrateButNoRingForNullRingtone() throws Exception {
         when(mockRingtoneFactory.getRingtone(
                  any(Call.class), nullable(VolumeShaper.Configuration.class), anyBoolean()))
@@ -475,10 +519,6 @@
         enableVibrationWhenRinging();
         assertFalse(startRingingAndWaitForAsync(mockCall2, false));
         verify(mockTonePlayer).stopTone();
-        // Try to play a silent haptics ringtone
-        verify(mockRingtoneFactory, atLeastOnce()).getHapticOnlyRingtone();
-        verifyNoMoreInteractions(mockRingtoneFactory);
-        verify(mockRingtone).play();
 
         // Play default vibration when future completes with no audio coupled haptics
         verify(mockVibrator).vibrate(eq(mRingerUnderTest.mDefaultVibrationEffect),
@@ -505,28 +545,6 @@
 
     @SmallTest
     @Test
-    public void testAudioCoupledHapticsForSilentRingtone() throws Exception {
-        Ringtone mockRingtone = ensureRingtoneMocked();
-
-        mRingerUnderTest.startCallWaiting(mockCall1);
-        when(mockAudioManager.getRingerMode()).thenReturn(AudioManager.RINGER_MODE_VIBRATE);
-        when(mockAudioManager.getStreamVolume(AudioManager.STREAM_RING)).thenReturn(0);
-        setIsUsingHaptics(mockRingtone, true);
-        enableVibrationWhenRinging();
-        assertFalse(startRingingAndWaitForAsync(mockCall2, false));
-
-        verify(mockRingtoneFactory, atLeastOnce()).getHapticOnlyRingtone();
-        verifyNoMoreInteractions(mockRingtoneFactory);
-        verify(mockTonePlayer).stopTone();
-        // Try to play a silent haptics ringtone
-        verify(mockRingtone).play();
-        // Skip vibration for audio coupled haptics
-        verify(mockVibrator, never()).vibrate(any(VibrationEffect.class),
-                any(VibrationAttributes.class));
-    }
-
-    @SmallTest
-    @Test
     public void testCustomVibrationForRingtone() throws Exception {
         mRingerUnderTest.startCallWaiting(mockCall1);
         Ringtone mockRingtone = ensureRingtoneMocked();
@@ -838,7 +856,6 @@
         when(mockRingtoneFactory.getRingtone(
                 any(Call.class), nullable(VolumeShaper.Configuration.class), anyBoolean()))
                 .thenReturn(ringtoneInfo);
-        when(mockRingtoneFactory.getHapticOnlyRingtone()).thenReturn(ringtoneInfo);
         return mockRingtone;
     }
 
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 e58c6c4..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);
+        doReturn(CallState.ON_HOLD).when(mMockCall1).getState();
         t.getCallStateListenerImpl().onCallStateChanged(CallState.ON_HOLD);
-        when(mMockCall1.getState()).thenReturn(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);
+        }
     }
 }