Reject incoming call for emergency call
An emergency call fails when receiving a call at the same time.
Reject incoming call before dialing an emergency call in ringing state.
Bug: 325705941
Test: atest TelephonyConnectionServiceTest
Manual test:
1. Make a call from A to DUT.
2. Check whether the call is in alerting state in A.
3. At that moment, dial 911 in DUT before the incoming call is notified.
Change-Id: I56e3f38d9fad23ef9de99bdc6932c132f47d9bfe
diff --git a/src/com/android/services/telephony/TelephonyConnectionService.java b/src/com/android/services/telephony/TelephonyConnectionService.java
index f77e52b..67b32df 100644
--- a/src/com/android/services/telephony/TelephonyConnectionService.java
+++ b/src/com/android/services/telephony/TelephonyConnectionService.java
@@ -141,6 +141,9 @@
// existing call.
private static final int DEFAULT_DSDA_OUTGOING_CALL_HOLD_TIMEOUT_MS = 2000;
+ // 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.
+
// 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}");
@@ -706,6 +709,20 @@
}
}
+ private static class OnDisconnectListener extends
+ com.android.internal.telephony.Connection.ListenerBase {
+ private final CompletableFuture<Boolean> mFuture;
+
+ OnDisconnectListener(CompletableFuture<Boolean> future) {
+ mFuture = future;
+ }
+
+ @Override
+ public void onDisconnect(int cause) {
+ mFuture.complete(true);
+ }
+ };
+
private final DomainSelectionConnection.DomainSelectionConnectionCallback
mEmergencyDomainSelectionConnectionCallback =
new DomainSelectionConnection.DomainSelectionConnectionCallback() {
@@ -2558,8 +2575,13 @@
}
Bundle extras = request.getExtras();
extras.putInt(PhoneConstants.EXTRA_DIAL_DOMAIN, result);
- placeOutgoingConnection(request, resultConnection, phone);
- mIsEmergencyCallPending = false;
+ CompletableFuture<Void> rejectFuture = checkAndRejectIncomingCall(phone, (ret) -> {
+ if (!ret) {
+ Log.i(this, "createEmergencyConnection reject incoming call failed");
+ }
+ });
+ rejectFuture.thenRun(() -> placeEmergencyConnectionOnSelectedDomain(request,
+ resultConnection, phone));
}, mDomainSelectionMainExecutor);
}
@@ -2574,10 +2596,26 @@
Log.i(this, "dialCsEmergencyCall dialing canceled");
return;
}
- placeOutgoingConnection(request, resultConnection, phone);
+ CompletableFuture<Void> future = checkAndRejectIncomingCall(phone, (ret) -> {
+ if (!ret) {
+ Log.i(this, "dialCsEmergencyCall reject incoming call failed");
+ }
+ });
+ future.thenRun(() -> placeEmergencyConnectionOnSelectedDomain(request,
+ resultConnection, phone));
});
}
+ private void placeEmergencyConnectionOnSelectedDomain(ConnectionRequest request,
+ TelephonyConnection resultConnection, Phone phone) {
+ if (mEmergencyConnection == null) {
+ Log.i(this, "placeEmergencyConnectionOnSelectedDomain dialing canceled");
+ return;
+ }
+ placeOutgoingConnection(request, resultConnection, phone);
+ mIsEmergencyCallPending = false;
+ }
+
private void releaseEmergencyCallDomainSelection(boolean cancel, boolean isActive) {
if (mEmergencyCallDomainSelectionConnection != null) {
if (cancel) mEmergencyCallDomainSelectionConnection.cancelSelection();
@@ -2827,11 +2865,29 @@
return false;
}
- private void onEmergencyRedialOnDomain(TelephonyConnection connection,
+ private void onEmergencyRedialOnDomain(final TelephonyConnection connection,
final Phone phone, @NetworkRegistrationInfo.Domain int domain) {
Log.i(this, "onEmergencyRedialOnDomain phoneId=" + phone.getPhoneId()
+ ", domain=" + DomainSelectionService.getDomainName(domain));
+ final Bundle extras = new Bundle();
+ extras.putInt(PhoneConstants.EXTRA_DIAL_DOMAIN, domain);
+
+ CompletableFuture<Void> future = checkAndRejectIncomingCall(phone, (ret) -> {
+ if (!ret) {
+ Log.i(this, "onEmergencyRedialOnDomain reject incoming call failed");
+ }
+ });
+ future.thenRun(() -> onEmergencyRedialOnDomainInternal(connection, phone, extras));
+ }
+
+ private void onEmergencyRedialOnDomainInternal(TelephonyConnection connection,
+ Phone phone, Bundle extras) {
+ if (mEmergencyConnection == null) {
+ Log.i(this, "onEmergencyRedialOnDomainInternal dialing canceled");
+ return;
+ }
+
String number = connection.getAddress().getSchemeSpecificPart();
// Indicates undetectable emergency number with DialArgs
@@ -2840,12 +2896,9 @@
if (connection.getEmergencyServiceCategory() != null) {
isEmergency = true;
eccCategory = connection.getEmergencyServiceCategory();
- Log.i(this, "onEmergencyRedialOnDomain eccCategory=" + eccCategory);
+ Log.i(this, "onEmergencyRedialOnDomainInternal eccCategory=" + eccCategory);
}
- Bundle extras = new Bundle();
- extras.putInt(PhoneConstants.EXTRA_DIAL_DOMAIN, domain);
-
com.android.internal.telephony.Connection originalConnection =
connection.getOriginalConnection();
try {
@@ -2860,14 +2913,14 @@
connection::registerForCallEvents);
}
} catch (CallStateException e) {
- Log.e(this, e, "onEmergencyRedialOnDomain, exception: " + e);
+ Log.e(this, e, "onEmergencyRedialOnDomainInternal, exception: " + e);
onLocalHangup(connection);
connection.unregisterForCallEvents();
handleCallStateException(e, connection, phone);
return;
}
if (originalConnection == null) {
- Log.d(this, "onEmergencyRedialOnDomain, phone.dial returned null");
+ Log.d(this, "onEmergencyRedialOnDomainInternal, phone.dial returned null");
onLocalHangup(connection);
connection.setTelephonyConnectionDisconnected(
mDisconnectCauseFactory.toTelecomDisconnectCause(
@@ -3421,6 +3474,52 @@
}
/**
+ * If needed, block until an incoming call is disconnected for outgoing emergency call,
+ * or timeout expires.
+ * @param phone The Phone to reject the incoming call
+ * @param completeConsumer The consumer to call once rejecting incoming call has been
+ * completed. {@code true} result if the operation commpletes successfully, or
+ * {@code false} if the operation timed out/failed.
+ */
+ private CompletableFuture<Void> checkAndRejectIncomingCall(Phone phone,
+ Consumer<Boolean> completeConsumer) {
+ if (phone == null) {
+ // Unexpected inputs
+ Log.i(this, "checkAndRejectIncomingCall phone is null");
+ completeConsumer.accept(false);
+ return CompletableFuture.completedFuture(null);
+ }
+
+ Call ringingCall = phone.getRingingCall();
+ if (ringingCall == null || !ringingCall.isRinging()) {
+ completeConsumer.accept(true);
+ return CompletableFuture.completedFuture(null);
+ }
+ Log.i(this, "checkAndRejectIncomingCall found a ringing call");
+
+ try {
+ ringingCall.hangup();
+ CompletableFuture<Boolean> future = new CompletableFuture<>();
+ com.android.internal.telephony.Connection cn = ringingCall.getLatestConnection();
+ cn.addListener(new OnDisconnectListener(future));
+ // 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_REJECT_INCOMING_CALL_TIMEOUT_MS);
+ // Ensure that the Consumer is completed on the main thread.
+ return future.acceptEitherAsync(timeout, completeConsumer,
+ phone.getContext().getMainExecutor()).exceptionally((ex) -> {
+ Log.w(this, "checkAndRejectIncomingCall - exceptionally= " + ex);
+ return null;
+ });
+ } catch (Exception e) {
+ Log.w(this, "checkAndRejectIncomingCall - exception= " + e.getMessage());
+ completeConsumer.accept(false);
+ return CompletableFuture.completedFuture(null);
+ }
+ }
+
+ /**
* Get the Phone to use for an emergency call of the given emergency number address:
* a) If there are multiple Phones with the Subscriptions that support the emergency number
* address, and one of them is the default voice Phone, consider the default voice phone
diff --git a/tests/src/com/android/services/telephony/TelephonyConnectionServiceTest.java b/tests/src/com/android/services/telephony/TelephonyConnectionServiceTest.java
index d25cdf0..e791d3c 100644
--- a/tests/src/com/android/services/telephony/TelephonyConnectionServiceTest.java
+++ b/tests/src/com/android/services/telephony/TelephonyConnectionServiceTest.java
@@ -325,6 +325,10 @@
@After
public void tearDown() throws Exception {
+ if (mTestConnectionService != null
+ && mTestConnectionService.getEmergencyConnection() != null) {
+ mTestConnectionService.onLocalHangup(mTestConnectionService.getEmergencyConnection());
+ }
mTestConnectionService = null;
super.tearDown();
}
@@ -2288,8 +2292,7 @@
ArgumentCaptor<DialArgs> argsCaptor = ArgumentCaptor.forClass(DialArgs.class);
- Connection nc = Mockito.mock(Connection.class);
- doReturn(nc).when(mPhone0).dial(anyString(), any(), any());
+ doReturn(mInternalConnection).when(mPhone0).dial(anyString(), any(), any());
verify(mPhone0).dial(anyString(), argsCaptor.capture(), any());
DialArgs dialArgs = argsCaptor.getValue();
@@ -2317,8 +2320,7 @@
ArgumentCaptor<DialArgs> argsCaptor = ArgumentCaptor.forClass(DialArgs.class);
- Connection nc = Mockito.mock(Connection.class);
- doReturn(nc).when(mPhone0).dial(anyString(), any(), any());
+ doReturn(mInternalConnection).when(mPhone0).dial(anyString(), any(), any());
verify(mPhone0).dial(anyString(), argsCaptor.capture(), any());
DialArgs dialArgs = argsCaptor.getValue();
@@ -2352,6 +2354,108 @@
}
@Test
+ public void testDomainSelectionRejectIncoming() throws Exception {
+ setupForCallTest();
+
+ int selectedDomain = DOMAIN_CS;
+
+ setupForDialForDomainSelection(mPhone0, selectedDomain, true);
+
+ doReturn(mInternalConnection2).when(mCall).getLatestConnection();
+ doReturn(true).when(mCall).isRinging();
+ doReturn(mCall).when(mPhone0).getRingingCall();
+
+ mTestConnectionService.onCreateOutgoingConnection(PHONE_ACCOUNT_HANDLE_1,
+ createConnectionRequest(PHONE_ACCOUNT_HANDLE_1,
+ TEST_EMERGENCY_NUMBER, TELECOM_CALL_ID1));
+
+ 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(mEmergencyCallDomainSelectionConnection).createEmergencyConnection(any(), any());
+
+ android.telecom.Connection tc = connectionCaptor.getValue();
+
+ assertNotNull(tc);
+ assertEquals(TELECOM_CALL_ID1, tc.getTelecomCallId());
+ assertEquals(mTestConnectionService.getEmergencyConnection(), tc);
+
+ ArgumentCaptor<Connection.Listener> listenerCaptor =
+ ArgumentCaptor.forClass(Connection.Listener.class);
+
+ verify(mInternalConnection2).addListener(listenerCaptor.capture());
+ verify(mCall).hangup();
+ verify(mPhone0, never()).dial(anyString(), any(), any());
+
+ Connection.Listener listener = listenerCaptor.getValue();
+
+ assertNotNull(listener);
+
+ listener.onDisconnect(0);
+
+ verify(mSatelliteSOSMessageRecommender).onEmergencyCallStarted(any());
+
+ ArgumentCaptor<DialArgs> argsCaptor = ArgumentCaptor.forClass(DialArgs.class);
+
+ verify(mPhone0).dial(anyString(), argsCaptor.capture(), any());
+
+ DialArgs dialArgs = argsCaptor.getValue();
+
+ assertNotNull("DialArgs param is null", dialArgs);
+ assertNotNull("intentExtras is null", dialArgs.intentExtras);
+ assertTrue(dialArgs.intentExtras.containsKey(PhoneConstants.EXTRA_DIAL_DOMAIN));
+ assertEquals(selectedDomain,
+ dialArgs.intentExtras.getInt(PhoneConstants.EXTRA_DIAL_DOMAIN, -1));
+ }
+
+ @Test
+ public void testDomainSelectionRedialRejectIncoming() throws Exception {
+ setupForCallTest();
+
+ int preciseDisconnectCause = com.android.internal.telephony.CallFailCause.ERROR_UNSPECIFIED;
+ int disconnectCause = android.telephony.DisconnectCause.ERROR_UNSPECIFIED;
+ int selectedDomain = DOMAIN_CS;
+
+ TestTelephonyConnection c = setupForReDialForDomainSelection(
+ mPhone0, selectedDomain, preciseDisconnectCause, disconnectCause, true);
+
+ doReturn(mInternalConnection2).when(mCall).getLatestConnection();
+ doReturn(true).when(mCall).isRinging();
+ doReturn(mCall).when(mPhone0).getRingingCall();
+
+ assertTrue(mTestConnectionService.maybeReselectDomain(c, null, true,
+ android.telephony.DisconnectCause.NOT_VALID));
+ verify(mEmergencyCallDomainSelectionConnection).reselectDomain(any());
+
+ ArgumentCaptor<Connection.Listener> listenerCaptor =
+ ArgumentCaptor.forClass(Connection.Listener.class);
+
+ verify(mInternalConnection2).addListener(listenerCaptor.capture());
+ verify(mCall).hangup();
+ verify(mPhone0, never()).dial(anyString(), any(), any());
+
+ Connection.Listener listener = listenerCaptor.getValue();
+
+ assertNotNull(listener);
+
+ listener.onDisconnect(0);
+
+ ArgumentCaptor<DialArgs> argsCaptor = ArgumentCaptor.forClass(DialArgs.class);
+
+ verify(mPhone0).dial(anyString(), argsCaptor.capture(), any());
+ DialArgs dialArgs = argsCaptor.getValue();
+ assertNotNull("DialArgs param is null", dialArgs);
+ assertNotNull("intentExtras is null", dialArgs.intentExtras);
+ assertTrue(dialArgs.intentExtras.containsKey(PhoneConstants.EXTRA_DIAL_DOMAIN));
+ assertEquals(selectedDomain,
+ dialArgs.intentExtras.getInt(PhoneConstants.EXTRA_DIAL_DOMAIN, -1));
+ }
+
+ @Test
public void testDomainSelectionNormalRoutingEmergencyNumber() throws Exception {
setupForCallTest();
int selectedDomain = DOMAIN_PS;