Merge "VoipCallMonitor updates" into main
diff --git a/flags/telecom_anomaly_report_flags.aconfig b/flags/telecom_anomaly_report_flags.aconfig
index 5d42b86..bc248c8 100644
--- a/flags/telecom_anomaly_report_flags.aconfig
+++ b/flags/telecom_anomaly_report_flags.aconfig
@@ -27,3 +27,11 @@
     purpose: PURPOSE_BUGFIX
   }
 }
+
+# OWNER=tjstuart TARGET=25Q2
+flag {
+  name: "enable_call_exception_anom_reports"
+  namespace: "telecom"
+  description: "When a new CallException is created, generate an anomaly report for metrics"
+  bug: "308932906"
+}
diff --git a/src/com/android/server/telecom/CallAudioWatchdog.java b/src/com/android/server/telecom/CallAudioWatchdog.java
index 3d4214d..4d06b62 100644
--- a/src/com/android/server/telecom/CallAudioWatchdog.java
+++ b/src/com/android/server/telecom/CallAudioWatchdog.java
@@ -395,7 +395,10 @@
 
     @Override
     public void onCallRemoved(Call call) {
-        // Nothing to do for call removal; sessions get cleaned up when their audio goes away.
+        // Only track for voip calls.
+        if (call.isSelfManaged() || call.isTransactionalCall()) {
+            maybeRemoveCall(call);
+        }
     }
 
     @VisibleForTesting
@@ -428,7 +431,7 @@
         }
         sessions.forEach(pw::println);
         pw.decreaseIndent();
-        pw.println("Non-Telecom Sessions:");
+        pw.println("Audio sessions Sessions:");
         pw.increaseIndent();
         mLocalLog.dump(pw);
         pw.decreaseIndent();
@@ -544,6 +547,27 @@
     }
 
     /**
+     * Given a telecom call, cleanup the session if there are no audio resources remaining for that
+     * session.
+     * @param call The call.
+     */
+    private void maybeRemoveCall(Call call) {
+        int uid = mPhoneAccountRegistrarProxy.getUidForPhoneAccountHandle(
+                call.getTargetPhoneAccount());
+        CommunicationSession session;
+        synchronized (mCommunicationSessionsLock) {
+            session = getSession(uid);
+            if (session == null) {
+                return;
+            }
+            if (!session.hasMediaResources()) {
+                mLocalLog.log(session.toString());
+                mCommunicationSessions.remove(uid);
+            }
+        }
+    }
+
+    /**
      * Returns an existing session for a uid, or {@code null} if none exists.
      * @param uid the uid,
      * @return The session found, or {@code null}.
@@ -621,14 +645,9 @@
 
                 // If audio resources are no longer held for a uid, then we'll clean up its
                 // media session.
-                if (!session.hasMediaResources()) {
+                if (!session.hasMediaResources() && session.getTelecomCall() == null) {
                     Log.i(this, "cleanupAttributeForSessions: removing session %s", session);
-                    // Only log the audio session if it has no telecom call; we'll correlate to
-                    // a telecom call if one was present so the logs for a telecom call will be
-                    // in the calls dumpsys.
-                    if (session.getTelecomCall() == null) {
-                        mLocalLog.log(session.toString());
-                    }
+                    mLocalLog.log(session.toString());
                     iterator.remove();
                 }
             }
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index ea66524..8425a10 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -329,6 +329,10 @@
             UUID.fromString("0a86157c-50ca-11ee-be56-0242ac120002");
     public static final String TELEPHONY_HAS_DEFAULT_BUT_TELECOM_DOES_NOT_MSG =
             "Telephony has a default MO acct but Telecom prompted user for MO";
+    public static final UUID CANNOT_HOLD_CURRENT_ACTIVE_CALL_ERROR_UUID =
+            UUID.fromString("1b6a9b88-5049-4ffa-a52a-134d7c3a40e6");
+    public static final UUID FAILED_TO_SWITCH_FOCUS_ERROR_UUID =
+            UUID.fromString("a1b2c3d4-e5f6-7890-1234-567890abcdef");
 
     public static final int[] OUTGOING_CALL_STATES =
             {CallState.CONNECTING, CallState.SELECT_PHONE_ACCOUNT, CallState.DIALING,
@@ -786,8 +790,9 @@
                 ? mContext.getSystemService(BlockedNumbersManager.class)
                 : null;
         mCallSequencingAdapter = new CallsManagerCallSequencingAdapter(this,
-                new CallSequencingController(this, mContext,
-                        mFeatureFlags), mFeatureFlags);
+                new CallSequencingController(this, mContext, mClockProxy,
+                        mAnomalyReporter, mTimeoutsAdapter, mMetricsController,
+                        mFeatureFlags), mCallAudioManager, mFeatureFlags);
 
         if (mFeatureFlags.useImprovedListenerOrder()) {
             mListeners.add(mInCallController);
@@ -3221,9 +3226,9 @@
      * CS: Hold any existing calls, request focus, and then set the call state to answered state.
      * <p>
      * T: Call TransactionalServiceWrapper, which then generates transactions to hold calls
-     * {@link #transactionHoldPotentialActiveCallForNewCall} and then move the active call focus
-     * {@link #requestNewCallFocusAndVerify} and notify the remote VOIP app of the call state
-     * moving to active.
+     * {@link CallsManagerCallSequencingAdapter#transactionHoldPotentialActiveCallForNewCall} and
+     * then move the active call focus {@link #requestNewCallFocusAndVerify} and notify the remote
+     * VOIP app of the call state moving to active.
      * <p>
      * Note: This is only used when {@link FeatureFlags#enableCallSequencing()} is false.
      */
@@ -4074,63 +4079,10 @@
     }
 
     /**
-     * attempt to hold or swap the current active call in favor of a new call request. The
-     * OutcomeReceiver will return onResult if the current active call is held or disconnected.
-     * Otherwise, the OutcomeReceiver will fail.
+     * Attempt to hold or swap the current active call in favor of a new call request. The old code
+     * path where {@link FeatureFlags#transactionalHoldDisconnectsUnholdable} is enabled but
+     * {@link FeatureFlags#enableCallSequencing()} is disabled.
      */
