Merge "Resolve cross account user icon validation." into main
diff --git a/res/values-kn/strings.xml b/res/values-kn/strings.xml
index 886ccdf..da7fef8 100644
--- a/res/values-kn/strings.xml
+++ b/res/values-kn/strings.xml
@@ -54,13 +54,13 @@
     <string name="no_vm_number_msg" msgid="1339245731058529388">"ಸಿಮ್‌ ಕಾರ್ಡ್‌ನಲ್ಲಿ ಯಾವುದೇ ಧ್ವನಿಮೇಲ್‌ ಸಂಖ್ಯೆಯನ್ನು ಸಂಗ್ರಹಿಸಿಲ್ಲ."</string>
     <string name="add_vm_number_str" msgid="5179510133063168998">"ಸಂಖ್ಯೆಯನ್ನು ಸೇರಿಸಿ"</string>
     <string name="change_default_dialer_dialog_title" msgid="5861469279421508060">"<xliff:g id="NEW_APP">%s</xliff:g> ಅನ್ನು ನಿಮ್ಮ ಡಿಫಾಲ್ಟ್ ಫೋನ್ ಆ್ಯಪ್ ಆಗಿ ಮಾಡಬೇಕೆ?"</string>
-    <string name="change_default_dialer_dialog_affirmative" msgid="8604665314757739550">"ಡಿಫಾಲ್ಟ್ ಹೊಂದಿಸಿ"</string>
+    <string name="change_default_dialer_dialog_affirmative" msgid="8604665314757739550">"ಡಿಫಾಲ್ಟ್ ಸೆಟ್ ಮಾಡಿ"</string>
     <string name="change_default_dialer_dialog_negative" msgid="8648669840052697821">"ರದ್ದುಮಾಡಿ"</string>
     <string name="change_default_dialer_warning_message" msgid="8461963987376916114">"<xliff:g id="NEW_APP">%s</xliff:g> ಗೆ ನಿಮ್ಮ ಕರೆಗಳ ಎಲ್ಲಾ ಅಂಶಗಳನ್ನು ನಿಯಂತ್ರಿಸಲು ಮತ್ತು ಕರೆಗಳನ್ನು ಮಾಡಲು ಸಾಧ್ಯವಾಗುತ್ತದೆ. ನೀವು ವಿಶ್ವಾಸವಿರಿಸಿರುವಂತಹ ಆ್ಯಪ್‌ಗಳನ್ನು ಮಾತ್ರ ನಿಮ್ಮ ಡಿಫಾಲ್ಟ್ ಆ್ಯಪ್‌ ಆಗಿ ಹೊಂದಿಸಬೇಕು."</string>
     <string name="change_default_call_screening_dialog_title" msgid="5365787219927262408">"<xliff:g id="NEW_APP">%s</xliff:g> ನಿಮ್ಮ ಡೀಫಾಲ್ಟ್ ಕರೆ ಸ್ಕ್ರೀನಿಂಗ್ ಆ್ಯಪ್‌ ಆಗಿ ಮಾಡಬೇಕೇ?"</string>
     <string name="change_default_call_screening_warning_message_for_disable_old_app" msgid="2039830033533243164">"<xliff:g id="OLD_APP">%s</xliff:g> ಇನ್ನು ಮುಂದೆ ಕರೆಗಳನ್ನು ಸ್ಕ್ರೀನ್‌ ಮಾಡಲು ಸಾಧ್ಯವಾಗುವುದಿಲ್ಲ."</string>
     <string name="change_default_call_screening_warning_message" msgid="9020537562292754269">"<xliff:g id="NEW_APP">%s</xliff:g> ಗೆ ನಿಮ್ಮ ಸಂಪರ್ಕಗಳಲ್ಲಿ ಇಲ್ಲದ ಕರೆದಾರರ ಬಗ್ಗೆ ಮಾಹಿತಿಯನ್ನು ನೋಡಲು ಮತ್ತು ಈ ಕರೆಗಳನ್ನು ಬ್ಲಾಕ್ ಮಾಡಲು ಸಾಧ್ಯವಾಗುತ್ತದೆ. ನೀವು ವಿಶ್ವಾಸವಿರಿಸಿರುವಂತಹ ಆ್ಯಪ್‌ಗಳನ್ನು ಮಾತ್ರ ನಿಮ್ಮ ಡೀಫಾಲ್ಟ್ ಕರೆ ಸ್ಕ್ರೀನಿಂಗ್ ಆ್ಯಪ್‌ ಆಗಿ ಹೊಂದಿಸಬೇಕು."</string>
