Merge "DSDA: Hold active call when placing 2nd call" into udc-dev
diff --git a/src/com/android/services/telephony/TelephonyConnectionService.java b/src/com/android/services/telephony/TelephonyConnectionService.java
index ca38110..a04fe33 100644
--- a/src/com/android/services/telephony/TelephonyConnectionService.java
+++ b/src/com/android/services/telephony/TelephonyConnectionService.java
@@ -119,6 +119,10 @@
// Timeout before we continue with the emergency call without waiting for DDS switch response
// from the modem.
private static final int DEFAULT_DATA_SWITCH_TIMEOUT_MS = 1000;
+
+ // Timeout before we terminate the outgoing DSDA call if HOLD did not complete in time on the
+ // existing call.
+ private static final int DEFAULT_DSDA_OUTGOING_CALL_HOLD_TIMEOUT_MS = 1000;
private static final String KEY_DOMAIN_COMPARE_FEATURE_ENABLED_FLAG =
"is_domain_selection_compare_feature_enabled";
@@ -599,6 +603,39 @@
}
};
+ private static class StateHoldingListener extends
+ TelephonyConnection.TelephonyConnectionListener {
+ private final CompletableFuture<Boolean> mStateHoldingFuture;
+
+ StateHoldingListener(CompletableFuture<Boolean> future) {
+ mStateHoldingFuture = future;
+ }
+
+ @Override
+ public void onStateChanged(
+ Connection connection, @Connection.ConnectionState int state) {
+ TelephonyConnection c = (TelephonyConnection) connection;
+ if (c != null) {
+ switch (c.getState()) {
+ case Connection.STATE_HOLDING: {
+ Log.d(LOG_TAG, "Connection " + connection
+ + " changed to STATE_HOLDING!");
+ mStateHoldingFuture.complete(true);
+ c.removeTelephonyConnectionListener(this);
+ }
+ break;
+ case Connection.STATE_DISCONNECTED: {
+ Log.d(LOG_TAG, "Connection " + connection
+ + " changed to STATE_DISCONNECTED!");
+ mStateHoldingFuture.complete(false);
+ c.removeTelephonyConnectionListener(this);
+ }
+ break;
+ }
+ }
+ }
+ }
+
private final DomainSelectionConnection.DomainSelectionConnectionCallback
mEmergencyDomainSelectionConnectionCallback =
new DomainSelectionConnection.DomainSelectionConnectionCallback() {
@@ -1083,6 +1120,21 @@
}
return resultConnection;
} else {
+ if (mTelephonyManagerProxy.isConcurrentCallsPossible()) {
+ delayDialForOtherSubHold(phone, request.getAccountHandle(), (result) -> {
+ Log.d(this,
+ "onCreateOutgoingConn - delayDialForOtherSubHold result = "
+ + result);
+ if (result) {
+ placeOutgoingConnection(request, resultConnection, phone);
+ } else {
+ ((TelephonyConnection) resultConnection).hangup(
+ android.telephony.DisconnectCause.LOCAL);
+ }
+ });
+ return resultConnection;
+ }
+ // The standard case.
return placeOutgoingConnection(request, resultConnection, phone);
}
} else {
@@ -2862,6 +2914,7 @@
if (phone == null) {
// Do not block indefinitely.
completeConsumer.accept(false);
+ return;
}
try {
// Waiting for PhoneSwitcher to complete the operation.
@@ -2981,6 +3034,64 @@
return modemResultFuture;
}
+ private void addTelephonyConnectionListener(Conferenceable c,
+ TelephonyConnection.TelephonyConnectionListener listener) {
+ if (c instanceof TelephonyConnection) {
+ TelephonyConnection telephonyConnection = (TelephonyConnection) c;
+ telephonyConnection.addTelephonyConnectionListener(listener);
+ } else if (c instanceof ImsConference) {
+ ImsConference imsConference = (ImsConference) c;
+ TelephonyConnection conferenceHost =
+ (TelephonyConnection) imsConference.getConferenceHost();
+ conferenceHost.addTelephonyConnectionListener(listener);
+ } else {
+ throw new IllegalArgumentException(
+ "addTelephonyConnectionListener(): Unexpected conferenceable! " + c);
+ }
+ }
+
+ private CompletableFuture<Boolean> listenForHoldStateChanged(
+ @NonNull Conferenceable conferenceable) {
+ CompletableFuture<Boolean> future = new CompletableFuture<>();
+ final StateHoldingListener stateHoldingListener = new StateHoldingListener(future);
+ addTelephonyConnectionListener(conferenceable, stateHoldingListener);
+ return future;
+ }
+
+ // If there are any live calls on the other subscription, sends a hold request for the live call
+ // and waits for the STATE_HOLDING confirmation, to sequence the dial of the outgoing call.
+ private void delayDialForOtherSubHold(Phone phone, PhoneAccountHandle phoneAccountHandle,
+ Consumer<Boolean> completeConsumer) {
+ Conferenceable c = maybeHoldCallsOnOtherSubs(phoneAccountHandle);
+ if (c == null) {
+ // Nothing to hold.
+ completeConsumer.accept(true);
+ return;
+ }
+
+ if (phone == null) {
+ completeConsumer.accept(false);
+ return;
+ }
+
+ try {
+ // We have dispatched a 'hold' command to a live call (Connection or Conference) on the
+ // other sub. Listen to state changed events to see if this entered hold state.
+ CompletableFuture<Boolean> stateHoldingFuture = listenForHoldStateChanged(c);
+ // 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_DSDA_OUTGOING_CALL_HOLD_TIMEOUT_MS);
+ // Ensure that the Consumer is completed on the main thread.
+ stateHoldingFuture.acceptEitherAsync(timeout, completeConsumer,
+ phone.getContext().getMainExecutor());
+ } catch (Exception e) {
+ Log.w(this, "delayDialForOtherSubHold - exception= "
+ + e.getMessage());
+ completeConsumer.accept(false);
+ }
+ }
+
/**
* 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
@@ -3598,7 +3709,20 @@
});
}
- private static void onUnhold(Conferenceable conferenceable) {
+ static void onHold(Conferenceable conferenceable) {
+ if (conferenceable instanceof Connection) {
+ Connection connection = (Connection) conferenceable;
+ connection.onHold();
+ } else if (conferenceable instanceof Conference) {
+ Conference conference = (Conference) conferenceable;
+ conference.onHold();
+ } else {
+ throw new IllegalArgumentException(
+ "onHold(): Unexpected conferenceable! " + conferenceable);
+ }
+ }
+
+ static void onUnhold(Conferenceable conferenceable) {
if (conferenceable instanceof Connection) {
Connection connection = (Connection) conferenceable;
connection.onUnhold();
@@ -3606,40 +3730,28 @@
Conference conference = (Conference) conferenceable;
conference.onUnhold();
} else {
- throw new IllegalArgumentException("Unexpected conferenceable! " + conferenceable);
+ throw new IllegalArgumentException(
+ "onUnhold(): Unexpected conferenceable! " + conferenceable);
}
}
- /**
- * Where there are ongoing calls on multiple subscriptions for DSDA devices, let the 'hold'
- * button perform an unhold on the other sub's Connection or Conference. This covers for Dialer
- * apps that may not have a dedicated 'swap' button for calls across different subs.
- * @param incomingHandle The incoming {@link PhoneAccountHandle}.
- */
- public void maybeUnholdCallsOnOtherSubs(@NonNull PhoneAccountHandle incomingHandle) {
- Log.i(this, "maybeUnholdCallsOnOtherSubs: check for calls not on %s",
- incomingHandle);
- maybeUnholdCallsOnOtherSubs(getAllConnections(), getAllConferences(), incomingHandle,
- mTelephonyManagerProxy);
- }
-
- /**
- * Used by {@link #maybeUnholdCallsOnOtherSubs(PhoneAccountHandle)} to evaluate whether and on
- * which connection / conference to call onUnhold(). This method exists as a convenience so that
- * it is possible to unit test the core functionality.
+ /**
+ * Evaluates whether a connection or conference exists on subscriptions other than the one
+ * corresponding to the existing {@link PhoneAccountHandle}.
* @param connections all individual connections, including conference participants.
* @param conferences all conferences.
- * @param incomingHandle the incoming handle.
+ * @param currentHandle the existing call handle;
* @param telephonyManagerProxy the proxy to the {@link TelephonyManager} instance.
*/
- @VisibleForTesting
- public static void maybeUnholdCallsOnOtherSubs(@NonNull Collection<Connection> connections,
+ private static @Nullable Conferenceable maybeGetFirstConferenceableFromOtherSubscription(
+ @NonNull Collection<Connection> connections,
@NonNull Collection<Conference> conferences,
- @NonNull PhoneAccountHandle incomingHandle,
+ @NonNull PhoneAccountHandle currentHandle,
TelephonyManagerProxy telephonyManagerProxy) {
if (!telephonyManagerProxy.isConcurrentCallsPossible()) {
- return;
+ return null;
}
+
List<Conference> otherSubConferences = conferences.stream()
.filter(c ->
// Exclude multiendpoint calls as they're not on this device.
@@ -3647,11 +3759,10 @@
& Connection.PROPERTY_IS_EXTERNAL_CALL) == 0
// Include any conferences not on same sub as current connection.
&& !Objects.equals(c.getPhoneAccountHandle(),
- incomingHandle))
+ currentHandle))
.toList();
if (!otherSubConferences.isEmpty()) {
- onUnhold(otherSubConferences.get(0));
- return;
+ return otherSubConferences.get(0);
}
// Considers Connections (including conference participants) only if no conferences.
@@ -3661,15 +3772,91 @@
(c.getConnectionProperties() & Connection.PROPERTY_IS_EXTERNAL_CALL) == 0
// Include any calls not on same sub as current connection.
&& !Objects.equals(c.getPhoneAccountHandle(),
- incomingHandle)).toList();
+ currentHandle)).toList();
if (!otherSubConnections.isEmpty()) {
if (otherSubConnections.size() > 1) {
- Log.w(LOG_TAG, "Unexpected number of conferenceables: "
+ Log.w(LOG_TAG, "Unexpected number of connections: "
+ otherSubConnections.size() + " on other sub!");
}
- onUnhold(otherSubConnections.get(0));
+ return otherSubConnections.get(0);
}
+ return null;
+ }
+
+ /**
+ * Where there are ongoing calls on multiple subscriptions for DSDA devices, let the 'hold'
+ * button perform an unhold on the other sub's Connection or Conference. This covers for Dialer
+ * apps that may not have a dedicated 'swap' button for calls across different subs.
+ * @param currentHandle The {@link PhoneAccountHandle} of the current active voice call.
+ */
+ public void maybeUnholdCallsOnOtherSubs(
+ @NonNull PhoneAccountHandle currentHandle) {
+ Log.i(this, "maybeUnholdCallsOnOtherSubs: check for calls not on %s",
+ currentHandle);
+ maybeUnholdCallsOnOtherSubs(getAllConnections(), getAllConferences(),
+ currentHandle, mTelephonyManagerProxy);
+ }
+
+ /**
+ * Where there are ongoing calls on multiple subscriptions for DSDA devices, let the 'hold'
+ * button perform an unhold on the other sub's Connection or Conference. This is a convenience
+ * method to unit test the core functionality.
+ *
+ * @param connections all individual connections, including conference participants.
+ * @param conferences all conferences.
+ * @param currentHandle The {@link PhoneAccountHandle} of the current active call.
+ * @param telephonyManagerProxy the proxy to the {@link TelephonyManager} instance.
+ */
+ @VisibleForTesting
+ protected static void maybeUnholdCallsOnOtherSubs(@NonNull Collection<Connection> connections,
+ @NonNull Collection<Conference> conferences,
+ @NonNull PhoneAccountHandle currentHandle,
+ TelephonyManagerProxy telephonyManagerProxy) {
+ Conferenceable c = maybeGetFirstConferenceableFromOtherSubscription(
+ connections, conferences, currentHandle, telephonyManagerProxy);
+ if (c != null) {
+ onUnhold(c);
+ }
+ }
+
+ /**
+ * For DSDA devices, when an outgoing call is dialed out from the 2nd sub, holds the first call.
+ *
+ * @param outgoingHandle The outgoing {@link PhoneAccountHandle}.
+ * @return the Conferenceable representing the Connection or Conference to be held.
+ */
+ private @Nullable Conferenceable maybeHoldCallsOnOtherSubs(
+ @NonNull PhoneAccountHandle outgoingHandle) {
+ Log.i(this, "maybeHoldCallsOnOtherSubs: check for calls not on %s",
+ outgoingHandle);
+ return maybeHoldCallsOnOtherSubs(getAllConnections(), getAllConferences(),
+ outgoingHandle, mTelephonyManagerProxy);
+ }
+
+ /**
+ * For DSDA devices, when an outgoing call is dialed out from the 2nd sub, holds the first call.
+ * This is a convenience method to unit test the core functionality.
+ *
+ * @param connections all individual connections, including conference participants.
+ * @param conferences all conferences.
+ * @param outgoingHandle The outgoing {@link PhoneAccountHandle}.
+ * @param telephonyManagerProxy the proxy to the {@link TelephonyManager} instance.
+ * @return the {@link Conferenceable} representing the Connection or Conference to be held.
+ */
+ @VisibleForTesting
+ protected static @Nullable Conferenceable maybeHoldCallsOnOtherSubs(
+ @NonNull Collection<Connection> connections,
+ @NonNull Collection<Conference> conferences,
+ @NonNull PhoneAccountHandle outgoingHandle,
+ TelephonyManagerProxy telephonyManagerProxy) {
+ Conferenceable c = maybeGetFirstConferenceableFromOtherSubscription(
+ connections, conferences, outgoingHandle, telephonyManagerProxy);
+ if (c != null) {
+ onHold(c);
+ return c;
+ }
+ return null;
}
private void disconnectAllCallsOnOtherSubs (@NonNull PhoneAccountHandle handle) {
diff --git a/tests/src/com/android/services/telephony/TelephonyConnectionServiceTest.java b/tests/src/com/android/services/telephony/TelephonyConnectionServiceTest.java
index b156103..eedeeac 100644
--- a/tests/src/com/android/services/telephony/TelephonyConnectionServiceTest.java
+++ b/tests/src/com/android/services/telephony/TelephonyConnectionServiceTest.java
@@ -56,6 +56,7 @@
import android.os.Bundle;
import android.os.Handler;
import android.telecom.Conference;
+import android.telecom.Conferenceable;
import android.telecom.ConnectionRequest;
import android.telecom.DisconnectCause;
import android.telecom.PhoneAccountHandle;
@@ -122,6 +123,7 @@
public static class SimpleTelephonyConnection extends TelephonyConnection {
public boolean wasDisconnected = false;
public boolean wasUnheld = false;
+ public boolean wasHeld = false;
@Override
public TelephonyConnection cloneConnection() {
@@ -137,6 +139,11 @@
public void onUnhold() {
wasUnheld = true;
}
+
+ @Override
+ public void onHold() {
+ wasHeld = true;
+ }
}
public static class SimpleConference extends Conference {
@@ -1666,6 +1673,24 @@
}
/**
+ * For DSDA devices, placing an outgoing call on a 2nd sub will hold the existing connection on
+ * the first sub.
+ */
+ @Test
+ @SmallTest
+ public void testHoldOnOtherSubForVirtualDsdaDevice() {
+ when(mTelephonyManagerProxy.isConcurrentCallsPossible()).thenReturn(true);
+
+ ArrayList<android.telecom.Connection> tcs = new ArrayList<>();
+ SimpleTelephonyConnection tc1 = createTestConnection(SUB1_HANDLE, 0, false);
+ tcs.add(tc1);
+ Conferenceable c = TelephonyConnectionService.maybeHoldCallsOnOtherSubs(
+ tcs, new ArrayList<>(), SUB2_HANDLE, mTelephonyManagerProxy);
+ assertTrue(c.equals(tc1));
+ assertTrue(tc1.wasHeld);
+ }
+
+ /**
* Verifies that TelephonyManager is used to determine whether a connection is Emergency when
* creating an outgoing connection.
*/