Merge "Resolve call audio refactoring issues." into main
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_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..40aa8b2 100644
--- a/flags/telecom_call_flags.aconfig
+++ b/flags/telecom_call_flags.aconfig
@@ -1,9 +1,17 @@
 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"
+}
+
+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"
 }
\ No newline at end of file
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..1608869 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,6 +73,7 @@
   bug: "301695370"
 }
 
+# OWNER=pmadapurmath TARGET=24Q3
 flag {
   name: "clear_communication_device_after_audio_ops_complete"
   namespace: "telecom"
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 f329ca6..28e9dd8 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,6 +17,7 @@
   bug: "309540769"
 }
 
+# OWNER=breadley TARGET=24Q3
 flag {
   name: "enable_call_sequencing"
   namespace: "telecom"
diff --git a/flags/telecom_connection_service_wrapper_flags.aconfig b/flags/telecom_connection_service_wrapper_flags.aconfig
index 80a8dfe..38e5e13 100644
--- a/flags/telecom_connection_service_wrapper_flags.aconfig
+++ b/flags/telecom_connection_service_wrapper_flags.aconfig
@@ -1,6 +1,7 @@
 package: "com.android.server.telecom.flags"
 container: "system"
 
+# OWNER=grantmenke TARGET=24Q2
 flag {
   name: "updated_rcs_call_count_tracking"
   namespace: "telecom"
diff --git a/flags/telecom_default_phone_account_flags.aconfig b/flags/telecom_default_phone_account_flags.aconfig
index e6badde..161b674 100644
--- a/flags/telecom_default_phone_account_flags.aconfig
+++ b/flags/telecom_default_phone_account_flags.aconfig
@@ -1,6 +1,7 @@
 package: "com.android.server.telecom.flags"
 container: "system"
 
+# OWNER=tjstuart TARGET=24Q3
 flag {
   name: "only_update_telephony_on_valid_sub_ids"
   namespace: "telecom"
@@ -8,6 +9,7 @@
   bug: "234846282"
 }
 
+# OWNER=tjstuart TARGET=24Q3
 flag {
   name: "telephony_has_default_but_telecom_does_not"
   namespace: "telecom"
diff --git a/flags/telecom_incallservice_flags.aconfig b/flags/telecom_incallservice_flags.aconfig
index 08a82ba..ea842ac 100644
--- a/flags/telecom_incallservice_flags.aconfig
+++ b/flags/telecom_incallservice_flags.aconfig
@@ -1,6 +1,7 @@
 package: "com.android.server.telecom.flags"
 container: "system"
 
+# OWNER=qingzhong TARGET=24Q2
 flag {
   name: "early_binding_to_incall_service"
   namespace: "telecom"
@@ -8,6 +9,7 @@
   bug: "282113261"
 }
 
+# OWNER=pmadapurmath TARGET=24Q2
 flag {
   name: "ecc_keyguard"
   namespace: "telecom"
@@ -15,6 +17,7 @@
   bug: "306582821"
 }
 
+# OWNER=pmadapurmath TARGET=24Q3
 flag {
   name: "separately_bind_to_bt_incall_service"
   namespace: "telecom"
diff --git a/flags/telecom_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
index 55c7536..a30f0b2 100644
--- a/flags/telecom_remote_connection_service.aconfig
+++ b/flags/telecom_remote_connection_service.aconfig
@@ -1,5 +1,7 @@
 package: "com.android.server.telecom.flags"
+container: "system"
 
+# OWNER=pmadapurmath TARGET=24Q3
 flag {
   name: "set_remote_connection_call_id"
   namespace: "telecom"
diff --git a/flags/telecom_resolve_hidden_dependencies.aconfig b/flags/telecom_resolve_hidden_dependencies.aconfig
index 674a968..a120b85 100644
--- a/flags/telecom_resolve_hidden_dependencies.aconfig
+++ b/flags/telecom_resolve_hidden_dependencies.aconfig
@@ -1,9 +1,19 @@
 package: "com.android.server.telecom.flags"
 container: "system"
 
+# OWNER=tgunn TARGET=24Q3
 flag {
     name: "telecom_resolve_hidden_dependencies"
+    is_exported: true
     namespace: "telecom"
     description: "Mainland cleanup for hidden dependencies"
     bug: "323414215"
 }
+
+flag {
+    name: "telecom_mainline_blocked_numbers_manager"
+    namespace: "telecom"
+    description: "Fixed read only flag used for setting up BlockedNumbersManager to be retrieved via context"
+    bug: "325049252"
+    is_fixed_read_only: true
+}
diff --git a/flags/telecom_ringer_flag_declarations.aconfig b/flags/telecom_ringer_flag_declarations.aconfig
index 13577bb..f126bf3 100644
--- a/flags/telecom_ringer_flag_declarations.aconfig
+++ b/flags/telecom_ringer_flag_declarations.aconfig
@@ -1,6 +1,7 @@
 package: "com.android.server.telecom.flags"
 container: "system"
 
+# OWNER=yeabkal TARGET=24Q2
 flag {
   name: "use_device_provided_serialized_ringer_vibration"
   namespace: "telecom"
diff --git a/flags/telecom_work_profile_flags.aconfig b/flags/telecom_work_profile_flags.aconfig
index 854568b..1891423 100644
--- a/flags/telecom_work_profile_flags.aconfig
+++ b/flags/telecom_work_profile_flags.aconfig
@@ -1,6 +1,7 @@
 package: "com.android.server.telecom.flags"
 container: "system"
 
+# OWNER=pmadapurmath TARGET=24Q3
 flag {
   name: "associated_user_refactor_for_work_profile"
   namespace: "telecom"
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
index ab74d61..0adb30c 100644
--- a/res/values-cs/strings.xml
+++ b/res/values-cs/strings.xml
@@ -36,7 +36,7 @@
     <string name="accessibility_call_muted" msgid="2968461092554300779">"Hovor ztlumen."</string>
     <string name="accessibility_speakerphone_enabled" msgid="555386652061614267">"Reproduktor je zapnutý."</string>
     <string name="respond_via_sms_canned_response_1" msgid="6332561460870382561">"Teď nemůžu mluvit, o co jde?"</string>
-    <string name="respond_via_sms_canned_response_2" msgid="2052951316129952406">"Zavolám později."</string>
+    <string name="respond_via_sms_canned_response_2" msgid="2052951316129952406">"Zavolám zpátky."</string>
     <string name="respond_via_sms_canned_response_3" msgid="6656147963478092035">"Zavolám později."</string>
     <string name="respond_via_sms_canned_response_4" msgid="9141132488345561047">"Nemůžu telefonovat. Zavoláš později?"</string>
     <string name="respond_via_sms_setting_title" msgid="4762275482898830160">"Rychlé odpovědi"</string>
@@ -63,7 +63,7 @@
     <string name="change_default_call_screening_dialog_affirmative" msgid="7162433828280058647">"Nastavit jako výchozí"</string>
     <string name="change_default_call_screening_dialog_negative" msgid="1839266125623106342">"Zrušit"</string>
     <string name="blocked_numbers" msgid="8322134197039865180">"Blokovaná čísla"</string>
-    <string name="blocked_numbers_msg" msgid="2797422132329662697">"Ze zablokovaných čísel už nebudete přijímat hovory ani zprávy SMS."</string>
+    <string name="blocked_numbers_msg" msgid="2797422132329662697">"Od zablokovaných čísel už nebudete přijímat hovory ani zprávy SMS."</string>
     <string name="block_number" msgid="3784343046852802722">"Přidat číslo"</string>
     <string name="unblock_dialog_body" msgid="2723393535797217261">"Odblokovat číslo <xliff:g id="NUMBER_TO_BLOCK">%1$s</xliff:g>?"</string>
     <string name="unblock_button" msgid="8732021675729981781">"Odblokovat"</string>
@@ -113,7 +113,7 @@
     <string name="phone_settings_private_num_summary_txt" msgid="6755758240544021037">"Blokovat volající, kteří skrývají své číslo"</string>
     <string name="phone_settings_payphone_txt" msgid="5003987966052543965">"Z veřejných telefonů"</string>
     <string name="phone_settings_payphone_summary_txt" msgid="3936631076065563665">"Blokovat hovory z veřejných telefonů"</string>
-    <string name="phone_settings_unknown_txt" msgid="3577926178354772728">"Z nerozpoznaných čísel"</string>
+    <string name="phone_settings_unknown_txt" msgid="3577926178354772728">"Nerozpoznaná čísla"</string>
     <string name="phone_settings_unknown_summary_txt" msgid="5446657192535779645">"Blokovat hovory od nerozpoznaných volajících"</string>
     <string name="phone_settings_unavailable_txt" msgid="825918186053980858">"Neznámé"</string>
     <string name="phone_settings_unavailable_summary_txt" msgid="8221686031038282633">"Blokovat hovory z neznámých čísel"</string>
diff --git a/res/values-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/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/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/Call.java b/src/com/android/server/telecom/Call.java
index ed72a3f..872b423 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -86,6 +86,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;
@@ -833,6 +834,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;
 
     /**
@@ -1562,6 +1573,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;
@@ -1590,6 +1604,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 +2018,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() {
@@ -2408,6 +2445,7 @@
 
     @VisibleForTesting
     public void setConnectionService(ConnectionServiceWrapper service) {
+        Log.i(this, "setConnectionService: service=[%s]", service);
         setConnectionService(service, null);
     }
 
@@ -2430,6 +2468,7 @@
         mConnectionService = service;
         mAnalytics.setCallConnectionService(service.getComponentName().flattenToShortString());
         mConnectionService.addCall(this);
+        processCachedCallbacks(service);
     }
 
     /**
@@ -3053,16 +3092,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 +3479,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;
         }
 
@@ -3699,8 +3746,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;
         }
 
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 ee3e9f5..a100185 100644
--- a/src/com/android/server/telecom/CallIntentProcessor.java
+++ b/src/com/android/server/telecom/CallIntentProcessor.java
@@ -174,8 +174,9 @@
             // 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),
diff --git a/src/com/android/server/telecom/CallLogManager.java b/src/com/android/server/telecom/CallLogManager.java
index 6395f2f..dc9e2ea 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;
             }
         }
@@ -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,55 +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;
-            boolean isCurrentUserUnlocked = mFeatureFlags.telecomResolveHiddenDependencies()
-                    ? userManager.isUserUnlocked(UserHandle.CURRENT)
-                    : userManager.isUserUnlocked(currentUserId);
-            if (isCurrentUserUnlocked) {
-                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/CallSourceService.java b/src/com/android/server/telecom/CallSourceService.java
new file mode 100644
index 0000000..132118b
--- /dev/null
+++ b/src/com/android/server/telecom/CallSourceService.java
@@ -0,0 +1,38 @@
+/*
+ * 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);
+}
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index 7cd7bc6..c3eb3b8 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;
 
@@ -556,7 +557,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);
             }
         }
@@ -719,6 +721,9 @@
         mCallStreamingNotification = callStreamingNotification;
         mFeatureFlags = featureFlags;
         mTelephonyFeatureFlags = telephonyFlags;
+        mBlockedNumbersManager = mFeatureFlags.telecomMainlineBlockedNumbersManager()
+                ? mContext.getSystemService(BlockedNumbersManager.class)
+                : null;
 
         if (mFeatureFlags.useImprovedListenerOrder()) {
             mListeners.add(mInCallController);
@@ -759,7 +764,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<>();
 
@@ -852,10 +857,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, "
@@ -864,7 +875,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()
@@ -913,7 +924,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,
@@ -948,6 +959,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
@@ -1401,7 +1413,7 @@
         return mCallEndpointController;
     }
 
-    EmergencyCallHelper getEmergencyCallHelper() {
+    public EmergencyCallHelper getEmergencyCallHelper() {
         return mEmergencyCallHelper;
     }
 
@@ -1909,19 +1921,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
@@ -2183,11 +2201,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);
+                                    }
                                 }
                             }
 
@@ -2671,6 +2694,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) {
@@ -2959,9 +2985,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(
@@ -3467,11 +3497,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;
@@ -3697,6 +3731,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;
     }
diff --git a/src/com/android/server/telecom/ConnectionServiceWrapper.java b/src/com/android/server/telecom/ConnectionServiceWrapper.java
index 53da8ff..607aa27 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;
@@ -1409,20 +1407,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 +1954,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 +1970,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);
@@ -1986,6 +1989,7 @@
 
     /** @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")) {
@@ -2548,9 +2552,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;
         }
@@ -2558,8 +2564,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 bcb2d2f..abb2d8c 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 14e5bf0..904ec81 100644
--- a/src/com/android/server/telecom/CreateConnectionTimeout.java
+++ b/src/com/android/server/telecom/CreateConnectionTimeout.java
@@ -16,21 +16,27 @@
 
 package com.android.server.telecom;
 
+import static com.android.internal.telephony.flags.Flags.carrierEnabledSatelliteFlag;
+
 import android.content.Context;
 import android.os.Handler;
 import android.os.Looper;
 import android.telecom.Log;
 import android.telecom.Logging.Runnable;
+import android.telecom.PhoneAccount;
 import android.telecom.PhoneAccountHandle;
 import android.telephony.TelephonyManager;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 import java.util.Collection;
 import java.util.Objects;
 
 /**
  * Registers a timeout for a call and disconnects the call when the timeout expires.
  */
-final class CreateConnectionTimeout extends Runnable {
+@VisibleForTesting
+public final class CreateConnectionTimeout extends Runnable {
     private final Context mContext;
     private final PhoneAccountRegistrar mPhoneAccountRegistrar;
     private final ConnectionServiceWrapper mConnectionService;
@@ -39,7 +45,8 @@
     private boolean mIsRegistered;
     private boolean mIsCallTimedOut;
 
-    CreateConnectionTimeout(Context context, PhoneAccountRegistrar phoneAccountRegistrar,
+    @VisibleForTesting
+    public CreateConnectionTimeout(Context context, PhoneAccountRegistrar phoneAccountRegistrar,
             ConnectionServiceWrapper service, Call call) {
         super("CCT", null /*lock*/);
         mContext = context;
@@ -48,7 +55,8 @@
         mCall = call;
     }
 
-    boolean isTimeoutNeededForCall(Collection<PhoneAccountHandle> accounts,
+    @VisibleForTesting
+    public boolean isTimeoutNeededForCall(Collection<PhoneAccountHandle> accounts,
             PhoneAccountHandle currentAccount) {
         // Non-emergency calls timeout automatically at the radio layer. No need for a timeout here.
         if (!mCall.isEmergencyCall()) {
@@ -75,6 +83,17 @@
             return false;
         }
 
+        // Timeout is not required if carrier is not in service.
+        if (carrierEnabledSatelliteFlag() && connectionManager != null) {
+            PhoneAccount account = mPhoneAccountRegistrar.getPhoneAccount(connectionManager,
+                    connectionManager.getUserHandle());
+            if (account.hasCapabilities(PhoneAccount.CAPABILITY_SUPPORTS_VOICE_CALLING_INDICATIONS)
+                    && !account.hasCapabilities(PhoneAccount.CAPABILITY_VOICE_CALLING_AVAILABLE)) {
+                Log.d(this, "isTimeoutNeededForCall, carrier is not in service.");
+                return false;
+            }
+        }
+
         Log.i(this, "isTimeoutNeededForCall, returning true");
         return true;
     }
@@ -122,13 +141,18 @@
     private long getTimeoutLengthMillis() {
         // If the radio is off then use a longer timeout. This gives us more time to power on the
         // radio.
-        TelephonyManager telephonyManager =
-            (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
-        if (telephonyManager.isRadioOn()) {
+        try {
+            TelephonyManager telephonyManager =
+                    (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
+            if (telephonyManager.isRadioOn()) {
+                return Timeouts.getEmergencyCallTimeoutMillis(mContext.getContentResolver());
+            } else {
+                return Timeouts.getEmergencyCallTimeoutRadioOffMillis(
+                        mContext.getContentResolver());
+            }
+        } catch (UnsupportedOperationException uoe) {
+            Log.e(this, uoe, "getTimeoutLengthMillis - telephony is not supported");
             return Timeouts.getEmergencyCallTimeoutMillis(mContext.getContentResolver());
-        } else {
-            return Timeouts.getEmergencyCallTimeoutRadioOffMillis(
-                    mContext.getContentResolver());
         }
     }
 }
diff --git a/src/com/android/server/telecom/InCallController.java b/src/com/android/server/telecom/InCallController.java
index fb2ca3e..e625bbe 100644
--- a/src/com/android/server/telecom/InCallController.java
+++ b/src/com/android/server/telecom/InCallController.java
@@ -2428,7 +2428,9 @@
         }
 
         String bluetoothPackage = mDefaultDialerCache.getBTInCallServicePackage();
-        if (serviceInfo.packageName != null && serviceInfo.packageName.equals(bluetoothPackage)
+        if (mFeatureFlags.separatelyBindToBtIncallService()
+                && serviceInfo.packageName != null
+                && serviceInfo.packageName.equals(bluetoothPackage)
                 && (hasControlInCallPermission || hasAppOpsPermittedManageOngoingCalls)) {
             return IN_CALL_SERVICE_TYPE_BLUETOOTH;
         }
@@ -3020,19 +3022,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.
      */
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/PhoneAccountRegistrar.java b/src/com/android/server/telecom/PhoneAccountRegistrar.java
index 6b384f4..9256387 100644
--- a/src/com/android/server/telecom/PhoneAccountRegistrar.java
+++ b/src/com/android/server/telecom/PhoneAccountRegistrar.java
@@ -242,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;
     }
@@ -425,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");
         }
     }
 
@@ -465,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() {
@@ -475,13 +489,18 @@
 
     public ComponentName getSystemSimCallManagerComponent(int subId) {
         String defaultSimCallManager = null;
-        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);
+        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);
@@ -1465,16 +1484,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");
         }
     }
 
@@ -1841,7 +1866,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();
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/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java
index a24064c..83386ad 100644
--- a/src/com/android/server/telecom/TelecomServiceImpl.java
+++ b/src/com/android/server/telecom/TelecomServiceImpl.java
@@ -60,6 +60,7 @@
 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;
@@ -95,8 +96,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;
@@ -243,6 +246,7 @@
                                                 callEventCallback, mCallsManager, call);
 
                         call.setTransactionServiceWrapper(serviceWrapper);
+
                         if (mFeatureFlags.transactionalVideoState()) {
                             call.setTransactionalCallSupportsVideoCalling(callAttributes);
                         }
@@ -993,6 +997,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;
@@ -1029,6 +1036,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;
@@ -1507,8 +1517,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);
                 }
@@ -2008,7 +2023,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);
                     }
@@ -2114,12 +2133,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]");
@@ -2672,10 +2703,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,
@@ -2723,6 +2754,9 @@
 
         mTransactionManager = TransactionManager.getInstance();
         mTransactionalServiceRepository = new TransactionalServiceRepository();
+        mBlockedNumbersManager = mFeatureFlags.telecomMainlineBlockedNumbersManager()
+                ? mContext.getSystemService(BlockedNumbersManager.class)
+                : null;
     }
 
     @VisibleForTesting
diff --git a/src/com/android/server/telecom/TelecomSystem.java b/src/com/android/server/telecom/TelecomSystem.java
index 9f72604..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;
@@ -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();
                     }
                 }
             };
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..df2f9af 100644
--- a/src/com/android/server/telecom/TransactionalServiceWrapper.java
+++ b/src/com/android/server/telecom/TransactionalServiceWrapper.java
@@ -62,7 +62,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
@@ -552,6 +552,7 @@
         }
     }
 