-    <string name="change_default_call_screening_dialog_affirmative" msgid="7162433828280058647">"ಡೀಫಾಲ್ಟ್ ಹೊಂದಿಸಿ"</string>
+    <string name="change_default_call_screening_dialog_affirmative" msgid="7162433828280058647">"ಡೀಫಾಲ್ಟ್ ಸೆಟ್ ಮಾಡಿ"</string>
     <string name="change_default_call_screening_dialog_negative" msgid="1839266125623106342">"ರದ್ದುಮಾಡಿ"</string>
     <string name="blocked_numbers" msgid="8322134197039865180">"ನಿರ್ಬಂಧಿಸಲಾದ ಸಂಖ್ಯೆಗಳು"</string>
     <string name="blocked_numbers_msg" msgid="2797422132329662697">"ನಿರ್ಬಂಧಿಸಲಾದ ಸಂಖ್ಯೆಗಳಿಂದ ಕರೆಗಳು ಅಥವಾ ಪಠ್ಯ ಸಂದೇಶಗಳನ್ನು ನೀವು ಸ್ವೀಕರಿಸುವುದಿಲ್ಲ."</string>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index aefd2e6..195bf97 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -417,4 +417,10 @@
          Call streaming is a feature where a user can see and interact with a call from another
          device like a tablet while the call takes place on their phone. -->
     <string name="call_streaming_notification_action_switch_here">Switch here</string>
+    <!-- In-call screen: error message shown when the user attempts to place a call, but calling has
+         been disabled using a debug property. -->
+    <string name="callFailed_too_many_calls">Cannot place a call as there are already two calls in progress. Disconnect one of the calls or merge them into a conference prior to placing a new call.</string>
+    <!-- In-call screen: error message shown when the user attempts to place a call, but the live
+         call cannot be held. -->
+    <string name="callFailed_unholdable_call">Cannot place a call as there is an unholdable call. Disconnect the call prior to placing a new call.</string>
 </resources>
diff --git a/src/com/android/server/telecom/CallAudioRouteController.java b/src/com/android/server/telecom/CallAudioRouteController.java
index 495f872..76da5ce 100644
--- a/src/com/android/server/telecom/CallAudioRouteController.java
+++ b/src/com/android/server/telecom/CallAudioRouteController.java
@@ -1520,15 +1520,16 @@
 
     private boolean isLeAudioNonLeadDeviceOrServiceUnavailable(@AudioRoute.AudioRouteType int type,
             BluetoothDevice device) {
+        BluetoothLeAudio leAudioService = getLeAudioService();
         if (type != AudioRoute.TYPE_BLUETOOTH_LE) {
             return false;
-        } else if (getLeAudioService() == null) {
+        } else if (leAudioService == null) {
             return true;
         }
 
-        int groupId = getLeAudioService().getGroupId(device);
+        int groupId = leAudioService.getGroupId(device);
         if (groupId != BluetoothLeAudio.GROUP_ID_INVALID) {
-            BluetoothDevice leadDevice = getLeAudioService().getConnectedGroupLeadDevice(groupId);
+            BluetoothDevice leadDevice = leAudioService.getConnectedGroupLeadDevice(groupId);
             Log.i(this, "Lead device for device (%s) is %s.", device, leadDevice);
             return leadDevice == null || !device.getAddress().equals(leadDevice.getAddress());
         }
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index 712c6a9..af4a56a 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -3875,7 +3875,7 @@
         return isRttModeSettingOn && !shouldIgnoreRttModeSetting;
     }
 
