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

am skip reason: contains skip directive

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

Change-Id: I874301750509b2e4cb8a2d159769ce5e28bdbee7
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/Android.bp b/Android.bp
index 0d1c81d..65e4402 100644
--- a/Android.bp
+++ b/Android.bp
@@ -31,6 +31,7 @@
         "androidx.annotation_annotation",
         "androidx.core_core",
         "telecom_flags_core_java_lib",
+        "modules-utils-handlerexecutor",
     ],
     resource_dirs: ["res"],
     proto: {
diff --git a/flags/Android.bp b/flags/Android.bp
index 501eba4..54b1443 100644
--- a/flags/Android.bp
+++ b/flags/Android.bp
@@ -44,6 +44,7 @@
         "telecom_bluetoothdevicemanager_flags.aconfig",
         "telecom_non_critical_security_flags.aconfig",
         "telecom_headless_system_user_mode.aconfig",
+        "telecom_session_flags.aconfig",
         "telecom_metrics_flags.aconfig",
     ],
 }
diff --git a/flags/telecom_callaudioroutestatemachine_flags.aconfig b/flags/telecom_callaudioroutestatemachine_flags.aconfig
index 33bccba..a60c0f1 100644
--- a/flags/telecom_callaudioroutestatemachine_flags.aconfig
+++ b/flags/telecom_callaudioroutestatemachine_flags.aconfig
@@ -17,6 +17,14 @@
   bug: "306395598"
 }
 
+# OWNER=pmadapurmath TARGET=25Q1
+flag {
+  name: "resolve_active_bt_routing_and_bt_timing_issue"
+  namespace: "telecom"
+  description: "Resolve the active BT device routing and flaky timing issues noted in BT routing."
+  bug: "372029371"
+}
+
 # OWNER=tgunn TARGET=24Q3
 flag {
   name: "ensure_audio_mode_updates_on_foreground_call_change"
@@ -99,3 +107,25 @@
     purpose: PURPOSE_BUGFIX
   }
 }
+
+# OWNER=pmadapurmath TARGET=25Q1
+flag {
+  name: "new_audio_path_speaker_broadcast_and_unfocused_routing"
+  namespace: "telecom"
+  description: "Replace the speaker broadcasts with the communication device changed listener and resolve baseline routing issues when a call ends."
+  bug: "353419513"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
+
+# OWNER=pmadapurmath TARGET=25Q2
+flag {
+  name: "fix_user_request_baseline_route_video_call"
+  namespace: "telecom"
+  description: "Ensure that audio is routed out of speaker in a video call when we receive USER_SWITCH_BASELINE_ROUTE."
+  bug: "374037591"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
diff --git a/flags/telecom_headless_system_user_mode.aconfig b/flags/telecom_headless_system_user_mode.aconfig
index f798733..4135794 100644
--- a/flags/telecom_headless_system_user_mode.aconfig
+++ b/flags/telecom_headless_system_user_mode.aconfig
@@ -11,4 +11,28 @@
     metadata {
         purpose: PURPOSE_BUGFIX
       }
+}
+
+# OWNER=grantmenke TARGET=25Q1
+flag {
+    name: "telecom_main_user_in_block_check"
+    is_exported: true
+    namespace: "telecom"
+    description: "Support HSUM mode by using the main user when checking if a number is blocked."
+    bug: "369062239"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+      }
+}
+
+# OWNER=grantmenke TARGET=25Q2
+flag {
+    name: "telecom_app_label_proxy_hsum_aware"
+    is_exported: true
+    namespace: "telecom"
+    description: "Support HSUM mode by ensuring AppLableProxy is multiuser aware."
+    bug: "321817633"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+      }
 }
\ No newline at end of file
diff --git a/flags/telecom_non_critical_security_flags.aconfig b/flags/telecom_non_critical_security_flags.aconfig
index 37929a8..e492073 100644
--- a/flags/telecom_non_critical_security_flags.aconfig
+++ b/flags/telecom_non_critical_security_flags.aconfig
@@ -7,4 +7,15 @@
   namespace: "telecom"
   description: "When set, Telecom will unregister accounts if the service is not resolvable"
   bug: "281061708"
+}
+
+# OWNER=tgunn TARGET=25Q2
+flag {
+  name: "enforce_transactional_exclusivity"
+  namespace: "telecom"
+  description: "When set, ensure that transactional accounts cannot also be call capable"
+  bug: "376936125"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
 }
\ No newline at end of file
diff --git a/flags/telecom_session_flags.aconfig b/flags/telecom_session_flags.aconfig
new file mode 100644
index 0000000..5b8075c
--- /dev/null
+++ b/flags/telecom_session_flags.aconfig
@@ -0,0 +1,13 @@
+package: "com.android.server.telecom.flags"
+container: "system"
+
+# OWNER=breadley TARGET=25Q1
+flag {
+  name: "end_session_improvements"
+  namespace: "telecom"
+  description: "Ensure that ending a session doesnt cause a stack overflow"
+  bug: "370349160"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
\ No newline at end of file
diff --git a/proto/pulled_atoms.proto b/proto/pulled_atoms.proto
index 7360b6a..6c9af46 100644
--- a/proto/pulled_atoms.proto
+++ b/proto/pulled_atoms.proto
@@ -101,13 +101,13 @@
  * From frameworks/proto_logging/stats/atoms/telecomm/telecom_extension_atom.proto
  */
 message TelecomErrorStats {
-    // The value should be converted to android.telecom.SubmoduleNameEnum
+    // The value should be converted to android.telecom.SubmoduleEnum
     // From frameworks/proto_logging/stats/enums/telecomm/enums.proto
-    optional int32 submodule_name = 1;
+    optional int32 submodule = 1;
 
-    // The value should be converted to android.telecom.ErrorNameEnum
+    // The value should be converted to android.telecom.ErrorEnum
     // From frameworks/proto_logging/stats/enums/telecomm/enums.proto
-    optional int32 error_name = 2;
+    optional int32 error = 2;
 
     // The number of times this error occurs
     optional int32 count = 3;
diff --git a/res/values-pa/strings.xml b/res/values-pa/strings.xml
index 96ee0e8..831f260 100644
--- a/res/values-pa/strings.xml
+++ b/res/values-pa/strings.xml
@@ -109,7 +109,7 @@
     <string name="phone_settings_call_blocking_txt" msgid="7311523114822507178">"ਕਾਲ ਬਲਾਕ ਕਰਨਾ"</string>
     <string name="phone_settings_number_not_in_contact_txt" msgid="2602249106007265757">"ਨੰਬਰ ਜੋ ਤੁਹਾਡੇ ਸੰਪਰਕਾਂ ਵਿੱਚ ਨਹੀਂ ਹਨ"</string>
     <string name="phone_settings_number_not_in_contact_summary_txt" msgid="963327038085718969">"ਉਹ ਨੰਬਰ ਬਲਾਕ ਕਰੋ ਜੋ ਤੁਹਾਡੇ ਸੰਪਰਕਾਂ ਵਿੱਚ ਨਹੀਂ ਹਨ"</string>
-    <string name="phone_settings_private_num_txt" msgid="6339272760338475619">"ਪ੍ਰਾਈਵੇਟ"</string>
+    <string name="phone_settings_private_num_txt" msgid="6339272760338475619">"ਨਿੱਜੀ"</string>
     <string name="phone_settings_private_num_summary_txt" msgid="6755758240544021037">"ਉਹ ਕਾਲਰ ਬਲਾਕ ਕਰੋ ਜਿਨ੍ਹਾਂ ਦਾ ਨੰਬਰ ਨਹੀਂ ਦਿਖਾਈ ਦਿੰਦਾ ਹੈ"</string>
     <string name="phone_settings_payphone_txt" msgid="5003987966052543965">"ਜਨਤਕ ਫ਼ੋਨ"</string>
     <string name="phone_settings_payphone_summary_txt" msgid="3936631076065563665">"ਜਨਤਕ ਫ਼ੋਨਾਂ ਵਾਲੀਆਂ ਕਾਲਾਂ ਬਲਾਕ ਕਰੋ"</string>
diff --git a/src/com/android/server/telecom/AppLabelProxy.java b/src/com/android/server/telecom/AppLabelProxy.java
index 7c00f28..c4d83dd 100644
--- a/src/com/android/server/telecom/AppLabelProxy.java
+++ b/src/com/android/server/telecom/AppLabelProxy.java
@@ -16,8 +16,11 @@
 
 package com.android.server.telecom;
 
+import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
+import com.android.server.telecom.flags.FeatureFlags;
+import android.os.UserHandle;
 import android.telecom.Log;
 
 /**
@@ -30,15 +33,34 @@
     class Util {
         /**
          * Default impl of getAppLabel.
-         * @param pm PackageManager instance
+         * @param context Context instance that is not necessarily associated with the correct user.
+         * @param userHandle UserHandle instance of the user that is associated with the app.
          * @param packageName package name to look up.
          */
-        public static CharSequence getAppLabel(PackageManager pm, String packageName) {
+        public static CharSequence getAppLabel(Context context, UserHandle userHandle,
+                String packageName, FeatureFlags featureFlags) {
             try {
-                ApplicationInfo info = pm.getApplicationInfo(packageName, 0);
-                CharSequence result = pm.getApplicationLabel(info);
-                Log.i(LOG_TAG, "package %s: name is %s", packageName, result);
-                return result;
+                if (featureFlags.telecomAppLabelProxyHsumAware()){
+                    Context userContext = context.createContextAsUser(userHandle, 0 /* flags */);
+                    PackageManager userPackageManager = userContext.getPackageManager();
+                    if (userPackageManager == null) {
+                        Log.w(LOG_TAG, "Could not determine app label since PackageManager is "
+                                + "null. Package name is %s", packageName);
+                        return null;
+                    }
+                    ApplicationInfo info = userPackageManager.getApplicationInfo(packageName, 0);
+                    CharSequence result = userPackageManager.getApplicationLabel(info);
+                    Log.i(LOG_TAG, "package %s: name is %s for user = %s", packageName, result,
+                            userHandle.toString());
+                    return result;
+                } else {
+                    // Legacy code path:
+                    PackageManager pm = context.getPackageManager();
+                    ApplicationInfo info = pm.getApplicationInfo(packageName, 0);
+                    CharSequence result = pm.getApplicationLabel(info);
+                    Log.i(LOG_TAG, "package %s: name is %s", packageName, result);
+                    return result;
+                }
             } catch (PackageManager.NameNotFoundException nnfe) {
                 Log.w(LOG_TAG, "Could not determine app label. Package name is %s", packageName);
             }
@@ -47,5 +69,5 @@
         }
     }
 
-    CharSequence getAppLabel(String packageName);
+    CharSequence getAppLabel(String packageName, UserHandle userHandle);
 }
diff --git a/src/com/android/server/telecom/AudioRoute.java b/src/com/android/server/telecom/AudioRoute.java
index d469a43..d3ed77d 100644
--- a/src/com/android/server/telecom/AudioRoute.java
+++ b/src/com/android/server/telecom/AudioRoute.java
@@ -318,15 +318,16 @@
     // sending SPEAKER_OFF, or disconnecting SCO).
     void onOrigRouteAsPendingRoute(boolean active, PendingAudioRoute pendingAudioRoute,
             AudioManager audioManager, BluetoothRouteManager bluetoothRouteManager) {
-        Log.i(this, "onOrigRouteAsPendingRoute: active (%b), type (%d)", active, mAudioRouteType);
+        Log.i(this, "onOrigRouteAsPendingRoute: active (%b), type (%s)", active,
+                DEVICE_TYPE_STRINGS.get(mAudioRouteType));
         if (active) {
-            if (mAudioRouteType == TYPE_SPEAKER) {
-                pendingAudioRoute.addMessage(SPEAKER_OFF, null);
-            }
             int result = clearCommunicationDevice(pendingAudioRoute, bluetoothRouteManager,
                     audioManager);
-            // Only send BT_AUDIO_DISCONNECTED for SCO if disconnect was successful.
-            if (mAudioRouteType == TYPE_BLUETOOTH_SCO && result == BluetoothStatusCodes.SUCCESS) {
+            if (mAudioRouteType == TYPE_SPEAKER) {
+                pendingAudioRoute.addMessage(SPEAKER_OFF, null);
+            } else if (mAudioRouteType == TYPE_BLUETOOTH_SCO
+                    && result == BluetoothStatusCodes.SUCCESS) {
+                // Only send BT_AUDIO_DISCONNECTED for SCO if disconnect was successful.
                 pendingAudioRoute.addMessage(BT_AUDIO_DISCONNECTED, mBluetoothAddress);
             }
         }
@@ -407,8 +408,26 @@
         }
 
         if (result == BluetoothStatusCodes.SUCCESS) {
+            if (pendingAudioRoute.getFeatureFlags().resolveActiveBtRoutingAndBtTimingIssue()) {
+                maybeClearConnectedPendingMessages(pendingAudioRoute);
+            }
             pendingAudioRoute.setCommunicationDeviceType(AudioRoute.TYPE_INVALID);
         }
         return result;
     }
+
+    private void maybeClearConnectedPendingMessages(PendingAudioRoute pendingAudioRoute) {
+        // If we're still waiting on BT_AUDIO_CONNECTED/SPEAKER_ON but have routed out of it
+        // since and disconnected the device, then remove that message so we aren't waiting for
+        // it in the message queue.
+        if (mAudioRouteType == TYPE_BLUETOOTH_SCO) {
+            Log.i(this, "clearCommunicationDevice: Clearing pending "
+                    + "BT_AUDIO_CONNECTED messages.");
+            pendingAudioRoute.clearPendingMessage(
+                    new Pair<>(BT_AUDIO_CONNECTED, mBluetoothAddress));
+        } else if (mAudioRouteType == TYPE_SPEAKER) {
+            Log.i(this, "clearCommunicationDevice: Clearing pending SPEAKER_ON messages.");
+            pendingAudioRoute.clearPendingMessage(new Pair<>(SPEAKER_ON, null));
+        }
+    }
 }
diff --git a/src/com/android/server/telecom/CachedVideoStateChange.java b/src/com/android/server/telecom/CachedVideoStateChange.java
index cefb92b..8aa6d40 100644
--- a/src/com/android/server/telecom/CachedVideoStateChange.java
+++ b/src/com/android/server/telecom/CachedVideoStateChange.java
@@ -16,7 +16,8 @@
 
 package com.android.server.telecom;
 
-import static com.android.server.telecom.voip.VideoStateTranslation.TransactionalVideoStateToString;
+import static com.android.server.telecom.callsequencing.voip.VideoStateTranslation
+        .TransactionalVideoStateToString;
 
 import android.telecom.Log;
 
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index c391641..df31e02 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -21,8 +21,10 @@
 
 import static com.android.server.telecom.CachedCallback.TYPE_QUEUE;
 import static com.android.server.telecom.CachedCallback.TYPE_STATE;
-import static com.android.server.telecom.voip.VideoStateTranslation.TransactionalVideoStateToString;
-import static com.android.server.telecom.voip.VideoStateTranslation.VideoProfileStateToTransactionalVideoState;
+import static com.android.server.telecom.callsequencing.voip.VideoStateTranslation
+        .TransactionalVideoStateToString;
+import static com.android.server.telecom.callsequencing.voip.VideoStateTranslation
+        .VideoProfileStateToTransactionalVideoState;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -78,9 +80,9 @@
 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.voip.TransactionManager;
-import com.android.server.telecom.voip.VerifyCallStateChangeTransaction;
-import com.android.server.telecom.voip.VoipCallTransactionResult;
+import com.android.server.telecom.callsequencing.TransactionManager;
+import com.android.server.telecom.callsequencing.VerifyCallStateChangeTransaction;
+import com.android.server.telecom.callsequencing.CallTransactionResult;
 
 import java.io.IOException;
 import java.text.SimpleDateFormat;
@@ -3185,7 +3187,7 @@
         tm.addTransaction(new VerifyCallStateChangeTransaction(mCallsManager.getLock(),
                 this, targetCallState), new OutcomeReceiver<>() {
             @Override
-            public void onResult(VoipCallTransactionResult result) {
+            public void onResult(CallTransactionResult result) {
                 Log.i(this, "awaitCallStateChangeAndMaybeDisconnectCall: %s: onResult:"
                         + " due to CallException=[%s]", callingMethod, result);
             }
diff --git a/src/com/android/server/telecom/CallAnomalyWatchdog.java b/src/com/android/server/telecom/CallAnomalyWatchdog.java
index 384110c..c331b29 100644
--- a/src/com/android/server/telecom/CallAnomalyWatchdog.java
+++ b/src/com/android/server/telecom/CallAnomalyWatchdog.java
@@ -153,7 +153,8 @@
     public static final UUID WATCHDOG_DISCONNECTED_STUCK_VOIP_CALL_UUID =
             UUID.fromString("3fbecd12-059d-4fd3-87b7-6c3079891c23");
     public static final String WATCHDOG_DISCONNECTED_STUCK_VOIP_CALL_MSG =
-            "Telecom CallAnomalyWatchdog caught stuck VoIP call in a starting state";
+            "A VoIP call was flagged due to exceeding a one-minute threshold in the DIALING or "
+                    + "RINGING state";
 
 
     @VisibleForTesting
diff --git a/src/com/android/server/telecom/CallAudioManager.java b/src/com/android/server/telecom/CallAudioManager.java
index 8c2f631..d156c0c 100644
--- a/src/com/android/server/telecom/CallAudioManager.java
+++ b/src/com/android/server/telecom/CallAudioManager.java
@@ -1101,6 +1101,9 @@
                     call.getId());
             disconnectedToneFuture.complete(null);
         }
+        // Make sure we schedule the unbinding of the BT ICS once the disconnected tone future has
+        // been completed.
+        mCallsManager.getInCallController().maybeScheduleBtUnbind(call);
     }
 
     @VisibleForTesting
diff --git a/src/com/android/server/telecom/CallAudioModeStateMachine.java b/src/com/android/server/telecom/CallAudioModeStateMachine.java
index fb196f2..e149bdd 100644
--- a/src/com/android/server/telecom/CallAudioModeStateMachine.java
+++ b/src/com/android/server/telecom/CallAudioModeStateMachine.java
@@ -17,7 +17,6 @@
 package com.android.server.telecom;
 
 import android.media.AudioAttributes;
-import android.media.AudioFocusRequest;
 import android.media.AudioManager;
 import android.os.Looper;
 import android.os.Message;
@@ -47,22 +46,6 @@
         }
     }
 
-    private static final AudioAttributes RING_AUDIO_ATTRIBUTES = new AudioAttributes.Builder()
-            .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
-            .setLegacyStreamType(AudioManager.STREAM_RING)
-            .build();
-    public static final AudioFocusRequest RING_AUDIO_FOCUS_REQUEST = new AudioFocusRequest
-            .Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
-            .setAudioAttributes(RING_AUDIO_ATTRIBUTES).build();
-
-    private static final AudioAttributes CALL_AUDIO_ATTRIBUTES = new AudioAttributes.Builder()
-            .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
-            .setLegacyStreamType(AudioManager.STREAM_VOICE_CALL)
-            .build();
-    public static final AudioFocusRequest CALL_AUDIO_FOCUS_REQUEST = new AudioFocusRequest
-            .Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
-            .setAudioAttributes(CALL_AUDIO_ATTRIBUTES).build();
-
     public static class MessageArgs {
         public boolean hasActiveOrDialingCalls;
         public boolean hasRingingCalls;
@@ -232,8 +215,6 @@
     public static final String STREAMING_STATE_NAME = StreamingFocusState.class.getSimpleName();
     public static final String COMMS_STATE_NAME = VoipCallFocusState.class.getSimpleName();
 
-    private AudioFocusRequest mCurrentAudioFocusRequest = null;
-
     private class BaseState extends State {
         @Override
         public boolean processMessage(Message msg) {
@@ -348,18 +329,9 @@
                             + args.toString());
                     return HANDLED;
                 case AUDIO_OPERATIONS_COMPLETE:
-                    if (mFeatureFlags.telecomResolveHiddenDependencies()) {
-                        if (mCurrentAudioFocusRequest != null) {
-                            Log.i(this, "AudioOperationsComplete: "
-                                    + "AudioManager#abandonAudioFocusRequest(); now unfocused");
-                            mAudioManager.abandonAudioFocusRequest(mCurrentAudioFocusRequest);
-                            mCurrentAudioFocusRequest = null;
-                        } else {
-                            Log.i(this, "AudioOperationsComplete: already unfocused");
-                        }
-                    } else {
-                        mAudioManager.abandonAudioFocusForCall();
-                    }
+                    Log.i(this, "AudioOperationsComplete: "
+                            + "AudioManager#abandonAudioFocusRequest(); now unfocused");
+                    mAudioManager.abandonAudioFocusForCall();
                     // Clear requested communication device after the call ends.
                     if (mFeatureFlags.clearCommunicationDeviceAfterAudioOpsComplete()) {
                         mCommunicationDeviceTracker.clearCommunicationDevice(
@@ -438,14 +410,7 @@
                 case AUDIO_OPERATIONS_COMPLETE:
                     Log.i(LOG_TAG, "AudioManager#abandonAudioFocusRequest: now "
                             + "AUDIO_PROCESSING");
-                    if (mFeatureFlags.telecomResolveHiddenDependencies()) {
-                        if (mCurrentAudioFocusRequest != null) {
-                            mAudioManager.abandonAudioFocusRequest(mCurrentAudioFocusRequest);
-                            mCurrentAudioFocusRequest = null;
-                        }
-                    } else {
-                        mAudioManager.abandonAudioFocusForCall();
-                    }
+                    mAudioManager.abandonAudioFocusForCall();
                     return HANDLED;
                 default:
                     // The forced focus switch commands are handled by BaseState.
@@ -468,14 +433,10 @@
             }
 
             if (mCallAudioManager.startRinging()) {
-                if (mFeatureFlags.telecomResolveHiddenDependencies()) {
-                    mCurrentAudioFocusRequest = RING_AUDIO_FOCUS_REQUEST;
-                    Log.i(this, "tryStartRinging: AudioManager#requestAudioFocus(RING)");
-                    mAudioManager.requestAudioFocus(RING_AUDIO_FOCUS_REQUEST);
-                } else {
-                    mAudioManager.requestAudioFocusForCall(
-                            AudioManager.STREAM_RING, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
-                }
+                Log.i(this, "tryStartRinging: AudioManager#requestAudioFocus(RING)");
+                mAudioManager.requestAudioFocusForCall(
+                        AudioManager.STREAM_RING, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+
                 // Do not set MODE_RINGTONE if we were previously in the CALL_SCREENING mode --
                 // this trips up the audio system.
                 if (mAudioManager.getMode() != AudioManager.MODE_CALL_SCREENING) {
@@ -570,14 +531,9 @@
         public void enter() {
             Log.i(LOG_TAG, "Audio focus entering SIM CALL state");
             mLocalLog.log("Enter SIM_CALL");
-            if (mFeatureFlags.telecomResolveHiddenDependencies()) {
-                mCurrentAudioFocusRequest = CALL_AUDIO_FOCUS_REQUEST;
-                Log.i(this, "enter: AudioManager#requestAudioFocus(CALL)");
-                mAudioManager.requestAudioFocus(CALL_AUDIO_FOCUS_REQUEST);
-            } else {
-                mAudioManager.requestAudioFocusForCall(AudioManager.STREAM_VOICE_CALL,
-                        AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
-            }
+            Log.i(this, "enter: AudioManager#requestAudioFocus(CALL)");
+            mAudioManager.requestAudioFocusForCall(AudioManager.STREAM_VOICE_CALL,
+                AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
             Log.i(this, "enter: AudioManager#setMode(MODE_IN_CALL)");
             mAudioManager.setMode(AudioManager.MODE_IN_CALL);
             mLocalLog.log("Mode MODE_IN_CALL");
@@ -660,14 +616,9 @@
         public void enter() {
             Log.i(LOG_TAG, "Audio focus entering VOIP CALL state");
             mLocalLog.log("Enter VOIP_CALL");
-            if (mFeatureFlags.telecomResolveHiddenDependencies()) {
-                mCurrentAudioFocusRequest = CALL_AUDIO_FOCUS_REQUEST;
-                Log.i(this, "enter: AudioManager#requestAudioFocus(CALL)");
-                mAudioManager.requestAudioFocus(CALL_AUDIO_FOCUS_REQUEST);
-            } else {
-                mAudioManager.requestAudioFocusForCall(AudioManager.STREAM_VOICE_CALL,
-                        AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
-            }
+            Log.i(this, "enter: AudioManager#requestAudioFocus(CALL)");
+            mAudioManager.requestAudioFocusForCall(AudioManager.STREAM_VOICE_CALL,
+                    AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
             Log.i(this, "enter: AudioManager#setMode(MODE_IN_COMMUNICATION)");
             mAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
             mLocalLog.log("Mode MODE_IN_COMMUNICATION");
@@ -823,14 +774,9 @@
         public void enter() {
             Log.i(LOG_TAG, "Audio focus entering TONE/HOLDING state");
             mLocalLog.log("Enter TONE/HOLDING");
-            if (mFeatureFlags.telecomResolveHiddenDependencies()) {
-                mCurrentAudioFocusRequest = CALL_AUDIO_FOCUS_REQUEST;
-                Log.i(this, "enter: AudioManager#requestAudioFocus(CALL)");
-                mAudioManager.requestAudioFocus(CALL_AUDIO_FOCUS_REQUEST);
-            } else {
-                mAudioManager.requestAudioFocusForCall(AudioManager.STREAM_VOICE_CALL,
-                        AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
-            }
+            Log.i(this, "enter: AudioManager#requestAudioFocus(CALL)");
+            mAudioManager.requestAudioFocusForCall(AudioManager.STREAM_VOICE_CALL,
+                    AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
             Log.i(this, "enter: AudioManager#setMode(%d)", mMostRecentMode);
             mAudioManager.setMode(mMostRecentMode);
             mLocalLog.log("Mode " + mMostRecentMode);
diff --git a/src/com/android/server/telecom/CallAudioRouteController.java b/src/com/android/server/telecom/CallAudioRouteController.java
index bc2c0cb..727d300 100644
--- a/src/com/android/server/telecom/CallAudioRouteController.java
+++ b/src/com/android/server/telecom/CallAudioRouteController.java
@@ -17,6 +17,7 @@
 package com.android.server.telecom;
 
 import static com.android.server.telecom.AudioRoute.BT_AUDIO_ROUTE_TYPES;
+import static com.android.server.telecom.AudioRoute.DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE;
 import static com.android.server.telecom.AudioRoute.TYPE_INVALID;
 import static com.android.server.telecom.AudioRoute.TYPE_SPEAKER;
 
@@ -52,9 +53,11 @@
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.server.telecom.bluetooth.BluetoothRouteManager;
 import com.android.server.telecom.flags.FeatureFlags;
+import com.android.server.telecom.metrics.ErrorStats;
 import com.android.server.telecom.metrics.TelecomMetricsController;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
@@ -62,6 +65,8 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 
 public class CallAudioRouteController implements CallAudioRouteAdapter {
     private static final AudioRoute DUMMY_ROUTE = new AudioRoute(TYPE_INVALID, null, null);
@@ -106,6 +111,8 @@
     private PendingAudioRoute mPendingAudioRoute;
     private AudioRoute.Factory mAudioRouteFactory;
     private StatusBarNotifier mStatusBarNotifier;
+    private AudioManager.OnCommunicationDeviceChangedListener mCommunicationDeviceListener;
+    private ExecutorService mCommunicationDeviceChangedExecutor;
     private FeatureFlags mFeatureFlags;
     private int mFocusType;
     private int mCallSupportedRouteMask = -1;
@@ -199,10 +206,12 @@
         handlerThread.start();
 
         // Register broadcast receivers
-        IntentFilter speakerChangedFilter = new IntentFilter(
-                AudioManager.ACTION_SPEAKERPHONE_STATE_CHANGED);
-        speakerChangedFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
-        context.registerReceiver(mSpeakerPhoneChangeReceiver, speakerChangedFilter);
+        if (!mFeatureFlags.newAudioPathSpeakerBroadcastAndUnfocusedRouting()) {
+            IntentFilter speakerChangedFilter = new IntentFilter(
+                    AudioManager.ACTION_SPEAKERPHONE_STATE_CHANGED);
+            speakerChangedFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
+            context.registerReceiver(mSpeakerPhoneChangeReceiver, speakerChangedFilter);
+        }
 
         IntentFilter micMuteChangedFilter = new IntentFilter(
                 AudioManager.ACTION_MICROPHONE_MUTE_CHANGED);
@@ -213,6 +222,27 @@
         muteChangedFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
         context.registerReceiver(mMuteChangeReceiver, muteChangedFilter);
 
+        // Register AudioManager#onCommunicationDeviceChangedListener listener to receive updates
+        // to communication device (via AudioManager#setCommunicationDevice). This is a replacement
+        // to using broadcasts in the hopes of improving performance.
+        mCommunicationDeviceChangedExecutor = Executors.newSingleThreadExecutor();
+        mCommunicationDeviceListener = new AudioManager.OnCommunicationDeviceChangedListener() {
+            @Override
+            public void onCommunicationDeviceChanged(AudioDeviceInfo device) {
+                @AudioRoute.AudioRouteType int audioType = device != null
+                        ? DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.get(device.getType())
+                        : TYPE_INVALID;
+                Log.i(this, "onCommunicationDeviceChanged: %d", audioType);
+                if (device != null && device.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) {
+                    if (mCurrentRoute.getType() != TYPE_SPEAKER) {
+                        sendMessageWithSessionInfo(SPEAKER_ON);
+                    }
+                } else {
+                    sendMessageWithSessionInfo(SPEAKER_OFF);
+                }
+            }
+        };
+
         // Create handler
         mHandler = new Handler(handlerThread.getLooper()) {
             @Override
@@ -277,12 +307,12 @@
                             break;
                         case SWITCH_BASELINE_ROUTE:
                             address = (String) ((SomeArgs) msg.obj).arg2;
-                            handleSwitchBaselineRoute(msg.arg1 == INCLUDE_BLUETOOTH_IN_BASELINE,
-                                    address);
+                            handleSwitchBaselineRoute(false,
+                                    msg.arg1 == INCLUDE_BLUETOOTH_IN_BASELINE, address);
                             break;
                         case USER_SWITCH_BASELINE_ROUTE:
-                            handleSwitchBaselineRoute(msg.arg1 == INCLUDE_BLUETOOTH_IN_BASELINE,
-                                    null);
+                            handleSwitchBaselineRoute(true,
+                                    msg.arg1 == INCLUDE_BLUETOOTH_IN_BASELINE, null);
                             break;
                         case SPEAKER_ON:
                             handleSpeakerOn();
@@ -342,7 +372,7 @@
     public void initialize() {
         mAvailableRoutes = new HashSet<>();
         mCallSupportedRoutes = new HashSet<>();
-        mBluetoothRoutes = new LinkedHashMap<>();
+        mBluetoothRoutes = Collections.synchronizedMap(new LinkedHashMap<>());
         mActiveDeviceCache = new HashMap<>();
         mActiveDeviceCache.put(AudioRoute.TYPE_BLUETOOTH_SCO, null);
         mActiveDeviceCache.put(AudioRoute.TYPE_BLUETOOTH_HA, null);
@@ -410,6 +440,11 @@
         mIsActive = false;
         mCallAudioState = new CallAudioState(mIsMute, ROUTE_MAP.get(mCurrentRoute.getType()),
                 supportMask, null, new HashSet<>());
+        if (mFeatureFlags.newAudioPathSpeakerBroadcastAndUnfocusedRouting()) {
+            mAudioManager.addOnCommunicationDeviceChangedListener(
+                    mCommunicationDeviceChangedExecutor,
+                    mCommunicationDeviceListener);
+        }
     }
 
     @Override
@@ -512,6 +547,10 @@
         if (destRoute == null || (!destRoute.equals(mStreamingRoute)
                 && !getCallSupportedRoutes().contains(destRoute))) {
             Log.i(this, "Ignore routing to unavailable route: %s", destRoute);
+            if (mFeatureFlags.telecomMetricsSupport()) {
+                mMetricsController.getErrorStats().log(ErrorStats.SUB_CALL_AUDIO,
+                        ErrorStats.ERROR_AUDIO_ROUTE_UNAVAILABLE);
+            }
             return;
         }
         if (mIsPending) {
@@ -522,7 +561,8 @@
                             + "%s(active=%b)",
                     mPendingAudioRoute.getDestRoute(), mIsActive, destRoute, active);
             // Ensure we don't keep waiting for SPEAKER_ON if dest route gets overridden.
-            if (active && mPendingAudioRoute.getDestRoute().getType() == TYPE_SPEAKER) {
+            if (!mFeatureFlags.resolveActiveBtRoutingAndBtTimingIssue() && active
+                    && mPendingAudioRoute.getDestRoute().getType() == TYPE_SPEAKER) {
                 mPendingAudioRoute.clearPendingMessage(new Pair<>(SPEAKER_ON, null));
             }
             // override pending route while keep waiting for still pending messages for the
@@ -558,6 +598,10 @@
             wiredHeadsetRoute = mAudioRouteFactory.create(AudioRoute.TYPE_WIRED, null,
                     mAudioManager);
         } 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_WIRED));
         }
@@ -597,6 +641,10 @@
         try {
             dockRoute = mAudioRouteFactory.create(AudioRoute.TYPE_DOCK, null, mAudioManager);
         } 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_WIRED));
         }
@@ -770,10 +818,30 @@
      * Message being handled: BT_ACTIVE_DEVICE_GONE
      */
     private void handleBtActiveDeviceGone(@AudioRoute.AudioRouteType int type) {
-        if ((mIsPending && mPendingAudioRoute.getDestRoute().getType() == type)
-                || (!mIsPending && mCurrentRoute.getType() == type)) {
-            // Fallback to an available route
-            routeTo(mIsActive, getBaseRoute(true, null));
+        // Determine what the active device for the BT audio type was so that we can exclude this
+        // device from being used when calculating the base route.
+        String previouslyActiveDeviceAddress = mFeatureFlags
+                .resolveActiveBtRoutingAndBtTimingIssue()
+                ? mActiveDeviceCache.get(type)
+                : null;
+        // It's possible that the dest route hasn't been set yet when the controller is first
+        // initialized.
+        boolean pendingRouteNeedsUpdate = mPendingAudioRoute.getDestRoute() != null
+                && mPendingAudioRoute.getDestRoute().getType() == type;
+        boolean currentRouteNeedsUpdate = mCurrentRoute.getType() == type;
+        if (mFeatureFlags.resolveActiveBtRoutingAndBtTimingIssue()) {
+            if (pendingRouteNeedsUpdate) {
+                pendingRouteNeedsUpdate = mPendingAudioRoute.getDestRoute().getBluetoothAddress()
+                        .equals(previouslyActiveDeviceAddress);
+            }
+            if (currentRouteNeedsUpdate) {
+                currentRouteNeedsUpdate = mCurrentRoute.getBluetoothAddress()
+                        .equals(previouslyActiveDeviceAddress);
+            }
+        }
+        if ((mIsPending && pendingRouteNeedsUpdate) || (!mIsPending && currentRouteNeedsUpdate)) {
+            // Fallback to an available route excluding the previously active device.
+            routeTo(mIsActive, getBaseRoute(true, previouslyActiveDeviceAddress));
         }
     }
 
@@ -789,6 +857,10 @@
                             mCallsManager.getCurrentUserHandle().getIdentifier(),
                             mContext.getAttributionTag());
                 } catch (RemoteException e) {
+                    if (mFeatureFlags.telecomMetricsSupport()) {
+                        mMetricsController.getErrorStats().log(ErrorStats.SUB_CALL_AUDIO,
+                                ErrorStats.ERROR_EXTERNAL_EXCEPTION);
+                    }
                     Log.e(this, e, "Remote exception while toggling mute.");
                     return;
                 }
@@ -802,19 +874,22 @@
         mFocusType = focus;
         switch (focus) {
             case NO_FOCUS -> {
-                if (mIsActive) {
-                    // Notify the CallAudioModeStateMachine that audio operations are complete so
-                    // that we can relinquish audio focus.
-                    mCallAudioManager.notifyAudioOperationsComplete();
-
-                    // Reset mute state after call ends.
-                    handleMuteChanged(false);
-                    // Route back to inactive route.
-                    routeTo(false, mCurrentRoute);
-                    // Clear pending messages
-                    mPendingAudioRoute.clearPendingMessages();
-                    clearRingingBluetoothAddress();
-                }
+                // Notify the CallAudioModeStateMachine that audio operations are complete so
+                // that we can relinquish audio focus.
+                mCallAudioManager.notifyAudioOperationsComplete();
+                // Reset mute state after call ends. This should remain unaffected if audio routing
+                // never went active.
+                handleMuteChanged(false);
+                // Ensure we reset call audio state at the end of the call (i.e. if we're on
+                // speaker, route back to earpiece). If we're on BT, remain on BT if it's still
+                // connected.
+                AudioRoute route = mFeatureFlags.resolveActiveBtRoutingAndBtTimingIssue()
+                        ? calculateBaselineRoute(false, true, null)
+                        : mCurrentRoute;
+                routeTo(false, route);
+                // Clear pending messages
+                mPendingAudioRoute.clearPendingMessages();
+                clearRingingBluetoothAddress();
             }
             case ACTIVE_FOCUS -> {
                 // Route to active baseline route (we may need to change audio route in the case
@@ -929,8 +1004,42 @@
         }
     }
 
-    private void handleSwitchBaselineRoute(boolean includeBluetooth, String btAddressToExclude) {
-        routeTo(mIsActive, calculateBaselineRoute(includeBluetooth, btAddressToExclude));
+    private void handleSwitchBaselineRoute(boolean isExplicitUserRequest, boolean includeBluetooth,
+            String btAddressToExclude) {
+        Log.i(this, "handleSwitchBaselineRoute: includeBluetooth: %b, "
+                + "btAddressToExclude: %s", includeBluetooth, btAddressToExclude);
+        boolean areExcludedBtAndDestBtSame = btAddressToExclude != null
+                && mPendingAudioRoute.getDestRoute() != null
+                && Objects.equals(btAddressToExclude, mPendingAudioRoute.getDestRoute()
+                .getBluetoothAddress());
+        Pair<Integer, String> btDevicePendingMsg =
+                new Pair<>(BT_AUDIO_CONNECTED, btAddressToExclude);
+
+        // If SCO is once again connected or there's a pending message for BT_AUDIO_CONNECTED, then
+        // we know that the device has reconnected or is in the middle of connecting. Ignore routing
+        // out of this BT device.
+        boolean isExcludedDeviceConnectingOrConnected = areExcludedBtAndDestBtSame
+                && (mIsScoAudioConnected || mPendingAudioRoute.getPendingMessages()
+                .contains(btDevicePendingMsg));
+        // Check if the pending audio route or current route is already different from the route
+        // including the BT device that should be excluded from route selection.
+        boolean isCurrentOrDestRouteDifferent = btAddressToExclude != null
+                && ((mIsPending && !btAddressToExclude.equals(mPendingAudioRoute.getDestRoute()
+                .getBluetoothAddress())) || (!mIsPending && !btAddressToExclude.equals(
+                        mCurrentRoute.getBluetoothAddress())));
+        if (mFeatureFlags.resolveActiveBtRoutingAndBtTimingIssue()) {
+            if (isExcludedDeviceConnectingOrConnected) {
+                Log.i(this, "BT device with address (%s) is currently connecting/connected. "
+                        + "Ignoring route switch.", btAddressToExclude);
+                return;
+            } else if (isCurrentOrDestRouteDifferent) {
+                Log.i(this, "Current or pending audio route isn't routed to device with address "
+                        + "(%s). Ignoring route switch.", btAddressToExclude);
+                return;
+            }
+        }
+        routeTo(mIsActive, calculateBaselineRoute(isExplicitUserRequest, includeBluetooth,
+                btAddressToExclude));
     }
 
     private void handleSpeakerOn() {
@@ -1112,7 +1221,7 @@
         }
 
         // Get corresponding audio route
-        @AudioRoute.AudioRouteType int type = AudioRoute.DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.get(
+        @AudioRoute.AudioRouteType int type = DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.get(
                 deviceAttr.getType());
         if (BT_AUDIO_ROUTE_TYPES.contains(type)) {
             return getBluetoothRoute(type, deviceAttr.getAddress());
@@ -1140,13 +1249,18 @@
         return mAudioManager.getPreferredDeviceForStrategy(strategy);
     }
 
-    private AudioRoute getPreferredAudioRouteFromDefault(boolean includeBluetooth,
-            String btAddressToExclude) {
-        boolean skipEarpiece;
+    private AudioRoute getPreferredAudioRouteFromDefault(boolean isExplicitUserRequest,
+            boolean includeBluetooth, String btAddressToExclude) {
+        boolean skipEarpiece = false;
         Call foregroundCall = mCallAudioManager.getForegroundCall();
-        synchronized (mTelecomLock) {
-            skipEarpiece = foregroundCall != null
-                    && VideoProfile.isVideo(foregroundCall.getVideoState());
+        if (!mFeatureFlags.fixUserRequestBaselineRouteVideoCall()) {
+            isExplicitUserRequest = false;
+        }
+        if (!isExplicitUserRequest) {
+            synchronized (mTelecomLock) {
+                skipEarpiece = foregroundCall != null
+                        && VideoProfile.isVideo(foregroundCall.getVideoState());
+            }
         }
         // Route to earpiece, wired, or speaker route if there are not bluetooth routes or if there
         // are only wearables available.
@@ -1246,7 +1360,7 @@
         Log.i(this, "getBaseRoute: preferred audio route is %s", destRoute);
         if (destRoute == null || (destRoute.getBluetoothAddress() != null && (!includeBluetooth
                 || destRoute.getBluetoothAddress().equals(btAddressToExclude)))) {
-            destRoute = getPreferredAudioRouteFromDefault(includeBluetooth, btAddressToExclude);
+            destRoute = getPreferredAudioRouteFromDefault(false, includeBluetooth, btAddressToExclude);
         }
         if (destRoute != null && !getCallSupportedRoutes().contains(destRoute)) {
             destRoute = null;
@@ -1255,8 +1369,9 @@
         return destRoute;
     }
 
-    private AudioRoute calculateBaselineRoute(boolean includeBluetooth, String btAddressToExclude) {
-        AudioRoute destRoute = getPreferredAudioRouteFromDefault(
+    private AudioRoute calculateBaselineRoute(boolean isExplicitUserRequest,
+            boolean includeBluetooth, String btAddressToExclude) {
+        AudioRoute destRoute = getPreferredAudioRouteFromDefault(isExplicitUserRequest,
                 includeBluetooth, btAddressToExclude);
         if (destRoute != null && !getCallSupportedRoutes().contains(destRoute)) {
             destRoute = null;
@@ -1322,7 +1437,7 @@
             return getMostRecentlyActiveBtRoute(btAddressToExclude);
         }
 
-        List<AudioRoute> bluetoothRoutes = mBluetoothRoutes.keySet().stream().toList();
+        List<AudioRoute> bluetoothRoutes = getAvailableBluetoothDevicesForRouting();
         // Traverse the routes from the most recently active recorded devices first.
         AudioRoute nonWatchDeviceRoute = null;
         for (int i = bluetoothRoutes.size() - 1; i >= 0; i--) {
@@ -1334,14 +1449,17 @@
                 continue;
             }
             // Check if the most recently active device is a watch device.
-            if (i == (bluetoothRoutes.size() - 1) && device.equals(mCallAudioState
-                    .getActiveBluetoothDevice()) && mBluetoothRouteManager.isWatch(device)) {
+            boolean isActiveDevice = mActiveBluetoothDevice != null
+                    && device.getAddress().equals(mActiveBluetoothDevice.second);
+            if (i == (bluetoothRoutes.size() - 1) && mBluetoothRouteManager.isWatch(device)
+                    && (device.equals(mCallAudioState.getActiveBluetoothDevice())
+                    || isActiveDevice)) {
                 Log.i(this, "getActiveWatchOrNonWatchDeviceRoute: Routing to active watch - %s",
                         bluetoothRoutes.get(0));
                 return bluetoothRoutes.get(0);
             }
             // Record the first occurrence of a non-watch device route if found.
-            if (!mBluetoothRouteManager.isWatch(device) && nonWatchDeviceRoute == null) {
+            if (!mBluetoothRouteManager.isWatch(device)) {
                 nonWatchDeviceRoute = route;
                 break;
             }
@@ -1351,6 +1469,22 @@
         return nonWatchDeviceRoute;
     }
 
+    private List<AudioRoute> getAvailableBluetoothDevicesForRouting() {
+        List<AudioRoute> bluetoothRoutes = new ArrayList<>(mBluetoothRoutes.keySet());
+        if (!mFeatureFlags.resolveActiveBtRoutingAndBtTimingIssue()) {
+            return bluetoothRoutes;
+        }
+        // Consider the active device (BT_ACTIVE_DEVICE_PRESENT) if it exists first.
+        AudioRoute activeDeviceRoute = getArbitraryBluetoothDevice();
+        if (activeDeviceRoute != null && (bluetoothRoutes.isEmpty()
+                || !bluetoothRoutes.get(bluetoothRoutes.size() - 1).equals(activeDeviceRoute))) {
+            Log.i(this, "getActiveWatchOrNonWatchDeviceRoute: active BT device (%s) present."
+                    + "Considering this device for selection first.", activeDeviceRoute);
+            bluetoothRoutes.add(activeDeviceRoute);
+        }
+        return bluetoothRoutes;
+    }
+
     /**
      * Returns the most actively reported bluetooth route excluding the passed in route.
      */
@@ -1438,9 +1572,16 @@
             // If a device was removed, check to ensure that no other device is still considered
             // active.
             boolean hasActiveDevice = false;
-            for (String address : mActiveDeviceCache.values()) {
+            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;
                 }
             }
diff --git a/src/com/android/server/telecom/CallStreamingController.java b/src/com/android/server/telecom/CallStreamingController.java
index efd458e..d14a553 100644
--- a/src/com/android/server/telecom/CallStreamingController.java
+++ b/src/com/android/server/telecom/CallStreamingController.java
@@ -39,10 +39,10 @@
 import android.telecom.Log;
 
 import com.android.internal.telecom.ICallStreamingService;
-import com.android.server.telecom.voip.ParallelTransaction;
-import com.android.server.telecom.voip.SerialTransaction;
-import com.android.server.telecom.voip.VoipCallTransaction;
-import com.android.server.telecom.voip.VoipCallTransactionResult;
+import com.android.server.telecom.callsequencing.voip.ParallelTransaction;
+import com.android.server.telecom.callsequencing.voip.SerialTransaction;
+import com.android.server.telecom.callsequencing.CallTransaction;
+import com.android.server.telecom.callsequencing.CallTransactionResult;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -112,7 +112,7 @@
         }
     }
 
-    public static class QueryCallStreamingTransaction extends VoipCallTransaction {
+    public static class QueryCallStreamingTransaction extends CallTransaction {
         private final CallsManager mCallsManager;
 
         public QueryCallStreamingTransaction(CallsManager callsManager) {
@@ -121,24 +121,24 @@
         }
 
         @Override
-        public CompletableFuture<VoipCallTransactionResult> processTransaction(Void v) {
+        public CompletableFuture<CallTransactionResult> processTransaction(Void v) {
             Log.i(this, "processTransaction");
-            CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
+            CompletableFuture<CallTransactionResult> future = new CompletableFuture<>();
 
             if (mCallsManager.getCallStreamingController().isStreaming()) {
-                future.complete(new VoipCallTransactionResult(
+                future.complete(new CallTransactionResult(
                         CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
                         "STREAMING_FAILED_ALREADY_STREAMING"));
             } else {
-                future.complete(new VoipCallTransactionResult(
-                        VoipCallTransactionResult.RESULT_SUCCEED, null));
+                future.complete(new CallTransactionResult(
+                        CallTransactionResult.RESULT_SUCCEED, null));
             }
 
             return future;
         }
     }
 
-    public static class AudioInterceptionTransaction extends VoipCallTransaction {
+    public static class AudioInterceptionTransaction extends CallTransaction {
         private Call mCall;
         private boolean mEnterInterception;
 
@@ -150,16 +150,16 @@
         }
 
         @Override
-        public CompletableFuture<VoipCallTransactionResult> processTransaction(Void v) {
+        public CompletableFuture<CallTransactionResult> processTransaction(Void v) {
             Log.i(this, "processTransaction");
-            CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
+            CompletableFuture<CallTransactionResult> future = new CompletableFuture<>();
 
             if (mEnterInterception) {
                 mCall.startStreaming();
             } else {
                 mCall.stopStreaming();
             }
-            future.complete(new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_SUCCEED,
+            future.complete(new CallTransactionResult(CallTransactionResult.RESULT_SUCCEED,
                     null));
             return future;
         }
@@ -170,7 +170,7 @@
         return new StreamingServiceTransaction(context, wrapper, call);
     }
 
-    public class StreamingServiceTransaction extends VoipCallTransaction {
+    public class StreamingServiceTransaction extends CallTransaction {
         public static final String MESSAGE = "STREAMING_FAILED_NO_SENDER";
         private final TransactionalServiceWrapper mWrapper;
         private final Context mContext;
@@ -188,14 +188,14 @@
 
         @SuppressLint("LongLogTag")
         @Override
-        public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+        public CompletionStage<CallTransactionResult> processTransaction(Void v) {
             Log.i(this, "processTransaction");
-            CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
+            CompletableFuture<CallTransactionResult> future = new CompletableFuture<>();
             RoleManager roleManager = mContext.getSystemService(RoleManager.class);
             PackageManager packageManager = mContext.getPackageManager();
             if (roleManager == null || packageManager == null) {
                 Log.w(this, "processTransaction: Can't find system service");
-                future.complete(new VoipCallTransactionResult(
+                future.complete(new CallTransactionResult(
                         CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
                         MESSAGE));
                 return future;
@@ -205,7 +205,7 @@
                     RoleManager.ROLE_SYSTEM_CALL_STREAMING, mUserHandle);
             if (holders.isEmpty()) {
                 Log.w(this, "processTransaction: Can't find streaming app");
-                future.complete(new VoipCallTransactionResult(
+                future.complete(new CallTransactionResult(
                         CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
                         MESSAGE));
                 return future;
@@ -217,7 +217,7 @@
                     PackageManager.GET_META_DATA, mUserHandle);
             if (infos.isEmpty()) {
                 Log.w(this, "processTransaction: Can't find streaming service");
-                future.complete(new VoipCallTransactionResult(
+                future.complete(new CallTransactionResult(
                         CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
                         MESSAGE));
                 return future;
@@ -229,7 +229,7 @@
                     Manifest.permission.BIND_CALL_STREAMING_SERVICE)) {
                 Log.w(this, "Must require BIND_CALL_STREAMING_SERVICE: " +
                         serviceInfo.packageName);
-                future.complete(new VoipCallTransactionResult(
+                future.complete(new CallTransactionResult(
                         CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
                         MESSAGE));
                 return future;
@@ -242,7 +242,7 @@
                     | Context.BIND_FOREGROUND_SERVICE
                     | Context.BIND_SCHEDULE_LIKE_TOP_APP, mUserHandle)) {
                 Log.w(this, "Can't bind to streaming service");
-                future.complete(new VoipCallTransactionResult(
+                future.complete(new CallTransactionResult(
                         CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
                         "STREAMING_FAILED_SENDER_BINDING_ERROR"));
             }
@@ -254,19 +254,19 @@
         return new UnbindStreamingServiceTransaction();
     }
 
-    public class UnbindStreamingServiceTransaction extends VoipCallTransaction {
+    public class UnbindStreamingServiceTransaction extends CallTransaction {
         public UnbindStreamingServiceTransaction() {
             super(mTelecomLock);
         }
 
         @SuppressLint("LongLogTag")
         @Override
-        public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+        public CompletionStage<CallTransactionResult> processTransaction(Void v) {
             Log.i(this, "processTransaction (unbindStreaming");
-            CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
+            CompletableFuture<CallTransactionResult> future = new CompletableFuture<>();
 
             resetController();
-            future.complete(new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_SUCCEED,
+            future.complete(new CallTransactionResult(CallTransactionResult.RESULT_SUCCEED,
                     null));
             return future;
         }
@@ -275,7 +275,7 @@
     public class StartStreamingTransaction extends SerialTransaction {
         private Call mCall;
 
-        public StartStreamingTransaction(List<VoipCallTransaction> subTransactions, Call call,
+        public StartStreamingTransaction(List<CallTransaction> subTransactions, Call call,
                 TelecomSystem.SyncRoot lock) {
             super(subTransactions, lock);
             mCall = call;
@@ -287,7 +287,7 @@
         }
     }
 
-    public VoipCallTransaction getStartStreamingTransaction(CallsManager callsManager,
+    public CallTransaction getStartStreamingTransaction(CallsManager callsManager,
             TransactionalServiceWrapper wrapper, Call call, TelecomSystem.SyncRoot lock) {
         // start streaming transaction flow:
         //     1. make sure there's no ongoing streaming call --> bind to EXO
@@ -296,7 +296,7 @@
         // If bind to EXO failed, add transaction for stop the streaming
 
         // create list for multiple transactions
-        List<VoipCallTransaction> transactions = new ArrayList<>();
+        List<CallTransaction> transactions = new ArrayList<>();
         transactions.add(new QueryCallStreamingTransaction(callsManager));
         transactions.add(new AudioInterceptionTransaction(call, true, lock));
         transactions.add(getCallStreamingServiceTransaction(
@@ -304,10 +304,10 @@
         return new StartStreamingTransaction(transactions, call, lock);
     }
 
-    public VoipCallTransaction getStopStreamingTransaction(Call call, TelecomSystem.SyncRoot lock) {
+    public CallTransaction getStopStreamingTransaction(Call call, TelecomSystem.SyncRoot lock) {
         // TODO: implement this
         // Stop streaming transaction flow:
-        List<VoipCallTransaction> transactions = new ArrayList<>();
+        List<CallTransaction> transactions = new ArrayList<>();
 
         // 1. unbind to call streaming service
         transactions.add(getUnbindStreamingServiceTransaction());
@@ -352,7 +352,7 @@
                 mTransactionalServiceWrapper.getTransactionManager().addTransaction(transaction,
                         new OutcomeReceiver<>() {
                             @Override
-                            public void onResult(VoipCallTransactionResult result) {
+                            public void onResult(CallTransactionResult result) {
                                 // ignore
                             }
 
@@ -366,7 +366,7 @@
         }
     }
 
-    private class CallStreamingStateChangeTransaction extends VoipCallTransaction {
+    private class CallStreamingStateChangeTransaction extends CallTransaction {
         @StreamingCall.StreamingCallState int mState;
 
         public CallStreamingStateChangeTransaction(@StreamingCall.StreamingCallState int state) {
@@ -375,14 +375,14 @@
         }
 
         @Override
-        public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
-            CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
+        public CompletionStage<CallTransactionResult> processTransaction(Void v) {
+            CompletableFuture<CallTransactionResult> future = new CompletableFuture<>();
             try {
                 mService.onCallStreamingStateChanged(mState);
-                future.complete(new VoipCallTransactionResult(
-                        VoipCallTransactionResult.RESULT_SUCCEED, null));
+                future.complete(new CallTransactionResult(
+                        CallTransactionResult.RESULT_SUCCEED, null));
             } catch (RemoteException e) {
-                future.complete(new VoipCallTransactionResult(
+                future.complete(new CallTransactionResult(
                         CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
                         "Exception when request "
                         + "setting state to streaming app."));
@@ -395,10 +395,10 @@
             ServiceConnection {
         private Call mCall;
         private TransactionalServiceWrapper mWrapper;
-        private CompletableFuture<VoipCallTransactionResult> mFuture;
+        private CompletableFuture<CallTransactionResult> mFuture;
 
         public CallStreamingServiceConnection(Call call, TransactionalServiceWrapper wrapper,
-                CompletableFuture<VoipCallTransactionResult> future) {
+                CompletableFuture<CallTransactionResult> future) {
             mCall = call;
             mWrapper = wrapper;
             mFuture = future;
@@ -409,11 +409,11 @@
             try {
                 Log.i(this, "onServiceConnected: " + name);
                 onConnectedInternal(mCall, mWrapper, service);
-                mFuture.complete(new VoipCallTransactionResult(
-                        VoipCallTransactionResult.RESULT_SUCCEED, null));
+                mFuture.complete(new CallTransactionResult(
+                        CallTransactionResult.RESULT_SUCCEED, null));
             } catch (RemoteException e) {
                 resetController();
-                mFuture.complete(new VoipCallTransactionResult(
+                mFuture.complete(new CallTransactionResult(
                         CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
                         StreamingServiceTransaction.MESSAGE));
             }
@@ -437,7 +437,7 @@
         private void clearBinding() {
             resetController();
             if (!mFuture.isDone()) {
-                mFuture.complete(new VoipCallTransactionResult(
+                mFuture.complete(new CallTransactionResult(
                         CallException.CODE_ERROR_UNKNOWN /* TODO:: define error b/335703584 */,
                         "STREAMING_FAILED_SENDER_BINDING_ERROR"));
             } else {
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index 028d8c1..9670d6a 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -131,9 +131,12 @@
 import com.android.server.telecom.callfiltering.IncomingCallFilterGraph;
 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.components.ErrorDialogActivity;
 import com.android.server.telecom.components.TelecomBroadcastReceiver;
+import com.android.server.telecom.callsequencing.CallsManagerCallSequencingAdapter;
 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 com.android.server.telecom.ui.AudioProcessingNotification;
@@ -143,8 +146,8 @@
 import com.android.server.telecom.ui.DisconnectedCallNotifier;
 import com.android.server.telecom.ui.IncomingCallNotifier;
 import com.android.server.telecom.ui.ToastFactory;
-import com.android.server.telecom.voip.VoipCallMonitor;
-import com.android.server.telecom.voip.TransactionManager;
+import com.android.server.telecom.callsequencing.voip.VoipCallMonitor;
+import com.android.server.telecom.callsequencing.TransactionManager;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -490,6 +493,7 @@
     private final UserManager mUserManager;
     private final CallStreamingNotification mCallStreamingNotification;
     private final BlockedNumbersManager mBlockedNumbersManager;
+    private final CallsManagerCallSequencingAdapter mCallSequencingAdapter;
     private final FeatureFlags mFeatureFlags;
     private final com.android.internal.telephony.flags.FeatureFlags mTelephonyFeatureFlags;
 
@@ -530,6 +534,8 @@
     private AnomalyReporterAdapter mAnomalyReporter = new AnomalyReporterAdapterImpl();
 
     private final MmiUtils mMmiUtils = new MmiUtils();
+
+    private TelecomMetricsController mMetricsController;
     /**
      * Listener to PhoneAccountRegistrar events.
      */
@@ -733,9 +739,13 @@
         mCallStreamingNotification = callStreamingNotification;
         mFeatureFlags = featureFlags;
         mTelephonyFeatureFlags = telephonyFlags;
+        mMetricsController = metricsController;
         mBlockedNumbersManager = mFeatureFlags.telecomMainlineBlockedNumbersManager()
                 ? mContext.getSystemService(BlockedNumbersManager.class)
                 : null;
+        mCallSequencingAdapter = new CallsManagerCallSequencingAdapter(this,
+                new CallSequencingController(this, mFeatureFlags.enableCallSequencing()),
+                mFeatureFlags.enableCallSequencing());
 
         if (mFeatureFlags.useImprovedListenerOrder()) {
             mListeners.add(mInCallController);
@@ -929,8 +939,8 @@
         String defaultDialerPackageName = telecomManager.getDefaultDialerPackage(userHandle);
         String userChosenPackageName = getRoleManagerAdapter().
                 getDefaultCallScreeningApp(userHandle);
-        AppLabelProxy appLabelProxy = packageName -> AppLabelProxy.Util.getAppLabel(
-                mContext.getPackageManager(), packageName);
+        AppLabelProxy appLabelProxy = (packageName, user) -> AppLabelProxy.Util.getAppLabel(
+                mContext, user, packageName, mFeatureFlags);
         ParcelableCallUtils.Converter converter = new ParcelableCallUtils.Converter();
 
         IncomingCallFilterGraph graph = mIncomingCallFilterGraphProvider.createGraph(incomingCall,
@@ -938,7 +948,7 @@
         DirectToVoicemailFilter voicemailFilter = new DirectToVoicemailFilter(incomingCall,
                 mCallerInfoLookupHelper);
         BlockCheckerFilter blockCheckerFilter = new BlockCheckerFilter(mContext, incomingCall,
-                mCallerInfoLookupHelper, new BlockCheckerAdapter(mFeatureFlags));
+                mCallerInfoLookupHelper, new BlockCheckerAdapter(mFeatureFlags), mFeatureFlags);
         DndCallFilter dndCallFilter = new DndCallFilter(incomingCall, getRinger());
         CallScreeningServiceFilter carrierCallScreeningServiceFilter =
                 new CallScreeningServiceFilter(incomingCall, carrierPackageName,
@@ -2029,10 +2039,18 @@
                     if (exception != null){
                         Log.e(TAG, exception, "Error retrieving list of potential phone accounts.");
                         if (finalCall.isEmergencyCall()) {
+                            if (mFeatureFlags.telecomMetricsSupport()) {
+                                mMetricsController.getErrorStats().log(ErrorStats.SUB_CALL_MANAGER,
+                                        ErrorStats.ERROR_RETRIEVING_ACCOUNT_EMERGENCY);
+                            }
                             mAnomalyReporter.reportAnomaly(
                                     EXCEPTION_RETRIEVING_PHONE_ACCOUNTS_EMERGENCY_ERROR_UUID,
                                     EXCEPTION_RETRIEVING_PHONE_ACCOUNTS_EMERGENCY_ERROR_MSG);
                         } else {
+                            if (mFeatureFlags.telecomMetricsSupport()) {
+                                mMetricsController.getErrorStats().log(ErrorStats.SUB_CALL_MANAGER,
+                                        ErrorStats.ERROR_RETRIEVING_ACCOUNT);
+                            }
                             mAnomalyReporter.reportAnomaly(
                                     EXCEPTION_RETRIEVING_PHONE_ACCOUNTS_ERROR_UUID,
                                     EXCEPTION_RETRIEVING_PHONE_ACCOUNTS_ERROR_MSG);
@@ -2071,21 +2089,18 @@
 
 
         // This future checks the status of existing calls and attempts to make room for the
-        // outgoing call. The future returned by the inner method will usually be pre-completed --
-        // we only pause here if user interaction is required to disconnect a self-managed call.
-        // It runs after the account handle is set, independently of the phone account suggestion
-        // future.
-        CompletableFuture<Call> makeRoomForCall = setAccountHandle.thenComposeAsync(
+        // outgoing call.
+        CompletableFuture<Boolean> makeRoomForCall = setAccountHandle.thenComposeAsync(
                 potentialPhoneAccounts -> {
                     Log.i(CallsManager.this, "make room for outgoing call stage");
                     if (mMmiUtils.isPotentialInCallMMICode(handle) && !isSelfManaged) {
-                        return CompletableFuture.completedFuture(finalCall);
+                        return CompletableFuture.completedFuture(true);
                     }
                     // If a call is being reused, then it has already passed the
                     // makeRoomForOutgoingCall check once and will fail the second time due to the
                     // call transitioning into the CONNECTING state.
                     if (isReusedCall) {
-                        return CompletableFuture.completedFuture(finalCall);
+                        return CompletableFuture.completedFuture(true);
                     } else {
                         Call reusableCall = reuseOutgoingCall(handle);
                         if (reusableCall != null) {
@@ -2112,48 +2127,75 @@
                                     finalCall.getTargetPhoneAccount(), finalCall);
                         }
                         finalCall.setStartFailCause(CallFailureCause.IN_EMERGENCY_CALL);
-                        return CompletableFuture.completedFuture(null);
+                        return CompletableFuture.completedFuture(false);
                     }
 
-                    // If we can not supportany more active calls, our options are to move a call
+                    // If we can not support any more active calls, our options are to move a call
                     // to hold, disconnect a call, or cancel this call altogether.
-                    boolean isRoomForCall = finalCall.isEmergencyCall() ?
-                            makeRoomForOutgoingEmergencyCall(finalCall) :
-                            makeRoomForOutgoingCall(finalCall);
-                    if (!isRoomForCall) {
-                        Call foregroundCall = getForegroundCall();
-                        Log.d(CallsManager.this, "No more room for outgoing call %s ", finalCall);
-                        if (foregroundCall.isSelfManaged()) {
-                            // If the ongoing call is a self-managed call, then prompt the user to
-                            // ask if they'd like to disconnect their ongoing call and place the
-                            // outgoing call.
-                            Log.i(CallsManager.this, "Prompting user to disconnect "
-                                    + "self-managed call");
-                            finalCall.setOriginalCallIntent(originalIntent);
-                            CompletableFuture<Call> completionFuture = new CompletableFuture<>();
-                            startCallConfirmation(finalCall, completionFuture);
-                            return completionFuture;
-                        } else {
-                            // If the ongoing call is a managed call, we will prevent the outgoing
-                            // call from dialing.
-                            if (isConference) {
-                                notifyCreateConferenceFailed(finalCall.getTargetPhoneAccount(),
-                                    finalCall);
-                            } else {
-                                notifyCreateConnectionFailed(
-                                        finalCall.getTargetPhoneAccount(), finalCall);
-                            }
+                    CompletableFuture<Boolean> isRoomForCallFuture =
+                            mCallSequencingAdapter.makeRoomForOutgoingCall(
+                                    finalCall.isEmergencyCall(), finalCall);
+                    isRoomForCallFuture.exceptionally((throwable -> {
+                        if (throwable != null) {
+                            Log.w(CallsManager.this,
+                                    "Exception thrown in makeRoomForOutgoing*Call, "
+                                            + "returning false. Ex:" + throwable);
                         }
-                        Log.i(CallsManager.this, "Aborting call since there's no room");
+                        return false;
+                    }));
+                    return isRoomForCallFuture;
+        }, new LoggedHandlerExecutor(outgoingCallHandler, "CM.dSMCP", mLock));
+
+        // The future returned by the inner method will usually be pre-completed --
+        // we only pause here if user interaction is required to disconnect a self-managed call.
+        // It runs after the account handle is set, independently of the phone account suggestion
+        // future.
+        CompletableFuture<Call> makeRoomResultHandler = makeRoomForCall
+                .thenComposeAsync((isRoom) -> {
+                    // If we have an ongoing emergency call, we would have already notified
+                    // connection failure for the new call being placed. Catch this so we don't
+                    // resend it again.
+                    boolean hasOngoingEmergencyCall = !finalCall.isEmergencyCall()
+                            && isInEmergencyCall();
+                    if (isRoom) {
+                        return CompletableFuture.completedFuture(finalCall);
+                    } else if (hasOngoingEmergencyCall) {
                         return CompletableFuture.completedFuture(null);
                     }
-                    return CompletableFuture.completedFuture(finalCall);
-        }, new LoggedHandlerExecutor(outgoingCallHandler, "CM.dSMCP", mLock));
+                    Call foregroundCall = getForegroundCall();
+                    Log.d(CallsManager.this, "No more room for outgoing call %s ",
+                            finalCall);
+                    if (foregroundCall.isSelfManaged()) {
+                        // If the ongoing call is a self-managed call, then prompt the
+                        // user to ask if they'd like to disconnect their ongoing call
+                        // and place the outgoing call.
+                        Log.i(CallsManager.this, "Prompting user to disconnect "
+                                + "self-managed call");
+                        finalCall.setOriginalCallIntent(originalIntent);
+                        CompletableFuture<Call> completionFuture =
+                                new CompletableFuture<>();
+                        startCallConfirmation(finalCall, completionFuture);
+                        return completionFuture;
+                    } else {
+                        // If the ongoing call is a managed call, we will prevent the
+                        // outgoing call from dialing.
+                        if (isConference) {
+                            notifyCreateConferenceFailed(
+                                    finalCall.getTargetPhoneAccount(),
+                                    finalCall);
+                        } else {
+                            notifyCreateConnectionFailed(
+                                    finalCall.getTargetPhoneAccount(), finalCall);
+                        }
+                    }
+                    Log.i(CallsManager.this,  "Aborting call since there's no room");
+                    return CompletableFuture.completedFuture(null);
+                }, new LoggedHandlerExecutor(outgoingCallHandler, "CM.mROC", mLock));
 
         // The outgoing call can be placed, go forward. This future glues together the results of
         // the account suggestion stage and the make room for call stage.
         CompletableFuture<Pair<Call, List<PhoneAccountSuggestion>>> preSelectStage =
-                makeRoomForCall.thenCombine(suggestionFuture, Pair::create);
+                makeRoomResultHandler.thenCombine(suggestionFuture, Pair::create);
         mLatestPreAccountSelectionFuture = preSelectStage;
 
         // This future takes the list of suggested accounts and the call and determines if more
@@ -2194,6 +2236,11 @@
                                 showErrorMessage(R.string.cant_call_due_to_no_supported_service);
                                 mListeners.forEach(l -> l.onCreateConnectionFailed(callToPlace));
                                 if (callToPlace.isEmergencyCall()) {
+                                    if (mFeatureFlags.telecomMetricsSupport()) {
+                                        mMetricsController.getErrorStats().log(
+                                                ErrorStats.SUB_CALL_MANAGER,
+                                                ErrorStats.ERROR_EMERGENCY_CALL_ABORTED_NO_ACCOUNT);
+                                    }
                                     mAnomalyReporter.reportAnomaly(
                                             EMERGENCY_CALL_ABORTED_NO_PHONE_ACCOUNTS_ERROR_UUID,
                                             EMERGENCY_CALL_ABORTED_NO_PHONE_ACCOUNTS_ERROR_MSG);
@@ -2219,6 +2266,11 @@
                                         PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION)) {
                                     if (SubscriptionManager.getDefaultVoiceSubscriptionId() !=
                                             SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+                                        if (mFeatureFlags.telecomMetricsSupport()) {
+                                            mMetricsController.getErrorStats().log(
+                                                    ErrorStats.SUB_CALL_MANAGER,
+                                                    ErrorStats.ERROR_DEFAULT_MO_ACCOUNT_MISMATCH);
+                                        }
                                         mAnomalyReporter.reportAnomaly(
                                                 TELEPHONY_HAS_DEFAULT_BUT_TELECOM_DOES_NOT_UUID,
                                                 TELEPHONY_HAS_DEFAULT_BUT_TELECOM_DOES_NOT_MSG);
@@ -2540,8 +2592,8 @@
                 theCall,
                 new AppLabelProxy() {
                     @Override
-                    public CharSequence getAppLabel(String packageName) {
-                        return Util.getAppLabel(mContext.getPackageManager(), packageName);
+                    public CharSequence getAppLabel(String packageName, UserHandle userHandle) {
+                        return Util.getAppLabel(mContext, userHandle, packageName, mFeatureFlags);
                     }
                 }).process();
         future.thenApply( v -> {
@@ -3033,6 +3085,10 @@
                     // If an exceptions is thrown while creating the connection, prompt the user to
                     // generate a bugreport and force disconnect.
                     Log.e(TAG, exception, "Exception thrown while establishing connection.");
+                    if (mFeatureFlags.telecomMetricsSupport()) {
+                        mMetricsController.getErrorStats().log(ErrorStats.SUB_CALL_MANAGER,
+                                ErrorStats.ERROR_ESTABLISHING_CONNECTION);
+                    }
                     mAnomalyReporter.reportAnomaly(
                             EXCEPTION_WHILE_ESTABLISHING_CONNECTION_ERROR_UUID,
                             EXCEPTION_WHILE_ESTABLISHING_CONNECTION_ERROR_MSG);
@@ -3076,7 +3132,22 @@
     public void answerCall(Call call, int videoState) {
         if (!mCalls.contains(call)) {
             Log.i(this, "Request to answer a non-existent call %s", call);
-        } else if (call.isTransactionalCall()) {
+        }
+        mCallSequencingAdapter.answerCall(call, videoState);
+    }
+
+    /**
+     * 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.
+     * <p>
+     * Note: This is only used when {@link FeatureFlags#enableCallSequencing()} is false.
+     */
+    public void answerCallOld(Call call, int videoState) {
+        if (call.isTransactionalCall()) {
             // InCallAdapter is requesting to answer the given transactioanl call. Must get an ack
             // from the client via a transaction before answering.
             call.answer(videoState);
@@ -3143,7 +3214,7 @@
         }
 
         CharSequence requestingAppName = AppLabelProxy.Util.getAppLabel(
-                mContext.getPackageManager(), requestingPackageName);
+                mContext, call.getAssociatedUser(), requestingPackageName, mFeatureFlags);
         if (requestingAppName == null) {
             requestingAppName = requestingPackageName;
         }
@@ -3410,10 +3481,10 @@
     public void holdCall(Call call) {
         if (!mCalls.contains(call)) {
             Log.w(this, "Unknown call (%s) asked to be put on hold", call);
-        } else {
-            Log.d(this, "Putting call on hold: (%s)", call);
-            call.hold();
+            return;
         }
+        Log.d(this, "Putting call on hold: (%s)", call);
+        mCallSequencingAdapter.holdCall(call);
     }
 
     /**
@@ -3425,44 +3496,57 @@
     public void unholdCall(Call call) {
         if (!mCalls.contains(call)) {
             Log.w(this, "Unknown call (%s) asked to be removed from hold", call);
-        } else {
-            if (getOutgoingCall() != null) {
-                Log.w(this, "There is an outgoing call, so it is unable to unhold this call %s",
-                        call);
-                return;
-            }
-            Call activeCall = (Call) mConnectionSvrFocusMgr.getCurrentFocusCall();
-            String activeCallId = null;
-            if (activeCall != null && !activeCall.isLocallyDisconnecting()) {
-                activeCallId = activeCall.getId();
-                if (canHold(activeCall)) {
-                    activeCall.hold("Swap to " + call.getId());
-                    Log.addEvent(activeCall, LogUtils.Events.SWAP, "To " + call.getId());
-                    Log.addEvent(call, LogUtils.Events.SWAP, "From " + activeCall.getId());
-                } else {
-                    // This call does not support hold. If it is from a different connection
-                    // service or connection manager, then disconnect it, otherwise invoke
-                    // call.hold() and allow the connection service or connection manager to handle
-                    // the situation.
-                    if (!areFromSameSource(activeCall, call)) {
-                        if (!activeCall.isEmergencyCall()) {
-                            activeCall.disconnect("Swap to " + call.getId());
-                        } else {
-                            Log.w(this, "unholdCall: % is an emergency call, aborting swap to %s",
-                                    activeCall.getId(), call.getId());
-                            // Don't unhold the call as requested; we don't want to drop an
-                            // emergency call.
-                            return;
-                        }
+            return;
+        }
+        if (getOutgoingCall() != null) {
+            Log.w(this, "There is an outgoing call, so it is unable to unhold this call %s",
+                    call);
+            return;
+        }
+        mCallSequencingAdapter.unholdCall(call);
+    }
+
+    /**
+     * Instructs telecom to hold any ongoing active calls and bring this call to the active state.
+     * <p>
+     * Note: This is only used when {@link FeatureFlags#enableCallSequencing()} is false.
+     */
+    public void unholdCallOld(Call call) {
+        Call activeCall = (Call) mConnectionSvrFocusMgr.getCurrentFocusCall();
+        String activeCallId = null;
+        if (activeCall != null && !activeCall.isLocallyDisconnecting()) {
+            activeCallId = activeCall.getId();
+            if (canHold(activeCall)) {
+                activeCall.hold("Swap to " + call.getId());
+                Log.addEvent(activeCall, LogUtils.Events.SWAP, "To " + call.getId());
+                Log.addEvent(call, LogUtils.Events.SWAP, "From " + activeCall.getId());
+            } else {
+                // This call does not support hold. If it is from a different connection
+                // service or connection manager, then disconnect it, otherwise invoke
+                // call.hold() and allow the connection service or connection manager to handle
+                // the situation.
+                if (!areFromSameSource(activeCall, call)) {
+                    if (!activeCall.isEmergencyCall()) {
+                        activeCall.disconnect("Swap to " + call.getId());
                     } else {
-                        activeCall.hold("Swap to " + call.getId());
+                        Log.w(this, "unholdCall: % is an emergency call, aborting swap to %s",
+                                activeCall.getId(), call.getId());
+                        // Don't unhold the call as requested; we don't want to drop an
+                        // emergency call.
+                        return;
                     }
+                } else {
+                    activeCall.hold("Swap to " + call.getId());
                 }
             }
-            mConnectionSvrFocusMgr.requestFocus(
-                    call,
-                    new RequestCallback(new ActionUnHoldCall(call, activeCallId)));
         }
+        requestActionUnholdCall(call, activeCallId);
+    }
+
+    public void requestActionUnholdCall(Call call, String activeCallId) {
+        mConnectionSvrFocusMgr.requestFocus(
+                call,
+                new RequestCallback(new ActionUnHoldCall(call, activeCallId)));
     }
 
     @Override
@@ -3831,6 +3915,11 @@
         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.
      */
@@ -3972,6 +4061,12 @@
         return supportsHold(activeCall) && areFromSameSource(activeCall, call);
     }
 
+    /**
+     * 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) {
         Log.i(this, "markCallAsActive, isSelfManaged: " + call.isSelfManaged());
@@ -4006,6 +4101,11 @@
         }
     }
 
+    /**
+     * 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");
     }
@@ -4160,6 +4260,10 @@
                     }, new LoggedHandlerExecutor(mHandler, "CM.pR", mLock))
                     .exceptionally((throwable) -> {
                         Log.e(TAG, throwable, "Error while executing call removal");
+                        if (mFeatureFlags.telecomMetricsSupport()) {
+                            mMetricsController.getErrorStats().log(ErrorStats.SUB_CALL_MANAGER,
+                                    ErrorStats.ERROR_REMOVING_CALL);
+                        }
                         mAnomalyReporter.reportAnomaly(CALL_REMOVAL_EXECUTION_ERROR_UUID,
                                 CALL_REMOVAL_EXECUTION_ERROR_MSG);
                         return null;
@@ -4177,12 +4281,24 @@
     private void doRemoval(Call call) {
         call.maybeCleanupHandover();
         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 (mLocallyDisconnectingCalls.contains(call)) {
-            boolean isDisconnectingChildCall = call.isDisconnectingChildCall();
-            Log.v(this, "performRemoval: isDisconnectingChildCall = "
-                    + isDisconnectingChildCall + "call -> %s", call);
-            mLocallyDisconnectingCalls.remove(call);
+        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).
@@ -4191,7 +4307,8 @@
             // implementations, especially if one is managed and the other is a VoIP CS.
             if (!isDisconnectingChildCall && foregroundCall != null
                     && foregroundCall.getState() == CallState.ON_HOLD
-                    && areFromSameSource(foregroundCall, call)) {
+                    && areFromSameSource(foregroundCall, removedCall)) {
+
                 foregroundCall.unhold();
             }
         } else if (foregroundCall != null &&
@@ -4201,8 +4318,8 @@
             // 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, "performRemoval: Auto-unholding held foreground call (call doesn't "
-                    + "support hold)");
+            Log.i(this, "maybeMoveHeldCallToForeground: Auto-unholding held foreground call (call "
+                    + "doesn't support hold)");
             foregroundCall.unhold();
         }
     }
@@ -4309,12 +4426,14 @@
                         return true;
                     }
                 } else {
+                    Log.addEvent(ringingCall, LogUtils.Events.INFO,
+                            "media btn short press - answer call.");
                     answerCall(ringingCall, VideoProfile.STATE_AUDIO_ONLY);
                     return true;
                 }
             } else if (HeadsetMediaButton.LONG_PRESS == type) {
                 if (ringingCall != null) {
-                    Log.addEvent(getForegroundCall(),
+                    Log.addEvent(ringingCall,
                             LogUtils.Events.INFO, "media btn long press - reject");
                     ringingCall.reject(false, null);
                 } else {
@@ -4335,6 +4454,7 @@
                 return true;
             }
         }
+        Log.i(this, "onMediaButton: type=%d; no active calls", type);
         return false;
     }
 
@@ -4426,6 +4546,10 @@
         return getFirstCallWithState(null, states);
     }
 
+    public Call getFirstCallWithLiveState() {
+        return getFirstCallWithState(null, LIVE_CALL_STATES);
+    }
+
     @VisibleForTesting
     public PhoneNumberUtilsAdapter getPhoneNumberUtilsAdapter() {
         return mPhoneNumberUtilsAdapter;
@@ -5007,7 +5131,7 @@
         return (int) callsStream.count();
     }
 
-    private boolean hasMaximumLiveCalls(Call exceptCall) {
+    public boolean hasMaximumLiveCalls(Call exceptCall) {
         return MAXIMUM_LIVE_CALLS <= getNumCallsWithState(CALL_FILTER_ALL,
                 exceptCall, null /* phoneAccountHandle*/, LIVE_CALL_STATES);
     }
@@ -5151,6 +5275,14 @@
                 && incomingCall.getHandoverSourceCall() == null;
     }
 
+    /**
+     * Make room for a pending outgoing emergency {@link Call}.
+     * <p>
+     * Note: This method is only applicable when {@link FeatureFlags#enableCallSequencing()}
+     * is false.
+     * @param call The new pending outgoing call.
+     * @return true if room was made, false if no room could be made.
+     */
     @VisibleForTesting
     public boolean makeRoomForOutgoingEmergencyCall(Call emergencyCall) {
         // Always disconnect any ringing/incoming calls when an emergency call is placed to minimize
@@ -5227,6 +5359,10 @@
 
         // If the live call is stuck in a connecting state, prompt the user to generate a bugreport.
         if (liveCall.getState() == CallState.CONNECTING) {
+            if (mFeatureFlags.telecomMetricsSupport()) {
+                mMetricsController.getErrorStats().log(ErrorStats.SUB_CALL_MANAGER,
+                        ErrorStats.ERROR_STUCK_CONNECTING_EMERGENCY);
+            }
             mAnomalyReporter.reportAnomaly(LIVE_CALL_STUCK_CONNECTING_EMERGENCY_ERROR_UUID,
                     LIVE_CALL_STUCK_CONNECTING_EMERGENCY_ERROR_MSG);
         }
@@ -5313,6 +5449,14 @@
         return false;
     }
 
+    /**
+     * Make room for a pending outgoing {@link Call}.
+     * <p>
+     * Note: This method is only applicable when {@link FeatureFlags#enableCallSequencing()}
+     * is false.
+     * @param call The new pending outgoing call.
+     * @return true if room was made, false if no room could be made.
+     */
     @VisibleForTesting
     public boolean makeRoomForOutgoingCall(Call call) {
         // Already room!
@@ -5343,6 +5487,10 @@
         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);
             liveCall.disconnect("Force disconnect CONNECTING call.");
@@ -6472,7 +6620,7 @@
                 call.can(Connection.CAPABILITY_HOLD)) && call.getState() != CallState.DIALING;
     }
 
-    private boolean supportsHold(Call call) {
+    public boolean supportsHold(Call call) {
         return call.can(Connection.CAPABILITY_SUPPORT_HOLD);
     }
 
diff --git a/src/com/android/server/telecom/ConnectionServiceWrapper.java b/src/com/android/server/telecom/ConnectionServiceWrapper.java
index 44686b7..14c8f62 100644
--- a/src/com/android/server/telecom/ConnectionServiceWrapper.java
+++ b/src/com/android/server/telecom/ConnectionServiceWrapper.java
@@ -45,6 +45,7 @@
 import android.telecom.GatewayInfo;
 import android.telecom.Log;
 import android.telecom.Logging.Session;
+import android.telecom.Logging.Runnable;
 import android.telecom.ParcelableConference;
 import android.telecom.ParcelableConnection;
 import android.telecom.PhoneAccountHandle;
@@ -73,10 +74,13 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.UUID;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
 import java.util.Objects;
 
@@ -90,10 +94,28 @@
 public class ConnectionServiceWrapper extends ServiceBinder implements
         ConnectionServiceFocusManager.ConnectionServiceFocus, CallSourceService {
 
+    /**
+     * Anomaly Report UUIDs and corresponding error descriptions specific to CallsManager.
+     */
+    public static final UUID CREATE_CONNECTION_TIMEOUT_ERROR_UUID =
+            UUID.fromString("54b7203d-a79f-4cbd-b639-85cd93a39cbb");
+    public static final String CREATE_CONNECTION_TIMEOUT_ERROR_MSG =
+            "Timeout expired before Telecom connection was created.";
+    public static final UUID CREATE_CONFERENCE_TIMEOUT_ERROR_UUID =
+            UUID.fromString("caafe5ea-2472-4c61-b2d8-acb9d47e13dd");
+    public static final String CREATE_CONFERENCE_TIMEOUT_ERROR_MSG =
+            "Timeout expired before Telecom conference was created.";
+
     private static final String TELECOM_ABBREVIATION = "cast";
+    private static final long SERVICE_BINDING_TIMEOUT = 15000L;
     private CompletableFuture<Pair<Integer, Location>> mQueryLocationFuture = null;
     private @Nullable CancellationSignal mOngoingQueryLocationRequest = null;
     private final ExecutorService mQueryLocationExecutor = Executors.newSingleThreadExecutor();
+    private ScheduledExecutorService mScheduledExecutor =
+            Executors.newSingleThreadScheduledExecutor();
+    // Pre-allocate space for 2 calls; realistically thats all we should ever need (tm)
+    private final Map<Call, ScheduledFuture<?>> mScheduledFutureMap = new ConcurrentHashMap<>(2);
+    private AnomalyReporterAdapter mAnomalyReporter = new AnomalyReporterAdapterImpl();
 
     private final class Adapter extends IConnectionServiceAdapter.Stub {
 
@@ -107,6 +129,12 @@
             try {
                 synchronized (mLock) {
                     logIncoming("handleCreateConnectionComplete %s", callId);
+                    Call call = mCallIdMapper.getCall(callId);
+                    if (call != null && mScheduledFutureMap.containsKey(call)) {
+                        ScheduledFuture<?> existingTimeout = mScheduledFutureMap.get(call);
+                        existingTimeout.cancel(false /* cancelIfRunning */);
+                        mScheduledFutureMap.remove(call);
+                    }
                     // Check status hints image for cross user access
                     if (connection.getStatusHints() != null) {
                         Icon icon = connection.getStatusHints().getIcon();
@@ -145,6 +173,12 @@
             try {
                 synchronized (mLock) {
                     logIncoming("handleCreateConferenceComplete %s", callId);
+                    Call call = mCallIdMapper.getCall(callId);
+                    if (call != null && mScheduledFutureMap.containsKey(call)) {
+                        ScheduledFuture<?> existingTimeout = mScheduledFutureMap.get(call);
+                        existingTimeout.cancel(false /* cancelIfRunning */);
+                        mScheduledFutureMap.remove(call);
+                    }
                     // Check status hints image for cross user access
                     if (conference.getStatusHints() != null) {
                         Icon icon = conference.getStatusHints().getIcon();
@@ -1611,6 +1645,29 @@
                         .setParticipants(call.getParticipants())
                         .setIsAdhocConferenceCall(call.isAdhocConferenceCall())
                         .build();
+                Runnable r = new Runnable("CSW.cC", mLock) {
+                    @Override
+                    public void loggedRun() {
+                        if (!call.isCreateConnectionComplete()) {
+                            Log.e(this, new Exception(),
+                                    "Conference %s creation timeout",
+                                    getComponentName());
+                            Log.addEvent(call, LogUtils.Events.CREATE_CONFERENCE_TIMEOUT,
+                                    Log.piiHandle(call.getHandle()) + " via:" +
+                                            getComponentName().getPackageName());
+                            mAnomalyReporter.reportAnomaly(
+                                    CREATE_CONFERENCE_TIMEOUT_ERROR_UUID,
+                                    CREATE_CONFERENCE_TIMEOUT_ERROR_MSG);
+                            response.handleCreateConferenceFailure(
+                                    new DisconnectCause(DisconnectCause.ERROR));
+                        }
+                    }
+                };
+                // Post cleanup to the executor service and cache the future, so we can cancel it if
+                // needed.
+                ScheduledFuture<?> future = mScheduledExecutor.schedule(r.getRunnableToCancel(),
+                        SERVICE_BINDING_TIMEOUT, TimeUnit.MILLISECONDS);
+                mScheduledFutureMap.put(call, future);
                 try {
                     mServiceInterface.createConference(
                             call.getConnectionManagerPhoneAccount(),
@@ -1711,6 +1768,29 @@
                         .setRttPipeFromInCall(call.getInCallToCsRttPipeForCs())
                         .setRttPipeToInCall(call.getCsToInCallRttPipeForCs())
                         .build();
+                Runnable r = new Runnable("CSW.cC", mLock) {
+                    @Override
+                    public void loggedRun() {
+                        if (!call.isCreateConnectionComplete()) {
+                            Log.e(this, new Exception(),
+                                    "Connection %s creation timeout",
+                                    getComponentName());
+                            Log.addEvent(call, LogUtils.Events.CREATE_CONNECTION_TIMEOUT,
+                                    Log.piiHandle(call.getHandle()) + " via:" +
+                                            getComponentName().getPackageName());
+                            mAnomalyReporter.reportAnomaly(
+                                    CREATE_CONNECTION_TIMEOUT_ERROR_UUID,
+                                    CREATE_CONNECTION_TIMEOUT_ERROR_MSG);
+                            response.handleCreateConnectionFailure(
+                                    new DisconnectCause(DisconnectCause.ERROR));
+                        }
+                    }
+                };
+                // Post cleanup to the executor service and cache the future, so we can cancel it if
+                // needed.
+                ScheduledFuture<?> future = mScheduledExecutor.schedule(r.getRunnableToCancel(),
+                        SERVICE_BINDING_TIMEOUT, TimeUnit.MILLISECONDS);
+                mScheduledFutureMap.put(call, future);
                 try {
                     if (mFlags.cswServiceInterfaceIsNull() && mServiceInterface == null) {
                         mPendingResponses.remove(callId).handleCreateConnectionFailure(
@@ -2182,7 +2262,8 @@
         }
     }
 
-    void addCall(Call call) {
+    @VisibleForTesting
+    public void addCall(Call call) {
         if (mCallIdMapper.getCallId(call) == null) {
             mCallIdMapper.addCall(call);
         }
@@ -2399,6 +2480,13 @@
     @Override
     protected void removeServiceInterface() {
         Log.v(this, "Removing Connection Service Adapter.");
+        if (mServiceInterface == null) {
+            // In some cases, we may receive multiple calls to
+            // remoteServiceInterface, such as when the remote process crashes
+            // (onBinderDied & onServiceDisconnected)
+            Log.w(this, "removeServiceInterface: mServiceInterface is null");
+            return;
+        }
         removeConnectionServiceAdapter(mAdapter);
         // We have lost our service connection. Notify the world that this service is done.
         // We must notify the adapter before CallsManager. The adapter will force any pending
@@ -2407,6 +2495,10 @@
         handleConnectionServiceDeath();
         mCallsManager.handleConnectionServiceDeath(this);
         mServiceInterface = null;
+        if (mScheduledExecutor != null) {
+            mScheduledExecutor.shutdown();
+            mScheduledExecutor = null;
+        }
     }
 
     @Override
@@ -2526,6 +2618,7 @@
             }
         }
         mCallIdMapper.clear();
+        mScheduledFutureMap.clear();
 
         if (mConnSvrFocusListener != null) {
             mConnSvrFocusListener.onConnectionServiceDeath(this);
@@ -2651,4 +2744,14 @@
         sb.append("]");
         return sb.toString();
     }
+
+    @VisibleForTesting
+    public void setScheduledExecutorService(ScheduledExecutorService service) {
+        mScheduledExecutor = service;
+    }
+
+    @VisibleForTesting
+    public void setAnomalyReporterAdapter(AnomalyReporterAdapter mAnomalyReporterAdapter){
+        mAnomalyReporter = mAnomalyReporterAdapter;
+    }
 }
diff --git a/src/com/android/server/telecom/DefaultDialerCache.java b/src/com/android/server/telecom/DefaultDialerCache.java
index 44b426a..98289ed 100644
--- a/src/com/android/server/telecom/DefaultDialerCache.java
+++ b/src/com/android/server/telecom/DefaultDialerCache.java
@@ -31,76 +31,58 @@
 import android.provider.Settings;
 import android.telecom.DefaultDialerManager;
 import android.telecom.Log;
-import android.util.SparseArray;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.IndentingPrintWriter;
 
 import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.Executor;
 import java.util.function.IntConsumer;
 
 public class DefaultDialerCache {
-    public interface DefaultDialerManagerAdapter {
-        String getDefaultDialerApplication(Context context);
-        String getDefaultDialerApplication(Context context, int userId);
-        boolean setDefaultDialerApplication(Context context, String packageName, int userId);
-    }
-
-    static class DefaultDialerManagerAdapterImpl implements DefaultDialerManagerAdapter {
-        @Override
-        public String getDefaultDialerApplication(Context context) {
-            return DefaultDialerManager.getDefaultDialerApplication(context);
-        }
-
-        @Override
-        public String getDefaultDialerApplication(Context context, int userId) {
-            return DefaultDialerManager.getDefaultDialerApplication(context, userId);
-        }
-
-        @Override
-        public boolean setDefaultDialerApplication(Context context, String packageName,
-                int userId) {
-            return DefaultDialerManager.setDefaultDialerApplication(context, packageName, userId);
-        }
-    }
-
     private static final String LOG_TAG = "DefaultDialerCache";
+    @VisibleForTesting
+    public final Handler mHandler = new Handler(Looper.getMainLooper());
+    private final Context mContext;
+    private final DefaultDialerManagerAdapter mDefaultDialerManagerAdapter;
+    private final ComponentName mSystemDialerComponentName;
+    private final RoleManagerAdapter mRoleManagerAdapter;
+    private final ConcurrentHashMap<Integer, String> mCurrentDefaultDialerPerUser =
+            new ConcurrentHashMap<>();
     private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
-            Log.startSession("DDC.oR");
-            try {
-                String packageName;
-                if (Intent.ACTION_PACKAGE_CHANGED.equals(intent.getAction())) {
-                    packageName = null;
-                } else if (Intent.ACTION_PACKAGE_REMOVED.equals(intent.getAction())
-                        && !intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
-                    packageName = intent.getData().getSchemeSpecificPart();
-                } else if (Intent.ACTION_PACKAGE_ADDED.equals(intent.getAction())) {
-                    packageName = null;
-                } else if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
-                    packageName = null;
-                } else {
-                    return;
-                }
+            mHandler.post(() -> {
+                Log.startSession("DDC.oR");
+                try {
+                    String packageName;
+                    if (Intent.ACTION_PACKAGE_CHANGED.equals(intent.getAction())) {
+                        packageName = null;
+                    } else if (Intent.ACTION_PACKAGE_REMOVED.equals(intent.getAction())
+                            && !intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
+                        packageName = intent.getData().getSchemeSpecificPart();
+                    } else if (Intent.ACTION_PACKAGE_ADDED.equals(intent.getAction())) {
+                        packageName = null;
+                    } else if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
+                        packageName = null;
+                    } else {
+                        return;
+                    }
 
-                synchronized (mLock) {
                     refreshCachesForUsersWithPackage(packageName);
+                } finally {
+                    Log.endSession();
                 }
-
-            } finally {
-                Log.endSession();
-            }
+            });
         }
     };
-
     private final BroadcastReceiver mUserRemovedReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
             if (Intent.ACTION_USER_REMOVED.equals(intent.getAction())) {
                 int removedUser = intent.getIntExtra(Intent.EXTRA_USER_HANDLE,
-                    UserHandle.USER_NULL);
+                        UserHandle.USER_NULL);
                 if (removedUser == UserHandle.USER_NULL) {
                     Log.w(LOG_TAG, "Expected EXTRA_USER_HANDLE with ACTION_USER_REMOVED");
                 } else {
@@ -110,8 +92,6 @@
             }
         }
     };
-
-    private final Handler mHandler = new Handler(Looper.getMainLooper());
     private final ContentObserver mDefaultDialerObserver = new ContentObserver(mHandler) {
         @Override
         public void onChange(boolean selfChange) {
@@ -119,9 +99,7 @@
             try {
                 // We don't get the user ID of the user that changed here, so we'll have to
                 // refresh all of the users.
-                synchronized (mLock) {
-                    refreshCachesForUsersWithPackage(null);
-                }
+                refreshCachesForUsersWithPackage(null);
             } finally {
                 Log.endSession();
             }
@@ -132,29 +110,21 @@
             return true;
         }
     };
-
-    private final Context mContext;
-    private final DefaultDialerManagerAdapter mDefaultDialerManagerAdapter;
-    private final TelecomSystem.SyncRoot mLock;
-    private final ComponentName mSystemDialerComponentName;
-    private final RoleManagerAdapter mRoleManagerAdapter;
-    private SparseArray<String> mCurrentDefaultDialerPerUser = new SparseArray<>();
     private ComponentName mOverrideSystemDialerComponentName;
 
     public DefaultDialerCache(Context context,
-                              DefaultDialerManagerAdapter defaultDialerManagerAdapter,
-                              RoleManagerAdapter roleManagerAdapter,
-                              TelecomSystem.SyncRoot lock) {
+            DefaultDialerManagerAdapter defaultDialerManagerAdapter,
+            RoleManagerAdapter roleManagerAdapter,
+            TelecomSystem.SyncRoot lock) {
         mContext = context;
         mDefaultDialerManagerAdapter = defaultDialerManagerAdapter;
         mRoleManagerAdapter = roleManagerAdapter;
-        mLock = lock;
+
         Resources resources = mContext.getResources();
         mSystemDialerComponentName = new ComponentName(resources.getString(
                 com.android.internal.R.string.config_defaultDialer),
                 resources.getString(R.string.incall_default_class));
 
-
         IntentFilter packageIntentFilter = new IntentFilter();
         packageIntentFilter.addAction(Intent.ACTION_PACKAGE_CHANGED);
         packageIntentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
@@ -195,7 +165,7 @@
         //
         //synchronized (mLock) {
         //    String defaultDialer = mCurrentDefaultDialerPerUser.get(userId);
-        //    if (defaultDialer != null) {
+        //    if (!TextUtils.isEmpty(defaultDialer)) {
         //        return defaultDialer;
         //    }
         //}
@@ -241,11 +211,9 @@
     public boolean setDefaultDialer(String packageName, int userId) {
         boolean isChanged = mDefaultDialerManagerAdapter.setDefaultDialerApplication(
                 mContext, packageName, userId);
-        if(isChanged) {
-            synchronized (mLock) {
-                // Update the cache synchronously so that there is no delay in cache update.
-                mCurrentDefaultDialerPerUser.put(userId, packageName);
-            }
+        if (isChanged) {
+            // Update the cache synchronously so that there is no delay in cache update.
+            mCurrentDefaultDialerPerUser.put(userId, packageName == null ? "" : packageName);
         }
         return isChanged;
     }
@@ -253,47 +221,39 @@
     private String refreshCacheForUser(int userId) {
         String currentDefaultDialer =
                 mRoleManagerAdapter.getDefaultDialerApp(userId);
-        synchronized (mLock) {
-            mCurrentDefaultDialerPerUser.put(userId, currentDefaultDialer);
-        }
+        mCurrentDefaultDialerPerUser.put(userId, currentDefaultDialer == null ? "" :
+                currentDefaultDialer);
         return currentDefaultDialer;
     }
 
     /**
      * Refreshes the cache for users that currently have packageName as their cached default dialer.
      * If packageName is null, refresh all caches.
+     *
      * @param packageName Name of the affected package.
      */
     private void refreshCachesForUsersWithPackage(String packageName) {
-        for (int i = 0; i < mCurrentDefaultDialerPerUser.size(); i++) {
-            int userId = mCurrentDefaultDialerPerUser.keyAt(i);
-            if (packageName == null ||
-                    Objects.equals(packageName, mCurrentDefaultDialerPerUser.get(userId))) {
+        mCurrentDefaultDialerPerUser.forEach((userId, currentName) -> {
+            if (packageName == null || Objects.equals(packageName, currentName)) {
                 String newDefaultDialer = refreshCacheForUser(userId);
                 Log.v(LOG_TAG, "Refreshing default dialer for user %d: now %s",
                         userId, newDefaultDialer);
             }
-        }
+        });
     }
 
     public void dumpCache(IndentingPrintWriter pw) {
-        synchronized (mLock) {
-            for (int i = 0; i < mCurrentDefaultDialerPerUser.size(); i++) {
-                pw.printf("User %d: %s\n", mCurrentDefaultDialerPerUser.keyAt(i),
-                        mCurrentDefaultDialerPerUser.valueAt(i));
-            }
-        }
+        mCurrentDefaultDialerPerUser.forEach((k, v) -> pw.printf("User %d: %s\n", k, v));
     }
 
     private void removeUserFromCache(int userId) {
-        synchronized (mLock) {
-            mCurrentDefaultDialerPerUser.remove(userId);
-        }
+        mCurrentDefaultDialerPerUser.remove(userId);
     }
 
     /**
      * registerContentObserver is really hard to mock out, so here is a getter method for the
      * content observer for testing instead.
+     *
      * @return The content observer
      */
     @VisibleForTesting
@@ -304,4 +264,30 @@
     public RoleManagerAdapter getRoleManagerAdapter() {
         return mRoleManagerAdapter;
     }
-}
\ No newline at end of file
+
+    public interface DefaultDialerManagerAdapter {
+        String getDefaultDialerApplication(Context context);
+
+        String getDefaultDialerApplication(Context context, int userId);
+
+        boolean setDefaultDialerApplication(Context context, String packageName, int userId);
+    }
+
+    static class DefaultDialerManagerAdapterImpl implements DefaultDialerManagerAdapter {
+        @Override
+        public String getDefaultDialerApplication(Context context) {
+            return DefaultDialerManager.getDefaultDialerApplication(context);
+        }
+
+        @Override
+        public String getDefaultDialerApplication(Context context, int userId) {
+            return DefaultDialerManager.getDefaultDialerApplication(context, userId);
+        }
+
+        @Override
+        public boolean setDefaultDialerApplication(Context context, String packageName,
+                int userId) {
+            return DefaultDialerManager.setDefaultDialerApplication(context, packageName, userId);
+        }
+    }
+}
diff --git a/src/com/android/server/telecom/HeadsetMediaButton.java b/src/com/android/server/telecom/HeadsetMediaButton.java
index 7458f54..afc82ae 100644
--- a/src/com/android/server/telecom/HeadsetMediaButton.java
+++ b/src/com/android/server/telecom/HeadsetMediaButton.java
@@ -103,7 +103,7 @@
                 if ((event != null) && ((event.getKeyCode() == KeyEvent.KEYCODE_HEADSETHOOK) ||
                         (event.getKeyCode() == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE))) {
                     synchronized (mLock) {
-                        Log.v(this, "SessionCallback: HEADSETHOOK/MEDIA_PLAY_PAUSE");
+                        Log.i(this, "onMediaButton: event=%s", event);
                         boolean consumed = handleCallMediaButton(event);
                         Log.v(this, "==> handleCallMediaButton(): consumed = %b.", consumed);
                         return consumed;
diff --git a/src/com/android/server/telecom/InCallController.java b/src/com/android/server/telecom/InCallController.java
index 529bc79..3f8f579 100644
--- a/src/com/android/server/telecom/InCallController.java
+++ b/src/com/android/server/telecom/InCallController.java
@@ -305,7 +305,7 @@
 
         //this is really used for cases where the userhandle for a call
         //does not match what we want to use for bindAsUser
-        private final UserHandle mUserHandleToUseForBinding;
+        private UserHandle mUserHandleToUseForBinding;
 
         public InCallServiceBindingConnection(InCallServiceInfo info) {
             mInCallServiceInfo = info;
@@ -388,6 +388,8 @@
                                     + "INTERACT_ACROSS_USERS permission");
                 }
             }
+            // Used for referencing what user we used to bind to the given ICS.
+            mUserHandleToUseForBinding = userToBind;
             Log.i(this, "using user id: %s for binding. User from Call is: %s", userToBind,
                     userFromCall);
             if (!mContext.bindServiceAsUser(intent, mServiceConnection,
@@ -1230,7 +1232,7 @@
             mCombinedInCallServiceMap = new ArrayMap<>();
 
     private final CallIdMapper mCallIdMapper = new CallIdMapper(Call::getId);
-    private final Collection<Call> mPendingEndToneCall = new ArraySet<>();
+    private final Collection<Call> mBtIcsCallTracker = new ArraySet<>();
 
     private final Context mContext;
     private final AppOpsManager mAppOpsManager;
@@ -1246,7 +1248,7 @@
             mInCallServiceConnections = new ArrayMap<>();
     private final Map<UserHandle, NonUIInCallServiceConnectionCollection>
             mNonUIInCallServiceConnections = new ArrayMap<>();
-    private final Map<UserHandle, InCallServiceConnection> mBTInCallServiceConnections =
+    private final Map<UserHandle, InCallServiceBindingConnection> mBTInCallServiceConnections =
             new ArrayMap<>();
     private final ClockProxy mClockProxy;
     private final IBinder mToken = new Binder();
@@ -1421,6 +1423,7 @@
                 bindingToBtRequired = true;
                 bindToBTService(call, null);
             }
+
             if (!isBoundAndConnectedToServices(userFromCall)) {
                 Log.i(this, "onCallAdded: %s; not bound or connected to other ICS.", call);
                 // We are not bound, or we're not connected.
@@ -1565,39 +1568,85 @@
                                 + "disconnected tone future");
                         mDisconnectedToneBtFutures.get(call.getId()).complete(null);
                     }
-                    mPendingEndToneCall.remove(call);
-                    if (!mPendingEndToneCall.isEmpty()) {
-                        return;
-                    }
-                    UserHandle userHandle = getUserFromCall(call);
-                    if (mBTInCallServiceConnections.containsKey(userHandle)) {
-                        Log.i(this, "onDisconnectedTonePlaying: Schedule unbind BT service");
-                        final InCallServiceConnection connection =
-                                mBTInCallServiceConnections.get(userHandle);
-
-                        // Similar to in onCallRemoved when we unbind from the other ICS, we need to
-                        // delay unbinding from the BT ICS because we need to give the ICS a
-                        // moment to finish the onCallRemoved signal it got just prior.
-                        mHandler.postDelayed(new Runnable("ICC.oDCTP", mLock) {
-                            @Override
-                            public void loggedRun() {
-                                Log.i(this, "onDisconnectedTonePlaying: unbinding");
-                                connection.disconnect();
-                            }
-                        }.prepare(), mTimeoutsAdapter.getCallRemoveUnbindInCallServicesDelay(
-                                mContext.getContentResolver()));
-
-                        mBTInCallServiceConnections.remove(userHandle);
-                    }
-                    // Ensure that BT ICS instance is cleaned up
-                    if (mBTInCallServices.remove(userHandle) != null) {
-                        updateCombinedInCallServiceMap(userHandle);
-                    }
+                    // Schedule unbinding of BT ICS.
+                    maybeScheduleBtUnbind(call);
                 }
             }
         }
     }
 
+    public void maybeScheduleBtUnbind(Call call) {
+        mBtIcsCallTracker.remove(call);
+        // Track the current calls that are being tracked by the BT ICS and determine the
+        // associated users of those calls as well as the users which have been used to bind to the
+        // ICS.
+        Set<UserHandle> usersFromOngoingCalls = new ArraySet<>();
+        Set<UserHandle> usersCurrentlyBound = new ArraySet<>();
+        for (Call pendingCall : mBtIcsCallTracker) {
+            UserHandle userFromPendingCall = getUserFromCall(pendingCall);
+            final InCallServiceBindingConnection pendingCallConnection =
+                    mBTInCallServiceConnections.get(userFromPendingCall);
+            usersFromOngoingCalls.add(userFromPendingCall);
+            if (pendingCallConnection != null) {
+                usersCurrentlyBound.add(pendingCallConnection.mUserHandleToUseForBinding);
+            }
+        }
+
+        UserHandle userHandle = getUserFromCall(call);
+        // Refrain from unbinding ICS and clearing the ICS mapping if there's an ongoing call under
+        // the same associated user. Make sure we keep the internal mappings so that they aren't
+        // cleared until that call is disconnected. Note here that if the associated users are the
+        // same, the user used for the binding will also be the same.
+        if (usersFromOngoingCalls.contains(userHandle)) {
+            Log.i(this, "scheduleBtUnbind: Refraining from unbinding BT service due to an ongoing "
+                    + "call detected under the same user (%s).", userHandle);
+            return;
+        }
+
+        if (mBTInCallServiceConnections.containsKey(userHandle)) {
+            Log.i(this, "scheduleBtUnbind: Schedule unbind BT service");
+            final InCallServiceBindingConnection connection =
+                    mBTInCallServiceConnections.get(userHandle);
+            // The user that was used for binding may be different than the user from call
+            // (associated user), which is what we use to reference the BT ICS bindings. For
+            // example, consider the work profile scenario where the BT ICS is only available under
+            // User 0: in this case, the user to bind to will be User 0 whereas we store the
+            // references to this connection and BT ICS under the work user. This logic ensures
+            // that we prevent unbinding the BT ICS if there is a personal (associatedUser: 0) call
+            // + work call (associatedUser: 10) and one of them gets disconnected.
+            if (usersCurrentlyBound.contains(connection.mUserHandleToUseForBinding)) {
+                Log.i(this, "scheduleBtUnbind: Refraining from unbinding BT service to an "
+                        + "ongoing call detected which is bound to the same user (%s).",
+                        connection.mUserHandleToUseForBinding);
+            } else {
+                // Similar to in onCallRemoved when we unbind from the other ICS, we need to
+                // delay unbinding from the BT ICS because we need to give the ICS a
+                // moment to finish the onCallRemoved signal it got just prior.
+                mHandler.postDelayed(new Runnable("ICC.sBU", mLock) {
+                    @Override
+                    public void loggedRun() {
+                        Log.i(this, "onDisconnectedTonePlaying: unbinding from BT ICS.");
+                        // Prevent unbinding in the case that this is run while another call
+                        // has been placed/received. Otherwise, we will early unbind from
+                        // the BT ICS and not be able to properly relay call state updates.
+                        if (!mBTInCallServiceConnections.containsKey(userHandle)) {
+                            connection.disconnect();
+                        } else {
+                            Log.i(this, "onDisconnectedTonePlaying: Refraining from "
+                                    + "unbinding BT ICS. Another call is ongoing.");
+                        }
+                    }
+                }.prepare(), mTimeoutsAdapter.getCallRemoveUnbindInCallServicesDelay(
+                        mContext.getContentResolver()));
+            }
+            mBTInCallServiceConnections.remove(userHandle);
+        }
+        // Ensure that BT ICS instance is cleaned up
+        if (mBTInCallServices.remove(userHandle) != null) {
+            updateCombinedInCallServiceMap(userHandle);
+        }
+    }
+
     @Override
     public void onExternalCallChanged(Call call, boolean isExternalCall) {
         Log.i(this, "onExternalCallChanged: %s -> %b", call, isExternalCall);
@@ -1873,7 +1922,6 @@
         }
     }
 
-    @VisibleForTesting
     public void bringToForeground(boolean showDialpad, UserHandle callingUser) {
         KeyguardManager keyguardManager = mContext.getSystemService(KeyguardManager.class);
         boolean isLockscreenRestricted = keyguardManager != null
@@ -2778,7 +2826,9 @@
                                     "updateCall: (deferred) Sending call disconnected update "
                                             + "to BT ICS.");
                             updateCallToIcs(inCallService, info, parcelableCall, componentName);
-                            mDisconnectedToneBtFutures.remove(call.getId());
+                            synchronized (mLock) {
+                                mDisconnectedToneBtFutures.remove(call.getId());
+                            }
                         });
                         mDisconnectedToneBtFutures.put(call.getId(), disconnectedToneFuture);
                     } else {
@@ -2832,7 +2882,7 @@
             mCallIdMapper.addCall(call);
             call.addListener(mCallListener);
             if (mFeatureFlags.separatelyBindToBtIncallService()) {
-                mPendingEndToneCall.add(call);
+                mBtIcsCallTracker.add(call);
             }
         }
 
diff --git a/src/com/android/server/telecom/LogUtils.java b/src/com/android/server/telecom/LogUtils.java
index 0d6acd5..d98ebfe 100644
--- a/src/com/android/server/telecom/LogUtils.java
+++ b/src/com/android/server/telecom/LogUtils.java
@@ -139,8 +139,10 @@
         public static final String STOP_CALL_WAITING_TONE = "STOP_CALL_WAITING_TONE";
         public static final String START_CONNECTION = "START_CONNECTION";
         public static final String CREATE_CONNECTION_FAILED = "CREATE_CONNECTION_FAILED";
+        public static final String CREATE_CONNECTION_TIMEOUT = "CREATE_CONNECTION_TIMEOUT";
         public static final String START_CONFERENCE = "START_CONFERENCE";
         public static final String CREATE_CONFERENCE_FAILED = "CREATE_CONFERENCE_FAILED";
+        public static final String CREATE_CONFERENCE_TIMEOUT = "CREATE_CONFERENCE_TIMEOUT";
         public static final String BIND_CS = "BIND_CS";
         public static final String CS_BOUND = "CS_BOUND";
         public static final String CONFERENCE_WITH = "CONF_WITH";
diff --git a/src/com/android/server/telecom/PendingAudioRoute.java b/src/com/android/server/telecom/PendingAudioRoute.java
index ffde964..d21ac56 100644
--- a/src/com/android/server/telecom/PendingAudioRoute.java
+++ b/src/com/android/server/telecom/PendingAudioRoute.java
@@ -130,6 +130,10 @@
         mPendingMessages.remove(message);
     }
 
+    public Set<Pair<Integer, String>> getPendingMessages() {
+        return mPendingMessages;
+    }
+
     public boolean isActive() {
         return mActive;
     }
@@ -146,4 +150,8 @@
     public void overrideDestRoute(AudioRoute route) {
         mDestRoute = route;
     }
+
+    public FeatureFlags getFeatureFlags() {
+        return mFeatureFlags;
+    }
 }
diff --git a/src/com/android/server/telecom/PhoneAccountRegistrar.java b/src/com/android/server/telecom/PhoneAccountRegistrar.java
index f0423c3..1a1af92 100644
--- a/src/com/android/server/telecom/PhoneAccountRegistrar.java
+++ b/src/com/android/server/telecom/PhoneAccountRegistrar.java
@@ -1284,12 +1284,15 @@
         boolean isNewAccount;
 
         // add self-managed capability for transactional accounts that are missing it
-        if (hasTransactionalCallCapabilities(account) &&
-                !account.hasCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)) {
+        if (hasTransactionalCallCapabilities(account)
+                && !account.hasCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)) {
             account = account.toBuilder()
                     .setCapabilities(account.getCapabilities()
                             | PhoneAccount.CAPABILITY_SELF_MANAGED)
                     .build();
+            // Note: below we will automatically remove CAPABILITY_CONNECTION_MANAGER,
+            // CAPABILITY_CALL_PROVIDER, and CAPABILITY_SIM_SUBSCRIPTION if this magically becomes
+            // a self-managed phone account here.
         }
 
         PhoneAccount oldAccount = getPhoneAccountUnchecked(account.getAccountHandle());
@@ -1310,6 +1313,12 @@
         if (account.hasCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)) {
             // Turn off bits we don't want to be able to set (TelecomServiceImpl protects against
             // this but we'll also prevent it from happening here, just to be safe).
+            if ((account.getCapabilities() & (PhoneAccount.CAPABILITY_CALL_PROVIDER
+                    | PhoneAccount.CAPABILITY_CONNECTION_MANAGER
+                    | PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) > 0) {
+                Log.w(this, "addOrReplacePhoneAccount: attempt to register a "
+                        + "VoIP phone account with call provider/cm/sim sub capabilities.");
+            }
             int newCapabilities = account.getCapabilities() &
                     ~(PhoneAccount.CAPABILITY_CALL_PROVIDER |
                         PhoneAccount.CAPABILITY_CONNECTION_MANAGER |
@@ -1317,7 +1326,10 @@
 
             // Ensure name is correct.
             CharSequence newLabel = mAppLabelProxy.getAppLabel(
-                    account.getAccountHandle().getComponentName().getPackageName());
+                    account.getAccountHandle().getComponentName().getPackageName(),
+                    UserUtil.getAssociatedUserForCall(
+                            mTelecomFeatureFlags.associatedUserRefactorForWorkProfile(),
+                            this, UserHandle.CURRENT, account.getAccountHandle()));
 
             account = account.toBuilder()
                     .setLabel(newLabel)
diff --git a/src/com/android/server/telecom/Ringer.java b/src/com/android/server/telecom/Ringer.java
index c309dd5..12778b0 100644
--- a/src/com/android/server/telecom/Ringer.java
+++ b/src/com/android/server/telecom/Ringer.java
@@ -358,6 +358,12 @@
 
             mVolumeShaperConfig = null;
 
+            String vibratorAttrs = String.format("hasVibrator=%b, userRequestsVibrate=%b, "
+                            + "ringerMode=%d, isVibratorEnabled=%b",
+                    mVibrator.hasVibrator(),
+                    mSystemSettingsUtil.isRingVibrationEnabled(mContext),
+                    mAudioManager.getRingerMode(), isVibratorEnabled);
+
             if (attributes.isRingerAudible()) {
                 mRingingCall = foregroundCall;
                 Log.addEvent(foregroundCall, LogUtils.Events.START_RINGER);
@@ -404,6 +410,8 @@
                     }
                 } else {
                     foregroundCall.setUserMissed(USER_MISSED_NO_VIBRATE);
+                    Log.addEvent(foregroundCall, LogUtils.Events.SKIP_VIBRATION,
+                            vibratorAttrs);
                     return attributes.shouldAcquireAudioFocus(); // ringer not audible
                 }
             }
@@ -436,11 +444,7 @@
             if (!vibratorReserved) {
                 foregroundCall.setUserMissed(USER_MISSED_NO_VIBRATE);
                 Log.addEvent(foregroundCall, LogUtils.Events.SKIP_VIBRATION,
-                        "hasVibrator=%b, userRequestsVibrate=%b, ringerMode=%d, "
-                                + "isVibratorEnabled=%b",
-                        mVibrator.hasVibrator(),
-                        mSystemSettingsUtil.isRingVibrationEnabled(mContext),
-                        mAudioManager.getRingerMode(), isVibratorEnabled);
+                        vibratorAttrs);
             }
 
             // The vibration logic depends on the loaded ringtone, but we need to defer the ringtone
@@ -556,6 +560,11 @@
                 mIsVibrating = true;
                 mVibrator.vibrate(effect, VIBRATION_ATTRIBUTES);
                 Log.i(this, "start vibration.");
+            } else {
+                Log.i(this, "vibrateIfNeeded: skip; isVibrating=%b, fgCallId=%s, vibratingCall=%s",
+                        mIsVibrating,
+                        (foregroundCall == null ? "null" : foregroundCall.getId()),
+                        (mVibratingCall == null ? "null" : mVibratingCall.getId()));
             }
             // else stopped already: this isn't started unless a reservation was made.
         }
diff --git a/src/com/android/server/telecom/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java
index b8141bf..5eb05dc 100644
--- a/src/com/android/server/telecom/TelecomServiceImpl.java
+++ b/src/com/android/server/telecom/TelecomServiceImpl.java
@@ -56,8 +56,6 @@
 import android.os.ParcelFileDescriptor;
 import android.os.Process;
 import android.os.RemoteException;
-import android.os.ResultReceiver;
-import android.os.ShellCallback;
 import android.os.UserHandle;
 import android.provider.BlockedNumberContract;
 import android.provider.BlockedNumbersManager;
@@ -77,22 +75,22 @@
 import android.util.EventLog;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.telecom.ICallControl;
 import com.android.internal.telecom.ICallEventCallback;
 import com.android.internal.telecom.ITelecomService;
 import com.android.internal.util.IndentingPrintWriter;
-import com.android.modules.utils.BasicShellCommandHandler;
 import com.android.server.telecom.components.UserCallIntentProcessorFactory;
 import com.android.server.telecom.flags.FeatureFlags;
+import com.android.server.telecom.metrics.ApiStats;
+import com.android.server.telecom.metrics.TelecomMetricsController;
 import com.android.server.telecom.settings.BlockedNumbersActivity;
-import com.android.server.telecom.voip.IncomingCallTransaction;
-import com.android.server.telecom.voip.OutgoingCallTransaction;
-import com.android.server.telecom.voip.TransactionManager;
-import com.android.server.telecom.voip.VoipCallTransaction;
-import com.android.server.telecom.voip.VoipCallTransactionResult;
+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;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -112,45 +110,6 @@
  */
 public class TelecomServiceImpl {
 
-    public interface SubscriptionManagerAdapter {
-        int getDefaultVoiceSubId();
-    }
-
-    static class SubscriptionManagerAdapterImpl implements SubscriptionManagerAdapter {
-        @Override
-        public int getDefaultVoiceSubId() {
-            return SubscriptionManager.getDefaultVoiceSubscriptionId();
-        }
-    }
-
-    public interface SettingsSecureAdapter {
-        void putStringForUser(ContentResolver resolver, String name, String value, int userHandle);
-
-        String getStringForUser(ContentResolver resolver, String name, int userHandle);
-    }
-
-    static class SettingsSecureAdapterImpl implements SettingsSecureAdapter {
-        @Override
-        public void putStringForUser(ContentResolver resolver, String name, String value,
-            int userHandle) {
-            Settings.Secure.putStringForUser(resolver, name, value, userHandle);
-        }
-
-        @Override
-        public String getStringForUser(ContentResolver resolver, String name, int userHandle) {
-            return Settings.Secure.getStringForUser(resolver, name, userHandle);
-        }
-    }
-
-    private static final String TAG = "TelecomServiceImpl";
-    private static final String TIME_LINE_ARG = "timeline";
-    private static final int DEFAULT_VIDEO_STATE = -1;
-    private static final String PERMISSION_HANDLE_CALL_INTENT =
-            "android.permission.HANDLE_CALL_INTENT";
-    private static final String ADD_CALL_ERR_MSG = "Call could not be created or found. "
-            + "Retry operation.";
-    private AnomalyReporterAdapter mAnomalyReporter = new AnomalyReporterAdapterImpl();
-
     /**
      * Anomaly Report UUIDs and corresponding error descriptions specific to TelecomServiceImpl.
      */
@@ -182,17 +141,38 @@
             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.";
-
-    @VisibleForTesting
-    public void setAnomalyReporterAdapter(AnomalyReporterAdapter mAnomalyReporterAdapter){
-        mAnomalyReporter = mAnomalyReporterAdapter;
-    }
-
+    private static final String TAG = "TelecomServiceImpl";
+    private static final String TIME_LINE_ARG = "timeline";
+    private static final int DEFAULT_VIDEO_STATE = -1;
+    private static final String PERMISSION_HANDLE_CALL_INTENT =
+            "android.permission.HANDLE_CALL_INTENT";
+    private static final String ADD_CALL_ERR_MSG = "Call could not be created or found. "
+            + "Retry operation.";
+    private final PhoneAccountRegistrar mPhoneAccountRegistrar;
+    private final CallIntentProcessor.Adapter mCallIntentProcessorAdapter;
+    private final UserCallIntentProcessorFactory mUserCallIntentProcessorFactory;
+    private final DefaultDialerCache mDefaultDialerCache;
+    private final SubscriptionManagerAdapter mSubscriptionManagerAdapter;
+    private final SettingsSecureAdapter mSettingsSecureAdapter;
+    private final TelecomSystem.SyncRoot mLock;
+    private final TransactionalServiceRepository mTransactionalServiceRepository;
+    private final BlockedNumbersManager mBlockedNumbersManager;
+    private final FeatureFlags mFeatureFlags;
+    private final com.android.internal.telephony.flags.FeatureFlags mTelephonyFeatureFlags;
+    private final TelecomMetricsController mMetricsController;
+    private AnomalyReporterAdapter mAnomalyReporter = new AnomalyReporterAdapterImpl();
+    private final Context mContext;
+    private final AppOpsManager mAppOpsManager;
+    private final PackageManager mPackageManager;
+    private final CallsManager mCallsManager;
+    private TransactionManager mTransactionManager;
     private final ITelecomService.Stub mBinderImpl = new ITelecomService.Stub() {
 
         @Override
         public void addCall(CallAttributes callAttributes, ICallEventCallback callEventCallback,
                 String callId, String callingPackage) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_ADDCALL,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             try {
                 Log.startSession("TSI.aC", Log.getPackageAbbreviation(callingPackage));
                 Log.i(TAG, "addCall: id=[%s], attributes=[%s]", callId, callAttributes);
@@ -205,12 +185,14 @@
                 enforcePhoneAccountIsRegisteredEnabled(handle, handle.getUserHandle());
                 enforceCallingPackage(callingPackage, "addCall");
 
+                event.setResult(ApiStats.RESULT_EXCEPTION);
+
                 // 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());
 
-                VoipCallTransaction transaction = null;
+                CallTransaction transaction = null;
                 // create transaction based on the call direction
                 switch (callAttributes.getDirection()) {
                     case DIRECTION_OUTGOING:
@@ -230,7 +212,7 @@
 
                 mTransactionManager.addTransaction(transaction, new OutcomeReceiver<>() {
                     @Override
-                    public void onResult(VoipCallTransactionResult result) {
+                    public void onResult(CallTransactionResult result) {
                         Log.d(TAG, "addCall: onResult");
                         Call call = result.getCall();
 
@@ -268,7 +250,9 @@
                         onAddCallControl(callId, callEventCallback, null, exception);
                     }
                 });
+                event.setResult(ApiStats.RESULT_NORMAL);
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
@@ -292,12 +276,17 @@
         @Override
         public PhoneAccountHandle getDefaultOutgoingPhoneAccount(String uriScheme,
                 String callingPackage, String callingFeatureId) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(
+                    ApiStats.API_GETDEFAULTOUTGOINGPHONEACCOUNT,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             try {
                 Log.startSession("TSI.gDOPA", Log.getPackageAbbreviation(callingPackage));
                 synchronized (mLock) {
                     PhoneAccountHandle phoneAccountHandle = null;
                     final UserHandle callingUserHandle = Binder.getCallingUserHandle();
                     long token = Binder.clearCallingIdentity();
+
+                    event.setResult(ApiStats.RESULT_EXCEPTION);
                     try {
                         phoneAccountHandle = mPhoneAccountRegistrar
                                 .getOutgoingPhoneAccountForScheme(uriScheme, callingUserHandle);
@@ -307,6 +296,8 @@
                     } finally {
                         Binder.restoreCallingIdentity(token);
                     }
+
+                    event.setResult(ApiStats.RESULT_NORMAL);
                     if (isCallerSimCallManager(phoneAccountHandle)
                             || canReadPhoneState(
                             callingPackage,
@@ -317,12 +308,16 @@
                     return null;
                 }
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
 
         @Override
         public PhoneAccountHandle getUserSelectedOutgoingPhoneAccount(String callingPackage) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(
+                    ApiStats.API_GETUSERSELECTEDOUTGOINGPHONEACCOUNT,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             synchronized (mLock) {
                 try {
                     Log.startSession("TSI.gUSOPA", Log.getPackageAbbreviation(callingPackage));
@@ -330,6 +325,7 @@
                         throw new SecurityException("Only the default dialer, or caller with "
                                 + "READ_PRIVILEGED_PHONE_STATE can call this method.");
                     }
+                    event.setResult(ApiStats.RESULT_NORMAL);
                     final UserHandle callingUserHandle = Binder.getCallingUserHandle();
                     return mPhoneAccountRegistrar.getUserSelectedOutgoingPhoneAccount(
                             callingUserHandle);
@@ -337,6 +333,7 @@
                     Log.e(this, e, "getUserSelectedOutgoingPhoneAccount");
                     throw e;
                 } finally {
+                    logEvent(event);
                     Log.endSession();
                 }
             }
@@ -344,6 +341,9 @@
 
         @Override
         public void setUserSelectedOutgoingPhoneAccount(PhoneAccountHandle accountHandle) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(
+                    ApiStats.API_SETUSERSELECTEDOUTGOINGPHONEACCOUNT,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             try {
                 Log.startSession("TSI.sUSOPA");
                 synchronized (mLock) {
@@ -353,6 +353,7 @@
                     try {
                         mPhoneAccountRegistrar.setUserSelectedOutgoingPhoneAccount(
                                 accountHandle, callingUserHandle);
+                        event.setResult(ApiStats.RESULT_NORMAL);
                     } catch (Exception e) {
                         Log.e(this, e, "setUserSelectedOutgoingPhoneAccount");
                         mAnomalyReporter.reportAnomaly(SET_USER_PHONE_ACCOUNT_ERROR_UUID,
@@ -363,6 +364,7 @@
                     }
                 }
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
@@ -371,6 +373,9 @@
         public ParceledListSlice<PhoneAccountHandle> getCallCapablePhoneAccounts(
                 boolean includeDisabledAccounts, String callingPackage,
                 String callingFeatureId, boolean acrossProfiles) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(
+                    ApiStats.API_GETCALLCAPABLEPHONEACCOUNTS,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             try {
                 Log.startSession("TSI.gCCPA", Log.getPackageAbbreviation(callingPackage));
 
@@ -400,13 +405,13 @@
                         "getCallCapablePhoneAccounts")) {
                     return ParceledListSlice.emptyList();
                 }
+                event.setResult(ApiStats.RESULT_NORMAL);
                 synchronized (mLock) {
                     final UserHandle callingUserHandle = Binder.getCallingUserHandle();
-                    boolean crossUserAccess = mTelephonyFeatureFlags.workProfileApiSplit()
-                            && !acrossProfiles ? false
-                            : (mTelephonyFeatureFlags.workProfileApiSplit()
-                                    ? hasInAppCrossProfilePermission()
-                                    : hasInAppCrossUserPermission());
+                    boolean crossUserAccess = (!mTelephonyFeatureFlags.workProfileApiSplit()
+                            || acrossProfiles) && (mTelephonyFeatureFlags.workProfileApiSplit()
+                            ? hasInAppCrossProfilePermission()
+                            : hasInAppCrossUserPermission());
                     long token = Binder.clearCallingIdentity();
                     try {
                         return new ParceledListSlice<>(
@@ -414,6 +419,7 @@
                                         includeDisabledAccounts, callingUserHandle,
                                         crossUserAccess));
                     } catch (Exception e) {
+                        event.setResult(ApiStats.RESULT_EXCEPTION);
                         Log.e(this, e, "getCallCapablePhoneAccounts");
                         mAnomalyReporter.reportAnomaly(GET_CALL_CAPABLE_ACCOUNTS_ERROR_UUID,
                                 GET_CALL_CAPABLE_ACCOUNTS_ERROR_MSG);
@@ -423,6 +429,7 @@
                     }
                 }
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
@@ -430,6 +437,9 @@
         @Override
         public ParceledListSlice<PhoneAccountHandle> getSelfManagedPhoneAccounts(
                 String callingPackage, String callingFeatureId) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(
+                    ApiStats.API_GETSELFMANAGEDPHONEACCOUNTS,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             try {
                 Log.startSession("TSI.gSMPA", Log.getPackageAbbreviation(callingPackage));
                 if (!canReadPhoneState(callingPackage, callingFeatureId,
@@ -439,10 +449,12 @@
                 synchronized (mLock) {
                     final UserHandle callingUserHandle = Binder.getCallingUserHandle();
                     long token = Binder.clearCallingIdentity();
+                    event.setResult(ApiStats.RESULT_NORMAL);
                     try {
                         return new ParceledListSlice<>(mPhoneAccountRegistrar
                                 .getSelfManagedPhoneAccounts(callingUserHandle));
                     } catch (Exception e) {
+                        event.setResult(ApiStats.RESULT_EXCEPTION);
                         Log.e(this, e, "getSelfManagedPhoneAccounts");
                         throw e;
                     } finally {
@@ -450,6 +462,7 @@
                     }
                 }
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
@@ -457,6 +470,9 @@
         @Override
         public ParceledListSlice<PhoneAccountHandle> getOwnSelfManagedPhoneAccounts(
                 String callingPackage, String callingFeatureId) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(
+                    ApiStats.API_GETOWNSELFMANAGEDPHONEACCOUNTS,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             try {
                 Log.startSession("TSI.gOSMPA", Log.getPackageAbbreviation(callingPackage));
                 try {
@@ -472,11 +488,13 @@
                 synchronized (mLock) {
                     final UserHandle callingUserHandle = Binder.getCallingUserHandle();
                     long token = Binder.clearCallingIdentity();
+                    event.setResult(ApiStats.RESULT_NORMAL);
                     try {
                         return new ParceledListSlice<>(mPhoneAccountRegistrar
                                 .getSelfManagedPhoneAccountsForPackage(callingPackage,
                                         callingUserHandle));
                     } catch (Exception e) {
+                        event.setResult(ApiStats.RESULT_EXCEPTION);
                         Log.e(this, e,
                                 "getSelfManagedPhoneAccountsForPackage");
                         throw e;
@@ -485,6 +503,7 @@
                     }
                 }
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
@@ -492,6 +511,9 @@
         @Override
         public ParceledListSlice<PhoneAccountHandle> getPhoneAccountsSupportingScheme(
                 String uriScheme, String callingPackage) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(
+                    ApiStats.API_GETPHONEACCOUNTSSUPPORTINGSCHEME,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             try {
                 Log.startSession("TSI.gPASS", Log.getPackageAbbreviation(callingPackage));
                 try {
@@ -506,11 +528,13 @@
                 synchronized (mLock) {
                     final UserHandle callingUserHandle = Binder.getCallingUserHandle();
                     long token = Binder.clearCallingIdentity();
+                    event.setResult(ApiStats.RESULT_NORMAL);
                     try {
                         return new ParceledListSlice<>(mPhoneAccountRegistrar
-                            .getCallCapablePhoneAccounts(uriScheme, false,
-                                    callingUserHandle, false));
+                                .getCallCapablePhoneAccounts(uriScheme, false,
+                                        callingUserHandle, false));
                     } catch (Exception e) {
+                        event.setResult(ApiStats.RESULT_EXCEPTION);
                         Log.e(this, e, "getPhoneAccountsSupportingScheme %s", uriScheme);
                         throw e;
                     } finally {
@@ -518,6 +542,7 @@
                     }
                 }
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
@@ -526,42 +551,53 @@
         public ParceledListSlice<PhoneAccountHandle> getPhoneAccountsForPackage(
                 String packageName) {
             //TODO: Deprecate this in S
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_GETPHONEACCOUNTSFORPACKAGE,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             try {
-                enforceCallingPackage(packageName, "getPhoneAccountsForPackage");
-            } catch (SecurityException se1) {
-                EventLog.writeEvent(0x534e4554, "153995334", Binder.getCallingUid(),
-                        "getPhoneAccountsForPackage: invalid calling package");
-                throw se1;
-            }
-
-            try {
-                enforcePermission(READ_PRIVILEGED_PHONE_STATE);
-            } catch (SecurityException se2) {
-                EventLog.writeEvent(0x534e4554, "153995334", Binder.getCallingUid(),
-                        "getPhoneAccountsForPackage: no permission");
-                throw se2;
-            }
-
-            synchronized (mLock) {
-                final UserHandle callingUserHandle = Binder.getCallingUserHandle();
-                long token = Binder.clearCallingIdentity();
                 try {
-                    Log.startSession("TSI.gPAFP");
-                    return new ParceledListSlice<>(mPhoneAccountRegistrar
-                            .getAllPhoneAccountHandlesForPackage(callingUserHandle, packageName));
-                } catch (Exception e) {
-                    Log.e(this, e, "getPhoneAccountsForPackage %s", packageName);
-                    throw e;
-                } finally {
-                    Binder.restoreCallingIdentity(token);
-                    Log.endSession();
+                    enforceCallingPackage(packageName, "getPhoneAccountsForPackage");
+                } catch (SecurityException se1) {
+                    EventLog.writeEvent(0x534e4554, "153995334", Binder.getCallingUid(),
+                            "getPhoneAccountsForPackage: invalid calling package");
+                    throw se1;
                 }
+
+                try {
+                    enforcePermission(READ_PRIVILEGED_PHONE_STATE);
+                } catch (SecurityException se2) {
+                    EventLog.writeEvent(0x534e4554, "153995334", Binder.getCallingUid(),
+                            "getPhoneAccountsForPackage: no permission");
+                    throw se2;
+                }
+
+                synchronized (mLock) {
+                    final UserHandle callingUserHandle = Binder.getCallingUserHandle();
+                    long token = Binder.clearCallingIdentity();
+                    event.setResult(ApiStats.RESULT_NORMAL);
+                    try {
+                        Log.startSession("TSI.gPAFP");
+                        return new ParceledListSlice<>(mPhoneAccountRegistrar
+                                .getAllPhoneAccountHandlesForPackage(
+                                        callingUserHandle, packageName));
+                    } catch (Exception e) {
+                        event.setResult(ApiStats.RESULT_EXCEPTION);
+                        Log.e(this, e, "getPhoneAccountsForPackage %s", packageName);
+                        throw e;
+                    } finally {
+                        Binder.restoreCallingIdentity(token);
+                        Log.endSession();
+                    }
+                }
+            } finally {
+                logEvent(event);
             }
         }
 
         @Override
         public PhoneAccount getPhoneAccount(PhoneAccountHandle accountHandle,
                 String callingPackage) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_GETPHONEACCOUNT,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             try {
                 Log.startSession("TSI.gPA", Log.getPackageAbbreviation(callingPackage));
                 try {
@@ -588,6 +624,7 @@
                     Set<String> permissions = computePermissionsForBoundPackage(
                             Set.of(MODIFY_PHONE_STATE), null);
                     long token = Binder.clearCallingIdentity();
+                    event.setResult(ApiStats.RESULT_NORMAL);
                     try {
                         // In ideal case, we should not resolve the handle across profiles. But
                         // given the fact that profile's call is handled by its parent user's
@@ -598,6 +635,7 @@
                                         /* acrossProfiles */ true);
                         return maybeCleansePhoneAccount(account, permissions);
                     } catch (Exception e) {
+                        event.setResult(ApiStats.RESULT_EXCEPTION);
                         Log.e(this, e, "getPhoneAccount %s", accountHandle);
                         mAnomalyReporter.reportAnomaly(GET_PHONE_ACCOUNT_ERROR_UUID,
                                 GET_PHONE_ACCOUNT_ERROR_MSG);
@@ -607,6 +645,7 @@
                     }
                 }
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
@@ -614,6 +653,8 @@
         @Override
         public ParceledListSlice<PhoneAccount> getRegisteredPhoneAccounts(String callingPackage,
                 String callingFeatureId) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_GETREGISTEREDPHONEACCOUNTS,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             try {
                 Log.startSession("TSI.gRPA", Log.getPackageAbbreviation(callingPackage));
                 try {
@@ -635,6 +676,7 @@
                 synchronized (mLock) {
                     final UserHandle callingUserHandle = Binder.getCallingUserHandle();
                     long token = Binder.clearCallingIdentity();
+                    event.setResult(ApiStats.RESULT_NORMAL);
                     try {
                         return new ParceledListSlice<>(
                                 mPhoneAccountRegistrar.getPhoneAccounts(
@@ -647,6 +689,7 @@
                                         hasCrossUserAccess /* crossUserAccess */,
                                         false /* includeAll */));
                     } catch (Exception e) {
+                        event.setResult(ApiStats.RESULT_EXCEPTION);
                         Log.e(this, e, "getRegisteredPhoneAccounts");
                         throw e;
                     } finally {
@@ -654,14 +697,18 @@
                     }
                 }
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
 
         @Override
         public int getAllPhoneAccountsCount() {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_GETALLPHONEACCOUNTSCOUNT,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             try {
                 Log.startSession("TSI.gAPAC");
+                event.setCallerUid(Binder.getCallingUid());
                 try {
                     enforceModifyPermission(
                             "getAllPhoneAccountsCount requires MODIFY_PHONE_STATE permission.");
@@ -672,22 +719,27 @@
                 }
 
                 synchronized (mLock) {
+                    event.setResult(ApiStats.RESULT_NORMAL);
                     try {
                         // This list is pre-filtered for the calling user.
                         return getAllPhoneAccounts().getList().size();
                     } catch (Exception e) {
+                        event.setResult(ApiStats.RESULT_EXCEPTION);
                         Log.e(this, e, "getAllPhoneAccountsCount");
                         throw e;
 
                     }
                 }
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
 
         @Override
         public ParceledListSlice<PhoneAccount> getAllPhoneAccounts() {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_GETALLPHONEACCOUNTS,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             synchronized (mLock) {
                 try {
                     Log.startSession("TSI.gAPA");
@@ -702,16 +754,19 @@
 
                     final UserHandle callingUserHandle = Binder.getCallingUserHandle();
                     long token = Binder.clearCallingIdentity();
+                    event.setResult(ApiStats.RESULT_NORMAL);
                     try {
                         return new ParceledListSlice<>(mPhoneAccountRegistrar
                                 .getAllPhoneAccounts(callingUserHandle, false));
                     } catch (Exception e) {
+                        event.setResult(ApiStats.RESULT_EXCEPTION);
                         Log.e(this, e, "getAllPhoneAccounts");
                         throw e;
                     } finally {
                         Binder.restoreCallingIdentity(token);
                     }
                 } finally {
+                    logEvent(event);
                     Log.endSession();
                 }
             }
@@ -719,6 +774,8 @@
 
         @Override
         public ParceledListSlice<PhoneAccountHandle> getAllPhoneAccountHandles() {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_GETALLPHONEACCOUNTHANDLES,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             try {
                 Log.startSession("TSI.gAPAH");
 
@@ -735,11 +792,13 @@
                     final UserHandle callingUserHandle = Binder.getCallingUserHandle();
                     boolean crossUserAccess = hasInAppCrossUserPermission();
                     long token = Binder.clearCallingIdentity();
+                    event.setResult(ApiStats.RESULT_NORMAL);
                     try {
                         return new ParceledListSlice<>(mPhoneAccountRegistrar
                                 .getAllPhoneAccountHandles(callingUserHandle,
                                         crossUserAccess));
                     } catch (Exception e) {
+                        event.setResult(ApiStats.RESULT_EXCEPTION);
                         Log.e(this, e, "getAllPhoneAccountsHandles");
                         throw e;
                     } finally {
@@ -747,12 +806,15 @@
                     }
                 }
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
 
         @Override
         public PhoneAccountHandle getSimCallManager(int subId, String callingPackage) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_GETSIMCALLMANAGER,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             synchronized (mLock) {
                 try {
                     Log.startSession("TSI.gSCM", Log.getPackageAbbreviation(callingPackage));
@@ -763,6 +825,7 @@
                         if (user != ActivityManager.getCurrentUser()) {
                             enforceCrossUserPermission(callingUid);
                         }
+                        event.setResult(ApiStats.RESULT_NORMAL);
                         return mPhoneAccountRegistrar.getSimCallManager(subId, UserHandle.of(user));
                     } finally {
                         Binder.restoreCallingIdentity(token);
@@ -773,6 +836,7 @@
                             GET_SIM_MANAGER_ERROR_MSG);
                     throw e;
                 } finally {
+                    logEvent(event);
                     Log.endSession();
                 }
             }
@@ -780,6 +844,8 @@
 
         @Override
         public PhoneAccountHandle getSimCallManagerForUser(int user, String callingPackage) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_GETSIMCALLMANAGERFORUSER,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             synchronized (mLock) {
                 try {
                     Log.startSession("TSI.gSCMFU", Log.getPackageAbbreviation(callingPackage));
@@ -788,6 +854,7 @@
                         enforceCrossUserPermission(callingUid);
                     }
                     long token = Binder.clearCallingIdentity();
+                    event.setResult(ApiStats.RESULT_NORMAL);
                     try {
                         return mPhoneAccountRegistrar.getSimCallManager(UserHandle.of(user));
                     } finally {
@@ -799,6 +866,7 @@
                             GET_SIM_MANAGER_FOR_USER_ERROR_MSG);
                     throw e;
                 } finally {
+                    logEvent(event);
                     Log.endSession();
                 }
             }
@@ -806,22 +874,27 @@
 
         @Override
         public void registerPhoneAccount(PhoneAccount account, String callingPackage) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_REGISTERPHONEACCOUNT,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             try {
                 Log.startSession("TSI.rPA", Log.getPackageAbbreviation(callingPackage));
                 synchronized (mLock) {
                     try {
                         enforcePhoneAccountModificationForPackage(
                                 account.getAccountHandle().getComponentName().getPackageName());
-                        if (account.hasCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)) {
+                        if (account.hasCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)
+                                || (mFeatureFlags.enforceTransactionalExclusivity()
+                                && account.hasCapabilities(
+                                PhoneAccount.CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS))) {
                             enforceRegisterSelfManaged();
                             if (account.hasCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER) ||
                                     account.hasCapabilities(
                                             PhoneAccount.CAPABILITY_CONNECTION_MANAGER) ||
                                     account.hasCapabilities(
                                             PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) {
-                                throw new SecurityException("Self-managed ConnectionServices " +
-                                        "cannot also be call capable, connection managers, or " +
-                                        "SIM accounts.");
+                                throw new SecurityException("Self-managed ConnectionServices "
+                                        + "cannot also be call capable, connection managers, or "
+                                        + "SIM accounts.");
                             }
 
                             // For self-managed CS, the phone account registrar will override the
@@ -878,6 +951,7 @@
                         }
 
                         final long token = Binder.clearCallingIdentity();
+                        event.setResult(ApiStats.RESULT_NORMAL);
                         try {
                             Log.i(this, "registerPhoneAccount: account=%s",
                                     account);
@@ -893,6 +967,7 @@
                     }
                 }
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
@@ -900,6 +975,8 @@
         @Override
         public void unregisterPhoneAccount(PhoneAccountHandle accountHandle,
                 String callingPackage) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_UNREGISTERPHONEACCOUNT,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             synchronized (mLock) {
                 try {
                     Log.startSession("TSI.uPA", Log.getPackageAbbreviation(callingPackage));
@@ -907,6 +984,7 @@
                             accountHandle.getComponentName().getPackageName());
                     enforceUserHandleMatchesCaller(accountHandle);
                     final long token = Binder.clearCallingIdentity();
+                    event.setResult(ApiStats.RESULT_NORMAL);
                     try {
                         mPhoneAccountRegistrar.unregisterPhoneAccount(accountHandle);
                     } finally {
@@ -916,6 +994,7 @@
                     Log.e(this, e, "unregisterPhoneAccount %s", accountHandle);
                     throw e;
                 } finally {
+                    logEvent(event);
                     Log.endSession();
                 }
             }
@@ -923,16 +1002,20 @@
 
         @Override
         public void clearAccounts(String packageName) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_CLEARACCOUNTS,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             synchronized (mLock) {
                 try {
                     Log.startSession("TSI.cA");
                     enforcePhoneAccountModificationForPackage(packageName);
+                    event.setResult(ApiStats.RESULT_NORMAL);
                     mPhoneAccountRegistrar
                             .clearAccounts(packageName, Binder.getCallingUserHandle());
                 } catch (Exception e) {
                     Log.e(this, e, "clearAccounts %s", packageName);
                     throw e;
                 } finally {
+                    logEvent(event);
                     Log.endSession();
                 }
             }
@@ -944,6 +1027,8 @@
         @Override
         public boolean isVoiceMailNumber(PhoneAccountHandle accountHandle, String number,
                 String callingPackage, String callingFeatureId) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_ISVOICEMAILNUMBER,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             try {
                 Log.startSession("TSI.iVMN", Log.getPackageAbbreviation(callingPackage));
                 synchronized (mLock) {
@@ -957,9 +1042,11 @@
                         return false;
                     }
                     long token = Binder.clearCallingIdentity();
+                    event.setResult(ApiStats.RESULT_NORMAL);
                     try {
                         return mPhoneAccountRegistrar.isVoiceMailNumber(accountHandle, number);
                     } catch (Exception e) {
+                        event.setResult(ApiStats.RESULT_EXCEPTION);
                         Log.e(this, e, "getSubscriptionIdForPhoneAccount");
                         throw e;
                     } finally {
@@ -967,6 +1054,7 @@
                     }
                 }
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
@@ -977,6 +1065,8 @@
         @Override
         public String getVoiceMailNumber(PhoneAccountHandle accountHandle, String callingPackage,
                 String callingFeatureId) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_GETVOICEMAILNUMBER,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             try {
                 Log.startSession("TSI.gVMN", Log.getPackageAbbreviation(callingPackage));
                 if (!canReadPhoneState(callingPackage, callingFeatureId, "getVoiceMailNumber")) {
@@ -997,8 +1087,10 @@
                                     .getSubscriptionIdForPhoneAccount(accountHandle);
                         }
                     }
+                    event.setResult(ApiStats.RESULT_NORMAL);
                     return getTelephonyManager(subId).getVoiceMailNumber();
                 } catch (UnsupportedOperationException ignored) {
+                    event.setResult(ApiStats.RESULT_EXCEPTION);
                     Log.w(this, "getVoiceMailNumber: no Telephony");
                     return null;
                 } catch (Exception e) {
@@ -1006,6 +1098,7 @@
                     throw e;
                 }
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
@@ -1016,6 +1109,8 @@
         @Override
         public String getLine1Number(PhoneAccountHandle accountHandle, String callingPackage,
                 String callingFeatureId) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_GETLINE1NUMBER,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             try {
                 Log.startSession("getL1N", Log.getPackageAbbreviation(callingPackage));
                 if (!canReadPhoneNumbers(callingPackage, callingFeatureId, "getLine1Number")) {
@@ -1036,8 +1131,10 @@
                         subId = mPhoneAccountRegistrar.getSubscriptionIdForPhoneAccount(
                                 accountHandle);
                     }
+                    event.setResult(ApiStats.RESULT_NORMAL);
                     return getTelephonyManager(subId).getLine1Number();
                 } catch (UnsupportedOperationException ignored) {
+                    event.setResult(ApiStats.RESULT_EXCEPTION);
                     Log.w(this, "getLine1Number: no telephony");
                     return null;
                 } catch (Exception e) {
@@ -1047,6 +1144,7 @@
                     Binder.restoreCallingIdentity(token);
                 }
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
@@ -1056,6 +1154,8 @@
          */
         @Override
         public void silenceRinger(String callingPackage) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_SILENCERINGER,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             try {
                 Log.startSession("TSI.sR", Log.getPackageAbbreviation(callingPackage));
                 synchronized (mLock) {
@@ -1063,17 +1163,20 @@
                     UserHandle callingUserHandle = Binder.getCallingUserHandle();
                     boolean crossUserAccess = hasInAppCrossUserPermission();
                     long token = Binder.clearCallingIdentity();
+                    event.setResult(ApiStats.RESULT_EXCEPTION);
                     try {
                         Log.i(this, "Silence Ringer requested by %s", callingPackage);
                         Set<UserHandle> userHandles = mCallsManager.getCallAudioManager().
                                 silenceRingers(mContext, callingUserHandle,
                                         crossUserAccess);
+                        event.setResult(ApiStats.RESULT_NORMAL);
                         mCallsManager.getInCallController().silenceRinger(userHandles);
                     } finally {
                         Binder.restoreCallingIdentity(token);
                     }
                 }
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
@@ -1085,10 +1188,13 @@
          */
         @Override
         public ComponentName getDefaultPhoneApp() {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_GETDEFAULTPHONEAPP,
+                    Binder.getCallingUid(), ApiStats.RESULT_NORMAL);
             try {
                 Log.startSession("TSI.gDPA");
                 return mDefaultDialerCache.getDialtactsSystemDialerComponent();
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
@@ -1101,6 +1207,8 @@
          */
         @Override
         public String getDefaultDialerPackage(String callingPackage) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_GETDEFAULTDIALERPACKAGE,
+                    Binder.getCallingUid(), ApiStats.RESULT_NORMAL);
             try {
                 Log.startSession("TSI.gDDP", Log.getPackageAbbreviation(callingPackage));
                 int callerUserId = UserHandle.getCallingUserId();
@@ -1112,6 +1220,7 @@
                     Binder.restoreCallingIdentity(token);
                 }
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
@@ -1125,18 +1234,23 @@
          */
         @Override
         public String getDefaultDialerPackageForUser(int userId) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(
+                    ApiStats.API_GETDEFAULTDIALERPACKAGEFORUSER,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             try {
                 Log.startSession("TSI.gDDPU");
                 mContext.enforceCallingOrSelfPermission(READ_PRIVILEGED_PHONE_STATE,
                         "READ_PRIVILEGED_PHONE_STATE permission required.");
 
                 final long token = Binder.clearCallingIdentity();
+                event.setResult(ApiStats.RESULT_NORMAL);
                 try {
                     return mDefaultDialerCache.getDefaultDialerApplication(userId);
                 } finally {
                     Binder.restoreCallingIdentity(token);
                 }
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
@@ -1146,10 +1260,13 @@
          */
         @Override
         public String getSystemDialerPackage(String callingPackage) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_GETSYSTEMDIALERPACKAGE,
+                    Binder.getCallingUid(), ApiStats.RESULT_NORMAL);
             try {
                 Log.startSession("TSI.gSDP", Log.getPackageAbbreviation(callingPackage));
                 return mDefaultDialerCache.getSystemDialerApplication();
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
@@ -1177,17 +1294,20 @@
          */
         @Override
         public boolean isInCall(String callingPackage, String callingFeatureId) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_ISINCALL,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             try {
                 Log.startSession("TSI.iIC", Log.getPackageAbbreviation(callingPackage));
                 if (!canReadPhoneState(callingPackage, callingFeatureId, "isInCall")) {
                     return false;
                 }
-
+                event.setResult(ApiStats.RESULT_NORMAL);
                 synchronized (mLock) {
                     return mCallsManager.hasOngoingCalls(Binder.getCallingUserHandle(),
                             hasInAppCrossUserPermission());
                 }
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
@@ -1197,9 +1317,13 @@
          */
         @Override
         public boolean hasManageOngoingCallsPermission(String callingPackage) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(
+                    ApiStats.API_HASMANAGEONGOINGCALLSPERMISSION,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             try {
                 Log.startSession("TSI.hMOCP", Log.getPackageAbbreviation(callingPackage));
                 enforceCallingPackage(callingPackage, "hasManageOngoingCallsPermission");
+                event.setResult(ApiStats.RESULT_NORMAL);
                 return PermissionChecker.checkPermissionForDataDeliveryFromDataSource(
                         mContext, Manifest.permission.MANAGE_ONGOING_CALLS,
                         Binder.getCallingPid(),
@@ -1209,6 +1333,7 @@
                         "Checking whether the caller has MANAGE_ONGOING_CALLS permission")
                         == PermissionChecker.PERMISSION_GRANTED;
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
@@ -1218,18 +1343,21 @@
          */
         @Override
         public boolean isInManagedCall(String callingPackage, String callingFeatureId) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_ISINMANAGEDCALL,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             try {
                 Log.startSession("TSI.iIMC", Log.getPackageAbbreviation(callingPackage));
                 if (!canReadPhoneState(callingPackage, callingFeatureId, "isInManagedCall")) {
                     throw new SecurityException("Only the default dialer or caller with " +
                             "READ_PHONE_STATE permission can use this method.");
                 }
-
+                event.setResult(ApiStats.RESULT_NORMAL);
                 synchronized (mLock) {
                     return mCallsManager.hasOngoingManagedCalls(Binder.getCallingUserHandle(),
                             hasInAppCrossUserPermission());
                 }
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
@@ -1239,6 +1367,8 @@
          */
         @Override
         public boolean isRinging(String callingPackage) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_ISRINGING,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             try {
                 Log.startSession("TSI.iR");
                 if (!isPrivilegedDialerCalling(callingPackage)) {
@@ -1251,6 +1381,7 @@
                     }
                 }
 
+                event.setResult(ApiStats.RESULT_NORMAL);
                 synchronized (mLock) {
                     // Note: We are explicitly checking the calls telecom is tracking rather than
                     // relying on mCallsManager#getCallState(). Since getCallState() relies on the
@@ -1260,6 +1391,7 @@
                     return mCallsManager.hasRingingOrSimulatedRingingCall();
                 }
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
@@ -1272,6 +1404,8 @@
         @Deprecated
         @Override
         public int getCallState() {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_GETCALLSTATE,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             try {
                 Log.startSession("TSI.getCallState(DEPRECATED)");
                 if (CompatChanges.isChangeEnabled(
@@ -1282,6 +1416,7 @@
                     throw new SecurityException("This method can only be used for applications "
                             + "targeting API version 30 or less.");
                 }
+                event.setResult(ApiStats.RESULT_NORMAL);
                 synchronized (mLock) {
                     return mCallsManager.getCallState();
                 }
@@ -1295,6 +1430,8 @@
          */
         @Override
         public int getCallStateUsingPackage(String callingPackage, String callingFeatureId) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_GETCALLSTATEUSINGPACKAGE,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             try {
                 Log.startSession("TSI.getCallStateUsingPackage");
 
@@ -1323,10 +1460,12 @@
                                 + " for API version 31+");
                     }
                 }
+                event.setResult(ApiStats.RESULT_NORMAL);
                 synchronized (mLock) {
                     return mCallsManager.getCallState();
                 }
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
@@ -1349,6 +1488,8 @@
          */
         @Override
         public boolean endCall(String callingPackage) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_ENDCALL,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             try {
                 Log.startSession("TSI.eC", Log.getPackageAbbreviation(callingPackage));
                 synchronized (mLock) {
@@ -1357,6 +1498,7 @@
                     }
 
                     long token = Binder.clearCallingIdentity();
+                    event.setResult(ApiStats.RESULT_NORMAL);
                     try {
                         return endCallInternal(callingPackage);
                     } finally {
@@ -1364,6 +1506,7 @@
                     }
                 }
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
@@ -1373,12 +1516,15 @@
          */
         @Override
         public void acceptRingingCall(String packageName) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_ACCEPTRINGINGCALL,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             try {
                 Log.startSession("TSI.aRC", Log.getPackageAbbreviation(packageName));
                 synchronized (mLock) {
                     if (!enforceAnswerCallPermission(packageName, Binder.getCallingUid())) return;
 
                     long token = Binder.clearCallingIdentity();
+                    event.setResult(ApiStats.RESULT_NORMAL);
                     try {
                         acceptRingingCallInternal(DEFAULT_VIDEO_STATE, packageName);
                     } finally {
@@ -1386,6 +1532,7 @@
                     }
                 }
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
@@ -1395,12 +1542,16 @@
          */
         @Override
         public void acceptRingingCallWithVideoState(String packageName, int videoState) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(
+                    ApiStats.API_ACCEPTRINGINGCALLWITHVIDEOSTATE,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             try {
                 Log.startSession("TSI.aRCWVS", Log.getPackageAbbreviation(packageName));
                 synchronized (mLock) {
                     if (!enforceAnswerCallPermission(packageName, Binder.getCallingUid())) return;
 
                     long token = Binder.clearCallingIdentity();
+                    event.setResult(ApiStats.RESULT_NORMAL);
                     try {
                         acceptRingingCallInternal(videoState, packageName);
                     } finally {
@@ -1408,6 +1559,7 @@
                     }
                 }
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
@@ -1418,6 +1570,8 @@
         @Override
         public void showInCallScreen(boolean showDialpad, String callingPackage,
                 String callingFeatureId) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_SHOWINCALLSCREEN,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             try {
                 Log.startSession("TSI.sICS", Log.getPackageAbbreviation(callingPackage));
                 if (!canReadPhoneState(callingPackage, callingFeatureId, "showInCallScreen")) {
@@ -1425,16 +1579,18 @@
                 }
 
                 synchronized (mLock) {
-
                     UserHandle callingUser = Binder.getCallingUserHandle();
                     long token = Binder.clearCallingIdentity();
+                    event.setResult(ApiStats.RESULT_NORMAL);
                     try {
-                        mCallsManager.getInCallController().bringToForeground(showDialpad, callingUser);
+                        mCallsManager.getInCallController().bringToForeground(
+                                showDialpad, callingUser);
                     } finally {
                         Binder.restoreCallingIdentity(token);
                     }
                 }
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
@@ -1444,12 +1600,16 @@
          */
         @Override
         public void cancelMissedCallsNotification(String callingPackage) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(
+                    ApiStats.API_CANCELMISSEDCALLSNOTIFICATION,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             try {
                 Log.startSession("TSI.cMCN", Log.getPackageAbbreviation(callingPackage));
                 synchronized (mLock) {
                     enforcePermissionOrPrivilegedDialer(MODIFY_PHONE_STATE, callingPackage);
                     UserHandle userHandle = Binder.getCallingUserHandle();
                     long token = Binder.clearCallingIdentity();
+                    event.setResult(ApiStats.RESULT_NORMAL);
                     try {
                         mCallsManager.getMissedCallNotifier().clearMissedCalls(userHandle);
                     } finally {
@@ -1457,6 +1617,7 @@
                     }
                 }
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
@@ -1466,6 +1627,8 @@
          */
         @Override
         public boolean handlePinMmi(String dialString, String callingPackage) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_HANDLEPINMMI,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             try {
                 Log.startSession("TSI.hPM", Log.getPackageAbbreviation(callingPackage));
                 enforcePermissionOrPrivilegedDialer(MODIFY_PHONE_STATE, callingPackage);
@@ -1473,6 +1636,7 @@
                 // Switch identity so that TelephonyManager checks Telecom's permissions
                 // instead.
                 long token = Binder.clearCallingIdentity();
+                event.setResult(ApiStats.RESULT_NORMAL);
                 boolean retval = false;
                 try {
                     retval = getTelephonyManager(
@@ -1484,6 +1648,7 @@
 
                 return retval;
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
@@ -1494,9 +1659,11 @@
         @Override
         public boolean handlePinMmiForPhoneAccount(PhoneAccountHandle accountHandle,
                 String dialString, String callingPackage) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(
+                    ApiStats.API_HANDLEPINMMIFORPHONEACCOUNT,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             try {
                 Log.startSession("TSI.hPMFPA", Log.getPackageAbbreviation(callingPackage));
-
                 enforcePermissionOrPrivilegedDialer(MODIFY_PHONE_STATE, callingPackage);
                 UserHandle callingUserHandle = Binder.getCallingUserHandle();
                 synchronized (mLock) {
@@ -1511,6 +1678,7 @@
                 // Switch identity so that TelephonyManager checks Telecom's permissions
                 // instead.
                 long token = Binder.clearCallingIdentity();
+                event.setResult(ApiStats.RESULT_NORMAL);
                 boolean retval = false;
                 int subId;
                 try {
@@ -1522,6 +1690,7 @@
                         retval = getTelephonyManager(subId)
                                 .handlePinMmiForSubscriber(subId, dialString);
                     } catch (UnsupportedOperationException uoe) {
+                        event.setResult(ApiStats.RESULT_EXCEPTION);
                         Log.w(this, "handlePinMmiForPhoneAccount: no telephony");
                         retval = false;
                     }
@@ -1530,6 +1699,7 @@
                 }
                 return retval;
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
@@ -1540,6 +1710,8 @@
         @Override
         public Uri getAdnUriForPhoneAccount(PhoneAccountHandle accountHandle,
                 String callingPackage) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_GETADNURIFORPHONEACCOUNT,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             try {
                 Log.startSession("TSI.aAUFPA", Log.getPackageAbbreviation(callingPackage));
                 enforcePermissionOrPrivilegedDialer(MODIFY_PHONE_STATE, callingPackage);
@@ -1554,6 +1726,7 @@
                 // Switch identity so that TelephonyManager checks Telecom's permissions
                 // instead.
                 long token = Binder.clearCallingIdentity();
+                event.setResult(ApiStats.RESULT_NORMAL);
                 String retval = "content://icc/adn/";
                 try {
                     long subId = mPhoneAccountRegistrar
@@ -1565,6 +1738,7 @@
 
                 return Uri.parse(retval);
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
@@ -1574,6 +1748,8 @@
          */
         @Override
         public boolean isTtySupported(String callingPackage, String callingFeatureId) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_ISTTYSUPPORTED,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             try {
                 Log.startSession("TSI.iTS", Log.getPackageAbbreviation(callingPackage));
                 if (!canReadPhoneState(callingPackage, callingFeatureId, "isTtySupported")) {
@@ -1581,10 +1757,12 @@
                             "READ_PRIVILEGED_PHONE_STATE or READ_PHONE_STATE can call this api");
                 }
 
+                event.setResult(ApiStats.RESULT_NORMAL);
                 synchronized (mLock) {
                     return mCallsManager.isTtySupported();
                 }
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
@@ -1594,16 +1772,20 @@
          */
         @Override
         public int getCurrentTtyMode(String callingPackage, String callingFeatureId) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_GETCURRENTTTYMODE,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             try {
                 Log.startSession("TSI.gCTM", Log.getPackageAbbreviation(callingPackage));
                 if (!canReadPhoneState(callingPackage, callingFeatureId, "getCurrentTtyMode")) {
                     return TelecomManager.TTY_MODE_OFF;
                 }
 
+                event.setResult(ApiStats.RESULT_NORMAL);
                 synchronized (mLock) {
                     return mCallsManager.getCurrentTtyMode();
                 }
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
@@ -1614,6 +1796,8 @@
         @Override
         public void addNewIncomingCall(PhoneAccountHandle phoneAccountHandle, Bundle extras,
                 String callingPackage) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_ADDNEWINCOMINGCALL,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             try {
                 Log.startSession("TSI.aNIC", Log.getPackageAbbreviation(callingPackage));
                 synchronized (mLock) {
@@ -1647,6 +1831,7 @@
                             }
                         }
                         long token = Binder.clearCallingIdentity();
+                        event.setResult(ApiStats.RESULT_NORMAL);
                         try {
                             Intent intent = new Intent(TelecomManager.ACTION_INCOMING_CALL);
                             intent.putExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE,
@@ -1685,11 +1870,14 @@
                             Binder.restoreCallingIdentity(token);
                         }
                     } else {
+                        // Invalid parameters are considered as an exception
+                        event.setResult(ApiStats.RESULT_EXCEPTION);
                         Log.w(this, "Null phoneAccountHandle. Ignoring request to add new" +
                                 " incoming call");
                     }
                 }
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
@@ -1700,6 +1888,8 @@
         @Override
         public void addNewIncomingConference(PhoneAccountHandle phoneAccountHandle, Bundle extras,
                 String callingPackage) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_ADDNEWINCOMINGCONFERENCE,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             try {
                 Log.startSession("TSI.aNIC", Log.getPackageAbbreviation(callingPackage));
                 synchronized (mLock) {
@@ -1727,6 +1917,7 @@
                             }
                         }
                         long token = Binder.clearCallingIdentity();
+                        event.setResult(ApiStats.RESULT_NORMAL);
                         try {
                             mCallsManager.processIncomingConference(
                                     phoneAccountHandle, extras);
@@ -1734,22 +1925,26 @@
                             Binder.restoreCallingIdentity(token);
                         }
                     } else {
+                        // Invalid parameters are considered as an exception
+                        event.setResult(ApiStats.RESULT_EXCEPTION);
                         Log.w(this, "Null phoneAccountHandle. Ignoring request to add new" +
                                 " incoming conference");
                     }
                 }
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
 
-
         /**
          * @see android.telecom.TelecomManager#acceptHandover
          */
         @Override
         public void acceptHandover(Uri srcAddr, int videoState, PhoneAccountHandle destAcct,
                 String callingPackage) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_ACCEPTHANDOVER,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             try {
                 Log.startSession("TSI.aHO", Log.getPackageAbbreviation(callingPackage));
                 synchronized (mLock) {
@@ -1779,17 +1974,22 @@
                         }
 
                         long token = Binder.clearCallingIdentity();
+                        event.setResult(ApiStats.RESULT_EXCEPTION);
                         try {
                             mCallsManager.acceptHandover(srcAddr, videoState, destAcct);
+                            event.setResult(ApiStats.RESULT_NORMAL);
                         } finally {
                             Binder.restoreCallingIdentity(token);
                         }
                     } else {
+                        // Invalid parameters are considered as an exception
+                        event.setResult(ApiStats.RESULT_EXCEPTION);
                         Log.w(this, "Null phoneAccountHandle. Ignoring request " +
                                 "to handover the call");
                     }
                 }
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
@@ -1799,6 +1999,8 @@
          */
         @Override
         public void addNewUnknownCall(PhoneAccountHandle phoneAccountHandle, Bundle extras) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_ADDNEWUNKNOWNCALL,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             try {
                 Log.startSession("TSI.aNUC");
                 try {
@@ -1822,7 +2024,7 @@
                         enforcePhoneAccountIsRegisteredEnabled(phoneAccountHandle,
                                 Binder.getCallingUserHandle());
                         long token = Binder.clearCallingIdentity();
-
+                        event.setResult(ApiStats.RESULT_NORMAL);
                         try {
                             Intent intent = new Intent(TelecomManager.ACTION_NEW_UNKNOWN_CALL);
                             if (extras != null) {
@@ -1838,12 +2040,15 @@
                             Binder.restoreCallingIdentity(token);
                         }
                     } else {
+                        // Invalid parameters are considered as an exception
+                        event.setResult(ApiStats.RESULT_EXCEPTION);
                         Log.i(this,
                                 "Null phoneAccountHandle or not initiated by Telephony. " +
                                         "Ignoring request to add new unknown call.");
                     }
                 }
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
@@ -1854,6 +2059,8 @@
         @Override
         public void startConference(List<Uri> participants, Bundle extras,
                 String callingPackage) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_STARTCONFERENCE,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             try {
                 Log.startSession("TSI.sC", Log.getPackageAbbreviation(callingPackage));
                 if (!canCallPhone(callingPackage, "startConference")) {
@@ -1863,6 +2070,7 @@
                 // Binder is clearing the identity, so we need to keep the store the handle
                 UserHandle currentUserHandle = Binder.getCallingUserHandle();
                 long token = Binder.clearCallingIdentity();
+                event.setResult(ApiStats.RESULT_NORMAL);
                 try {
                     mCallsManager.startConference(participants, extras, callingPackage,
                             currentUserHandle);
@@ -1870,6 +2078,7 @@
                     Binder.restoreCallingIdentity(token);
                 }
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
@@ -1880,6 +2089,8 @@
         @Override
         public void placeCall(Uri handle, Bundle extras, String callingPackage,
                 String callingFeatureId) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_PLACECALL,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             try {
                 Log.startSession("TSI.pC", Log.getPackageAbbreviation(callingPackage));
                 enforceCallingPackage(callingPackage, "placeCall");
@@ -1955,6 +2166,7 @@
                 synchronized (mLock) {
                     final UserHandle userHandle = Binder.getCallingUserHandle();
                     long token = Binder.clearCallingIdentity();
+                    event.setResult(ApiStats.RESULT_NORMAL);
                     try {
                         final Intent intent = new Intent(hasCallPrivilegedPermission ?
                                 Intent.ACTION_CALL_PRIVILEGED : Intent.ACTION_CALL, handle);
@@ -1972,6 +2184,7 @@
                     }
                 }
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
@@ -1981,11 +2194,14 @@
          */
         @Override
         public boolean enablePhoneAccount(PhoneAccountHandle accountHandle, boolean isEnabled) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_ENABLEPHONEACCOUNT,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             try {
                 Log.startSession("TSI.ePA");
                 enforceModifyPermission();
                 synchronized (mLock) {
                     long token = Binder.clearCallingIdentity();
+                    event.setResult(ApiStats.RESULT_NORMAL);
                     try {
                         // enable/disable phone account
                         return mPhoneAccountRegistrar.enablePhoneAccount(accountHandle, isEnabled);
@@ -1994,12 +2210,15 @@
                     }
                 }
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
 
         @Override
         public boolean setDefaultDialer(String packageName) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_SETDEFAULTDIALER,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             try {
                 Log.startSession("TSI.sDD");
                 enforcePermission(MODIFY_PHONE_STATE);
@@ -2007,6 +2226,7 @@
                 synchronized (mLock) {
                     int callerUserId = UserHandle.getCallingUserId();
                     long token = Binder.clearCallingIdentity();
+                    event.setResult(ApiStats.RESULT_NORMAL);
                     try {
                         return mDefaultDialerCache.setDefaultDialer(packageName,
                                 callerUserId);
@@ -2015,6 +2235,7 @@
                     }
                 }
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
@@ -2047,11 +2268,15 @@
 
         @Override
         public TelecomAnalytics dumpCallAnalytics() {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_DUMPCALLANALYTICS,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             try {
                 Log.startSession("TSI.dCA");
                 enforcePermission(DUMP);
+                event.setResult(ApiStats.RESULT_NORMAL);
                 return Analytics.dumpToParcelableAnalytics();
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
@@ -2066,6 +2291,8 @@
          */
         @Override
         protected void dump(FileDescriptor fd, final PrintWriter writer, String[] args) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_DUMP,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             if (mContext.checkCallingOrSelfPermission(
                     android.Manifest.permission.DUMP)
                     != PackageManager.PERMISSION_GRANTED) {
@@ -2075,6 +2302,8 @@
                 return;
             }
 
+            event.setResult(ApiStats.RESULT_NORMAL);
+            logEvent(event);
 
             if (args != null && args.length > 0 && Analytics.ANALYTICS_DUMPSYS_ARG.equals(
                     args[0])) {
@@ -2154,7 +2383,7 @@
                 }
 
                 for (Method m : methods) {
-                    String flagEnabled = (Boolean) m.invoke(mFeatureFlags) ? "[✅]": "[❌]";
+                    String flagEnabled = (Boolean) m.invoke(mFeatureFlags) ? "[✅]" : "[❌]";
                     String methodName = m.getName();
                     String camelCaseName = methodName.replaceAll("([a-z])([A-Z]+)", "$1_$2")
                             .toLowerCase(Locale.US);
@@ -2171,17 +2400,23 @@
          */
         @Override
         public Intent createManageBlockedNumbersIntent(String callingPackage) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(
+                    ApiStats.API_CREATEMANAGEBLOCKEDNUMBERSINTENT,
+                    Binder.getCallingUid(), ApiStats.RESULT_NORMAL);
             try {
                 Log.startSession("TSI.cMBNI", Log.getPackageAbbreviation(callingPackage));
                 return BlockedNumbersActivity.getIntentForStartingActivity();
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
 
-
         @Override
         public Intent createLaunchEmergencyDialerIntent(String number) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(
+                    ApiStats.API_CREATELAUNCHEMERGENCYDIALERINTENT,
+                    Binder.getCallingUid(), ApiStats.RESULT_NORMAL);
             String packageName = mContext.getApplicationContext().getString(
                     com.android.internal.R.string.config_emergency_dialer_package);
             Intent intent = new Intent(Intent.ACTION_DIAL_EMERGENCY)
@@ -2194,6 +2429,7 @@
             if (!TextUtils.isEmpty(number) && TextUtils.isDigitsOnly(number)) {
                 intent.setData(Uri.parse("tel:" + number));
             }
+            logEvent(event);
             return intent;
         }
 
@@ -2203,6 +2439,8 @@
         @Override
         public boolean isIncomingCallPermitted(PhoneAccountHandle phoneAccountHandle,
                 String callingPackage) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_ISINCOMINGCALLPERMITTED,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             Log.startSession("TSI.iICP", Log.getPackageAbbreviation(callingPackage));
             try {
                 enforceCallingPackage(callingPackage, "isIncomingCallPermitted");
@@ -2211,6 +2449,7 @@
                 enforceUserHandleMatchesCaller(phoneAccountHandle);
                 synchronized (mLock) {
                     long token = Binder.clearCallingIdentity();
+                    event.setResult(ApiStats.RESULT_NORMAL);
                     try {
                         return mCallsManager.isIncomingCallPermitted(phoneAccountHandle);
                     } finally {
@@ -2218,6 +2457,7 @@
                     }
                 }
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
@@ -2228,6 +2468,8 @@
         @Override
         public boolean isOutgoingCallPermitted(PhoneAccountHandle phoneAccountHandle,
                 String callingPackage) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_ISOUTGOINGCALLPERMITTED,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             Log.startSession("TSI.iOCP", Log.getPackageAbbreviation(callingPackage));
             try {
                 enforceCallingPackage(callingPackage, "isOutgoingCallPermitted");
@@ -2236,6 +2478,7 @@
                 enforceUserHandleMatchesCaller(phoneAccountHandle);
                 synchronized (mLock) {
                     long token = Binder.clearCallingIdentity();
+                    event.setResult(ApiStats.RESULT_NORMAL);
                     try {
                         return mCallsManager.isOutgoingCallPermitted(phoneAccountHandle);
                     } finally {
@@ -2243,6 +2486,7 @@
                     }
                 }
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
@@ -2296,11 +2540,14 @@
          */
         @Override
         public boolean isInEmergencyCall() {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_ISINEMERGENCYCALL,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             try {
                 Log.startSession("TSI.iIEC");
                 enforceModifyPermission();
                 synchronized (mLock) {
                     long token = Binder.clearCallingIdentity();
+                    event.setResult(ApiStats.RESULT_NORMAL);
                     try {
                         boolean isInEmergencyCall = mCallsManager.isInEmergencyCall();
                         Log.i(this, "isInEmergencyCall: %b", isInEmergencyCall);
@@ -2310,6 +2557,7 @@
                     }
                 }
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
@@ -2383,7 +2631,7 @@
             }
         }
 
-        private boolean isDisconnectingOrDisconnected(Call call){
+        private boolean isDisconnectingOrDisconnected(Call call) {
             return call.getState() == CallState.DISCONNECTED
                     || call.getState() == CallState.DISCONNECTING;
         }
@@ -2631,6 +2879,8 @@
         @Override
         public boolean isInSelfManagedCall(String packageName, UserHandle userHandle,
                 String callingPackage) {
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_ISINSELFMANAGEDCALL,
+                    Binder.getCallingUid(), ApiStats.RESULT_PERMISSION);
             try {
                 mContext.enforceCallingOrSelfPermission(READ_PRIVILEGED_PHONE_STATE,
                         "READ_PRIVILEGED_PHONE_STATE required.");
@@ -2643,6 +2893,7 @@
                 Log.startSession("TSI.iISMC", Log.getPackageAbbreviation(callingPackage));
                 synchronized (mLock) {
                     long token = Binder.clearCallingIdentity();
+                    event.setResult(ApiStats.RESULT_NORMAL);
                     try {
                         return mCallsManager.isInSelfManagedCall(
                                 packageName, userHandle);
@@ -2651,10 +2902,68 @@
                     }
                 }
             } finally {
+                logEvent(event);
                 Log.endSession();
             }
         }
     };
+    public TelecomServiceImpl(
+            Context context,
+            CallsManager callsManager,
+            PhoneAccountRegistrar phoneAccountRegistrar,
+            CallIntentProcessor.Adapter callIntentProcessorAdapter,
+            UserCallIntentProcessorFactory userCallIntentProcessorFactory,
+            DefaultDialerCache defaultDialerCache,
+            SubscriptionManagerAdapter subscriptionManagerAdapter,
+            SettingsSecureAdapter settingsSecureAdapter,
+            FeatureFlags featureFlags,
+            com.android.internal.telephony.flags.FeatureFlags telephonyFeatureFlags,
+            TelecomSystem.SyncRoot lock, TelecomMetricsController metricsController) {
+        mContext = context;
+        mAppOpsManager = mContext.getSystemService(AppOpsManager.class);
+
+        mPackageManager = mContext.getPackageManager();
+
+        mCallsManager = callsManager;
+        mFeatureFlags = featureFlags;
+        if (telephonyFeatureFlags != null) {
+            mTelephonyFeatureFlags = telephonyFeatureFlags;
+        } else {
+            mTelephonyFeatureFlags =
+                    new com.android.internal.telephony.flags.FeatureFlagsImpl();
+        }
+        mLock = lock;
+        mPhoneAccountRegistrar = phoneAccountRegistrar;
+        mUserCallIntentProcessorFactory = userCallIntentProcessorFactory;
+        mDefaultDialerCache = defaultDialerCache;
+        mCallIntentProcessorAdapter = callIntentProcessorAdapter;
+        mSubscriptionManagerAdapter = subscriptionManagerAdapter;
+        mSettingsSecureAdapter = settingsSecureAdapter;
+        mMetricsController = metricsController;
+
+        mDefaultDialerCache.observeDefaultDialerApplication(mContext.getMainExecutor(), userId -> {
+            String defaultDialer = mDefaultDialerCache.getDefaultDialerApplication(userId);
+            if (defaultDialer == null) {
+                // We are replacing the dialer, just wait for the upcoming callback.
+                return;
+            }
+            final Intent intent = new Intent(TelecomManager.ACTION_DEFAULT_DIALER_CHANGED)
+                    .putExtra(TelecomManager.EXTRA_CHANGE_DEFAULT_DIALER_PACKAGE_NAME,
+                            defaultDialer);
+            mContext.sendBroadcastAsUser(intent, UserHandle.of(userId));
+        });
+
+        mTransactionManager = TransactionManager.getInstance();
+        mTransactionalServiceRepository = new TransactionalServiceRepository(mFeatureFlags);
+        mBlockedNumbersManager = mFeatureFlags.telecomMainlineBlockedNumbersManager()
+                ? mContext.getSystemService(BlockedNumbersManager.class)
+                : null;
+    }
+
+    @VisibleForTesting
+    public void setAnomalyReporterAdapter(AnomalyReporterAdapter mAnomalyReporterAdapter) {
+        mAnomalyReporter = mAnomalyReporterAdapter;
+    }
 
     private boolean enforceCallStreamingPermission(String packageName, PhoneAccountHandle handle,
             int uid) {
@@ -2684,7 +2993,7 @@
             final int opCode = AppOpsManager.permissionToOpCode(permission);
             if (opCode != AppOpsManager.OP_NONE
                     && mAppOpsManager.checkOp(opCode, uid, packageName)
-                        != AppOpsManager.MODE_ALLOWED) {
+                    != AppOpsManager.MODE_ALLOWED) {
                 return false;
             }
         }
@@ -2701,85 +3010,12 @@
                 "App requires ACCEPT_HANDOVER permission to accept handovers.");
 
         final int opCode = AppOpsManager.permissionToOpCode(Manifest.permission.ACCEPT_HANDOVER);
-        if (opCode != AppOpsManager.OP_ACCEPT_HANDOVER || (
-                mAppOpsManager.checkOp(opCode, uid, packageName)
-                        != AppOpsManager.MODE_ALLOWED)) {
-            return false;
-        }
-        return true;
-    }
-
-    private Context mContext;
-    private AppOpsManager mAppOpsManager;
-    private PackageManager mPackageManager;
-    private CallsManager mCallsManager;
-    private final PhoneAccountRegistrar mPhoneAccountRegistrar;
-    private final CallIntentProcessor.Adapter mCallIntentProcessorAdapter;
-    private final UserCallIntentProcessorFactory mUserCallIntentProcessorFactory;
-    private final DefaultDialerCache mDefaultDialerCache;
-    private final SubscriptionManagerAdapter mSubscriptionManagerAdapter;
-    private final SettingsSecureAdapter mSettingsSecureAdapter;
-    private final TelecomSystem.SyncRoot mLock;
-    private TransactionManager mTransactionManager;
-    private final TransactionalServiceRepository mTransactionalServiceRepository;
-    private final BlockedNumbersManager mBlockedNumbersManager;
-    private final FeatureFlags mFeatureFlags;
-    private final com.android.internal.telephony.flags.FeatureFlags mTelephonyFeatureFlags;
-
-    public TelecomServiceImpl(
-            Context context,
-            CallsManager callsManager,
-            PhoneAccountRegistrar phoneAccountRegistrar,
-            CallIntentProcessor.Adapter callIntentProcessorAdapter,
-            UserCallIntentProcessorFactory userCallIntentProcessorFactory,
-            DefaultDialerCache defaultDialerCache,
-            SubscriptionManagerAdapter subscriptionManagerAdapter,
-            SettingsSecureAdapter settingsSecureAdapter,
-            FeatureFlags featureFlags,
-            com.android.internal.telephony.flags.FeatureFlags telephonyFeatureFlags,
-            TelecomSystem.SyncRoot lock) {
-        mContext = context;
-        mAppOpsManager = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
-
-        mPackageManager = mContext.getPackageManager();
-
-        mCallsManager = callsManager;
-        mFeatureFlags = featureFlags;
-        if (telephonyFeatureFlags != null) {
-            mTelephonyFeatureFlags = telephonyFeatureFlags;
-        } else {
-            mTelephonyFeatureFlags =
-                    new com.android.internal.telephony.flags.FeatureFlagsImpl();
-        }
-        mLock = lock;
-        mPhoneAccountRegistrar = phoneAccountRegistrar;
-        mUserCallIntentProcessorFactory = userCallIntentProcessorFactory;
-        mDefaultDialerCache = defaultDialerCache;
-        mCallIntentProcessorAdapter = callIntentProcessorAdapter;
-        mSubscriptionManagerAdapter = subscriptionManagerAdapter;
-        mSettingsSecureAdapter = settingsSecureAdapter;
-
-        mDefaultDialerCache.observeDefaultDialerApplication(mContext.getMainExecutor(), userId -> {
-            String defaultDialer = mDefaultDialerCache.getDefaultDialerApplication(userId);
-            if (defaultDialer == null) {
-                // We are replacing the dialer, just wait for the upcoming callback.
-                return;
-            }
-            final Intent intent = new Intent(TelecomManager.ACTION_DEFAULT_DIALER_CHANGED)
-                    .putExtra(TelecomManager.EXTRA_CHANGE_DEFAULT_DIALER_PACKAGE_NAME,
-                            defaultDialer);
-            mContext.sendBroadcastAsUser(intent, UserHandle.of(userId));
-        });
-
-        mTransactionManager = TransactionManager.getInstance();
-        mTransactionalServiceRepository = new TransactionalServiceRepository();
-        mBlockedNumbersManager = mFeatureFlags.telecomMainlineBlockedNumbersManager()
-                ? mContext.getSystemService(BlockedNumbersManager.class)
-                : null;
+        return opCode == AppOpsManager.OP_ACCEPT_HANDOVER
+                && (mAppOpsManager.checkOp(opCode, uid, packageName) == AppOpsManager.MODE_ALLOWED);
     }
 
     @VisibleForTesting
-    public void setTransactionManager(TransactionManager transactionManager){
+    public void setTransactionManager(TransactionManager transactionManager) {
         mTransactionManager = transactionManager;
     }
 
@@ -2787,10 +3023,6 @@
         return mBinderImpl;
     }
 
-    //
-    // Supporting methods for the ITelecomService interface implementation.
-    //
-
     private boolean isPhoneAccountHandleVisibleToCallingUser(
             PhoneAccountHandle phoneAccountUserHandle, UserHandle callingUser) {
         synchronized (mLock) {
@@ -2839,6 +3071,10 @@
         }
     }
 
+    //
+    // Supporting methods for the ITelecomService interface implementation.
+    //
+
     private boolean endCallInternal(String callingPackage) {
         // Always operate on the foreground call if one exists, otherwise get the first call in
         // priority order by call-state.
@@ -2880,14 +3116,14 @@
     // Enforce that the PhoneAccountHandle being passed in is both registered to the current user
     // and enabled.
     private void enforcePhoneAccountIsRegisteredEnabled(PhoneAccountHandle phoneAccountHandle,
-                                                        UserHandle callingUserHandle) {
+            UserHandle callingUserHandle) {
         PhoneAccount phoneAccount = mPhoneAccountRegistrar.getPhoneAccount(phoneAccountHandle,
                 callingUserHandle);
-        if(phoneAccount == null) {
+        if (phoneAccount == null) {
             EventLog.writeEvent(0x534e4554, "26864502", Binder.getCallingUid(), "R");
             throw new SecurityException("This PhoneAccountHandle is not registered for this user!");
         }
-        if(!phoneAccount.isEnabled()) {
+        if (!phoneAccount.isEnabled()) {
             EventLog.writeEvent(0x534e4554, "26864502", Binder.getCallingUid(), "E");
             throw new SecurityException("This PhoneAccountHandle is not enabled for this user!");
         }
@@ -2959,7 +3195,7 @@
     /**
      * helper method that compares the binder_uid to what the packageManager_uid reports for the
      * passed in packageName.
-     *
+     * <p>
      * returns true if the binder_uid matches the packageManager_uid records
      */
     private boolean callingUidMatchesPackageManagerRecords(String packageName) {
@@ -2967,13 +3203,12 @@
         int callingUid = Binder.getCallingUid();
         PackageManager pm;
         long token = Binder.clearCallingIdentity();
-        try{
+        try {
             pm = mContext.createContextAsUser(
                     UserHandle.getUserHandleForUid(callingUid), 0).getPackageManager();
-        }
-        catch (Exception e){
+        } catch (Exception e) {
             Log.i(this, "callingUidMatchesPackageManagerRecords:"
-                            + " createContextAsUser hit exception=[%s]", e.toString());
+                    + " createContextAsUser hit exception=[%s]", e.toString());
             return false;
         } finally {
             Binder.restoreCallingIdentity(token);
@@ -2988,7 +3223,7 @@
 
         if (packageUid != callingUid) {
             Log.i(this, "callingUidMatchesPackageManagerRecords: uid mismatch found for"
-                            + "packageName=[%s]. packageManager reports packageUid=[%d] but "
+                    + "packageName=[%s]. packageManager reports packageUid=[%d] but "
                     + "binder reports callingUid=[%d]", packageName, packageUid, callingUid);
         }
 
@@ -3079,7 +3314,7 @@
         boolean permissionsOk =
                 isCallerSimCallManagerForAnySim(account.getAccountHandle())
                         || mContext.checkCallingOrSelfPermission(REGISTER_SIM_SUBSCRIPTION)
-                                == PackageManager.PERMISSION_GRANTED;
+                        == PackageManager.PERMISSION_GRANTED;
         if (!prerequisiteCapabilitiesOk || !permissionsOk) {
             throw new SecurityException(
                     "Only SIM subscriptions and connection managers are allowed to declare "
@@ -3091,7 +3326,7 @@
     private void enforceRegisterSkipCallFiltering() {
         if (!isCallerSystemApp()) {
             throw new SecurityException(
-                "EXTRA_SKIP_CALL_FILTERING is only available to system apps.");
+                    "EXTRA_SKIP_CALL_FILTERING is only available to system apps.");
         }
     }
 
@@ -3261,9 +3496,9 @@
 
     private boolean isSelfManagedConnectionService(PhoneAccountHandle phoneAccountHandle) {
         if (phoneAccountHandle != null) {
-                PhoneAccount phoneAccount = mPhoneAccountRegistrar.getPhoneAccountUnchecked(
-                        phoneAccountHandle);
-                return phoneAccount != null && phoneAccount.isSelfManaged();
+            PhoneAccount phoneAccount = mPhoneAccountRegistrar.getPhoneAccountUnchecked(
+                    phoneAccountHandle);
+            return phoneAccount != null && phoneAccount.isSelfManaged();
         }
         return false;
     }
@@ -3365,10 +3600,11 @@
         // Note: Important to clear the calling identity since the code below calls into RoleManager
         // to check who holds the dialer role, and that requires MANAGE_ROLE_HOLDERS permission
         // which is a system permission.
+        int callingUserId = Binder.getCallingUserHandle().getIdentifier();
         long token = Binder.clearCallingIdentity();
         try {
             return mDefaultDialerCache.isDefaultOrSystemDialer(
-                    callingPackage, Binder.getCallingUserHandle().getIdentifier());
+                    callingPackage, callingUserId);
         } finally {
             Binder.restoreCallingIdentity(token);
         }
@@ -3399,7 +3635,7 @@
     }
 
     private void broadcastCallScreeningAppChangedIntent(String componentName,
-        boolean isDefault) {
+            boolean isDefault) {
         if (TextUtils.isEmpty(componentName)) {
             return;
         }
@@ -3408,11 +3644,11 @@
 
         if (broadcastComponentName != null) {
             Intent intent = new Intent(TelecomManager
-                .ACTION_DEFAULT_CALL_SCREENING_APP_CHANGED);
+                    .ACTION_DEFAULT_CALL_SCREENING_APP_CHANGED);
             intent.putExtra(TelecomManager
-                .EXTRA_IS_DEFAULT_CALL_SCREENING_APP, isDefault);
+                    .EXTRA_IS_DEFAULT_CALL_SCREENING_APP, isDefault);
             intent.putExtra(TelecomManager
-                .EXTRA_DEFAULT_CALL_SCREENING_APP_COMPONENT_NAME, componentName);
+                    .EXTRA_DEFAULT_CALL_SCREENING_APP_COMPONENT_NAME, componentName);
             intent.setPackage(broadcastComponentName.getPackageName());
             mContext.sendBroadcast(intent);
         }
@@ -3451,4 +3687,40 @@
             }
         }
     }
+
+    private void logEvent(ApiStats.ApiEvent event) {
+        if (mFeatureFlags.telecomMetricsSupport()) {
+            mMetricsController.getApiStats().log(event);
+        }
+    }
+
+    public interface SubscriptionManagerAdapter {
+        int getDefaultVoiceSubId();
+    }
+
+    public interface SettingsSecureAdapter {
+        void putStringForUser(ContentResolver resolver, String name, String value, int userHandle);
+
+        String getStringForUser(ContentResolver resolver, String name, int userHandle);
+    }
+
+    static class SubscriptionManagerAdapterImpl implements SubscriptionManagerAdapter {
+        @Override
+        public int getDefaultVoiceSubId() {
+            return SubscriptionManager.getDefaultVoiceSubscriptionId();
+        }
+    }
+
+    static class SettingsSecureAdapterImpl implements SettingsSecureAdapter {
+        @Override
+        public void putStringForUser(ContentResolver resolver, String name, String value,
+                int userHandle) {
+            Settings.Secure.putStringForUser(resolver, name, value, userHandle);
+        }
+
+        @Override
+        public String getStringForUser(ContentResolver resolver, String name, int userHandle) {
+            return Settings.Secure.getStringForUser(resolver, name, userHandle);
+        }
+    }
 }
diff --git a/src/com/android/server/telecom/TelecomSystem.java b/src/com/android/server/telecom/TelecomSystem.java
index fd1053f..a714096 100644
--- a/src/com/android/server/telecom/TelecomSystem.java
+++ b/src/com/android/server/telecom/TelecomSystem.java
@@ -55,7 +55,7 @@
 import com.android.server.telecom.ui.IncomingCallNotifier;
 import com.android.server.telecom.ui.MissedCallNotifierImpl.MissedCallNotifierImplFactory;
 import com.android.server.telecom.ui.ToastFactory;
-import com.android.server.telecom.voip.TransactionManager;
+import com.android.server.telecom.callsequencing.TransactionManager;
 
 import java.io.FileNotFoundException;
 import java.io.InputStream;
@@ -245,8 +245,8 @@
         // Wrap this in a try block to ensure session cleanup occurs in the case of error.
         try {
             mPhoneAccountRegistrar = new PhoneAccountRegistrar(mContext, mLock, defaultDialerCache,
-                    packageName -> AppLabelProxy.Util.getAppLabel(
-                            mContext.getPackageManager(), packageName), null, mFeatureFlags);
+                    (packageName, userHandle) -> AppLabelProxy.Util.getAppLabel(mContext,
+                            userHandle, packageName, mFeatureFlags), null, mFeatureFlags);
 
             mContactsAsyncHelper = contactsAsyncHelperFactory.create(
                     new ContactsAsyncHelper.ContentResolverAdapter() {
@@ -386,8 +386,8 @@
 
             CallStreamingNotification callStreamingNotification =
                     new CallStreamingNotification(mContext,
-                            packageName -> AppLabelProxy.Util.getAppLabel(
-                                    mContext.getPackageManager(), packageName), asyncTaskExecutor);
+                            (packageName, userHandle) -> AppLabelProxy.Util.getAppLabel(mContext,
+                                    userHandle, packageName, mFeatureFlags), asyncTaskExecutor);
 
             mCallsManager = new CallsManager(
                     mContext,
@@ -502,7 +502,8 @@
                     new TelecomServiceImpl.SettingsSecureAdapterImpl(),
                     featureFlags,
                     null,
-                    mLock);
+                    mLock,
+                    metricsController);
         } finally {
             Log.endSession();
         }
diff --git a/src/com/android/server/telecom/Timeouts.java b/src/com/android/server/telecom/Timeouts.java
index 0ed71df..ee18250 100644
--- a/src/com/android/server/telecom/Timeouts.java
+++ b/src/com/android/server/telecom/Timeouts.java
@@ -447,12 +447,12 @@
 
     /**
      * Returns the duration of time a VoIP call can be in an intermediate state before Telecom will
-     * try to clean up the call.
+     * try to clean up the call.  The default is 2 minutes.
      * @return the state timeout in millis.
      */
     public static long getVoipCallIntermediateStateTimeoutMillis() {
         return DeviceConfig.getLong(DeviceConfig.NAMESPACE_TELEPHONY,
-                INTERMEDIATE_STATE_VOIP_NORMAL_TIMEOUT_MILLIS, 60000L);
+                INTERMEDIATE_STATE_VOIP_NORMAL_TIMEOUT_MILLIS, 120000L);
     }
 
     /**
diff --git a/src/com/android/server/telecom/TransactionalServiceRepository.java b/src/com/android/server/telecom/TransactionalServiceRepository.java
index 793840e..5ae459e 100644
--- a/src/com/android/server/telecom/TransactionalServiceRepository.java
+++ b/src/com/android/server/telecom/TransactionalServiceRepository.java
@@ -20,6 +20,8 @@
 import android.telecom.PhoneAccountHandle;
 
 import com.android.internal.telecom.ICallEventCallback;
+import com.android.server.telecom.flags.FeatureFlags;
+import com.android.server.telecom.callsequencing.TransactionManager;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -32,8 +34,10 @@
     private static final String TAG = TransactionalServiceRepository.class.getSimpleName();
     private static final Map<PhoneAccountHandle, TransactionalServiceWrapper> mServiceLookupTable =
             new HashMap<>();
+    private final FeatureFlags mFlags;
 
-    public TransactionalServiceRepository() {
+    public TransactionalServiceRepository(FeatureFlags flags) {
+        mFlags = flags;
     }
 
     public TransactionalServiceWrapper addNewCallForTransactionalServiceWrapper
@@ -45,7 +49,8 @@
         if (!hasExistingServiceWrapper(phoneAccountHandle)) {
             Log.d(TAG, "creating a new TSW; handle=[%s]", phoneAccountHandle);
             service = new TransactionalServiceWrapper(callEventCallback,
-                    callsManager, phoneAccountHandle, call, this);
+                    callsManager, phoneAccountHandle, call, this,
+                    TransactionManager.getInstance(), mFlags.enableCallSequencing());
         } 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 b73de23..cf5ef41 100644
--- a/src/com/android/server/telecom/TransactionalServiceWrapper.java
+++ b/src/com/android/server/telecom/TransactionalServiceWrapper.java
@@ -39,23 +39,18 @@
 
 import com.android.internal.telecom.ICallControl;
 import com.android.internal.telecom.ICallEventCallback;
-import com.android.server.telecom.voip.CallEventCallbackAckTransaction;
-import com.android.server.telecom.voip.EndpointChangeTransaction;
-import com.android.server.telecom.voip.HoldCallTransaction;
-import com.android.server.telecom.voip.EndCallTransaction;
-import com.android.server.telecom.voip.MaybeHoldCallForNewCallTransaction;
-import com.android.server.telecom.voip.RequestNewActiveCallTransaction;
-import com.android.server.telecom.voip.SerialTransaction;
-import com.android.server.telecom.voip.SetMuteStateTransaction;
-import com.android.server.telecom.voip.RequestVideoStateTransaction;
-import com.android.server.telecom.voip.TransactionManager;
-import com.android.server.telecom.voip.VoipCallTransaction;
-import com.android.server.telecom.voip.VoipCallTransactionResult;
+import com.android.server.telecom.callsequencing.TransactionalCallSequencingAdapter;
+import com.android.server.telecom.callsequencing.voip.CallEventCallbackAckTransaction;
+import com.android.server.telecom.callsequencing.voip.EndpointChangeTransaction;
+import com.android.server.telecom.callsequencing.voip.SetMuteStateTransaction;
+import com.android.server.telecom.callsequencing.voip.RequestVideoStateTransaction;
+import com.android.server.telecom.callsequencing.TransactionManager;
+import com.android.server.telecom.callsequencing.CallTransaction;
+import com.android.server.telecom.callsequencing.CallTransactionResult;
 
-import java.util.ArrayList;
-import java.util.List;
 import java.util.Locale;
 import java.util.Set;
+import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentHashMap;
 
 /**
@@ -73,6 +68,8 @@
     public static final String DISCONNECT = "Disconnect";
     public static final String START_STREAMING = "StartStreaming";
     public static final String REQUEST_VIDEO_STATE = "RequestVideoState";
+    public static final String SET_MUTE_STATE = "SetMuteState";
+    public static final String CALL_ENDPOINT_CHANGE = "CallEndpointChange";
 
     // CallEventCallback : Telecom --> Client (ex. voip app)
     public static final String ON_SET_ACTIVE = "onSetActive";
@@ -80,6 +77,7 @@
     public static final String ON_ANSWER = "onAnswer";
     public static final String ON_DISCONNECT = "onDisconnect";
     public static final String ON_STREAMING_STARTED = "onStreamingStarted";
+    public static final String STOP_STREAMING = "stopStreaming";
 
     private final CallsManager mCallsManager;
     private final ICallEventCallback mICallEventCallback;
@@ -93,6 +91,7 @@
     // needs to be non-final for testing
     private TransactionManager mTransactionManager;
     private CallStreamingController mStreamingController;
+    private final TransactionalCallSequencingAdapter mCallSequencingAdapter;
 
 
     // Each TransactionalServiceWrapper should have their own Binder.DeathRecipient to clean up
@@ -108,26 +107,24 @@
 
     public TransactionalServiceWrapper(ICallEventCallback callEventCallback,
             CallsManager callsManager, PhoneAccountHandle phoneAccountHandle, Call call,
-            TransactionalServiceRepository repo) {
+            TransactionalServiceRepository repo, TransactionManager transactionManager,
+            boolean isCallSequencingEnabled) {
         // passed args
         mICallEventCallback = callEventCallback;
         mCallsManager = callsManager;
         mPhoneAccountHandle = phoneAccountHandle;
         mTrackedCalls.put(call.getId(), call); // service is now tracking its first call
         mRepository = repo;
+        mTransactionManager = transactionManager;
         // init instance vars
         mPackageName = phoneAccountHandle.getComponentName().getPackageName();
-        mTransactionManager = TransactionManager.getInstance();
         mStreamingController = mCallsManager.getCallStreamingController();
         mLock = mCallsManager.getLock();
+        mCallSequencingAdapter = new TransactionalCallSequencingAdapter(mTransactionManager,
+                mCallsManager, isCallSequencingEnabled);
         setDeathRecipient(callEventCallback);
     }
 
-    @VisibleForTesting
-    public void setTransactionManager(TransactionManager transactionManager) {
-        mTransactionManager = transactionManager;
-    }
-
     public TransactionManager getTransactionManager() {
         return mTransactionManager;
     }
@@ -170,11 +167,7 @@
     }
 
     private void cleanupTransactionalServiceWrapper() {
-        for (Call call : mTrackedCalls.values()) {
-            mCallsManager.markCallAsDisconnected(call,
-                    new DisconnectCause(DisconnectCause.ERROR, "process died"));
-            mCallsManager.removeCall(call); // This will clear mTrackedCalls && ClientTWS
-        }
+        mCallSequencingAdapter.cleanup(mTrackedCalls.values());
     }
 
     /***
@@ -184,8 +177,7 @@
      */
     private final ICallControl mICallControl = new ICallControl.Stub() {
         @Override
-        public void setActive(String callId, android.os.ResultReceiver callback)
-                throws RemoteException {
+        public void setActive(String callId, android.os.ResultReceiver callback) {
             long token = Binder.clearCallingIdentity();
             try {
                 Log.startSession("TSW.sA");
@@ -197,8 +189,8 @@
         }
 
         @Override
-        public void answer(int videoState, String callId, android.os.ResultReceiver callback)
-                throws RemoteException {
+
+        public void answer(int videoState, String callId, android.os.ResultReceiver callback) {
             long token = Binder.clearCallingIdentity();
             try {
                 Log.startSession("TSW.a");
@@ -210,8 +202,7 @@
         }
 
         @Override
-        public void setInactive(String callId, android.os.ResultReceiver callback)
-                throws RemoteException {
+        public void setInactive(String callId, android.os.ResultReceiver callback) {
             long token = Binder.clearCallingIdentity();
             try {
                 Log.startSession("TSW.sI");
@@ -224,8 +215,7 @@
 
         @Override
         public void disconnect(String callId, DisconnectCause disconnectCause,
-                android.os.ResultReceiver callback)
-                throws RemoteException {
+                android.os.ResultReceiver callback) {
             long token = Binder.clearCallingIdentity();
             try {
                 Log.startSession("TSW.d");
@@ -237,12 +227,11 @@
         }
 
         @Override
-        public void setMuteState(boolean isMuted, android.os.ResultReceiver callback)
-                throws RemoteException {
+        public void setMuteState(boolean isMuted, android.os.ResultReceiver callback) {
             long token = Binder.clearCallingIdentity();
             try {
                 Log.startSession("TSW.sMS");
-                addTransactionsToManager(
+                addTransactionsToManager(SET_MUTE_STATE,
                         new SetMuteStateTransaction(mCallsManager, isMuted), callback);
             } finally {
                 Binder.restoreCallingIdentity(token);
@@ -251,8 +240,7 @@
         }
 
         @Override
-        public void startCallStreaming(String callId, android.os.ResultReceiver callback)
-                throws RemoteException {
+        public void startCallStreaming(String callId, android.os.ResultReceiver callback) {
             long token = Binder.clearCallingIdentity();
             try {
                 Log.startSession("TSW.sCS");
@@ -264,8 +252,7 @@
         }
 
         @Override
-        public void requestVideoState(int videoState, String callId, ResultReceiver callback)
-                throws RemoteException {
+        public void requestVideoState(int videoState, String callId, ResultReceiver callback) {
             long token = Binder.clearCallingIdentity();
             try {
                 Log.startSession("TSW.rVS");
@@ -283,27 +270,29 @@
             if (call != null) {
                 switch (action) {
                     case SET_ACTIVE:
-                        handleCallControlNewCallFocusTransactions(call, SET_ACTIVE,
-                                false /* isAnswer */, 0/*VideoState (ignored)*/, callback);
+                        mCallSequencingAdapter.setActive(call,
+                                getCompleteReceiver(action, callback));
                         break;
                     case ANSWER:
-                        handleCallControlNewCallFocusTransactions(call, ANSWER,
-                                true /* isAnswer */, (int) objects[0] /*VideoState*/, callback);
+                        mCallSequencingAdapter.setAnswered(call, (int) objects[0] /*VideoState*/,
+                                getCompleteReceiver(action, callback));
                         break;
                     case DISCONNECT:
-                        addTransactionsToManager(new EndCallTransaction(mCallsManager,
-                                (DisconnectCause) objects[0], call), callback);
+                        DisconnectCause dc = (DisconnectCause) objects[0];
+                        mCallSequencingAdapter.setDisconnected(call, dc,
+                                getCompleteReceiver(action, callback));
                         break;
                     case SET_INACTIVE:
-                        addTransactionsToManager(
-                                new HoldCallTransaction(mCallsManager, call), callback);
+                        mCallSequencingAdapter.setInactive(call,
+                                getCompleteReceiver(action,callback));
                         break;
                     case START_STREAMING:
-                        addTransactionsToManager(mStreamingController.getStartStreamingTransaction(mCallsManager,
-                                TransactionalServiceWrapper.this, call, mLock), callback);
+                        addTransactionsToManager(action,
+                                mStreamingController.getStartStreamingTransaction(mCallsManager,
+                                TransactionalServiceWrapper.this, call, mLock),  callback);
                         break;
                     case REQUEST_VIDEO_STATE:
-                        addTransactionsToManager(
+                        addTransactionsToManager(action,
                                 new RequestVideoStateTransaction(mCallsManager, call,
                                         (int) objects[0]), callback);
                         break;
@@ -321,40 +310,13 @@
             }
         }
 
-        // The client is request their VoIP call state go ACTIVE/ANSWERED.
-        // This request is originating from the VoIP application.
-        private void handleCallControlNewCallFocusTransactions(Call call, String action,
-                boolean isAnswer, int potentiallyNewVideoState, ResultReceiver callback) {
-            mTransactionManager.addTransaction(
-                    createSetActiveTransactions(call, true /* isCallControlRequest */),
-                    new OutcomeReceiver<>() {
-                        @Override
-                        public void onResult(VoipCallTransactionResult result) {
-                            Log.i(TAG, String.format(Locale.US,
-                                    "%s: onResult: callId=[%s]", action, call.getId()));
-                            if (isAnswer) {
-                                call.setVideoState(potentiallyNewVideoState);
-                            }
-                            callback.send(TELECOM_TRANSACTION_SUCCESS, new Bundle());
-                        }
-
-                        @Override
-                        public void onError(CallException exception) {
-                            Bundle extras = new Bundle();
-                            extras.putParcelable(TRANSACTION_EXCEPTION_KEY, exception);
-                            callback.send(exception == null ? CallException.CODE_ERROR_UNKNOWN :
-                                    exception.getCode(), extras);
-                        }
-                    });
-        }
-
         @Override
         public void requestCallEndpointChange(CallEndpoint endpoint, ResultReceiver callback) {
             long token = Binder.clearCallingIdentity();
             try {
                 Log.startSession("TSW.rCEC");
-                addTransactionsToManager(new EndpointChangeTransaction(endpoint, mCallsManager),
-                        callback);
+                addTransactionsToManager(CALL_ENDPOINT_CHANGE,
+                        new EndpointChangeTransaction(endpoint, mCallsManager), callback);
             } finally {
                 Binder.restoreCallingIdentity(token);
                 Log.endSession();
@@ -384,26 +346,31 @@
         }
     };
 
-    private void addTransactionsToManager(VoipCallTransaction transaction,
+    private void addTransactionsToManager(String action, CallTransaction transaction,
             ResultReceiver callback) {
         Log.d(TAG, "addTransactionsToManager");
+        CompletableFuture<Boolean> transactionResult = mTransactionManager
+                .addTransaction(transaction, getCompleteReceiver(action, callback));
+    }
 
-        mTransactionManager.addTransaction(transaction, new OutcomeReceiver<>() {
+    private OutcomeReceiver<CallTransactionResult, CallException> getCompleteReceiver(
+            String action, ResultReceiver callback) {
+        return new OutcomeReceiver<>() {
             @Override
-            public void onResult(VoipCallTransactionResult result) {
-                Log.d(TAG, "addTransactionsToManager: onResult:");
+            public void onResult(CallTransactionResult result) {
+                Log.d(TAG, "completeReceiver: onResult[" + action + "]:" + result);
                 callback.send(TELECOM_TRANSACTION_SUCCESS, new Bundle());
             }
 
             @Override
             public void onError(CallException exception) {
-                Log.d(TAG, "addTransactionsToManager: onError");
+                Log.d(TAG, "completeReceiver: onError[" + action + "]" + exception);
                 Bundle extras = new Bundle();
                 extras.putParcelable(TRANSACTION_EXCEPTION_KEY, exception);
                 callback.send(exception == null ? CallException.CODE_ERROR_UNKNOWN :
                         exception.getCode(), extras);
             }
-        });
+        };
     }
 
     public ICallControl getICallControl() {
@@ -416,89 +383,53 @@
      **********************************************************************************************
      */
 
-    public void onSetActive(Call call) {
+    public CompletableFuture<Boolean> onSetActive(Call call) {
+        CallTransaction callTransaction = new CallEventCallbackAckTransaction(
+                mICallEventCallback, ON_SET_ACTIVE, call.getId(), mLock);
+        CompletableFuture<Boolean> onSetActiveFuture;
         try {
             Log.startSession("TSW.oSA");
             Log.d(TAG, String.format(Locale.US, "onSetActive: callId=[%s]", call.getId()));
-            handleCallEventCallbackNewFocus(call, ON_SET_ACTIVE, false /*isAnswerRequest*/,
-                    0 /*VideoState*/);
+            onSetActiveFuture = mCallSequencingAdapter.onSetActive(call,
+                    callTransaction, result ->
+                            Log.i(TAG, String.format(Locale.US,
+                                    "%s: onResult: callId=[%s], result=[%s]", ON_SET_ACTIVE,
+                                    call.getId(), result)));
         } finally {
             Log.endSession();
         }
+        return onSetActiveFuture;
     }
 
     public void onAnswer(Call call, int videoState) {
         try {
             Log.startSession("TSW.oA");
             Log.d(TAG, String.format(Locale.US, "onAnswer: callId=[%s]", call.getId()));
-            handleCallEventCallbackNewFocus(call, ON_ANSWER, true /*isAnswerRequest*/,
-                    videoState /*VideoState*/);
+            mCallSequencingAdapter.onSetAnswered(call, videoState,
+                    new CallEventCallbackAckTransaction(mICallEventCallback,
+                            ON_ANSWER, call.getId(), videoState, mLock),
+                    result -> Log.i(TAG, String.format(Locale.US,
+                            "%s: onResult: callId=[%s], result=[%s]",
+                            ON_ANSWER, call.getId(), result)));
         } finally {
             Log.endSession();
         }
     }
 
-    // handle a CallEventCallback to set a call ACTIVE/ANSWERED. Must get ack from client since the
-    // request has come from another source (ex. Android Auto is requesting a call to go active)
-    private void handleCallEventCallbackNewFocus(Call call, String action, boolean isAnswerRequest,
-            int potentiallyNewVideoState) {
-        // save CallsManager state before sending client state changes
-        Call foregroundCallBeforeSwap = mCallsManager.getForegroundCall();
-        boolean wasActive = foregroundCallBeforeSwap != null && foregroundCallBeforeSwap.isActive();
-
-        SerialTransaction serialTransactions = createSetActiveTransactions(call,
-                false /* isCallControlRequest */);
-        // 3. get ack from client (that the requested call can go active)
-        if (isAnswerRequest) {
-            serialTransactions.appendTransaction(
-                    new CallEventCallbackAckTransaction(mICallEventCallback,
-                            action, call.getId(), potentiallyNewVideoState, mLock));
-        } else {
-            serialTransactions.appendTransaction(
-                    new CallEventCallbackAckTransaction(mICallEventCallback,
-                            action, call.getId(), mLock));
-        }
-
-        // do CallsManager workload before asking client and
-        //   reset CallsManager state if client does NOT ack
-        mTransactionManager.addTransaction(serialTransactions,
-                new OutcomeReceiver<>() {
-                    @Override
-                    public void onResult(VoipCallTransactionResult result) {
-                        Log.i(TAG, String.format(Locale.US,
-                                "%s: onResult: callId=[%s]", action, call.getId()));
-                        if (isAnswerRequest) {
-                            call.setVideoState(potentiallyNewVideoState);
-                        }
-                    }
-
-                    @Override
-                    public void onError(CallException exception) {
-                        if (isAnswerRequest) {
-                            // 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"));
-                        } else {
-                            mCallsManager.markCallAsOnHold(call);
-                        }
-                        maybeResetForegroundCall(foregroundCallBeforeSwap, wasActive);
-                    }
-                });
-    }
-
-
-    public void onSetInactive(Call call) {
+    public CompletableFuture<Boolean> onSetInactive(Call call) {
+        CallTransaction callTransaction = new CallEventCallbackAckTransaction(
+                mICallEventCallback, ON_SET_INACTIVE, call.getId(), mLock);
+        CompletableFuture<Boolean> onSetInactiveFuture;
         try {
             Log.startSession("TSW.oSI");
             Log.i(TAG, String.format(Locale.US, "onSetInactive: callId=[%s]", call.getId()));
-            mTransactionManager.addTransaction(
-                    new CallEventCallbackAckTransaction(mICallEventCallback,
-                            ON_SET_INACTIVE, call.getId(), mLock), new OutcomeReceiver<>() {
+            onSetInactiveFuture = mCallSequencingAdapter.onSetInactive(call,
+                    callTransaction, new OutcomeReceiver<>() {
                         @Override
-                        public void onResult(VoipCallTransactionResult result) {
-                            mCallsManager.markCallAsOnHold(call);
+                        public void onResult(CallTransactionResult result) {
+                            Log.i(TAG, String.format(Locale.US, "onSetInactive: callId=[%s]"
+                                            + ", result=[%s]",
+                                    call.getId(), result));
                         }
 
                         @Override
@@ -510,30 +441,26 @@
         } finally {
             Log.endSession();
         }
+        return onSetInactiveFuture;
     }
 
-    public void onDisconnect(Call call, DisconnectCause cause) {
+    public CompletableFuture<Boolean> onDisconnect(Call call,
+            DisconnectCause cause) {
+        CallTransaction callTransaction = new CallEventCallbackAckTransaction(
+                mICallEventCallback, ON_DISCONNECT, call.getId(), cause, mLock);
+        CompletableFuture<Boolean> onDisconnectFuture;
         try {
             Log.startSession("TSW.oD");
             Log.d(TAG, String.format(Locale.US, "onDisconnect: callId=[%s]", call.getId()));
-
-            mTransactionManager.addTransaction(
-                    new CallEventCallbackAckTransaction(mICallEventCallback, ON_DISCONNECT,
-                            call.getId(), cause, mLock), new OutcomeReceiver<>() {
-                        @Override
-                        public void onResult(VoipCallTransactionResult result) {
-                            removeCallFromCallsManager(call, cause);
-                        }
-
-                        @Override
-                        public void onError(CallException exception) {
-                            removeCallFromCallsManager(call, cause);
-                        }
-                    }
-            );
+            onDisconnectFuture = mCallSequencingAdapter.onSetDisconnected(call, cause,
+                    callTransaction,
+                    result -> Log.i(TAG, String.format(Locale.US,
+                            "%s: onResult: callId=[%s], result=[%s]",
+                            ON_DISCONNECT, call.getId(), result)));
         } finally {
             Log.endSession();
         }
+        return onDisconnectFuture;
     }
 
     public void onCallStreamingStarted(Call call) {
@@ -546,7 +473,7 @@
                     new CallEventCallbackAckTransaction(mICallEventCallback, ON_STREAMING_STARTED,
                             call.getId(), mLock), new OutcomeReceiver<>() {
                         @Override
-                        public void onResult(VoipCallTransactionResult result) {
+                        public void onResult(CallTransactionResult result) {
                         }
 
                         @Override
@@ -641,35 +568,6 @@
      **                                Helpers                                                  **
      **********************************************************************************************
      */
-    private void maybeResetForegroundCall(Call foregroundCallBeforeSwap, boolean wasActive) {
-        if (foregroundCallBeforeSwap == null) {
-            return;
-        }
-        if (wasActive && !foregroundCallBeforeSwap.isActive()) {
-            mCallsManager.markCallAsActive(foregroundCallBeforeSwap);
-        }
-    }
-
-    private void removeCallFromCallsManager(Call call, DisconnectCause cause) {
-        if (cause.getCode() != DisconnectCause.REJECTED) {
-            mCallsManager.markCallAsDisconnected(call, cause);
-        }
-        mCallsManager.removeCall(call);
-    }
-
-    private SerialTransaction createSetActiveTransactions(Call call, boolean isCallControlRequest) {
-        // create list for multiple transactions
-        List<VoipCallTransaction> transactions = new ArrayList<>();
-
-        // potentially hold the current active call in order to set a new call (active/answered)
-        transactions.add(
-                new MaybeHoldCallForNewCallTransaction(mCallsManager, call, isCallControlRequest));
-        // And request a new focus call update
-        transactions.add(new RequestNewActiveCallTransaction(mCallsManager, call));
-
-        return new SerialTransaction(transactions, mLock);
-    }
-
     private void setDeathRecipient(ICallEventCallback callEventCallback) {
         try {
             callEventCallback.asBinder().linkToDeath(mAppDeathListener, 0);
@@ -720,9 +618,10 @@
     public void stopCallStreaming(Call call) {
         Log.i(this, "stopCallStreaming; callid=%s", call.getId());
         if (call != null && call.isStreaming()) {
-            VoipCallTransaction stopStreamingTransaction = mStreamingController
+            CallTransaction stopStreamingTransaction = mStreamingController
                     .getStopStreamingTransaction(call, mLock);
-            addTransactionsToManager(stopStreamingTransaction, new ResultReceiver(null));
+            addTransactionsToManager(STOP_STREAMING, stopStreamingTransaction,
+                    new ResultReceiver(null));
         }
     }
 }
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
index 0f27dad..4f0aa89 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
@@ -308,19 +308,19 @@
         mFeatureFlags = featureFlags;
         if (bluetoothAdapter != null) {
             mBluetoothAdapter = bluetoothAdapter;
-            if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
-                mBluetoothHeadsetFuture = new CompletableFuture<>();
-            }
             bluetoothAdapter.getProfileProxy(context, mBluetoothProfileServiceListener,
                     BluetoothProfile.HEADSET);
             bluetoothAdapter.getProfileProxy(context, mBluetoothProfileServiceListener,
                     BluetoothProfile.HEARING_AID);
             bluetoothAdapter.getProfileProxy(context, mBluetoothProfileServiceListener,
                     BluetoothProfile.LE_AUDIO);
-            mAudioManager = context.getSystemService(AudioManager.class);
-            mExecutor = context.getMainExecutor();
-            mCommunicationDeviceTracker = communicationDeviceTracker;
         }
+        if (mFeatureFlags.useRefactoredAudioRouteSwitching()) {
+            mBluetoothHeadsetFuture = new CompletableFuture<>();
+        }
+        mAudioManager = context.getSystemService(AudioManager.class);
+        mExecutor = context.getMainExecutor();
+        mCommunicationDeviceTracker = communicationDeviceTracker;
     }
 
     public void setBluetoothRouteManager(BluetoothRouteManager brm) {
@@ -759,7 +759,7 @@
             Log.w(this, "setCommunicationDeviceForAddress: Device %s not found.", address);
             return false;
         }
-        if (mAudioManager.getCommunicationDevice().equals(deviceInfo)) {
+        if (deviceInfo.equals(mAudioManager.getCommunicationDevice())) {
             Log.i(this, "setCommunicationDeviceForAddress: Device %s already active.", address);
             return true;
         }
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java b/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
index cd52889..7fe8246 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
@@ -252,10 +252,12 @@
             CallAudioRouteController audioRouteController = (CallAudioRouteController)
                     mCallAudioRouteAdapter;
             if (device == null) {
+                // Update the active device cache immediately.
                 audioRouteController.updateActiveBluetoothDevice(new Pair(audioRouteType, null));
                 mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_GONE,
                         audioRouteType);
             } else {
+                // Update the active device cache immediately.
                 audioRouteController.updateActiveBluetoothDevice(
                         new Pair(audioRouteType, device.getAddress()));
                 mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
@@ -265,11 +267,17 @@
                     if (!mBluetoothDeviceManager.setCommunicationDeviceForAddress(
                             device.getAddress())) {
                         Log.i(this, "handleActiveDeviceChanged: Failed to set "
-                                + "communication device for %s. Sending PENDING_ROUTE_FAILED to "
-                                + "pending audio route.", device);
-                        mCallAudioRouteAdapter.getPendingAudioRoute()
-                                .onMessageReceived(new Pair<>(PENDING_ROUTE_FAILED,
-                                        device.getAddress()), device.getAddress());
+                                + "communication device for %s.", device);
+                        if (!mFeatureFlags.resolveActiveBtRoutingAndBtTimingIssue()) {
+                            Log.i(this, "Sending PENDING_ROUTE_FAILED "
+                                    + "to pending audio route.");
+                            mCallAudioRouteAdapter.getPendingAudioRoute()
+                                    .onMessageReceived(new Pair<>(PENDING_ROUTE_FAILED,
+                                            device.getAddress()), device.getAddress());
+                        } else {
+                            Log.i(this, "Refrain from sending PENDING_ROUTE_FAILED"
+                                    + " to pending audio route.");
+                        }
                     } else {
                         // Track the currently set communication device.
                         int routeType = deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO
diff --git a/src/com/android/server/telecom/callfiltering/BlockCheckerFilter.java b/src/com/android/server/telecom/callfiltering/BlockCheckerFilter.java
index 5beb5f0..7e3837d 100644
--- a/src/com/android/server/telecom/callfiltering/BlockCheckerFilter.java
+++ b/src/com/android/server/telecom/callfiltering/BlockCheckerFilter.java
@@ -21,6 +21,7 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.HandlerThread;
+import android.os.UserManager;
 import android.provider.BlockedNumberContract;
 import android.provider.CallLog;
 import android.telecom.CallerInfo;
@@ -29,6 +30,7 @@
 
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallerInfoLookupHelper;
+import com.android.server.telecom.flags.FeatureFlags;
 import com.android.server.telecom.LogUtils;
 import com.android.server.telecom.LoggedHandlerExecutor;
 import com.android.server.telecom.settings.BlockedNumbersUtil;
@@ -45,6 +47,7 @@
     private boolean mContactExists;
     private HandlerThread mHandlerThread;
     private Handler mHandler;
+    private FeatureFlags mFeatureFlags;
 
     public static final long CALLER_INFO_QUERY_TIMEOUT = 5000;
 
@@ -105,7 +108,7 @@
 
     public BlockCheckerFilter(Context context, Call call,
             CallerInfoLookupHelper callerInfoLookupHelper,
-            BlockCheckerAdapter blockCheckerAdapter) {
+            BlockCheckerAdapter blockCheckerAdapter, FeatureFlags featureFlags) {
         mCall = call;
         mContext = context;
         mCallerInfoLookupHelper = callerInfoLookupHelper;
@@ -114,6 +117,7 @@
         mHandlerThread = new HandlerThread(TAG);
         mHandlerThread.start();
         mHandler = new Handler(mHandlerThread.getLooper());
+        mFeatureFlags = featureFlags;
     }
 
     @Override
@@ -121,7 +125,13 @@
         Log.addEvent(mCall, LogUtils.Events.BLOCK_CHECK_INITIATED);
         CompletableFuture<CallFilteringResult> resultFuture = new CompletableFuture<>();
         Bundle extras = new Bundle();
-        if (BlockedNumbersUtil.isEnhancedCallBlockingEnabledByPlatform(mContext)) {
+        final Context userContext;
+        if (mFeatureFlags.telecomMainUserInBlockCheck()) {
+            userContext = mContext.createContextAsUser(mCall.getAssociatedUser(), 0);
+        } else {
+            userContext = mContext;
+        }
+        if (BlockedNumbersUtil.isEnhancedCallBlockingEnabledByPlatform(userContext)) {
             int presentation = mCall.getHandlePresentation();
             extras.putInt(BlockedNumberContract.EXTRA_CALL_PRESENTATION, presentation);
             if (presentation == TelecomManager.PRESENTATION_ALLOWED) {
@@ -132,7 +142,7 @@
                                 if (info != null && info.contactExists) {
                                     mContactExists = true;
                                 }
-                                getBlockStatus(resultFuture);
+                                getBlockStatus(resultFuture, userContext);
                             }
 
                             @Override
@@ -141,22 +151,22 @@
                             }
                         });
             } else {
-                getBlockStatus(resultFuture);
+                getBlockStatus(resultFuture, userContext);
             }
         } else {
-            getBlockStatus(resultFuture);
+            getBlockStatus(resultFuture, userContext);
         }
         return resultFuture;
     }
 
     private void getBlockStatus(
-            CompletableFuture<CallFilteringResult> resultFuture) {
+            CompletableFuture<CallFilteringResult> resultFuture, Context userContext) {
         // Set presentation and if contact exists. Used in determining if the system should block
         // the passed in number. Use default values as they would be returned if the keys didn't
         // exist in the extras to maintain existing behavior.
         int presentation;
         boolean isNumberInContacts;
-        if (BlockedNumbersUtil.isEnhancedCallBlockingEnabledByPlatform(mContext)) {
+        if (BlockedNumbersUtil.isEnhancedCallBlockingEnabledByPlatform(userContext)) {
             presentation = mCall.getHandlePresentation();
         } else {
             presentation = 0;
@@ -173,7 +183,7 @@
                 mCall.getHandle().getSchemeSpecificPart();
 
         CompletableFuture.supplyAsync(
-                () -> mBlockCheckerAdapter.getBlockStatus(mContext, number,
+                () -> mBlockCheckerAdapter.getBlockStatus(userContext, number,
                         presentation, isNumberInContacts),
                 new LoggedHandlerExecutor(mHandler, "BCF.gBS", null))
                 .thenApplyAsync((x) -> completeResult(resultFuture, x),
diff --git a/src/com/android/server/telecom/callfiltering/CallScreeningServiceFilter.java b/src/com/android/server/telecom/callfiltering/CallScreeningServiceFilter.java
index f07c0aa..efac87d 100644
--- a/src/com/android/server/telecom/callfiltering/CallScreeningServiceFilter.java
+++ b/src/com/android/server/telecom/callfiltering/CallScreeningServiceFilter.java
@@ -269,7 +269,8 @@
         mContext = context;
         mPackageManager = mContext.getPackageManager();
         mCallsManager = callsManager;
-        mAppName = appLabelProxy.getAppLabel(mPackageName);
+        mAppName = appLabelProxy.getAppLabel(mPackageName,
+                mCall.getAssociatedUser());
         mParcelableCallUtilsConverter = parcelableCallUtilsConverter;
     }
 
diff --git a/src/com/android/server/telecom/callsequencing/CallSequencingController.java b/src/com/android/server/telecom/callsequencing/CallSequencingController.java
new file mode 100644
index 0000000..2f0ae45
--- /dev/null
+++ b/src/com/android/server/telecom/callsequencing/CallSequencingController.java
@@ -0,0 +1,82 @@
+/*
+ * 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;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallsManager;
+
+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.
+ */
+public class CallSequencingController {
+//    private final CallsManager mCallsManager;
+    private final TransactionManager mTransactionManager;
+//    private final Handler mHandler;
+//    private boolean mCallSequencingEnabled;
+
+    public CallSequencingController(CallsManager callsManager, boolean callSequencingEnabled) {
+//        mCallsManager = callsManager;
+        mTransactionManager = TransactionManager.getInstance();
+        HandlerThread handlerThread = new HandlerThread(this.toString());
+        handlerThread.start();
+//        mHandler = new Handler(handlerThread.getLooper());
+//        mCallSequencingEnabled = callSequencingEnabled;
+    }
+
+    public void answerCall(Call incomingCall, int videoState) {
+        // Todo: call sequencing logic (stubbed)
+    }
+
+//    private CompletableFuture<Boolean> holdActiveCallForNewCallWithSequencing(Call call) {
+//        // Todo: call sequencing logic (stubbed)
+//        return null;
+//    }
+
+    public void unholdCall(Call call) {
+        // Todo: call sequencing logic (stubbed)
+    }
+
+    public CompletableFuture<Boolean> makeRoomForOutgoingCall(boolean isEmergency, Call call) {
+        // Todo: call sequencing logic (stubbed)
+        return CompletableFuture.completedFuture(true);
+//        return isEmergency ? makeRoomForOutgoingEmergencyCall(call) : makeRoomForOutgoingCall(call);
+    }
+
+//    private CompletableFuture<Boolean> makeRoomForOutgoingEmergencyCall(Call emergencyCall) {
+//        // Todo: call sequencing logic (stubbed)
+//        return CompletableFuture.completedFuture(true);
+//    }
+
+//    private CompletableFuture<Boolean> makeRoomForOutgoingCall(Call call) {
+//        // Todo: call sequencing logic (stubbed)
+//        return CompletableFuture.completedFuture(true);
+//    }
+
+//    private void resetProcessingCallSequencing() {
+//        mTransactionManager.setProcessingCallSequencing(false);
+//    }
+
+    public CompletableFuture<Boolean> disconnectCall() {
+        return CompletableFuture.completedFuture(true);
+    }
+}
diff --git a/src/com/android/server/telecom/voip/VoipCallTransaction.java b/src/com/android/server/telecom/callsequencing/CallTransaction.java
similarity index 85%
rename from src/com/android/server/telecom/voip/VoipCallTransaction.java
rename to src/com/android/server/telecom/callsequencing/CallTransaction.java
index a589a6d..8d7da7c 100644
--- a/src/com/android/server/telecom/voip/VoipCallTransaction.java
+++ b/src/com/android/server/telecom/callsequencing/CallTransaction.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 The Android Open Source Project
+ * 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.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.server.telecom.voip;
+package com.android.server.telecom.callsequencing;
 
 import android.os.Handler;
 import android.os.HandlerThread;
@@ -34,7 +34,7 @@
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Function;
 
-public class VoipCallTransaction {
+public class CallTransaction {
     //TODO: add log events
     private static final long DEFAULT_TRANSACTION_TIMEOUT_MS = 5000L;
 
@@ -52,7 +52,7 @@
         private long mFinishedTimeNs = -1L;
         // If finished, did this transaction finish because it timed out?
         private boolean mIsTimedOut = false;
-        private VoipCallTransactionResult  mTransactionResult = null;
+        private CallTransactionResult  mTransactionResult = null;
 
         public Stats() {
             addedTimeStamp = LocalDateTime.now();
@@ -70,7 +70,7 @@
         /**
          * Mark the transaction as completed and record the time.
          */
-        public void markComplete(boolean isTimedOut, VoipCallTransactionResult result) {
+        public void markComplete(boolean isTimedOut, CallTransactionResult result) {
             if (mFinishedTimeNs > -1) return;
             mFinishedTimeNs = System.nanoTime();
             mIsTimedOut = isTimedOut;
@@ -124,7 +124,7 @@
          * @return the result if the transaction completed, null if it timed out or hasn't completed
          * yet.
          */
-        public VoipCallTransactionResult getTransactionResult() {
+        public CallTransactionResult getTransactionResult() {
             return mTransactionResult;
         }
     }
@@ -134,13 +134,13 @@
     private final HandlerThread mHandlerThread;
     protected final Handler mHandler;
     protected TransactionManager.TransactionCompleteListener mCompleteListener;
-    protected final List<VoipCallTransaction> mSubTransactions;
+    protected final List<CallTransaction> mSubTransactions;
     protected final TelecomSystem.SyncRoot mLock;
     protected final long mTransactionTimeoutMs;
     protected final Stats mStats;
 
-    public VoipCallTransaction(
-            List<VoipCallTransaction> subTransactions, TelecomSystem.SyncRoot lock,
+    public CallTransaction(
+            List<CallTransaction> subTransactions, TelecomSystem.SyncRoot lock,
             long timeoutMs) {
         mSubTransactions = subTransactions;
         mHandlerThread = new HandlerThread(this.toString());
@@ -151,15 +151,15 @@
         mStats = Flags.enableCallSequencing() ? new Stats() : null;
     }
 
-    public VoipCallTransaction(List<VoipCallTransaction> subTransactions,
+    public CallTransaction(List<CallTransaction> subTransactions,
             TelecomSystem.SyncRoot lock) {
         this(subTransactions, lock, DEFAULT_TRANSACTION_TIMEOUT_MS);
     }
-    public VoipCallTransaction(TelecomSystem.SyncRoot lock, long timeoutMs) {
+    public CallTransaction(TelecomSystem.SyncRoot lock, long timeoutMs) {
         this(null /* mSubTransactions */, lock, timeoutMs);
     }
 
-    public VoipCallTransaction(TelecomSystem.SyncRoot lock) {
+    public CallTransaction(TelecomSystem.SyncRoot lock) {
         this(null /* mSubTransactions */, lock);
     }
 
@@ -178,7 +178,7 @@
     }
 
     /**
-     * By default, this processes this transaction. For VoipCallTransactions with sub-transactions,
+     * By default, this processes this transaction. For CallTransaction with sub-transactions,
      * this implementation should be overwritten to handle also processing sub-transactions.
      */
     protected void processTransactions() {
@@ -187,7 +187,7 @@
 
     /**
      * This method is called when the transaction has finished either successfully or exceptionally.
-     * VoipCallTransactions that are extending this class should override this method to clean up
+     * CallTransaction that are extending this class should override this method to clean up
      * any leftover state.
      */
     protected void finishTransaction() {
@@ -199,7 +199,7 @@
                 mTransactionName + "@" + hashCode() + ".sT", mLock);
         CompletableFuture<Void> future = CompletableFuture.completedFuture(null);
         future.thenComposeAsync(this::processTransaction, executor)
-                .thenApplyAsync((Function<VoipCallTransactionResult, Void>) result -> {
+                .thenApplyAsync((Function<CallTransactionResult, Void>) result -> {
                     notifyListenersOfResult(result);
                     return null;
                 }, executor)
@@ -208,14 +208,14 @@
                     // Instead, propagate the failure to the other transactions immediately!
                     String errorMessage = throwable != null ? throwable.getMessage() :
                             "encountered an exception while processing " + mTransactionName;
-                    notifyListenersOfResult(new VoipCallTransactionResult(
+                    notifyListenersOfResult(new CallTransactionResult(
                             CallException.CODE_ERROR_UNKNOWN, errorMessage));
                     Log.e(this, throwable, "Error while executing transaction.");
                     return null;
                 }));
     }
 
-    protected void notifyListenersOfResult(VoipCallTransactionResult result){
+    protected void notifyListenersOfResult(CallTransactionResult result){
         mCompleted.set(true);
         finish(result);
         if (mCompleteListener != null) {
@@ -223,9 +223,9 @@
         }
     }
 
-    protected CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+    protected CompletionStage<CallTransactionResult> processTransaction(Void v) {
         return CompletableFuture.completedFuture(
-                new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_SUCCEED, null));
+                new CallTransactionResult(CallTransactionResult.RESULT_SUCCEED, null));
     }
 
     public final void setCompleteListener(TransactionManager.TransactionCompleteListener listener) {
@@ -248,11 +248,11 @@
         return mHandler;
     }
 
-    public final void finish(VoipCallTransactionResult result) {
+    public final void finish(CallTransactionResult result) {
         finish(false, result);
     }
 
-    private void finish(boolean isTimedOut, VoipCallTransactionResult result) {
+    private void finish(boolean isTimedOut, CallTransactionResult result) {
         if (mStats != null) mStats.markComplete(isTimedOut, result);
         finishTransaction();
         // finish all sub transactions
diff --git a/src/com/android/server/telecom/voip/VoipCallTransactionResult.java b/src/com/android/server/telecom/callsequencing/CallTransactionResult.java
similarity index 66%
rename from src/com/android/server/telecom/voip/VoipCallTransactionResult.java
rename to src/com/android/server/telecom/callsequencing/CallTransactionResult.java
index 50871f2..8b5f5bf 100644
--- a/src/com/android/server/telecom/voip/VoipCallTransactionResult.java
+++ b/src/com/android/server/telecom/callsequencing/CallTransactionResult.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 The Android Open Source Project
+ * 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.
@@ -14,33 +14,38 @@
  * limitations under the License.
  */
 
-package com.android.server.telecom.voip;
+package com.android.server.telecom.callsequencing;
 
 import com.android.server.telecom.Call;
 
 import java.util.Objects;
 
-public class VoipCallTransactionResult {
+public class CallTransactionResult {
     public static final int RESULT_SUCCEED = 0;
+    private static final String VOIP_TRANSACTION_TAG = "VoipCallTransactionResult";
+    private static final String PSTN_TRANSACTION_TAG = "PstnTransactionResult";
 
-    // NOTE: if the VoipCallTransactionResult should not use the RESULT_SUCCEED to represent a
+    // NOTE: if the CallTransactionResult should not use the RESULT_SUCCEED to represent a
     // successful transaction, use an error code defined in the
     // {@link android.telecom.CallException} class
 
     private final int mResult;
     private final String mMessage;
     private final Call mCall;
+    private final String mCallType;
 
-    public VoipCallTransactionResult(int result, String message) {
+    public CallTransactionResult(int result, String message) {
         mResult = result;
         mMessage = message;
         mCall = null;
+        mCallType = "";
     }
 
-    public VoipCallTransactionResult(int result, Call call, String message) {
+    public CallTransactionResult(int result, Call call, String message, boolean isVoip) {
         mResult = result;
         mCall = call;
         mMessage = message;
+        mCallType = isVoip ? VOIP_TRANSACTION_TAG : PSTN_TRANSACTION_TAG;
     }
 
     public int getResult() {
@@ -58,8 +63,8 @@
     @Override
     public boolean equals(Object o) {
         if (this == o) return true;
-        if (!(o instanceof VoipCallTransactionResult)) return false;
-        VoipCallTransactionResult that = (VoipCallTransactionResult) o;
+        if (!(o instanceof CallTransactionResult)) return false;
+        CallTransactionResult that = (CallTransactionResult) o;
         return mResult == that.mResult && Objects.equals(mMessage, that.mMessage);
     }
 
@@ -71,7 +76,9 @@
     @Override
     public String toString() {
         return new StringBuilder().
-                append("{ VoipCallTransactionResult: [mResult: ").
+                append("{ ").
+                append(mCallType).
+                append(": [mResult: ").
                 append(mResult).
                 append("], [mCall: ").
                 append((mCall != null) ? mCall : "null").
diff --git a/src/com/android/server/telecom/callsequencing/CallsManagerCallSequencingAdapter.java b/src/com/android/server/telecom/callsequencing/CallsManagerCallSequencingAdapter.java
new file mode 100644
index 0000000..8410c54
--- /dev/null
+++ b/src/com/android/server/telecom/callsequencing/CallsManagerCallSequencingAdapter.java
@@ -0,0 +1,88 @@
+/*
+ * 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;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallsManager;
+
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * Abstraction layer for CallsManager to perform call sequencing operations through CallsManager
+ * or CallSequencingController, which is controlled by {@link FeatureFlags#enableCallSequencing()}.
+ */
+public class CallsManagerCallSequencingAdapter {
+
+    private final CallsManager mCallsManager;
+    private final CallSequencingController mSequencingController;
+    private final boolean mIsCallSequencingEnabled;
+
+    public CallsManagerCallSequencingAdapter(CallsManager callsManager,
+            CallSequencingController sequencingController,
+            boolean isCallSequencingEnabled) {
+        mCallsManager = callsManager;
+        mSequencingController = sequencingController;
+        mIsCallSequencingEnabled = isCallSequencingEnabled;
+    }
+
+    public void answerCall(Call incomingCall, int videoState) {
+        if (mIsCallSequencingEnabled && !incomingCall.isTransactionalCall()) {
+            mSequencingController.answerCall(incomingCall, videoState);
+        } else {
+            mCallsManager.answerCallOld(incomingCall, videoState);
+        }
+    }
+
+    public void unholdCall(Call call) {
+        if (mIsCallSequencingEnabled) {
+            mSequencingController.unholdCall(call);
+        } else {
+            mCallsManager.unholdCallOld(call);
+        }
+    }
+
+    public void holdCall(Call call) {
+        // Sequencing already taken care of for CSW/TSW in Call class.
+        call.hold();
+    }
+
+    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);
+    }
+
+    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));
+        }
+    }
+
+    private boolean makeRoomForOutgoingCallFlagOff(Call call) {
+        return mCallsManager.makeRoomForOutgoingCall(call);
+    }
+
+    private boolean makeRoomForOutgoingEmergencyCallFlagOff(Call call) {
+        return mCallsManager.makeRoomForOutgoingEmergencyCall(call);
+    }
+}
diff --git a/src/com/android/server/telecom/voip/TransactionManager.java b/src/com/android/server/telecom/callsequencing/TransactionManager.java
similarity index 76%
rename from src/com/android/server/telecom/voip/TransactionManager.java
rename to src/com/android/server/telecom/callsequencing/TransactionManager.java
index 0086d07..a3b3828 100644
--- a/src/com/android/server/telecom/voip/TransactionManager.java
+++ b/src/com/android/server/telecom/callsequencing/TransactionManager.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.server.telecom.voip;
+package com.android.server.telecom.callsequencing;
 
 import static android.telecom.CallException.CODE_OPERATION_TIMED_OUT;
 
@@ -32,18 +32,20 @@
 import java.util.List;
 import java.util.Locale;
 import java.util.Queue;
+import java.util.concurrent.CompletableFuture;
 
 public class TransactionManager {
-    private static final String TAG = "VoipCallTransactionManager";
+    private static final String TAG = "CallTransactionManager";
     private static final int TRANSACTION_HISTORY_SIZE = 20;
     private static TransactionManager INSTANCE = null;
     private static final Object sLock = new Object();
-    private final Queue<VoipCallTransaction> mTransactions;
-    private final Deque<VoipCallTransaction> mCompletedTransactions;
-    private VoipCallTransaction mCurrentTransaction;
+    private final Queue<CallTransaction> mTransactions;
+    private final Deque<CallTransaction> mCompletedTransactions;
+    private CallTransaction mCurrentTransaction;
+    private boolean mProcessingCallSequencing;
 
     public interface TransactionCompleteListener {
-        void onTransactionCompleted(VoipCallTransactionResult result, String transactionName);
+        void onTransactionCompleted(CallTransactionResult result, String transactionName);
         void onTransactionTimeout(String transactionName);
     }
 
@@ -70,28 +72,32 @@
         return new TransactionManager();
     }
 
-    public void addTransaction(VoipCallTransaction transaction,
-            OutcomeReceiver<VoipCallTransactionResult, CallException> receiver) {
+    public CompletableFuture<Boolean> addTransaction(CallTransaction transaction,
+            OutcomeReceiver<CallTransactionResult, CallException> receiver) {
+        CompletableFuture<Boolean> transactionCompleteFuture = new CompletableFuture<>();
         synchronized (sLock) {
             mTransactions.add(transaction);
         }
         transaction.setCompleteListener(new TransactionCompleteListener() {
             @Override
-            public void onTransactionCompleted(VoipCallTransactionResult result,
+            public void onTransactionCompleted(CallTransactionResult result,
                     String transactionName) {
                 Log.i(TAG, String.format("transaction %s completed: with result=[%d]",
                         transactionName, result.getResult()));
                 try {
                     if (result.getResult() == TelecomManager.TELECOM_TRANSACTION_SUCCESS) {
                         receiver.onResult(result);
+                        transactionCompleteFuture.complete(true);
                     } else {
                         receiver.onError(
                                 new CallException(result.getMessage(),
                                         result.getResult()));
+                        transactionCompleteFuture.complete(false);
                     }
                 } catch (Exception e) {
                     Log.e(TAG, String.format("onTransactionCompleted: Notifying transaction result"
                             + " %s resulted in an Exception.", result), e);
+                    transactionCompleteFuture.complete(false);
                 }
                 finishTransaction();
             }
@@ -102,15 +108,18 @@
                 try {
                     receiver.onError(new CallException(transactionName + " timeout",
                             CODE_OPERATION_TIMED_OUT));
+                    transactionCompleteFuture.complete(false);
                 } catch (Exception e) {
                     Log.e(TAG, String.format("onTransactionTimeout: Notifying transaction "
                             + " %s resulted in an Exception.", transactionName), e);
+                    transactionCompleteFuture.complete(false);
                 }
                 finishTransaction();
             }
         });
 
         startTransactions();
+        return transactionCompleteFuture;
     }
 
     private void startTransactions() {
@@ -141,17 +150,17 @@
 
     @VisibleForTesting
     public void clear() {
-        List<VoipCallTransaction> pendingTransactions;
+        List<CallTransaction> pendingTransactions;
         synchronized (sLock) {
             pendingTransactions = new ArrayList<>(mTransactions);
         }
-        for (VoipCallTransaction t : pendingTransactions) {
-            t.finish(new VoipCallTransactionResult(CallException.CODE_ERROR_UNKNOWN
+        for (CallTransaction t : pendingTransactions) {
+            t.finish(new CallTransactionResult(CallException.CODE_ERROR_UNKNOWN
                     /* TODO:: define error b/335703584 */, "clear called"));
         }
     }
 
-    private void addTransactionToHistory(VoipCallTransaction t) {
+    private void addTransactionToHistory(CallTransaction t) {
         if (!Flags.enableCallSequencing()) return;
 
         mCompletedTransactions.add(t);
@@ -160,6 +169,14 @@
         }
     }
 
+    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.
      */
@@ -171,7 +188,7 @@
         synchronized (sLock) {
             pw.println("Pending Transactions:");
             pw.increaseIndent();
-            for (VoipCallTransaction t : mTransactions) {
+            for (CallTransaction t : mTransactions) {
                 printPendingTransactionStats(t, pw);
             }
             pw.decreaseIndent();
@@ -185,7 +202,7 @@
 
             pw.println("Completed Transactions:");
             pw.increaseIndent();
-            for (VoipCallTransaction t : mCompletedTransactions) {
+            for (CallTransaction t : mCompletedTransactions) {
                 printCompleteTransactionStats(t, pw);
             }
             pw.decreaseIndent();
@@ -193,12 +210,12 @@
     }
 
     /**
-     * Recursively print the pending {@link VoipCallTransaction} stats for logging purposes.
+     * Recursively print the pending {@link CallTransaction} stats for logging purposes.
      * @param t The transaction that stats should be printed for
      * @param pw The IndentingPrintWriter to print the result to
      */
-    private void printPendingTransactionStats(VoipCallTransaction t, IndentingPrintWriter pw) {
-        VoipCallTransaction.Stats s = t.getStats();
+    private void printPendingTransactionStats(CallTransaction t, IndentingPrintWriter pw) {
+        CallTransaction.Stats s = t.getStats();
         if (s == null) {
             pw.println(String.format(Locale.getDefault(), "%s: <NO STATS>", t.mTransactionName));
             return;
@@ -215,7 +232,7 @@
             return;
         }
         pw.increaseIndent();
-        for (VoipCallTransaction subTransaction : t.mSubTransactions) {
+        for (CallTransaction subTransaction : t.mSubTransactions) {
             printPendingTransactionStats(subTransaction, pw);
         }
         pw.decreaseIndent();
@@ -226,8 +243,8 @@
      * @param t The transaction that stats should be printed for
      * @param pw The IndentingPrintWriter to print the result to
      */
-    private void printCompleteTransactionStats(VoipCallTransaction t, IndentingPrintWriter pw) {
-        VoipCallTransaction.Stats s = t.getStats();
+    private void printCompleteTransactionStats(CallTransaction t, IndentingPrintWriter pw) {
+        CallTransaction.Stats s = t.getStats();
         if (s == null) {
             pw.println(String.format(Locale.getDefault(), "%s: <NO STATS>", t.mTransactionName));
             return;
@@ -242,16 +259,16 @@
             return;
         }
         pw.increaseIndent();
-        for (VoipCallTransaction subTransaction : t.mSubTransactions) {
+        for (CallTransaction subTransaction : t.mSubTransactions) {
             printCompleteTransactionStats(subTransaction, pw);
         }
         pw.decreaseIndent();
     }
 
-    private String parseTransactionResult(VoipCallTransaction.Stats s) {
+    private String parseTransactionResult(CallTransaction.Stats s) {
         if (s.isTimedOut()) return "TIMED OUT";
         if (s.getTransactionResult() == null) return "PENDING";
-        if (s.getTransactionResult().getResult() == VoipCallTransactionResult.RESULT_SUCCEED) {
+        if (s.getTransactionResult().getResult() == CallTransactionResult.RESULT_SUCCEED) {
             return "SUCCESS";
         }
         return s.getTransactionResult().toString();
diff --git a/src/com/android/server/telecom/callsequencing/TransactionalCallSequencingAdapter.java b/src/com/android/server/telecom/callsequencing/TransactionalCallSequencingAdapter.java
new file mode 100644
index 0000000..7c8bbe4
--- /dev/null
+++ b/src/com/android/server/telecom/callsequencing/TransactionalCallSequencingAdapter.java
@@ -0,0 +1,304 @@
+/*
+ * 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;
+
+import android.os.OutcomeReceiver;
+import android.telecom.CallException;
+import android.telecom.DisconnectCause;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.callsequencing.voip.EndCallTransaction;
+import com.android.server.telecom.callsequencing.voip.HoldCallTransaction;
+import com.android.server.telecom.callsequencing.voip.MaybeHoldCallForNewCallTransaction;
+import com.android.server.telecom.callsequencing.voip.RequestNewActiveCallTransaction;
+import com.android.server.telecom.callsequencing.voip.SerialTransaction;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * Helper adapter class used to centralized code that will be affected by toggling the
+ * {@link Flags#enableCallSequencing()} flag.
+ */
+public class TransactionalCallSequencingAdapter {
+    private final TransactionManager mTransactionManager;
+    private final CallsManager mCallsManager;
+//    private final boolean mIsCallSequencingEnabled;
+
+    public TransactionalCallSequencingAdapter(TransactionManager transactionManager,
+            CallsManager callsManager, boolean isCallSequencingEnabled) {
+        mTransactionManager = transactionManager;
+        mCallsManager = callsManager;
+        // TODO implement call sequencing changes
+//        mIsCallSequencingEnabled = isCallSequencingEnabled;
+    }
+
+    /**
+     * Client -> Server request to set a call active
+     */
+    public void setActive(Call call,
+            OutcomeReceiver<CallTransactionResult, CallException> receiver) {
+        setActiveFlagOff(call, receiver);
+    }
+
+    /**
+     * Client -> Server request to answer a call
+     */
+    public void setAnswered(Call call, int newVideoState,
+            OutcomeReceiver<CallTransactionResult, CallException> receiver) {
+        setAnsweredFlagOff(call, newVideoState, receiver);
+    }
+
+    /**
+     * Client -> Server request to set a call to disconnected
+     */
+    public void setDisconnected(Call call, DisconnectCause dc,
+            OutcomeReceiver<CallTransactionResult, CallException> receiver) {
+        setDisconnectedFlagOff(call, dc, receiver);
+    }
+
+    /**
+     * Client -> Server request to set a call to inactive
+     */
+    public void setInactive(Call call,
+            OutcomeReceiver<CallTransactionResult, CallException> receiver) {
+        setInactiveFlagOff(call, receiver);
+    }
+
+    /**
+     * Server -> Client command to set the call active, which if it fails, will try to reset the
+     * state to what it was before the call was set to active.
+     */
+    public CompletableFuture<Boolean> onSetActive(Call call,
+            CallTransaction clientCbT,
+            OutcomeReceiver<CallTransactionResult, CallException> receiver) {
+        return onSetActiveFlagOff(call, clientCbT, receiver);
+    }
+
+    /**
+     * 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);
+    }
+
+    /**
+     * Server -> Client command to set the call as inactive
+     */
+    public CompletableFuture<Boolean> onSetInactive(Call call,
+            CallTransaction clientCbT,
+            OutcomeReceiver<CallTransactionResult, CallException> receiver) {
+        return onSetInactiveFlagOff(call, clientCbT, receiver);
+    }
+
+    /**
+     * Server -> Client command to disconnect the call
+     */
+    public CompletableFuture<Boolean> onSetDisconnected(Call call,
+            DisconnectCause dc, CallTransaction clientCbT, OutcomeReceiver<CallTransactionResult,
+            CallException> receiver) {
+        return onSetDisconnectedFlagOff(call, dc, clientCbT, receiver);
+    }
+
+    /**
+     * Clean up the calls that have been passed in from CallsManager
+     */
+    public void cleanup(Collection<Call> calls) {
+        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<>();
+
+        // potentially hold the current active call in order to set a new call (active/answered)
+        transactions.add(new MaybeHoldCallForNewCallTransaction(mCallsManager, call,
+                isCallControlRequest));
+        // And request a new focus call update
+        transactions.add(new RequestNewActiveCallTransaction(mCallsManager, call));
+
+        return new SerialTransaction(transactions, mCallsManager.getLock());
+    }
+
+    private void removeCallFromCallsManager(Call call, DisconnectCause cause) {
+        if (cause.getCode() != DisconnectCause.REJECTED) {
+            mCallsManager.markCallAsDisconnected(call, cause);
+        }
+        mCallsManager.removeCall(call);
+    }
+
+    private void maybeResetForegroundCall(Call foregroundCallBeforeSwap, boolean wasActive) {
+        if (foregroundCallBeforeSwap == null) {
+            return;
+        }
+        if (wasActive && !foregroundCallBeforeSwap.isActive()) {
+            mCallsManager.markCallAsActive(foregroundCallBeforeSwap);
+        }
+    }
+    private void cleanupFlagOff(Collection<Call> calls) {
+        for (Call call : calls) {
+            mCallsManager.markCallAsDisconnected(call,
+                    new DisconnectCause(DisconnectCause.ERROR, "process died"));
+            mCallsManager.removeCall(call); // This will clear mTrackedCalls && ClientTWS
+        }
+    }
+}
diff --git a/src/com/android/server/telecom/voip/VerifyCallStateChangeTransaction.java b/src/com/android/server/telecom/callsequencing/VerifyCallStateChangeTransaction.java
similarity index 69%
rename from src/com/android/server/telecom/voip/VerifyCallStateChangeTransaction.java
rename to src/com/android/server/telecom/callsequencing/VerifyCallStateChangeTransaction.java
index 5de4b1d..82b32fb 100644
--- a/src/com/android/server/telecom/voip/VerifyCallStateChangeTransaction.java
+++ b/src/com/android/server/telecom/callsequencing/VerifyCallStateChangeTransaction.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.server.telecom.voip;
+package com.android.server.telecom.callsequencing;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.telecom.Call;
@@ -22,8 +22,11 @@
 
 import android.telecom.Log;
 
+import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionStage;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
 
 /**
  * VerifyCallStateChangeTransaction is a transaction that verifies a CallState change and has
@@ -31,21 +34,22 @@
  * <p>
  * Note: This transaction has a timeout of 2 seconds.
  */
-public class VerifyCallStateChangeTransaction extends VoipCallTransaction {
+public class VerifyCallStateChangeTransaction extends CallTransaction {
     private static final String TAG = VerifyCallStateChangeTransaction.class.getSimpleName();
     private static final long CALL_STATE_TIMEOUT_MILLISECONDS = 2000L;
     private final Call mCall;
-    private final int mTargetCallState;
-    private final CompletableFuture<VoipCallTransactionResult> mTransactionResult =
+    private final Set<Integer> mTargetCallStates;
+    private final CompletableFuture<CallTransactionResult> mTransactionResult =
             new CompletableFuture<>();
 
     private final Call.CallStateListener mCallStateListenerImpl = new Call.CallStateListener() {
         @Override
         public void onCallStateChanged(int newCallState) {
-            Log.d(TAG, "newState=[%d], expectedState=[%d]", newCallState, mTargetCallState);
-            if (newCallState == mTargetCallState) {
-                mTransactionResult.complete(new VoipCallTransactionResult(
-                        VoipCallTransactionResult.RESULT_SUCCEED, TAG));
+            Log.d(TAG, "newState=[%d], possible expected state(s)=[%s]", newCallState,
+                    mTargetCallStates);
+            if (mTargetCallStates.contains(newCallState)) {
+                mTransactionResult.complete(new CallTransactionResult(
+                        CallTransactionResult.RESULT_SUCCEED, TAG));
             }
             // NOTE:: keep listening to the call state until the timeout is reached. It's possible
             // another call state is reached in between...
@@ -53,19 +57,19 @@
     };
 
     public VerifyCallStateChangeTransaction(TelecomSystem.SyncRoot lock,  Call call,
-            int targetCallState) {
+            int... targetCallStates) {
         super(lock, CALL_STATE_TIMEOUT_MILLISECONDS);
         mCall = call;
-        mTargetCallState = targetCallState;
+        mTargetCallStates = IntStream.of(targetCallStates).boxed().collect(Collectors.toSet());;
     }
 
     @Override
-    public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+    public CompletionStage<CallTransactionResult> processTransaction(Void v) {
         Log.d(TAG, "processTransaction:");
         // It's possible the Call is already in the expected call state
         if (isNewCallStateTargetCallState()) {
-            mTransactionResult.complete(new VoipCallTransactionResult(
-                    VoipCallTransactionResult.RESULT_SUCCEED, TAG));
+            mTransactionResult.complete(new CallTransactionResult(
+                    CallTransactionResult.RESULT_SUCCEED, TAG));
             return mTransactionResult;
         }
         mCall.addCallStateListener(mCallStateListenerImpl);
@@ -78,11 +82,11 @@
     }
 
     private boolean isNewCallStateTargetCallState() {
-        return mCall.getState() == mTargetCallState;
+        return mTargetCallStates.contains(mCall.getState());
     }
 
     @VisibleForTesting
-    public CompletableFuture<VoipCallTransactionResult> getTransactionResult() {
+    public CompletableFuture<CallTransactionResult> getTransactionResult() {
         return mTransactionResult;
     }
 
diff --git a/src/com/android/server/telecom/voip/CallEventCallbackAckTransaction.java b/src/com/android/server/telecom/callsequencing/voip/CallEventCallbackAckTransaction.java
similarity index 90%
rename from src/com/android/server/telecom/voip/CallEventCallbackAckTransaction.java
rename to src/com/android/server/telecom/callsequencing/voip/CallEventCallbackAckTransaction.java
index 9e140a7..802ea7e 100644
--- a/src/com/android/server/telecom/voip/CallEventCallbackAckTransaction.java
+++ b/src/com/android/server/telecom/callsequencing/voip/CallEventCallbackAckTransaction.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.server.telecom.voip;
+package com.android.server.telecom.callsequencing.voip;
 
 import static android.telecom.TelecomManager.TELECOM_TRANSACTION_SUCCESS;
 import static android.telecom.CallException.CODE_OPERATION_TIMED_OUT;
@@ -29,6 +29,8 @@
 import com.android.internal.telecom.ICallEventCallback;
 import com.android.server.telecom.TelecomSystem;
 import com.android.server.telecom.TransactionalServiceWrapper;
+import com.android.server.telecom.callsequencing.CallTransaction;
+import com.android.server.telecom.callsequencing.CallTransactionResult;
 
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionStage;
@@ -39,7 +41,7 @@
  * SRP: using the ICallEventCallback binder, reach out to the client for the pending call event and
  * get an acknowledgement that the call event can be completed.
  */
-public class CallEventCallbackAckTransaction extends VoipCallTransaction {
+public class CallEventCallbackAckTransaction extends CallTransaction {
     private static final String TAG = CallEventCallbackAckTransaction.class.getSimpleName();
     private final ICallEventCallback mICallEventCallback;
     private final String mAction;
@@ -48,7 +50,7 @@
     private int mVideoState = CallAttributes.AUDIO_CALL;
     private DisconnectCause mDisconnectCause = null;
 
-    private final VoipCallTransactionResult TRANSACTION_FAILED = new VoipCallTransactionResult(
+    private final CallTransactionResult TRANSACTION_FAILED = new CallTransactionResult(
             CODE_OPERATION_TIMED_OUT, "failed to complete the operation before timeout");
 
     private static class AckResultReceiver extends ResultReceiver {
@@ -96,7 +98,7 @@
 
 
     @Override
-    public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+    public CompletionStage<CallTransactionResult> processTransaction(Void v) {
         Log.d(TAG, "processTransaction");
         CountDownLatch latch = new CountDownLatch(1);
         ResultReceiver receiver = new AckResultReceiver(latch);
@@ -134,7 +136,7 @@
             } else {
                 // success
                 return CompletableFuture.completedFuture(
-                        new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_SUCCEED,
+                        new CallTransactionResult(CallTransactionResult.RESULT_SUCCEED,
                                 "success"));
             }
         } catch (InterruptedException ie) {
diff --git a/src/com/android/server/telecom/voip/EndCallTransaction.java b/src/com/android/server/telecom/callsequencing/voip/EndCallTransaction.java
similarity index 82%
rename from src/com/android/server/telecom/voip/EndCallTransaction.java
rename to src/com/android/server/telecom/callsequencing/voip/EndCallTransaction.java
index 0cb7458..b4c92fe 100644
--- a/src/com/android/server/telecom/voip/EndCallTransaction.java
+++ b/src/com/android/server/telecom/callsequencing/voip/EndCallTransaction.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.server.telecom.voip;
+package com.android.server.telecom.callsequencing.voip;
 
 import android.telecom.DisconnectCause;
 import android.util.Log;
@@ -22,6 +22,8 @@
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallState;
 import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.callsequencing.CallTransaction;
+import com.android.server.telecom.callsequencing.CallTransactionResult;
 
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionStage;
@@ -29,7 +31,7 @@
 /**
  * This transaction should only be created for a CallControl action.
  */
-public class EndCallTransaction extends VoipCallTransaction {
+public class EndCallTransaction extends CallTransaction {
     private static final String TAG = EndCallTransaction.class.getSimpleName();
     private final CallsManager mCallsManager;
     private final Call mCall;
@@ -43,7 +45,7 @@
     }
 
     @Override
-    public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+    public CompletionStage<CallTransactionResult> processTransaction(Void v) {
         int code = mCause.getCode();
         Log.d(TAG, String.format("processTransaction: mCode=[%d], mCall=[%s]", code, mCall));
 
@@ -56,7 +58,7 @@
         mCallsManager.markCallAsRemoved(mCall);
 
         return CompletableFuture.completedFuture(
-                new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_SUCCEED,
+                new CallTransactionResult(CallTransactionResult.RESULT_SUCCEED,
                         "EndCallTransaction: RESULT_SUCCEED"));
     }
 }
diff --git a/src/com/android/server/telecom/voip/EndpointChangeTransaction.java b/src/com/android/server/telecom/callsequencing/voip/EndpointChangeTransaction.java
similarity index 75%
rename from src/com/android/server/telecom/voip/EndpointChangeTransaction.java
rename to src/com/android/server/telecom/callsequencing/voip/EndpointChangeTransaction.java
index 6841fcf..46678da 100644
--- a/src/com/android/server/telecom/voip/EndpointChangeTransaction.java
+++ b/src/com/android/server/telecom/callsequencing/voip/EndpointChangeTransaction.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.server.telecom.voip;
+package com.android.server.telecom.callsequencing.voip;
 
 import android.os.Bundle;
 import android.os.ResultReceiver;
@@ -23,11 +23,13 @@
 import android.util.Log;
 
 import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.callsequencing.CallTransaction;
+import com.android.server.telecom.callsequencing.CallTransactionResult;
 
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionStage;
 
-public class EndpointChangeTransaction extends VoipCallTransaction {
+public class EndpointChangeTransaction extends CallTransaction {
     private static final String TAG = EndpointChangeTransaction.class.getSimpleName();
     private final CallEndpoint mCallEndpoint;
     private final CallsManager mCallsManager;
@@ -39,19 +41,19 @@
     }
 
     @Override
-    public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+    public CompletionStage<CallTransactionResult> processTransaction(Void v) {
         Log.i(TAG, "processTransaction");
-        CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
+        CompletableFuture<CallTransactionResult> future = new CompletableFuture<>();
         mCallsManager.requestCallEndpointChange(mCallEndpoint, new ResultReceiver(null) {
             @Override
             protected void onReceiveResult(int resultCode, Bundle resultData) {
                 Log.i(TAG, "processTransaction: code=" + resultCode);
                 if (resultCode == CallEndpoint.ENDPOINT_OPERATION_SUCCESS) {
-                    future.complete(new VoipCallTransactionResult(
-                            VoipCallTransactionResult.RESULT_SUCCEED, null));
+                    future.complete(new CallTransactionResult(
+                            CallTransactionResult.RESULT_SUCCEED, null));
                 } else {
                     // TODO:: define errors in CallException class. b/335703584
-                    future.complete(new VoipCallTransactionResult(
+                    future.complete(new CallTransactionResult(
                             CallException.CODE_ERROR_UNKNOWN, null));
                 }
             }
diff --git a/src/com/android/server/telecom/voip/HoldCallTransaction.java b/src/com/android/server/telecom/callsequencing/voip/HoldCallTransaction.java
similarity index 72%
rename from src/com/android/server/telecom/voip/HoldCallTransaction.java
rename to src/com/android/server/telecom/callsequencing/voip/HoldCallTransaction.java
index 6c4e8b7..2fa7ff7 100644
--- a/src/com/android/server/telecom/voip/HoldCallTransaction.java
+++ b/src/com/android/server/telecom/callsequencing/voip/HoldCallTransaction.java
@@ -14,18 +14,20 @@
  * limitations under the License.
  */
 
-package com.android.server.telecom.voip;
+package com.android.server.telecom.callsequencing.voip;
 
 import android.telecom.CallException;
 import android.util.Log;
 
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.callsequencing.CallTransaction;
+import com.android.server.telecom.callsequencing.CallTransactionResult;
 
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionStage;
 
-public class HoldCallTransaction extends VoipCallTransaction {
+public class HoldCallTransaction extends CallTransaction {
 
     private static final String TAG = HoldCallTransaction.class.getSimpleName();
     private final CallsManager mCallsManager;
@@ -38,17 +40,17 @@
     }
 
     @Override
-    public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+    public CompletionStage<CallTransactionResult> processTransaction(Void v) {
         Log.d(TAG, "processTransaction");
-        CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
+        CompletableFuture<CallTransactionResult> future = new CompletableFuture<>();
 
         if (mCallsManager.canHold(mCall)) {
             mCallsManager.markCallAsOnHold(mCall);
-            future.complete(new VoipCallTransactionResult(
-                    VoipCallTransactionResult.RESULT_SUCCEED, null));
+            future.complete(new CallTransactionResult(
+                    CallTransactionResult.RESULT_SUCCEED, null));
         } else {
             Log.d(TAG, "processTransaction: onError");
-            future.complete(new VoipCallTransactionResult(
+            future.complete(new CallTransactionResult(
                     CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL, "cannot hold call"));
         }
         return future;
diff --git a/src/com/android/server/telecom/voip/IncomingCallTransaction.java b/src/com/android/server/telecom/callsequencing/voip/IncomingCallTransaction.java
similarity index 85%
rename from src/com/android/server/telecom/voip/IncomingCallTransaction.java
rename to src/com/android/server/telecom/callsequencing/voip/IncomingCallTransaction.java
index ed0c7d6..31ce303 100644
--- a/src/com/android/server/telecom/voip/IncomingCallTransaction.java
+++ b/src/com/android/server/telecom/callsequencing/voip/IncomingCallTransaction.java
@@ -14,12 +14,13 @@
  * limitations under the License.
  */
 
-package com.android.server.telecom.voip;
+package com.android.server.telecom.callsequencing.voip;
 
 import static android.telecom.CallAttributes.CALL_CAPABILITIES_KEY;
 import static android.telecom.CallAttributes.DISPLAY_NAME_KEY;
 
-import static com.android.server.telecom.voip.VideoStateTranslation.TransactionalVideoStateToVideoProfileState;
+import static com.android.server.telecom.callsequencing.voip.VideoStateTranslation
+        .TransactionalVideoStateToVideoProfileState;
 
 import android.os.Bundle;
 import android.telecom.CallAttributes;
@@ -30,12 +31,14 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallsManager;
+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 IncomingCallTransaction extends VoipCallTransaction {
+public class IncomingCallTransaction extends CallTransaction {
 
     private static final String TAG = IncomingCallTransaction.class.getSimpleName();
     private final String mCallId;
@@ -64,7 +67,7 @@
     }
 
     @Override
-    public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+    public CompletionStage<CallTransactionResult> processTransaction(Void v) {
         Log.d(TAG, "processTransaction");
 
         if (mCallsManager.isIncomingCallPermitted(mCallAttributes.getPhoneAccountHandle())) {
@@ -75,13 +78,13 @@
                     generateExtras(mCallAttributes), false);
 
             return CompletableFuture.completedFuture(
-                    new VoipCallTransactionResult(
-                            VoipCallTransactionResult.RESULT_SUCCEED, call, "success"));
+                    new CallTransactionResult(
+                            CallTransactionResult.RESULT_SUCCEED, call, "success", true));
         } else {
             Log.d(TAG, "processTransaction: incoming call is not permitted at this time");
 
             return CompletableFuture.completedFuture(
-                    new VoipCallTransactionResult(
+                    new CallTransactionResult(
                             CallException.CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME,
                             "incoming call not permitted at the current time"));
         }
diff --git a/src/com/android/server/telecom/voip/MaybeHoldCallForNewCallTransaction.java b/src/com/android/server/telecom/callsequencing/voip/MaybeHoldCallForNewCallTransaction.java
similarity index 74%
rename from src/com/android/server/telecom/voip/MaybeHoldCallForNewCallTransaction.java
rename to src/com/android/server/telecom/callsequencing/voip/MaybeHoldCallForNewCallTransaction.java
index 3bed088..32062b5 100644
--- a/src/com/android/server/telecom/voip/MaybeHoldCallForNewCallTransaction.java
+++ b/src/com/android/server/telecom/callsequencing/voip/MaybeHoldCallForNewCallTransaction.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.server.telecom.voip;
+package com.android.server.telecom.callsequencing.voip;
 
 import android.os.OutcomeReceiver;
 import android.telecom.CallException;
@@ -22,15 +22,17 @@
 
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.callsequencing.CallTransaction;
+import com.android.server.telecom.callsequencing.CallTransactionResult;
 
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionStage;
 
 /**
- * This VoipCallTransaction is responsible for holding any active call in favor of a new call
+ * This VOIP CallTransaction is responsible for holding any active call in favor of a new call
  * request. If the active call cannot be held or disconnected, the transaction will fail.
  */
-public class MaybeHoldCallForNewCallTransaction extends VoipCallTransaction {
+public class MaybeHoldCallForNewCallTransaction extends CallTransaction {
 
     private static final String TAG = MaybeHoldCallForNewCallTransaction.class.getSimpleName();
     private final CallsManager mCallsManager;
@@ -46,23 +48,23 @@
     }
 
     @Override
-    public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+    public CompletionStage<CallTransactionResult> processTransaction(Void v) {
         Log.d(TAG, "processTransaction");
-        CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
+        CompletableFuture<CallTransactionResult> future = new CompletableFuture<>();
 
         mCallsManager.transactionHoldPotentialActiveCallForNewCall(mCall, mIsCallControlRequest,
                 new OutcomeReceiver<>() {
             @Override
             public void onResult(Boolean result) {
                 Log.d(TAG, "processTransaction: onResult");
-                future.complete(new VoipCallTransactionResult(
-                        VoipCallTransactionResult.RESULT_SUCCEED, null));
+                future.complete(new CallTransactionResult(
+                        CallTransactionResult.RESULT_SUCCEED, null));
             }
 
             @Override
             public void onError(CallException exception) {
                 Log.d(TAG, "processTransaction: onError");
-                future.complete(new VoipCallTransactionResult(
+                future.complete(new CallTransactionResult(
                        exception.getCode(), exception.getMessage()));
             }
         });
diff --git a/src/com/android/server/telecom/voip/OutgoingCallTransaction.java b/src/com/android/server/telecom/callsequencing/voip/OutgoingCallTransaction.java
similarity index 87%
rename from src/com/android/server/telecom/voip/OutgoingCallTransaction.java
rename to src/com/android/server/telecom/callsequencing/voip/OutgoingCallTransaction.java
index 68ffecf..572de55 100644
--- a/src/com/android/server/telecom/voip/OutgoingCallTransaction.java
+++ b/src/com/android/server/telecom/callsequencing/voip/OutgoingCallTransaction.java
@@ -14,14 +14,15 @@
  * limitations under the License.
  */
 
-package com.android.server.telecom.voip;
+package com.android.server.telecom.callsequencing.voip;
 
 import static android.Manifest.permission.CALL_PRIVILEGED;
 import static android.telecom.CallAttributes.CALL_CAPABILITIES_KEY;
 import static android.telecom.CallAttributes.DISPLAY_NAME_KEY;
 import static android.telecom.CallException.CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME;
 
-import static com.android.server.telecom.voip.VideoStateTranslation.TransactionalVideoStateToVideoProfileState;
+import static com.android.server.telecom.callsequencing.voip.VideoStateTranslation
+        .TransactionalVideoStateToVideoProfileState;
 
 import android.content.Context;
 import android.content.Intent;
@@ -35,12 +36,14 @@
 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 OutgoingCallTransaction extends VoipCallTransaction {
+public class OutgoingCallTransaction extends CallTransaction {
 
     private static final String TAG = OutgoingCallTransaction.class.getSimpleName();
     private final String mCallId;
@@ -73,7 +76,7 @@
     }
 
     @Override
-    public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+    public CompletionStage<CallTransactionResult> processTransaction(Void v) {
         Log.d(TAG, "processTransaction");
 
         final boolean hasCallPrivilegedPermission = mContext.checkCallingPermission(
@@ -95,11 +98,11 @@
 
             if (callFuture == null) {
                 return CompletableFuture.completedFuture(
-                        new VoipCallTransactionResult(
+                        new CallTransactionResult(
                                 CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME,
                                 "incoming call not permitted at the current time"));
             }
-            CompletionStage<VoipCallTransactionResult> result = callFuture.thenComposeAsync(
+            CompletionStage<CallTransactionResult> result = callFuture.thenComposeAsync(
                     (call) -> {
 
                         Log.d(TAG, "processTransaction: completing future");
@@ -107,7 +110,7 @@
                         if (call == null) {
                             Log.d(TAG, "processTransaction: call is null");
                             return CompletableFuture.completedFuture(
-                                    new VoipCallTransactionResult(
+                                    new CallTransactionResult(
                                             CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME,
                                             "call could not be created at this time"));
                         } else {
@@ -121,16 +124,16 @@
                         }
 
                         return CompletableFuture.completedFuture(
-                                new VoipCallTransactionResult(
-                                        VoipCallTransactionResult.RESULT_SUCCEED,
-                                        call, null));
+                                new CallTransactionResult(
+                                        CallTransactionResult.RESULT_SUCCEED,
+                                        call, null, true));
                     }
                     , new LoggedHandlerExecutor(mHandler, "OCT.pT", null));
 
             return result;
         } else {
             return CompletableFuture.completedFuture(
-                    new VoipCallTransactionResult(
+                    new CallTransactionResult(
                             CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME,
                             "incoming call not permitted at the current time"));
 
diff --git a/src/com/android/server/telecom/voip/ParallelTransaction.java b/src/com/android/server/telecom/callsequencing/voip/ParallelTransaction.java
similarity index 79%
rename from src/com/android/server/telecom/voip/ParallelTransaction.java
rename to src/com/android/server/telecom/callsequencing/voip/ParallelTransaction.java
index e235ead..77e93f9 100644
--- a/src/com/android/server/telecom/voip/ParallelTransaction.java
+++ b/src/com/android/server/telecom/callsequencing/voip/ParallelTransaction.java
@@ -14,22 +14,25 @@
  * limitations under the License.
  */
 
-package com.android.server.telecom.voip;
+package com.android.server.telecom.callsequencing.voip;
 
 import android.telecom.CallException;
 
 import com.android.server.telecom.LoggedHandlerExecutor;
 import com.android.server.telecom.TelecomSystem;
+import com.android.server.telecom.callsequencing.CallTransaction;
+import com.android.server.telecom.callsequencing.CallTransactionResult;
+import com.android.server.telecom.callsequencing.TransactionManager;
 
 import java.util.List;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.atomic.AtomicInteger;
 
 /**
- * A VoipCallTransaction implementation that its sub transactions will be executed in parallel
+ * A CallTransaction implementation that its sub transactions will be executed in parallel
  */
-public class ParallelTransaction extends VoipCallTransaction {
-    public ParallelTransaction(List<VoipCallTransaction> subTransactions,
+public class ParallelTransaction extends CallTransaction {
+    public ParallelTransaction(List<CallTransaction> subTransactions,
             TelecomSystem.SyncRoot lock) {
         super(subTransactions, lock);
     }
@@ -45,9 +48,9 @@
                     private final AtomicInteger mCount = new AtomicInteger(mSubTransactions.size());
 
                     @Override
-                    public void onTransactionCompleted(VoipCallTransactionResult result,
+                    public void onTransactionCompleted(CallTransactionResult result,
                             String transactionName) {
-                        if (result.getResult() != VoipCallTransactionResult.RESULT_SUCCEED) {
+                        if (result.getResult() != CallTransactionResult.RESULT_SUCCEED) {
                             CompletableFuture.completedFuture(null).thenApplyAsync(
                                     (x) -> {
                                         finish(result);
@@ -68,8 +71,8 @@
                     public void onTransactionTimeout(String transactionName) {
                         CompletableFuture.completedFuture(null).thenApplyAsync(
                                 (x) -> {
-                                    VoipCallTransactionResult mainResult =
-                                            new VoipCallTransactionResult(
+                                    CallTransactionResult mainResult =
+                                            new CallTransactionResult(
                                                     CallException.CODE_OPERATION_TIMED_OUT,
                                             String.format("sub transaction %s timed out",
                                                     transactionName));
@@ -82,7 +85,7 @@
                                                 + ".oTT", mLock));
                     }
                 };
-        for (VoipCallTransaction transaction : mSubTransactions) {
+        for (CallTransaction transaction : mSubTransactions) {
             transaction.setCompleteListener(subTransactionListener);
             transaction.start();
         }
diff --git a/src/com/android/server/telecom/voip/RequestNewActiveCallTransaction.java b/src/com/android/server/telecom/callsequencing/voip/RequestNewActiveCallTransaction.java
similarity index 83%
rename from src/com/android/server/telecom/voip/RequestNewActiveCallTransaction.java
rename to src/com/android/server/telecom/callsequencing/voip/RequestNewActiveCallTransaction.java
index e3aed8e..8e6e354 100644
--- a/src/com/android/server/telecom/voip/RequestNewActiveCallTransaction.java
+++ b/src/com/android/server/telecom/callsequencing/voip/RequestNewActiveCallTransaction.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.server.telecom.voip;
+package com.android.server.telecom.callsequencing.voip;
 
 import android.os.OutcomeReceiver;
 import android.telecom.CallException;
@@ -24,6 +24,8 @@
 import com.android.server.telecom.CallState;
 import com.android.server.telecom.CallsManager;
 import com.android.server.telecom.ConnectionServiceFocusManager;
+import com.android.server.telecom.callsequencing.CallTransaction;
+import com.android.server.telecom.callsequencing.CallTransactionResult;
 import com.android.server.telecom.flags.Flags;
 
 import java.util.concurrent.CompletableFuture;
@@ -42,7 +44,7 @@
  * - MaybeHoldCallForNewCallTransaction was performed before this so any potential active calls
  * should be held now.
  */
-public class RequestNewActiveCallTransaction extends VoipCallTransaction {
+public class RequestNewActiveCallTransaction extends CallTransaction {
 
     private static final String TAG = RequestNewActiveCallTransaction.class.getSimpleName();
     private final CallsManager mCallsManager;
@@ -55,14 +57,14 @@
     }
 
     @Override
-    public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+    public CompletionStage<CallTransactionResult> processTransaction(Void v) {
         Log.d(TAG, "processTransaction");
-        CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
+        CompletableFuture<CallTransactionResult> future = new CompletableFuture<>();
         int currentCallState = mCall.getState();
 
         // certain calls cannot go active/answered (ex. disconnect calls, etc.)
         if (!canBecomeNewCallFocus(currentCallState)) {
-            future.complete(new VoipCallTransactionResult(
+            future.complete(new CallTransactionResult(
                     CallException.CODE_CALL_CANNOT_BE_SET_TO_ACTIVE,
                     "CallState cannot be set to active or answered due to current call"
                             + " state being in invalid state"));
@@ -71,7 +73,7 @@
 
         if (!Flags.transactionalHoldDisconnectsUnholdable() &&
                 mCallsManager.getActiveCall() != null) {
-            future.complete(new VoipCallTransactionResult(
+            future.complete(new CallTransactionResult(
                     CallException.CODE_CALL_CANNOT_BE_SET_TO_ACTIVE,
                     "Already an active call. Request hold on current active call."));
             return future;
@@ -81,14 +83,14 @@
                     @Override
                     public void onResult(Boolean result) {
                         Log.d(TAG, "processTransaction: onResult");
-                        future.complete(new VoipCallTransactionResult(
-                                VoipCallTransactionResult.RESULT_SUCCEED, null));
+                        future.complete(new CallTransactionResult(
+                                CallTransactionResult.RESULT_SUCCEED, null));
                     }
 
                     @Override
                     public void onError(CallException exception) {
                         Log.d(TAG, "processTransaction: onError");
-                        future.complete(new VoipCallTransactionResult(
+                        future.complete(new CallTransactionResult(
                                 exception.getCode(), exception.getMessage()));
                     }
                 });
diff --git a/src/com/android/server/telecom/voip/RequestVideoStateTransaction.java b/src/com/android/server/telecom/callsequencing/voip/RequestVideoStateTransaction.java
similarity index 73%
rename from src/com/android/server/telecom/voip/RequestVideoStateTransaction.java
rename to src/com/android/server/telecom/callsequencing/voip/RequestVideoStateTransaction.java
index c1bc343..6fb1836 100644
--- a/src/com/android/server/telecom/voip/RequestVideoStateTransaction.java
+++ b/src/com/android/server/telecom/callsequencing/voip/RequestVideoStateTransaction.java
@@ -14,9 +14,10 @@
  * limitations under the License.
  */
 
-package com.android.server.telecom.voip;
+package com.android.server.telecom.callsequencing.voip;
 
-import static com.android.server.telecom.voip.VideoStateTranslation.TransactionalVideoStateToVideoProfileState;
+import static com.android.server.telecom.callsequencing.voip.VideoStateTranslation
+        .TransactionalVideoStateToVideoProfileState;
 
 import android.telecom.CallException;
 import android.telecom.VideoProfile;
@@ -24,11 +25,13 @@
 
 import com.android.server.telecom.CallsManager;
 import com.android.server.telecom.Call;
+import com.android.server.telecom.callsequencing.CallTransaction;
+import com.android.server.telecom.callsequencing.CallTransactionResult;
 
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionStage;
 
-public class RequestVideoStateTransaction extends VoipCallTransaction {
+public class RequestVideoStateTransaction extends CallTransaction {
 
     private static final String TAG = RequestVideoStateTransaction.class.getSimpleName();
     private final Call mCall;
@@ -42,19 +45,19 @@
     }
 
     @Override
-    public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+    public CompletionStage<CallTransactionResult> processTransaction(Void v) {
         Log.d(TAG, "processTransaction");
-        CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
+        CompletableFuture<CallTransactionResult> future = new CompletableFuture<>();
 
         if (isRequestingVideoTransmission(mVideoProfileState) &&
                 !mCall.isVideoCallingSupportedByPhoneAccount()) {
-            future.complete(new VoipCallTransactionResult(
+            future.complete(new CallTransactionResult(
                     CallException.CODE_ERROR_UNKNOWN /*TODO:: define error code. b/335703584 */,
                     "Video calling is not supported by the target account"));
         } else {
             mCall.setVideoState(mVideoProfileState);
-            future.complete(new VoipCallTransactionResult(
-                    VoipCallTransactionResult.RESULT_SUCCEED,
+            future.complete(new CallTransactionResult(
+                    CallTransactionResult.RESULT_SUCCEED,
                     "The Video State was changed successfully"));
         }
         return future;
diff --git a/src/com/android/server/telecom/voip/SerialTransaction.java b/src/com/android/server/telecom/callsequencing/voip/SerialTransaction.java
similarity index 79%
rename from src/com/android/server/telecom/voip/SerialTransaction.java
rename to src/com/android/server/telecom/callsequencing/voip/SerialTransaction.java
index 748f285..d5d75d0 100644
--- a/src/com/android/server/telecom/voip/SerialTransaction.java
+++ b/src/com/android/server/telecom/callsequencing/voip/SerialTransaction.java
@@ -14,27 +14,30 @@
  * limitations under the License.
  */
 
-package com.android.server.telecom.voip;
+package com.android.server.telecom.callsequencing.voip;
 
 import android.telecom.CallException;
 
 import com.android.server.telecom.LoggedHandlerExecutor;
 import com.android.server.telecom.TelecomSystem;
+import com.android.server.telecom.callsequencing.CallTransaction;
+import com.android.server.telecom.callsequencing.CallTransactionResult;
+import com.android.server.telecom.callsequencing.TransactionManager;
 
 import java.util.List;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.atomic.AtomicInteger;
 
 /**
- * A VoipCallTransaction implementation that its sub transactions will be executed in serial
+ * A CallTransaction implementation that its sub transactions will be executed in serial
  */
-public class SerialTransaction extends VoipCallTransaction {
-    public SerialTransaction(List<VoipCallTransaction> subTransactions,
+public class SerialTransaction extends CallTransaction {
+    public SerialTransaction(List<CallTransaction> subTransactions,
             TelecomSystem.SyncRoot lock) {
         super(subTransactions, lock);
     }
 
-    public void appendTransaction(VoipCallTransaction transaction){
+    public void appendTransaction(CallTransaction transaction){
         mSubTransactions.add(transaction);
     }
 
@@ -49,9 +52,9 @@
                     private final AtomicInteger mTransactionIndex = new AtomicInteger(0);
 
                     @Override
-                    public void onTransactionCompleted(VoipCallTransactionResult result,
+                    public void onTransactionCompleted(CallTransactionResult result,
                             String transactionName) {
-                        if (result.getResult() != VoipCallTransactionResult.RESULT_SUCCEED) {
+                        if (result.getResult() != CallTransactionResult.RESULT_SUCCEED) {
                             handleTransactionFailure();
                             CompletableFuture.completedFuture(null).thenApplyAsync(
                                     (x) -> {
@@ -65,7 +68,7 @@
                         } else {
                             int currTransactionIndex = mTransactionIndex.incrementAndGet();
                             if (currTransactionIndex < mSubTransactions.size()) {
-                                VoipCallTransaction transaction = mSubTransactions.get(
+                                CallTransaction transaction = mSubTransactions.get(
                                         currTransactionIndex);
                                 transaction.setCompleteListener(this);
                                 transaction.start();
@@ -80,8 +83,8 @@
                         handleTransactionFailure();
                         CompletableFuture.completedFuture(null).thenApplyAsync(
                                 (x) -> {
-                                    VoipCallTransactionResult mainResult =
-                                            new VoipCallTransactionResult(
+                                    CallTransactionResult mainResult =
+                                            new CallTransactionResult(
                                                     CallException.CODE_OPERATION_TIMED_OUT,
                                             String.format("sub transaction %s timed out",
                                                     transactionName));
@@ -94,7 +97,7 @@
                                                 + ".oTT", mLock));
                     }
                 };
-        VoipCallTransaction transaction = mSubTransactions.get(0);
+        CallTransaction transaction = mSubTransactions.get(0);
         transaction.setCompleteListener(subTransactionListener);
         transaction.start();
 
diff --git a/src/com/android/server/telecom/voip/SetMuteStateTransaction.java b/src/com/android/server/telecom/callsequencing/voip/SetMuteStateTransaction.java
similarity index 73%
rename from src/com/android/server/telecom/voip/SetMuteStateTransaction.java
rename to src/com/android/server/telecom/callsequencing/voip/SetMuteStateTransaction.java
index d9f7329..14f8945 100644
--- a/src/com/android/server/telecom/voip/SetMuteStateTransaction.java
+++ b/src/com/android/server/telecom/callsequencing/voip/SetMuteStateTransaction.java
@@ -14,11 +14,13 @@
  * limitations under the License.
  */
 
-package com.android.server.telecom.voip;
+package com.android.server.telecom.callsequencing.voip;
 
 import android.util.Log;
 
 import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.callsequencing.CallTransaction;
+import com.android.server.telecom.callsequencing.CallTransactionResult;
 
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionStage;
@@ -27,7 +29,7 @@
  * This transaction should be used to change the global mute state for transactional
  * calls. There is currently no way for this transaction to fail.
  */
-public class SetMuteStateTransaction extends VoipCallTransaction {
+public class SetMuteStateTransaction extends CallTransaction {
 
     private static final String TAG = SetMuteStateTransaction.class.getSimpleName();
     private final CallsManager mCallsManager;
@@ -40,14 +42,14 @@
     }
 
     @Override
-    public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+    public CompletionStage<CallTransactionResult> processTransaction(Void v) {
         Log.d(TAG, "processTransaction");
-        CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
+        CompletableFuture<CallTransactionResult> future = new CompletableFuture<>();
 
         mCallsManager.mute(mIsMuted);
 
-        future.complete(new VoipCallTransactionResult(
-                VoipCallTransactionResult.RESULT_SUCCEED,
+        future.complete(new CallTransactionResult(
+                CallTransactionResult.RESULT_SUCCEED,
                 "The Mute State was changed successfully"));
 
         return future;
diff --git a/src/com/android/server/telecom/voip/VideoStateTranslation.java b/src/com/android/server/telecom/callsequencing/voip/VideoStateTranslation.java
similarity index 95%
rename from src/com/android/server/telecom/voip/VideoStateTranslation.java
rename to src/com/android/server/telecom/callsequencing/voip/VideoStateTranslation.java
index 3812d15..4610f96 100644
--- a/src/com/android/server/telecom/voip/VideoStateTranslation.java
+++ b/src/com/android/server/telecom/callsequencing/voip/VideoStateTranslation.java
@@ -14,17 +14,12 @@
  * limitations under the License.
  */
 
-package com.android.server.telecom.voip;
+package com.android.server.telecom.callsequencing.voip;
 
 import android.telecom.CallAttributes;
 import android.telecom.Log;
 import android.telecom.VideoProfile;
 
-import com.android.server.telecom.AnomalyReporterAdapter;
-import com.android.server.telecom.AnomalyReporterAdapterImpl;
-
-import java.util.UUID;
-
 /**
  * This remapping class is needed because {@link VideoProfile} has more fine grain levels of video
  * states as apposed to Transactional video states (defined in  {@link CallAttributes.CallType}.
diff --git a/src/com/android/server/telecom/voip/VoipCallMonitor.java b/src/com/android/server/telecom/callsequencing/voip/VoipCallMonitor.java
similarity index 99%
rename from src/com/android/server/telecom/voip/VoipCallMonitor.java
rename to src/com/android/server/telecom/callsequencing/voip/VoipCallMonitor.java
index 8f6ad51..1d1a1a6 100644
--- a/src/com/android/server/telecom/voip/VoipCallMonitor.java
+++ b/src/com/android/server/telecom/callsequencing/voip/VoipCallMonitor.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.server.telecom.voip;
+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;
diff --git a/src/com/android/server/telecom/metrics/ApiStats.java b/src/com/android/server/telecom/metrics/ApiStats.java
index b37569f..f0b5dc7 100644
--- a/src/com/android/server/telecom/metrics/ApiStats.java
+++ b/src/com/android/server/telecom/metrics/ApiStats.java
@@ -18,10 +18,12 @@
 
 import static com.android.server.telecom.TelecomStatsLog.TELECOM_API_STATS;
 
+import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.app.StatsManager;
 import android.content.Context;
 import android.os.Looper;
+import android.telecom.Log;
 import android.util.StatsEvent;
 
 import androidx.annotation.VisibleForTesting;
@@ -29,6 +31,8 @@
 import com.android.server.telecom.TelecomStatsLog;
 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;
@@ -36,9 +40,134 @@
 import java.util.Objects;
 
 public class ApiStats extends TelecomPulledAtom {
-
+    public static final int API_UNSPECIFIC = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_UNSPECIFIED;
+    public static final int API_ACCEPTHANDOVER = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_ACCEPT_HANDOVER;
+    public static final int API_ACCEPTRINGINGCALL = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_ACCEPT_RINGING_CALL;
+    public static final int API_ACCEPTRINGINGCALLWITHVIDEOSTATE = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_ACCEPT_RINGING_CALL_WITH_VIDEO_STATE;
+    public static final int API_ADDCALL = TelecomStatsLog.TELECOM_API_STATS__API_NAME__API_ADD_CALL;
+    public static final int API_ADDNEWINCOMINGCALL = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_ADD_NEW_INCOMING_CALL;
+    public static final int API_ADDNEWINCOMINGCONFERENCE = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_ADD_NEW_INCOMING_CONFERENCE;
+    public static final int API_ADDNEWUNKNOWNCALL = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_ADD_NEW_UNKNOWN_CALL;
+    public static final int API_CANCELMISSEDCALLSNOTIFICATION = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_CANCEL_MISSED_CALLS_NOTIFICATION;
+    public static final int API_CLEARACCOUNTS = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_CLEAR_ACCOUNTS;
+    public static final int API_CREATELAUNCHEMERGENCYDIALERINTENT = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_CREATE_LAUNCH_EMERGENCY_DIALER_INTENT;
+    public static final int API_CREATEMANAGEBLOCKEDNUMBERSINTENT = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_CREATE_MANAGE_BLOCKED_NUMBERS_INTENT;
+    public static final int API_DUMP = TelecomStatsLog.TELECOM_API_STATS__API_NAME__API_DUMP;
+    public static final int API_DUMPCALLANALYTICS = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_DUMP_CALL_ANALYTICS;
+    public static final int API_ENABLEPHONEACCOUNT = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_ENABLE_PHONE_ACCOUNT;
+    public static final int API_ENDCALL = TelecomStatsLog.TELECOM_API_STATS__API_NAME__API_END_CALL;
+    public static final int API_GETADNURIFORPHONEACCOUNT = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_GET_ADN_URI_FOR_PHONE_ACCOUNT;
+    public static final int API_GETALLPHONEACCOUNTHANDLES = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_GET_ALL_PHONE_ACCOUNT_HANDLES;
+    public static final int API_GETALLPHONEACCOUNTS = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_GET_ALL_PHONE_ACCOUNTS;
+    public static final int API_GETALLPHONEACCOUNTSCOUNT = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_GET_ALL_PHONE_ACCOUNTS_COUNT;
+    public static final int API_GETCALLCAPABLEPHONEACCOUNTS = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_GET_CALL_CAPABLE_PHONE_ACCOUNTS;
+    public static final int API_GETCALLSTATE = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_GET_CALL_STATE;
+    public static final int API_GETCALLSTATEUSINGPACKAGE = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_GET_CALL_STATE_USING_PACKAGE;
+    public static final int API_GETCURRENTTTYMODE = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_GET_CURRENT_TTY_MODE;
+    public static final int API_GETDEFAULTDIALERPACKAGE = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_GET_DEFAULT_DIALER_PACKAGE;
+    public static final int API_GETDEFAULTDIALERPACKAGEFORUSER = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_GET_DEFAULT_DIALER_PACKAGE_FOR_USER;
+    public static final int API_GETDEFAULTOUTGOINGPHONEACCOUNT = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_GET_DEFAULT_OUTGOING_PHONE_ACCOUNT;
+    public static final int API_GETDEFAULTPHONEAPP = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_GET_DEFAULT_PHONE_APP;
+    public static final int API_GETLINE1NUMBER = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_GET_LINE1_NUMBER;
+    public static final int API_GETOWNSELFMANAGEDPHONEACCOUNTS = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_GET_OWN_SELF_MANAGED_PHONE_ACCOUNTS;
+    public static final int API_GETPHONEACCOUNT = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_GET_PHONE_ACCOUNT;
+    public static final int API_GETPHONEACCOUNTSFORPACKAGE = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_GET_PHONE_ACCOUNTS_FOR_PACKAGE;
+    public static final int API_GETPHONEACCOUNTSSUPPORTINGSCHEME = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_GET_PHONE_ACCOUNTS_SUPPORTING_SCHEME;
+    public static final int API_GETREGISTEREDPHONEACCOUNTS = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_GET_REGISTERED_PHONE_ACCOUNTS;
+    public static final int API_GETSELFMANAGEDPHONEACCOUNTS = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_GET_SELF_MANAGED_PHONE_ACCOUNTS;
+    public static final int API_GETSIMCALLMANAGER = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_GET_SIM_CALL_MANAGER;
+    public static final int API_GETSIMCALLMANAGERFORUSER = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_GET_SIM_CALL_MANAGER_FOR_USER;
+    public static final int API_GETSYSTEMDIALERPACKAGE = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_GET_SYSTEM_DIALER_PACKAGE;
+    public static final int API_GETUSERSELECTEDOUTGOINGPHONEACCOUNT = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_GET_USER_SELECTED_OUTGOING_PHONE_ACCOUNT;
+    public static final int API_GETVOICEMAILNUMBER = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_GET_VOICE_MAIL_NUMBER;
+    public static final int API_HANDLEPINMMI = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_HANDLE_PIN_MMI;
+    public static final int API_HANDLEPINMMIFORPHONEACCOUNT = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_HANDLE_PIN_MMI_FOR_PHONE_ACCOUNT;
+    public static final int API_HASMANAGEONGOINGCALLSPERMISSION = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_HAS_MANAGE_ONGOING_CALLS_PERMISSION;
+    public static final int API_ISINCALL = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_IS_IN_CALL;
+    public static final int API_ISINCOMINGCALLPERMITTED = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_IS_IN_EMERGENCY_CALL;
+    public static final int API_ISINEMERGENCYCALL = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_IS_IN_MANAGED_CALL;
+    public static final int API_ISINMANAGEDCALL = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_IS_IN_SELF_MANAGED_CALL;
+    public static final int API_ISINSELFMANAGEDCALL = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_IS_INCOMING_CALL_PERMITTED;
+    public static final int API_ISOUTGOINGCALLPERMITTED = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_IS_OUTGOING_CALL_PERMITTED;
+    public static final int API_ISRINGING = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_IS_RINGING;
+    public static final int API_ISTTYSUPPORTED = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_IS_TTY_SUPPORTED;
+    public static final int API_ISVOICEMAILNUMBER = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_IS_VOICE_MAIL_NUMBER;
+    public static final int API_PLACECALL = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_PLACE_CALL;
+    public static final int API_REGISTERPHONEACCOUNT = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_REGISTER_PHONE_ACCOUNT;
+    public static final int API_SETDEFAULTDIALER = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_SET_DEFAULT_DIALER;
+    public static final int API_SETUSERSELECTEDOUTGOINGPHONEACCOUNT = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_SET_USER_SELECTED_OUTGOING_PHONE_ACCOUNT;
+    public static final int API_SHOWINCALLSCREEN = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_SHOW_IN_CALL_SCREEN;
+    public static final int API_SILENCERINGER = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_SILENCE_RINGER;
+    public static final int API_STARTCONFERENCE = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_START_CONFERENCE;
+    public static final int API_UNREGISTERPHONEACCOUNT = TelecomStatsLog
+            .TELECOM_API_STATS__API_NAME__API_UNREGISTER_PHONE_ACCOUNT;
+    public static final int RESULT_UNKNOWN = TelecomStatsLog
+            .TELECOM_API_STATS__API_RESULT__RESULT_UNKNOWN;
+    public static final int RESULT_NORMAL = TelecomStatsLog
+            .TELECOM_API_STATS__API_RESULT__RESULT_SUCCESS;
+    public static final int RESULT_PERMISSION = TelecomStatsLog
+            .TELECOM_API_STATS__API_RESULT__RESULT_PERMISSION;
+    public static final int RESULT_EXCEPTION = TelecomStatsLog
+            .TELECOM_API_STATS__API_RESULT__RESULT_EXCEPTION;
+    private static final String TAG = ApiStats.class.getSimpleName();
     private static final String FILE_NAME = "api_stats";
-    private Map<ApiStatsKey, Integer> mApiStatsMap;
+    private Map<ApiEvent, Integer> mApiStatsMap;
 
     public ApiStats(@NonNull Context context, @NonNull Looper looper) {
         super(context, looper);
@@ -73,7 +202,7 @@
         if (mPulledAtoms.telecomApiStats != null) {
             mApiStatsMap = new HashMap<>();
             for (PulledAtomsClass.TelecomApiStats v : mPulledAtoms.telecomApiStats) {
-                mApiStatsMap.put(new ApiStatsKey(v.getApiName(), v.getUid(), v.getApiResult()),
+                mApiStatsMap.put(new ApiEvent(v.getApiName(), v.getUid(), v.getApiResult()),
                         v.getCount());
             }
             mLastPulledTimestamps = mPulledAtoms.getTelecomApiStatsPullTimestampMillis();
@@ -83,6 +212,7 @@
     @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
     @Override
     public synchronized void onAggregate() {
+        Log.d(TAG, "onAggregate: %s", mApiStatsMap);
         clearAtoms();
         if (mApiStatsMap.isEmpty()) {
             return;
@@ -93,7 +223,7 @@
         int[] index = new int[1];
         mApiStatsMap.forEach((k, v) -> {
             mPulledAtoms.telecomApiStats[index[0]] = new PulledAtomsClass.TelecomApiStats();
-            mPulledAtoms.telecomApiStats[index[0]].setApiName(k.mApiId);
+            mPulledAtoms.telecomApiStats[index[0]].setApiName(k.mId);
             mPulledAtoms.telecomApiStats[index[0]].setUid(k.mCallerUid);
             mPulledAtoms.telecomApiStats[index[0]].setApiResult(k.mResult);
             mPulledAtoms.telecomApiStats[index[0]].setCount(v);
@@ -102,46 +232,131 @@
         save(DELAY_FOR_PERSISTENT_MILLIS);
     }
 
-    public void log(int apiId, int callerUid, int result) {
+    public void log(@NonNull ApiEvent event) {
         post(() -> {
-            ApiStatsKey key = new ApiStatsKey(apiId, callerUid, result);
-            mApiStatsMap.put(key, mApiStatsMap.getOrDefault(key, 0) + 1);
+            mApiStatsMap.put(event, mApiStatsMap.getOrDefault(event, 0) + 1);
             onAggregate();
         });
     }
 
-    static class ApiStatsKey {
+    @IntDef(prefix = "API", value = {
+            API_UNSPECIFIC,
+            API_ACCEPTHANDOVER,
+            API_ACCEPTRINGINGCALL,
+            API_ACCEPTRINGINGCALLWITHVIDEOSTATE,
+            API_ADDCALL,
+            API_ADDNEWINCOMINGCALL,
+            API_ADDNEWINCOMINGCONFERENCE,
+            API_ADDNEWUNKNOWNCALL,
+            API_CANCELMISSEDCALLSNOTIFICATION,
+            API_CLEARACCOUNTS,
+            API_CREATELAUNCHEMERGENCYDIALERINTENT,
+            API_CREATEMANAGEBLOCKEDNUMBERSINTENT,
+            API_DUMP,
+            API_DUMPCALLANALYTICS,
+            API_ENABLEPHONEACCOUNT,
+            API_ENDCALL,
+            API_GETADNURIFORPHONEACCOUNT,
+            API_GETALLPHONEACCOUNTHANDLES,
+            API_GETALLPHONEACCOUNTS,
+            API_GETALLPHONEACCOUNTSCOUNT,
+            API_GETCALLCAPABLEPHONEACCOUNTS,
+            API_GETCALLSTATE,
+            API_GETCALLSTATEUSINGPACKAGE,
+            API_GETCURRENTTTYMODE,
+            API_GETDEFAULTDIALERPACKAGE,
+            API_GETDEFAULTDIALERPACKAGEFORUSER,
+            API_GETDEFAULTOUTGOINGPHONEACCOUNT,
+            API_GETDEFAULTPHONEAPP,
+            API_GETLINE1NUMBER,
+            API_GETOWNSELFMANAGEDPHONEACCOUNTS,
+            API_GETPHONEACCOUNT,
+            API_GETPHONEACCOUNTSFORPACKAGE,
+            API_GETPHONEACCOUNTSSUPPORTINGSCHEME,
+            API_GETREGISTEREDPHONEACCOUNTS,
+            API_GETSELFMANAGEDPHONEACCOUNTS,
+            API_GETSIMCALLMANAGER,
+            API_GETSIMCALLMANAGERFORUSER,
+            API_GETSYSTEMDIALERPACKAGE,
+            API_GETUSERSELECTEDOUTGOINGPHONEACCOUNT,
+            API_GETVOICEMAILNUMBER,
+            API_HANDLEPINMMI,
+            API_HANDLEPINMMIFORPHONEACCOUNT,
+            API_HASMANAGEONGOINGCALLSPERMISSION,
+            API_ISINCALL,
+            API_ISINCOMINGCALLPERMITTED,
+            API_ISINEMERGENCYCALL,
+            API_ISINMANAGEDCALL,
+            API_ISINSELFMANAGEDCALL,
+            API_ISOUTGOINGCALLPERMITTED,
+            API_ISRINGING,
+            API_ISTTYSUPPORTED,
+            API_ISVOICEMAILNUMBER,
+            API_PLACECALL,
+            API_REGISTERPHONEACCOUNT,
+            API_SETDEFAULTDIALER,
+            API_SETUSERSELECTEDOUTGOINGPHONEACCOUNT,
+            API_SHOWINCALLSCREEN,
+            API_SILENCERINGER,
+            API_STARTCONFERENCE,
+            API_UNREGISTERPHONEACCOUNT,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ApiId {
+    }
 
-        int mApiId;
+    @IntDef(prefix = "RESULT", value = {
+            RESULT_UNKNOWN,
+            RESULT_NORMAL,
+            RESULT_PERMISSION,
+            RESULT_EXCEPTION,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ResultId {
+    }
+
+    public static class ApiEvent {
+
+        @ApiId
+        int mId;
         int mCallerUid;
+        @ResultId
         int mResult;
 
-        ApiStatsKey(int apiId, int callerUid, int result) {
-            mApiId = apiId;
+        public ApiEvent(@ApiId int id, int callerUid, @ResultId int result) {
+            mId = id;
             mCallerUid = callerUid;
             mResult = result;
         }
 
+        public void setCallerUid(int uid) {
+            this.mCallerUid = uid;
+        }
+
+        public void setResult(@ResultId int result) {
+            this.mResult = result;
+        }
+
         @Override
         public boolean equals(Object other) {
             if (this == other) {
                 return true;
             }
-            if (other == null || !(other instanceof ApiStatsKey obj)) {
+            if (!(other instanceof ApiEvent obj)) {
                 return false;
             }
-            return this.mApiId == obj.mApiId && this.mCallerUid == obj.mCallerUid
+            return this.mId == obj.mId && this.mCallerUid == obj.mCallerUid
                     && this.mResult == obj.mResult;
         }
 
         @Override
         public int hashCode() {
-            return Objects.hash(mApiId, mCallerUid, mResult);
+            return Objects.hash(mId, mCallerUid, mResult);
         }
 
         @Override
         public String toString() {
-            return "[ApiStatsKey: mApiId=" + mApiId + ", mCallerUid=" + mCallerUid
+            return "[ApiEvent: mApiId=" + mId + ", mCallerUid=" + mCallerUid
                     + ", mResult=" + mResult + "]";
         }
     }
diff --git a/src/com/android/server/telecom/metrics/CallStats.java b/src/com/android/server/telecom/metrics/CallStats.java
index 39b0e6d..7ebeba6 100644
--- a/src/com/android/server/telecom/metrics/CallStats.java
+++ b/src/com/android/server/telecom/metrics/CallStats.java
@@ -129,7 +129,7 @@
     }
 
     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) {
         post(() -> {
             CallStatsKey key = new CallStatsKey(direction, isExternal, isEmergency,
                     isMultipleAudioAvailable, accountType, uid);
@@ -158,13 +158,23 @@
                     : (call.isOutgoing() ? CALL_STATS__CALL_DIRECTION__DIR_OUTGOING
                     : CALL_STATS__CALL_DIRECTION__DIR_UNKNOWN);
             final int accountType = getAccountType(call.getPhoneAccountFromHandle());
-            final int uid = call.getAssociatedUser().getIdentifier();
+            int uid = call.getCallingPackageIdentity().mCallingPackageUid;
+            try {
+                uid = mContext.getPackageManager().getApplicationInfo(
+                        call.getTargetPhoneAccount().getComponentName().getPackageName(), 0).uid;
+            } catch (Exception e) {
+                Log.i(TAG, "failed to get the uid for " + e);
+            }
+
             log(direction, call.isExternalCall(), call.isEmergencyCall(), hasMultipleAudioDevices,
                     accountType, uid, duration);
         });
     }
 
     private int getAccountType(PhoneAccount account) {
+        if (account == null) {
+            return CALL_STATS__ACCOUNT_TYPE__ACCOUNT_UNKNOWN;
+        }
         if (account.hasCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)) {
             return account.hasCapabilities(
                     PhoneAccount.CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS)
@@ -202,7 +212,7 @@
         final int mUid;
 
         CallStatsKey(int direction, boolean isExternal, boolean isEmergency,
-                     boolean isMultipleAudioAvailable, int accountType, int uid) {
+                boolean isMultipleAudioAvailable, int accountType, int uid) {
             mDirection = direction;
             mIsExternal = isExternal;
             mIsEmergency = isEmergency;
diff --git a/src/com/android/server/telecom/metrics/ErrorStats.java b/src/com/android/server/telecom/metrics/ErrorStats.java
index e4d0a51..f70f6d8 100644
--- a/src/com/android/server/telecom/metrics/ErrorStats.java
+++ b/src/com/android/server/telecom/metrics/ErrorStats.java
@@ -18,10 +18,12 @@
 
 import static com.android.server.telecom.TelecomStatsLog.TELECOM_ERROR_STATS;
 
+import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.app.StatsManager;
 import android.content.Context;
 import android.os.Looper;
+import android.telecom.Log;
 import android.util.StatsEvent;
 
 import androidx.annotation.VisibleForTesting;
@@ -29,6 +31,8 @@
 import com.android.server.telecom.TelecomStatsLog;
 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;
@@ -36,9 +40,83 @@
 import java.util.Objects;
 
 public class ErrorStats extends TelecomPulledAtom {
-
+    public static final int SUB_UNKNOWN = TelecomStatsLog
+            .TELECOM_ERROR_STATS__SUBMODULE__SUB_UNKNOWN;
+    public static final int SUB_CALL_AUDIO = TelecomStatsLog
+            .TELECOM_ERROR_STATS__SUBMODULE__SUB_CALL_AUDIO;
+    public static final int SUB_CALL_LOGS = TelecomStatsLog
+            .TELECOM_ERROR_STATS__SUBMODULE__SUB_CALL_LOGS;
+    public static final int SUB_CALL_MANAGER = TelecomStatsLog
+            .TELECOM_ERROR_STATS__SUBMODULE__SUB_CALL_MANAGER;
+    public static final int SUB_CONNECTION_SERVICE = TelecomStatsLog
+            .TELECOM_ERROR_STATS__SUBMODULE__SUB_CONNECTION_SERVICE;
+    public static final int SUB_EMERGENCY_CALL = TelecomStatsLog
+            .TELECOM_ERROR_STATS__SUBMODULE__SUB_EMERGENCY_CALL;
+    public static final int SUB_IN_CALL_SERVICE = TelecomStatsLog
+            .TELECOM_ERROR_STATS__SUBMODULE__SUB_IN_CALL_SERVICE;
+    public static final int SUB_MISC = TelecomStatsLog.TELECOM_ERROR_STATS__SUBMODULE__SUB_MISC;
+    public static final int SUB_PHONE_ACCOUNT = TelecomStatsLog
+            .TELECOM_ERROR_STATS__SUBMODULE__SUB_PHONE_ACCOUNT;
+    public static final int SUB_SYSTEM_SERVICE = TelecomStatsLog
+            .TELECOM_ERROR_STATS__SUBMODULE__SUB_SYSTEM_SERVICE;
+    public static final int SUB_TELEPHONY = TelecomStatsLog
+            .TELECOM_ERROR_STATS__SUBMODULE__SUB_TELEPHONY;
+    public static final int SUB_UI = TelecomStatsLog.TELECOM_ERROR_STATS__SUBMODULE__SUB_UI;
+    public static final int SUB_VOIP_CALL = TelecomStatsLog
+            .TELECOM_ERROR_STATS__SUBMODULE__SUB_VOIP_CALL;
+    public static final int ERROR_UNKNOWN = TelecomStatsLog
+            .TELECOM_ERROR_STATS__ERROR__ERROR_UNKNOWN;
+    public static final int ERROR_EXTERNAL_EXCEPTION = TelecomStatsLog
+            .TELECOM_ERROR_STATS__ERROR__ERROR_EXTERNAL_EXCEPTION;
+    public static final int ERROR_INTERNAL_EXCEPTION = TelecomStatsLog
+            .TELECOM_ERROR_STATS__ERROR__ERROR_INTERNAL_EXCEPTION;
+    public static final int ERROR_AUDIO_ROUTE_RETRY_REJECTED = TelecomStatsLog
+            .TELECOM_ERROR_STATS__ERROR__ERROR_AUDIO_ROUTE_RETRY_REJECTED;
+    public static final int ERROR_BT_GET_SERVICE_FAILURE = TelecomStatsLog
+            .TELECOM_ERROR_STATS__ERROR__ERROR_BT_GET_SERVICE_FAILURE;
+    public static final int ERROR_BT_REGISTER_CALLBACK_FAILURE = TelecomStatsLog
+            .TELECOM_ERROR_STATS__ERROR__ERROR_BT_REGISTER_CALLBACK_FAILURE;
+    public static final int ERROR_AUDIO_ROUTE_UNAVAILABLE = TelecomStatsLog
+            .TELECOM_ERROR_STATS__ERROR__ERROR_AUDIO_ROUTE_UNAVAILABLE;
+    public static final int ERROR_EMERGENCY_NUMBER_DETERMINED_FAILURE = TelecomStatsLog
+            .TELECOM_ERROR_STATS__ERROR__ERROR_EMERGENCY_NUMBER_DETERMINED_FAILURE;
+    public static final int ERROR_NOTIFY_CALL_STREAM_START_FAILURE = TelecomStatsLog
+            .TELECOM_ERROR_STATS__ERROR__ERROR_NOTIFY_CALL_STREAM_START_FAILURE;
+    public static final int ERROR_NOTIFY_CALL_STREAM_STATE_CHANGED_FAILURE = TelecomStatsLog
+            .TELECOM_ERROR_STATS__ERROR__ERROR_NOTIFY_CALL_STREAM_STATE_CHANGED_FAILURE;
+    public static final int ERROR_NOTIFY_CALL_STREAM_STOP_FAILURE = TelecomStatsLog
+            .TELECOM_ERROR_STATS__ERROR__ERROR_NOTIFY_CALL_STREAM_STOP_FAILURE;
+    public static final int ERROR_RTT_STREAM_CLOSE_FAILURE = TelecomStatsLog
+            .TELECOM_ERROR_STATS__ERROR__ERROR_RTT_STREAM_CLOSE_FAILURE;
+    public static final int ERROR_RTT_STREAM_CREATE_FAILURE = TelecomStatsLog
+            .TELECOM_ERROR_STATS__ERROR__ERROR_RTT_STREAM_CREATE_FAILURE;
+    public static final int ERROR_SET_MUTED_FAILURE = TelecomStatsLog
+            .TELECOM_ERROR_STATS__ERROR__ERROR_SET_MUTED_FAILURE;
+    public static final int ERROR_VIDEO_PROVIDER_SET_FAILURE = TelecomStatsLog
+            .TELECOM_ERROR_STATS__ERROR__ERROR_VIDEO_PROVIDER_SET_FAILURE;
+    public static final int ERROR_WIRED_HEADSET_NOT_AVAILABLE = TelecomStatsLog
+            .TELECOM_ERROR_STATS__ERROR__ERROR_WIRED_HEADSET_NOT_AVAILABLE;
+    public static final int ERROR_LOG_CALL_FAILURE = TelecomStatsLog
+            .TELECOM_ERROR_STATS__ERROR__ERROR_LOG_CALL_FAILURE;
+    public static final int ERROR_RETRIEVING_ACCOUNT_EMERGENCY = TelecomStatsLog
+            .TELECOM_ERROR_STATS__ERROR__ERROR_RETRIEVING_ACCOUNT_EMERGENCY;
+    public static final int ERROR_RETRIEVING_ACCOUNT = TelecomStatsLog
+            .TELECOM_ERROR_STATS__ERROR__ERROR_RETRIEVING_ACCOUNT;
+    public static final int ERROR_EMERGENCY_CALL_ABORTED_NO_ACCOUNT = TelecomStatsLog
+            .TELECOM_ERROR_STATS__ERROR__ERROR_EMERGENCY_CALL_ABORTED_NO_ACCOUNT;
+    public static final int ERROR_DEFAULT_MO_ACCOUNT_MISMATCH = TelecomStatsLog
+            .TELECOM_ERROR_STATS__ERROR__ERROR_DEFAULT_MO_ACCOUNT_MISMATCH;
+    public static final int ERROR_ESTABLISHING_CONNECTION = TelecomStatsLog
+            .TELECOM_ERROR_STATS__ERROR__ERROR_ESTABLISHING_CONNECTION;
+    public static final int ERROR_REMOVING_CALL = TelecomStatsLog
+            .TELECOM_ERROR_STATS__ERROR__ERROR_REMOVING_CALL;
+    public static final int ERROR_STUCK_CONNECTING_EMERGENCY = TelecomStatsLog
+            .TELECOM_ERROR_STATS__ERROR__ERROR_STUCK_CONNECTING_EMERGENCY;
+    public static final int ERROR_STUCK_CONNECTING = TelecomStatsLog
+            .TELECOM_ERROR_STATS__ERROR__ERROR_STUCK_CONNECTING;
+    private static final String TAG = ErrorStats.class.getSimpleName();
     private static final String FILE_NAME = "error_stats";
-    private Map<ErrorStatsKey, Integer> mErrorStatsMap;
+    private Map<ErrorEvent, Integer> mErrorStatsMap;
 
     public ErrorStats(@NonNull Context context, @NonNull Looper looper) {
         super(context, looper);
@@ -61,7 +139,7 @@
         if (mPulledAtoms.telecomErrorStats.length != 0) {
             Arrays.stream(mPulledAtoms.telecomErrorStats).forEach(v -> data.add(
                     TelecomStatsLog.buildStatsEvent(getTag(),
-                            v.getSubmoduleName(), v.getErrorName(), v.getCount())));
+                            v.getSubmodule(), v.getError(), v.getCount())));
             return StatsManager.PULL_SUCCESS;
         } else {
             return StatsManager.PULL_SKIP;
@@ -73,7 +151,7 @@
         if (mPulledAtoms.telecomErrorStats != null) {
             mErrorStatsMap = new HashMap<>();
             for (PulledAtomsClass.TelecomErrorStats v : mPulledAtoms.telecomErrorStats) {
-                mErrorStatsMap.put(new ErrorStatsKey(v.getSubmoduleName(), v.getErrorName()),
+                mErrorStatsMap.put(new ErrorEvent(v.getSubmodule(), v.getError()),
                         v.getCount());
             }
             mLastPulledTimestamps = mPulledAtoms.getTelecomErrorStatsPullTimestampMillis();
@@ -83,6 +161,7 @@
     @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
     @Override
     public synchronized void onAggregate() {
+        Log.d(TAG, "onAggregate: %s", mErrorStatsMap);
         clearAtoms();
         if (mErrorStatsMap.isEmpty()) {
             return;
@@ -93,28 +172,78 @@
         int[] index = new int[1];
         mErrorStatsMap.forEach((k, v) -> {
             mPulledAtoms.telecomErrorStats[index[0]] = new PulledAtomsClass.TelecomErrorStats();
-            mPulledAtoms.telecomErrorStats[index[0]].setSubmoduleName(k.mModuleId);
-            mPulledAtoms.telecomErrorStats[index[0]].setErrorName(k.mErrorId);
+            mPulledAtoms.telecomErrorStats[index[0]].setSubmodule(k.mModuleId);
+            mPulledAtoms.telecomErrorStats[index[0]].setError(k.mErrorId);
             mPulledAtoms.telecomErrorStats[index[0]].setCount(v);
             index[0]++;
         });
         save(DELAY_FOR_PERSISTENT_MILLIS);
     }
 
-    public void log(int moduleId, int errorId) {
+    public void log(@SubModuleId int moduleId, @ErrorId int errorId) {
         post(() -> {
-            ErrorStatsKey key = new ErrorStatsKey(moduleId, errorId);
+            ErrorEvent key = new ErrorEvent(moduleId, errorId);
             mErrorStatsMap.put(key, mErrorStatsMap.getOrDefault(key, 0) + 1);
             onAggregate();
         });
     }
 
-    static class ErrorStatsKey {
+    @IntDef(prefix = "SUB", value = {
+            SUB_UNKNOWN,
+            SUB_CALL_AUDIO,
+            SUB_CALL_LOGS,
+            SUB_CALL_MANAGER,
+            SUB_CONNECTION_SERVICE,
+            SUB_EMERGENCY_CALL,
+            SUB_IN_CALL_SERVICE,
+            SUB_MISC,
+            SUB_PHONE_ACCOUNT,
+            SUB_SYSTEM_SERVICE,
+            SUB_TELEPHONY,
+            SUB_UI,
+            SUB_VOIP_CALL,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface SubModuleId {
+    }
 
-        final int mModuleId;
-        final int mErrorId;
+    @IntDef(prefix = "ERROR", value = {
+            ERROR_UNKNOWN,
+            ERROR_EXTERNAL_EXCEPTION,
+            ERROR_INTERNAL_EXCEPTION,
+            ERROR_AUDIO_ROUTE_RETRY_REJECTED,
+            ERROR_BT_GET_SERVICE_FAILURE,
+            ERROR_BT_REGISTER_CALLBACK_FAILURE,
+            ERROR_AUDIO_ROUTE_UNAVAILABLE,
+            ERROR_EMERGENCY_NUMBER_DETERMINED_FAILURE,
+            ERROR_NOTIFY_CALL_STREAM_START_FAILURE,
+            ERROR_NOTIFY_CALL_STREAM_STATE_CHANGED_FAILURE,
+            ERROR_NOTIFY_CALL_STREAM_STOP_FAILURE,
+            ERROR_RTT_STREAM_CLOSE_FAILURE,
+            ERROR_RTT_STREAM_CREATE_FAILURE,
+            ERROR_SET_MUTED_FAILURE,
+            ERROR_VIDEO_PROVIDER_SET_FAILURE,
+            ERROR_WIRED_HEADSET_NOT_AVAILABLE,
+            ERROR_LOG_CALL_FAILURE,
+            ERROR_RETRIEVING_ACCOUNT_EMERGENCY,
+            ERROR_RETRIEVING_ACCOUNT,
+            ERROR_EMERGENCY_CALL_ABORTED_NO_ACCOUNT,
+            ERROR_DEFAULT_MO_ACCOUNT_MISMATCH,
+            ERROR_ESTABLISHING_CONNECTION,
+            ERROR_REMOVING_CALL,
+            ERROR_STUCK_CONNECTING_EMERGENCY,
+            ERROR_STUCK_CONNECTING,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ErrorId {
+    }
 
-        ErrorStatsKey(int moduleId, int errorId) {
+    static class ErrorEvent {
+
+        final @SubModuleId int mModuleId;
+        final @ErrorId int mErrorId;
+
+        ErrorEvent(@SubModuleId int moduleId, @ErrorId int errorId) {
             mModuleId = moduleId;
             mErrorId = errorId;
         }
@@ -124,7 +253,7 @@
             if (this == other) {
                 return true;
             }
-            if (!(other instanceof ErrorStatsKey obj)) {
+            if (!(other instanceof ErrorEvent obj)) {
                 return false;
             }
             return this.mModuleId == obj.mModuleId && this.mErrorId == obj.mErrorId;
@@ -137,7 +266,7 @@
 
         @Override
         public String toString() {
-            return "[ErrorStatsKey: mModuleId=" + mModuleId + ", mErrorId=" + mErrorId + "]";
+            return "[ErrorEvent: mModuleId=" + mModuleId + ", mErrorId=" + mErrorId + "]";
         }
     }
 }
diff --git a/src/com/android/server/telecom/metrics/TelecomMetricsController.java b/src/com/android/server/telecom/metrics/TelecomMetricsController.java
index 8903b02..df735c0 100644
--- a/src/com/android/server/telecom/metrics/TelecomMetricsController.java
+++ b/src/com/android/server/telecom/metrics/TelecomMetricsController.java
@@ -30,6 +30,8 @@
 
 import androidx.annotation.VisibleForTesting;
 
+import com.android.modules.utils.HandlerExecutor;
+
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -51,7 +53,7 @@
 
     @NonNull
     public static TelecomMetricsController make(@NonNull Context context) {
-        Log.i(TAG, "TMC.iN1");
+        Log.i(TAG, "TMC.m1");
         HandlerThread handlerThread = new HandlerThread(TAG);
         handlerThread.start();
         return make(context, handlerThread);
@@ -61,7 +63,7 @@
     @NonNull
     public static TelecomMetricsController make(@NonNull Context context,
                                                 @NonNull HandlerThread handlerThread) {
-        Log.i(TAG, "TMC.iN2");
+        Log.i(TAG, "TMC.m2");
         Objects.requireNonNull(context);
         Objects.requireNonNull(handlerThread);
         return new TelecomMetricsController(context, handlerThread);
@@ -122,10 +124,23 @@
 
     @VisibleForTesting
     public void registerAtom(int tag, TelecomPulledAtom atom) {
-        mStats.put(tag, atom);
+        final StatsManager statsManager = mContext.getSystemService(StatsManager.class);
+        if (statsManager != null) {
+            statsManager.setPullAtomCallback(tag, null, new HandlerExecutor(atom), this);
+            mStats.put(tag, atom);
+        } else {
+            Log.w(TAG, "Unable to register the pulled atom as StatsManager is null");
+        }
     }
 
     public void destroy() {
+        final StatsManager statsManager = mContext.getSystemService(StatsManager.class);
+        if (statsManager != null) {
+            mStats.forEach((tag, stat) -> statsManager.clearPullAtomCallback(tag));
+        } 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 d6eb039..161eaa8 100644
--- a/src/com/android/server/telecom/metrics/TelecomPulledAtom.java
+++ b/src/com/android/server/telecom/metrics/TelecomPulledAtom.java
@@ -44,7 +44,7 @@
     private static final String TAG = TelecomPulledAtom.class.getSimpleName();
     private static final long MIN_PULL_INTERVAL_MILLIS = 23L * 60 * 60 * 1000;
     private static final int EVENT_SAVE = 1;
-    private final Context mContext;
+    protected final Context mContext;
     @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
     public PulledAtoms mPulledAtoms;
     protected long mLastPulledTimestamps;
diff --git a/src/com/android/server/telecom/ui/CallStreamingNotification.java b/src/com/android/server/telecom/ui/CallStreamingNotification.java
index 8414047..06da5e3 100644
--- a/src/com/android/server/telecom/ui/CallStreamingNotification.java
+++ b/src/com/android/server/telecom/ui/CallStreamingNotification.java
@@ -192,7 +192,7 @@
         // Use the caller name for the label if available, default to app name if none.
         if (TextUtils.isEmpty(callerName)) {
             // App did not provide a caller name, so default to app's name.
-            callerName = mAppLabelProxy.getAppLabel(appPackageName).toString();
+            callerName = mAppLabelProxy.getAppLabel(appPackageName, userHandle).toString();
         }
 
         // Action to hangup; this can use the default hangup action from the call style
diff --git a/tests/src/com/android/server/telecom/tests/BasicCallTests.java b/tests/src/com/android/server/telecom/tests/BasicCallTests.java
index 4bca30d..7646c2d 100644
--- a/tests/src/com/android/server/telecom/tests/BasicCallTests.java
+++ b/tests/src/com/android/server/telecom/tests/BasicCallTests.java
@@ -1036,6 +1036,7 @@
         call.setTargetPhoneAccount(mPhoneAccountA1.getAccountHandle());
         assert(call.isVideoCallingSupportedByPhoneAccount());
         assertEquals(VideoProfile.STATE_BIDIRECTIONAL, call.getVideoState());
+        call.setIsCreateConnectionComplete(true);
     }
 
     /**
@@ -1059,6 +1060,7 @@
         call.setTargetPhoneAccount(mPhoneAccountA2.getAccountHandle());
         assert(!call.isVideoCallingSupportedByPhoneAccount());
         assertEquals(VideoProfile.STATE_AUDIO_ONLY, call.getVideoState());
+        call.setIsCreateConnectionComplete(true);
     }
 
     /**
diff --git a/tests/src/com/android/server/telecom/tests/BlockCheckerFilterTest.java b/tests/src/com/android/server/telecom/tests/BlockCheckerFilterTest.java
index e76989c..a706f4b 100644
--- a/tests/src/com/android/server/telecom/tests/BlockCheckerFilterTest.java
+++ b/tests/src/com/android/server/telecom/tests/BlockCheckerFilterTest.java
@@ -88,7 +88,7 @@
         super.setUp();
         when(mCall.getHandle()).thenReturn(TEST_HANDLE);
         mFilter = new BlockCheckerFilter(mContext, mCall, mCallerInfoLookupHelper,
-                mBlockCheckerAdapter);
+                mBlockCheckerAdapter, mFeatureFlags);
     }
 
     @SmallTest
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioModeStateMachineTest.java b/tests/src/com/android/server/telecom/tests/CallAudioModeStateMachineTest.java
index 4513c65..9414e16 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioModeStateMachineTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioModeStateMachineTest.java
@@ -16,15 +16,10 @@
 
 package com.android.server.telecom.tests;
 
-import static com.android.server.telecom.CallAudioModeStateMachine.CALL_AUDIO_FOCUS_REQUEST;
-import static com.android.server.telecom.CallAudioModeStateMachine.RING_AUDIO_FOCUS_REQUEST;
-
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
@@ -49,7 +44,6 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
-import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 
 @RunWith(JUnit4.class)
@@ -329,33 +323,6 @@
         verify(mCallAudioManager, times(2)).startRinging();
     }
 
-    @SmallTest
-    @Test
-    public void testAudioFocusRequestWithResolveHiddenDependencies() {
-        CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mSystemStateHelper,
-                mAudioManager, mTestThread.getLooper(), mFeatureFlags, mCommunicationDeviceTracker);
-        when(mFeatureFlags.telecomResolveHiddenDependencies()).thenReturn(true);
-        ArgumentCaptor<AudioFocusRequest> captor = ArgumentCaptor.forClass(AudioFocusRequest.class);
-        sm.setCallAudioManager(mCallAudioManager);
-
-        resetMocks();
-        when(mCallAudioManager.startRinging()).thenReturn(true);
-        when(mCallAudioManager.isRingtonePlaying()).thenReturn(false);
-
-        sm.sendMessage(CallAudioModeStateMachine.ENTER_RING_FOCUS_FOR_TESTING);
-        waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
-        verify(mAudioManager).requestAudioFocus(captor.capture());
-        assertTrue(areAudioFocusRequestsMatch(captor.getValue(), RING_AUDIO_FOCUS_REQUEST));
-
-        sm.sendMessage(CallAudioModeStateMachine.ENTER_CALL_FOCUS_FOR_TESTING);
-        waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
-        verify(mAudioManager, atLeast(1)).requestAudioFocus(captor.capture());
-        AudioFocusRequest request = captor.getValue();
-        assertTrue(areAudioFocusRequestsMatch(request, CALL_AUDIO_FOCUS_REQUEST));
-
-        sm.sendMessage(CallAudioModeStateMachine.ABANDON_FOCUS_FOR_TESTING);
-    }
-
     private void resetMocks() {
         clearInvocations(mCallAudioManager, mAudioManager);
     }
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
index ade2a22..809abb4 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
@@ -20,6 +20,7 @@
 import static com.android.server.telecom.CallAudioRouteAdapter.BT_ACTIVE_DEVICE_GONE;
 import static com.android.server.telecom.CallAudioRouteAdapter.BT_ACTIVE_DEVICE_PRESENT;
 import static com.android.server.telecom.CallAudioRouteAdapter.BT_AUDIO_CONNECTED;
+import static com.android.server.telecom.CallAudioRouteAdapter.BT_AUDIO_DISCONNECTED;
 import static com.android.server.telecom.CallAudioRouteAdapter.BT_DEVICE_ADDED;
 import static com.android.server.telecom.CallAudioRouteAdapter.BT_DEVICE_REMOVED;
 import static com.android.server.telecom.CallAudioRouteAdapter.CONNECT_DOCK;
@@ -36,10 +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.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;
@@ -70,6 +74,7 @@
 import android.os.UserHandle;
 import android.telecom.CallAudioState;
 import android.telecom.VideoProfile;
+import android.util.Pair;
 
 import androidx.test.filters.SmallTest;
 
@@ -189,6 +194,9 @@
         when(mCall.getSupportedAudioRoutes()).thenReturn(CallAudioState.ROUTE_ALL);
         when(mFeatureFlags.ignoreAutoRouteToWatchDevice()).thenReturn(false);
         when(mFeatureFlags.useRefactoredAudioRouteSwitching()).thenReturn(true);
+        when(mFeatureFlags.resolveActiveBtRoutingAndBtTimingIssue()).thenReturn(false);
+        when(mFeatureFlags.newAudioPathSpeakerBroadcastAndUnfocusedRouting()).thenReturn(false);
+        when(mFeatureFlags.fixUserRequestBaselineRouteVideoCall()).thenReturn(false);
     }
 
     @After
@@ -908,6 +916,279 @@
 
     }
 
+    @SmallTest
+    @Test
+    public void testMimicVoiceDialWithBt() {
+        when(mFeatureFlags.resolveActiveBtRoutingAndBtTimingIssue()).thenReturn(true);
+        mController.initialize();
+        mController.setActive(true);
+
+        mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
+                BLUETOOTH_DEVICE_1);
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, null, BLUETOOTH_DEVICES);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS, 0);
+        // Mimic behavior of controller processing BT_AUDIO_DISCONNECTED
+        mController.sendMessageWithSessionInfo(SWITCH_BASELINE_ROUTE,
+                INCLUDE_BLUETOOTH_IN_BASELINE, BLUETOOTH_DEVICE_1.getAddress());
+        // Process BT_AUDIO_CONNECTED from connecting to BT device in active focus request.
+        mController.setIsScoAudioConnected(true);
+        mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0, BLUETOOTH_DEVICE_1);
+        // Verify SCO not disconnected and route stays on connected BT device.
+        verify(mBluetoothDeviceManager, timeout(TEST_TIMEOUT).times(0)).disconnectSco();
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+    }
+
+    @SmallTest
+    @Test
+    public void testTransactionalCallBtConnectingAndSwitchCallEndpoint() {
+        when(mFeatureFlags.resolveActiveBtRoutingAndBtTimingIssue()).thenReturn(true);
+        mController.initialize();
+        mController.setActive(true);
+
+        mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
+                BLUETOOTH_DEVICE_1);
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, null, BLUETOOTH_DEVICES);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        mController.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
+                AudioRoute.TYPE_BLUETOOTH_SCO, BT_ADDRESS_1);
+        // Omit sending BT_AUDIO_CONNECTED to mimic scenario where BT is still connecting and user
+        // switches to speaker.
+        mController.sendMessageWithSessionInfo(USER_SWITCH_SPEAKER);
+        mController.sendMessageWithSessionInfo(SPEAKER_ON);
+        mController.sendMessageWithSessionInfo(BT_AUDIO_DISCONNECTED, 0,
+                BLUETOOTH_DEVICE_1);
+
+        // Verify SCO disconnected
+        verify(mBluetoothDeviceManager, timeout(TEST_TIMEOUT)).disconnectSco();
+        // Verify audio properly routes into speaker.
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, null, BLUETOOTH_DEVICES);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+    }
+
+    @Test
+    @SmallTest
+    public void testBluetoothRouteToActiveDevice() {
+        when(mFeatureFlags.resolveActiveBtRoutingAndBtTimingIssue()).thenReturn(true);
+        // Connect first BT device.
+        verifyConnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_SCO);
+        // Connect another BT device.
+        String scoDeviceAddress = "00:00:00:00:00:03";
+        BluetoothDevice scoDevice =
+                BluetoothRouteManagerTest.makeBluetoothDevice(scoDeviceAddress);
+        BLUETOOTH_DEVICES.add(scoDevice);
+        mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
+                scoDevice);
+        mController.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
+                AudioRoute.TYPE_BLUETOOTH_SCO, scoDeviceAddress);
+        mController.sendMessageWithSessionInfo(BT_AUDIO_DISCONNECTED, 0,
+                BLUETOOTH_DEVICE_1);
+        mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0,
+                scoDevice);
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, scoDevice, BLUETOOTH_DEVICES);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        // Mimic behavior when inactive headset is used to answer the call (i.e. tap headset). In
+        // this case, the inactive BT device will become the active device (reported to us from BT
+        // stack to controller via BT_ACTIVE_DEVICE_PRESENT).
+        mController.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
+                AudioRoute.TYPE_BLUETOOTH_SCO, BLUETOOTH_DEVICE_1.getAddress());
+        mController.sendMessageWithSessionInfo(BT_AUDIO_DISCONNECTED, 0,
+                scoDevice);
+        mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0,
+                BLUETOOTH_DEVICE_1);
+        // Verify audio routed to BLUETOOTH_DEVICE_1
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        // Now switch call to active focus so that base route can be recalculated.
+        mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS, 0);
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+        // Verify that audio is still routed into BLUETOOTH_DEVICE_1 and not the 2nd BT device.
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        // Clean up BLUETOOTH_DEVICES for subsequent tests.
+        BLUETOOTH_DEVICES.remove(scoDevice);
+    }
+
+    @Test
+    @SmallTest
+    public void verifyRouteReinitializedAfterCallEnd() {
+        when(mFeatureFlags.resolveActiveBtRoutingAndBtTimingIssue()).thenReturn(true);
+        mController.initialize();
+        mController.setActive(true);
+
+        // Switch to speaker
+        mController.sendMessageWithSessionInfo(SPEAKER_ON);
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        // Verify that call audio route is reinitialized to default (in this case, earpiece) when
+        // call audio focus is lost.
+        mController.sendMessageWithSessionInfo(SWITCH_FOCUS, NO_FOCUS, 0);
+        mController.sendMessageWithSessionInfo(SPEAKER_OFF);
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+    }
+
+    @Test
+    @SmallTest
+    public void testUserSwitchBaselineRouteVideoCall() {
+        when(mFeatureFlags.fixUserRequestBaselineRouteVideoCall()).thenReturn(true);
+        mController.initialize();
+        mController.setActive(true);
+        // Set capabilities for video call.
+        when(mCall.getVideoState()).thenReturn(VideoProfile.STATE_BIDIRECTIONAL);
+
+        // Turn on speaker
+        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));
+
+        // USER_SWITCH_BASELINE_ROUTE (explicit user request). Verify that audio is routed back to
+        // earpiece.
+        mController.sendMessageWithSessionInfo(USER_SWITCH_BASELINE_ROUTE,
+                CallAudioRouteController.INCLUDE_BLUETOOTH_IN_BASELINE);
+        mController.sendMessageWithSessionInfo(SPEAKER_OFF);
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        // SWITCH_BASELINE_ROUTE. Verify that audio is routed to speaker for non-user requests.
+        mController.sendMessageWithSessionInfo(SWITCH_BASELINE_ROUTE,
+                CallAudioRouteController.INCLUDE_BLUETOOTH_IN_BASELINE);
+        mController.sendMessageWithSessionInfo(SPEAKER_ON);
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+    }
+
+    @Test
+    @SmallTest
+    public void testRouteToWatchWhenCallAnsweredOnWatch_MultipleBtDevices() {
+        when(mFeatureFlags.resolveActiveBtRoutingAndBtTimingIssue()).thenReturn(true);
+        // Connect first BT device.
+        verifyConnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_SCO);
+        // Connect another BT device.
+        String scoDeviceAddress = "00:00:00:00:00:03";
+        BluetoothDevice watchDevice =
+                BluetoothRouteManagerTest.makeBluetoothDevice(scoDeviceAddress);
+        when(mBluetoothRouteManager.isWatch(eq(watchDevice))).thenReturn(true);
+        BLUETOOTH_DEVICES.add(watchDevice);
+
+        mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
+                watchDevice);
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER
+                | CallAudioState.ROUTE_BLUETOOTH, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        // Signal that watch is now the active device. This is done in BluetoothStateReceiver and
+        // then BT_ACTIVE_DEVICE_PRESENT will be sent to the controller to be processed.
+        mController.updateActiveBluetoothDevice(
+                new Pair<>(AudioRoute.TYPE_BLUETOOTH_SCO, watchDevice.getAddress()));
+        // Emulate scenario with call answered on watch. Ensure at this point that audio was routed
+        // into watch
+        mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS, 0);
+        mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED,
+                0, watchDevice);
+        mController.sendMessageWithSessionInfo(BT_AUDIO_DISCONNECTED,
+                0, BLUETOOTH_DEVICE_1);
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER
+                        | CallAudioState.ROUTE_BLUETOOTH, watchDevice, BLUETOOTH_DEVICES);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        // Hardcode signal from BT stack signaling to Telecom that watch is now the active device.
+        // This should just be a no-op since audio was already routed when processing active focus.
+        mController.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
+                AudioRoute.TYPE_BLUETOOTH_SCO, scoDeviceAddress);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        // Mimic behavior of controller processing BT_AUDIO_DISCONNECTED for BLUETOOTH_DEVICE_1 and
+        // verify that audio remains routed to the watch and not routed to earpiece (this should
+        // be taking into account what the BT active device is as reported to us by the BT stack).
+        mController.sendMessageWithSessionInfo(SWITCH_BASELINE_ROUTE,
+                INCLUDE_BLUETOOTH_IN_BASELINE, BLUETOOTH_DEVICE_1.getAddress());
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        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);
+
+        mController.initialize();
+        mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
+                BLUETOOTH_DEVICE_1);
+
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+        mController.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
+                AudioRoute.TYPE_BLUETOOTH_SCO, BT_ADDRESS_1);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+        assertFalse(mController.isActive());
+
+        // Verify route never went active due to in-band ringing being disabled.
+        mController.sendMessageWithSessionInfo(SWITCH_FOCUS, RINGING_FOCUS, 0);
+        assertFalse(mController.isActive());
+
+        // Emulate scenario of rejecting an incoming call so that call focus is lost and verify
+        // that we abandon the call audio focus that was gained from when the call went to
+        // ringing state.
+        mController.sendMessageWithSessionInfo(SWITCH_FOCUS, NO_FOCUS, 0);
+        // Ensure we tell the CallAudioManager that audio operations are done so that we can ensure
+        // audio focus is relinquished.
+        verify(mCallAudioManager, timeout(TEST_TIMEOUT)).notifyAudioOperationsComplete();
+    }
+
     private void verifyConnectBluetoothDevice(int audioType) {
         mController.initialize();
         mController.setActive(true);
diff --git a/tests/src/com/android/server/telecom/tests/CallScreeningServiceFilterTest.java b/tests/src/com/android/server/telecom/tests/CallScreeningServiceFilterTest.java
index d1427db..d97263d 100644
--- a/tests/src/com/android/server/telecom/tests/CallScreeningServiceFilterTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallScreeningServiceFilterTest.java
@@ -17,6 +17,7 @@
 package com.android.server.telecom.tests;
 
 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;
@@ -136,7 +137,7 @@
         when(mContext.getSystemService(TelecomManager.class))
                 .thenReturn(mTelecomManager);
         when(mTelecomManager.getSystemDialerPackage()).thenReturn(PKG_NAME);
-        when(mAppLabelProxy.getAppLabel(PKG_NAME)).thenReturn(APP_NAME);
+        when(mAppLabelProxy.getAppLabel(PKG_NAME, PA_HANDLE.getUserHandle())).thenReturn(APP_NAME);
         when(mParcelableCallUtilsConverter.toParcelableCall(
                 eq(mCall), anyBoolean(), eq(mPhoneAccountRegistrar))).thenReturn(null);
         when(mContext.bindServiceAsUser(nullable(Intent.class), nullable(ServiceConnection.class),
diff --git a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
index 265d7b2..79fd3d5 100644
--- a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
@@ -59,6 +59,7 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Handler;
+import android.os.IBinder;
 import android.os.Looper;
 import android.os.OutcomeReceiver;
 import android.os.Process;
@@ -87,6 +88,7 @@
 import androidx.test.filters.MediumTest;
 import androidx.test.filters.SmallTest;
 
+import com.android.internal.telecom.IConnectionService;
 import com.android.server.telecom.AnomalyReporterAdapter;
 import com.android.server.telecom.AsyncRingtonePlayer;
 import com.android.server.telecom.Call;
@@ -105,6 +107,7 @@
 import com.android.server.telecom.ConnectionServiceFocusManager;
 import com.android.server.telecom.ConnectionServiceFocusManager.ConnectionServiceFocusManagerFactory;
 import com.android.server.telecom.ConnectionServiceWrapper;
+import com.android.server.telecom.CreateConnectionResponse;
 import com.android.server.telecom.DefaultDialerCache;
 import com.android.server.telecom.EmergencyCallDiagnosticLogger;
 import com.android.server.telecom.EmergencyCallHelper;
@@ -139,7 +142,7 @@
 import com.android.server.telecom.ui.CallStreamingNotification;
 import com.android.server.telecom.ui.DisconnectedCallNotifier;
 import com.android.server.telecom.ui.ToastFactory;
-import com.android.server.telecom.voip.TransactionManager;
+import com.android.server.telecom.callsequencing.TransactionManager;
 
 import com.google.common.base.Objects;
 
@@ -316,6 +319,7 @@
     @Mock private IncomingCallFilterGraph mIncomingCallFilterGraph;
     @Mock private Context mMockCreateContextAsUser;
     @Mock private UserManager mMockCurrentUserManager;
+    @Mock private IConnectionService mIConnectionService;
     @Mock private TelecomMetricsController mMockTelecomMetricsController;
     private CallsManager mCallsManager;
 
@@ -416,11 +420,17 @@
                 .thenReturn(mMockCreateContextAsUser);
         when(mMockCreateContextAsUser.getSystemService(UserManager.class))
                 .thenReturn(mMockCurrentUserManager);
+        when(mIConnectionService.asBinder()).thenReturn(mock(IBinder.class));
+
+        mComponentContextFixture.addConnectionService(
+                SIM_1_ACCOUNT.getAccountHandle().getComponentName(), mIConnectionService);
     }
 
     @Override
     @After
     public void tearDown() throws Exception {
+        mComponentContextFixture.removeConnectionService(
+                SIM_1_ACCOUNT.getAccountHandle().getComponentName(), mIConnectionService);
         super.tearDown();
     }
 
@@ -3241,6 +3251,35 @@
         assertTrue(result.contains("onReceiveResult"));
     }
 
+    @Test
+    public void testConnectionServiceCreateConnectionTimeout() throws Exception {
+        ConnectionServiceWrapper service = new ConnectionServiceWrapper(
+                SIM_1_ACCOUNT.getAccountHandle().getComponentName(), null,
+                mPhoneAccountRegistrar, mCallsManager, mContext, mLock, null, mFeatureFlags);
+        TestScheduledExecutorService scheduledExecutorService = new TestScheduledExecutorService();
+        service.setScheduledExecutorService(scheduledExecutorService);
+        Call call = addSpyCall();
+        service.addCall(call);
+        when(call.isCreateConnectionComplete()).thenReturn(false);
+        CreateConnectionResponse response = mock(CreateConnectionResponse.class);
+
+        service.createConnection(call, response);
+        waitUntilConditionIsTrueOrTimeout(new Condition() {
+            @Override
+            public Object expected() {
+                return true;
+            }
+
+            @Override
+            public Object actual() {
+                return scheduledExecutorService.isRunnableScheduledAtTime(15000L);
+            }
+        }, 5000L, "Expected job failed to schedule");
+        scheduledExecutorService.advanceTime(15000L);
+        verify(response).handleCreateConnectionFailure(
+                eq(new DisconnectCause(DisconnectCause.ERROR)));
+    }
+
     @SmallTest
     @Test
     public void testOnFailedOutgoingCallUnholdsCallAfterLocallyDisconnect() {
diff --git a/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java b/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
index 25f94c6..1432834 100644
--- a/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
+++ b/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
@@ -32,6 +32,7 @@
 import android.annotation.RequiresPermission;
 import android.app.AppOpsManager;
 import android.app.NotificationManager;
+import android.app.StatsManager;
 import android.app.StatusBarManager;
 import android.app.UiModeManager;
 import android.app.role.RoleManager;
@@ -258,6 +259,8 @@
                     return mAccessibilityManager;
                 case Context.BLOCKED_NUMBERS_SERVICE:
                     return mBlockedNumbersManager;
+                case Context.STATS_MANAGER_SERVICE:
+                    return mStatsManager;
                 default:
                     return null;
             }
@@ -301,6 +304,10 @@
                 return Context.TELECOM_SERVICE;
             } else if (svcClass == BlockedNumbersManager.class) {
                 return Context.BLOCKED_NUMBERS_SERVICE;
+            } else if (svcClass == AppOpsManager.class) {
+                return Context.APP_OPS_SERVICE;
+            } else if (svcClass == StatsManager.class) {
+                return Context.STATS_MANAGER_SERVICE;
             }
             throw new UnsupportedOperationException(svcClass.getName());
         }
@@ -642,6 +649,7 @@
     private final PermissionInfo mPermissionInfo = mock(PermissionInfo.class);
     private final SensorPrivacyManager mSensorPrivacyManager = mock(SensorPrivacyManager.class);
     private final List<BroadcastReceiver> mBroadcastReceivers = new ArrayList<>();
+    private final StatsManager mStatsManager = mock(StatsManager.class);
 
     private TelecomManager mTelecomManager = mock(TelecomManager.class);
     private BlockedNumbersManager mBlockedNumbersManager = mock(BlockedNumbersManager.class);
diff --git a/tests/src/com/android/server/telecom/tests/DefaultDialerCacheTest.java b/tests/src/com/android/server/telecom/tests/DefaultDialerCacheTest.java
index 18f2eb0..3da9284 100644
--- a/tests/src/com/android/server/telecom/tests/DefaultDialerCacheTest.java
+++ b/tests/src/com/android/server/telecom/tests/DefaultDialerCacheTest.java
@@ -56,14 +56,17 @@
     private static final int USER0 = 0;
     private static final int USER1 = 1;
     private static final int USER2 = 2;
+    private static final int DELAY_TOLERANCE = 100;
 
     private DefaultDialerCache mDefaultDialerCache;
     private ContentObserver mDefaultDialerSettingObserver;
     private BroadcastReceiver mPackageChangeReceiver;
     private BroadcastReceiver mUserRemovedReceiver;
 
-    @Mock private DefaultDialerCache.DefaultDialerManagerAdapter mMockDefaultDialerManager;
-    @Mock private RoleManagerAdapter mRoleManagerAdapter;
+    @Mock
+    private DefaultDialerCache.DefaultDialerManagerAdapter mMockDefaultDialerManager;
+    @Mock
+    private RoleManagerAdapter mRoleManagerAdapter;
 
     @Override
     @Before
@@ -76,18 +79,19 @@
 
         mDefaultDialerCache = new DefaultDialerCache(
                 mContext, mMockDefaultDialerManager, mRoleManagerAdapter,
-                new TelecomSystem.SyncRoot() { });
+                new TelecomSystem.SyncRoot() {
+                });
 
         verify(mContext, times(2)).registerReceiverAsUser(
-            packageReceiverCaptor.capture(), eq(UserHandle.ALL), any(IntentFilter.class),
+                packageReceiverCaptor.capture(), eq(UserHandle.ALL), any(IntentFilter.class),
                 isNull(String.class), isNull(Handler.class));
         // Receive the first receiver that was captured, the package change receiver.
         mPackageChangeReceiver = packageReceiverCaptor.getAllValues().get(0);
 
         ArgumentCaptor<BroadcastReceiver> userRemovedReceiverCaptor =
-            ArgumentCaptor.forClass(BroadcastReceiver.class);
+                ArgumentCaptor.forClass(BroadcastReceiver.class);
         verify(mContext).registerReceiver(
-            userRemovedReceiverCaptor.capture(), any(IntentFilter.class));
+                userRemovedReceiverCaptor.capture(), any(IntentFilter.class));
         mUserRemovedReceiver = userRemovedReceiverCaptor.getAllValues().get(0);
 
         mDefaultDialerSettingObserver = mDefaultDialerCache.getContentObserver();
@@ -140,7 +144,10 @@
         Intent packageChangeIntent = new Intent(Intent.ACTION_PACKAGE_CHANGED,
                 Uri.fromParts("package", DIALER1, null));
         when(mRoleManagerAdapter.getDefaultDialerApp(eq(USER0))).thenReturn(DIALER2);
+
         mPackageChangeReceiver.onReceive(mContext, packageChangeIntent);
+        waitForHandlerAction(mDefaultDialerCache.mHandler, DELAY_TOLERANCE);
+
         verify(mRoleManagerAdapter, times(2)).getDefaultDialerApp(eq(USER0));
         verify(mRoleManagerAdapter, times(2)).getDefaultDialerApp(eq(USER1));
         verify(mRoleManagerAdapter, times(2)).getDefaultDialerApp(eq(USER2));
@@ -158,6 +165,8 @@
         Intent packageChangeIntent = new Intent(Intent.ACTION_PACKAGE_CHANGED,
                 Uri.fromParts("package", "red.orange.blue", null));
         mPackageChangeReceiver.onReceive(mContext, packageChangeIntent);
+        waitForHandlerAction(mDefaultDialerCache.mHandler, DELAY_TOLERANCE);
+
         verify(mRoleManagerAdapter, times(2)).getDefaultDialerApp(eq(USER0));
         verify(mRoleManagerAdapter, times(2)).getDefaultDialerApp(eq(USER1));
         verify(mRoleManagerAdapter, times(2)).getDefaultDialerApp(eq(USER2));
@@ -192,6 +201,8 @@
         packageChangeIntent.putExtra(Intent.EXTRA_REPLACING, false);
 
         mPackageChangeReceiver.onReceive(mContext, packageChangeIntent);
+        waitForHandlerAction(mDefaultDialerCache.mHandler, DELAY_TOLERANCE);
+
         verify(mRoleManagerAdapter, times(2)).getDefaultDialerApp(eq(USER0));
         verify(mRoleManagerAdapter, times(1)).getDefaultDialerApp(eq(USER1));
         verify(mRoleManagerAdapter, times(1)).getDefaultDialerApp(eq(USER2));
@@ -208,6 +219,8 @@
                 Uri.fromParts("package", "ppp.qqq.zzz", null));
 
         mPackageChangeReceiver.onReceive(mContext, packageChangeIntent);
+        waitForHandlerAction(mDefaultDialerCache.mHandler, DELAY_TOLERANCE);
+
         verify(mRoleManagerAdapter, times(2)).getDefaultDialerApp(eq(USER0));
         verify(mRoleManagerAdapter, times(2)).getDefaultDialerApp(eq(USER1));
         verify(mRoleManagerAdapter, times(2)).getDefaultDialerApp(eq(USER2));
@@ -225,6 +238,8 @@
         packageChangeIntent.putExtra(Intent.EXTRA_REPLACING, true);
 
         mPackageChangeReceiver.onReceive(mContext, packageChangeIntent);
+        waitForHandlerAction(mDefaultDialerCache.mHandler, DELAY_TOLERANCE);
+
         verify(mRoleManagerAdapter, times(1)).getDefaultDialerApp(eq(USER0));
         verify(mRoleManagerAdapter, times(1)).getDefaultDialerApp(eq(USER1));
         verify(mRoleManagerAdapter, times(1)).getDefaultDialerApp(eq(USER2));
@@ -240,7 +255,9 @@
         when(mRoleManagerAdapter.getDefaultDialerApp(eq(USER0))).thenReturn(DIALER2);
         when(mRoleManagerAdapter.getDefaultDialerApp(eq(USER1))).thenReturn(DIALER2);
         when(mRoleManagerAdapter.getDefaultDialerApp(eq(USER2))).thenReturn(DIALER2);
+
         mDefaultDialerSettingObserver.onChange(false);
+        waitForHandlerAction(mDefaultDialerCache.mHandler, DELAY_TOLERANCE);
 
         verify(mRoleManagerAdapter, times(2)).getDefaultDialerApp(eq(USER0));
         verify(mRoleManagerAdapter, times(2)).getDefaultDialerApp(eq(USER2));
diff --git a/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java b/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java
index 45b4ed1..a480a7b 100644
--- a/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java
+++ b/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java
@@ -113,7 +113,8 @@
     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 = new UserHandle(10);
+    private final UserHandle USER_HANDLE_10 = UserHandle.of(10);
+    private final UserHandle USER_HANDLE_1000 = UserHandle.of(1000);
     private final TelecomSystem.SyncRoot mLock = new TelecomSystem.SyncRoot() { };
     private PhoneAccountRegistrar mRegistrar;
     @Mock private SubscriptionManager mSubscriptionManager;
@@ -135,11 +136,12 @@
                 .delete();
         when(mDefaultDialerCache.getDefaultDialerApplication(anyInt()))
                 .thenReturn("com.android.dialer");
-        when(mAppLabelProxy.getAppLabel(anyString()))
+        when(mAppLabelProxy.getAppLabel(anyString(), any()))
                 .thenReturn(TEST_LABEL);
         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);
@@ -1306,8 +1308,7 @@
                 Mockito.mock(IConnectionService.class));
         UserManager userManager = mContext.getSystemService(UserManager.class);
 
-        List<UserHandle> users = Arrays.asList(new UserHandle(0),
-                new UserHandle(1000));
+        List<UserHandle> users = Arrays.asList(UserHandle.SYSTEM, USER_HANDLE_1000);
 
         PhoneAccount pa1 = new PhoneAccount.Builder(
                 new PhoneAccountHandle(new ComponentName(PACKAGE_1, COMPONENT_NAME), "1234",
@@ -1606,7 +1607,7 @@
                 .setCapabilities(PhoneAccount.CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS);
 
         // WHEN
-        when(mAppLabelProxy.getAppLabel(anyString())).thenReturn(invalidLabel);
+        when(mAppLabelProxy.getAppLabel(anyString(), any())).thenReturn(invalidLabel);
 
         // THEN
         try {
diff --git a/tests/src/com/android/server/telecom/tests/SessionManagerTest.java b/tests/src/com/android/server/telecom/tests/SessionManagerTest.java
index 3e82eac..631d522 100644
--- a/tests/src/com/android/server/telecom/tests/SessionManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/SessionManagerTest.java
@@ -21,10 +21,14 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
+import android.telecom.Log;
 import android.telecom.Logging.Session;
 import android.telecom.Logging.SessionManager;
 
+import com.android.server.telecom.flags.Flags;
+
 import androidx.test.filters.SmallTest;
 
 import org.junit.After;
@@ -57,13 +61,11 @@
     @Before
     public void setUp() throws Exception {
         super.setUp();
-        mTestSessionManager = new SessionManager();
+        mTestSessionManager = new SessionManager(null);
         mTestSessionManager.registerSessionListener(((sessionName, timeMs) -> {
             mfullSessionCompleteTime = timeMs;
             mFullSessionMethodName = sessionName;
         }));
-        // Remove automatic stale session cleanup for testing
-        mTestSessionManager.mCleanStaleSessions = null;
     }
 
     @Override
@@ -411,4 +413,33 @@
         assertTrue(mTestSessionManager.mSessionMapper.isEmpty());
         assertNull(sessionRef.get());
     }
+
+    /**
+     * If Telecom gets into a situation where there are MANY sub-sessions created in a deep tree,
+     * ensure that cleanup still happens properly.
+     */
+    @SmallTest
+    @Test
+    public void testManySubsessionCleanupStress() {
+        // This test will mostly likely fail with recursion due to stack overflow
+        if (!Flags.endSessionImprovements()) return;
+        Log.setIsExtendedLoggingEnabled(false);
+        mTestSessionManager.mCurrentThreadId = () -> TEST_PARENT_THREAD_ID;
+        mTestSessionManager.startSession(TEST_PARENT_NAME, null);
+        Session parentSession = mTestSessionManager.mSessionMapper.get(TEST_PARENT_THREAD_ID);
+        Session subsession;
+        try {
+            for (int i = 0; i < 10000; i++) {
+                subsession = mTestSessionManager.createSubsession();
+                mTestSessionManager.endSession();
+                mTestSessionManager.continueSession(subsession, TEST_CHILD_NAME + i);
+            }
+            mTestSessionManager.endSession();
+        } catch (Exception e) {
+            fail("Exception: " + e);
+        }
+        assertTrue(mTestSessionManager.mSessionMapper.isEmpty());
+        assertTrue(parentSession.isSessionCompleted());
+        assertTrue(parentSession.getChildSessions().isEmpty());
+    }
 }
diff --git a/tests/src/com/android/server/telecom/tests/SessionTest.java b/tests/src/com/android/server/telecom/tests/SessionTest.java
index 5378596..4cddc89 100644
--- a/tests/src/com/android/server/telecom/tests/SessionTest.java
+++ b/tests/src/com/android/server/telecom/tests/SessionTest.java
@@ -269,6 +269,6 @@
     }
 
     private Session createTestSession(String name, String methodName) {
-        return new Session(name, methodName, 0, false, null);
+        return new Session(name, methodName, 0, false, false ,null);
     }
 }
diff --git a/tests/src/com/android/server/telecom/tests/TelecomMetricsControllerTest.java b/tests/src/com/android/server/telecom/tests/TelecomMetricsControllerTest.java
index e2ab8d6..4d494f3 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomMetricsControllerTest.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomMetricsControllerTest.java
@@ -21,8 +21,11 @@
 import static com.android.server.telecom.TelecomStatsLog.TELECOM_ERROR_STATS;
 import static com.google.common.truth.Truth.assertThat;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyObject;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
 import android.app.StatsManager;
@@ -119,18 +122,27 @@
     }
 
     @Test
-    public void testRegisterAtomIsSameInstance() {
+    public void testRegisterAtom() {
+        StatsManager statsManager = mContext.getSystemService(StatsManager.class);
         ApiStats stats = mock(ApiStats.class);
 
         mTelecomMetricsController.registerAtom(TELECOM_API_STATS, stats);
 
+        verify(statsManager, times(1)).setPullAtomCallback(eq(TELECOM_API_STATS), anyObject(),
+                anyObject(), eq(mTelecomMetricsController));
         assertThat(mTelecomMetricsController.getStats().get(TELECOM_API_STATS))
                 .isSameInstanceAs(stats);
     }
 
     @Test
     public void testDestroy() {
+        StatsManager statsManager = mContext.getSystemService(StatsManager.class);
         mTelecomMetricsController.destroy();
+
+        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();
     }
 
diff --git a/tests/src/com/android/server/telecom/tests/TelecomPulledAtomTest.java b/tests/src/com/android/server/telecom/tests/TelecomPulledAtomTest.java
index bc8aeac..8ae734c 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomPulledAtomTest.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomPulledAtomTest.java
@@ -37,10 +37,13 @@
 import static org.mockito.Mockito.verify;
 
 import android.app.StatsManager;
+import android.content.ComponentName;
 import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
 import android.os.Looper;
-import android.os.UserHandle;
 import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
 import android.util.StatsEvent;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -66,7 +69,10 @@
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.Random;
 
 @RunWith(AndroidJUnit4.class)
 public class TelecomPulledAtomTest extends TelecomTestCase {
@@ -288,26 +294,108 @@
     }
 
     @Test
-    public void testApiStatsLog() throws Exception {
+    public void testApiStatsLogCount() throws Exception {
         ApiStats apiStats = spy(new ApiStats(mSpyContext, mLooper));
+        ApiStats.ApiEvent event = new ApiStats.ApiEvent(VALUE_API_ID, VALUE_UID, VALUE_API_RESULT);
 
-        apiStats.log(VALUE_API_ID, VALUE_UID, VALUE_API_RESULT);
-        waitForHandlerAction(apiStats, TEST_TIMEOUT);
+        for (int i = 0; i < 10; i++) {
+            apiStats.log(event);
+            waitForHandlerAction(apiStats, TEST_TIMEOUT);
 
-        verify(apiStats, times(1)).onAggregate();
-        verify(apiStats, times(1)).save(eq(DELAY_FOR_PERSISTENT_MILLIS));
-        assertEquals(apiStats.mPulledAtoms.telecomApiStats.length, 1);
-        verifyMessageForApiStats(apiStats.mPulledAtoms.telecomApiStats[0], VALUE_API_ID,
-                VALUE_UID, VALUE_API_RESULT, 1);
+            verify(apiStats, times(i + 1)).onAggregate();
+            verify(apiStats, times(i + 1)).save(eq(DELAY_FOR_PERSISTENT_MILLIS));
+            assertEquals(apiStats.mPulledAtoms.telecomApiStats.length, 1);
+            verifyMessageForApiStats(apiStats.mPulledAtoms.telecomApiStats[0], VALUE_API_ID,
+                    VALUE_UID, VALUE_API_RESULT, i + 1);
+        }
+    }
 
-        apiStats.log(VALUE_API_ID, VALUE_UID, VALUE_API_RESULT);
-        waitForHandlerAction(apiStats, TEST_TIMEOUT);
+    @Test
+    public void testApiStatsLogEvent() throws Exception {
+        final int[] apis = {
+                ApiStats.API_UNSPECIFIC,
+                ApiStats.API_ACCEPTHANDOVER,
+                ApiStats.API_ACCEPTRINGINGCALL,
+                ApiStats.API_ACCEPTRINGINGCALLWITHVIDEOSTATE,
+                ApiStats.API_ADDCALL,
+                ApiStats.API_ADDNEWINCOMINGCALL,
+                ApiStats.API_ADDNEWINCOMINGCONFERENCE,
+                ApiStats.API_ADDNEWUNKNOWNCALL,
+                ApiStats.API_CANCELMISSEDCALLSNOTIFICATION,
+                ApiStats.API_CLEARACCOUNTS,
+                ApiStats.API_CREATELAUNCHEMERGENCYDIALERINTENT,
+                ApiStats.API_CREATEMANAGEBLOCKEDNUMBERSINTENT,
+                ApiStats.API_DUMP,
+                ApiStats.API_DUMPCALLANALYTICS,
+                ApiStats.API_ENABLEPHONEACCOUNT,
+                ApiStats.API_ENDCALL,
+                ApiStats.API_GETADNURIFORPHONEACCOUNT,
+                ApiStats.API_GETALLPHONEACCOUNTHANDLES,
+                ApiStats.API_GETALLPHONEACCOUNTS,
+                ApiStats.API_GETALLPHONEACCOUNTSCOUNT,
+                ApiStats.API_GETCALLCAPABLEPHONEACCOUNTS,
+                ApiStats.API_GETCALLSTATE,
+                ApiStats.API_GETCALLSTATEUSINGPACKAGE,
+                ApiStats.API_GETCURRENTTTYMODE,
+                ApiStats.API_GETDEFAULTDIALERPACKAGE,
+                ApiStats.API_GETDEFAULTDIALERPACKAGEFORUSER,
+                ApiStats.API_GETDEFAULTOUTGOINGPHONEACCOUNT,
+                ApiStats.API_GETDEFAULTPHONEAPP,
+                ApiStats.API_GETLINE1NUMBER,
+                ApiStats.API_GETOWNSELFMANAGEDPHONEACCOUNTS,
+                ApiStats.API_GETPHONEACCOUNT,
+                ApiStats.API_GETPHONEACCOUNTSFORPACKAGE,
+                ApiStats.API_GETPHONEACCOUNTSSUPPORTINGSCHEME,
+                ApiStats.API_GETREGISTEREDPHONEACCOUNTS,
+                ApiStats.API_GETSELFMANAGEDPHONEACCOUNTS,
+                ApiStats.API_GETSIMCALLMANAGER,
+                ApiStats.API_GETSIMCALLMANAGERFORUSER,
+                ApiStats.API_GETSYSTEMDIALERPACKAGE,
+                ApiStats.API_GETUSERSELECTEDOUTGOINGPHONEACCOUNT,
+                ApiStats.API_GETVOICEMAILNUMBER,
+                ApiStats.API_HANDLEPINMMI,
+                ApiStats.API_HANDLEPINMMIFORPHONEACCOUNT,
+                ApiStats.API_HASMANAGEONGOINGCALLSPERMISSION,
+                ApiStats.API_ISINCALL,
+                ApiStats.API_ISINCOMINGCALLPERMITTED,
+                ApiStats.API_ISINEMERGENCYCALL,
+                ApiStats.API_ISINMANAGEDCALL,
+                ApiStats.API_ISINSELFMANAGEDCALL,
+                ApiStats.API_ISOUTGOINGCALLPERMITTED,
+                ApiStats.API_ISRINGING,
+                ApiStats.API_ISTTYSUPPORTED,
+                ApiStats.API_ISVOICEMAILNUMBER,
+                ApiStats.API_PLACECALL,
+                ApiStats.API_REGISTERPHONEACCOUNT,
+                ApiStats.API_SETDEFAULTDIALER,
+                ApiStats.API_SETUSERSELECTEDOUTGOINGPHONEACCOUNT,
+                ApiStats.API_SHOWINCALLSCREEN,
+                ApiStats.API_SILENCERINGER,
+                ApiStats.API_STARTCONFERENCE,
+                ApiStats.API_UNREGISTERPHONEACCOUNT,
+        };
+        final int[] results = {ApiStats.RESULT_UNKNOWN, ApiStats.RESULT_NORMAL,
+                ApiStats.RESULT_EXCEPTION, ApiStats.RESULT_PERMISSION};
+        ApiStats apiStats = spy(new ApiStats(mSpyContext, mLooper));
+        Random rand = new Random();
+        Map<ApiStats.ApiEvent, Integer> eventMap = new HashMap<>();
 
-        verify(apiStats, times(2)).onAggregate();
-        verify(apiStats, times(2)).save(eq(DELAY_FOR_PERSISTENT_MILLIS));
-        assertEquals(apiStats.mPulledAtoms.telecomApiStats.length, 1);
-        verifyMessageForApiStats(apiStats.mPulledAtoms.telecomApiStats[0], VALUE_API_ID,
-                VALUE_UID, VALUE_API_RESULT, 2);
+        for (int i = 0; i < 10; i++) {
+            int api = apis[rand.nextInt(apis.length)];
+            int uid = rand.nextInt(65535);
+            int result = results[rand.nextInt(results.length)];
+            ApiStats.ApiEvent event = new ApiStats.ApiEvent(api, uid, result);
+            eventMap.put(event, eventMap.getOrDefault(event, 0) + 1);
+
+            apiStats.log(event);
+            waitForHandlerAction(apiStats, TEST_TIMEOUT);
+
+            verify(apiStats, times(i + 1)).onAggregate();
+            verify(apiStats, times(i + 1)).save(eq(DELAY_FOR_PERSISTENT_MILLIS));
+            assertEquals(apiStats.mPulledAtoms.telecomApiStats.length, eventMap.size());
+            assertTrue(hasMessageForApiStats(apiStats.mPulledAtoms.telecomApiStats,
+                    api, uid, result, eventMap.get(event)));
+        }
     }
 
     @Test
@@ -570,8 +658,19 @@
     @Test
     public void testCallStatsOnStartThenEnd() throws Exception {
         int duration = 1000;
-        UserHandle uh = UserHandle.of(UserHandle.USER_SYSTEM);
+        int fakeUid = 10010;
         PhoneAccount account = mock(PhoneAccount.class);
+        Call.CallingPackageIdentity callingPackage = new Call.CallingPackageIdentity();
+        PackageManager pm = mock(PackageManager.class);
+        ApplicationInfo ai = new ApplicationInfo();
+        ai.uid = fakeUid;
+        doReturn(ai).when(pm).getApplicationInfo(any(), anyInt());
+        doReturn(pm).when(mSpyContext).getPackageManager();
+        Context fakeContext = spy(mContext);
+        doReturn("").when(fakeContext).getPackageName();
+        ComponentName cn = new ComponentName(fakeContext, this.getClass());
+        PhoneAccountHandle handle = mock(PhoneAccountHandle.class);
+        doReturn(cn).when(handle).getComponentName();
         Call call = mock(Call.class);
         doReturn(true).when(call).isIncoming();
         doReturn(account).when(call).getPhoneAccountFromHandle();
@@ -579,7 +678,8 @@
         doReturn(false).when(account).hasCapabilities(eq(PhoneAccount.CAPABILITY_SELF_MANAGED));
         doReturn(true).when(account).hasCapabilities(eq(PhoneAccount.CAPABILITY_CALL_PROVIDER));
         doReturn(true).when(account).hasCapabilities(eq(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION));
-        doReturn(uh).when(call).getAssociatedUser();
+        doReturn(callingPackage).when(call).getCallingPackageIdentity();
+        doReturn(handle).when(call).getTargetPhoneAccount();
         CallStats callStats = spy(new CallStats(mSpyContext, mLooper));
 
         callStats.onCallStart(call);
@@ -590,14 +690,25 @@
 
         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(UserHandle.USER_SYSTEM), eq(duration));
+                eq(fakeUid), eq(duration));
     }
 
     @Test
     public void testCallStatsOnMultipleAudioDevices() throws Exception {
         int duration = 1000;
-        UserHandle uh = UserHandle.of(UserHandle.USER_SYSTEM);
+        int fakeUid = 10010;
         PhoneAccount account = mock(PhoneAccount.class);
+        Call.CallingPackageIdentity callingPackage = new Call.CallingPackageIdentity();
+        PackageManager pm = mock(PackageManager.class);
+        ApplicationInfo ai = new ApplicationInfo();
+        ai.uid = fakeUid;
+        doReturn(ai).when(pm).getApplicationInfo(any(), anyInt());
+        doReturn(pm).when(mSpyContext).getPackageManager();
+        Context fakeContext = spy(mContext);
+        doReturn("").when(fakeContext).getPackageName();
+        ComponentName cn = new ComponentName(fakeContext, this.getClass());
+        PhoneAccountHandle handle = mock(PhoneAccountHandle.class);
+        doReturn(cn).when(handle).getComponentName();
         Call call = mock(Call.class);
         doReturn(true).when(call).isIncoming();
         doReturn(account).when(call).getPhoneAccountFromHandle();
@@ -605,7 +716,8 @@
         doReturn(false).when(account).hasCapabilities(eq(PhoneAccount.CAPABILITY_SELF_MANAGED));
         doReturn(true).when(account).hasCapabilities(eq(PhoneAccount.CAPABILITY_CALL_PROVIDER));
         doReturn(true).when(account).hasCapabilities(eq(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION));
-        doReturn(uh).when(call).getAssociatedUser();
+        doReturn(callingPackage).when(call).getCallingPackageIdentity();
+        doReturn(handle).when(call).getTargetPhoneAccount();
         CallStats callStats = spy(new CallStats(mSpyContext, mLooper));
 
         callStats.onCallStart(call);
@@ -619,30 +731,88 @@
 
         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(UserHandle.USER_SYSTEM), eq(duration));
+                eq(fakeUid), eq(duration));
     }
 
     @Test
-    public void testErrorStatsLog() throws Exception {
+    public void testErrorStatsLogCount() throws Exception {
         ErrorStats errorStats = spy(new ErrorStats(mSpyContext, mLooper));
+        for (int i = 0; i < 10; i++) {
+            errorStats.log(VALUE_MODULE_ID, VALUE_ERROR_ID);
+            waitForHandlerAction(errorStats, TEST_TIMEOUT);
 
-        errorStats.log(VALUE_MODULE_ID, VALUE_ERROR_ID);
-        waitForHandlerAction(errorStats, TEST_TIMEOUT);
+            verify(errorStats, times(i + 1)).onAggregate();
+            verify(errorStats, times(i + 1)).save(eq(DELAY_FOR_PERSISTENT_MILLIS));
+            assertEquals(errorStats.mPulledAtoms.telecomErrorStats.length, 1);
+            verifyMessageForErrorStats(errorStats.mPulledAtoms.telecomErrorStats[0],
+                    VALUE_MODULE_ID,
+                    VALUE_ERROR_ID, i + 1);
+        }
+    }
 
-        verify(errorStats, times(1)).onAggregate();
-        verify(errorStats, times(1)).save(eq(DELAY_FOR_PERSISTENT_MILLIS));
-        assertEquals(errorStats.mPulledAtoms.telecomErrorStats.length, 1);
-        verifyMessageForErrorStats(errorStats.mPulledAtoms.telecomErrorStats[0], VALUE_MODULE_ID,
-                VALUE_ERROR_ID, 1);
+    @Test
+    public void testErrorStatsLogEvent() throws Exception {
+        ErrorStats errorStats = spy(new ErrorStats(mSpyContext, mLooper));
+        int[] modules = {
+                ErrorStats.SUB_UNKNOWN,
+                ErrorStats.SUB_CALL_AUDIO,
+                ErrorStats.SUB_CALL_LOGS,
+                ErrorStats.SUB_CALL_MANAGER,
+                ErrorStats.SUB_CONNECTION_SERVICE,
+                ErrorStats.SUB_EMERGENCY_CALL,
+                ErrorStats.SUB_IN_CALL_SERVICE,
+                ErrorStats.SUB_MISC,
+                ErrorStats.SUB_PHONE_ACCOUNT,
+                ErrorStats.SUB_SYSTEM_SERVICE,
+                ErrorStats.SUB_TELEPHONY,
+                ErrorStats.SUB_UI,
+                ErrorStats.SUB_VOIP_CALL,
+        };
+        int[] errors = {
+                ErrorStats.ERROR_UNKNOWN,
+                ErrorStats.ERROR_EXTERNAL_EXCEPTION,
+                ErrorStats.ERROR_INTERNAL_EXCEPTION,
+                ErrorStats.ERROR_AUDIO_ROUTE_RETRY_REJECTED,
+                ErrorStats.ERROR_BT_GET_SERVICE_FAILURE,
+                ErrorStats.ERROR_BT_REGISTER_CALLBACK_FAILURE,
+                ErrorStats.ERROR_AUDIO_ROUTE_UNAVAILABLE,
+                ErrorStats.ERROR_EMERGENCY_NUMBER_DETERMINED_FAILURE,
+                ErrorStats.ERROR_NOTIFY_CALL_STREAM_START_FAILURE,
+                ErrorStats.ERROR_NOTIFY_CALL_STREAM_STATE_CHANGED_FAILURE,
+                ErrorStats.ERROR_NOTIFY_CALL_STREAM_STOP_FAILURE,
+                ErrorStats.ERROR_RTT_STREAM_CLOSE_FAILURE,
+                ErrorStats.ERROR_RTT_STREAM_CREATE_FAILURE,
+                ErrorStats.ERROR_SET_MUTED_FAILURE,
+                ErrorStats.ERROR_VIDEO_PROVIDER_SET_FAILURE,
+                ErrorStats.ERROR_WIRED_HEADSET_NOT_AVAILABLE,
+                ErrorStats.ERROR_LOG_CALL_FAILURE,
+                ErrorStats.ERROR_RETRIEVING_ACCOUNT_EMERGENCY,
+                ErrorStats.ERROR_RETRIEVING_ACCOUNT,
+                ErrorStats.ERROR_EMERGENCY_CALL_ABORTED_NO_ACCOUNT,
+                ErrorStats.ERROR_DEFAULT_MO_ACCOUNT_MISMATCH,
+                ErrorStats.ERROR_ESTABLISHING_CONNECTION,
+                ErrorStats.ERROR_REMOVING_CALL,
+                ErrorStats.ERROR_STUCK_CONNECTING_EMERGENCY,
+                ErrorStats.ERROR_STUCK_CONNECTING,
+        };
+        Random rand = new Random();
+        Map<Long, Integer> eventMap = new HashMap<>();
 
-        errorStats.log(VALUE_MODULE_ID, VALUE_ERROR_ID);
-        waitForHandlerAction(errorStats, TEST_TIMEOUT);
+        for (int i = 0; i < 10; i++) {
+            int module = modules[rand.nextInt(modules.length)];
+            int error = errors[rand.nextInt(errors.length)];
+            long key = (long) module << 32 | error;
+            eventMap.put(key, eventMap.getOrDefault(key, 0) + 1);
 
-        verify(errorStats, times(2)).onAggregate();
-        verify(errorStats, times(2)).save(eq(DELAY_FOR_PERSISTENT_MILLIS));
-        assertEquals(errorStats.mPulledAtoms.telecomErrorStats.length, 1);
-        verifyMessageForErrorStats(errorStats.mPulledAtoms.telecomErrorStats[0], VALUE_MODULE_ID,
-                VALUE_ERROR_ID, 2);
+            errorStats.log(module, error);
+            waitForHandlerAction(errorStats, DELAY_TOLERANCE);
+
+            verify(errorStats, times(i + 1)).onAggregate();
+            verify(errorStats, times(i + 1)).save(eq(DELAY_FOR_PERSISTENT_MILLIS));
+            assertEquals(errorStats.mPulledAtoms.telecomErrorStats.length, eventMap.size());
+            assertTrue(hasMessageForErrorStats(
+                    errorStats.mPulledAtoms.telecomErrorStats, module, error, eventMap.get(key)));
+        }
     }
 
     private void createTestFileForApiStats(long timestamps) throws IOException {
@@ -664,7 +834,7 @@
     }
 
     private void verifyTestDataForApiStats(final PulledAtomsClass.PulledAtoms atom,
-                                           long timestamps) {
+            long timestamps) {
         assertNotNull(atom);
         assertEquals(atom.getTelecomApiStatsPullTimestampMillis(), timestamps);
         assertNotNull(atom.telecomApiStats);
@@ -677,13 +847,24 @@
     }
 
     private void verifyMessageForApiStats(final PulledAtomsClass.TelecomApiStats msg, int apiId,
-                                          int uid, int result, int count) {
+            int uid, int result, int count) {
         assertEquals(msg.getApiName(), apiId);
         assertEquals(msg.getUid(), uid);
         assertEquals(msg.getApiResult(), result);
         assertEquals(msg.getCount(), count);
     }
 
+    private boolean hasMessageForApiStats(final PulledAtomsClass.TelecomApiStats[] msgs, int apiId,
+            int uid, int result, int count) {
+        for (PulledAtomsClass.TelecomApiStats msg : msgs) {
+            if (msg.getApiName() == apiId && msg.getUid() == uid && msg.getApiResult() == result
+                    && msg.getCount() == count) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     private void createTestFileForAudioRouteStats(long timestamps) throws IOException {
         PulledAtomsClass.PulledAtoms atom = new PulledAtomsClass.PulledAtoms();
         atom.callAudioRouteStats =
@@ -704,7 +885,7 @@
     }
 
     private void verifyTestDataForAudioRouteStats(final PulledAtomsClass.PulledAtoms atom,
-                                                  long timestamps) {
+            long timestamps) {
         assertNotNull(atom);
         assertEquals(atom.getCallAudioRouteStatsPullTimestampMillis(), timestamps);
         assertNotNull(atom.callAudioRouteStats);
@@ -750,7 +931,7 @@
     }
 
     private void verifyTestDataForCallStats(final PulledAtomsClass.PulledAtoms atom,
-                                            long timestamps) {
+            long timestamps) {
         assertNotNull(atom);
         assertEquals(atom.getCallStatsPullTimestampMillis(), timestamps);
         assertNotNull(atom.callStats);
@@ -782,8 +963,8 @@
                 new PulledAtomsClass.TelecomErrorStats[VALUE_ATOM_COUNT];
         for (int i = 0; i < VALUE_ATOM_COUNT; i++) {
             atom.telecomErrorStats[i] = new PulledAtomsClass.TelecomErrorStats();
-            atom.telecomErrorStats[i].setSubmoduleName(VALUE_MODULE_ID);
-            atom.telecomErrorStats[i].setErrorName(VALUE_ERROR_ID);
+            atom.telecomErrorStats[i].setSubmodule(VALUE_MODULE_ID);
+            atom.telecomErrorStats[i].setError(VALUE_ERROR_ID);
             atom.telecomErrorStats[i].setCount(VALUE_ERROR_COUNT);
         }
         atom.setTelecomErrorStatsPullTimestampMillis(timestamps);
@@ -807,8 +988,19 @@
 
     private void verifyMessageForErrorStats(final PulledAtomsClass.TelecomErrorStats msg,
             int moduleId, int errorId, int count) {
-        assertEquals(msg.getSubmoduleName(), moduleId);
-        assertEquals(msg.getErrorName(), errorId);
+        assertEquals(msg.getSubmodule(), moduleId);
+        assertEquals(msg.getError(), errorId);
         assertEquals(msg.getCount(), count);
     }
+
+    private boolean hasMessageForErrorStats(final PulledAtomsClass.TelecomErrorStats[] msgs,
+            int moduleId, int errorId, int count) {
+        for (PulledAtomsClass.TelecomErrorStats msg : msgs) {
+            if (msg.getSubmodule() == moduleId && msg.getError() == errorId
+                    && 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 dc5f325..07a12c4 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java
@@ -96,9 +96,10 @@
 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.voip.IncomingCallTransaction;
-import com.android.server.telecom.voip.OutgoingCallTransaction;
-import com.android.server.telecom.voip.TransactionManager;
+import com.android.server.telecom.metrics.TelecomMetricsController;
+import com.android.server.telecom.callsequencing.voip.IncomingCallTransaction;
+import com.android.server.telecom.callsequencing.voip.OutgoingCallTransaction;
+import com.android.server.telecom.callsequencing.TransactionManager;
 
 import org.junit.After;
 import org.junit.Before;
@@ -204,6 +205,7 @@
     @Mock private com.android.internal.telephony.flags.FeatureFlags mTelephonyFeatureFlags;
 
     @Mock private InCallController mInCallController;
+    @Mock private TelecomMetricsController mMockTelecomMetricsController;
 
     private final TelecomSystem.SyncRoot mLock = new TelecomSystem.SyncRoot() { };
 
@@ -257,7 +259,8 @@
                 mSettingsSecureAdapter,
                 mFeatureFlags,
                 mTelephonyFeatureFlags,
-                mLock);
+                mLock,
+                mMockTelecomMetricsController);
         telecomServiceImpl.setTransactionManager(mTransactionManager);
         telecomServiceImpl.setAnomalyReporterAdapter(mAnomalyReporterAdapter);
         mTSIBinder = telecomServiceImpl.getBinder();
diff --git a/tests/src/com/android/server/telecom/tests/TelecomTestCase.java b/tests/src/com/android/server/telecom/tests/TelecomTestCase.java
index e8389a0..5b5c3ed 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomTestCase.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomTestCase.java
@@ -51,8 +51,8 @@
 
         mComponentContextFixture = new ComponentContextFixture(mFeatureFlags);
         mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
-        Log.setSessionContext(mComponentContextFixture.getTestDouble().getApplicationContext());
-        Log.getSessionManager().mCleanStaleSessions = null;
+        Log.setSessionManager(mComponentContextFixture.getTestDouble().getApplicationContext(),
+                null);
     }
 
     public void tearDown() throws Exception {
diff --git a/tests/src/com/android/server/telecom/tests/TransactionTests.java b/tests/src/com/android/server/telecom/tests/TransactionTests.java
index 5876474..78c2210 100644
--- a/tests/src/com/android/server/telecom/tests/TransactionTests.java
+++ b/tests/src/com/android/server/telecom/tests/TransactionTests.java
@@ -16,24 +16,25 @@
 
 package com.android.server.telecom.tests;
 
-import static com.android.server.telecom.voip.VideoStateTranslation.TransactionalVideoStateToVideoProfileState;
-import static com.android.server.telecom.voip.VideoStateTranslation.VideoProfileStateToTransactionalVideoState;
+import static com.android.server.telecom.callsequencing.voip.VideoStateTranslation
+        .TransactionalVideoStateToVideoProfileState;
+import static com.android.server.telecom.callsequencing.voip.VideoStateTranslation
+        .VideoProfileStateToTransactionalVideoState;
 
+import static org.junit.Assert.assertEquals;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.atLeastOnce;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.timeout;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.isA;
-import static org.junit.Assert.assertEquals;
 import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.isA;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.content.ComponentName;
@@ -59,30 +60,26 @@
 import com.android.server.telecom.CallsManager;
 import com.android.server.telecom.ClockProxy;
 import com.android.server.telecom.ConnectionServiceWrapper;
-import com.android.server.telecom.flags.FeatureFlags;
 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.TransactionManager;
+import com.android.server.telecom.callsequencing.VerifyCallStateChangeTransaction;
+import com.android.server.telecom.callsequencing.voip.EndCallTransaction;
+import com.android.server.telecom.callsequencing.voip.HoldCallTransaction;
+import com.android.server.telecom.callsequencing.voip.IncomingCallTransaction;
+import com.android.server.telecom.callsequencing.voip.MaybeHoldCallForNewCallTransaction;
+import com.android.server.telecom.callsequencing.voip.OutgoingCallTransaction;
+import com.android.server.telecom.callsequencing.voip.RequestNewActiveCallTransaction;
 import com.android.server.telecom.ui.ToastFactory;
-import com.android.server.telecom.voip.EndCallTransaction;
-import com.android.server.telecom.voip.HoldCallTransaction;
-import com.android.server.telecom.voip.IncomingCallTransaction;
-import com.android.server.telecom.voip.OutgoingCallTransaction;
-import com.android.server.telecom.voip.MaybeHoldCallForNewCallTransaction;
-import com.android.server.telecom.voip.RequestNewActiveCallTransaction;
-import com.android.server.telecom.voip.TransactionManager;
-import com.android.server.telecom.voip.VerifyCallStateChangeTransaction;
-import com.android.server.telecom.voip.VideoStateTranslation;
-import com.android.server.telecom.voip.VoipCallTransactionResult;
 
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
-
 import org.mockito.Mock;
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
-import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
@@ -421,7 +418,7 @@
 
         // THEN
         verify(mMockCall1, times(1)).addCallStateListener(t.getCallStateListenerImpl());
-        assertEquals(VoipCallTransactionResult.RESULT_SUCCEED,
+        assertEquals(CallTransactionResult.RESULT_SUCCEED,
                 t.getTransactionResult().get(2, TimeUnit.SECONDS).getResult());
         verify(mMockCall1, atLeastOnce()).removeCallStateListener(any());
     }
diff --git a/tests/src/com/android/server/telecom/tests/TransactionalServiceWrapperTest.java b/tests/src/com/android/server/telecom/tests/TransactionalServiceWrapperTest.java
index fa5f2a2..fea6135 100644
--- a/tests/src/com/android/server/telecom/tests/TransactionalServiceWrapperTest.java
+++ b/tests/src/com/android/server/telecom/tests/TransactionalServiceWrapperTest.java
@@ -39,10 +39,10 @@
 import com.android.server.telecom.TelecomSystem;
 import com.android.server.telecom.TransactionalServiceRepository;
 import com.android.server.telecom.TransactionalServiceWrapper;
-import com.android.server.telecom.voip.EndCallTransaction;
-import com.android.server.telecom.voip.HoldCallTransaction;
-import com.android.server.telecom.voip.SerialTransaction;
-import com.android.server.telecom.voip.TransactionManager;
+import com.android.server.telecom.callsequencing.voip.EndCallTransaction;
+import com.android.server.telecom.callsequencing.voip.HoldCallTransaction;
+import com.android.server.telecom.callsequencing.voip.SerialTransaction;
+import com.android.server.telecom.callsequencing.TransactionManager;
 
 import org.junit.After;
 import org.junit.Before;
@@ -83,9 +83,8 @@
         Mockito.when(mCallsManager.getLock()).thenReturn(mLock);
         Mockito.when(mCallEventCallback.asBinder()).thenReturn(mIBinder);
         mTransactionalServiceWrapper = new TransactionalServiceWrapper(mCallEventCallback,
-                mCallsManager, SERVICE_HANDLE, mMockCall1, mRepository);
-
-        mTransactionalServiceWrapper.setTransactionManager(mTransactionManager);
+                mCallsManager, SERVICE_HANDLE, mMockCall1, mRepository, mTransactionManager,
+                false /*call sequencing*/);
     }
 
     @Override
@@ -98,7 +97,8 @@
     public void testTransactionalServiceWrapperStartState() throws Exception {
         TransactionalServiceWrapper service =
                 new TransactionalServiceWrapper(mCallEventCallback,
-                        mCallsManager, SERVICE_HANDLE, mMockCall1, mRepository);
+                        mCallsManager, SERVICE_HANDLE, mMockCall1, mRepository, mTransactionManager,
+                        false /*call sequencing*/);
 
         assertEquals(SERVICE_HANDLE, service.getPhoneAccountHandle());
         assertEquals(1, service.getNumberOfTrackedCalls());
@@ -108,7 +108,8 @@
     public void testTransactionalServiceWrapperCallCount() throws Exception {
         TransactionalServiceWrapper service =
                 new TransactionalServiceWrapper(mCallEventCallback,
-                        mCallsManager, SERVICE_HANDLE, mMockCall1, mRepository);
+                        mCallsManager, SERVICE_HANDLE, mMockCall1, mRepository, mTransactionManager,
+                        false /*call sequencing*/);
 
         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 7f7399c..bf68f8c 100644
--- a/tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java
+++ b/tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java
@@ -49,7 +49,7 @@
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallState;
 import com.android.server.telecom.TelecomSystem;
-import com.android.server.telecom.voip.VoipCallMonitor;
+import com.android.server.telecom.callsequencing.voip.VoipCallMonitor;
 
 import org.junit.Before;
 import org.junit.Test;
diff --git a/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java b/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java
index c5be130..c479aac 100644
--- a/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java
+++ b/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java
@@ -25,11 +25,11 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.server.telecom.TelecomSystem;
-import com.android.server.telecom.voip.ParallelTransaction;
-import com.android.server.telecom.voip.SerialTransaction;
-import com.android.server.telecom.voip.TransactionManager;
-import com.android.server.telecom.voip.VoipCallTransaction;
-import com.android.server.telecom.voip.VoipCallTransactionResult;
+import com.android.server.telecom.callsequencing.voip.ParallelTransaction;
+import com.android.server.telecom.callsequencing.voip.SerialTransaction;
+import com.android.server.telecom.callsequencing.TransactionManager;
+import com.android.server.telecom.callsequencing.CallTransaction;
+import com.android.server.telecom.callsequencing.CallTransactionResult;
 
 import org.junit.After;
 import org.junit.Before;
@@ -51,7 +51,7 @@
     private TransactionManager mTransactionManager;
     private static final TelecomSystem.SyncRoot mLock = new TelecomSystem.SyncRoot() { };
 
-    private class TestVoipCallTransaction extends VoipCallTransaction {
+    private class TestVoipCallTransaction extends CallTransaction {
         public static final int SUCCESS = 0;
         public static final int FAILED = 1;
         public static final int TIMEOUT = 2;
@@ -70,27 +70,27 @@
         }
 
         @Override
-        public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+        public CompletionStage<CallTransactionResult> processTransaction(Void v) {
             if (mType == EXCEPTION) {
                 mLog.append(mName).append(" exception;\n");
                 throw new IllegalStateException("TEST EXCEPTION");
             }
-            CompletableFuture<VoipCallTransactionResult> resultFuture = new CompletableFuture<>();
+            CompletableFuture<CallTransactionResult> resultFuture = new CompletableFuture<>();
             mHandler.postDelayed(() -> {
                 if (mType == SUCCESS) {
                     mLog.append(mName).append(" success;\n");
                     resultFuture.complete(
-                            new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_SUCCEED,
+                            new CallTransactionResult(CallTransactionResult.RESULT_SUCCEED,
                                     null));
                 } else if (mType == FAILED) {
                     mLog.append(mName).append(" failed;\n");
                     resultFuture.complete(
-                            new VoipCallTransactionResult(CallException.CODE_ERROR_UNKNOWN,
+                            new CallTransactionResult(CallException.CODE_ERROR_UNKNOWN,
                                     null));
                 } else {
                     mLog.append(mName).append(" timeout;\n");
                     resultFuture.complete(
-                            new VoipCallTransactionResult(CallException.CODE_ERROR_UNKNOWN,
+                            new CallTransactionResult(CallException.CODE_ERROR_UNKNOWN,
                                     "timeout"));
                 }
             }, mSleepTime);
@@ -122,7 +122,7 @@
     @Test
     public void testSerialTransactionSuccess()
             throws ExecutionException, InterruptedException, TimeoutException {
-        List<VoipCallTransaction> subTransactions = new ArrayList<>();
+        List<CallTransaction> subTransactions = new ArrayList<>();
         TestVoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
                 TestVoipCallTransaction.SUCCESS);
         TestVoipCallTransaction t2 = new TestVoipCallTransaction("t2", 1000L,
@@ -132,13 +132,13 @@
         subTransactions.add(t1);
         subTransactions.add(t2);
         subTransactions.add(t3);
-        CompletableFuture<VoipCallTransactionResult> resultFuture = new CompletableFuture<>();
-        OutcomeReceiver<VoipCallTransactionResult, CallException> outcomeReceiver =
+        CompletableFuture<CallTransactionResult> resultFuture = new CompletableFuture<>();
+        OutcomeReceiver<CallTransactionResult, CallException> outcomeReceiver =
                 resultFuture::complete;
         String expectedLog = "t1 success;\nt2 success;\nt3 success;\n";
         mTransactionManager.addTransaction(new SerialTransaction(subTransactions, mLock),
                 outcomeReceiver);
-        assertEquals(VoipCallTransactionResult.RESULT_SUCCEED,
+        assertEquals(CallTransactionResult.RESULT_SUCCEED,
                 resultFuture.get(5000L, TimeUnit.MILLISECONDS).getResult());
         assertEquals(expectedLog, mLog.toString());
         verifyTransactionsFinished(t1, t2, t3);
@@ -148,7 +148,7 @@
     @Test
     public void testSerialTransactionFailed()
             throws ExecutionException, InterruptedException, TimeoutException {
-        List<VoipCallTransaction> subTransactions = new ArrayList<>();
+        List<CallTransaction> subTransactions = new ArrayList<>();
         TestVoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
                 TestVoipCallTransaction.SUCCESS);
         TestVoipCallTransaction t2 = new TestVoipCallTransaction("t2", 1000L,
@@ -159,10 +159,10 @@
         subTransactions.add(t2);
         subTransactions.add(t3);
         CompletableFuture<String> exceptionFuture = new CompletableFuture<>();
-        OutcomeReceiver<VoipCallTransactionResult, CallException> outcomeReceiver =
-                new OutcomeReceiver<VoipCallTransactionResult, CallException>() {
+        OutcomeReceiver<CallTransactionResult, CallException> outcomeReceiver =
+                new OutcomeReceiver<CallTransactionResult, CallException>() {
                     @Override
-                    public void onResult(VoipCallTransactionResult result) {
+                    public void onResult(CallTransactionResult result) {
 
                     }
 
@@ -183,7 +183,7 @@
     @Test
     public void testParallelTransactionSuccess()
             throws ExecutionException, InterruptedException, TimeoutException {
-        List<VoipCallTransaction> subTransactions = new ArrayList<>();
+        List<CallTransaction> subTransactions = new ArrayList<>();
         TestVoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
                 TestVoipCallTransaction.SUCCESS);
         TestVoipCallTransaction t2 = new TestVoipCallTransaction("t2", 500L,
@@ -193,12 +193,12 @@
         subTransactions.add(t1);
         subTransactions.add(t2);
         subTransactions.add(t3);
-        CompletableFuture<VoipCallTransactionResult> resultFuture = new CompletableFuture<>();
-        OutcomeReceiver<VoipCallTransactionResult, CallException> outcomeReceiver =
+        CompletableFuture<CallTransactionResult> resultFuture = new CompletableFuture<>();
+        OutcomeReceiver<CallTransactionResult, CallException> outcomeReceiver =
                 resultFuture::complete;
         mTransactionManager.addTransaction(new ParallelTransaction(subTransactions, mLock),
                 outcomeReceiver);
-        assertEquals(VoipCallTransactionResult.RESULT_SUCCEED,
+        assertEquals(CallTransactionResult.RESULT_SUCCEED,
                 resultFuture.get(5000L, TimeUnit.MILLISECONDS).getResult());
         String log = mLog.toString();
         assertTrue(log.contains("t1 success;\n"));
@@ -211,7 +211,7 @@
     @Test
     public void testParallelTransactionFailed()
             throws ExecutionException, InterruptedException, TimeoutException {
-        List<VoipCallTransaction> subTransactions = new ArrayList<>();
+        List<CallTransaction> subTransactions = new ArrayList<>();
         TestVoipCallTransaction t1 = new TestVoipCallTransaction("t1", 1000L,
                 TestVoipCallTransaction.SUCCESS);
         TestVoipCallTransaction t2 = new TestVoipCallTransaction("t2", 500L,
@@ -222,10 +222,10 @@
         subTransactions.add(t2);
         subTransactions.add(t3);
         CompletableFuture<String> exceptionFuture = new CompletableFuture<>();
-        OutcomeReceiver<VoipCallTransactionResult, CallException> outcomeReceiver =
+        OutcomeReceiver<CallTransactionResult, CallException> outcomeReceiver =
                 new OutcomeReceiver<>() {
             @Override
-            public void onResult(VoipCallTransactionResult result) {
+            public void onResult(CallTransactionResult result) {
 
             }
 
@@ -248,10 +248,10 @@
         TestVoipCallTransaction t = new TestVoipCallTransaction("t", 10000L,
                 TestVoipCallTransaction.SUCCESS);
         CompletableFuture<String> exceptionFuture = new CompletableFuture<>();
-        OutcomeReceiver<VoipCallTransactionResult, CallException> outcomeReceiver =
+        OutcomeReceiver<CallTransactionResult, CallException> outcomeReceiver =
                 new OutcomeReceiver<>() {
                     @Override
-                    public void onResult(VoipCallTransactionResult result) {
+                    public void onResult(CallTransactionResult result) {
 
                     }
 
@@ -275,10 +275,10 @@
         TestVoipCallTransaction t2 = new TestVoipCallTransaction("t2", 1000L,
                 TestVoipCallTransaction.SUCCESS);
         CompletableFuture<String> exceptionFuture = new CompletableFuture<>();
-        OutcomeReceiver<VoipCallTransactionResult, CallException> outcomeExceptionReceiver =
+        OutcomeReceiver<CallTransactionResult, CallException> outcomeExceptionReceiver =
                 new OutcomeReceiver<>() {
                     @Override
-                    public void onResult(VoipCallTransactionResult result) {
+                    public void onResult(CallTransactionResult result) {
                     }
 
                     @Override
@@ -291,12 +291,12 @@
         exceptionFuture.get(7000L, TimeUnit.MILLISECONDS);
         assertTrue(mLog.toString().contains("t1 exception;\n"));
         // Verify an exception in a processing a previous transaction does not stall the next one.
-        CompletableFuture<VoipCallTransactionResult> resultFuture = new CompletableFuture<>();
-        OutcomeReceiver<VoipCallTransactionResult, CallException> outcomeReceiver =
+        CompletableFuture<CallTransactionResult> resultFuture = new CompletableFuture<>();
+        OutcomeReceiver<CallTransactionResult, CallException> outcomeReceiver =
                 resultFuture::complete;
         mTransactionManager.addTransaction(t2, outcomeReceiver);
         String expectedLog = "t1 exception;\nt2 success;\n";
-        assertEquals(VoipCallTransactionResult.RESULT_SUCCEED,
+        assertEquals(CallTransactionResult.RESULT_SUCCEED,
                 resultFuture.get(5000L, TimeUnit.MILLISECONDS).getResult());
         assertEquals(expectedLog, mLog.toString());
         verifyTransactionsFinished(t1, t2);
@@ -317,10 +317,10 @@
                 TestVoipCallTransaction.EXCEPTION);
         // verify the TransactionManager informs the client of the failed transaction
         CompletableFuture<String> exceptionFuture = new CompletableFuture<>();
-        OutcomeReceiver<VoipCallTransactionResult, CallException> outcomeExceptionReceiver =
+        OutcomeReceiver<CallTransactionResult, CallException> outcomeExceptionReceiver =
                 new OutcomeReceiver<>() {
                     @Override
-                    public void onResult(VoipCallTransactionResult result) {
+                    public void onResult(CallTransactionResult result) {
                     }
 
                     @Override
@@ -346,10 +346,10 @@
                 TestVoipCallTransaction.SUCCESS);
         TestVoipCallTransaction t3 = new TestVoipCallTransaction("t3", 1000L,
                 TestVoipCallTransaction.SUCCESS);
-        OutcomeReceiver<VoipCallTransactionResult, CallException> outcomeExceptionReceiver =
+        OutcomeReceiver<CallTransactionResult, CallException> outcomeExceptionReceiver =
                 new OutcomeReceiver<>() {
                     @Override
-                    public void onResult(VoipCallTransactionResult result) {
+                    public void onResult(CallTransactionResult result) {
                         throw new IllegalStateException("RESULT EXCEPTION");
                     }
 
@@ -358,10 +358,10 @@
                     }
                 };
         mTransactionManager.addTransaction(t1, outcomeExceptionReceiver);
-        OutcomeReceiver<VoipCallTransactionResult, CallException> outcomeException2Receiver =
+        OutcomeReceiver<CallTransactionResult, CallException> outcomeException2Receiver =
                 new OutcomeReceiver<>() {
                     @Override
-                    public void onResult(VoipCallTransactionResult result) {
+                    public void onResult(CallTransactionResult result) {
                     }
 
                     @Override
@@ -371,12 +371,12 @@
                 };
         mTransactionManager.addTransaction(t2, outcomeException2Receiver);
         // Verify an exception in a previous transaction result does not stall the next one.
-        CompletableFuture<VoipCallTransactionResult> resultFuture = new CompletableFuture<>();
-        OutcomeReceiver<VoipCallTransactionResult, CallException> outcomeReceiver =
+        CompletableFuture<CallTransactionResult> resultFuture = new CompletableFuture<>();
+        OutcomeReceiver<CallTransactionResult, CallException> outcomeReceiver =
                 resultFuture::complete;
         mTransactionManager.addTransaction(t3, outcomeReceiver);
         String expectedLog = "t1 success;\nt2 success;\nt3 success;\n";
-        assertEquals(VoipCallTransactionResult.RESULT_SUCCEED,
+        assertEquals(CallTransactionResult.RESULT_SUCCEED,
                 resultFuture.get(5000L, TimeUnit.MILLISECONDS).getResult());
         assertEquals(expectedLog, mLog.toString());
         verifyTransactionsFinished(t1, t2, t3);