+    @Override
     public void onCallEndpointChanged(Call call, CallEndpoint endpoint) {
         if (call != null) {
             try {
@@ -561,6 +562,7 @@
         }
     }
 
+    @Override
     public void onAvailableCallEndpointsChanged(Call call, Set<CallEndpoint> endpoints) {
         if (call != null) {
             try {
@@ -571,6 +573,7 @@
         }
     }
 
+    @Override
     public void onMuteStateChanged(Call call, boolean isMuted) {
         if (call != null) {
             try {
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/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/settings/BlockNumberTaskFragment.java b/src/com/android/server/telecom/settings/BlockNumberTaskFragment.java
index d96b3e1..6ca4d2a 100644
--- a/src/com/android/server/telecom/settings/BlockNumberTaskFragment.java
+++ b/src/com/android/server/telecom/settings/BlockNumberTaskFragment.java
@@ -23,7 +23,6 @@
 import android.os.AsyncTask;
 import android.os.Bundle;
 import android.provider.BlockedNumberContract;
-import com.android.server.telecom.R;
 
 /**
  * Retained fragment that runs an async task to add a blocked number.
diff --git a/src/com/android/server/telecom/settings/BlockedNumbersActivity.java b/src/com/android/server/telecom/settings/BlockedNumbersActivity.java
index 819b270..edc8da6 100644
--- a/src/com/android/server/telecom/settings/BlockedNumbersActivity.java
+++ b/src/com/android/server/telecom/settings/BlockedNumbersActivity.java
@@ -34,6 +34,7 @@
 import android.database.Cursor;
 import android.os.Bundle;
 import android.provider.BlockedNumberContract;
+import android.provider.BlockedNumbersManager;
 import android.telephony.PhoneNumberFormattingTextWatcher;
 import android.telephony.PhoneNumberUtils;
 import android.telephony.TelephonyManager;
@@ -53,7 +54,10 @@
 import android.widget.TextView;
 import android.widget.Toast;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.telecom.R;
+import com.android.server.telecom.flags.FeatureFlags;
+import com.android.server.telecom.flags.FeatureFlagsImpl;
 
 
 /**
@@ -75,8 +79,10 @@
             BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER + " NOTNULL) AND (" +
             BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER + " != '' ))";
 
+    private BlockedNumbersManager mBlockedNumbersManager;
     private BlockNumberTaskFragment mBlockNumberTaskFragment;
     private BlockedNumbersAdapter mAdapter;
+    private FeatureFlags mFeatureFlags;
     private TextView mAddButton;
     private ProgressBar mProgressBar;
     private RelativeLayout mButterBar;
@@ -114,6 +120,7 @@
             return;
         }
 
+        mFeatureFlags = new FeatureFlagsImpl();
         FragmentManager fm = getFragmentManager();
         mBlockNumberTaskFragment =
                 (BlockNumberTaskFragment) fm.findFragmentByTag(TAG_BLOCK_NUMBER_TASK_FRAGMENT);
@@ -155,12 +162,15 @@
             }
         };
         IntentFilter blockStatusIntentFilter = new IntentFilter(
-                BlockedNumberContract.BlockedNumbers.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED);
+                BlockedNumbersManager.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED);
         blockStatusIntentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
         registerReceiver(mBlockingStatusReceiver, blockStatusIntentFilter,
                 Context.RECEIVER_EXPORTED);
 
         getLoaderManager().initLoader(0, null, this);
+        mBlockedNumbersManager = mFeatureFlags.telecomMainlineBlockedNumbersManager()
+                ? getSystemService(BlockedNumbersManager.class)
+                : null;
     }
 
     @Override
@@ -183,8 +193,10 @@
     }
 
     private void updateButterBar() {
-        if (BlockedNumberContract.BlockedNumbers
-                .getBlockSuppressionStatus(this).getIsSuppressed()) {
+        boolean isBlockSuppressionEnabled = mBlockedNumbersManager != null
+                ? mBlockedNumbersManager.getBlockSuppressionStatus().getIsSuppressed()
+                : BlockedNumberContract.SystemContract.getBlockSuppressionStatus(this).isSuppressed;
+        if (isBlockSuppressionEnabled) {
             mButterBar.setVisibility(View.VISIBLE);
         } else {
             mButterBar.setVisibility(View.GONE);
@@ -239,7 +251,11 @@
         if (view == mAddButton) {
             showAddBlockedNumberDialog();
         } else if (view == mReEnableButton) {
-            BlockedNumberContract.BlockedNumbers.endBlockSuppression(this);
+            if (mBlockedNumbersManager != null) {
+                mBlockedNumbersManager.endBlockSuppression();
+            } else {
+                BlockedNumberContract.SystemContract.endBlockSuppression(this);
+            }
             mButterBar.setVisibility(View.GONE);
         }
     }
@@ -302,12 +318,13 @@
         }
     }
 
-    private boolean isEmergencyNumber(Context context, String number) {
+    @VisibleForTesting
+    public static boolean isEmergencyNumber(Context context, String number) {
         try {
             TelephonyManager tm = (TelephonyManager) context.getSystemService(
                     Context.TELEPHONY_SERVICE);
             return tm.isEmergencyNumber(number);
-        } catch (IllegalStateException ise) {
+        } catch (UnsupportedOperationException | IllegalStateException ignored) {
             return false;
         }
     }
diff --git a/src/com/android/server/telecom/settings/BlockedNumbersUtil.java b/src/com/android/server/telecom/settings/BlockedNumbersUtil.java
index e0fe81e..3e1da17 100644
--- a/src/com/android/server/telecom/settings/BlockedNumbersUtil.java
+++ b/src/com/android/server/telecom/settings/BlockedNumbersUtil.java
@@ -23,7 +23,8 @@
 import android.content.Intent;
 import android.os.PersistableBundle;
 import android.os.UserHandle;
-import android.provider.BlockedNumberContract.BlockedNumbers;
+import android.provider.BlockedNumberContract;
+import android.provider.BlockedNumbersManager;
 import android.telephony.CarrierConfigManager;
 import android.telephony.PhoneNumberUtils;
 import android.text.BidiFormatter;
@@ -34,6 +35,7 @@
 
 import com.android.server.telecom.R;
 import com.android.server.telecom.SystemSettingsUtil;
+import com.android.server.telecom.flags.FeatureFlags;
 import com.android.server.telecom.ui.NotificationChannelManager;
 
 import java.util.Locale;
@@ -148,8 +150,11 @@
      * @return If {@code true} means the key enabled in the SharedPreferences,
      *            {@code false} otherwise.
      */
-    public static boolean getBlockedNumberSetting(Context context, String key) {
-        return BlockedNumbers.getBlockedNumberSetting(context, key);
+    public static boolean getBlockedNumberSetting(Context context, String key,
+            FeatureFlags featureFlags) {
+        return featureFlags.telecomMainlineBlockedNumbersManager()
+                ? context.getSystemService(BlockedNumbersManager.class).getBlockedNumberSetting(key)
+                : BlockedNumberContract.SystemContract.getEnhancedBlockSetting(context, key);
     }
 
     /**
@@ -159,7 +164,13 @@
      * @param key preference key of SharedPreferences.
      * @param value the register value to the SharedPreferences.
      */
-    public static void setBlockedNumberSetting(Context context, String key, boolean value) {
-        BlockedNumbers.setBlockedNumberSetting(context, key, value);
+    public static void setBlockedNumberSetting(Context context, String key, boolean value,
+            FeatureFlags featureFlags) {
+        if (featureFlags.telecomMainlineBlockedNumbersManager()) {
+            context.getSystemService(BlockedNumbersManager.class).setBlockedNumberSetting(key,
+                    value);
+        } else {
+            BlockedNumberContract.SystemContract.setEnhancedBlockSetting(context, key, value);
+        }
     }
 }
diff --git a/src/com/android/server/telecom/settings/CallBlockDisabledActivity.java b/src/com/android/server/telecom/settings/CallBlockDisabledActivity.java
index 35b7f70..cc66a2d 100644
--- a/src/com/android/server/telecom/settings/CallBlockDisabledActivity.java
+++ b/src/com/android/server/telecom/settings/CallBlockDisabledActivity.java
@@ -20,19 +20,23 @@
 import android.app.AlertDialog;
 import android.content.DialogInterface;
 import android.os.Bundle;
-import android.provider.BlockedNumberContract;
+import android.provider.BlockedNumbersManager;
 
 import com.android.server.telecom.R;
+import com.android.server.telecom.flags.FeatureFlags;
+import com.android.server.telecom.flags.FeatureFlagsImpl;
 
 /**
  * Shows a dialog when user taps an notification in notification tray.
  */
 public class CallBlockDisabledActivity extends Activity {
     private AlertDialog mDialog;
+    private FeatureFlags mFeatureFlags;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        mFeatureFlags = new FeatureFlagsImpl();
         showCallBlockingOffDialog();
     }
 
@@ -60,9 +64,9 @@
                     public void onClick(DialogInterface dialog, int which) {
                         BlockedNumbersUtil.setBlockedNumberSetting(
                                 CallBlockDisabledActivity.this,
-                                BlockedNumberContract.BlockedNumbers
+                                BlockedNumbersManager
                                         .ENHANCED_SETTING_KEY_SHOW_EMERGENCY_CALL_NOTIFICATION,
-                                false);
+                                false, mFeatureFlags);
                         BlockedNumbersUtil.updateEmergencyCallNotification(
                                 CallBlockDisabledActivity.this, false);
                         finish();
diff --git a/src/com/android/server/telecom/settings/EnhancedCallBlockingFragment.java b/src/com/android/server/telecom/settings/EnhancedCallBlockingFragment.java
index 7ea8926..b54e273 100644
--- a/src/com/android/server/telecom/settings/EnhancedCallBlockingFragment.java
+++ b/src/com/android/server/telecom/settings/EnhancedCallBlockingFragment.java
@@ -23,7 +23,7 @@
 import android.preference.PreferenceFragment;
 import android.preference.PreferenceScreen;
 import android.preference.SwitchPreference;
-import android.provider.BlockedNumberContract.BlockedNumbers;
+import android.provider.BlockedNumbersManager;
 import android.telephony.CarrierConfigManager;
 import android.telephony.SubscriptionManager;
 import android.telecom.Log;
@@ -32,6 +32,8 @@
 import android.view.ViewGroup;
 
 import com.android.server.telecom.R;
+import com.android.server.telecom.flags.FeatureFlags;
+import com.android.server.telecom.flags.FeatureFlagsImpl;
 
 public class EnhancedCallBlockingFragment extends PreferenceFragment
         implements Preference.OnPreferenceChangeListener {
@@ -45,22 +47,25 @@
             "block_unavailable_calls_setting";
     private boolean mIsCombiningRestrictedAndUnknownOption = false;
     private boolean mIsCombiningUnavailableAndUnknownOption = false;
+    private FeatureFlags mFeatureFlags;
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         addPreferencesFromResource(R.xml.enhanced_call_blocking_settings);
+        mFeatureFlags = new FeatureFlagsImpl();
 
         maybeConfigureCallBlockingOptions();
 
-        setOnPreferenceChangeListener(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_UNREGISTERED);
-        setOnPreferenceChangeListener(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_PRIVATE);
-        setOnPreferenceChangeListener(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_PAYPHONE);
-        setOnPreferenceChangeListener(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_UNKNOWN);
-        setOnPreferenceChangeListener(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_UNAVAILABLE);
+        setOnPreferenceChangeListener(
+                BlockedNumbersManager.ENHANCED_SETTING_KEY_BLOCK_UNREGISTERED);
+        setOnPreferenceChangeListener(BlockedNumbersManager.ENHANCED_SETTING_KEY_BLOCK_PRIVATE);
+        setOnPreferenceChangeListener(BlockedNumbersManager.ENHANCED_SETTING_KEY_BLOCK_PAYPHONE);
+        setOnPreferenceChangeListener(BlockedNumbersManager.ENHANCED_SETTING_KEY_BLOCK_UNKNOWN);
+        setOnPreferenceChangeListener(BlockedNumbersManager.ENHANCED_SETTING_KEY_BLOCK_UNAVAILABLE);
         if (!showPayPhoneBlocking()) {
             Preference payPhoneOption = getPreferenceScreen()
-                    .findPreference(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_PAYPHONE);
+                    .findPreference(BlockedNumbersManager.ENHANCED_SETTING_KEY_BLOCK_PAYPHONE);
             getPreferenceScreen().removePreference(payPhoneOption);
         }
     }
@@ -122,13 +127,13 @@
     public void onResume() {
         super.onResume();
 
-        updateEnhancedBlockPref(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_UNREGISTERED);
-        updateEnhancedBlockPref(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_PRIVATE);
+        updateEnhancedBlockPref(BlockedNumbersManager.ENHANCED_SETTING_KEY_BLOCK_UNREGISTERED);
+        updateEnhancedBlockPref(BlockedNumbersManager.ENHANCED_SETTING_KEY_BLOCK_PRIVATE);
         if (showPayPhoneBlocking()) {
-            updateEnhancedBlockPref(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_PAYPHONE);
+            updateEnhancedBlockPref(BlockedNumbersManager.ENHANCED_SETTING_KEY_BLOCK_PAYPHONE);
         }
-        updateEnhancedBlockPref(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_UNKNOWN);
-        updateEnhancedBlockPref(BlockedNumbers.ENHANCED_SETTING_KEY_BLOCK_UNAVAILABLE);
+        updateEnhancedBlockPref(BlockedNumbersManager.ENHANCED_SETTING_KEY_BLOCK_UNKNOWN);
+        updateEnhancedBlockPref(BlockedNumbersManager.ENHANCED_SETTING_KEY_BLOCK_UNAVAILABLE);
     }
 
     /**
@@ -137,7 +142,8 @@
     private void updateEnhancedBlockPref(String key) {
         SwitchPreference pref = (SwitchPreference) findPreference(key);
         if (pref != null) {
-            pref.setChecked(BlockedNumbersUtil.getBlockedNumberSetting(getActivity(), key));
+            pref.setChecked(BlockedNumbersUtil.getBlockedNumberSetting(
+                    getActivity(), key, mFeatureFlags));
         }
     }
 
@@ -148,18 +154,18 @@
                 Log.i(this, "onPreferenceChange: changing %s and %s to %b",
                         preference.getKey(), BLOCK_RESTRICTED_NUMBERS_KEY, (boolean) objValue);
                 BlockedNumbersUtil.setBlockedNumberSetting(getActivity(),
-                        BLOCK_RESTRICTED_NUMBERS_KEY, (boolean) objValue);
+                        BLOCK_RESTRICTED_NUMBERS_KEY, (boolean) objValue, mFeatureFlags);
             }
 
             if (mIsCombiningUnavailableAndUnknownOption) {
                 Log.i(this, "onPreferenceChange: changing %s and %s to %b",
                         preference.getKey(), BLOCK_UNAVAILABLE_NUMBERS_KEY, (boolean) objValue);
                 BlockedNumbersUtil.setBlockedNumberSetting(getActivity(),
-                        BLOCK_UNAVAILABLE_NUMBERS_KEY, (boolean) objValue);
+                        BLOCK_UNAVAILABLE_NUMBERS_KEY, (boolean) objValue, mFeatureFlags);
             }
         }
         BlockedNumbersUtil.setBlockedNumberSetting(getActivity(), preference.getKey(),
-                (boolean) objValue);
+                (boolean) objValue, mFeatureFlags);
         return true;
     }
 
diff --git a/src/com/android/server/telecom/ui/DisconnectedCallNotifier.java b/src/com/android/server/telecom/ui/DisconnectedCallNotifier.java
index 1604285..04228c1 100644
--- a/src/com/android/server/telecom/ui/DisconnectedCallNotifier.java
+++ b/src/com/android/server/telecom/ui/DisconnectedCallNotifier.java
@@ -41,6 +41,7 @@
 import android.text.TextDirectionHeuristics;
 import android.text.TextUtils;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallState;
 import com.android.server.telecom.CallsManager;
@@ -285,12 +286,18 @@
      *      network location.  If the network location does not exist, fall back to the locale
      *      setting.
      */
-    private String getCurrentCountryIso(Context context) {
+    @VisibleForTesting
+    public String getCurrentCountryIso(Context context) {
         // Without framework function calls, this seems to be the most accurate location service
         // we can rely on.
         final TelephonyManager telephonyManager =
                 (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
-        String countryIso = telephonyManager.getNetworkCountryIso().toUpperCase();
+        String countryIso;
+        try {
+            countryIso = telephonyManager.getNetworkCountryIso().toUpperCase();
+        } catch (UnsupportedOperationException ignored) {
+            countryIso = null;
+        }
 
         if (countryIso == null) {
             countryIso = Locale.getDefault().getCountry();
diff --git a/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java b/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java
index 25ce0ca..220b44e 100644
--- a/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java
+++ b/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java
@@ -59,6 +59,7 @@
 import android.util.ArrayMap;
 import android.util.ArraySet;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.telecom.CallerInfoLookupHelper;
 import com.android.server.telecom.CallsManagerListenerBase;
 import com.android.server.telecom.Constants;
@@ -506,12 +507,18 @@
      *      network location.  If the network location does not exist, fall back to the locale
      *      setting.
      */
-    private String getCurrentCountryIso(Context context) {
+    @VisibleForTesting
+    public String getCurrentCountryIso(Context context) {
         // Without framework function calls, this seems to be the most accurate location service
         // we can rely on.
         final TelephonyManager telephonyManager =
                 (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
-        String countryIso = telephonyManager.getNetworkCountryIso().toUpperCase();
+        String countryIso;
+        try {
+            countryIso = telephonyManager.getNetworkCountryIso().toUpperCase();
+        } catch (UnsupportedOperationException ignored) {
+            countryIso = null;
+        }
 
         if (countryIso == null) {
             countryIso = Locale.getDefault().getCountry();
diff --git a/src/com/android/server/telecom/ui/ToastFactory.java b/src/com/android/server/telecom/ui/ToastFactory.java
index 75b69da..1561f0b 100644
--- a/src/com/android/server/telecom/ui/ToastFactory.java
+++ b/src/com/android/server/telecom/ui/ToastFactory.java
@@ -21,6 +21,6 @@
 import android.widget.Toast;
 
 public interface ToastFactory {
-    Toast makeText(Context context, @StringRes int resId, int duration);
-    Toast makeText(Context context, CharSequence text, int duration);
+    void makeText(Context context, @StringRes int resId, int duration);
+    void makeText(Context context, CharSequence text, int duration);
 }
diff --git a/src/com/android/server/telecom/voip/CallEventCallbackAckTransaction.java b/src/com/android/server/telecom/voip/CallEventCallbackAckTransaction.java
index 93d9836..9e140a7 100644
--- a/src/com/android/server/telecom/voip/CallEventCallbackAckTransaction.java
+++ b/src/com/android/server/telecom/voip/CallEventCallbackAckTransaction.java
@@ -125,7 +125,7 @@
 
         try {
             // wait for the client to ack that CallEventCallback
-            boolean success = latch.await(VoipCallTransaction.TIMEOUT_LIMIT, TimeUnit.MILLISECONDS);
+            boolean success = latch.await(mTransactionTimeoutMs, TimeUnit.MILLISECONDS);
             if (!success) {
                 // client send onError and failed to complete transaction
                 Log.i(TAG, String.format("CallEventCallbackAckTransaction:"
diff --git a/src/com/android/server/telecom/voip/ParallelTransaction.java b/src/com/android/server/telecom/voip/ParallelTransaction.java
index 621892a..79a940b 100644
--- a/src/com/android/server/telecom/voip/ParallelTransaction.java
+++ b/src/com/android/server/telecom/voip/ParallelTransaction.java
@@ -33,78 +33,62 @@
     }
 
     @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));
+                                                        VoipCallTransactionResult.RESULT_FAILED,
+                                                        String.format(
+                                                                "sub transaction %s failed",
+                                                                transactionName));
+                                        finish(mainResult);
                                         mCompleteListener.onTransactionCompleted(mainResult,
                                                 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(
+                                            VoipCallTransactionResult.RESULT_FAILED,
+                                            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/SerialTransaction.java b/src/com/android/server/telecom/voip/SerialTransaction.java
index 7d5a178..55d2065 100644
--- a/src/com/android/server/telecom/voip/SerialTransaction.java
+++ b/src/com/android/server/telecom/voip/SerialTransaction.java
@@ -37,86 +37,71 @@
     }
 
     @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));
+                                                        VoipCallTransactionResult.RESULT_FAILED,
+                                                        String.format(
+                                                                "sub transaction %s failed",
+                                                                transactionName));
+                                        finish(mainResult);
                                         mCompleteListener.onTransactionCompleted(mainResult,
                                                 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(
+                                            VoipCallTransactionResult.RESULT_FAILED,
+                                            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/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/VoipCallTransaction.java b/src/com/android/server/telecom/voip/VoipCallTransaction.java
index 3c91158..ceb8d55 100644
--- a/src/com/android/server/telecom/voip/VoipCallTransaction.java
+++ b/src/com/android/server/telecom/voip/VoipCallTransaction.java
@@ -20,6 +20,7 @@
 import android.os.HandlerThread;
 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 +35,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,58 +130,80 @@
 
     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);
         CompletableFuture<Void> future = CompletableFuture.completedFuture(null);
         future.thenComposeAsync(this::processTransaction, executor)
                 .thenApplyAsync((Function<VoipCallTransactionResult, Void>) result -> {
                     mCompleted.set(true);
+                    finish(result);
                     if (mCompleteListener != null) {
                         mCompleteListener.onTransactionCompleted(result, mTransactionName);
                     }
-                    finish(result);
                     return null;
                     }, executor)
                 .exceptionallyAsync((throwable -> {
@@ -189,25 +212,38 @@
                 }), executor);
     }
 
-    public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+    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));
@@ -218,7 +254,7 @@
     /**
      * @return Stats related to this transaction if stats are enabled, null otherwise.
      */
-    public Stats getStats() {
+    public final Stats getStats() {
         return mStats;
     }
 }
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/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/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..58d3302 100644
--- a/tests/src/com/android/server/telecom/tests/CallTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallTest.java
@@ -22,14 +22,17 @@
 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;
@@ -40,6 +43,7 @@
 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 +60,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 +70,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 +86,7 @@
 import org.mockito.Mockito;
 
 import java.util.Collections;
+import java.util.Set;
 
 @RunWith(AndroidJUnit4.class)
 public class CallTest extends TelecomTestCase {
@@ -100,7 +109,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 +125,9 @@
                 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();
+        EmergencyCallHelper helper = mock(EmergencyCallHelper.class);
+        doReturn(helper).when(mMockCallsManager).getEmergencyCallHelper();
     }
 
     @After
@@ -137,6 +146,148 @@
         assertTrue(call.hasGoneActiveBefore());
     }
 
+    @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.
      */
@@ -304,7 +455,6 @@
         doReturn(true).when(mMockCallsManager).isInEmergencyCall();
         call.pullExternalCall();
         verify(mMockConnectionService, never()).pullExternalCall(any());
-        verify(mMockToast).show();
     }
 
     @Test
@@ -746,6 +896,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() {
@@ -785,6 +953,10 @@
     }
 
     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 +964,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 a5a811e..e7f2d83 100644
--- a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
@@ -36,6 +36,7 @@
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
@@ -64,7 +65,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 +299,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;
@@ -405,8 +405,6 @@
                 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)))
@@ -1443,6 +1441,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() {
@@ -2730,6 +2758,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.
@@ -3440,18 +3486,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));
     }
diff --git a/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java b/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
index 54aaa4c..79b4cc8 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;
@@ -251,6 +252,8 @@
                     return mSensorPrivacyManager;
                 case Context.ACCESSIBILITY_SERVICE:
                     return mAccessibilityManager;
+                case Context.BLOCKED_NUMBERS_SERVICE:
+                    return mBlockedNumbersManager;
                 default:
                     return null;
             }
@@ -292,6 +295,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 +640,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 +843,10 @@
         mSubscriptionManager = subscriptionManager;
     }
 
+    public SubscriptionManager getSubscriptionManager() {
+        return mSubscriptionManager;
+    }
+
     public TelephonyManager getTelephonyManager() {
         return mTelephonyManager;
     }
@@ -857,6 +867,10 @@
         return mBroadcastReceivers;
     }
 
