Merge "Add a new field is_multi into Satellite atoms" into main
diff --git a/src/com/android/services/telephony/TelephonyConnectionService.java b/src/com/android/services/telephony/TelephonyConnectionService.java
index 457e8f8..4cb0575 100644
--- a/src/com/android/services/telephony/TelephonyConnectionService.java
+++ b/src/com/android/services/telephony/TelephonyConnectionService.java
@@ -153,6 +153,10 @@
     // Timeout to wait for the termination of incoming call before continue with the emergency call.
     private static final int DEFAULT_REJECT_INCOMING_CALL_TIMEOUT_MS = 10 * 1000; // 10 seconds.
 
+    // Timeout to wait for ending active call on other domain before continuing with
+    // the emergency call.
+    private static final int DEFAULT_DISCONNECT_CALL_ON_OTHER_DOMAIN_TIMEOUT_MS = 2 * 1000;
+
     // If configured, reject attempts to dial numbers matching this pattern.
     private static final Pattern CDMA_ACTIVATION_CODE_REGEX_PATTERN =
             Pattern.compile("\\*228[0-9]{0,2}");
@@ -1366,7 +1370,11 @@
                     }
                     return resultConnection;
                 } else {
-                    if (mTelephonyManagerProxy.isConcurrentCallsPossible()) {
+                    // If call sequencing is enabled, Telecom will take care of holding calls across
+                    // subscriptions if needed before delegating the connection creation over to
+                    // Telephony.
+                    if (mTelephonyManagerProxy.isConcurrentCallsPossible()
+                            && !mTelecomFlags.enableCallSequencing()) {
                         Conferenceable c = maybeHoldCallsOnOtherSubs(request.getAccountHandle());
                         if (c != null) {
                             delayDialForOtherSubHold(phone, c, (success) -> {
@@ -2851,11 +2859,142 @@
                             + "reject incoming, dialing canceled");
                     return;
                 }
-                placeEmergencyConnectionOnSelectedDomain(request, resultConnection, phone);
+                // Hang up the active calls if the domain of currently active call is different
+                // from the domain selected by domain selector.
+                if (Flags.hangupActiveCallBasedOnEmergencyCallDomain()) {
+                    CompletableFuture<Void> disconnectCall = maybeDisconnectCallsOnOtherDomain(
+                            phone, resultConnection, result,
+                            getAllConnections(), getAllConferences(), (ret) -> {
+                                if (!ret) {
+                                    Log.i(this, "createEmergencyConnection: "
+                                            + "disconnecting call on other domain failed");
+                                }
+                            });
+
+                    CompletableFuture<Void> unused = disconnectCall.thenRun(() -> {
+                        if (resultConnection.getState() == Connection.STATE_DISCONNECTED) {
+                            Log.i(this, "createEmergencyConnection: "
+                                    + "disconnect call on other domain, dialing canceled");
+                            return;
+                        }
+                        placeEmergencyConnectionOnSelectedDomain(request, resultConnection, phone);
+                    });
+                } else {
+                    placeEmergencyConnectionOnSelectedDomain(request, resultConnection, phone);
+                }
             });
         }, mDomainSelectionMainExecutor);
     }
 
