Merge "Propagate associated user into call" into main
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 09ebfe2..1355343 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -47,11 +47,9 @@
           "exclude-annotation": "androidx.test.filters.FlakyTest"
         }
       ]
-    }
-  ],
-  "presubmit-large": [
+    },
     {
-      "name": "CtsTelecomTestCases",
+      "name": "CtsTelecomCujTestCases",
       "options": [
         {
           "exclude-annotation": "androidx.test.filters.FlakyTest"
@@ -59,9 +57,9 @@
       ]
     }
   ],
-  "postsubmit": [
+  "presubmit-large": [
     {
-      "name": "CtsTelecomCujTestCases",
+      "name": "CtsTelecomTestCases",
       "options": [
         {
           "exclude-annotation": "androidx.test.filters.FlakyTest"
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index 411449c..8cd5266 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -2651,7 +2651,7 @@
             return;
         }
         mCreateConnectionProcessor = new CreateConnectionProcessor(this, mRepository, this,
-                phoneAccountRegistrar, mContext, mFlags, new Timeouts.Adapter());
+                phoneAccountRegistrar, mCallsManager, mContext, mFlags, new Timeouts.Adapter());
         mCreateConnectionProcessor.process();
     }
 
diff --git a/src/com/android/server/telecom/CallAudioWatchdog.java b/src/com/android/server/telecom/CallAudioWatchdog.java
index dcfc80f..4ca237a 100644
--- a/src/com/android/server/telecom/CallAudioWatchdog.java
+++ b/src/com/android/server/telecom/CallAudioWatchdog.java
@@ -28,6 +28,7 @@
 import android.media.AudioTrack;
 import android.media.MediaRecorder;
 import android.os.Handler;
+import android.os.Process;
 import android.telecom.Log;
 import android.telecom.Logging.EventManager;
 import android.telecom.PhoneAccountHandle;
@@ -288,6 +289,11 @@
                         && config.getAudioAttributes().getUsage()
                         == AudioAttributes.USAGE_VOICE_COMMUNICATION) {
 
+                    // Skip if the client's pid is same as myself
+                    if (config.getClientPid() == Process.myPid()) {
+                        continue;
+                    }
+
                     // If an audio session is idle, we don't count it as playing.  It must be in a
                     // started state.
                     boolean isPlaying = config.getPlayerState() == PLAYER_STATE_STARTED;
diff --git a/src/com/android/server/telecom/CreateConnectionProcessor.java b/src/com/android/server/telecom/CreateConnectionProcessor.java
index a2c742d..c2b5da1 100644
--- a/src/com/android/server/telecom/CreateConnectionProcessor.java
+++ b/src/com/android/server/telecom/CreateConnectionProcessor.java
@@ -37,12 +37,13 @@
 
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
 import java.util.stream.Collectors;
 
 /**
@@ -127,6 +128,21 @@
         }
     };
 
+    /**
+     * Call states which should be prioritized when sorting phone accounts. The ordering is
+     * intentional and should NOT be modified. Other call states will not have any priority.
+     */
+    private static final int[] PRIORITY_CALL_STATES = new int []
+            {CallState.ACTIVE, CallState.ON_HOLD, CallState.DIALING, CallState.RINGING};
+    private static final int DEFAULT_CALL_STATE_PRIORITY = PRIORITY_CALL_STATES.length;
+    private static final Map<Integer, Integer> mCallStatePriorityMap = new HashMap<>();
+    static {
+        for (int i = 0; i < PRIORITY_CALL_STATES.length; i++) {
+            mCallStatePriorityMap.put(PRIORITY_CALL_STATES[i], i);
+        }
+    }
+
+
     private ITelephonyManagerAdapter mTelephonyAdapter = new ITelephonyManagerAdapterImpl();
 
     private final Call mCall;
@@ -136,6 +152,7 @@
     private CreateConnectionResponse mCallResponse;
     private DisconnectCause mLastErrorDisconnectCause;
     private final PhoneAccountRegistrar mPhoneAccountRegistrar;
+    private final CallsManager mCallsManager;
     private final Context mContext;
     private final FeatureFlags mFlags;
     private final Timeouts.Adapter mTimeoutsAdapter;
@@ -148,6 +165,7 @@
             ConnectionServiceRepository repository,
             CreateConnectionResponse response,
             PhoneAccountRegistrar phoneAccountRegistrar,
+            CallsManager callsManager,
             Context context,
             FeatureFlags featureFlags,
             Timeouts.Adapter timeoutsAdapter) {
@@ -156,6 +174,7 @@
         mRepository = repository;
         mCallResponse = response;
         mPhoneAccountRegistrar = phoneAccountRegistrar;
+        mCallsManager = callsManager;
         mContext = context;
         mConnectionAttempt = 0;
         mFlags = featureFlags;
@@ -693,6 +712,23 @@
                 return retval;
             }
 
+            // Sort accounts by ongoing call states
+            Set<Integer> callStatesAccount1 = mCallsManager.getCalls().stream()
+                    .filter(c -> Objects.equals(account1.getAccountHandle(),
+                            c.getTargetPhoneAccount()))
+                    .map(Call::getState).collect(Collectors.toSet());
+            Set<Integer> callStatesAccount2 = mCallsManager.getCalls().stream()
+                    .filter(c -> Objects.equals(account2.getAccountHandle(),
+                            c.getTargetPhoneAccount()))
+                    .map(Call::getState).collect(Collectors.toSet());
+            int account1Priority = computeCallStatePriority(callStatesAccount1);
+            int account2Priority = computeCallStatePriority(callStatesAccount2);
+            Log.d(this, "account1: %s, call state priority: %s", account1, account1Priority);
+            Log.d(this, "account2: %s, call state priority: %s", account2, account2Priority);
+            if (account1Priority != account2Priority) {
+                return account1Priority < account2Priority ? -1 : 1;
+            }
+
             // Prefer the user's choice if all PhoneAccounts are associated with valid logical
             // slots.
             if (userPreferredAccount != null) {
@@ -731,6 +767,25 @@
         });
     }
 
+    /**
+     * Computes the call state priority based on the passed in call states associated with the
+     * calls present on the phone account. The lower the value, the higher the priority (i.e.
+     * ACTIVE (0) < HOLDING (1) < DIALING (2) < RINGING (3) equates to ACTIVE holding the highest
+     * priority).
+     */
+    private int computeCallStatePriority(Set<Integer> callStates) {
+        int priority = DEFAULT_CALL_STATE_PRIORITY;
+        for (int state: callStates) {
+            if (priority == mCallStatePriorityMap.get(CallState.ACTIVE)) {
+                return priority;
+            } else if (mCallStatePriorityMap.containsKey(state)
+                    && priority > mCallStatePriorityMap.get(state)) {
+                priority = mCallStatePriorityMap.get(state);
+            }
+        }
+        return priority;
+    }
+
     private static String nullToEmpty(String str) {
         return str == null ? "" : str;
     }