+    public TelephonyRegistryManager getTelephonyRegistryManager() {
+        return mTelephonyRegistryManager;
+    }
+
     private void addService(String action, ComponentName name, IInterface service) {
         mComponentNamesByAction.put(action, name);
         mServiceByComponentName.put(name, service);
diff --git a/tests/src/com/android/server/telecom/tests/ConnectionServiceWrapperTest.java b/tests/src/com/android/server/telecom/tests/ConnectionServiceWrapperTest.java
new file mode 100644
index 0000000..c815e8e
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/ConnectionServiceWrapperTest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom.tests;
+
+import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.content.ComponentName;
+import android.os.UserHandle;
+
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.ConnectionServiceRepository;
+import com.android.server.telecom.ConnectionServiceWrapper;
+import com.android.server.telecom.PhoneAccountRegistrar;
+import com.android.server.telecom.TelecomSystem;
+import com.android.server.telecom.flags.FeatureFlags;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class ConnectionServiceWrapperTest extends TelecomTestCase {
+    @Override
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+    }
+
+    @Override
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    /**
+     * Verify we don't crash when getting the last known cell id and there is no telephony.
+     */
+    @Test
+    public void testGetLastKnownCellIdWhenNoTelephony() {
+        ConnectionServiceWrapper wrapper = new ConnectionServiceWrapper(
+                ComponentName.unflattenFromString("foo/baz"),
+                mock(ConnectionServiceRepository.class),
+                mock(PhoneAccountRegistrar.class),
+                mock(CallsManager.class),
+                mContext,
+                new TelecomSystem.SyncRoot() {},
+                UserHandle.CURRENT,
+                mock(FeatureFlags.class));
+        when(mComponentContextFixture.getTelephonyManager().getLastKnownCellIdentity())
+                .thenThrow(new UnsupportedOperationException("Bee boop"));
+        assertNull(wrapper.getLastKnownCellIdentity());
+   }
+}
diff --git a/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java b/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java
index 2f27bb5..5bfacaa 100644
--- a/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java
+++ b/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java
@@ -16,8 +16,12 @@
 
 package com.android.server.telecom.tests;
 
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyList;
 import static org.mockito.ArgumentMatchers.nullable;
 import static org.mockito.ArgumentMatchers.any;