+    /**
+     * Disconnect the active calls on the other domain for an emergency call.
+     * For example,
+     *  - Active IMS normal call and CS emergency call
+     *  - Active CS normal call and IMS emergency call
+     *
+     * @param phone The Phone to be used for an emergency call.
+     * @param emergencyConnection The connection created for an emergency call.
+     * @param emergencyDomain The selected domain for an emergency call.
+     * @param connections All individual connections, including conference participants.
+     * @param conferences All conferences.
+     * @param completeConsumer The consumer to call once the call hangup has been completed.
+     *        {@code true} if the operation commpletes successfully, or
+     *        {@code false} if the operation timed out/failed.
+     */
+    @VisibleForTesting
+    public static CompletableFuture<Void> maybeDisconnectCallsOnOtherDomain(Phone phone,
+            Connection emergencyConnection,
+            @NetworkRegistrationInfo.Domain int emergencyDomain,
+            @NonNull Collection<Connection> connections,
+            @NonNull Collection<Conference> conferences,
+            Consumer<Boolean> completeConsumer) {
+        List<Connection> activeConnections = connections.stream()
+                .filter(c -> {
+                    return !c.equals(emergencyConnection)
+                            && isConnectionOnOtherDomain(c, phone, emergencyDomain);
+                }).toList();
+        List<Conference> activeConferences = conferences.stream()
+                .filter(c -> {
+                    Connection pc = c.getPrimaryConnection();
+                    return isConnectionOnOtherDomain(pc, phone, emergencyDomain);
+                }).toList();
+
+        if (activeConnections.isEmpty() && activeConferences.isEmpty()) {
+            // There are no active calls.
+            completeConsumer.accept(true);
+            return CompletableFuture.completedFuture(null);
+        }
+
+        Log.i(LOG_TAG, "maybeDisconnectCallsOnOtherDomain: "
+                + "connections=" + activeConnections.size()
+                + ", conferences=" + activeConferences.size());
+
+        try {
+            CompletableFuture<Boolean> future = null;
+
+            for (Connection c : activeConnections) {
+                TelephonyConnection tc = (TelephonyConnection) c;
+                if (tc.getState() != Connection.STATE_DISCONNECTED) {
+                    if (future == null) {
+                        future = new CompletableFuture<>();
+                        tc.getOriginalConnection().addListener(new OnDisconnectListener(future));
+                    }
+                    tc.hangup(android.telephony.DisconnectCause.OUTGOING_EMERGENCY_CALL_PLACED);
+                }
+            }
+
+            for (Conference c : activeConferences) {
+                if (c.getState() != Connection.STATE_DISCONNECTED) {
+                    c.onDisconnect();
+                }
+            }
+
+            if (future != null) {
+                // A timeout that will complete the future to not block the outgoing call
+                // indefinitely.
+                CompletableFuture<Boolean> timeout = new CompletableFuture<>();
+                phone.getContext().getMainThreadHandler().postDelayed(
+                        () -> timeout.complete(false),
+                        DEFAULT_DISCONNECT_CALL_ON_OTHER_DOMAIN_TIMEOUT_MS);
+                // Ensure that the Consumer is completed on the main thread.
+                return future.acceptEitherAsync(timeout, completeConsumer,
+                        phone.getContext().getMainExecutor()).exceptionally((ex) -> {
+                            Log.w(LOG_TAG, "maybeDisconnectCallsOnOtherDomain: exceptionally="
+                                    + ex);
+                            return null;
+                        });
+            } else {
+                completeConsumer.accept(true);
+                return CompletableFuture.completedFuture(null);
+            }
+        } catch (Exception e) {
+            Log.w(LOG_TAG, "maybeDisconnectCallsOnOtherDomain: exception=" + e.getMessage());
+            completeConsumer.accept(false);
+            return CompletableFuture.completedFuture(null);
+        }
+    }
+
+    private static boolean isConnectionOnOtherDomain(Connection c, Phone phone,
+            @NetworkRegistrationInfo.Domain int domain) {
+        if (c instanceof TelephonyConnection) {
+            TelephonyConnection tc = (TelephonyConnection) c;
+            Phone callPhone = tc.getPhone();
+            int callDomain = NetworkRegistrationInfo.DOMAIN_UNKNOWN;
+
+            if (callPhone != null && callPhone.getSubId() == phone.getSubId()) {
+                if (tc.isGsmCdmaConnection()) {
+                    callDomain = NetworkRegistrationInfo.DOMAIN_CS;
+                } else if (tc.isImsConnection()) {
+                    callDomain = NetworkRegistrationInfo.DOMAIN_PS;
+                }
+            }
+
+            return callDomain != NetworkRegistrationInfo.DOMAIN_UNKNOWN
+                    && callDomain != domain;
+        }
+        return false;
+    }
+
     private void dialCsEmergencyCall(final Phone phone,
             final TelephonyConnection resultConnection, final ConnectionRequest request) {
         Log.d(this, "dialCsEmergencyCall");
diff --git a/testapps/TestSatelliteApp/AndroidManifest.xml b/testapps/TestSatelliteApp/AndroidManifest.xml
index 1825df2..de455f2 100644
--- a/testapps/TestSatelliteApp/AndroidManifest.xml
+++ b/testapps/TestSatelliteApp/AndroidManifest.xml
@@ -17,7 +17,9 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.phone.testapps.satellitetestapp">
 
-    <application android:label="SatelliteTestApp">
+    <application
+        android:networkSecurityConfig="@xml/network_security_config"
+        android:label="SatelliteTestApp">
         <activity
             android:name=".SatelliteTestApp"
             android:exported="true"
@@ -55,10 +57,17 @@
                 <action android:name="android.telephony.satellite.SatelliteService" />
             </intent-filter>
         </service>
+
+        <meta-data
+            android:name="android.telephony.PROPERTY_SATELLITE_DATA_OPTIMIZED"
+            android:value="true"/>
     </application>
 
     <uses-permission android:name="android.permission.SATELLITE_COMMUNICATION" />
     <uses-permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE" />
     <uses-permission android:name="android.permission.SEND_SMS" />
     <uses-permission android:name="android.permission.BIND_SATELLITE_SERVICE" />
+    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.WRITE_SETTINGS" />
+    <uses-permission android:name="android.permission.INTERNET" />
 </manifest>
diff --git a/testapps/TestSatelliteApp/res/layout/activity_SatelliteTestApp.xml b/testapps/TestSatelliteApp/res/layout/activity_SatelliteTestApp.xml
index 26b45e3..43cce9b 100644
--- a/testapps/TestSatelliteApp/res/layout/activity_SatelliteTestApp.xml
+++ b/testapps/TestSatelliteApp/res/layout/activity_SatelliteTestApp.xml
@@ -81,5 +81,12 @@
             android:paddingStart="4dp"
             android:paddingEnd="4dp"
             android:text="@string/TestSatelliteWrapper"/>
+        <Button
+            android:id="@+id/TestSatelliteConstrainConnection"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:paddingStart="4dp"
+            android:paddingEnd="4dp"
+            android:text="@string/TestSatelliteConstrainConnection"/>
     </LinearLayout>
 </ScrollView>
diff --git a/testapps/TestSatelliteApp/res/values/donottranslate_strings.xml b/testapps/TestSatelliteApp/res/values/donottranslate_strings.xml
index 5c3a72d..f48c022 100644
--- a/testapps/TestSatelliteApp/res/values/donottranslate_strings.xml
+++ b/testapps/TestSatelliteApp/res/values/donottranslate_strings.xml
@@ -62,6 +62,8 @@
     <string name="sendMessage">sendMessage</string>
     <string name="receiveMessage">receiveMessage</string>
 
+    <string name="TestSatelliteConstrainConnection">Test Satellite Constrain Connection</string>
+
     <string name="TestSatelliteWrapper">Test Satellite Wrapper</string>
     <string name="requestNtnSignalStrength">requestNtnSignalStrength</string>
     <string name="registerForNtnSignalStrengthChanged">registerForNtnSignalStrengthChanged</string>
diff --git a/testapps/TestSatelliteApp/res/xml/network_security_config.xml b/testapps/TestSatelliteApp/res/xml/network_security_config.xml
new file mode 100644
index 0000000..463e65a
--- /dev/null
+++ b/testapps/TestSatelliteApp/res/xml/network_security_config.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+  <domain-config cleartextTrafficPermitted="true">
+    <domain includeSubdomains="true">www.google.com</domain>
+  </domain-config>
+</network-security-config>
\ No newline at end of file
diff --git a/testapps/TestSatelliteApp/src/com/android/phone/testapps/satellitetestapp/PingTask.java b/testapps/TestSatelliteApp/src/com/android/phone/testapps/satellitetestapp/PingTask.java
new file mode 100644
index 0000000..fe86c21
--- /dev/null
+++ b/testapps/TestSatelliteApp/src/com/android/phone/testapps/satellitetestapp/PingTask.java
@@ -0,0 +1,69 @@
+/*
+ * 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.phone.testapps.satellitetestapp;
+
+import android.net.Network;
+import android.os.AsyncTask;
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.Scanner;
+
+class PingTask extends AsyncTask<Network, Integer, Integer> {
+  protected Integer doInBackground(Network... network) {
+    ping(network[0]);
+    return 0;
+  }
+  String ping(Network network) {
+    URL url = null;
+    try {
+      url = new URL("http://www.google.com");
+    } catch (Exception e) {
+      Log.d("SatelliteDataConstrained", "exception: " + e);
+    }
+    if (url != null) {
+      try {
+        Log.d("SatelliteDataConstrained", "ping " + url);
+        String result = httpGet(network, url);
+        Log.d("SatelliteDataConstrained", "Ping Success");
+        return result;
+      } catch (Exception e) {
+        Log.d("SatelliteDataConstrained", "exception: " + e);
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Performs a HTTP GET to the specified URL on the specified Network, and returns
+   * the response body decoded as UTF-8.
+   */
+  private static String httpGet(Network network, URL httpUrl) throws IOException {
+    HttpURLConnection connection = (HttpURLConnection) network.openConnection(httpUrl);
+    try {
+      InputStream inputStream = connection.getInputStream();
+      Log.d("httpGet", "httpUrl + " + httpUrl);
+      Scanner scanner = new Scanner(inputStream).useDelimiter("\\A");
+      return scanner.hasNext() ? scanner.next() : "";
+    } finally {
+      connection.disconnect();
+    }
+  }
+}
diff --git a/testapps/TestSatelliteApp/src/com/android/phone/testapps/satellitetestapp/SatelliteTestApp.java b/testapps/TestSatelliteApp/src/com/android/phone/testapps/satellitetestapp/SatelliteTestApp.java
index cb56e87..911e179 100644
--- a/testapps/TestSatelliteApp/src/com/android/phone/testapps/satellitetestapp/SatelliteTestApp.java
+++ b/testapps/TestSatelliteApp/src/com/android/phone/testapps/satellitetestapp/SatelliteTestApp.java
@@ -23,15 +23,24 @@
 import android.content.Intent;
 import android.content.ServiceConnection;
 import android.content.pm.PackageManager;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
 import android.os.Bundle;