diff --git a/src/com/android/server/telecom/PhoneAccountRegistrar.java b/src/com/android/server/telecom/PhoneAccountRegistrar.java
index cc7f6ab..c59cf2c 100644
--- a/src/com/android/server/telecom/PhoneAccountRegistrar.java
+++ b/src/com/android/server/telecom/PhoneAccountRegistrar.java
@@ -181,7 +181,7 @@
     private final TelecomSystem.SyncRoot mLock;
     private State mState;
     private UserHandle mCurrentUserHandle;
-    private String mTestPhoneAccountPackageNameFilter;
+    private final Set<String> mTestPhoneAccountPackageNameFilters;
     private interface PhoneAccountRegistrarWriteLock {}
     private final PhoneAccountRegistrarWriteLock mWriteLock =
             new PhoneAccountRegistrarWriteLock() {};
@@ -215,6 +215,7 @@
         mAppLabelProxy = appLabelProxy;
         mCurrentUserHandle = Process.myUserHandle();
         mTelecomFeatureFlags = telecomFeatureFlags;
+        mTestPhoneAccountPackageNameFilters = new HashSet<>();
 
         if (telephonyFeatureFlags != null) {
             mTelephonyFeatureFlags = telephonyFeatureFlags;
@@ -607,23 +608,33 @@
      * {@link PhoneAccount}s with the same package name.
      */
     public void setTestPhoneAccountPackageNameFilter(String packageNameFilter) {
-        mTestPhoneAccountPackageNameFilter = packageNameFilter;
-        Log.i(this, "filter set for PhoneAccounts, packageName=" + packageNameFilter);
+        mTestPhoneAccountPackageNameFilters.clear();
+        if (packageNameFilter == null) {
+            return;
+        }
+        String [] pkgNamesFilter = packageNameFilter.split(",");
+        mTestPhoneAccountPackageNameFilters.addAll(Arrays.asList(pkgNamesFilter));
+        StringBuilder pkgNames = new StringBuilder();
+        for (int i = 0; i < pkgNamesFilter.length; i++) {
+            pkgNames.append(pkgNamesFilter[i])
+                    .append(i != pkgNamesFilter.length - 1 ? ", " : ".");
+        }
+        Log.i(this, "filter set for PhoneAccounts, packageNames: %s", pkgNames.toString());
     }
 
     /**
      * Filter the given {@link List<PhoneAccount>} and keep only {@link PhoneAccount}s that have the
-     * #mTestPhoneAccountPackageNameFilter.
+     * #mTestPhoneAccountPackageNameFilters.
      * @param accounts List of {@link PhoneAccount}s to filter.
      * @return new list of filtered {@link PhoneAccount}s.
      */
     public List<PhoneAccount> filterRestrictedPhoneAccounts(List<PhoneAccount> accounts) {
-        if (TextUtils.isEmpty(mTestPhoneAccountPackageNameFilter)) {
+        if (mTestPhoneAccountPackageNameFilters.isEmpty()) {
             return new ArrayList<>(accounts);
         }
-        // Remove all PhoneAccounts that do not have the same package name as the filter.
-        return accounts.stream().filter(account -> mTestPhoneAccountPackageNameFilter.equals(
-                account.getAccountHandle().getComponentName().getPackageName()))
+        // Remove all PhoneAccounts that do not have the same package name (prefix) as the filter.
+        return accounts.stream().filter(account -> mTestPhoneAccountPackageNameFilters
+                .contains(account.getAccountHandle().getComponentName().getPackageName()))
                 .collect(Collectors.toList());
     }
 
@@ -1977,7 +1988,7 @@
             }
             pw.decreaseIndent();
             pw.increaseIndent();
-            pw.println("test emergency PhoneAccount filter: " + mTestPhoneAccountPackageNameFilter);
+            pw.println("test emergency PhoneAccount filter: " + mTestPhoneAccountPackageNameFilters);
             pw.decreaseIndent();
         }
     }
diff --git a/src/com/android/server/telecom/Ringer.java b/src/com/android/server/telecom/Ringer.java
index bfaadf0..d0fd201 100644
--- a/src/com/android/server/telecom/Ringer.java
+++ b/src/com/android/server/telecom/Ringer.java
@@ -31,6 +31,7 @@
 import android.media.AudioAttributes;
 import android.media.AudioManager;
 import android.media.Ringtone;
+import android.media.RingtoneManager;
 import android.media.Utils;
 import android.media.VolumeShaper;
 import android.media.audio.Flags;
@@ -431,6 +432,11 @@
                     && isVibratorEnabled) {
                 Log.i(this, "Muted haptic channels since audio coupled ramping ringer is disabled");
                 hapticChannelsMuted = true;
+                if (useCustomVibration(foregroundCall)) {
+                    Log.i(this,
+                            "Not muted haptic channel for customization when apply ramping ringer");
+                    hapticChannelsMuted = false;
+                }
             } else if (hapticChannelsMuted) {
                 Log.i(this,
                         "Muted haptic channels isVibratorEnabled=%s, hapticPlaybackSupported=%s",
@@ -442,7 +448,7 @@
             if (!isHapticOnly) {
                 ringtoneInfoSupplier = () -> mRingtoneFactory.getRingtone(
                         foregroundCall, mVolumeShaperConfig, finalHapticChannelsMuted);
-            } else if (Flags.enableRingtoneHapticsCustomization() && mRingtoneVibrationSupported) {
+            } else if (useCustomVibration(foregroundCall)) {
                 ringtoneInfoSupplier = () -> mRingtoneFactory.getRingtone(
                         foregroundCall, null, false);
             }
@@ -521,6 +527,21 @@
         }
     }
 