@@ -35,6 +39,7 @@
 import android.net.Uri;
 import android.os.Binder;
 import android.os.UserHandle;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.telecom.DisconnectCause;
 import android.telecom.PhoneAccount;
 import android.telecom.PhoneAccountHandle;
@@ -42,6 +47,7 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.internal.telephony.flags.Flags;
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallIdMapper;
 import com.android.server.telecom.ConnectionServiceFocusManager;
@@ -49,11 +55,13 @@
 import com.android.server.telecom.ConnectionServiceWrapper;
 import com.android.server.telecom.CreateConnectionProcessor;
 import com.android.server.telecom.CreateConnectionResponse;
+import com.android.server.telecom.CreateConnectionTimeout;
 import com.android.server.telecom.PhoneAccountRegistrar;
 import com.android.server.telecom.flags.FeatureFlags;
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -73,6 +81,7 @@
  */
 @RunWith(JUnit4.class)
 public class CreateConnectionProcessorTest extends TelecomTestCase {
+    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
 
     private static final String TEST_PACKAGE = "com.android.server.telecom.tests";
     private static final String TEST_CLASS =
@@ -91,10 +100,11 @@
     ConnectionServiceFocusManager mConnectionServiceFocusManager;
 
     CreateConnectionProcessor mTestCreateConnectionProcessor;
+    CreateConnectionTimeout mTestCreateConnectionTimeout;
 
     private ArrayList<PhoneAccount> phoneAccounts;
-    private HashMap<Integer,Integer> mSubToSlot;
-    private HashMap<PhoneAccount,Integer> mAccountToSub;
+    private HashMap<Integer, Integer> mSubToSlot;
+    private HashMap<PhoneAccount, Integer> mAccountToSub;
 
     @Override
     @Before
@@ -117,7 +127,7 @@
                          return null;
                      }
                  }