+import android.os.Looper;
 import android.os.IBinder;
 import android.telephony.satellite.stub.SatelliteDatagram;
 import android.util.Log;
 import android.view.View;
 import android.view.View.OnClickListener;
+import android.widget.Toast;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 
 /**
  * SatelliteTestApp main activity to navigate to other APIs related to satellite.
@@ -41,14 +50,23 @@
     private static final String TAG = "SatelliteTestApp";
     public static TestSatelliteService sSatelliteService;
     private final Object mSendDatagramLock = new Object();
-
+    Network mNetwork = null;
+    Context mContext;
+    ConnectivityManager mConnectivityManager;
+    NetworkCallback mSatelliteConstrainNetworkCallback;
+    private final ExecutorService executor = Executors.newSingleThreadExecutor();
     private TestSatelliteServiceConnection mSatelliteServiceConn;
     private List<SatelliteDatagram> mSentSatelliteDatagrams = new ArrayList<>();
     private static final int REQUEST_CODE_SEND_SMS = 1;
+    private final int NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED = 37;
+    private boolean isNetworkRequested = false;
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        mContext = getApplicationContext();
+
+        mConnectivityManager = getSystemService(ConnectivityManager.class);
 
         if (mSatelliteServiceConn == null) {
             mSatelliteServiceConn = new TestSatelliteServiceConnection();
@@ -106,6 +124,21 @@
                 startActivity(intent);
             }
         });
+
+      findViewById(R.id.TestSatelliteConstrainConnection).setOnClickListener(view -> {
+        executor.execute(() -> {
+          Log.e(TAG, "onClick");
+          mSatelliteConstrainNetworkCallback = new NetworkCallback() {
+            @Override
+            public void onAvailable(final Network network) {
+              makeSatelliteDataConstrainedPing(network);
+            }
+          };
+          if(isNetworkRequested == false) {
+            requestingNetwork();
+          }
+        });
+      });
     }
 
     @Override
@@ -117,6 +150,61 @@
         }
     }
 
+    @Override
+    protected void onDestroy() {
+      super.onDestroy();
+      if(isNetworkRequested == true) {
+        releasingNetwork();
+      }
+    }
+
+    private void requestingNetwork() {
+      Log.e(TAG, "Requesting Network");
+      isNetworkRequested = true;
+      NetworkRequest request = new NetworkRequest.Builder()
+          .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+          .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED)
+          .removeCapability(NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED)
+          .addTransportType(NetworkCapabilities.TRANSPORT_SATELLITE)
+          .build();
+
+      // Requesting for Network
+      mConnectivityManager.requestNetwork(request, mSatelliteConstrainNetworkCallback);
+      Log.e(TAG, "onClick + " + request);
+    }
+
+
+    private void makeSatelliteDataConstrainedPing(final Network network) {
+      Log.e(TAG, "onAvailable + " + network);
+      mNetwork = network;
+
+      try {
+        PingTask pingTask = new PingTask();
+        Log.d(TAG, "Connecting Satellite for ping");
+        String pingResult = pingTask.ping(mNetwork);
+        if(pingResult != null) {
+          Toast.makeText(mContext, "Ping Passed!", Toast.LENGTH_SHORT).show();
+        } else {
+          Toast.makeText(mContext, "Ping Failed!", Toast.LENGTH_SHORT).show();
+        }
+      } catch (Exception e) {
+        Log.d(TAG, "Exception at ping: " + e);
+      } finally {
+        // Releasing the callback in the background thread
+        releasingNetwork();
+      }
+    }
+
+    private void releasingNetwork() {
+      Log.e(TAG, "Realsing Network");
+      try {
+        mConnectivityManager
+            .unregisterNetworkCallback(mSatelliteConstrainNetworkCallback);
+      } catch (Exception e) {
+        Log.d("SatelliteDataConstrined", "Exception: " + e);
+      }
+      isNetworkRequested = false;
+    }
 
     private final ILocalSatelliteListener mSatelliteListener =
             new ILocalSatelliteListener.Stub() {
diff --git a/tests/src/com/android/TelephonyTestBase.java b/tests/src/com/android/TelephonyTestBase.java
index 5e7cdda..d8c3727 100644
--- a/tests/src/com/android/TelephonyTestBase.java
+++ b/tests/src/com/android/TelephonyTestBase.java
@@ -24,7 +24,9 @@
 import android.content.ContextWrapper;
 import android.content.res.Resources;
 import android.os.Handler;
+import android.os.HandlerThread;
 import android.os.Looper;
+import android.os.TestLooperManager;
 import android.util.Log;
 
 import androidx.test.InstrumentationRegistry;
@@ -70,6 +72,10 @@
     @Mock protected DataNetworkController mDataNetworkController;
     @Mock private MetricsCollector mMetricsCollector;
 
+    private HandlerThread mTestHandlerThread;
+    protected Looper mTestLooper;
+    protected TestLooperManager mLooperManager;
+
     private final HashMap<InstanceKey, Object> mOldInstances = new HashMap<>();
     private final LinkedList<InstanceKey> mInstanceKeys = new LinkedList<>();
 
@@ -118,9 +124,47 @@
     public void tearDown() throws Exception {
         // Ensure there are no static references to handlers after test completes.
         PhoneConfigurationManager.unregisterAllMultiSimConfigChangeRegistrants();
+        cleanupTestLooper();
         restoreInstances();
     }
 
+    protected void setupTestLooper() {
+        mTestHandlerThread = new HandlerThread("TestHandlerThread");
+        mTestHandlerThread.start();
+        mTestLooper = mTestHandlerThread.getLooper();
+        mLooperManager = new TestLooperManager(mTestLooper);
+    }
+
+    private void cleanupTestLooper() {
+        mTestLooper = null;
+        if (mLooperManager != null) {
+            mLooperManager.release();
+            mLooperManager = null;
+        }
+        if (mTestHandlerThread != null) {
+            mTestHandlerThread.quit();
+            try {
+                mTestHandlerThread.join();
+            } catch (InterruptedException ex) {
+                Log.w("TelephonyTestBase", "HandlerThread join interrupted", ex);
+            }
+            mTestHandlerThread = null;
+        }
+    }
+
+    protected void processOneMessage() {
+        var msg = mLooperManager.next();
+        mLooperManager.execute(msg);
+        mLooperManager.recycle(msg);
+    }
+
+    protected void processAllMessages() {
+        for (var msg = mLooperManager.poll(); msg != null && msg.getTarget() != null;) {
+            mLooperManager.execute(msg);
+            mLooperManager.recycle(msg);
+        }
+    }
+
     protected final boolean waitForExecutorAction(Executor executor, long timeoutMillis) {
         final CountDownLatch lock = new CountDownLatch(1);
         executor.execute(() -> {
diff --git a/tests/src/com/android/phone/CarrierConfigLoaderTest.java b/tests/src/com/android/phone/CarrierConfigLoaderTest.java
index 324bcbc..5b306e6 100644
--- a/tests/src/com/android/phone/CarrierConfigLoaderTest.java
+++ b/tests/src/com/android/phone/CarrierConfigLoaderTest.java
@@ -52,7 +52,6 @@
 import android.telephony.TelephonyManager;
 import android.telephony.TelephonyRegistryManager;
 import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
 
 import androidx.test.InstrumentationRegistry;
 
@@ -82,7 +81,6 @@
  * Unit Test for CarrierConfigLoader.
  */
 @RunWith(AndroidTestingRunner.class)