-    public void transactionHoldPotentialActiveCallForNewCall(Call newCall,
-            boolean isCallControlRequest, OutcomeReceiver<Boolean, CallException> callback) {
-        String mTag = "transactionHoldPotentialActiveCallForNewCall: ";
-        Call activeCall = (Call) mConnectionSvrFocusMgr.getCurrentFocusCall();
-        Log.i(this, mTag + "newCall=[%s], activeCall=[%s]", newCall, activeCall);
-
-        if (activeCall == null || activeCall == newCall) {
-            Log.i(this, mTag + "no need to hold activeCall");
-            callback.onResult(true);
-            return;
-        }
-
-        if (mFeatureFlags.transactionalHoldDisconnectsUnholdable()) {
-            // prevent bad actors from disconnecting the activeCall. Instead, clients will need to
-            // notify the user that they need to disconnect the ongoing call before making the
-            // new call ACTIVE.
-            if (isCallControlRequest && !canHoldOrSwapActiveCall(activeCall, newCall)) {
-                Log.i(this, mTag + "CallControlRequest exit");
-                callback.onError(new CallException("activeCall is NOT holdable or swappable, please"
-                        + " request the user disconnect the call.",
-                        CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
-                return;
-            }
-
-            mCallSequencingAdapter.transactionHoldPotentialActiveCallForNewCall(newCall,
-                    activeCall, callback);
-        } else {
-            // before attempting CallsManager#holdActiveCallForNewCall(Call), check if it'll fail
-            // early
-            if (!canHold(activeCall) &&
-                    !(supportsHold(activeCall) && areFromSameSource(activeCall, newCall))) {
-                Log.i(this, "transactionHoldPotentialActiveCallForNewCall: "
-                        + "conditions show the call cannot be held.");
-                callback.onError(new CallException("call does not support hold",
-                        CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
-                return;
-            }
-
-            // attempt to hold the active call
-            if (!holdActiveCallForNewCall(newCall)) {
-                Log.i(this, "transactionHoldPotentialActiveCallForNewCall: "
-                        + "attempted to hold call but failed.");
-                callback.onError(new CallException("cannot hold active call failed",
-                        CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
-                return;
-            }
-
-            // officially mark the activeCall as held
-            markCallAsOnHold(activeCall);
-            callback.onResult(true);
-        }
-    }
-
     public void transactionHoldPotentialActiveCallForNewCallOld(Call newCall,
             Call activeCall, OutcomeReceiver<Boolean, CallException> callback) {
         if (holdActiveCallForNewCall(newCall)) {
@@ -4144,16 +4096,57 @@
             if (activeCall.isLocallyDisconnecting()) {
                 callback.onResult(true);
             } else {
-                Log.i(this, "transactionHoldPotentialActiveCallForNewCallOld: active call could "
-                        + "not be held or disconnected");
+                String msg = "active call could not be held or disconnected";
+                Log.i(this, "transactionHoldPotentialActiveCallForNewCallOld: " + msg);
                 callback.onError(
-                        new CallException("activeCall could not be held or disconnected",
+                        new CallException(msg,
                                 CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
+                if (mFeatureFlags.enableCallExceptionAnomReports()) {
+                    mAnomalyReporter.reportAnomaly(CANNOT_HOLD_CURRENT_ACTIVE_CALL_ERROR_UUID, msg);
+                }
             }
         }
     }
 
-    private boolean canHoldOrSwapActiveCall(Call activeCall, Call newCall) {
+    /**
+     * The transactional unflagged (original) code path to hold or swap the active call in favor of
+     * a new call request. Refer to
+     * {@link CallsManagerCallSequencingAdapter#transactionHoldPotentialActiveCallForNewCall}.
+     */
+    public void transactionHoldPotentialActiveCallForNewCallUnflagged(Call activeCall, Call newCall,
+            OutcomeReceiver<Boolean, CallException> callback) {
+        // before attempting CallsManager#holdActiveCallForNewCall(Call), check if it'll fail
+        // early
+        if (!canHold(activeCall) &&
+                !(supportsHold(activeCall) && areFromSameSource(activeCall, newCall))) {
+            String msg = "call does not support hold";
+            Log.i(this, "transactionHoldPotentialActiveCallForNewCall: " + msg);
+            callback.onError(new CallException(msg,
+                    CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
+            if (mFeatureFlags.enableCallExceptionAnomReports()) {
+                mAnomalyReporter.reportAnomaly(CANNOT_HOLD_CURRENT_ACTIVE_CALL_ERROR_UUID, msg);
+            }
+            return;
+        }
+
+        // attempt to hold the active call
+        if (!holdActiveCallForNewCall(newCall)) {
+            String msg = "cannot hold active call failed";
+            Log.i(this, "transactionHoldPotentialActiveCallForNewCall: " + msg);
+            callback.onError(new CallException(msg,
+                    CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
+            if (mFeatureFlags.enableCallExceptionAnomReports()) {
+                mAnomalyReporter.reportAnomaly(CANNOT_HOLD_CURRENT_ACTIVE_CALL_ERROR_UUID, msg);
+            }
+            return;
+        }
+
+        // officially mark the activeCall as held
+        markCallAsOnHold(activeCall);
+        callback.onResult(true);
+    }
+
+    public boolean canHoldOrSwapActiveCall(Call activeCall, Call newCall) {
         return canHold(activeCall) || sameSourceHoldCase(activeCall, newCall);
     }
 
@@ -4373,54 +4366,7 @@
         removeCall(call);
         boolean isLocallyDisconnecting = mLocallyDisconnectingCalls.contains(call);
         mLocallyDisconnectingCalls.remove(call);
-        maybeMoveHeldCallToForeground(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.
-     *
-     * Note: If {@link FeatureFlags#enableCallSequencing()} is enabled, we will verify that the
-     * transaction to unhold the call succeeded or failed.
-     */
-    public void maybeMoveHeldCallToForeground(Call removedCall, boolean isLocallyDisconnecting) {
-        CompletableFuture<Boolean> unholdForegroundCallFuture = null;
-        Call foregroundCall = mCallAudioManager.getPossiblyHeldForegroundCall();
-        if (isLocallyDisconnecting) {
-            boolean isDisconnectingChildCall = removedCall.isDisconnectingChildCall();
-            Log.v(this, "maybeMoveHeldCallToForeground: isDisconnectingChildCall = "
-                    + isDisconnectingChildCall + "call -> %s", removedCall);
-            // Auto-unhold the foreground call due to a locally disconnected call, except if the
-            // call which was disconnected is a member of a conference (don't want to auto
-            // un-hold the conference if we remove a member of the conference).
-            // Also, ensure that the call we're removing is from the same ConnectionService as
-            // the one we're removing.  We don't want to auto-unhold between ConnectionService
-            // implementations, especially if one is managed and the other is a VoIP CS.
-            if (!isDisconnectingChildCall && foregroundCall != null
-                    && foregroundCall.getState() == CallState.ON_HOLD
-                    && areFromSameSource(foregroundCall, removedCall)) {
-
-                unholdForegroundCallFuture = foregroundCall.unhold();
-            }
-        } else if (foregroundCall != null &&
-                !foregroundCall.can(Connection.CAPABILITY_SUPPORT_HOLD) &&
-                foregroundCall.getState() == CallState.ON_HOLD) {
-
-            // The new foreground call is on hold, however the carrier does not display the hold
-            // button in the UI.  Therefore, we need to auto unhold the held call since the user
-            // has no means of unholding it themselves.
-            Log.i(this, "maybeMoveHeldCallToForeground: Auto-unholding held foreground call (call "
-                    + "doesn't support hold)");
-            unholdForegroundCallFuture = foregroundCall.unhold();
-        }
-
-        if (mFeatureFlags.enableCallSequencing() && unholdForegroundCallFuture != null) {
-            mCallSequencingAdapter.logFutureResultTransaction(unholdForegroundCallFuture,
-                    "maybeMoveHeldCallToForeground", "CM.mMHCTF",
-                    "Successfully unheld the foreground call.",
-                    "Failed to unhold the foreground call.");
-        }
+        mCallSequencingAdapter.maybeMoveHeldCallToForeground(call, isLocallyDisconnecting);
     }
 
     /**
@@ -5574,12 +5520,41 @@
             return true;
         }
 
-        CompletableFuture<Boolean> disconnectFuture =
-                maybeDisconnectExistingCallForNewOutgoingCall(call, liveCall);
-        // If future is instantiated, it will always be completed when call sequencing
-        // isn't enabled.
-        if (!mFeatureFlags.enableCallSequencing() && disconnectFuture != null) {
-            return disconnectFuture.getNow(false);
+        // If the live call is stuck in a connecting state for longer than the transitory timeout,
+        // then we should disconnect it in favor of the new outgoing call and prompt the user to
+        // generate a bugreport.
+        // TODO: In the future we should let the CallAnomalyWatchDog do this disconnection of the
+        // live call stuck in the connecting state.  Unfortunately that code will get tripped up by
+        // calls that have a longer than expected new outgoing call broadcast response time.  This
+        // mitigation is intended to catch calls stuck in a CONNECTING state for a long time that
+        // block outgoing calls.  However, if the user dials two calls in quick succession it will
+        // result in both calls getting disconnected, which is not optimal.
+        if (liveCall.getState() == CallState.CONNECTING
+                && ((mClockProxy.elapsedRealtime() - liveCall.getCreationElapsedRealtimeMillis())
+                > mTimeoutsAdapter.getNonVoipCallTransitoryStateTimeoutMillis())) {
+            if (mFeatureFlags.telecomMetricsSupport()) {
+                mMetricsController.getErrorStats().log(ErrorStats.SUB_CALL_MANAGER,
+                        ErrorStats.ERROR_STUCK_CONNECTING);
+            }
+            mAnomalyReporter.reportAnomaly(LIVE_CALL_STUCK_CONNECTING_ERROR_UUID,
+                    LIVE_CALL_STUCK_CONNECTING_ERROR_MSG);
+            liveCall.disconnect("Force disconnect CONNECTING call.");
+            return true;
+        }
+
+        if (hasMaximumOutgoingCalls(call)) {
+            Call outgoingCall = getFirstCallWithState(OUTGOING_CALL_STATES);
+            if (outgoingCall.getState() == CallState.SELECT_PHONE_ACCOUNT) {
+                // If there is an orphaned call in the {@link CallState#SELECT_PHONE_ACCOUNT}
+                // state, just disconnect it since the user has explicitly started a new call.
+                call.getAnalytics().setCallIsAdditional(true);
+                outgoingCall.getAnalytics().setCallIsInterrupted(true);
+                outgoingCall.disconnect("Disconnecting call in SELECT_PHONE_ACCOUNT in favor"
+                        + " of new outgoing call.");
+                return true;
+            }
+            call.setStartFailCause(CallFailureCause.MAX_OUTGOING_CALLS);
+            return false;
         }
 
         // TODO: Remove once b/23035408 has been corrected.
@@ -5645,64 +5620,6 @@
     }
 
     /**
-     * Potentially disconnects the live call if it has been stuck in a connecting state for more
-     * than the designated timeout or the outgoing call if it's stuck in the
-     * {@link CallState#SELECT_PHONE_ACCOUNT} stage.
-     *
-     * @param call The new outgoing call that is being placed.
-     * @param liveCall The first live call that has been detected.
-     * @return The {@link CompletableFuture<Boolean>} representing if room for the outgoing call
-     * could be made, null if further processing is required.
-     */
-    public CompletableFuture<Boolean> maybeDisconnectExistingCallForNewOutgoingCall(Call call,
-            Call liveCall) {
-        // If the live call is stuck in a connecting state for longer than the transitory timeout,
-        // then we should disconnect it in favor of the new outgoing call and prompt the user to
-        // generate a bugreport.
-        // TODO: In the future we should let the CallAnomalyWatchDog do this disconnection of the
-        // live call stuck in the connecting state.  Unfortunately that code will get tripped up by
-        // calls that have a longer than expected new outgoing call broadcast response time.  This
-        // mitigation is intended to catch calls stuck in a CONNECTING state for a long time that
-        // block outgoing calls.  However, if the user dials two calls in quick succession it will
-        // result in both calls getting disconnected, which is not optimal.
-        if (liveCall.getState() == CallState.CONNECTING
-                && ((mClockProxy.elapsedRealtime() - liveCall.getCreationElapsedRealtimeMillis())
-                > mTimeoutsAdapter.getNonVoipCallTransitoryStateTimeoutMillis())) {
-            if (mFeatureFlags.telecomMetricsSupport()) {
-                mMetricsController.getErrorStats().log(ErrorStats.SUB_CALL_MANAGER,
-                        ErrorStats.ERROR_STUCK_CONNECTING);
-            }
-            mAnomalyReporter.reportAnomaly(LIVE_CALL_STUCK_CONNECTING_ERROR_UUID,
-                    LIVE_CALL_STUCK_CONNECTING_ERROR_MSG);
-            CompletableFuture<Boolean> disconnectFuture =
-                    liveCall.disconnect("Force disconnect CONNECTING call.");
-            return mFeatureFlags.enableCallSequencing()
-                    ? disconnectFuture
-                    : CompletableFuture.completedFuture(true);
-        }
-
-        if (hasMaximumOutgoingCalls(call)) {
-            Call outgoingCall = getFirstCallWithState(OUTGOING_CALL_STATES);
-            if (outgoingCall.getState() == CallState.SELECT_PHONE_ACCOUNT) {
-                // If there is an orphaned call in the {@link CallState#SELECT_PHONE_ACCOUNT}
-                // state, just disconnect it since the user has explicitly started a new call.
-                call.getAnalytics().setCallIsAdditional(true);
-                outgoingCall.getAnalytics().setCallIsInterrupted(true);
-                CompletableFuture<Boolean> disconnectFuture = outgoingCall.disconnect(
-                        "Disconnecting call in SELECT_PHONE_ACCOUNT in favor of new "
-                                + "outgoing call.");
-                return mFeatureFlags.enableCallSequencing()
-                        ? disconnectFuture
-                        : CompletableFuture.completedFuture(true);
-            }
-            call.setStartFailCause(CallFailureCause.MAX_OUTGOING_CALLS);
-            return CompletableFuture.completedFuture(false);
-        }
-
-        return null;
-    }
-
-    /**
      * Given a call, find the first non-null phone account handle of its children.
      *
      * @param parentCall The parent call.
@@ -6796,13 +6713,11 @@
                 Log.d(this, "perform unhold call for %s", mCall);
                 CompletableFuture<Boolean> unholdFuture =
                         mCall.unhold("held " + mPreviouslyHeldCallId);
-                if (mFeatureFlags.enableCallSequencing() && unholdFuture != null) {
-                    mCallSequencingAdapter.logFutureResultTransaction(unholdFuture,
-                            "performAction", "AUC.pA", "performAction: unhold call transaction "
-                                    + "succeeded. Call state is active.",
-                            "performAction: unhold call transaction failed. Call state did not "
-                                    + "move to active in designated time.");
-                }
+                mCallSequencingAdapter.maybeLogFutureResultTransaction(unholdFuture,
+                        "performAction", "AUC.pA", "performAction: unhold call transaction "
+                                + "succeeded. Call state is active.",
+                        "performAction: unhold call transaction failed. Call state did not "
+                                + "move to active in designated time.");
             }
         }
     }
@@ -6845,13 +6760,11 @@
                 if (isSpeakerphoneAutoEnabledForVideoCalls(mVideoState)) {
                     mCall.setStartWithSpeakerphoneOn(true);
                 }
-                if (mFeatureFlags.enableCallSequencing() && answerCallFuture != null) {
-                    mCallSequencingAdapter.logFutureResultTransaction(answerCallFuture,
-                            "performAction", "AAC.pA", "performAction: answer call transaction "
-                                    + "succeeded. Call state is active.",
-                            "performAction: answer call transaction failed. Call state did not "
-                                    + "move to active in designated time.");
-                }
+                mCallSequencingAdapter.maybeLogFutureResultTransaction(answerCallFuture,
+                        "performAction", "AAC.pA", "performAction: answer call transaction "
+                                + "succeeded. Call state is active.",
+                        "performAction: answer call transaction failed. Call state did not "
+                                + "move to active in designated time.");
             }
         }
     }
@@ -6937,8 +6850,12 @@
                 if (mTargetCallFocus.getState() != mPreviousCallState) {
                     mTargetCallFocus.setState(mPreviousCallState, "resetting call state");
                 }
-                mCallback.onError(new CallException("failed to switch focus to requested call",
+                String msg = "failed to switch focus to requested call";
+                mCallback.onError(new CallException(msg,
                         CallException.CODE_CALL_CANNOT_BE_SET_TO_ACTIVE));
+                if (mFeatureFlags.enableCallExceptionAnomReports()) {
+                    mAnomalyReporter.reportAnomaly(FAILED_TO_SWITCH_FOCUS_ERROR_UUID, msg);
+                }
                 return;
             }
             // at this point, we know the FocusManager is able to update successfully
@@ -7165,4 +7082,9 @@
     public void addCallBeingSetup(Call call) {
         mSelfManagedCallsBeingSetup.add(call);
     }
+
+    @VisibleForTesting
+    public CallsManagerCallSequencingAdapter getCallSequencingAdapter() {
+        return mCallSequencingAdapter;
+    }
 }
diff --git a/src/com/android/server/telecom/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java
index af094b7..b9841ba 100644
--- a/src/com/android/server/telecom/TelecomServiceImpl.java
+++ b/src/com/android/server/telecom/TelecomServiceImpl.java
@@ -27,8 +27,6 @@
 import static android.Manifest.permission.READ_SMS;
 import static android.Manifest.permission.REGISTER_SIM_SUBSCRIPTION;
 import static android.Manifest.permission.WRITE_SECURE_SETTINGS;
-import static android.telecom.CallAttributes.DIRECTION_INCOMING;
-import static android.telecom.CallAttributes.DIRECTION_OUTGOING;
 import static android.telecom.CallException.CODE_ERROR_UNKNOWN;
 import static android.telecom.TelecomManager.TELECOM_TRANSACTION_SUCCESS;
 
@@ -52,8 +50,6 @@
 import android.os.Binder;
 import android.os.Build;
 import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
 import android.os.OutcomeReceiver;
 import android.os.ParcelFileDescriptor;
 import android.os.Process;
@@ -84,15 +80,12 @@
 import com.android.internal.telecom.ICallEventCallback;
 import com.android.internal.telecom.ITelecomService;
 import com.android.internal.util.IndentingPrintWriter;
-import com.android.server.telecom.callsequencing.voip.OutgoingCallTransactionSequencing;
 import com.android.server.telecom.callsequencing.voip.VoipCallMonitor;
 import com.android.server.telecom.components.UserCallIntentProcessorFactory;
 import com.android.server.telecom.flags.FeatureFlags;
 import com.android.server.telecom.metrics.ApiStats;
 import com.android.server.telecom.metrics.TelecomMetricsController;
 import com.android.server.telecom.settings.BlockedNumbersActivity;
-import com.android.server.telecom.callsequencing.voip.IncomingCallTransaction;
-import com.android.server.telecom.callsequencing.voip.OutgoingCallTransaction;
 import com.android.server.telecom.callsequencing.TransactionManager;
 import com.android.server.telecom.callsequencing.CallTransaction;
 import com.android.server.telecom.callsequencing.CallTransactionResult;
@@ -147,6 +140,13 @@
             UUID.fromString("4edf6c8d-1e43-4c94-b0fc-a40c8d80cfe8");
     public static final String PLACE_CALL_SECURITY_EXCEPTION_ERROR_MSG =
             "Security exception thrown while placing an outgoing call.";
+    public static final UUID CALL_IS_NULL_OR_ID_MISMATCH_UUID =
+            UUID.fromString("b11f3251-474c-4f90-96d6-a256aebc3c19");
+    public static final String CALL_IS_NULL_OR_ID_MISMATCH_MSG =
+            "call is null or id mismatch";
+    public static final UUID ADD_CALL_ON_ERROR_UUID =
+            UUID.fromString("f8e7d6c5-b4a3-9210-8765-432109abcdef");
+
     private static final String TAG = "TelecomServiceImpl";
     private static final String TIME_LINE_ARG = "timeline";
     private static final int DEFAULT_VIDEO_STATE = -1;
@@ -239,6 +239,11 @@
                                     onAddCallControl(callId, callEventCallback, null,
                                             new CallException(ADD_CALL_ERR_MSG,
                                                     CODE_ERROR_UNKNOWN));
+                                    if (mFeatureFlags.enableCallExceptionAnomReports()) {
+                                        mAnomalyReporter.reportAnomaly(
+                                                CALL_IS_NULL_OR_ID_MISMATCH_UUID,
+                                                CALL_IS_NULL_OR_ID_MISMATCH_MSG);
+                                    }
                                     return;
                                 }
 
@@ -268,6 +273,11 @@
                             public void onError(@NonNull CallException exception) {
                                 Log.d(TAG, "addCall: onError: e=[%s]", exception.toString());
                                 onAddCallControl(callId, callEventCallback, null, exception);
+                                if (mFeatureFlags.enableCallExceptionAnomReports()) {
+                                    mAnomalyReporter.reportAnomaly(
+                                            ADD_CALL_ON_ERROR_UUID,
+                                            exception.getMessage());
+                                }
                             }
                         });
                     }
@@ -3024,7 +3034,10 @@
         });
 
         mTransactionManager = TransactionManager.getInstance();
-        mTransactionalServiceRepository = new TransactionalServiceRepository(mFeatureFlags);
+        mTransactionManager.setFeatureFlag(mFeatureFlags);
+        mTransactionManager.setAnomalyReporter(mAnomalyReporter);
+        mTransactionalServiceRepository = new TransactionalServiceRepository(mFeatureFlags,
+                mAnomalyReporter);
         mBlockedNumbersManager = mFeatureFlags.telecomMainlineBlockedNumbersManager()
                 ? mContext.getSystemService(BlockedNumbersManager.class)
                 : null;
diff --git a/src/com/android/server/telecom/TransactionalServiceRepository.java b/src/com/android/server/telecom/TransactionalServiceRepository.java
index 5ae459e..954307a 100644
--- a/src/com/android/server/telecom/TransactionalServiceRepository.java
+++ b/src/com/android/server/telecom/TransactionalServiceRepository.java
@@ -35,9 +35,13 @@
     private static final Map<PhoneAccountHandle, TransactionalServiceWrapper> mServiceLookupTable =
             new HashMap<>();
     private final FeatureFlags mFlags;
+    private final AnomalyReporterAdapter mAnomalyReporter;
 
-    public TransactionalServiceRepository(FeatureFlags flags) {
+    public TransactionalServiceRepository(
+            FeatureFlags flags,
+            AnomalyReporterAdapter anomalyReporter) {
         mFlags = flags;
+        mAnomalyReporter = anomalyReporter;
     }
 
     public TransactionalServiceWrapper addNewCallForTransactionalServiceWrapper
@@ -50,7 +54,8 @@
             Log.d(TAG, "creating a new TSW; handle=[%s]", phoneAccountHandle);
             service = new TransactionalServiceWrapper(callEventCallback,
                     callsManager, phoneAccountHandle, call, this,
-                    TransactionManager.getInstance(), mFlags.enableCallSequencing());
+                    TransactionManager.getInstance(), mFlags.enableCallSequencing(),
+                    mFlags, mAnomalyReporter);
         } else {
             Log.d(TAG, "add a new call to an existing TSW; handle=[%s]", phoneAccountHandle);
             service = getTransactionalServiceWrapper(phoneAccountHandle);
diff --git a/src/com/android/server/telecom/TransactionalServiceWrapper.java b/src/com/android/server/telecom/TransactionalServiceWrapper.java
index d63a0bd..cc0d547 100644
--- a/src/com/android/server/telecom/TransactionalServiceWrapper.java
+++ b/src/com/android/server/telecom/TransactionalServiceWrapper.java
@@ -47,9 +47,11 @@
 import com.android.server.telecom.callsequencing.TransactionManager;
 import com.android.server.telecom.callsequencing.CallTransaction;
 import com.android.server.telecom.callsequencing.CallTransactionResult;
+import com.android.server.telecom.flags.FeatureFlags;
 
 import java.util.Locale;
 import java.util.Set;
+import java.util.UUID;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentHashMap;
 
@@ -92,7 +94,12 @@
     private TransactionManager mTransactionManager;
     private CallStreamingController mStreamingController;
     private final TransactionalCallSequencingAdapter mCallSequencingAdapter;
-
+    private final FeatureFlags mFeatureFlags;
+    private final AnomalyReporterAdapter mAnomalyReporter;
+    public static final UUID CALL_IS_NO_LONGER_BEING_TRACKED_ERROR_UUID =
+            UUID.fromString("8187cd59-97a7-4e9f-a772-638dda4b69bb");
+    public static final String CALL_IS_NO_LONGER_BEING_TRACKED_ERROR_MSG =
+            "A call update was attempted for a call no longer being tracked";
 
     // Each TransactionalServiceWrapper should have their own Binder.DeathRecipient to clean up
     // any calls in the event the application crashes or is force stopped.
@@ -108,7 +115,8 @@
     public TransactionalServiceWrapper(ICallEventCallback callEventCallback,
             CallsManager callsManager, PhoneAccountHandle phoneAccountHandle, Call call,
             TransactionalServiceRepository repo, TransactionManager transactionManager,
-            boolean isCallSequencingEnabled) {
+            boolean isCallSequencingEnabled, FeatureFlags featureFlags,
+            AnomalyReporterAdapter anomalyReporterAdapter) {
         // passed args
         mICallEventCallback = callEventCallback;
         mCallsManager = callsManager;
@@ -123,6 +131,8 @@
         mCallSequencingAdapter = new TransactionalCallSequencingAdapter(mTransactionManager,
                 mCallsManager, isCallSequencingEnabled);
         setDeathRecipient(callEventCallback);
+        mFeatureFlags = featureFlags;
+        mAnomalyReporter = anomalyReporterAdapter;
     }
 
     public TransactionManager getTransactionManager() {
@@ -307,6 +317,11 @@
                                 + " via TelecomManager#addCall", action, callId),
                                 CODE_CALL_IS_NOT_BEING_TRACKED));
                 callback.send(CODE_CALL_IS_NOT_BEING_TRACKED, exceptionBundle);
+                if (mFeatureFlags.enableCallExceptionAnomReports()) {
+                    mAnomalyReporter.reportAnomaly(
+                            CALL_IS_NO_LONGER_BEING_TRACKED_ERROR_UUID,
+                            CALL_IS_NO_LONGER_BEING_TRACKED_ERROR_MSG);
+                }
             }
         }
 
diff --git a/src/com/android/server/telecom/callsequencing/CallSequencingController.java b/src/com/android/server/telecom/callsequencing/CallSequencingController.java
index 69740dd..034f02a 100644
--- a/src/com/android/server/telecom/callsequencing/CallSequencingController.java
+++ b/src/com/android/server/telecom/callsequencing/CallSequencingController.java
@@ -20,6 +20,8 @@
 
 import static com.android.server.telecom.CallsManager.LIVE_CALL_STUCK_CONNECTING_EMERGENCY_ERROR_MSG;
 import static com.android.server.telecom.CallsManager.LIVE_CALL_STUCK_CONNECTING_EMERGENCY_ERROR_UUID;
+import static com.android.server.telecom.CallsManager.LIVE_CALL_STUCK_CONNECTING_ERROR_MSG;
+import static com.android.server.telecom.CallsManager.LIVE_CALL_STUCK_CONNECTING_ERROR_UUID;
 import static com.android.server.telecom.CallsManager.OUTGOING_CALL_STATES;
 import static com.android.server.telecom.UserUtil.showErrorDialogForRestrictedOutgoingCall;
 
@@ -40,21 +42,26 @@
 import android.telecom.PhoneAccountHandle;
 import android.telephony.AnomalyReporter;
 import android.telephony.CarrierConfigManager;
-import android.util.Pair;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.telecom.AnomalyReporterAdapter;
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallState;
 import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.ClockProxy;
 import com.android.server.telecom.LogUtils;
 import com.android.server.telecom.LoggedHandlerExecutor;
 import com.android.server.telecom.R;
+import com.android.server.telecom.Timeouts;
 import com.android.server.telecom.callsequencing.voip.OutgoingCallTransaction;
 import com.android.server.telecom.callsequencing.voip.OutgoingCallTransactionSequencing;
 import com.android.server.telecom.flags.FeatureFlags;
+import com.android.server.telecom.metrics.ErrorStats;
+import com.android.server.telecom.metrics.TelecomMetricsController;
 import com.android.server.telecom.stats.CallFailureCause;
 
 import java.util.Objects;
+import java.util.UUID;
 import java.util.concurrent.CompletableFuture;
 
 /**
@@ -66,14 +73,28 @@
  */
 public class CallSequencingController {
     private final CallsManager mCallsManager;
+    private final ClockProxy mClockProxy;
+    private final AnomalyReporterAdapter mAnomalyReporter;
+    private final Timeouts.Adapter mTimeoutsAdapter;
+    private final TelecomMetricsController mMetricsController;
     private final Handler mHandler;
     private final Context mContext;
     private final FeatureFlags mFeatureFlags;
     private static String TAG = CallSequencingController.class.getSimpleName();
+    public static final UUID SEQUENCING_CANNOT_HOLD_ACTIVE_CALL_UUID =
+            UUID.fromString("ea094d77-6ea9-4e40-891e-14bff5d485d7");
+    public static final String SEQUENCING_CANNOT_HOLD_ACTIVE_CALL_MSG =
+            "Cannot hold active call";
 
     public CallSequencingController(CallsManager callsManager, Context context,
+            ClockProxy clockProxy, AnomalyReporterAdapter anomalyReporter,
+            Timeouts.Adapter timeoutsAdapter, TelecomMetricsController metricsController,
             FeatureFlags featureFlags) {
         mCallsManager = callsManager;
+        mClockProxy = clockProxy;
+        mAnomalyReporter = anomalyReporter;
+        mMetricsController = metricsController;
+        mTimeoutsAdapter = timeoutsAdapter;
         HandlerThread handlerThread = new HandlerThread(this.toString());
         handlerThread.start();
         mHandler = new Handler(handlerThread.getLooper());
@@ -119,16 +140,18 @@
             if (callFuture == null) {
                 Log.d(this, "createTransactionalOutgoingCall: Outgoing call not permitted at the "
                         + "current time.");
-                return CompletableFuture.completedFuture(null);
+                return CompletableFuture.completedFuture(new OutgoingCallTransactionSequencing(
+                        mCallsManager, null, true /* callNotPermitted */, mFeatureFlags));
             }
             return callFuture.thenComposeAsync((call) -> CompletableFuture.completedFuture(
                     new OutgoingCallTransactionSequencing(mCallsManager, callFuture,
-                            mFeatureFlags)),
+                            false /* callNotPermitted */, mFeatureFlags)),
                     new LoggedHandlerExecutor(mHandler, "CSC.aC", mCallsManager.getLock()));
         } else {
             Log.d(this, "createTransactionalOutgoingCall: outgoing call not permitted at the "
                     + "current time.");
-            return CompletableFuture.completedFuture(null);
+            return CompletableFuture.completedFuture(new OutgoingCallTransactionSequencing(
+                    mCallsManager, null, true /* callNotPermitted */, mFeatureFlags));
         }
     }
 
@@ -198,6 +221,12 @@
                         callback.onError(
                                 new CallException("activeCall could not be held or disconnected",
                                 CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
+                        if (mFeatureFlags.enableCallExceptionAnomReports()) {
+                            mAnomalyReporter.reportAnomaly(
+                                    SEQUENCING_CANNOT_HOLD_ACTIVE_CALL_UUID,
+                                    SEQUENCING_CANNOT_HOLD_ACTIVE_CALL_MSG
+                            );
+                        }
                     }
                     return CompletableFuture.completedFuture(result);
                 }, new LoggedHandlerExecutor(mHandler, "CM.mCAA", mCallsManager.getLock()));
@@ -208,11 +237,11 @@
      * call sequencing and the resulting future is an indication of whether that request
      * has succeeded.
      * @param call The call that's waiting to go active.
-     * @return The {@code Pair<CompletableFuture, Boolean>} indicating the result of whether the
-     *         active call was able to be held (if applicable) and also if sequencing would be
-     *         required between the last and subsequent transaction to be performed on the call.
+     * @return The {@link CompletableFuture} indicating the result of whether the
+     *         active call was able to be held (if applicable).
      */
-    private CompletableFuture<Boolean> holdActiveCallForNewCallWithSequencing(
+    @VisibleForTesting
+    public CompletableFuture<Boolean> holdActiveCallForNewCallWithSequencing(
             Call call) {
         Call activeCall = (Call) mCallsManager.getConnectionServiceFocusManager()
                 .getCurrentFocusCall();
@@ -274,15 +303,21 @@
                     holdFutureHandler = disconnectFutureHandler
                             .thenComposeAsync((result) -> {
                                 if (result) {
-                                    return activeCall.hold();
+                                    return activeCall.hold().thenCompose((holdSuccess) -> {
+                                        if (holdSuccess) {
+                                            // Increase hold count only if hold succeeds.
+                                            call.increaseHeldByThisCallCount();
+                                        }
+                                        return CompletableFuture.completedFuture(holdSuccess);
+                                    });
                                 }
                                 return CompletableFuture.completedFuture(false);
                             }, new LoggedHandlerExecutor(mHandler,
                                     "CSC.hACFNCWS", mCallsManager.getLock()));
                 } else {
                     holdFutureHandler = activeCall.hold();
+                    call.increaseHeldByThisCallCount();
                 }
-                call.increaseHeldByThisCallCount();
                 // Next transaction will be performed on the call passed in and the last transaction
                 // was performed on the active call so ensure that the caller has this information
                 // to determine if sequencing is required.
@@ -293,37 +328,33 @@
                 // This call does not support hold. If it is from a different connection
                 // service or connection manager, then disconnect it, otherwise allow the connection
                 // service or connection manager to figure out the right states.
-                if (isSequencingRequiredActiveAndCall) {
-                    Log.i(this, "holdActiveCallForNewCallWithSequencing: disconnecting %s "
-                            + "so that %s can be made active.", activeCall.getId(), call.getId());
-                    if (!activeCall.isEmergencyCall()) {
-                        // We don't want to allow VOIP apps to disconnect carrier calls. We are
-                        // purposely completing the future with false so that the call isn't
-                        // answered.
-                        if (call.isSelfManaged() && !activeCall.isSelfManaged()) {
-                            Log.w(this, "holdActiveCallForNewCallWithSequencing: ignore "
-                                    + "disconnecting carrier call for making VOIP call active");
-                            return CompletableFuture.completedFuture(false);
-                        } else {
-                            return activeCall.disconnect();
-                        }
-                    } else {
-                        // It's not possible to hold the active call, and it's an emergency call so
-                        // we will silently reject the incoming call instead of answering it.
-                        Log.w(this, "holdActiveCallForNewCallWithSequencing: rejecting incoming "
-                                + "call %s as the active call is an emergency call and "
-                                + "it cannot be held.", call.getId());
-                        call.reject(false /* rejectWithMessage */, "" /* message */,
-                                "active emergency call can't be held");
+                Log.i(this, "holdActiveCallForNewCallWithSequencing: disconnecting %s "
+                        + "so that %s can be made active.", activeCall.getId(), call.getId());
+                if (!activeCall.isEmergencyCall()) {
+                    // We don't want to allow VOIP apps to disconnect carrier calls. We are
+                    // purposely completing the future with false so that the call isn't
+                    // answered.
+                    if (isSequencingRequiredActiveAndCall && call.isSelfManaged()
+                            && !activeCall.isSelfManaged()) {
+                        Log.w(this, "holdActiveCallForNewCallWithSequencing: ignore "
+                                + "disconnecting carrier call for making VOIP call active");
                         return CompletableFuture.completedFuture(false);
+                    } else {
+                        CompletableFuture<Boolean> disconnectFuture = activeCall.disconnect(
+                                "Active call disconnected in favor of new call.");
+                        return isSequencingRequiredActiveAndCall
+                                ? disconnectFuture
+                                : CompletableFuture.completedFuture(true);
                     }
                 } else {
-                    // Same source case: if the active call cannot be held, then the user has
-                    // willingly chosen to accept the incoming call knowing that the active call
-                    // will be disconnected.
-                    activeCall.disconnect("Active call disconnected in favor "
-                            + "of accepting incoming call.");
-                    return CompletableFuture.completedFuture(true);
+                    // It's not possible to hold the active call, and it's an emergency call so
+                    // we will silently reject the incoming call instead of answering it.
+                    Log.w(this, "holdActiveCallForNewCallWithSequencing: rejecting incoming "
+                            + "call %s as the active call is an emergency call and "
+                            + "it cannot be held.", call.getId());
+                    call.reject(false /* rejectWithMessage */, "" /* message */,
+                            "active emergency call can't be held");
+                    return CompletableFuture.completedFuture(false);
                 }
             }
         }
@@ -362,20 +393,15 @@
                     // is managed, abort the transaction. Otherwise, disconnect the call. We also
                     // don't want to drop an emergency call.
                     if (!activeCall.isEmergencyCall()) {
-                        if (!activeCall.isSelfManaged() && call.isSelfManaged()) {
-                            Log.w(this, "unholdCall: %s and %s are using different phone accounts. "
-                                            + "Aborting swap to %s", activeCallId, call.getId(),
-                                    call.getId());
-                            return;
-                        } else {
-                            unholdCallFutureHandler = activeCall.disconnect("Swap to "
-                                    + call.getId());
-                        }
+                        Log.w(this, "unholdCall: Unable to hold the active call (%s),"
+                                        + " aborting swap to %s", activeCallId, call.getId(),
+                                call.getId());
+                        showErrorDialogForCannotHoldCall(call, false);
                     } else {
                         Log.w(this, "unholdCall: %s is an emergency call, aborting swap to %s",
                                 activeCallId, call.getId());
-                        return;
                     }
+                    return;
                 } else {
                     activeCall.hold("Swap to " + call.getId());
                 }
@@ -593,9 +619,9 @@
         // will not be that one and we do not want multiple PhoneAccounts active during an
         // emergency call if possible. Disconnect the active call in favor of the emergency call
         // instead of trying to hold.
-        if (liveCall.getTargetPhoneAccount() != null) {
+        if (liveCallPhoneAccount != null) {
             PhoneAccount pa = mCallsManager.getPhoneAccountRegistrar().getPhoneAccountUnchecked(
-                    liveCall.getTargetPhoneAccount());
+                    liveCallPhoneAccount);
             if((pa.getCapabilities() & PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS) == 0) {
                 liveCall.setOverrideDisconnectCauseCode(new DisconnectCause(
                         DisconnectCause.LOCAL, DisconnectCause.REASON_EMERGENCY_CALL_PLACED));
@@ -619,8 +645,6 @@
                 } else {
                     return liveCall.disconnect(disconnectReason);
                 }
-            } else {
-                return CompletableFuture.completedFuture(true);
             }
         }
 
@@ -685,26 +709,48 @@
             return CompletableFuture.completedFuture(true);
         }
 
-        CompletableFuture<Boolean> disconnectFuture = mCallsManager
-                .maybeDisconnectExistingCallForNewOutgoingCall(call, liveCall);
-        if (disconnectFuture != null) {
-            return disconnectFuture;
+        // If the live call is stuck in a connecting state for longer than the transitory timeout,
+        // then we should disconnect it in favor of the new outgoing call and prompt the user to
+        // generate a bugreport.
+        // TODO: In the future we should let the CallAnomalyWatchDog do this disconnection of the
+        // live call stuck in the connecting state.  Unfortunately that code will get tripped up by
+        // calls that have a longer than expected new outgoing call broadcast response time.  This
+        // mitigation is intended to catch calls stuck in a CONNECTING state for a long time that
+        // block outgoing calls.  However, if the user dials two calls in quick succession it will
+        // result in both calls getting disconnected, which is not optimal.
+        if (liveCall.getState() == CallState.CONNECTING
+                && ((mClockProxy.elapsedRealtime() - liveCall.getCreationElapsedRealtimeMillis())
+                > mTimeoutsAdapter.getNonVoipCallTransitoryStateTimeoutMillis())) {
+            if (mFeatureFlags.telecomMetricsSupport()) {
+                mMetricsController.getErrorStats().log(ErrorStats.SUB_CALL_MANAGER,
+                        ErrorStats.ERROR_STUCK_CONNECTING);
+            }
+            mAnomalyReporter.reportAnomaly(LIVE_CALL_STUCK_CONNECTING_ERROR_UUID,
+                    LIVE_CALL_STUCK_CONNECTING_ERROR_MSG);
+            return liveCall.disconnect("Force disconnect CONNECTING call.");
         }
 
-        // TODO: Remove once b/23035408 has been corrected.
-        // If the live call is a conference, it will not have a target phone account set.  This
-        // means the check to see if the live call has the same target phone account as the new
-        // call will not cause us to bail early.  As a result, we'll end up holding the
-        // ongoing conference call.  However, the ConnectionService is already doing that.  This
-        // has caused problems with some carriers.  As a workaround until b/23035408 is
-        // corrected, we will try and get the target phone account for one of the conference's
-        // children and use that instead.
-        PhoneAccountHandle liveCallPhoneAccount = liveCall.getTargetPhoneAccount();
-        if (liveCallPhoneAccount == null && liveCall.isConference() &&
-                !liveCall.getChildCalls().isEmpty()) {
-            liveCallPhoneAccount = mCallsManager.getFirstChildPhoneAccount(liveCall);
-            Log.i(this, "makeRoomForOutgoingCall: using child call PhoneAccount = " +
-                    liveCallPhoneAccount);
+        if (mCallsManager.hasMaximumOutgoingCalls(call)) {
+            Call outgoingCall = mCallsManager.getFirstCallWithState(OUTGOING_CALL_STATES);
+            if (outgoingCall.getState() == CallState.SELECT_PHONE_ACCOUNT) {
+                // If there is an orphaned call in the {@link CallState#SELECT_PHONE_ACCOUNT}
+                // state, just disconnect it since the user has explicitly started a new call.
+                call.getAnalytics().setCallIsAdditional(true);
+                outgoingCall.getAnalytics().setCallIsInterrupted(true);
+                return outgoingCall.disconnect(
+                        "Disconnecting call in SELECT_PHONE_ACCOUNT in favor of new "
+                                + "outgoing call.");
+            }
+            showErrorDialogForMaxOutgoingCall(call);
+            return CompletableFuture.completedFuture(false);
+        }
+
+        // Early check to see if we already have a held call + live call. It's possible if a device
+        // switches to DSDS with two ongoing calls for the phone account to be null in which case
+        // we will return true from this method and report a different failure cause instead.
+        if (mCallsManager.hasMaximumManagedHoldingCalls(call) && !mCallsManager.canHold(liveCall)) {
+            showErrorDialogForMaxOutgoingCall(call);
+            return CompletableFuture.completedFuture(false);
         }
 
         if (call.getTargetPhoneAccount() == null) {
@@ -726,19 +772,7 @@
         }
 
         // The live call cannot be held so we're out of luck here.  There's no room.
-        int stringId;
-        String reason;
-        if (mCallsManager.hasMaximumManagedHoldingCalls(call)) {
-            call.setStartFailCause(CallFailureCause.MAX_OUTGOING_CALLS);
-            stringId = R.string.callFailed_too_many_calls;
-            reason = " there are two calls already in progress. Disconnect one of the calls "
-                    + "or merge the calls.";
-        } else {
-            call.setStartFailCause(CallFailureCause.CANNOT_HOLD_CALL);
-            stringId = R.string.callFailed_unholdable_call;
-            reason = " unable to hold live call. Disconnect the unholdable call.";
-        }
-        showErrorDialogForRestrictedOutgoingCall(mContext, stringId, TAG, reason);
+        showErrorDialogForCannotHoldCall(call, true);
         return CompletableFuture.completedFuture(false);
     }
 
@@ -819,14 +853,8 @@
                 CarrierConfigManager.KEY_ALLOW_HOLD_CALL_DURING_EMERGENCY_BOOL, true);
     }
 
-    /* Miscellaneous helpers/getters */
-
-    /**
-     * Checks if the phone accounts for any two calls is the same. This is used to determine if
-     * call sequencing is required between the two calls. That is if the phone accounts are
-     * different, then sequencing is required.
-     */
-    private boolean arePhoneAccountsSame(Call call1, Call call2) {
+    @VisibleForTesting
+    public boolean arePhoneAccountsSame(Call call1, Call call2) {
         if (call1 == null || call2 == null) {
             return false;
         }
@@ -847,6 +875,42 @@
                 && callToUnhold.getState() == CallState.ON_HOLD;
     }
 
+    /**
+     * Generic helper to log the result of the {@link CompletableFuture} containing the transactions
+     * that are being processed in the context of call sequencing.
+     * @param future The {@link CompletableFuture} encompassing the transaction that's being
+     *               computed.
+     * @param methodName The method name to describe the type of transaction being processed.
+     * @param sessionName The session name to identify the log.
+     * @param successMsg The message to be logged if the transaction succeeds.
+     * @param failureMsg The message to be logged if the transaction fails.
+     */
+    public void logFutureResultTransaction(CompletableFuture<Boolean> future, String methodName,
+            String sessionName, String successMsg, String failureMsg) {
+        future.thenApplyAsync((result) -> {
+            String msg = methodName + ": " + (result ? successMsg : failureMsg);
+            Log.i(this, msg);
+            return CompletableFuture.completedFuture(result);
+        }, new LoggedHandlerExecutor(mHandler, sessionName, mCallsManager.getLock()));
+    }
+
+    private void showErrorDialogForMaxOutgoingCall(Call call) {
+        call.setStartFailCause(CallFailureCause.MAX_OUTGOING_CALLS);
+        int stringId = R.string.callFailed_too_many_calls;
+        String reason = " there are two calls already in progress. Disconnect one of the calls "
+                + "or merge the calls.";
+        showErrorDialogForRestrictedOutgoingCall(mContext, stringId, TAG, reason);
+    }
+
+    private void showErrorDialogForCannotHoldCall(Call call, boolean setCallFailure) {
+        if (setCallFailure) {
+            call.setStartFailCause(CallFailureCause.CANNOT_HOLD_CALL);
+        }
+        int stringId = R.string.callFailed_unholdable_call;
+        String reason = " unable to hold live call. Disconnect the unholdable call.";
+        showErrorDialogForRestrictedOutgoingCall(mContext, stringId, TAG, reason);
+    }
+
     public Handler getHandler() {
         return mHandler;
     }
diff --git a/src/com/android/server/telecom/callsequencing/CallsManagerCallSequencingAdapter.java b/src/com/android/server/telecom/callsequencing/CallsManagerCallSequencingAdapter.java
index df0837d..101f570 100644
--- a/src/com/android/server/telecom/callsequencing/CallsManagerCallSequencingAdapter.java
+++ b/src/com/android/server/telecom/callsequencing/CallsManagerCallSequencingAdapter.java
@@ -21,9 +21,12 @@
 import android.os.OutcomeReceiver;
 import android.telecom.CallAttributes;
 import android.telecom.CallException;
+import android.telecom.Connection;
 import android.telecom.Log;
 
 import com.android.server.telecom.Call;
+import com.android.server.telecom.CallAudioManager;
+import com.android.server.telecom.CallState;
 import com.android.server.telecom.CallsManager;
 import com.android.server.telecom.LoggedHandlerExecutor;
 import com.android.server.telecom.callsequencing.voip.OutgoingCallTransaction;
@@ -39,42 +42,23 @@
 
     private final CallsManager mCallsManager;
     private final CallSequencingController mSequencingController;
+    private final CallAudioManager mCallAudioManager;
     private final Handler mHandler;
     private final FeatureFlags mFeatureFlags;
     private final boolean mIsCallSequencingEnabled;
 
     public CallsManagerCallSequencingAdapter(CallsManager callsManager,
-            CallSequencingController sequencingController,
+            CallSequencingController sequencingController, CallAudioManager callAudioManager,
             FeatureFlags featureFlags) {
         mCallsManager = callsManager;
         mSequencingController = sequencingController;
+        mCallAudioManager = callAudioManager;
         mHandler = sequencingController.getHandler();
         mFeatureFlags = featureFlags;
         mIsCallSequencingEnabled = featureFlags.enableCallSequencing();
     }
 
     /**
-     * Helps create the transaction representing the outgoing transactional call. For outgoing
-     * calls, there can be more than one transaction that will need to complete when
-     * mIsCallSequencingEnabled is true. Otherwise, rely on the old behavior of creating an
-     * {@link OutgoingCallTransaction}.
-     * @param callAttributes The call attributes associated with the call.
-     * @param extras The extras that are associated with the call.
-     * @param callingPackage The calling package representing where the request was invoked from.
-     * @return The {@link CompletableFuture<CallTransaction>} that encompasses the request to
-     *         place/receive the transactional call.
-     */
-    public CompletableFuture<CallTransaction> createTransactionalOutgoingCall(String callId,
-            CallAttributes callAttributes, Bundle extras, String callingPackage) {
-        return mIsCallSequencingEnabled
-                ? mSequencingController.createTransactionalOutgoingCall(callId,
-                        callAttributes, extras, callingPackage)
-                : CompletableFuture.completedFuture(new OutgoingCallTransaction(callId,
-                        mCallsManager.getContext(), callAttributes, mCallsManager, extras,
-                        mFeatureFlags));
-    }
-
-    /**
      * Conditionally try to answer the call depending on whether call sequencing
      * (mIsCallSequencingEnabled) is enabled.
      * @param incomingCall The incoming call that should be answered.
@@ -109,10 +93,8 @@
     public void holdCall(Call call) {
         // Sequencing already taken care of for CSW/TSW in Call class.
         CompletableFuture<Boolean> holdFuture = call.hold();
-        if (mIsCallSequencingEnabled) {
-            logFutureResultTransaction(holdFuture, "holdCall", "CMCSA.hC",
-                    "hold call transaction succeeded.", "hold call transaction failed.");
-        }
+        maybeLogFutureResultTransaction(holdFuture, "holdCall", "CMCSA.hC",
+                "hold call transaction succeeded.", "hold call transaction failed.");
     }
 
     /**
@@ -167,22 +149,115 @@
     }
 
     /**
-     * Attempts to hold the active call for transactional call cases with call sequencing support
-     * if mIsCallSequencingEnabled is true.
+     * Helps create the transaction representing the outgoing transactional call. For outgoing
+     * calls, there can be more than one transaction that will need to complete when
+     * mIsCallSequencingEnabled is true. Otherwise, rely on the old behavior of creating an
+     * {@link OutgoingCallTransaction}.
+     * @param callAttributes The call attributes associated with the call.
+     * @param extras The extras that are associated with the call.
+     * @param callingPackage The calling package representing where the request was invoked from.
+     * @return The {@link CompletableFuture<CallTransaction>} that encompasses the request to
+     *         place/receive the transactional call.
+     */
+    public CompletableFuture<CallTransaction> createTransactionalOutgoingCall(String callId,
+            CallAttributes callAttributes, Bundle extras, String callingPackage) {
+        return mIsCallSequencingEnabled
+                ? mSequencingController.createTransactionalOutgoingCall(callId,
+                callAttributes, extras, callingPackage)
+                : CompletableFuture.completedFuture(new OutgoingCallTransaction(callId,
+                        mCallsManager.getContext(), callAttributes, mCallsManager, extras,
+                        mFeatureFlags));
+    }
+
+    /**
+     * attempt to hold or swap the current active call in favor of a new call request. The
+     * OutcomeReceiver will return onResult if the current active call is held or disconnected.
+     * Otherwise, the OutcomeReceiver will fail.
      * @param newCall The new (transactional) call that's waiting to go active.
-     * @param activeCall The currently active call.
-     * @param callback The callback to report the result of the aforementioned hold transaction.
-     * @return {@code CompletableFuture} indicating the result of holding the active call.
+     * @param isCallControlRequest Indication of whether this is a call control request.
+     * @param callback The callback to report the result of the aforementioned hold
+     *      transaction.
      */
     public void transactionHoldPotentialActiveCallForNewCall(Call newCall,
-            Call activeCall, OutcomeReceiver<Boolean, CallException> callback) {
-        if (mIsCallSequencingEnabled) {
-            mSequencingController.transactionHoldPotentialActiveCallForNewCallSequencing(
-                    newCall, callback);
-        } else {
-            mCallsManager.transactionHoldPotentialActiveCallForNewCallOld(newCall,
-                    activeCall, callback);
+            boolean isCallControlRequest, OutcomeReceiver<Boolean, CallException> callback) {
+        String mTag = "transactionHoldPotentialActiveCallForNewCall: ";
+        Call activeCall = (Call) mCallsManager.getConnectionServiceFocusManager()
+                .getCurrentFocusCall();
+        Log.i(this, mTag + "newCall=[%s], activeCall=[%s]", newCall, activeCall);
+
+        if (activeCall == null || activeCall == newCall) {
+            Log.i(this, mTag + "no need to hold activeCall");
+            callback.onResult(true);
+            return;
         }
+
+        if (mFeatureFlags.transactionalHoldDisconnectsUnholdable()) {
+            // prevent bad actors from disconnecting the activeCall. Instead, clients will need to
+            // notify the user that they need to disconnect the ongoing call before making the
+            // new call ACTIVE.
+            if (isCallControlRequest
+                    && !mCallsManager.canHoldOrSwapActiveCall(activeCall, newCall)) {
+                Log.i(this, mTag + "CallControlRequest exit");
+                callback.onError(new CallException("activeCall is NOT holdable or swappable, please"
+                        + " request the user disconnect the call.",
+                        CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
+                return;
+            }
+
+            if (mIsCallSequencingEnabled) {
+                mSequencingController.transactionHoldPotentialActiveCallForNewCallSequencing(
+                        newCall, callback);
+            } else {
+                // The code path without sequencing but where transactionalHoldDisconnectsUnholdable
+                // flag is enabled.
+                mCallsManager.transactionHoldPotentialActiveCallForNewCallOld(newCall,
+                        activeCall, callback);
+            }
+        } else {
+            // The unflagged path (aka original code with no flags).
+            mCallsManager.transactionHoldPotentialActiveCallForNewCallUnflagged(activeCall,
+                    newCall, callback);
+        }
+    }
+
+    /**
+     * Attempts to move the held call to the foreground in cases where we need to auto-unhold the
+     * call.
+     */
+    public void maybeMoveHeldCallToForeground(Call removedCall, boolean isLocallyDisconnecting) {
+        CompletableFuture<Boolean> unholdForegroundCallFuture = null;
+        Call foregroundCall = mCallAudioManager.getPossiblyHeldForegroundCall();
+        if (isLocallyDisconnecting) {
+            boolean isDisconnectingChildCall = removedCall.isDisconnectingChildCall();
+            Log.v(this, "maybeMoveHeldCallToForeground: isDisconnectingChildCall = "
+                    + isDisconnectingChildCall + "call -> %s", removedCall);
+            // Auto-unhold the foreground call due to a locally disconnected call, except if the
+            // call which was disconnected is a member of a conference (don't want to auto
+            // un-hold the conference if we remove a member of the conference).
+            // Also, ensure that the call we're removing is from the same ConnectionService as
+            // the one we're removing.  We don't want to auto-unhold between ConnectionService
+            // implementations, especially if one is managed and the other is a VoIP CS.
+            if (!isDisconnectingChildCall && foregroundCall != null
+                    && foregroundCall.getState() == CallState.ON_HOLD
+                    && CallsManager.areFromSameSource(foregroundCall, removedCall)) {
+
+                unholdForegroundCallFuture = foregroundCall.unhold();
+            }
+        } else if (foregroundCall != null &&
+                !foregroundCall.can(Connection.CAPABILITY_SUPPORT_HOLD) &&
+                foregroundCall.getState() == CallState.ON_HOLD) {
+
+            // The new foreground call is on hold, however the carrier does not display the hold
+            // button in the UI.  Therefore, we need to auto unhold the held call since the user
+            // has no means of unholding it themselves.
+            Log.i(this, "maybeMoveHeldCallToForeground: Auto-unholding held foreground call (call "
+                    + "doesn't support hold)");
+            unholdForegroundCallFuture = foregroundCall.unhold();
+        }
+        maybeLogFutureResultTransaction(unholdForegroundCallFuture,
+                "maybeMoveHeldCallToForeground", "CM.mMHCTF",
+                "Successfully unheld the foreground call.",
+                "Failed to unhold the foreground call.");
     }
 
     /**
@@ -195,14 +270,12 @@
      * @param successMsg The message to be logged if the transaction succeeds.
      * @param failureMsg The message to be logged if the transaction fails.
      */
-    public void logFutureResultTransaction(CompletableFuture<Boolean> future, String methodName,
-            String sessionName, String successMsg, String failureMsg) {
-        future.thenApplyAsync((result) -> {
-            StringBuilder msg = new StringBuilder(methodName).append(": ");
-            msg.append(result ? successMsg : failureMsg);
-            Log.i(this, String.valueOf(msg));
-            return CompletableFuture.completedFuture(result);
-        }, new LoggedHandlerExecutor(mHandler, sessionName, mCallsManager.getLock()));
+    public void maybeLogFutureResultTransaction(CompletableFuture<Boolean> future,
+            String methodName, String sessionName, String successMsg, String failureMsg) {
+        if (mFeatureFlags.enableCallSequencing() && future != null) {
+            mSequencingController.logFutureResultTransaction(future, methodName, sessionName,
+                    successMsg, failureMsg);
+        }
     }
 
     public Handler getHandler() {
diff --git a/src/com/android/server/telecom/callsequencing/TransactionManager.java b/src/com/android/server/telecom/callsequencing/TransactionManager.java
index 2a6431b..98d54da 100644
--- a/src/com/android/server/telecom/callsequencing/TransactionManager.java
+++ b/src/com/android/server/telecom/callsequencing/TransactionManager.java
@@ -25,6 +25,8 @@
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.telecom.AnomalyReporterAdapter;
+import com.android.server.telecom.flags.FeatureFlags;
 import com.android.server.telecom.flags.Flags;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
@@ -32,6 +34,7 @@
 import java.util.List;
 import java.util.Locale;
 import java.util.Queue;
+import java.util.UUID;
 import java.util.concurrent.CompletableFuture;
 
 public class TransactionManager {
@@ -43,6 +46,12 @@
     private final Deque<CallTransaction> mCompletedTransactions;
     private CallTransaction mCurrentTransaction;
     private boolean mProcessingCallSequencing;
+    private AnomalyReporterAdapter mAnomalyReporter;
+    private FeatureFlags mFeatureFlags;
+    public static final UUID TRANSACTION_MANAGER_TIMEOUT_UUID =
+            UUID.fromString("9ccce52e-6694-4357-9e5e-516a9531b062");
+    public static final String TRANSACTION_MANAGER_TIMEOUT_MSG =
+            "TransactionManager hit a timeout while processing a transaction";
 
     public interface TransactionCompleteListener {
         void onTransactionCompleted(CallTransactionResult result, String transactionName);
@@ -67,6 +76,14 @@
         return INSTANCE;
     }
 
+    public void setFeatureFlag(FeatureFlags flag){
+       mFeatureFlags = flag;
+    }
+
+    public void setAnomalyReporter(AnomalyReporterAdapter callAnomalyReporter){
+        mAnomalyReporter = callAnomalyReporter;
+    }
+
     @VisibleForTesting
     public static TransactionManager getTestInstance() {
         return new TransactionManager();
@@ -109,6 +126,12 @@
                     receiver.onError(new CallException(transactionName + " timeout",
                             CODE_OPERATION_TIMED_OUT));
                     transactionCompleteFuture.complete(false);
+                    if (mFeatureFlags != null && mAnomalyReporter != null &&
+                            mFeatureFlags.enableCallExceptionAnomReports()) {
+                        mAnomalyReporter.reportAnomaly(
+                                TRANSACTION_MANAGER_TIMEOUT_UUID,
+                                TRANSACTION_MANAGER_TIMEOUT_MSG);
+                    }
                 } catch (Exception e) {
                     Log.e(TAG, String.format("onTransactionTimeout: Notifying transaction "
                             + " %s resulted in an Exception.", transactionName), e);
diff --git a/src/com/android/server/telecom/callsequencing/TransactionalCallSequencingAdapter.java b/src/com/android/server/telecom/callsequencing/TransactionalCallSequencingAdapter.java
index 570c2cc..4adc8d0 100644
--- a/src/com/android/server/telecom/callsequencing/TransactionalCallSequencingAdapter.java
+++ b/src/com/android/server/telecom/callsequencing/TransactionalCallSequencingAdapter.java
@@ -254,7 +254,7 @@
             }
         };
 
-        mCallsManager.transactionHoldPotentialActiveCallForNewCall(call,
+        mCallsManager.getCallSequencingAdapter().transactionHoldPotentialActiveCallForNewCall(call,
                 isCallControlRequest, maybePerformHoldCallback);
         return createSetActiveFuture[0];
     }
diff --git a/src/com/android/server/telecom/callsequencing/voip/MaybeHoldCallForNewCallTransaction.java b/src/com/android/server/telecom/callsequencing/voip/MaybeHoldCallForNewCallTransaction.java
index 32062b5..cb839dc 100644
--- a/src/com/android/server/telecom/callsequencing/voip/MaybeHoldCallForNewCallTransaction.java
+++ b/src/com/android/server/telecom/callsequencing/voip/MaybeHoldCallForNewCallTransaction.java
@@ -52,8 +52,8 @@
         Log.d(TAG, "processTransaction");
         CompletableFuture<CallTransactionResult> future = new CompletableFuture<>();
 
-        mCallsManager.transactionHoldPotentialActiveCallForNewCall(mCall, mIsCallControlRequest,
-                new OutcomeReceiver<>() {
+        mCallsManager.getCallSequencingAdapter().transactionHoldPotentialActiveCallForNewCall(
+                mCall, mIsCallControlRequest, new OutcomeReceiver<>() {
             @Override
             public void onResult(Boolean result) {
                 Log.d(TAG, "processTransaction: onResult");
diff --git a/src/com/android/server/telecom/callsequencing/voip/OutgoingCallTransactionSequencing.java b/src/com/android/server/telecom/callsequencing/voip/OutgoingCallTransactionSequencing.java
index c38b55d..af6af34 100644
--- a/src/com/android/server/telecom/callsequencing/voip/OutgoingCallTransactionSequencing.java
+++ b/src/com/android/server/telecom/callsequencing/voip/OutgoingCallTransactionSequencing.java
@@ -20,6 +20,7 @@
 
 import android.util.Log;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallsManager;
 import com.android.server.telecom.LoggedHandlerExecutor;
@@ -35,20 +36,23 @@
     private static final String TAG = OutgoingCallTransactionSequencing.class.getSimpleName();
     private final CompletableFuture<Call> mCallFuture;
     private final CallsManager mCallsManager;
+    private final boolean mCallNotPermitted;
     private FeatureFlags mFeatureFlags;
 
     public OutgoingCallTransactionSequencing(CallsManager callsManager,
-            CompletableFuture<Call> callFuture, FeatureFlags featureFlags) {
+            CompletableFuture<Call> callFuture, boolean callNotPermitted,
+            FeatureFlags featureFlags) {
         super(callsManager.getLock());
         mCallsManager = callsManager;
         mCallFuture = callFuture;
+        mCallNotPermitted = callNotPermitted;
         mFeatureFlags = featureFlags;
     }
 
     @Override
     public CompletionStage<CallTransactionResult> processTransaction(Void v) {
         Log.d(TAG, "processTransaction");
-        if (mCallFuture == null) {
+        if (mCallNotPermitted) {
             return CompletableFuture.completedFuture(
                     new CallTransactionResult(
                             CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME,
@@ -60,4 +64,9 @@
                         mCallsManager, mFeatureFlags)
                 , new LoggedHandlerExecutor(mHandler, "OCT.pT", null));
     }
+
+    @VisibleForTesting
+    public boolean getCallNotPermitted() {
+        return mCallNotPermitted;
+    }
 }
diff --git a/tests/src/com/android/server/telecom/tests/CallSequencingTests.java b/tests/src/com/android/server/telecom/tests/CallSequencingTests.java
new file mode 100644
index 0000000..22427cb
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/CallSequencingTests.java
@@ -0,0 +1,669 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.telecom.tests;
+
+import static com.android.server.telecom.UserUtil.showErrorDialogForRestrictedOutgoingCall;
+
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.TestCase.fail;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.OutcomeReceiver;
+import android.os.PersistableBundle;
+import android.os.UserHandle;
+import android.telecom.CallAttributes;
+import android.telecom.CallException;
+import android.telecom.Connection;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.CarrierConfigManager;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.server.telecom.Analytics;
+import com.android.server.telecom.AnomalyReporterAdapter;
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallState;
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.ClockProxy;
+import com.android.server.telecom.ConnectionServiceFocusManager;
+import com.android.server.telecom.PhoneAccountRegistrar;
+import com.android.server.telecom.Timeouts;
+import com.android.server.telecom.callsequencing.CallSequencingController;
+import com.android.server.telecom.callsequencing.CallTransaction;
+import com.android.server.telecom.callsequencing.voip.OutgoingCallTransactionSequencing;
+import com.android.server.telecom.metrics.TelecomMetricsController;
+import com.android.server.telecom.stats.CallFailureCause;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(JUnit4.class)
+public class CallSequencingTests extends TelecomTestCase {
+    private static final long SEQUENCING_TIMEOUT_MS = 2000L;
+    private static final PhoneAccountHandle mHandle1 = new PhoneAccountHandle(
+            new ComponentName("foo", "bar"), "1");
+    private static final PhoneAccountHandle mHandle2 = new PhoneAccountHandle(
+            new ComponentName("bar", "foo"), "2");
+    private static final String TEST_NAME = "Alan Turing";
+    private static final Uri TEST_URI = Uri.fromParts("tel", "abc", "123");
+    private static final String ACTIVE_CALL_ID = "TC@1";
+    private static final String NEW_CALL_ID = "TC@2";
+
+    private CallSequencingController mController;
+    @Mock
+    private CallsManager mCallsManager;
+    @Mock Context mContext;
+    @Mock ClockProxy mClockProxy;
+    @Mock AnomalyReporterAdapter mAnomalyReporter;
+    @Mock Timeouts.Adapter mTimeoutsAdapter;
+    @Mock TelecomMetricsController mMetricsController;
+    @Mock
+    ConnectionServiceFocusManager mConnectionServiceFocusManager;
+    @Mock Call mActiveCall;
+    @Mock Call mHeldCall;
+    @Mock Call mNewCall;
+    @Mock Call mRingingCall;
+
+    @Override
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        when(mFeatureFlags.enableCallSequencing()).thenReturn(true);
+        mController = new CallSequencingController(mCallsManager, mContext, mClockProxy,
+                mAnomalyReporter, mTimeoutsAdapter, mMetricsController, mFeatureFlags);
+
+        when(mActiveCall.getState()).thenReturn(CallState.ACTIVE);
+        when(mRingingCall.getState()).thenReturn(CallState.RINGING);
+        when(mHeldCall.getState()).thenReturn(CallState.ON_HOLD);
+
+        when(mActiveCall.getId()).thenReturn(ACTIVE_CALL_ID);
+        when(mNewCall.getId()).thenReturn(NEW_CALL_ID);
+    }
+
+    @Override
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+
+    @Test
+    @SmallTest
+    public void testTransactionOutgoingCall_CallNotPermitted() {
+        String callingPkg = "testPkg";
+        CallAttributes outgoingCallAttributes = getOutgoingCallAttributes();
+
+        // Outgoing call is not permitted
+        when(mCallsManager.isOutgoingCallPermitted(mHandle1)).thenReturn(false);
+        CompletableFuture<CallTransaction> transactionFuture = mController
+                .createTransactionalOutgoingCall("callId", outgoingCallAttributes,
+                        new Bundle(), callingPkg);
+        OutgoingCallTransactionSequencing transaction = (OutgoingCallTransactionSequencing)
+                transactionFuture.getNow(null);
+        assertNotNull(transaction);
+        assertTrue(transaction.getCallNotPermitted());
+
+        // Call future is null
+        when(mCallsManager.isOutgoingCallPermitted(mHandle1)).thenReturn(true);
+        when(mCallsManager.startOutgoingCall(any(Uri.class), any(PhoneAccountHandle.class),
+                any(Bundle.class), any(UserHandle.class), any(Intent.class), anyString()))
+                .thenReturn(null);
+        transactionFuture = mController
+                .createTransactionalOutgoingCall("callId", outgoingCallAttributes,
+                        new Bundle(), callingPkg);
+        transaction = (OutgoingCallTransactionSequencing) transactionFuture
+                .getNow(null);
+        assertNotNull(transaction);
+        assertTrue(transaction.getCallNotPermitted());
+    }
+
+    @Test
+    @SmallTest
+    public void testTransactionOutgoingCall() {
+        String callingPkg = "testPkg";
+        CallAttributes outgoingCallAttributes = getOutgoingCallAttributes();
+
+        when(mCallsManager.isOutgoingCallPermitted(mHandle1)).thenReturn(true);
+        when(mCallsManager.startOutgoingCall(any(Uri.class), any(PhoneAccountHandle.class),
+                any(Bundle.class), any(UserHandle.class), any(Intent.class), anyString()))
+                .thenReturn(CompletableFuture.completedFuture(mNewCall));
+        CompletableFuture<CallTransaction> transactionFuture = mController
+                .createTransactionalOutgoingCall("callId", outgoingCallAttributes,
+                        new Bundle(), callingPkg);
+        try {
+            OutgoingCallTransactionSequencing transaction = (OutgoingCallTransactionSequencing)
+                    transactionFuture.get(SEQUENCING_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+            assertNotNull(transaction);
+            assertFalse(transaction.getCallNotPermitted());
+        } catch (Exception e) {
+            fail("Failed to retrieve future in allocated time (" + SEQUENCING_TIMEOUT_MS + ").");
+        }
+    }
+
+    @SmallTest
+    @Test
+    public void testAnswerCall() {
+        // This will allow holdActiveCallForNewCallWithSequencing to immediately return true
+        setActiveCallFocus(null);
+        mController.answerCall(mNewCall, 0);
+        verify(mCallsManager, timeout(SEQUENCING_TIMEOUT_MS))
+                .requestFocusActionAnswerCall(eq(mNewCall), eq(0));
+    }
+
+    @SmallTest
+    @Test
+    public void testAnswerCallFail() {
+        setupHoldActiveCallForNewCallFailMocks();
+        mController.answerCall(mNewCall, 0);
+        verify(mCallsManager, timeout(SEQUENCING_TIMEOUT_MS).times(0))
+                .requestFocusActionAnswerCall(eq(mNewCall), eq(0));
+    }
+
+    @SmallTest
+    @Test
+    public void testSetSelfManagedCallActive() {
+        // This will allow holdActiveCallForNewCallWithSequencing to immediately return true
+        setActiveCallFocus(null);
+        mController.handleSetSelfManagedCallActive(mNewCall);
+        verify(mCallsManager, timeout(SEQUENCING_TIMEOUT_MS))
+                .requestActionSetActiveCall(eq(mNewCall), anyString());
+    }
+
+    @SmallTest
+    @Test
+    public void testSetSelfManagedCallActiveFail() {
+        setupHoldActiveCallForNewCallFailMocks();
+        mController.handleSetSelfManagedCallActive(mNewCall);
+        verify(mCallsManager, timeout(SEQUENCING_TIMEOUT_MS).times(0))
+                .requestActionSetActiveCall(eq(mNewCall), anyString());
+    }
+
+    @SmallTest
+    @Test
+    public void testTransactionHoldActiveCallForNewCall() throws InterruptedException {
+        // This will allow holdActiveCallForNewCallWithSequencing to immediately return true
+        setActiveCallFocus(null);
+        CountDownLatch latch = new CountDownLatch(1);
+        OutcomeReceiver<Boolean, CallException> callback = new OutcomeReceiver<>() {
+            @Override
+            public void onResult(Boolean result) {
+                // Expected result
+                latch.countDown();
+            }
+            @Override
+            public void onError(CallException exception) {
+            }
+        };
+        verifyTransactionHoldActiveCallForNewCall(callback, latch);
+    }
+
+    @SmallTest
+    @Test
+    public void testTransactionHoldActiveCallForNewCallFail() {
+        setupHoldActiveCallForNewCallFailMocks();
+        CountDownLatch latch = new CountDownLatch(1);
+        OutcomeReceiver<Boolean, CallException> callback = new OutcomeReceiver<>() {
+            @Override
+            public void onResult(Boolean result) {
+            }
+
+            @Override
+            public void onError(CallException exception) {
+                // Expected result
+                latch.countDown();
+            }
+        };
+        verifyTransactionHoldActiveCallForNewCall(callback, latch);
+    }
+
+    @Test
+    @SmallTest
+    public void testHoldCallForNewCall_NoActiveCall() {
+        setActiveCallFocus(null);
+        CompletableFuture<Boolean> resultFuture = mController
+                .holdActiveCallForNewCallWithSequencing(mNewCall);
+        assertTrue(waitForFutureResult(resultFuture, false));
+    }
+
+    @Test
+    @SmallTest
+    public void testHoldCallForNewCall_CanHold() {
+        setPhoneAccounts(mNewCall, mActiveCall, false);
+        setActiveCallFocus(mActiveCall);
+        when(mCallsManager.canHold(mActiveCall)).thenReturn(true);
+        when(mActiveCall.hold(anyString())).thenReturn(CompletableFuture.completedFuture(true));
+
+        // Cross phone account case (sequencing enabled)
+        assertFalse(mController.arePhoneAccountsSame(mNewCall, mActiveCall));
+        CompletableFuture<Boolean> resultFuture = mController
+                .holdActiveCallForNewCallWithSequencing(mNewCall);
+        assertTrue(waitForFutureResult(resultFuture, false));
+
+        // Same phone account case
+        setPhoneAccounts(mNewCall, mActiveCall, true);
+        assertTrue(mController.arePhoneAccountsSame(mNewCall, mActiveCall));
+        resultFuture = mController.holdActiveCallForNewCallWithSequencing(mNewCall);
+        assertTrue(waitForFutureResult(resultFuture, false));
+    }
+
+    @Test
+    @SmallTest
+    public void testHoldCallForNewCall_SupportsHold() {
+        setPhoneAccounts(mNewCall, mActiveCall, false);
+        setActiveCallFocus(mActiveCall);
+        when(mCallsManager.canHold(mActiveCall)).thenReturn(false);
+        when(mCallsManager.supportsHold(mActiveCall)).thenReturn(true);
+        when(mCallsManager.getFirstCallWithState(anyInt())).thenReturn(mHeldCall);
+        when(mHeldCall.isSelfManaged()).thenReturn(true);
+        when(mNewCall.isSelfManaged()).thenReturn(false);
+        when(mHeldCall.disconnect()).thenReturn(CompletableFuture.completedFuture(true));
+        when(mActiveCall.hold()).thenReturn(CompletableFuture.completedFuture(true));
+
+        // Verify that we abort transaction when there's a new (VOIP) call and we're trying to
+        // disconnect the active (carrier) call.
+        assertFalse(mController.arePhoneAccountsSame(mNewCall, mActiveCall));
+        CompletableFuture<Boolean> resultFuture = mController
+                .holdActiveCallForNewCallWithSequencing(mNewCall);
+        verify(mHeldCall, timeout(SEQUENCING_TIMEOUT_MS)).disconnect();
+        verify(mActiveCall, timeout(SEQUENCING_TIMEOUT_MS)).hold();
+        verify(mNewCall).increaseHeldByThisCallCount();
+        assertTrue(waitForFutureResult(resultFuture, false));
+    }
+
+    @Test
+    @SmallTest
+    public void testHoldCallForNewCall_SupportsHold_NoHeldCall() {
+        setPhoneAccounts(mNewCall, mActiveCall, false);
+        setActiveCallFocus(mActiveCall);
+        when(mCallsManager.canHold(mActiveCall)).thenReturn(false);
+        when(mCallsManager.supportsHold(mActiveCall)).thenReturn(true);
+        when(mCallsManager.getFirstCallWithState(anyInt())).thenReturn(null);
+        when(mActiveCall.hold()).thenReturn(CompletableFuture.completedFuture(true));
+
+        // Cross phone account case (sequencing enabled)
+        assertFalse(mController.arePhoneAccountsSame(mNewCall, mActiveCall));
+        CompletableFuture<Boolean> resultFuture = mController
+                .holdActiveCallForNewCallWithSequencing(mNewCall);
+        verify(mActiveCall, timeout(SEQUENCING_TIMEOUT_MS)).hold();
+        verify(mNewCall).increaseHeldByThisCallCount();
+        assertTrue(waitForFutureResult(resultFuture, false));
+    }
+
+    @Test
+    @SmallTest
+    public void testHoldCallForNewCall_DoesNotSupportHold_Disconnect() {
+        setPhoneAccounts(mNewCall, mActiveCall, false);
+        setActiveCallFocus(mActiveCall);
+        when(mCallsManager.canHold(mActiveCall)).thenReturn(false);
+        when(mCallsManager.supportsHold(mActiveCall)).thenReturn(false);
+        when(mActiveCall.disconnect(anyString())).thenReturn(
+                CompletableFuture.completedFuture(true));
+        when(mActiveCall.isEmergencyCall()).thenReturn(false);
+
+        assertFalse(mController.arePhoneAccountsSame(mNewCall, mActiveCall));
+        CompletableFuture<Boolean> resultFuture = mController
+                .holdActiveCallForNewCallWithSequencing(mNewCall);
+        verify(mActiveCall, timeout(SEQUENCING_TIMEOUT_MS)).disconnect(anyString());
+        assertTrue(waitForFutureResult(resultFuture, false));
+    }
+
+    @Test
+    @SmallTest
+    public void testHoldCallForNewCallFail_SupportsHold_VoipPstn() {
+        setPhoneAccounts(mNewCall, mActiveCall, false);
+        setActiveCallFocus(mActiveCall);
+        when(mCallsManager.canHold(mActiveCall)).thenReturn(false);
+        when(mCallsManager.supportsHold(mActiveCall)).thenReturn(true);
+        when(mCallsManager.getFirstCallWithState(anyInt())).thenReturn(mHeldCall);
+        when(mHeldCall.isSelfManaged()).thenReturn(false);
+        when(mNewCall.isSelfManaged()).thenReturn(true);
+
+        // Verify that we abort transaction when there's a new (VOIP) call and we're trying to
+        // disconnect the active (carrier) call.
+        assertFalse(mController.arePhoneAccountsSame(mNewCall, mActiveCall));
+        CompletableFuture<Boolean> resultFuture = mController
+                .holdActiveCallForNewCallWithSequencing(mNewCall);
+        assertFalse(waitForFutureResult(resultFuture, true));
+    }
+
+    @Test
+    @SmallTest
+    public void testHoldCallForNewCallFail_DoesNotSupportHold_Reject() {
+        setPhoneAccounts(mNewCall, mActiveCall, false);
+        setActiveCallFocus(mActiveCall);
+        when(mCallsManager.canHold(mActiveCall)).thenReturn(false);
+        when(mCallsManager.supportsHold(mActiveCall)).thenReturn(false);
+        when(mNewCall.reject(anyBoolean(), anyString(), anyString()))
+                .thenReturn(CompletableFuture.completedFuture(true));
+        when(mActiveCall.isEmergencyCall()).thenReturn(true);
+
+        assertFalse(mController.arePhoneAccountsSame(mNewCall, mActiveCall));
+        CompletableFuture<Boolean> resultFuture = mController
+                .holdActiveCallForNewCallWithSequencing(mNewCall);
+        verify(mNewCall, timeout(SEQUENCING_TIMEOUT_MS)).reject(
+                anyBoolean(), anyString(), anyString());
+        assertFalse(waitForFutureResult(resultFuture, true));
+    }
+
+    @Test
+    @SmallTest
+    public void testHoldCallForNewCallFail_DoesNotSupportHold_Abort() {
+        setPhoneAccounts(mNewCall, mActiveCall, false);
+        setActiveCallFocus(mActiveCall);
+        when(mCallsManager.canHold(mActiveCall)).thenReturn(false);
+        when(mCallsManager.supportsHold(mActiveCall)).thenReturn(false);
+        when(mActiveCall.isEmergencyCall()).thenReturn(false);
+        when(mActiveCall.isSelfManaged()).thenReturn(false);
+        when(mNewCall.isSelfManaged()).thenReturn(true);
+
+        assertFalse(mController.arePhoneAccountsSame(mNewCall, mActiveCall));
+        CompletableFuture<Boolean> resultFuture = mController
+                .holdActiveCallForNewCallWithSequencing(mNewCall);
+        assertFalse(waitForFutureResult(resultFuture, true));
+    }
+
+    @Test
+    @SmallTest
+    public void testUnholdCallNoActiveCall() {
+        setActiveCallFocus(null);
+        mController.unholdCall(mHeldCall);
+        verify(mCallsManager).requestActionUnholdCall(eq(mHeldCall), eq(null));
+    }
+
+    @Test
+    @SmallTest
+    public void testUnholdCallSwapCase() {
+        when(mActiveCall.can(eq(Connection.CAPABILITY_SUPPORT_HOLD))).thenReturn(true);
+        when(mActiveCall.hold(anyString())).thenReturn(CompletableFuture.completedFuture(true));
+        when(mActiveCall.isLocallyDisconnecting()).thenReturn(false);
+        setPhoneAccounts(mHeldCall, mActiveCall, false);
+        setActiveCallFocus(mActiveCall);
+
+        mController.unholdCall(mHeldCall);
+        assertFalse(mController.arePhoneAccountsSame(mActiveCall, mHeldCall));
+        verify(mActiveCall).hold(anyString());
+        verify(mCallsManager, timeout(SEQUENCING_TIMEOUT_MS))
+                .requestActionUnholdCall(eq(mHeldCall), eq(ACTIVE_CALL_ID));
+    }
+
+    @Test
+    @SmallTest
+    public void testUnholdCallFail_DoesNotSupportHold() {
+        when(mActiveCall.can(eq(Connection.CAPABILITY_SUPPORT_HOLD))).thenReturn(false);
+        when(mActiveCall.isEmergencyCall()).thenReturn(true);
+        when(mActiveCall.isLocallyDisconnecting()).thenReturn(false);
+        setPhoneAccounts(mHeldCall, mActiveCall, false);
+        setActiveCallFocus(mActiveCall);
+
+        // Emergency call case
+        mController.unholdCall(mHeldCall);
+        assertFalse(mController.arePhoneAccountsSame(mActiveCall, mHeldCall));
+        verify(mCallsManager, timeout(SEQUENCING_TIMEOUT_MS).times(0))
+                .requestActionUnholdCall(eq(mHeldCall), anyString());
+    }
+
+    @Test
+    @SmallTest
+    public void testUnholdFail() {
+        // Fail the hold.
+        when(mActiveCall.can(eq(Connection.CAPABILITY_SUPPORT_HOLD))).thenReturn(true);
+        when(mActiveCall.hold(anyString())).thenReturn(CompletableFuture.completedFuture(false));
+        when(mActiveCall.isLocallyDisconnecting()).thenReturn(false);
+        // Use different phone accounts so that the sequencing code path is hit.
+        setPhoneAccounts(mHeldCall, mActiveCall, false);
+        setActiveCallFocus(mActiveCall);
+
+        mController.unholdCall(mHeldCall);
+        assertFalse(mController.arePhoneAccountsSame(mActiveCall, mHeldCall));
+        verify(mActiveCall).hold(anyString());
+        // Verify unhold is never reached.
+        verify(mCallsManager, never())
+                .requestActionUnholdCall(eq(mHeldCall), anyString());
+    }
+
+    @SmallTest
+    @Test
+    public void testMakeRoomForOutgoingEmergencyCall_SamePkg() {
+        // Ensure that the live call and emergency call are from the same pkg.
+        when(mActiveCall.getTargetPhoneAccount()).thenReturn(mHandle1);
+        when(mNewCall.getTargetPhoneAccount()).thenReturn(mHandle1);
+        when(mRingingCall.getTargetPhoneAccount()).thenReturn(mHandle2);
+        setupMakeRoomForOutgoingEmergencyCallMocks();
+
+        CompletableFuture<Boolean> future = mController.makeRoomForOutgoingCall(true, mNewCall);
+        verify(mRingingCall)
+                .reject(anyBoolean(), eq(null), anyString());
+        verify(mActiveCall, timeout(SEQUENCING_TIMEOUT_MS)).hold(anyString());
+        assertTrue(waitForFutureResult(future, false));
+    }
+
+    @SmallTest
+    @Test
+    public void testMakeRoomForOutgoingEmergencyCall_CanHold() {
+        // Ensure that the live call and emergency call are from different pkgs.
+        when(mActiveCall.getTargetPhoneAccount()).thenReturn(mHandle1);
+        when(mNewCall.getTargetPhoneAccount()).thenReturn(mHandle2);
+        when(mRingingCall.getTargetPhoneAccount()).thenReturn(mHandle2);
+        setupMakeRoomForOutgoingEmergencyCallMocks();
+
+        CompletableFuture<Boolean> future = mController.makeRoomForOutgoingCall(true, mNewCall);
+        verify(mRingingCall)
+                .reject(anyBoolean(), eq(null), anyString());
+        verify(mActiveCall, timeout(SEQUENCING_TIMEOUT_MS)).hold(anyString());
+        assertTrue(waitForFutureResult(future, false));
+    }
+
+    @Test
+    @SmallTest
+    public void testMakeRoomForOutgoingCall() {
+        setupMakeRoomForOutgoingCallMocks();
+        when(mActiveCall.hold(anyString())).thenReturn(CompletableFuture.completedFuture(true));
+        Analytics.CallInfo newCallAnalytics = mock(Analytics.CallInfo.class);
+        Analytics.CallInfo activeCallAnalytics = mock(Analytics.CallInfo.class);
+        when(mNewCall.getAnalytics()).thenReturn(newCallAnalytics);
+        when(mActiveCall.getAnalytics()).thenReturn(activeCallAnalytics);
+        when(mCallsManager.canHold(mActiveCall)).thenReturn(true);
+
+        CompletableFuture<Boolean> future = mController.makeRoomForOutgoingCall(false, mNewCall);
+        verify(mActiveCall, timeout(SEQUENCING_TIMEOUT_MS)).hold(anyString());
+        verify(newCallAnalytics).setCallIsAdditional(eq(true));
+        verify(activeCallAnalytics).setCallIsInterrupted(eq(true));
+        assertTrue(waitForFutureResult(future, false));
+    }
+
+    @Test
+    @SmallTest
+    public void testMakeRoomForOutgoingCallFail_MaxCalls() {
+        setupMakeRoomForOutgoingCallMocks();
+        when(mCallsManager.canHold(mActiveCall)).thenReturn(false);
+        when(mCallsManager.hasMaximumManagedHoldingCalls(mNewCall)).thenReturn(true);
+
+        CompletableFuture<Boolean> future = mController.makeRoomForOutgoingCall(false, mNewCall);
+        verify(mNewCall).setStartFailCause(eq(CallFailureCause.MAX_OUTGOING_CALLS));
+        assertFalse(waitForFutureResult(future, true));
+    }
+
+    @Test
+    @SmallTest
+    public void testMakeRoomForOutgoingCallFail_CannotHold() {
+        setupMakeRoomForOutgoingCallMocks();
+        when(mCallsManager.canHold(mActiveCall)).thenReturn(false);
+        when(mCallsManager.hasMaximumManagedHoldingCalls(mNewCall)).thenReturn(false);
+
+        CompletableFuture<Boolean> future = mController.makeRoomForOutgoingCall(false, mNewCall);
+        verify(mNewCall).setStartFailCause(eq(CallFailureCause.CANNOT_HOLD_CALL));
+        assertFalse(waitForFutureResult(future, true));
+    }
+
+    @Test
+    @SmallTest
+    public void testDisconnectCallSuccess() {
+        when(mActiveCall.disconnect()).thenReturn(CompletableFuture.completedFuture(true));
+        int previousState = CallState.ACTIVE;
+        mController.disconnectCall(mActiveCall, previousState);
+        verify(mCallsManager, timeout(SEQUENCING_TIMEOUT_MS))
+                .processDisconnectCallAndCleanup(eq(mActiveCall), eq(previousState));
+    }
+
+    @Test
+    @SmallTest
+    public void testDisconnectCallFail() {
+        when(mActiveCall.disconnect()).thenReturn(CompletableFuture.completedFuture(false));
+        int previousState = CallState.ACTIVE;
+        mController.disconnectCall(mActiveCall, previousState);
+        verify(mCallsManager, timeout(SEQUENCING_TIMEOUT_MS).times(0))
+                .processDisconnectCallAndCleanup(eq(mActiveCall), eq(previousState));
+    }
+
+    /* Helpers */
+    private void setPhoneAccounts(Call call1, Call call2, boolean useSamePhoneAccount) {
+        when(call1.getTargetPhoneAccount()).thenReturn(mHandle1);
+        when(call2.getTargetPhoneAccount()).thenReturn(useSamePhoneAccount ? mHandle1 : mHandle2);
+    }
+
+    private void setActiveCallFocus(Call call) {
+        when(mCallsManager.getConnectionServiceFocusManager())
+                .thenReturn(mConnectionServiceFocusManager);
+        when(mConnectionServiceFocusManager.getCurrentFocusCall()).thenReturn(call);
+    }
+
+    private void setupMakeRoomForOutgoingEmergencyCallMocks() {
+        when(mNewCall.isEmergencyCall()).thenReturn(true);
+        when(mCallsManager.hasRingingOrSimulatedRingingCall()).thenReturn(true);
+        when(mCallsManager.getRingingOrSimulatedRingingCall()).thenReturn(mRingingCall);
+        when(mCallsManager.hasMaximumLiveCalls(mNewCall)).thenReturn(true);
+        when(mCallsManager.getFirstCallWithLiveState()).thenReturn(mActiveCall);
+        when(mCallsManager.hasMaximumOutgoingCalls(mNewCall)).thenReturn(false);
+        when(mCallsManager.hasMaximumManagedHoldingCalls(mNewCall)).thenReturn(false);
+        when(mCallsManager.canHold(mActiveCall)).thenReturn(true);
+
+        // Setup analytics mocks
+        setupCallAnalytics(Arrays.asList(mNewCall, mActiveCall, mRingingCall));
+
+        // Setup ecall related checks
+        setupEmergencyCallPaCapabilities();
+        setupCarrierConfigAllowEmergencyCallHold();
+
+        // Setup CompletableFuture mocking for call actions
+        when(mRingingCall.reject(anyBoolean(), eq(null), anyString()))
+                .thenReturn(CompletableFuture.completedFuture(true));
+        when(mActiveCall.hold(anyString())).thenReturn(
+                CompletableFuture.completedFuture(true));
+    }
+
+    private void setupEmergencyCallPaCapabilities() {
+        PhoneAccount pa = mock(PhoneAccount.class);
+        PhoneAccountRegistrar paRegistrar = mock(PhoneAccountRegistrar.class);
+        when(mCallsManager.getPhoneAccountRegistrar()).thenReturn(paRegistrar);
+        when(paRegistrar.getPhoneAccountUnchecked(any(PhoneAccountHandle.class))).thenReturn(pa);
+        when(pa.getCapabilities()).thenReturn(PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS);
+    }
+
+    private void setupCarrierConfigAllowEmergencyCallHold() {
+        PersistableBundle bundle = mock(PersistableBundle.class);
+        when(mCallsManager.getCarrierConfigForPhoneAccount(any(PhoneAccountHandle.class)))
+                .thenReturn(bundle);
+        when(bundle.getBoolean(
+                CarrierConfigManager.KEY_ALLOW_HOLD_CALL_DURING_EMERGENCY_BOOL, true))
+                .thenReturn(true);
+    }
+
+    private void setupMakeRoomForOutgoingCallMocks() {
+        when(mCallsManager.hasMaximumLiveCalls(mNewCall)).thenReturn(true);
+        when(mCallsManager.getFirstCallWithLiveState()).thenReturn(mActiveCall);
+        setPhoneAccounts(mActiveCall, mNewCall, false);
+        when(mActiveCall.isConference()).thenReturn(false);
+        when(mCallsManager.hasMaximumOutgoingCalls(mNewCall)).thenReturn(false);
+    }
+
+    private void setupHoldActiveCallForNewCallFailMocks() {
+        // Setup holdActiveCallForNewCallWithSequencing to fail.
+        setPhoneAccounts(mNewCall, mActiveCall, false);
+        setActiveCallFocus(mActiveCall);
+        when(mCallsManager.canHold(mActiveCall)).thenReturn(true);
+        when(mActiveCall.hold(anyString())).thenReturn(CompletableFuture.completedFuture(false));
+    }
+
+    private void verifyTransactionHoldActiveCallForNewCall(
+            OutcomeReceiver<Boolean, CallException> callback, CountDownLatch latch) {
+        mController.transactionHoldPotentialActiveCallForNewCallSequencing(mNewCall, callback);
+        while (latch.getCount() > 0) {
+            try {
+                latch.await(SEQUENCING_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+            } catch (InterruptedException e) {
+                // do nothing
+            }
+        }
+        assertEquals(latch.getCount(), 0);
+    }
+
+    private CallAttributes getOutgoingCallAttributes() {
+        return new CallAttributes.Builder(mHandle1,
+                CallAttributes.DIRECTION_OUTGOING, TEST_NAME, TEST_URI)
+                .setCallType(CallAttributes.AUDIO_CALL)
+                .setCallCapabilities(CallAttributes.SUPPORTS_SET_INACTIVE)
+                .build();
+    }
+
+    private void setupCallAnalytics(List<Call> calls) {
+        for (Call call: calls) {
+            Analytics.CallInfo analyticsInfo = mock(Analytics.CallInfo.class);
+            when(call.getAnalytics()).thenReturn(analyticsInfo);
+        }
+    }
+
+    private boolean waitForFutureResult(CompletableFuture<Boolean> future, boolean defaultValue) {
+        boolean result = defaultValue;
+        try {
+            result = future.get(SEQUENCING_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        } catch (Exception e) {
+            // Pass through
+        }
+        return result;
+    }
+}
+
diff --git a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
index 34d8830..126e4cc 100644
--- a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
@@ -103,6 +103,7 @@
 import com.android.server.telecom.CallState;
 import com.android.server.telecom.CallerInfoLookupHelper;
 import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.callsequencing.CallsManagerCallSequencingAdapter;
 import com.android.server.telecom.ClockProxy;
 import com.android.server.telecom.ConnectionServiceFocusManager;
 import com.android.server.telecom.ConnectionServiceFocusManager.ConnectionServiceFocusManagerFactory;
@@ -3044,9 +3045,9 @@
 
     /**
      * Verify that
-     * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
-     * OutcomeReceiver)}s OutcomeReceiver returns onResult when there is no active call to place
-     * on hold.
+     * {@link CallsManagerCallSequencingAdapter#transactionHoldPotentialActiveCallForNewCall(Call,
+     * boolean, OutcomeReceiver)}s OutcomeReceiver returns onResult when there is no active call to
+     * place on hold.
      */
     @MediumTest
     @Test
@@ -3068,8 +3069,8 @@
 
     /**
      * Verify that
-     * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
-     * OutcomeReceiver)}s OutcomeReceiver returns onError when there is an active call that
+     * {@link CallsManagerCallSequencingAdapter#transactionHoldPotentialActiveCallForNewCall(Call,
+     * boolean, OutcomeReceiver)}s OutcomeReceiver returns onError when there is an active call that
      * cannot be held, and it's a CallControlRequest.
      */
     @MediumTest
@@ -3086,9 +3087,9 @@
 
     /**
      * Verify that
-     * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
-     * OutcomeReceiver)}s OutcomeReceiver returns onResult when there is a holdable call and
-     * it's a CallControlRequest.
+     * {@link CallsManagerCallSequencingAdapter#transactionHoldPotentialActiveCallForNewCall(Call,
+     * boolean, OutcomeReceiver)}s OutcomeReceiver returns onResult when there is a holdable call
+     * and it's a CallControlRequest.
      */
     @MediumTest
     @Test
@@ -3105,9 +3106,9 @@
 
     /**
      * Verify that
-     * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
-     * OutcomeReceiver)}s OutcomeReceiver returns onResult when there is an active call that
-     * supports hold, and it's a CallControlRequest.
+     * {@link CallsManagerCallSequencingAdapter#transactionHoldPotentialActiveCallForNewCall(Call,
+     * boolean, OutcomeReceiver)}s OutcomeReceiver returns onResult when there is an active call
+     * that supports hold, and it's a CallControlRequest.
      */
     @MediumTest
     @Test
@@ -3124,9 +3125,9 @@
 
     /**
      * Verify that
-     * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
-     * OutcomeReceiver)}s OutcomeReceiver returns onResult when there is an active call that
-     * supports hold + can hold, and it's a CallControlRequest.
+     * {@link CallsManagerCallSequencingAdapter#transactionHoldPotentialActiveCallForNewCall(Call,
+     * boolean, OutcomeReceiver)}s OutcomeReceiver returns onResult when there is an active call
+     * that supports hold + can hold, and it's a CallControlRequest.
      */
     @MediumTest
     @Test
@@ -3145,9 +3146,9 @@
 
     /**
      * Verify that
-     * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
-     * OutcomeReceiver)}s OutcomeReceiver returns onResult when there is an active call that
-     * supports hold + can hold, and it's a CallControlCallbackRequest.
+     * {@link CallsManagerCallSequencingAdapter#transactionHoldPotentialActiveCallForNewCall(Call,
+     * boolean, OutcomeReceiver)}s OutcomeReceiver returns onResult when there is an active call
+     * that supports hold + can hold, and it's a CallControlCallbackRequest.
      */
     @MediumTest
     @Test
@@ -3165,9 +3166,9 @@
 
     /**
      * Verify that
-     * {@link CallsManager#transactionHoldPotentialActiveCallForNewCall(Call, boolean,
-     * OutcomeReceiver)}s OutcomeReceiver returns onResult when there is an active unholdable call,
-     * and it's a CallControlCallbackRequest.
+     * {@link CallsManagerCallSequencingAdapter#transactionHoldPotentialActiveCallForNewCall(Call,
+     * boolean, OutcomeReceiver)}s OutcomeReceiver returns onResult when there is an active
+     * unholdable call, and it's a CallControlCallbackRequest.
      */
     @MediumTest
     @Test
@@ -3940,7 +3941,7 @@
         CountDownLatch latch = new CountDownLatch(1);
         when(mFeatureFlags.transactionalHoldDisconnectsUnholdable()).thenReturn(true);
         when(mConnectionSvrFocusMgr.getCurrentFocusCall()).thenReturn(activeCall);
-        mCallsManager.transactionHoldPotentialActiveCallForNewCall(
+        mCallsManager.getCallSequencingAdapter().transactionHoldPotentialActiveCallForNewCall(
                 newCall,
                 isCallControlRequest,
                 new LatchedOutcomeReceiver(latch, expectOnResult));
diff --git a/tests/src/com/android/server/telecom/tests/TransactionTests.java b/tests/src/com/android/server/telecom/tests/TransactionTests.java
index 0a23913..6c049f6 100644
--- a/tests/src/com/android/server/telecom/tests/TransactionTests.java
+++ b/tests/src/com/android/server/telecom/tests/TransactionTests.java
@@ -63,6 +63,7 @@
 import com.android.server.telecom.PhoneNumberUtilsAdapter;
 import com.android.server.telecom.TelecomSystem;
 import com.android.server.telecom.callsequencing.CallTransactionResult;
+import com.android.server.telecom.callsequencing.CallsManagerCallSequencingAdapter;
 import com.android.server.telecom.callsequencing.TransactionManager;
 import com.android.server.telecom.callsequencing.VerifyCallStateChangeTransaction;
 import com.android.server.telecom.callsequencing.voip.EndCallTransaction;
@@ -97,6 +98,7 @@
     @Mock private Call mMockCall1;
     @Mock private Context mMockContext;
     @Mock private CallsManager mCallsManager;
+    @Mock private CallsManagerCallSequencingAdapter mCallSequencingAdapter;
     @Mock private ToastFactory mToastFactory;
     @Mock private ClockProxy mClockProxy;
     @Mock private PhoneNumberUtilsAdapter mPhoneNumberUtilsAdapter;
@@ -113,6 +115,7 @@
         MockitoAnnotations.initMocks(this);
         Mockito.when(mMockCall1.getId()).thenReturn(CALL_ID_1);
         Mockito.when(mMockContext.getResources()).thenReturn(Mockito.mock(Resources.class));
+        when(mCallsManager.getCallSequencingAdapter()).thenReturn(mCallSequencingAdapter);
     }
 
     @Override
@@ -220,7 +223,7 @@
         transaction.processTransaction(null);
 
         // THEN
-        verify(mCallsManager, times(1))
+        verify(mCallsManager.getCallSequencingAdapter(), times(1))
                 .transactionHoldPotentialActiveCallForNewCall(eq(mMockCall1), eq(false),
                         isA(OutcomeReceiver.class));
     }
diff --git a/tests/src/com/android/server/telecom/tests/TransactionalServiceWrapperTest.java b/tests/src/com/android/server/telecom/tests/TransactionalServiceWrapperTest.java
index fea6135..16b6e44 100644
--- a/tests/src/com/android/server/telecom/tests/TransactionalServiceWrapperTest.java
+++ b/tests/src/com/android/server/telecom/tests/TransactionalServiceWrapperTest.java
@@ -34,6 +34,7 @@
 
 import com.android.internal.telecom.ICallControl;
 import com.android.internal.telecom.ICallEventCallback;
+import com.android.server.telecom.AnomalyReporterAdapter;
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallsManager;
 import com.android.server.telecom.TelecomSystem;
@@ -70,6 +71,7 @@
     @Mock private TransactionManager mTransactionManager;
     @Mock private ICallEventCallback mCallEventCallback;
     @Mock private TransactionalServiceRepository mRepository;
+    @Mock private AnomalyReporterAdapter mAnomalyReporterAdapter;
     @Mock private IBinder mIBinder;
     private final TelecomSystem.SyncRoot mLock = new TelecomSystem.SyncRoot() {};
 
@@ -84,7 +86,7 @@
         Mockito.when(mCallEventCallback.asBinder()).thenReturn(mIBinder);
         mTransactionalServiceWrapper = new TransactionalServiceWrapper(mCallEventCallback,
                 mCallsManager, SERVICE_HANDLE, mMockCall1, mRepository, mTransactionManager,
-                false /*call sequencing*/);
+                false /*call sequencing*/, mFeatureFlags, mAnomalyReporterAdapter);
     }
 
     @Override
@@ -98,7 +100,7 @@
         TransactionalServiceWrapper service =
                 new TransactionalServiceWrapper(mCallEventCallback,
                         mCallsManager, SERVICE_HANDLE, mMockCall1, mRepository, mTransactionManager,
-                        false /*call sequencing*/);
+                        false /*call sequencing*/, mFeatureFlags, mAnomalyReporterAdapter);
 
         assertEquals(SERVICE_HANDLE, service.getPhoneAccountHandle());
         assertEquals(1, service.getNumberOfTrackedCalls());
@@ -109,7 +111,7 @@
         TransactionalServiceWrapper service =
                 new TransactionalServiceWrapper(mCallEventCallback,
                         mCallsManager, SERVICE_HANDLE, mMockCall1, mRepository, mTransactionManager,
-                        false /*call sequencing*/);
+                        false /*call sequencing*/, mFeatureFlags, mAnomalyReporterAdapter);
 
         assertEquals(1, service.getNumberOfTrackedCalls());
         service.trackCall(mMockCall2);