-                ).when(mConnectionServiceFocusManager).requestFocus(any(), any());
+        ).when(mConnectionServiceFocusManager).requestFocus(any(), any());
 
         mTestCreateConnectionProcessor = new CreateConnectionProcessor(mMockCall,
                 mMockConnectionServiceRepository, mMockCreateConnectionResponse,
@@ -144,6 +154,11 @@
                 .thenReturn(phoneAccounts);
         when(mMockCall.getAssociatedUser()).
                 thenReturn(Binder.getCallingUserHandle());
+
+        mTestCreateConnectionTimeout = new CreateConnectionTimeout(mContext, mMockAccountRegistrar,
+                makeConnectionServiceWrapper(), mMockCall);
+
+        mSetFlagsRule.enableFlags(Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG);
     }
 
     @Override
@@ -300,7 +315,8 @@
 
     /**
      * Ensure that when a test emergency number is being dialed and we restrict the usable
-     * PhoneAccounts using {@link PhoneAccountRegistrar#filterRestrictedPhoneAccounts(List)}, the
+     * PhoneAccounts using {@link PhoneAccountRegistrar#filterRestrictedPhoneAccounts(List)},
+     * the
      * test emergency call is sent on the filtered PhoneAccount.
      */
     @SmallTest
@@ -338,7 +354,8 @@
     }
 
     /**
-     * Ensure that when no phone accounts (visible to the user) are available for the call, we use
+     * Ensure that when no phone accounts (visible to the user) are available for the call, we
+     * use
      * an available sim from other another user (on the condition that the user has the
      * INTERACT_ACROSS_USERS permission).
      */