-@TestableLooper.RunWithLooper(setAsMainLooper = true)
 public class CarrierConfigLoaderTest extends TelephonyTestBase {
     @Rule
     public TestRule compatChangeRule = new PlatformCompatChangeRule();
@@ -109,7 +107,6 @@
     private TelephonyManager mTelephonyManager;
     private CarrierConfigLoader mCarrierConfigLoader;
     private Handler mHandler;
-    private TestableLooper mTestableLooper;
 
     // The AIDL stub will use PermissionEnforcer to check permission from the caller.
     private FakePermissionEnforcer mFakePermissionEnforcer = new FakePermissionEnforcer();
@@ -117,6 +114,7 @@
     @Before
     public void setUp() throws Exception {
         super.setUp();
+        setupTestLooper();
         doReturn(true).when(mPackageManager).hasSystemFeature(
                 eq(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION));
         doReturn(Context.PERMISSION_ENFORCER_SERVICE).when(mContext).getSystemServiceName(
@@ -152,8 +150,7 @@
         when(mContext.getSystemService(TelephonyRegistryManager.class)).thenReturn(
                 mTelephonyRegistryManager);
 
-        mTestableLooper = TestableLooper.get(this);
-        mCarrierConfigLoader = new CarrierConfigLoader(mContext, mTestableLooper.getLooper(),
+        mCarrierConfigLoader = new CarrierConfigLoader(mContext, mTestLooper,
                 mFeatureFlags);
         mHandler = mCarrierConfigLoader.getHandler();
 
@@ -213,7 +210,10 @@
         mCarrierConfigLoader.saveNoSimConfigToXml(PLATFORM_CARRIER_CONFIG_PACKAGE, config);
         mCarrierConfigLoader.updateConfigForPhoneId(DEFAULT_PHONE_ID,
                 IccCardConstants.INTENT_VALUE_ICC_ABSENT);
-        mTestableLooper.processAllMessages();
+        processOneMessage();
+        processOneMessage();
+        processOneMessage();
+        processOneMessage();
 
         assertThat(mCarrierConfigLoader.getConfigFromDefaultApp(DEFAULT_PHONE_ID)).isNull();
         assertThat(mCarrierConfigLoader.getConfigFromCarrierApp(DEFAULT_PHONE_ID)).isNull();
@@ -252,7 +252,7 @@
                 DEFAULT_PHONE_ID, carrierId, config);
         mCarrierConfigLoader.updateConfigForPhoneId(DEFAULT_PHONE_ID,
                 IccCardConstants.INTENT_VALUE_ICC_LOADED);
-        mTestableLooper.processAllMessages();
+        processAllMessages();
 
         assertThat(mCarrierConfigLoader.getConfigFromDefaultApp(DEFAULT_PHONE_ID).getInt(
                 CARRIER_CONFIG_EXAMPLE_KEY)).isEqualTo(CARRIER_CONFIG_EXAMPLE_VALUE);
@@ -294,7 +294,8 @@
 
         mCarrierConfigLoader.overrideConfig(DEFAULT_SUB_ID, null /*overrides*/,
                 false/*persistent*/);
-        mTestableLooper.processAllMessages();
+        processOneMessage();
+        processOneMessage();
 
         assertThat(mCarrierConfigLoader.getOverrideConfig(DEFAULT_PHONE_ID).isEmpty()).isTrue();
         verify(mSubscriptionManagerService).updateSubscriptionByCarrierConfig(
@@ -316,7 +317,8 @@
         PersistableBundle config = getTestConfig();
         mCarrierConfigLoader.overrideConfig(DEFAULT_SUB_ID, config /*overrides*/,
                 false/*persistent*/);
-        mTestableLooper.processAllMessages();
+        processOneMessage();
+        processOneMessage();
 
         assertThat(mCarrierConfigLoader.getOverrideConfig(DEFAULT_PHONE_ID).getInt(
                 CARRIER_CONFIG_EXAMPLE_KEY)).isEqualTo(CARRIER_CONFIG_EXAMPLE_VALUE);
@@ -480,10 +482,10 @@
                 any(Intent.class), any(ServiceConnection.class), anyInt());
         doNothing().when(mContext).sendBroadcastAsUser(any(Intent.class), any(UserHandle.class));
         mHandler.sendMessage(mHandler.obtainMessage(17 /* EVENT_MULTI_SIM_CONFIG_CHANGED */));
-        mTestableLooper.processAllMessages();
+        processAllMessages();
 
         mCarrierConfigLoader.updateConfigForPhoneId(1, IccCardConstants.INTENT_VALUE_ICC_ABSENT);
-        mTestableLooper.processAllMessages();
+        processAllMessages();
     }
 
     @Test
@@ -503,18 +505,20 @@
         Mockito.clearInvocations(mTelephonyRegistryManager);
         Mockito.clearInvocations(mContext);
         mHandler.sendMessage(mHandler.obtainMessage(13 /* EVENT_SYSTEM_UNLOCKED */));
-        mTestableLooper.processAllMessages();
+        processOneMessage();
         mHandler.sendMessage(mHandler.obtainMessage(5 /* EVENT_FETCH_DEFAULT_DONE */));
-        mTestableLooper.processAllMessages();
+        processOneMessage();
+        processOneMessage();
         mHandler.sendMessage(mHandler.obtainMessage(6 /* EVENT_FETCH_CARRIER_DONE */));
-        mTestableLooper.processAllMessages();
+        processOneMessage();
+        processOneMessage();
 
         ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class);
         verify(mSubscriptionManagerService).updateSubscriptionByCarrierConfig(eq(0), anyString(),
                 any(PersistableBundle.class), runnableCaptor.capture());
 
         runnableCaptor.getValue().run();
-        mTestableLooper.processAllMessages();
+        processAllMessages();
 
         // Broadcast should be sent for backwards compatibility.
         verify(mContext).sendBroadcastAsUser(any(Intent.class), any(UserHandle.class));
diff --git a/tests/src/com/android/phone/ImsStateCallbackControllerTest.java b/tests/src/com/android/phone/ImsStateCallbackControllerTest.java
index 5521ac0..0c91af7 100644
--- a/tests/src/com/android/phone/ImsStateCallbackControllerTest.java
+++ b/tests/src/com/android/phone/ImsStateCallbackControllerTest.java
@@ -985,7 +985,9 @@
         when(mSubscriptionManager.getActiveSubscriptionIdList()).thenReturn(subIds);
     }
 