+    private boolean useCustomVibration(@NonNull Call foregroundCall) {
+        return Flags.enableRingtoneHapticsCustomization() && mRingtoneVibrationSupported
+                && hasExplicitVibration(foregroundCall);
+    }
+
+    private boolean hasExplicitVibration(@NonNull Call foregroundCall) {
+        final Uri ringtoneUri = foregroundCall.getRingtone();
+        if (ringtoneUri != null) {
+            // TODO(b/399265235) : Avoid this hidden API access for mainline
+            return Utils.hasVibration(ringtoneUri);
+        }
+        return Utils.hasVibration(RingtoneManager.getActualDefaultRingtoneUri(
+                mContext, RingtoneManager.TYPE_RINGTONE));
+    }
+
     /**
      * Try to reserve the vibrator for this call, returning false if it's already committed.
      * The vibration will be started by AsyncRingtonePlayer to ensure timing is aligned with the
diff --git a/src/com/android/server/telecom/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java
index 9d1a382..7bb041d 100644
--- a/src/com/android/server/telecom/TelecomServiceImpl.java
+++ b/src/com/android/server/telecom/TelecomServiceImpl.java
@@ -198,6 +198,7 @@
         public void addCall(CallAttributes callAttributes, ICallEventCallback callEventCallback,
                 String callId, String callingPackage) {
             int uid = Binder.getCallingUid();
+            int pid = Binder.getCallingPid();
             ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_ADDCALL,
                     uid, ApiStats.RESULT_PERMISSION);
             try {
@@ -217,7 +218,7 @@
                 // add extras about info used for FGS delegation
                 Bundle extras = new Bundle();
                 extras.putInt(CallAttributes.CALLER_UID_KEY, uid);
-                extras.putInt(CallAttributes.CALLER_PID_KEY, uid);
+                extras.putInt(CallAttributes.CALLER_PID_KEY, pid);
 
 
                 CompletableFuture<CallTransaction> transactionFuture;
diff --git a/src/com/android/server/telecom/callsequencing/CallSequencingController.java b/src/com/android/server/telecom/callsequencing/CallSequencingController.java
index d6e02c1..acda1a1 100644
--- a/src/com/android/server/telecom/callsequencing/CallSequencingController.java
+++ b/src/com/android/server/telecom/callsequencing/CallSequencingController.java
@@ -44,6 +44,7 @@
 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;
@@ -62,8 +63,10 @@
 import com.android.server.telecom.metrics.TelecomMetricsController;
 import com.android.server.telecom.stats.CallFailureCause;
 
+import java.util.HashSet;
 import java.util.List;
 import java.util.Objects;
+import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.CompletableFuture;
 
@@ -456,11 +459,26 @@
      *         made for the emergency call.
      */
     private CompletableFuture<Boolean> makeRoomForOutgoingEmergencyCall(Call emergencyCall) {
-        // Always disconnect any ringing/incoming calls when an emergency call is placed to minimize
-        // distraction. This does not affect live call count.
-        CompletableFuture<Boolean> ringingCallFuture = null;
-        Call ringingCall = null;
-        if (mCallsManager.hasRingingOrSimulatedRingingCall()) {
+        // Disconnect all self-managed + transactional calls. We will never use these accounts for
+        // emergency calling. Disconnect non-holdable calls (in the dual-sim case) as well. For
+        // the single sim case (like Verizon), we should support the existing behavior of
+        // disconnecting the active call; refrain from disconnecting the held call in this case if
+        // it exists.
+        boolean areMultiplePhoneAccountsActive = areMultiplePhoneAccountsActive();
+        Pair<Set<Call>, CompletableFuture<Boolean>> disconnectCallsForEmergencyPair =
+                disconnectCallsForEmergencyCall(emergencyCall, areMultiplePhoneAccountsActive);
+        // The list of calls that were disconnected
+        Set<Call> disconnectedCalls = disconnectCallsForEmergencyPair.first;
+        // The future encompassing the result of the disconnect transaction(s). Because of the
+        // bulk transaction, we will always opt to perform sequencing on this future. Note that this
+        // future will always be completed with true if no disconnects occurred.
+        CompletableFuture<Boolean> transactionFuture = disconnectCallsForEmergencyPair.second;
+
+        Call ringingCall;
+        if (mCallsManager.hasRingingOrSimulatedRingingCall() && !disconnectedCalls
+                .contains(mCallsManager.getRingingOrSimulatedRingingCall())) {
+            // Always disconnect any ringing/incoming calls when an emergency call is placed to
+            // minimize distraction. This does not affect live call count.
             ringingCall = mCallsManager.getRingingOrSimulatedRingingCall();
             ringingCall.getAnalytics().setCallIsAdditional(true);
             ringingCall.getAnalytics().setCallIsInterrupted(true);
@@ -469,39 +487,54 @@
                     // If this is an incoming call that is currently in SIMULATED_RINGING only
                     // after a call screen, disconnect to make room and mark as missed, since
                     // the user didn't get a chance to accept/reject.
-                    ringingCallFuture = ringingCall.disconnect("emergency call dialed during "
-                            + "simulated ringing after screen.");
+                    transactionFuture = transactionFuture.thenComposeAsync((result) ->
+                                    ringingCall.disconnect("emergency call dialed during simulated "
+                                            + "ringing after screen."),
+                            new LoggedHandlerExecutor(mHandler, "CSC.mRFOEC",
+                                    mCallsManager.getLock()));
                 } else {
                     // If this is a simulated ringing call after being active and put in
                     // AUDIO_PROCESSING state again, disconnect normally.
-                    ringingCallFuture = ringingCall.reject(false, null, "emergency call dialed "
-                            + "during simulated ringing.");
+                    transactionFuture = transactionFuture.thenComposeAsync((result) ->
+                                    ringingCall.reject(false, null,
+                                            "emergency call dialed during simulated ringing."),
+                            new LoggedHandlerExecutor(mHandler, "CSC.mRFOEC",
+                                    mCallsManager.getLock()));
                 }
             } else { // normal incoming ringing call.
                 // Hang up the ringing call to make room for the emergency call and mark as missed,
                 // since the user did not reject.
                 ringingCall.setOverrideDisconnectCauseCode(
                         new DisconnectCause(DisconnectCause.MISSED));
-                ringingCallFuture = ringingCall.reject(false, null, "emergency call dialed "
-                        + "during ringing.");
+                transactionFuture = transactionFuture.thenComposeAsync((result) ->
+                                ringingCall.reject(false, null,
+                                        "emergency call dialed during ringing."),
+                        new LoggedHandlerExecutor(mHandler, "CSC.mRFOEC",
+                                mCallsManager.getLock()));
             }
+        } else {
+            ringingCall = null;
         }
 
         // There is already room!
         if (!mCallsManager.hasMaximumLiveCalls(emergencyCall)) {
-            return CompletableFuture.completedFuture(true);
+            return transactionFuture;
         }
 
         Call liveCall = mCallsManager.getFirstCallWithLiveState();
         Log.i(this, "makeRoomForOutgoingEmergencyCall: call = " + emergencyCall
                 + " livecall = " + liveCall);
 