@@ -368,7 +385,8 @@
     }
 
     /**
-     * Ensure that the non-emergency capable PhoneAccount and the SIM manager is not chosen to place
+     * Ensure that the non-emergency capable PhoneAccount and the SIM manager is not chosen to
+     * place
      * the emergency call if there is an emergency capable PhoneAccount available as well.
      */
     @SmallTest
@@ -405,7 +423,8 @@
     }
 
     /**
-     * 1) Ensure that if there is a non-SIM PhoneAccount, it is not chosen as the Phone Account to
+     * 1) Ensure that if there is a non-SIM PhoneAccount, it is not chosen as the Phone Account
+     * to
      * dial the emergency call.
      * 2) Register multiple emergency capable PhoneAccounts. Since there is not preference, we
      * default to sending on the lowest slot.
@@ -445,8 +464,10 @@
     }
 
     /**
-     * Ensure that the call goes out on the PhoneAccount that has the CAPABILITY_EMERGENCY_PREFERRED
-     * capability, even if the user specifically chose the other emergency capable PhoneAccount.
+     * Ensure that the call goes out on the PhoneAccount that has the
+     * CAPABILITY_EMERGENCY_PREFERRED
+     * capability, even if the user specifically chose the other emergency capable
+     * PhoneAccount.
      */
     @SmallTest
     @Test