-    private PersistableBundle getCarrierConfigForPhoneAccount(PhoneAccountHandle handle) {
+    public PersistableBundle getCarrierConfigForPhoneAccount(PhoneAccountHandle handle) {
         int subscriptionId = mPhoneAccountRegistrar.getSubscriptionIdForPhoneAccount(handle);
         CarrierConfigManager carrierConfigManager =
                 mContext.getSystemService(CarrierConfigManager.class);
diff --git a/src/com/android/server/telecom/ConnectionServiceWrapper.java b/src/com/android/server/telecom/ConnectionServiceWrapper.java
index 260c238..f2becbb 100644
--- a/src/com/android/server/telecom/ConnectionServiceWrapper.java
+++ b/src/com/android/server/telecom/ConnectionServiceWrapper.java
@@ -95,7 +95,8 @@
         ConnectionServiceFocusManager.ConnectionServiceFocus, CallSourceService {
 
     /**
-     * Anomaly Report UUIDs and corresponding error descriptions specific to CallsManager.
+     * Anomaly Report UUIDs and corresponding error descriptions specific to
+     * ConnectionServiceWrapper.
      */
     public static final UUID CREATE_CONNECTION_TIMEOUT_ERROR_UUID =
             UUID.fromString("54b7203d-a79f-4cbd-b639-85cd93a39cbb");
@@ -105,6 +106,10 @@
             UUID.fromString("caafe5ea-2472-4c61-b2d8-acb9d47e13dd");
     public static final String CREATE_CONFERENCE_TIMEOUT_ERROR_MSG =
             "Timeout expired before Telecom conference was created.";
+    public static final UUID NULL_SCHEDULED_EXECUTOR_ERROR_UUID =
+            UUID.fromString("af6b293b-239f-4ccf-bf3a-db212594e29d");
+    public static final String NULL_SCHEDULED_EXECUTOR_ERROR_MSG =
+            "Scheduled executor is null when creating connection/conference.";
 
     private static final String TELECOM_ABBREVIATION = "cast";
     private static final long SERVICE_BINDING_TIMEOUT = 15000L;
@@ -1655,11 +1660,18 @@
                         }
                     }
                 };
-                // Post cleanup to the executor service and cache the future, so we can cancel it if
-                // needed.
-                ScheduledFuture<?> future = mScheduledExecutor.schedule(r.getRunnableToCancel(),
-                        SERVICE_BINDING_TIMEOUT, TimeUnit.MILLISECONDS);
-                mScheduledFutureMap.put(call, future);
+                if (mScheduledExecutor != null) {
+                    // Post cleanup to the executor service and cache the future,
+                    // so we can cancel it if needed.
+                    ScheduledFuture<?> future = mScheduledExecutor.schedule(
+                        r.getRunnableToCancel(),SERVICE_BINDING_TIMEOUT, TimeUnit.MILLISECONDS);
+                    mScheduledFutureMap.put(call, future);
+                } else {
+                    Log.w(this, "createConference: Scheduled executor is null");
+                    mAnomalyReporter.reportAnomaly(
+                        NULL_SCHEDULED_EXECUTOR_ERROR_UUID,
+                        NULL_SCHEDULED_EXECUTOR_ERROR_MSG);
+                }
                 try {
                     mServiceInterface.createConference(
                             call.getConnectionManagerPhoneAccount(),
@@ -1784,11 +1796,18 @@
                         }
                     }
                 };