-    private void processAllMessages() {
+    // Override - not using mTestLooper from the base class
+    @Override
+    protected void processAllMessages() {
         while (!mLooper.getLooper().getQueue().isIdle()) {
             mLooper.processAllMessages();
         }
diff --git a/tests/src/com/android/phone/PhoneInterfaceManagerTest.java b/tests/src/com/android/phone/PhoneInterfaceManagerTest.java
index 88b84a7..266481e 100644
--- a/tests/src/com/android/phone/PhoneInterfaceManagerTest.java
+++ b/tests/src/com/android/phone/PhoneInterfaceManagerTest.java
@@ -59,6 +59,7 @@
 import com.android.internal.telephony.Phone;
 import com.android.internal.telephony.RILConstants;
 import com.android.internal.telephony.flags.FeatureFlags;
+import com.android.internal.telephony.satellite.SatelliteController;
 import com.android.internal.telephony.subscription.SubscriptionManagerService;
 import com.android.phone.satellite.accesscontrol.SatelliteAccessController;
 
@@ -118,6 +119,9 @@
         replaceInstance(SatelliteAccessController.class, "sInstance", null,
                 Mockito.mock(SatelliteAccessController.class));
 
+        replaceInstance(SatelliteController.class, "sInstance", null,
+                Mockito.mock(SatelliteController.class));
+
         mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(
                 InstrumentationRegistry.getInstrumentation().getTargetContext());
         doReturn(mSharedPreferences).when(mPhoneGlobals)
diff --git a/tests/src/com/android/phone/slice/SlicePurchaseControllerTest.java b/tests/src/com/android/phone/slice/SlicePurchaseControllerTest.java
index 5637c3a..a792780 100644
--- a/tests/src/com/android/phone/slice/SlicePurchaseControllerTest.java
+++ b/tests/src/com/android/phone/slice/SlicePurchaseControllerTest.java
@@ -21,6 +21,7 @@
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyBoolean;
 import static org.mockito.Mockito.anyInt;
@@ -40,6 +41,7 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
 import android.os.AsyncResult;
 import android.os.Handler;
 import android.os.HandlerThread;
@@ -56,6 +58,7 @@
 import android.telephony.data.UrspRule;
 import android.testing.TestableLooper;
 
+import androidx.test.InstrumentationRegistry;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 
 import com.android.TelephonyTestBase;
@@ -335,6 +338,12 @@
         mSlicePurchaseController.purchasePremiumCapability(
                 TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY, mHandler.obtainMessage());
         mTestableLooper.processAllMessages();
+        if (isAutomotive()) {
+            // TODO(b/401032628): this test is flaky here
+            assumeTrue(
+                    TelephonyManager.PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_NOT_AVAILABLE
+                    != mResult);
+        }
         assertEquals(
                 TelephonyManager.PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA_SUBSCRIPTION,
                 mResult);
@@ -350,6 +359,11 @@
                 mResult);
     }
 