@@ -478,7 +499,8 @@
     }
 
     /**
-     * If there is no phone account with CAPABILITY_EMERGENCY_PREFERRED capability, choose the user
+     * If there is no phone account with CAPABILITY_EMERGENCY_PREFERRED capability, choose the
+     * user
      * chosen target account.
      */
     @SmallTest
@@ -552,7 +574,8 @@
     }
 
     /**
-     * If the user preferred PhoneAccount is associated with an invalid slot, place on the other,
+     * If the user preferred PhoneAccount is associated with an invalid slot, place on the
+     * other,
      * valid slot.
      */
     @SmallTest
@@ -690,7 +713,8 @@
 
     /**
      * Tests to verify that the
-     * {@link CreateConnectionProcessor#sortSimPhoneAccountsForEmergency(List, PhoneAccount)} can
+     * {@link CreateConnectionProcessor#sortSimPhoneAccountsForEmergency(List, PhoneAccount)}
+     * can
      * successfully sort without running into sort issues related to the hashcodes of the
      * PhoneAccounts.
      */
@@ -707,6 +731,127 @@
         }
     }
 
+    @Test
+    public void testIsTimeoutNeededForCall_nonEmergencyCall() {
+        when(mMockCall.isEmergencyCall()).thenReturn(false);
+
+        assertFalse(mTestCreateConnectionTimeout.isTimeoutNeededForCall(null, null));
+    }
+
+    @Test
+    public void testIsTimeoutNeededForCall_noConnectionManager() {
+        when(mMockCall.isEmergencyCall()).thenReturn(true);
+        List<PhoneAccountHandle> phoneAccountHandles = new ArrayList<>();
+        // Put in a regular phone account handle
+        PhoneAccount regularAccount = makePhoneAccount("tel_acct1",
+                PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION);
+        phoneAccountHandles.add(regularAccount.getAccountHandle());
+        // Create a connection manager for the call and do not include in phoneAccountHandles
+        createNewConnectionManagerPhoneAccountForCall(mMockCall, "cm_acct", 0);
+
+        assertFalse(mTestCreateConnectionTimeout.isTimeoutNeededForCall(phoneAccountHandles, null));
+    }
+
+    @Test
+    public void testIsTimeoutNeededForCall_usingConnectionManager() {
+        when(mMockCall.isEmergencyCall()).thenReturn(true);
+        List<PhoneAccountHandle> phoneAccountHandles = new ArrayList<>();
+        // Put in a regular phone account handle
+        PhoneAccount regularAccount = makePhoneAccount("tel_acct1",
+                PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION);
+        phoneAccountHandles.add(regularAccount.getAccountHandle());
+        // Create a connection manager for the call and include it in phoneAccountHandles
+        PhoneAccount callManagerPA = createNewConnectionManagerPhoneAccountForCall(mMockCall,
+                "cm_acct", 0);
+        phoneAccountHandles.add(callManagerPA.getAccountHandle());
+
+        assertFalse(mTestCreateConnectionTimeout.isTimeoutNeededForCall(
+                phoneAccountHandles, callManagerPA.getAccountHandle()));
+    }
+
+    @Test
+    public void testIsTimeoutNeededForCall_NotSystemSimCallManager() {
+        when(mMockCall.isEmergencyCall()).thenReturn(true);
+        List<PhoneAccountHandle> phoneAccountHandles = new ArrayList<>();
+        // Put in a regular phone account handle
+        PhoneAccount regularAccount = makePhoneAccount("tel_acct1",
+                PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION);
+        phoneAccountHandles.add(regularAccount.getAccountHandle());
+        // Create a connection manager for the call and include it in phoneAccountHandles
+        PhoneAccount callManagerPA = createNewConnectionManagerPhoneAccountForCall(mMockCall,
+                "cm_acct", 0);
+        phoneAccountHandles.add(callManagerPA.getAccountHandle());
+
+        assertFalse(mTestCreateConnectionTimeout.isTimeoutNeededForCall(phoneAccountHandles, null));
+    }
+
+    @Test
+    public void testIsTimeoutNeededForCall_carrierNotInService() {
+        when(mMockCall.isEmergencyCall()).thenReturn(true);
+        when(mMockAccountRegistrar.getSystemSimCallManagerComponent()).thenReturn(
+                new ComponentName(TEST_PACKAGE, TEST_CLASS));
+
+        List<PhoneAccountHandle> phoneAccountHandles = new ArrayList<>();
+        // Put in a regular phone account handle
+        PhoneAccount regularAccount = makePhoneAccount("tel_acct1",
+                PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION);
+        phoneAccountHandles.add(regularAccount.getAccountHandle());
+        // Create a connection manager for the call and include it in phoneAccountHandles
+        int capability = PhoneAccount.CAPABILITY_SUPPORTS_VOICE_CALLING_INDICATIONS;
+        PhoneAccount callManagerPA = createNewConnectionManagerPhoneAccountForCall(mMockCall,
+                "cm_acct", capability);
+        PhoneAccount phoneAccountWithoutService = makeQuickAccount("cm_acct", capability, null);
+        when(mMockAccountRegistrar.getPhoneAccount(callManagerPA.getAccountHandle(),
+                callManagerPA.getAccountHandle().getUserHandle()))
+                .thenReturn(phoneAccountWithoutService);
+        phoneAccountHandles.add(callManagerPA.getAccountHandle());
+
+        assertFalse(mTestCreateConnectionTimeout.isTimeoutNeededForCall(phoneAccountHandles, null));
+    }
+
+    @Test
+    public void testIsTimeoutNeededForCall() {
+        when(mMockCall.isEmergencyCall()).thenReturn(true);
+        when(mMockAccountRegistrar.getSystemSimCallManagerComponent()).thenReturn(
+                new ComponentName(TEST_PACKAGE, TEST_CLASS));
+
+        List<PhoneAccountHandle> phoneAccountHandles = new ArrayList<>();
+        // Put in a regular phone account handle
+        PhoneAccount regularAccount = makePhoneAccount("tel_acct1",
+                PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION);
+        phoneAccountHandles.add(regularAccount.getAccountHandle());
+        // Create a connection manager for the call and include it in phoneAccountHandles
+        int capability = PhoneAccount.CAPABILITY_SUPPORTS_VOICE_CALLING_INDICATIONS
+                | PhoneAccount.CAPABILITY_VOICE_CALLING_AVAILABLE;
+        PhoneAccount callManagerPA = createNewConnectionManagerPhoneAccountForCall(mMockCall,
+                "cm_acct", capability);
+        PhoneAccount phoneAccountWithService = makeQuickAccount("cm_acct", capability, null);
+        when(mMockAccountRegistrar.getPhoneAccount(callManagerPA.getAccountHandle(),
+                callManagerPA.getAccountHandle().getUserHandle()))
+                .thenReturn(phoneAccountWithService);
+        phoneAccountHandles.add(callManagerPA.getAccountHandle());
+
+        assertTrue(mTestCreateConnectionTimeout.isTimeoutNeededForCall(phoneAccountHandles, null));
+    }
+
+    /**
+     * Verifies when telephony is not available that we just get invalid sub id for a phone acct.
+     */
+    @SmallTest
+    @Test
+    public void testTelephonyAdapterWhenNoTelephony() {
+        PhoneAccount telephonyAcct = makePhoneAccount("test-acct",
+                PhoneAccount.CAPABILITY_CALL_PROVIDER
+                        | PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS);
+
+        CreateConnectionProcessor.ITelephonyManagerAdapterImpl impl
+                = new CreateConnectionProcessor.ITelephonyManagerAdapterImpl();
+        when(mComponentContextFixture.getTelephonyManager().
+                getSubscriptionId(any(PhoneAccountHandle.class)))
+                .thenThrow(new UnsupportedOperationException("Bee boop"));
+        assertEquals(-1, impl.getSubIdForPhoneAccount(mContext, telephonyAcct));
+    }
+
     /**
      * Generates random phone accounts.
      * @param seed random seed to use for random UUIDs; passed in for determinism.
diff --git a/tests/src/com/android/server/telecom/tests/DisconnectedCallNotifierTest.java b/tests/src/com/android/server/telecom/tests/DisconnectedCallNotifierTest.java
index 05c5071..3eacc54 100644
--- a/tests/src/com/android/server/telecom/tests/DisconnectedCallNotifierTest.java
+++ b/tests/src/com/android/server/telecom/tests/DisconnectedCallNotifierTest.java
@@ -146,6 +146,19 @@
         verify(mNotificationManager).cancelAsUser(anyString(), anyInt(), any());
     }
 
+    /**
+     * Verifies when there is no telephony available, that we'll still be able to determine a
+     * country iso.
+     */
+    @Test
+    @SmallTest
+    public void testGetCountryIsoWithNoTelephony() {
+        DisconnectedCallNotifier notifier = new DisconnectedCallNotifier(mContext, mCallsManager);
+        when(mComponentContextFixture.getTelephonyManager().getNetworkCountryIso())
+                .thenThrow(new UnsupportedOperationException("Bee boop"));
+        assertNotNull(notifier.getCurrentCountryIso(mContext));
+    }
+
     private Call createCall(DisconnectCause cause) {
         Call call = mock(Call.class);
         when(call.getDisconnectCause()).thenReturn(cause);
diff --git a/tests/src/com/android/server/telecom/tests/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 d93a251..9d87aaf 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;
@@ -1869,7 +1870,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());
@@ -1965,6 +1966,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);
diff --git a/tests/src/com/android/server/telecom/tests/PhoneStateBroadcasterTest.java b/tests/src/com/android/server/telecom/tests/PhoneStateBroadcasterTest.java
new file mode 100644
index 0000000..b18c2ce
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/PhoneStateBroadcasterTest.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom.tests;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.ComponentName;
+import android.net.Uri;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.SubscriptionManager;
+import android.telephony.emergency.EmergencyNumber;
+import android.util.ArrayMap;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.PhoneStateBroadcaster;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+@RunWith(JUnit4.class)
+public class PhoneStateBroadcasterTest extends TelecomTestCase {
+    @Override
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+    }
+
+    @Override
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    /**
+     * Tests behavior where FEATURE_TELEPHONY_CALLING is not available, but
+     * FEATURE_TELEPHONY_SUBSCRIPTION is; in this case we can't detect that the number is emergency
+     * so we will not bother sending out anything.
+     */
+    @Test
+    public void testNotifyOutgoingEmergencyCallWithNoTelephonyCalling() {
+        CallsManager cm = mock(CallsManager.class);
+        when(cm.getContext()).thenReturn(mContext);
+        when(mComponentContextFixture.getTelephonyManager().isEmergencyNumber(anyString()))
+                .thenThrow(new UnsupportedOperationException("Bee boop"));
+        PhoneStateBroadcaster psb = new PhoneStateBroadcaster(cm);
+
+        Call call = mock(Call.class);
+        when(call.isExternalCall()).thenReturn(false);
+        when(call.isEmergencyCall()).thenReturn(true);
+        when(call.isIncoming()).thenReturn(false);
+        when(call.getHandle()).thenReturn(Uri.parse("tel:911"));
+
+        psb.onCallAdded(call);
+        verify(mComponentContextFixture.getTelephonyRegistryManager(), never())
+                .notifyOutgoingEmergencyCall(anyInt(), anyInt(), any(EmergencyNumber.class));
+    }
+
+    /**
+     * Tests behavior where FEATURE_TELEPHONY_CALLING is available, but
+     * FEATURE_TELEPHONY_SUBSCRIPTION is; in this case we can detect that this is an emergency
+     * call, but we can't figure out any of the subscription parameters.  It is doubtful we'd ever
+     * see this in practice since technically FEATURE_TELEPHONY_CALLING needs
+     * FEATURE_TELEPHONY_SUBSCRIPTION.
+     */
+    @Test
+    public void testNotifyOutgoingEmergencyCallWithNoTelephonySubscription() {
+        CallsManager cm = mock(CallsManager.class);
+        when(cm.getContext()).thenReturn(mContext);
+        Map<Integer, List<EmergencyNumber>> nums = new ArrayMap<Integer, List<EmergencyNumber>>();
+        nums.put(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID,
+                Arrays.asList(new EmergencyNumber("911", "US", null, 0, Collections.EMPTY_LIST,
+                        0, 0)));
+        when(mComponentContextFixture.getTelephonyManager().getEmergencyNumberList())
+                .thenReturn(nums);
+        when(mComponentContextFixture.getTelephonyManager().getSubscriptionId(any(
+                        PhoneAccountHandle.class)))
+                .thenThrow(new UnsupportedOperationException("Bee boop"));
+        PhoneStateBroadcaster psb = new PhoneStateBroadcaster(cm);
+
+        Call call = mock(Call.class);
+        when(call.isExternalCall()).thenReturn(false);
+        when(call.isEmergencyCall()).thenReturn(true);
+        when(call.isIncoming()).thenReturn(false);
+        when(call.getHandle()).thenReturn(Uri.parse("tel:911"));
+        when(call.getTargetPhoneAccount()).thenReturn(new PhoneAccountHandle(
+                ComponentName.unflattenFromString("foo/bar"), "90210"));
+
+        psb.onCallAdded(call);
+        verify(mComponentContextFixture.getTelephonyRegistryManager())
+                .notifyOutgoingEmergencyCall(eq(SubscriptionManager.INVALID_SIM_SLOT_INDEX),
+                        eq(SubscriptionManager.INVALID_SUBSCRIPTION_ID),
+                        any(EmergencyNumber.class));
+    }
+}
diff --git a/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java b/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java
index a36e8ea..d1dd20c 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java
@@ -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/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..b5a0c26 100644
--- a/tests/src/com/android/server/telecom/tests/TransactionTests.java
+++ b/tests/src/com/android/server/telecom/tests/TransactionTests.java
@@ -17,11 +17,13 @@
 package com.android.server.telecom.tests;
 
 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;