-                // Post cleanup to the executor service and cache the future, so we can cancel it if
-                // needed.
-                ScheduledFuture<?> future = mScheduledExecutor.schedule(r.getRunnableToCancel(),
-                        SERVICE_BINDING_TIMEOUT, TimeUnit.MILLISECONDS);
-                mScheduledFutureMap.put(call, future);
+                if (mScheduledExecutor != null) {
+                    // Post cleanup to the executor service and cache the future,
+                    // so we can cancel it if needed.
+                    ScheduledFuture<?> future = mScheduledExecutor.schedule(
+                        r.getRunnableToCancel(),SERVICE_BINDING_TIMEOUT, TimeUnit.MILLISECONDS);
+                    mScheduledFutureMap.put(call, future);
+                } else {
+                    Log.w(this, "createConnection: Scheduled executor is null");
+                    mAnomalyReporter.reportAnomaly(
+                        NULL_SCHEDULED_EXECUTOR_ERROR_UUID,
+                        NULL_SCHEDULED_EXECUTOR_ERROR_MSG);
+                }
                 try {
                     if (mFlags.cswServiceInterfaceIsNull() && mServiceInterface == null) {
                         if (mFlags.dontTimeoutDestroyedCalls()) {
diff --git a/src/com/android/server/telecom/UserUtil.java b/src/com/android/server/telecom/UserUtil.java
index 57906d4..8c124c8 100644
--- a/src/com/android/server/telecom/UserUtil.java
+++ b/src/com/android/server/telecom/UserUtil.java
@@ -35,15 +35,28 @@
     private UserUtil() {
     }
 
+    private static final String LOG_TAG = "UserUtil";
+
     private static UserInfo getUserInfoFromUserHandle(Context context, UserHandle userHandle) {
         UserManager userManager = context.getSystemService(UserManager.class);
         return userManager.getUserInfo(userHandle.getIdentifier());
     }
 
+    private static UserManager getUserManagerFromUserHandle(Context context,
+            UserHandle userHandle) {
+        UserManager userManager = null;
+        try {
+            userManager = context.createContextAsUser(userHandle, 0)
+                    .getSystemService(UserManager.class);
+        } catch (IllegalStateException e) {
+            Log.e(LOG_TAG, e, "Error while creating context as user = " + userHandle);
+        }
+        return userManager;
+    }
+
     public static boolean isManagedProfile(Context context, UserHandle userHandle,
             FeatureFlags featureFlags) {
-        UserManager userManager = context.createContextAsUser(userHandle, 0)
-                .getSystemService(UserManager.class);
+        UserManager userManager = getUserManagerFromUserHandle(context, userHandle);
         UserInfo userInfo = getUserInfoFromUserHandle(context, userHandle);
         return featureFlags.telecomResolveHiddenDependencies()
                 ? userManager != null && userManager.isManagedProfile()
@@ -51,15 +64,13 @@
     }
 
     public static boolean isPrivateProfile(UserHandle userHandle, Context context) {
-        UserManager um = context.createContextAsUser(userHandle, 0).getSystemService(
-                UserManager.class);
+        UserManager um = getUserManagerFromUserHandle(context, userHandle);
         return um != null && um.isPrivateProfile();
     }
 
     public static boolean isProfile(Context context, UserHandle userHandle,
             FeatureFlags featureFlags) {
-        UserManager userManager = context.createContextAsUser(userHandle, 0)
-                .getSystemService(UserManager.class);
+        UserManager userManager = getUserManagerFromUserHandle(context, userHandle);
         UserInfo userInfo = getUserInfoFromUserHandle(context, userHandle);
         return featureFlags.telecomResolveHiddenDependencies()
                 ? userManager != null && userManager.isProfile()
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
index 550a815..27f7f96 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
@@ -167,6 +167,12 @@
                             mLocalLog.log(logString);
                             return;
                         }
+                        if (mBluetoothLeAudioService == null) {
+                            logString += ", but leAudio service is unavailable";
+                            Log.i(BluetoothDeviceManager.this, logString);
+                            mLocalLog.log(logString);
+                            return;
+                        }
                         try {
                             mLeAudioCallbackRegistered = true;
                             mBluetoothLeAudioService.registerCallback(
diff --git a/src/com/android/server/telecom/callredirection/CallRedirectionProcessor.java b/src/com/android/server/telecom/callredirection/CallRedirectionProcessor.java
index 05e73d5..15b8aa9 100644
--- a/src/com/android/server/telecom/callredirection/CallRedirectionProcessor.java
+++ b/src/com/android/server/telecom/callredirection/CallRedirectionProcessor.java
@@ -133,6 +133,14 @@
                             + mServiceType + " call redirection service");
                 }
             }
+            Log.i(this, "notifyTimeout: call redirection has timed out so "
+                    + "unbinding the connection");
+            if (mConnection != null) {
+                // We still need to call unbind even if the service disconnected.
+                mContext.unbindService(mConnection);
+                mConnection = null;
+            }
+            mService = null;
         }
 
         private class CallRedirectionServiceConnection implements ServiceConnection {
diff --git a/src/com/android/server/telecom/callsequencing/CallSequencingController.java b/src/com/android/server/telecom/callsequencing/CallSequencingController.java
index f0aa8ef..a6744e5 100644
--- a/src/com/android/server/telecom/callsequencing/CallSequencingController.java
+++ b/src/com/android/server/telecom/callsequencing/CallSequencingController.java
@@ -21,7 +21,9 @@
 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.OUTGOING_CALL_STATES;
+import static com.android.server.telecom.UserUtil.showErrorDialogForRestrictedOutgoingCall;
 
+import android.annotation.NonNull;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
@@ -32,22 +34,27 @@
 import android.os.OutcomeReceiver;
 import android.telecom.CallAttributes;
 import android.telecom.CallException;
+import android.telecom.Connection;
 import android.telecom.DisconnectCause;
 import android.telecom.Log;
 import android.telecom.PhoneAccount;
 import android.telecom.PhoneAccountHandle;
 import android.telephony.AnomalyReporter;
+import android.telephony.CarrierConfigManager;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallState;
 import com.android.server.telecom.CallsManager;
 import com.android.server.telecom.LogUtils;
 import com.android.server.telecom.LoggedHandlerExecutor;
+import com.android.server.telecom.R;
 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.stats.CallFailureCause;
 
+import java.util.Objects;
 import java.util.concurrent.CompletableFuture;
 
 /**
@@ -63,6 +70,7 @@
     private final Context mContext;
     private final FeatureFlags mFeatureFlags;
     private boolean mProcessingCallSequencing;
+    private static String TAG = CallSequencingController.class.getSimpleName();
 
     public CallSequencingController(CallsManager callsManager, Context context,
             FeatureFlags featureFlags) {
@@ -257,18 +265,18 @@
                 Call heldCall = mCallsManager.getFirstCallWithState(CallState.ON_HOLD);
                 CompletableFuture<Boolean> disconnectFutureHandler = null;
                 // Assume default case (no sequencing required).
-                boolean areIncomingHeldFromSameSource;
+                boolean areIncomingHeldFromSamePhoneAccount;
 
                 if (heldCall != null) {
                     processCallSequencing(heldCall, activeCall);
                     processCallSequencing(call, heldCall);
-                    areIncomingHeldFromSameSource = CallsManager.areFromSameSource(call, heldCall);
+                    areIncomingHeldFromSamePhoneAccount = arePhoneAccountsSame(call, heldCall);
 
                     // If the calls are from the same source or the incoming call isn't a VOIP call
                     // and the held call is a carrier call, then disconnect the held call. The
                     // idea is that if we have a held carrier call and the incoming call is a
                     // VOIP call, we don't want to force the carrier call to auto-disconnect).
-                    if (areIncomingHeldFromSameSource || !(call.isSelfManaged()
+                    if (areIncomingHeldFromSamePhoneAccount || !(call.isSelfManaged()
                             && !heldCall.isSelfManaged())) {
                         disconnectFutureHandler = heldCall.disconnect();
                         Log.i(this, "holdActiveCallForNewCallWithSequencing: "
@@ -351,27 +359,27 @@
         if (activeCall != null && !activeCall.isLocallyDisconnecting()) {
             activeCallId = activeCall.getId();
             // Determine whether the calls are placed on different phone accounts.
-            boolean areFromSameSource = CallsManager.areFromSameSource(activeCall, call);
+            boolean areFromSamePhoneAccount = arePhoneAccountsSame(activeCall, call);
             processCallSequencing(activeCall, call);
-            boolean canHoldActiveCall = mCallsManager.canHold(activeCall);
+            boolean canSwapCalls = canSwap(activeCall, call);
 
             // If the active + held call are from different phone accounts, ensure that the call
             // sequencing states are verified at each step.
-            if (canHoldActiveCall) {
+            if (canSwapCalls) {
                 unholdCallFutureHandler = activeCall.hold("Swap to " + call.getId());
                 Log.addEvent(activeCall, LogUtils.Events.SWAP, "To " + call.getId());
                 Log.addEvent(call, LogUtils.Events.SWAP, "From " + activeCallId);
             } else {
-                if (!areFromSameSource) {
+                if (!areFromSamePhoneAccount) {
                     // Don't unhold the call as requested if the active and held call are on
                     // different phone accounts - consider the WhatsApp (held) and PSTN (active)
                     // case. We also don't want to drop an emergency call.
                     if (!activeCall.isEmergencyCall()) {
-                        Log.w(this, "unholdCall: % and %s are using different phone accounts. "
+                        Log.w(this, "unholdCall: %s and %s are using different phone accounts. "
                                         + "Aborting swap to %s", activeCallId, call.getId(),
                                 call.getId());
                     } else {
-                        Log.w(this, "unholdCall: % is an emergency call, aborting swap to %s",
+                        Log.w(this, "unholdCall: %s is an emergency call, aborting swap to %s",
                                 activeCallId, call.getId());
                     }
                     return;
@@ -546,9 +554,12 @@
         }
 
         // If we have the max number of held managed calls and we're placing an emergency call,
-        // we'll disconnect the ongoing call if it cannot be held.
+        // we'll disconnect the ongoing call if it cannot be held. If we have a self-managed call
+        // that can't be held, then we should disconnect the call in favor of the emergency call.
+        // Likewise, if there's only one active managed call which can't be held, then it should
+        // also be disconnected.
         if (mCallsManager.hasMaximumManagedHoldingCalls(emergencyCall)
-                && !mCallsManager.canHold(liveCall)) {
+                || !mCallsManager.canHold(liveCall)) {
             emergencyCall.getAnalytics().setCallIsAdditional(true);
             liveCall.getAnalytics().setCallIsInterrupted(true);
             // Disconnect the active call instead of the holding call because it is historically
@@ -625,18 +636,16 @@
             }
         }
 
-        // First thing, if we are trying to make an emergency call with the same package name as
-        // the live call, then allow it so that the connection service can make its own decision
-        // about how to handle the new call relative to the current one.
-        // By default, for telephony, it will try to hold the existing call before placing the new
-        // emergency call except for if the carrier does not support holding calls for emergency.
-        // In this case, telephony will disconnect the call.
+        // If we are trying to make an emergency call with the same package name as
+        // the live call, then attempt to hold the call if the carrier config supports holding
+        // emergency calls. Otherwise, disconnect the live call in order to make room for the
+        // emergency call.
         if (PhoneAccountHandle.areFromSamePackage(liveCallPhoneAccount,
                 emergencyCall.getTargetPhoneAccount())) {
-            Log.i(this, "makeRoomForOutgoingEmergencyCall: phoneAccount matches.");
-            emergencyCall.getAnalytics().setCallIsAdditional(true);
-            liveCall.getAnalytics().setCallIsInterrupted(true);
-            return CompletableFuture.completedFuture(true);
+            Log.i(this, "makeRoomForOutgoingEmergencyCall: phoneAccounts are from same "
+                    + "package. Attempting to hold live call before placing emergency call.");
+            return maybeHoldLiveCallForEmergency(ringingCallFuture, liveCall, emergencyCall,
+                    shouldHoldForEmergencyCall(liveCallPhoneAccount) /* shouldHoldForEmergency */);
         } else if (emergencyCall.getTargetPhoneAccount() == null) {
             // Without a phone account, we can't say reliably that the call will fail.
             // If the user chooses the same phone account as the live call, then it's
@@ -650,12 +659,25 @@
         // Hold the live call if possible before attempting the new outgoing emergency call.
         if (mCallsManager.canHold(liveCall)) {
             Log.i(this, "makeRoomForOutgoingEmergencyCall: holding live call.");
-            emergencyCall.getAnalytics().setCallIsAdditional(true);
-            emergencyCall.increaseHeldByThisCallCount();
-            liveCall.getAnalytics().setCallIsInterrupted(true);
-            final String holdReason = "calling " + emergencyCall.getId();
+            return maybeHoldLiveCallForEmergency(ringingCallFuture, 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);
+    }
+
+    private CompletableFuture<Boolean> maybeHoldLiveCallForEmergency(
+            CompletableFuture<Boolean> ringingCallFuture, 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 && isProcessingCallSequencing()) {
-                return ringingCallFuture.thenComposeAsync((result) -> {
+                holdResultFuture = ringingCallFuture.thenComposeAsync((result) -> {
                     if (result) {
                         Log.i(this, "makeRoomForOutgoingEmergencyCall: Request to disconnect "
                                 + "ringing call succeeded. Attempting to hold live call.");
@@ -668,13 +690,33 @@
                 }, new LoggedHandlerExecutor(mHandler, "CSC.mRFOEC",
                         mCallsManager.getLock()));
             } else {
+                emergencyCall.increaseHeldByThisCallCount();
                 return liveCall.hold(holdReason);
             }
         }
+        return holdResultFuture.thenComposeAsync((result) -> {
+            if (!result) {
+                Log.i(this, "makeRoomForOutgoingEmergencyCall: Attempt to hold live call "
+                        + "failed. Disconnecting live call in favor of emergency call.");
+                return liveCall.disconnect("Disconnecting live call which failed to be held");
+            } else {
+                Log.i(this, "makeRoomForOutgoingEmergencyCall: Attempt to hold live call "
+                        + "transaction succeeded.");
+                emergencyCall.increaseHeldByThisCallCount();
+                return CompletableFuture.completedFuture(true);
+            }
+        }, new LoggedHandlerExecutor(mHandler, "CSC.mRFOEC", mCallsManager.getLock()));
+    }
 
-        // 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);
+    /**
+     * Checks the carrier config to see if the carrier supports holding emergency calls.
+     * @param handle The {@code PhoneAccountHandle} to check
+     * @return {@code true} if the carrier supports holding emergency calls, {@code} false
+     *         otherwise.
+     */
+    private boolean shouldHoldForEmergencyCall(PhoneAccountHandle handle) {
+        return mCallsManager.getCarrierConfigForPhoneAccount(handle).getBoolean(
+                CarrierConfigManager.KEY_ALLOW_HOLD_CALL_DURING_EMERGENCY_BOOL, true);
     }
 
     /**
@@ -726,29 +768,7 @@
                     liveCallPhoneAccount);
         }
 
-        // First thing, for managed calls, if we are trying to make a call with the same phone
-        // account as the live call, then allow it so that the connection service can make its own
-        // decision about how to handle the new call relative to the current one.
-        // Note: This behavior is primarily in place because Telephony historically manages the
-        // state of the calls it tracks by itself, holding and unholding as needed.  Self-managed
-        // calls, even though from the same package are normally held/unheld automatically by
-        // Telecom.  Calls within a single ConnectionService get held/unheld automatically during
-        // "swap" operations by CallsManager#holdActiveCallForNewCall.  There is, however, a quirk
-        // in that if an app declares TWO different ConnectionServices, holdActiveCallForNewCall
-        // would not work correctly because focus switches between ConnectionServices, yet we
-        // tended to assume that if the calls are from the same package that the hold/unhold should
-        // be done by the app.  That was a bad assumption as it meant that we could have two active
-        // calls.
-        // TODO(b/280826075): We need to come back and revisit all this logic in a holistic manner.
-        if (PhoneAccountHandle.areFromSamePackage(liveCallPhoneAccount,
-                call.getTargetPhoneAccount())
-                && !call.isSelfManaged()
-                && !liveCall.isSelfManaged()) {
-            Log.i(this, "makeRoomForOutgoingCall: managed phoneAccount matches");
-            call.getAnalytics().setCallIsAdditional(true);
-            liveCall.getAnalytics().setCallIsInterrupted(true);
-            return CompletableFuture.completedFuture(true);
-        } else if (call.getTargetPhoneAccount() == null) {
+        if (call.getTargetPhoneAccount() == null) {
             // Without a phone account, we can't say reliably that the call will fail.
             // If the user chooses the same phone account as the live call, then it's
             // still possible that the call can be made (like with CDMA calls not supporting
@@ -767,7 +787,19 @@
         }
 
         // The live call cannot be held so we're out of luck here.  There's no room.
-        call.setStartFailCause(CallFailureCause.CANNOT_HOLD_CALL);
+        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);
         return CompletableFuture.completedFuture(false);
     }
 
@@ -807,13 +839,31 @@
      * mProcessingCallSequencing field if they aren't in order to signal that sequencing is
      * required to verify the call state changes.
      */
-    private void processCallSequencing(Call call1, Call call2) {
-        boolean areCallsFromSameSource = CallsManager.areFromSameSource(call1, call2);
-        if (!areCallsFromSameSource) {
+    private void processCallSequencing(@NonNull Call call1, @NonNull Call call2) {
+        boolean areCallsFromSamePhoneAccount = arePhoneAccountsSame(call1, call2);
+        if (!areCallsFromSamePhoneAccount) {
             setProcessingCallSequencing(true);
         }
     }
 
+    private boolean arePhoneAccountsSame(@NonNull Call call1, @NonNull Call call2) {
+        return Objects.equals(call1.getTargetPhoneAccount(), call2.getTargetPhoneAccount());
+    }
+
+    /**
+     * Checks to see if two calls can be swapped. This is granted that the call to be unheld is
+     * already ON_HOLD and the active call supports holding. Note that in HoldTracker, there can
+     * only be one top call that is holdable (if there are two, the calls are not holdable) and only
+     * that connection would have the CAPABILITY_HOLD present. For swapping logic, we should take
+     * this into account and request to hold regardless.
+     */
+    @VisibleForTesting
+    public boolean canSwap(Call callToBeHeld, Call callToUnhold) {
+        return callToBeHeld.can(Connection.CAPABILITY_SUPPORT_HOLD)
+                && callToBeHeld.getState() != CallState.DIALING
+                && callToUnhold.getState() == CallState.ON_HOLD;
+    }
+
     public boolean isProcessingCallSequencing() {
         return mProcessingCallSequencing;
     }
diff --git a/tests/src/com/android/server/telecom/tests/CallRedirectionProcessorTest.java b/tests/src/com/android/server/telecom/tests/CallRedirectionProcessorTest.java
index 241216a..185c08f 100644
--- a/tests/src/com/android/server/telecom/tests/CallRedirectionProcessorTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallRedirectionProcessorTest.java
@@ -221,6 +221,9 @@
         verify(mCallsManager, times(1)).onCallRedirectionComplete(eq(mCall), any(),
                 eq(mPhoneAccountHandle), eq(null), eq(SPEAKER_PHONE_ON), eq(VIDEO_STATE),
                 eq(false), eq(CallRedirectionProcessor.UI_TYPE_NO_ACTION));
+        // Verify service was unbound
+        verify(mContext, times(1)).
+                unbindService(any(ServiceConnection.class));
     }
 
     @Test
@@ -249,6 +252,9 @@
         verify(mCallsManager, times(1)).onCallRedirectionComplete(eq(mCall), any(),
                 eq(mPhoneAccountHandle), eq(null), eq(SPEAKER_PHONE_ON), eq(VIDEO_STATE),
                 eq(true), eq(CallRedirectionProcessor.UI_TYPE_USER_DEFINED_TIMEOUT));
+        // Verify service was unbound
+        verify(mContext, times(1)).
+                unbindService(any(ServiceConnection.class));
     }
 
     @Test
@@ -280,6 +286,9 @@
         verify(mCallsManager, times(1)).onCallRedirectionComplete(eq(mCall), any(),
                 eq(mPhoneAccountHandle), eq(null), eq(SPEAKER_PHONE_ON), eq(VIDEO_STATE),
                 eq(true), eq(CallRedirectionProcessor.UI_TYPE_USER_DEFINED_TIMEOUT));
+        // Verify service was unbound
+        verify(mContext, times(1)).
+                unbindService(any(ServiceConnection.class));
 
         // Wait for another carrier timeout time, but should not expect any carrier service request
         // is triggered.
@@ -289,6 +298,9 @@
         verify(mCallsManager, times(1)).onCallRedirectionComplete(eq(mCall), any(),
                 eq(mPhoneAccountHandle), eq(null), eq(SPEAKER_PHONE_ON), eq(VIDEO_STATE),
                 eq(true), eq(CallRedirectionProcessor.UI_TYPE_USER_DEFINED_TIMEOUT));
+        // Verify service was unbound
+        verify(mContext, times(1)).
+                unbindService(any(ServiceConnection.class));
     }
 
     @Test