+    private boolean isAutomotive() {
+        return InstrumentationRegistry.getTargetContext().getPackageManager()
+                .hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
+    }
+
     @Test
     public void testPurchasePremiumCapabilityResultNetworkNotAvailable() {
         doReturn((int) TelephonyManager.NETWORK_TYPE_BITMASK_NR).when(mPhone)
diff --git a/tests/src/com/android/services/telephony/TelephonyConnectionServiceTest.java b/tests/src/com/android/services/telephony/TelephonyConnectionServiceTest.java
index e27bea5..2605114 100644
--- a/tests/src/com/android/services/telephony/TelephonyConnectionServiceTest.java
+++ b/tests/src/com/android/services/telephony/TelephonyConnectionServiceTest.java
@@ -109,6 +109,8 @@
 import com.android.internal.telephony.flags.Flags;
 import com.android.internal.telephony.gsm.SuppServiceNotification;
 import com.android.internal.telephony.imsphone.ImsPhone;
+import com.android.internal.telephony.imsphone.ImsPhoneCall;
+import com.android.internal.telephony.imsphone.ImsPhoneConnection;
 import com.android.internal.telephony.satellite.SatelliteController;
 import com.android.internal.telephony.satellite.SatelliteSOSMessageRecommender;
 import com.android.internal.telephony.subscription.SubscriptionInfoInternal;
@@ -197,9 +199,15 @@
         public void onHold() {
             wasHeld = true;
         }
+
+        @Override
+        void setOriginalConnection(com.android.internal.telephony.Connection connection) {
+            mOriginalConnection = connection;
+        }
     }
 
     public static class SimpleConference extends Conference {
+        public boolean wasDisconnected = false;
         public boolean wasUnheld = false;
 
         public SimpleConference(PhoneAccountHandle phoneAccountHandle) {
@@ -207,6 +215,11 @@
         }
 
         @Override
+        public void onDisconnect() {
+            wasDisconnected = true;
+        }
+
+        @Override
         public void onUnhold() {
             wasUnheld = true;
         }
@@ -340,6 +353,7 @@
         mBinderStub = (IConnectionService.Stub) mTestConnectionService.onBind(null);
         mSetFlagsRule.enableFlags(Flags.FLAG_DO_NOT_OVERRIDE_PRECISE_LABEL);
         mSetFlagsRule.enableFlags(Flags.FLAG_CALL_EXTRA_FOR_NON_HOLD_SUPPORTED_CARRIERS);
+        mSetFlagsRule.disableFlags(Flags.FLAG_HANGUP_ACTIVE_CALL_BASED_ON_EMERGENCY_CALL_DOMAIN);
     }
 
     @After
@@ -3739,6 +3753,234 @@
     }
 
     @Test