-        if (emergencyCall == liveCall) {
-            // Not likely, but a good correctness check.
-            return CompletableFuture.completedFuture(true);
+        // Don't need to proceed further if we already disconnected the live call or if the live
+        // call is the emergency call being placed (not likely).
+        if (emergencyCall == liveCall || disconnectedCalls.contains(liveCall)) {
+            return transactionFuture;
         }
 
-        if (mCallsManager.hasMaximumOutgoingCalls(emergencyCall)) {
+        // If we already disconnected the outgoing call, then don't perform any additional ops on
+        // it.
+        if (mCallsManager.hasMaximumOutgoingCalls(emergencyCall) && !disconnectedCalls
+                .contains(mCallsManager.getFirstCallWithState(OUTGOING_CALL_STATES))) {
             Call outgoingCall = mCallsManager.getFirstCallWithState(OUTGOING_CALL_STATES);
             String disconnectReason = null;
             if (!outgoingCall.isEmergencyCall()) {
@@ -520,27 +553,10 @@
                         + " of new outgoing call.";
             }
             if (disconnectReason != null) {
-                boolean isSequencingRequiredRingingAndOutgoing = !arePhoneAccountsSame(
-                        ringingCall, outgoingCall);
-                if (ringingCallFuture != null && isSequencingRequiredRingingAndOutgoing) {
-                    String finalDisconnectReason = disconnectReason;
-                    return ringingCallFuture.thenComposeAsync((result) -> {
-                        if (result) {
-                            Log.i(this, "makeRoomForOutgoingEmergencyCall: Request to disconnect"
-                                    + " ringing call succeeded. Attempting to disconnect "
-                                    + "outgoing call.");
-                            return outgoingCall.disconnect(finalDisconnectReason);
-                        } else {
-                            Log.i(this, "makeRoomForOutgoingEmergencyCall: Request to disconnect"
-                                    + "ringing call failed. Aborting attempt to disconnect "
-                                    + "outgoing call");
-                            return CompletableFuture.completedFuture(false);
-                        }
-                    }, new LoggedHandlerExecutor(mHandler, "CSC.mRFOEC",
-                            mCallsManager.getLock()));
-                } else {
-                    return outgoingCall.disconnect(disconnectReason);
-                }
+                boolean isSequencingRequiredRingingAndOutgoing = ringingCall == null
+                        || !arePhoneAccountsSame(ringingCall, outgoingCall);
+                return disconnectOngoingCallForEmergencyCall(transactionFuture, outgoingCall,
+                        disconnectReason, isSequencingRequiredRingingAndOutgoing);
             }
             //  If the user tries to make two outgoing calls to different emergency call numbers,
             //  we will try to connect the first outgoing call and reject the second.
@@ -548,28 +564,14 @@
             return CompletableFuture.completedFuture(false);
         }
 
-        boolean isSequencingRequiredRingingAndLive = ringingCall != null
-                && !arePhoneAccountsSame(ringingCall, liveCall);
+        boolean isSequencingRequiredLive = ringingCall == null
+                || !arePhoneAccountsSame(ringingCall, liveCall);
         if (liveCall.getState() == CallState.AUDIO_PROCESSING) {
             emergencyCall.getAnalytics().setCallIsAdditional(true);
             liveCall.getAnalytics().setCallIsInterrupted(true);
             final String disconnectReason = "disconnecting audio processing call for emergency";
-            if (ringingCallFuture != null && isSequencingRequiredRingingAndLive) {
-                return ringingCallFuture.thenComposeAsync((result) -> {
-                    if (result) {
-                        Log.i(this, "makeRoomForOutgoingEmergencyCall: Request to disconnect "
-                                + "ringing call succeeded. Attempting to disconnect live call.");
-                        return liveCall.disconnect(disconnectReason);
-                    } else {
-                        Log.i(this, "makeRoomForOutgoingEmergencyCall: Request to disconnect "
-                                + "ringing call failed. Aborting attempt to disconnect live call.");
-                        return CompletableFuture.completedFuture(false);
-                    }
-                }, new LoggedHandlerExecutor(mHandler, "CSC.mRFOEC",
-                        mCallsManager.getLock()));
-            } else {
-                return liveCall.disconnect(disconnectReason);
-            }
+            return disconnectOngoingCallForEmergencyCall(transactionFuture, liveCall,
+                    disconnectReason, isSequencingRequiredLive);
         }
 
         // If the live call is stuck in a connecting state, prompt the user to generate a bugreport.
@@ -582,30 +584,37 @@
         // we'll disconnect the ongoing call if it cannot be held. If we have a self-managed call
         // that can't be held, then we should disconnect the call in favor of the emergency call.
         // Likewise, if there's only one active managed call which can't be held, then it should
-        // also be disconnected.
+        // also be disconnected. This will only happen for the single sim scenario to support
+        // backwards compatibility. For dual sim, we should try disconnecting the held call and
+        // hold the active call.
+        Call heldCall = null;
         if (mCallsManager.hasMaximumManagedHoldingCalls(emergencyCall)
                 || !mCallsManager.canHold(liveCall)) {
-            emergencyCall.getAnalytics().setCallIsAdditional(true);
-            liveCall.getAnalytics().setCallIsInterrupted(true);
-            // Disconnect the active call instead of the holding call because it is historically
-            // easier to do, rather than disconnect a held call.
             final String disconnectReason = "disconnecting to make room for emergency call "
                     + emergencyCall.getId();
-            if (ringingCallFuture != null && isSequencingRequiredRingingAndLive) {
-                return ringingCallFuture.thenComposeAsync((result) -> {
-                    if (result) {
-                        Log.i(this, "makeRoomForOutgoingEmergencyCall: Request to disconnect "
-                                + "ringing call succeeded. Attempting to disconnect live call.");
-                        return liveCall.disconnect(disconnectReason);
-                    } else {
-                        Log.i(this, "makeRoomForOutgoingEmergencyCall: Request to disconnect "
-                                + "ringing call failed. Aborting attempt to disconnect live call.");
-                        return CompletableFuture.completedFuture(false);
-                    }
-                }, new LoggedHandlerExecutor(mHandler, "CSC.mRFOEC",
-                        mCallsManager.getLock()));
-            } else {
-                return liveCall.disconnect(disconnectReason);
+            emergencyCall.getAnalytics().setCallIsAdditional(true);
+            // Single sim case
+            if (!areMultiplePhoneAccountsActive) {
+                liveCall.getAnalytics().setCallIsInterrupted(true);
+                // Disconnect the active call instead of the holding call because it is historically
+                // easier to do, rather than disconnect a held call.
+                return disconnectOngoingCallForEmergencyCall(transactionFuture, liveCall,
+                        disconnectReason, isSequencingRequiredLive);
+            } else { // Dual sim case
+                // If the live call can't be held, we would've already disconnected it
+                // in disconnectCallsForEmergencyCall. Note at this point, we should always have
+                // a held call then that should be disconnected (over the active call).
+                if (!mCallsManager.canHold(liveCall)) {
+                    return transactionFuture;
+                }
+                heldCall = mCallsManager.getFirstCallWithState(CallState.ON_HOLD);
+                boolean isSequencingRequiredRingingAndHeld = ringingCall == null
+                        || !arePhoneAccountsSame(ringingCall, heldCall);
+                isSequencingRequiredLive = !arePhoneAccountsSame(heldCall, liveCall);
+                heldCall.getAnalytics().setCallIsInterrupted(true);
+                // Disconnect the held call.
+                transactionFuture = disconnectOngoingCallForEmergencyCall(transactionFuture,
+                        heldCall, disconnectReason, isSequencingRequiredRingingAndHeld);
             }
         }
 
@@ -638,24 +647,8 @@
                         DisconnectCause.LOCAL, DisconnectCause.REASON_EMERGENCY_CALL_PLACED));
                 final String disconnectReason = "outgoing call does not support emergency calls, "
                         + "disconnecting.";
-                if (ringingCallFuture != null && isSequencingRequiredRingingAndLive) {
-                    return ringingCallFuture.thenComposeAsync((result) -> {
-                        if (result) {
-                            Log.i(this, "makeRoomForOutgoingEmergencyCall: Request to disconnect "
-                                    + "ringing call succeeded. "
-                                    + "Attempting to disconnect live call.");
-                            return liveCall.disconnect(disconnectReason);
-                        } else {
-                            Log.i(this, "makeRoomForOutgoingEmergencyCall: Request to disconnect "
-                                    + "ringing call failed. "
-                                    + "Aborting attempt to disconnect live call.");
-                            return CompletableFuture.completedFuture(false);
-                        }
-                    }, new LoggedHandlerExecutor(mHandler, "CSC.mRFOEC",
-                            mCallsManager.getLock()));
-                } else {
-                    return liveCall.disconnect(disconnectReason);
-                }
+                return disconnectOngoingCallForEmergencyCall(transactionFuture, liveCall,
+                        disconnectReason, isSequencingRequiredLive);
             }
         }
 
