Hang up active call based on emergency call domain
If AP domain selection is enabled and an emergency call is connected to
other domain during an active call, there is a problem in which the
existing active call is not disconnected.
This change is intended to allow outgoing emergency call to continue to
be processed after first disconnecting the active calls before making an
emergency call over other domain.
Bug: 391287237
Flag: com.android.internal.telephony.flags.hangup_active_call_based_on_emergency_call_domain
Test: atest TelephonyConnectionServiceTest
Test: manual (verified the active call is disconnected on other domain
first before an emergency call is being processed)
Change-Id: Id8fb96dfd8c83e1636a8b8cac5e30f04f50668e2
diff --git a/src/com/android/services/telephony/TelephonyConnectionService.java b/src/com/android/services/telephony/TelephonyConnectionService.java
index 320e8be..dac566e 100644
--- a/src/com/android/services/telephony/TelephonyConnectionService.java
+++ b/src/com/android/services/telephony/TelephonyConnectionService.java
@@ -152,6 +152,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}");
@@ -2805,11 +2809,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/tests/src/com/android/services/telephony/TelephonyConnectionServiceTest.java b/tests/src/com/android/services/telephony/TelephonyConnectionServiceTest.java
index 3710d6a..6c577a5 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
@@ -3710,6 +3724,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());