DSDA: Unhold other call on holding 1st call.
- When there is a performHold() on a call on 1 sub, try to unhold
all connections and conferences on the other sub.
- Effectively constitutes a 'swap' functionality, but from the telephony
layer instead of the Dialer layer.
Bug:240169164
Test: TelephonyConnectionTest
Live test on Pixel 7: receiving 2 calls on different subs.
Change-Id: I88912fd042daa3aff103133d67fac48d904e3993
diff --git a/src/com/android/services/telephony/TelephonyConnection.java b/src/com/android/services/telephony/TelephonyConnection.java
index e278240..f381f11 100644
--- a/src/com/android/services/telephony/TelephonyConnection.java
+++ b/src/com/android/services/telephony/TelephonyConnection.java
@@ -1326,6 +1326,8 @@
if (phone.getPhoneType() == PhoneConstants.PHONE_TYPE_IMS) {
ImsPhone imsPhone = (ImsPhone) phone;
imsPhone.holdActiveCall();
+ mTelephonyConnectionService.maybeUnholdCallsOnOtherSubs(
+ getPhoneAccountHandle());
return;
}
phone.switchHoldingAndActive();
diff --git a/src/com/android/services/telephony/TelephonyConnectionService.java b/src/com/android/services/telephony/TelephonyConnectionService.java
index 6c4a832..8cd0b13 100644
--- a/src/com/android/services/telephony/TelephonyConnectionService.java
+++ b/src/com/android/services/telephony/TelephonyConnectionService.java
@@ -34,6 +34,7 @@
import android.os.ParcelUuid;
import android.provider.DeviceConfig;
import android.telecom.Conference;
+import android.telecom.Conferenceable;
import android.telecom.Connection;
import android.telecom.ConnectionRequest;
import android.telecom.ConnectionService;
@@ -137,6 +138,12 @@
}
@Override
public void addConference(ImsConference mImsConference) {
+ Connection conferenceHost = mImsConference.getConferenceHost();
+ if (conferenceHost instanceof TelephonyConnection) {
+ TelephonyConnection tcConferenceHost = (TelephonyConnection) conferenceHost;
+ tcConferenceHost.setTelephonyConnectionService(TelephonyConnectionService.this);
+ tcConferenceHost.setPhoneAccountHandle(mImsConference.getPhoneAccountHandle());
+ }
TelephonyConnectionService.this.addTelephonyConference(mImsConference);
}
@Override
@@ -3578,6 +3585,80 @@
});
}
+ private static void onUnhold(Conferenceable conferenceable) {
+ if (conferenceable instanceof Connection) {
+ Connection connection = (Connection) conferenceable;
+ connection.onUnhold();
+ } else if (conferenceable instanceof Conference) {
+ Conference conference = (Conference) conferenceable;
+ conference.onUnhold();
+ } else {
+ throw new IllegalArgumentException("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.
+ * @param connections all individual connections, including conference participants.
+ * @param conferences all conferences.
+ * @param incomingHandle the incoming handle.
+ * @param telephonyManagerProxy the proxy to the {@link TelephonyManager} instance.
+ */
+ @VisibleForTesting
+ public static void maybeUnholdCallsOnOtherSubs(@NonNull Collection<Connection> connections,
+ @NonNull Collection<Conference> conferences,
+ @NonNull PhoneAccountHandle incomingHandle,
+ TelephonyManagerProxy telephonyManagerProxy) {
+ if (!telephonyManagerProxy.isConcurrentCallsPossible()) {
+ return;
+ }
+ List<Conference> otherSubConferences = conferences.stream()
+ .filter(c ->
+ // Exclude multiendpoint calls as they're not on this device.
+ (c.getConnectionProperties()
+ & Connection.PROPERTY_IS_EXTERNAL_CALL) == 0
+ // Include any conferences not on same sub as current connection.
+ && !Objects.equals(c.getPhoneAccountHandle(),
+ incomingHandle))
+ .toList();
+ if (!otherSubConferences.isEmpty()) {
+ onUnhold(otherSubConferences.get(0));
+ return;
+ }
+
+ // Considers Connections (including conference participants) only if no conferences.
+ List<Connection> otherSubConnections = connections.stream()
+ .filter(c ->
+ // Exclude multiendpoint calls as they're not on this device.
+ (c.getConnectionProperties() & Connection.PROPERTY_IS_EXTERNAL_CALL) == 0
+ // Include any calls not on same sub as current connection.
+ && !Objects.equals(c.getPhoneAccountHandle(),
+ incomingHandle)).toList();
+
+ if (!otherSubConnections.isEmpty()) {
+ if (otherSubConnections.size() > 1) {
+ Log.w(LOG_TAG, "Unexpected number of conferenceables: "
+ + otherSubConnections.size() + " on other sub!");
+ }
+ onUnhold(otherSubConnections.get(0));
+ }
+ }
+
private void disconnectAllCallsOnOtherSubs (@NonNull PhoneAccountHandle handle) {
Collection<Connection>connections = getAllConnections();
connections.stream()
diff --git a/tests/src/com/android/services/telephony/TelephonyConnectionServiceTest.java b/tests/src/com/android/services/telephony/TelephonyConnectionServiceTest.java
index 734d7e3..b156103 100644
--- a/tests/src/com/android/services/telephony/TelephonyConnectionServiceTest.java
+++ b/tests/src/com/android/services/telephony/TelephonyConnectionServiceTest.java
@@ -55,6 +55,7 @@
import android.os.AsyncResult;
import android.os.Bundle;
import android.os.Handler;
+import android.telecom.Conference;
import android.telecom.ConnectionRequest;
import android.telecom.DisconnectCause;
import android.telecom.PhoneAccountHandle;
@@ -100,6 +101,8 @@
import org.mockito.Mockito;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -118,6 +121,7 @@
*/
public static class SimpleTelephonyConnection extends TelephonyConnection {
public boolean wasDisconnected = false;
+ public boolean wasUnheld = false;
@Override
public TelephonyConnection cloneConnection() {
@@ -128,6 +132,24 @@
public void hangup(int telephonyDisconnectCode) {
wasDisconnected = true;
}
+
+ @Override
+ public void onUnhold() {
+ wasUnheld = true;
+ }
+ }
+
+ public static class SimpleConference extends Conference {
+ public boolean wasUnheld = false;
+
+ public SimpleConference(PhoneAccountHandle phoneAccountHandle) {
+ super(phoneAccountHandle);
+ }
+
+ @Override
+ public void onUnhold() {
+ wasUnheld = true;
+ }
}
private static final long TIMEOUT_MS = 100;
@@ -1582,6 +1604,67 @@
assertFalse(tc1.wasDisconnected);
}
+
+ /**
+ * For calls on the same sub, the Dialer implements the 'swap' functionality to perform hold and
+ * unhold, so we do not additionally unhold when 'hold' button is pressed.
+ */
+ @Test
+ @SmallTest
+ public void testDontUnholdOnSameSubForVirtualDsdaDevice() {
+ when(mTelephonyManagerProxy.isConcurrentCallsPossible()).thenReturn(true);
+
+ ArrayList<android.telecom.Connection> tcs = new ArrayList<>();
+ Collection<Conference> conferences = new ArrayList<>();
+ SimpleTelephonyConnection tc1 = createTestConnection(SUB1_HANDLE, 0, false);
+ tcs.add(tc1);
+ TelephonyConnectionService.maybeUnholdCallsOnOtherSubs(
+ tcs, conferences, SUB1_HANDLE, mTelephonyManagerProxy);
+ assertFalse(tc1.wasUnheld);
+ }
+
+ /**
+ * Triggering 'Hold' on 1 call will unhold the other call for DSDA or Virtual DSDA
+ * enabled devices, effectively constituting 'swap' functionality.
+ */
+ @Test
+ @SmallTest
+ public void testUnholdOnOtherSubForVirtualDsdaDevice() {
+ when(mTelephonyManagerProxy.isConcurrentCallsPossible()).thenReturn(true);
+
+ ArrayList<android.telecom.Connection> tcs = new ArrayList<>();
+ SimpleTelephonyConnection tc1 = createTestConnection(SUB1_HANDLE, 0, false);
+ tcs.add(tc1);
+ TelephonyConnectionService.maybeUnholdCallsOnOtherSubs(
+ tcs, new ArrayList<>(), SUB2_HANDLE, mTelephonyManagerProxy);
+ assertTrue(tc1.wasUnheld);
+ }
+
+ /**
+ * Verifies hold/unhold behavior for a conference on the other sub. It does not disturb the
+ * individual connections that participate in the conference.
+ */
+ @Test
+ @SmallTest
+ public void testUnholdConferenceOnOtherSubForVirtualDsdaDevice() {
+ when(mTelephonyManagerProxy.isConcurrentCallsPossible()).thenReturn(true);
+ SimpleTelephonyConnection tc1 =
+ createTestConnection(SUB1_HANDLE, 0, false);
+ SimpleTelephonyConnection tc2 =
+ createTestConnection(SUB1_HANDLE, 0, false);
+ List<android.telecom.Connection> conferenceParticipants = Arrays.asList(tc1, tc2);
+
+ SimpleConference testConference = createTestConference(SUB1_HANDLE, 0);
+ List<Conference> conferences = Arrays.asList(testConference);
+
+ TelephonyConnectionService.maybeUnholdCallsOnOtherSubs(
+ conferenceParticipants, conferences, SUB2_HANDLE, mTelephonyManagerProxy);
+
+ assertTrue(testConference.wasUnheld);
+ assertFalse(tc1.wasUnheld);
+ assertFalse(tc2.wasUnheld);
+ }
+
/**
* Verifies that TelephonyManager is used to determine whether a connection is Emergency when
* creating an outgoing connection.
@@ -2369,6 +2452,12 @@
return connection;
}
+ private SimpleConference createTestConference(PhoneAccountHandle handle, int properties) {
+ SimpleConference conference = new SimpleConference(handle);
+ conference.setConnectionProperties(properties);
+ return conference;
+ }
+
/**
* Setup the mess of mocks for {@link #testSecondCallSameSubWontDisconnect()} and
* {@link #testIncomingDoesntRequestDisconnect()}.