@@ -667,8 +660,8 @@
                 emergencyCall.getTargetPhoneAccount())) {
             Log.i(this, "makeRoomForOutgoingEmergencyCall: phoneAccounts are from same "
                     + "package. Attempting to hold live call before placing emergency call.");
-            return maybeHoldLiveCallForEmergency(ringingCallFuture,
-                    isSequencingRequiredRingingAndLive, liveCall, emergencyCall,
+            return maybeHoldLiveCallForEmergency(transactionFuture,
+                    isSequencingRequiredLive, liveCall, emergencyCall,
                     shouldHoldForEmergencyCall(liveCallPhoneAccount) /* shouldHoldForEmergency */);
         } else if (emergencyCall.getTargetPhoneAccount() == null) {
             // Without a phone account, we can't say reliably that the call will fail.
@@ -677,20 +670,24 @@
             // hold but they still support adding a call by going immediately into conference
             // mode). Return true here and we'll run this code again after user chooses an
             // account.
-            return CompletableFuture.completedFuture(true);
+            return transactionFuture;
         }
 
-        // Hold the live call if possible before attempting the new outgoing emergency call.
-        if (mCallsManager.canHold(liveCall)) {
+        // Hold the live call if possible before attempting the new outgoing emergency call. Also,
+        // ensure that we try holding if we disconnected a held call and the live call supports
+        // holding.
+        if (mCallsManager.canHold(liveCall) || (heldCall != null
+                && mCallsManager.supportsHold(liveCall))) {
             Log.i(this, "makeRoomForOutgoingEmergencyCall: holding live call.");
-            return maybeHoldLiveCallForEmergency(ringingCallFuture,
-                    isSequencingRequiredRingingAndLive, liveCall,
-                    emergencyCall, true /* shouldHoldForEmergency */);
+            return maybeHoldLiveCallForEmergency(transactionFuture, isSequencingRequiredLive,
+                    liveCall, emergencyCall, true /* shouldHoldForEmergency */);
         }
 
-        // The live call cannot be held so we're out of luck here.  There's no room.
-        emergencyCall.setStartFailCause(CallFailureCause.CANNOT_HOLD_CALL);
-        return CompletableFuture.completedFuture(false);
+        // Refrain from failing the call in Telecom if possible. Additional processing will be done
+        // in the Telephony layer to hold/disconnect calls (across subs, if needed) and we will fail
+        // there instead. This should be treated as the preprocessing steps required to set up the
+        // ability to place an emergency call.
+        return transactionFuture;
     }
 
     /**
@@ -828,25 +825,31 @@
 
     /* makeRoomForOutgoingEmergencyCall helpers */
 
+    /**
+     * Tries to hold the live call before placing the emergency call. If the hold fails, then we
+     * will instead disconnect the call.
+     *
+     * Note: This only applies when the live call and emergency call are from the same phone
+     * account.
+     */
     private CompletableFuture<Boolean> maybeHoldLiveCallForEmergency(
-            CompletableFuture<Boolean> ringingCallFuture, boolean isSequencingRequired,
+            CompletableFuture<Boolean> transactionFuture, boolean isSequencingRequired,
             Call liveCall, Call emergencyCall, boolean shouldHoldForEmergency) {
         emergencyCall.getAnalytics().setCallIsAdditional(true);
         liveCall.getAnalytics().setCallIsInterrupted(true);
         final String holdReason = "calling " + emergencyCall.getId();
         CompletableFuture<Boolean> holdResultFuture = CompletableFuture.completedFuture(false);
         if (shouldHoldForEmergency) {
-            if (ringingCallFuture != null && isSequencingRequired) {
-                holdResultFuture = ringingCallFuture.thenComposeAsync((result) -> {
+            if (transactionFuture != null && isSequencingRequired) {
+                holdResultFuture = transactionFuture.thenComposeAsync((result) -> {
                     if (result) {
                         Log.i(this, "makeRoomForOutgoingEmergencyCall: Request to disconnect "
-                                + "ringing call succeeded. Attempting to hold live call.");
-                        return liveCall.hold(holdReason);
-                    } else {
+                                + "previous call succeeded. Attempting to hold live call.");
+                    } else { // Log the failure but proceed with hold transaction.
                         Log.i(this, "makeRoomForOutgoingEmergencyCall: Request to disconnect "
-                                + "ringing call failed. Aborting attempt to hold live call.");
-                        return CompletableFuture.completedFuture(false);
+                                + "previous call failed. Still attempting to hold live call.");
                     }
+                    return liveCall.hold(holdReason);
                 }, new LoggedHandlerExecutor(mHandler, "CSC.mRFOEC",
                         mCallsManager.getLock()));
             } else {
@@ -868,6 +871,117 @@
     }
 
     /**
+     * Disconnects all VOIP + non-holdable calls as well as those that don't support placing
+     * emergency calls before placing an emergency call.
+     *
+     * Note: If a call can't be held, it will be active to begin with.
+     * @return The list of calls to be disconnected alongside the future keeping track of the
+     *         disconnect transaction.
+     */
+    private Pair<Set<Call>, CompletableFuture<Boolean>> disconnectCallsForEmergencyCall(
+            Call emergencyCall, boolean areMultiplePhoneAccountsActive) {
+        Set<Call> callsDisconnected = new HashSet<>();
+        Call previousCall = null;
+        Call ringingCall = mCallsManager.getRingingOrSimulatedRingingCall();
+        CompletableFuture<Boolean> disconnectFuture = CompletableFuture.completedFuture(true);
+        for (Call call: mCallsManager.getCalls()) {
+            // Conditions for checking if call doesn't need to be disconnected immediately.
+            boolean isManaged = !call.isSelfManaged() && !call.isTransactionalCall();
+            boolean callSupportsHold = call.can(Connection.CAPABILITY_SUPPORT_HOLD);
+            boolean callSupportsHoldingEmergencyCall = shouldHoldForEmergencyCall(
+                    call.getTargetPhoneAccount());
+
+            // Skip the ringing call; we'll handle the disconnect explicitly later.
+            if (call.equals(ringingCall)) {
+                continue;
+            }
+
+            // If the call is managed and supports holding + capability to place emergency calls,
+            // don't disconnect the call.
+            if (isManaged && callSupportsHoldingEmergencyCall) {
+                // If call supports hold, we can skip. Other condition we check here is if calls
+                // are on single sim, in which case we will refrain from disconnecting a potentially
+                // held call (i.e. Verizon ACTIVE + HOLD case) here and let that be determined later
+                // down in makeRoomForOutgoingEmergencyCall.
+                if (callSupportsHold || (!areMultiplePhoneAccountsActive)) {
+                    continue;
+                }
+            }
+
+            Log.i(this, "Disconnecting call (%s). isManaged: %b, call supports hold: %b, call "
+                            + "supports holding emergency call: %b", call.getId(), isManaged,
+                    callSupportsHold, callSupportsHoldingEmergencyCall);
+            emergencyCall.getAnalytics().setCallIsAdditional(true);
+            call.getAnalytics().setCallIsInterrupted(true);
+            call.setOverrideDisconnectCauseCode(new DisconnectCause(
+                    DisconnectCause.LOCAL, DisconnectCause.REASON_EMERGENCY_CALL_PLACED));
+
+            Call finalPreviousCall = previousCall;
+            disconnectFuture = disconnectFuture.thenComposeAsync((result) -> {
+                if (!result) {
+                    // Log the failure if it happens but proceed with the disconnects.
+                    Log.i(this, "Call (%s) failed to be disconnected",
+                            finalPreviousCall);
+                }
+                return call.disconnect("Disconnecting call with phone account that does not "
+                        + "support emergency call");
+            }, new LoggedHandlerExecutor(mHandler, "CSC.dAVC",
+                    mCallsManager.getLock()));
+            previousCall = call;
+            callsDisconnected.add(call);
+        }
+        return new Pair<>(callsDisconnected, disconnectFuture);
+    }
+
+    /**
+     * Waiting on the passed future completion when sequencing is required, this will try to the
+     * disconnect the call passed in.
+     */
+    private CompletableFuture<Boolean> disconnectOngoingCallForEmergencyCall(
+            CompletableFuture<Boolean> transactionFuture, Call callToDisconnect,
+            String disconnectReason, boolean isSequencingRequired) {
+        if (isSequencingRequired) {
+            return transactionFuture.thenComposeAsync((result) -> {
+                if (result) {
+                    Log.i(this, "makeRoomForOutgoingEmergencyCall: Request to disconnect "
+                            + "previous call succeeded. Attempting to disconnect ongoing call"
+                            + " %s.", callToDisconnect);
+                } else {
+                    Log.i(this, "makeRoomForOutgoingEmergencyCall: Request to disconnect "
+                            + "previous call failed. Still attempting to disconnect ongoing call"
+                            + " %s.", callToDisconnect);
+                }
+                return callToDisconnect.disconnect(disconnectReason);
+            }, new LoggedHandlerExecutor(mHandler, "CSC.mRFOEC",
+                    mCallsManager.getLock()));
+        } else {
+            return callToDisconnect.disconnect(disconnectReason);
+        }
+    }
+
+    /**
+     * Determines if DSDA is being used (i.e. calls present on more than one phone account).
+     */
+    private boolean areMultiplePhoneAccountsActive() {
+        List<Call> calls = mCallsManager.getCalls().stream().toList();
+        PhoneAccountHandle handle1 = null;
+        if (!calls.isEmpty()) {
+            // Find the first handle different from the one retrieved from the first call in
+            // the list.
+            for(int i = 0; i < calls.size(); i++) {
+                if (handle1 == null && calls.get(i).getTargetPhoneAccount() != null) {
+                    handle1 = calls.getFirst().getTargetPhoneAccount();
+                }
+                if (handle1 != null && calls.get(i).getTargetPhoneAccount() != null
+                        && !handle1.equals(calls.get(i).getTargetPhoneAccount())) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
      * Checks the carrier config to see if the carrier supports holding emergency calls.
      * @param handle The {@code PhoneAccountHandle} to check
      * @return {@code true} if the carrier supports holding emergency calls, {@code} false
diff --git a/src/com/android/server/telecom/callsequencing/TransactionalCallSequencingAdapter.java b/src/com/android/server/telecom/callsequencing/TransactionalCallSequencingAdapter.java
index 4adc8d0..37bc065 100644
--- a/src/com/android/server/telecom/callsequencing/TransactionalCallSequencingAdapter.java
+++ b/src/com/android/server/telecom/callsequencing/TransactionalCallSequencingAdapter.java
@@ -260,9 +260,7 @@
     }
 
     private void removeCallFromCallsManager(Call call, DisconnectCause cause) {
-        if (cause.getCode() != DisconnectCause.REJECTED) {
-            mCallsManager.markCallAsDisconnected(call, cause);
-        }
+        mCallsManager.markCallAsDisconnected(call, cause);
         mCallsManager.removeCall(call);
     }
 
diff --git a/tests/src/com/android/server/telecom/tests/CallSequencingTests.java b/tests/src/com/android/server/telecom/tests/CallSequencingTests.java
index 9cfc95c..2f511b8 100644
--- a/tests/src/com/android/server/telecom/tests/CallSequencingTests.java
+++ b/tests/src/com/android/server/telecom/tests/CallSequencingTests.java
@@ -516,6 +516,25 @@
         assertTrue(waitForFutureResult(future, false));
     }
 
+    @SmallTest
+    @Test
+    public void testMakeRoomForOutgoingEmergencyCall_DoesNotSupportHoldingEmergency() {
+        setupMakeRoomForOutgoingEmergencyCallMocks();
+        when(mCallsManager.getCalls()).thenReturn(List.of(mActiveCall, mRingingCall));
+        when(mActiveCall.getTargetPhoneAccount()).thenReturn(mHandle1);
+        // Set the KEY_ALLOW_HOLD_CALL_DURING_EMERGENCY_BOOL carrier config to false for the active
+        // call's phone account.
+        PersistableBundle bundle = new PersistableBundle();
+        bundle.putBoolean(CarrierConfigManager.KEY_ALLOW_HOLD_CALL_DURING_EMERGENCY_BOOL, false);
+        when(mCallsManager.getCarrierConfigForPhoneAccount(eq(mHandle1))).thenReturn(bundle);
+        when(mNewCall.getTargetPhoneAccount()).thenReturn(mHandle2);
+        when(mRingingCall.getTargetPhoneAccount()).thenReturn(mHandle2);
+
+        mController.makeRoomForOutgoingCall(true, mNewCall);
+        // Verify that the active call got disconnected as it doesn't support holding for emergency.
+        verify(mActiveCall, timeout(SEQUENCING_TIMEOUT_MS)).disconnect(anyString());
+    }
+
     @Test
     @SmallTest
     public void testMakeRoomForOutgoingCall() {
diff --git a/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java b/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java
index 475133c..406bc8a 100644
--- a/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java
+++ b/tests/src/com/android/server/telecom/tests/CreateConnectionProcessorTest.java
@@ -51,6 +51,7 @@
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallIdMapper;
 import com.android.server.telecom.CallState;
+import com.android.server.telecom.CallsManager;
 import com.android.server.telecom.ConnectionServiceFocusManager;
 import com.android.server.telecom.ConnectionServiceRepository;
 import com.android.server.telecom.ConnectionServiceWrapper;
@@ -97,6 +98,8 @@
     @Mock
     PhoneAccountRegistrar mMockAccountRegistrar;
     @Mock
+    CallsManager mCallsManager;
+    @Mock
     CreateConnectionResponse mMockCreateConnectionResponse;
     @Mock
     Call mMockCall;
@@ -136,7 +139,7 @@
 
         mTestCreateConnectionProcessor = new CreateConnectionProcessor(mMockCall,
                 mMockConnectionServiceRepository, mMockCreateConnectionResponse,
-                mMockAccountRegistrar, mContext, mFeatureFlags, mTimeoutsAdapter);
+                mMockAccountRegistrar, mCallsManager, mContext, mFeatureFlags, mTimeoutsAdapter);
 
         mAccountToSub = new HashMap<>();
         phoneAccounts = new ArrayList<>();
diff --git a/tests/src/com/android/server/telecom/tests/RingerTest.java b/tests/src/com/android/server/telecom/tests/RingerTest.java
index ad62643..d9bf6e1 100644
--- a/tests/src/com/android/server/telecom/tests/RingerTest.java
+++ b/tests/src/com/android/server/telecom/tests/RingerTest.java
@@ -22,6 +22,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeNotNull;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
@@ -46,6 +47,7 @@
 import android.media.AudioAttributes;
 import android.media.AudioManager;
 import android.media.Ringtone;
+import android.media.RingtoneManager;
 import android.media.VolumeShaper;
 import android.media.audio.Flags;
 import android.net.Uri;
@@ -65,6 +67,7 @@
 import android.telecom.TelecomManager;
 import android.util.Pair;
 
+import androidx.test.core.app.ApplicationProvider;
 import androidx.test.filters.SmallTest;
 import androidx.test.platform.app.InstrumentationRegistry;
 
@@ -85,6 +88,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.Spy;
 
@@ -846,8 +850,12 @@
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_RINGTONE_HAPTICS_CUSTOMIZATION)
     public void testNoVibrateForSilentRingtoneIfRingtoneHasVibration() throws Exception {
+        final Context context = ApplicationProvider.getApplicationContext();
+        Uri defaultRingtoneUri = RingtoneManager.getActualDefaultRingtoneUri(context,
+                RingtoneManager.TYPE_RINGTONE);
+        assumeNotNull(defaultRingtoneUri);
         Uri FAKE_RINGTONE_VIBRATION_URI =
-                FAKE_RINGTONE_URI.buildUpon().appendQueryParameter(
+                defaultRingtoneUri.buildUpon().appendQueryParameter(
                         VIBRATION_PARAM, FAKE_VIBRATION_URI.toString()).build();
         Ringtone mockRingtone = mock(Ringtone.class);
         Pair<Uri, Ringtone> ringtoneInfo = new Pair(FAKE_RINGTONE_VIBRATION_URI, mockRingtone);
@@ -856,21 +864,61 @@
                 .thenReturn(ringtoneInfo);
         mComponentContextFixture.putBooleanResource(
                 com.android.internal.R.bool.config_ringtoneVibrationSettingsSupported, true);
-        createRingerUnderTest(); // Needed after mock the config.
+        try {
+            RingtoneManager.setActualDefaultRingtoneUri(context, RingtoneManager.TYPE_RINGTONE,
+                    FAKE_RINGTONE_VIBRATION_URI);
+            createRingerUnderTest(); // Needed after mock the config.
 
-        mRingerUnderTest.startCallWaiting(mockCall1);
-        when(mockAudioManager.getRingerMode()).thenReturn(AudioManager.RINGER_MODE_VIBRATE);
-        when(mockAudioManager.getStreamVolume(AudioManager.STREAM_RING)).thenReturn(0);
-        enableVibrationWhenRinging();
-        assertFalse(startRingingAndWaitForAsync(mockCall2, false));
+            mRingerUnderTest.startCallWaiting(mockCall1);
+            when(mockAudioManager.getRingerMode()).thenReturn(AudioManager.RINGER_MODE_VIBRATE);
+            when(mockAudioManager.getStreamVolume(AudioManager.STREAM_RING)).thenReturn(0);
+            enableVibrationWhenRinging();
+            assertFalse(startRingingAndWaitForAsync(mockCall2, false));
 
-        verify(mockRingtoneFactory, atLeastOnce())
-                .getRingtone(any(Call.class), eq(null), eq(false));
-        verifyNoMoreInteractions(mockRingtoneFactory);
-        verify(mockTonePlayer).stopTone();
-        // Skip vibration play in Ringer if a vibration was specified to the ringtone
-        verify(mockVibrator, never()).vibrate(any(VibrationEffect.class),
-                any(VibrationAttributes.class));
+            verify(mockRingtoneFactory, atLeastOnce())
+                    .getRingtone(any(Call.class), eq(null), eq(false));
+            verifyNoMoreInteractions(mockRingtoneFactory);
+            verify(mockTonePlayer).stopTone();
+            // Skip vibration play in Ringer if a vibration was specified to the ringtone
+            verify(mockVibrator, never()).vibrate(any(VibrationEffect.class),
+                    any(VibrationAttributes.class));
+        } finally {
+            // Restore the default ringtone Uri
+            RingtoneManager.setActualDefaultRingtoneUri(context, RingtoneManager.TYPE_RINGTONE,
+                    defaultRingtoneUri);
+        }
+    }
+
+    @SmallTest
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_RINGTONE_HAPTICS_CUSTOMIZATION)
+    public void testNotMuteHapticChannelWithRampingRinger() throws Exception {
+        final Context context = ApplicationProvider.getApplicationContext();
+        Uri defaultRingtoneUri = RingtoneManager.getActualDefaultRingtoneUri(context,
+                RingtoneManager.TYPE_RINGTONE);
+        assumeNotNull(defaultRingtoneUri);
+        Uri FAKE_RINGTONE_VIBRATION_URI = defaultRingtoneUri.buildUpon().appendQueryParameter(
+                        VIBRATION_PARAM, FAKE_VIBRATION_URI.toString()).build();
+        mComponentContextFixture.putBooleanResource(
+                com.android.internal.R.bool.config_ringtoneVibrationSettingsSupported, true);
+        ArgumentCaptor<Boolean> muteHapticChannelCaptor = ArgumentCaptor.forClass(Boolean.class);
+        try {
+            RingtoneManager.setActualDefaultRingtoneUri(context, RingtoneManager.TYPE_RINGTONE,
+                    FAKE_RINGTONE_VIBRATION_URI);
+            createRingerUnderTest(); // Needed after mock the config.
+            mRingerUnderTest.startCallWaiting(mockCall1);
+            ensureRingerIsAudible();
+            enableRampingRinger();
+            enableVibrationWhenRinging();
+            assertTrue(startRingingAndWaitForAsync(mockCall2, false));
+            verify(mockRingtoneFactory, atLeastOnce()).getRingtone(any(Call.class),
+                    nullable(VolumeShaper.Configuration.class), muteHapticChannelCaptor.capture());
+            assertFalse(muteHapticChannelCaptor.getValue());
+        } finally {
+            // Restore the default ringtone Uri
+            RingtoneManager.setActualDefaultRingtoneUri(context, RingtoneManager.TYPE_RINGTONE,
+                    defaultRingtoneUri);
+        }
     }
 
     /**
diff --git a/tests/src/com/android/server/telecom/tests/TransactionalCallSequencingAdapterTest.java b/tests/src/com/android/server/telecom/tests/TransactionalCallSequencingAdapterTest.java
new file mode 100644
index 0000000..6449ea7
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/TransactionalCallSequencingAdapterTest.java
@@ -0,0 +1,171 @@
+/*
+ * 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 org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.OutcomeReceiver;
+import android.telecom.CallException;
+import android.telecom.DisconnectCause;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.callsequencing.CallTransaction;
+import com.android.server.telecom.callsequencing.CallTransactionResult;
+import com.android.server.telecom.callsequencing.TransactionManager;
+import com.android.server.telecom.callsequencing.TransactionalCallSequencingAdapter;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+
+
+/**
+ * Unit tests for {@link TransactionalCallSequencingAdapter}.
+ *
+ * These tests verify the behavior of the TransactionalCallSequencingAdapter, focusing on
+ * how it interacts with the TransactionManager and CallsManager, particularly in the
+ * context of asynchronous operations and feature flag configurations (e.g., setting
+ * rejected calls to a disconnected state).
+ */
+public class TransactionalCallSequencingAdapterTest extends TelecomTestCase {
+
+    private static final String CALL_ID_1 = "1";
+    private static final DisconnectCause REJECTED_DISCONNECT_CAUSE =
+            new DisconnectCause(DisconnectCause.REJECTED);
+
+    @Mock private Call mMockCall1;
+    @Mock private Context mMockContext;
+    @Mock private CallsManager mCallsManager;
+    @Mock private TransactionManager mTransactionManager;
+
+    private TransactionalCallSequencingAdapter mAdapter;
+
+    @Override
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        MockitoAnnotations.initMocks(this);
+        when(mMockCall1.getId()).thenReturn(CALL_ID_1);
+        when(mMockContext.getResources()).thenReturn(Mockito.mock(Resources.class));
+        mAdapter = new TransactionalCallSequencingAdapter(
+                mTransactionManager, mCallsManager, true);
+    }
+
+    @Override
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    /**
+     * Tests the scenario where an incoming call is rejected and the onSetDisconnect is called.
+     * Verifies that {@link CallsManager#markCallAsDisconnected} *is* called and that the
+     * {@link OutcomeReceiver} receives the correct result, handling the asynchronous nature of
+     * the operation.
+     */
+    @Test
+    public void testOnSetDisconnected() {
+        // GIVEN -a new incoming call that is rejected
+
+        // Create a CompletableFuture to control the asynchronous operation.
+        CompletableFuture<Boolean> future = new CompletableFuture<>();
+
+        // Mock the TransactionManager's addTransaction method.
+        setupAddTransactionMock(future);
+
+        // Create a mock OutcomeReceiver to verify interactions.
+        OutcomeReceiver<CallTransactionResult, CallException> resultReceiver =
+                mock(OutcomeReceiver.class);
+
+        // WHEN - Call onSetDisconnected and get the result future.
+        mAdapter.onSetDisconnected(
+                mMockCall1,
+                REJECTED_DISCONNECT_CAUSE,
+                mock(CallTransaction.class),
+                resultReceiver);
+
+        // Simulate the asynchronous operation completing.
+        completeAddTransactionSuccessfully(future);
+
+        // THEN - Verify that markCallAsDisconnected and the receiver's onResult were called.
+        verifyMarkCallAsDisconnectedAndReceiverResult(resultReceiver);
+    }
+    /**
+     * Sets up the mock behavior for {@link TransactionManager#addTransaction}.
+     *
+     * @param future The CompletableFuture to be returned by the mocked method.
+     */
+    private void setupAddTransactionMock(CompletableFuture<Boolean> future) {
+        when(mTransactionManager.addTransaction(any(), any())).thenAnswer(invocation -> {
+            return future; // Return the provided future.
+        });
+    }
+    /**
+     * Simulates the successful completion of the asynchronous operation tracked by the given
+     * future. Captures the {@link OutcomeReceiver} passed to
+     * {@link TransactionManager#addTransaction}, completes the future, and invokes
+     * {@link OutcomeReceiver#onResult} with a successful result.
+     *
+     * @param future The CompletableFuture to complete.
+     */
+    private void completeAddTransactionSuccessfully(CompletableFuture<Boolean> future) {
+        // Capture the OutcomeReceiver passed to addTransaction.
+        ArgumentCaptor<OutcomeReceiver<CallTransactionResult, CallException>> captor =
+                ArgumentCaptor.forClass(OutcomeReceiver.class);
+        verify(mTransactionManager).addTransaction(any(CallTransaction.class), captor.capture());
+
+        // Complete the future to signal the end of the asynchronous operation.
+        future.complete(true);
+
+        // Create a successful CallTransactionResult.
+        CallTransactionResult callTransactionResult = new CallTransactionResult(
+                CallTransactionResult.RESULT_SUCCEED,
+                "EndCallTransaction: RESULT_SUCCEED");
+
+        // Invoke onResult on the captured OutcomeReceiver.
+        captor.getValue().onResult(callTransactionResult);
+
+    }
+    /**
+     * Verifies that {@link CallsManager#markCallAsDisconnected} and the provided
+     * {@link OutcomeReceiver}'s {@code onResult} method were called.  Also waits for the future
+     * to complete.
+     *
+     * @param resultReceiver The mock OutcomeReceiver.
+     */
+    private void verifyMarkCallAsDisconnectedAndReceiverResult(
+            OutcomeReceiver<CallTransactionResult, CallException> resultReceiver) {
+        verify(mCallsManager, times(1)).markCallAsDisconnected(
+                mMockCall1,
+                REJECTED_DISCONNECT_CAUSE);
+        verify(resultReceiver).onResult(any());
+    }
+}
\ No newline at end of file