+    public void testDomainSelectionAddCsEmergencyCallWhenImsCallActive() throws Exception {
+        mSetFlagsRule.enableFlags(Flags.FLAG_HANGUP_ACTIVE_CALL_BASED_ON_EMERGENCY_CALL_DOMAIN);
+
+        setupForCallTest();
+        doReturn(1).when(mPhone0).getSubId();
+        doReturn(1).when(mImsPhone).getSubId();
+        ImsPhoneCall imsPhoneCall = Mockito.mock(ImsPhoneCall.class);
+        ImsPhoneConnection imsPhoneConnection = Mockito.mock(ImsPhoneConnection.class);
+        when(imsPhoneCall.getPhone()).thenReturn(mImsPhone);
+        when(imsPhoneConnection.getCall()).thenReturn(imsPhoneCall);
+        when(imsPhoneConnection.getPhoneType()).thenReturn(PhoneConstants.PHONE_TYPE_IMS);
+
+        // PROPERTY_IS_EXTERNAL_CALL: to avoid extra processing that is not related to this test.
+        SimpleTelephonyConnection tc1 = createTestConnection(PHONE_ACCOUNT_HANDLE_1,
+                android.telecom.Connection.PROPERTY_IS_EXTERNAL_CALL, false);
+        // IMS connection is set.
+        tc1.setOriginalConnection(imsPhoneConnection);
+        mTestConnectionService.addExistingConnection(PHONE_ACCOUNT_HANDLE_1, tc1);
+
+        assertEquals(1, mTestConnectionService.getAllConnections().size());
+        TelephonyConnection connection1 = (TelephonyConnection)
+                mTestConnectionService.getAllConnections().toArray()[0];
+        assertEquals(tc1, connection1);
+
+        // Add a CS emergency call.
+        String telecomCallId2 = "TC2";
+        int selectedDomain = DOMAIN_CS;
+        setupForDialForDomainSelection(mPhone0, selectedDomain, true);
+        getTestContext().getCarrierConfig(0 /*subId*/).putBoolean(
+                CarrierConfigManager.KEY_ALLOW_HOLD_CALL_DURING_EMERGENCY_BOOL, true);
+
+        mTestConnectionService.onCreateOutgoingConnection(PHONE_ACCOUNT_HANDLE_1,
+                createConnectionRequest(PHONE_ACCOUNT_HANDLE_1,
+                        TEST_EMERGENCY_NUMBER, telecomCallId2));
+
+        // Hang up the active IMS call due to CS emergency call.
+        ArgumentCaptor<Connection.Listener> listenerCaptor =
+                ArgumentCaptor.forClass(Connection.Listener.class);
+        verify(imsPhoneConnection).addListener(listenerCaptor.capture());
+        assertTrue(tc1.wasDisconnected);
+
+        // Call disconnection completed.
+        Connection.Listener listener = listenerCaptor.getValue();
+        assertNotNull(listener);
+        listener.onDisconnect(0);
+
+        // Continue to proceed the outgoing emergency call after active call is disconnected.
+        ArgumentCaptor<android.telecom.Connection> connectionCaptor =
+                ArgumentCaptor.forClass(android.telecom.Connection.class);
+        verify(mDomainSelectionResolver)
+                .getDomainSelectionConnection(eq(mPhone0), eq(SELECTOR_TYPE_CALLING), eq(true));
+        verify(mEmergencyStateTracker)
+                .startEmergencyCall(eq(mPhone0), connectionCaptor.capture(), eq(false));
+        verify(mSatelliteSOSMessageRecommender, times(2))
+                .onEmergencyCallStarted(any(), anyBoolean());
+        verify(mEmergencyCallDomainSelectionConnection).createEmergencyConnection(any(), any());
+        verify(mPhone0).dial(anyString(), any(), any());
+
+        android.telecom.Connection tc = connectionCaptor.getValue();
+        assertNotNull(tc);
+        assertEquals(telecomCallId2, tc.getTelecomCallId());
+        assertEquals(mTestConnectionService.getEmergencyConnection(), tc);
+    }
+
+    @Test
+    public void testDomainSelectionAddImsEmergencyCallWhenCsCallActive() throws Exception {
+        mSetFlagsRule.enableFlags(Flags.FLAG_HANGUP_ACTIVE_CALL_BASED_ON_EMERGENCY_CALL_DOMAIN);
+
+        setupForCallTest();
+
+        // PROPERTY_IS_EXTERNAL_CALL: to avoid extra processing that is not related to this test.
+        SimpleTelephonyConnection tc1 = createTestConnection(PHONE_ACCOUNT_HANDLE_1,
+                android.telecom.Connection.PROPERTY_IS_EXTERNAL_CALL, false);
+        // CS connection is set.
+        tc1.setOriginalConnection(mInternalConnection);
+        mTestConnectionService.addExistingConnection(PHONE_ACCOUNT_HANDLE_1, tc1);
+
+        assertEquals(1, mTestConnectionService.getAllConnections().size());
+        TelephonyConnection connection1 = (TelephonyConnection)
+                mTestConnectionService.getAllConnections().toArray()[0];
+        assertEquals(tc1, connection1);
+
+        // Add an IMS emergency call.
+        String telecomCallId2 = "TC2";
+        int selectedDomain = DOMAIN_PS;
+        setupForDialForDomainSelection(mPhone0, selectedDomain, true);
+        getTestContext().getCarrierConfig(0 /*subId*/).putBoolean(
+                CarrierConfigManager.KEY_ALLOW_HOLD_CALL_DURING_EMERGENCY_BOOL, true);
+
+        mTestConnectionService.onCreateOutgoingConnection(PHONE_ACCOUNT_HANDLE_1,
+                createConnectionRequest(PHONE_ACCOUNT_HANDLE_1,
+                        TEST_EMERGENCY_NUMBER, telecomCallId2));
+
+        // Hang up the active CS call due to IMS emergency call.
+        ArgumentCaptor<Connection.Listener> listenerCaptor =
+                ArgumentCaptor.forClass(Connection.Listener.class);
+        verify(mInternalConnection).addListener(listenerCaptor.capture());
+        assertTrue(tc1.wasDisconnected);
+
+        // Call disconnection completed.
+        Connection.Listener listener = listenerCaptor.getValue();
+        assertNotNull(listener);
+        listener.onDisconnect(0);
+
+        // Continue to proceed the outgoing emergency call after active call is disconnected.
+        ArgumentCaptor<android.telecom.Connection> connectionCaptor =
+                ArgumentCaptor.forClass(android.telecom.Connection.class);
+        verify(mDomainSelectionResolver)
+                .getDomainSelectionConnection(eq(mPhone0), eq(SELECTOR_TYPE_CALLING), eq(true));
+        verify(mEmergencyStateTracker)
+                .startEmergencyCall(eq(mPhone0), connectionCaptor.capture(), eq(false));
+        verify(mSatelliteSOSMessageRecommender, times(2))
+                .onEmergencyCallStarted(any(), anyBoolean());
+        verify(mEmergencyCallDomainSelectionConnection).createEmergencyConnection(any(), any());
+        verify(mPhone0).dial(anyString(), any(), any());
+
+        android.telecom.Connection tc = connectionCaptor.getValue();
+        assertNotNull(tc);
+        assertEquals(telecomCallId2, tc.getTelecomCallId());
+        assertEquals(mTestConnectionService.getEmergencyConnection(), tc);
+    }
+
+    @Test
+    @SmallTest
+    public void testDomainSelectionMaybeDisconnectCallsOnOtherDomainWhenNoActiveCalls() {
+        SimpleTelephonyConnection ec = createTestConnection(PHONE_ACCOUNT_HANDLE_1, 0, true);
+        Consumer<Boolean> consumer = (result) -> {
+            if (!result) {
+                fail("Unexpected result=" + result);
+            }
+        };
+        CompletableFuture<Void> unused =
+                TelephonyConnectionService.maybeDisconnectCallsOnOtherDomain(mPhone0,
+                        ec, DOMAIN_PS, Collections.emptyList(), Collections.emptyList(), consumer);
+
+        assertTrue(unused.isDone());
+    }
+
+    @Test
+    @SmallTest
+    public void testDomainSelectionMaybeDisconnectCallsOnOtherDomainWhenConferenceOnly() {
+        setupForCallTest();
+        ArrayList<android.telecom.Conference> conferences = new ArrayList<>();
+        SimpleTelephonyConnection tc1 = createTestConnection(PHONE_ACCOUNT_HANDLE_1, 0, false);
+        SimpleConference conference = createTestConference(PHONE_ACCOUNT_HANDLE_1, 0);
+        tc1.setOriginalConnection(mInternalConnection);
+        conference.addConnection(tc1);
+        conferences.add(conference);
+
+        SimpleTelephonyConnection ec = createTestConnection(PHONE_ACCOUNT_HANDLE_1, 0, true);
+        Consumer<Boolean> consumer = (result) -> {
+            if (!result) {
+                fail("Unexpected result=" + result);
+            }
+        };
+        CompletableFuture<Void> unused =
+                TelephonyConnectionService.maybeDisconnectCallsOnOtherDomain(
+                        mPhone0, ec, DOMAIN_PS, Collections.emptyList(), conferences, consumer);
+
+        assertTrue(unused.isDone());
+        assertTrue(conference.wasDisconnected);
+    }
+
+    @Test
+    @SmallTest
+    public void testDomainSelectionMaybeDisconnectCallsOnOtherDomainWhenActiveCall() {
+        setupForCallTest();
+        ArrayList<android.telecom.Connection> connections = new ArrayList<>();
+        ArrayList<android.telecom.Conference> conferences = new ArrayList<>();
+        SimpleTelephonyConnection tc1 = createTestConnection(PHONE_ACCOUNT_HANDLE_1, 0, false);
+        SimpleConference conference = createTestConference(PHONE_ACCOUNT_HANDLE_1, 0);
+        tc1.setOriginalConnection(mInternalConnection);
+        connections.add(tc1);
+        conference.addConnection(tc1);
+        conferences.add(conference);
+
+        SimpleTelephonyConnection ec = createTestConnection(PHONE_ACCOUNT_HANDLE_1, 0, true);
+        Consumer<Boolean> consumer = (result) -> {
+            if (!result) {
+                fail("Unexpected result=" + result);
+            }
+        };
+        CompletableFuture<Void> unused =
+                TelephonyConnectionService.maybeDisconnectCallsOnOtherDomain(
+                        mPhone0, ec, DOMAIN_PS, connections, conferences, consumer);
+
+        assertFalse(unused.isDone());
+        assertTrue(tc1.wasDisconnected);
+        assertTrue(conference.wasDisconnected);
+
+        ArgumentCaptor<Connection.Listener> listenerCaptor =
+                ArgumentCaptor.forClass(Connection.Listener.class);
+        verify(mInternalConnection).addListener(listenerCaptor.capture());
+
+        // Call disconnection completed.
+        Connection.Listener listener = listenerCaptor.getValue();
+        assertNotNull(listener);
+        listener.onDisconnect(0);
+
+        assertTrue(unused.isDone());
+    }
+
+    @Test
+    @SmallTest
+    public void testDomainSelectionMaybeDisconnectCallsOnOtherDomainWhenExceptionOccurs() {
+        setupForCallTest();
+        ArrayList<android.telecom.Connection> connections = new ArrayList<>();
+        SimpleTelephonyConnection tc1 = createTestConnection(PHONE_ACCOUNT_HANDLE_1, 0, false);
+        tc1.setOriginalConnection(mInternalConnection);
+        connections.add(tc1);
+        doThrow(new NullPointerException("Intended: Connection is null"))
+                .when(mInternalConnection).addListener(any());
+
+        SimpleTelephonyConnection ec = createTestConnection(PHONE_ACCOUNT_HANDLE_1, 0, true);
+        Consumer<Boolean> consumer = (result) -> {
+            if (result) {
+                fail("Unexpected result=" + result);
+            }
+        };
+        CompletableFuture<Void> unused =
+                TelephonyConnectionService.maybeDisconnectCallsOnOtherDomain(
+                        mPhone0, ec, DOMAIN_PS, connections, Collections.emptyList(), consumer);
+
+        assertTrue(unused.isDone());
+        assertFalse(tc1.wasDisconnected);
+    }
+
+    @Test
     public void testDomainSelectionWithMmiCode() {
         //UT domain selection should not be handled by new domain selector.
         doNothing().when(mContext).startActivityAsUser(any(), any());