@@ -61,6 +63,7 @@
 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.VoipCallTransactionResult;
 
@@ -271,27 +274,24 @@
      */
     @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 +303,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..30cfc2e 100644
--- a/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java
+++ b/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java
@@ -61,6 +61,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);
@@ -96,6 +97,11 @@
             }, mSleepTime);
             return resultFuture;
         }
+
+        @Override
+        public void finishTransaction() {
+            isFinished = true;
+        }
     }
 
     @Override
@@ -109,7 +115,6 @@
     @Override
     @After
     public void tearDown() throws Exception {
-        Log.i("Grace", mLog.toString());
         mTransactionManager.clear();
         super.tearDown();
     }
@@ -119,11 +124,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 +142,7 @@
         assertEquals(VoipCallTransactionResult.RESULT_SUCCEED,
                 resultFuture.get(5000L, TimeUnit.MILLISECONDS).getResult());
         assertEquals(expectedLog, mLog.toString());
+        verifyTransactionsFinished(t1, t2, t3);
     }
 
     @SmallTest
@@ -144,11 +150,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 +177,7 @@
         exceptionFuture.get(5000L, TimeUnit.MILLISECONDS);
         String expectedLog = "t1 success;\nt2 failed;\n";
         assertEquals(expectedLog, mLog.toString());
+        verifyTransactionsFinished(t1, t2, t3);
     }
 
     @SmallTest
@@ -178,11 +185,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 +205,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 +213,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 +239,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 +264,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 +300,18 @@
         assertEquals(VoipCallTransactionResult.RESULT_SUCCEED,
                 resultFuture.get(5000L, TimeUnit.MILLISECONDS).getResult());
         assertEquals(expectedLog, mLog.toString());
+        verifyTransactionsFinished(t1, t2);
     }
 
     @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 +346,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);
+        }
     }
 }