[automerger skipped] Merge 25Q1 (ab/12770256) to aosp-main-future am: a025046919 -s ours

am skip reason: Merged-In I0b2b75f5a2532aaba0e0f53a1e32bcb544e4638c with SHA-1 9f30609a1d is already in history

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

Change-Id: I5b3ebdb61ab700136db7cfbd65975dc7acc1c187
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 09ebfe2..1355343 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -47,11 +47,9 @@
           "exclude-annotation": "androidx.test.filters.FlakyTest"
         }
       ]
-    }
-  ],
-  "presubmit-large": [
+    },
     {
-      "name": "CtsTelecomTestCases",
+      "name": "CtsTelecomCujTestCases",
       "options": [
         {
           "exclude-annotation": "androidx.test.filters.FlakyTest"
@@ -59,9 +57,9 @@
       ]
     }
   ],
-  "postsubmit": [
+  "presubmit-large": [
     {
-      "name": "CtsTelecomCujTestCases",
+      "name": "CtsTelecomTestCases",
       "options": [
         {
           "exclude-annotation": "androidx.test.filters.FlakyTest"
diff --git a/flags/Android.bp b/flags/Android.bp
index 54b1443..6f9caae 100644
--- a/flags/Android.bp
+++ b/flags/Android.bp
@@ -46,5 +46,6 @@
         "telecom_headless_system_user_mode.aconfig",
         "telecom_session_flags.aconfig",
         "telecom_metrics_flags.aconfig",
+        "telecom_voip_flags.aconfig",
     ],
 }
diff --git a/flags/telecom_anomaly_report_flags.aconfig b/flags/telecom_anomaly_report_flags.aconfig
index 5d42b86..bc248c8 100644
--- a/flags/telecom_anomaly_report_flags.aconfig
+++ b/flags/telecom_anomaly_report_flags.aconfig
@@ -27,3 +27,11 @@
     purpose: PURPOSE_BUGFIX
   }
 }
+
+# OWNER=tjstuart TARGET=25Q2
+flag {
+  name: "enable_call_exception_anom_reports"
+  namespace: "telecom"
+  description: "When a new CallException is created, generate an anomaly report for metrics"
+  bug: "308932906"
+}
diff --git a/flags/telecom_bluetoothdevicemanager_flags.aconfig b/flags/telecom_bluetoothdevicemanager_flags.aconfig
index 5dd5831..1c8bd0c 100644
--- a/flags/telecom_bluetoothdevicemanager_flags.aconfig
+++ b/flags/telecom_bluetoothdevicemanager_flags.aconfig
@@ -18,3 +18,13 @@
     purpose: PURPOSE_BUGFIX
   }
 }
+# OWNER=grantmenke TARGET=25Q2
+flag {
+  name: "skip_baseline_switch_when_route_not_bluetooth"
+  namespace: "telecom"
+  description: "Only switch back to baseline if the call audio is currently routed to bluetooth"
+  bug: "333417369"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
\ No newline at end of file
diff --git a/flags/telecom_callaudioroutestatemachine_flags.aconfig b/flags/telecom_callaudioroutestatemachine_flags.aconfig
index a60c0f1..ae146f8 100644
--- a/flags/telecom_callaudioroutestatemachine_flags.aconfig
+++ b/flags/telecom_callaudioroutestatemachine_flags.aconfig
@@ -89,6 +89,17 @@
   bug: "315865533"
 }
 
+# OWNER=tgunn TARGET=24Q3
+flag {
+  name: "dont_use_communication_device_tracker"
+  namespace: "telecom"
+  description: "Do not use the communication device tracker with useRefactoredAudioRouteSwitching."
+  bug: "346472575"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
+
 # OWNER=pmadapurmath TARGET=24Q3
 flag {
   name: "resolve_switching_bt_devices_computation"
@@ -129,3 +140,47 @@
     purpose: PURPOSE_BUGFIX
   }
 }
+
+# OWNER=tgunn TARGET=25Q2
+flag {
+  name: "only_clear_communication_device_on_inactive"
+  namespace: "telecom"
+  description: "Only clear the communication device when transitioning to an inactive route."
+  bug: "376781369"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
+
+# OWNER=tgunn TARGET=25Q2
+flag {
+  name: "check_device_type_on_route_change"
+  namespace: "telecom"
+  description: "When comparing devices on route change, also consider device type."
+  bug: "388509460"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
+
+# OWNER=pmadapurmath TARGET=25Q2
+flag {
+  name: "default_speaker_on_wired_headset_disconnect"
+  namespace: "telecom"
+  description: "Maybe route back into speaker (if it was the previous route) when a wired headset disconnects"
+  bug: "376364368"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
+
+# OWNER=tgunn TARGET=25Q2
+flag {
+  name: "bus_device_is_a_speaker"
+  namespace: "telecom"
+  description: "Treat TYPE_BUS devices like TYPE_SPEAKER"
+  bug: "395647782"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
diff --git a/flags/telecom_calls_manager_flags.aconfig b/flags/telecom_calls_manager_flags.aconfig
index f46e844..6b8b772 100644
--- a/flags/telecom_calls_manager_flags.aconfig
+++ b/flags/telecom_calls_manager_flags.aconfig
@@ -35,3 +35,14 @@
     purpose: PURPOSE_BUGFIX
   }
 }
+
+# OWNER=tgunn TARGET=25Q2
+flag {
+  name: "enable_call_audio_watchdog"
+  namespace: "telecom"
+  description: "Enables tracking of audio resources for voip calls to aid in diagnostics."
+  bug: "384570270"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
diff --git a/flags/telecom_voip_flags.aconfig b/flags/telecom_voip_flags.aconfig
new file mode 100644
index 0000000..67635e9
--- /dev/null
+++ b/flags/telecom_voip_flags.aconfig
@@ -0,0 +1,13 @@
+package: "com.android.server.telecom.flags"
+container: "system"
+
+# OWNER=tjstuart TARGET=25Q2
+flag {
+  name: "voip_call_monitor_refactor"
+  namespace: "telecom"
+  description: "VoipCallMonitor reworked to handle multi calling scenarios for the same app"
+  bug: "381129034"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
\ No newline at end of file
diff --git a/proto/pulled_atoms.proto b/proto/pulled_atoms.proto
index 6c9af46..2916dad 100644
--- a/proto/pulled_atoms.proto
+++ b/proto/pulled_atoms.proto
@@ -14,6 +14,8 @@
   optional int64 telecom_api_stats_pull_timestamp_millis = 6;
   repeated TelecomErrorStats telecom_error_stats = 7;
   optional int64 telecom_error_stats_pull_timestamp_millis = 8;
+  repeated TelecomEventStats telecom_event_stats = 9;
+  optional int64 telecom_event_stats_pull_timestamp_millis = 10;
 }
 
 /**
@@ -48,6 +50,15 @@
 
     // Average elapsed time between CALL_STATE_ACTIVE to CALL_STATE_DISCONNECTED.
     optional int32 average_duration_ms = 8;
+
+    // The disconnect cause of the call. Eg. ERROR, LOCAL, REMOTE, etc.
+    // From frameworks/proto_logging/stats/enums/telecomm/enums.proto
+    optional int32 disconnect_cause = 9;
+
+    // The type of simultaneous call type. Eg. SINGLE, DUAL_SAME_ACCOUNT,
+    // DUAL_DIFF_ACCOUNT, etc.
+    // From frameworks/proto_logging/stats/enums/telecomm/enums.proto
+    optional int32 simultaneous_type = 10;
 }
 
 /**
@@ -112,3 +123,22 @@
     // The number of times this error occurs
     optional int32 count = 3;
 }
+
+/**
+ * Pulled atom to capture stats of Telecom critical events
+ */
+message TelecomEventStats {
+    // The event name
+    // From frameworks/proto_logging/stats/enums/telecomm/enums.proto
+    optional int32 event = 1;
+
+    // UID of the caller. This is always -1/unknown for the private space.
+    optional int32 uid = 2;
+
+    // The cause related to the event
+    // From frameworks/proto_logging/stats/enums/telecomm/enums.proto
+    optional int32 event_cause = 3;
+
+    // The number of times this event occurs
+    optional int32 count = 4;
+}
diff --git a/res/values-af/strings.xml b/res/values-af/strings.xml
index 71564e8..2226ac2 100644
--- a/res/values-af/strings.xml
+++ b/res/values-af/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Stroom oudio na ander toestel"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Beëindig oproep"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Skakel hier oor"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Kan nie ’n oproep maak nie omdat daar reeds twee oproepe aan die gang is. Beëindig een van die oproepe of voeg dit saam in ’n konferensie voordat ’n nuwe oproep gemaak word."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Kan nie ’n oproep maak nie omdat daar ’n oproep is wat nie aangehou kan word nie. Beëindig die oproep voordat ’n nuwe oproep gemaak word."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Kan nie ’n oproep maak nie, aangesien daar ’n onbeantwoorde inkomende oproep is. Beantwoord of weier die inkomende oproep voordat jy ’n nuwe oproep plaas."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"Hierdie MMI-kode is nie beskikbaar vir oproepe tussen verskeie rekeninge nie."</string>
 </resources>
diff --git a/res/values-am/strings.xml b/res/values-am/strings.xml
index dafbe6e..8958a5f 100644
--- a/res/values-am/strings.xml
+++ b/res/values-am/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"ኦዲዮን ወደ ሌላ መሣሪያ በመልቀቅ ላይ"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"ዝጋ"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"እዚህ ቀይር"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"ቀድሞውኑ ሁለት ጥሪዎች በሂደት ላይ ስለሆኑ ጥሪ ማድረግ አልተቻለም። አዲስ ጥሪ ከማድረግዎ በፊት ከጥሪዎቹ ላይ የአንዱን ግንኙነት ያቋርጡ ወይም ወደ ጉባዔ ያዋህዷቸው።"</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"ይቆይ ሊደረግ የማይችል ጥሪ በመኖሩ ጥሪ ማድረግ አልተቻለም። አዲስ ጥሪ ከማድረግዎ በፊት የጥሪውን ግንኙነት ያቋርጡ።"</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"ያልተመለሰ ገቢ ጥሪ ስላለ ጥሪ ማድረግ አይቻልም። አዲስ ጥሪ ከማድረግዎ በፊት ገቢ ጥሪን ይመልሱ ወይም ይዝጉ።"</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"ይህ MMI ኮድ በርካታ መለያዎች ላይ ላሉ ጥሪዎች አይገኝም።"</string>
 </resources>
diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml
index 9eb3a35..a3f1d0b 100644
--- a/res/values-ar/strings.xml
+++ b/res/values-ar/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"بث الصوت على جهاز آخر"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"قطع الاتصال"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"الانتقال إلى هنا"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"لا يمكن إجراء مكالمة لأنّ هناك مكالمتين جاريتين حاليًا. يُرجى إنهاء إحدى المكالمتين أو دمجهما في مكالمة جماعية قبل إجراء مكالمة جديدة."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"أنت في مكالمة غير قابلة للتعليق، لذا لا يمكن إجراء مكالمة أخرى. يُرجى إنهاء المكالمة الحالية لإجراء مكالمة جديدة."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"لا يمكن إجراء مكالمة لأن هناك مكالمة واردة لم يتم الرد عليها. يُرجى الرد على المكالمة الواردة أو رفضها قبل إجراء مكالمة جديدة."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"‏لا يتوفّر رمز MMI هذا للمكالمات على مستوى حسابات متعددة."</string>
 </resources>
diff --git a/res/values-as/strings.xml b/res/values-as/strings.xml
index 668f5e5..ff62402 100644
--- a/res/values-as/strings.xml
+++ b/res/values-as/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"অন্য এটা ডিভাইচলৈ অডিঅ’ ষ্ট্ৰীম কৰি থকা হৈছে"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"কলটো কাটি দিয়ক"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"ইয়াত সলনি কৰক"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"কল কৰিব নোৱাৰি, কাৰণ ইতিমধ্যে দুটা কল চলি আছে। এটা নতুন কল কৰাৰ আগতে সেই দুটা কলৰ এটাৰ সংযোগ বিচ্ছিন্ন কৰক বা কল দুটা একত্ৰিত কৰি এটা কনফাৰেন্স কললৈ সলনি কৰক।"</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"হ’ল্ডত ৰাখিব নোৱাৰা কল এটা চলি থকাৰ বাবে কল কৰিব নোৱাৰি। এটা নতুন কল কৰাৰ আগেয়ে কলটোৰ সংযোগ বিচ্ছিন্ন কৰক।"</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"কল কৰিব নোৱাৰি, কাৰণ এটা অন্তৰ্গামী কল প্ৰগতিত আছে যাৰ উত্তৰ দিয়া হোৱা নাই। এটা নতুন কল কৰাৰ আগেয়ে অন্তৰ্গামী কলটোৰ উত্তৰ দিয়ক বা সেইটো প্ৰত্যাখ্যান কৰক।"</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"একাধিক একাউণ্টৰ মাজত কল কৰাৰ বাবে এই MMI ক’ডটো উপলব্ধ নহয়।"</string>
 </resources>
diff --git a/res/values-az/strings.xml b/res/values-az/strings.xml
index c975159..b39586a 100644
--- a/res/values-az/strings.xml
+++ b/res/values-az/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Audio digər cihaza ötürülür"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Zəngi sonlandırın"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Buraya keçin"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Davam edən iki zəng olduğuna görə zəng etmək mümkün deyil. Yeni zəng etməzdən əvvəl zənglərin birini dayandırın və ya onları konfransa birləşdirin."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Canlı zəngi dayandırmaq mümkün olmadığına görə yeni zəng etmək olmur. Yeni zəng etməzdən əvvəl digər zəngi dayandırın."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Cavabsız gələn zəng olduğuna görə zəng etmək mümkün deyil. Yeni zəngə başlamazdan əvvəl gələn zəngə cavab verin və ya rədd edin."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"Bu MMI kodu birdən çox hesab üzrə zənglər üçün əlçatan deyil."</string>
 </resources>
diff --git a/res/values-b+sr+Latn/strings.xml b/res/values-b+sr+Latn/strings.xml
index 3709c25..d062a0a 100644
--- a/res/values-b+sr+Latn/strings.xml
+++ b/res/values-b+sr+Latn/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Zvuk se strimuje na drugi uređaj"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Prekini vezu"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Prebaci ovde"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Ne možete da uputite poziv jer su dva poziva već u toku. Prekinite jedan od njih ili ih objedinite u konferenciju da biste uputili novi poziv."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Ne možete da uputite poziv jer je u toku poziv koji ne može da se stavi na čekanje. Prekinite taj poziv pre upućivanja novog poziva."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Ne možete da pozovete jer imate dolazni poziv na koji niste odgovorili. Primite ga ili odbijte pre upućivanja novog poziva."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"Ovaj MMI kôd nije dostupan za pozive na više naloga."</string>
 </resources>
diff --git a/res/values-be/strings.xml b/res/values-be/strings.xml
index c3c6e2f..65ab4e7 100644
--- a/res/values-be/strings.xml
+++ b/res/values-be/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Перадача аўдыя плынню на іншую прыладу"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Завяршыць выклік"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Пераключыцца"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Немагчыма зрабіць новы выклік, бо ўжо выконваюцца два іншыя. Каб зрабіць новы выклік, завяршыце адзін з бягучых ці аб’яднайце іх у канферэнц-выклік."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Немагчыма зрабіць выклік, бо ўжо выконваецца выклік, які нельга пераключыць у рэжым утрымання. Перш чым зрабіць новы выклік, завяршыце актыўны."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Нельга зрабіць выклік, паколькі ёсць уваходны выклік без адказу. Адкажыце на ўваходны выклік або адхіліце яго, каб зрабіць новы."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"Гэты код MMI недаступны для выклікаў паміж некалькімі ўліковымі запісамі."</string>
 </resources>
diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml
index 116c884..7cd6ef2 100644
--- a/res/values-bg/strings.xml
+++ b/res/values-bg/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Звукът се предава поточно към друго устройство"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Затваряне"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Превключете тук"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Не може да се извърши обаждане, тъй като вече се провеждат две обаждания. Прекъснете едно от тях или ги обединете в конферентен разговор, преди да извършите ново обаждане."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Не може да се извърши обаждане, тъй като има обаждане, което не може да бъде поставено на изчакване. Прекъснете обаждането, преди да извършите ново."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Не може да се извърши обаждане, тъй като има неотговорено входящо обаждане. Отговорете му или го отхвърлете, преди да извършите ново обаждане."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"Този MMI код не е налице, докато се провежда обаждане в друг профил."</string>
 </resources>
diff --git a/res/values-bn/strings.xml b/res/values-bn/strings.xml
index 4f4fea6..54c5e10 100644
--- a/res/values-bn/strings.xml
+++ b/res/values-bn/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"অন্য ডিভাইসে অডিও স্ট্রিম করা হচ্ছে"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"কল কেটে দিন"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"এখানে পাল্টান"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"দুটি কল চলছে, তাই আরেকটি কল করা যাচ্ছে না। নতুন কল করার আগে যেকোনও একটি কল কেটে দিন অথবা দুটিকে একসাথে একটি কনফারেন্সে মার্জ করুন।"</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"হোল্ড করা যাবে না এমন কল রয়েছে তাই আরেকটি কল করা যাচ্ছে না। নতুন কল করার আগে কলটি ডিসকানেক্ট করুন।"</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"উত্তর দেওয়া হয়নি এমন একটি ইনকামিং কল রয়েছে, তাই কল করা যাচ্ছে না। নতুন কল করার আগে ইনকামিং কলটির উত্তর দিন বা সেটি বাতিল করুন।"</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"একাধিক অ্যাকাউন্ট জুড়ে কলের জন্য এই MMI কোড উপলভ্য নেই।"</string>
 </resources>
diff --git a/res/values-bs/strings.xml b/res/values-bs/strings.xml
index ba75d0c..1ffa430 100644
--- a/res/values-bs/strings.xml
+++ b/res/values-bs/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Prenos zvuka na drugom uređaju"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Prekini vezu"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Prebaci ovdje"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Nije moguće uputiti poziv jer su dva poziva već u toku. Prekinite jedan od njih ili ih spojite u konferencijski poziv prije upućivanja novog poziva."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Nije moguće uputiti poziv zbog poziva koji se ne može staviti na čekanje. Prekinite taj poziv prije upućivanja novog poziva."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Nije moguće uputiti poziv zbog neodgovorenog dolaznog poziva. Odgovorite ili odbijte dolazni poziv prije upućivanja novog poziva."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"MMI kôd nije dostupan za pozive na više računa."</string>
 </resources>
diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml
index 5793449..8090189 100644
--- a/res/values-ca/strings.xml
+++ b/res/values-ca/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"S\'està reproduint àudio en continu en un altre dispositiu"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Penja"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Canvia aquí"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"No es pot fer la trucada perquè ja n\'hi ha dues en curs. Desconnecta\'n una o combina-les en una conferència abans de fer-ne més de noves."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"No es pot fer la trucada perquè n\'hi ha una que no es pot posar en espera. Desconnecta-la abans de fer-ne més de noves."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"No es pot fer la trucada perquè hi ha una trucada entrant sense resposta. Respon-hi o rebutja-la abans de fer més trucades."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"Aquest codi MMI no es pot utilitzar per fer trucades amb diversos comptes."</string>
 </resources>
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
index 0619308..8b1ca9b 100644
--- a/res/values-cs/strings.xml
+++ b/res/values-cs/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Streamování zvuku do druhého zařízení"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Zavěsit"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Přepnout sem"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Nemůžete uskutečnit hovor, protože už probíhají dva hovory. Než zahájíte nový hovor, jeden ze stávajících odpojte nebo je slučte do konference."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Nemůžete uskutečnit hovor, protože už probíhá hovor, který nelze podržet. Než zahájíte nový hovor, odpojte ten předchozí."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Nemůžete uskutečnit hovor, protože máte nepřijatý příchozí hovor. Nejdřív ho tedy přijměte nebo odmítněte."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"Tento kód MMI není k dispozici pro hovory ve více účtech."</string>
 </resources>
diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml
index 0eb69ff..2ac4e6f 100644
--- a/res/values-da/strings.xml
+++ b/res/values-da/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Streamer lyd til en anden enhed"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Læg på"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Skift hertil"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Der kan ikke foretages et opkald, fordi der allerede er to igangværende opkald. Afslut et af opkaldene, eller flet dem til et telefonmøde, før du foretager et nyt opkald."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Opkaldet kan ikke foretages, fordi der er et opkald i gang, som ikke kan sættes på hold. Afslut opkaldet, før du foretager et nyt."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Der kan ikke foretages et opkald, fordi et indgående opkald ringer. Besvar eller afvis det indgående opkald, før du foretager et nyt opkald."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"Denne MMI-kode er ikke tilgængelig for opkald på tværs af flere konti."</string>
 </resources>
diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml
index 665124a..d0dde7d 100644
--- a/res/values-de/strings.xml
+++ b/res/values-de/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Audio auf einem anderen Gerät streamen"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Anruf beenden"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Auf dieses Gerät wechseln"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Anruf nicht möglich, weil bereits zwei Anrufe aktiv sind. Beende einen der Anrufe oder führe beide Anrufe in einer Telefonkonferenz zusammen, bevor du einen neuen Anruf startest."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Anruf nicht möglich, da ein Anruf nicht gehalten werden kann. Beende den Anruf, bevor du einen neuen Anruf startest."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Anruf nicht möglich, da ein nicht angenommener eingehender Anruf vorhanden ist. Nimm den eingehenden Anruf an oder lehne ihn ab, bevor du einen neuen Anruf startest."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"Dieser MMI-Code ist nicht für Anrufe mit mehreren Konten verfügbar."</string>
 </resources>
diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml
index ba504d7..c7ce4b6 100644
--- a/res/values-el/strings.xml
+++ b/res/values-el/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Ροή ήχου σε άλλη συσκευή"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Απόρριψη"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Εναλλαγή εδώ"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Δεν είναι δυνατή η πραγματοποίηση κλήσης, επειδή υπάρχουν ήδη δύο κλήσεις σε εξέλιξη. Τερματίστε μια από τις κλήσεις ή συγχωνεύστε τις σε μια διάσκεψη, προτού πραγματοποιήσετε νέα κλήση."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Δεν είναι δυνατή η πραγματοποίηση κλήσης, επειδή υπάρχει κλήση που δεν μπορεί να τεθεί σε αναμονή. Τερματίστε την κλήση πριν πραγματοποιήσετε νέα κλήση."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Δεν είναι δυνατή η πραγματοποίηση κλήσης, επειδή υπάρχει αναπάντητη εισερχόμενη κλήση. Απαντήστε ή απορρίψτε την εισερχόμενη κλήση, προτού πραγματοποιήσετε μια νέα κλήση."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"Αυτός ο κωδικός MMI δεν είναι διαθέσιμος για κλήσεις σε πολλούς λογαριασμούς."</string>
 </resources>
diff --git a/res/values-en-rAU/strings.xml b/res/values-en-rAU/strings.xml
index 1ce62df..7f9023e 100644
--- a/res/values-en-rAU/strings.xml
+++ b/res/values-en-rAU/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Streaming audio to other device"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Hang up"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Switch here"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Cannot place a call as there are already two calls in progress. Disconnect one of the calls or merge them into a conference prior to placing a new call."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Cannot place a call as there is an unholdable call. Disconnect the call prior to placing a new call."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Cannot place a call as there is an unanswered incoming call. Answer or reject the incoming call prior to placing a new call."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"This MMI code is not available for calls across multiple accounts."</string>
 </resources>
diff --git a/res/values-en-rCA/strings.xml b/res/values-en-rCA/strings.xml
index 8ae9c0a..b3908d7 100644
--- a/res/values-en-rCA/strings.xml
+++ b/res/values-en-rCA/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Streaming audio to other device"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Hang up"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Switch here"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Cannot place a call as there are already two calls in progress. Disconnect one of the calls or merge them into a conference prior to placing a new call."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Cannot place a call as there is an unholdable call. Disconnect the call prior to placing a new call."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Cannot place a call as there is an unanswered incoming call. Answer or reject the incoming call prior to placing a new call."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"This MMI code is not available for calls across multiple accounts."</string>
 </resources>
diff --git a/res/values-en-rGB/strings.xml b/res/values-en-rGB/strings.xml
index 1ce62df..7f9023e 100644
--- a/res/values-en-rGB/strings.xml
+++ b/res/values-en-rGB/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Streaming audio to other device"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Hang up"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Switch here"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Cannot place a call as there are already two calls in progress. Disconnect one of the calls or merge them into a conference prior to placing a new call."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Cannot place a call as there is an unholdable call. Disconnect the call prior to placing a new call."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Cannot place a call as there is an unanswered incoming call. Answer or reject the incoming call prior to placing a new call."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"This MMI code is not available for calls across multiple accounts."</string>
 </resources>
diff --git a/res/values-en-rIN/strings.xml b/res/values-en-rIN/strings.xml
index 1ce62df..7f9023e 100644
--- a/res/values-en-rIN/strings.xml
+++ b/res/values-en-rIN/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Streaming audio to other device"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Hang up"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Switch here"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Cannot place a call as there are already two calls in progress. Disconnect one of the calls or merge them into a conference prior to placing a new call."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Cannot place a call as there is an unholdable call. Disconnect the call prior to placing a new call."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Cannot place a call as there is an unanswered incoming call. Answer or reject the incoming call prior to placing a new call."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"This MMI code is not available for calls across multiple accounts."</string>
 </resources>
diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml
index 668a696..52c4575 100644
--- a/res/values-es-rUS/strings.xml
+++ b/res/values-es-rUS/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Transmitiendo el audio a otro dispositivo"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Colgar"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Cambiar aquí"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"No puedes realizar la llamada porque hay otras dos en curso. Finaliza una de ellas o combínalas en una conferencia antes de iniciar una nueva."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"No puedes realizar la llamada porque hay otra que no se puede mantener en espera. Finalízala antes de iniciar una nueva."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"No puedes realizar la llamada porque hay una llamada entrante que aún no contestas. Contéstala o recházala antes de realizar una nueva."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"Este código MMI no está disponible para llamadas en varias cuentas."</string>
 </resources>
diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml
index 96163b3..c36fb9b 100644
--- a/res/values-es/strings.xml
+++ b/res/values-es/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Transmitiendo audio a otro dispositivo"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Colgar"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Cambiar aquí"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"No se puede llamar porque ya hay dos llamadas en curso. Interrumpe una de ellas o combínalas en una conferencia antes de hacer otra llamada."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"No se puede hacer una llamada porque ya hay otra que no se puede poner en espera. Interrumpe la llamada antes de hacer otra."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"No se puede llamar porque hay una llamada entrante sin responder. Contéstala o recházala antes de hacer otra llamada."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"Este código MMI no está disponible para hacer llamadas con varias cuentas."</string>
 </resources>
diff --git a/res/values-et/strings.xml b/res/values-et/strings.xml
index 6fd5592..bdcde33 100644
--- a/res/values-et/strings.xml
+++ b/res/values-et/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Heli voogesitamine teise seadmesse"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Lõpeta kõne"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Vaheta siia"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Kõnet ei saa teha, kuna kaks kõnet on juba pooleli. Enne uue kõne tegemist katkestage üks kõnedest või liitke need konverentskõneks."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Uut kõnet ei saa teha, kuna pooleliolevat kõnet ei saa ootele panna. Enne uue kõne tegemist katkestage pooleliolev kõne."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Kõnet ei saa teha, kuna teil on vastamata sissetulev kõne. Enne uue kõne tegemist vastake sissetulevale kõnele või keelduge sellest."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"See MMI-kood pole saadaval mitmel kontol toimuvate kõnede jaoks."</string>
 </resources>
diff --git a/res/values-eu/strings.xml b/res/values-eu/strings.xml
index 3efbc07..0de1b5c 100644
--- a/res/values-eu/strings.xml
+++ b/res/values-eu/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Audioa beste gailu batera igortzen ari da"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Amaitu deia"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Aldatu hona"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Ezin da egin deia, dagoeneko 2 dei daudelako abian. Beste dei bat egin aurretik, eten deietako bat edo bateratu deiak konferentzia-dei bakarrean."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Ezin da egin deia, zain utzi ezin den dei bat abian delako. Deskonektatu dei hori beste dei bat egin ahal izateko."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Ezin da egin deia, oraindik erantzun ez diozun dei bat jasotzen ari zarelako. Beste dei bat egin aurretik, erantzun deiari edo bazter ezazu."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"MMI kode hau ezin da erabili kontu baten baino gehiagoren bidez deiak egiteko."</string>
 </resources>
diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml
index 6bd2ff6..9d6e55c 100644
--- a/res/values-fa/strings.xml
+++ b/res/values-fa/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"درحال جاری‌سازی صدا به دستگاه دیگر"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"قطع تماس"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"انتقال در اینجا انجام شود"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"نمی‌توانید تماسی برقرار کنید، زیرا هم‌اکنون دو تماس دیگر درحال انجام است. قبل‌از برقراری تماس جدید، یکی از تماس‌ها را قطع کنید یا آن‌ها را به‌صورت کنفرانسی ادغام کنید."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"نمی‌توانید تماسی برقرار کنید زیرا هم‌اکنون تماسی بدون قابلیت انتظار درحال انجام است. قبل‌از برقراری تماس جدید، تماس را قطع کنید."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"نمی‌توانید تماسی برقرار کنید، چون تماس ورودی بی‌پاسخی درحال انجام است. قبل‌از برقراری تماس جدید، به تماس ورودی پاسخ دهید یا آن را رد کنید."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"‏این کد MMI برای تماس در چندین حساب دردسترس نیست."</string>
 </resources>
diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml
index 0d5fdbb..19eeb1e 100644
--- a/res/values-fi/strings.xml
+++ b/res/values-fi/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Audiota striimataan toiselle laitteelle"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Lopeta puhelu"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Vaihda puhelimeen"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Puhelua ei voi soittaa, koska kaksi puhelua on jo käynnissä. Katkaise toinen puheluista tai yhdistä ne puhelinneuvotteluksi ennen uuden puhelun soittamista."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Puhelua ei voi soittaa, koska puhelua ei voi asettaa pitoon. Katkaise puhelu ennen uuden puhelun soittamista."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Puhelua ei voi soittaa, koska saapuvaan puheluun ei ole vielä vastattu. Vastaa saapuvaan puheluun tai hylkää se, ennen kuin soitat uuden puhelun."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"MMI-koodi ei ole käytettävissä useilla tileillä käytävissä puheluissa."</string>
 </resources>
diff --git a/res/values-fr-rCA/strings.xml b/res/values-fr-rCA/strings.xml
index cfd153b..420575f 100644
--- a/res/values-fr-rCA/strings.xml
+++ b/res/values-fr-rCA/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Diffusion audio en continu vers un autre appareil en cours…"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Raccrocher"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Revenir à cet appareil"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Impossible de passer un appel parce que deux appels sont déjà en cours. Déconnectez-en un ou fusionnez-les en conférence téléphonique avant de passer un nouvel appel."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Impossible de passer un appel parce qu\'un appel impossible à mettre en attente est en cours. Débranchez l\'appel avant de passer un nouvel appel."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Impossible de passer un appel parce qu\'un appel entrant attend une réponse. Répondez à cet appel ou refusez-le avant de passer un nouvel appel."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"Ce code IHM n\'est pas disponible pour les appels utilisant plusieurs comptes."</string>
 </resources>
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
index 9dbca8f..69e25e4 100644
--- a/res/values-fr/strings.xml
+++ b/res/values-fr/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Streaming de l\'audio sur un autre appareil"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Raccrocher"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Passer ici"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Impossible de passer un appel, car deux appels sont déjà en cours. Mettez fin à l\'un des appels ou fusionnez-les afin de créer une conférence avant de passer un nouvel appel."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Impossible de passer un appel, car un appel est en cours et ne peut pas être mis en attente. Mettez fin à l\'appel avant de passer un nouvel appel."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Impossible de passer un appel lorsqu\'un appel entrant attend une réponse. Répondez à cet appel ou refusez-le avant de passer un nouvel appel."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"Ce code IHM n\'est pas disponible pour les appels sur plusieurs comptes."</string>
 </resources>
diff --git a/res/values-gl/strings.xml b/res/values-gl/strings.xml
index f8eb32c..257c351 100644
--- a/res/values-gl/strings.xml
+++ b/res/values-gl/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Emitindo audio noutro dispositivo"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Colgar"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Volver aquí"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Non se pode facer ningunha chamada porque xa hai dúas en curso. Para poder facer unha nova, desconecta unha desas dúas ou combínaas nunha conferencia."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Non se pode facer ningunha chamada porque hai unha que non é posible poñer en espera. Desconéctaa para poder facer unha nova."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Non podes chamar porque te están chamando nestes momentos. Para poder facer unha chamada, primeiro tes que responder á outra ou rexeitala."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"Este código MMI non está dispoñible para chamadas en varias contas."</string>
 </resources>
diff --git a/res/values-gu/strings.xml b/res/values-gu/strings.xml
index dd04bcf..e9dc8d7 100644
--- a/res/values-gu/strings.xml
+++ b/res/values-gu/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"ઑડિયોને અન્ય ડિવાઇસ પર સ્ટ્રીમ કરી રહ્યાં છીએ"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"સમાપ્ત કરો"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"અહીં સ્વિચ કરો"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"કૉલ કરી શકાતો નથી, કારણ કે બે કૉલ પહેલેથી ચાલુ છે. કોઈ નવો કૉલ કરતા પહેલાં તેમાંના એક કૉલને ડિસ્કનેક્ટ કરો અથવા તેમને કોઈ કૉન્ફરન્સમાં મર્જ કરો."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"કૉલ કરી શકાતો નથી, કારણ કે હોલ્ડ ન કરી શકાય તેવો કોઈ કૉલ ચાલુ છે. કોઈ નવો કૉલ કરતા પહેલાં કૉલને ડિસ્કનેક્ટ કરો."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"કૉલ કરી શકાતો નથી કારણ કે ઇનકમિંગ કૉલનો જવાબ આપવામાં આવી રહ્યો નથી. નવો કૉલ કરતા પહેલાં ઇનકમિંગ કૉલનો જવાબ આપો અથવા તેને નકારો."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"આ MMI કોડનો ઉપયોગ એકથી વધુ એકાઉન્ટ પર ચાલી રહેલા કૉલ માટે ઉપલબ્ધ નથી."</string>
 </resources>
diff --git a/res/values-hi/strings.xml b/res/values-hi/strings.xml
index 683a5ab..a712f2f 100644
--- a/res/values-hi/strings.xml
+++ b/res/values-hi/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"ऑडियो को दूसरे डिवाइस पर स्ट्रीम किया जा रहा है"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"कॉल खत्म करें"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"यहां स्विच करें"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"कॉल नहीं किया जा सका, क्योंकि पहले से ही दो कॉल जारी हैं. नया कॉल करने से पहले, उनमें से किसी एक कॉल को बंद करें या उन्हें कॉन्फ़्रेंस कॉल में मर्ज करें."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"कॉल नहीं किया जा सकता, क्योंकि पहले से चल रहे कॉल को होल्ड नहीं किया जा सकता. नया कॉल करने से पहले, मौजूदा कॉल को डिसकनेक्ट करें."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"कॉल नहीं किया जा सकता, क्योंकि एक इनकमिंग कॉल का जवाब नहीं दिया जा रहा है. नया कॉल करने से पहले इनकमिंग कॉल का जवाब दें या उसे अस्वीकार करें."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"किसी दूसरे खाते पर चल रहे कॉल के दौरान, इस एमएमआई कोड का इस्तेमाल नहीं किया जा सकता."</string>
 </resources>
diff --git a/res/values-hr/strings.xml b/res/values-hr/strings.xml
index b664e5c..96c4a14 100644
--- a/res/values-hr/strings.xml
+++ b/res/values-hr/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Streaming zvuka na drugi uređaj"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Prekini vezu"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Promijeni ovdje"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Poziv se ne može uputiti jer već su dva poziva u tijeku. Prije upućivanja novog poziva prekinite jedan od ta dva poziva ili ih spojite u konferencijski poziv."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Poziv se ne može uputiti jer je u tijeku poziv koji se ne može zadržati. Prekinite taj poziv prije upućivanja novog."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Poziv se ne može uputiti jer je aktivan neodgovoreni dolazni poziv. Odgovorite ili odbijte dolazni poziv prije upućivanja novog poziva."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"Taj MMI kôd nije dostupan za pozive na više računa."</string>
 </resources>
diff --git a/res/values-hu/strings.xml b/res/values-hu/strings.xml
index 0a0c377..433d793 100644
--- a/res/values-hu/strings.xml
+++ b/res/values-hu/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Hang átvitele másik eszközre"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Hívás befejezése"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Váltás itt"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Nem kezdeményezhet hívást, mert már két hívás van folyamatban. Mielőtt új hívást indítana, tegye le az egyiket, vagy egyesítse őket egy konferenciahívásban."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Nem kezdeményezhet hívást, mert folyamatban van egy nem tartható hívás. Mielőtt új hívást indítana, szakítsa meg a hívást."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Nem kezdeményezhet hívást, mert folyamatban van egy megválaszolatlan bejövő hívás. Mielőtt új hívást indítana, vegye fel vagy utasítsa el a bejövő hívást."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"Ez az MMI-kód nem áll rendelkezésre hívásokhoz több fiók használata esetén."</string>
 </resources>
diff --git a/res/values-hy/strings.xml b/res/values-hy/strings.xml
index 7f877c5..27c9af6 100644
--- a/res/values-hy/strings.xml
+++ b/res/values-hy/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Աուդիոյի հեռարձակում այլ սարքում"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Ավարտել զանգը"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Անցնել այստեղ"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Հնարավոր չէ զանգել, քանի որ արդեն երկու ընթացիկ զանգ կա։ Նախքան նոր զանգ կատարելը ավարտեք զանգերից մեկը կամ միավորեք դրանք մեկ խմբային զանգում։"</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Հնարավոր չէ զանգել, քանի որ ընթացիկ զանգը չի կարելի սպասման մեջ դնել։ Նախքան նոր զանգ կատարելը պատասխանեք ավարտեք այս զանգը։"</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Հնարավոր չէ զանգել, քանի որ անպատասխան մուտքային զանգ կա։ Նախքան նոր զանգ կատարելը պատասխանեք մուտքային զանգին կամ մերժեք այն։"</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"Այս MMI կոդը հասանելի չէ մի քանի հաշիվների օգտագործմամբ զանգերի համար։"</string>
 </resources>
diff --git a/res/values-in/strings.xml b/res/values-in/strings.xml
index 34c0c66..182122e 100644
--- a/res/values-in/strings.xml
+++ b/res/values-in/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Streaming audio ke perangkat lain"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Akhiri"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Beralih ke sini"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Tidak dapat melakukan panggilan karena ada dua panggilan yang sedang berlangsung. Putuskan salah satu panggilan atau gabungkan keduanya menjadi satu konferensi sebelum melakukan panggilan baru."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Tidak dapat melakukan panggilan karena ada panggilan yang tidak dapat ditahan. Putuskan panggilan sebelum melakukan panggilan baru."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Tidak dapat melakukan panggilan karena ada panggilan masuk yang belum terjawab. Jawab atau tolak panggilan masuk sebelum melakukan panggilan baru."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"Kode MMI ini tidak tersedia untuk panggilan di beberapa akun."</string>
 </resources>
diff --git a/res/values-is/strings.xml b/res/values-is/strings.xml
index c2fcf8f..8eb91d5 100644
--- a/res/values-is/strings.xml
+++ b/res/values-is/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Streymir hljóði í annað tæki"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Leggja á"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Skipta hingað"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Ekki er hægt að hringja símtal vegna þess að þegar eru tvö símtöl í gangi. Aftengdu annað símtalið eða sameinaðu þau í símafund áður en þú hringir nýtt símtal."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Ekki er hægt að hringja símtal vegna símtals sem ekki er hægt að setja í bið. Slíttu símtalinu áður en þú hringir nýtt símtal."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Ekki er hægt að hringja símtal, þar sem ósvarað símtal er að berast. Svaraðu eða hafnaðu símtalinu áður en þú hringir nýtt símtal."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"Þessi MMI-kóði er ekki í boði ef hringt er í marga reikninga."</string>
 </resources>
diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml
index 42cb0c8..ef95c4b 100644
--- a/res/values-it/strings.xml
+++ b/res/values-it/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Streaming audio all\'altro dispositivo"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Riaggancia"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Passa qui"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Impossibile effettuare una chiamata perché due chiamate sono già in corso. Unisci le chiamate in una conferenza o scollegane una prima di effettuare una nuova chiamata."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Impossibile effettuare una chiamata perché è presente una chiamata non bloccabile. Termina la chiamata prima di effettuarne una nuova."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Impossibile effettuare una chiamata perché è presente una chiamata in arrivo senza risposta. Rispondi o rifiuta la chiamata in arrivo prima di effettuare una nuova chiamata."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"Questo codice MMI non è disponibile per le chiamate su più account."</string>
 </resources>
diff --git a/res/values-iw/strings.xml b/res/values-iw/strings.xml
index 98c5347..9fd02ff 100644
--- a/res/values-iw/strings.xml
+++ b/res/values-iw/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"הקול מושמע במכשיר אחר"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"ניתוק"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"העברת השיחה בחזרה לטלפון"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"אי אפשר להתקשר כי כבר יש שתי שיחות פעילות. צריך לנתק את אחת מהשיחות או למזג אותן לשיחת ועידה ורק אז לנסות להתקשר למספר אחר."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"אי אפשר להתקשר כי כבר יש שיחה פעילה ואי אפשר להעביר אותה להמתנה. צריך לנתק את השיחה ורק אז לנסות להתקשר למספר אחר."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"אי אפשר להתקשר כי יש שיחה נכנסת אחרת. צריך לענות לשיחה או לדחות אותה ורק אז לנסות להתקשר למספר אחר."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"‏אי אפשר להשתמש בקוד ה-MMI הזה לשיחות במספר חשבונות."</string>
 </resources>
diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml
index 2df6736..2d47db0 100644
--- a/res/values-ja/strings.xml
+++ b/res/values-ja/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"他のデバイスに音声をストリーミングしています"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"通話を終了"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"このデバイスに切り替える"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"進行中の通話がすでに 2 件あるため、新しく通話を発信することはできません。進行中の通話のどちらかを終了するか、2 件の通話を統合してグループ通話にすると、新しく通話を発信できるようになります。"</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"保留できない通話があるため、新しく通話を発信できません。通話を終了すると、新しく通話を発信できるようになります。"</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"通話を着信中のため、新しく通話を発信することはできません。着信中の通話に応答するか、通話を拒否すると、新しく通話を発信できるようになります。"</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"この MMI コードは、複数のアカウントにまたがる通話には使用できません。"</string>
 </resources>
diff --git a/res/values-ka/strings.xml b/res/values-ka/strings.xml
index f2a3e90..912a313 100644
--- a/res/values-ka/strings.xml
+++ b/res/values-ka/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"მიმდინარეობს აუდიოს სტრიმინგი სხვა მოწყობილობაზე"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"გათიშვა"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"გადართვა"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"ზარის განხორციელება შეუძლებელია, რადგან უკვე ორი ზარი მიმდინარეობს. ახალი ზარის განსახორციელებლად გათიშეთ ერთ-ერთი ზარი ან გააერთიანეთ ისინი კონფერენციად."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"ზარის განხორციელება ვერ ხერხდება, რადგან მიმდინარეობს ზარი, რომლის მოცდის რეჟიმში გადაყვანაც შეუძლებელია. ახალი ზარის განსახორციელებლად გათიშეთ აღნიშნული ზარი."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"ზარის განხორციელება შეუძლებელია, რადგან გაქვთ უპასუხო შემომავალი ზარი. უპასუხეთ ან უარყავით შემომავალი ზარი ახალი ზარის განხორციელებამდე."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"ეს MMI კოდი ხელმისაწვდომი არ არის სხვადასხვა ანგარიშზე ზარების განხორცილებისას."</string>
 </resources>
diff --git a/res/values-kk/strings.xml b/res/values-kk/strings.xml
index 22ac1fc..1bda0aa 100644
--- a/res/values-kk/strings.xml
+++ b/res/values-kk/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Аудионы басқа құрылғыға трансляциялау"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Қоңырауды аяқтау"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Осы жерде ауысу"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Қоңырау шалу мүмкін емес, себебі онсыз да екі қоңырау жүріп жатыр. Жаңа қоңырау бастау үшін олардың біреуін тоқтатыңыз немесе екеуін бір конференцияға біріктіріңіз."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Қоңырау шалу мүмкін емес, себебі жүріп жатқан қоңырау кідіртілмейді. Жаңадан қоңырау шалу үшін жүріп жатқан қоңырауды тоқтатыңыз."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Қоңырау шалу мүмкін емес, себебі жауап бермеген кіріс қоңырау бар. Жаңа қоңырау шалу үшін кіріс қоңырауға жауап беріңіз немесе оны қабылдамаңыз."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"Бұл MMI кодын бірнеше аккаунттағы қоңыраулар үшін пайдалану мүмкін емес."</string>
 </resources>
diff --git a/res/values-km/strings.xml b/res/values-km/strings.xml
index 41b02f3..4525dd4 100644
--- a/res/values-km/strings.xml
+++ b/res/values-km/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"កំពុង​ផ្សាយ​សំឡេង​ទៅឧបករណ៍​ផ្សេងទៀត"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"បញ្ចប់​ការហៅ​ទូរសព្ទ"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"ប្ដូរនៅទីនេះ"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"មិន​អាច​ធ្វើ​ការហៅ​ទូរសព្ទ​បាន​ទេ ដោយសារ​មាន​ការហៅ​ទូរសព្ទ​ពីរ​កំពុង​ដំណើរការ​រួច​ហើយ។ ផ្ដាច់​ការហៅ​ទូរសព្ទ​មួយ ឬ​ដាក់​ការហៅ​ទូរសព្ទ​ទាំងនេះ​ចូល​គ្នា​ជា​ការហៅជាក្រុម មុន​នឹង​ធ្វើ​ការហៅ​ទូរសព្ទ​ថ្មី។"</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"មិនអាច​ធ្វើការហៅ​ទូរសព្ទ​បានទេ ដោយសារ​មានការហៅ​ទូរសព្ទ​ដែលមិនអាច​ដាក់ឱ្យ​រង់ចាំបាន។ សូមផ្ដាច់​ការហៅ​ទូរសព្ទ​នោះសិន មុនពេល​ធ្វើការហៅ​ទូរសព្ទថ្មី។"</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"មិន​អាច​ធ្វើ​ការហៅ​ទូរសព្ទ​បាន​ទេ ដោយសារ​មាន​ការហៅ​ចូល​មួយ​ដែល​មិន​បាន​ឆ្លើយតប។ ឆ្លើយតប ឬ​ច្រានចោល​ការហៅ​ចូល មុន​ពេល​ធ្វើ​ការហៅ​ទូរសព្ទ​ថ្មី។"</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"មិនអាចប្រើលេខកូដ MMI នេះសម្រាប់ការហៅទូរសព្ទនៅលើ​គណនី​ច្រើនបានទេ។"</string>
 </resources>
diff --git a/res/values-kn/strings.xml b/res/values-kn/strings.xml
index 886ccdf..982952b 100644
--- a/res/values-kn/strings.xml
+++ b/res/values-kn/strings.xml
@@ -54,13 +54,13 @@
     <string name="no_vm_number_msg" msgid="1339245731058529388">"ಸಿಮ್‌ ಕಾರ್ಡ್‌ನಲ್ಲಿ ಯಾವುದೇ ಧ್ವನಿಮೇಲ್‌ ಸಂಖ್ಯೆಯನ್ನು ಸಂಗ್ರಹಿಸಿಲ್ಲ."</string>
     <string name="add_vm_number_str" msgid="5179510133063168998">"ಸಂಖ್ಯೆಯನ್ನು ಸೇರಿಸಿ"</string>
     <string name="change_default_dialer_dialog_title" msgid="5861469279421508060">"<xliff:g id="NEW_APP">%s</xliff:g> ಅನ್ನು ನಿಮ್ಮ ಡಿಫಾಲ್ಟ್ ಫೋನ್ ಆ್ಯಪ್ ಆಗಿ ಮಾಡಬೇಕೆ?"</string>
-    <string name="change_default_dialer_dialog_affirmative" msgid="8604665314757739550">"ಡಿಫಾಲ್ಟ್ ಹೊಂದಿಸಿ"</string>
+    <string name="change_default_dialer_dialog_affirmative" msgid="8604665314757739550">"ಡಿಫಾಲ್ಟ್ ಸೆಟ್ ಮಾಡಿ"</string>
     <string name="change_default_dialer_dialog_negative" msgid="8648669840052697821">"ರದ್ದುಮಾಡಿ"</string>
     <string name="change_default_dialer_warning_message" msgid="8461963987376916114">"<xliff:g id="NEW_APP">%s</xliff:g> ಗೆ ನಿಮ್ಮ ಕರೆಗಳ ಎಲ್ಲಾ ಅಂಶಗಳನ್ನು ನಿಯಂತ್ರಿಸಲು ಮತ್ತು ಕರೆಗಳನ್ನು ಮಾಡಲು ಸಾಧ್ಯವಾಗುತ್ತದೆ. ನೀವು ವಿಶ್ವಾಸವಿರಿಸಿರುವಂತಹ ಆ್ಯಪ್‌ಗಳನ್ನು ಮಾತ್ರ ನಿಮ್ಮ ಡಿಫಾಲ್ಟ್ ಆ್ಯಪ್‌ ಆಗಿ ಹೊಂದಿಸಬೇಕು."</string>
     <string name="change_default_call_screening_dialog_title" msgid="5365787219927262408">"<xliff:g id="NEW_APP">%s</xliff:g> ನಿಮ್ಮ ಡೀಫಾಲ್ಟ್ ಕರೆ ಸ್ಕ್ರೀನಿಂಗ್ ಆ್ಯಪ್‌ ಆಗಿ ಮಾಡಬೇಕೇ?"</string>
     <string name="change_default_call_screening_warning_message_for_disable_old_app" msgid="2039830033533243164">"<xliff:g id="OLD_APP">%s</xliff:g> ಇನ್ನು ಮುಂದೆ ಕರೆಗಳನ್ನು ಸ್ಕ್ರೀನ್‌ ಮಾಡಲು ಸಾಧ್ಯವಾಗುವುದಿಲ್ಲ."</string>
     <string name="change_default_call_screening_warning_message" msgid="9020537562292754269">"<xliff:g id="NEW_APP">%s</xliff:g> ಗೆ ನಿಮ್ಮ ಸಂಪರ್ಕಗಳಲ್ಲಿ ಇಲ್ಲದ ಕರೆದಾರರ ಬಗ್ಗೆ ಮಾಹಿತಿಯನ್ನು ನೋಡಲು ಮತ್ತು ಈ ಕರೆಗಳನ್ನು ಬ್ಲಾಕ್ ಮಾಡಲು ಸಾಧ್ಯವಾಗುತ್ತದೆ. ನೀವು ವಿಶ್ವಾಸವಿರಿಸಿರುವಂತಹ ಆ್ಯಪ್‌ಗಳನ್ನು ಮಾತ್ರ ನಿಮ್ಮ ಡೀಫಾಲ್ಟ್ ಕರೆ ಸ್ಕ್ರೀನಿಂಗ್ ಆ್ಯಪ್‌ ಆಗಿ ಹೊಂದಿಸಬೇಕು."</string>
-    <string name="change_default_call_screening_dialog_affirmative" msgid="7162433828280058647">"ಡೀಫಾಲ್ಟ್ ಹೊಂದಿಸಿ"</string>
+    <string name="change_default_call_screening_dialog_affirmative" msgid="7162433828280058647">"ಡೀಫಾಲ್ಟ್ ಸೆಟ್ ಮಾಡಿ"</string>
     <string name="change_default_call_screening_dialog_negative" msgid="1839266125623106342">"ರದ್ದುಮಾಡಿ"</string>
     <string name="blocked_numbers" msgid="8322134197039865180">"ನಿರ್ಬಂಧಿಸಲಾದ ಸಂಖ್ಯೆಗಳು"</string>
     <string name="blocked_numbers_msg" msgid="2797422132329662697">"ನಿರ್ಬಂಧಿಸಲಾದ ಸಂಖ್ಯೆಗಳಿಂದ ಕರೆಗಳು ಅಥವಾ ಪಠ್ಯ ಸಂದೇಶಗಳನ್ನು ನೀವು ಸ್ವೀಕರಿಸುವುದಿಲ್ಲ."</string>
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"ಇತರ ಸಾಧನಕ್ಕೆ ಆಡಿಯೊವನ್ನು ಸ್ಟ್ರೀಮ್ ಮಾಡಲಾಗುತ್ತಿದೆ"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"ಹ್ಯಾಂಗ್ ಅಪ್"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"ಇಲ್ಲಿಗೆ ಬದಲಾಯಿಸಿ"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"ಈಗಾಗಲೇ ಎರಡು ಕರೆಗಳು ಪ್ರಗತಿಯಲ್ಲಿರುವುದರಿಂದ, ಕರೆ ಮಾಡಲು ಸಾಧ್ಯವಾಗುವುದಿಲ್ಲ. ಒಂದು ಕರೆಯನ್ನು ಡಿಸ್‌ಕನೆಕ್ಟ್ ಮಾಡಿ ಅಥವಾ ಹೊಸ ಕರೆಯನ್ನು ಮಾಡುವ ಮೊದಲು ಎರಡು ಕರೆಗಳನ್ನು ಒಂದೇ ಕಾನ್ಫರೆನ್ಸ್‌ನಲ್ಲಿ ವಿಲೀನಗೊಳಿಸಿ."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"ಈಗಾಗಲೇ ಪ್ರಗತಿಯಲ್ಲಿರುವ ಕರೆಯನ್ನು ಹೋಲ್ಡ್ ಮಾಡಲು ಸಾಧ್ಯವಿಲ್ಲದ ಕಾರಣ, ಕರೆ ಮಾಡಲು ಸಾಧ್ಯವಿಲ್ಲ. ಹೊಸ ಕರೆಯನ್ನು ಮಾಡುವ ಮೊದಲು ಕರೆಯನ್ನು ಡಿಸ್‌ಕನೆಕ್ಟ್ ಮಾಡಿ."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"ಉತ್ತರಿಸದ ಒಳಬರುವ ಕರೆ ಬರುತ್ತಿರುವುದರಿಂದ ಕರೆ ಮಾಡಲು ಸಾಧ್ಯವಾಗುವುದಿಲ್ಲ. ಹೊಸ ಕರೆಯನ್ನು ಮಾಡುವ ಮೊದಲು ಕರೆಗೆ ಉತ್ತರ ನೀಡಿ ಅಥವಾ ತಿರಸ್ಕರಿಸಿ."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"ಬಹು ಖಾತೆಗಳಾದ್ಯಂತ ಕರೆಗಳಿಗೆ ಈ MMI ಕೋಡ್ ಲಭ್ಯವಿರುವುದಿಲ್ಲ."</string>
 </resources>
diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml
index f0b95fd..1e4d035 100644
--- a/res/values-ko/strings.xml
+++ b/res/values-ko/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"다른 기기로 오디오 스트리밍"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"전화 끊기"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"현재 기기로 전환"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"이미 진행 중인 두 건의 통화가 있으므로 전화를 걸 수 없습니다. 새로 전화를 걸기 전에 통화 중 하나를 연결 해제하거나 두 통화를 다자간 통화로 병합하세요."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"대기할 수 없는 통화가 있으므로 전화를 걸 수 없습니다. 새로 전화를 걸기 전에 통화를 종료하세요."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"받지 않은 수신 전화가 있으므로 전화를 걸 수 없습니다. 새로 전화를 걸기 전에 수신 전화를 받거나 거절하세요."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"이 MMI 코드는 여러 계정 간에 통화에 사용할 수 없습니다."</string>
 </resources>
diff --git a/res/values-ky/strings.xml b/res/values-ky/strings.xml
index ad19dd7..9d6e263 100644
--- a/res/values-ky/strings.xml
+++ b/res/values-ky/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Аудио башка түзмөккө берилүүдө"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Чалууну бүтүрүү"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Бул жерге которулуу"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Чалуу аткарылбайт, анткени эки чалуу аткарылууда. Бир чалууну өчүрүңүз же аларды конференцияга бириктириңиз."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Чалуу мүмкүн эмес, анткени кармалбаган чалуу бар. Жаңы чалуудан мурда учурдагыны бүтүрүңүз."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Чалуу аткарылбайт, анткени кирүүчү чалууга жооп берилген жок. Жаңы чалуу аткаруудан мурун кирүүчү чалууга жооп бериңиз же четке кагыңыз."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"Бул MMI коду бир нече аккаунт аркылуу чалуулар үчүн жеткиликсиз."</string>
 </resources>
diff --git a/res/values-lo/strings.xml b/res/values-lo/strings.xml
index 8e43935..bf3a1b2 100644
--- a/res/values-lo/strings.xml
+++ b/res/values-lo/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"ສະຕຣີມສຽງໄປໃສ່ອຸປະກອນອື່ນ"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"ວາງສາຍ"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"ສະຫຼັບບ່ອນນີ້"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"ບໍ່ສາມາດໂທໄດ້ເນື່ອງຈາກມີສອງສາຍກຳລັງໂທຢູ່. ກະລຸນາຕັດການເຊື່ອມຕໍ່ສາຍໃດໜຶ່ງອອກ ຫຼື ຮວມສາຍເປັນການປະຊຸມທາງໂທລະສັບກ່ອນໂທໃໝ່."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"ບໍ່ສາມາດໂທໄດ້ເນື່ອງຈາກມີການໂທທີ່ບໍ່ສາມາດຖືສາຍຄ້າງໄວ້ໄດ້. ຕັດການເຊື່ອມຕໍ່ສາຍກ່ອນໂທໃໝ່."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"ບໍ່ສາມາດໂທອອກໄດ້ເນື່ອງຈາກມີສາຍໂທເຂົ້າທີ່ຍັງບໍ່ໄດ້ຮັບຢູ່. ກະລຸນາຮັບສາຍ ຫຼື ວາງສາຍທີ່ກຳລັງໂທເຂົ້າມາກ່ອນຈະໂທໃໝ່."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"ລະຫັດ MMI ນີ້ແມ່ນໃຊ້ບໍ່ໄດ້ສໍາລັບການໂທດ້ວຍຫຼາຍບັນຊີ."</string>
 </resources>
diff --git a/res/values-lt/strings.xml b/res/values-lt/strings.xml
index 04f4c96..d71e257 100644
--- a/res/values-lt/strings.xml
+++ b/res/values-lt/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Srautinis garso perdavimas į kitą įrenginį"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Baigti skambutį"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Perjungti čia"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Negalite skambinti, nes jau dalyvaujate dviejuose skambučiuose. Prieš pradėdami naują skambutį užbaikite vieną iš skambučių arba sujunkite juos į konferenciją."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Negalite skambinti, nes yra skambutis, kurio negalima sulaikyti. Prieš pradėdami naują skambutį nutraukite esamą."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Negalite skambinti, nes yra neatsakytas gaunamasis skambutis. Atsiliepkite arba atmeskite gaunamąjį skambutį prieš pradėdami naują."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"Šis MMI kodas nepasiekiamas skambučiuose keliose paskyrose."</string>
 </resources>
diff --git a/res/values-lv/strings.xml b/res/values-lv/strings.xml
index ee807da..61d1fac 100644
--- a/res/values-lv/strings.xml
+++ b/res/values-lv/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Notiek audio straumēšana uz citu ierīci."</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Beigt zvanu"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Pārslēgties šeit"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Nevar veikt zvanu, jo pašlaik jau notiek divi zvani. Pirms jauna zvana veikšanas pārtrauciet vienu no pašreizējiem zvaniem vai apvienojiet tos konferences zvanā."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Nevar veikt zvanu, jo pašlaik notiek zvans, ko nevar pārtraukt. Pirms jauna zvana veikšanas pārtrauciet pašreizējo zvanu."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Nevar veikt zvanu, jo ir neatbildēts ienākošais zvans. Pirms jauna zvana veikšanas atbildiet uz ienākošo zvanu vai noraidiet to."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"Šis MMI kods nav pieejams zvaniem vairākos kontos."</string>
 </resources>
diff --git a/res/values-mk/strings.xml b/res/values-mk/strings.xml
index 0f6e41f..7a7e7a1 100644
--- a/res/values-mk/strings.xml
+++ b/res/values-mk/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Звукот се стримува на друг уред"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Спушти"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Префрли овде"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Не може да се воспостави повик бидејќи веќе се во тек два повика. Исклучете го едниот од повиците или спојте ги во конференциски повик пред да воспоставите нов повик."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Не може да се воспостави повик бидејќи има повик што не може да се стави на чекање. Исклучете го повикот пред да воспоставите нов повик."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Не може да се воспостави повик затоа што има неодговорен дојдовен повик. Одговорете или одбијте го дојдовниот повик пред воспоставувањето на новиот повик."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"MMI-кодов не е достапен за повици на повеќе сметки."</string>
 </resources>
diff --git a/res/values-ml/strings.xml b/res/values-ml/strings.xml
index 1301b44..7562eea 100644
--- a/res/values-ml/strings.xml
+++ b/res/values-ml/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"ഓഡിയോ മറ്റൊരു ഉപകരണത്തിലേക്ക് സ്‌ട്രീം ചെയ്യുന്നു"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"മാറ്റി വയ്‌ക്കുക"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"ഇവിടേക്ക് മാറുക"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"രണ്ട് കോളുകൾ നിലവിൽ പുരോഗമിക്കുന്നതിനാൽ, ഇനിയൊരു കോൾ കൂടി ചെയ്യാനാകില്ല. പുതിയൊരു കോൾ ചെയ്യുന്നതിന് മുമ്പ്, കോളുകളിലൊരെണ്ണം വിച്ഛേദിക്കുകയോ അവ കോൺഫറൻസ് കോളായി ലയിപ്പിക്കുകയോ ചെയ്യുക."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"ഹോൾഡ് ചെയ്യാനാകാത്ത കോൾ പുരോഗമിക്കുന്നതിനാൽ ഇനിയൊരു കോൾ കൂടി ചെയ്യാനാകില്ല. പുതിയൊരു കോൾ ചെയ്യുന്നതിന് മുമ്പ് കോൾ വിച്ഛേദിക്കുക."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"മറുപടി ലഭിക്കാത്ത ഒരു ഇൻ‌കമിംഗ് കോൾ ഉള്ളതിനാൽ, പുതിയൊരു കോൾ ചെയ്യാനാവില്ല. പുതിയ കോൾ ചെയ്യുന്നതിന് മുമ്പ് ഇൻകമിംഗ് കോളിന് മറുപടി നൽകുകയോ നിരസിക്കുകയോ ചെയ്യുക."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"ഒന്നിലധികം അക്കൗണ്ടുകളിലുടനീളം കോളുകൾക്ക് ഈ MMI കോഡ് ലഭ്യമല്ല."</string>
 </resources>
diff --git a/res/values-mn/strings.xml b/res/values-mn/strings.xml
index 0b26e7e..d46ece2 100644
--- a/res/values-mn/strings.xml
+++ b/res/values-mn/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Бусад төхөөрөмж рүү аудио дамжуулж байна"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Таслах"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Ийшээ сэлгэх"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Аль хэдийн хоёр дуудлага хийж байгаа тул дуудлага хийх боломжгүй байна. Шинэ дуудлага хийхийн өмнө аль нэг дуудлагыг салгах эсвэл тэдгээрийг хурал болгож нэгтгэнэ үү."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Хүлээлгэх боломжгүй дуудлага байгаа тул дуудлага хийх боломжгүй. Шинэ дуудлага хийхээсээ өмнө тухайн дуудлагыг салгана уу."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Хариулаагүй ирсэн дуудлага байгаа тул дуудлага хийх боломжгүй. Шинэ дуудлага хийхээсээ өмнө ирсэн дуудлагад хариулах эсвэл тасална уу."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"Энэ MMI код нь олон бүртгэл дээрх дуудлагад боломжгүй."</string>
 </resources>
diff --git a/res/values-mr/strings.xml b/res/values-mr/strings.xml
index eca7b4d..c93a665 100644
--- a/res/values-mr/strings.xml
+++ b/res/values-mr/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"ऑडिओ हा दुसऱ्या डिव्हाइसवर स्ट्रीम करत आहे"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"बंद करा"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"येथे स्विच करा"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"दोन कॉल आधीच प्रगतीपथावर असल्यामुळे कॉल करू शकत नाही. नवीन कॉल करण्याआधी त्यांपैकी एक कॉल डिस्कनेक्ट करा किंवा त्यांना कॉन्फरन्स कॉलमध्ये मर्ज करा."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"होल्डवर न ठेवता येणारा कॉल असल्यामुळे, कॉल करू शकत नाही. नवीन कॉल करण्यापूर्वी, कॉल डिस्कनेक्ट करा."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"इनकमिंग कॉलला उत्तर दिले जात नसल्यामुळे कॉल करू शकत नाही. नवीन कॉल करण्याआधी येणार्‍या कॉलला उत्तर द्या किंवा त्याला नकार द्या."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"हा MMI कोड एकाहून अधिक खात्यांवरील कॉलसाठी उपलब्ध नाही."</string>
 </resources>
diff --git a/res/values-ms/strings.xml b/res/values-ms/strings.xml
index ebfffd0..1d69dbf 100644
--- a/res/values-ms/strings.xml
+++ b/res/values-ms/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Penstriman audio pada peranti lain"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Tamatkan panggilan"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Tukar di sini"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Tidak dapat membuat panggilan kerana sudah terdapat dua panggilan yang sedang berlangsung. Putuskan satu daripada panggilan itu atau gabungkan panggilan tersebut menjadi persidangan sebelum membuat panggilan baharu."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Tidak dapat membuat panggilan kerana terdapat panggilan yang sedang menunggu. Putuskan panggilan sebelum membuat panggilan baharu."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Tidak dapat membuat panggilan kerana terdapat panggilan masuk yang tidak dijawab. Jawab atau tolak panggilan masuk itu sebelum membuat panggilan baharu."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"Kod MMI ini tidak tersedia untuk panggilan merentas berbilang akaun."</string>
 </resources>
diff --git a/res/values-my/strings.xml b/res/values-my/strings.xml
index e7f0fd4..fb8769e 100644
--- a/res/values-my/strings.xml
+++ b/res/values-my/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"အသံကို အခြားစက်တွင် တိုက်ရိုက်လွှင့်နေသည်"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"ဖုန်းချရန်"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"ဤနေရာသို့ လွှဲပြောင်းရန်"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"ဖုန်းခေါ်ဆိုမှုနှစ်ခုကို ပြုလုပ်နေသဖြင့် ဖုန်းထပ်ခေါ်၍မရပါ။ ခေါ်ဆိုမှုအသစ် မပြုလုပ်မီ ၎င်းတို့အနက် တစ်ခုကို ဖုန်းချပါ (သို့) အစည်းအဝေးအဖြစ် ပေါင်းစည်းပါ။"</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"ဆိုင်းငံ့၍မရသော ခေါ်ဆိုမှုရှိနေသဖြင့် ဖုန်းထပ်ခေါ်၍မရပါ။ ခေါ်ဆိုမှုအသစ် မပြုလုပ်မီ ဤခေါ်ဆိုမှုကို ဖုန်းချပါ။"</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"မဖြေကြားရသေးသော အဝင်ခေါ်ဆိုမှု ရှိနေသဖြင့် ဖုန်းခေါ်၍ မရနိုင်ပါ။ အသစ်မခေါ်ဆိုမီ ဖုန်းကိုင်ပါ (သို့) ငြင်းပယ်ပါ။"</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"ဤ MMI ကုဒ်ကို အကောင့်အများအပြား၌ ခေါ်ဆိုမှုများအတွက် မရနိုင်ပါ။"</string>
 </resources>
diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml
index 66e6ffc..2f22a36 100644
--- a/res/values-nb/strings.xml
+++ b/res/values-nb/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Strømmer lyden til en annen enhet"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Legg på"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Flytt hit"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Du kan ikke ringe fordi to andre anrop allerede pågår. Koble fra ett av anropene eller slå dem sammen i en konferansesamtale, før du ringer på nytt."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Kan ikke ringe fordi det pågår en samtale som ikke kan settes på vent. Avslutt samtalen før du ringer på nytt."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Du kan ikke ringe, fordi du har et innkommende anrop. Svar på eller avvis anropet før du prøver å ringe igjen."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"Denne MMI-koden kan ikke brukes til anrop på flere kontoer samtidig."</string>
 </resources>
diff --git a/res/values-ne/strings.xml b/res/values-ne/strings.xml
index 4aeceef..1932cb3 100644
--- a/res/values-ne/strings.xml
+++ b/res/values-ne/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"अर्को डिभाइसमा अडियो स्ट्रिम गरिँदै छ"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"कल काट्नुहोस्"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"यहाँ गई बदल्नुहोस्"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"दुई वटा कल चलिरहेका हुनाले नयाँ कल गर्न सकिँदैन। नयाँ कल गर्नुअघि दुईमध्ये एउटा कल डिस्कनेक्ट गर्नुहोस् वा तिनलाई मर्ज गरी कन्फ्रेन्स बनाउनुहोस्।"</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"होल्ड गर्न नमिल्ने कल चलिरहेको हुनाले नयाँ कल गर्न सकिँदैन। नयाँ कल गर्नुअघि यो कल डिस्कनेक्ट गर्नुहोस्।"</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"अहिले आइरहेको कल नउठाइएको हुनाले नयाँ कल गर्न सकिँदैन। नयाँ कल गर्नुअघि उक्त कल उठाउनुहोस् वा काट्नुहोस्।"</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"अर्को खातामार्फत कल चलिरहेका बेला यो MMI कोड प्रयोग गर्न मिल्दैन।"</string>
 </resources>
diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml
index e395ef1..a7ee856 100644
--- a/res/values-nl/strings.xml
+++ b/res/values-nl/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Audio streamen naar ander apparaat"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Ophangen"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Hiernaartoe schakelen"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Kan gesprek niet plaatsen omdat er al 2 actieve gesprekken zijn. Verbreek de verbinding in een van de gesprekken of voeg ze samen tot een conferencecall voordat je een nieuw gesprek plaatst."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Kan gesprek niet plaatsen omdat je het live gesprek niet in de wacht kunt zetten. Verbreek de verbinding van het live gesprek voordat je een nieuw gesprek plaatst."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Kan niet bellen omdat er een niet-beantwoord inkomend gesprek is. Beantwoord of weiger het inkomende gesprek voordat je opnieuw belt."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"Deze MMI-code is niet beschikbaar voor gesprekken met meerdere accounts."</string>
 </resources>
diff --git a/res/values-or/strings.xml b/res/values-or/strings.xml
index 535583a..c7dc2ac 100644
--- a/res/values-or/strings.xml
+++ b/res/values-or/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"ଅନ୍ୟ ଡିଭାଇସରେ ଅଡିଓ ଷ୍ଟ୍ରିମ କରାଯାଉଛି"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"କଲ ସମାପ୍ତ କରନ୍ତୁ"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"ଏଠାରେ ସୁଇଚ କରନ୍ତୁ"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"ପୂର୍ବରୁ ଦୁଇଟି କଲ ଚାଲୁ ଥିବା ଯୋଗୁଁ ଆଉ ଏକ କଲ କରାଯାଇପାରିବ ନାହିଁ। ଏକ ନୂଆ କଲ କରିବା ପୂର୍ବରୁ ଗୋଟିଏ କଲକୁ ଡିସକନେଜ୍ଟ କରନ୍ତୁ କିମ୍ବା ସେଗୁଡ଼ିକୁ ଏକ କନଫରେନ୍ସ କଲରେ ମର୍ଜ କରନ୍ତୁ।"</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"ହୋଲ୍ଡ କରିହେଉନଥିବା ଏକ କଲ ଚାଲୁ ଥିବା ଯୋଗୁଁ ଆଉ ଏକ କଲ କରାଯାଇପାରିବ ନାହିଁ। ଏକ ନୂଆ କଲ କରିବା ପୂର୍ବରୁ କଲକୁ ଡିସକନେକ୍ଟ କରନ୍ତୁ।"</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"ଗୋଟିଏ ଉତ୍ତର ଦିଆଯାଇନଥିବା ଇନକମିଂ କଲ ଥିବା ଯୋଗୁଁ ଅନ୍ୟ ଏକ କଲ କରିପାରିବେ ନାହିଁ। ଏକ ନୂଆ କଲ କରିବା ପୂର୍ବରୁ ଇନକମିଂ କଲଟିର ଉତ୍ତର ଦିଅନ୍ତୁ କିମ୍ବା ଏହାକୁ ଅଗ୍ରାହ୍ୟ କରନ୍ତୁ।"</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"ଏକାଧିକ ଆକାଉଣ୍ଟରେ କଲଗୁଡ଼ିକ ପାଇଁ ଏହି MMI କୋଡ ଉପଲବ୍ଧ ନାହିଁ।"</string>
 </resources>
diff --git a/res/values-pa/strings.xml b/res/values-pa/strings.xml
index 96ee0e8..fc4de6a 100644
--- a/res/values-pa/strings.xml
+++ b/res/values-pa/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"ਆਡੀਓ ਨੂੰ ਕਿਸੇ ਹੋਰ ਡੀਵਾਈਸ \'ਤੇ ਸਟ੍ਰੀਮ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"ਕਾਲ ਸਮਾਪਤ ਕਰੋ"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"ਇੱਥੇ ਸਵਿੱਚ ਕਰੋ"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"ਕਾਲ ਨਹੀਂ ਕੀਤੀ ਜਾ ਸਕਦੀ ਕਿਉਂਕਿ ਪਹਿਲਾਂ ਤੋਂ ਦੋ ਕਾਲਾਂ ਚੱਲ ਰਹੀਆਂ ਹਨ। ਨਵੀਂ ਕਾਲ ਕਰਨ ਤੋਂ ਪਹਿਲਾਂ ਇੱਕ ਕਾਲ ਨੂੰ ਡਿਸਕਨੈਕਟ ਕਰੋ ਜਾਂ ਦੋਨੋਂ ਕਾਲਾਂ ਨੂੰ ਮਿਲਾ ਕੇ ਕਾਨਫਰੰਸ ਕਾਲ ਵਿੱਚ ਬਦਲੋ।"</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"ਕਾਲ ਨਹੀਂ ਕੀਤੀ ਜਾ ਸਕਦੀ ਕਿਉਂਕਿ ਪਹਿਲਾਂ ਤੋਂ ਇੱਕ ਕਾਲ ਚੱਲ ਰਹੀ ਹੈ, ਜਿਸਨੂੰ ਹੋਲਡ \'ਤੇ ਨਹੀਂ ਰੱਖਿਆ ਜਾ ਸਕਦਾ। ਨਵੀਂ ਕਾਲ ਕਰਨ ਤੋਂ ਪਹਿਲਾਂ ਕਾਲ ਨੂੰ ਡਿਸਕਨੈਕਟ ਕਰੋ।"</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"ਕਾਲ ਨਹੀਂ ਕੀਤੀ ਜਾ ਸਕਦੀ ਕਿਉਂਕਿ ਇੱਕ ਜਵਾਬ ਨਾ ਦਿੱਤੀ ਗਈ ਇਨਕਮਿੰਗ ਕਾਲ ਪਹਿਲਾਂ ਤੋਂ ਹੀ ਆ ਰਹੀ ਹੈ। ਨਵੀਂ ਕਾਲ ਕਰਨ ਤੋਂ ਪਹਿਲਾਂ ਇਨਕਮਿੰਗ ਕਾਲ ਦਾ ਜਵਾਬ ਦਿਓ ਜਾਂ ਅਸਵੀਕਾਰ ਕਰੋ।"</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"ਇਹ MMI ਕੋਡ ਇੱਕ ਤੋਂ ਵੱਧ ਖਾਤਿਆਂ ਵਿੱਚ ਕਾਲਾਂ ਲਈ ਉਪਲਬਧ ਨਹੀਂ ਹੈ।"</string>
 </resources>
diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml
index 23776f5..2bea396 100644
--- a/res/values-pl/strings.xml
+++ b/res/values-pl/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Strumieniowanie dźwięku na inne urządzenie"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Rozłącz"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Przełącz tutaj"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Nie można nawiązać połączenia, ponieważ trwają już 2 inne połączenia. Aby nawiązać nowe połączenie, zakończ jedno z nich lub scal je w połączenie konferencyjne."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Nie można nawiązać połączenia, ponieważ trwa połączenie, którego nie można wstrzymać. Aby nawiązać nowe połączenie, zakończ to połączenie."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Nie można nawiązać połączenia, ponieważ masz nieodebrane połączenie przychodzące. Odbierz je lub odrzuć przed nawiązaniem nowego."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"Ten kod MMI nie jest dostępny w przypadku połączeń na więcej niż 1 koncie."</string>
 </resources>
diff --git a/res/values-pt-rPT/strings.xml b/res/values-pt-rPT/strings.xml
index 122615a..be5ed69 100644
--- a/res/values-pt-rPT/strings.xml
+++ b/res/values-pt-rPT/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"A fazer stream de áudio para outro dispositivo"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Desligar"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Mudar aqui"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Não pode fazer uma chamada porque já estão 2 chamadas em curso. Desligue uma delas ou una-as numa conferência antes de fazer uma nova chamada."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Não pode fazer uma chamada porque tem uma chamada que não pode ser colocada em espera. Termine essa chamada antes de fazer uma nova chamada."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Não pode fazer uma chamada porque há uma chamada recebida não atendida. Atenda ou rejeite a chamada recebida antes de fazer uma nova chamada."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"Este código MMI não está disponível para chamadas em várias contas."</string>
 </resources>
diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml
index e302ea6..11a12b6 100644
--- a/res/values-pt/strings.xml
+++ b/res/values-pt/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Fazendo streaming de áudio para outro dispositivo"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Desligar"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Mudar para este dispositivo"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Não é possível ligar porque já há duas chamadas em andamento. Encerre uma delas ou mescle-as em uma conferência antes de fazer outra."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Não é possível ligar porque há uma chamada que não pode ficar em espera. Encerre essa ligação antes de fazer outra."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Não é possível ligar porque há uma ligação recebida que não foi atendida. Atenda ou rejeite essa chamada antes de fazer outra."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"Este código MMI não está disponível para chamadas em várias contas."</string>
 </resources>
diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml
index fe5ad93..f44f93a 100644
--- a/res/values-ro/strings.xml
+++ b/res/values-ro/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Streaming audio pe alt dispozitiv"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Încheie apelul"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Treci la alt cont aici"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Nu se poate iniția un apel când există deja două apeluri în desfășurare. Deconectează unul dintre ele sau îmbină-le într-o conferință înainte de a iniția un apel nou."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Nu se poate iniția un apel când există un apel care nu poate fi pus în așteptare. Închide apelul înainte de a iniția un apel nou."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Nu se poate iniția un apel când primești un apel la care nu ai răspuns. Răspunde sau respinge apelul primit înainte de a iniția un apel nou."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"Acest cod MMI nu este disponibil pentru apelurile din mai multe conturi."</string>
 </resources>
diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml
index cc69d40..718aa84 100644
--- a/res/values-ru/strings.xml
+++ b/res/values-ru/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Потоковая передача аудио на другое устройство"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Завершить"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Переключиться"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Невозможно позвонить, поскольку ещё не завершены два текущих вызова. Сбросьте один из вызовов или объедините их в конференцию."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Невозможно позвонить, поскольку нельзя поставить текущий вызов на удержание. Сбросьте вызов, чтобы начать новый."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Невозможно позвонить, поскольку вы не ответили на входящий вызов. Примите или отклоните текущий звонок."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"Этот код MMI недоступен для вызовов с использованием нескольких аккаунтов."</string>
 </resources>
diff --git a/res/values-si/strings.xml b/res/values-si/strings.xml
index 2ea058f..d46a6b7 100644
--- a/res/values-si/strings.xml
+++ b/res/values-si/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"වෙනත් උපාංගයකට ශ්‍රව්‍ය ප්‍රවාහ කිරීම"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"විසන්ධි කරන්න"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"මෙතැනට මාරු වෙන්න"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"දැනටමත් ඇමතුම් දෙකක් කෙරෙමින් පවතින නිසා ඇමතුමක් ගැනීමට නොහැක. නව ඇමතුමක් ගැනීමට පෙරාතුව ඇමතුම්වලින් එකක් විසන්ධි කරන්න නැතහොත් ඒවා සම්මන්ත්‍රණයකට ඒකාබද්ධ කරන්න."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"රඳවා ගත නොහැකි ඇමතුමක් ඇති බැවින් ඇමතුමක් ලබා ගත නොහැක. නව ඇමතුමක් ලබා ගැනීමට පෙර ඇමතුම විසන්ධි කරන්න."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"පිළිතුරු නොලැබෙන ඇමතුමක් ඇති බැවින් ඇමතුමක් ලබා ගත නොහැක. නව ඇමතුමක් ලබා ගැනීමට පෙර ලැබෙන ඇමතුමට පිළිතුරු දෙන්න හෝ ප්‍රතික්ෂේප කරන්න."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"මෙම MMI කේතය බහු ගිණුම් හරහා ඇමතුම් සඳහා ලබා ගත නොහැක."</string>
 </resources>
diff --git a/res/values-sk/strings.xml b/res/values-sk/strings.xml
index fc7108a..444fa4b 100644
--- a/res/values-sk/strings.xml
+++ b/res/values-sk/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Streamovanie zvuku do iného zariadenia"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Zložiť"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Prepnúť sem"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Hovor sa nedá uskutočniť, pretože už prebiehajú dva hovory. Odpojte jeden hovor alebo ich zlúčte do konferencie a až potom uskutočnite nový hovor."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Hovor sa nedá uskutočniť, pretože prebieha hovor, ktorý sa nedá podržať. Pred uskutočnením nového hovoru najprv ukončite ten prebiehajúci."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Hovor sa nedá uskutočniť, pretože máte neprijatý prichádzajúci hovor. Prijmite alebo odmietnite prichádzajúci hovor a až potom uskutočnite nový hovor."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"Tento kód MMI nie je k dispozícii pre hovory v rámci viacerých účtov."</string>
 </resources>
diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml
index 7ee0b0b..0664882 100644
--- a/res/values-sl/strings.xml
+++ b/res/values-sl/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Pretočno predvajanje zvoka v drugo napravo"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Prekini klic"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Preklopi sem"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Klica ni mogoče opraviti, ker potekata že dva klica. Preden začnete nov klic, prekinite enega od klicev ali ju združite v konferenčni klic."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Klica ni mogoče opraviti, ker že imate klic, ki ga ni mogoče zadržati. Preden opravite nov klic, prekinite omenjeni klic."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Klica ni mogoče opraviti, ker imate dohodni klic, na katerega še niste odgovorili. Preden začnete nov klic, sprejmite ali zavrnite dohodni klic."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"Ta koda MMI ni na voljo za klice v več računih."</string>
 </resources>
diff --git a/res/values-sq/strings.xml b/res/values-sq/strings.xml
index 7d8045a..1bc7c0a 100644
--- a/res/values-sq/strings.xml
+++ b/res/values-sq/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Po transmetohet audioja te një pajisje tjetër"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Mbyll"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Ndërro këtu"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Nuk mund të kryhet një telefonatë sepse janë tashmë dy telefonata në vazhdim. Shkëput një nga telefonatat ose shkriji ato në një konferencë para se të kryesh një telefonatë të re."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Nuk mund të kryhet një telefonatë pasi është në telefonatë që nuk mund të vendoset në pritje. Shkëput telefonatën para se të kryesh një telefonatë të re."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Nuk mund të kryhet një telefonatë sepse është një telefonatë hyrëse që nuk ka marrë përgjigje. Përgjigju ose refuzoje telefonatën hyrëse para se të kryesh një telefonatë të re."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"Ky kod MMI nuk ofrohet për telefonatat në disa llogari."</string>
 </resources>
diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml
index 148cb14..69addf7 100644
--- a/res/values-sr/strings.xml
+++ b/res/values-sr/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Звук се стримује на други уређај"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Прекини везу"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Пребаци овде"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Не можете да упутите позив јер су два позива већ у току. Прекините један од њих или их обједините у конференцију да бисте упутили нови позив."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Не можете да упутите позив јер је у току позив који не може да се стави на чекање. Прекините тај позив пре упућивања новог позива."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Не можете да позовете јер имате долазни позив на који нисте одговорили. Примите га или одбијте пре упућивања новог позива."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"Овај MMI кôд није доступан за позиве на више налога."</string>
 </resources>
diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml
index d4a930c..d6229fa 100644
--- a/res/values-sv/strings.xml
+++ b/res/values-sv/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Streama ljud till en annan enhet"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Lägg på"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Koppla hit"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Det går inte att ringa eftersom det redan finns två pågående samtal. Koppla bort ett eller slå ihop dem till en konferens innan du ringer ett nytt samtal."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Det går inte att ringa eftersom det finns ett samtal som inte kan sättas i vänteläge. Koppla bort samtalet innan du ringer ett nytt samtal."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Det går inte att ringa eftersom det finns ett obesvarat inkommande samtal. Svara eller avvisa det innan du ringer ett nytt samtal."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"Den här MMI-koden är inte tillgänglig för samtal på flera konton."</string>
 </resources>
diff --git a/res/values-sw/strings.xml b/res/values-sw/strings.xml
index ac0518d..1d56f6d 100644
--- a/res/values-sw/strings.xml
+++ b/res/values-sw/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Inatiririsha sauti kwenye kifaa kingine"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Kata simu"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Badili hapa"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Imeshindwa kupiga simu kwa sababu tayari kuna simu mbili zinazoendelea. Kata mojawapo ya simu hizo au uziunganishe ili ziwe simu ya mkutano kabla ya kupiga simu mpya."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Imeshindwa kupiga simu kwa sababu kuna simu isiyoweza kusubirishwa. Kata simu kabla ya kupiga simu mpya."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Haiwezi kupiga simu kwa sababu kuna simu unayopigiwa ambayo hujajibu. Jibu au ukatae simu hiyo unayopigiwa kabla ya kupiga simu mpya."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"Msimbo huu wa MMI haupatikani katika simu kwenye akaunti nyingi."</string>
 </resources>
diff --git a/res/values-ta/strings.xml b/res/values-ta/strings.xml
index 57c70f4..dd6aebe 100644
--- a/res/values-ta/strings.xml
+++ b/res/values-ta/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"வேறு சாதனத்திற்கு ஆடியோவை ஸ்ட்ரீம் செய்கிறது"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"அழைப்பைத் துண்டி"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"இங்கே மாற்று"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"ஏற்கெனவே இரண்டு அழைப்புகள் செயலில் இருப்பதால் தற்போது புதிய அழைப்பை மேற்கொள்ள முடியாது. செயலில் உள்ள அழைப்புகளில் ஏதேனும் ஒன்றைத் துண்டித்தோ அவற்றை இணைத்து குழு அழைப்பாக மாற்றியோ புதிய அழைப்பை மேற்கொள்ளவும்."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"ஹோல்டு செய்ய முடியாத ஓர் அழைப்பு ஏற்கெனவே செயலில் இருப்பதால் அழைப்பை மேற்கொள்ள முடியவில்லை. செயலில் உள்ள அழைப்பைத் துண்டித்து புதிய அழைப்பை மேற்கொள்ளவும்."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"உள்வரும் அழைப்பிற்கு இன்னும் பதிலளிக்காததால் இந்த அழைப்பைச் செய்ய முடியாது. புதிதாக ஓர் அழைப்பைச் செய்யும் முன்னர் உள்வரும் அழைப்பிற்குப் பதிலளிக்கவும் அல்லது நிராகரிக்கவும்."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"பல கணக்குகளில் மேற்கொள்ளப்படும் அழைப்புகளுக்கு இந்த MMI குறியீட்டைப் பயன்படுத்த முடியாது."</string>
 </resources>
diff --git a/res/values-te/strings.xml b/res/values-te/strings.xml
index 22f4b8a..51ae29f 100644
--- a/res/values-te/strings.xml
+++ b/res/values-te/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"ఆడియోను ఇతర పరికరానికి స్ట్రీమింగ్ చేయండి"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"ముగించండి"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"ఇక్కడకు స్విచ్ అవ్వండి"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"ఇప్పటికే రెండు కాల్స్ జరుగుతున్నందున కాల్ చేయడం సాధ్యపడదు. ఆ కాల్స్‌లో ఒకదానిని డిస్‌కనెక్ట్ చేయండి లేదా అవి రెండింటినీ కలిపి ఒక కాన్ఫరెన్స్ కాల్‌గా మార్చి, తర్వాత కొత్త కాల్ చేయండి."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"హోల్డ్‌లో పెట్టడం సాధ్యం కాని కాల్ జరుగుతున్నందున కాల్ చేయడం సాధ్యం కాదు. కొత్త కాల్ చేయడానికంటే ముందుగా ప్రస్తుత కాల్‌ను డిస్‌కనెక్ట్ చేయండి."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"ఇన్‌కమింగ్ కాల్ వస్తున్నంతసేపు వేరొక కాల్ చేయడం సాధ్యపడదు. కొత్త కాల్ చేయడానికి ముందుగా ఇన్‌కమింగ్ కాల్‌కు సమాధానమివ్వండి లేదా కాల్‌ను నిరాకరించండి."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"ఈ MMI కోడ్ పలు ఖాతాలలో కాల్స్ కోసం అందుబాటులో లేదు."</string>
 </resources>
diff --git a/res/values-th/strings.xml b/res/values-th/strings.xml
index e3a20b1..d1e4f75 100644
--- a/res/values-th/strings.xml
+++ b/res/values-th/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"กำลังสตรีมเสียงไปยังอุปกรณ์อื่นๆ"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"วางสาย"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"เปลี่ยนที่นี่"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"โทรออกไม่ได้เนื่องจากมีการโทร 2 สายที่กำลังดำเนินอยู่ โปรดยกเลิกการเชื่อมต่อสายใดสายหนึ่งหรือรวมเป็นการประชุมสายก่อนโทรออกใหม่"</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"โทรออกไม่ได้เนื่องจากมีการโทรที่ไม่สามารถพักสายได้ โปรดยกเลิกการเชื่อมต่อสายดังกล่าวก่อนโทรออกใหม่"</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"โทรออกไม่ได้เนื่องจากมีสายเรียกเข้าที่ยังไม่ได้รับ โปรดรับหรือปฏิเสธสายเรียกเข้าก่อนจึงค่อยโทรออกใหม่"</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"รหัส MMI นี้ใช้ไม่ได้กับการโทรผ่านหลายบัญชี"</string>
 </resources>
diff --git a/res/values-tl/strings.xml b/res/values-tl/strings.xml
index 001a19a..98d0233 100644
--- a/res/values-tl/strings.xml
+++ b/res/values-tl/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Naka-stream ang audio sa ibang device"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Mag-hang up"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Lumipat dito"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Hindi puwedeng tumawag dahil mayroon nang dalawang tawag na kasalukuyang nagaganap. Idiskonekta ang isa sa mga tawag o i-merge ang mga ito sa isang conference bago gumawa ng bagong pagtawag."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Hindi puwedeng tumawag dahil may tawag na hindi puwedeng i-hold. Idiskonekta ang tawag bago gumawa ng bagong pagtawag."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Hindi puwedeng tumawag dahil mayroong hindi nasagot na papasok na tawag. Sagutin o tanggihan ang papasok na tawag bago gumawa ng bagong pagtawag."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"Hindi available ang MMI code na ito para sa mga tawag sa magkakaibang account."</string>
 </resources>
diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml
index 1924d92..8160987 100644
--- a/res/values-tr/strings.xml
+++ b/res/values-tr/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Ses başka bir cihaza aktarılıyor"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Görüşmeyi bitir"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Buraya dön"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Devam eden iki arama olduğu için arama yapılamıyor. Yeni bir arama yapmadan önce aramalardan birini sonlandırın veya iki aramayı bir konferans aramasında birleştirin."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Bekletilemeyen bir arama devam ettiğinden arama yapılamıyor. Yeni bir arama yapmadan önce mevcut aramayı sonlandırın."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Gelen arama olduğu için arama yapılamıyor. Yeni bir arama yapmadan önce gelen aramayı cevaplayın veya reddedin."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"Bu MMI kodu, birden fazla hesaptan arama yapılırken kullanılamaz."</string>
 </resources>
diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml
index 2d4f5bc..6e70c2e 100644
--- a/res/values-uk/strings.xml
+++ b/res/values-uk/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Аудіо транслюється на інший пристрій"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Завершити"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Перевести сюди"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Неможливо зателефонувати, оскільки тривають уже два виклики. Припиніть один із них або об’єднайте їх у конференцію, перш ніж здійснити новий."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Неможливо зателефонувати, оскільки поточний виклик не можна поставити на утримання. Припиніть виклик, перш ніж здійснити новий."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Неможливо зателефонувати. Прийміть або відхиліть вхідний дзвінок, перш ніж здійснювати новий."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"Цей код MMI недоступний для дзвінків із використанням кількох облікових записів."</string>
 </resources>
diff --git a/res/values-ur/strings.xml b/res/values-ur/strings.xml
index b09f244..1ce365a 100644
--- a/res/values-ur/strings.xml
+++ b/res/values-ur/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"دوسرے آلے پر آڈیو کی سلسلہ بندی کی جا رہی ہے"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"منقطع کریں"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"یہاں سوئچ کریں"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"پہلے سے دو کالز کے پیش رفت میں ہونے کی وجہ سے کال نہیں کی جا سکتی۔ نئی کال کرنے کیلئے پہلے ان میں سے ایک کو غیر منسلک کریں یا انہیں کانفرنس میں ضم کریں۔"</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"کال نہیں کر سکتے کیونکہ پہلے سے جاری کال کو ہولڈ نہیں کیا جا سکتا۔ نئی کال کرنے سے پہلے موجودہ کال کو غیر منسلک کریں۔"</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"ایک جواب نہ ملنے والی اِن کمنگ کال کی وجہ سے کال نہیں کی جا سکتی۔ نئی کال کرنے کے لیے پہلے اِن کمنگ کال کا جواب دیں یا مسترد کریں۔"</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"‏یہ MMI کوڈ متعدد اکاؤنٹس پر کالز کے لیے دستیاب نہیں ہے۔"</string>
 </resources>
diff --git a/res/values-uz/strings.xml b/res/values-uz/strings.xml
index ff04903..ad00cc3 100644
--- a/res/values-uz/strings.xml
+++ b/res/values-uz/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Audio translatsiyani boshqa qurilmaga olish"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Tugatish"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Shu yerga olish"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Telefon qilish imkonsiz, chunki ayni paytda ikkita chaqiruv davom etmoqda. Telefon qilish uchun chaqiruvlardan birini yakunlang yoki ularni konferens-aloqaga birlashtiring."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Chaqirish imkonsiz, chunki joriy chaqiruv pauza qilinmaydi. Yangisini boshlash uchun chaqiruvni bekor qiling."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Telefon qilish imkonsiz. Telefon qilish uchun avval kiruvchi chaqiruvni qabul qiling yoki rad eting."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"Bu MMI kodi bir nechta hisobdagi chaqiruvlarda ishlamaydi."</string>
 </resources>
diff --git a/res/values-vi/strings.xml b/res/values-vi/strings.xml
index 142026c..a15eabb 100644
--- a/res/values-vi/strings.xml
+++ b/res/values-vi/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Đang truyền trực tuyến âm thanh tới thiết bị khác"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Kết thúc"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Chuyển qua thiết bị này"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Không thể gọi điện vì có 2 cuộc gọi đang diễn ra. Hãy ngắt kết nối 1 trong 2 cuộc gọi hoặc gộp thành 1 cuộc gọi kiểu hội nghị truyền hình trước khi thực hiện cuộc gọi mới."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Không thể thực hiện cuộc gọi vì có một cuộc gọi không thể tạm ngưng. Hãy ngắt kết nối với cuộc gọi đó trước khi thực hiện cuộc gọi mới."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Bạn không thể gọi vì chưa trả lời cuộc gọi đến. Hãy trả lời hoặc từ chối cuộc gọi đến trước khi thực hiện cuộc gọi mới."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"Mã MMI này không dùng được cho các cuộc gọi trên nhiều tài khoản."</string>
 </resources>
diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml
index 7cb8a7a..bad9b02 100644
--- a/res/values-zh-rCN/strings.xml
+++ b/res/values-zh-rCN/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"将音频流式传输到其他设备"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"挂断"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"在此处切换"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"由于已有两个正在进行的通话,因此无法拨打电话。请先断开其中一个通话的连接或将两个通话合并到同一个会议中,然后才能拨打新电话。"</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"由于有无法暂停的通话,因此不能拨打电话。请先断开通话,然后再拨打新电话。"</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"由于有未接来电,因此无法拨打电话。请先接听或拒绝来电,然后才能拨打新电话。"</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"此 MMI 码无法用于跨多个账号的通话。"</string>
 </resources>
diff --git a/res/values-zh-rHK/strings.xml b/res/values-zh-rHK/strings.xml
index 213255a..1480227 100644
--- a/res/values-zh-rHK/strings.xml
+++ b/res/values-zh-rHK/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"正在串流音訊至其他裝置"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"結束通話"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"在這裡切換"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"目前已有兩個通話正在進行,因此無法撥打電話。請先結束其中一個通話,或將兩個通話合併為一個會議,然後再撥打電話。"</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"目前有一個無法保留的通話,因此無法撥打電話。請先結束通話,然後再撥打電話。"</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"你尚未接聽目前的來電,因此無法撥打電話。請先接聽或拒絕來電,然後再撥打電話。"</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"在透過多個帳戶進行通話時,無法使用 MMI 碼。"</string>
 </resources>
diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml
index 287f627..5b151ee 100644
--- a/res/values-zh-rTW/strings.xml
+++ b/res/values-zh-rTW/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"正在將音訊串流到其他裝置"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"掛斷"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"切換到這部裝置"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"目前有兩個進行中的通話,因此無法撥號。如要撥打電話,請掛斷其中一個通話,或將通話合併成會議。"</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"目前有無法保留的通話,因此無法撥號。請先掛斷通話,再撥打電話。"</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"有人打電話給你,因此你目前無法撥打電話。你必須先接聽或拒接來電,才能撥打電話。"</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"這個 MMI 代碼無法用於透過多個帳戶進行通話。"</string>
 </resources>
diff --git a/res/values-zu/strings.xml b/res/values-zu/strings.xml
index 8d0437d..a41dc42 100644
--- a/res/values-zu/strings.xml
+++ b/res/values-zu/strings.xml
@@ -134,4 +134,8 @@
     <string name="call_streaming_notification_body" msgid="502216105683378263">"Sakaza umsindo kwenye idivayisi"</string>
     <string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"Beka phansi"</string>
     <string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"Shintsha lapha"</string>
+    <string name="callFailed_too_many_calls" msgid="4249997210954876420">"Awukwazi ukubeka ikholi njengoba kunamakholi amabili aqhubekayo kakade. Nqamula eyodwa yamakholi noma wahlanganisele enkofeni ngaphambi kokubeka ikholi entsha."</string>
+    <string name="callFailed_unholdable_call" msgid="7580834131274566524">"Ayikwazi ukwenza ikholi njengoba kukhona ikholi engabanjwa. Nqamula ikholi ngaphambi kokwenza ikholi entsha."</string>
+    <string name="callFailed_already_ringing" msgid="7931232733958098270">"Ayikwazi ukubeka ikholi njengoba kunekholi engenayo engaphenduliwe. Phendula noma nqaba ikholi engenayo ngaphambi kokubeka ikholi entsha."</string>
+    <string name="callFailed_reject_mmi" msgid="5219280796733595167">"Le khodi ye-MMI ayitholakali ngamakholi kuma-akhawunti amaningi."</string>
 </resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index aefd2e6..9d9f5e1 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -417,4 +417,16 @@
          Call streaming is a feature where a user can see and interact with a call from another
          device like a tablet while the call takes place on their phone. -->
     <string name="call_streaming_notification_action_switch_here">Switch here</string>
+    <!-- In-call screen: error message shown when the user attempts to place a call, but calling has
+         been disabled using a debug property. -->
+    <string name="callFailed_too_many_calls">Cannot place a call as there are already two calls in progress. Disconnect one of the calls or merge them into a conference prior to placing a new call.</string>
+    <!-- In-call screen: error message shown when the user attempts to place a call, but the live
+         call cannot be held. -->
+    <string name="callFailed_unholdable_call">Cannot place a call as there is an unholdable call. Disconnect the call prior to placing a new call.</string>
+    <!-- In-call screen: error message shown when the user has attempted to place a new outgoing
+         call while there is already a call in ringing state. -->
+    <string name="callFailed_already_ringing">Cannot place a call as there is an unanswered incoming call. Answer or reject the incoming call prior to placing a new call.</string>
+    <!-- In-call screen: error message shown when the user attempts to dial an MMI code, but there
+         is an ongoing call on a different phone account. -->
+    <string name="callFailed_reject_mmi">This MMI code is not available for calls across multiple accounts.</string>
 </resources>
diff --git a/src/com/android/server/telecom/AsyncRingtonePlayer.java b/src/com/android/server/telecom/AsyncRingtonePlayer.java
index 3b5e342..7cb05cd 100644
--- a/src/com/android/server/telecom/AsyncRingtonePlayer.java
+++ b/src/com/android/server/telecom/AsyncRingtonePlayer.java
@@ -23,6 +23,7 @@
 import android.net.Uri;
 import android.os.Handler;
 import android.os.HandlerThread;
+import android.os.Looper;
 import android.os.Message;
 import android.telecom.Log;
 import android.telecom.Logging.Session;
@@ -184,6 +185,13 @@
         }
     }
 
+    public @NonNull Looper getLooper() {
+        if (mHandler == null) {
+            mHandler = getNewHandler();
+        }
+        return mHandler.getLooper();
+    }
+
     /**
      * Creates a new ringtone Handler running in its own thread.
      */
diff --git a/src/com/android/server/telecom/AudioRoute.java b/src/com/android/server/telecom/AudioRoute.java
index d3ed77d..9898ca0 100644
--- a/src/com/android/server/telecom/AudioRoute.java
+++ b/src/com/android/server/telecom/AudioRoute.java
@@ -24,9 +24,11 @@
 
 import android.annotation.IntDef;
 import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHeadset;
 import android.bluetooth.BluetoothStatusCodes;
 import android.media.AudioDeviceInfo;
 import android.media.AudioManager;
+import android.sysprop.BluetoothProperties;
 import android.telecom.Log;
 import android.util.Pair;
 
@@ -138,6 +140,7 @@
     private String mBluetoothAddress;
     private AudioDeviceInfo mInfo;
     private boolean mIsDestRouteForWatch;
+    private boolean mIsScoManagedByAudio;
     public static final Set<Integer> BT_AUDIO_DEVICE_INFO_TYPES = Set.of(
             AudioDeviceInfo.TYPE_BLE_HEADSET,
             AudioDeviceInfo.TYPE_BLE_SPEAKER,
@@ -264,7 +267,7 @@
                 boolean connectedBtAudio = connectBtAudio(pendingAudioRoute, device,
                         audioManager, bluetoothRouteManager);
                 // Special handling for SCO case.
-                if (mAudioRouteType == TYPE_BLUETOOTH_SCO) {
+                if (!mIsScoManagedByAudio && mAudioRouteType == TYPE_BLUETOOTH_SCO) {
                     // Set whether the dest route is for the watch
                     mIsDestRouteForWatch = bluetoothRouteManager.isWatch(device);
                     // Check if the communication device was set for the device, even if
@@ -290,17 +293,31 @@
             for (AudioDeviceInfo deviceInfo : devices) {
                 // It's possible for the AudioDeviceInfo to be updated for the BT device so adjust
                 // mInfo accordingly.
+                // Note: we need to check the device type as well since a dual mode (LE and HFP) BT
+                // device can change type during a call if the user toggles LE for the device.
+                boolean isSameDeviceType =
+                        !pendingAudioRoute.getFeatureFlags().checkDeviceTypeOnRouteChange() ||
+                                (pendingAudioRoute.getFeatureFlags().checkDeviceTypeOnRouteChange()
+                                        && mAudioRouteType
+                                        == DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.get(
+                                        deviceInfo.getType()));
                 if (BT_AUDIO_ROUTE_TYPES.contains(mAudioRouteType) && mBluetoothAddress
-                        .equals(deviceInfo.getAddress())) {
+                        .equals(deviceInfo.getAddress())
+                        && isSameDeviceType) {
                     mInfo = deviceInfo;
                 }
                 if (deviceInfo.equals(mInfo)) {
                     result = audioManager.setCommunicationDevice(mInfo);
                     if (result) {
                         pendingAudioRoute.setCommunicationDeviceType(mAudioRouteType);
+                        if (mAudioRouteType == TYPE_BLUETOOTH_SCO && !isScoAudioConnected
+                                && mIsScoManagedByAudio) {
+                            pendingAudioRoute.addMessage(BT_AUDIO_CONNECTED, mBluetoothAddress);
+                        }
                     }
                     Log.i(this, "onDestRouteAsPendingRoute: route=%s, "
-                            + "AudioManager#setCommunicationDevice()=%b", this, result);
+                            + "AudioManager#setCommunicationDevice(%s)=%b", this,
+                            audioDeviceTypeToString(mInfo.getType()), result);
                     break;
                 }
             }
@@ -314,13 +331,19 @@
         }
     }
 
-    // Takes care of cleaning up original audio route (i.e. clearCommunicationDevice,
-    // sending SPEAKER_OFF, or disconnecting SCO).
-    void onOrigRouteAsPendingRoute(boolean active, PendingAudioRoute pendingAudioRoute,
+    /**
+     * Takes care of cleaning up original audio route (i.e. clearCommunicationDevice,
+     * sending SPEAKER_OFF, or disconnecting SCO).
+     * @param wasActive Was the origin route active or not.
+     * @param pendingAudioRoute The pending audio route change we're performing.
+     * @param audioManager Good 'ol audio manager.
+     * @param bluetoothRouteManager The BT route manager.
+     */
+    void onOrigRouteAsPendingRoute(boolean wasActive, PendingAudioRoute pendingAudioRoute,
             AudioManager audioManager, BluetoothRouteManager bluetoothRouteManager) {
-        Log.i(this, "onOrigRouteAsPendingRoute: active (%b), type (%s)", active,
-                DEVICE_TYPE_STRINGS.get(mAudioRouteType));
-        if (active) {
+        Log.i(this, "onOrigRouteAsPendingRoute: wasActive (%b), type (%s), pending(%s)", wasActive,
+                DEVICE_TYPE_STRINGS.get(mAudioRouteType), pendingAudioRoute);
+        if (wasActive) {
             int result = clearCommunicationDevice(pendingAudioRoute, bluetoothRouteManager,
                     audioManager);
             if (mAudioRouteType == TYPE_SPEAKER) {
@@ -338,6 +361,9 @@
         mAudioRouteType = type;
         mBluetoothAddress = bluetoothAddress;
         mInfo = info;
+        // Indication that SCO is managed by audio (i.e. supports setCommunicationDevice).
+        mIsScoManagedByAudio = android.media.audio.Flags.scoManagedByAudio()
+                && BluetoothProperties.isScoManagedByAudioEnabled().orElse(false);
     }
 
     @Override
@@ -381,7 +407,7 @@
         boolean success = false;
         if (device != null) {
             success = bluetoothRouteManager.getDeviceManager()
-                    .connectAudio(device, mAudioRouteType);
+                    .connectAudio(device, mAudioRouteType, mIsScoManagedByAudio);
         }
 
         Log.i(this, "connectBtAudio: routeToConnectTo = %s, successful = %b",
@@ -389,6 +415,20 @@
         return success;
     }
 
+    /**
+     * Clears the communication device; this takes into account the fact that SCO devices require
+     * us to call {@link BluetoothHeadset#disconnectAudio()} rather than
+     * {@link AudioManager#clearCommunicationDevice()}.
+     * As a general rule, if we are transitioning from an active route to another active route, we
+     * do NOT need to call {@link AudioManager#clearCommunicationDevice()}, but if the device is a
+     * legacy SCO device we WILL need to call {@link BluetoothHeadset#disconnectAudio()}.  We rely
+     * on the {@link PendingAudioRoute#isActive()} indicator to tell us if the destination route
+     * is going to be active or not.
+     * @param pendingAudioRoute The pending audio route transition we're implementing.
+     * @param bluetoothRouteManager The BT route manager.
+     * @param audioManager The audio manager.
+     * @return -1 if nothing was done, or the result code from the BT SCO disconnect.
+     */
     int clearCommunicationDevice(PendingAudioRoute pendingAudioRoute,
             BluetoothRouteManager bluetoothRouteManager, AudioManager audioManager) {
         // Try to see if there's a previously set device for communication that should be cleared.
@@ -398,13 +438,22 @@
         }
 
         int result = BluetoothStatusCodes.SUCCESS;
-        if (pendingAudioRoute.getCommunicationDeviceType() == TYPE_BLUETOOTH_SCO) {
-            Log.i(this, "clearCommunicationDevice: Disconnecting SCO device.");
+        if (pendingAudioRoute.getCommunicationDeviceType() == TYPE_BLUETOOTH_SCO
+                && !mIsScoManagedByAudio) {
+            Log.i(this, "Disconnecting SCO device via BluetoothHeadset.");
             result = bluetoothRouteManager.getDeviceManager().disconnectSco();
         } else {
-            Log.i(this, "clearCommunicationDevice: AudioManager#clearCommunicationDevice, type=%s",
-                    DEVICE_TYPE_STRINGS.get(pendingAudioRoute.getCommunicationDeviceType()));
-            audioManager.clearCommunicationDevice();
+            // Only clear communication device if the destination route will be inactive; route to
+            // route transitions do not require clearing the communication device.
+            boolean onlyClearCommunicationDeviceOnInactive =
+                    pendingAudioRoute.getFeatureFlags().onlyClearCommunicationDeviceOnInactive();
+            if (!onlyClearCommunicationDeviceOnInactive
+                    || (onlyClearCommunicationDeviceOnInactive && !pendingAudioRoute.isActive())) {
+                Log.i(this,
+                        "clearCommunicationDevice: AudioManager#clearCommunicationDevice, type=%s",
+                        DEVICE_TYPE_STRINGS.get(pendingAudioRoute.getCommunicationDeviceType()));
+                audioManager.clearCommunicationDevice();
+            }
         }
 
         if (result == BluetoothStatusCodes.SUCCESS) {
@@ -430,4 +479,23 @@
             pendingAudioRoute.clearPendingMessage(new Pair<>(SPEAKER_ON, null));
         }
     }
+
+    /**
+     * Get a human readable (for logs) version of an an audio device type.
+     * @param type the device type
+     * @return the human readable string
+     */
+    private static String audioDeviceTypeToString(int type) {
+        return switch (type) {
+            case AudioDeviceInfo.TYPE_BUILTIN_EARPIECE -> "earpiece";
+            case AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> "speaker";
+            case AudioDeviceInfo.TYPE_BUS -> "bus(auto speaker)";
+            case AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> "bt sco";
+            case AudioDeviceInfo.TYPE_BLE_HEADSET -> "bt le";
+            case AudioDeviceInfo.TYPE_HEARING_AID -> "bt hearing aid";
+            case AudioDeviceInfo.TYPE_USB_HEADSET -> "usb headset";
+            case AudioDeviceInfo.TYPE_WIRED_HEADSET -> "wired headset";
+            default -> Integer.toString(type);
+        };
+    }
 }
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index df31e02..411449c 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -80,6 +80,7 @@
 import com.android.server.telecom.stats.CallFailureCause;
 import com.android.server.telecom.stats.CallStateChangedAtomWriter;
 import com.android.server.telecom.ui.ToastFactory;
+import com.android.server.telecom.callsequencing.CallTransaction;
 import com.android.server.telecom.callsequencing.TransactionManager;
 import com.android.server.telecom.callsequencing.VerifyCallStateChangeTransaction;
 import com.android.server.telecom.callsequencing.CallTransactionResult;
@@ -130,6 +131,10 @@
 
     private static final char NO_DTMF_TONE = '\0';
 
+    public static final int CALL_SIMULTANEOUS_UNKNOWN = 0;
+    public static final int CALL_SIMULTANEOUS_SINGLE = 1;
+    public static final int CALL_DIRECTION_DUAL_SAME_ACCOUNT = 2;
+    public static final int CALL_DIRECTION_DUAL_DIFF_ACCOUNT = 3;
 
     /**
      * Listener for CallState changes which can be leveraged by a Transaction.
@@ -500,6 +505,11 @@
      */
     private DisconnectCause mOverrideDisconnectCause = new DisconnectCause(DisconnectCause.UNKNOWN);
 
+    /**
+     * Simultaneous type of the call.
+     */
+    private int mSimultaneousType = CALL_SIMULTANEOUS_UNKNOWN;
+
     private Bundle mIntentExtras = new Bundle();
 
     /**
@@ -1882,7 +1892,6 @@
         return mTargetPhoneAccountHandle;
     }
 
-    @VisibleForTesting
     public PhoneAccountHandle getTargetPhoneAccount() {
         return mTargetPhoneAccountHandle;
     }
@@ -2832,20 +2841,20 @@
     }
 
     @VisibleForTesting
-    public void disconnect() {
-        disconnect(0);
+    public CompletableFuture<Boolean> disconnect() {
+        return disconnect(0);
     }
 
-    public void disconnect(String reason) {
-        disconnect(0, reason);
+    public CompletableFuture<Boolean> disconnect(String reason) {
+        return disconnect(0, reason);
     }
 
     /**
      * Attempts to disconnect the call through the connection service.
      */
     @VisibleForTesting
-    public void disconnect(long disconnectionTimeout) {
-        disconnect(disconnectionTimeout, "internal" /** reason */);
+    public CompletableFuture<Boolean> disconnect(long disconnectionTimeout) {
+        return disconnect(disconnectionTimeout, "internal" /* reason */);
     }
 
     /**
@@ -2855,16 +2864,24 @@
      *               as TelecomManager.
      */
     @VisibleForTesting
-    public void disconnect(long disconnectionTimeout, String reason) {
+    public CompletableFuture<Boolean> disconnect(long disconnectionTimeout,
+            String reason) {
         Log.addEvent(this, LogUtils.Events.REQUEST_DISCONNECT, reason);
 
         // Track that the call is now locally disconnecting.
         setLocallyDisconnecting(true);
         maybeSetCallAsDisconnectingChild();
 
+        CompletableFuture<Boolean> disconnectFutureHandler =
+                CompletableFuture.completedFuture(false);
         if (mState == CallState.NEW || mState == CallState.SELECT_PHONE_ACCOUNT ||
                 mState == CallState.CONNECTING) {
             Log.i(this, "disconnect: Aborting call %s", getId());
+            if (mFlags.enableCallSequencing()) {
+                disconnectFutureHandler = awaitCallStateChangeAndMaybeDisconnectCall(
+                        false /* shouldDisconnectUponTimeout */, "disconnect",
+                        CallState.DISCONNECTED, CallState.ABORTED);
+            }
             abort(disconnectionTimeout);
         } else if (mState != CallState.ABORTED && mState != CallState.DISCONNECTED) {
             if (mState == CallState.AUDIO_PROCESSING && !hasGoneActiveBefore()) {
@@ -2876,7 +2893,8 @@
                 setOverrideDisconnectCauseCode(new DisconnectCause(DisconnectCause.MISSED));
             }
             if (mTransactionalService != null) {
-                mTransactionalService.onDisconnect(this, getDisconnectCause());
+                disconnectFutureHandler = mTransactionalService.onDisconnect(this,
+                        getDisconnectCause());
                 Log.i(this, "Send Disconnect to transactional service for call");
             } else if (mConnectionService == null) {
                 Log.e(this, new Exception(), "disconnect() request on a call without a"
@@ -2887,9 +2905,15 @@
                 // confirms that the call was actually disconnected. Only then is the
                 // association between call and connection service severed, see
                 // {@link CallsManager#markCallAsDisconnected}.
+                if (mFlags.enableCallSequencing()) {
+                    disconnectFutureHandler = awaitCallStateChangeAndMaybeDisconnectCall(
+                            false /* shouldDisconnectUponTimeout */, "disconnect",
+                            CallState.DISCONNECTED);
+                }
                 mConnectionService.disconnect(this);
             }
         }
+        return disconnectFutureHandler;
     }
 
     void abort(long disconnectionTimeout) {
@@ -2932,29 +2956,35 @@
      * @param videoState The video state in which to answer the call.
      */
     @VisibleForTesting
-    public void answer(int videoState) {
+    public CompletableFuture<Boolean> answer(int videoState) {
+        CompletableFuture<Boolean> answerCallFuture = CompletableFuture.completedFuture(false);
         // Check to verify that the call is still in the ringing state. A call can change states
         // between the time the user hits 'answer' and Telecom receives the command.
         if (isRinging("answer")) {
+            Log.addEvent(this, LogUtils.Events.REQUEST_ACCEPT);
             if (!isVideoCallingSupportedByPhoneAccount() && VideoProfile.isVideo(videoState)) {
                 // Video calling is not supported, yet the InCallService is attempting to answer as
                 // video.  We will simply answer as audio-only.
                 videoState = VideoProfile.STATE_AUDIO_ONLY;
             }
             // At this point, we are asking the connection service to answer but we don't assume
-            // that it will work. Instead, we wait until confirmation from the connectino service
+            // that it will work. Instead, we wait until confirmation from the connection service
             // that the call is in a non-STATE_RINGING state before changing the UI. See
             // {@link ConnectionServiceAdapter#setActive} and other set* methods.
             if (mConnectionService != null) {
+                if (mFlags.enableCallSequencing()) {
+                    answerCallFuture = awaitCallStateChangeAndMaybeDisconnectCall(
+                            false /* shouldDisconnectUponTimeout */, "answer", CallState.ACTIVE);
+                }
                 mConnectionService.answer(this, videoState);
             } else if (mTransactionalService != null) {
-                mTransactionalService.onAnswer(this, videoState);
+                return mTransactionalService.onAnswer(this, videoState);
             } else {
                 Log.e(this, new NullPointerException(),
                         "answer call failed due to null CS callId=%s", getId());
             }
-            Log.addEvent(this, LogUtils.Events.REQUEST_ACCEPT);
         }
+        return answerCallFuture;
     }
 
     /**
@@ -3034,74 +3064,101 @@
      *               if the reject is initiated from an API such as TelecomManager.
      */
     @VisibleForTesting
-    public void reject(boolean rejectWithMessage, String textMessage, String reason) {
+    public CompletableFuture<Boolean> reject(boolean rejectWithMessage,
+            String textMessage, String reason) {
+        CompletableFuture<Boolean> rejectFutureHandler = CompletableFuture.completedFuture(false);
         if (mState == CallState.SIMULATED_RINGING) {
+            Log.addEvent(this, LogUtils.Events.REQUEST_REJECT, reason);
             // This handles the case where the user manually rejects a call that's in simulated
             // ringing. Since the call is already active on the connectionservice side, we want to
             // hangup, not reject.
             setOverrideDisconnectCauseCode(new DisconnectCause(DisconnectCause.REJECTED));
             if (mTransactionalService != null) {
-                mTransactionalService.onDisconnect(this,
+                return mTransactionalService.onDisconnect(this,
                         new DisconnectCause(DisconnectCause.REJECTED));
             } else if (mConnectionService != null) {
+                if (mFlags.enableCallSequencing()) {
+                    rejectFutureHandler = awaitCallStateChangeAndMaybeDisconnectCall(
+                            false /* shouldDisconnectUponTimeout */, "reject",
+                            CallState.DISCONNECTED);
+                }
                 mConnectionService.disconnect(this);
+                return rejectFutureHandler;
             } else {
                 Log.e(this, new NullPointerException(),
                         "reject call failed due to null CS callId=%s", getId());
             }
-            Log.addEvent(this, LogUtils.Events.REQUEST_REJECT, reason);
         } else if (isRinging("reject") || isAnswered("reject")) {
+            Log.addEvent(this, LogUtils.Events.REQUEST_REJECT, reason);
             // Ensure video state history tracks video state at time of rejection.
             mVideoStateHistory |= mVideoState;
 
             if (mTransactionalService != null) {
-                mTransactionalService.onDisconnect(this,
+                return mTransactionalService.onDisconnect(this,
                         new DisconnectCause(DisconnectCause.REJECTED));
             } else if (mConnectionService != null) {
+                if (mFlags.enableCallSequencing()) {
+                    rejectFutureHandler = awaitCallStateChangeAndMaybeDisconnectCall(
+                            false /* shouldDisconnectUponTimeout */, "reject",
+                            CallState.DISCONNECTED);
+                }
                 mConnectionService.reject(this, rejectWithMessage, textMessage);
+                return rejectFutureHandler;
             } else {
                 Log.e(this, new NullPointerException(),
                         "reject call failed due to null CS callId=%s", getId());
             }
-            Log.addEvent(this, LogUtils.Events.REQUEST_REJECT, reason);
         }
+        return rejectFutureHandler;
     }
 
     /**
      * Reject this Telecom call with the user-indicated reason.
      * @param rejectReason The user-indicated reason fore rejecting the call.
      */
-    public void reject(@android.telecom.Call.RejectReason int rejectReason) {
+    public CompletableFuture<Boolean> reject(@android.telecom.Call.RejectReason int rejectReason) {
+        CompletableFuture<Boolean> rejectFutureHandler = CompletableFuture.completedFuture(false);
         if (mState == CallState.SIMULATED_RINGING) {
+            Log.addEvent(this, LogUtils.Events.REQUEST_REJECT);
             // This handles the case where the user manually rejects a call that's in simulated
             // ringing. Since the call is already active on the connectionservice side, we want to
             // hangup, not reject.
             // Since its simulated reason we can't pass along the reject reason.
             setOverrideDisconnectCauseCode(new DisconnectCause(DisconnectCause.REJECTED));
             if (mTransactionalService != null) {
-                mTransactionalService.onDisconnect(this,
+                return mTransactionalService.onDisconnect(this,
                         new DisconnectCause(DisconnectCause.REJECTED));
             } else if (mConnectionService != null) {
+                if (mFlags.enableCallSequencing()) {
+                    rejectFutureHandler = awaitCallStateChangeAndMaybeDisconnectCall(
+                            false /* shouldDisconnectUponTimeout */, "reject",
+                            CallState.DISCONNECTED);
+                }
                 mConnectionService.disconnect(this);
             } else {
                 Log.e(this, new NullPointerException(),
                         "reject call failed due to null CS callId=%s", getId());
             }
-            Log.addEvent(this, LogUtils.Events.REQUEST_REJECT);
         } else if (isRinging("reject") || isAnswered("reject")) {
+            Log.addEvent(this, LogUtils.Events.REQUEST_REJECT, rejectReason);
             // Ensure video state history tracks video state at time of rejection.
             mVideoStateHistory |= mVideoState;
             if (mTransactionalService != null) {
-                mTransactionalService.onDisconnect(this,
+                return mTransactionalService.onDisconnect(this,
                         new DisconnectCause(DisconnectCause.REJECTED));
             } else if (mConnectionService != null) {
+                if (mFlags.enableCallSequencing()) {
+                    rejectFutureHandler = awaitCallStateChangeAndMaybeDisconnectCall(
+                            false /* shouldDisconnectUponTimeout */, "reject",
+                            CallState.DISCONNECTED);
+                }
                 mConnectionService.rejectWithReason(this, rejectReason);
             } else {
                 Log.e(this, new NullPointerException(),
                         "reject call failed due to null CS callId=%s", getId());
             }
-            Log.addEvent(this, LogUtils.Events.REQUEST_REJECT, rejectReason);
         }
+        return rejectFutureHandler;
     }
 
     /**
@@ -3151,41 +3208,57 @@
      * Puts the call on hold if it is currently active.
      */
     @VisibleForTesting
-    public void hold() {
-        hold(null /* reason */);
+    public CompletableFuture<Boolean> hold() {
+        return hold(null /* reason */);
     }
 
     /**
      * This method requests the ConnectionService or TransactionalService hosting the call to put
      * the call on hold
      */
-    public void hold(String reason) {
+    public CompletableFuture<Boolean> hold(String reason) {
+        CompletableFuture<Boolean> holdFutureHandler = CompletableFuture.completedFuture(false);
         if (mState == CallState.ACTIVE) {
+            Log.addEvent(this, LogUtils.Events.REQUEST_HOLD, reason);
             if (mTransactionalService != null) {
-                mTransactionalService.onSetInactive(this);
+                return mTransactionalService.onSetInactive(this);
             } else if (mConnectionService != null) {
-                if (mFlags.transactionalCsVerifier()) {
-                    awaitCallStateChangeAndMaybeDisconnectCall(CallState.ON_HOLD, isSelfManaged(),
-                            "hold");
+                if (mFlags.transactionalCsVerifier() || mFlags.enableCallSequencing()) {
+                    holdFutureHandler = awaitCallStateChangeAndMaybeDisconnectCall(isSelfManaged(),
+                            "hold", CallState.ON_HOLD, CallState.DISCONNECTED).thenCompose(
+                                    (result) -> {
+                                        // Explicitly handle self-managed hold failures where we
+                                        // explicitly disconnect the call and treat it as a
+                                        // completed transaction.
+                                        if (!result && isSelfManaged()) {
+                                            Log.i(this, "hold: Completing transaction "
+                                                    + "after disconnecting held call.");
+                                            return CompletableFuture.completedFuture(true);
+                                        }
+                                        return CompletableFuture.completedFuture(result);
+                                    });;
                 }
                 mConnectionService.hold(this);
+                return holdFutureHandler;
             } else {
                 Log.e(this, new NullPointerException(),
                         "hold call failed due to null CS callId=%s", getId());
             }
-            Log.addEvent(this, LogUtils.Events.REQUEST_HOLD, reason);
         }
+        return holdFutureHandler;
     }
 
     /**
      * helper that can be used for any callback that requests a call state change and wants to
      * verify the change
      */
-    public void awaitCallStateChangeAndMaybeDisconnectCall(int targetCallState,
-            boolean shouldDisconnectUponTimeout, String callingMethod) {
+    public CompletableFuture<Boolean> awaitCallStateChangeAndMaybeDisconnectCall(
+            boolean shouldDisconnectUponTimeout, String callingMethod, int... targetCallStates) {
         TransactionManager tm = TransactionManager.getInstance();
-        tm.addTransaction(new VerifyCallStateChangeTransaction(mCallsManager.getLock(),
-                this, targetCallState), new OutcomeReceiver<>() {
+        CallTransaction callTransaction = new VerifyCallStateChangeTransaction(
+                mCallsManager.getLock(), this, targetCallStates);
+        return tm.addTransaction(callTransaction,
+                new OutcomeReceiver<>() {
             @Override
             public void onResult(CallTransactionResult result) {
                 Log.i(this, "awaitCallStateChangeAndMaybeDisconnectCall: %s: onResult:"
@@ -3210,22 +3283,29 @@
      * Releases the call from hold if it is currently active.
      */
     @VisibleForTesting
-    public void unhold() {
-        unhold(null /* reason */);
+    public CompletableFuture<Boolean> unhold() {
+        return unhold(null /* reason */);
     }
 
-    public void unhold(String reason) {
+    public CompletableFuture<Boolean> unhold(String reason) {
+        CompletableFuture<Boolean> unholdFutureHandler = CompletableFuture.completedFuture(false);
         if (mState == CallState.ON_HOLD) {
+            Log.addEvent(this, LogUtils.Events.REQUEST_UNHOLD, reason);
             if (mTransactionalService != null){
-                mTransactionalService.onSetActive(this);
+                return mTransactionalService.onSetActive(this);
             } else if (mConnectionService != null){
+                if (mFlags.enableCallSequencing()) {
+                    unholdFutureHandler = awaitCallStateChangeAndMaybeDisconnectCall(
+                            false /* shouldDisconnectUponTimeout */, "unhold", CallState.ACTIVE);
+                }
                 mConnectionService.unhold(this);
+                return unholdFutureHandler;
             } else {
                 Log.e(this, new NullPointerException(),
                         "unhold call failed due to null CS callId=%s", getId());
             }
-            Log.addEvent(this, LogUtils.Events.REQUEST_UNHOLD, reason);
         }
+        return unholdFutureHandler;
     }
 
     /** Checks if this is a live call or not. */
@@ -3325,6 +3405,13 @@
             }
         }
 
+        if (extras.containsKey(Connection.EXTRA_ANSWERING_DROPS_FG_CALL)) {
+            CharSequence appName =
+                    extras.getCharSequence(Connection.EXTRA_ANSWERING_DROPS_FG_CALL_APP_NAME);
+            Log.addEvent(this, LogUtils.Events.ANSWER_DROPS_FG,
+                    "Answering will drop FG call from %s", appName);
+        }
+
         // The remote connection service API can track the phone account which was originally
         // requested to create a connection via the remote connection service API; we store that so
         // we have some visibility into how a call was actually placed.
@@ -4993,4 +5080,12 @@
             }
         }
     }
+
+    public void setSimultaneousType(int simultaneousType) {
+        mSimultaneousType = simultaneousType;
+    }
+
+    public int getSimultaneousType() {
+        return mSimultaneousType;
+    }
 }
diff --git a/src/com/android/server/telecom/CallAudioCommunicationDeviceTracker.java b/src/com/android/server/telecom/CallAudioCommunicationDeviceTracker.java
index 8d5f9fd..7bd4dca 100644
--- a/src/com/android/server/telecom/CallAudioCommunicationDeviceTracker.java
+++ b/src/com/android/server/telecom/CallAudioCommunicationDeviceTracker.java
@@ -144,7 +144,8 @@
         boolean handleLeAudioDeviceSwitch = btDevice != null
                 && !btDevice.getAddress().equals(mBtAudioDevice);
         if ((audioDeviceType == mAudioDeviceType
-                || isUsbHeadsetType(audioDeviceType, mAudioDeviceType))
+                || isUsbHeadsetType(audioDeviceType, mAudioDeviceType)
+                || isSpeakerType(audioDeviceType, mAudioDeviceType))
                 && !handleLeAudioDeviceSwitch) {
             Log.i(this, "Communication device is already set for this audio type");
             return false;
@@ -161,7 +162,8 @@
             Log.i(this, "Available device type: " + device.getType());
             // Ensure that we do not select the same BT LE audio device for communication.
             if ((audioDeviceType == device.getType()
-                    || isUsbHeadsetType(audioDeviceType, device.getType()))
+                    || isUsbHeadsetType(audioDeviceType, device.getType())
+                    || isSpeakerType(audioDeviceType, device.getType()))
                     && !device.getAddress().equals(mBtAudioDevice)) {
                 activeDevice = device;
                 break;
@@ -234,13 +236,15 @@
                 audioDeviceType, isBtDevice);
 
         if (audioDeviceType != mAudioDeviceType
-                && !isUsbHeadsetType(audioDeviceType, mAudioDeviceType)) {
-            Log.i(this, "Unable to clear communication device of type(s), %s. "
-                            + "Device does not correspond to the locally requested device type.",
+                && !isUsbHeadsetType(audioDeviceType, mAudioDeviceType)
+                && !isSpeakerType(audioDeviceType, mAudioDeviceType)) {
+            Log.i(this, "Unable to clear communication device of type(s) %s. "
+                            + "Device does not correspond to the locally requested device type %s.",
                     audioDeviceType == AudioDeviceInfo.TYPE_WIRED_HEADSET
                             ? Arrays.asList(AudioDeviceInfo.TYPE_WIRED_HEADSET,
                             AudioDeviceInfo.TYPE_USB_HEADSET)
-                            : audioDeviceType
+                            : audioDeviceType,
+                    mAudioDeviceType
             );
             return;
         }
@@ -251,6 +255,7 @@
         }
 
         // Clear device and reset locally saved device type.
+        Log.i(this, "clearCommunicationDevice: AudioManager#clearCommunicationDevice()");
         mAudioManager.clearCommunicationDevice();
         mAudioDeviceType = sAUDIO_DEVICE_TYPE_INVALID;
 
@@ -266,4 +271,11 @@
         return audioDeviceType == AudioDeviceInfo.TYPE_WIRED_HEADSET
                 && sourceType == AudioDeviceInfo.TYPE_USB_HEADSET;
     }
+
+    private boolean isSpeakerType(@AudioDeviceInfo.AudioDeviceType int audioDeviceType,
+        @AudioDeviceInfo.AudioDeviceType int sourceType) {
+        if (!Flags.busDeviceIsASpeaker()) return false;
+        return audioDeviceType == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER
+                && sourceType == AudioDeviceInfo.TYPE_BUS;
+    }
 }
diff --git a/src/com/android/server/telecom/CallAudioModeStateMachine.java b/src/com/android/server/telecom/CallAudioModeStateMachine.java
index e149bdd..d1fd564 100644
--- a/src/com/android/server/telecom/CallAudioModeStateMachine.java
+++ b/src/com/android/server/telecom/CallAudioModeStateMachine.java
@@ -334,9 +334,15 @@
                     mAudioManager.abandonAudioFocusForCall();
                     // Clear requested communication device after the call ends.
                     if (mFeatureFlags.clearCommunicationDeviceAfterAudioOpsComplete()) {
-                        mCommunicationDeviceTracker.clearCommunicationDevice(
-                                mCommunicationDeviceTracker
-                                        .getCurrentLocallyRequestedCommunicationDevice());
+                        // Oh flags!  If we're using the refactored audio route switching, we should
+                        // not be using the communication device tracker; that is exclusively for
+                        // the old code path.
+                        if (!mFeatureFlags.dontUseCommunicationDeviceTracker()
+                                || !mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+                            mCommunicationDeviceTracker.clearCommunicationDevice(
+                                    mCommunicationDeviceTracker
+                                            .getCurrentLocallyRequestedCommunicationDevice());
+                        }
                     }
                     return HANDLED;
                 default:
diff --git a/src/com/android/server/telecom/CallAudioRouteController.java b/src/com/android/server/telecom/CallAudioRouteController.java
index 04f1934..d8e4a5e 100644
--- a/src/com/android/server/telecom/CallAudioRouteController.java
+++ b/src/com/android/server/telecom/CallAudioRouteController.java
@@ -65,6 +65,7 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 
@@ -120,6 +121,8 @@
     private boolean mAvailableRoutesUpdated;
     private final Object mLock = new Object();
     private final TelecomSystem.SyncRoot mTelecomLock;
+    private CountDownLatch mAudioOperationsCompleteLatch;
+    private CountDownLatch mAudioActiveCompleteLatch;
     private final BroadcastReceiver mSpeakerPhoneChangeReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
@@ -230,10 +233,12 @@
             @Override
             public void onCommunicationDeviceChanged(AudioDeviceInfo device) {
                 @AudioRoute.AudioRouteType int audioType = device != null
-                        ? DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.get(device.getType())
+                        ? DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.getOrDefault(
+                                device.getType(), TYPE_INVALID)
                         : TYPE_INVALID;
-                Log.i(this, "onCommunicationDeviceChanged: %d", audioType);
-                if (device != null && device.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) {
+                Log.i(this, "onCommunicationDeviceChanged: device (%s), audioType (%d)",
+                        device, audioType);
+                if (audioType == TYPE_SPEAKER) {
                     if (mCurrentRoute.getType() != TYPE_SPEAKER) {
                         sendMessageWithSessionInfo(SPEAKER_ON);
                     }
@@ -343,6 +348,9 @@
                         case MUTE_EXTERNALLY_CHANGED:
                             handleMuteChanged(mAudioManager.isMicrophoneMute());
                             break;
+                        case TOGGLE_MUTE:
+                            handleMuteChanged(!mIsMute);
+                            break;
                         case SWITCH_FOCUS:
                             focus = msg.arg1;
                             handleEndTone = (int) ((SomeArgs) msg.obj).arg2;
@@ -437,6 +445,12 @@
         } else {
             mCurrentRoute = DUMMY_ROUTE;
         }
+        // Audio ops will only ever be completed if there's a call placed and it gains
+        // ACTIVE/RINGING focus, hence why the initial value is 0.
+        mAudioOperationsCompleteLatch = new CountDownLatch(0);
+        // This latch will be count down when ACTIVE/RINGING focus is gained. This is determined
+        // when the routing goes active.
+        mAudioActiveCompleteLatch = new CountDownLatch(1);
         mIsActive = false;
         mCallAudioState = new CallAudioState(mIsMute, ROUTE_MAP.get(mCurrentRoute.getType()),
                 supportMask, null, new HashSet<>());
@@ -567,7 +581,8 @@
             }
             // override pending route while keep waiting for still pending messages for the
             // previous pending route
-            mPendingAudioRoute.setOrigRoute(mIsActive, mPendingAudioRoute.getDestRoute());
+            mPendingAudioRoute.setOrigRoute(mIsActive /* origin */,
+                    mPendingAudioRoute.getDestRoute(), active /* dest */);
         } else {
             if (mCurrentRoute.equals(destRoute) && (mIsActive == active)) {
                 return;
@@ -576,10 +591,12 @@
                     mIsActive, destRoute, active);
             // route to pending route
             if (getCallSupportedRoutes().contains(mCurrentRoute)) {
-                mPendingAudioRoute.setOrigRoute(mIsActive, mCurrentRoute);
+                mPendingAudioRoute.setOrigRoute(mIsActive /* origin */, mCurrentRoute,
+                        active /* dest */);
             } else {
                 // Avoid waiting for pending messages for an unavailable route
-                mPendingAudioRoute.setOrigRoute(mIsActive, DUMMY_ROUTE);
+                mPendingAudioRoute.setOrigRoute(mIsActive /* origin */, DUMMY_ROUTE,
+                        active /* dest */);
             }
             mIsPending = true;
         }
@@ -623,16 +640,40 @@
             updateAvailableRoutes(wiredHeadsetRoute, false);
             mEarpieceWiredRoute = null;
         }
-        AudioRoute earpieceRoute = mTypeRoutes.get(AudioRoute.TYPE_EARPIECE);
+        AudioRoute earpieceRoute = null;
+        try {
+            earpieceRoute = mTypeRoutes.get(AudioRoute.TYPE_EARPIECE) == null
+                ? mAudioRouteFactory.create(AudioRoute.TYPE_EARPIECE, null,
+                    mAudioManager)
+                : mTypeRoutes.get(AudioRoute.TYPE_EARPIECE);
+        } catch (IllegalArgumentException e) {
+            if (mFeatureFlags.telecomMetricsSupport()) {
+                mMetricsController.getErrorStats().log(ErrorStats.SUB_CALL_AUDIO,
+                        ErrorStats.ERROR_EXTERNAL_EXCEPTION);
+            }
+            Log.e(this, e, "Can't find available audio device info for route type:"
+                    + AudioRoute.DEVICE_TYPE_STRINGS.get(AudioRoute.TYPE_EARPIECE));
+        }
         if (earpieceRoute != null) {
             updateAvailableRoutes(earpieceRoute, true);
             mEarpieceWiredRoute = earpieceRoute;
+            // In the case that the route was never created, ensure that we update the map.
+            mTypeRoutes.putIfAbsent(AudioRoute.TYPE_EARPIECE, mEarpieceWiredRoute);
         }
         onAvailableRoutesChanged();
 
         // Route to expected state
         if (mCurrentRoute.equals(wiredHeadsetRoute)) {
-            routeTo(mIsActive, getBaseRoute(true, null));
+            // Preserve speaker routing if it was the last audio routing path when the wired headset
+            // disconnects. Ignore this special cased routing when the route isn't active
+            // (in other words, when we're not in a call).
+            AudioRoute route = mFeatureFlags.defaultSpeakerOnWiredHeadsetDisconnect()
+                    && mIsActive && mPendingAudioRoute.getOrigRoute() != null
+                    && mPendingAudioRoute.getOrigRoute().getType() == TYPE_SPEAKER
+                    && mSpeakerDockRoute != null
+                    && mSpeakerDockRoute.getType() == AudioRoute.TYPE_SPEAKER
+                    ? mSpeakerDockRoute : getBaseRoute(true, null);
+            routeTo(mIsActive, route);
         }
     }
 
@@ -917,7 +958,8 @@
                     BluetoothDevice device = mBluetoothRoutes.get(route);
                     // Check if in-band ringtone is enabled for the device; if it isn't, move to
                     // inactive route.
-                    if (device != null && !mBluetoothRouteManager.isInbandRingEnabled(device)) {
+                    if (device != null && !mBluetoothRouteManager
+                            .isInbandRingEnabled(route.getType(), device)) {
                         routeTo(false, route);
                     } else {
                         routeTo(true, route);
@@ -925,7 +967,8 @@
                 } else {
                     // Route is already active.
                     BluetoothDevice device = mBluetoothRoutes.get(mCurrentRoute);
-                    if (device != null && !mBluetoothRouteManager.isInbandRingEnabled(device)) {
+                    if (device != null && !mBluetoothRouteManager
+                            .isInbandRingEnabled(mCurrentRoute.getType(), device)) {
                         routeTo(false, mCurrentRoute);
                     }
                 }
@@ -961,8 +1004,9 @@
 
         if (bluetoothRoute != null && bluetoothDevice != null) {
             if (mFocusType == RINGING_FOCUS) {
-                routeTo(mBluetoothRouteManager.isInbandRingEnabled(bluetoothDevice) && mIsActive,
-                        bluetoothRoute);
+                routeTo(mBluetoothRouteManager
+                                .isInbandRingEnabled(bluetoothRoute.getType(), bluetoothDevice)
+                                && mIsActive, bluetoothRoute);
                 mBluetoothAddressForRinging = bluetoothDevice.getAddress();
             } else {
                 routeTo(mIsActive, bluetoothRoute);
@@ -978,12 +1022,16 @@
      * @return {@link AudioRoute} of the BT device.
      */
     private AudioRoute getArbitraryBluetoothDevice() {
-        if (mActiveBluetoothDevice != null) {
-            return getBluetoothRoute(mActiveBluetoothDevice.first, mActiveBluetoothDevice.second);
-        } else if (!mBluetoothRoutes.isEmpty()) {
-            return mBluetoothRoutes.keySet().stream().toList().get(mBluetoothRoutes.size() - 1);
+        synchronized (mLock) {
+            if (mActiveBluetoothDevice != null) {
+                return getBluetoothRoute(
+                    mActiveBluetoothDevice.first, mActiveBluetoothDevice.second);
+            } else if (!mBluetoothRoutes.isEmpty()) {
+                return mBluetoothRoutes.keySet().stream().toList()
+                    .get(mBluetoothRoutes.size() - 1);
+            }
+            return null;
         }
-        return null;
     }
 
     private void handleSwitchHeadset() {
@@ -1091,6 +1139,21 @@
             mIsPending = false;
             mPendingAudioRoute.clearPendingMessages();
             onCurrentRouteChanged();
+            if (mIsActive) {
+                // Reinitialize the audio ops complete latch since the routing went active. We
+                // should always expect operations to complete after this point.
+                if (mAudioOperationsCompleteLatch.getCount() == 0) {
+                    mAudioOperationsCompleteLatch = new CountDownLatch(1);
+                }
+                mAudioActiveCompleteLatch.countDown();
+            } else {
+                // Reinitialize the active routing latch when audio ops are complete so that it can
+                // once again be processed when a new call is placed/received.
+                if (mAudioActiveCompleteLatch.getCount() == 0) {
+                    mAudioActiveCompleteLatch = new CountDownLatch(1);
+                }
+                mAudioOperationsCompleteLatch.countDown();
+            }
             if (mFeatureFlags.telecomMetricsSupport()) {
                 mMetricsController.getAudioRouteStats().onRouteExit(mPendingAudioRoute, true);
             }
@@ -1450,8 +1513,11 @@
                 continue;
             }
             // Check if the most recently active device is a watch device.
-            boolean isActiveDevice = mActiveBluetoothDevice != null
+            boolean isActiveDevice;
+            synchronized (mLock) {
+                isActiveDevice = mActiveBluetoothDevice != null
                     && device.getAddress().equals(mActiveBluetoothDevice.second);
+            }
             if (i == (bluetoothRoutes.size() - 1) && mBluetoothRouteManager.isWatch(device)
                     && (device.equals(mCallAudioState.getActiveBluetoothDevice())
                     || isActiveDevice)) {
@@ -1507,15 +1573,16 @@
 
     private boolean isLeAudioNonLeadDeviceOrServiceUnavailable(@AudioRoute.AudioRouteType int type,
             BluetoothDevice device) {
+        BluetoothLeAudio leAudioService = getLeAudioService();
         if (type != AudioRoute.TYPE_BLUETOOTH_LE) {
             return false;
-        } else if (getLeAudioService() == null) {
+        } else if (leAudioService == null) {
             return true;
         }
 
-        int groupId = getLeAudioService().getGroupId(device);
+        int groupId = leAudioService.getGroupId(device);
         if (groupId != BluetoothLeAudio.GROUP_ID_INVALID) {
-            BluetoothDevice leadDevice = getLeAudioService().getConnectedGroupLeadDevice(groupId);
+            BluetoothDevice leadDevice = leAudioService.getConnectedGroupLeadDevice(groupId);
             Log.i(this, "Lead device for device (%s) is %s.", device, leadDevice);
             return leadDevice == null || !device.getAddress().equals(leadDevice.getAddress());
         }
@@ -1565,29 +1632,32 @@
      *                           address of the device.
      */
     public void updateActiveBluetoothDevice(Pair<Integer, String> device) {
-        mActiveDeviceCache.put(device.first, device.second);
-        // Update most recently active device if address isn't null (meaning some device is active).
-        if (device.second != null) {
-            mActiveBluetoothDevice = device;
-        } else {
-            // If a device was removed, check to ensure that no other device is still considered
-            // active.
-            boolean hasActiveDevice = false;
-            List<Map.Entry<Integer, String>> activeBtDevices = new ArrayList<>(
-                    mActiveDeviceCache.entrySet());
-            for (Map.Entry<Integer,String> activeDevice : activeBtDevices) {
-                Integer btAudioType = activeDevice.getKey();
-                String address = activeDevice.getValue();
-                if (address != null) {
-                    hasActiveDevice = true;
-                    if (mFeatureFlags.resolveActiveBtRoutingAndBtTimingIssue()) {
-                        mActiveBluetoothDevice = new Pair<>(btAudioType, address);
+        synchronized (mLock) {
+            mActiveDeviceCache.put(device.first, device.second);
+            // Update most recently active device if address isn't null (meaning
+            // some device is active).
+            if (device.second != null) {
+                mActiveBluetoothDevice = device;
+            } else {
+                // If a device was removed, check to ensure that no other device is
+                //still considered active.
+                boolean hasActiveDevice = false;
+                List<Map.Entry<Integer, String>> activeBtDevices =
+                        new ArrayList<>(mActiveDeviceCache.entrySet());
+                for (Map.Entry<Integer, String> activeDevice : activeBtDevices) {
+                    Integer btAudioType = activeDevice.getKey();
+                    String address = activeDevice.getValue();
+                    if (address != null) {
+                        hasActiveDevice = true;
+                        if (mFeatureFlags.resolveActiveBtRoutingAndBtTimingIssue()) {
+                            mActiveBluetoothDevice = new Pair<>(btAudioType, address);
+                        }
+                        break;
                     }
-                    break;
                 }
-            }
-            if (!hasActiveDevice) {
-                mActiveBluetoothDevice = null;
+                if (!hasActiveDevice) {
+                    mActiveBluetoothDevice = null;
+                }
             }
         }
     }
@@ -1616,4 +1686,12 @@
         sendMessageWithSessionInfo(SWITCH_BASELINE_ROUTE, INCLUDE_BLUETOOTH_IN_BASELINE,
                 btAddressToExclude);
     }
+
+    public CountDownLatch getAudioOperationsCompleteLatch() {
+        return mAudioOperationsCompleteLatch;
+    }
+
+    public CountDownLatch getAudioActiveCompleteLatch() {
+        return mAudioActiveCompleteLatch;
+    }
 }
diff --git a/src/com/android/server/telecom/CallAudioWatchdog.java b/src/com/android/server/telecom/CallAudioWatchdog.java
new file mode 100644
index 0000000..dcfc80f
--- /dev/null
+++ b/src/com/android/server/telecom/CallAudioWatchdog.java
@@ -0,0 +1,699 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.telecom;
+
+import static android.media.AudioPlaybackConfiguration.PLAYER_STATE_STARTED;
+
+import android.annotation.IntDef;
+import android.media.AudioAttributes;
+import android.media.AudioManager;
+import android.media.AudioManager.AudioPlaybackCallback;
+import android.media.AudioPlaybackConfiguration;
+import android.media.AudioRecord;
+import android.media.AudioRecordingConfiguration;
+import android.media.AudioTrack;
+import android.media.MediaRecorder;
+import android.os.Handler;
+import android.telecom.Log;
+import android.telecom.Logging.EventManager;
+import android.telecom.PhoneAccountHandle;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.LocalLog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.telecom.metrics.TelecomMetricsController;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.text.SimpleDateFormat;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Monitors {@link AudioRecord}, {@link AudioTrack}, and {@link AudioManager#getMode()} to determine
+ * the reliability of audio operations for a call.  Augments the Telecom dumpsys with Telecom calls
+ * with information about calls.
+ */
+public class CallAudioWatchdog extends CallsManagerListenerBase {
+    /**
+     * Bit flag set on a {@link CommunicationSession#sessionAttr} to indicate that the session has
+     * audio recording resources.
+     */
+    public static final int SESSION_ATTR_HAS_AUDIO_RECORD = 1 << 0;
+
+    /**
+     * Bit flag set on a {@link CommunicationSession#sessionAttr} to indicate that the session has
+     * audio playback resources.
+     */
+    public static final int SESSION_ATTR_HAS_AUDIO_PLAYBACK = 1 << 1;
+
+    /**
+     * Bit flag set on a {@link CommunicationSession#sessionAttr} to indicate that the uid for the
+     * session has a phone account allocated.  This helps us track cases where an app is telecom
+     * capable but chooses not to use the telecom integration.
+     */
+    public static final int SESSION_ATTR_HAS_PHONE_ACCOUNT = 1 << 2;
+
+    @IntDef(prefix = { "SESSION_ATTR_" },
+            value = {SESSION_ATTR_HAS_AUDIO_RECORD, SESSION_ATTR_HAS_AUDIO_PLAYBACK,
+                    SESSION_ATTR_HAS_PHONE_ACCOUNT},
+            flag = true)
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface SessionAttribute {}
+
+    /**
+     * Proxy for operations related to phone accounts.
+     */
+    public interface PhoneAccountRegistrarProxy {
+        /**
+         * Determines if a specified {@code uid} has an associated phone account registered.
+         * @param uid the uid.
+         * @return {@code true} if there is a phone account registered, {@code false} otherwise
+         */
+        boolean hasPhoneAccountForUid(int uid);
+
+        /**
+         * Given a {@link PhoneAccountHandle} determines the uid for the app owning the account.
+         * @param handle The phone account; the phone account handle's package and userhandle are
+         *               ultimately used to find the associated uid.
+         * @return the uid for the phone account.
+         */
+        int getUidForPhoneAccountHandle(PhoneAccountHandle handle);
+    }
+
+    /**
+     * Keyed on uid, tracks a communication session and whether there are audio record and playback
+     * resources for that session.
+     */
+    public class CommunicationSession {
+        private int uid;
+        @SessionAttribute
+        private int sessionAttr;
+        private ArrayMap<Integer, Set<Integer>> audioResourcesByType = new ArrayMap<>();
+        private EventManager.Loggable telecomCall;
+        private long sessionStartMillis;
+        private long sessionStartClockMillis;
+
+        /**
+         * @return {@code true} if audio record or playback is held for the session, {@code false}
+         * otherwise.
+         */
+        public boolean hasMediaResources() {
+            return (getSessionAttr()
+                    & (SESSION_ATTR_HAS_AUDIO_RECORD | SESSION_ATTR_HAS_AUDIO_PLAYBACK)) != 0;
+        }
+
+        /**
+         * Sets a bit enabled for the session.
+         * @param bit the bit
+         */
+        public void setBit(@SessionAttribute int bit) {
+            setSessionAttr(getSessionAttr() | bit);
+        }
+
+        /**
+         * Clears the specified bit for the session.
+         * @param bit the bit
+         */
+        public void clearBit(@SessionAttribute int bit) {
+            setSessionAttr(getSessionAttr() & ~bit);
+        }
+
+        /**
+         * Determines if a bit is set in the given bitmask.
+         * @param mask the bitmask.
+         * @param bit The bit
+         * @return {@code true} if set, {@code false} otherwise.
+         */
+        public static boolean isBitSet(@SessionAttribute int mask, @SessionAttribute int bit) {
+            return (mask & bit) == bit;
+        }
+
+        /**
+         * Determines if a bit is set for the current session.
+         * @param bit The bit
+         * @return {@code true} if set, {@code false} otherwise.
+         */
+        public boolean isBitSet(@SessionAttribute int bit) {
+            return isBitSet(getSessionAttr(), bit);
+        }
+
+        /**
+         * Generate a string representing the session attributes bitmask, suitable for logging.
+         * @param attr The session attributes.
+         * @return String of bits!
+         */
+        public static String sessionAttrToString(@SessionAttribute int attr) {
+            return (isBitSet(attr, SESSION_ATTR_HAS_PHONE_ACCOUNT) ? "phac, " : "") +
+                    (isBitSet(attr, SESSION_ATTR_HAS_AUDIO_PLAYBACK) ? "ap, " : "") +
+                    (isBitSet(attr, SESSION_ATTR_HAS_AUDIO_RECORD) ? "ar, " : "");
+        }
+
+        @Override
+        public String toString() {
+            return "CommSess{" +
+                    "uid=" + getUid() +
+                    ", created=" + SimpleDateFormat.getDateTimeInstance().format(
+                    new Date(getSessionStartClockMillis())) +
+                    ", attr=" + sessionAttrToString(getSessionAttr()) +
+                    ", callId=" + (getTelecomCall() != null ? getTelecomCall().getId() : "none") +
+                    ", duration=" + (mClockProxy.elapsedRealtime() - getSessionStartMillis())/1000 +
+                    '}';
+        }
+
+        /**
+         * The uid for the session.
+         */
+        public int getUid() {
+            return uid;
+        }
+
+        public void setUid(int uid) {
+            this.uid = uid;
+        }
+
+        /**
+         * The attributes for the session.
+         */
+        public int getSessionAttr() {
+            return sessionAttr;
+        }
+
+        public void setSessionAttr(int sessionAttr) {
+            this.sessionAttr = sessionAttr;
+        }
+
+        /**
+         * ArrayMap, keyed by {@link #SESSION_ATTR_HAS_AUDIO_PLAYBACK} and
+         * {@link #SESSION_ATTR_HAS_AUDIO_RECORD}. For each, contains a set of the
+         * {@link AudioManager} ids associated with active playback and recording sessions for a
+         * uid.
+         *
+         * {@link AudioPlaybackConfiguration#getPlayerInterfaceId()} is used for audio playback;
+         * per docs, this is an identifier unique for the lifetime of the player.
+         *
+         * {@link AudioRecordingConfiguration#getClientAudioSessionId()} is used for audio record
+         * tracking; this is unique similar to the audio playback config.
+         */
+        public ArrayMap<Integer, Set<Integer>> getAudioResourcesByType() {
+            return audioResourcesByType;
+        }
+
+        public void setAudioResourcesByType(
+                ArrayMap<Integer, Set<Integer>> audioResourcesByType) {
+            this.audioResourcesByType = audioResourcesByType;
+        }
+
+        /**
+         * The Telecom call this session is associated with; set if the call takes place during a
+         * telecom call.
+         */
+        public EventManager.Loggable getTelecomCall() {
+            return telecomCall;
+        }
+
+        public void setTelecomCall(EventManager.Loggable telecomCall) {
+            this.telecomCall = telecomCall;
+        }
+
+        /**
+         * The time in {@link android.os.SystemClock#elapsedRealtime()} timebase when the session
+         * started.  Used only to determine duration.
+         */
+        public long getSessionStartMillis() {
+            return sessionStartMillis;
+        }
+
+        public void setSessionStartMillis(long sessionStartMillis) {
+            this.sessionStartMillis = sessionStartMillis;
+        }
+
+        /**
+         * The time in {@link System#currentTimeMillis()} timebase when the session started; used
+         * to indicate the wall block time when the session started.
+         */
+        public long getSessionStartClockMillis() {
+            return sessionStartClockMillis;
+        }
+
+        public void setSessionStartClockMillis(long sessionStartClockMillis) {
+            this.sessionStartClockMillis = sessionStartClockMillis;
+        }
+    }
+
+    /**
+     * Listener for AudioManager audio playback changes.  Finds audio playback tagged for voice
+     * communication.  Updates the {@link #mCommunicationSessions} based on this data to track if
+     * audio playback it taking place.
+     *
+     * Note: {@link AudioPlaybackCallback} reports information about audio playback for an app; if
+     * an app releases audio playback resources, the list of audio playback configurations no longer
+     * includes a {@link AudioPlaybackConfiguration} for that specific audio playback session.  This
+     * API semantic is why the code below is a bit confusing; in the listener we need to track all
+     * the ids we've seen and then correlate that back to what we knew about it from the last
+     * callback.
+     *
+     * An app may have MULTIPLE {@link AudioPlaybackConfiguration} for voip use-cases and switch
+     * between them for a single call -- this was observed in live app testing.
+     */
+    public class WatchdogAudioPlaybackCallback extends AudioPlaybackCallback {
+        @Override
+        public void onPlaybackConfigChanged(List<AudioPlaybackConfiguration> configs) {
+            Map<Integer,Set<Integer>> sessionIdentifiersByUid = new ArrayMap<>();
+            for (AudioPlaybackConfiguration config : configs) {
+                Log.d(this, "onPlaybackConfigChanged: config=%s", config);
+                // only track USAGE_VOICE_COMMUNICATION as this is for VOIP calls.
+                if (config.getAudioAttributes() != null
+                        && config.getAudioAttributes().getUsage()
+                        == AudioAttributes.USAGE_VOICE_COMMUNICATION) {
+
+                    // If an audio session is idle, we don't count it as playing.  It must be in a
+                    // started state.
+                    boolean isPlaying = config.getPlayerState() == PLAYER_STATE_STARTED;
+
+                    maybeTrackAudioPlayback(config.getClientUid(), config.getPlayerInterfaceId(),
+                            isPlaying);
+                    if (isPlaying) {
+                        // Track the list of player id active for each uid; we use it later for
+                        // cleanup of stale sessions.
+                        putOrDefault(sessionIdentifiersByUid,config.getClientUid(),
+                                new ArraySet<>()).add(config.getPlayerInterfaceId());
+                    }
+                }
+            }
+
+            // The listener will drop uid/playerInterfaceIds no longer active, so we need to go back
+            // and see if any sessions need to be removed now.
+            cleanupAttributeForSessions(SESSION_ATTR_HAS_AUDIO_PLAYBACK,
+                    sessionIdentifiersByUid);
+        }
+    }
+
+    /**
+     * Similar to {@link WatchdogAudioPlaybackCallback}, tracks audio recording an app performs.
+     * This code is handling the onRecordingConfigChanged event from the AudioManager. The event
+     * is fired when the list of active recording configurations changes. In this case, the code
+     * is only interested in recording configurations that are using the VOICE_COMMUNICATION
+     * audio source. For these configurations, the code tracks the session identifiers and
+     * potentially adds them to the SESSION_ATTR_HAS_AUDIO_RECORD attribute. The code also cleans
+     * up the attribute for any sessions that are no longer active.
+     * The same caveat/note applies here; a single app can have many audio recording sessions that
+     * the app swaps between during a call.
+     */
+    public class WatchdogAudioRecordCallback extends AudioManager.AudioRecordingCallback {
+        @Override
+        public void onRecordingConfigChanged(List<AudioRecordingConfiguration> configs) {
+            List<AudioRecordingConfiguration> theConfigs =
+                    mAudioManager.getActiveRecordingConfigurations();
+            Map<Integer,Set<Integer>> sessionIdentifiersByUid = new ArrayMap<>();
+            for (AudioRecordingConfiguration config : theConfigs) {
+                if (config.getClientAudioSource()
+                        == MediaRecorder.AudioSource.VOICE_COMMUNICATION) {
+
+                    putOrDefault(sessionIdentifiersByUid, config.getClientUid(),
+                            new ArraySet<>()).add(config.getClientAudioSessionId());
+                    maybeTrackAudioRecord(config.getClientUid(), config.getClientAudioSessionId(),
+                            true);
+                }
+            }
+            // The listener stops reporting audio sessions that go away, so we need to clean up the
+            // session potentially.
+            cleanupAttributeForSessions(
+                    SESSION_ATTR_HAS_AUDIO_RECORD,
+                    sessionIdentifiersByUid);
+        }
+    }
+
+    // Proxies to make testing possible-ish.
+    private final ClockProxy mClockProxy;
+    private final PhoneAccountRegistrarProxy mPhoneAccountRegistrarProxy;
+
+    private final WatchdogAudioPlaybackCallback mWatchdogAudioPlayback =
+            new WatchdogAudioPlaybackCallback();
+    private final WatchdogAudioRecordCallback
+            mWatchdogAudioRecordCallack = new WatchdogAudioRecordCallback();
+    private final AudioManager mAudioManager;
+    private final Handler mHandler;
+
+    // Guards access to mCommunicationSessions.
+    private final Object mCommunicationSessionsLock = new Object();
+
+    /**
+     * Key - UID of communication app.
+     * Value - an instance of {@link CommunicationSession} tracking data for that uid.
+     */
+    private final Map<Integer, CommunicationSession> mCommunicationSessions = new ArrayMap<>();
+
+    // Local logs for tracking non-telecom calls.
+    private final LocalLog mLocalLog = new LocalLog(30);
+
+    private final TelecomMetricsController mMetricsController;
+
+    public CallAudioWatchdog(AudioManager audioManager,
+            PhoneAccountRegistrarProxy phoneAccountRegistrarProxy, ClockProxy clockProxy,
+            Handler handler, TelecomMetricsController metricsController) {
+        mPhoneAccountRegistrarProxy = phoneAccountRegistrarProxy;
+        mClockProxy = clockProxy;
+        mAudioManager = audioManager;
+        mHandler = handler;
+        mAudioManager.registerAudioPlaybackCallback(mWatchdogAudioPlayback, mHandler);
+        mAudioManager.registerAudioRecordingCallback(mWatchdogAudioRecordCallack, mHandler);
+        mMetricsController = metricsController;
+    }
+
+    /**
+     * Tracks Telecom adding a call; we use this to associate a uid's sessions with a call.
+     * Note: this is not 100% accurate if there are multiple calls -- we just associate with the
+     * first call and leave it at that.  It's not possible to know which audio sessions belong to
+     * which Telecom calls.
+     * @param call the Telecom call being added.
+     */
+    @Override
+    public void onCallAdded(Call call) {
+        // Only track for voip calls.
+        if (call.isSelfManaged() || call.isTransactionalCall()) {
+            maybeTrackTelecomCall(call);
+        }
+    }
+
+    @Override
+    public void onCallRemoved(Call call) {
+        // Only track for voip calls.
+        if (call.isSelfManaged() || call.isTransactionalCall()) {
+            maybeRemoveCall(call);
+        }
+    }
+
+    @VisibleForTesting
+    public WatchdogAudioPlaybackCallback getWatchdogAudioPlayback() {
+        return mWatchdogAudioPlayback;
+    }
+
+    @VisibleForTesting
+    public WatchdogAudioRecordCallback getWatchdogAudioRecordCallack() {
+        return mWatchdogAudioRecordCallack;
+    }
+
+    @VisibleForTesting
+    public Map<Integer, CommunicationSession> getCommunicationSessions() {
+        return mCommunicationSessions;
+    }
+
+    /**
+     * Include info on audio stuff in the telecom dumpsys.
+     * @param pw
+     */
+    void dump(IndentingPrintWriter pw) {
+        pw.println("CallAudioWatchdog:");
+        pw.increaseIndent();
+        pw.println("Active Sessions:");
+        pw.increaseIndent();
+        Collection<CommunicationSession> sessions;
+        synchronized (mCommunicationSessionsLock) {
+            sessions = mCommunicationSessions.values();
+        }
+        sessions.forEach(pw::println);
+        pw.decreaseIndent();
+        pw.println("Audio sessions Sessions:");
+        pw.increaseIndent();
+        mLocalLog.dump(pw);
+        pw.decreaseIndent();
+        pw.decreaseIndent();
+    }
+
+    /**
+     * Tracks audio playback for a uid.
+     * @param uid the uid of the app having audio back change.
+     * @param playerInterfaceId From {@link AudioPlaybackConfiguration#getPlayerInterfaceId()} (see
+     * {@link CommunicationSession#audioResourcesByType} for keying info).
+     * @param isPlaying {@code true} if audio is starting for the client.
+     */
+    private void maybeTrackAudioPlayback(int uid, int playerInterfaceId, boolean isPlaying) {
+        CommunicationSession session;
+        synchronized (mCommunicationSessionsLock) {
+            if (!isPlaying) {
+                // A session can start in an idle state and never go active; in this case we will
+                // not proactively add a new session; we'll just get one if it's already there.
+                // When the session goes active we can add it then.
+                session = getSession(uid);
+            } else {
+                // The playback is active, so we need to get or add a new communication session.
+                session = getOrAddSession(uid);
+            }
+        }
+        if (session == null) {
+            return;
+        }
+
+        // First track individual player interface id playing status.
+        if (isPlaying) {
+            putOrDefault(session.getAudioResourcesByType(), SESSION_ATTR_HAS_AUDIO_PLAYBACK,
+                    new ArraySet<>()).add(playerInterfaceId);
+        } else {
+            putOrDefault(session.getAudioResourcesByType(), SESSION_ATTR_HAS_AUDIO_PLAYBACK,
+                    new ArraySet<>()).remove(playerInterfaceId);
+        }
+
+        // Keep the bitmask up to date so that we have quicker access to the audio playback state.
+        int originalAttrs = session.getSessionAttr();
+        // If there are active audio playback clients, then the session has playback.
+        if (!session.getAudioResourcesByType().get(SESSION_ATTR_HAS_AUDIO_PLAYBACK).isEmpty()) {
+            session.setBit(SESSION_ATTR_HAS_AUDIO_PLAYBACK);
+        } else {
+            session.clearBit(SESSION_ATTR_HAS_AUDIO_PLAYBACK);
+        }
+
+        // If there was a change, log to a call if set.
+        if (originalAttrs != session.getSessionAttr() && session.getTelecomCall() != null) {
+            Log.addEvent(session.getTelecomCall(), LogUtils.Events.AUDIO_ATTR,
+                    CommunicationSession.sessionAttrToString(originalAttrs)
+                            + " -> " + CommunicationSession.sessionAttrToString(
+                            session.getSessionAttr()));
+        }
+        Log.d(this, "maybeTrackAudioPlayback: %s", session);
+    }
+
+    /**
+     * Similar to {@link #maybeTrackAudioPlayback(int, int, boolean)}, except tracks audio records
+     * for an app.
+     * @param uid the app uid.
+     * @param recordSessionID The recording session (per
+     * @param isRecording {@code true} if recording, {@code false} otherwise.
+     */
+    private void maybeTrackAudioRecord(int uid, int recordSessionID, boolean isRecording) {
+        synchronized (mCommunicationSessionsLock) {
+            CommunicationSession session = getOrAddSession(uid);
+
+            // First track individual recording status.
+            if (isRecording) {
+                putOrDefault(session.getAudioResourcesByType(), SESSION_ATTR_HAS_AUDIO_RECORD,
+                        new ArraySet<>()).add(recordSessionID);
+            } else {
+                putOrDefault(session.getAudioResourcesByType(), SESSION_ATTR_HAS_AUDIO_RECORD,
+                        new ArraySet<>()).remove(recordSessionID);
+            }
+
+            int originalAttrs = session.getSessionAttr();
+            if (!session.getAudioResourcesByType().get(SESSION_ATTR_HAS_AUDIO_RECORD).isEmpty()) {
+                session.setBit(SESSION_ATTR_HAS_AUDIO_RECORD);
+            } else {
+                session.clearBit(SESSION_ATTR_HAS_AUDIO_RECORD);
+            }
+
+            if (originalAttrs != session.getSessionAttr() && session.getTelecomCall() != null) {
+                Log.addEvent(session.getTelecomCall(), LogUtils.Events.AUDIO_ATTR,
+                        CommunicationSession.sessionAttrToString(originalAttrs)
+                        + " -> " + CommunicationSession.sessionAttrToString(
+                                session.getSessionAttr()));
+            }
+
+            Log.d(this, "maybeTrackAudioRecord: %s", session);
+        }
+    }
+
+    /**
+     * Given a new Telecom call, start a new session or annotate an existing one with this call.
+     * Helps to associated resources with a telecom call.
+     * @param call the call!
+     */
+    private void maybeTrackTelecomCall(Call call) {
+        int uid = mPhoneAccountRegistrarProxy.getUidForPhoneAccountHandle(
+                call.getTargetPhoneAccount());
+        CommunicationSession session;
+        synchronized (mCommunicationSessionsLock) {
+            session = getOrAddSession(uid);
+        }
+        session.setTelecomCall(call);
+        Log.d(this, "maybeTrackTelecomCall: %s", session);
+        Log.addEvent(session.getTelecomCall(), LogUtils.Events.AUDIO_ATTR,
+                CommunicationSession.sessionAttrToString(session.getSessionAttr()));
+    }
+
+    /**
+     * Given a telecom call, cleanup the session if there are no audio resources remaining for that
+     * session.
+     * @param call The call.
+     */
+    private void maybeRemoveCall(Call call) {
+        int uid = mPhoneAccountRegistrarProxy.getUidForPhoneAccountHandle(
+                call.getTargetPhoneAccount());
+        CommunicationSession session;
+        synchronized (mCommunicationSessionsLock) {
+            session = getSession(uid);
+            if (session == null) {
+                return;
+            }
+            if (!session.hasMediaResources()) {
+                mLocalLog.log(session.toString());
+                maybeLogMetrics(session);
+                mCommunicationSessions.remove(uid);
+            }
+        }
+    }
+
+    /**
+     * Returns an existing session for a uid, or {@code null} if none exists.
+     * @param uid the uid,
+     * @return The session found, or {@code null}.
+     */
+    private CommunicationSession getSession(int uid) {
+        return mCommunicationSessions.get(uid);
+    }
+
+    /**
+     * Locates an existing session for the specified uid or creates a new one.
+     * @param uid the uid
+     * @return The session.
+     */
+    private CommunicationSession getOrAddSession(int uid) {
+        CommunicationSession session = mCommunicationSessions.get(uid);
+        if (session != null) {
+            Log.i(this, "getOrAddSession: uid=%d, ex, %s", uid, session);
+            return session;
+        } else {
+            CommunicationSession newSession = new CommunicationSession();
+            newSession.setSessionStartMillis(mClockProxy.elapsedRealtime());
+            newSession.setSessionStartClockMillis(mClockProxy.currentTimeMillis());
+            newSession.setUid(uid);
+            if (mPhoneAccountRegistrarProxy.hasPhoneAccountForUid(uid)) {
+                newSession.setBit(SESSION_ATTR_HAS_PHONE_ACCOUNT);
+            }
+            mCommunicationSessions.put(uid, newSession);
+            Log.i(this, "getOrAddSession: uid=%d, new, %s", uid, newSession);
+            return newSession;
+        }
+    }
+
+    /**
+     * This method is used to cleanup any playback or recording sessions that may have went away
+     * after the {@link AudioPlaybackConfiguration} or {@link AudioRecordingConfiguration} updates.
+     *
+     * {@link CommunicationSession#audioResourcesByType} is keyed by
+     * {@link #SESSION_ATTR_HAS_AUDIO_RECORD} and {@link #SESSION_ATTR_HAS_AUDIO_PLAYBACK} and
+     * contains a list of each of the record or playback sessions we've been tracking.
+     *
+     * @param bit the type of resources to cleanup.
+     * @param sessionsByUid A map, keyed on uid of the set of play or record ids that were provided
+     *                      in the most recent {@link AudioPlaybackConfiguration} or
+     *                      {@link AudioRecordingConfiguration} update.
+     */
+    private void cleanupAttributeForSessions(int bit, Map<Integer, Set<Integer>> sessionsByUid) {
+        synchronized (mCommunicationSessionsLock) {
+            // Use an iterator so we can do in-place removal.
+            Iterator<Map.Entry<Integer, CommunicationSession>> iterator =
+                    mCommunicationSessions.entrySet().iterator();
+
+            // Lets loop through all the uids we're tracking and see that they still have an audio
+            // resource of type {@code bit} in {@code sessionsByUid}.
+            while (iterator.hasNext()) {
+                Map.Entry<Integer, CommunicationSession> next = iterator.next();
+                int existingUid = next.getKey();
+                CommunicationSession session = next.getValue();
+
+                // Get the set of sessions for this type, or emptyset if none present.
+                Set<Integer> sessionsForThisUid = sessionsByUid.getOrDefault(existingUid,
+                        Collections.emptySet());
+
+                // Update the known sessions of this resource type in the CommunicationSession.
+                Set<Integer> trackedSessions = putOrDefault(session.getAudioResourcesByType(), bit,
+                        new ArraySet<>());
+                trackedSessions.clear();
+                trackedSessions.addAll(sessionsForThisUid);
+
+                // Set or unset the bit in the bitmask for quicker access.
+                if (!trackedSessions.isEmpty()) {
+                    session.setBit(bit);
+                } else {
+                    session.clearBit(bit);
+                }
+
+                // If audio resources are no longer held for a uid, then we'll clean up its
+                // media session.
+                if (!session.hasMediaResources() && session.getTelecomCall() == null) {
+                    Log.i(this, "cleanupAttributeForSessions: removing session %s", session);
+                    mLocalLog.log(session.toString());
+                    maybeLogMetrics(session);
+                    iterator.remove();
+                }
+            }
+        }
+    }
+
+    /**
+     * Generic method to put a key value to a map and set to a default it not found, in both cases
+     * returning the value.
+     *
+     * This is a concession due to the fact that {@link Map#putIfAbsent(Object, Object)} returns
+     * null if the default is set. 🙄
+     *
+     * @param map The map.
+     * @param key The key to find.
+     * @param theDefault The default value for the key to use and return if nothing found.
+     * @return The existing key value or the default after adding.
+     * @param <K> The map key
+     * @param <V> The map value
+     */
+    private <K,V> V putOrDefault(Map<K,V> map, K key, V theDefault) {
+        if (map.containsKey(key)) {
+            return map.get(key);
+        }
+
+        map.put(key, theDefault);
+        return theDefault;
+    }
+
+    /**
+     * If this call has no associated Telecom {@link Call} and metrics are enabled, log this as a
+     * non-telecom call.
+     * @param session the session to log.
+     */
+    private void maybeLogMetrics(CommunicationSession session) {
+        if (mMetricsController != null && session.getTelecomCall() == null) {
+            mMetricsController.getCallStats().onNonTelecomCallEnd(
+                    session.isBitSet(SESSION_ATTR_HAS_PHONE_ACCOUNT),
+                    session.getUid(),
+                    mClockProxy.elapsedRealtime() - session.getSessionStartMillis());
+        }
+    }
+}
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index 22b28b5..4d2d533 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -27,6 +27,8 @@
 import static android.provider.CallLog.Calls.USER_MISSED_NOT_RUNNING;
 import static android.provider.CallLog.Calls.USER_MISSED_NO_ANSWER;
 import static android.provider.CallLog.Calls.USER_MISSED_SHORT_RING;
+import static android.telecom.CallAttributes.DIRECTION_INCOMING;
+import static android.telecom.CallAttributes.DIRECTION_OUTGOING;
 import static android.telecom.TelecomManager.ACTION_POST_CALL;
 import static android.telecom.TelecomManager.DURATION_LONG;
 import static android.telecom.TelecomManager.DURATION_MEDIUM;
@@ -54,6 +56,7 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.PackageManager.ResolveInfoFlags;
 import android.content.pm.ResolveInfo;
 import android.content.pm.UserInfo;
@@ -132,6 +135,8 @@
 import com.android.server.telecom.callfiltering.IncomingCallFilterGraphProvider;
 import com.android.server.telecom.callredirection.CallRedirectionProcessor;
 import com.android.server.telecom.callsequencing.CallSequencingController;
+import com.android.server.telecom.callsequencing.CallTransaction;
+import com.android.server.telecom.callsequencing.voip.IncomingCallTransaction;
 import com.android.server.telecom.components.ErrorDialogActivity;
 import com.android.server.telecom.components.TelecomBroadcastReceiver;
 import com.android.server.telecom.callsequencing.CallsManagerCallSequencingAdapter;
@@ -148,6 +153,7 @@
 import com.android.server.telecom.ui.ToastFactory;
 import com.android.server.telecom.callsequencing.voip.VoipCallMonitor;
 import com.android.server.telecom.callsequencing.TransactionManager;
+import com.android.server.telecom.callsequencing.voip.VoipCallMonitorLegacy;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -272,7 +278,7 @@
      * {@link #getNumCallsWithState(int, Call, PhoneAccountHandle, int...)} to indicate both managed
      * and self-managed calls should be included.
      */
-    private static final int CALL_FILTER_ALL = 3;
+    public static final int CALL_FILTER_ALL = 3;
 
     private static final String PERMISSION_PROCESS_PHONE_ACCOUNT_REGISTRATION =
             "android.permission.PROCESS_PHONE_ACCOUNT_REGISTRATION";
@@ -323,8 +329,12 @@
             UUID.fromString("0a86157c-50ca-11ee-be56-0242ac120002");
     public static final String TELEPHONY_HAS_DEFAULT_BUT_TELECOM_DOES_NOT_MSG =
             "Telephony has a default MO acct but Telecom prompted user for MO";
+    public static final UUID CANNOT_HOLD_CURRENT_ACTIVE_CALL_ERROR_UUID =
+            UUID.fromString("1b6a9b88-5049-4ffa-a52a-134d7c3a40e6");
+    public static final UUID FAILED_TO_SWITCH_FOCUS_ERROR_UUID =
+            UUID.fromString("a1b2c3d4-e5f6-7890-1234-567890abcdef");
 
-    private static final int[] OUTGOING_CALL_STATES =
+    public static final int[] OUTGOING_CALL_STATES =
             {CallState.CONNECTING, CallState.SELECT_PHONE_ACCOUNT, CallState.DIALING,
                     CallState.PULLING};
 
@@ -369,6 +379,7 @@
                 Analytics.THIRD_PARTY_PHONE);
     }
 
+    private static final long WAIT_FOR_AUDIO_UPDATE_TIMEOUT = 4000L;
     /**
      * The main call repository. Keeps an instance of all live calls. New incoming and outgoing
      * calls are added to the map and removed when the calls move to the disconnected state.
@@ -480,9 +491,13 @@
     private final ConnectionServiceFocusManager mConnectionSvrFocusMgr;
     /* Handler tied to thread in which CallManager was initialized. */
     private final Handler mHandler = new Handler(Looper.getMainLooper());
+    private final HandlerThread mHandlerThread = new HandlerThread("telecomAudioCallbacks",
+            android.os.Process.THREAD_PRIORITY_BACKGROUND);
+    private final Handler mAudioCallbackHandler;
     private final EmergencyCallHelper mEmergencyCallHelper;
     private final RoleManagerAdapter mRoleManagerAdapter;
     private final VoipCallMonitor mVoipCallMonitor;
+    private final VoipCallMonitorLegacy mVoipCallMonitorLegacy;
     private final CallEndpointController mCallEndpointController;
     private final CallAnomalyWatchdog mCallAnomalyWatchdog;
 
@@ -498,6 +513,8 @@
     private final com.android.internal.telephony.flags.FeatureFlags mTelephonyFeatureFlags;
 
     private final IncomingCallFilterGraphProvider mIncomingCallFilterGraphProvider;
+    private final CallAudioWatchdog mCallAudioWatchDog;
+    private final CallAudioRouteAdapter mCallAudioRouteAdapter;
 
     private final ConnectionServiceFocusManager.CallsManagerRequester mRequester =
             new ConnectionServiceFocusManager.CallsManagerRequester() {
@@ -644,15 +661,43 @@
         mCallerInfoLookupHelper = callerInfoLookupHelper;
         mEmergencyCallDiagnosticLogger = emergencyCallDiagnosticLogger;
         mIncomingCallFilterGraphProvider = incomingCallFilterGraphProvider;
+        if (featureFlags.enableCallAudioWatchdog()) {
+            mHandlerThread.start();
+            mAudioCallbackHandler = new Handler(mHandlerThread.getLooper());
+            mCallAudioWatchDog = new CallAudioWatchdog(
+                    mContext.getSystemService(AudioManager.class),
+                    new CallAudioWatchdog.PhoneAccountRegistrarProxy() {
+                        @Override
+                        public boolean hasPhoneAccountForUid(int uid) {
+                            return mPhoneAccountRegistrar.hasPhoneAccountForUid(uid);
+                        }
+
+                        @Override
+                        public int getUidForPhoneAccountHandle(PhoneAccountHandle handle) {
+                            Context userContext = mContext.createContextAsUser(
+                                    handle.getUserHandle(),
+                                    0 /*flags */);
+                            try {
+                                return userContext.getPackageManager().getPackageUid(
+                                        handle.getComponentName().getPackageName(), 0 /* flags */);
+                            } catch (NameNotFoundException nfe) {
+                                return -1;
+                            }
+                        }
+                    }, clockProxy, mAudioCallbackHandler,
+                    featureFlags.telecomMetricsSupport() ? metricsController : null);
+        } else {
+            mAudioCallbackHandler = null;
+            mCallAudioWatchDog = null;
+        }
 
         mDtmfLocalTonePlayer =
                 new DtmfLocalTonePlayer(new DtmfLocalTonePlayer.ToneGeneratorProxy());
-        CallAudioRouteAdapter callAudioRouteAdapter;
         // TODO: add another flag check when
         // bluetoothDeviceManager.getBluetoothHeadset().isScoManagedByAudio()
         // available and return true
         if (!featureFlags.useRefactoredAudioRouteSwitching()) {
-            callAudioRouteAdapter = callAudioRouteStateMachineFactory.create(
+            mCallAudioRouteAdapter = callAudioRouteStateMachineFactory.create(
                     context,
                     this,
                     bluetoothManager,
@@ -665,17 +710,17 @@
                     featureFlags
             );
         } else {
-            callAudioRouteAdapter = new CallAudioRouteController(context, this, audioServiceFactory,
-                    new AudioRoute.Factory(), wiredHeadsetManager, mBluetoothRouteManager,
-                    statusBarNotifier, featureFlags, metricsController);
+            mCallAudioRouteAdapter = new CallAudioRouteController(context, this,
+                    audioServiceFactory, new AudioRoute.Factory(), wiredHeadsetManager,
+                    mBluetoothRouteManager, statusBarNotifier, featureFlags, metricsController);
         }
-        callAudioRouteAdapter.initialize();
-        bluetoothStateReceiver.setCallAudioRouteAdapter(callAudioRouteAdapter);
-        bluetoothDeviceManager.setCallAudioRouteAdapter(callAudioRouteAdapter);
+        mCallAudioRouteAdapter.initialize();
+        bluetoothStateReceiver.setCallAudioRouteAdapter(mCallAudioRouteAdapter);
+        bluetoothDeviceManager.setCallAudioRouteAdapter(mCallAudioRouteAdapter);
 
         CallAudioRoutePeripheralAdapter callAudioRoutePeripheralAdapter =
                 new CallAudioRoutePeripheralAdapter(
-                        callAudioRouteAdapter,
+                        mCallAudioRouteAdapter,
                         bluetoothManager,
                         wiredHeadsetManager,
                         mDockManager,
@@ -711,7 +756,7 @@
             mCallRecordingTonePlayer = new CallRecordingTonePlayer(mContext, audioManager,
                     mTimeoutsAdapter, mLock);
         }
-        mCallAudioManager = new CallAudioManager(callAudioRouteAdapter,
+        mCallAudioManager = new CallAudioManager(mCallAudioRouteAdapter,
                 this, callAudioModeStateMachineFactory.create(systemStateHelper,
                 (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE),
                 featureFlags, communicationDeviceTracker),
@@ -732,20 +777,30 @@
         mClockProxy = clockProxy;
         mToastFactory = toastFactory;
         mRoleManagerAdapter = roleManagerAdapter;
-        mVoipCallMonitor = new VoipCallMonitor(mContext, mLock);
         mTransactionManager = transactionManager;
         mBlockedNumbersAdapter = blockedNumbersAdapter;
         mCallStreamingController = new CallStreamingController(mContext, mLock);
         mCallStreamingNotification = callStreamingNotification;
         mFeatureFlags = featureFlags;
+        if (mFeatureFlags.voipCallMonitorRefactor()) {
+            mVoipCallMonitor = new VoipCallMonitor(
+                    mContext,
+                    new Handler(Looper.getMainLooper()),
+                    mLock);
+            mVoipCallMonitorLegacy = null;
+        } else {
+            mVoipCallMonitor = null;
+            mVoipCallMonitorLegacy = new VoipCallMonitorLegacy(mContext, mLock);
+        }
         mTelephonyFeatureFlags = telephonyFlags;
         mMetricsController = metricsController;
         mBlockedNumbersManager = mFeatureFlags.telecomMainlineBlockedNumbersManager()
                 ? mContext.getSystemService(BlockedNumbersManager.class)
                 : null;
-        mCallSequencingAdapter = new CallsManagerCallSequencingAdapter(this,
-                new CallSequencingController(this, mFeatureFlags.enableCallSequencing()),
-                mFeatureFlags.enableCallSequencing());
+        mCallSequencingAdapter = new CallsManagerCallSequencingAdapter(this, mContext,
+                new CallSequencingController(this, mContext, mClockProxy,
+                        mAnomalyReporter, mTimeoutsAdapter, mMetricsController,
+                        mFeatureFlags), mCallAudioManager, mFeatureFlags);
 
         if (mFeatureFlags.useImprovedListenerOrder()) {
             mListeners.add(mInCallController);
@@ -773,10 +828,18 @@
 
         // this needs to be after the mCallAudioManager
         mListeners.add(mPhoneStateBroadcaster);
-        mListeners.add(mVoipCallMonitor);
         mListeners.add(mCallStreamingNotification);
+        if (featureFlags.enableCallAudioWatchdog()) {
+            mListeners.add(mCallAudioWatchDog);
+        }
 
-        mVoipCallMonitor.startMonitor();
+        if (mFeatureFlags.voipCallMonitorRefactor()) {
+            mVoipCallMonitor.registerNotificationListener();
+            mListeners.add(mVoipCallMonitor);
+        } else {
+            mVoipCallMonitorLegacy.startMonitor();
+            mListeners.add(mVoipCallMonitorLegacy);
+        }
 
         // There is no USER_SWITCHED broadcast for user 0, handle it here explicitly.
         final UserManager userManager = mContext.getSystemService(UserManager.class);
@@ -1591,6 +1654,7 @@
             call.setAssociatedUser(associatedUser);
         }
 
+        Call activeCall = (Call) mConnectionSvrFocusMgr.getCurrentFocusCall();
         if (phoneAccount != null) {
             Bundle phoneAccountExtras = phoneAccount.getExtras();
             if (call.isSelfManaged()) {
@@ -1599,23 +1663,8 @@
                 call.setVisibleToInCallService(phoneAccountExtras == null
                         || phoneAccountExtras.getBoolean(
                         PhoneAccount.EXTRA_ADD_SELF_MANAGED_CALLS_TO_INCALLSERVICE, true));
-            } else {
-                // Incoming call is managed, the active call is self-managed and can't be held.
-                // We need to set extras on it to indicate whether answering will cause a
-                // active self-managed call to drop.
-                Call activeCall = (Call) mConnectionSvrFocusMgr.getCurrentFocusCall();
-                if (activeCall != null && !canHold(activeCall) && activeCall.isSelfManaged()) {
-                    Bundle dropCallExtras = new Bundle();
-                    dropCallExtras.putBoolean(Connection.EXTRA_ANSWERING_DROPS_FG_CALL, true);
-
-                    // Include the name of the app which will drop the call.
-                    CharSequence droppedApp = activeCall.getTargetPhoneAccountLabel();
-                    dropCallExtras.putCharSequence(
-                            Connection.EXTRA_ANSWERING_DROPS_FG_CALL_APP_NAME, droppedApp);
-                    Log.i(this, "Incoming managed call will drop %s call.", droppedApp);
-                    call.putConnectionServiceExtras(dropCallExtras);
-                }
             }
+            mCallSequencingAdapter.maybeAddAnsweringCallDropsFg(activeCall, call);
 
             if (phoneAccountExtras != null
                     && phoneAccountExtras.getBoolean(
@@ -1626,6 +1675,7 @@
             }
         }
 
+
         boolean isRttSettingOn = isRttSettingOn(phoneAccountHandle);
         if (isRttSettingOn ||
                 extras.getBoolean(TelecomManager.EXTRA_START_CALL_WITH_RTT, false)) {
@@ -1765,6 +1815,24 @@
             } else {
                 notifyCreateConnectionFailed(phoneAccountHandle, call);
             }
+        } else if (mFeatureFlags.enableCallSequencing() && (hasMaximumManagedRingingCalls(call)
+                || hasMaximumManagedDialingCalls(call))) {
+            // Fail incoming call if there's already a ringing or dialing call present.
+            boolean maxRinging = hasMaximumManagedRingingCalls(call);
+            if (maxRinging) {
+                call.setMissedReason(AUTO_MISSED_MAXIMUM_RINGING);
+                call.setStartFailCause(CallFailureCause.MAX_RINGING_CALLS);
+            } else {
+                call.setMissedReason(AUTO_MISSED_MAXIMUM_DIALING);
+            }
+            call.getAnalytics().setMissedReason(call.getMissedReason());
+            mCallLogManager.logCall(call, Calls.MISSED_TYPE,
+                    true /*showNotificationForMissedCall*/, null /*CallFilteringResult*/);
+            if (isConference) {
+                notifyCreateConferenceFailed(phoneAccountHandle, call);
+            } else {
+                notifyCreateConnectionFailed(phoneAccountHandle, call);
+            }
         } else if (call.isTransactionalCall()) {
             // transactional calls should skip Call#startCreateConnection below
             // as that is meant for Call objects with a ConnectionServiceWrapper
@@ -1779,6 +1847,25 @@
         return call;
     }
 
+    public void maybeAddAnsweringCallDropsFgOld(Call activeCall, Call incomingCall) {
+        // Incoming call is managed, the active call is self-managed and can't be held.
+        // We need to set extras on it to indicate whether answering will cause a
+        // active self-managed call to drop.
+        // Only runs if call sequencing is enabled.
+        if (!incomingCall.isSelfManaged() && activeCall != null && !canHold(activeCall)
+                && activeCall.isSelfManaged()) {
+            Bundle dropCallExtras = new Bundle();
+            dropCallExtras.putBoolean(Connection.EXTRA_ANSWERING_DROPS_FG_CALL, true);
+
+            // Include the name of the app which will drop the call.
+            CharSequence droppedApp = activeCall.getTargetPhoneAccountLabel();
+            dropCallExtras.putCharSequence(
+                    Connection.EXTRA_ANSWERING_DROPS_FG_CALL_APP_NAME, droppedApp);
+            Log.i(this, "Incoming managed call will drop %s call.", droppedApp);
+            incomingCall.putConnectionServiceExtras(dropCallExtras);
+        }
+    }
+
     void addNewUnknownCall(PhoneAccountHandle phoneAccountHandle, Bundle extras) {
         Uri handle = extras.getParcelable(TelecomManager.EXTRA_UNKNOWN_CALL_HANDLE);
         Log.i(this, "addNewUnknownCall with handle: %s", Log.pii(handle));
@@ -1874,6 +1961,37 @@
                 originalIntent, callingPackage, false);
     }
 
+    /**
+     * Creates a transaction representing either the outgoing or incoming transactional call.
+     * @param callId The call id associated with the call.
+     * @param callAttributes The call attributes associated with the call.
+     * @param extras The extras that are associated with the call.
+     * @param callingPackage The calling package representing where the request was invoked from.
+     * @return The {@link CompletableFuture<CallTransaction>} that encompasses the request to
+     *         place/receive the transactional call.
+     */
+    public CompletableFuture<CallTransaction> createTransactionalCall(String callId,
+            CallAttributes callAttributes, Bundle extras, String callingPackage) {
+        CompletableFuture<CallTransaction> transaction;
+        // create transaction based on the call direction
+        switch (callAttributes.getDirection()) {
+            case DIRECTION_OUTGOING:
+                transaction = mCallSequencingAdapter.createTransactionalOutgoingCall(callId,
+                        callAttributes, extras, callingPackage);
+                break;
+            case DIRECTION_INCOMING:
+                transaction = CompletableFuture.completedFuture(new IncomingCallTransaction(
+                        callId, callAttributes, this, extras, mFeatureFlags));
+                break;
+            default:
+                throw new IllegalArgumentException(String.format("Invalid Call Direction. "
+                                + "Was [%d] but should be within [%d,%d]",
+                        callAttributes.getDirection(), DIRECTION_INCOMING,
+                        DIRECTION_OUTGOING));
+        }
+        return transaction;
+    }
+
     private String generateNextCallId(Bundle extras) {
         if (extras != null && extras.containsKey(TelecomManager.TRANSACTION_CALL_ID_KEY)) {
             return extras.getString(TelecomManager.TRANSACTION_CALL_ID_KEY);
@@ -2094,7 +2212,15 @@
                 potentialPhoneAccounts -> {
                     Log.i(CallsManager.this, "make room for outgoing call stage");
                     if (mMmiUtils.isPotentialInCallMMICode(handle) && !isSelfManaged) {
-                        return CompletableFuture.completedFuture(true);
+                        boolean shouldAllowMmiCode = mCallSequencingAdapter
+                                .shouldAllowMmiCode(finalCall);
+                        if (shouldAllowMmiCode) {
+                            return CompletableFuture.completedFuture(true);
+                        } else {
+                            Log.i(this, "Rejecting the MMI code because there is an "
+                                    + "ongoing call on a different phone account.");
+                            return CompletableFuture.completedFuture(false);
+                        }
                     }
                     // If a call is being reused, then it has already passed the
                     // makeRoomForOutgoingCall check once and will fail the second time due to the
@@ -3140,9 +3266,9 @@
      * CS: Hold any existing calls, request focus, and then set the call state to answered state.
      * <p>
      * T: Call TransactionalServiceWrapper, which then generates transactions to hold calls
-     * {@link #transactionHoldPotentialActiveCallForNewCall} and then move the active call focus
-     * {@link #requestNewCallFocusAndVerify} and notify the remote VOIP app of the call state
-     * moving to active.
+     * {@link CallsManagerCallSequencingAdapter#transactionHoldPotentialActiveCallForNewCall} and
+     * then move the active call focus {@link #requestNewCallFocusAndVerify} and notify the remote
+     * VOIP app of the call state moving to active.
      * <p>
      * Note: This is only used when {@link FeatureFlags#enableCallSequencing()} is false.
      */
@@ -3430,35 +3556,42 @@
             Log.w(this, "Unknown call (%s) asked to disconnect", call);
         } else {
             mLocallyDisconnectingCalls.add(call);
-            int previousState = call.getState();
-            call.disconnect();
-            for (CallsManagerListener listener : mListeners) {
-                listener.onCallStateChanged(call, previousState, call.getState());
-            }
-            // Cancel any of the outgoing call futures if they're still around.
-            if (mPendingCallConfirm != null && !mPendingCallConfirm.isDone()) {
-                mPendingCallConfirm.complete(null);
-                mPendingCallConfirm = null;
-            }
-            if (mPendingAccountSelection != null && !mPendingAccountSelection.isDone()) {
-                mPendingAccountSelection.complete(null);
-                mPendingAccountSelection = null;
-            }
+            mCallSequencingAdapter.disconnectCall(call);
         }
     }
 
     /**
-     * Instructs Telecom to disconnect all calls.
+     * Disconnects the provided call. This is only used when
+     * {@link FeatureFlags#enableCallSequencing()} is false.
+     * @param call The call to disconnect.
+     * @param previousState The previous call state before the call is disconnected.
      */
-    void disconnectAllCalls() {
-        Log.v(this, "disconnectAllCalls");
-
-        for (Call call : mCalls) {
-            disconnectCall(call);
+    public void disconnectCallOld(Call call, int previousState) {
+        call.disconnect();
+        for (CallsManagerListener listener : mListeners) {
+            listener.onCallStateChanged(call, previousState, call.getState());
         }
+        processDisconnectCallAndCleanup(call, previousState);
     }
 
     /**
+     * Helper to process the call state change upon disconnecting the provided call and performs
+     * local cleanup to clear the outgoing call futures, if they exist.
+     * @param call The call to disconnect.
+     * @param previousState The previous call state before the call is disconnected.
+     */
+    public void processDisconnectCallAndCleanup(Call call, int previousState) {
+        // Cancel any of the outgoing call futures if they're still around.
+        if (mPendingCallConfirm != null && !mPendingCallConfirm.isDone()) {
+            mPendingCallConfirm.complete(null);
+            mPendingCallConfirm = null;
+        }
+        if (mPendingAccountSelection != null && !mPendingAccountSelection.isDone()) {
+            mPendingAccountSelection.complete(null);
+            mPendingAccountSelection = null;
+        }
+    }
+    /**
      * Disconnects calls for any other {@link PhoneAccountHandle} but the one specified.
      * Note: As a protective measure, will NEVER disconnect an emergency call.  Although that
      * situation should never arise, its a good safeguard.
@@ -3549,6 +3682,16 @@
                 new RequestCallback(new ActionUnHoldCall(call, activeCallId)));
     }
 
+    public void requestActionSetActiveCall(Call call, String tag) {
+        mConnectionSvrFocusMgr.requestFocus(call,
+                new RequestCallback(new ActionSetCallState(call, CallState.ACTIVE, tag)));
+    }
+
+    public void requestFocusActionAnswerCall(Call call, int videoState) {
+        mConnectionSvrFocusMgr.requestFocus(call, new CallsManager.RequestCallback(
+                new ActionAnswerCall(call, videoState)));
+    }
+
     @Override
     public void onExtrasRemoved(Call c, int source, List<String> keys) {
         if (source != Call.SOURCE_CONNECTION_SERVICE) {
@@ -3823,7 +3966,7 @@
         return isRttModeSettingOn && !shouldIgnoreRttModeSetting;
     }
 
-    private PersistableBundle getCarrierConfigForPhoneAccount(PhoneAccountHandle handle) {
+    public PersistableBundle getCarrierConfigForPhoneAccount(PhoneAccountHandle handle) {
         int subscriptionId = mPhoneAccountRegistrar.getSubscriptionIdForPhoneAccount(handle);
         CarrierConfigManager carrierConfigManager =
                 mContext.getSystemService(CarrierConfigManager.class);
@@ -3915,15 +4058,10 @@
         maybeMoveToSpeakerPhone(call);
     }
 
-    void requestFocusActionAnswerCall(Call call, int videoState) {
-        mConnectionSvrFocusMgr.requestFocus(call, new CallsManager.RequestCallback(
-                new CallsManager.ActionAnswerCall(call, videoState)));
-    }
-
     /**
      * Returns true if the active call is held.
      */
-    boolean holdActiveCallForNewCall(Call call) {
+    public boolean holdActiveCallForNewCall(Call call) {
         Call activeCall = (Call) mConnectionSvrFocusMgr.getCurrentFocusCall();
         Log.i(this, "holdActiveCallForNewCall, newCall: %s, activeCall: %s", call.getId(),
                 (activeCall == null ? "<none>" : activeCall.getId()));
@@ -3981,79 +4119,74 @@
     }
 
     /**
-     * attempt to hold or swap the current active call in favor of a new call request. The
-     * OutcomeReceiver will return onResult if the current active call is held or disconnected.
-     * Otherwise, the OutcomeReceiver will fail.
+     * Attempt to hold or swap the current active call in favor of a new call request. The old code
+     * path where {@link FeatureFlags#transactionalHoldDisconnectsUnholdable} is enabled but
+     * {@link FeatureFlags#enableCallSequencing()} is disabled.
      */
-    public void transactionHoldPotentialActiveCallForNewCall(Call newCall,
-            boolean isCallControlRequest, OutcomeReceiver<Boolean, CallException> callback) {
-        String mTag = "transactionHoldPotentialActiveCallForNewCall: ";
-        Call activeCall = (Call) mConnectionSvrFocusMgr.getCurrentFocusCall();
-        Log.i(this, mTag + "newCall=[%s], activeCall=[%s]", newCall, activeCall);
-
-        if (activeCall == null || activeCall == newCall) {
-            Log.i(this, mTag + "no need to hold activeCall");
-            callback.onResult(true);
-            return;
-        }
-
-        if (mFeatureFlags.transactionalHoldDisconnectsUnholdable()) {
-            // prevent bad actors from disconnecting the activeCall. Instead, clients will need to
-            // notify the user that they need to disconnect the ongoing call before making the
-            // new call ACTIVE.
-            if (isCallControlRequest && !canHoldOrSwapActiveCall(activeCall, newCall)) {
-                Log.i(this, mTag + "CallControlRequest exit");
-                callback.onError(new CallException("activeCall is NOT holdable or swappable, please"
-                        + " request the user disconnect the call.",
-                        CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
-                return;
-            }
-
-            if (holdActiveCallForNewCall(newCall)) {
-                // Transactional clients do not call setHold but the request was sent to set the
-                // call as inactive and it has already been acked by this point.
-                markCallAsOnHold(activeCall);
-                callback.onResult(true);
-            } else {
-                // It's possible that holdActiveCallForNewCall disconnected the activeCall.
-                // Therefore, the activeCalls state should be checked before failing.
-                if (activeCall.isLocallyDisconnecting()) {
-                    callback.onResult(true);
-                } else {
-                    Log.i(this, mTag + "active call could not be held or disconnected");
-                    callback.onError(
-                            new CallException("activeCall could not be held or disconnected",
-                                    CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
-                }
-            }
-        } else {
-            // before attempting CallsManager#holdActiveCallForNewCall(Call), check if it'll fail
-            // early
-            if (!canHold(activeCall) &&
-                    !(supportsHold(activeCall) && areFromSameSource(activeCall, newCall))) {
-                Log.i(this, "transactionHoldPotentialActiveCallForNewCall: "
-                        + "conditions show the call cannot be held.");
-                callback.onError(new CallException("call does not support hold",
-                        CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
-                return;
-            }
-
-            // attempt to hold the active call
-            if (!holdActiveCallForNewCall(newCall)) {
-                Log.i(this, "transactionHoldPotentialActiveCallForNewCall: "
-                        + "attempted to hold call but failed.");
-                callback.onError(new CallException("cannot hold active call failed",
-                        CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
-                return;
-            }
-
-            // officially mark the activeCall as held
+    public void transactionHoldPotentialActiveCallForNewCallOld(Call newCall,
+            Call activeCall, OutcomeReceiver<Boolean, CallException> callback) {
+        if (holdActiveCallForNewCall(newCall)) {
+            // Transactional clients do not call setHold but the request was sent to set the
+            // call as inactive and it has already been acked by this point.
             markCallAsOnHold(activeCall);
             callback.onResult(true);
+        } else {
+            // It's possible that holdActiveCallForNewCall disconnected the activeCall.
+            // Therefore, the activeCalls state should be checked before failing.
+            if (activeCall.isLocallyDisconnecting()) {
+                callback.onResult(true);
+            } else {
+                String msg = "active call could not be held or disconnected";
+                Log.i(this, "transactionHoldPotentialActiveCallForNewCallOld: " + msg);
+                callback.onError(
+                        new CallException(msg,
+                                CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
+                if (mFeatureFlags.enableCallExceptionAnomReports()) {
+                    mAnomalyReporter.reportAnomaly(CANNOT_HOLD_CURRENT_ACTIVE_CALL_ERROR_UUID, msg);
+                }
+            }
         }
     }
 
-    private boolean canHoldOrSwapActiveCall(Call activeCall, Call newCall) {
+    /**
+     * The transactional unflagged (original) code path to hold or swap the active call in favor of
+     * a new call request. Refer to
+     * {@link CallsManagerCallSequencingAdapter#transactionHoldPotentialActiveCallForNewCall}.
+     */
+    public void transactionHoldPotentialActiveCallForNewCallUnflagged(Call activeCall, Call newCall,
+            OutcomeReceiver<Boolean, CallException> callback) {
+        // before attempting CallsManager#holdActiveCallForNewCall(Call), check if it'll fail
+        // early
+        if (!canHold(activeCall) &&
+                !(supportsHold(activeCall) && areFromSameSource(activeCall, newCall))) {
+            String msg = "call does not support hold";
+            Log.i(this, "transactionHoldPotentialActiveCallForNewCall: " + msg);
+            callback.onError(new CallException(msg,
+                    CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
+            if (mFeatureFlags.enableCallExceptionAnomReports()) {
+                mAnomalyReporter.reportAnomaly(CANNOT_HOLD_CURRENT_ACTIVE_CALL_ERROR_UUID, msg);
+            }
+            return;
+        }
+
+        // attempt to hold the active call
+        if (!holdActiveCallForNewCall(newCall)) {
+            String msg = "cannot hold active call failed";
+            Log.i(this, "transactionHoldPotentialActiveCallForNewCall: " + msg);
+            callback.onError(new CallException(msg,
+                    CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
+            if (mFeatureFlags.enableCallExceptionAnomReports()) {
+                mAnomalyReporter.reportAnomaly(CANNOT_HOLD_CURRENT_ACTIVE_CALL_ERROR_UUID, msg);
+            }
+            return;
+        }
+
+        // officially mark the activeCall as held
+        markCallAsOnHold(activeCall);
+        callback.onResult(true);
+    }
+
+    public boolean canHoldOrSwapActiveCall(Call activeCall, Call newCall) {
         return canHold(activeCall) || sameSourceHoldCase(activeCall, newCall);
     }
 
@@ -4064,8 +4197,6 @@
     /**
      * CS: Mark a call as active. If the call is self-mangaed, we will also hold any active call
      * before moving the self-managed call to active.
-     * <p>
-     * Note: Only used when {@link FeatureFlags#enableCallSequencing()} is false.
      */
     @VisibleForTesting
     public void markCallAsActive(Call call) {
@@ -4075,13 +4206,7 @@
             // to active directly. We should hold or disconnect the current active call based on the
             // holdability, and request the call focus for the self-managed call before the state
             // change.
-            holdActiveCallForNewCall(call);
-            mConnectionSvrFocusMgr.requestFocus(
-                    call,
-                    new RequestCallback(new ActionSetCallState(
-                            call,
-                            CallState.ACTIVE,
-                            "active set explicitly for self-managed")));
+            mCallSequencingAdapter.markCallAsActiveSelfManagedCall(call);
         } else {
             if (mPendingAudioProcessingCall == call) {
                 if (mCalls.contains(call)) {
@@ -4103,8 +4228,6 @@
 
     /**
      * Mark a call as on hold after the hold operation has already completed.
-     * <p>
-     * Note: only used when {@link FeatureFlags#enableCallSequencing()} is false.
      */
     public void markCallAsOnHold(Call call) {
         setCallState(call, CallState.ON_HOLD, "on-hold set explicitly");
@@ -4283,45 +4406,7 @@
         removeCall(call);
         boolean isLocallyDisconnecting = mLocallyDisconnectingCalls.contains(call);
         mLocallyDisconnectingCalls.remove(call);
-        mCallSequencingAdapter.unholdCallForRemoval(call, isLocallyDisconnecting);
-    }
-
-    /**
-     * Move the held call to foreground in the event that there is a held call and the disconnected
-     * call was disconnected locally or the held call has no way to auto-unhold because it does not
-     * support hold capability.
-     * <p>
-     * Note: This is only used when {@link FeatureFlags#enableCallSequencing()} is set to false.
-     */
-    public void maybeMoveHeldCallToForeground(Call removedCall, boolean isLocallyDisconnecting) {
-        Call foregroundCall = mCallAudioManager.getPossiblyHeldForegroundCall();
-        if (isLocallyDisconnecting) {
-            boolean isDisconnectingChildCall = removedCall.isDisconnectingChildCall();
-            Log.v(this, "maybeMoveHeldCallToForeground: isDisconnectingChildCall = "
-                    + isDisconnectingChildCall + "call -> %s", removedCall);
-            // Auto-unhold the foreground call due to a locally disconnected call, except if the
-            // call which was disconnected is a member of a conference (don't want to auto
-            // un-hold the conference if we remove a member of the conference).
-            // Also, ensure that the call we're removing is from the same ConnectionService as
-            // the one we're removing.  We don't want to auto-unhold between ConnectionService
-            // implementations, especially if one is managed and the other is a VoIP CS.
-            if (!isDisconnectingChildCall && foregroundCall != null
-                    && foregroundCall.getState() == CallState.ON_HOLD
-                    && areFromSameSource(foregroundCall, removedCall)) {
-
-                foregroundCall.unhold();
-            }
-        } else if (foregroundCall != null &&
-                !foregroundCall.can(Connection.CAPABILITY_SUPPORT_HOLD) &&
-                foregroundCall.getState() == CallState.ON_HOLD) {
-
-            // The new foreground call is on hold, however the carrier does not display the hold
-            // button in the UI.  Therefore, we need to auto unhold the held call since the user
-            // has no means of unholding it themselves.
-            Log.i(this, "maybeMoveHeldCallToForeground: Auto-unholding held foreground call (call "
-                    + "doesn't support hold)");
-            foregroundCall.unhold();
-        }
+        mCallSequencingAdapter.maybeMoveHeldCallToForeground(call, isLocallyDisconnecting);
     }
 
     /**
@@ -4395,11 +4480,16 @@
         return getFirstCallWithState(CallState.RINGING, CallState.ANSWERED) != null;
     }
 
-    boolean hasRingingOrSimulatedRingingCall() {
+    public boolean hasRingingOrSimulatedRingingCall() {
         return getFirstCallWithState(
                 CallState.SIMULATED_RINGING, CallState.RINGING, CallState.ANSWERED) != null;
     }
 
+    public boolean hasManagedRingingOrSimulatedRingingCall() {
+        return getFirstCallWithState(null /* callToSkip */, true /* skipSelfManaged */,
+                CallState.SIMULATED_RINGING, CallState.RINGING, CallState.ANSWERED) != null;
+    }
+
     @VisibleForTesting
     public boolean onMediaButton(int type) {
         if (hasAnyCalls()) {
@@ -4543,11 +4633,11 @@
 
     @VisibleForTesting
     public Call getFirstCallWithState(int... states) {
-        return getFirstCallWithState(null, states);
+        return getFirstCallWithState(null, false /* skipSelfManaged */, states);
     }
 
     public Call getFirstCallWithLiveState() {
-        return getFirstCallWithState(null, LIVE_CALL_STATES);
+        return getFirstCallWithState(null, false /* skipSelfManaged */, LIVE_CALL_STATES);
     }
 
     @VisibleForTesting
@@ -4572,7 +4662,7 @@
      *
      * @param callToSkip Call that this method should skip while searching
      */
-    Call getFirstCallWithState(Call callToSkip, int... states) {
+    Call getFirstCallWithState(Call callToSkip, boolean skipSelfManaged, int... states) {
         for (int currentState : states) {
             // check the foreground first
             Call foregroundCall = getForegroundCall();
@@ -4594,6 +4684,10 @@
                     continue;
                 }
 
+                if (skipSelfManaged && call.isSelfManaged()) {
+                    continue;
+                }
+
                 if (currentState == call.getState()) {
                     return call;
                 }
@@ -5043,14 +5137,40 @@
      *                   ({@link #CALL_FILTER_ALL}).
      * @param excludeCall Where {@code non-null}, this call is excluded from the count.
      * @param phoneAccountHandle Where {@code non-null}, calls for this {@link PhoneAccountHandle}
-     *                           are excluded from the count.
+     *                           are included in the count.
      * @param states The list of {@link CallState}s to include in the count.
      * @return Count of calls matching criteria.
      */
     @VisibleForTesting
     public int getNumCallsWithState(final int callFilter, Call excludeCall,
                                     PhoneAccountHandle phoneAccountHandle, int... states) {
+        Stream<Call> callsStream = getCallsWithState(callFilter, excludeCall, states);
 
+        // If a phone account handle was specified, only consider calls for that phone account.
+        if (phoneAccountHandle != null) {
+            callsStream = callsStream.filter(
+                    call -> phoneAccountHandle.equals(call.getTargetPhoneAccount()));
+        }
+
+        return (int) callsStream.count();
+    }
+
+    @VisibleForTesting
+    public int getNumCallsWithStateWithoutHandle(final int callFilter, Call excludeCall,
+            PhoneAccountHandle phoneAccountHandle, int... states) {
+        Stream<Call> callsStream = getCallsWithState(callFilter, excludeCall, states);
+
+        // If a phone account handle was specified, only consider calls not associated with that
+        // phone account.
+        if (phoneAccountHandle != null) {
+            callsStream = callsStream.filter(
+                    call -> !phoneAccountHandle.equals(call.getTargetPhoneAccount()));
+        }
+
+        return (int) callsStream.count();
+    }
+
+    private Stream<Call> getCallsWithState(final int callFilter, Call excludeCall, int... states) {
         Set<Integer> desiredStates = IntStream.of(states).boxed().collect(Collectors.toSet());
 
         Stream<Call> callsStream = mCalls.stream()
@@ -5068,15 +5188,8 @@
             callsStream = callsStream.filter(call -> call != excludeCall);
         }
 
-        // If a phone account handle was specified, only consider calls for that phone account.
-        if (phoneAccountHandle != null) {
-            callsStream = callsStream.filter(
-                    call -> phoneAccountHandle.equals(call.getTargetPhoneAccount()));
-        }
-
-        return (int) callsStream.count();
+        return callsStream;
     }
-
     /**
      * Determines the number of calls (visible to the calling user) matching the specified criteria.
      * This is an overloaded method which is being used in a security patch to fix up the call
@@ -5093,7 +5206,7 @@
      *                    {@link UserHandle}.
      * @param hasCrossUserAccess indicates if calling user has the INTERACT_ACROSS_USERS permission.
      * @param phoneAccountHandle Where {@code non-null}, calls for this {@link PhoneAccountHandle}
-     *                           are excluded from the count.
+     *                           are included in the count.
      * @param states The list of {@link CallState}s to include in the count.
      * @return Count of calls matching criteria.
      */
@@ -5147,7 +5260,7 @@
                 exceptCall, phoneAccountHandle, ANY_CALL_STATE);
     }
 
-    private boolean hasMaximumManagedHoldingCalls(Call exceptCall) {
+    public boolean hasMaximumManagedHoldingCalls(Call exceptCall) {
         return MAXIMUM_HOLD_CALLS <= getNumCallsWithState(false /* isSelfManaged */, exceptCall,
                 null /* phoneAccountHandle */, CallState.ON_HOLD);
     }
@@ -5163,7 +5276,7 @@
                 phoneAccountHandle, CallState.RINGING, CallState.ANSWERED);
     }
 
-    private boolean hasMaximumOutgoingCalls(Call exceptCall) {
+    public boolean hasMaximumOutgoingCalls(Call exceptCall) {
         return MAXIMUM_LIVE_CALLS <= getNumCallsWithState(CALL_FILTER_ALL,
                 exceptCall, null /* phoneAccountHandle */, OUTGOING_CALL_STATES);
     }
@@ -5280,7 +5393,7 @@
      * <p>
      * Note: This method is only applicable when {@link FeatureFlags#enableCallSequencing()}
      * is false.
-     * @param call The new pending outgoing call.
+     * @param emergencyCall The new pending outgoing call.
      * @return true if room was made, false if no room could be made.
      */
     @VisibleForTesting
@@ -5580,7 +5693,7 @@
      * @param parentCall The parent call.
      * @return The first non-null phone account handle of the children, or {@code null} if none.
      */
-    private PhoneAccountHandle getFirstChildPhoneAccount(Call parentCall) {
+    public PhoneAccountHandle getFirstChildPhoneAccount(Call parentCall) {
         for (Call childCall : parentCall.getChildCalls()) {
             PhoneAccountHandle childPhoneAccount = childCall.getTargetPhoneAccount();
             if (childPhoneAccount != null) {
@@ -6156,6 +6269,10 @@
             mConnectionSvrFocusMgr.dump(pw);
             pw.decreaseIndent();
         }
+
+        if (mCallAudioWatchDog != null) {
+            mCallAudioWatchDog.dump(pw);
+        }
     }
 
     /**
@@ -6662,7 +6779,13 @@
         public void performAction() {
             synchronized (mLock) {
                 Log.d(this, "perform unhold call for %s", mCall);
-                mCall.unhold("held " + mPreviouslyHeldCallId);
+                CompletableFuture<Boolean> unholdFuture =
+                        mCall.unhold("held " + mPreviouslyHeldCallId);
+                mCallSequencingAdapter.maybeLogFutureResultTransaction(unholdFuture,
+                        "performAction", "AUC.pA", "performAction: unhold call transaction "
+                                + "succeeded. Call state is active.",
+                        "performAction: unhold call transaction failed. Call state did not "
+                                + "move to active in designated time.");
             }
         }
     }
@@ -6684,10 +6807,11 @@
                     listener.onIncomingCallAnswered(mCall);
                 }
 
+                CompletableFuture<Boolean> answerCallFuture = null;
                 // We do not update the UI until we get confirmation of the answer() through
                 // {@link #markCallAsActive}.
                 if (mCall.getState() == CallState.RINGING) {
-                    mCall.answer(mVideoState);
+                    answerCallFuture = mCall.answer(mVideoState);
                     setCallState(mCall, CallState.ANSWERED, "answered");
                 } else if (mCall.getState() == CallState.SIMULATED_RINGING) {
                     // If the call's in simulated ringing, we don't have to wait for the CS --
@@ -6698,12 +6822,17 @@
                     // In certain circumstances, the connection service can lose track of a request
                     // to answer a call. Therefore, if the user presses answer again, still send it
                     // on down, but log a warning in the process and don't change the call state.
-                    mCall.answer(mVideoState);
+                    answerCallFuture = mCall.answer(mVideoState);
                     Log.w(this, "Duplicate answer request for call %s", mCall.getId());
                 }
                 if (isSpeakerphoneAutoEnabledForVideoCalls(mVideoState)) {
                     mCall.setStartWithSpeakerphoneOn(true);
                 }
+                mCallSequencingAdapter.maybeLogFutureResultTransaction(answerCallFuture,
+                        "performAction", "AAC.pA", "performAction: answer call transaction "
+                                + "succeeded. Call state is active.",
+                        "performAction: answer call transaction failed. Call state did not "
+                                + "move to active in designated time.");
             }
         }
     }
@@ -6789,8 +6918,12 @@
                 if (mTargetCallFocus.getState() != mPreviousCallState) {
                     mTargetCallFocus.setState(mPreviousCallState, "resetting call state");
                 }
-                mCallback.onError(new CallException("failed to switch focus to requested call",
+                String msg = "failed to switch focus to requested call";
+                mCallback.onError(new CallException(msg,
                         CallException.CODE_CALL_CANNOT_BE_SET_TO_ACTIVE));
+                if (mFeatureFlags.enableCallExceptionAnomReports()) {
+                    mAnomalyReporter.reportAnomaly(FAILED_TO_SWITCH_FOCUS_ERROR_UUID, msg);
+                }
                 return;
             }
             // at this point, we know the FocusManager is able to update successfully
@@ -7017,4 +7150,28 @@
     public void addCallBeingSetup(Call call) {
         mSelfManagedCallsBeingSetup.add(call);
     }
+
+    @VisibleForTesting
+    public CallsManagerCallSequencingAdapter getCallSequencingAdapter() {
+        return mCallSequencingAdapter;
+    }
+
+    public void waitForAudioToUpdate(boolean expectActive) {
+        Log.i(this, "waitForAudioToUpdate");
+        if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+            try {
+                CallAudioRouteController audioRouteController =
+                        (CallAudioRouteController) mCallAudioRouteAdapter;
+                if (expectActive) {
+                    audioRouteController.getAudioActiveCompleteLatch().await(
+                            WAIT_FOR_AUDIO_UPDATE_TIMEOUT, TimeUnit.MILLISECONDS);
+                } else {
+                    audioRouteController.getAudioOperationsCompleteLatch().await(
+                            WAIT_FOR_AUDIO_UPDATE_TIMEOUT, TimeUnit.MILLISECONDS);
+                }
+            } catch (InterruptedException e) {
+                Log.w(this, e.toString());
+            }
+        }
+    }
 }
diff --git a/src/com/android/server/telecom/ConnectionServiceWrapper.java b/src/com/android/server/telecom/ConnectionServiceWrapper.java
index 260c238..7a95cc8 100644
--- a/src/com/android/server/telecom/ConnectionServiceWrapper.java
+++ b/src/com/android/server/telecom/ConnectionServiceWrapper.java
@@ -79,6 +79,7 @@
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
+import java.util.concurrent.RejectedExecutionException;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
@@ -95,7 +96,8 @@
         ConnectionServiceFocusManager.ConnectionServiceFocus, CallSourceService {
 
     /**
-     * Anomaly Report UUIDs and corresponding error descriptions specific to CallsManager.
+     * Anomaly Report UUIDs and corresponding error descriptions specific to
+     * ConnectionServiceWrapper.
      */
     public static final UUID CREATE_CONNECTION_TIMEOUT_ERROR_UUID =
             UUID.fromString("54b7203d-a79f-4cbd-b639-85cd93a39cbb");
@@ -105,6 +107,15 @@
             UUID.fromString("caafe5ea-2472-4c61-b2d8-acb9d47e13dd");
     public static final String CREATE_CONFERENCE_TIMEOUT_ERROR_MSG =
             "Timeout expired before Telecom conference was created.";
+    public static final UUID NULL_SCHEDULED_EXECUTOR_ERROR_UUID =
+            UUID.fromString("af6b293b-239f-4ccf-bf3a-db212594e29d");
+    public static final String NULL_SCHEDULED_EXECUTOR_ERROR_MSG =
+            "Scheduled executor is null when creating connection/conference.";
+    public static final UUID EXECUTOR_REJECTED_EXECUTION_ERROR_UUID =
+            UUID.fromString("649b348c-9d3f-451e-bae9-d9920e7b422c");
+
+    public static final String EXECUTOR_REJECTED_EXECUTION_ERROR_MSG =
+            "Scheduled executor caused a Rejected Execution Exception when creating connection.";
 
     private static final String TELECOM_ABBREVIATION = "cast";
     private static final long SERVICE_BINDING_TIMEOUT = 15000L;
@@ -1655,11 +1666,27 @@
                         }
                     }
                 };
-                // Post cleanup to the executor service and cache the future, so we can cancel it if
-                // needed.
-                ScheduledFuture<?> future = mScheduledExecutor.schedule(r.getRunnableToCancel(),
-                        SERVICE_BINDING_TIMEOUT, TimeUnit.MILLISECONDS);
-                mScheduledFutureMap.put(call, future);
+                if (mScheduledExecutor != null && !mScheduledExecutor.isShutdown()) {
+                    try {
+                        // Post cleanup to the executor service and cache the future,
+                        // so we can cancel it if needed.
+                        ScheduledFuture<?> future = mScheduledExecutor.schedule(
+                                r.getRunnableToCancel(),SERVICE_BINDING_TIMEOUT,
+                                TimeUnit.MILLISECONDS);
+                        mScheduledFutureMap.put(call, future);
+                    } catch (RejectedExecutionException e) {
+                        Log.e(this, e, "createConference: mScheduledExecutor was "
+                                + "already shutdown");
+                        mAnomalyReporter.reportAnomaly(
+                                EXECUTOR_REJECTED_EXECUTION_ERROR_UUID,
+                                EXECUTOR_REJECTED_EXECUTION_ERROR_MSG);
+                    }
+                } else {
+                    Log.w(this, "createConference: Scheduled executor is null or shutdown");
+                    mAnomalyReporter.reportAnomaly(
+                        NULL_SCHEDULED_EXECUTOR_ERROR_UUID,
+                        NULL_SCHEDULED_EXECUTOR_ERROR_MSG);
+                }
                 try {
                     mServiceInterface.createConference(
                             call.getConnectionManagerPhoneAccount(),
@@ -1784,11 +1811,27 @@
                         }
                     }
                 };
-                // Post cleanup to the executor service and cache the future, so we can cancel it if
-                // needed.
-                ScheduledFuture<?> future = mScheduledExecutor.schedule(r.getRunnableToCancel(),
-                        SERVICE_BINDING_TIMEOUT, TimeUnit.MILLISECONDS);
-                mScheduledFutureMap.put(call, future);
+                if (mScheduledExecutor != null && !mScheduledExecutor.isShutdown()) {
+                    try {
+                        // Post cleanup to the executor service and cache the future,
+                        // so we can cancel it if needed.
+                        ScheduledFuture<?> future = mScheduledExecutor.schedule(
+                                r.getRunnableToCancel(),SERVICE_BINDING_TIMEOUT,
+                                TimeUnit.MILLISECONDS);
+                        mScheduledFutureMap.put(call, future);
+                    } catch (RejectedExecutionException e) {
+                        Log.e(this, e, "createConnection: mScheduledExecutor was "
+                                + "already shutdown");
+                        mAnomalyReporter.reportAnomaly(
+                                EXECUTOR_REJECTED_EXECUTION_ERROR_UUID,
+                                EXECUTOR_REJECTED_EXECUTION_ERROR_MSG);
+                    }
+                } else {
+                    Log.w(this, "createConnection: Scheduled executor is null or shutdown");
+                    mAnomalyReporter.reportAnomaly(
+                        NULL_SCHEDULED_EXECUTOR_ERROR_UUID,
+                        NULL_SCHEDULED_EXECUTOR_ERROR_MSG);
+                }
                 try {
                     if (mFlags.cswServiceInterfaceIsNull() && mServiceInterface == null) {
                         if (mFlags.dontTimeoutDestroyedCalls()) {
diff --git a/src/com/android/server/telecom/CreateConnectionTimeout.java b/src/com/android/server/telecom/CreateConnectionTimeout.java
index 3046ca4..2889e13 100644
--- a/src/com/android/server/telecom/CreateConnectionTimeout.java
+++ b/src/com/android/server/telecom/CreateConnectionTimeout.java
@@ -16,8 +16,6 @@
 
 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;
@@ -117,11 +115,6 @@
 
     @Override
     public void loggedRun() {
-        if (!carrierEnabledSatelliteFlag()) {
-            timeoutCallIfNeeded();
-            return;
-        }
-
         PhoneAccountHandle connectionManager =
                 mPhoneAccountRegistrar.getSimCallManagerFromCall(mCall);
         if (connectionManager != null) {
diff --git a/src/com/android/server/telecom/LogUtils.java b/src/com/android/server/telecom/LogUtils.java
index d98ebfe..d927f8f 100644
--- a/src/com/android/server/telecom/LogUtils.java
+++ b/src/com/android/server/telecom/LogUtils.java
@@ -226,10 +226,14 @@
         public static final String FLASH_NOTIFICATION_START = "FLASH_NOTIFICATION_START";
         public static final String FLASH_NOTIFICATION_STOP = "FLASH_NOTIFICATION_STOP";
         public static final String GAINED_FGS_DELEGATION = "GAINED_FGS_DELEGATION";
+        public static final String ALREADY_HAS_FGS_DELEGATION = "ALREADY_HAS_FGS_DELEGATION";
+        public static final String MAINTAINING_FGS_DELEGATION = "MAINTAINING_FGS_DELEGATION";
         public static final String GAIN_FGS_DELEGATION_FAILED = "GAIN_FGS_DELEGATION_FAILED";
         public static final String LOST_FGS_DELEGATION = "LOST_FGS_DELEGATION";
         public static final String START_STREAMING = "START_STREAMING";
         public static final String STOP_STREAMING = "STOP_STREAMING";
+        public static final String AUDIO_ATTR = "AUDIO_ATTR";
+        public static final String ANSWER_DROPS_FG = "ANSWER_DROPS_FG";
 
         public static class Timings {
             public static final String ACCEPT_TIMING = "accept";
diff --git a/src/com/android/server/telecom/PendingAudioRoute.java b/src/com/android/server/telecom/PendingAudioRoute.java
index d21ac56..dde1d8d 100644
--- a/src/com/android/server/telecom/PendingAudioRoute.java
+++ b/src/com/android/server/telecom/PendingAudioRoute.java
@@ -70,8 +70,23 @@
         mCommunicationDeviceType = AudioRoute.TYPE_INVALID;
     }
 
-    void setOrigRoute(boolean active, AudioRoute origRoute) {
-        origRoute.onOrigRouteAsPendingRoute(active, this, mAudioManager, mBluetoothRouteManager);
+    /**
+     * Sets the originating route information, and begins the process of transitioning OUT of the
+     * originating route.
+     * Note: We also pass in whether the destination route is going to be active.  This is so that
+     * {@link AudioRoute#onOrigRouteAsPendingRoute(boolean, PendingAudioRoute, AudioManager,
+     * BluetoothRouteManager)} knows whether or not the destination route will be active or not and
+     * can determine whether or not it needs to call {@link AudioManager#clearCommunicationDevice()}
+     * or not.  To optimize audio performance we only need to clear the communication device if the
+     * end result is going to be that we are in an inactive state.
+     * @param isOriginActive Whether the origin is active.
+     * @param origRoute The origin.
+     * @param isDestActive Whether the destination will be active.
+     */
+    void setOrigRoute(boolean isOriginActive, AudioRoute origRoute, boolean isDestActive) {
+        mActive = isDestActive;
+        origRoute.onOrigRouteAsPendingRoute(isOriginActive, this, mAudioManager,
+                mBluetoothRouteManager);
         mOrigRoute = origRoute;
     }
 
@@ -134,6 +149,10 @@
         return mPendingMessages;
     }
 
+    /**
+     * Whether the destination {@link #getDestRoute()} will be active or not.
+     * @return {@code true} if destination will be active, {@code false} otherwise.
+     */
     public boolean isActive() {
         return mActive;
     }
@@ -154,4 +173,14 @@
     public FeatureFlags getFeatureFlags() {
         return mFeatureFlags;
     }
+
+    @Override
+    public String toString() {
+        return "PendingAudioRoute{" +
+                ", mOrigRoute=" + mOrigRoute +
+                ", mDestRoute=" + mDestRoute +
+                ", mActive=" + mActive +
+                ", mCommunicationDeviceType=" + mCommunicationDeviceType +
+                '}';
+    }
 }
diff --git a/src/com/android/server/telecom/PhoneAccountRegistrar.java b/src/com/android/server/telecom/PhoneAccountRegistrar.java
index 1a1af92..cc7f6ab 100644
--- a/src/com/android/server/telecom/PhoneAccountRegistrar.java
+++ b/src/com/android/server/telecom/PhoneAccountRegistrar.java
@@ -79,6 +79,7 @@
 import java.lang.SecurityException;
 import java.lang.String;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
@@ -2940,4 +2941,24 @@
             return null;
         }
     };
+
+    /**
+     * Determines if an app specified by a uid has a phone account for that uid.
+     * @param uid the uid to check
+     * @return {@code true} if there is a phone account for that UID, {@code false} otherwise.
+     */
+    public boolean hasPhoneAccountForUid(int uid) {
+        String[] packageNames = mContext.getPackageManager().getPackagesForUid(uid);
+        if (packageNames == null || packageNames.length == 0) {
+            return false;
+        }
+        UserHandle userHandle = UserHandle.getUserHandleForUid(uid);
+        return mState.accounts.stream()
+                .anyMatch(p -> {
+                    PhoneAccountHandle handle = p.getAccountHandle();
+                    return handle.getUserHandle().equals(userHandle)
+                            && Arrays.stream(packageNames).anyMatch( s -> s.equals(
+                                    handle.getComponentName().getPackageName()));
+                });
+    }
 }
diff --git a/src/com/android/server/telecom/Ringer.java b/src/com/android/server/telecom/Ringer.java
index bfaadf0..d0fd201 100644
--- a/src/com/android/server/telecom/Ringer.java
+++ b/src/com/android/server/telecom/Ringer.java
@@ -31,6 +31,7 @@
 import android.media.AudioAttributes;
 import android.media.AudioManager;
 import android.media.Ringtone;
+import android.media.RingtoneManager;
 import android.media.Utils;
 import android.media.VolumeShaper;
 import android.media.audio.Flags;
@@ -431,6 +432,11 @@
                     && isVibratorEnabled) {
                 Log.i(this, "Muted haptic channels since audio coupled ramping ringer is disabled");
                 hapticChannelsMuted = true;
+                if (useCustomVibration(foregroundCall)) {
+                    Log.i(this,
+                            "Not muted haptic channel for customization when apply ramping ringer");
+                    hapticChannelsMuted = false;
+                }
             } else if (hapticChannelsMuted) {
                 Log.i(this,
                         "Muted haptic channels isVibratorEnabled=%s, hapticPlaybackSupported=%s",
@@ -442,7 +448,7 @@
             if (!isHapticOnly) {
                 ringtoneInfoSupplier = () -> mRingtoneFactory.getRingtone(
                         foregroundCall, mVolumeShaperConfig, finalHapticChannelsMuted);
-            } else if (Flags.enableRingtoneHapticsCustomization() && mRingtoneVibrationSupported) {
+            } else if (useCustomVibration(foregroundCall)) {
                 ringtoneInfoSupplier = () -> mRingtoneFactory.getRingtone(
                         foregroundCall, null, false);
             }
@@ -521,6 +527,21 @@
         }
     }
 
+    private boolean useCustomVibration(@NonNull Call foregroundCall) {
+        return Flags.enableRingtoneHapticsCustomization() && mRingtoneVibrationSupported
+                && hasExplicitVibration(foregroundCall);
+    }
+
+    private boolean hasExplicitVibration(@NonNull Call foregroundCall) {
+        final Uri ringtoneUri = foregroundCall.getRingtone();
+        if (ringtoneUri != null) {
+            // TODO(b/399265235) : Avoid this hidden API access for mainline
+            return Utils.hasVibration(ringtoneUri);
+        }
+        return Utils.hasVibration(RingtoneManager.getActualDefaultRingtoneUri(
+                mContext, RingtoneManager.TYPE_RINGTONE));
+    }
+
     /**
      * Try to reserve the vibrator for this call, returning false if it's already committed.
      * The vibration will be started by AsyncRingtonePlayer to ensure timing is aligned with the
diff --git a/src/com/android/server/telecom/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java
index 488524f..7bb041d 100644
--- a/src/com/android/server/telecom/TelecomServiceImpl.java
+++ b/src/com/android/server/telecom/TelecomServiceImpl.java
@@ -27,8 +27,6 @@
 import static android.Manifest.permission.READ_SMS;
 import static android.Manifest.permission.REGISTER_SIM_SUBSCRIPTION;
 import static android.Manifest.permission.WRITE_SECURE_SETTINGS;
-import static android.telecom.CallAttributes.DIRECTION_INCOMING;
-import static android.telecom.CallAttributes.DIRECTION_OUTGOING;
 import static android.telecom.CallException.CODE_ERROR_UNKNOWN;
 import static android.telecom.TelecomManager.TELECOM_TRANSACTION_SUCCESS;
 
@@ -66,6 +64,7 @@
 import android.telecom.Log;
 import android.telecom.PhoneAccount;
 import android.telecom.PhoneAccountHandle;
+import android.telecom.StatusHints;
 import android.telecom.TelecomAnalytics;
 import android.telecom.TelecomManager;
 import android.telecom.VideoProfile;
@@ -81,13 +80,14 @@
 import com.android.internal.telecom.ICallEventCallback;
 import com.android.internal.telecom.ITelecomService;
 import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.telecom.callsequencing.voip.VoipCallMonitor;
 import com.android.server.telecom.components.UserCallIntentProcessorFactory;
 import com.android.server.telecom.flags.FeatureFlags;
 import com.android.server.telecom.metrics.ApiStats;
+import com.android.server.telecom.metrics.EventStats;
+import com.android.server.telecom.metrics.EventStats.CriticalEvent;
 import com.android.server.telecom.metrics.TelecomMetricsController;
 import com.android.server.telecom.settings.BlockedNumbersActivity;
-import com.android.server.telecom.callsequencing.voip.IncomingCallTransaction;
-import com.android.server.telecom.callsequencing.voip.OutgoingCallTransaction;
 import com.android.server.telecom.callsequencing.TransactionManager;
 import com.android.server.telecom.callsequencing.CallTransaction;
 import com.android.server.telecom.callsequencing.CallTransactionResult;
@@ -102,6 +102,7 @@
 import java.util.Objects;
 import java.util.Set;
 import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
 
 // TODO: Needed for move to system service: import com.android.internal.R;
 
@@ -141,6 +142,13 @@
             UUID.fromString("4edf6c8d-1e43-4c94-b0fc-a40c8d80cfe8");
     public static final String PLACE_CALL_SECURITY_EXCEPTION_ERROR_MSG =
             "Security exception thrown while placing an outgoing call.";
+    public static final UUID CALL_IS_NULL_OR_ID_MISMATCH_UUID =
+            UUID.fromString("b11f3251-474c-4f90-96d6-a256aebc3c19");
+    public static final String CALL_IS_NULL_OR_ID_MISMATCH_MSG =
+            "call is null or id mismatch";
+    public static final UUID ADD_CALL_ON_ERROR_UUID =
+            UUID.fromString("f8e7d6c5-b4a3-9210-8765-432109abcdef");
+
     private static final String TAG = "TelecomServiceImpl";
     private static final String TIME_LINE_ARG = "timeline";
     private static final int DEFAULT_VIDEO_STATE = -1;
@@ -170,10 +178,29 @@
     private final ITelecomService.Stub mBinderImpl = new ITelecomService.Stub() {
 
         @Override
+        public boolean hasForegroundServiceDelegation(
+                PhoneAccountHandle handle,
+                String packageName) {
+            enforceCallingPackage(packageName, "hasForegroundServiceDelegation");
+            long token = Binder.clearCallingIdentity();
+            try {
+                VoipCallMonitor vcm = mCallsManager.getVoipCallMonitor();
+                if (vcm != null) {
+                    return vcm.hasForegroundServiceDelegation(handle);
+                }
+                return false;
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+
+        @Override
         public void addCall(CallAttributes callAttributes, ICallEventCallback callEventCallback,
                 String callId, String callingPackage) {
+            int uid = Binder.getCallingUid();
+            int pid = Binder.getCallingPid();
             ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_ADDCALL,
-                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
+                    uid, ApiStats.RESULT_PERMISSION);
             try {
                 Log.startSession("TSI.aC", Log.getPackageAbbreviation(callingPackage));
                 Log.i(TAG, "addCall: id=[%s], attributes=[%s]", callId, callAttributes);
@@ -190,68 +217,88 @@
 
                 // add extras about info used for FGS delegation
                 Bundle extras = new Bundle();
-                extras.putInt(CallAttributes.CALLER_UID_KEY, Binder.getCallingUid());
-                extras.putInt(CallAttributes.CALLER_PID_KEY, Binder.getCallingPid());
+                extras.putInt(CallAttributes.CALLER_UID_KEY, uid);
+                extras.putInt(CallAttributes.CALLER_PID_KEY, pid);
 
-                CallTransaction transaction = null;
-                // create transaction based on the call direction
-                switch (callAttributes.getDirection()) {
-                    case DIRECTION_OUTGOING:
-                        transaction = new OutgoingCallTransaction(callId, mContext, callAttributes,
-                                mCallsManager, extras, mFeatureFlags);
-                        break;
-                    case DIRECTION_INCOMING:
-                        transaction = new IncomingCallTransaction(callId, callAttributes,
-                                mCallsManager, extras, mFeatureFlags);
-                        break;
-                    default:
-                        throw new IllegalArgumentException(String.format("Invalid Call Direction. "
-                                        + "Was [%d] but should be within [%d,%d]",
-                                callAttributes.getDirection(), DIRECTION_INCOMING,
-                                DIRECTION_OUTGOING));
+
+                CompletableFuture<CallTransaction> transactionFuture;
+                long token = Binder.clearCallingIdentity();
+                try {
+                    transactionFuture = mCallsManager.createTransactionalCall(callId,
+                            callAttributes, extras, callingPackage);
+                } finally {
+                    Binder.restoreCallingIdentity(token);
                 }
 
-                mTransactionManager.addTransaction(transaction, new OutcomeReceiver<>() {
-                    @Override
-                    public void onResult(CallTransactionResult result) {
-                        Log.d(TAG, "addCall: onResult");
-                        Call call = result.getCall();
+                transactionFuture.thenCompose((transaction) -> {
+                    if (transaction != null) {
+                        mTransactionManager.addTransaction(transaction, new OutcomeReceiver<>() {
+                            @Override
+                            public void onResult(CallTransactionResult result) {
+                                Log.d(TAG, "addCall: onResult");
+                                Call call = result.getCall();
+                                if (mFeatureFlags.telecomMetricsSupport()) {
+                                    mMetricsController.getEventStats().log(new CriticalEvent(
+                                            EventStats.ID_ADD_CALL, uid,
+                                            EventStats.CAUSE_CALL_TRANSACTION_SUCCESS));
+                                }
 
-                        if (call == null || !call.getId().equals(callId)) {
-                            Log.i(TAG, "addCall: onResult: call is null or id mismatch");
-                            onAddCallControl(callId, callEventCallback, null,
-                                    new CallException(ADD_CALL_ERR_MSG, CODE_ERROR_UNKNOWN));
-                            return;
-                        }
+                                if (call == null || !call.getId().equals(callId)) {
+                                    Log.i(TAG, "addCall: onResult: call is null or id mismatch");
+                                    onAddCallControl(callId, callEventCallback, null,
+                                            new CallException(ADD_CALL_ERR_MSG,
+                                                    CODE_ERROR_UNKNOWN));
+                                    if (mFeatureFlags.enableCallExceptionAnomReports()) {
+                                        mAnomalyReporter.reportAnomaly(
+                                                CALL_IS_NULL_OR_ID_MISMATCH_UUID,
+                                                CALL_IS_NULL_OR_ID_MISMATCH_MSG);
+                                    }
+                                    return;
+                                }
 
-                        TransactionalServiceWrapper serviceWrapper =
-                                mTransactionalServiceRepository
-                                        .addNewCallForTransactionalServiceWrapper(handle,
-                                                callEventCallback, mCallsManager, call);
+                                TransactionalServiceWrapper serviceWrapper =
+                                        mTransactionalServiceRepository
+                                                .addNewCallForTransactionalServiceWrapper(handle,
+                                                        callEventCallback, mCallsManager, call);
 
-                        call.setTransactionServiceWrapper(serviceWrapper);
+                                call.setTransactionServiceWrapper(serviceWrapper);
 
-                        if (mFeatureFlags.transactionalVideoState()) {
-                            call.setTransactionalCallSupportsVideoCalling(callAttributes);
-                        }
-                        ICallControl clientCallControl = serviceWrapper.getICallControl();
+                                if (mFeatureFlags.transactionalVideoState()) {
+                                    call.setTransactionalCallSupportsVideoCalling(callAttributes);
+                                }
+                                ICallControl clientCallControl = serviceWrapper.getICallControl();
 
-                        if (clientCallControl == null) {
-                            throw new IllegalStateException("TransactionalServiceWrapper"
-                                    + "#ICallControl is null.");
-                        }
+                                if (clientCallControl == null) {
+                                    throw new IllegalStateException("TransactionalServiceWrapper"
+                                            + "#ICallControl is null.");
+                                }
 
-                        // finally, send objects back to the client
-                        onAddCallControl(callId, callEventCallback, clientCallControl, null);
+                                // finally, send objects back to the client
+                                onAddCallControl(callId, callEventCallback, clientCallControl,
+                                        null);
+                            }
+
+                            @Override
+                            public void onError(@NonNull CallException exception) {
+                                Log.d(TAG, "addCall: onError: e=[%s]", exception.toString());
+                                onAddCallControl(callId, callEventCallback, null, exception);
+                                if (mFeatureFlags.enableCallExceptionAnomReports()) {
+                                    mAnomalyReporter.reportAnomaly(
+                                            ADD_CALL_ON_ERROR_UUID,
+                                            exception.getMessage());
+                                }
+                                if (mFeatureFlags.telecomMetricsSupport()) {
+                                    mMetricsController.getEventStats().log(new CriticalEvent(
+                                            EventStats.ID_ADD_CALL, uid,
+                                            EventStats.CAUSE_CALL_TRANSACTION_BASE
+                                                    + exception.getCode()));
+                                }
+                            }
+                        });
                     }
-
-                    @Override
-                    public void onError(@NonNull CallException exception) {
-                        Log.d(TAG, "addCall: onError: e=[%s]", exception.toString());
-                        onAddCallControl(callId, callEventCallback, null, exception);
-                    }
+                    event.setResult(ApiStats.RESULT_NORMAL);
+                    return CompletableFuture.completedFuture(transaction);
                 });
-                event.setResult(ApiStats.RESULT_NORMAL);
             } finally {
                 logEvent(event);
                 Log.endSession();
@@ -1474,14 +1521,18 @@
         private boolean isPrivilegedUid() {
             int callingUid = Binder.getCallingUid();
             return mFeatureFlags.allowSystemAppsResolveVoipCalls()
-                    ? (UserHandle.isSameApp(callingUid, Process.ROOT_UID)
-                            || UserHandle.isSameApp(callingUid, Process.SYSTEM_UID)
-                            || UserHandle.isSameApp(callingUid, Process.SHELL_UID))
+                    ? (isSameApp(callingUid, Process.ROOT_UID)
+                            || isSameApp(callingUid, Process.SYSTEM_UID)
+                            || isSameApp(callingUid, Process.SHELL_UID))
                     : (callingUid == Process.ROOT_UID
                             || callingUid == Process.SYSTEM_UID
                             || callingUid == Process.SHELL_UID);
         }
 
+        private boolean isSameApp(int uid1, int uid2) {
+            return UserHandle.getAppId(uid1) == UserHandle.getAppId(uid2);
+        }
+
         private boolean isSysUiUid() {
             int callingUid = Binder.getCallingUid();
             int systemUiUid;
@@ -1490,7 +1541,7 @@
                     systemUiUid = mPackageManager.getPackageUid(mSystemUiPackageName, 0);
                     Log.i(TAG, "isSysUiUid: callingUid = " + callingUid + "; systemUiUid = "
                             + systemUiUid);
-                    return UserHandle.isSameApp(callingUid, systemUiUid);
+                    return isSameApp(callingUid, systemUiUid);
                 } catch (PackageManager.NameNotFoundException e) {
                     Log.w(TAG, "isSysUiUid: caught PackageManager NameNotFoundException = " + e);
                     return false;
@@ -2909,6 +2960,17 @@
             }
         }
 
+        @Override
+        public void setMetricsTestMode(boolean enabled) {
+            if (mFeatureFlags.telecomMetricsSupport()) {
+                mMetricsController.setTestMode(enabled);
+            }
+        }
+
+        @Override
+        public void waitForAudioToUpdate(boolean expectActive) {
+            mCallsManager.waitForAudioToUpdate(expectActive);
+        }
         /**
          * Determines whether there are any ongoing {@link PhoneAccount#CAPABILITY_SELF_MANAGED}
          * calls for a given {@code packageName} and {@code userHandle}.
@@ -2998,7 +3060,10 @@
         });
 
         mTransactionManager = TransactionManager.getInstance();
-        mTransactionalServiceRepository = new TransactionalServiceRepository(mFeatureFlags);
+        mTransactionManager.setFeatureFlag(mFeatureFlags);
+        mTransactionManager.setAnomalyReporter(mAnomalyReporter);
+        mTransactionalServiceRepository = new TransactionalServiceRepository(mFeatureFlags,
+                mAnomalyReporter);
         mBlockedNumbersManager = mFeatureFlags.telecomMainlineBlockedNumbersManager()
                 ? mContext.getSystemService(BlockedNumbersManager.class)
                 : null;
@@ -3705,15 +3770,13 @@
         // incompatible types.
         if (icon != null && (icon.getType() == Icon.TYPE_URI
                 || icon.getType() == Icon.TYPE_URI_ADAPTIVE_BITMAP)) {
-            String encodedUser = icon.getUri().getEncodedUserInfo();
-            // If there is no encoded user, the URI is calling into the calling user space
-            if (encodedUser != null) {
-                int userId = Integer.parseInt(encodedUser);
-                if (userId != UserHandle.getUserId(Binder.getCallingUid())) {
-                    // If we are transcending the profile boundary, throw an error.
-                    throw new IllegalArgumentException("Attempting to register a phone account with"
-                            + " an image icon belonging to another user.");
-                }
+            int callingUserId = UserHandle.getCallingUserId();
+            int requestingUserId = StatusHints.getUserIdFromAuthority(
+                    icon.getUri().getAuthority(), callingUserId);
+            if(callingUserId != requestingUserId) {
+                // If we are transcending the profile boundary, throw an error.
+                throw new IllegalArgumentException("Attempting to register a phone account with"
+                        + " an image icon belonging to another user.");
             }
         }
     }
diff --git a/src/com/android/server/telecom/TelecomShellCommand.java b/src/com/android/server/telecom/TelecomShellCommand.java
index 11ceb26..2e955a9 100644
--- a/src/com/android/server/telecom/TelecomShellCommand.java
+++ b/src/com/android/server/telecom/TelecomShellCommand.java
@@ -67,6 +67,10 @@
     private static final String COMMAND_RESET_CAR_MODE = "reset-car-mode";
     private static final String COMMAND_IS_NON_IN_CALL_SERVICE_BOUND =
             "is-non-ui-in-call-service-bound";
+    private static final String COMMAND_WAIT_FOR_AUDIO_OPS_COMPLETION =
+            "wait-for-audio-ops-complete";
+    private static final String COMMAND_WAIT_FOR_AUDIO_ACTIVE_COMPLETION =
+            "wait-for-audio-active";
 
     /**
      * Change the system dialer package name if a package name was specified,
@@ -83,6 +87,8 @@
     private static final String COMMAND_GET_MAX_PHONES = "get-max-phones";
     private static final String COMMAND_SET_TEST_EMERGENCY_PHONE_ACCOUNT_PACKAGE_FILTER =
             "set-test-emergency-phone-account-package-filter";
+    private static final String COMMAND_SET_METRICS_TEST_ENABLED = "set-metrics-test-enabled";
+    private static final String COMMAND_SET_METRICS_TEST_DISABLED = "set-metrics-test-disabled";
     /**
      * Command used to emit a distinct "mark" in the logs.
      */
@@ -184,6 +190,18 @@
                 case COMMAND_LOG_MARK:
                     runLogMark();
                     break;
+                case COMMAND_SET_METRICS_TEST_ENABLED:
+                    mTelecomService.setMetricsTestMode(true);
+                    break;
+                case COMMAND_SET_METRICS_TEST_DISABLED:
+                    mTelecomService.setMetricsTestMode(false);
+                    break;
+                case COMMAND_WAIT_FOR_AUDIO_OPS_COMPLETION:
+                    mTelecomService.waitForAudioToUpdate(false);
+                    break;
+                case COMMAND_WAIT_FOR_AUDIO_ACTIVE_COMPLETION:
+                    mTelecomService.waitForAudioToUpdate(true);
+                    break;
                 default:
                     return handleDefaultCommands(command);
             }
@@ -262,6 +280,8 @@
                 + "testers to indicate where in the logs various test steps take place.\n"
                 + "telecom is-non-ui-in-call-service-bound <PACKAGE>: queries a particular "
                 + "non-ui-InCallService in InCallController to determine if it is bound \n"
+                + "telecom set-metrics-test-enabled: Enable the metrics test mode.\n"
+                + "telecom set-metrics-test-disabled: Disable the metrics test mode.\n"
         );
     }
     private void runSetPhoneAccountEnabled(boolean enabled) throws RemoteException {
diff --git a/src/com/android/server/telecom/TelecomSystem.java b/src/com/android/server/telecom/TelecomSystem.java
index 7020885..00341a5 100644
--- a/src/com/android/server/telecom/TelecomSystem.java
+++ b/src/com/android/server/telecom/TelecomSystem.java
@@ -28,6 +28,7 @@
 import android.net.Uri;
 import android.os.BugreportManager;
 import android.os.DropBoxManager;
+import android.os.Looper;
 import android.os.UserHandle;
 import android.telecom.Log;
 import android.telecom.PhoneAccountHandle;
@@ -48,6 +49,7 @@
 import com.android.server.telecom.components.UserCallIntentProcessor;
 import com.android.server.telecom.components.UserCallIntentProcessorFactory;
 import com.android.server.telecom.flags.FeatureFlags;
+import com.android.server.telecom.metrics.EventStats;
 import com.android.server.telecom.metrics.TelecomMetricsController;
 import com.android.server.telecom.ui.AudioProcessingNotification;
 import com.android.server.telecom.ui.CallStreamingNotification;
@@ -230,7 +232,8 @@
             Executor asyncCallAudioTaskExecutor,
             BlockedNumbersAdapter blockedNumbersAdapter,
             FeatureFlags featureFlags,
-            com.android.internal.telephony.flags.FeatureFlags telephonyFlags) {
+            com.android.internal.telephony.flags.FeatureFlags telephonyFlags,
+            Looper looper) {
         mContext = context.getApplicationContext();
         mFeatureFlags = featureFlags;
         LogUtils.initLogging(mContext);
@@ -264,7 +267,7 @@
                     communicationDeviceTracker, featureFlags);
             BluetoothRouteManager bluetoothRouteManager = new BluetoothRouteManager(mContext, mLock,
                     bluetoothDeviceManager, new Timeouts.Adapter(),
-                    communicationDeviceTracker, featureFlags);
+                    communicationDeviceTracker, featureFlags, looper);
             BluetoothStateReceiver bluetoothStateReceiver = new BluetoothStateReceiver(
                     bluetoothDeviceManager, bluetoothRouteManager,
                     communicationDeviceTracker, featureFlags);
diff --git a/src/com/android/server/telecom/TransactionalServiceRepository.java b/src/com/android/server/telecom/TransactionalServiceRepository.java
index 5ae459e..954307a 100644
--- a/src/com/android/server/telecom/TransactionalServiceRepository.java
+++ b/src/com/android/server/telecom/TransactionalServiceRepository.java
@@ -35,9 +35,13 @@
     private static final Map<PhoneAccountHandle, TransactionalServiceWrapper> mServiceLookupTable =
             new HashMap<>();
     private final FeatureFlags mFlags;
+    private final AnomalyReporterAdapter mAnomalyReporter;
 
-    public TransactionalServiceRepository(FeatureFlags flags) {
+    public TransactionalServiceRepository(
+            FeatureFlags flags,
+            AnomalyReporterAdapter anomalyReporter) {
         mFlags = flags;
+        mAnomalyReporter = anomalyReporter;
     }
 
     public TransactionalServiceWrapper addNewCallForTransactionalServiceWrapper
@@ -50,7 +54,8 @@
             Log.d(TAG, "creating a new TSW; handle=[%s]", phoneAccountHandle);
             service = new TransactionalServiceWrapper(callEventCallback,
                     callsManager, phoneAccountHandle, call, this,
-                    TransactionManager.getInstance(), mFlags.enableCallSequencing());
+                    TransactionManager.getInstance(), mFlags.enableCallSequencing(),
+                    mFlags, mAnomalyReporter);
         } else {
             Log.d(TAG, "add a new call to an existing TSW; handle=[%s]", phoneAccountHandle);
             service = getTransactionalServiceWrapper(phoneAccountHandle);
diff --git a/src/com/android/server/telecom/TransactionalServiceWrapper.java b/src/com/android/server/telecom/TransactionalServiceWrapper.java
index cf5ef41..cc0d547 100644
--- a/src/com/android/server/telecom/TransactionalServiceWrapper.java
+++ b/src/com/android/server/telecom/TransactionalServiceWrapper.java
@@ -47,9 +47,11 @@
 import com.android.server.telecom.callsequencing.TransactionManager;
 import com.android.server.telecom.callsequencing.CallTransaction;
 import com.android.server.telecom.callsequencing.CallTransactionResult;
+import com.android.server.telecom.flags.FeatureFlags;
 
 import java.util.Locale;
 import java.util.Set;
+import java.util.UUID;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentHashMap;
 
@@ -92,7 +94,12 @@
     private TransactionManager mTransactionManager;
     private CallStreamingController mStreamingController;
     private final TransactionalCallSequencingAdapter mCallSequencingAdapter;
-
+    private final FeatureFlags mFeatureFlags;
+    private final AnomalyReporterAdapter mAnomalyReporter;
+    public static final UUID CALL_IS_NO_LONGER_BEING_TRACKED_ERROR_UUID =
+            UUID.fromString("8187cd59-97a7-4e9f-a772-638dda4b69bb");
+    public static final String CALL_IS_NO_LONGER_BEING_TRACKED_ERROR_MSG =
+            "A call update was attempted for a call no longer being tracked";
 
     // Each TransactionalServiceWrapper should have their own Binder.DeathRecipient to clean up
     // any calls in the event the application crashes or is force stopped.
@@ -108,7 +115,8 @@
     public TransactionalServiceWrapper(ICallEventCallback callEventCallback,
             CallsManager callsManager, PhoneAccountHandle phoneAccountHandle, Call call,
             TransactionalServiceRepository repo, TransactionManager transactionManager,
-            boolean isCallSequencingEnabled) {
+            boolean isCallSequencingEnabled, FeatureFlags featureFlags,
+            AnomalyReporterAdapter anomalyReporterAdapter) {
         // passed args
         mICallEventCallback = callEventCallback;
         mCallsManager = callsManager;
@@ -123,6 +131,8 @@
         mCallSequencingAdapter = new TransactionalCallSequencingAdapter(mTransactionManager,
                 mCallsManager, isCallSequencingEnabled);
         setDeathRecipient(callEventCallback);
+        mFeatureFlags = featureFlags;
+        mAnomalyReporter = anomalyReporterAdapter;
     }
 
     public TransactionManager getTransactionManager() {
@@ -307,6 +317,11 @@
                                 + " via TelecomManager#addCall", action, callId),
                                 CODE_CALL_IS_NOT_BEING_TRACKED));
                 callback.send(CODE_CALL_IS_NOT_BEING_TRACKED, exceptionBundle);
+                if (mFeatureFlags.enableCallExceptionAnomReports()) {
+                    mAnomalyReporter.reportAnomaly(
+                            CALL_IS_NO_LONGER_BEING_TRACKED_ERROR_UUID,
+                            CALL_IS_NO_LONGER_BEING_TRACKED_ERROR_MSG);
+                }
             }
         }
 
@@ -401,11 +416,12 @@
         return onSetActiveFuture;
     }
 
-    public void onAnswer(Call call, int videoState) {
+    public CompletableFuture<Boolean> onAnswer(Call call, int videoState) {
+        CompletableFuture<Boolean> onAnswerFuture;
         try {
             Log.startSession("TSW.oA");
             Log.d(TAG, String.format(Locale.US, "onAnswer: callId=[%s]", call.getId()));
-            mCallSequencingAdapter.onSetAnswered(call, videoState,
+            onAnswerFuture = mCallSequencingAdapter.onSetAnswered(call, videoState,
                     new CallEventCallbackAckTransaction(mICallEventCallback,
                             ON_ANSWER, call.getId(), videoState, mLock),
                     result -> Log.i(TAG, String.format(Locale.US,
@@ -414,6 +430,7 @@
         } finally {
             Log.endSession();
         }
+        return onAnswerFuture;
     }
 
     public CompletableFuture<Boolean> onSetInactive(Call call) {
diff --git a/src/com/android/server/telecom/UserUtil.java b/src/com/android/server/telecom/UserUtil.java
index 57906d4..8c124c8 100644
--- a/src/com/android/server/telecom/UserUtil.java
+++ b/src/com/android/server/telecom/UserUtil.java
@@ -35,15 +35,28 @@
     private UserUtil() {
     }
 
+    private static final String LOG_TAG = "UserUtil";
+
     private static UserInfo getUserInfoFromUserHandle(Context context, UserHandle userHandle) {
         UserManager userManager = context.getSystemService(UserManager.class);
         return userManager.getUserInfo(userHandle.getIdentifier());
     }
 
+    private static UserManager getUserManagerFromUserHandle(Context context,
+            UserHandle userHandle) {
+        UserManager userManager = null;
+        try {
+            userManager = context.createContextAsUser(userHandle, 0)
+                    .getSystemService(UserManager.class);
+        } catch (IllegalStateException e) {
+            Log.e(LOG_TAG, e, "Error while creating context as user = " + userHandle);
+        }
+        return userManager;
+    }
+
     public static boolean isManagedProfile(Context context, UserHandle userHandle,
             FeatureFlags featureFlags) {
-        UserManager userManager = context.createContextAsUser(userHandle, 0)
-                .getSystemService(UserManager.class);
+        UserManager userManager = getUserManagerFromUserHandle(context, userHandle);
         UserInfo userInfo = getUserInfoFromUserHandle(context, userHandle);
         return featureFlags.telecomResolveHiddenDependencies()
                 ? userManager != null && userManager.isManagedProfile()
@@ -51,15 +64,13 @@
     }
 
     public static boolean isPrivateProfile(UserHandle userHandle, Context context) {
-        UserManager um = context.createContextAsUser(userHandle, 0).getSystemService(
-                UserManager.class);
+        UserManager um = getUserManagerFromUserHandle(context, userHandle);
         return um != null && um.isPrivateProfile();
     }
 
     public static boolean isProfile(Context context, UserHandle userHandle,
             FeatureFlags featureFlags) {
-        UserManager userManager = context.createContextAsUser(userHandle, 0)
-                .getSystemService(UserManager.class);
+        UserManager userManager = getUserManagerFromUserHandle(context, userHandle);
         UserInfo userInfo = getUserInfoFromUserHandle(context, userHandle);
         return featureFlags.telecomResolveHiddenDependencies()
                 ? userManager != null && userManager.isProfile()
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
index f4d6041..eda8c64 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
@@ -34,6 +34,7 @@
 import android.media.AudioDeviceInfo;
 import android.media.AudioManager;
 import android.os.Bundle;
+import android.telecom.CallAudioState;
 import android.telecom.Log;
 import android.util.ArraySet;
 import android.util.LocalLog;
@@ -166,6 +167,12 @@
                             mLocalLog.log(logString);
                             return;
                         }
+                        if (mBluetoothLeAudioService == null) {
+                            logString += ", but leAudio service is unavailable";
+                            Log.i(BluetoothDeviceManager.this, logString);
+                            mLocalLog.log(logString);
+                            return;
+                        }
                         try {
                             mLeAudioCallbackRegistered = true;
                             mBluetoothLeAudioService.registerCallback(
@@ -233,21 +240,25 @@
                 }
            };
 
-    private void handleAudioRefactoringServiceDisconnected(int profile) {
+    @VisibleForTesting
+    public void handleAudioRefactoringServiceDisconnected(int profile) {
         CallAudioRouteController controller = (CallAudioRouteController)
                 mCallAudioRouteAdapter;
         Map<AudioRoute, BluetoothDevice> btRoutes = controller
                 .getBluetoothRoutes();
         List<Pair<AudioRoute, BluetoothDevice>> btRoutesToRemove =
                 new ArrayList<>();
-        for (AudioRoute route: btRoutes.keySet()) {
-            if (route.getType() != PROFILE_TO_AUDIO_ROUTE_MAP.get(profile)) {
-                continue;
+        // Prevent concurrent modification exception by just iterating
+        //through keys instead of simultaneously removing them. Ensure that
+        // we synchronize on the map while we traverse via an Iterator.
+        synchronized (btRoutes) {
+            for (AudioRoute route: btRoutes.keySet()) {
+                if (route.getType() != PROFILE_TO_AUDIO_ROUTE_MAP.get(profile)) {
+                    continue;
+                }
+                BluetoothDevice device = btRoutes.get(route);
+                btRoutesToRemove.add(new Pair<>(route, device));
             }
-            BluetoothDevice device = btRoutes.get(route);
-            // Prevent concurrent modification exception by just iterating through keys instead of
-            // simultaneously removing them.
-            btRoutesToRemove.add(new Pair<>(route, device));
         }
 
         for (Pair<AudioRoute, BluetoothDevice> routeToRemove:
@@ -257,8 +268,23 @@
             mCallAudioRouteAdapter.sendMessageWithSessionInfo(
                     BT_DEVICE_REMOVED, route.getType(), device);
         }
-        mCallAudioRouteAdapter.sendMessageWithSessionInfo(
-                SWITCH_BASELINE_ROUTE, INCLUDE_BLUETOOTH_IN_BASELINE, (String) null);
+
+        if (mFeatureFlags.skipBaselineSwitchWhenRouteNotBluetooth()) {
+            CallAudioState currentAudioState = controller.getCurrentCallAudioState();
+            int currentRoute = currentAudioState.getRoute();
+            if (currentRoute == CallAudioState.ROUTE_BLUETOOTH) {
+                Log.d(this, "handleAudioRefactoringServiceDisconnected: call audio "
+                        + "is currently routed to BT so switching back to baseline");
+                mCallAudioRouteAdapter.sendMessageWithSessionInfo(
+                        SWITCH_BASELINE_ROUTE, INCLUDE_BLUETOOTH_IN_BASELINE, (String) null);
+            } else {
+                Log.d(this, "handleAudioRefactoringServiceDisconnected: call audio "
+                        + "is not currently routed to BT so skipping switch to baseline");
+            }
+        } else {
+            mCallAudioRouteAdapter.sendMessageWithSessionInfo(
+                    SWITCH_BASELINE_ROUTE, INCLUDE_BLUETOOTH_IN_BASELINE, (String) null);
+        }
     }
 
     private final LinkedHashMap<String, BluetoothDevice> mHfpDevicesByAddress =
@@ -579,7 +605,8 @@
             Log.w(this, "disconnectSco: Trying to disconnect audio but no headset service exists.");
         } else {
             result = mBluetoothHeadset.disconnectAudio();
-            Log.i(this, "disconnectSco: BluetoothHeadset#disconnectAudio()=%b", result);
+            Log.i(this, "disconnectSco: BluetoothHeadset#disconnectAudio()=%s",
+                    btCodeToString(result));
         }
         return result;
     }
@@ -825,6 +852,7 @@
         if (callProfile == BluetoothProfile.LE_AUDIO) {
             if (mBluetoothAdapter.setActiveDevice(
                     device, BluetoothAdapter.ACTIVE_DEVICE_ALL)) {
+                Log.i(this, "connectAudio: BluetoothAdapter#setActiveDevice(%s)=true", address);
                 /* ACTION_ACTIVE_DEVICE_CHANGED intent will trigger setting communication device.
                  * Only after receiving ACTION_ACTIVE_DEVICE_CHANGED it is known that device that
                  * will be audio switched to is available to be choose as communication device */
@@ -836,9 +864,11 @@
                 }
                 return true;
             }
+            Log.i(this, "connectAudio: BluetoothAdapter#setActiveDevice(%s)=false", address);
             return false;
         } else if (callProfile == BluetoothProfile.HEARING_AID) {
             if (mBluetoothAdapter.setActiveDevice(device, BluetoothAdapter.ACTIVE_DEVICE_ALL)) {
+                Log.i(this, "connectAudio: BluetoothAdapter#setActiveDevice(%s)=true", address);
                 /* ACTION_ACTIVE_DEVICE_CHANGED intent will trigger setting communication device.
                  * Only after receiving ACTION_ACTIVE_DEVICE_CHANGED it is known that device that
                  * will be audio switched to is available to be choose as communication device */
@@ -850,19 +880,20 @@
                 }
                 return true;
             }
+            Log.i(this, "connectAudio: BluetoothAdapter#setActiveDevice(%s)=false", address);
             return false;
         } else if (callProfile == BluetoothProfile.HEADSET) {
             boolean success = mBluetoothAdapter.setActiveDevice(device,
                 BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL);
+            Log.i(this, "connectAudio: BluetoothAdapter#setActiveDevice(%s)=%b", address, success);
             if (!success) {
                 Log.w(this, "connectAudio: Couldn't set active device to %s", address);
                 return false;
             }
-            Log.i(this, "connectAudio: BluetoothAdapter#setActiveDevice(%s)", address);
             if (getBluetoothHeadset() != null) {
                 int scoConnectionRequest = mBluetoothHeadset.connectAudio();
-                Log.i(this, "connectAudio: BluetoothHeadset#connectAudio()=%d",
-                        scoConnectionRequest);
+                Log.i(this, "connectAudio: BluetoothHeadset#connectAudio()=%s",
+                        btCodeToString(scoConnectionRequest));
                 return scoConnectionRequest == BluetoothStatusCodes.SUCCESS ||
                         scoConnectionRequest
                                 == BluetoothStatusCodes.ERROR_AUDIO_DEVICE_ALREADY_CONNECTED;
@@ -883,7 +914,8 @@
      * @param type {@link AudioRoute.AudioRouteType} associated with the device.
      * @return {@code true} if device was successfully connected, {@code false} otherwise.
      */
-    public boolean connectAudio(BluetoothDevice device, @AudioRoute.AudioRouteType int type) {
+    public boolean connectAudio(BluetoothDevice device, @AudioRoute.AudioRouteType int type,
+            boolean isScoManagedByAudio) {
         String address = device.getAddress();
         int callProfile = BluetoothProfile.LE_AUDIO;
         if (type == TYPE_BLUETOOTH_SCO) {
@@ -901,19 +933,23 @@
         }
 
         if (callProfile == BluetoothProfile.LE_AUDIO
-                || callProfile == BluetoothProfile.HEARING_AID) {
-            return mBluetoothAdapter.setActiveDevice(device, BluetoothAdapter.ACTIVE_DEVICE_ALL);
+                || callProfile == BluetoothProfile.HEARING_AID || isScoManagedByAudio) {
+            boolean success = mBluetoothAdapter.setActiveDevice(device,
+                    BluetoothAdapter.ACTIVE_DEVICE_ALL);
+            Log.i(this, "connectAudio: BluetoothAdapter#setActiveDevice(%s)=%b", address, success);
+            return success;
         } else if (callProfile == BluetoothProfile.HEADSET) {
             boolean success = mBluetoothAdapter.setActiveDevice(device,
                     BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL);
+            Log.i(this, "connectAudio: BluetoothAdapter#setActiveDevice(%s)=%b", address, success);
             if (!success) {
                 Log.w(this, "connectAudio: Couldn't set active device to %s", address);
                 return false;
             }
             if (getBluetoothHeadset() != null) {
                 int scoConnectionRequest = mBluetoothHeadset.connectAudio();
-                Log.i(this, "connectaudio: BluetoothHeadset#connectAudio()=%d",
-                        scoConnectionRequest);
+                Log.i(this, "connectAudio: BluetoothHeadset#connectAudio()=%s",
+                        btCodeToString(scoConnectionRequest));
                 return scoConnectionRequest == BluetoothStatusCodes.SUCCESS ||
                         scoConnectionRequest
                                 == BluetoothStatusCodes.ERROR_AUDIO_DEVICE_ALREADY_CONNECTED;
@@ -956,6 +992,34 @@
         return isInbandRingEnabled(activeDevice);
     }
 
+    /**
+     * Check if inband ringing is enabled for the specified BT device.
+     * This is intended for use by {@link CallAudioRouteController}.
+     * @param audioRouteType The BT device type.
+     * @param bluetoothDevice The BT device.
+     * @return {@code true} if inband ringing is enabled, {@code false} otherwise.
+     */
+    public boolean isInbandRingEnabled(@AudioRoute.AudioRouteType int audioRouteType,
+            BluetoothDevice bluetoothDevice) {
+        if (audioRouteType == AudioRoute.TYPE_BLUETOOTH_LE) {
+            if (mBluetoothLeAudioService == null) {
+                Log.i(this, "isInbandRingingEnabled: no leaudio service available.");
+                return false;
+            }
+            int groupId = mBluetoothLeAudioService.getGroupId(bluetoothDevice);
+            return mBluetoothLeAudioService.isInbandRingtoneEnabled(groupId);
+        } else {
+            if (getBluetoothHeadset() == null) {
+                Log.i(this, "isInbandRingingEnabled: no headset service available.");
+                return false;
+            }
+            boolean isEnabled = mBluetoothHeadset.isInbandRingingEnabled();
+            Log.i(this, "isInbandRingEnabled: device: %s, isEnabled: %b", bluetoothDevice,
+                    isEnabled);
+            return isEnabled;
+        }
+    }
+
     public boolean isInbandRingEnabled(BluetoothDevice bluetoothDevice) {
         if (mBluetoothRouteManager.isCachedLeAudioDevice(bluetoothDevice)) {
             if (mBluetoothLeAudioService == null) {
@@ -983,4 +1047,33 @@
     public void dump(IndentingPrintWriter pw) {
         mLocalLog.dump(pw);
     }
+
+    private String btCodeToString(int code) {
+        switch (code) {
+            case BluetoothStatusCodes.SUCCESS:
+                return "SUCCESS";
+            case BluetoothStatusCodes.ERROR_UNKNOWN:
+                return "ERROR_UNKNOWN";
+            case BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND:
+                return "ERROR_PROFILE_SERVICE_NOT_BOUND";
+            case BluetoothStatusCodes.ERROR_TIMEOUT:
+                return "ERROR_TIMEOUT";
+            case BluetoothStatusCodes.ERROR_AUDIO_DEVICE_ALREADY_CONNECTED:
+                return "ERROR_AUDIO_DEVICE_ALREADY_CONNECTED";
+            case BluetoothStatusCodes.ERROR_NO_ACTIVE_DEVICES:
+                return "ERROR_NO_ACTIVE_DEVICES";
+            case BluetoothStatusCodes.ERROR_NOT_ACTIVE_DEVICE:
+                return "ERROR_NOT_ACTIVE_DEVICE";
+            case BluetoothStatusCodes.ERROR_AUDIO_ROUTE_BLOCKED:
+                return "ERROR_AUDIO_ROUTE_BLOCKED";
+            case BluetoothStatusCodes.ERROR_CALL_ACTIVE:
+                return "ERROR_CALL_ACTIVE";
+            case BluetoothStatusCodes.ERROR_PROFILE_NOT_CONNECTED:
+                return "ERROR_PROFILE_NOT_CONNECTED";
+            case BluetoothStatusCodes.ERROR_AUDIO_DEVICE_ALREADY_DISCONNECTED:
+                return "BluetoothStatusCodes.ERROR_AUDIO_DEVICE_ALREADY_DISCONNECTED";
+            default:
+                return Integer.toString(code);
+        }
+    }
 }
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java b/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
index 5a44041..93dbed6 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
@@ -26,6 +26,7 @@
 import android.content.Context;
 import android.media.AudioDeviceInfo;
 import android.os.Message;
+import android.os.Looper;
 import android.telecom.Log;
 import android.telecom.Logging.Session;
 import android.util.Pair;
@@ -36,6 +37,7 @@
 import com.android.internal.util.IState;
 import com.android.internal.util.State;
 import com.android.internal.util.StateMachine;
+import com.android.server.telecom.AudioRoute;
 import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
 import com.android.server.telecom.TelecomSystem;
 import com.android.server.telecom.Timeouts;
@@ -607,8 +609,8 @@
     public BluetoothRouteManager(Context context, TelecomSystem.SyncRoot lock,
             BluetoothDeviceManager deviceManager, Timeouts.Adapter timeoutsAdapter,
             CallAudioCommunicationDeviceTracker communicationDeviceTracker,
-            FeatureFlags featureFlags) {
-        super(BluetoothRouteManager.class.getSimpleName());
+            FeatureFlags featureFlags, Looper looper) {
+        super(BluetoothRouteManager.class.getSimpleName(), looper);
         mContext = context;
         mLock = lock;
         mDeviceManager = deviceManager;
@@ -1179,6 +1181,11 @@
         return mDeviceManager.isInbandRingEnabled(bluetoothDevice);
     }
 
+    public boolean isInbandRingEnabled(@AudioRoute.AudioRouteType int audioRouteType,
+            BluetoothDevice bluetoothDevice) {
+        return mDeviceManager.isInbandRingEnabled(audioRouteType, bluetoothDevice);
+    }
+
     private boolean addDevice(String address) {
         if (mAudioConnectingStates.containsKey(address)) {
             Log.i(this, "Attempting to add device %s twice.", address);
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java b/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
index 1cea531..1330be4 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
@@ -40,6 +40,7 @@
 import android.content.IntentFilter;
 import android.media.AudioDeviceInfo;
 import android.os.Bundle;
+import android.sysprop.BluetoothProperties;
 import android.telecom.Log;
 import android.telecom.Logging.Session;
 import android.util.Pair;
@@ -50,7 +51,6 @@
 import com.android.server.telecom.CallAudioRouteAdapter;
 import com.android.server.telecom.CallAudioRouteController;
 import com.android.server.telecom.flags.FeatureFlags;
-import com.android.server.telecom.flags.Flags;
 
 public class BluetoothStateReceiver extends BroadcastReceiver {
     private static final String LOG_TAG = BluetoothStateReceiver.class.getSimpleName();
@@ -74,6 +74,7 @@
     private final BluetoothDeviceManager mBluetoothDeviceManager;
     private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
     private FeatureFlags mFeatureFlags;
+    private boolean mIsScoManagedByAudio;
     private CallAudioRouteAdapter mCallAudioRouteAdapter;
 
     public void onReceive(Context context, Intent intent) {
@@ -269,7 +270,12 @@
                 mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
                         audioRouteType, device.getAddress());
                 if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID
-                        || deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO) {
+                        || deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO
+                        || mIsScoManagedByAudio) {
+                    if (!mIsInCall) {
+                        Log.i(LOG_TAG, "Ignoring audio on since we're not in a call");
+                        return;
+                    }
                     if (!mBluetoothDeviceManager.setCommunicationDeviceForAddress(
                             device.getAddress())) {
                         Log.i(this, "handleActiveDeviceChanged: Failed to set "
@@ -286,11 +292,12 @@
                         }
                     } else {
                         // Track the currently set communication device.
-                        int routeType = deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO
-                                ? AudioRoute.TYPE_BLUETOOTH_LE
-                                : AudioRoute.TYPE_BLUETOOTH_HA;
                         mCallAudioRouteAdapter.getPendingAudioRoute()
-                                .setCommunicationDeviceType(routeType);
+                                .setCommunicationDeviceType(audioRouteType);
+                        if (audioRouteType == AudioRoute.TYPE_BLUETOOTH_SCO) {
+                            mCallAudioRouteAdapter.getPendingAudioRoute()
+                                    .addMessage(BT_AUDIO_CONNECTED, device.getAddress());
+                        }
                     }
                 }
             }
@@ -379,6 +386,9 @@
         mBluetoothRouteManager = routeManager;
         mCommunicationDeviceTracker = communicationDeviceTracker;
         mFeatureFlags = featureFlags;
+        // Indication that SCO is managed by audio (i.e. supports setCommunicationDevice).
+        mIsScoManagedByAudio = android.media.audio.Flags.scoManagedByAudio()
+                && BluetoothProperties.isScoManagedByAudioEnabled().orElse(false);
     }
 
     public void setIsInCall(boolean isInCall) {
diff --git a/src/com/android/server/telecom/callredirection/CallRedirectionProcessor.java b/src/com/android/server/telecom/callredirection/CallRedirectionProcessor.java
index 05e73d5..15b8aa9 100644
--- a/src/com/android/server/telecom/callredirection/CallRedirectionProcessor.java
+++ b/src/com/android/server/telecom/callredirection/CallRedirectionProcessor.java
@@ -133,6 +133,14 @@
                             + mServiceType + " call redirection service");
                 }
             }
+            Log.i(this, "notifyTimeout: call redirection has timed out so "
+                    + "unbinding the connection");
+            if (mConnection != null) {
+                // We still need to call unbind even if the service disconnected.
+                mContext.unbindService(mConnection);
+                mConnection = null;
+            }
+            mService = null;
         }
 
         private class CallRedirectionServiceConnection implements ServiceConnection {
diff --git a/src/com/android/server/telecom/callsequencing/CallSequencingController.java b/src/com/android/server/telecom/callsequencing/CallSequencingController.java
index 2f0ae45..d6e02c1 100644
--- a/src/com/android/server/telecom/callsequencing/CallSequencingController.java
+++ b/src/com/android/server/telecom/callsequencing/CallSequencingController.java
@@ -16,67 +16,994 @@
 
 package com.android.server.telecom.callsequencing;
 
+import static android.Manifest.permission.CALL_PRIVILEGED;
+
+import static com.android.server.telecom.CallsManager.CALL_FILTER_ALL;
+import static com.android.server.telecom.CallsManager.LIVE_CALL_STUCK_CONNECTING_EMERGENCY_ERROR_MSG;
+import static com.android.server.telecom.CallsManager.LIVE_CALL_STUCK_CONNECTING_EMERGENCY_ERROR_UUID;
+import static com.android.server.telecom.CallsManager.LIVE_CALL_STUCK_CONNECTING_ERROR_MSG;
+import static com.android.server.telecom.CallsManager.LIVE_CALL_STUCK_CONNECTING_ERROR_UUID;
+import static com.android.server.telecom.CallsManager.ONGOING_CALL_STATES;
+import static com.android.server.telecom.CallsManager.OUTGOING_CALL_STATES;
+import static com.android.server.telecom.UserUtil.showErrorDialogForRestrictedOutgoingCall;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Bundle;
 import android.os.Handler;
 import android.os.HandlerThread;
+import android.os.OutcomeReceiver;
+import android.telecom.CallAttributes;
+import android.telecom.CallException;
+import android.telecom.Connection;
+import android.telecom.DisconnectCause;
+import android.telecom.Log;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.AnomalyReporter;
+import android.telephony.CarrierConfigManager;
 
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.telecom.AnomalyReporterAdapter;
 import com.android.server.telecom.Call;
+import com.android.server.telecom.CallState;
 import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.ClockProxy;
+import com.android.server.telecom.LogUtils;
+import com.android.server.telecom.LoggedHandlerExecutor;
+import com.android.server.telecom.R;
+import com.android.server.telecom.Timeouts;
+import com.android.server.telecom.callsequencing.voip.OutgoingCallTransaction;
+import com.android.server.telecom.callsequencing.voip.OutgoingCallTransactionSequencing;
+import com.android.server.telecom.flags.FeatureFlags;
+import com.android.server.telecom.metrics.ErrorStats;
+import com.android.server.telecom.metrics.TelecomMetricsController;
+import com.android.server.telecom.stats.CallFailureCause;
 
+import java.util.List;
+import java.util.Objects;
+import java.util.UUID;
 import java.util.concurrent.CompletableFuture;
 
 /**
  * Controls the sequencing between calls when moving between the user ACTIVE (RINGING/ACTIVE) and
- * user INACTIVE (INCOMING/HOLD/DISCONNECTED) states.
+ * user INACTIVE (INCOMING/HOLD/DISCONNECTED) states. This controller is gated by the
+ * {@link FeatureFlags#enableCallSequencing()} flag. Call state changes are verified on a
+ * transactional basis where each operation is verified step by step for cross-phone account calls
+ * or just for the focus call in the case of processing calls on the same phone account.
  */
 public class CallSequencingController {
-//    private final CallsManager mCallsManager;
-    private final TransactionManager mTransactionManager;
-//    private final Handler mHandler;
-//    private boolean mCallSequencingEnabled;
+    private final CallsManager mCallsManager;
+    private final ClockProxy mClockProxy;
+    private final AnomalyReporterAdapter mAnomalyReporter;
+    private final Timeouts.Adapter mTimeoutsAdapter;
+    private final TelecomMetricsController mMetricsController;
+    private final Handler mHandler;
+    private final Context mContext;
+    private final FeatureFlags mFeatureFlags;
+    private static String TAG = CallSequencingController.class.getSimpleName();
+    public static final UUID SEQUENCING_CANNOT_HOLD_ACTIVE_CALL_UUID =
+            UUID.fromString("ea094d77-6ea9-4e40-891e-14bff5d485d7");
+    public static final String SEQUENCING_CANNOT_HOLD_ACTIVE_CALL_MSG =
+            "Cannot hold active call";
 
-    public CallSequencingController(CallsManager callsManager, boolean callSequencingEnabled) {
-//        mCallsManager = callsManager;
-        mTransactionManager = TransactionManager.getInstance();
+    public CallSequencingController(CallsManager callsManager, Context context,
+            ClockProxy clockProxy, AnomalyReporterAdapter anomalyReporter,
+            Timeouts.Adapter timeoutsAdapter, TelecomMetricsController metricsController,
+            FeatureFlags featureFlags) {
+        mCallsManager = callsManager;
+        mClockProxy = clockProxy;
+        mAnomalyReporter = anomalyReporter;
+        mMetricsController = metricsController;
+        mTimeoutsAdapter = timeoutsAdapter;
         HandlerThread handlerThread = new HandlerThread(this.toString());
         handlerThread.start();
-//        mHandler = new Handler(handlerThread.getLooper());
-//        mCallSequencingEnabled = callSequencingEnabled;
+        mHandler = new Handler(handlerThread.getLooper());
+        mFeatureFlags = featureFlags;
+        mContext = context;
     }
 
+    /**
+     * Creates the outgoing call transaction given that call sequencing is enabled. Two separate
+     * transactions are being tracked here; one is if room needs to be made for the outgoing call
+     * and another to verify that the new call was placed. We need to ensure that the transaction
+     * to make room for the outgoing call is processed beforehand (i.e. see
+     * {@link OutgoingCallTransaction}.
+     * @param callAttributes The call attributes associated with the call.
+     * @param extras The extras that are associated with the call.
+     * @param callingPackage The calling package representing where the request was invoked from.
+     * @return The {@link CompletableFuture<CallTransaction>} that encompasses the request to
+     *         place/receive the transactional call.
+     */
+    public CompletableFuture<CallTransaction> createTransactionalOutgoingCall(String callId,
+            CallAttributes callAttributes, Bundle extras, String callingPackage) {
+        PhoneAccountHandle requestedAccountHandle = callAttributes.getPhoneAccountHandle();
+        Uri address = callAttributes.getAddress();
+        if (mCallsManager.isOutgoingCallPermitted(requestedAccountHandle)) {
+            Log.d(this, "createTransactionalOutgoingCall: outgoing call permitted");
+            final boolean hasCallPrivilegedPermission = mContext.checkCallingPermission(
+                    CALL_PRIVILEGED) == PackageManager.PERMISSION_GRANTED;
+
+            final Intent intent = new Intent(hasCallPrivilegedPermission ?
+                    Intent.ACTION_CALL_PRIVILEGED : Intent.ACTION_CALL, address);
+            Bundle updatedExtras = OutgoingCallTransaction.generateExtras(callId, extras,
+                    callAttributes, mFeatureFlags);
+            // Note that this may start a potential transaction to make room for the outgoing call
+            // so we want to ensure that transaction is queued up first and then create another
+            // transaction to complete the call future.
+            CompletableFuture<Call> callFuture = mCallsManager.startOutgoingCall(address,
+                    requestedAccountHandle, updatedExtras, requestedAccountHandle.getUserHandle(),
+                    intent, callingPackage);
+            // The second transaction is represented below which will contain the result of whether
+            // the new outgoing call was placed or not. To simplify the logic, we will wait on the
+            // result of the outgoing call future before adding the transaction so that we can wait
+            // for the make room future to complete first.
+            if (callFuture == null) {
+                Log.d(this, "createTransactionalOutgoingCall: Outgoing call not permitted at the "
+                        + "current time.");
+                return CompletableFuture.completedFuture(new OutgoingCallTransactionSequencing(
+                        mCallsManager, null, true /* callNotPermitted */, mFeatureFlags));
+            }
+            return callFuture.thenComposeAsync((call) -> CompletableFuture.completedFuture(
+                    new OutgoingCallTransactionSequencing(mCallsManager, callFuture,
+                            false /* callNotPermitted */, mFeatureFlags)),
+                    new LoggedHandlerExecutor(mHandler, "CSC.aC", mCallsManager.getLock()));
+        } else {
+            Log.d(this, "createTransactionalOutgoingCall: outgoing call not permitted at the "
+                    + "current time.");
+            return CompletableFuture.completedFuture(new OutgoingCallTransactionSequencing(
+                    mCallsManager, null, true /* callNotPermitted */, mFeatureFlags));
+        }
+    }
+
+    /**
+     * Processes the answer call request from the app and verifies the call state changes with
+     * sequencing provided that the calls that are being manipulated are across phone accounts.
+     * @param incomingCall The incoming call to be answered.
+     * @param videoState The video state configuration for the provided call.
+     */
     public void answerCall(Call incomingCall, int videoState) {
-        // Todo: call sequencing logic (stubbed)
+        Log.i(this, "answerCall: Beginning call sequencing transaction for answering "
+                + "incoming call.");
+        holdActiveCallForNewCallWithSequencing(incomingCall).thenComposeAsync((result) -> {
+                if (result) {
+                    mCallsManager.requestFocusActionAnswerCall(incomingCall, videoState);
+                } else {
+                    Log.i(this, "answerCall: Hold active call transaction failed. Aborting "
+                            + "request to answer the incoming call.");
+                }
+                return CompletableFuture.completedFuture(result);
+            }, new LoggedHandlerExecutor(mHandler, "CSC.aC",
+                mCallsManager.getLock()));
     }
 
-//    private CompletableFuture<Boolean> holdActiveCallForNewCallWithSequencing(Call call) {
-//        // Todo: call sequencing logic (stubbed)
-//        return null;
-//    }
+    /**
+     * Handles the case of setting a self-managed call active with call sequencing support.
+     * @param call The self-managed call that's waiting to go active.
+     */
+    public void handleSetSelfManagedCallActive(Call call) {
+        holdActiveCallForNewCallWithSequencing(call).thenComposeAsync((result) -> {
+                if (result) {
+                    Log.i(this, "markCallAsActive: requesting focus for self managed call "
+                            + "before setting active.");
+                    mCallsManager.requestActionSetActiveCall(call,
+                            "active set explicitly for self-managed");
+                } else {
+                    Log.i(this, "markCallAsActive: Unable to hold active call. "
+                            + "Aborting transaction to set self managed call active.");
+                }
+                return CompletableFuture.completedFuture(result);
+            }, new LoggedHandlerExecutor(mHandler,
+                "CM.mCAA", mCallsManager.getLock()));
+    }
 
+    /**
+     * This applies to transactional calls which request to hold the active call with call
+     * sequencing support. The resulting future is an indication of whether the hold request
+     * succeeded which is then used to create additional transactions to request call focus for the
+     * new call.
+     * @param newCall The new transactional call that's waiting to go active.
+     * @param callback The callback used to report the result of holding the active call and if
+     *                 the new call can go active.
+     * @return The {@code CompletableFuture} indicating the result of holding the active call
+     *         (if applicable).
+     */
+    public void transactionHoldPotentialActiveCallForNewCallSequencing(
+            Call newCall, OutcomeReceiver<Boolean, CallException> callback) {
+        holdActiveCallForNewCallWithSequencing(newCall)
+                .thenComposeAsync((result) -> {
+                    if (result) {
+                        // Either we were able to hold the active call or the active call was
+                        // disconnected in favor of the new call.
+                        callback.onResult(true);
+                    } else {
+                        Log.i(this, "transactionHoldPotentialActiveCallForNewCallSequencing: "
+                                + "active call could not be held or disconnected");
+                        callback.onError(
+                                new CallException("activeCall could not be held or disconnected",
+                                CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
+                        if (mFeatureFlags.enableCallExceptionAnomReports()) {
+                            mAnomalyReporter.reportAnomaly(
+                                    SEQUENCING_CANNOT_HOLD_ACTIVE_CALL_UUID,
+                                    SEQUENCING_CANNOT_HOLD_ACTIVE_CALL_MSG
+                            );
+                        }
+                    }
+                    return CompletableFuture.completedFuture(result);
+                }, new LoggedHandlerExecutor(mHandler, "CM.mCAA", mCallsManager.getLock()));
+    }
+
+    /**
+     * Attempts to hold the active call so that the provided call can go active. This is done via
+     * call sequencing and the resulting future is an indication of whether that request
+     * has succeeded.
+     * @param call The call that's waiting to go active.
+     * @return The {@link CompletableFuture} indicating the result of whether the
+     *         active call was able to be held (if applicable).
+     */
+    @VisibleForTesting
+    public CompletableFuture<Boolean> holdActiveCallForNewCallWithSequencing(
+            Call call) {
+        Call activeCall = (Call) mCallsManager.getConnectionServiceFocusManager()
+                .getCurrentFocusCall();
+        Log.i(this, "holdActiveCallForNewCallWithSequencing, newCall: %s, "
+                        + "activeCall: %s", call.getId(),
+                (activeCall == null ? "<none>" : activeCall.getId()));
+        if (activeCall != null && activeCall != call) {
+            boolean isSequencingRequiredActiveAndCall = !arePhoneAccountsSame(call, activeCall);
+            if (mCallsManager.canHold(activeCall)) {
+                CompletableFuture<Boolean> holdFuture = activeCall.hold("swap to " + call.getId());
+                return isSequencingRequiredActiveAndCall
+                        ? holdFuture
+                        : CompletableFuture.completedFuture(true);
+            } else if (mCallsManager.supportsHold(activeCall)) {
+                // Handle the case where active call supports hold but can't currently be held.
+                // In this case, we'll look for the currently held call to disconnect prior to
+                // holding the active call.
+                // E.g.
+                // Call A - Held   (Supports hold, can't hold)
+                // Call B - Active (Supports hold, can't hold)
+                // Call C - Incoming
+                // Here we need to disconnect A prior to holding B so that C can be answered.
+                // This case is driven by telephony requirements ultimately.
+                //
+                // These cases can further be broken down at the phone account level:
+                // E.g. All cases not outlined below...
+                // (1)                              (2)
+                // Call A (Held) - PA1              Call A (Held) - PA1
+                // Call B (Active) - PA2            Call B (Active) - PA2
+                // Call C (Incoming) - PA1          Call C (Incoming) - PA2
+                // We should ensure that only operations across phone accounts require sequencing.
+                // Otherwise, we can send the requests up til the focus call state in question.
+                Call heldCall = mCallsManager.getFirstCallWithState(CallState.ON_HOLD);
+                CompletableFuture<Boolean> disconnectFutureHandler = null;
+
+                boolean isSequencingRequiredHeldAndActive = false;
+                if (heldCall != null) {
+                    // If the calls are from the same source or the incoming call isn't a VOIP call
+                    // and the held call is a carrier call, then disconnect the held call. The
+                    // idea is that if we have a held carrier call and the incoming call is a
+                    // VOIP call, we don't want to force the carrier call to auto-disconnect).
+                    if (!heldCall.isSelfManaged() && call.isSelfManaged()) {
+                        // Otherwise, fail the transaction.
+                        return CompletableFuture.completedFuture(false);
+                    } else {
+                        isSequencingRequiredHeldAndActive = !arePhoneAccountsSame(
+                                heldCall, activeCall);
+                        disconnectFutureHandler = heldCall.disconnect();
+                        Log.i(this, "holdActiveCallForNewCallWithSequencing: "
+                                        + "Disconnect held call %s before holding active call %s.",
+                                heldCall.getId(), activeCall.getId());
+                    }
+                }
+                Log.i(this, "holdActiveCallForNewCallWithSequencing: Holding active "
+                        + "%s before making %s active.", activeCall.getId(), call.getId());
+
+                CompletableFuture<Boolean> holdFutureHandler;
+                if (isSequencingRequiredHeldAndActive && disconnectFutureHandler != null) {
+                    holdFutureHandler = disconnectFutureHandler
+                            .thenComposeAsync((result) -> {
+                                if (result) {
+                                    return activeCall.hold().thenCompose((holdSuccess) -> {
+                                        if (holdSuccess) {
+                                            // Increase hold count only if hold succeeds.
+                                            call.increaseHeldByThisCallCount();
+                                        }
+                                        return CompletableFuture.completedFuture(holdSuccess);
+                                    });
+                                }
+                                return CompletableFuture.completedFuture(false);
+                            }, new LoggedHandlerExecutor(mHandler,
+                                    "CSC.hACFNCWS", mCallsManager.getLock()));
+                } else {
+                    holdFutureHandler = activeCall.hold();
+                    call.increaseHeldByThisCallCount();
+                }
+                // Next transaction will be performed on the call passed in and the last transaction
+                // was performed on the active call so ensure that the caller has this information
+                // to determine if sequencing is required.
+                return isSequencingRequiredActiveAndCall
+                        ? holdFutureHandler
+                        : CompletableFuture.completedFuture(true);
+            } else {
+                // This call does not support hold. If it is from a different connection
+                // service or connection manager, then disconnect it, otherwise allow the connection
+                // service or connection manager to figure out the right states.
+                Log.i(this, "holdActiveCallForNewCallWithSequencing: evaluating disconnecting %s "
+                        + "so that %s can be made active.", activeCall.getId(), call.getId());
+                if (!activeCall.isEmergencyCall()) {
+                    // We don't want to allow VOIP apps to disconnect carrier calls. We are
+                    // purposely completing the future with false so that the call isn't
+                    // answered.
+                    if (isSequencingRequiredActiveAndCall && call.isSelfManaged()
+                            && !activeCall.isSelfManaged()) {
+                        Log.w(this, "holdActiveCallForNewCallWithSequencing: ignore "
+                                + "disconnecting carrier call for making VOIP call active");
+                        return CompletableFuture.completedFuture(false);
+                    } else {
+                        if (isSequencingRequiredActiveAndCall) {
+                            // Disconnect all calls with the same phone account as the active call
+                            // as they do would not support holding.
+                            Log.i(this, "Disconnecting non-holdable calls from account (%s).",
+                                    activeCall.getTargetPhoneAccount());
+                            return disconnectAllCallsWithPhoneAccount(
+                                    activeCall.getTargetPhoneAccount());
+                        } else {
+                            Log.i(this, "holdActiveCallForNewCallWithSequencing: "
+                                    + "allowing ConnectionService to determine how to handle "
+                                    + "this case");
+                            CompletableFuture.completedFuture(true);
+                        }
+                    }
+                } else {
+                    // It's not possible to hold the active call, and it's an emergency call so
+                    // we will silently reject the incoming call instead of answering it.
+                    Log.w(this, "holdActiveCallForNewCallWithSequencing: rejecting incoming "
+                            + "call %s as the active call is an emergency call and "
+                            + "it cannot be held.", call.getId());
+                    call.reject(false /* rejectWithMessage */, "" /* message */,
+                            "active emergency call can't be held");
+                    return CompletableFuture.completedFuture(false);
+                }
+            }
+        }
+        return CompletableFuture.completedFuture(true);
+    }
+
+    /**
+     * Processes the unhold call request sent by the app with call sequencing support.
+     * @param call The call to be unheld.
+     */
     public void unholdCall(Call call) {
-        // Todo: call sequencing logic (stubbed)
+        // Cases: set active call on hold and then set this call to active
+        // Calls could be made on different phone accounts, in which case, we need to verify state
+        // change for each call.
+        CompletableFuture<Boolean> unholdCallFutureHandler = null;
+        Call activeCall = (Call) mCallsManager.getConnectionServiceFocusManager()
+                .getCurrentFocusCall();
+        String activeCallId = null;
+        boolean isSequencingRequiredActiveAndCall = false;
+        if (activeCall != null && !activeCall.isLocallyDisconnecting()) {
+            activeCallId = activeCall.getId();
+            // Determine whether the calls are placed on different phone accounts.
+            isSequencingRequiredActiveAndCall = !arePhoneAccountsSame(activeCall, call);
+            boolean canSwapCalls = canSwap(activeCall, call);
+
+            // If the active + held call are from different phone accounts, ensure that the call
+            // sequencing states are verified at each step.
+            if (canSwapCalls) {
+                unholdCallFutureHandler = activeCall.hold("Swap to " + call.getId());
+                Log.addEvent(activeCall, LogUtils.Events.SWAP, "To " + call.getId());
+                Log.addEvent(call, LogUtils.Events.SWAP, "From " + activeCallId);
+            } else {
+                if (isSequencingRequiredActiveAndCall) {
+                    // If hold isn't supported and the active and held call are on
+                    // different phone accounts where the held call is self-managed and active call
+                    // is managed, abort the transaction. Otherwise, disconnect the call. We also
+                    // don't want to drop an emergency call.
+                    if (!activeCall.isEmergencyCall()) {
+                        Log.w(this, "unholdCall: Unable to hold the active call (%s),"
+                                        + " aborting swap to %s", activeCallId, call.getId(),
+                                call.getId());
+                        showErrorDialogForCannotHoldCall(call, false);
+                    } else {
+                        Log.w(this, "unholdCall: %s is an emergency call, aborting swap to %s",
+                                activeCallId, call.getId());
+                    }
+                    return;
+                } else {
+                    activeCall.hold("Swap to " + call.getId());
+                }
+            }
+        }
+
+        // Verify call state was changed to ACTIVE state
+        if (isSequencingRequiredActiveAndCall && unholdCallFutureHandler != null) {
+            String fixedActiveCallId = activeCallId;
+            // Only attempt to unhold call if previous request to hold/disconnect call (on different
+            // phone account) succeeded.
+            unholdCallFutureHandler.thenComposeAsync((result) -> {
+                if (result) {
+                    Log.i(this, "unholdCall: Request to hold active call transaction succeeded.");
+                    mCallsManager.requestActionUnholdCall(call, fixedActiveCallId);
+                } else {
+                    Log.i(this, "unholdCall: Request to hold active call transaction failed. "
+                            + "Aborting unhold transaction.");
+                }
+                return CompletableFuture.completedFuture(result);
+            }, new LoggedHandlerExecutor(mHandler, "CSC.uC",
+                    mCallsManager.getLock()));
+        } else {
+            // Otherwise, we should verify call unhold succeeded for focus call.
+            mCallsManager.requestActionUnholdCall(call, activeCallId);
+        }
     }
 
     public CompletableFuture<Boolean> makeRoomForOutgoingCall(boolean isEmergency, Call call) {
-        // Todo: call sequencing logic (stubbed)
-        return CompletableFuture.completedFuture(true);
-//        return isEmergency ? makeRoomForOutgoingEmergencyCall(call) : makeRoomForOutgoingCall(call);
+        return isEmergency
+                ? makeRoomForOutgoingEmergencyCall(call)
+                : makeRoomForOutgoingCall(call);
     }
 
-//    private CompletableFuture<Boolean> makeRoomForOutgoingEmergencyCall(Call emergencyCall) {
-//        // Todo: call sequencing logic (stubbed)
-//        return CompletableFuture.completedFuture(true);
-//    }
+    /**
+     * This function tries to make room for the new emergency outgoing call via call sequencing.
+     * The resulting future is an indication of whether room was able to be made for the emergency
+     * call if needed.
+     * @param emergencyCall The outgoing emergency call to be placed.
+     * @return The {@code CompletableFuture} indicating the result of whether room was able to be
+     *         made for the emergency call.
+     */
+    private CompletableFuture<Boolean> makeRoomForOutgoingEmergencyCall(Call emergencyCall) {
+        // Always disconnect any ringing/incoming calls when an emergency call is placed to minimize
+        // distraction. This does not affect live call count.
+        CompletableFuture<Boolean> ringingCallFuture = null;
+        Call ringingCall = null;
+        if (mCallsManager.hasRingingOrSimulatedRingingCall()) {
+            ringingCall = mCallsManager.getRingingOrSimulatedRingingCall();
+            ringingCall.getAnalytics().setCallIsAdditional(true);
+            ringingCall.getAnalytics().setCallIsInterrupted(true);
+            if (ringingCall.getState() == CallState.SIMULATED_RINGING) {
+                if (!ringingCall.hasGoneActiveBefore()) {
+                    // If this is an incoming call that is currently in SIMULATED_RINGING only
+                    // after a call screen, disconnect to make room and mark as missed, since
+                    // the user didn't get a chance to accept/reject.
+                    ringingCallFuture = ringingCall.disconnect("emergency call dialed during "
+                            + "simulated ringing after screen.");
+                } else {
+                    // If this is a simulated ringing call after being active and put in
+                    // AUDIO_PROCESSING state again, disconnect normally.
+                    ringingCallFuture = ringingCall.reject(false, null, "emergency call dialed "
+                            + "during simulated ringing.");
+                }
+            } else { // normal incoming ringing call.
+                // Hang up the ringing call to make room for the emergency call and mark as missed,
+                // since the user did not reject.
+                ringingCall.setOverrideDisconnectCauseCode(
+                        new DisconnectCause(DisconnectCause.MISSED));
+                ringingCallFuture = ringingCall.reject(false, null, "emergency call dialed "
+                        + "during ringing.");
+            }
+        }
 
-//    private CompletableFuture<Boolean> makeRoomForOutgoingCall(Call call) {
-//        // Todo: call sequencing logic (stubbed)
-//        return CompletableFuture.completedFuture(true);
-//    }
+        // There is already room!
+        if (!mCallsManager.hasMaximumLiveCalls(emergencyCall)) {
+            return CompletableFuture.completedFuture(true);
+        }
 
-//    private void resetProcessingCallSequencing() {
-//        mTransactionManager.setProcessingCallSequencing(false);
-//    }
+        Call liveCall = mCallsManager.getFirstCallWithLiveState();
+        Log.i(this, "makeRoomForOutgoingEmergencyCall: call = " + emergencyCall
+                + " livecall = " + liveCall);
 
-    public CompletableFuture<Boolean> disconnectCall() {
-        return CompletableFuture.completedFuture(true);
+        if (emergencyCall == liveCall) {
+            // Not likely, but a good correctness check.
+            return CompletableFuture.completedFuture(true);
+        }
+
+        if (mCallsManager.hasMaximumOutgoingCalls(emergencyCall)) {
+            Call outgoingCall = mCallsManager.getFirstCallWithState(OUTGOING_CALL_STATES);
+            String disconnectReason = null;
+            if (!outgoingCall.isEmergencyCall()) {
+                emergencyCall.getAnalytics().setCallIsAdditional(true);
+                outgoingCall.getAnalytics().setCallIsInterrupted(true);
+                disconnectReason = "Disconnecting dialing call in favor of new dialing"
+                        + " emergency call.";
+            }
+            if (outgoingCall.getState() == CallState.SELECT_PHONE_ACCOUNT) {
+                // Correctness check: if there is an orphaned emergency call in the
+                // {@link CallState#SELECT_PHONE_ACCOUNT} state, just disconnect it since the user
+                // has explicitly started a new call.
+                emergencyCall.getAnalytics().setCallIsAdditional(true);
+                outgoingCall.getAnalytics().setCallIsInterrupted(true);
+                disconnectReason = "Disconnecting call in SELECT_PHONE_ACCOUNT in favor"
+                        + " of new outgoing call.";
+            }
+            if (disconnectReason != null) {
+                boolean isSequencingRequiredRingingAndOutgoing = !arePhoneAccountsSame(
+                        ringingCall, outgoingCall);
+                if (ringingCallFuture != null && isSequencingRequiredRingingAndOutgoing) {
+                    String finalDisconnectReason = disconnectReason;
+                    return ringingCallFuture.thenComposeAsync((result) -> {
+                        if (result) {
+                            Log.i(this, "makeRoomForOutgoingEmergencyCall: Request to disconnect"
+                                    + " ringing call succeeded. Attempting to disconnect "
+                                    + "outgoing call.");
+                            return outgoingCall.disconnect(finalDisconnectReason);
+                        } else {
+                            Log.i(this, "makeRoomForOutgoingEmergencyCall: Request to disconnect"
+                                    + "ringing call failed. Aborting attempt to disconnect "
+                                    + "outgoing call");
+                            return CompletableFuture.completedFuture(false);
+                        }
+                    }, new LoggedHandlerExecutor(mHandler, "CSC.mRFOEC",
+                            mCallsManager.getLock()));
+                } else {
+                    return outgoingCall.disconnect(disconnectReason);
+                }
+            }
+            //  If the user tries to make two outgoing calls to different emergency call numbers,
+            //  we will try to connect the first outgoing call and reject the second.
+            emergencyCall.setStartFailCause(CallFailureCause.IN_EMERGENCY_CALL);
+            return CompletableFuture.completedFuture(false);
+        }
+
+        boolean isSequencingRequiredRingingAndLive = ringingCall != null
+                && !arePhoneAccountsSame(ringingCall, liveCall);
+        if (liveCall.getState() == CallState.AUDIO_PROCESSING) {
+            emergencyCall.getAnalytics().setCallIsAdditional(true);
+            liveCall.getAnalytics().setCallIsInterrupted(true);
+            final String disconnectReason = "disconnecting audio processing call for emergency";
+            if (ringingCallFuture != null && isSequencingRequiredRingingAndLive) {
+                return ringingCallFuture.thenComposeAsync((result) -> {
+                    if (result) {
+                        Log.i(this, "makeRoomForOutgoingEmergencyCall: Request to disconnect "
+                                + "ringing call succeeded. Attempting to disconnect live call.");
+                        return liveCall.disconnect(disconnectReason);
+                    } else {
+                        Log.i(this, "makeRoomForOutgoingEmergencyCall: Request to disconnect "
+                                + "ringing call failed. Aborting attempt to disconnect live call.");
+                        return CompletableFuture.completedFuture(false);
+                    }
+                }, new LoggedHandlerExecutor(mHandler, "CSC.mRFOEC",
+                        mCallsManager.getLock()));
+            } else {
+                return liveCall.disconnect(disconnectReason);
+            }
+        }
+
+        // If the live call is stuck in a connecting state, prompt the user to generate a bugreport.
+        if (liveCall.getState() == CallState.CONNECTING) {
+            AnomalyReporter.reportAnomaly(LIVE_CALL_STUCK_CONNECTING_EMERGENCY_ERROR_UUID,
+                    LIVE_CALL_STUCK_CONNECTING_EMERGENCY_ERROR_MSG);
+        }
+
+        // If we have the max number of held managed calls and we're placing an emergency call,
+        // we'll disconnect the ongoing call if it cannot be held. If we have a self-managed call
+        // that can't be held, then we should disconnect the call in favor of the emergency call.
+        // Likewise, if there's only one active managed call which can't be held, then it should
+        // also be disconnected.
+        if (mCallsManager.hasMaximumManagedHoldingCalls(emergencyCall)
+                || !mCallsManager.canHold(liveCall)) {
+            emergencyCall.getAnalytics().setCallIsAdditional(true);
+            liveCall.getAnalytics().setCallIsInterrupted(true);
+            // Disconnect the active call instead of the holding call because it is historically
+            // easier to do, rather than disconnect a held call.
+            final String disconnectReason = "disconnecting to make room for emergency call "
+                    + emergencyCall.getId();
+            if (ringingCallFuture != null && isSequencingRequiredRingingAndLive) {
+                return ringingCallFuture.thenComposeAsync((result) -> {
+                    if (result) {
+                        Log.i(this, "makeRoomForOutgoingEmergencyCall: Request to disconnect "
+                                + "ringing call succeeded. Attempting to disconnect live call.");
+                        return liveCall.disconnect(disconnectReason);
+                    } else {
+                        Log.i(this, "makeRoomForOutgoingEmergencyCall: Request to disconnect "
+                                + "ringing call failed. Aborting attempt to disconnect live call.");
+                        return CompletableFuture.completedFuture(false);
+                    }
+                }, new LoggedHandlerExecutor(mHandler, "CSC.mRFOEC",
+                        mCallsManager.getLock()));
+            } else {
+                return liveCall.disconnect(disconnectReason);
+            }
+        }
+
+        // TODO: Remove once b/23035408 has been corrected.
+        // If the live call is a conference, it will not have a target phone account set.  This
+        // means the check to see if the live call has the same target phone account as the new
+        // call will not cause us to bail early.  As a result, we'll end up holding the
+        // ongoing conference call.  However, the ConnectionService is already doing that.  This
+        // has caused problems with some carriers.  As a workaround until b/23035408 is
+        // corrected, we will try and get the target phone account for one of the conference's
+        // children and use that instead.
+        PhoneAccountHandle liveCallPhoneAccount = liveCall.getTargetPhoneAccount();
+        if (liveCallPhoneAccount == null && liveCall.isConference() &&
+                !liveCall.getChildCalls().isEmpty()) {
+            liveCallPhoneAccount = mCallsManager.getFirstChildPhoneAccount(liveCall);
+            Log.i(this, "makeRoomForOutgoingEmergencyCall: using child call PhoneAccount = " +
+                    liveCallPhoneAccount);
+        }
+
+        // We may not know which PhoneAccount the emergency call will be placed on yet, but if
+        // the liveCall PhoneAccount does not support placing emergency calls, then we know it
+        // will not be that one and we do not want multiple PhoneAccounts active during an
+        // emergency call if possible. Disconnect the active call in favor of the emergency call
+        // instead of trying to hold.
+        if (liveCallPhoneAccount != null) {
+            PhoneAccount pa = mCallsManager.getPhoneAccountRegistrar().getPhoneAccountUnchecked(
+                    liveCallPhoneAccount);
+            if((pa.getCapabilities() & PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS) == 0) {
+                liveCall.setOverrideDisconnectCauseCode(new DisconnectCause(
+                        DisconnectCause.LOCAL, DisconnectCause.REASON_EMERGENCY_CALL_PLACED));
+                final String disconnectReason = "outgoing call does not support emergency calls, "
+                        + "disconnecting.";
+                if (ringingCallFuture != null && isSequencingRequiredRingingAndLive) {
+                    return ringingCallFuture.thenComposeAsync((result) -> {
+                        if (result) {
+                            Log.i(this, "makeRoomForOutgoingEmergencyCall: Request to disconnect "
+                                    + "ringing call succeeded. "
+                                    + "Attempting to disconnect live call.");
+                            return liveCall.disconnect(disconnectReason);
+                        } else {
+                            Log.i(this, "makeRoomForOutgoingEmergencyCall: Request to disconnect "
+                                    + "ringing call failed. "
+                                    + "Aborting attempt to disconnect live call.");
+                            return CompletableFuture.completedFuture(false);
+                        }
+                    }, new LoggedHandlerExecutor(mHandler, "CSC.mRFOEC",
+                            mCallsManager.getLock()));
+                } else {
+                    return liveCall.disconnect(disconnectReason);
+                }
+            }
+        }
+
+        // If we are trying to make an emergency call with the same package name as
+        // the live call, then attempt to hold the call if the carrier config supports holding
+        // emergency calls. Otherwise, disconnect the live call in order to make room for the
+        // emergency call.
+        if (PhoneAccountHandle.areFromSamePackage(liveCallPhoneAccount,
+                emergencyCall.getTargetPhoneAccount())) {
+            Log.i(this, "makeRoomForOutgoingEmergencyCall: phoneAccounts are from same "
+                    + "package. Attempting to hold live call before placing emergency call.");
+            return maybeHoldLiveCallForEmergency(ringingCallFuture,
+                    isSequencingRequiredRingingAndLive, liveCall, emergencyCall,
+                    shouldHoldForEmergencyCall(liveCallPhoneAccount) /* shouldHoldForEmergency */);
+        } else if (emergencyCall.getTargetPhoneAccount() == null) {
+            // Without a phone account, we can't say reliably that the call will fail.
+            // If the user chooses the same phone account as the live call, then it's
+            // still possible that the call can be made (like with CDMA calls not supporting
+            // hold but they still support adding a call by going immediately into conference
+            // mode). Return true here and we'll run this code again after user chooses an
+            // account.
+            return CompletableFuture.completedFuture(true);
+        }
+
+        // Hold the live call if possible before attempting the new outgoing emergency call.
+        if (mCallsManager.canHold(liveCall)) {
+            Log.i(this, "makeRoomForOutgoingEmergencyCall: holding live call.");
+            return maybeHoldLiveCallForEmergency(ringingCallFuture,
+                    isSequencingRequiredRingingAndLive, liveCall,
+                    emergencyCall, true /* shouldHoldForEmergency */);
+        }
+
+        // The live call cannot be held so we're out of luck here.  There's no room.
+        emergencyCall.setStartFailCause(CallFailureCause.CANNOT_HOLD_CALL);
+        return CompletableFuture.completedFuture(false);
+    }
+
+    /**
+     * This function tries to make room for the new outgoing call via call sequencing. The
+     * resulting future is an indication of whether room was able to be made for the call if
+     * needed.
+     * @param call The outgoing call to make room for.
+     * @return The {@code CompletableFuture} indicating the result of whether room was able to be
+     *         made for the outgoing call.
+     */
+    private CompletableFuture<Boolean> makeRoomForOutgoingCall(Call call) {
+        // For the purely managed CS cases, check if there's a ringing call, in which case we will
+        // disallow the outgoing call.
+        if (!call.isSelfManaged() && mCallsManager.hasManagedRingingOrSimulatedRingingCall()) {
+            showErrorDialogForOutgoingDuringRingingCall(call);
+            return CompletableFuture.completedFuture(false);
+        }
+        // Already room!
+        if (!mCallsManager.hasMaximumLiveCalls(call)) {
+            return CompletableFuture.completedFuture(true);
+        }
+
+        // NOTE: If the amount of live calls changes beyond 1, this logic will probably
+        // have to change.
+        Call liveCall = mCallsManager.getFirstCallWithLiveState();
+        Log.i(this, "makeRoomForOutgoingCall call = " + call + " livecall = " +
+                liveCall);
+
+        if (call == liveCall) {
+            // If the call is already the foreground call, then we are golden.
+            // This can happen after the user selects an account in the SELECT_PHONE_ACCOUNT
+            // state since the call was already populated into the list.
+            return CompletableFuture.completedFuture(true);
+        }
+
+        // If the live call is stuck in a connecting state for longer than the transitory timeout,
+        // then we should disconnect it in favor of the new outgoing call and prompt the user to
+        // generate a bugreport.
+        // TODO: In the future we should let the CallAnomalyWatchDog do this disconnection of the
+        // live call stuck in the connecting state.  Unfortunately that code will get tripped up by
+        // calls that have a longer than expected new outgoing call broadcast response time.  This
+        // mitigation is intended to catch calls stuck in a CONNECTING state for a long time that
+        // block outgoing calls.  However, if the user dials two calls in quick succession it will
+        // result in both calls getting disconnected, which is not optimal.
+        if (liveCall.getState() == CallState.CONNECTING
+                && ((mClockProxy.elapsedRealtime() - liveCall.getCreationElapsedRealtimeMillis())
+                > mTimeoutsAdapter.getNonVoipCallTransitoryStateTimeoutMillis())) {
+            if (mFeatureFlags.telecomMetricsSupport()) {
+                mMetricsController.getErrorStats().log(ErrorStats.SUB_CALL_MANAGER,
+                        ErrorStats.ERROR_STUCK_CONNECTING);
+            }
+            mAnomalyReporter.reportAnomaly(LIVE_CALL_STUCK_CONNECTING_ERROR_UUID,
+                    LIVE_CALL_STUCK_CONNECTING_ERROR_MSG);
+            return liveCall.disconnect("Force disconnect CONNECTING call.");
+        }
+
+        if (mCallsManager.hasMaximumOutgoingCalls(call)) {
+            Call outgoingCall = mCallsManager.getFirstCallWithState(OUTGOING_CALL_STATES);
+            if (outgoingCall.getState() == CallState.SELECT_PHONE_ACCOUNT) {
+                // If there is an orphaned call in the {@link CallState#SELECT_PHONE_ACCOUNT}
+                // state, just disconnect it since the user has explicitly started a new call.
+                call.getAnalytics().setCallIsAdditional(true);
+                outgoingCall.getAnalytics().setCallIsInterrupted(true);
+                return outgoingCall.disconnect(
+                        "Disconnecting call in SELECT_PHONE_ACCOUNT in favor of new "
+                                + "outgoing call.");
+            }
+            showErrorDialogForMaxOutgoingCall(call);
+            return CompletableFuture.completedFuture(false);
+        }
+
+        // Early check to see if we already have a held call + live call. It's possible if a device
+        // switches to DSDS with two ongoing calls for the phone account to be null in which case
+        // we will return true from this method and report a different failure cause instead.
+        if (mCallsManager.hasMaximumManagedHoldingCalls(call) && !mCallsManager.canHold(liveCall)) {
+            showErrorDialogForMaxOutgoingCall(call);
+            return CompletableFuture.completedFuture(false);
+        }
+
+        // Self-Managed + Transactional calls require Telecom to manage calls in the same
+        // PhoneAccount, whereas managed calls require the ConnectionService to manage calls in the
+        // same PhoneAccount for legacy reasons (Telephony).
+        if (arePhoneAccountsSame(call, liveCall) && !call.isSelfManaged()) {
+            Log.i(this, "makeRoomForOutgoingCall: allowing managed CS to handle "
+                    + "calls from the same self-managed account");
+            return CompletableFuture.completedFuture(true);
+        } else if (call.getTargetPhoneAccount() == null) {
+            Log.i(this, "makeRoomForOutgoingCall: no PA specified, allowing");
+            // Without a phone account, we can't say reliably that the call will fail.
+            // If the user chooses the same phone account as the live call, then it's
+            // still possible that the call can be made (like with CDMA calls not supporting
+            // hold but they still support adding a call by going immediately into conference
+            // mode). Return true here and we'll run this code again after user chooses an
+            // account.
+            return CompletableFuture.completedFuture(true);
+        }
+
+        // Try to hold the live call before attempting the new outgoing call.
+        if (mCallsManager.canHold(liveCall)) {
+            Log.i(this, "makeRoomForOutgoingCall: holding live call.");
+            call.getAnalytics().setCallIsAdditional(true);
+            liveCall.getAnalytics().setCallIsInterrupted(true);
+            return liveCall.hold("calling " + call.getId());
+        }
+
+        // The live call cannot be held so we're out of luck here.  There's no room.
+        showErrorDialogForCannotHoldCall(call, true);
+        return CompletableFuture.completedFuture(false);
+    }
+
+    /**
+     * Processes the request from the app to disconnect a call. This is done via call sequencing
+     * so that Telecom properly cleans up the call locally provided that the call has been
+     * properly disconnected on the connection side.
+     * @param call The call to disconnect.
+     * @param previousState The previous state of the call before disconnecting.
+     */
+    public void disconnectCall(Call call, int previousState) {
+        CompletableFuture<Boolean> disconnectFuture = call.disconnect();
+        disconnectFuture.thenComposeAsync((result) -> {
+            if (result) {
+                Log.i(this, "disconnectCall: Disconnect call transaction succeeded. "
+                        + "Processing associated cleanup.");
+                mCallsManager.processDisconnectCallAndCleanup(call, previousState);
+            } else {
+                Log.i(this, "disconnectCall: Disconnect call transaction failed. "
+                        + "Aborting associated cleanup.");
+            }
+            return CompletableFuture.completedFuture(false);
+        }, new LoggedHandlerExecutor(mHandler, "CSC.dC",
+                mCallsManager.getLock()));
+    }
+
+    /* HELPERS */
+
+    /* makeRoomForOutgoingEmergencyCall helpers */
+
+    private CompletableFuture<Boolean> maybeHoldLiveCallForEmergency(
+            CompletableFuture<Boolean> ringingCallFuture, boolean isSequencingRequired,
+            Call liveCall, Call emergencyCall, boolean shouldHoldForEmergency) {
+        emergencyCall.getAnalytics().setCallIsAdditional(true);
+        liveCall.getAnalytics().setCallIsInterrupted(true);
+        final String holdReason = "calling " + emergencyCall.getId();
+        CompletableFuture<Boolean> holdResultFuture = CompletableFuture.completedFuture(false);
+        if (shouldHoldForEmergency) {
+            if (ringingCallFuture != null && isSequencingRequired) {
+                holdResultFuture = ringingCallFuture.thenComposeAsync((result) -> {
+                    if (result) {
+                        Log.i(this, "makeRoomForOutgoingEmergencyCall: Request to disconnect "
+                                + "ringing call succeeded. Attempting to hold live call.");
+                        return liveCall.hold(holdReason);
+                    } else {
+                        Log.i(this, "makeRoomForOutgoingEmergencyCall: Request to disconnect "
+                                + "ringing call failed. Aborting attempt to hold live call.");
+                        return CompletableFuture.completedFuture(false);
+                    }
+                }, new LoggedHandlerExecutor(mHandler, "CSC.mRFOEC",
+                        mCallsManager.getLock()));
+            } else {
+                holdResultFuture = liveCall.hold(holdReason);
+            }
+        }
+        return holdResultFuture.thenComposeAsync((result) -> {
+            if (!result) {
+                Log.i(this, "makeRoomForOutgoingEmergencyCall: Attempt to hold live call "
+                        + "failed. Disconnecting live call in favor of emergency call.");
+                return liveCall.disconnect("Disconnecting live call which failed to be held");
+            } else {
+                Log.i(this, "makeRoomForOutgoingEmergencyCall: Attempt to hold live call "
+                        + "transaction succeeded.");
+                emergencyCall.increaseHeldByThisCallCount();
+                return CompletableFuture.completedFuture(true);
+            }
+        }, new LoggedHandlerExecutor(mHandler, "CSC.mRFOEC", mCallsManager.getLock()));
+    }
+
+    /**
+     * Checks the carrier config to see if the carrier supports holding emergency calls.
+     * @param handle The {@code PhoneAccountHandle} to check
+     * @return {@code true} if the carrier supports holding emergency calls, {@code} false
+     *         otherwise.
+     */
+    private boolean shouldHoldForEmergencyCall(PhoneAccountHandle handle) {
+        return mCallsManager.getCarrierConfigForPhoneAccount(handle).getBoolean(
+                CarrierConfigManager.KEY_ALLOW_HOLD_CALL_DURING_EMERGENCY_BOOL, true);
+    }
+
+    @VisibleForTesting
+    public boolean arePhoneAccountsSame(Call call1, Call call2) {
+        if (call1 == null || call2 == null) {
+            return false;
+        }
+        return Objects.equals(call1.getTargetPhoneAccount(), call2.getTargetPhoneAccount());
+    }
+
+    /**
+     * Checks to see if two calls can be swapped. This is granted that the call to be unheld is
+     * already ON_HOLD and the active call supports holding. Note that in HoldTracker, there can
+     * only be one top call that is holdable (if there are two, the calls are not holdable) and only
+     * that connection would have the CAPABILITY_HOLD present. For swapping logic, we should take
+     * this into account and request to hold regardless.
+     */
+    @VisibleForTesting
+    private boolean canSwap(Call callToBeHeld, Call callToUnhold) {
+        return callToBeHeld.can(Connection.CAPABILITY_SUPPORT_HOLD)
+                && callToBeHeld.getState() != CallState.DIALING
+                && callToUnhold.getState() == CallState.ON_HOLD;
+    }
+
+    private CompletableFuture<Boolean> disconnectAllCallsWithPhoneAccount(
+            PhoneAccountHandle handle) {
+        CompletableFuture<Boolean> disconnectFuture = CompletableFuture.completedFuture(true);
+        List<Call> calls = mCallsManager.getCalls().stream()
+                .filter(c -> c.getTargetPhoneAccount().equals(handle)).toList();
+        for (Call call: calls) {
+            // Wait for all disconnects before we accept the new call.
+            disconnectFuture = disconnectFuture.thenComposeAsync((result) -> {
+                if (!result) {
+                    Log.i(this, "disconnectAllCallsWithPhoneAccount: "
+                            + "Failed to disconnect %s.", call);
+                }
+                return call.disconnect("Un-holdable call " + call + " disconnected "
+                        + "in favor of new call.");
+            }, new LoggedHandlerExecutor(mHandler, "CSC.dACWPA", mCallsManager.getLock()));
+        }
+        return disconnectFuture;
+    }
+
+    /**
+     * Generic helper to log the result of the {@link CompletableFuture} containing the transactions
+     * that are being processed in the context of call sequencing.
+     * @param future The {@link CompletableFuture} encompassing the transaction that's being
+     *               computed.
+     * @param methodName The method name to describe the type of transaction being processed.
+     * @param sessionName The session name to identify the log.
+     * @param successMsg The message to be logged if the transaction succeeds.
+     * @param failureMsg The message to be logged if the transaction fails.
+     */
+    public void logFutureResultTransaction(CompletableFuture<Boolean> future, String methodName,
+            String sessionName, String successMsg, String failureMsg) {
+        future.thenApplyAsync((result) -> {
+            String msg = methodName + ": " + (result ? successMsg : failureMsg);
+            Log.i(this, msg);
+            return CompletableFuture.completedFuture(result);
+        }, new LoggedHandlerExecutor(mHandler, sessionName, mCallsManager.getLock()));
+    }
+
+    public boolean hasMmiCodeRestriction(Call call) {
+        if (mCallsManager.getNumCallsWithStateWithoutHandle(
+                CALL_FILTER_ALL, call, call.getTargetPhoneAccount(), ONGOING_CALL_STATES) > 0) {
+            // Set disconnect cause so that error will be printed out when call is disconnected.
+            CharSequence msg = mContext.getText(R.string.callFailed_reject_mmi);
+            call.setOverrideDisconnectCauseCode(new DisconnectCause(DisconnectCause.ERROR, msg, msg,
+                    "Rejected MMI code due to an ongoing call on another phone account."));
+            return true;
+        }
+        return false;
+    }
+
+    public void maybeAddAnsweringCallDropsFg(Call activeCall, Call incomingCall) {
+        // Don't set the extra when we have an incoming self-managed call that would potentially
+        // disconnect the active managed call.
+        if (activeCall == null || (incomingCall.isSelfManaged() && !activeCall.isSelfManaged())) {
+            return;
+        }
+        // Check if the active call doesn't support hold. If it doesn't we should indicate to the
+        // user via the EXTRA_ANSWERING_DROPS_FG_CALL extra that the call would be dropped by
+        // answering the incoming call.
+        if (!mCallsManager.supportsHold(activeCall)) {
+            CharSequence droppedApp = activeCall.getTargetPhoneAccountLabel();
+            Bundle dropCallExtras = new Bundle();
+            dropCallExtras.putBoolean(Connection.EXTRA_ANSWERING_DROPS_FG_CALL, true);
+
+            // Include the name of the app which will drop the call.
+            dropCallExtras.putCharSequence(
+                    Connection.EXTRA_ANSWERING_DROPS_FG_CALL_APP_NAME, droppedApp);
+            Log.i(this, "Incoming call will drop %s call.", droppedApp);
+            incomingCall.putConnectionServiceExtras(dropCallExtras);
+        }
+    }
+
+    private void showErrorDialogForMaxOutgoingCall(Call call) {
+        int resourceId = R.string.callFailed_too_many_calls;
+        String reason = " there are two calls already in progress. Disconnect one of the calls "
+                + "or merge the calls.";
+        showErrorDialogForFailedCall(call, CallFailureCause.MAX_OUTGOING_CALLS, resourceId, reason);
+    }
+
+    private void showErrorDialogForOutgoingDuringRingingCall(Call call) {
+        int resourceId = R.string.callFailed_already_ringing;
+        String reason = " can't place outgoing call with an unanswered incoming call.";
+        showErrorDialogForFailedCall(call, null, resourceId, reason);
+    }
+
+    private void showErrorDialogForCannotHoldCall(Call call, boolean setCallFailure) {
+        CallFailureCause cause = null;
+        if (setCallFailure) {
+            cause = CallFailureCause.CANNOT_HOLD_CALL;
+        }
+        int resourceId = R.string.callFailed_unholdable_call;
+        String reason = " unable to hold live call. Disconnect the unholdable call.";
+        showErrorDialogForFailedCall(call, cause, resourceId, reason);
+    }
+
+    private void showErrorDialogForFailedCall(Call call, CallFailureCause cause, int resourceId,
+            String reason) {
+        if (cause != null) {
+            call.setStartFailCause(cause);
+        }
+        showErrorDialogForRestrictedOutgoingCall(mContext, resourceId, TAG, reason);
+    }
+
+    public Handler getHandler() {
+        return mHandler;
     }
 }
diff --git a/src/com/android/server/telecom/callsequencing/CallsManagerCallSequencingAdapter.java b/src/com/android/server/telecom/callsequencing/CallsManagerCallSequencingAdapter.java
index 8410c54..66ff42b 100644
--- a/src/com/android/server/telecom/callsequencing/CallsManagerCallSequencingAdapter.java
+++ b/src/com/android/server/telecom/callsequencing/CallsManagerCallSequencingAdapter.java
@@ -16,8 +16,26 @@
 
 package com.android.server.telecom.callsequencing;
 
+import static com.android.server.telecom.CallsManager.CALL_FILTER_ALL;
+import static com.android.server.telecom.CallsManager.ONGOING_CALL_STATES;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.OutcomeReceiver;
+import android.telecom.CallAttributes;
+import android.telecom.CallException;
+import android.telecom.Connection;
+import android.telecom.DisconnectCause;
+import android.telecom.Log;
+
 import com.android.server.telecom.Call;
+import com.android.server.telecom.CallAudioManager;
+import com.android.server.telecom.CallState;
 import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.callsequencing.voip.OutgoingCallTransaction;
+import com.android.server.telecom.flags.FeatureFlags;
+import com.android.server.telecom.R;
 
 import java.util.concurrent.CompletableFuture;
 
@@ -28,17 +46,31 @@
 public class CallsManagerCallSequencingAdapter {
 
     private final CallsManager mCallsManager;
+    private final Context mContext;
     private final CallSequencingController mSequencingController;
+    private final CallAudioManager mCallAudioManager;
+    private final Handler mHandler;
+    private final FeatureFlags mFeatureFlags;
     private final boolean mIsCallSequencingEnabled;
 
-    public CallsManagerCallSequencingAdapter(CallsManager callsManager,
-            CallSequencingController sequencingController,
-            boolean isCallSequencingEnabled) {
+    public CallsManagerCallSequencingAdapter(CallsManager callsManager, Context context,
+            CallSequencingController sequencingController, CallAudioManager callAudioManager,
+            FeatureFlags featureFlags) {
         mCallsManager = callsManager;
+        mContext = context;
         mSequencingController = sequencingController;
-        mIsCallSequencingEnabled = isCallSequencingEnabled;
+        mCallAudioManager = callAudioManager;
+        mHandler = sequencingController.getHandler();
+        mFeatureFlags = featureFlags;
+        mIsCallSequencingEnabled = featureFlags.enableCallSequencing();
     }
 
+    /**
+     * Conditionally try to answer the call depending on whether call sequencing
+     * (mIsCallSequencingEnabled) is enabled.
+     * @param incomingCall The incoming call that should be answered.
+     * @param videoState The video state configuration associated with the call.
+     */
     public void answerCall(Call incomingCall, int videoState) {
         if (mIsCallSequencingEnabled && !incomingCall.isTransactionalCall()) {
             mSequencingController.answerCall(incomingCall, videoState);
@@ -47,6 +79,11 @@
         }
     }
 
+    /**
+     * Conditionally attempt to unhold the provided call depending on whether call sequencing
+     * (mIsCallSequencingEnabled) is enabled.
+     * @param call The call to unhold.
+     */
     public void unholdCall(Call call) {
         if (mIsCallSequencingEnabled) {
             mSequencingController.unholdCall(call);
@@ -55,34 +92,224 @@
         }
     }
 
+    /**
+     * Conditionally attempt to hold the provided call depending on whether call sequencing
+     * (mIsCallSequencingEnabled) is enabled.
+     * @param call The call to hold.
+     */
     public void holdCall(Call call) {
         // Sequencing already taken care of for CSW/TSW in Call class.
-        call.hold();
+        CompletableFuture<Boolean> holdFuture = call.hold();
+        maybeLogFutureResultTransaction(holdFuture, "holdCall", "CMCSA.hC",
+                "hold call transaction succeeded.", "hold call transaction failed.");
     }
 
-    public void unholdCallForRemoval(Call removedCall,
-            boolean isLocallyDisconnecting) {
-        // Todo: confirm verification of disconnect logic
-        // Sequencing already taken care of for CSW/TSW in Call class.
-        mCallsManager.maybeMoveHeldCallToForeground(removedCall, isLocallyDisconnecting);
+    /**
+     * Conditionally disconnect the provided call depending on whether call sequencing
+     * (mIsCallSequencingEnabled) is enabled. The sequencing functionality ensures that we wait for
+     * the call to be disconnected as signalled by CSW/TSW as to ensure that subsequent call
+     * operations don't overlap with this one.
+     * @param call The call to disconnect.
+     */
+    public void disconnectCall(Call call) {
+        int previousState = call.getState();
+        if (mIsCallSequencingEnabled) {
+            mSequencingController.disconnectCall(call, previousState);
+        } else {
+            mCallsManager.disconnectCallOld(call, previousState);
+        }
     }
 
+    /**
+     * Conditionally make room for the outgoing call depending on whether call sequencing
+     * (mIsCallSequencingEnabled) is enabled.
+     * @param isEmergency Indicator of whether the call is an emergency call.
+     * @param call The call to potentially make room for.
+     * @return {@link CompletableFuture} which will contain the result of the transaction if room
+     *         was able to made for the call.
+     */
     public CompletableFuture<Boolean> makeRoomForOutgoingCall(boolean isEmergency, Call call) {
         if (mIsCallSequencingEnabled) {
             return mSequencingController.makeRoomForOutgoingCall(isEmergency, call);
         } else {
             return isEmergency
                     ? CompletableFuture.completedFuture(
-                            makeRoomForOutgoingEmergencyCallFlagOff(call))
-                    : CompletableFuture.completedFuture(makeRoomForOutgoingCallFlagOff(call));
+                            mCallsManager.makeRoomForOutgoingEmergencyCall(call))
+                    : CompletableFuture.completedFuture(
+                            mCallsManager.makeRoomForOutgoingCall(call));
         }
     }
 
-    private boolean makeRoomForOutgoingCallFlagOff(Call call) {
-        return mCallsManager.makeRoomForOutgoingCall(call);
+    /**
+     * Attempts to mark the self-managed call as active by first holding the active call and then
+     * requesting call focus for the self-managed call.
+     * @param call The self-managed call to set active
+     */
+    public void markCallAsActiveSelfManagedCall(Call call) {
+        if (mIsCallSequencingEnabled) {
+            mSequencingController.handleSetSelfManagedCallActive(call);
+        } else {
+            mCallsManager.holdActiveCallForNewCall(call);
+            mCallsManager.requestActionSetActiveCall(call,
+                    "active set explicitly for self-managed");
+        }
     }
 
-    private boolean makeRoomForOutgoingEmergencyCallFlagOff(Call call) {
-        return mCallsManager.makeRoomForOutgoingEmergencyCall(call);
+    /**
+     * Helps create the transaction representing the outgoing transactional call. For outgoing
+     * calls, there can be more than one transaction that will need to complete when
+     * mIsCallSequencingEnabled is true. Otherwise, rely on the old behavior of creating an
+     * {@link OutgoingCallTransaction}.
+     * @param callAttributes The call attributes associated with the call.
+     * @param extras The extras that are associated with the call.
+     * @param callingPackage The calling package representing where the request was invoked from.
+     * @return The {@link CompletableFuture<CallTransaction>} that encompasses the request to
+     *         place/receive the transactional call.
+     */
+    public CompletableFuture<CallTransaction> createTransactionalOutgoingCall(String callId,
+            CallAttributes callAttributes, Bundle extras, String callingPackage) {
+        return mIsCallSequencingEnabled
+                ? mSequencingController.createTransactionalOutgoingCall(callId,
+                callAttributes, extras, callingPackage)
+                : CompletableFuture.completedFuture(new OutgoingCallTransaction(callId,
+                        mCallsManager.getContext(), callAttributes, mCallsManager, extras,
+                        mFeatureFlags));
+    }
+
+    /**
+     * attempt to hold or swap the current active call in favor of a new call request. The
+     * OutcomeReceiver will return onResult if the current active call is held or disconnected.
+     * Otherwise, the OutcomeReceiver will fail.
+     * @param newCall The new (transactional) call that's waiting to go active.
+     * @param isCallControlRequest Indication of whether this is a call control request.
+     * @param callback The callback to report the result of the aforementioned hold
+     *      transaction.
+     */
+    public void transactionHoldPotentialActiveCallForNewCall(Call newCall,
+            boolean isCallControlRequest, OutcomeReceiver<Boolean, CallException> callback) {
+        String mTag = "transactionHoldPotentialActiveCallForNewCall: ";
+        Call activeCall = (Call) mCallsManager.getConnectionServiceFocusManager()
+                .getCurrentFocusCall();
+        Log.i(this, mTag + "newCall=[%s], activeCall=[%s]", newCall, activeCall);
+
+        if (activeCall == null || activeCall == newCall) {
+            Log.i(this, mTag + "no need to hold activeCall");
+            callback.onResult(true);
+            return;
+        }
+
+        if (mFeatureFlags.transactionalHoldDisconnectsUnholdable()) {
+            // prevent bad actors from disconnecting the activeCall. Instead, clients will need to
+            // notify the user that they need to disconnect the ongoing call before making the
+            // new call ACTIVE.
+            if (isCallControlRequest
+                    && !mCallsManager.canHoldOrSwapActiveCall(activeCall, newCall)) {
+                Log.i(this, mTag + "CallControlRequest exit");
+                callback.onError(new CallException("activeCall is NOT holdable or swappable, please"
+                        + " request the user disconnect the call.",
+                        CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
+                return;
+            }
+
+            if (mIsCallSequencingEnabled) {
+                mSequencingController.transactionHoldPotentialActiveCallForNewCallSequencing(
+                        newCall, callback);
+            } else {
+                // The code path without sequencing but where transactionalHoldDisconnectsUnholdable
+                // flag is enabled.
+                mCallsManager.transactionHoldPotentialActiveCallForNewCallOld(newCall,
+                        activeCall, callback);
+            }
+        } else {
+            // The unflagged path (aka original code with no flags).
+            mCallsManager.transactionHoldPotentialActiveCallForNewCallUnflagged(activeCall,
+                    newCall, callback);
+        }
+    }
+
+    /**
+     * Attempts to move the held call to the foreground in cases where we need to auto-unhold the
+     * call.
+     */
+    public void maybeMoveHeldCallToForeground(Call removedCall, boolean isLocallyDisconnecting) {
+        CompletableFuture<Boolean> unholdForegroundCallFuture = null;
+        Call foregroundCall = mCallAudioManager.getPossiblyHeldForegroundCall();
+        if (isLocallyDisconnecting) {
+            boolean isDisconnectingChildCall = removedCall.isDisconnectingChildCall();
+            Log.v(this, "maybeMoveHeldCallToForeground: isDisconnectingChildCall = "
+                    + isDisconnectingChildCall + "call -> %s", removedCall);
+            // Auto-unhold the foreground call due to a locally disconnected call, except if the
+            // call which was disconnected is a member of a conference (don't want to auto
+            // un-hold the conference if we remove a member of the conference).
+            // Also, ensure that the call we're removing is from the same ConnectionService as
+            // the one we're removing.  We don't want to auto-unhold between ConnectionService
+            // implementations, especially if one is managed and the other is a VoIP CS.
+            if (!isDisconnectingChildCall && foregroundCall != null
+                    && foregroundCall.getState() == CallState.ON_HOLD
+                    && CallsManager.areFromSameSource(foregroundCall, removedCall)) {
+
+                unholdForegroundCallFuture = foregroundCall.unhold();
+            }
+        } else if (foregroundCall != null &&
+                !foregroundCall.can(Connection.CAPABILITY_SUPPORT_HOLD) &&
+                foregroundCall.getState() == CallState.ON_HOLD) {
+
+            // The new foreground call is on hold, however the carrier does not display the hold
+            // button in the UI.  Therefore, we need to auto unhold the held call since the user
+            // has no means of unholding it themselves.
+            Log.i(this, "maybeMoveHeldCallToForeground: Auto-unholding held foreground call (call "
+                    + "doesn't support hold)");
+            unholdForegroundCallFuture = foregroundCall.unhold();
+        }
+        maybeLogFutureResultTransaction(unholdForegroundCallFuture,
+                "maybeMoveHeldCallToForeground", "CM.mMHCTF",
+                "Successfully unheld the foreground call.",
+                "Failed to unhold the foreground call.");
+    }
+
+    /**
+     * Generic helper to log the result of the {@link CompletableFuture} containing the transactions
+     * that are being processed in the context of call sequencing.
+     * @param future The {@link CompletableFuture} encompassing the transaction that's being
+     *               computed.
+     * @param methodName The method name to describe the type of transaction being processed.
+     * @param sessionName The session name to identify the log.
+     * @param successMsg The message to be logged if the transaction succeeds.
+     * @param failureMsg The message to be logged if the transaction fails.
+     */
+    public void maybeLogFutureResultTransaction(CompletableFuture<Boolean> future,
+            String methodName, String sessionName, String successMsg, String failureMsg) {
+        if (mIsCallSequencingEnabled && future != null) {
+            mSequencingController.logFutureResultTransaction(future, methodName, sessionName,
+                    successMsg, failureMsg);
+        }
+    }
+
+    /**
+     * Determines if we need to add the {@link Connection#EXTRA_ANSWERING_DROPS_FG_CALL} extra to
+     * the incoming connection. This is set if the ongoing calls don't support hold.
+     */
+    public void maybeAddAnsweringCallDropsFg(Call activeCall, Call incomingCall) {
+        if (mIsCallSequencingEnabled) {
+            mSequencingController.maybeAddAnsweringCallDropsFg(activeCall, incomingCall);
+        } else {
+            mCallsManager.maybeAddAnsweringCallDropsFgOld(activeCall, incomingCall);
+        }
+    }
+
+    /**
+     * Tries to see if there are any ongoing calls on another phone account when an MMI code is
+     * detected to determine whether it should be allowed. For DSDA purposes, we will not allow any
+     * MMI codes when there's a call on a different phone account.
+     * @param call The call to ignore and the associated phone account to exclude when getting the
+     *             total call count.
+     * @return {@code true} if the MMI code should be allowed, {@code false} otherwise.
+     */
+    public boolean shouldAllowMmiCode(Call call) {
+        return !mIsCallSequencingEnabled || !mSequencingController.hasMmiCodeRestriction(call);
+    }
+
+    public Handler getHandler() {
+        return mHandler;
     }
 }
diff --git a/src/com/android/server/telecom/callsequencing/TransactionManager.java b/src/com/android/server/telecom/callsequencing/TransactionManager.java
index a3b3828..98d54da 100644
--- a/src/com/android/server/telecom/callsequencing/TransactionManager.java
+++ b/src/com/android/server/telecom/callsequencing/TransactionManager.java
@@ -25,6 +25,8 @@
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.telecom.AnomalyReporterAdapter;
+import com.android.server.telecom.flags.FeatureFlags;
 import com.android.server.telecom.flags.Flags;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
@@ -32,6 +34,7 @@
 import java.util.List;
 import java.util.Locale;
 import java.util.Queue;
+import java.util.UUID;
 import java.util.concurrent.CompletableFuture;
 
 public class TransactionManager {
@@ -43,6 +46,12 @@
     private final Deque<CallTransaction> mCompletedTransactions;
     private CallTransaction mCurrentTransaction;
     private boolean mProcessingCallSequencing;
+    private AnomalyReporterAdapter mAnomalyReporter;
+    private FeatureFlags mFeatureFlags;
+    public static final UUID TRANSACTION_MANAGER_TIMEOUT_UUID =
+            UUID.fromString("9ccce52e-6694-4357-9e5e-516a9531b062");
+    public static final String TRANSACTION_MANAGER_TIMEOUT_MSG =
+            "TransactionManager hit a timeout while processing a transaction";
 
     public interface TransactionCompleteListener {
         void onTransactionCompleted(CallTransactionResult result, String transactionName);
@@ -67,6 +76,14 @@
         return INSTANCE;
     }
 
+    public void setFeatureFlag(FeatureFlags flag){
+       mFeatureFlags = flag;
+    }
+
+    public void setAnomalyReporter(AnomalyReporterAdapter callAnomalyReporter){
+        mAnomalyReporter = callAnomalyReporter;
+    }
+
     @VisibleForTesting
     public static TransactionManager getTestInstance() {
         return new TransactionManager();
@@ -109,6 +126,12 @@
                     receiver.onError(new CallException(transactionName + " timeout",
                             CODE_OPERATION_TIMED_OUT));
                     transactionCompleteFuture.complete(false);
+                    if (mFeatureFlags != null && mAnomalyReporter != null &&
+                            mFeatureFlags.enableCallExceptionAnomReports()) {
+                        mAnomalyReporter.reportAnomaly(
+                                TRANSACTION_MANAGER_TIMEOUT_UUID,
+                                TRANSACTION_MANAGER_TIMEOUT_MSG);
+                    }
                 } catch (Exception e) {
                     Log.e(TAG, String.format("onTransactionTimeout: Notifying transaction "
                             + " %s resulted in an Exception.", transactionName), e);
@@ -169,14 +192,6 @@
         }
     }
 
-    public void setProcessingCallSequencing(boolean processingCallSequencing) {
-        mProcessingCallSequencing = processingCallSequencing;
-    }
-
-    public boolean isProcessingCallSequencing() {
-        return mProcessingCallSequencing;
-    }
-
     /**
      * Called when the dumpsys is created for telecom to capture the current state.
      */
diff --git a/src/com/android/server/telecom/callsequencing/TransactionalCallSequencingAdapter.java b/src/com/android/server/telecom/callsequencing/TransactionalCallSequencingAdapter.java
index 7c8bbe4..4adc8d0 100644
--- a/src/com/android/server/telecom/callsequencing/TransactionalCallSequencingAdapter.java
+++ b/src/com/android/server/telecom/callsequencing/TransactionalCallSequencingAdapter.java
@@ -40,14 +40,13 @@
 public class TransactionalCallSequencingAdapter {
     private final TransactionManager mTransactionManager;
     private final CallsManager mCallsManager;
-//    private final boolean mIsCallSequencingEnabled;
+    private final boolean mIsCallSequencingEnabled;
 
     public TransactionalCallSequencingAdapter(TransactionManager transactionManager,
             CallsManager callsManager, boolean isCallSequencingEnabled) {
         mTransactionManager = transactionManager;
         mCallsManager = callsManager;
-        // TODO implement call sequencing changes
-//        mIsCallSequencingEnabled = isCallSequencingEnabled;
+        mIsCallSequencingEnabled = isCallSequencingEnabled;
     }
 
     /**
@@ -55,7 +54,13 @@
      */
     public void setActive(Call call,
             OutcomeReceiver<CallTransactionResult, CallException> receiver) {
-        setActiveFlagOff(call, receiver);
+        if (mIsCallSequencingEnabled) {
+            createSetActiveTransactionSequencing(call, true /* callControlRequest */, null,
+                    receiver, receiver);
+        } else {
+            mTransactionManager.addTransaction(createSetActiveTransactions(call,
+                    true /* callControlRequest */), receiver);
+        }
     }
 
     /**
@@ -63,7 +68,18 @@
      */
     public void setAnswered(Call call, int newVideoState,
             OutcomeReceiver<CallTransactionResult, CallException> receiver) {
-        setAnsweredFlagOff(call, newVideoState, receiver);
+        boolean isCallControlRequest = true;
+        OutcomeReceiver<CallTransactionResult, CallException> receiverForTransaction =
+                getSetAnswerReceiver(call, null /* foregroundCallBeforeSwap */,
+                        false /* wasForegroundActive */, newVideoState, receiver,
+                        isCallControlRequest);
+        if (mIsCallSequencingEnabled) {
+            createSetActiveTransactionSequencing(call, isCallControlRequest, null,
+                    receiver, receiverForTransaction /* receiverForTransaction */);
+        } else {
+            mTransactionManager.addTransaction(createSetActiveTransactions(call,
+                    isCallControlRequest), receiverForTransaction);
+        }
     }
 
     /**
@@ -71,7 +87,8 @@
      */
     public void setDisconnected(Call call, DisconnectCause dc,
             OutcomeReceiver<CallTransactionResult, CallException> receiver) {
-        setDisconnectedFlagOff(call, dc, receiver);
+        mTransactionManager.addTransaction(
+                new EndCallTransaction(mCallsManager, dc, call), receiver);
     }
 
     /**
@@ -79,7 +96,7 @@
      */
     public void setInactive(Call call,
             OutcomeReceiver<CallTransactionResult, CallException> receiver) {
-        setInactiveFlagOff(call, receiver);
+        mTransactionManager.addTransaction(new HoldCallTransaction(mCallsManager,call), receiver);
     }
 
     /**
@@ -89,16 +106,52 @@
     public CompletableFuture<Boolean> onSetActive(Call call,
             CallTransaction clientCbT,
             OutcomeReceiver<CallTransactionResult, CallException> receiver) {
-        return onSetActiveFlagOff(call, clientCbT, receiver);
+        // save CallsManager state before sending client state changes
+        Call foregroundCallBeforeSwap = mCallsManager.getForegroundCall();
+        boolean wasActive = foregroundCallBeforeSwap != null && foregroundCallBeforeSwap.isActive();
+        OutcomeReceiver<CallTransactionResult, CallException> receiverForTransaction =
+                getOnSetActiveReceiver(call, foregroundCallBeforeSwap, wasActive, receiver);
+
+        if (mIsCallSequencingEnabled) {
+            return createSetActiveTransactionSequencing(call, false /* callControlRequest */,
+                    clientCbT, receiver, receiverForTransaction);
+        } else {
+            SerialTransaction serialTransactions = createSetActiveTransactions(call,
+                    false /* callControlRequest */);
+            serialTransactions.appendTransaction(clientCbT);
+            // do CallsManager workload before asking client and
+            //   reset CallsManager state if client does NOT ack
+            return mTransactionManager.addTransaction(
+                    serialTransactions, receiverForTransaction);
+        }
     }
 
     /**
      * Server -> Client command to answer an incoming call, which if it fails, will trigger the
      * disconnect of the call and then reset the state of the other call back to what it was before.
      */
-    public void onSetAnswered(Call call, int videoState, CallTransaction clientCbT,
-            OutcomeReceiver<CallTransactionResult, CallException> receiver) {
-        onSetAnsweredFlagOff(call, videoState, clientCbT, receiver);
+    public CompletableFuture<Boolean> onSetAnswered(Call call, int videoState,
+            CallTransaction clientCbT, OutcomeReceiver<CallTransactionResult,
+            CallException> receiver) {
+        boolean isCallControlRequest = false;
+        // save CallsManager state before sending client state changes
+        Call foregroundCallBeforeSwap = mCallsManager.getForegroundCall();
+        boolean wasActive = foregroundCallBeforeSwap != null && foregroundCallBeforeSwap.isActive();
+        OutcomeReceiver<CallTransactionResult, CallException> receiverForTransaction =
+                getSetAnswerReceiver(call, foregroundCallBeforeSwap, wasActive,
+                        videoState, receiver, isCallControlRequest);
+
+        if (mIsCallSequencingEnabled) {
+            return createSetActiveTransactionSequencing(call, false /* callControlRequest */,
+                    clientCbT, receiver, receiverForTransaction);
+        } else {
+            SerialTransaction serialTransactions = createSetActiveTransactions(call,
+                    isCallControlRequest);
+            serialTransactions.appendTransaction(clientCbT);
+            // do CallsManager workload before asking client and
+            //   reset CallsManager state if client does NOT ack
+            return mTransactionManager.addTransaction(serialTransactions, receiverForTransaction);
+        }
     }
 
     /**
@@ -107,7 +160,19 @@
     public CompletableFuture<Boolean> onSetInactive(Call call,
             CallTransaction clientCbT,
             OutcomeReceiver<CallTransactionResult, CallException> receiver) {
-        return onSetInactiveFlagOff(call, clientCbT, receiver);
+        return mTransactionManager.addTransaction(clientCbT,
+                new OutcomeReceiver<>() {
+                    @Override
+                    public void onResult(CallTransactionResult callTransactionResult) {
+                        mCallsManager.markCallAsOnHold(call);
+                        receiver.onResult(callTransactionResult);
+                    }
+
+                    @Override
+                    public void onError(CallException error) {
+                        receiver.onError(error);
+                    }
+                });
     }
 
     /**
@@ -116,7 +181,20 @@
     public CompletableFuture<Boolean> onSetDisconnected(Call call,
             DisconnectCause dc, CallTransaction clientCbT, OutcomeReceiver<CallTransactionResult,
             CallException> receiver) {
-        return onSetDisconnectedFlagOff(call, dc, clientCbT, receiver);
+        return mTransactionManager.addTransaction(clientCbT,
+                new OutcomeReceiver<>() {
+                    @Override
+                    public void onResult(CallTransactionResult result) {
+                        removeCallFromCallsManager(call, dc);
+                        receiver.onResult(result);
+                    }
+
+                    @Override
+                    public void onError(CallException exception) {
+                        removeCallFromCallsManager(call, dc);
+                        receiver.onError(exception);
+                    }
+                });
     }
 
     /**
@@ -126,146 +204,6 @@
         cleanupFlagOff(calls);
     }
 
-    private void setActiveFlagOff(Call call,
-            OutcomeReceiver<CallTransactionResult, CallException> receiver) {
-        CompletableFuture<Boolean> transactionResult = mTransactionManager
-                .addTransaction(createSetActiveTransactions(call,
-                true /* callControlRequest */), receiver);
-    }
-
-    private void setAnsweredFlagOff(Call call, int newVideoState,
-            OutcomeReceiver<CallTransactionResult, CallException> receiver) {
-        CompletableFuture<Boolean> transactionResult = mTransactionManager
-                .addTransaction(createSetActiveTransactions(call,
-                                true /* callControlRequest */),
-                new OutcomeReceiver<>() {
-                    @Override
-                    public void onResult(CallTransactionResult callTransactionResult) {
-                        call.setVideoState(newVideoState);
-                        receiver.onResult(callTransactionResult);
-                    }
-
-                    @Override
-                    public void onError(CallException error) {
-                        receiver.onError(error);
-                    }
-                });
-    }
-
-    private void setDisconnectedFlagOff(Call call, DisconnectCause dc,
-            OutcomeReceiver<CallTransactionResult, CallException> receiver) {
-        CompletableFuture<Boolean> transactionResult = mTransactionManager
-                .addTransaction(new EndCallTransaction(mCallsManager,
-                        dc, call), receiver);
-    }
-
-    private void setInactiveFlagOff(Call call,
-            OutcomeReceiver<CallTransactionResult, CallException> receiver) {
-        CompletableFuture<Boolean> transactionResult = mTransactionManager
-                .addTransaction(new HoldCallTransaction(mCallsManager,call), receiver);
-    }
-
-    private CompletableFuture<Boolean> onSetActiveFlagOff(Call call,
-            CallTransaction clientCbT,
-            OutcomeReceiver<CallTransactionResult, CallException> receiver) {
-        // save CallsManager state before sending client state changes
-        Call foregroundCallBeforeSwap = mCallsManager.getForegroundCall();
-        boolean wasActive = foregroundCallBeforeSwap != null && foregroundCallBeforeSwap.isActive();
-        SerialTransaction serialTransactions = createSetActiveTransactions(call,
-                false /* callControlRequest */);
-        serialTransactions.appendTransaction(clientCbT);
-        // do CallsManager workload before asking client and
-        //   reset CallsManager state if client does NOT ack
-        return mTransactionManager.addTransaction(
-                serialTransactions,
-                new OutcomeReceiver<>() {
-                    @Override
-                    public void onResult(CallTransactionResult result) {
-                        receiver.onResult(result);
-                    }
-
-                    @Override
-                    public void onError(CallException exception) {
-                        mCallsManager.markCallAsOnHold(call);
-                        maybeResetForegroundCall(foregroundCallBeforeSwap, wasActive);
-                        receiver.onError(exception);
-                    }
-                });
-    }
-
-    private void onSetAnsweredFlagOff(Call call, int videoState, CallTransaction clientCbT,
-            OutcomeReceiver<CallTransactionResult, CallException> receiver) {
-        // save CallsManager state before sending client state changes
-        Call foregroundCallBeforeSwap = mCallsManager.getForegroundCall();
-        boolean wasActive = foregroundCallBeforeSwap != null && foregroundCallBeforeSwap.isActive();
-        SerialTransaction serialTransactions = createSetActiveTransactions(call,
-                false /* callControlRequest */);
-        serialTransactions.appendTransaction(clientCbT);
-        // do CallsManager workload before asking client and
-        //   reset CallsManager state if client does NOT ack
-        CompletableFuture<Boolean> transactionResult = mTransactionManager
-                .addTransaction(serialTransactions,
-                new OutcomeReceiver<>() {
-                    @Override
-                    public void onResult(CallTransactionResult result) {
-                        call.setVideoState(videoState);
-                        receiver.onResult(result);
-                    }
-
-                    @Override
-                    public void onError(CallException exception) {
-                        // This also sends the signal to untrack from TSW and the client_TSW
-                        removeCallFromCallsManager(call,
-                                new DisconnectCause(DisconnectCause.REJECTED,
-                                        "client rejected to answer the call;"
-                                                + " force disconnecting"));
-                        maybeResetForegroundCall(foregroundCallBeforeSwap, wasActive);
-                        receiver.onError(exception);
-                    }
-                });
-    }
-
-    private CompletableFuture<Boolean> onSetInactiveFlagOff(Call call,
-            CallTransaction clientCbT,
-            OutcomeReceiver<CallTransactionResult, CallException> receiver) {
-        return mTransactionManager.addTransaction(clientCbT,
-                new OutcomeReceiver<>() {
-                    @Override
-                    public void onResult(CallTransactionResult callTransactionResult) {
-                        mCallsManager.markCallAsOnHold(call);
-                        receiver.onResult(callTransactionResult);
-                    }
-
-                    @Override
-                    public void onError(CallException error) {
-                        receiver.onError(error);
-                    }
-                });
-    }
-
-    /**
-     * Server -> Client command to disconnect the call
-     */
-    private CompletableFuture<Boolean> onSetDisconnectedFlagOff(Call call,
-            DisconnectCause dc, CallTransaction clientCbT,
-            OutcomeReceiver<CallTransactionResult, CallException> receiver) {
-        return mTransactionManager.addTransaction(clientCbT,
-                new OutcomeReceiver<>() {
-                    @Override
-                    public void onResult(CallTransactionResult result) {
-                        removeCallFromCallsManager(call, dc);
-                        receiver.onResult(result);
-                    }
-
-                    @Override
-                    public void onError(CallException exception) {
-                        removeCallFromCallsManager(call, dc);
-                        receiver.onError(exception);
-                    }
-                }
-        );
-    }
-
     private SerialTransaction createSetActiveTransactions(Call call, boolean isCallControlRequest) {
         // create list for multiple transactions
         List<CallTransaction> transactions = new ArrayList<>();
@@ -279,6 +217,48 @@
         return new SerialTransaction(transactions, mCallsManager.getLock());
     }
 
+    /**
+     * This code path is invoked when mIsCallSequencingEnabled is true. We will first try to hold
+     * the active call before adding the transactions to request call focus for the new call as well
+     * as verify the client ack for the transaction (if applicable). If the hold transaction
+     * succeeds, we will continue processing the rest of the transactions via a SerialTransaction.
+     */
+    private CompletableFuture<Boolean> createSetActiveTransactionSequencing(
+            Call call, boolean isCallControlRequest, CallTransaction clientCbT,
+            OutcomeReceiver<CallTransactionResult, CallException> receiver,
+            OutcomeReceiver<CallTransactionResult, CallException> receiverForTransaction) {
+        final CompletableFuture<Boolean>[] createSetActiveFuture =
+                new CompletableFuture[]{new CompletableFuture<>()};
+        OutcomeReceiver<Boolean, CallException> maybePerformHoldCallback = new OutcomeReceiver<>() {
+            @Override
+            public void onResult(Boolean result) {
+                // Transaction not yet completed. Still need to request focus for active call and
+                // process client callback transaction if applicable.
+                // create list for multiple transactions
+                List<CallTransaction> transactions = new ArrayList<>();
+                // And request a new focus call update
+                transactions.add(new RequestNewActiveCallTransaction(mCallsManager, call));
+                if (clientCbT != null){
+                    transactions.add(clientCbT);
+                }
+                SerialTransaction serialTransactions = new SerialTransaction(
+                        transactions, mCallsManager.getLock());
+                createSetActiveFuture[0] = mTransactionManager.addTransaction(serialTransactions,
+                        receiverForTransaction);
+            }
+
+            @Override
+            public void onError(CallException exception) {
+                createSetActiveFuture[0] = CompletableFuture.completedFuture(false);
+                receiver.onError(exception);
+            }
+        };
+
+        mCallsManager.getCallSequencingAdapter().transactionHoldPotentialActiveCallForNewCall(call,
+                isCallControlRequest, maybePerformHoldCallback);
+        return createSetActiveFuture[0];
+    }
+
     private void removeCallFromCallsManager(Call call, DisconnectCause cause) {
         if (cause.getCode() != DisconnectCause.REJECTED) {
             mCallsManager.markCallAsDisconnected(call, cause);
@@ -301,4 +281,49 @@
             mCallsManager.removeCall(call); // This will clear mTrackedCalls && ClientTWS
         }
     }
+
+    private OutcomeReceiver<CallTransactionResult, CallException> getOnSetActiveReceiver(
+            Call call, Call foregroundCallBeforeSwap, boolean wasForegroundActive,
+            OutcomeReceiver<CallTransactionResult, CallException> receiver) {
+        return new OutcomeReceiver<>() {
+            @Override
+            public void onResult(CallTransactionResult result) {
+                receiver.onResult(result);
+            }
+
+            @Override
+            public void onError(CallException exception) {
+                mCallsManager.markCallAsOnHold(call);
+                maybeResetForegroundCall(foregroundCallBeforeSwap, wasForegroundActive);
+                receiver.onError(exception);
+            }
+        };
+    }
+
+    private OutcomeReceiver<CallTransactionResult, CallException> getSetAnswerReceiver(
+            Call call, Call foregroundCallBeforeSwap, boolean wasForegroundActive, int videoState,
+            OutcomeReceiver<CallTransactionResult, CallException> receiver,
+            boolean isCallControlRequest) {
+        return new OutcomeReceiver<>() {
+            @Override
+            public void onResult(CallTransactionResult result) {
+                call.setVideoState(videoState);
+                receiver.onResult(result);
+            }
+
+            @Override
+            public void onError(CallException exception) {
+                if (!isCallControlRequest) {
+                    // This also sends the signal to untrack from TSW and the
+                    // client_TSW
+                    removeCallFromCallsManager(call,
+                            new DisconnectCause(DisconnectCause.REJECTED,
+                                    "client rejected to answer the call;"
+                                            + " force disconnecting"));
+                    maybeResetForegroundCall(foregroundCallBeforeSwap, wasForegroundActive);
+                }
+                receiver.onError(exception);
+            }
+        };
+    }
 }
diff --git a/src/com/android/server/telecom/callsequencing/voip/CallEventCallbackAckTransaction.java b/src/com/android/server/telecom/callsequencing/voip/CallEventCallbackAckTransaction.java
index 802ea7e..ae10dee 100644
--- a/src/com/android/server/telecom/callsequencing/voip/CallEventCallbackAckTransaction.java
+++ b/src/com/android/server/telecom/callsequencing/voip/CallEventCallbackAckTransaction.java
@@ -54,17 +54,17 @@
             CODE_OPERATION_TIMED_OUT, "failed to complete the operation before timeout");
 
     private static class AckResultReceiver extends ResultReceiver {
-        CountDownLatch mCountDownLatch;
+        CompletableFuture<Boolean> mCompletableFuture;
 
-        public AckResultReceiver(CountDownLatch latch) {
+        public AckResultReceiver(CompletableFuture<Boolean> future) {
             super(null);
-            mCountDownLatch = latch;
+            mCompletableFuture = future;
         }
 
         @Override
         protected void onReceiveResult(int resultCode, Bundle resultData) {
             if (resultCode == TELECOM_TRANSACTION_SUCCESS) {
-                mCountDownLatch.countDown();
+                mCompletableFuture.complete(true);
             }
         }
     }
@@ -99,9 +99,10 @@
 
     @Override
     public CompletionStage<CallTransactionResult> processTransaction(Void v) {
-        Log.d(TAG, "processTransaction");
-        CountDownLatch latch = new CountDownLatch(1);
-        ResultReceiver receiver = new AckResultReceiver(latch);
+        Log.d(TAG, "processTransaction: action [" + mAction + "]");
+        CompletableFuture<Boolean> future = new CompletableFuture<Boolean>()
+                .completeOnTimeout(false, mTransactionTimeoutMs, TimeUnit.MILLISECONDS);
+        ResultReceiver receiver = new AckResultReceiver(future);
 
         try {
             switch (mAction) {
@@ -125,9 +126,7 @@
             return CompletableFuture.completedFuture(TRANSACTION_FAILED);
         }
 
-        try {
-            // wait for the client to ack that CallEventCallback
-            boolean success = latch.await(mTransactionTimeoutMs, TimeUnit.MILLISECONDS);
+        return future.thenCompose((success) -> {
             if (!success) {
                 // client send onError and failed to complete transaction
                 Log.i(TAG, String.format("CallEventCallbackAckTransaction:"
@@ -139,8 +138,6 @@
                         new CallTransactionResult(CallTransactionResult.RESULT_SUCCEED,
                                 "success"));
             }
-        } catch (InterruptedException ie) {
-            return CompletableFuture.completedFuture(TRANSACTION_FAILED);
-        }
+        });
     }
 }
diff --git a/src/com/android/server/telecom/callsequencing/voip/MaybeHoldCallForNewCallTransaction.java b/src/com/android/server/telecom/callsequencing/voip/MaybeHoldCallForNewCallTransaction.java
index 32062b5..cb839dc 100644
--- a/src/com/android/server/telecom/callsequencing/voip/MaybeHoldCallForNewCallTransaction.java
+++ b/src/com/android/server/telecom/callsequencing/voip/MaybeHoldCallForNewCallTransaction.java
@@ -52,8 +52,8 @@
         Log.d(TAG, "processTransaction");
         CompletableFuture<CallTransactionResult> future = new CompletableFuture<>();
 
-        mCallsManager.transactionHoldPotentialActiveCallForNewCall(mCall, mIsCallControlRequest,
-                new OutcomeReceiver<>() {
+        mCallsManager.getCallSequencingAdapter().transactionHoldPotentialActiveCallForNewCall(
+                mCall, mIsCallControlRequest, new OutcomeReceiver<>() {
             @Override
             public void onResult(Boolean result) {
                 Log.d(TAG, "processTransaction: onResult");
diff --git a/src/com/android/server/telecom/callsequencing/voip/OutgoingCallTransaction.java b/src/com/android/server/telecom/callsequencing/voip/OutgoingCallTransaction.java
index 572de55..b221579 100644
--- a/src/com/android/server/telecom/callsequencing/voip/OutgoingCallTransaction.java
+++ b/src/com/android/server/telecom/callsequencing/voip/OutgoingCallTransaction.java
@@ -91,7 +91,7 @@
             CompletableFuture<Call> callFuture =
                     mCallsManager.startOutgoingCall(mCallAttributes.getAddress(),
                             mCallAttributes.getPhoneAccountHandle(),
-                            generateExtras(mCallAttributes),
+                            generateExtras(mCallId, mExtras, mCallAttributes, mFeatureFlags),
                             mCallAttributes.getPhoneAccountHandle().getUserHandle(),
                             intent,
                             mCallingPackage);
@@ -102,35 +102,11 @@
                                 CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME,
                                 "incoming call not permitted at the current time"));
             }
-            CompletionStage<CallTransactionResult> result = callFuture.thenComposeAsync(
-                    (call) -> {
 
-                        Log.d(TAG, "processTransaction: completing future");
-
-                        if (call == null) {
-                            Log.d(TAG, "processTransaction: call is null");
-                            return CompletableFuture.completedFuture(
-                                    new CallTransactionResult(
-                                            CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME,
-                                            "call could not be created at this time"));
-                        } else {
-                            Log.d(TAG, "processTransaction: call done. id=" + call.getId());
-                        }
-
-                        if (mFeatureFlags.disconnectSelfManagedStuckStartupCalls()) {
-                            // set to dialing so the CallAnomalyWatchdog gives the VoIP calls 1
-                            // minute to timeout rather than 5 seconds.
-                            mCallsManager.markCallAsDialing(call);
-                        }
-
-                        return CompletableFuture.completedFuture(
-                                new CallTransactionResult(
-                                        CallTransactionResult.RESULT_SUCCEED,
-                                        call, null, true));
-                    }
+            return callFuture.thenComposeAsync(
+                    (call) -> processOutgoingCallTransactionHelper(call, TAG,
+                            mCallsManager, mFeatureFlags)
                     , new LoggedHandlerExecutor(mHandler, "OCT.pT", null));
-
-            return result;
         } else {
             return CompletableFuture.completedFuture(
                     new CallTransactionResult(
@@ -141,20 +117,47 @@
     }
 
     @VisibleForTesting
-    public Bundle generateExtras(CallAttributes callAttributes) {
-        mExtras.setDefusable(true);
-        mExtras.putString(TelecomManager.TRANSACTION_CALL_ID_KEY, mCallId);
-        mExtras.putInt(CALL_CAPABILITIES_KEY, callAttributes.getCallCapabilities());
-        if (mFeatureFlags.transactionalVideoState()) {
+    public static Bundle generateExtras(String callId, Bundle extras,
+            CallAttributes callAttributes, FeatureFlags featureFlags) {
+        extras.setDefusable(true);
+        extras.putString(TelecomManager.TRANSACTION_CALL_ID_KEY, callId);
+        extras.putInt(CALL_CAPABILITIES_KEY, callAttributes.getCallCapabilities());
+        if (featureFlags.transactionalVideoState()) {
             // Transactional calls need to remap the CallAttributes video state to the existing
             // VideoProfile for consistency.
-            mExtras.putInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
+            extras.putInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
                     TransactionalVideoStateToVideoProfileState(callAttributes.getCallType()));
         } else {
-            mExtras.putInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
+            extras.putInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
                     callAttributes.getCallType());
         }
-        mExtras.putCharSequence(DISPLAY_NAME_KEY, callAttributes.getDisplayName());
-        return mExtras;
+        extras.putCharSequence(DISPLAY_NAME_KEY, callAttributes.getDisplayName());
+        return extras;
+    }
+
+    public static CompletableFuture<CallTransactionResult> processOutgoingCallTransactionHelper(
+            Call call, String tag, CallsManager callsManager, FeatureFlags featureFlags) {
+        Log.d(tag, "processTransaction: completing future");
+
+        if (call == null) {
+            Log.d(tag, "processTransaction: call is null");
+            return CompletableFuture.completedFuture(
+                    new CallTransactionResult(
+                            CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME,
+                            "call could not be created at this time"));
+        } else {
+            Log.d(tag, "processTransaction: call done. id=" + call.getId());
+        }
+
+        if (featureFlags.disconnectSelfManagedStuckStartupCalls()) {
+            // set to dialing so the CallAnomalyWatchdog gives the VoIP calls 1
+            // minute to timeout rather than 5 seconds.
+            callsManager.markCallAsDialing(call);
+        }
+
+        return CompletableFuture.completedFuture(
+                new CallTransactionResult(
+                        CallTransactionResult.RESULT_SUCCEED,
+                        call, null, true));
     }
 }
diff --git a/src/com/android/server/telecom/callsequencing/voip/OutgoingCallTransactionSequencing.java b/src/com/android/server/telecom/callsequencing/voip/OutgoingCallTransactionSequencing.java
new file mode 100644
index 0000000..af6af34
--- /dev/null
+++ b/src/com/android/server/telecom/callsequencing/voip/OutgoingCallTransactionSequencing.java
@@ -0,0 +1,72 @@
+/*
+ * 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.callsequencing.voip;
+
+import static android.telecom.CallException.CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME;
+
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.LoggedHandlerExecutor;
+import com.android.server.telecom.callsequencing.CallTransaction;
+import com.android.server.telecom.callsequencing.CallTransactionResult;
+import com.android.server.telecom.flags.FeatureFlags;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+
+public class OutgoingCallTransactionSequencing extends CallTransaction {
+
+    private static final String TAG = OutgoingCallTransactionSequencing.class.getSimpleName();
+    private final CompletableFuture<Call> mCallFuture;
+    private final CallsManager mCallsManager;
+    private final boolean mCallNotPermitted;
+    private FeatureFlags mFeatureFlags;
+
+    public OutgoingCallTransactionSequencing(CallsManager callsManager,
+            CompletableFuture<Call> callFuture, boolean callNotPermitted,
+            FeatureFlags featureFlags) {
+        super(callsManager.getLock());
+        mCallsManager = callsManager;
+        mCallFuture = callFuture;
+        mCallNotPermitted = callNotPermitted;
+        mFeatureFlags = featureFlags;
+    }
+
+    @Override
+    public CompletionStage<CallTransactionResult> processTransaction(Void v) {
+        Log.d(TAG, "processTransaction");
+        if (mCallNotPermitted) {
+            return CompletableFuture.completedFuture(
+                    new CallTransactionResult(
+                            CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME,
+                            "outgoing call not permitted at the current time"));
+        }
+
+        return mCallFuture.thenComposeAsync(
+                (call) -> OutgoingCallTransaction.processOutgoingCallTransactionHelper(call, TAG,
+                        mCallsManager, mFeatureFlags)
+                , new LoggedHandlerExecutor(mHandler, "OCT.pT", null));
+    }
+
+    @VisibleForTesting
+    public boolean getCallNotPermitted() {
+        return mCallNotPermitted;
+    }
+}
diff --git a/src/com/android/server/telecom/callsequencing/voip/VoipCallMonitor.java b/src/com/android/server/telecom/callsequencing/voip/VoipCallMonitor.java
index 1d1a1a6..8c74510 100644
--- a/src/com/android/server/telecom/callsequencing/voip/VoipCallMonitor.java
+++ b/src/com/android/server/telecom/callsequencing/voip/VoipCallMonitor.java
@@ -30,10 +30,8 @@
 import android.content.Context;
 import android.content.ServiceConnection;
 import android.os.Handler;
-import android.os.HandlerThread;
 import android.os.IBinder;
 import android.os.RemoteException;
-import android.os.UserHandle;
 import android.service.notification.NotificationListenerService;
 import android.service.notification.StatusBarNotification;
 import android.telecom.Log;
@@ -42,271 +40,366 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.LocalServices;
 import com.android.server.telecom.Call;
-
 import com.android.server.telecom.CallsManagerListenerBase;
 import com.android.server.telecom.LogUtils;
-import com.android.server.telecom.LoggedHandlerExecutor;
 import com.android.server.telecom.TelecomSystem;
 
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
-import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
 
 public class VoipCallMonitor extends CallsManagerListenerBase {
-
-    private final List<Call> mNotificationPendingCalls;
-    // Same notification may be passed as different object in onNotificationPosted and
-    // onNotificationRemoved. Use its string as key to cache ongoing notifications.
-    private final Map<NotificationInfo, Call> mNotificationInfoToCallMap;
-    private final Map<PhoneAccountHandle, Set<Call>> mAccountHandleToCallMap;
+    public static final long NOTIFICATION_NOT_POSTED_IN_TIME_TIMEOUT = 5000L;
+    public static final long NOTIFICATION_REMOVED_BUT_CALL_IS_STILL_ONGOING_TIMEOUT = 5000L;
+    private static final String TAG = VoipCallMonitor.class.getSimpleName();
+    private static final String DElIMITER = "#";
+    // This list caches calls that are added to the VoipCallMonitor and need an accompanying
+    // Call-Style Notification!
+    private final ConcurrentLinkedQueue<Call> mNewCallsMissingCallStyleNotification;
+    private final ConcurrentHashMap<String, Call> mNotificationIdToCall;
+    private final ConcurrentHashMap<PhoneAccountHandle, Set<Call>> mAccountHandleToCallMap;
+    private final ConcurrentHashMap<PhoneAccountHandle, ServiceConnection> mServices;
     private ActivityManagerInternal mActivityManagerInternal;
-    private final Map<PhoneAccountHandle, ServiceConnection> mServices;
-    private NotificationListenerService mNotificationListener;
-    private final Object mLock = new Object();
-    private final HandlerThread mHandlerThread;
-    private final Handler mHandler;
+    private final NotificationListenerService mNotificationListener;
+    private final Handler mHandlerForClass;
     private final Context mContext;
-    private List<NotificationInfo> mCachedNotifications;
-    private TelecomSystem.SyncRoot mSyncRoot;
+    private final TelecomSystem.SyncRoot mSyncRoot;
 
-    public VoipCallMonitor(Context context, TelecomSystem.SyncRoot lock) {
+    public VoipCallMonitor(Context context, Handler handler, TelecomSystem.SyncRoot lock) {
         mSyncRoot = lock;
         mContext = context;
-        mHandlerThread = new HandlerThread(this.getClass().getSimpleName());
-        mHandlerThread.start();
-        mHandler = new Handler(mHandlerThread.getLooper());
-        mNotificationPendingCalls = new ArrayList<>();
-        mCachedNotifications = new ArrayList<>();
-        mNotificationInfoToCallMap = new HashMap<>();
-        mServices = new HashMap<>();
-        mAccountHandleToCallMap = new HashMap<>();
+        mHandlerForClass = handler;
+        mNewCallsMissingCallStyleNotification = new ConcurrentLinkedQueue<>();
+        mNotificationIdToCall = new ConcurrentHashMap<>();
+        mServices = new ConcurrentHashMap<>();
+        mAccountHandleToCallMap = new ConcurrentHashMap<>();
         mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
-
         mNotificationListener = new NotificationListenerService() {
             @Override
             public void onNotificationPosted(StatusBarNotification sbn) {
-                synchronized (mLock) {
-                    if (sbn.getNotification().isStyle(Notification.CallStyle.class)) {
-                        NotificationInfo info = new NotificationInfo(sbn.getPackageName(),
-                                sbn.getUser());
-                        boolean sbnMatched = false;
-                        for (Call call : mNotificationPendingCalls) {
-                            if (info.matchesCall(call)) {
-                                Log.i(this, "onNotificationPosted: found a pending "
-                                                + "callId=[%s] for the call notification w/ "
-                                                + "id=[%s]",
-                                        call.getId(), sbn.getId());
-                                mNotificationPendingCalls.remove(call);
-                                mNotificationInfoToCallMap.put(info, call);
-                                sbnMatched = true;
-                                break;
-                            }
+                if (isCallStyleNotification(sbn)) {
+                    Log.i(TAG, "onNotificationPosted: sbn=[%s]", sbn);
+                    // Case 1: Call added to this class (via onCallAdded) BEFORE Call-Style
+                    //         Notification is posted by the app (only supported scenario)
+                    Call newCallNoLongerAwaitingNotification = null;
+                    for (Call call : mNewCallsMissingCallStyleNotification) {
+                        if (isNotificationForCall(sbn, call)) {
+                            Log.i(TAG, "onNotificationPosted: found a pending "
+                                    + "call=[%s] for sbn.id=[%s]", call, sbn.getId());
+                            mNotificationIdToCall.put(
+                                    getNotificationIdToCallKey(sbn),
+                                    call);
+                            newCallNoLongerAwaitingNotification = call;
+                            break;
                         }
-                        if (!sbnMatched &&
-                                !mCachedNotifications.contains(info) /* don't re-add if update */) {
-                            Log.i(this, "onNotificationPosted: could not find a"
-                                            + "call for the call notification w/ id=[%s]",
-                                    sbn.getId());
-                            // notification may post before we started to monitor the call, cache
-                            // this notification and try to match it later with new added call.
-                            mCachedNotifications.add(info);
-                        }
+                    }
+                    // Case 2: Call-Style Notification was posted BEFORE the Call was added
+                    // --> Currently do not support this
+                    // Case 3: Call-Style Notification was updated (ex. incoming -> ongoing)
+                    // --> do nothing
+                    if (newCallNoLongerAwaitingNotification == null) {
+                        Log.i(TAG, "onNotificationPosted: could not find a call for the"
+                                + " sbn.id=[%s]. This could mean the notification posted"
+                                + " BEFORE the call is added (error) or it's an update from"
+                                + " incoming to ongoing (ok).", sbn.getId());
+                    } else {
+                        // --> remove the newly added call from
+                        // mNewCallsMissingCallStyleNotification so FGS is not revoked when the
+                        // timeout is hit in VoipCallMonitor#startMonitoringNotification(...). The
+                        // timeout ensures the voip app posts a call-style notification within
+                        // 5 seconds!
+                        mNewCallsMissingCallStyleNotification
+                                .remove(newCallNoLongerAwaitingNotification);
                     }
                 }
             }
 
             @Override
             public void onNotificationRemoved(StatusBarNotification sbn) {
-                synchronized (mLock) {
-                    NotificationInfo info = new NotificationInfo(sbn.getPackageName(),
-                            sbn.getUser());
-                    mCachedNotifications.remove(info);
-                    if (mNotificationInfoToCallMap.isEmpty()) {
-                        return;
+                if (!isCallStyleNotification(sbn)) {
+                    return;
+                }
+                Log.i(TAG, "onNotificationRemoved: Call-Style notification=[%s] removed", sbn);
+                Call call = getCallFromStatusBarNotificationId(sbn);
+                if (call != null) {
+                    if (!isCallDisconnected(call)) {
+                        mHandlerForClass.postDelayed(() -> {
+                            if (isCallStillBeingTracked(call)) {
+                                Log.w(TAG,
+                                        "onNotificationRemoved: notification has been removed for"
+                                                + " more than 5 seconds but call still ongoing "
+                                                + "c=[%s]", call);
+                                // TODO:: stopFGSDelegation(call, handle) when b/383403913 is fixed
+                            }
+                        }, NOTIFICATION_REMOVED_BUT_CALL_IS_STILL_ONGOING_TIMEOUT);
                     }
-                    Call call = mNotificationInfoToCallMap.getOrDefault(info, null);
-                    if (call != null) {
-                        // TODO: fix potential bug for multiple calls of same voip app.
-                        mNotificationInfoToCallMap.remove(info, call);
-                        stopFGSDelegation(call);
+                    mNotificationIdToCall.remove(getNotificationIdToCallKey(sbn));
+                }
+            }
+
+            // TODO:: b/383403913 fix gap in matching notifications
+            private boolean isNotificationForCall(StatusBarNotification sbn, Call call) {
+                PhoneAccountHandle callHandle = getTargetPhoneAccount(call);
+                if (callHandle == null) {
+                    return false;
+                }
+                String callPackageName = VoipCallMonitor.this.getPackageName(call);
+                return Objects.equals(sbn.getUser(), callHandle.getUserHandle()) &&
+                        Objects.equals(sbn.getPackageName(), callPackageName);
+            }
+
+            private Call getCallFromStatusBarNotificationId(StatusBarNotification sbn) {
+                if (mNotificationIdToCall.size() == 0) {
+                    return null;
+                }
+                String targetKey = getNotificationIdToCallKey(sbn);
+                for (Map.Entry<String, Call> entry : mNotificationIdToCall.entrySet()) {
+                    if (targetKey.equals(entry.getKey())) {
+                        return entry.getValue();
                     }
                 }
+                return null;
+            }
+
+            private String getNotificationIdToCallKey(StatusBarNotification sbn) {
+                return sbn.getPackageName() + DElIMITER + sbn.getId();
+            }
+
+            private boolean isCallStyleNotification(StatusBarNotification sbn) {
+                return sbn.getNotification().isStyle(Notification.CallStyle.class);
+            }
+
+            private boolean isCallStillBeingTracked(Call call) {
+                PhoneAccountHandle handle = getTargetPhoneAccount(call);
+                if (call == null || handle == null) {
+                    return false;
+                }
+                return mAccountHandleToCallMap
+                        .computeIfAbsent(handle, k -> new HashSet<>())
+                        .contains(call);
             }
         };
 
     }
 
-    public void startMonitor() {
+    public void registerNotificationListener() {
         try {
             mNotificationListener.registerAsSystemService(mContext,
                     new ComponentName(this.getClass().getPackageName(),
                             this.getClass().getCanonicalName()), ActivityManager.getCurrentUser());
         } catch (RemoteException e) {
-            Log.e(this, e, "Cannot register notification listener");
+            Log.e(TAG, e, "Cannot register notification listener");
         }
     }
 
-    public void stopMonitor() {
+    public void unregisterNotificationListener() {
         try {
             mNotificationListener.unregisterAsSystemService();
         } catch (RemoteException e) {
-            Log.e(this, e, "Cannot unregister notification listener");
+            Log.e(TAG, e, "Cannot unregister notification listener");
         }
     }
 
     @Override
     public void onCallAdded(Call call) {
-        if (!call.isTransactionalCall()) {
+        PhoneAccountHandle handle = getTargetPhoneAccount(call);
+        if (!isTransactional(call) || handle == null) {
             return;
         }
-
-        synchronized (mLock) {
-            PhoneAccountHandle phoneAccountHandle = call.getTargetPhoneAccount();
-            Set<Call> callList = mAccountHandleToCallMap.computeIfAbsent(phoneAccountHandle,
-                    k -> new HashSet<>());
-            callList.add(call);
-            CompletableFuture.completedFuture(null).thenComposeAsync(
-                    (x) -> {
-                        startFGSDelegation(call.getCallingPackageIdentity().mCallingPackagePid,
-                                call.getCallingPackageIdentity().mCallingPackageUid, call);
-                        return null;
-                    }, new LoggedHandlerExecutor(mHandler, "VCM.oCA", mSyncRoot));
-        }
+        int callingPid = getCallingPackagePid(call);
+        int callingUid = getCallingPackageUid(call);
+        mAccountHandleToCallMap
+                .computeIfAbsent(handle, k -> new HashSet<>())
+                .add(call);
+        maybeStartFGSDelegation(callingPid, callingUid, handle, call);
     }
 
     @Override
     public void onCallRemoved(Call call) {
-        if (!call.isTransactionalCall()) {
+        PhoneAccountHandle handle = getTargetPhoneAccount(call);
+        if (!isTransactional(call) || handle == null) {
             return;
         }
-
-        synchronized (mLock) {
-            stopMonitorWorks(call);
-            PhoneAccountHandle phoneAccountHandle = call.getTargetPhoneAccount();
-            Set<Call> callList = mAccountHandleToCallMap.computeIfAbsent(phoneAccountHandle,
-                    k -> new HashSet<>());
-            callList.remove(call);
-
-            if (callList.isEmpty()) {
-                stopFGSDelegation(call);
-            }
+        Set<Call> ongoingCalls = mAccountHandleToCallMap
+                .computeIfAbsent(handle, k -> new HashSet<>());
+        ongoingCalls.remove(call);
+        Log.d(TAG, "onCallRemoved: callList.size=[%d]", ongoingCalls.size());
+        if (ongoingCalls.isEmpty()) {
+            stopFGSDelegation(call, handle);
+        } else {
+            Log.addEvent(call, LogUtils.Events.MAINTAINING_FGS_DELEGATION);
         }
     }
 
-    private void startFGSDelegation(int pid, int uid, Call call) {
-        Log.i(this, "startFGSDelegation for call %s", call.getId());
+    private void maybeStartFGSDelegation(int pid, int uid, PhoneAccountHandle handle, Call call) {
+        Log.i(TAG, "maybeStartFGSDelegation for call=[%s]", call);
         if (mActivityManagerInternal != null) {
-            PhoneAccountHandle handle = call.getTargetPhoneAccount();
+            if (mServices.containsKey(handle)) {
+                Log.addEvent(call, LogUtils.Events.ALREADY_HAS_FGS_DELEGATION);
+                startMonitoringNotification(call, handle);
+                return;
+            }
             ForegroundServiceDelegationOptions options = new ForegroundServiceDelegationOptions(pid,
                     uid, handle.getComponentName().getPackageName(), null /* clientAppThread */,
                     false /* isSticky */, String.valueOf(handle.hashCode()),
                     FOREGROUND_SERVICE_TYPE_PHONE_CALL |
-                    FOREGROUND_SERVICE_TYPE_MICROPHONE |
-                    FOREGROUND_SERVICE_TYPE_CAMERA |
-                    FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE /* foregroundServiceTypes */,
+                            FOREGROUND_SERVICE_TYPE_MICROPHONE |
+                            FOREGROUND_SERVICE_TYPE_CAMERA |
+                            FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE /* foregroundServiceTypes */,
                     DELEGATION_SERVICE_PHONE_CALL /* delegationService */);
             ServiceConnection fgsConnection = new ServiceConnection() {
                 @Override
                 public void onServiceConnected(ComponentName name, IBinder service) {
+                    Log.addEvent(call, LogUtils.Events.GAINED_FGS_DELEGATION);
                     mServices.put(handle, this);
-                    startMonitorWorks(call);
+                    startMonitoringNotification(call, handle);
                 }
 
                 @Override
                 public void onServiceDisconnected(ComponentName name) {
+                    Log.addEvent(call, LogUtils.Events.LOST_FGS_DELEGATION);
                     mServices.remove(handle);
                 }
             };
             try {
                 if (mActivityManagerInternal
                         .startForegroundServiceDelegate(options, fgsConnection)) {
-                    Log.addEvent(call, LogUtils.Events.GAINED_FGS_DELEGATION);
+                    Log.i(TAG, "maybeStartFGSDelegation: startForegroundServiceDelegate success");
                 } else {
                     Log.addEvent(call, LogUtils.Events.GAIN_FGS_DELEGATION_FAILED);
                 }
             } catch (Exception e) {
-                Log.i(this, "startForegroundServiceDelegate failed due to: " + e);
+                Log.i(TAG, "startForegroundServiceDelegate failed due to: " + e);
             }
         }
     }
 
     @VisibleForTesting
-    public void stopFGSDelegation(Call call) {
-        synchronized (mLock) {
-            Log.i(this, "stopFGSDelegation of call %s", call);
-            PhoneAccountHandle handle = call.getTargetPhoneAccount();
-            Set<Call> calls = mAccountHandleToCallMap.get(handle);
+    public void stopFGSDelegation(Call call, PhoneAccountHandle handle) {
+        Log.i(TAG, "stopFGSDelegation of call=[%s]", call);
+        if (handle == null) {
+            return;
+        }
 
-            // Every call for the package that is losing foreground service delegation should be
-            // removed from tracking maps/contains in this class
-            if (calls != null) {
-                for (Call c : calls) {
-                    stopMonitorWorks(c); // remove the call from tacking in this class
-                }
+        // In the event this class is waiting for any new calls to post a notification, cleanup
+        List<Call> toRemove = new ArrayList<>();
+        for (Call callAwaitingNotification : mNewCallsMissingCallStyleNotification) {
+            if (handle.equals(callAwaitingNotification.getTargetPhoneAccount())) {
+                Log.d(TAG, "stopFGSDelegation: removing call from notification tracking c=[%s]",
+                        callAwaitingNotification);
+                toRemove.add(callAwaitingNotification);
             }
+        }
+        mNewCallsMissingCallStyleNotification.removeAll(toRemove);
 
-            mAccountHandleToCallMap.remove(handle);
+        if (mActivityManagerInternal != null) {
+            ServiceConnection fgsConnection = mServices.get(handle);
+            if (fgsConnection != null) {
+                Log.i(TAG, "stopFGSDelegation: requesting stopForegroundServiceDelegate");
+                mActivityManagerInternal.stopForegroundServiceDelegate(fgsConnection);
+            }
+        }
+        mAccountHandleToCallMap.remove(handle);
+    }
 
-            if (mActivityManagerInternal != null) {
-                ServiceConnection fgsConnection = mServices.get(handle);
-                if (fgsConnection != null) {
-                    mActivityManagerInternal.stopForegroundServiceDelegate(fgsConnection);
-                    Log.addEvent(call, LogUtils.Events.LOST_FGS_DELEGATION);
-                }
+    private void startMonitoringNotification(Call call, PhoneAccountHandle handle) {
+        String packageName = getPackageName(call);
+        String callId = getCallId(call);
+        // Wait 5 seconds for a CallStyle notification to be posted for the call.
+        // If the Call-Style Notification is not posted, FGS delegation needs to be revoked!
+        Log.i(TAG, "startMonitoringNotification: starting timeout for call.id=[%s]", callId);
+        mNewCallsMissingCallStyleNotification.add(call);
+        // If no notification is posted, stop foreground service delegation!
+        mHandlerForClass.postDelayed(() -> {
+            if (mNewCallsMissingCallStyleNotification.contains(call)) {
+                Log.i(TAG, "startMonitoringNotification: A Call-Style-Notification"
+                        + " for voip-call=[%s] hasn't posted in time,"
+                        + " stopping delegation for app=[%s].", call, packageName);
+                stopFGSDelegation(call, handle);
+            } else {
+                Log.i(TAG, "startMonitoringNotification: found a call-style"
+                        + " notification for call.id[%s] at timeout", callId);
+            }
+        }, NOTIFICATION_NOT_POSTED_IN_TIME_TIMEOUT);
+    }
+
+    /**
+     * Helpers
+     */
+
+    private PhoneAccountHandle getTargetPhoneAccount(Call call) {
+        synchronized (mSyncRoot) {
+            if (call == null) {
+                return null;
+            } else {
+                return call.getTargetPhoneAccount();
             }
         }
     }
 
-    private void startMonitorWorks(Call call) {
-        startMonitorNotification(call);
-    }
-
-    private void stopMonitorWorks(Call call) {
-        stopMonitorNotification(call);
-    }
-
-    private void startMonitorNotification(Call call) {
-        synchronized (mLock) {
-            boolean sbnMatched = false;
-            for (NotificationInfo info : mCachedNotifications) {
-                if (info.matchesCall(call)) {
-                    Log.i(this, "startMonitorNotification: found a cached call "
-                            + "notification for call=[%s]", call);
-                    mCachedNotifications.remove(info);
-                    mNotificationInfoToCallMap.put(info, call);
-                    sbnMatched = true;
-                    break;
-                }
-            }
-            if (!sbnMatched) {
-                // Only continue to
-                Log.i(this, "startMonitorNotification: could not find a call"
-                        + " notification for the call=[%s];", call);
-                mNotificationPendingCalls.add(call);
-                CompletableFuture<Void> future = new CompletableFuture<>();
-                mHandler.postDelayed(() -> future.complete(null), 5000L);
-                future.thenComposeAsync(
-                        (x) -> {
-                            if (mNotificationPendingCalls.contains(call)) {
-                                Log.i(this, "Notification for voip-call %s haven't "
-                                        + "posted in time, stop delegation.", call.getId());
-                                stopFGSDelegation(call);
-                                mNotificationPendingCalls.remove(call);
-                                return null;
-                            }
-                            return null;
-                        }, new LoggedHandlerExecutor(mHandler, "VCM.sMN", mSyncRoot));
+    private int getCallingPackageUid(Call call) {
+        synchronized (mSyncRoot) {
+            if (call == null) {
+                return -1;
+            } else {
+                return call.getCallingPackageIdentity().mCallingPackageUid;
             }
         }
     }
 
-    private void stopMonitorNotification(Call call) {
-        mNotificationPendingCalls.remove(call);
+    private int getCallingPackagePid(Call call) {
+        synchronized (mSyncRoot) {
+            if (call == null) {
+                return -1;
+            } else {
+                return call.getCallingPackageIdentity().mCallingPackagePid;
+            }
+        }
+    }
+
+    private String getCallId(Call call) {
+        synchronized (mSyncRoot) {
+            if (call == null) {
+                return "";
+            } else {
+                return call.getId();
+            }
+        }
+    }
+
+    private boolean isCallDisconnected(Call call) {
+        synchronized (mSyncRoot) {
+            if (call == null) {
+                return true;
+            } else {
+                return call.isDisconnected();
+            }
+        }
+    }
+
+    private boolean isTransactional(Call call) {
+        synchronized (mSyncRoot) {
+            if (call == null) {
+                return false;
+            } else {
+                return call.isTransactionalCall();
+            }
+        }
+    }
+
+    private String getPackageName(Call call) {
+        String pn = "";
+        try {
+            pn = getTargetPhoneAccount(call).getComponentName().getPackageName();
+        } catch (Exception e) {
+            // fall through
+        }
+        return pn;
     }
 
     @VisibleForTesting
@@ -314,49 +407,6 @@
         mActivityManagerInternal = ami;
     }
 
-    private static class NotificationInfo extends Object {
-        private String mPackageName;
-        private UserHandle mUserHandle;
-
-        NotificationInfo(String packageName, UserHandle userHandle) {
-            mPackageName = packageName;
-            mUserHandle = userHandle;
-        }
-
-        boolean matchesCall(Call call) {
-            PhoneAccountHandle accountHandle = call.getTargetPhoneAccount();
-            return mPackageName != null && mPackageName.equals(
-                    accountHandle.getComponentName().getPackageName())
-                    && mUserHandle != null && mUserHandle.equals(accountHandle.getUserHandle());
-        }
-
-        @Override
-        public boolean equals(Object obj) {
-            if (!(obj instanceof NotificationInfo)) {
-                return false;
-            }
-            NotificationInfo that = (NotificationInfo) obj;
-            return Objects.equals(this.mPackageName, that.mPackageName)
-                    && Objects.equals(this.mUserHandle, that.mUserHandle);
-        }
-
-        @Override
-        public int hashCode() {
-            return Objects.hash(mPackageName, mUserHandle);
-        }
-
-        @Override
-        public String toString() {
-            StringBuilder sb = new StringBuilder();
-            sb.append("{ NotificationInfo: [mPackageName: ")
-                    .append(mPackageName)
-                    .append("], [mUserHandle=")
-                    .append(mUserHandle)
-                    .append("]  }");
-            return sb.toString();
-        }
-    }
-
     @VisibleForTesting
     public void postNotification(StatusBarNotification statusBarNotification) {
         mNotificationListener.onNotificationPosted(statusBarNotification);
@@ -367,8 +417,19 @@
         mNotificationListener.onNotificationRemoved(statusBarNotification);
     }
 
+    public boolean hasForegroundServiceDelegation(PhoneAccountHandle handle) {
+        boolean hasFgs = mServices.containsKey(handle);
+        Log.i(TAG, "hasForegroundServiceDelegation: handle=[%s], hasFgs=[%b]", handle, hasFgs);
+        return hasFgs;
+    }
+
     @VisibleForTesting
-    public Set<Call> getCallsForHandle(PhoneAccountHandle handle){
-        return mAccountHandleToCallMap.get(handle);
+    public ConcurrentHashMap<PhoneAccountHandle, Set<Call>> getAccountToCallsMapping() {
+        return mAccountHandleToCallMap;
+    }
+
+    @VisibleForTesting
+    public  ConcurrentLinkedQueue<Call> getNewCallsMissingCallStyleNotificationQueue(){
+        return mNewCallsMissingCallStyleNotification;
     }
 }
diff --git a/src/com/android/server/telecom/callsequencing/voip/VoipCallMonitorLegacy.java b/src/com/android/server/telecom/callsequencing/voip/VoipCallMonitorLegacy.java
new file mode 100644
index 0000000..78f5d52
--- /dev/null
+++ b/src/com/android/server/telecom/callsequencing/voip/VoipCallMonitorLegacy.java
@@ -0,0 +1,373 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom.callsequencing.voip;
+
+import static android.app.ForegroundServiceDelegationOptions.DELEGATION_SERVICE_PHONE_CALL;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL;
+
+import android.app.ActivityManager;
+import android.app.ActivityManagerInternal;
+import android.app.ForegroundServiceDelegationOptions;
+import android.app.Notification;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.ServiceConnection;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.service.notification.NotificationListenerService;
+import android.service.notification.StatusBarNotification;
+import android.telecom.Log;
+import android.telecom.PhoneAccountHandle;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.LocalServices;
+import com.android.server.telecom.Call;
+
+import com.android.server.telecom.CallsManagerListenerBase;
+import com.android.server.telecom.LogUtils;
+import com.android.server.telecom.LoggedHandlerExecutor;
+import com.android.server.telecom.TelecomSystem;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+
+public class VoipCallMonitorLegacy extends CallsManagerListenerBase {
+
+    private final List<Call> mNotificationPendingCalls;
+    // Same notification may be passed as different object in onNotificationPosted and
+    // onNotificationRemoved. Use its string as key to cache ongoing notifications.
+    private final Map<NotificationInfo, Call> mNotificationInfoToCallMap;
+    private final Map<PhoneAccountHandle, Set<Call>> mAccountHandleToCallMap;
+    private ActivityManagerInternal mActivityManagerInternal;
+    private final Map<PhoneAccountHandle, ServiceConnection> mServices;
+    private NotificationListenerService mNotificationListener;
+    private final Object mLock = new Object();
+    private final HandlerThread mHandlerThread;
+    private final Handler mHandler;
+    private final Context mContext;
+    private List<NotificationInfo> mCachedNotifications;
+    private TelecomSystem.SyncRoot mSyncRoot;
+
+    public VoipCallMonitorLegacy(Context context, TelecomSystem.SyncRoot lock) {
+        mSyncRoot = lock;
+        mContext = context;
+        mHandlerThread = new HandlerThread(this.getClass().getSimpleName());
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper());
+        mNotificationPendingCalls = new ArrayList<>();
+        mCachedNotifications = new ArrayList<>();
+        mNotificationInfoToCallMap = new HashMap<>();
+        mServices = new HashMap<>();
+        mAccountHandleToCallMap = new HashMap<>();
+        mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
+
+        mNotificationListener = new NotificationListenerService() {
+            @Override
+            public void onNotificationPosted(StatusBarNotification sbn) {
+                synchronized (mLock) {
+                    if (sbn.getNotification().isStyle(Notification.CallStyle.class)) {
+                        NotificationInfo info = new NotificationInfo(sbn.getPackageName(),
+                                sbn.getUser());
+                        boolean sbnMatched = false;
+                        for (Call call : mNotificationPendingCalls) {
+                            if (info.matchesCall(call)) {
+                                Log.i(this, "onNotificationPosted: found a pending "
+                                                + "callId=[%s] for the call notification w/ "
+                                                + "id=[%s]",
+                                        call.getId(), sbn.getId());
+                                mNotificationPendingCalls.remove(call);
+                                mNotificationInfoToCallMap.put(info, call);
+                                sbnMatched = true;
+                                break;
+                            }
+                        }
+                        if (!sbnMatched &&
+                                !mCachedNotifications.contains(info) /* don't re-add if update */) {
+                            Log.i(this, "onNotificationPosted: could not find a"
+                                            + "call for the call notification w/ id=[%s]",
+                                    sbn.getId());
+                            // notification may post before we started to monitor the call, cache
+                            // this notification and try to match it later with new added call.
+                            mCachedNotifications.add(info);
+                        }
+                    }
+                }
+            }
+
+            @Override
+            public void onNotificationRemoved(StatusBarNotification sbn) {
+                synchronized (mLock) {
+                    NotificationInfo info = new NotificationInfo(sbn.getPackageName(),
+                            sbn.getUser());
+                    mCachedNotifications.remove(info);
+                    if (mNotificationInfoToCallMap.isEmpty()) {
+                        return;
+                    }
+                    Call call = mNotificationInfoToCallMap.getOrDefault(info, null);
+                    if (call != null) {
+                        mNotificationInfoToCallMap.remove(info, call);
+                        CompletableFuture<Void> future = new CompletableFuture<>();
+                        mHandler.postDelayed(() -> future.complete(null), 5000L);
+                        stopFGSDelegation(call);
+                    }
+                }
+            }
+        };
+
+    }
+
+    public void startMonitor() {
+        try {
+            mNotificationListener.registerAsSystemService(mContext,
+                    new ComponentName(this.getClass().getPackageName(),
+                            this.getClass().getCanonicalName()), ActivityManager.getCurrentUser());
+        } catch (RemoteException e) {
+            Log.e(this, e, "Cannot register notification listener");
+        }
+    }
+
+    public void stopMonitor() {
+        try {
+            mNotificationListener.unregisterAsSystemService();
+        } catch (RemoteException e) {
+            Log.e(this, e, "Cannot unregister notification listener");
+        }
+    }
+
+    @Override
+    public void onCallAdded(Call call) {
+        if (!call.isTransactionalCall()) {
+            return;
+        }
+
+        synchronized (mLock) {
+            PhoneAccountHandle phoneAccountHandle = call.getTargetPhoneAccount();
+            Set<Call> callList = mAccountHandleToCallMap.computeIfAbsent(phoneAccountHandle,
+                    k -> new HashSet<>());
+            callList.add(call);
+            CompletableFuture.completedFuture(null).thenComposeAsync(
+                    (x) -> {
+                        startFGSDelegation(call.getCallingPackageIdentity().mCallingPackagePid,
+                                call.getCallingPackageIdentity().mCallingPackageUid, call);
+                        return null;
+                    }, new LoggedHandlerExecutor(mHandler, "VCM.oCA", mSyncRoot));
+        }
+    }
+
+    @Override
+    public void onCallRemoved(Call call) {
+        if (!call.isTransactionalCall()) {
+            return;
+        }
+        synchronized (mLock) {
+            stopMonitorWorks(call);
+            PhoneAccountHandle phoneAccountHandle = call.getTargetPhoneAccount();
+            Set<Call> callList = mAccountHandleToCallMap.computeIfAbsent(phoneAccountHandle,
+                    k -> new HashSet<>());
+            callList.remove(call);
+            if (callList.isEmpty()) {
+                stopFGSDelegation(call);
+            }
+        }
+    }
+
+    private void startFGSDelegation(int pid, int uid, Call call) {
+        Log.i(this, "startFGSDelegation for call %s", call.getId());
+        if (mActivityManagerInternal != null) {
+            PhoneAccountHandle handle = call.getTargetPhoneAccount();
+            ForegroundServiceDelegationOptions options = new ForegroundServiceDelegationOptions(pid,
+                    uid, handle.getComponentName().getPackageName(), null /* clientAppThread */,
+                    false /* isSticky */, String.valueOf(handle.hashCode()),
+                    FOREGROUND_SERVICE_TYPE_PHONE_CALL |
+                    FOREGROUND_SERVICE_TYPE_MICROPHONE |
+                    FOREGROUND_SERVICE_TYPE_CAMERA |
+                    FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE /* foregroundServiceTypes */,
+                    DELEGATION_SERVICE_PHONE_CALL /* delegationService */);
+            ServiceConnection fgsConnection = new ServiceConnection() {
+                @Override
+                public void onServiceConnected(ComponentName name, IBinder service) {
+                    mServices.put(handle, this);
+                    startMonitorWorks(call);
+                }
+
+                @Override
+                public void onServiceDisconnected(ComponentName name) {
+                    mServices.remove(handle);
+                }
+            };
+            try {
+                if (mActivityManagerInternal
+                        .startForegroundServiceDelegate(options, fgsConnection)) {
+                    Log.addEvent(call, LogUtils.Events.GAINED_FGS_DELEGATION);
+                } else {
+                    Log.addEvent(call, LogUtils.Events.GAIN_FGS_DELEGATION_FAILED);
+                }
+            } catch (Exception e) {
+                Log.i(this, "startForegroundServiceDelegate failed due to: " + e);
+            }
+        }
+    }
+
+    @VisibleForTesting
+    public void stopFGSDelegation(Call call) {
+        synchronized (mLock) {
+            Log.i(this, "stopFGSDelegation of call %s", call);
+            PhoneAccountHandle handle = call.getTargetPhoneAccount();
+            Set<Call> calls = mAccountHandleToCallMap.get(handle);
+
+            // Every call for the package that is losing foreground service delegation should be
+            // removed from tracking maps/contains in this class
+            if (calls != null) {
+                for (Call c : calls) {
+                    stopMonitorWorks(c); // remove the call from tacking in this class
+                }
+            }
+
+            mAccountHandleToCallMap.remove(handle);
+
+            if (mActivityManagerInternal != null) {
+                ServiceConnection fgsConnection = mServices.get(handle);
+                if (fgsConnection != null) {
+                    mActivityManagerInternal.stopForegroundServiceDelegate(fgsConnection);
+                    Log.addEvent(call, LogUtils.Events.LOST_FGS_DELEGATION);
+                }
+            }
+        }
+    }
+
+    private void startMonitorWorks(Call call) {
+        startMonitorNotification(call);
+    }
+
+    private void stopMonitorWorks(Call call) {
+        stopMonitorNotification(call);
+    }
+
+    private void startMonitorNotification(Call call) {
+        synchronized (mLock) {
+            boolean sbnMatched = false;
+            for (NotificationInfo info : mCachedNotifications) {
+                if (info.matchesCall(call)) {
+                    Log.i(this, "startMonitorNotification: found a cached call "
+                            + "notification for call=[%s]", call);
+                    mCachedNotifications.remove(info);
+                    mNotificationInfoToCallMap.put(info, call);
+                    sbnMatched = true;
+                    break;
+                }
+            }
+            if (!sbnMatched) {
+                // Only continue to
+                Log.i(this, "startMonitorNotification: could not find a call"
+                        + " notification for the call=[%s];", call);
+                mNotificationPendingCalls.add(call);
+                CompletableFuture<Void> future = new CompletableFuture<>();
+                mHandler.postDelayed(() -> future.complete(null), 5000L);
+                future.thenComposeAsync(
+                        (x) -> {
+                            if (mNotificationPendingCalls.contains(call)) {
+                                Log.i(this, "Notification for voip-call %s haven't "
+                                        + "posted in time, stop delegation.", call.getId());
+                                stopFGSDelegation(call);
+                                mNotificationPendingCalls.remove(call);
+                                return null;
+                            }
+                            return null;
+                        }, new LoggedHandlerExecutor(mHandler, "VCM.sMN", mSyncRoot));
+            }
+        }
+    }
+
+    private void stopMonitorNotification(Call call) {
+        mNotificationPendingCalls.remove(call);
+    }
+
+    @VisibleForTesting
+    public void setActivityManagerInternal(ActivityManagerInternal ami) {
+        mActivityManagerInternal = ami;
+    }
+
+    private static class NotificationInfo extends Object {
+        private String mPackageName;
+        private UserHandle mUserHandle;
+
+        NotificationInfo(String packageName, UserHandle userHandle) {
+            mPackageName = packageName;
+            mUserHandle = userHandle;
+        }
+
+        boolean matchesCall(Call call) {
+            PhoneAccountHandle accountHandle = call.getTargetPhoneAccount();
+            return mPackageName != null && mPackageName.equals(
+                    accountHandle.getComponentName().getPackageName())
+                    && mUserHandle != null && mUserHandle.equals(accountHandle.getUserHandle());
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (!(obj instanceof NotificationInfo)) {
+                return false;
+            }
+            NotificationInfo that = (NotificationInfo) obj;
+            return Objects.equals(this.mPackageName, that.mPackageName)
+                    && Objects.equals(this.mUserHandle, that.mUserHandle);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mPackageName, mUserHandle);
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder();
+            sb.append("{ NotificationInfo: [mPackageName: ")
+                    .append(mPackageName)
+                    .append("], [mUserHandle=")
+                    .append(mUserHandle)
+                    .append("]  }");
+            return sb.toString();
+        }
+    }
+
+    @VisibleForTesting
+    public void postNotification(StatusBarNotification statusBarNotification) {
+        mNotificationListener.onNotificationPosted(statusBarNotification);
+    }
+
+    @VisibleForTesting
+    public void removeNotification(StatusBarNotification statusBarNotification) {
+        mNotificationListener.onNotificationRemoved(statusBarNotification);
+    }
+
+    @VisibleForTesting
+    public Set<Call> getCallsForHandle(PhoneAccountHandle handle){
+        return mAccountHandleToCallMap.get(handle);
+    }
+}
diff --git a/src/com/android/server/telecom/components/TelecomService.java b/src/com/android/server/telecom/components/TelecomService.java
index 4db3e14..2fbdf8b 100644
--- a/src/com/android/server/telecom/components/TelecomService.java
+++ b/src/com/android/server/telecom/components/TelecomService.java
@@ -22,6 +22,7 @@
 import android.content.Intent;
 import android.media.IAudioService;
 import android.media.ToneGenerator;
+import android.os.HandlerThread;
 import android.os.IBinder;
 import android.os.PowerManager;
 import android.os.ServiceManager;
@@ -111,6 +112,9 @@
                     new NotificationChannelManager();
             notificationChannelManager.createChannels(context);
 
+            HandlerThread handlerThread = new HandlerThread("TelecomSystem");
+            handlerThread.start();
+
             TelecomSystem.setInstance(
                     new TelecomSystem(
                             context,
@@ -242,7 +246,8 @@
                                 }
                             },
                             featureFlags,
-                            new com.android.internal.telephony.flags.FeatureFlagsImpl()));
+                            new com.android.internal.telephony.flags.FeatureFlagsImpl(),
+                            handlerThread.getLooper()));
         }
     }
 
diff --git a/src/com/android/server/telecom/metrics/ApiStats.java b/src/com/android/server/telecom/metrics/ApiStats.java
index 4b23e47..d962276 100644
--- a/src/com/android/server/telecom/metrics/ApiStats.java
+++ b/src/com/android/server/telecom/metrics/ApiStats.java
@@ -169,8 +169,8 @@
     private static final String FILE_NAME = "api_stats";
     private Map<ApiEvent, Integer> mApiStatsMap;
 
-    public ApiStats(@NonNull Context context, @NonNull Looper looper) {
-        super(context, looper);
+    public ApiStats(@NonNull Context context, @NonNull Looper looper, boolean isTestMode) {
+        super(context, looper, isTestMode);
     }
 
     @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
diff --git a/src/com/android/server/telecom/metrics/AudioRouteStats.java b/src/com/android/server/telecom/metrics/AudioRouteStats.java
index 4611b22..a79fdea 100644
--- a/src/com/android/server/telecom/metrics/AudioRouteStats.java
+++ b/src/com/android/server/telecom/metrics/AudioRouteStats.java
@@ -76,8 +76,8 @@
     private Pair<AudioRouteStatsKey, long[]> mCur;
     private boolean mIsOngoing;
 
-    public AudioRouteStats(@NonNull Context context, @NonNull Looper looper) {
-        super(context, looper);
+    public AudioRouteStats(@NonNull Context context, @NonNull Looper looper, boolean isTestMode) {
+        super(context, looper, isTestMode);
     }
 
     @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
diff --git a/src/com/android/server/telecom/metrics/CallStats.java b/src/com/android/server/telecom/metrics/CallStats.java
index 8bdeffb..5f00446 100644
--- a/src/com/android/server/telecom/metrics/CallStats.java
+++ b/src/com/android/server/telecom/metrics/CallStats.java
@@ -22,6 +22,8 @@
 import static com.android.server.telecom.TelecomStatsLog.CALL_STATS__ACCOUNT_TYPE__ACCOUNT_SIM;
 import static com.android.server.telecom.TelecomStatsLog.CALL_STATS__ACCOUNT_TYPE__ACCOUNT_UNKNOWN;
 import static com.android.server.telecom.TelecomStatsLog.CALL_STATS__ACCOUNT_TYPE__ACCOUNT_VOIP_API;
+import static com.android.server.telecom.TelecomStatsLog.CALL_STATS__ACCOUNT_TYPE__ACCOUNT_NON_TELECOM_VOIP;
+import static com.android.server.telecom.TelecomStatsLog.CALL_STATS__ACCOUNT_TYPE__ACCOUNT_NON_TELECOM_VOIP_WITH_TELECOM_SUPPORT;
 import static com.android.server.telecom.TelecomStatsLog.CALL_STATS__CALL_DIRECTION__DIR_INCOMING;
 import static com.android.server.telecom.TelecomStatsLog.CALL_STATS__CALL_DIRECTION__DIR_OUTGOING;
 import static com.android.server.telecom.TelecomStatsLog.CALL_STATS__CALL_DIRECTION__DIR_UNKNOWN;
@@ -40,6 +42,7 @@
 import com.android.server.telecom.TelecomStatsLog;
 import com.android.server.telecom.nano.PulledAtomsClass;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -57,8 +60,8 @@
     private Map<CallStatsKey, CallStatsData> mCallStatsMap;
     private boolean mHasMultipleAudioDevices;
 
-    public CallStats(@NonNull Context context, @NonNull Looper looper) {
-        super(context, looper);
+    public CallStats(@NonNull Context context, @NonNull Looper looper, boolean isTestMode) {
+        super(context, looper, isTestMode);
     }
 
     @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
@@ -80,7 +83,8 @@
                     TelecomStatsLog.buildStatsEvent(getTag(),
                             v.getCallDirection(), v.getExternalCall(), v.getEmergencyCall(),
                             v.getMultipleAudioAvailable(), v.getAccountType(), v.getUid(),
-                            v.getCount(), v.getAverageDurationMs())));
+                            v.getCount(), v.getAverageDurationMs(), v.getDisconnectCause(),
+                            v.getSimultaneousType())));
             mCallStatsMap.clear();
             onAggregate();
             return StatsManager.PULL_SUCCESS;
@@ -95,10 +99,11 @@
             mCallStatsMap = new HashMap<>();
             for (PulledAtomsClass.CallStats v : mPulledAtoms.callStats) {
                 mCallStatsMap.put(new CallStatsKey(v.getCallDirection(),
-                                v.getExternalCall(), v.getEmergencyCall(),
-                                v.getMultipleAudioAvailable(),
-                                v.getAccountType(), v.getUid()),
-                        new CallStatsData(v.getCount(), v.getAverageDurationMs()));
+                        v.getExternalCall(), v.getEmergencyCall(),
+                        v.getMultipleAudioAvailable(), v.getAccountType(),
+                        v.getUid(), v.getDisconnectCause(), v.getSimultaneousType()),
+                        new CallStatsData(
+                                v.getCount(), v.getAverageDurationMs()));
             }
             mLastPulledTimestamps = mPulledAtoms.getCallStatsPullTimestampMillis();
         }
@@ -123,6 +128,8 @@
             mPulledAtoms.callStats[index[0]].setMultipleAudioAvailable(k.mIsMultipleAudioAvailable);
             mPulledAtoms.callStats[index[0]].setAccountType(k.mAccountType);
             mPulledAtoms.callStats[index[0]].setUid(k.mUid);
+            mPulledAtoms.callStats[index[0]].setDisconnectCause(k.mCause);
+            mPulledAtoms.callStats[index[0]].setSimultaneousType(k.mSimultaneousType);
             mPulledAtoms.callStats[index[0]].setCount(v.mCount);
             mPulledAtoms.callStats[index[0]].setAverageDurationMs(v.mAverageDuration);
             index[0]++;
@@ -131,10 +138,16 @@
     }
 
     public void log(int direction, boolean isExternal, boolean isEmergency,
-            boolean isMultipleAudioAvailable, int accountType, int uid, int duration) {
+        boolean isMultipleAudioAvailable, int accountType, int uid, int duration) {
+        log(direction, isExternal, isEmergency, isMultipleAudioAvailable, accountType, uid,
+                0, 0, duration);
+    }
+    public void log(int direction, boolean isExternal, boolean isEmergency,
+            boolean isMultipleAudioAvailable, int accountType, int uid,
+            int disconnectCause, int simultaneousType, int duration) {
         post(() -> {
             CallStatsKey key = new CallStatsKey(direction, isExternal, isEmergency,
-                    isMultipleAudioAvailable, accountType, uid);
+                    isMultipleAudioAvailable, accountType, uid, disconnectCause, simultaneousType);
             CallStatsData data = mCallStatsMap.computeIfAbsent(key, k -> new CallStatsData(0, 0));
             data.add(duration);
             onAggregate();
@@ -169,10 +182,33 @@
             }
 
             log(direction, call.isExternalCall(), call.isEmergencyCall(), hasMultipleAudioDevices,
-                    accountType, uid, duration);
+                    accountType, uid, call.getDisconnectCause().getCode(),
+                    call.getSimultaneousType(), duration);
         });
     }
 
+    /**
+     * Used for logging non-telecom calls that have no associated {@link Call}.  This is inferred
+     * from the {@link com.android.server.telecom.CallAudioWatchdog}.
+     *
+     * @param hasTelecomSupport {@code true} if the app making the non-telecom call has Telecom
+     *                                      support (i.e. has a phone account};
+     *                                      {@code false} otherwise.
+     * @param uid The uid of the app making the call.
+     * @param durationMillis The duration of the call, in millis.
+     */
+    public void onNonTelecomCallEnd(final boolean hasTelecomSupport, final int uid,
+            final long durationMillis) {
+        post(() -> log(CALL_STATS__CALL_DIRECTION__DIR_UNKNOWN,
+                false /* isExternalCall */,
+                false /* isEmergencyCall */,
+                false /* hasMultipleAudioDevices  */,
+                hasTelecomSupport ?
+                        CALL_STATS__ACCOUNT_TYPE__ACCOUNT_NON_TELECOM_VOIP_WITH_TELECOM_SUPPORT :
+                        CALL_STATS__ACCOUNT_TYPE__ACCOUNT_NON_TELECOM_VOIP,
+                uid, (int) durationMillis));
+    }
+
     private int getAccountType(PhoneAccount account) {
         if (account == null) {
             return CALL_STATS__ACCOUNT_TYPE__ACCOUNT_UNKNOWN;
@@ -212,15 +248,26 @@
         final boolean mIsMultipleAudioAvailable;
         final int mAccountType;
         final int mUid;
+        final int mCause;
+        final int mSimultaneousType;
 
         CallStatsKey(int direction, boolean isExternal, boolean isEmergency,
-                boolean isMultipleAudioAvailable, int accountType, int uid) {
+            boolean isMultipleAudioAvailable, int accountType, int uid) {
+            this(direction, isExternal, isEmergency, isMultipleAudioAvailable, accountType, uid,
+                    0, 0);
+        }
+
+        CallStatsKey(int direction, boolean isExternal, boolean isEmergency,
+                boolean isMultipleAudioAvailable, int accountType, int uid,
+                int cause, int simultaneousType) {
             mDirection = direction;
             mIsExternal = isExternal;
             mIsEmergency = isEmergency;
             mIsMultipleAudioAvailable = isMultipleAudioAvailable;
             mAccountType = accountType;
             mUid = uid;
+            mCause = cause;
+            mSimultaneousType = simultaneousType;
         }
 
         @Override
@@ -234,13 +281,14 @@
             return this.mDirection == obj.mDirection && this.mIsExternal == obj.mIsExternal
                     && this.mIsEmergency == obj.mIsEmergency
                     && this.mIsMultipleAudioAvailable == obj.mIsMultipleAudioAvailable
-                    && this.mAccountType == obj.mAccountType && this.mUid == obj.mUid;
+                    && this.mAccountType == obj.mAccountType && this.mUid == obj.mUid
+                    && this.mCause == obj.mCause && this.mSimultaneousType == obj.mSimultaneousType;
         }
 
         @Override
         public int hashCode() {
             return Objects.hash(mDirection, mIsExternal, mIsEmergency, mIsMultipleAudioAvailable,
-                    mAccountType, mUid);
+                    mAccountType, mUid, mCause, mSimultaneousType);
         }
 
         @Override
@@ -248,7 +296,7 @@
             return "[CallStatsKey: mDirection=" + mDirection + ", mIsExternal=" + mIsExternal
                     + ", mIsEmergency=" + mIsEmergency + ", mIsMultipleAudioAvailable="
                     + mIsMultipleAudioAvailable + ", mAccountType=" + mAccountType + ", mUid="
-                    + mUid + "]";
+                    + mUid + ", mCause=" + mCause + ", mScType=" + mSimultaneousType + "]";
         }
     }
 
diff --git a/src/com/android/server/telecom/metrics/ErrorStats.java b/src/com/android/server/telecom/metrics/ErrorStats.java
index f334710..7f8ddd7 100644
--- a/src/com/android/server/telecom/metrics/ErrorStats.java
+++ b/src/com/android/server/telecom/metrics/ErrorStats.java
@@ -118,8 +118,8 @@
     private static final String FILE_NAME = "error_stats";
     private Map<ErrorEvent, Integer> mErrorStatsMap;
 
-    public ErrorStats(@NonNull Context context, @NonNull Looper looper) {
-        super(context, looper);
+    public ErrorStats(@NonNull Context context, @NonNull Looper looper, boolean isTestMode) {
+        super(context, looper, isTestMode);
     }
 
     @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
diff --git a/src/com/android/server/telecom/metrics/EventStats.java b/src/com/android/server/telecom/metrics/EventStats.java
new file mode 100644
index 0000000..18e68fb
--- /dev/null
+++ b/src/com/android/server/telecom/metrics/EventStats.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2025 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.metrics;
+
+import static com.android.server.telecom.TelecomStatsLog.TELECOM_EVENT_STATS;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.app.StatsManager;
+import android.content.Context;
+import android.os.Looper;
+import android.telecom.CallException;
+import android.telecom.Log;
+import android.util.StatsEvent;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.server.telecom.TelecomStatsLog;
+import com.android.server.telecom.metrics.ApiStats.ApiEvent;
+import com.android.server.telecom.nano.PulledAtomsClass;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+public class EventStats extends TelecomPulledAtom {
+    public static final int ID_UNKNOWN = TelecomStatsLog.TELECOM_EVENT_STATS__EVENT__EVENT_UNKNOWN;
+    public static final int ID_INIT = TelecomStatsLog.TELECOM_EVENT_STATS__EVENT__EVENT_INIT;
+    public static final int ID_DEFAULT_DIALER_CHANGED = TelecomStatsLog
+            .TELECOM_EVENT_STATS__EVENT__EVENT_DEFAULT_DIALER_CHANGED;
+    public static final int ID_ADD_CALL = TelecomStatsLog
+            .TELECOM_EVENT_STATS__EVENT__EVENT_ADD_CALL;
+
+    public static final int CAUSE_UNKNOWN = TelecomStatsLog
+            .TELECOM_EVENT_STATS__EVENT_CAUSE__CAUSE_UNKNOWN;
+    public static final int CAUSE_GENERIC_SUCCESS = TelecomStatsLog
+            .TELECOM_EVENT_STATS__EVENT_CAUSE__CAUSE_GENERIC_SUCCESS;
+    public static final int CAUSE_GENERIC_FAILURE = TelecomStatsLog
+            .TELECOM_EVENT_STATS__EVENT_CAUSE__CAUSE_GENERIC_FAILURE;
+    public static final int CAUSE_CALL_TRANSACTION_SUCCESS = TelecomStatsLog
+            .TELECOM_EVENT_STATS__EVENT_CAUSE__CALL_TRANSACTION_SUCCESS;
+    public static final int CAUSE_CALL_TRANSACTION_BASE = CAUSE_CALL_TRANSACTION_SUCCESS;
+    public static final int CAUSE_CALL_TRANSACTION_ERROR_UNKNOWN =
+            CAUSE_CALL_TRANSACTION_BASE + CallException.CODE_ERROR_UNKNOWN;
+    public static final int CAUSE_CALL_TRANSACTION_CANNOT_HOLD_CURRENT_ACTIVE_CALL =
+            CAUSE_CALL_TRANSACTION_BASE + CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL;
+    public static final int CAUSE_CALL_TRANSACTION_CALL_IS_NOT_BEING_TRACKED =
+            CAUSE_CALL_TRANSACTION_BASE + CallException.CODE_CALL_IS_NOT_BEING_TRACKED;
+    public static final int CAUSE_CALL_TRANSACTION_CALL_CANNOT_BE_SET_TO_ACTIVE =
+            CAUSE_CALL_TRANSACTION_BASE + CallException.CODE_CALL_CANNOT_BE_SET_TO_ACTIVE;
+    public static final int CAUSE_CALL_TRANSACTION_CALL_NOT_PERMITTED_AT_PRESENT_TIME =
+            CAUSE_CALL_TRANSACTION_BASE + CallException.CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME;
+    public static final int CAUSE_CALL_TRANSACTION_OPERATION_TIMED_OUT =
+            CAUSE_CALL_TRANSACTION_BASE + CallException.CODE_OPERATION_TIMED_OUT;
+    private static final String TAG = EventStats.class.getSimpleName();
+    private static final String FILE_NAME = "event_stats";
+    private Map<CriticalEvent, Integer> mEventStatsMap;
+
+    public EventStats(@NonNull Context context, @NonNull Looper looper,
+                      boolean isTestMode) {
+        super(context, looper, isTestMode);
+    }
+
+    @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+    @Override
+    public int getTag() {
+        return TELECOM_EVENT_STATS;
+    }
+
+    @Override
+    protected String getFileName() {
+        return FILE_NAME;
+    }
+
+    @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+    @Override
+    public synchronized int onPull(final List<StatsEvent> data) {
+        if (mPulledAtoms.telecomEventStats.length != 0) {
+            Arrays.stream(mPulledAtoms.telecomEventStats).forEach(v -> data.add(
+                    TelecomStatsLog.buildStatsEvent(getTag(),
+                            v.getEvent(), v.getUid(), v.getEventCause(), v.getCount())));
+            mEventStatsMap.clear();
+            onAggregate();
+            return StatsManager.PULL_SUCCESS;
+        } else {
+            return StatsManager.PULL_SKIP;
+        }
+    }
+
+    @Override
+    protected synchronized void onLoad() {
+        if (mPulledAtoms.telecomEventStats != null) {
+            mEventStatsMap = new HashMap<>();
+            for (PulledAtomsClass.TelecomEventStats v : mPulledAtoms.telecomEventStats) {
+                mEventStatsMap.put(new CriticalEvent(v.getEvent(), v.getUid(),
+                        v.getEventCause()), v.getCount());
+            }
+            mLastPulledTimestamps = mPulledAtoms.getTelecomEventStatsPullTimestampMillis();
+        }
+    }
+
+    @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+    @Override
+    public synchronized void onAggregate() {
+        Log.d(TAG, "onAggregate: %s", mEventStatsMap);
+        clearAtoms();
+        if (mEventStatsMap.isEmpty()) {
+            return;
+        }
+        mPulledAtoms.setTelecomEventStatsPullTimestampMillis(mLastPulledTimestamps);
+        mPulledAtoms.telecomEventStats =
+                new PulledAtomsClass.TelecomEventStats[mEventStatsMap.size()];
+        int[] index = new int[1];
+        mEventStatsMap.forEach((k, v) -> {
+            mPulledAtoms.telecomEventStats[index[0]] = new PulledAtomsClass.TelecomEventStats();
+            mPulledAtoms.telecomEventStats[index[0]].setEvent(k.mId);
+            mPulledAtoms.telecomEventStats[index[0]].setUid(k.mUid);
+            mPulledAtoms.telecomEventStats[index[0]].setEventCause(k.mCause);
+            mPulledAtoms.telecomEventStats[index[0]].setCount(v);
+            index[0]++;
+        });
+        save(DELAY_FOR_PERSISTENT_MILLIS);
+    }
+
+    public void log(@NonNull CriticalEvent event) {
+        post(() -> {
+            mEventStatsMap.put(event, mEventStatsMap.getOrDefault(event, 0) + 1);
+            onAggregate();
+        });
+    }
+
+    @IntDef(prefix = "ID_", value = {
+            ID_UNKNOWN,
+            ID_INIT,
+            ID_DEFAULT_DIALER_CHANGED,
+            ID_ADD_CALL
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface EventId {
+    }
+
+    @IntDef(prefix = "CAUSE_", value = {
+            CAUSE_UNKNOWN,
+            CAUSE_GENERIC_SUCCESS,
+            CAUSE_GENERIC_FAILURE,
+            CAUSE_CALL_TRANSACTION_SUCCESS,
+            CAUSE_CALL_TRANSACTION_ERROR_UNKNOWN,
+            CAUSE_CALL_TRANSACTION_CANNOT_HOLD_CURRENT_ACTIVE_CALL,
+            CAUSE_CALL_TRANSACTION_CALL_IS_NOT_BEING_TRACKED,
+            CAUSE_CALL_TRANSACTION_CALL_CANNOT_BE_SET_TO_ACTIVE,
+            CAUSE_CALL_TRANSACTION_CALL_NOT_PERMITTED_AT_PRESENT_TIME,
+            CAUSE_CALL_TRANSACTION_OPERATION_TIMED_OUT
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface CauseId {
+    }
+
+    public static class CriticalEvent {
+
+        @EventId
+        int mId;
+        int mUid;
+        @CauseId
+        int mCause;
+
+        public CriticalEvent(@EventId int id, int uid, @CauseId int cause) {
+            mId = id;
+            mUid = uid;
+            mCause = cause;
+        }
+
+        public void setUid(int uid) {
+            this.mUid = uid;
+        }
+
+        public void setResult(@CauseId int result) {
+            this.mCause = result;
+        }
+
+        @Override
+        public boolean equals(Object other) {
+            if (this == other) {
+                return true;
+            }
+            if (!(other instanceof ApiEvent obj)) {
+                return false;
+            }
+            return this.mId == obj.mId && this.mUid == obj.mCallerUid
+                    && this.mCause == obj.mResult;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mId, mUid, mCause);
+        }
+
+        @Override
+        public String toString() {
+            return "[CriticalEvent: mId=" + mId + ", m"
+                    + "Uid=" + mUid
+                    + ", mResult=" + mCause + "]";
+        }
+    }
+
+
+}
diff --git a/src/com/android/server/telecom/metrics/TelecomMetricsController.java b/src/com/android/server/telecom/metrics/TelecomMetricsController.java
index df735c0..980c180 100644
--- a/src/com/android/server/telecom/metrics/TelecomMetricsController.java
+++ b/src/com/android/server/telecom/metrics/TelecomMetricsController.java
@@ -20,10 +20,12 @@
 import static com.android.server.telecom.TelecomStatsLog.CALL_STATS;
 import static com.android.server.telecom.TelecomStatsLog.TELECOM_API_STATS;
 import static com.android.server.telecom.TelecomStatsLog.TELECOM_ERROR_STATS;
+import static com.android.server.telecom.TelecomStatsLog.TELECOM_EVENT_STATS;
 
 import android.annotation.NonNull;
 import android.app.StatsManager;
 import android.content.Context;
+import android.os.Binder;
 import android.os.HandlerThread;
 import android.telecom.Log;
 import android.util.StatsEvent;
@@ -36,6 +38,7 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 public class TelecomMetricsController implements StatsManager.StatsPullAtomCallback {
 
@@ -44,6 +47,7 @@
     private final Context mContext;
     private final HandlerThread mHandlerThread;
     private final ConcurrentHashMap<Integer, TelecomPulledAtom> mStats = new ConcurrentHashMap<>();
+    private final AtomicBoolean mIsTestMode = new AtomicBoolean(false);
 
     private TelecomMetricsController(@NonNull Context context,
                                      @NonNull HandlerThread handlerThread) {
@@ -73,8 +77,13 @@
     public ApiStats getApiStats() {
         ApiStats stats = (ApiStats) mStats.get(TELECOM_API_STATS);
         if (stats == null) {
-            stats = new ApiStats(mContext, mHandlerThread.getLooper());
-            registerAtom(stats.getTag(), stats);
+            long token = Binder.clearCallingIdentity();
+            try {
+                stats = new ApiStats(mContext, mHandlerThread.getLooper(), isTestMode());
+                registerAtom(stats.getTag(), stats);
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
         }
         return stats;
     }
@@ -83,7 +92,7 @@
     public AudioRouteStats getAudioRouteStats() {
         AudioRouteStats stats = (AudioRouteStats) mStats.get(CALL_AUDIO_ROUTE_STATS);
         if (stats == null) {
-            stats = new AudioRouteStats(mContext, mHandlerThread.getLooper());
+            stats = new AudioRouteStats(mContext, mHandlerThread.getLooper(), isTestMode());
             registerAtom(stats.getTag(), stats);
         }
         return stats;
@@ -93,7 +102,7 @@
     public CallStats getCallStats() {
         CallStats stats = (CallStats) mStats.get(CALL_STATS);
         if (stats == null) {
-            stats = new CallStats(mContext, mHandlerThread.getLooper());
+            stats = new CallStats(mContext, mHandlerThread.getLooper(), isTestMode());
             registerAtom(stats.getTag(), stats);
         }
         return stats;
@@ -103,7 +112,17 @@
     public ErrorStats getErrorStats() {
         ErrorStats stats = (ErrorStats) mStats.get(TELECOM_ERROR_STATS);
         if (stats == null) {
-            stats = new ErrorStats(mContext, mHandlerThread.getLooper());
+            stats = new ErrorStats(mContext, mHandlerThread.getLooper(), isTestMode());
+            registerAtom(stats.getTag(), stats);
+        }
+        return stats;
+    }
+
+    @NonNull
+    public EventStats getEventStats() {
+        EventStats stats = (EventStats) mStats.get(TELECOM_EVENT_STATS);
+        if (stats == null) {
+            stats = new EventStats(mContext, mHandlerThread.getLooper(), isTestMode());
             registerAtom(stats.getTag(), stats);
         }
         return stats;
@@ -134,14 +153,30 @@
     }
 
     public void destroy() {
+        clearStats();
+        mHandlerThread.quitSafely();
+    }
+
+    public void setTestMode(boolean enabled) {
+        mIsTestMode.set(enabled);
+        clearStats();
+    }
+
+    public boolean isTestMode() {
+        return mIsTestMode.get();
+    }
+
+    private void clearStats() {
         final StatsManager statsManager = mContext.getSystemService(StatsManager.class);
         if (statsManager != null) {
-            mStats.forEach((tag, stat) -> statsManager.clearPullAtomCallback(tag));
+            mStats.forEach((tag, stat) -> {
+                statsManager.clearPullAtomCallback(tag);
+                stat.flush();
+            });
         } else {
             Log.w(TAG, "Unable to clear pulled atoms as StatsManager is null");
         }
 
         mStats.clear();
-        mHandlerThread.quitSafely();
     }
 }
diff --git a/src/com/android/server/telecom/metrics/TelecomPulledAtom.java b/src/com/android/server/telecom/metrics/TelecomPulledAtom.java
index 161eaa8..d60fc77 100644
--- a/src/com/android/server/telecom/metrics/TelecomPulledAtom.java
+++ b/src/com/android/server/telecom/metrics/TelecomPulledAtom.java
@@ -45,23 +45,28 @@
     private static final long MIN_PULL_INTERVAL_MILLIS = 23L * 60 * 60 * 1000;
     private static final int EVENT_SAVE = 1;
     protected final Context mContext;
+    protected final boolean mIsTestMode;
     @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
     public PulledAtoms mPulledAtoms;
     protected long mLastPulledTimestamps;
 
-    protected TelecomPulledAtom(@NonNull Context context, @NonNull Looper looper) {
+    protected TelecomPulledAtom(@NonNull Context context, @NonNull Looper looper,
+                                boolean isTestMode) {
         super(looper);
         mContext = context;
+        mIsTestMode = isTestMode;
         mPulledAtoms = loadAtomsFromFile();
         onLoad();
     }
 
     public synchronized int pull(final List<StatsEvent> data) {
-        long cur = System.currentTimeMillis();
-        if (cur - mLastPulledTimestamps < MIN_PULL_INTERVAL_MILLIS) {
-            return StatsManager.PULL_SKIP;
+        if (!mIsTestMode) {
+            long cur = System.currentTimeMillis();
+            if (cur - mLastPulledTimestamps < MIN_PULL_INTERVAL_MILLIS) {
+                return StatsManager.PULL_SKIP;
+            }
+            mLastPulledTimestamps = cur;
         }
-        mLastPulledTimestamps = cur;
         return onPull(data);
     }
 
@@ -76,21 +81,22 @@
     @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
     public abstract void onAggregate();
 
-    public void onFlush() {
+    public void flush() {
         save(0);
     }
 
     protected abstract String getFileName();
 
     private synchronized PulledAtoms loadAtomsFromFile() {
-        try {
-            return
-                    PulledAtoms.parseFrom(
-                            Files.readAllBytes(mContext.getFileStreamPath(getFileName()).toPath()));
-        } catch (NoSuchFileException e) {
-            Log.e(TAG, e, "the atom file not found");
-        } catch (IOException | NullPointerException e) {
-            Log.e(TAG, e, "cannot load/parse the atom file");
+        if (!mIsTestMode) {
+            try {
+                return PulledAtoms.parseFrom(
+                        Files.readAllBytes(mContext.getFileStreamPath(getFileName()).toPath()));
+            } catch (NoSuchFileException e) {
+                Log.e(TAG, e, "the atom file not found");
+            } catch (IOException | NullPointerException e) {
+                Log.e(TAG, e, "cannot load/parse the atom file");
+            }
         }
         return makeNewPulledAtoms();
     }
@@ -100,14 +106,16 @@
     }
 
     private synchronized void onSave() {
-        try (FileOutputStream stream = mContext.openFileOutput(getFileName(),
-                Context.MODE_PRIVATE)) {
-            Log.d(TAG, "save " + getTag());
-            stream.write(PulledAtoms.toByteArray(mPulledAtoms));
-        } catch (IOException e) {
-            Log.e(TAG, e, "cannot save the atom to file");
-        } catch (UnsupportedOperationException e) {
-            Log.e(TAG, e, "cannot open the file");
+        if (!mIsTestMode) {
+            try (FileOutputStream stream = mContext.openFileOutput(getFileName(),
+                    Context.MODE_PRIVATE)) {
+                Log.d(TAG, "save " + getTag());
+                stream.write(PulledAtoms.toByteArray(mPulledAtoms));
+            } catch (IOException e) {
+                Log.e(TAG, e, "cannot save the atom to file");
+            } catch (UnsupportedOperationException e) {
+                Log.e(TAG, e, "cannot open the file");
+            }
         }
     }
 
diff --git a/src/com/android/server/telecom/settings/BlockedNumbersActivity.java b/src/com/android/server/telecom/settings/BlockedNumbersActivity.java
index edc8da6..801ec49 100644
--- a/src/com/android/server/telecom/settings/BlockedNumbersActivity.java
+++ b/src/com/android/server/telecom/settings/BlockedNumbersActivity.java
@@ -18,6 +18,7 @@
 
 import android.annotation.Nullable;
 import android.app.ActionBar;
+import android.app.Activity;
 import android.app.AlertDialog;
 import android.app.Fragment;
 import android.app.FragmentManager;
@@ -54,6 +55,10 @@
 import android.widget.TextView;
 import android.widget.Toast;
 
+import androidx.core.graphics.Insets;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.WindowInsetsCompat;
+
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.telecom.R;
 import com.android.server.telecom.flags.FeatureFlags;
@@ -109,6 +114,7 @@
             // set the talkback voice prompt to "Back" instead of "Navigate Up"
             actionBar.setHomeActionContentDescription(R.string.back);
         }
+        setupEdgeToEdge(this);
 
         if (!BlockedNumberContract.canCurrentUserBlockNumbers(this)) {
             TextView nonPrimaryUserText = (TextView) findViewById(R.id.non_primary_user);
@@ -358,4 +364,23 @@
         }
         mAddButton.setEnabled(true);
     }
+
+    /**
+     * Given an activity, configure the activity to adjust for edge to edge restrictions.
+     * @param activity the activity.
+     */
+    public static void setupEdgeToEdge(Activity activity) {
+        ViewCompat.setOnApplyWindowInsetsListener(activity.findViewById(android.R.id.content),
+                (v, windowInsets) -> {
+                    Insets insets = windowInsets.getInsets(
+                            WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.ime());
+
+                    // Apply the insets paddings to the view.
+                    v.setPadding(insets.left, insets.top, insets.right, insets.bottom);
+
+                    // Return CONSUMED if you don't want the window insets to keep being
+                    // passed down to descendant views.
+                    return WindowInsetsCompat.CONSUMED;
+                });
+    }
 }
\ No newline at end of file
diff --git a/tests/src/com/android/server/telecom/tests/BasicCallTests.java b/tests/src/com/android/server/telecom/tests/BasicCallTests.java
index 7646c2d..1b7b0ef 100644
--- a/tests/src/com/android/server/telecom/tests/BasicCallTests.java
+++ b/tests/src/com/android/server/telecom/tests/BasicCallTests.java
@@ -695,7 +695,10 @@
                 ArgumentCaptor.forClass(AudioDeviceInfo.class);
         verify(audioManager, timeout(TEST_TIMEOUT).atLeast(1))
                 .setCommunicationDevice(infoArgumentCaptor.capture());
-        assertEquals(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, infoArgumentCaptor.getValue().getType());
+        var deviceType = infoArgumentCaptor.getValue().getType();
+        if (deviceType != AudioDeviceInfo.TYPE_BUS) { // on automotive, we expect BUS
+            assertEquals(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, deviceType);
+        }
         mInCallServiceFixtureX.mInCallAdapter.setAudioRoute(CallAudioState.ROUTE_EARPIECE, null);
         waitForHandlerAction(mTelecomSystem.getCallsManager().getCallAudioManager()
                 .getCallAudioRouteAdapter().getAdapterHandler(), TEST_TIMEOUT);
@@ -1282,7 +1285,7 @@
         // Stub intent for call2
         Intent callIntent2 = new Intent();
         Bundle callExtras1 = new Bundle();
-        Icon icon = Icon.createWithContentUri("content://10@media/external/images/media/");
+        Icon icon = Icon.createWithContentUri("content://12@media/external/images/media/");
         // Load StatusHints extra into TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS to be processed
         // as the call extras. This will be leveraged in ConnectionServiceFixture to set the
         // StatusHints for the given connection.
@@ -1315,7 +1318,7 @@
     @Test
     public void testValidateStatusHintsImage_handleCreateConnectionComplete() throws Exception {
         Bundle extras = new Bundle();
-        Icon icon = Icon.createWithContentUri("content://10@media/external/images/media/");
+        Icon icon = Icon.createWithContentUri("content://12@media/external/images/media/");
         // Load the bundle with the test extra in order to simulate an app directly invoking the
         // binder on ConnectionServiceWrapper#handleCreateConnectionComplete.
         StatusHints statusHints = new StatusHints(icon);
@@ -1349,7 +1352,7 @@
                 mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA);
 
         // Modify existing connection with StatusHints image exploit
-        Icon icon = Icon.createWithContentUri("content://10@media/external/images/media/");
+        Icon icon = Icon.createWithContentUri("content://12@media/external/images/media/");
         StatusHints statusHints = new StatusHints(icon);
         assertNotNull(statusHints.getIcon());
         ConnectionServiceFixture.ConnectionInfo connectionInfo = mConnectionServiceFixtureA
@@ -1384,7 +1387,7 @@
                 mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA);
 
         // Modify existing connection with StatusHints image exploit
-        Icon icon = Icon.createWithContentUri("content://10@media/external/images/media/");
+        Icon icon = Icon.createWithContentUri("content://12@media/external/images/media/");
         StatusHints modifiedStatusHints = new StatusHints(icon);
         assertNotNull(modifiedStatusHints.getIcon());
         ConnectionServiceFixture.ConnectionInfo connectionInfo = mConnectionServiceFixtureA
diff --git a/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java b/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java
index ac4a94e..9f97bbe 100644
--- a/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java
@@ -18,6 +18,9 @@
 
 import static android.media.AudioDeviceInfo.TYPE_BUILTIN_SPEAKER;
 
+import static com.android.server.telecom.CallAudioRouteAdapter.SWITCH_BASELINE_ROUTE;
+import static com.android.server.telecom.CallAudioRouteController.INCLUDE_BLUETOOTH_IN_BASELINE;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
@@ -44,11 +47,14 @@
 import android.media.AudioManager;
 import android.os.Bundle;
 import android.os.Parcel;
+import android.telecom.CallAudioState;
 
 import androidx.test.filters.SmallTest;
 
+import com.android.server.telecom.AudioRoute;
 import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
 import com.android.server.telecom.CallAudioRouteAdapter;
+import com.android.server.telecom.CallAudioRouteController;
 import com.android.server.telecom.bluetooth.BluetoothDeviceManager;
 import com.android.server.telecom.bluetooth.BluetoothRouteManager;
 import com.android.server.telecom.bluetooth.BluetoothStateReceiver;
@@ -64,7 +70,9 @@
 import static org.mockito.Mockito.reset;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.Executor;
 
 @RunWith(JUnit4.class)
@@ -79,6 +87,8 @@
     @Mock AudioManager mockAudioManager;
     @Mock AudioDeviceInfo mSpeakerInfo;
     @Mock Executor mExecutor;
+    @Mock CallAudioRouteController mCallAudioRouteController;
+    @Mock CallAudioState mCallAudioState;
 
     BluetoothDeviceManager mBluetoothDeviceManager;
     BluetoothProfile.ServiceListener serviceListenerUnderTest;
@@ -115,6 +125,7 @@
         mBluetoothDeviceManager = new BluetoothDeviceManager(mContext, mAdapter,
                 mCommunicationDeviceTracker, mFeatureFlags);
         mBluetoothDeviceManager.setBluetoothRouteManager(mRouteManager);
+        mBluetoothDeviceManager.setCallAudioRouteAdapter(mCallAudioRouteController);
         mCommunicationDeviceTracker.setBluetoothRouteManager(mRouteManager);
 
         mockAudioManager = mContext.getSystemService(AudioManager.class);
@@ -299,6 +310,38 @@
 
     @SmallTest
     @Test
+    public void testHandleAudioRefactoringServiceDisconnectedWhileBluetooth() {
+        when(mFeatureFlags.skipBaselineSwitchWhenRouteNotBluetooth()).thenReturn(true);
+        Map<AudioRoute, BluetoothDevice> btRoutes = new HashMap<>();
+        when(mCallAudioRouteController.getBluetoothRoutes()).thenReturn(btRoutes);
+        when(mCallAudioRouteController.getCurrentCallAudioState()).thenReturn(mCallAudioState);
+        when(mCallAudioState.getRoute()).thenReturn(CallAudioState.ROUTE_BLUETOOTH);
+
+        mBluetoothDeviceManager
+                .handleAudioRefactoringServiceDisconnected(BluetoothProfile.LE_AUDIO);
+
+        verify(mCallAudioRouteController).sendMessageWithSessionInfo(SWITCH_BASELINE_ROUTE,
+                INCLUDE_BLUETOOTH_IN_BASELINE, (String) null);
+    }
+
+    @SmallTest
+    @Test
+    public void testHandleAudioRefactoringServiceDisconnectedWhileSpeaker() {
+        when(mFeatureFlags.skipBaselineSwitchWhenRouteNotBluetooth()).thenReturn(true);
+        Map<AudioRoute, BluetoothDevice> btRoutes = new HashMap<>();
+        when(mCallAudioRouteController.getBluetoothRoutes()).thenReturn(btRoutes);
+        when(mCallAudioRouteController.getCurrentCallAudioState()).thenReturn(mCallAudioState);
+        when(mCallAudioState.getRoute()).thenReturn(CallAudioState.ROUTE_SPEAKER);
+
+        mBluetoothDeviceManager
+                .handleAudioRefactoringServiceDisconnected(BluetoothProfile.LE_AUDIO);
+
+        verify(mCallAudioRouteController, never()).sendMessageWithSessionInfo(SWITCH_BASELINE_ROUTE,
+                INCLUDE_BLUETOOTH_IN_BASELINE, (String) null);
+    }
+
+    @SmallTest
+    @Test
     public void testHeadsetServiceDisconnect() {
         receiverUnderTest.onReceive(mContext,
                 buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device1,
diff --git a/tests/src/com/android/server/telecom/tests/BluetoothRouteManagerTest.java b/tests/src/com/android/server/telecom/tests/BluetoothRouteManagerTest.java
index 1c885c1..4913904 100644
--- a/tests/src/com/android/server/telecom/tests/BluetoothRouteManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/BluetoothRouteManagerTest.java
@@ -35,6 +35,7 @@
 import android.bluetooth.BluetoothProfile;
 import android.bluetooth.BluetoothStatusCodes;
 import android.content.ContentResolver;
+import android.media.AudioDeviceInfo;
 import android.os.Parcel;
 import android.telecom.Log;
 
@@ -104,6 +105,8 @@
             BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID);
         when(mDeviceManager.connectAudio(anyString(), anyBoolean())).thenReturn(true);
         when(mDeviceManager.isHearingAidSetAsCommunicationDevice()).thenReturn(true);
+        when(mCommunicationDeviceTracker.isAudioDeviceSetForType(
+                eq(AudioDeviceInfo.TYPE_HEARING_AID))).thenReturn(true);
 
         setupConnectedDevices(null, HEARING_AIDS, null, null, HEARING_AIDS, null);
         when(mBluetoothHeadset.getAudioState(nullable(BluetoothDevice.class)))
@@ -130,7 +133,8 @@
             BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID);
         when(mDeviceManager.connectAudio(anyString(), anyBoolean())).thenReturn(true);
         when(mDeviceManager.isHearingAidSetAsCommunicationDevice()).thenReturn(true);
-
+        when(mCommunicationDeviceTracker.isAudioDeviceSetForType(
+                eq(AudioDeviceInfo.TYPE_HEARING_AID))).thenReturn(true);
 
         setupConnectedDevices(null, HEARING_AIDS, null, null, HEARING_AIDS, null);
         when(mBluetoothHeadset.getAudioState(nullable(BluetoothDevice.class)))
@@ -299,7 +303,8 @@
         resetMocks();
         BluetoothRouteManager sm = new BluetoothRouteManager(mContext,
                 new TelecomSystem.SyncRoot() { }, mDeviceManager,
-                mTimeoutsAdapter, mCommunicationDeviceTracker, mFeatureFlags);
+                mTimeoutsAdapter, mCommunicationDeviceTracker, mFeatureFlags,
+                mContext.getMainLooper());
         sm.setListener(mListener);
         sm.setInitialStateForTesting(initialState, initialDevice);
         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
diff --git a/tests/src/com/android/server/telecom/tests/BluetoothRouteTransitionTests.java b/tests/src/com/android/server/telecom/tests/BluetoothRouteTransitionTests.java
index c546c3f..004bcd3 100644
--- a/tests/src/com/android/server/telecom/tests/BluetoothRouteTransitionTests.java
+++ b/tests/src/com/android/server/telecom/tests/BluetoothRouteTransitionTests.java
@@ -420,7 +420,8 @@
                 nullable(ContentResolver.class))).thenReturn(100000L);
         BluetoothRouteManager sm = new BluetoothRouteManager(mContext,
                 new TelecomSystem.SyncRoot() { }, mDeviceManager,
-                mTimeoutsAdapter, mCommunicationDeviceTracker, mFeatureFlags);
+                mTimeoutsAdapter, mCommunicationDeviceTracker, mFeatureFlags,
+                mContext.getMainLooper());
         sm.setListener(mListener);
         sm.setInitialStateForTesting(initialState, initialDevice);
         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
index 809abb4..b0b1d4e 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
@@ -37,13 +37,13 @@
 import static com.android.server.telecom.CallAudioRouteAdapter.STREAMING_FORCE_ENABLED;
 import static com.android.server.telecom.CallAudioRouteAdapter.SWITCH_BASELINE_ROUTE;
 import static com.android.server.telecom.CallAudioRouteAdapter.SWITCH_FOCUS;
+import static com.android.server.telecom.CallAudioRouteAdapter.TOGGLE_MUTE;
 import static com.android.server.telecom.CallAudioRouteAdapter.USER_SWITCH_BASELINE_ROUTE;
 import static com.android.server.telecom.CallAudioRouteAdapter.USER_SWITCH_BLUETOOTH;
 import static com.android.server.telecom.CallAudioRouteAdapter.USER_SWITCH_EARPIECE;
 import static com.android.server.telecom.CallAudioRouteAdapter.USER_SWITCH_HEADSET;
 import static com.android.server.telecom.CallAudioRouteAdapter.USER_SWITCH_SPEAKER;
 import static com.android.server.telecom.CallAudioRouteController.INCLUDE_BLUETOOTH_IN_BASELINE;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
@@ -56,6 +56,7 @@
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.eq;
 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;
@@ -106,39 +107,57 @@
 
 @RunWith(JUnit4.class)
 public class CallAudioRouteControllerTest extends TelecomTestCase {
-    private CallAudioRouteController mController;
-    @Mock WiredHeadsetManager mWiredHeadsetManager;
-    @Mock AudioManager mAudioManager;
-    @Mock AudioDeviceInfo mEarpieceDeviceInfo;
-    @Mock CallsManager mCallsManager;
-    @Mock CallAudioManager.AudioServiceFactory mAudioServiceFactory;
-    @Mock IAudioService mAudioService;
-    @Mock BluetoothRouteManager mBluetoothRouteManager;
-    @Mock BluetoothDeviceManager mBluetoothDeviceManager;
-    @Mock BluetoothAdapter mBluetoothAdapter;
-    @Mock StatusBarNotifier mockStatusBarNotifier;
-    @Mock AudioDeviceInfo mAudioDeviceInfo;
-    @Mock BluetoothLeAudio mBluetoothLeAudio;
-    @Mock CallAudioManager mCallAudioManager;
-    @Mock Call mCall;
-    @Mock private TelecomSystem.SyncRoot mLock;
-    @Mock private TelecomMetricsController mMockTelecomMetricsController;
-    private AudioRoute mEarpieceRoute;
-    private AudioRoute mSpeakerRoute;
-    private boolean mOverrideSpeakerToBus;
     private static final String BT_ADDRESS_1 = "00:00:00:00:00:01";
     private static final BluetoothDevice BLUETOOTH_DEVICE_1 =
             BluetoothRouteManagerTest.makeBluetoothDevice("00:00:00:00:00:01");
     private static final Set<BluetoothDevice> BLUETOOTH_DEVICES;
+    private static final int TEST_TIMEOUT = 500;
+
     static {
         BLUETOOTH_DEVICES = new HashSet<>();
         BLUETOOTH_DEVICES.add(BLUETOOTH_DEVICE_1);
     }
-    private static final int TEST_TIMEOUT = 500;
+
+    @Mock
+    WiredHeadsetManager mWiredHeadsetManager;
+    @Mock
+    AudioManager mAudioManager;
+    @Mock
+    AudioDeviceInfo mEarpieceDeviceInfo;
+    @Mock
+    CallsManager mCallsManager;
+    @Mock
+    CallAudioManager.AudioServiceFactory mAudioServiceFactory;
+    @Mock
+    IAudioService mAudioService;
+    @Mock
+    BluetoothRouteManager mBluetoothRouteManager;
+    @Mock
+    BluetoothDeviceManager mBluetoothDeviceManager;
+    @Mock
+    BluetoothAdapter mBluetoothAdapter;
+    @Mock
+    StatusBarNotifier mockStatusBarNotifier;
+    @Mock
+    AudioDeviceInfo mAudioDeviceInfo;
+    @Mock
+    BluetoothLeAudio mBluetoothLeAudio;
+    @Mock
+    CallAudioManager mCallAudioManager;
+    @Mock
+    Call mCall;
+    private CallAudioRouteController mController;
+    @Mock
+    private TelecomSystem.SyncRoot mLock;
+    @Mock
+    private TelecomMetricsController mMockTelecomMetricsController;
+    private AudioRoute mEarpieceRoute;
+    private AudioRoute mSpeakerRoute;
+    private boolean mOverrideSpeakerToBus;
     AudioRoute.Factory mAudioRouteFactory = new AudioRoute.Factory() {
         @Override
         public AudioRoute create(@AudioRoute.AudioRouteType int type, String bluetoothAddress,
-                                 AudioManager audioManager) {
+                AudioManager audioManager) {
             if (mOverrideSpeakerToBus && type == AudioRoute.TYPE_SPEAKER) {
                 type = AudioRoute.TYPE_BUS;
             }
@@ -152,7 +171,7 @@
         when(mWiredHeadsetManager.isPluggedIn()).thenReturn(false);
         when(mEarpieceDeviceInfo.getType()).thenReturn(AudioDeviceInfo.TYPE_BUILTIN_EARPIECE);
         when(mAudioManager.getDevices(eq(AudioManager.GET_DEVICES_OUTPUTS))).thenReturn(
-                new AudioDeviceInfo[] {
+                new AudioDeviceInfo[]{
                         mEarpieceDeviceInfo
                 });
         when(mAudioManager.getPreferredDeviceForStrategy(nullable(AudioProductStrategy.class)))
@@ -171,7 +190,8 @@
         when(mCallsManager.getLock()).thenReturn(mLock);
         when(mCallsManager.getForegroundCall()).thenReturn(mCall);
         when(mBluetoothRouteManager.getDeviceManager()).thenReturn(mBluetoothDeviceManager);
-        when(mBluetoothDeviceManager.connectAudio(any(BluetoothDevice.class), anyInt()))
+        when(mBluetoothDeviceManager.connectAudio(any(BluetoothDevice.class), anyInt(),
+                anyBoolean()))
                 .thenReturn(true);
         when(mBluetoothDeviceManager.getBluetoothAdapter()).thenReturn(mBluetoothAdapter);
         when(mBluetoothAdapter.getActiveDevices(anyInt())).thenReturn(List.of(BLUETOOTH_DEVICE_1));
@@ -219,7 +239,7 @@
     @Test
     public void testInitializeWithoutEarpiece() {
         when(mAudioManager.getDevices(eq(AudioManager.GET_DEVICES_OUTPUTS))).thenReturn(
-                new AudioDeviceInfo[] {});
+                new AudioDeviceInfo[]{});
 
         mController.initialize();
         assertEquals(mSpeakerRoute, mController.getCurrentRoute());
@@ -238,6 +258,28 @@
 
     @SmallTest
     @Test
+    public void testEarpieceCreatedWhenWiredHeadsetDisconnected() {
+        // Initialize the controller with the wired headset.
+        AudioRoute wiredHeadsetRoute = new AudioRoute(AudioRoute.TYPE_WIRED, null, null);
+        when(mWiredHeadsetManager.isPluggedIn()).thenReturn(true);
+        mController.initialize();
+        assertEquals(wiredHeadsetRoute, mController.getCurrentRoute());
+        // Verify that the earpiece route isn't created.
+        assertFalse(mController.getAvailableRoutes().contains(mEarpieceRoute));
+        // When we disconnect the wired headset, we should create the earpiece route if it hasn't
+        // already been created.
+        mController.sendMessageWithSessionInfo(DISCONNECT_WIRED_HEADSET);
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+        // Verify that the earpiece route is created.
+        assertTrue(mController.getAvailableRoutes().contains(mEarpieceRoute));
+    }
+
+    @SmallTest
+    @Test
     public void testNormalCallRouteToEarpiece() {
         mController.initialize();
         mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS, 0);
@@ -367,7 +409,8 @@
     @SmallTest
     @Test
     public void testSwitchFocusForBluetoothDeviceSupportInbandRinging() {
-        when(mBluetoothRouteManager.isInbandRingEnabled(eq(BLUETOOTH_DEVICE_1))).thenReturn(true);
+        when(mBluetoothRouteManager.isInbandRingEnabled(eq(AudioRoute.TYPE_BLUETOOTH_SCO),
+                eq(BLUETOOTH_DEVICE_1))).thenReturn(true);
 
         mController.initialize();
         mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
@@ -384,7 +427,7 @@
 
         mController.sendMessageWithSessionInfo(SWITCH_FOCUS, RINGING_FOCUS, 0);
         verify(mBluetoothDeviceManager, timeout(TEST_TIMEOUT))
-                .connectAudio(BLUETOOTH_DEVICE_1, AudioRoute.TYPE_BLUETOOTH_SCO);
+                .connectAudio(BLUETOOTH_DEVICE_1, AudioRoute.TYPE_BLUETOOTH_SCO, false);
         assertTrue(mController.isActive());
 
         mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS, 0);
@@ -421,6 +464,28 @@
 
     @SmallTest
     @Test
+    public void testDefaultSpeakerOnWiredHeadsetDisconnect() {
+        when(mFeatureFlags.defaultSpeakerOnWiredHeadsetDisconnect()).thenReturn(true);
+        mController.initialize();
+        mController.setActive(true);
+        verifyMaybeDefaultSpeakerOnDisconnectWiredHeadset(
+                CallAudioState.ROUTE_SPEAKER /* expectedAudioType */);
+    }
+
+    @SmallTest
+    @Test
+    public void testIgnoreDefaultSpeakerOnWiredHeadsetDisconnect() {
+        when(mFeatureFlags.defaultSpeakerOnWiredHeadsetDisconnect()).thenReturn(true);
+        // Note here that the routing isn't active to represent that we're not in a call. If a wired
+        // headset is disconnected and the last route was speaker, we shouldn't switch back to
+        // speaker when we're not in a call.
+        mController.initialize();
+        verifyMaybeDefaultSpeakerOnDisconnectWiredHeadset(
+                CallAudioState.ROUTE_EARPIECE /* expectedAudioType */);
+    }
+
+    @SmallTest
+    @Test
     public void testConnectAndDisconnectDock() {
         mController.initialize();
         mController.sendMessageWithSessionInfo(CONNECT_DOCK);
@@ -619,7 +684,6 @@
         assertTrue(foundValid);
     }
 
-
     @SmallTest
     @Test
     public void testToggleMute() throws Exception {
@@ -645,6 +709,17 @@
                 anyInt(), anyString());
         verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
                 any(CallAudioState.class), eq(expectedState));
+
+        // Send TOGGLE_MUTE
+        when(mAudioManager.isMicrophoneMute()).thenReturn(false);
+        mController.sendMessageWithSessionInfo(TOGGLE_MUTE);
+        expectedState = new CallAudioState(true, CallAudioState.ROUTE_EARPIECE,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        verify(mAudioService, timeout(TEST_TIMEOUT)).setMicrophoneMute(eq(true), anyString(),
+                anyInt(), anyString());
+        verify(mCallsManager, timeout(TEST_TIMEOUT).atLeastOnce()).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
     }
 
     @SmallTest
@@ -705,7 +780,7 @@
         mController.sendMessageWithSessionInfo(DISCONNECT_WIRED_HEADSET);
         expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
                 CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER
-                        | CallAudioState.ROUTE_BLUETOOTH, null , BLUETOOTH_DEVICES);
+                        | CallAudioState.ROUTE_BLUETOOTH, null, BLUETOOTH_DEVICES);
         verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
                 any(CallAudioState.class), eq(expectedState));
     }
@@ -719,6 +794,14 @@
 
     @SmallTest
     @Test
+    public void testConnectDisconnectScoDuringCallNoClear() {
+        when(mFeatureFlags.onlyClearCommunicationDeviceOnInactive()).thenReturn(true);
+        verifyConnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_SCO);
+        verifyDisconnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_SCO);
+    }
+
+    @SmallTest
+    @Test
     public void testConnectAndDisconnectLeDeviceDuringCall() {
         when(mBluetoothLeAudio.getConnectedGroupLeadDevice(anyInt()))
                 .thenReturn(BLUETOOTH_DEVICE_1);
@@ -728,6 +811,16 @@
 
     @SmallTest
     @Test
+    public void testConnectAndDisconnectLeDeviceDuringCallNoClear() {
+        when(mFeatureFlags.onlyClearCommunicationDeviceOnInactive()).thenReturn(true);
+        when(mBluetoothLeAudio.getConnectedGroupLeadDevice(anyInt()))
+                .thenReturn(BLUETOOTH_DEVICE_1);
+        verifyConnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_LE);
+        verifyDisconnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_LE);
+    }
+
+    @SmallTest
+    @Test
     public void testConnectAndDisconnectHearingAidDuringCall() {
         verifyConnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_HA);
         verifyDisconnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_HA);
@@ -735,6 +828,14 @@
 
     @SmallTest
     @Test
+    public void testConnectAndDisconnectHearingAidDuringCallNoClear() {
+        when(mFeatureFlags.onlyClearCommunicationDeviceOnInactive()).thenReturn(true);
+        verifyConnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_HA);
+        verifyDisconnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_HA);
+    }
+
+    @SmallTest
+    @Test
     public void testSwitchBetweenLeAndScoDevices() {
         when(mBluetoothLeAudio.getConnectedGroupLeadDevice(anyInt()))
                 .thenReturn(BLUETOOTH_DEVICE_1);
@@ -775,7 +876,8 @@
     @SmallTest
     @Test
     public void testFallbackWhenBluetoothConnectionFails() {
-        when(mBluetoothDeviceManager.connectAudio(any(BluetoothDevice.class), anyInt()))
+        when(mBluetoothDeviceManager.connectAudio(any(BluetoothDevice.class), anyInt(),
+                anyBoolean()))
                 .thenReturn(false);
 
         AudioDeviceInfo mockAudioDeviceInfo = mock(AudioDeviceInfo.class);
@@ -798,7 +900,7 @@
         mController.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
                 AudioRoute.TYPE_BLUETOOTH_SCO, scoDevice.getAddress());
         verify(mBluetoothDeviceManager, timeout(TEST_TIMEOUT))
-                .connectAudio(scoDevice, AudioRoute.TYPE_BLUETOOTH_SCO);
+                .connectAudio(scoDevice, AudioRoute.TYPE_BLUETOOTH_SCO, false);
         expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
                 CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
                         | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
@@ -831,35 +933,36 @@
     public void testRouteFromBtSwitchInRingingSelected() {
         when(mFeatureFlags.ignoreAutoRouteToWatchDevice()).thenReturn(true);
         when(mBluetoothRouteManager.isWatch(any(BluetoothDevice.class))).thenReturn(true);
-        when(mBluetoothRouteManager.isInbandRingEnabled(eq(BLUETOOTH_DEVICE_1))).thenReturn(false);
+        when(mBluetoothRouteManager.isInbandRingEnabled(eq(AudioRoute.TYPE_BLUETOOTH_SCO),
+                eq(BLUETOOTH_DEVICE_1))).thenReturn(false);
 
         mController.initialize();
         mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
-            BLUETOOTH_DEVICE_1);
+                BLUETOOTH_DEVICE_1);
         CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
-            CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
-                | CallAudioState.ROUTE_SPEAKER, null, BLUETOOTH_DEVICES);
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, null, BLUETOOTH_DEVICES);
         verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
-            any(CallAudioState.class), eq(expectedState));
+                any(CallAudioState.class), eq(expectedState));
 
         mController.sendMessageWithSessionInfo(SWITCH_FOCUS, RINGING_FOCUS, 0);
         assertFalse(mController.isActive());
 
         // BT device should be cached. Verify routing into BT device once focus becomes active.
         mController.sendMessageWithSessionInfo(USER_SWITCH_BLUETOOTH, 0,
-            BLUETOOTH_DEVICE_1.getAddress());
+                BLUETOOTH_DEVICE_1.getAddress());
         expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
-            CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
-                | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
         verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
-            any(CallAudioState.class), eq(expectedState));
+                any(CallAudioState.class), eq(expectedState));
         mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS, 0);
         mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0, BLUETOOTH_DEVICE_1);
         expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
-            CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
-                | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
         verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
-            any(CallAudioState.class), eq(expectedState));
+                any(CallAudioState.class), eq(expectedState));
     }
 
     @SmallTest
@@ -1117,7 +1220,7 @@
                 watchDevice);
         CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
                 CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER
-                | CallAudioState.ROUTE_BLUETOOTH, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+                        | CallAudioState.ROUTE_BLUETOOTH, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
         verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
                 any(CallAudioState.class), eq(expectedState));
 
@@ -1156,12 +1259,12 @@
         BLUETOOTH_DEVICES.remove(watchDevice);
     }
 
-
     @Test
     @SmallTest
     public void testAbandonCallAudioFocusAfterCallEnd() {
         // Make sure in-band ringing is disabled so that route never becomes active
-        when(mBluetoothRouteManager.isInbandRingEnabled(eq(BLUETOOTH_DEVICE_1))).thenReturn(false);
+        when(mBluetoothRouteManager.isInbandRingEnabled(eq(AudioRoute.TYPE_BLUETOOTH_SCO),
+                eq(BLUETOOTH_DEVICE_1))).thenReturn(false);
 
         mController.initialize();
         mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
@@ -1203,7 +1306,7 @@
         mController.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT, audioType, BT_ADDRESS_1);
         if (audioType == AudioRoute.TYPE_BLUETOOTH_SCO) {
             verify(mBluetoothDeviceManager, timeout(TEST_TIMEOUT))
-                    .connectAudio(BLUETOOTH_DEVICE_1, AudioRoute.TYPE_BLUETOOTH_SCO);
+                    .connectAudio(BLUETOOTH_DEVICE_1, AudioRoute.TYPE_BLUETOOTH_SCO, false);
             mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED,
                     0, BLUETOOTH_DEVICE_1);
         } else {
@@ -1239,9 +1342,46 @@
         if (audioType == AudioRoute.TYPE_BLUETOOTH_SCO) {
             verify(mBluetoothDeviceManager, timeout(TEST_TIMEOUT)).disconnectSco();
         } else {
-            verify(mAudioManager, timeout(TEST_TIMEOUT)).clearCommunicationDevice();
+            if (mFeatureFlags.onlyClearCommunicationDeviceOnInactive()) {
+                verify(mAudioManager, timeout(TEST_TIMEOUT).times(2))
+                        .setCommunicationDevice(any(AudioDeviceInfo.class));
+                // Don't use a timeout here because that will cause the test to pause for a long
+                // period of time to verify; the previous verify has a timeout on it, so it will
+                // have already waited for any AudioManager invocations to take place.  Any
+                // potential clear would have happened by now.
+                verify(mAudioManager, never()).clearCommunicationDevice();
+            } else {
+                verify(mAudioManager, timeout(TEST_TIMEOUT)).clearCommunicationDevice();
+            }
         }
         verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
                 any(CallAudioState.class), eq(expectedState));
     }
+
+    private void verifyMaybeDefaultSpeakerOnDisconnectWiredHeadset(int expectedAudioType) {
+        // Ensure audio is routed to speaker initially
+        mController.sendMessageWithSessionInfo(SPEAKER_ON);
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        // Then simulate wired headset being connected after speaker was initially the audio route
+        mController.sendMessageWithSessionInfo(CONNECT_WIRED_HEADSET);
+        mController.sendMessageWithSessionInfo(SPEAKER_OFF);
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_WIRED_HEADSET,
+                CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        // Verify that we route back into speaker once the wired headset disconnects
+        mController.sendMessageWithSessionInfo(DISCONNECT_WIRED_HEADSET);
+        expectedState = new CallAudioState(false, expectedAudioType,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+    }
 }
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
index e97de2e..ac90fd1 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
@@ -113,7 +113,6 @@
     @Before
     public void setUp() throws Exception {
         super.setUp();
-        MockitoAnnotations.initMocks(this);
         mThreadHandler = new HandlerThread("CallAudioRouteStateMachineTest");
         mThreadHandler.start();
         mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
@@ -144,7 +143,6 @@
         doNothing().when(mockConnectionServiceWrapper).onCallAudioStateChanged(any(Call.class),
                 any(CallAudioState.class));
         when(mFeatureFlags.ignoreAutoRouteToWatchDevice()).thenReturn(false);
-        when(mFeatureFlags.callAudioCommunicationDeviceRefactor()).thenReturn(false);
     }
 
     @Override
@@ -838,7 +836,10 @@
         ArgumentCaptor<AudioDeviceInfo> infoArgumentCaptor = ArgumentCaptor.forClass(
                 AudioDeviceInfo.class);
         verify(mockAudioManager).setCommunicationDevice(infoArgumentCaptor.capture());
-        assertEquals(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, infoArgumentCaptor.getValue().getType());
+        var deviceType = infoArgumentCaptor.getValue().getType();
+        if (deviceType != AudioDeviceInfo.TYPE_BUS) { // on automotive, we expect BUS
+            assertEquals(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, deviceType);
+        }
     }
 
     @SmallTest
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteTransitionTests.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteTransitionTests.java
index 6b9b5c8..c288d17 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRouteTransitionTests.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteTransitionTests.java
@@ -172,7 +172,6 @@
     @Before
     public void setUp() throws Exception {
         super.setUp();
-        MockitoAnnotations.initMocks(this);
         mHandlerThread = new HandlerThread("CallAudioRouteTransitionTests");
         mHandlerThread.start();
         mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
@@ -344,8 +343,10 @@
                 ArgumentCaptor<AudioDeviceInfo> infoArgumentCaptor = ArgumentCaptor.forClass(
                         AudioDeviceInfo.class);
                 verify(mockAudioManager).setCommunicationDevice(infoArgumentCaptor.capture());
-                assertEquals(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER,
-                        infoArgumentCaptor.getValue().getType());
+                var deviceType = infoArgumentCaptor.getValue().getType();
+                if (deviceType != AudioDeviceInfo.TYPE_BUS) { // on automotive, we expect BUS
+                    assertEquals(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, deviceType);
+                }
                 break;
             case OFF:
                 verify(mockAudioManager).clearCommunicationDevice();
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioWatchdogTest.java b/tests/src/com/android/server/telecom/tests/CallAudioWatchdogTest.java
new file mode 100644
index 0000000..4f988f1
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/CallAudioWatchdogTest.java
@@ -0,0 +1,296 @@
+/*
+ * 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 android.media.AudioPlaybackConfiguration.PLAYER_STATE_IDLE;
+import static android.media.AudioPlaybackConfiguration.PLAYER_STATE_STARTED;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+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.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+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.annotation.NonNull;
+import android.content.ComponentName;
+import android.media.AudioAttributes;
+import android.media.AudioDeviceInfo;
+import android.media.AudioPlaybackConfiguration;
+import android.media.AudioRecordingConfiguration;
+import android.media.IPlayer;
+import android.media.MediaRecorder;
+import android.media.PlayerBase;
+import android.telecom.PhoneAccountHandle;
+import android.util.ArrayMap;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallAudioWatchdog;
+import com.android.server.telecom.ClockProxy;
+import com.android.server.telecom.metrics.CallStats;
+import com.android.server.telecom.metrics.TelecomMetricsController;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * Tests for {@link com.android.server.telecom.CallAudioWatchdog}.
+ */
+@RunWith(JUnit4.class)
+public class CallAudioWatchdogTest extends TelecomTestCase {
+    private static final String TEST_CALL_ID = "TC@90210";
+    private static final int TEST_APP_1_UID = 10001;
+    private static final int TEST_APP_2_UID = 10002;
+    private static final PhoneAccountHandle TEST_APP_1_HANDLE = new PhoneAccountHandle(
+            new ComponentName("com.app1.package", "class1"), "1");
+    private static final ArrayMap<Integer, PhoneAccountHandle> TEST_UID_TO_PHAC = new ArrayMap<>();
+    private CallAudioWatchdog.PhoneAccountRegistrarProxy mPhoneAccountRegistrarProxy =
+            new CallAudioWatchdog.PhoneAccountRegistrarProxy() {
+                @Override
+                public boolean hasPhoneAccountForUid(int uid) {
+                    return TEST_UID_TO_PHAC.containsKey(uid);
+                }
+
+                @Override
+                public int getUidForPhoneAccountHandle(PhoneAccountHandle handle) {
+                    Optional<Map.Entry<Integer, PhoneAccountHandle>> entry =
+                            TEST_UID_TO_PHAC.entrySet().stream().filter(
+                                    e -> e.getValue().equals(handle)).findFirst();
+                    if (entry.isPresent()) {
+                        return entry.get().getKey();
+                    } else {
+                        return -1;
+                    }
+                }
+            };
+
+    @Mock private ClockProxy mClockProxy;
+    @Mock private TelecomMetricsController mMetricsController;
+    @Mock private CallStats mCallStats;
+    private CallAudioWatchdog mCallAudioWatchdog;
+
+    @Override
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        when(mMetricsController.getCallStats()).thenReturn(mCallStats);
+        when(mClockProxy.elapsedRealtime()).thenReturn(0L);
+        TEST_UID_TO_PHAC.put(TEST_APP_1_UID, TEST_APP_1_HANDLE);
+        mCallAudioWatchdog = new CallAudioWatchdog(mComponentContextFixture.getAudioManager(),
+                mPhoneAccountRegistrarProxy, mClockProxy, null /* mHandler */, mMetricsController);
+    }
+
+    @Override
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    /**
+     * Verifies that a new Telecom call added results in a session being added for that call.
+     */
+    @Test
+    public void testAddTelecomCall() {
+        Call mockCall = createMockCall();
+        mCallAudioWatchdog.onCallAdded(mockCall);
+        assertTrue(mCallAudioWatchdog.getCommunicationSessions().containsKey(TEST_APP_1_UID));
+        CallAudioWatchdog.CommunicationSession session = mCallAudioWatchdog
+                .getCommunicationSessions().get(TEST_APP_1_UID);
+        assertFalse(session.hasMediaResources());
+        assertEquals(TEST_CALL_ID, session.getTelecomCall().getId());
+    }
+
+    /**
+     * Verifies tracking of multiple audio sessions.
+     */
+    @Test
+    public void testTrackAudioPlayback() {
+        var client1Idle = makeAudioPlaybackConfiguration(
+                TEST_APP_1_UID, PLAYER_STATE_IDLE, 1);
+        mCallAudioWatchdog.getWatchdogAudioPlayback().onPlaybackConfigChanged(
+                Arrays.asList(client1Idle));
+        assertFalse(mCallAudioWatchdog.getCommunicationSessions().containsKey(TEST_APP_1_UID));
+
+        var client1Playing = makeAudioPlaybackConfiguration(
+                TEST_APP_1_UID, PLAYER_STATE_STARTED, 1);
+        mCallAudioWatchdog.getWatchdogAudioPlayback().onPlaybackConfigChanged(
+                Arrays.asList(client1Playing));
+        assertTrue(mCallAudioWatchdog.getCommunicationSessions().containsKey(TEST_APP_1_UID));
+
+        var client2Playing = makeAudioPlaybackConfiguration(
+                TEST_APP_1_UID, PLAYER_STATE_STARTED, 2);
+        mCallAudioWatchdog.getWatchdogAudioPlayback().onPlaybackConfigChanged(
+                Arrays.asList(client1Playing, client2Playing));
+        assertTrue(mCallAudioWatchdog.getCommunicationSessions().containsKey(TEST_APP_1_UID));
+
+        mCallAudioWatchdog.getWatchdogAudioPlayback().onPlaybackConfigChanged(
+                Arrays.asList(client2Playing));
+        assertTrue(mCallAudioWatchdog.getCommunicationSessions().containsKey(TEST_APP_1_UID));
+
+        mCallAudioWatchdog.getWatchdogAudioPlayback().onPlaybackConfigChanged(
+                Arrays.asList(makeAudioPlaybackConfiguration(
+                        TEST_APP_1_UID, PLAYER_STATE_IDLE, 2)));
+        assertFalse(mCallAudioWatchdog.getCommunicationSessions().containsKey(TEST_APP_1_UID));
+    }
+
+    /**
+     * Verifies ability of the audio watchdog to handle changes to the audio record configs.
+     */
+    @Test
+    public void testTrackAudioRecord() {
+        var client1Recording = makeAudioRecordingConfiguration(TEST_APP_1_UID, 1);
+        var theRecords = Arrays.asList(client1Recording);
+        when(mComponentContextFixture.getAudioManager().getActiveRecordingConfigurations())
+                .thenReturn(theRecords);
+        mCallAudioWatchdog.getWatchdogAudioRecordCallack().onRecordingConfigChanged(theRecords);
+        assertTrue(mCallAudioWatchdog.getCommunicationSessions().containsKey(TEST_APP_1_UID));
+
+        var client2Recording = makeAudioRecordingConfiguration(TEST_APP_1_UID, 2);
+        theRecords = Arrays.asList(client1Recording, client2Recording);
+        when(mComponentContextFixture.getAudioManager().getActiveRecordingConfigurations())
+                .thenReturn(theRecords);
+        mCallAudioWatchdog.getWatchdogAudioRecordCallack().onRecordingConfigChanged(theRecords);
+        assertTrue(mCallAudioWatchdog.getCommunicationSessions().containsKey(TEST_APP_1_UID));
+
+        theRecords = Arrays.asList(client2Recording);
+        when(mComponentContextFixture.getAudioManager().getActiveRecordingConfigurations())
+                .thenReturn(theRecords);
+        mCallAudioWatchdog.getWatchdogAudioRecordCallack().onRecordingConfigChanged(theRecords);
+        assertTrue(mCallAudioWatchdog.getCommunicationSessions().containsKey(TEST_APP_1_UID));
+
+        when(mComponentContextFixture.getAudioManager().getActiveRecordingConfigurations())
+                .thenReturn(Collections.EMPTY_LIST);
+        when(mClockProxy.elapsedRealtime()).thenReturn(1000L);
+        mCallAudioWatchdog.getWatchdogAudioRecordCallack().onRecordingConfigChanged(
+                Collections.EMPTY_LIST);
+        assertFalse(mCallAudioWatchdog.getCommunicationSessions().containsKey(TEST_APP_1_UID));
+
+        // Ensure that a call with telecom support but which did not use Telecom gets logged to
+        // metrics as a non-telecom call.
+        verify(mCallStats).onNonTelecomCallEnd(eq(true), eq(TEST_APP_1_UID), eq(1000L));
+    }
+
+    /**
+     * Verifies ability of the audio watchdog to track non-telecom calls where there is no Telecom
+     * integration.
+     */
+    @Test
+    public void testNonTelecomCallMetricsTracking() {
+        var client1Recording = makeAudioRecordingConfiguration(TEST_APP_2_UID, 1);
+        var theRecords = Arrays.asList(client1Recording);
+        when(mComponentContextFixture.getAudioManager().getActiveRecordingConfigurations())
+                .thenReturn(theRecords);
+        mCallAudioWatchdog.getWatchdogAudioRecordCallack().onRecordingConfigChanged(theRecords);
+        assertTrue(mCallAudioWatchdog.getCommunicationSessions().containsKey(TEST_APP_2_UID));
+
+        when(mComponentContextFixture.getAudioManager().getActiveRecordingConfigurations())
+                .thenReturn(Collections.EMPTY_LIST);
+        when(mClockProxy.elapsedRealtime()).thenReturn(1000L);
+        mCallAudioWatchdog.getWatchdogAudioRecordCallack().onRecordingConfigChanged(
+                Collections.EMPTY_LIST);
+        assertFalse(mCallAudioWatchdog.getCommunicationSessions().containsKey(TEST_APP_2_UID));
+
+        // This should log as a non-telecom call with no telecom support.
+        verify(mCallStats).onNonTelecomCallEnd(eq(false), eq(TEST_APP_2_UID), eq(1000L));
+    }
+
+    /**
+     * Verifies that if a call known to Telecom is added, that we don't try to track it in the
+     * non-telecom metrics.
+     */
+    @Test
+    public void testTelecomCallMetricsTracking() {
+        var client1Recording = makeAudioRecordingConfiguration(TEST_APP_1_UID, 1);
+        var theRecords = Arrays.asList(client1Recording);
+        when(mComponentContextFixture.getAudioManager().getActiveRecordingConfigurations())
+                .thenReturn(theRecords);
+        mCallAudioWatchdog.getWatchdogAudioRecordCallack().onRecordingConfigChanged(theRecords);
+        assertTrue(mCallAudioWatchdog.getCommunicationSessions().containsKey(TEST_APP_1_UID));
+
+        Call mockCall = mock(Call.class);
+        when(mockCall.isSelfManaged()).thenReturn(true);
+        when(mockCall.isExternalCall()).thenReturn(false);
+        when(mockCall.getTargetPhoneAccount()).thenReturn(TEST_APP_1_HANDLE);
+        when(mockCall.getId()).thenReturn("90210");
+        mCallAudioWatchdog.onCallAdded(mockCall);
+
+        when(mComponentContextFixture.getAudioManager().getActiveRecordingConfigurations())
+                .thenReturn(Collections.EMPTY_LIST);
+        when(mClockProxy.elapsedRealtime()).thenReturn(1000L);
+        mCallAudioWatchdog.getWatchdogAudioRecordCallack().onRecordingConfigChanged(
+                Collections.EMPTY_LIST);
+        assertTrue(mCallAudioWatchdog.getCommunicationSessions().containsKey(TEST_APP_1_UID));
+
+        mCallAudioWatchdog.onCallRemoved(mockCall);
+        assertFalse(mCallAudioWatchdog.getCommunicationSessions().containsKey(TEST_APP_1_UID));
+
+        // We should not log a non-telecom call.  Note; we are purposely NOT trying to check if a
+        // Telecom call metric is logged here since that is done elsewhere and this unit test is
+        // only testing CallAudioWatchdog in isolation.
+        verify(mCallStats, never()).onNonTelecomCallEnd(anyBoolean(), anyInt(), anyLong());
+
+    }
+
+    private AudioPlaybackConfiguration makeAudioPlaybackConfiguration(int clientUid,
+            int playerState, int playerInterfaceId) {
+        AudioAttributes attributes = new AudioAttributes.Builder()
+                             .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
+                             .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
+                             .build();
+        AudioPlaybackConfiguration configuration = mock(AudioPlaybackConfiguration.class);
+        when(configuration.getAudioAttributes()).thenReturn(attributes);
+        when(configuration.getClientUid()).thenReturn(clientUid);
+        when(configuration.getPlayerState()).thenReturn(playerState);
+        when(configuration.getPlayerInterfaceId()).thenReturn(playerInterfaceId);
+        return configuration;
+    }
+
+    private AudioRecordingConfiguration makeAudioRecordingConfiguration(int clientUid,
+            int clientAudioSessionId) {
+        AudioRecordingConfiguration configuration = mock(AudioRecordingConfiguration.class);
+        when(configuration.getClientUid()).thenReturn(clientUid);
+        when(configuration.getClientAudioSource()).thenReturn(
+                MediaRecorder.AudioSource.VOICE_COMMUNICATION);
+        when(configuration.getClientAudioSessionId()).thenReturn(clientAudioSessionId);
+        return configuration;
+    }
+
+    private Call createMockCall() {
+        Call mockCall = mock(Call.class);
+        when(mockCall.getId()).thenReturn(TEST_CALL_ID);
+        when(mockCall.isSelfManaged()).thenReturn(true);
+        when(mockCall.getTargetPhoneAccount()).thenReturn(TEST_APP_1_HANDLE);
+        return mockCall;
+    }
+}
diff --git a/tests/src/com/android/server/telecom/tests/CallRedirectionProcessorTest.java b/tests/src/com/android/server/telecom/tests/CallRedirectionProcessorTest.java
index 241216a..185c08f 100644
--- a/tests/src/com/android/server/telecom/tests/CallRedirectionProcessorTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallRedirectionProcessorTest.java
@@ -221,6 +221,9 @@
         verify(mCallsManager, times(1)).onCallRedirectionComplete(eq(mCall), any(),
                 eq(mPhoneAccountHandle), eq(null), eq(SPEAKER_PHONE_ON), eq(VIDEO_STATE),
                 eq(false), eq(CallRedirectionProcessor.UI_TYPE_NO_ACTION));
+        // Verify service was unbound
+        verify(mContext, times(1)).
+                unbindService(any(ServiceConnection.class));
     }
 
     @Test
@@ -249,6 +252,9 @@
         verify(mCallsManager, times(1)).onCallRedirectionComplete(eq(mCall), any(),
                 eq(mPhoneAccountHandle), eq(null), eq(SPEAKER_PHONE_ON), eq(VIDEO_STATE),
                 eq(true), eq(CallRedirectionProcessor.UI_TYPE_USER_DEFINED_TIMEOUT));
+        // Verify service was unbound
+        verify(mContext, times(1)).
+                unbindService(any(ServiceConnection.class));
     }
 
     @Test
@@ -280,6 +286,9 @@
         verify(mCallsManager, times(1)).onCallRedirectionComplete(eq(mCall), any(),
                 eq(mPhoneAccountHandle), eq(null), eq(SPEAKER_PHONE_ON), eq(VIDEO_STATE),
                 eq(true), eq(CallRedirectionProcessor.UI_TYPE_USER_DEFINED_TIMEOUT));
+        // Verify service was unbound
+        verify(mContext, times(1)).
+                unbindService(any(ServiceConnection.class));
 
         // Wait for another carrier timeout time, but should not expect any carrier service request
         // is triggered.
@@ -289,6 +298,9 @@
         verify(mCallsManager, times(1)).onCallRedirectionComplete(eq(mCall), any(),
                 eq(mPhoneAccountHandle), eq(null), eq(SPEAKER_PHONE_ON), eq(VIDEO_STATE),
                 eq(true), eq(CallRedirectionProcessor.UI_TYPE_USER_DEFINED_TIMEOUT));
+        // Verify service was unbound
+        verify(mContext, times(1)).
+                unbindService(any(ServiceConnection.class));
     }
 
     @Test
diff --git a/tests/src/com/android/server/telecom/tests/CallSequencingTests.java b/tests/src/com/android/server/telecom/tests/CallSequencingTests.java
new file mode 100644
index 0000000..9cfc95c
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/CallSequencingTests.java
@@ -0,0 +1,722 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.telecom.tests;
+
+import static com.android.server.telecom.CallsManager.CALL_FILTER_ALL;
+import static com.android.server.telecom.CallsManager.ONGOING_CALL_STATES;
+
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.TestCase.fail;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.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.when;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.OutcomeReceiver;
+import android.os.PersistableBundle;
+import android.os.UserHandle;
+import android.telecom.CallAttributes;
+import android.telecom.CallException;
+import android.telecom.Connection;
+import android.telecom.DisconnectCause;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.CarrierConfigManager;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.server.telecom.Analytics;
+import com.android.server.telecom.AnomalyReporterAdapter;
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallState;
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.ClockProxy;
+import com.android.server.telecom.ConnectionServiceFocusManager;
+import com.android.server.telecom.PhoneAccountRegistrar;
+import com.android.server.telecom.Timeouts;
+import com.android.server.telecom.callsequencing.CallSequencingController;
+import com.android.server.telecom.callsequencing.CallTransaction;
+import com.android.server.telecom.callsequencing.voip.OutgoingCallTransactionSequencing;
+import com.android.server.telecom.metrics.TelecomMetricsController;
+import com.android.server.telecom.stats.CallFailureCause;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(JUnit4.class)
+public class CallSequencingTests extends TelecomTestCase {
+    private static final long SEQUENCING_TIMEOUT_MS = 2000L;
+    private static final PhoneAccountHandle mHandle1 = new PhoneAccountHandle(
+            new ComponentName("foo", "bar"), "1");
+    private static final PhoneAccountHandle mHandle2 = new PhoneAccountHandle(
+            new ComponentName("bar", "foo"), "2");
+    private static final String TEST_NAME = "Alan Turing";
+    private static final Uri TEST_URI = Uri.fromParts("tel", "abc", "123");
+    private static final String ACTIVE_CALL_ID = "TC@1";
+    private static final String NEW_CALL_ID = "TC@2";
+
+    private CallSequencingController mController;
+    @Mock
+    private CallsManager mCallsManager;
+    @Mock Context mContext;
+    @Mock ClockProxy mClockProxy;
+    @Mock AnomalyReporterAdapter mAnomalyReporter;
+    @Mock Timeouts.Adapter mTimeoutsAdapter;
+    @Mock TelecomMetricsController mMetricsController;
+    @Mock
+    ConnectionServiceFocusManager mConnectionServiceFocusManager;
+    @Mock Call mActiveCall;
+    @Mock Call mHeldCall;
+    @Mock Call mNewCall;
+    @Mock Call mRingingCall;
+
+    @Override
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        when(mFeatureFlags.enableCallSequencing()).thenReturn(true);
+        mController = new CallSequencingController(mCallsManager, mContext, mClockProxy,
+                mAnomalyReporter, mTimeoutsAdapter, mMetricsController, mFeatureFlags);
+
+        when(mActiveCall.getState()).thenReturn(CallState.ACTIVE);
+        when(mRingingCall.getState()).thenReturn(CallState.RINGING);
+        when(mHeldCall.getState()).thenReturn(CallState.ON_HOLD);
+
+        when(mActiveCall.getId()).thenReturn(ACTIVE_CALL_ID);
+        when(mNewCall.getId()).thenReturn(NEW_CALL_ID);
+    }
+
+    @Override
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+
+    @Test
+    @SmallTest
+    public void testTransactionOutgoingCall_CallNotPermitted() {
+        String callingPkg = "testPkg";
+        CallAttributes outgoingCallAttributes = getOutgoingCallAttributes();
+
+        // Outgoing call is not permitted
+        when(mCallsManager.isOutgoingCallPermitted(mHandle1)).thenReturn(false);
+        CompletableFuture<CallTransaction> transactionFuture = mController
+                .createTransactionalOutgoingCall("callId", outgoingCallAttributes,
+                        new Bundle(), callingPkg);
+        OutgoingCallTransactionSequencing transaction = (OutgoingCallTransactionSequencing)
+                transactionFuture.getNow(null);
+        assertNotNull(transaction);
+        assertTrue(transaction.getCallNotPermitted());
+
+        // Call future is null
+        when(mCallsManager.isOutgoingCallPermitted(mHandle1)).thenReturn(true);
+        when(mCallsManager.startOutgoingCall(any(Uri.class), any(PhoneAccountHandle.class),
+                any(Bundle.class), any(UserHandle.class), any(Intent.class), anyString()))
+                .thenReturn(null);
+        transactionFuture = mController
+                .createTransactionalOutgoingCall("callId", outgoingCallAttributes,
+                        new Bundle(), callingPkg);
+        transaction = (OutgoingCallTransactionSequencing) transactionFuture
+                .getNow(null);
+        assertNotNull(transaction);
+        assertTrue(transaction.getCallNotPermitted());
+    }
+
+    @Test
+    @SmallTest
+    public void testTransactionOutgoingCall() {
+        String callingPkg = "testPkg";
+        CallAttributes outgoingCallAttributes = getOutgoingCallAttributes();
+
+        when(mCallsManager.isOutgoingCallPermitted(mHandle1)).thenReturn(true);
+        when(mCallsManager.startOutgoingCall(any(Uri.class), any(PhoneAccountHandle.class),
+                any(Bundle.class), any(UserHandle.class), any(Intent.class), anyString()))
+                .thenReturn(CompletableFuture.completedFuture(mNewCall));
+        CompletableFuture<CallTransaction> transactionFuture = mController
+                .createTransactionalOutgoingCall("callId", outgoingCallAttributes,
+                        new Bundle(), callingPkg);
+        try {
+            OutgoingCallTransactionSequencing transaction = (OutgoingCallTransactionSequencing)
+                    transactionFuture.get(SEQUENCING_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+            assertNotNull(transaction);
+            assertFalse(transaction.getCallNotPermitted());
+        } catch (Exception e) {
+            fail("Failed to retrieve future in allocated time (" + SEQUENCING_TIMEOUT_MS + ").");
+        }
+    }
+
+    @SmallTest
+    @Test
+    public void testAnswerCall() {
+        // This will allow holdActiveCallForNewCallWithSequencing to immediately return true
+        setActiveCallFocus(null);
+        mController.answerCall(mNewCall, 0);
+        verify(mCallsManager, timeout(SEQUENCING_TIMEOUT_MS))
+                .requestFocusActionAnswerCall(eq(mNewCall), eq(0));
+    }
+
+    @SmallTest
+    @Test
+    public void testAnswerCallFail() {
+        setupHoldActiveCallForNewCallFailMocks();
+        mController.answerCall(mNewCall, 0);
+        verify(mCallsManager, timeout(SEQUENCING_TIMEOUT_MS).times(0))
+                .requestFocusActionAnswerCall(eq(mNewCall), eq(0));
+    }
+
+    @SmallTest
+    @Test
+    public void testSetSelfManagedCallActive() {
+        // This will allow holdActiveCallForNewCallWithSequencing to immediately return true
+        setActiveCallFocus(null);
+        mController.handleSetSelfManagedCallActive(mNewCall);
+        verify(mCallsManager, timeout(SEQUENCING_TIMEOUT_MS))
+                .requestActionSetActiveCall(eq(mNewCall), anyString());
+    }
+
+    @SmallTest
+    @Test
+    public void testSetSelfManagedCallActiveFail() {
+        setupHoldActiveCallForNewCallFailMocks();
+        mController.handleSetSelfManagedCallActive(mNewCall);
+        verify(mCallsManager, timeout(SEQUENCING_TIMEOUT_MS).times(0))
+                .requestActionSetActiveCall(eq(mNewCall), anyString());
+    }
+
+    @SmallTest
+    @Test
+    public void testTransactionHoldActiveCallForNewCall() throws InterruptedException {
+        // This will allow holdActiveCallForNewCallWithSequencing to immediately return true
+        setActiveCallFocus(null);
+        CountDownLatch latch = new CountDownLatch(1);
+        OutcomeReceiver<Boolean, CallException> callback = new OutcomeReceiver<>() {
+            @Override
+            public void onResult(Boolean result) {
+                // Expected result
+                latch.countDown();
+            }
+            @Override
+            public void onError(CallException exception) {
+            }
+        };
+        verifyTransactionHoldActiveCallForNewCall(callback, latch);
+    }
+
+    @SmallTest
+    @Test
+    public void testTransactionHoldActiveCallForNewCallFail() {
+        setupHoldActiveCallForNewCallFailMocks();
+        CountDownLatch latch = new CountDownLatch(1);
+        OutcomeReceiver<Boolean, CallException> callback = new OutcomeReceiver<>() {
+            @Override
+            public void onResult(Boolean result) {
+            }
+
+            @Override
+            public void onError(CallException exception) {
+                // Expected result
+                latch.countDown();
+            }
+        };
+        verifyTransactionHoldActiveCallForNewCall(callback, latch);
+    }
+
+    @Test
+    @SmallTest
+    public void testHoldCallForNewCall_NoActiveCall() {
+        setActiveCallFocus(null);
+        CompletableFuture<Boolean> resultFuture = mController
+                .holdActiveCallForNewCallWithSequencing(mNewCall);
+        assertTrue(waitForFutureResult(resultFuture, false));
+    }
+
+    @Test
+    @SmallTest
+    public void testHoldCallForNewCall_CanHold() {
+        setPhoneAccounts(mNewCall, mActiveCall, false);
+        setActiveCallFocus(mActiveCall);
+        when(mCallsManager.canHold(mActiveCall)).thenReturn(true);
+        when(mActiveCall.hold(anyString())).thenReturn(CompletableFuture.completedFuture(true));
+
+        // Cross phone account case (sequencing enabled)
+        assertFalse(mController.arePhoneAccountsSame(mNewCall, mActiveCall));
+        CompletableFuture<Boolean> resultFuture = mController
+                .holdActiveCallForNewCallWithSequencing(mNewCall);
+        assertTrue(waitForFutureResult(resultFuture, false));
+
+        // Same phone account case
+        setPhoneAccounts(mNewCall, mActiveCall, true);
+        assertTrue(mController.arePhoneAccountsSame(mNewCall, mActiveCall));
+        resultFuture = mController.holdActiveCallForNewCallWithSequencing(mNewCall);
+        assertTrue(waitForFutureResult(resultFuture, false));
+    }
+
+    @Test
+    @SmallTest
+    public void testHoldCallForNewCall_SupportsHold() {
+        setPhoneAccounts(mNewCall, mActiveCall, false);
+        setActiveCallFocus(mActiveCall);
+        when(mCallsManager.canHold(mActiveCall)).thenReturn(false);
+        when(mCallsManager.supportsHold(mActiveCall)).thenReturn(true);
+        when(mCallsManager.getFirstCallWithState(anyInt())).thenReturn(mHeldCall);
+        when(mHeldCall.isSelfManaged()).thenReturn(true);
+        when(mNewCall.isSelfManaged()).thenReturn(false);
+        when(mHeldCall.disconnect()).thenReturn(CompletableFuture.completedFuture(true));
+        when(mActiveCall.hold()).thenReturn(CompletableFuture.completedFuture(true));
+
+        // Verify that we abort transaction when there's a new (VOIP) call and we're trying to
+        // disconnect the active (carrier) call.
+        assertFalse(mController.arePhoneAccountsSame(mNewCall, mActiveCall));
+        CompletableFuture<Boolean> resultFuture = mController
+                .holdActiveCallForNewCallWithSequencing(mNewCall);
+        verify(mHeldCall, timeout(SEQUENCING_TIMEOUT_MS)).disconnect();
+        verify(mActiveCall, timeout(SEQUENCING_TIMEOUT_MS)).hold();
+        verify(mNewCall).increaseHeldByThisCallCount();
+        assertTrue(waitForFutureResult(resultFuture, false));
+    }
+
+    @Test
+    @SmallTest
+    public void testHoldCallForNewCall_SupportsHold_NoHeldCall() {
+        setPhoneAccounts(mNewCall, mActiveCall, false);
+        setActiveCallFocus(mActiveCall);
+        when(mCallsManager.canHold(mActiveCall)).thenReturn(false);
+        when(mCallsManager.supportsHold(mActiveCall)).thenReturn(true);
+        when(mCallsManager.getFirstCallWithState(anyInt())).thenReturn(null);
+        when(mActiveCall.hold()).thenReturn(CompletableFuture.completedFuture(true));
+
+        // Cross phone account case (sequencing enabled)
+        assertFalse(mController.arePhoneAccountsSame(mNewCall, mActiveCall));
+        CompletableFuture<Boolean> resultFuture = mController
+                .holdActiveCallForNewCallWithSequencing(mNewCall);
+        verify(mActiveCall, timeout(SEQUENCING_TIMEOUT_MS)).hold();
+        verify(mNewCall).increaseHeldByThisCallCount();
+        assertTrue(waitForFutureResult(resultFuture, false));
+    }
+
+    @Test
+    @SmallTest
+    public void testHoldCallForNewCall_DoesNotSupportHold_Disconnect() {
+        setPhoneAccounts(mNewCall, mActiveCall, false);
+        setActiveCallFocus(mActiveCall);
+        when(mCallsManager.getCalls()).thenReturn(Collections.singletonList(mActiveCall));
+        when(mCallsManager.canHold(mActiveCall)).thenReturn(false);
+        when(mCallsManager.supportsHold(mActiveCall)).thenReturn(false);
+        when(mActiveCall.disconnect(anyString())).thenReturn(
+                CompletableFuture.completedFuture(true));
+        when(mActiveCall.isEmergencyCall()).thenReturn(false);
+
+        assertFalse(mController.arePhoneAccountsSame(mNewCall, mActiveCall));
+        CompletableFuture<Boolean> resultFuture = mController
+                .holdActiveCallForNewCallWithSequencing(mNewCall);
+        verify(mActiveCall, timeout(SEQUENCING_TIMEOUT_MS)).disconnect(anyString());
+        assertTrue(waitForFutureResult(resultFuture, false));
+    }
+
+    @Test
+    @SmallTest
+    public void testHoldCallForNewCallFail_SupportsHold_VoipPstn() {
+        setPhoneAccounts(mNewCall, mActiveCall, false);
+        setActiveCallFocus(mActiveCall);
+        when(mCallsManager.canHold(mActiveCall)).thenReturn(false);
+        when(mCallsManager.supportsHold(mActiveCall)).thenReturn(true);
+        when(mCallsManager.getFirstCallWithState(anyInt())).thenReturn(mHeldCall);
+        when(mHeldCall.isSelfManaged()).thenReturn(false);
+        when(mNewCall.isSelfManaged()).thenReturn(true);
+
+        // Verify that we abort transaction when there's a new (VOIP) call and we're trying to
+        // disconnect the active (carrier) call.
+        assertFalse(mController.arePhoneAccountsSame(mNewCall, mActiveCall));
+        CompletableFuture<Boolean> resultFuture = mController
+                .holdActiveCallForNewCallWithSequencing(mNewCall);
+        assertFalse(waitForFutureResult(resultFuture, true));
+    }
+
+    @Test
+    @SmallTest
+    public void testHoldCallForNewCall_DoesNotSupportHold_SameManagedPA() {
+        setPhoneAccounts(mNewCall, mActiveCall, true);
+        setActiveCallFocus(mActiveCall);
+        when(mCallsManager.canHold(mActiveCall)).thenReturn(false);
+        when(mCallsManager.supportsHold(mActiveCall)).thenReturn(false);
+        when(mActiveCall.isEmergencyCall()).thenReturn(false);
+
+        assertTrue(mController.arePhoneAccountsSame(mNewCall, mActiveCall));
+        CompletableFuture<Boolean> resultFuture = mController
+                .holdActiveCallForNewCallWithSequencing(mNewCall);
+        assertTrue(waitForFutureResult(resultFuture, true));
+    }
+
+    @Test
+    @SmallTest
+    public void testHoldCallForNewCallFail_DoesNotSupportHold_Reject() {
+        setPhoneAccounts(mNewCall, mActiveCall, false);
+        setActiveCallFocus(mActiveCall);
+        when(mCallsManager.canHold(mActiveCall)).thenReturn(false);
+        when(mCallsManager.supportsHold(mActiveCall)).thenReturn(false);
+        when(mNewCall.reject(anyBoolean(), anyString(), anyString()))
+                .thenReturn(CompletableFuture.completedFuture(true));
+        when(mActiveCall.isEmergencyCall()).thenReturn(true);
+
+        assertFalse(mController.arePhoneAccountsSame(mNewCall, mActiveCall));
+        CompletableFuture<Boolean> resultFuture = mController
+                .holdActiveCallForNewCallWithSequencing(mNewCall);
+        verify(mNewCall, timeout(SEQUENCING_TIMEOUT_MS)).reject(
+                anyBoolean(), anyString(), anyString());
+        assertFalse(waitForFutureResult(resultFuture, true));
+    }
+
+    @Test
+    @SmallTest
+    public void testHoldCallForNewCallFail_DoesNotSupportHold_Abort() {
+        setPhoneAccounts(mNewCall, mActiveCall, false);
+        setActiveCallFocus(mActiveCall);
+        when(mCallsManager.canHold(mActiveCall)).thenReturn(false);
+        when(mCallsManager.supportsHold(mActiveCall)).thenReturn(false);
+        when(mActiveCall.isEmergencyCall()).thenReturn(false);
+        when(mActiveCall.isSelfManaged()).thenReturn(false);
+        when(mNewCall.isSelfManaged()).thenReturn(true);
+
+        assertFalse(mController.arePhoneAccountsSame(mNewCall, mActiveCall));
+        CompletableFuture<Boolean> resultFuture = mController
+                .holdActiveCallForNewCallWithSequencing(mNewCall);
+        assertFalse(waitForFutureResult(resultFuture, true));
+    }
+
+    @Test
+    @SmallTest
+    public void testUnholdCallNoActiveCall() {
+        setActiveCallFocus(null);
+        mController.unholdCall(mHeldCall);
+        verify(mCallsManager).requestActionUnholdCall(eq(mHeldCall), eq(null));
+    }
+
+    @Test
+    @SmallTest
+    public void testUnholdCallSwapCase() {
+        when(mActiveCall.can(eq(Connection.CAPABILITY_SUPPORT_HOLD))).thenReturn(true);
+        when(mActiveCall.hold(anyString())).thenReturn(CompletableFuture.completedFuture(true));
+        when(mActiveCall.isLocallyDisconnecting()).thenReturn(false);
+        setPhoneAccounts(mHeldCall, mActiveCall, false);
+        setActiveCallFocus(mActiveCall);
+
+        mController.unholdCall(mHeldCall);
+        assertFalse(mController.arePhoneAccountsSame(mActiveCall, mHeldCall));
+        verify(mActiveCall).hold(anyString());
+        verify(mCallsManager, timeout(SEQUENCING_TIMEOUT_MS))
+                .requestActionUnholdCall(eq(mHeldCall), eq(ACTIVE_CALL_ID));
+    }
+
+    @Test
+    @SmallTest
+    public void testUnholdCallFail_DoesNotSupportHold() {
+        when(mActiveCall.can(eq(Connection.CAPABILITY_SUPPORT_HOLD))).thenReturn(false);
+        when(mActiveCall.isEmergencyCall()).thenReturn(true);
+        when(mActiveCall.isLocallyDisconnecting()).thenReturn(false);
+        setPhoneAccounts(mHeldCall, mActiveCall, false);
+        setActiveCallFocus(mActiveCall);
+
+        // Emergency call case
+        mController.unholdCall(mHeldCall);
+        assertFalse(mController.arePhoneAccountsSame(mActiveCall, mHeldCall));
+        verify(mCallsManager, timeout(SEQUENCING_TIMEOUT_MS).times(0))
+                .requestActionUnholdCall(eq(mHeldCall), anyString());
+    }
+
+    @Test
+    @SmallTest
+    public void testUnholdFail() {
+        // Fail the hold.
+        when(mActiveCall.can(eq(Connection.CAPABILITY_SUPPORT_HOLD))).thenReturn(true);
+        when(mActiveCall.hold(anyString())).thenReturn(CompletableFuture.completedFuture(false));
+        when(mActiveCall.isLocallyDisconnecting()).thenReturn(false);
+        // Use different phone accounts so that the sequencing code path is hit.
+        setPhoneAccounts(mHeldCall, mActiveCall, false);
+        setActiveCallFocus(mActiveCall);
+
+        mController.unholdCall(mHeldCall);
+        assertFalse(mController.arePhoneAccountsSame(mActiveCall, mHeldCall));
+        verify(mActiveCall).hold(anyString());
+        // Verify unhold is never reached.
+        verify(mCallsManager, never())
+                .requestActionUnholdCall(eq(mHeldCall), anyString());
+    }
+
+    @SmallTest
+    @Test
+    public void testMakeRoomForOutgoingEmergencyCall_SamePkg() {
+        // Ensure that the live call and emergency call are from the same pkg.
+        when(mActiveCall.getTargetPhoneAccount()).thenReturn(mHandle1);
+        when(mNewCall.getTargetPhoneAccount()).thenReturn(mHandle1);
+        when(mRingingCall.getTargetPhoneAccount()).thenReturn(mHandle2);
+        setupMakeRoomForOutgoingEmergencyCallMocks();
+
+        CompletableFuture<Boolean> future = mController.makeRoomForOutgoingCall(true, mNewCall);
+        verify(mRingingCall)
+                .reject(anyBoolean(), eq(null), anyString());
+        verify(mActiveCall, timeout(SEQUENCING_TIMEOUT_MS)).hold(anyString());
+        assertTrue(waitForFutureResult(future, false));
+    }
+
+    @SmallTest
+    @Test
+    public void testMakeRoomForOutgoingEmergencyCall_CanHold() {
+        // Ensure that the live call and emergency call are from different pkgs.
+        when(mActiveCall.getTargetPhoneAccount()).thenReturn(mHandle1);
+        when(mNewCall.getTargetPhoneAccount()).thenReturn(mHandle2);
+        when(mRingingCall.getTargetPhoneAccount()).thenReturn(mHandle2);
+        setupMakeRoomForOutgoingEmergencyCallMocks();
+
+        CompletableFuture<Boolean> future = mController.makeRoomForOutgoingCall(true, mNewCall);
+        verify(mRingingCall)
+                .reject(anyBoolean(), eq(null), anyString());
+        verify(mActiveCall, timeout(SEQUENCING_TIMEOUT_MS)).hold(anyString());
+        assertTrue(waitForFutureResult(future, false));
+    }
+
+    @Test
+    @SmallTest
+    public void testMakeRoomForOutgoingCall() {
+        setupMakeRoomForOutgoingCallMocks();
+        when(mActiveCall.hold(anyString())).thenReturn(CompletableFuture.completedFuture(true));
+        Analytics.CallInfo newCallAnalytics = mock(Analytics.CallInfo.class);
+        Analytics.CallInfo activeCallAnalytics = mock(Analytics.CallInfo.class);
+        when(mNewCall.getAnalytics()).thenReturn(newCallAnalytics);
+        when(mActiveCall.getAnalytics()).thenReturn(activeCallAnalytics);
+        when(mCallsManager.canHold(mActiveCall)).thenReturn(true);
+
+        CompletableFuture<Boolean> future = mController.makeRoomForOutgoingCall(false, mNewCall);
+        verify(mActiveCall, timeout(SEQUENCING_TIMEOUT_MS)).hold(anyString());
+        verify(newCallAnalytics).setCallIsAdditional(eq(true));
+        verify(activeCallAnalytics).setCallIsInterrupted(eq(true));
+        assertTrue(waitForFutureResult(future, false));
+    }
+
+    @Test
+    @SmallTest
+    public void testMakeRoomForOutgoingCallFail_MaxCalls() {
+        setupMakeRoomForOutgoingCallMocks();
+        when(mCallsManager.canHold(mActiveCall)).thenReturn(false);
+        when(mCallsManager.hasMaximumManagedHoldingCalls(mNewCall)).thenReturn(true);
+
+        CompletableFuture<Boolean> future = mController.makeRoomForOutgoingCall(false, mNewCall);
+        verify(mNewCall).setStartFailCause(eq(CallFailureCause.MAX_OUTGOING_CALLS));
+        assertFalse(waitForFutureResult(future, true));
+    }
+
+    @Test
+    @SmallTest
+    public void testMakeRoomForOutgoingCallFail_CannotHold() {
+        setupMakeRoomForOutgoingCallMocks();
+        when(mCallsManager.canHold(mActiveCall)).thenReturn(false);
+        when(mCallsManager.hasMaximumManagedHoldingCalls(mNewCall)).thenReturn(false);
+
+        CompletableFuture<Boolean> future = mController.makeRoomForOutgoingCall(false, mNewCall);
+        verify(mNewCall).setStartFailCause(eq(CallFailureCause.CANNOT_HOLD_CALL));
+        assertFalse(waitForFutureResult(future, true));
+    }
+
+    @Test
+    @SmallTest
+    public void testMakeRoomForOutgoingCallFail_RingingCall() {
+        when(mNewCall.isSelfManaged()).thenReturn(false);
+        when(mCallsManager.hasManagedRingingOrSimulatedRingingCall()).thenReturn(true);
+
+        CompletableFuture<Boolean> future = mController.makeRoomForOutgoingCall(false, mNewCall);
+        assertFalse(waitForFutureResult(future, true));
+    }
+
+    @Test
+    @SmallTest
+    public void testDisconnectCallSuccess() {
+        when(mActiveCall.disconnect()).thenReturn(CompletableFuture.completedFuture(true));
+        int previousState = CallState.ACTIVE;
+        mController.disconnectCall(mActiveCall, previousState);
+        verify(mCallsManager, timeout(SEQUENCING_TIMEOUT_MS))
+                .processDisconnectCallAndCleanup(eq(mActiveCall), eq(previousState));
+    }
+
+    @Test
+    @SmallTest
+    public void testDisconnectCallFail() {
+        when(mActiveCall.disconnect()).thenReturn(CompletableFuture.completedFuture(false));
+        int previousState = CallState.ACTIVE;
+        mController.disconnectCall(mActiveCall, previousState);
+        verify(mCallsManager, timeout(SEQUENCING_TIMEOUT_MS).times(0))
+                .processDisconnectCallAndCleanup(eq(mActiveCall), eq(previousState));
+    }
+
+    @Test
+    @SmallTest
+    public void testMmiCodeRestrictionReject() {
+        // Verify that when calls are detected across other phone accounts,
+        // that the MMI code is rejected.
+        when(mNewCall.getTargetPhoneAccount()).thenReturn(mHandle1);
+        when(mCallsManager.getNumCallsWithStateWithoutHandle(CALL_FILTER_ALL, mNewCall,
+                mHandle1, ONGOING_CALL_STATES)).thenReturn(1);
+        assertTrue(mController.hasMmiCodeRestriction(mNewCall));
+        verify(mNewCall).setOverrideDisconnectCauseCode(any(DisconnectCause.class));
+    }
+
+    @Test
+    @SmallTest
+    public void testMmiCodeRestrictionAllow() {
+        // Verify that when no calls are detected across other phone accounts,
+        // that the MMI code is allowed.
+        when(mNewCall.getTargetPhoneAccount()).thenReturn(mHandle1);
+        when(mCallsManager.getNumCallsWithStateWithoutHandle(CALL_FILTER_ALL, mNewCall,
+                mHandle1, ONGOING_CALL_STATES)).thenReturn(0);
+        assertFalse(mController.hasMmiCodeRestriction(mNewCall));
+        verify(mNewCall, times(0)).setOverrideDisconnectCauseCode(any(DisconnectCause.class));
+    }
+
+    /* Helpers */
+    private void setPhoneAccounts(Call call1, Call call2, boolean useSamePhoneAccount) {
+        when(call1.getTargetPhoneAccount()).thenReturn(mHandle1);
+        when(call2.getTargetPhoneAccount()).thenReturn(useSamePhoneAccount ? mHandle1 : mHandle2);
+    }
+
+    private void setActiveCallFocus(Call call) {
+        when(mCallsManager.getConnectionServiceFocusManager())
+                .thenReturn(mConnectionServiceFocusManager);
+        when(mConnectionServiceFocusManager.getCurrentFocusCall()).thenReturn(call);
+    }
+
+    private void setupMakeRoomForOutgoingEmergencyCallMocks() {
+        when(mNewCall.isEmergencyCall()).thenReturn(true);
+        when(mCallsManager.hasRingingOrSimulatedRingingCall()).thenReturn(true);
+        when(mCallsManager.getRingingOrSimulatedRingingCall()).thenReturn(mRingingCall);
+        when(mCallsManager.hasMaximumLiveCalls(mNewCall)).thenReturn(true);
+        when(mCallsManager.getFirstCallWithLiveState()).thenReturn(mActiveCall);
+        when(mCallsManager.hasMaximumOutgoingCalls(mNewCall)).thenReturn(false);
+        when(mCallsManager.hasMaximumManagedHoldingCalls(mNewCall)).thenReturn(false);
+        when(mCallsManager.canHold(mActiveCall)).thenReturn(true);
+
+        // Setup analytics mocks
+        setupCallAnalytics(Arrays.asList(mNewCall, mActiveCall, mRingingCall));
+
+        // Setup ecall related checks
+        setupEmergencyCallPaCapabilities();
+        setupCarrierConfigAllowEmergencyCallHold();
+
+        // Setup CompletableFuture mocking for call actions
+        when(mRingingCall.reject(anyBoolean(), eq(null), anyString()))
+                .thenReturn(CompletableFuture.completedFuture(true));
+        when(mActiveCall.hold(anyString())).thenReturn(
+                CompletableFuture.completedFuture(true));
+    }
+
+    private void setupEmergencyCallPaCapabilities() {
+        PhoneAccount pa = mock(PhoneAccount.class);
+        PhoneAccountRegistrar paRegistrar = mock(PhoneAccountRegistrar.class);
+        when(mCallsManager.getPhoneAccountRegistrar()).thenReturn(paRegistrar);
+        when(paRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class))).thenReturn(pa);
+        when(pa.getCapabilities()).thenReturn(PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS);
+    }
+
+    private void setupCarrierConfigAllowEmergencyCallHold() {
+        PersistableBundle bundle = mock(PersistableBundle.class);
+        when(mCallsManager.getCarrierConfigForPhoneAccount(any(PhoneAccountHandle.class)))
+                .thenReturn(bundle);
+        when(bundle.getBoolean(
+                CarrierConfigManager.KEY_ALLOW_HOLD_CALL_DURING_EMERGENCY_BOOL, true))
+                .thenReturn(true);
+    }
+
+    private void setupMakeRoomForOutgoingCallMocks() {
+        when(mCallsManager.hasMaximumLiveCalls(mNewCall)).thenReturn(true);
+        when(mCallsManager.getFirstCallWithLiveState()).thenReturn(mActiveCall);
+        setPhoneAccounts(mActiveCall, mNewCall, false);
+        when(mActiveCall.isConference()).thenReturn(false);
+        when(mCallsManager.hasMaximumOutgoingCalls(mNewCall)).thenReturn(false);
+    }
+
+    private void setupHoldActiveCallForNewCallFailMocks() {
+        // Setup holdActiveCallForNewCallWithSequencing to fail.
+        setPhoneAccounts(mNewCall, mActiveCall, false);
+        setActiveCallFocus(mActiveCall);
+        when(mCallsManager.canHold(mActiveCall)).thenReturn(true);
+        when(mActiveCall.hold(anyString())).thenReturn(CompletableFuture.completedFuture(false));
+    }
+
+    private void verifyTransactionHoldActiveCallForNewCall(
+            OutcomeReceiver<Boolean, CallException> callback, CountDownLatch latch) {
+        mController.transactionHoldPotentialActiveCallForNewCallSequencing(mNewCall, callback);
+        while (latch.getCount() > 0) {
+            try {
+                latch.await(SEQUENCING_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+            } catch (InterruptedException e) {
+                // do nothing
+            }
+        }
+        assertEquals(latch.getCount(), 0);
+    }
+
+    private CallAttributes getOutgoingCallAttributes() {
+        return new CallAttributes.Builder(mHandle1,
+                CallAttributes.DIRECTION_OUTGOING, TEST_NAME, TEST_URI)
+                .setCallType(CallAttributes.AUDIO_CALL)
+                .setCallCapabilities(CallAttributes.SUPPORTS_SET_INACTIVE)
+                .build();
+    }
+
+    private void setupCallAnalytics(List<Call> calls) {
+        for (Call call: calls) {
+            Analytics.CallInfo analyticsInfo = mock(Analytics.CallInfo.class);
+            when(call.getAnalytics()).thenReturn(analyticsInfo);
+        }
+    }
+
+    private boolean waitForFutureResult(CompletableFuture<Boolean> future, boolean defaultValue) {
+        boolean result = defaultValue;
+        try {
+            result = future.get(SEQUENCING_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        } catch (Exception e) {
+            // Pass through
+        }
+        return result;
+    }
+}
+
diff --git a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
index 79fd3d5..b95dfef 100644
--- a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
@@ -103,6 +103,7 @@
 import com.android.server.telecom.CallState;
 import com.android.server.telecom.CallerInfoLookupHelper;
 import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.callsequencing.CallsManagerCallSequencingAdapter;
 import com.android.server.telecom.ClockProxy;
 import com.android.server.telecom.ConnectionServiceFocusManager;
 import com.android.server.telecom.ConnectionServiceFocusManager.ConnectionServiceFocusManagerFactory;
@@ -199,7 +200,8 @@
     private static final PhoneAccountHandle SELF_MANAGED_2_HANDLE = new PhoneAccountHandle(
             ComponentName.unflattenFromString("com.baz/.Self2"), "Self2");
     private static final PhoneAccountHandle WORK_HANDLE = new PhoneAccountHandle(
-            ComponentName.unflattenFromString("com.foo/.Blah"), "work", new UserHandle(10));
+            ComponentName.unflattenFromString("com.foo/.Blah"), "work",
+            new UserHandle(SECONDARY_USER_ID));
     private static final PhoneAccountHandle SELF_MANAGED_W_CUSTOM_HANDLE = new PhoneAccountHandle(
             new ComponentName(TEST_PACKAGE_NAME, "class"), "1", TEST_USER_HANDLE);
     private static final PhoneAccount SIM_1_ACCOUNT = new PhoneAccount.Builder(SIM_1_HANDLE, "Sim1")
@@ -3044,9 +3046,9 @@
 
     /**
      * Verify that
-     * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
-     * OutcomeReceiver)}s OutcomeReceiver returns onResult when there is no active call to place
-     * on hold.
+     * {@link CallsManagerCallSequencingAdapter#transactionHoldPotentialActiveCallForNewCall(Call,
+     * boolean, OutcomeReceiver)}s OutcomeReceiver returns onResult when there is no active call to
+     * place on hold.
      */
     @MediumTest
     @Test
@@ -3068,8 +3070,8 @@
 
     /**
      * Verify that
-     * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
-     * OutcomeReceiver)}s OutcomeReceiver returns onError when there is an active call that
+     * {@link CallsManagerCallSequencingAdapter#transactionHoldPotentialActiveCallForNewCall(Call,
+     * boolean, OutcomeReceiver)}s OutcomeReceiver returns onError when there is an active call that
      * cannot be held, and it's a CallControlRequest.
      */
     @MediumTest
@@ -3086,9 +3088,9 @@
 
     /**
      * Verify that
-     * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
-     * OutcomeReceiver)}s OutcomeReceiver returns onResult when there is a holdable call and
-     * it's a CallControlRequest.
+     * {@link CallsManagerCallSequencingAdapter#transactionHoldPotentialActiveCallForNewCall(Call,
+     * boolean, OutcomeReceiver)}s OutcomeReceiver returns onResult when there is a holdable call
+     * and it's a CallControlRequest.
      */
     @MediumTest
     @Test
@@ -3105,9 +3107,9 @@
 
     /**
      * Verify that
-     * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
-     * OutcomeReceiver)}s OutcomeReceiver returns onResult when there is an active call that
-     * supports hold, and it's a CallControlRequest.
+     * {@link CallsManagerCallSequencingAdapter#transactionHoldPotentialActiveCallForNewCall(Call,
+     * boolean, OutcomeReceiver)}s OutcomeReceiver returns onResult when there is an active call
+     * that supports hold, and it's a CallControlRequest.
      */
     @MediumTest
     @Test
@@ -3124,9 +3126,9 @@
 
     /**
      * Verify that
-     * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
-     * OutcomeReceiver)}s OutcomeReceiver returns onResult when there is an active call that
-     * supports hold + can hold, and it's a CallControlRequest.
+     * {@link CallsManagerCallSequencingAdapter#transactionHoldPotentialActiveCallForNewCall(Call,
+     * boolean, OutcomeReceiver)}s OutcomeReceiver returns onResult when there is an active call
+     * that supports hold + can hold, and it's a CallControlRequest.
      */
     @MediumTest
     @Test
@@ -3145,9 +3147,9 @@
 
     /**
      * Verify that
-     * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
-     * OutcomeReceiver)}s OutcomeReceiver returns onResult when there is an active call that
-     * supports hold + can hold, and it's a CallControlCallbackRequest.
+     * {@link CallsManagerCallSequencingAdapter#transactionHoldPotentialActiveCallForNewCall(Call,
+     * boolean, OutcomeReceiver)}s OutcomeReceiver returns onResult when there is an active call
+     * that supports hold + can hold, and it's a CallControlCallbackRequest.
      */
     @MediumTest
     @Test
@@ -3165,9 +3167,9 @@
 
     /**
      * Verify that
-     * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
-     * OutcomeReceiver)}s OutcomeReceiver returns onResult when there is an active unholdable call,
-     * and it's a CallControlCallbackRequest.
+     * {@link CallsManagerCallSequencingAdapter#transactionHoldPotentialActiveCallForNewCall(Call,
+     * boolean, OutcomeReceiver)}s OutcomeReceiver returns onResult when there is an active
+     * unholdable call, and it's a CallControlCallbackRequest.
      */
     @MediumTest
     @Test
@@ -3803,9 +3805,9 @@
         Call callSpy = Mockito.spy(ongoingCall);
 
         // Mocks some methods to not call the real method.
-        doNothing().when(callSpy).unhold();
-        doNothing().when(callSpy).hold();
-        doNothing().when(callSpy).answer(ArgumentMatchers.anyInt());
+        doReturn(null).when(callSpy).unhold();
+        doReturn(null).when(callSpy).hold();
+        doReturn(null).when(callSpy).answer(ArgumentMatchers.anyInt());
         doNothing().when(callSpy).setStartWithSpeakerphoneOn(ArgumentMatchers.anyBoolean());
 
         mCallsManager.addCall(callSpy);
@@ -3817,10 +3819,10 @@
         Call callSpy = Mockito.spy(ongoingCall);
 
         // Mocks some methods to not call the real method.
-        doNothing().when(callSpy).unhold();
-        doNothing().when(callSpy).hold();
-        doNothing().when(callSpy).disconnect();
-        doNothing().when(callSpy).answer(ArgumentMatchers.anyInt());
+        doReturn(null).when(callSpy).unhold();
+        doReturn(null).when(callSpy).hold();
+        doReturn(null).when(callSpy).disconnect();
+        doReturn(null).when(callSpy).answer(ArgumentMatchers.anyInt());
         doNothing().when(callSpy).setStartWithSpeakerphoneOn(ArgumentMatchers.anyBoolean());
 
         return callSpy;
@@ -3940,7 +3942,7 @@
         CountDownLatch latch = new CountDownLatch(1);
         when(mFeatureFlags.transactionalHoldDisconnectsUnholdable()).thenReturn(true);
         when(mConnectionSvrFocusMgr.getCurrentFocusCall()).thenReturn(activeCall);
-        mCallsManager.transactionHoldPotentialActiveCallForNewCall(
+        mCallsManager.getCallSequencingAdapter().transactionHoldPotentialActiveCallForNewCall(
                 newCall,
                 isCallControlRequest,
                 new LatchedOutcomeReceiver(latch, expectOnResult));
diff --git a/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java b/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
index 1432834..1261265 100644
--- a/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
+++ b/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
@@ -756,6 +756,17 @@
         Log.VERBOSE = true;
     }
 
+    public void destroy() {
+        if (mHandlerThread == null) return;
+        mHandlerThread.quit();
+        try {
+            mHandlerThread.join();
+        } catch (InterruptedException ex) {
+            Log.w(this, "HandlerThread join interrupted", ex);
+        }
+        mHandlerThread = null;
+    }
+
     @Override
     public Context getTestDouble() {
         return mContext;
diff --git a/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java b/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java
index e497f48..475133c 100644
--- a/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java
+++ b/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java
@@ -162,8 +162,6 @@
 
         mTestCreateConnectionTimeout = new CreateConnectionTimeout(mContext, mMockAccountRegistrar,
                 makeConnectionServiceWrapper(), mMockCall, mTimeoutsAdapter);
-
-        mSetFlagsRule.enableFlags(Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG);
     }
 
     @Override
@@ -1127,4 +1125,4 @@
                 .setIsEnabled(true)
                 .build();
     }
-}
\ No newline at end of file
+}
diff --git a/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java b/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java
index a480a7b..23e8dab 100644
--- a/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java
+++ b/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java
@@ -113,8 +113,7 @@
     private final String PACKAGE_1 = "PACKAGE_1";
     private final String PACKAGE_2 = "PACKAGE_2";
     private final String COMPONENT_NAME = "com.android.server.telecom.tests.MockConnectionService";
-    private final UserHandle USER_HANDLE_10 = UserHandle.of(10);
-    private final UserHandle USER_HANDLE_1000 = UserHandle.of(1000);
+    private final UserHandle USER_HANDLE_10 = new UserHandle(10);
     private final TelecomSystem.SyncRoot mLock = new TelecomSystem.SyncRoot() { };
     private PhoneAccountRegistrar mRegistrar;
     @Mock private SubscriptionManager mSubscriptionManager;
@@ -141,7 +140,6 @@
         mRegistrar = new PhoneAccountRegistrar(
                 mComponentContextFixture.getTestDouble().getApplicationContext(), mLock, FILE_NAME,
                 mDefaultDialerCache, mAppLabelProxy, mTelephonyFeatureFlags, mFeatureFlags);
-        mRegistrar.setCurrentUserHandle(UserHandle.SYSTEM);
         when(mFeatureFlags.onlyUpdateTelephonyOnValidSubIds()).thenReturn(false);
         when(mFeatureFlags.unregisterUnresolvableAccounts()).thenReturn(true);
         when(mTelephonyFeatureFlags.workProfileApiSplit()).thenReturn(false);
@@ -1308,7 +1306,8 @@
                 Mockito.mock(IConnectionService.class));
         UserManager userManager = mContext.getSystemService(UserManager.class);
 
-        List<UserHandle> users = Arrays.asList(UserHandle.SYSTEM, USER_HANDLE_1000);
+        List<UserHandle> users = Arrays.asList(new UserHandle(0),
+                new UserHandle(10));
 
         PhoneAccount pa1 = new PhoneAccount.Builder(
                 new PhoneAccountHandle(new ComponentName(PACKAGE_1, COMPONENT_NAME), "1234",
diff --git a/tests/src/com/android/server/telecom/tests/RingerTest.java b/tests/src/com/android/server/telecom/tests/RingerTest.java
index 46916fd..d9bf6e1 100644
--- a/tests/src/com/android/server/telecom/tests/RingerTest.java
+++ b/tests/src/com/android/server/telecom/tests/RingerTest.java
@@ -22,6 +22,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeNotNull;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
@@ -46,10 +47,12 @@
 import android.media.AudioAttributes;
 import android.media.AudioManager;
 import android.media.Ringtone;
+import android.media.RingtoneManager;
 import android.media.VolumeShaper;
 import android.media.audio.Flags;
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.TestLooperManager;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.os.VibrationAttributes;
@@ -64,7 +67,9 @@
 import android.telecom.TelecomManager;
 import android.util.Pair;
 
+import androidx.test.core.app.ApplicationProvider;
 import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.server.telecom.AnomalyReporterAdapter;
 import com.android.server.telecom.AsyncRingtonePlayer;
@@ -83,6 +88,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.Spy;
 
@@ -136,6 +142,7 @@
             new PhoneAccountHandle(new ComponentName("pa_pkg", "pa_cls"),
                     "pa_id");
 
+    TestLooperManager mLooperManager;
     boolean mIsHapticPlaybackSupported = true;  // Note: initializeRinger() after changes.
     AsyncRingtonePlayer asyncRingtonePlayer = new AsyncRingtonePlayer();
     Ringer mRingerUnderTest;
@@ -191,6 +198,18 @@
         super.tearDown();
     }
 
+    private void acquireLooper() {
+        mLooperManager = InstrumentationRegistry.getInstrumentation()
+                .acquireLooperManager(asyncRingtonePlayer.getLooper());
+    }
+
+    private void processAllMessages() {
+        for (var msg = mLooperManager.poll(); msg != null && msg.getTarget() != null;) {
+            mLooperManager.execute(msg);
+            mLooperManager.recycle(msg);
+        }
+    }
+
     @SmallTest
     @Test
     public void testSimpleVibrationPrecedesValidSupportedDefaultRingVibrationOverride()
@@ -643,16 +662,20 @@
     @SmallTest
     @Test
     public void testDelayRingerForBtHfpDevices() throws Exception {
+        acquireLooper();
+
         asyncRingtonePlayer.updateBtActiveState(false);
         Ringtone mockRingtone = ensureRingtoneMocked();
 
         ensureRingerIsAudible();
         assertTrue(mRingerUnderTest.startRinging(mockCall1, true));
         assertTrue(mRingerUnderTest.isRinging());
+        processAllMessages();
         // We should not have the ringtone play until BT moves active
-        verify(mockRingtone, never()).play();
+        // TODO(b/395089048): verify(mockRingtone, never()).play();
 
         asyncRingtonePlayer.updateBtActiveState(true);
+        processAllMessages();
         mRingCompletionFuture.get();
         verify(mockRingtoneFactory, atLeastOnce())
                 .getRingtone(any(Call.class), nullable(VolumeShaper.Configuration.class),
@@ -661,25 +684,31 @@
         verify(mockRingtone).play();
 
         mRingerUnderTest.stopRinging();
-        verify(mockRingtone, timeout(1000/*ms*/)).stop();
+        processAllMessages();
+        verify(mockRingtone).stop();
         assertFalse(mRingerUnderTest.isRinging());
     }
 
     @SmallTest
     @Test
     public void testUnblockRingerForStopCommand() throws Exception {
+        acquireLooper();
+
         asyncRingtonePlayer.updateBtActiveState(false);
         Ringtone mockRingtone = ensureRingtoneMocked();
 
         ensureRingerIsAudible();
         assertTrue(mRingerUnderTest.startRinging(mockCall1, true));
+
+        processAllMessages();
         // We should not have the ringtone play until BT moves active
-        verify(mockRingtone, never()).play();
+        // TODO(b/395089048): verify(mockRingtone, never()).play();
 
         // We are not setting BT active, but calling stop ringing while the other thread is waiting
         // for BT active should also unblock it.
         mRingerUnderTest.stopRinging();
-        verify(mockRingtone, timeout(1000/*ms*/)).stop();
+        processAllMessages();
+        verify(mockRingtone).stop();
     }
 
     /**
@@ -821,8 +850,12 @@
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_RINGTONE_HAPTICS_CUSTOMIZATION)
     public void testNoVibrateForSilentRingtoneIfRingtoneHasVibration() throws Exception {
+        final Context context = ApplicationProvider.getApplicationContext();
+        Uri defaultRingtoneUri = RingtoneManager.getActualDefaultRingtoneUri(context,
+                RingtoneManager.TYPE_RINGTONE);
+        assumeNotNull(defaultRingtoneUri);
         Uri FAKE_RINGTONE_VIBRATION_URI =
-                FAKE_RINGTONE_URI.buildUpon().appendQueryParameter(
+                defaultRingtoneUri.buildUpon().appendQueryParameter(
                         VIBRATION_PARAM, FAKE_VIBRATION_URI.toString()).build();
         Ringtone mockRingtone = mock(Ringtone.class);
         Pair<Uri, Ringtone> ringtoneInfo = new Pair(FAKE_RINGTONE_VIBRATION_URI, mockRingtone);
@@ -831,21 +864,61 @@
                 .thenReturn(ringtoneInfo);
         mComponentContextFixture.putBooleanResource(
                 com.android.internal.R.bool.config_ringtoneVibrationSettingsSupported, true);
-        createRingerUnderTest(); // Needed after mock the config.
+        try {
+            RingtoneManager.setActualDefaultRingtoneUri(context, RingtoneManager.TYPE_RINGTONE,
+                    FAKE_RINGTONE_VIBRATION_URI);
+            createRingerUnderTest(); // Needed after mock the config.
 
-        mRingerUnderTest.startCallWaiting(mockCall1);
-        when(mockAudioManager.getRingerMode()).thenReturn(AudioManager.RINGER_MODE_VIBRATE);
-        when(mockAudioManager.getStreamVolume(AudioManager.STREAM_RING)).thenReturn(0);
-        enableVibrationWhenRinging();
-        assertFalse(startRingingAndWaitForAsync(mockCall2, false));
+            mRingerUnderTest.startCallWaiting(mockCall1);
+            when(mockAudioManager.getRingerMode()).thenReturn(AudioManager.RINGER_MODE_VIBRATE);
+            when(mockAudioManager.getStreamVolume(AudioManager.STREAM_RING)).thenReturn(0);
+            enableVibrationWhenRinging();
+            assertFalse(startRingingAndWaitForAsync(mockCall2, false));
 
-        verify(mockRingtoneFactory, atLeastOnce())
-                .getRingtone(any(Call.class), eq(null), eq(false));
-        verifyNoMoreInteractions(mockRingtoneFactory);
-        verify(mockTonePlayer).stopTone();
-        // Skip vibration play in Ringer if a vibration was specified to the ringtone
-        verify(mockVibrator, never()).vibrate(any(VibrationEffect.class),
-                any(VibrationAttributes.class));
+            verify(mockRingtoneFactory, atLeastOnce())
+                    .getRingtone(any(Call.class), eq(null), eq(false));
+            verifyNoMoreInteractions(mockRingtoneFactory);
+            verify(mockTonePlayer).stopTone();
+            // Skip vibration play in Ringer if a vibration was specified to the ringtone
+            verify(mockVibrator, never()).vibrate(any(VibrationEffect.class),
+                    any(VibrationAttributes.class));
+        } finally {
+            // Restore the default ringtone Uri
+            RingtoneManager.setActualDefaultRingtoneUri(context, RingtoneManager.TYPE_RINGTONE,
+                    defaultRingtoneUri);
+        }
+    }
+
+    @SmallTest
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_RINGTONE_HAPTICS_CUSTOMIZATION)
+    public void testNotMuteHapticChannelWithRampingRinger() throws Exception {
+        final Context context = ApplicationProvider.getApplicationContext();
+        Uri defaultRingtoneUri = RingtoneManager.getActualDefaultRingtoneUri(context,
+                RingtoneManager.TYPE_RINGTONE);
+        assumeNotNull(defaultRingtoneUri);
+        Uri FAKE_RINGTONE_VIBRATION_URI = defaultRingtoneUri.buildUpon().appendQueryParameter(
+                        VIBRATION_PARAM, FAKE_VIBRATION_URI.toString()).build();
+        mComponentContextFixture.putBooleanResource(
+                com.android.internal.R.bool.config_ringtoneVibrationSettingsSupported, true);
+        ArgumentCaptor<Boolean> muteHapticChannelCaptor = ArgumentCaptor.forClass(Boolean.class);
+        try {
+            RingtoneManager.setActualDefaultRingtoneUri(context, RingtoneManager.TYPE_RINGTONE,
+                    FAKE_RINGTONE_VIBRATION_URI);
+            createRingerUnderTest(); // Needed after mock the config.
+            mRingerUnderTest.startCallWaiting(mockCall1);
+            ensureRingerIsAudible();
+            enableRampingRinger();
+            enableVibrationWhenRinging();
+            assertTrue(startRingingAndWaitForAsync(mockCall2, false));
+            verify(mockRingtoneFactory, atLeastOnce()).getRingtone(any(Call.class),
+                    nullable(VolumeShaper.Configuration.class), muteHapticChannelCaptor.capture());
+            assertFalse(muteHapticChannelCaptor.getValue());
+        } finally {
+            // Restore the default ringtone Uri
+            RingtoneManager.setActualDefaultRingtoneUri(context, RingtoneManager.TYPE_RINGTONE,
+                    defaultRingtoneUri);
+        }
     }
 
     /**
diff --git a/tests/src/com/android/server/telecom/tests/TelecomMetricsControllerTest.java b/tests/src/com/android/server/telecom/tests/TelecomMetricsControllerTest.java
index 4d494f3..3716b4d 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomMetricsControllerTest.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomMetricsControllerTest.java
@@ -19,6 +19,7 @@
 import static com.android.server.telecom.TelecomStatsLog.CALL_STATS;
 import static com.android.server.telecom.TelecomStatsLog.TELECOM_API_STATS;
 import static com.android.server.telecom.TelecomStatsLog.TELECOM_ERROR_STATS;
+import static com.android.server.telecom.TelecomStatsLog.TELECOM_EVENT_STATS;
 import static com.google.common.truth.Truth.assertThat;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyObject;
@@ -38,6 +39,7 @@
 import com.android.server.telecom.metrics.AudioRouteStats;
 import com.android.server.telecom.metrics.CallStats;
 import com.android.server.telecom.metrics.ErrorStats;
+import com.android.server.telecom.metrics.EventStats;
 import com.android.server.telecom.metrics.TelecomMetricsController;
 
 import org.junit.After;
@@ -61,6 +63,8 @@
     CallStats mCallStats;
     @Mock
     ErrorStats mErrorStats;
+    @Mock
+    EventStats mEventStats;
 
     HandlerThread mHandlerThread;
 
@@ -114,6 +118,13 @@
     }
 
     @Test
+    public void testGetEventStatsReturnsSameInstance() {
+        EventStats stats1 = mTelecomMetricsController.getEventStats();
+        EventStats stats2 = mTelecomMetricsController.getEventStats();
+        assertThat(stats1).isSameInstanceAs(stats2);
+    }
+
+    @Test
     public void testOnPullAtomReturnsPullSkipIfAtomNotRegistered() {
         mTelecomMetricsController.getStats().clear();
 
@@ -143,6 +154,7 @@
         verify(statsManager, times(1)).clearPullAtomCallback(eq(CALL_STATS));
         verify(statsManager, times(1)).clearPullAtomCallback(eq(TELECOM_API_STATS));
         verify(statsManager, times(1)).clearPullAtomCallback(eq(TELECOM_ERROR_STATS));
+        verify(statsManager, times(1)).clearPullAtomCallback(eq(TELECOM_EVENT_STATS));
         assertThat(mTelecomMetricsController.getStats()).isEmpty();
     }
 
@@ -159,11 +171,42 @@
         assertThat(captor.getValue()).isEqualTo(data);
     }
 
+    @Test
+    public void testSetTestMode() {
+        StatsManager statsManager = mContext.getSystemService(StatsManager.class);
+        ApiStats apiStats1 = mTelecomMetricsController.getApiStats();
+        AudioRouteStats audioStats1 = mTelecomMetricsController.getAudioRouteStats();
+        CallStats callStats1 = mTelecomMetricsController.getCallStats();
+        ErrorStats errorStats1 = mTelecomMetricsController.getErrorStats();
+        mTelecomMetricsController.setTestMode(true);
+
+        verify(statsManager, times(1)).clearPullAtomCallback(eq(CALL_AUDIO_ROUTE_STATS));
+        verify(statsManager, times(1)).clearPullAtomCallback(eq(CALL_STATS));
+        verify(statsManager, times(1)).clearPullAtomCallback(eq(TELECOM_API_STATS));
+        verify(statsManager, times(1)).clearPullAtomCallback(eq(TELECOM_ERROR_STATS));
+        assertThat(mTelecomMetricsController.getStats()).isEmpty();
+
+        ApiStats apiStats2 = mTelecomMetricsController.getApiStats();
+        AudioRouteStats audioStats2 = mTelecomMetricsController.getAudioRouteStats();
+        CallStats callStats2 = mTelecomMetricsController.getCallStats();
+        ErrorStats errorStats2 = mTelecomMetricsController.getErrorStats();
+
+        assertThat(apiStats1).isNotSameInstanceAs(apiStats2);
+        assertThat(audioStats1).isNotSameInstanceAs(audioStats2);
+        assertThat(callStats1).isNotSameInstanceAs(callStats2);
+        assertThat(errorStats1).isNotSameInstanceAs(errorStats2);
+
+        mTelecomMetricsController.setTestMode(false);
+
+        assertThat(mTelecomMetricsController.getStats()).isEmpty();
+    }
+
     private void setUpStats() {
         mTelecomMetricsController.getStats().put(CALL_AUDIO_ROUTE_STATS,
                 mAudioRouteStats);
         mTelecomMetricsController.getStats().put(CALL_STATS, mCallStats);
         mTelecomMetricsController.getStats().put(TELECOM_API_STATS, mApiStats);
         mTelecomMetricsController.getStats().put(TELECOM_ERROR_STATS, mErrorStats);
+        mTelecomMetricsController.getStats().put(TELECOM_EVENT_STATS, mEventStats);
     }
 }
diff --git a/tests/src/com/android/server/telecom/tests/TelecomPulledAtomTest.java b/tests/src/com/android/server/telecom/tests/TelecomPulledAtomTest.java
index d3c7859..875617f 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomPulledAtomTest.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomPulledAtomTest.java
@@ -42,6 +42,7 @@
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.os.Looper;
+import android.telecom.DisconnectCause;
 import android.telecom.PhoneAccount;
 import android.telecom.PhoneAccountHandle;
 import android.util.StatsEvent;
@@ -55,6 +56,7 @@
 import com.android.server.telecom.metrics.AudioRouteStats;
 import com.android.server.telecom.metrics.CallStats;
 import com.android.server.telecom.metrics.ErrorStats;
+import com.android.server.telecom.metrics.EventStats;
 import com.android.server.telecom.nano.PulledAtomsClass;
 
 import org.junit.After;
@@ -104,6 +106,10 @@
     private static final int VALUE_ERROR_ID = 1;
     private static final int VALUE_ERROR_COUNT = 1;
 
+    private static final int VALUE_EVENT_ID = 1;
+    private static final int VALUE_CAUSE_ID = 1;
+    private static final int VALUE_EVENT_COUNT = 1;
+
     @Rule
     public TemporaryFolder mTempFolder = new TemporaryFolder();
     @Mock
@@ -145,22 +151,22 @@
     public void testNewPulledAtomsFromFileInvalid() throws Exception {
         mTempFile.delete();
 
-        ApiStats apiStats = new ApiStats(mSpyContext, mLooper);
+        ApiStats apiStats = new ApiStats(mSpyContext, mLooper, false);
 
         assertNotNull(apiStats.mPulledAtoms);
         assertEquals(apiStats.mPulledAtoms.telecomApiStats.length, 0);
 
-        AudioRouteStats audioRouteStats = new AudioRouteStats(mSpyContext, mLooper);
+        AudioRouteStats audioRouteStats = new AudioRouteStats(mSpyContext, mLooper, false);
 
         assertNotNull(audioRouteStats.mPulledAtoms);
         assertEquals(audioRouteStats.mPulledAtoms.callAudioRouteStats.length, 0);
 
-        CallStats callStats = new CallStats(mSpyContext, mLooper);
+        CallStats callStats = new CallStats(mSpyContext, mLooper, false);
 
         assertNotNull(callStats.mPulledAtoms);
         assertEquals(callStats.mPulledAtoms.callStats.length, 0);
 
-        ErrorStats errorStats = new ErrorStats(mSpyContext, mLooper);
+        ErrorStats errorStats = new ErrorStats(mSpyContext, mLooper, false);
 
         assertNotNull(errorStats.mPulledAtoms);
         assertEquals(errorStats.mPulledAtoms.telecomErrorStats.length, 0);
@@ -169,30 +175,35 @@
     @Test
     public void testNewPulledAtomsFromFileValid() throws Exception {
         createTestFileForApiStats(DEFAULT_TIMESTAMPS_MILLIS);
-        ApiStats apiStats = new ApiStats(mSpyContext, mLooper);
+        ApiStats apiStats = new ApiStats(mSpyContext, mLooper, false);
 
         verifyTestDataForApiStats(apiStats.mPulledAtoms, DEFAULT_TIMESTAMPS_MILLIS);
 
         createTestFileForAudioRouteStats(DEFAULT_TIMESTAMPS_MILLIS);
-        AudioRouteStats audioRouteStats = new AudioRouteStats(mSpyContext, mLooper);
+        AudioRouteStats audioRouteStats = new AudioRouteStats(mSpyContext, mLooper, false);
 
         verifyTestDataForAudioRouteStats(audioRouteStats.mPulledAtoms, DEFAULT_TIMESTAMPS_MILLIS);
 
         createTestFileForCallStats(DEFAULT_TIMESTAMPS_MILLIS);
-        CallStats callStats = new CallStats(mSpyContext, mLooper);
+        CallStats callStats = new CallStats(mSpyContext, mLooper, false);
 
         verifyTestDataForCallStats(callStats.mPulledAtoms, DEFAULT_TIMESTAMPS_MILLIS);
 
         createTestFileForErrorStats(DEFAULT_TIMESTAMPS_MILLIS);
-        ErrorStats errorStats = new ErrorStats(mSpyContext, mLooper);
+        ErrorStats errorStats = new ErrorStats(mSpyContext, mLooper, false);
 
         verifyTestDataForErrorStats(errorStats.mPulledAtoms, DEFAULT_TIMESTAMPS_MILLIS);
+
+        createTestFileForEventStats(DEFAULT_TIMESTAMPS_MILLIS);
+        EventStats eventStats = new EventStats(mSpyContext, mLooper, false);
+
+        verifyTestDataForEventStats(eventStats.mPulledAtoms, DEFAULT_TIMESTAMPS_MILLIS);
     }
 
     @Test
     public void testPullApiStatsLessThanMinPullIntervalShouldSkip() throws Exception {
         createTestFileForApiStats(System.currentTimeMillis() - MIN_PULL_INTERVAL_MILLIS / 2);
-        ApiStats apiStats = spy(new ApiStats(mSpyContext, mLooper));
+        ApiStats apiStats = spy(new ApiStats(mSpyContext, mLooper, false));
         final List<StatsEvent> data = new ArrayList<>();
 
         int result = apiStats.pull(data);
@@ -205,7 +216,7 @@
     @Test
     public void testPullApiStatsGreaterThanMinPullIntervalShouldNotSkip() throws Exception {
         createTestFileForApiStats(System.currentTimeMillis() - MIN_PULL_INTERVAL_MILLIS - 1);
-        ApiStats apiStats = spy(new ApiStats(mSpyContext, mLooper));
+        ApiStats apiStats = spy(new ApiStats(mSpyContext, mLooper, false));
         final List<StatsEvent> data = new ArrayList<>();
         int sizePulled = apiStats.mPulledAtoms.telecomApiStats.length;
 
@@ -220,7 +231,7 @@
     @Test
     public void testPullAudioRouteStatsLessThanMinPullIntervalShouldSkip() throws Exception {
         createTestFileForAudioRouteStats(System.currentTimeMillis() - MIN_PULL_INTERVAL_MILLIS / 2);
-        AudioRouteStats audioRouteStats = spy(new AudioRouteStats(mSpyContext, mLooper));
+        AudioRouteStats audioRouteStats = spy(new AudioRouteStats(mSpyContext, mLooper, false));
         final List<StatsEvent> data = new ArrayList<>();
 
         int result = audioRouteStats.pull(data);
@@ -233,7 +244,7 @@
     @Test
     public void testPullAudioRouteStatsGreaterThanMinPullIntervalShouldNotSkip() throws Exception {
         createTestFileForAudioRouteStats(System.currentTimeMillis() - MIN_PULL_INTERVAL_MILLIS - 1);
-        AudioRouteStats audioRouteStats = spy(new AudioRouteStats(mSpyContext, mLooper));
+        AudioRouteStats audioRouteStats = spy(new AudioRouteStats(mSpyContext, mLooper, false));
         final List<StatsEvent> data = new ArrayList<>();
         int sizePulled = audioRouteStats.mPulledAtoms.callAudioRouteStats.length;
 
@@ -248,7 +259,7 @@
     @Test
     public void testPullCallStatsLessThanMinPullIntervalShouldSkip() throws Exception {
         createTestFileForCallStats(System.currentTimeMillis() - MIN_PULL_INTERVAL_MILLIS / 2);
-        CallStats callStats = spy(new CallStats(mSpyContext, mLooper));
+        CallStats callStats = spy(new CallStats(mSpyContext, mLooper, false));
         final List<StatsEvent> data = new ArrayList<>();
 
         int result = callStats.pull(data);
@@ -261,7 +272,7 @@
     @Test
     public void testPullCallStatsGreaterThanMinPullIntervalShouldNotSkip() throws Exception {
         createTestFileForCallStats(System.currentTimeMillis() - MIN_PULL_INTERVAL_MILLIS - 1);
-        CallStats callStats = spy(new CallStats(mSpyContext, mLooper));
+        CallStats callStats = spy(new CallStats(mSpyContext, mLooper, false));
         final List<StatsEvent> data = new ArrayList<>();
         int sizePulled = callStats.mPulledAtoms.callStats.length;
 
@@ -276,7 +287,7 @@
     @Test
     public void testPullErrorStatsLessThanMinPullIntervalShouldSkip() throws Exception {
         createTestFileForErrorStats(System.currentTimeMillis() - MIN_PULL_INTERVAL_MILLIS / 2);
-        ErrorStats errorStats = spy(new ErrorStats(mSpyContext, mLooper));
+        ErrorStats errorStats = spy(new ErrorStats(mSpyContext, mLooper, false));
         final List<StatsEvent> data = new ArrayList<>();
 
         int result = errorStats.pull(data);
@@ -289,7 +300,7 @@
     @Test
     public void testPullErrorStatsGreaterThanMinPullIntervalShouldNotSkip() throws Exception {
         createTestFileForErrorStats(System.currentTimeMillis() - MIN_PULL_INTERVAL_MILLIS - 1);
-        ErrorStats errorStats = spy(new ErrorStats(mSpyContext, mLooper));
+        ErrorStats errorStats = spy(new ErrorStats(mSpyContext, mLooper, false));
         final List<StatsEvent> data = new ArrayList<>();
         int sizePulled = errorStats.mPulledAtoms.telecomErrorStats.length;
 
@@ -303,7 +314,7 @@
 
     @Test
     public void testApiStatsLogCount() throws Exception {
-        ApiStats apiStats = spy(new ApiStats(mSpyContext, mLooper));
+        ApiStats apiStats = spy(new ApiStats(mSpyContext, mLooper, false));
         ApiStats.ApiEvent event = new ApiStats.ApiEvent(VALUE_API_ID, VALUE_UID, VALUE_API_RESULT);
 
         for (int i = 0; i < 10; i++) {
@@ -384,7 +395,7 @@
         };
         final int[] results = {ApiStats.RESULT_UNKNOWN, ApiStats.RESULT_NORMAL,
                 ApiStats.RESULT_EXCEPTION, ApiStats.RESULT_PERMISSION};
-        ApiStats apiStats = spy(new ApiStats(mSpyContext, mLooper));
+        ApiStats apiStats = spy(new ApiStats(mSpyContext, mLooper, false));
         Random rand = new Random();
         Map<ApiStats.ApiEvent, Integer> eventMap = new HashMap<>();
 
@@ -408,7 +419,7 @@
 
     @Test
     public void testAudioRouteStatsLog() throws Exception {
-        AudioRouteStats audioRouteStats = spy(new AudioRouteStats(mSpyContext, mLooper));
+        AudioRouteStats audioRouteStats = spy(new AudioRouteStats(mSpyContext, mLooper, false));
 
         audioRouteStats.log(VALUE_AUDIO_ROUTE_TYPE1, VALUE_AUDIO_ROUTE_TYPE2, true, false,
                 VALUE_AUDIO_ROUTE_LATENCY);
@@ -436,7 +447,7 @@
     @Test
     public void testAudioRouteStatsOnEnterThenExit() throws Exception {
         int latency = 500;
-        AudioRouteStats audioRouteStats = spy(new AudioRouteStats(mSpyContext, mLooper));
+        AudioRouteStats audioRouteStats = spy(new AudioRouteStats(mSpyContext, mLooper, false));
 
         audioRouteStats.onRouteEnter(mMockPendingAudioRoute);
         waitForHandlerActionDelayed(audioRouteStats, TEST_TIMEOUT, latency);
@@ -466,7 +477,7 @@
         int delay = 100;
         int latency = 500;
         int duration = 1000;
-        AudioRouteStats audioRouteStats = spy(new AudioRouteStats(mSpyContext, mLooper));
+        AudioRouteStats audioRouteStats = spy(new AudioRouteStats(mSpyContext, mLooper, false));
 
         audioRouteStats.onRouteEnter(mMockPendingAudioRoute);
         waitForHandlerActionDelayed(audioRouteStats, TEST_TIMEOUT, latency);
@@ -502,7 +513,7 @@
     public void testAudioRouteStatsOnRevertToSourceBeyondThreshold() throws Exception {
         int delay = 100;
         int latency = 500;
-        AudioRouteStats audioRouteStats = spy(new AudioRouteStats(mSpyContext, mLooper));
+        AudioRouteStats audioRouteStats = spy(new AudioRouteStats(mSpyContext, mLooper, false));
 
         audioRouteStats.onRouteEnter(mMockPendingAudioRoute);
         waitForHandlerActionDelayed(audioRouteStats, TEST_TIMEOUT, latency);
@@ -540,7 +551,7 @@
         int delay = 100;
         int latency = 500;
         int duration = 1000;
-        AudioRouteStats audioRouteStats = spy(new AudioRouteStats(mSpyContext, mLooper));
+        AudioRouteStats audioRouteStats = spy(new AudioRouteStats(mSpyContext, mLooper, false));
 
         audioRouteStats.onRouteEnter(mMockPendingAudioRoute);
         waitForHandlerActionDelayed(audioRouteStats, TEST_TIMEOUT, latency);
@@ -575,7 +586,7 @@
     @Test
     public void testAudioRouteStatsOnMultipleEnterWithoutExit() throws Exception {
         int latency = 500;
-        AudioRouteStats audioRouteStats = spy(new AudioRouteStats(mSpyContext, mLooper));
+        AudioRouteStats audioRouteStats = spy(new AudioRouteStats(mSpyContext, mLooper, false));
 
         audioRouteStats.onRouteEnter(mMockPendingAudioRoute);
         waitForHandlerActionDelayed(audioRouteStats, TEST_TIMEOUT, latency);
@@ -596,7 +607,7 @@
     @Test
     public void testAudioRouteStatsOnMultipleEnterWithExit() throws Exception {
         int latency = 500;
-        AudioRouteStats audioRouteStats = spy(new AudioRouteStats(mSpyContext, mLooper));
+        AudioRouteStats audioRouteStats = spy(new AudioRouteStats(mSpyContext, mLooper, false));
 
         audioRouteStats.onRouteEnter(mMockPendingAudioRoute);
         waitForHandlerActionDelayed(audioRouteStats, TEST_TIMEOUT, latency);
@@ -619,7 +630,7 @@
     @Test
     public void testAudioRouteStatsOnRouteToSameDestWithExit() throws Exception {
         int latency = 500;
-        AudioRouteStats audioRouteStats = spy(new AudioRouteStats(mSpyContext, mLooper));
+        AudioRouteStats audioRouteStats = spy(new AudioRouteStats(mSpyContext, mLooper, false));
         doReturn(mMockSourceRoute).when(mMockPendingAudioRoute).getDestRoute();
 
         audioRouteStats.onRouteEnter(mMockPendingAudioRoute);
@@ -640,7 +651,7 @@
 
     @Test
     public void testCallStatsLog() throws Exception {
-        CallStats callStats = spy(new CallStats(mSpyContext, mLooper));
+        CallStats callStats = spy(new CallStats(mSpyContext, mLooper, false));
 
         callStats.log(VALUE_CALL_DIRECTION, false, false, true, VALUE_CALL_ACCOUNT_TYPE,
                 VALUE_UID, VALUE_CALL_DURATION);
@@ -681,6 +692,8 @@
         doReturn(cn).when(handle).getComponentName();
         Call call = mock(Call.class);
         doReturn(true).when(call).isIncoming();
+        doReturn(new DisconnectCause(0)).when(call).getDisconnectCause();
+        doReturn(0).when(call).getSimultaneousType();
         doReturn(account).when(call).getPhoneAccountFromHandle();
         doReturn((long) duration).when(call).getAgeMillis();
         doReturn(false).when(account).hasCapabilities(eq(PhoneAccount.CAPABILITY_SELF_MANAGED));
@@ -688,7 +701,7 @@
         doReturn(true).when(account).hasCapabilities(eq(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION));
         doReturn(callingPackage).when(call).getCallingPackageIdentity();
         doReturn(handle).when(call).getTargetPhoneAccount();
-        CallStats callStats = spy(new CallStats(mSpyContext, mLooper));
+        CallStats callStats = spy(new CallStats(mSpyContext, mLooper, false));
 
         callStats.onCallStart(call);
         waitForHandlerAction(callStats, TEST_TIMEOUT);
@@ -698,7 +711,7 @@
 
         verify(callStats, times(1)).log(eq(CALL_STATS__CALL_DIRECTION__DIR_INCOMING),
                 eq(false), eq(false), eq(false), eq(CALL_STATS__ACCOUNT_TYPE__ACCOUNT_SIM),
-                eq(fakeUid), eq(duration));
+                eq(fakeUid), eq(0), eq(0), eq(duration));
     }
 
     @Test
@@ -719,6 +732,8 @@
         doReturn(cn).when(handle).getComponentName();
         Call call = mock(Call.class);
         doReturn(true).when(call).isIncoming();
+        doReturn(new DisconnectCause(0)).when(call).getDisconnectCause();
+        doReturn(0).when(call).getSimultaneousType();
         doReturn(account).when(call).getPhoneAccountFromHandle();
         doReturn((long) duration).when(call).getAgeMillis();
         doReturn(false).when(account).hasCapabilities(eq(PhoneAccount.CAPABILITY_SELF_MANAGED));
@@ -726,7 +741,7 @@
         doReturn(true).when(account).hasCapabilities(eq(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION));
         doReturn(callingPackage).when(call).getCallingPackageIdentity();
         doReturn(handle).when(call).getTargetPhoneAccount();
-        CallStats callStats = spy(new CallStats(mSpyContext, mLooper));
+        CallStats callStats = spy(new CallStats(mSpyContext, mLooper, false));
 
         callStats.onCallStart(call);
         waitForHandlerAction(callStats, TEST_TIMEOUT);
@@ -739,12 +754,12 @@
 
         verify(callStats, times(1)).log(eq(CALL_STATS__CALL_DIRECTION__DIR_INCOMING),
                 eq(false), eq(false), eq(true), eq(CALL_STATS__ACCOUNT_TYPE__ACCOUNT_SIM),
-                eq(fakeUid), eq(duration));
+                eq(fakeUid), eq(0), eq(0), eq(duration));
     }
 
     @Test
     public void testErrorStatsLogCount() throws Exception {
-        ErrorStats errorStats = spy(new ErrorStats(mSpyContext, mLooper));
+        ErrorStats errorStats = spy(new ErrorStats(mSpyContext, mLooper, false));
         for (int i = 0; i < 10; i++) {
             errorStats.log(VALUE_MODULE_ID, VALUE_ERROR_ID);
             waitForHandlerAction(errorStats, TEST_TIMEOUT);
@@ -760,7 +775,7 @@
 
     @Test
     public void testErrorStatsLogEvent() throws Exception {
-        ErrorStats errorStats = spy(new ErrorStats(mSpyContext, mLooper));
+        ErrorStats errorStats = spy(new ErrorStats(mSpyContext, mLooper, false));
         int[] modules = {
                 ErrorStats.SUB_UNKNOWN,
                 ErrorStats.SUB_CALL_AUDIO,
@@ -823,6 +838,142 @@
         }
     }
 
+    @Test
+    public void testApiStatsWithTestModeOn() throws Exception {
+        final List<StatsEvent> data = new ArrayList<>();
+        ApiStats apiStats = spy(new ApiStats(mSpyContext, mLooper, true));
+        apiStats.pull(data);
+        apiStats.flush();
+
+        verify(mSpyContext, never()).getFileStreamPath(anyString());
+        verify(apiStats, times(1)).onPull(any());
+        verify(mSpyContext, never()).openFileOutput(anyString(), anyInt());
+    }
+
+    @Test
+    public void testAudioRouteStatsWithTestModeOn() throws Exception {
+        final List<StatsEvent> data = new ArrayList<>();
+        AudioRouteStats audioRouteStats = spy(new AudioRouteStats(mSpyContext, mLooper, true));
+        audioRouteStats.pull(data);
+        audioRouteStats.flush();
+
+        verify(mSpyContext, never()).getFileStreamPath(anyString());
+        verify(audioRouteStats, times(1)).onPull(any());
+        verify(mSpyContext, never()).openFileOutput(anyString(), anyInt());
+    }
+
+    @Test
+    public void testCallStatsWithTestModeOn() throws Exception {
+        final List<StatsEvent> data = new ArrayList<>();
+        CallStats callStats = spy(new CallStats(mSpyContext, mLooper, true));
+        callStats.pull(data);
+        callStats.flush();
+
+        verify(mSpyContext, never()).getFileStreamPath(anyString());
+        verify(callStats, times(1)).onPull(any());
+        verify(mSpyContext, never()).openFileOutput(anyString(), anyInt());
+    }
+
+    @Test
+    public void testErrorStatsWithTestModeOn() throws Exception {
+        final List<StatsEvent> data = new ArrayList<>();
+        ErrorStats errorStats = spy(new ErrorStats(mSpyContext, mLooper, true));
+        errorStats.pull(data);
+        errorStats.flush();
+
+        verify(mSpyContext, never()).getFileStreamPath(anyString());
+        verify(errorStats, times(1)).onPull(any());
+        verify(mSpyContext, never()).openFileOutput(anyString(), anyInt());
+    }
+
+    @Test
+    public void testPullEventStatsLessThanMinPullIntervalShouldSkip() throws Exception {
+        createTestFileForEventStats(System.currentTimeMillis() - MIN_PULL_INTERVAL_MILLIS / 2);
+        EventStats eventStats = spy(new EventStats(mSpyContext, mLooper, false));
+        final List<StatsEvent> data = new ArrayList<>();
+
+        int result = eventStats.pull(data);
+
+        assertEquals(StatsManager.PULL_SKIP, result);
+        verify(eventStats, never()).onPull(any());
+        assertEquals(data.size(), 0);
+    }
+
+    @Test
+    public void testPullEventStatsGreaterThanMinPullIntervalShouldNotSkip() throws Exception {
+        createTestFileForEventStats(System.currentTimeMillis() - MIN_PULL_INTERVAL_MILLIS - 1);
+        EventStats eventStats = spy(new EventStats(mSpyContext, mLooper, false));
+        final List<StatsEvent> data = new ArrayList<>();
+        int sizePulled = eventStats.mPulledAtoms.telecomEventStats.length;
+
+        int result = eventStats.pull(data);
+
+        assertEquals(StatsManager.PULL_SUCCESS, result);
+        verify(eventStats).onPull(eq(data));
+        assertEquals(data.size(), sizePulled);
+        assertEquals(eventStats.mPulledAtoms.telecomEventStats.length, 0);
+    }
+
+    @Test
+    public void testEventStatsLogCount() throws Exception {
+        EventStats eventStats = spy(new EventStats(mSpyContext, mLooper, false));
+        EventStats.CriticalEvent event = new EventStats.CriticalEvent(
+                VALUE_EVENT_ID, VALUE_UID, VALUE_CAUSE_ID);
+
+        for (int i = 0; i < 10; i++) {
+            eventStats.log(event);
+            waitForHandlerAction(eventStats, TEST_TIMEOUT);
+
+            verify(eventStats, times(i + 1)).onAggregate();
+            verify(eventStats, times(i + 1)).save(eq(DELAY_FOR_PERSISTENT_MILLIS));
+            assertEquals(eventStats.mPulledAtoms.telecomEventStats.length, 1);
+            verifyMessageForEventStats(eventStats.mPulledAtoms.telecomEventStats[0],
+                    VALUE_EVENT_ID, VALUE_UID, VALUE_CAUSE_ID, i + 1);
+        }
+    }
+
+    @Test
+    public void testEventStatsLogEvent() throws Exception {
+        EventStats eventStats = spy(new EventStats(mSpyContext, mLooper, false));
+        int[] events = {
+                EventStats.ID_UNKNOWN,
+                EventStats.ID_INIT,
+                EventStats.ID_DEFAULT_DIALER_CHANGED,
+                EventStats.ID_ADD_CALL,
+        };
+        int[] causes = {
+                EventStats.CAUSE_UNKNOWN,
+                EventStats.CAUSE_GENERIC_SUCCESS,
+                EventStats.CAUSE_GENERIC_FAILURE,
+                EventStats.CAUSE_CALL_TRANSACTION_SUCCESS,
+                EventStats.CAUSE_CALL_TRANSACTION_ERROR_UNKNOWN,
+                EventStats.CAUSE_CALL_TRANSACTION_CALL_CANNOT_BE_SET_TO_ACTIVE,
+                EventStats.CAUSE_CALL_TRANSACTION_CALL_IS_NOT_BEING_TRACKED,
+                EventStats.CAUSE_CALL_TRANSACTION_CANNOT_HOLD_CURRENT_ACTIVE_CALL,
+                EventStats.CAUSE_CALL_TRANSACTION_CALL_NOT_PERMITTED_AT_PRESENT_TIME,
+                EventStats.CAUSE_CALL_TRANSACTION_OPERATION_TIMED_OUT,
+        };
+        Random rand = new Random();
+        Map<EventStats.CriticalEvent, Integer> eventMap = new HashMap<>();
+
+        for (int i = 0; i < 10; i++) {
+            int e = events[rand.nextInt(events.length)];
+            int uid = rand.nextInt(65535);
+            int cause = causes[rand.nextInt(causes.length)];
+            EventStats.CriticalEvent ce = new EventStats.CriticalEvent(e, uid, cause);
+            eventMap.put(ce, eventMap.getOrDefault(ce, 0) + 1);
+
+            eventStats.log(ce);
+            waitForHandlerAction(eventStats, TEST_TIMEOUT);
+
+            verify(eventStats, times(i + 1)).onAggregate();
+            verify(eventStats, times(i + 1)).save(eq(DELAY_FOR_PERSISTENT_MILLIS));
+            assertEquals(eventStats.mPulledAtoms.telecomEventStats.length, eventMap.size());
+            assertTrue(hasMessageForEventStats(eventStats.mPulledAtoms.telecomEventStats,
+                    e, uid, cause, eventMap.get(ce)));
+        }
+    }
+
     private void createTestFileForApiStats(long timestamps) throws IOException {
         PulledAtomsClass.PulledAtoms atom = new PulledAtomsClass.PulledAtoms();
         atom.telecomApiStats =
@@ -989,8 +1140,8 @@
         assertEquals(atom.telecomErrorStats.length, VALUE_ATOM_COUNT);
         for (int i = 0; i < VALUE_ATOM_COUNT; i++) {
             assertNotNull(atom.telecomErrorStats[i]);
-            verifyMessageForErrorStats(atom.telecomErrorStats[i], VALUE_MODULE_ID, VALUE_ERROR_ID
-                    , VALUE_ERROR_COUNT);
+            verifyMessageForErrorStats(atom.telecomErrorStats[i], VALUE_MODULE_ID,
+                    VALUE_ERROR_ID, VALUE_ERROR_COUNT);
         }
     }
 
@@ -1011,4 +1162,53 @@
         }
         return false;
     }
+
+    private void createTestFileForEventStats(long timestamps) throws IOException {
+        PulledAtomsClass.PulledAtoms atom = new PulledAtomsClass.PulledAtoms();
+        atom.telecomEventStats =
+                new PulledAtomsClass.TelecomEventStats[VALUE_ATOM_COUNT];
+        for (int i = 0; i < VALUE_ATOM_COUNT; i++) {
+            atom.telecomEventStats[i] = new PulledAtomsClass.TelecomEventStats();
+            atom.telecomEventStats[i].setEvent(VALUE_EVENT_ID + i);
+            atom.telecomEventStats[i].setUid(VALUE_UID);
+            atom.telecomEventStats[i].setEventCause(VALUE_CAUSE_ID);
+            atom.telecomEventStats[i].setCount(VALUE_EVENT_COUNT);
+        }
+        atom.setTelecomEventStatsPullTimestampMillis(timestamps);
+        FileOutputStream stream = new FileOutputStream(mTempFile);
+        stream.write(PulledAtomsClass.PulledAtoms.toByteArray(atom));
+        stream.close();
+    }
+
+    private void verifyTestDataForEventStats(
+            final PulledAtomsClass.PulledAtoms atom, long timestamps) {
+        assertNotNull(atom);
+        assertEquals(atom.getTelecomEventStatsPullTimestampMillis(), timestamps);
+        assertNotNull(atom.telecomEventStats);
+        assertEquals(atom.telecomEventStats.length, VALUE_ATOM_COUNT);
+        for (int i = 0; i < VALUE_ATOM_COUNT; i++) {
+            assertNotNull(atom.telecomEventStats[i]);
+            verifyMessageForEventStats(atom.telecomEventStats[i], VALUE_EVENT_ID + i,
+                    VALUE_UID, VALUE_CAUSE_ID, VALUE_EVENT_COUNT);
+        }
+    }
+
+    private void verifyMessageForEventStats(final PulledAtomsClass.TelecomEventStats msg,
+                                            int eventId, int uid, int causeId, int count) {
+        assertEquals(msg.getEvent(), eventId);
+        assertEquals(msg.getUid(), uid);
+        assertEquals(msg.getEventCause(), causeId);
+        assertEquals(msg.getCount(), count);
+    }
+
+    private boolean hasMessageForEventStats(final PulledAtomsClass.TelecomEventStats[] msgs,
+                                            int eventId, int uid, int causeId, int count) {
+        for (PulledAtomsClass.TelecomEventStats msg : msgs) {
+            if (msg.getEvent() == eventId && msg.getUid() == uid
+                    && msg.getEventCause() == causeId && msg.getCount() == count) {
+                return true;
+            }
+        }
+        return false;
+    }
 }
diff --git a/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java b/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java
index 6b0555c..d002739 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java
@@ -93,6 +93,7 @@
 import com.android.server.telecom.PhoneAccountRegistrar;
 import com.android.server.telecom.TelecomServiceImpl;
 import com.android.server.telecom.TelecomSystem;
+import com.android.server.telecom.callsequencing.CallTransaction;
 import com.android.server.telecom.components.UserCallIntentProcessor;
 import com.android.server.telecom.components.UserCallIntentProcessorFactory;
 import com.android.server.telecom.flags.FeatureFlags;
@@ -116,6 +117,7 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.Executor;
 import java.util.function.IntConsumer;
 
@@ -206,6 +208,8 @@
 
     @Mock private InCallController mInCallController;
     @Mock private TelecomMetricsController mMockTelecomMetricsController;
+    @Mock private OutgoingCallTransaction mOutgoingCallTransaction;
+    @Mock private IncomingCallTransaction mIncomingCallTransaction;
 
     private final TelecomSystem.SyncRoot mLock = new TelecomSystem.SyncRoot() { };
 
@@ -282,6 +286,7 @@
         when(mPackageManager.getPackageUid(anyString(), eq(0))).thenReturn(Binder.getCallingUid());
         when(mFeatureFlags.earlyBindingToIncallService()).thenReturn(true);
         when(mTelephonyFeatureFlags.workProfileApiSplit()).thenReturn(false);
+        when(mFeatureFlags.enableCallSequencing()).thenReturn(false);
     }
 
     @Override
@@ -457,6 +462,9 @@
         // WHEN
         when(mFakePhoneAccountRegistrar.getPhoneAccountUnchecked(TEL_PA_HANDLE_CURRENT)).thenReturn(
                 phoneAccount);
+        when(mFakeCallsManager.createTransactionalCall(any(String.class),
+                any(CallAttributes.class), any(Bundle.class), any(String.class)))
+                .thenReturn(CompletableFuture.completedFuture(mOutgoingCallTransaction));
 
         doReturn(phoneAccount).when(mFakePhoneAccountRegistrar).getPhoneAccount(
                 eq(TEL_PA_HANDLE_CURRENT), any(UserHandle.class));
@@ -485,6 +493,9 @@
 
         doReturn(phoneAccount).when(mFakePhoneAccountRegistrar).getPhoneAccount(
                 eq(TEL_PA_HANDLE_CURRENT), any(UserHandle.class));
+        when(mFakeCallsManager.createTransactionalCall(any(String.class),
+                any(CallAttributes.class), any(Bundle.class), any(String.class)))
+                .thenReturn(CompletableFuture.completedFuture(mIncomingCallTransaction));
 
         mTSIBinder.addCall(mIncomingCallAttributes, mICallEventCallback, "1", CALLING_PACKAGE);
 
@@ -1085,9 +1096,10 @@
     @Test
     public void testRegisterPhoneAccountImageIconCrossUser() throws RemoteException {
         String packageNameToUse = "com.android.officialpackage";
+        String callingUserId = String.valueOf(Binder.getCallingUserHandle().getIdentifier());
         PhoneAccountHandle phHandle = new PhoneAccountHandle(new ComponentName(
                 packageNameToUse, "cs"), "test", Binder.getCallingUserHandle());
-        Icon icon = Icon.createWithContentUri("content://10@media/external/images/media/");
+        Icon icon = Icon.createWithContentUri("content://12@media/external/images/media/");
         PhoneAccount phoneAccount = makePhoneAccount(phHandle).setIcon(icon).build();
         doReturn(PackageManager.PERMISSION_GRANTED)
                 .when(mContext).checkCallingOrSelfPermission(MODIFY_PHONE_STATE);
@@ -1095,7 +1107,23 @@
         // This should fail; security exception will be thrown.
         registerPhoneAccountTestHelper(phoneAccount, false);
 
-        icon = Icon.createWithContentUri("content://0@media/external/images/media/");
+        icon = Icon.createWithContentUri(
+                new Uri.Builder().scheme("content")
+                        .encodedAuthority("12%40media")
+                        .path("external/images/media/${mediaId.text}".trim())
+                        .build());
+        phoneAccount = makePhoneAccount(phHandle).setIcon(icon).build();
+        // This should fail; security exception will be thrown
+        registerPhoneAccountTestHelper(phoneAccount, false);
+
+        icon = Icon.createWithContentUri( Uri.parse("content://12%40play.ground"));
+        phoneAccount = makePhoneAccount(phHandle).setIcon(icon).build();
+        // This should fail; security exception will be thrown
+        registerPhoneAccountTestHelper(phoneAccount, false);
+
+        // Generate a URI referencing the calling/current user ID:
+        String currentUserUri = "content://" + callingUserId + "@media/external/images/media/";
+        icon = Icon.createWithContentUri(currentUserUri);
         phoneAccount = makePhoneAccount(phHandle).setIcon(icon).build();
         // This should succeed.
         registerPhoneAccountTestHelper(phoneAccount, true);
diff --git a/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java b/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
index 1e65011..4aceae4 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
@@ -99,6 +99,7 @@
 import com.android.server.telecom.WiredHeadsetManager;
 import com.android.server.telecom.bluetooth.BluetoothRouteManager;
 import com.android.server.telecom.callfiltering.BlockedNumbersAdapter;
+import com.android.server.telecom.callsequencing.voip.VoipCallMonitor;
 import com.android.server.telecom.components.UserCallIntentProcessor;
 import com.android.server.telecom.flags.FeatureFlags;
 import com.android.server.telecom.ui.IncomingCallNotifier;
@@ -418,7 +419,11 @@
                 handlerThread.quitSafely();
             }
             handlerThreads.clear();
-            mTelecomSystem.getCallsManager().getVoipCallMonitor().stopMonitor();
+
+            VoipCallMonitor vcm = mTelecomSystem.getCallsManager().getVoipCallMonitor();
+            if (vcm != null) {
+                vcm.unregisterNotificationListener();
+            }
         }
         waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT);
         waitForHandlerAction(mHandlerThread.getThreadHandler(), TEST_TIMEOUT);
@@ -518,6 +523,7 @@
         when(mRoleManagerAdapter.getDefaultCallScreeningApp(any(UserHandle.class)))
                 .thenReturn(null);
         when(mRoleManagerAdapter.getBTInCallService()).thenReturn(new String[] {"bt_pkg"});
+        when(mFeatureFlags.callAudioCommunicationDeviceRefactor()).thenReturn(true);
         when(mFeatureFlags.useRefactoredAudioRouteSwitching()).thenReturn(false);
         mTelecomSystem = new TelecomSystem(
                 mComponentContextFixture.getTestDouble(),
@@ -587,7 +593,8 @@
                 Runnable::run,
                 mBlockedNumbersAdapter,
                 mFeatureFlags,
-                mTelephonyFlags);
+                mTelephonyFlags,
+                mHandlerThread.getLooper());
 
         mComponentContextFixture.setTelecomManager(new TelecomManager(
                 mComponentContextFixture.getTestDouble(),
diff --git a/tests/src/com/android/server/telecom/tests/TelecomTestCase.java b/tests/src/com/android/server/telecom/tests/TelecomTestCase.java
index 5b5c3ed..0b1c17c 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomTestCase.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomTestCase.java
@@ -49,6 +49,7 @@
         mMockitoHelper.setUp(InstrumentationRegistry.getContext(), getClass());
         MockitoAnnotations.initMocks(this);
 
+        Mockito.when(mFeatureFlags.callAudioCommunicationDeviceRefactor()).thenReturn(true);
         mComponentContextFixture = new ComponentContextFixture(mFeatureFlags);
         mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
         Log.setSessionManager(mComponentContextFixture.getTestDouble().getApplicationContext(),
@@ -56,6 +57,7 @@
     }
 
     public void tearDown() throws Exception {
+        mComponentContextFixture.destroy();
         mComponentContextFixture = null;
         mMockitoHelper.tearDown();
         Mockito.framework().clearInlineMocks();
diff --git a/tests/src/com/android/server/telecom/tests/TransactionTests.java b/tests/src/com/android/server/telecom/tests/TransactionTests.java
index 78c2210..6c049f6 100644
--- a/tests/src/com/android/server/telecom/tests/TransactionTests.java
+++ b/tests/src/com/android/server/telecom/tests/TransactionTests.java
@@ -63,6 +63,7 @@
 import com.android.server.telecom.PhoneNumberUtilsAdapter;
 import com.android.server.telecom.TelecomSystem;
 import com.android.server.telecom.callsequencing.CallTransactionResult;
+import com.android.server.telecom.callsequencing.CallsManagerCallSequencingAdapter;
 import com.android.server.telecom.callsequencing.TransactionManager;
 import com.android.server.telecom.callsequencing.VerifyCallStateChangeTransaction;
 import com.android.server.telecom.callsequencing.voip.EndCallTransaction;
@@ -97,6 +98,7 @@
     @Mock private Call mMockCall1;
     @Mock private Context mMockContext;
     @Mock private CallsManager mCallsManager;
+    @Mock private CallsManagerCallSequencingAdapter mCallSequencingAdapter;
     @Mock private ToastFactory mToastFactory;
     @Mock private ClockProxy mClockProxy;
     @Mock private PhoneNumberUtilsAdapter mPhoneNumberUtilsAdapter;
@@ -113,6 +115,7 @@
         MockitoAnnotations.initMocks(this);
         Mockito.when(mMockCall1.getId()).thenReturn(CALL_ID_1);
         Mockito.when(mMockContext.getResources()).thenReturn(Mockito.mock(Resources.class));
+        when(mCallsManager.getCallSequencingAdapter()).thenReturn(mCallSequencingAdapter);
     }
 
     @Override
@@ -220,7 +223,7 @@
         transaction.processTransaction(null);
 
         // THEN
-        verify(mCallsManager, times(1))
+        verify(mCallsManager.getCallSequencingAdapter(), times(1))
                 .transactionHoldPotentialActiveCallForNewCall(eq(mMockCall1), eq(false),
                         isA(OutcomeReceiver.class));
     }
@@ -292,20 +295,21 @@
                 .setCallType(CallAttributes.VIDEO_CALL)
                 .build();
 
+        Bundle extras = new Bundle();
         OutgoingCallTransaction t = new OutgoingCallTransaction(null,
-                mContext, null, mCallsManager, new Bundle(), mFeatureFlags);
+                mContext, null, mCallsManager, extras, mFeatureFlags);
 
         // WHEN
         when(mFeatureFlags.transactionalVideoState()).thenReturn(true);
         t.setFeatureFlags(mFeatureFlags);
 
         // THEN
-        assertEquals(VideoProfile.STATE_AUDIO_ONLY, t
-                .generateExtras(audioOnlyAttributes)
+        assertEquals(VideoProfile.STATE_AUDIO_ONLY, OutgoingCallTransaction
+                .generateExtras(null, extras, audioOnlyAttributes, mFeatureFlags)
                 .getInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE));
 
-        assertEquals(VideoProfile.STATE_BIDIRECTIONAL, t
-                .generateExtras(videoAttributes)
+        assertEquals(VideoProfile.STATE_BIDIRECTIONAL, OutgoingCallTransaction
+                .generateExtras(null, extras, videoAttributes, mFeatureFlags)
                 .getInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE));
     }
 
@@ -448,9 +452,9 @@
         callSpy.setState(initialState, "manual set in test");
 
         // Mocks some methods to not call the real method.
-        doNothing().when(callSpy).unhold();
-        doNothing().when(callSpy).hold();
-        doNothing().when(callSpy).disconnect();
+        doReturn(null).when(callSpy).unhold();
+        doReturn(null).when(callSpy).hold();
+        doReturn(null).when(callSpy).disconnect();
 
         return callSpy;
     }
diff --git a/tests/src/com/android/server/telecom/tests/TransactionalServiceWrapperTest.java b/tests/src/com/android/server/telecom/tests/TransactionalServiceWrapperTest.java
index fea6135..16b6e44 100644
--- a/tests/src/com/android/server/telecom/tests/TransactionalServiceWrapperTest.java
+++ b/tests/src/com/android/server/telecom/tests/TransactionalServiceWrapperTest.java
@@ -34,6 +34,7 @@
 
 import com.android.internal.telecom.ICallControl;
 import com.android.internal.telecom.ICallEventCallback;
+import com.android.server.telecom.AnomalyReporterAdapter;
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallsManager;
 import com.android.server.telecom.TelecomSystem;
@@ -70,6 +71,7 @@
     @Mock private TransactionManager mTransactionManager;
     @Mock private ICallEventCallback mCallEventCallback;
     @Mock private TransactionalServiceRepository mRepository;
+    @Mock private AnomalyReporterAdapter mAnomalyReporterAdapter;
     @Mock private IBinder mIBinder;
     private final TelecomSystem.SyncRoot mLock = new TelecomSystem.SyncRoot() {};
 
@@ -84,7 +86,7 @@
         Mockito.when(mCallEventCallback.asBinder()).thenReturn(mIBinder);
         mTransactionalServiceWrapper = new TransactionalServiceWrapper(mCallEventCallback,
                 mCallsManager, SERVICE_HANDLE, mMockCall1, mRepository, mTransactionManager,
-                false /*call sequencing*/);
+                false /*call sequencing*/, mFeatureFlags, mAnomalyReporterAdapter);
     }
 
     @Override
@@ -98,7 +100,7 @@
         TransactionalServiceWrapper service =
                 new TransactionalServiceWrapper(mCallEventCallback,
                         mCallsManager, SERVICE_HANDLE, mMockCall1, mRepository, mTransactionManager,
-                        false /*call sequencing*/);
+                        false /*call sequencing*/, mFeatureFlags, mAnomalyReporterAdapter);
 
         assertEquals(SERVICE_HANDLE, service.getPhoneAccountHandle());
         assertEquals(1, service.getNumberOfTrackedCalls());
@@ -109,7 +111,7 @@
         TransactionalServiceWrapper service =
                 new TransactionalServiceWrapper(mCallEventCallback,
                         mCallsManager, SERVICE_HANDLE, mMockCall1, mRepository, mTransactionManager,
-                        false /*call sequencing*/);
+                        false /*call sequencing*/, mFeatureFlags, mAnomalyReporterAdapter);
 
         assertEquals(1, service.getNumberOfTrackedCalls());
         service.trackCall(mMockCall2);
diff --git a/tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java b/tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java
index bf68f8c..1b3856c 100644
--- a/tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java
+++ b/tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java
@@ -22,11 +22,16 @@
 import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -39,6 +44,7 @@
 import android.content.Intent;
 import android.content.ServiceConnection;
 import android.os.Bundle;
+import android.os.Handler;
 import android.os.IBinder;
 import android.os.UserHandle;
 import android.service.notification.StatusBarNotification;
@@ -51,13 +57,18 @@
 import com.android.server.telecom.TelecomSystem;
 import com.android.server.telecom.callsequencing.voip.VoipCallMonitor;
 
+import org.junit.After;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
 @RunWith(JUnit4.class)
 public class VoipCallMonitorTest extends TelecomTestCase {
     private VoipCallMonitor mMonitor;
@@ -68,8 +79,9 @@
     private static final String ID_1 = "id1";
     public static final String CHANNEL_ID = "TelecomVoipAppChannelId";
     private static final UserHandle USER_HANDLE_1 = new UserHandle(1);
-    private static final long TIMEOUT = 5000L;
+    private static final long TIMEOUT = 6000L;
 
+    @Mock private Handler mHandler;
     @Mock private TelecomSystem.SyncRoot mLock;
     @Mock private ActivityManagerInternal mActivityManagerInternal;
     @Mock private IBinder mServiceConnection;
@@ -83,15 +95,23 @@
     @Before
     public void setUp() throws Exception {
         super.setUp();
-        mMonitor = new VoipCallMonitor(mContext, mLock);
+        mHandler = mock(Handler.class);
+        mMonitor = new VoipCallMonitor(mContext, mHandler, mLock);
         mActivityManagerInternal = mock(ActivityManagerInternal.class);
         mMonitor.setActivityManagerInternal(mActivityManagerInternal);
-        mMonitor.startMonitor();
+        mMonitor.registerNotificationListener();
         when(mActivityManagerInternal.startForegroundServiceDelegate(any(
                 ForegroundServiceDelegationOptions.class), any(ServiceConnection.class)))
                 .thenReturn(true);
     }
 
+    @Override
+    @After
+    public void tearDown() throws Exception {
+        mMonitor.unregisterNotificationListener();
+        super.tearDown();
+    }
+
     /**
      * This test ensures VoipCallMonitor is passing the correct foregroundServiceTypes when starting
      * foreground service delegation on behalf of a client.
@@ -106,65 +126,98 @@
         mMonitor.onCallAdded(call);
 
         verify(mActivityManagerInternal, timeout(TIMEOUT)).startForegroundServiceDelegate(
-                 optionsCaptor.capture(), any(ServiceConnection.class));
+                optionsCaptor.capture(), any(ServiceConnection.class));
 
-        assertEquals( FOREGROUND_SERVICE_TYPE_PHONE_CALL |
-                FOREGROUND_SERVICE_TYPE_MICROPHONE |
-                FOREGROUND_SERVICE_TYPE_CAMERA |
-                FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE,
+        assertEquals(FOREGROUND_SERVICE_TYPE_PHONE_CALL |
+                        FOREGROUND_SERVICE_TYPE_MICROPHONE |
+                        FOREGROUND_SERVICE_TYPE_CAMERA |
+                        FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE,
                 optionsCaptor.getValue().mForegroundServiceTypes);
 
         mMonitor.onCallRemoved(call);
     }
 
+    /**
+     * Tests that {@link VoipCallMonitor#stopFGSDelegation} does not throw a NullPointerException
+     * when called on a transactional call that has not been tracked by the account to calls
+     * mapping, and that no calls are made to ActivityManagerInternal.stopForegroundServiceDelegate.
+     */
     @SmallTest
     @Test
-    public void testStartMonitorForOneCall() {
+    public void testStopFgsDelegationWithoutAnyTrackedCalls() {
+        //GIVEN: a transactional call that has NOT been added to the monitor tracking
         Call call = createTestCall("testCall", mHandle1User1);
-        IBinder service = mock(IBinder.class);
+        ConcurrentHashMap<PhoneAccountHandle, Set<Call>> m = mMonitor.getAccountToCallsMapping();
+        assertEquals(0, m.size());
+        assertNull(m.get(mHandle1User1));
 
-        ArgumentCaptor<ServiceConnection> captor = ArgumentCaptor.forClass(ServiceConnection.class);
-        mMonitor.onCallAdded(call);
-        verify(mActivityManagerInternal, timeout(TIMEOUT)).startForegroundServiceDelegate(any(
-                ForegroundServiceDelegationOptions.class), captor.capture());
-        ServiceConnection conn = captor.getValue();
-        conn.onServiceConnected(mHandle1User1.getComponentName(), service);
+        // WHEN: stop is called on the transactional call
+        mMonitor.stopFGSDelegation(call, mHandle1User1);
 
-        mMonitor.onCallRemoved(call);
-        verify(mActivityManagerInternal, timeout(TIMEOUT)).stopForegroundServiceDelegate(eq(conn));
+        // THEN: a NullPointerException should not be thrown at runtime
+        verify(mActivityManagerInternal, times(0))
+                .stopForegroundServiceDelegate(any(ServiceConnection.class));
+        assertEquals(0, m.size());
+        assertNull(m.get(mHandle1User1));
     }
 
     @SmallTest
     @Test
-    public void testMonitorForTwoCallsOnSameHandle() {
+    public void testStartMonitorForOneCall() {
+        // GIVEN - a single call and notification for a voip app
+        Call call = createTestCall("testCall", mHandle1User1);
+        StatusBarNotification sbn = createStatusBarNotificationFromHandle(mHandle1User1, 1);
+
+        // WHEN - the Voip call is added and a notification is posted, verify FGS is gained
+        addCallAndVerifyFgsIsGained(call);
+        mMonitor.postNotification(sbn);
+        assertNotificationTimeoutTriggered();
+        assertFalse(mMonitor.getNewCallsMissingCallStyleNotificationQueue().contains(call));
+
+        // THEN - when the Voip call is removed, verify that FGS is revoked for the app
+        mMonitor.onCallRemoved(call);
+        mMonitor.removeNotification(sbn);
+        verify(mActivityManagerInternal, times(1))
+                .stopForegroundServiceDelegate(any(ServiceConnection.class));
+    }
+
+    /**
+     * Verify FGS is not lost if another call is ongoing for a Voip app
+     */
+    @SmallTest
+    @Test
+    public void testStopDelegation_SameApp() {
+        // GIVEN - 2 consecutive calls for a single Voip app
         Call call1 = createTestCall("testCall1", mHandle1User1);
+        StatusBarNotification sbn1 = createStatusBarNotificationFromHandle(mHandle1User1, 1);
         Call call2 = createTestCall("testCall2", mHandle1User1);
-        IBinder service = mock(IBinder.class);
+        StatusBarNotification sbn2 = createStatusBarNotificationFromHandle(mHandle1User1, 2);
 
-        ArgumentCaptor<ServiceConnection> captor1 =
-                ArgumentCaptor.forClass(ServiceConnection.class);
-        mMonitor.onCallAdded(call1);
-        verify(mActivityManagerInternal, timeout(TIMEOUT).times(1))
-                .startForegroundServiceDelegate(any(ForegroundServiceDelegationOptions.class),
-                        captor1.capture());
-        ServiceConnection conn1 = captor1.getValue();
-        conn1.onServiceConnected(mHandle1User1.getComponentName(), service);
-
-        ArgumentCaptor<ServiceConnection> captor2 =
-                ArgumentCaptor.forClass(ServiceConnection.class);
+        // WHEN - the second call is added and the first is disconnected
+        // -- add the first all and post the corresponding notification
+        addCallAndVerifyFgsIsGained(call1);
+        assertTrue(mMonitor.getNewCallsMissingCallStyleNotificationQueue().contains(call1));
+        mMonitor.postNotification(sbn1);
+        assertNotificationTimeoutTriggered();
+        assertFalse(mMonitor.getNewCallsMissingCallStyleNotificationQueue().contains(call1));
+        // -- add the second call and post the corresponding notification
         mMonitor.onCallAdded(call2);
-        verify(mActivityManagerInternal, timeout(TIMEOUT).times(2))
-                .startForegroundServiceDelegate(any(ForegroundServiceDelegationOptions.class),
-                        captor2.capture());
-        ServiceConnection conn2 = captor2.getValue();
-        conn2.onServiceConnected(mHandle1User1.getComponentName(), service);
+        assertTrue(mMonitor.getNewCallsMissingCallStyleNotificationQueue().contains(call2));
+        mMonitor.postNotification(sbn2);
+        assertNotificationTimeoutTriggered();
+        assertFalse(mMonitor.getNewCallsMissingCallStyleNotificationQueue().contains(call2));
 
+        // THEN - assert FGS is maintained for the process since there is still an ongoing call
         mMonitor.onCallRemoved(call1);
-        verify(mActivityManagerInternal, never()).stopForegroundServiceDelegate(
-                any(ServiceConnection.class));
+        mMonitor.removeNotification(sbn1);
+        assertNotificationTimeoutTriggered();
+        verify(mActivityManagerInternal, times(0))
+                .stopForegroundServiceDelegate(any(ServiceConnection.class));
+        // once all calls are removed, verify FGS is stopped
         mMonitor.onCallRemoved(call2);
-        verify(mActivityManagerInternal, timeout(TIMEOUT).times(1))
-                .stopForegroundServiceDelegate(eq(conn2));
+        mMonitor.removeNotification(sbn2);
+        verify(mActivityManagerInternal, times(1))
+                .stopForegroundServiceDelegate(any(ServiceConnection.class));
     }
 
     @SmallTest
@@ -204,40 +257,6 @@
         verify(mActivityManagerInternal).stopForegroundServiceDelegate(eq(conn1));
     }
 
-    @SmallTest
-    @Test
-    public void testStopDelegation() {
-        Call call1 = createTestCall("testCall1", mHandle1User1);
-        Call call2 = createTestCall("testCall2", mHandle1User1);
-        IBinder service = mock(IBinder.class);
-
-        ArgumentCaptor<ServiceConnection> captor1 =
-                ArgumentCaptor.forClass(ServiceConnection.class);
-        mMonitor.onCallAdded(call1);
-        verify(mActivityManagerInternal, timeout(TIMEOUT).times(1))
-                .startForegroundServiceDelegate(any(ForegroundServiceDelegationOptions.class),
-                        captor1.capture());
-        ServiceConnection conn1 = captor1.getValue();
-        conn1.onServiceConnected(mHandle1User1.getComponentName(), service);
-
-        ArgumentCaptor<ServiceConnection> captor2 =
-                ArgumentCaptor.forClass(ServiceConnection.class);
-        mMonitor.onCallAdded(call2);
-        verify(mActivityManagerInternal, timeout(TIMEOUT).times(2))
-                .startForegroundServiceDelegate(any(ForegroundServiceDelegationOptions.class),
-                        captor2.capture());
-        ServiceConnection conn2 = captor2.getValue();
-        conn2.onServiceConnected(mHandle1User1.getComponentName(), service);
-
-        mMonitor.stopFGSDelegation(call1);
-        verify(mActivityManagerInternal, timeout(TIMEOUT).times(1))
-                .stopForegroundServiceDelegate(eq(conn2));
-        conn2.onServiceDisconnected(mHandle1User1.getComponentName());
-        mMonitor.onCallRemoved(call1);
-        verify(mActivityManagerInternal, timeout(TIMEOUT).times(1))
-                .stopForegroundServiceDelegate(any(ServiceConnection.class));
-    }
-
     /**
      * Ensure an app loses foreground service delegation if the user dismisses the call style
      * notification or the app removes the notification.
@@ -245,9 +264,10 @@
      */
     @SmallTest
     @Test
+    @Ignore("b/383403913") // when b/383403913 is fixed, remove the @Ignore
     public void testStopFgsIfCallNotificationIsRemoved_PostedAfterFgsIsGained() {
         // GIVEN
-        StatusBarNotification sbn = createStatusBarNotificationFromHandle(mHandle1User1);
+        StatusBarNotification sbn = createStatusBarNotificationFromHandle(mHandle1User1, 1);
 
         // WHEN
         // FGS is gained after the call is added to VoipCallMonitor
@@ -259,33 +279,71 @@
         // shortly after posting the notification, simulate the user dismissing it
         mMonitor.removeNotification(sbn);
         // FGS should be removed once the notification is removed
-        verify(mActivityManagerInternal, timeout(TIMEOUT)).stopForegroundServiceDelegate(c);
+        assertNotificationTimeoutTriggered();
+        verify(mActivityManagerInternal, times(1)).stopForegroundServiceDelegate(c);
     }
 
+
     /**
-     * Ensure an app loses foreground service delegation if the user dismisses the call style
-     * notification or the app removes the notification.
-     * Note: post the notification BEFORE foreground service delegation is gained
+     * Tests the behavior of foreground service (FGS) delegation for a VoIP app during a scenario
+     * with two consecutive calls.  In this scenario, the first call is disconnected shortly after
+     * being created but the second call continues.  The apps foreground service should be
+     * maintained.
+     *
+     * GIVEN: Two calls (call1 and call2) are created for the same VoIP app.
+     * WHEN:
+     *  - call1 is added, starting the FGS.
+     *  - call2 is added immediately after.
+     *  - call1 is removed.
+     *  - call1 notification is finally posted (late)
+     *  - call1 notification is removed shortly after since the call was disconnected
+     * THEN:
+     *  - Verifies that the FGS is NOT stopped while call2 is still active.
+     *  - Verifies that the FGS IS stopped after call2 is removed and its notification is gone.
      */
     @SmallTest
     @Test
-    public void testStopFgsIfCallNotificationIsRemoved_PostedBeforeFgsIsGained() {
-        // GIVEN
-        StatusBarNotification sbn = createStatusBarNotificationFromHandle(mHandle1User1);
+    public void test2CallsInQuickSuccession() {
+        // GIVEN - 2 consecutive calls for a single Voip app
+        Call call1 = createTestCall("testCall1", mHandle1User1);
+        StatusBarNotification sbn1 = createStatusBarNotificationFromHandle(mHandle1User1, 1);
+        Call call2 = createTestCall("testCall2", mHandle1User1);
+        StatusBarNotification sbn2 = createStatusBarNotificationFromHandle(mHandle1User1, 2);
 
-        // WHEN
-        //  an app posts a call style notification before FGS is gained
-        mMonitor.postNotification(sbn);
-        // FGS is gained after the call is added to VoipCallMonitor
-        ServiceConnection c = addCallAndVerifyFgsIsGained(createTestCall("1", mHandle1User1));
+        // WHEN - add the calls to the VoipCallMonitor class
+        addCallAndVerifyFgsIsGained(call1);
+        mMonitor.onCallAdded(call2);
+        assertTrue(mMonitor.getNewCallsMissingCallStyleNotificationQueue().contains(call1));
+        assertTrue(mMonitor.getNewCallsMissingCallStyleNotificationQueue().contains(call2));
+        // -- mock the app disconnecting the first
+        mMonitor.onCallRemoved(call1);
+        // Shortly after, simulate the notification updates coming in to the class
+        // -- post and remove the first call-style notification
+        mMonitor.postNotification(sbn1);
+        assertFalse(mMonitor.getNewCallsMissingCallStyleNotificationQueue().contains(call1));
+        mMonitor.removeNotification(sbn1);
+        assertNotificationTimeoutTriggered();
 
-        // THEN
-        // shortly after posting the notification, simulate the user dismissing it
-        mMonitor.removeNotification(sbn);
-        // FGS should be removed once the notification is removed
-        verify(mActivityManagerInternal, timeout(TIMEOUT)).stopForegroundServiceDelegate(c);
+        // -- keep the second notification up since the call will continue
+        mMonitor.postNotification(sbn2);
+        assertFalse(mMonitor.getNewCallsMissingCallStyleNotificationQueue().contains(call2));
+
+        // THEN - assert FGS is maintained for the process since there is still an ongoing call
+        assertNotificationTimeoutTriggered();
+        verify(mActivityManagerInternal, times(0))
+                .stopForegroundServiceDelegate(any(ServiceConnection.class));
+
+        // once all calls are removed, verify FGS is stopped
+        mMonitor.onCallRemoved(call2);
+        mMonitor.removeNotification(sbn2);
+        verify(mActivityManagerInternal, timeout(TIMEOUT).times(1))
+                .stopForegroundServiceDelegate(any(ServiceConnection.class));
     }
 
+    /**
+     * Helpers for testing
+     */
+
     private Call createTestCall(String id, PhoneAccountHandle handle) {
         Call call = mock(Call.class);
         when(call.getTargetPhoneAccount()).thenReturn(handle);
@@ -311,9 +369,10 @@
                 .build();
     }
 
-    private StatusBarNotification createStatusBarNotificationFromHandle(PhoneAccountHandle handle) {
+    private StatusBarNotification createStatusBarNotificationFromHandle(
+            PhoneAccountHandle handle, int id) {
         return new StatusBarNotification(
-                handle.getComponentName().getPackageName(), "", 0, "", 0, 0,
+                handle.getComponentName().getPackageName(), "", id, "", 0, 0,
                 createCallStyleNotification(), handle.getUserHandle(), "", 0);
     }
 
@@ -329,7 +388,22 @@
         // onServiceConnected must be called in order for VoipCallMonitor to start monitoring for
         // a notification before the timeout expires
         ServiceConnection serviceConnection = captor.getValue();
-        serviceConnection.onServiceConnected(mHandle1User1.getComponentName(), mServiceConnection);
+        serviceConnection.onServiceConnected(
+                call.getTargetPhoneAccount().getComponentName(),
+                mServiceConnection);
         return serviceConnection;
     }
+
+    /**
+     * Verifies that a delayed runnable is posted to the handler to handle the notification timeout.
+     * This also executes the captured runnable to simulate the timeout occurring.
+     */
+    private void assertNotificationTimeoutTriggered() {
+        ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class);
+        verify(mHandler, atLeastOnce()).postDelayed(
+                runnableCaptor.capture(),
+                eq(VoipCallMonitor.NOTIFICATION_NOT_POSTED_IN_TIME_TIMEOUT));
+        Runnable capturedRunnable = runnableCaptor.getValue();
+        capturedRunnable.run();
+    }
 }