DSDA: Set call EXTRA if either sub doesn't allow hold

- When either the existing call or the incoming call is on a
  non-hold-supported carrier, answering the incoming call should
  disconnect the existing call.
- TelephonyConnectionService takes care of both setting the call extra
  on receiving an incoming call, and terminating the existing call when
  the incoming call is answered.
- Dialer UX presentation and behavior remains unchanged.

Bug:315993953
Test: Live test with VZW + TMO DSDA- compared behavior with the feature flag
 set and not set; updated TelephonyConnectionServiceTest.
Change-Id: I72cef9d6c8f9932d3b3d3974029e27b6788f9713
diff --git a/src/com/android/services/telephony/TelephonyConnection.java b/src/com/android/services/telephony/TelephonyConnection.java
index ddff3d9..9fe73e2 100644
--- a/src/com/android/services/telephony/TelephonyConnection.java
+++ b/src/com/android/services/telephony/TelephonyConnection.java
@@ -83,6 +83,7 @@
 import com.android.internal.telephony.d2d.RtpTransport;
 import com.android.internal.telephony.d2d.Timeouts;
 import com.android.internal.telephony.d2d.TransportProtocol;
+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;
@@ -1287,12 +1288,22 @@
         originalConnection.sendRttModifyResponse(textStream);
     }
 
+    private boolean answeringDropsFgCalls() {
+        if (Flags.callExtraForNonHoldSupportedCarriers()) {
+            Bundle extras = getExtras();
+            if (extras != null) {
+                return extras.getBoolean(Connection.EXTRA_ANSWERING_DROPS_FG_CALL);
+            }
+        }
+        return false;
+    }
+
     public void performAnswer(int videoState) {
         Log.v(this, "performAnswer");
         if (isValidRingingCall() && getPhone() != null) {
             try {
                 mTelephonyConnectionService.maybeDisconnectCallsOnOtherSubs(
-                        getPhoneAccountHandle());
+                        getPhoneAccountHandle(), answeringDropsFgCalls());
                 getPhone().acceptCall(videoState);
             } catch (CallStateException e) {
                 Log.e(this, e, "Failed to accept call.");
diff --git a/src/com/android/services/telephony/TelephonyConnectionService.java b/src/com/android/services/telephony/TelephonyConnectionService.java
index 8ab2723..dedbc26 100644
--- a/src/com/android/services/telephony/TelephonyConnectionService.java
+++ b/src/com/android/services/telephony/TelephonyConnectionService.java
@@ -89,6 +89,7 @@
 import com.android.internal.telephony.emergency.EmergencyStateTracker;
 import com.android.internal.telephony.emergency.RadioOnHelper;
 import com.android.internal.telephony.emergency.RadioOnStateListener;
+import com.android.internal.telephony.flags.Flags;
 import com.android.internal.telephony.imsphone.ImsExternalCallTracker;
 import com.android.internal.telephony.imsphone.ImsPhone;
 import com.android.internal.telephony.imsphone.ImsPhoneConnection;
@@ -3907,10 +3908,34 @@
         return origAccountHandle;
     }
 
+    /*
+     * Returns true if both existing connections on-device and the incoming connection support HOLD,
+     * false otherwise. Assumes that a TelephonyConference supports HOLD.
+     */
+    private boolean allCallsSupportHold(@NonNull TelephonyConnection incomingConnection) {
+        if (Flags.callExtraForNonHoldSupportedCarriers()) {
+            if (getAllConnections().stream()
+                    .filter(c ->
+                            // Exclude multiendpoint calls as they're not on this device.
+                            (c.getConnectionProperties() & Connection.PROPERTY_IS_EXTERNAL_CALL)
+                                    == 0
+                                    && (c.getConnectionCapabilities()
+                                    & Connection.CAPABILITY_SUPPORT_HOLD) != 0).count() == 0) {
+                return false;
+            }
+            if ((incomingConnection.getConnectionCapabilities()
+                    & Connection.CAPABILITY_SUPPORT_HOLD) == 0) {
+                return false;
+            }
+        }
+        return true;
+    }
+
     /**
-     * For the passed in incoming {@link TelephonyConnection}, for non- dual active voice devices,
+     * For the passed in incoming {@link TelephonyConnection}, for non-dual active voice devices,
      * adds {@link Connection#EXTRA_ANSWERING_DROPS_FG_CALL} if there are ongoing calls on another
-     * subscription (ie phone account handle) than the one passed in.
+     * subscription (ie phone account handle) than the one passed in. For dual active voice devices,
+     * still sets the EXTRA if either subscription has connections that don't support hold.
      * @param connection The connection.
      * @param phoneAccountHandle The {@link PhoneAccountHandle} the incoming call originated on;
      *                           this is passed in because
@@ -3920,10 +3945,11 @@
      */
     public void maybeIndicateAnsweringWillDisconnect(@NonNull TelephonyConnection connection,
             @NonNull PhoneAccountHandle phoneAccountHandle) {
-        if (mTelephonyManagerProxy.isConcurrentCallsPossible()) {
-            return;
-        }
         if (isCallPresentOnOtherSub(phoneAccountHandle)) {
+            if (mTelephonyManagerProxy.isConcurrentCallsPossible()
+                    && allCallsSupportHold(connection)) {
+                return;
+            }
             Log.i(this, "maybeIndicateAnsweringWillDisconnect; answering call %s will cause a call "
                     + "on another subscription to drop.", connection.getTelecomCallId());
             Bundle extras = new Bundle();
@@ -3948,13 +3974,16 @@
 
     /**
      * Where there are ongoing calls on another subscription other than the one specified,
-     * disconnect these calls for non-DSDA devices. This is used where there is an incoming call on
-     * one sub, but there are ongoing calls on another sub which need to be disconnected.
+     * disconnect these calls. This is used where there is an incoming call on one sub, but there
+     * are ongoing calls on another sub which need to be disconnected.
      * @param incomingHandle The incoming {@link PhoneAccountHandle}.
+     * @param answeringDropsFgCall Whether for dual-SIM dual active devices, answering the incoming
+     *                            call should drop the second call.
      */
-    public void maybeDisconnectCallsOnOtherSubs(@NonNull PhoneAccountHandle incomingHandle) {
+    public void maybeDisconnectCallsOnOtherSubs(
+            @NonNull PhoneAccountHandle incomingHandle, boolean answeringDropsFgCall) {
         Log.i(this, "maybeDisconnectCallsOnOtherSubs: check for calls not on %s", incomingHandle);
-        maybeDisconnectCallsOnOtherSubs(getAllConnections(), incomingHandle,
+        maybeDisconnectCallsOnOtherSubs(getAllConnections(), incomingHandle, answeringDropsFgCall,
                 mTelephonyManagerProxy);
     }
 
@@ -3964,13 +3993,16 @@
      * the core functionality.
      * @param connections the calls to check.
      * @param incomingHandle the incoming handle.
+     * @param answeringDropsFgCall Whether for dual-SIM dual active devices, answering the incoming
+     *                            call should drop the second call.
      * @param telephonyManagerProxy the proxy to the {@link TelephonyManager} instance.
      */
     @VisibleForTesting
     public static void maybeDisconnectCallsOnOtherSubs(@NonNull Collection<Connection> connections,
             @NonNull PhoneAccountHandle incomingHandle,
+            boolean answeringDropsFgCall,
             TelephonyManagerProxy telephonyManagerProxy) {
-        if (telephonyManagerProxy.isConcurrentCallsPossible()) {
+        if (telephonyManagerProxy.isConcurrentCallsPossible() && !answeringDropsFgCall) {
             return;
         }
         connections.stream()
diff --git a/tests/src/com/android/services/telephony/TelephonyConnectionServiceTest.java b/tests/src/com/android/services/telephony/TelephonyConnectionServiceTest.java
index f0a5220..250d27c 100644
--- a/tests/src/com/android/services/telephony/TelephonyConnectionServiceTest.java
+++ b/tests/src/com/android/services/telephony/TelephonyConnectionServiceTest.java
@@ -319,6 +319,7 @@
         mBinderStub = (IConnectionService.Stub) mTestConnectionService.onBind(null);
         mSetFlagsRule.disableFlags(Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG);
         mSetFlagsRule.enableFlags(Flags.FLAG_DO_NOT_OVERRIDE_PRECISE_LABEL);
+        mSetFlagsRule.enableFlags(Flags.FLAG_CALL_EXTRA_FOR_NON_HOLD_SUPPORTED_CARRIERS);
     }
 
     @After
@@ -1816,7 +1817,7 @@
         SimpleTelephonyConnection tc1 = createTestConnection(SUB1_HANDLE, 0, false);
         tcs.add(tc1);
         TelephonyConnectionService.maybeDisconnectCallsOnOtherSubs(
-                tcs, SUB1_HANDLE, mTelephonyManagerProxy);
+                tcs, SUB1_HANDLE, false, mTelephonyManagerProxy);
         // Would've preferred to use mockito, but can't mock out TelephonyConnection/Connection
         // easily.
         assertFalse(tc1.wasDisconnected);
@@ -1829,7 +1830,7 @@
         SimpleTelephonyConnection tc1 = createTestConnection(SUB1_HANDLE, 0, true);
         tcs.add(tc1);
         TelephonyConnectionService.maybeDisconnectCallsOnOtherSubs(
-                tcs, SUB2_HANDLE, mTelephonyManagerProxy);
+                tcs, SUB2_HANDLE, false, mTelephonyManagerProxy);
         // Other call is an emergency call, so don't disconnect it.
         assertFalse(tc1.wasDisconnected);
     }
@@ -1842,7 +1843,7 @@
                 android.telecom.Connection.PROPERTY_IS_EXTERNAL_CALL, false);
         tcs.add(tc1);
         TelephonyConnectionService.maybeDisconnectCallsOnOtherSubs(
-                tcs, SUB2_HANDLE, mTelephonyManagerProxy);
+                tcs, SUB2_HANDLE, false, mTelephonyManagerProxy);
         // Other call is an external call, so don't disconnect it.
         assertFalse(tc1.wasDisconnected);
     }
@@ -1854,7 +1855,7 @@
         SimpleTelephonyConnection tc1 = createTestConnection(SUB1_HANDLE, 0, false);
         tcs.add(tc1);
         TelephonyConnectionService.maybeDisconnectCallsOnOtherSubs(
-                tcs, SUB2_HANDLE, mTelephonyManagerProxy);
+                tcs, SUB2_HANDLE, false, mTelephonyManagerProxy);
         assertTrue(tc1.wasDisconnected);
     }
 
@@ -1868,14 +1869,14 @@
         tcs.add(tc1);
         tcs.add(tc2);
         TelephonyConnectionService.maybeDisconnectCallsOnOtherSubs(
-                tcs, SUB2_HANDLE, mTelephonyManagerProxy);
+                tcs, SUB2_HANDLE, false, mTelephonyManagerProxy);
         assertTrue(tc1.wasDisconnected);
         assertTrue(tc2.wasDisconnected);
     }
 
     /**
      * Verifies that DSDA or virtual DSDA-enabled devices can support active non-emergency calls on
-     * separate subs.
+     * separate subs, when the extra EXTRA_ANSWERING_DROPS_FG_CALL is not set on the incoming call.
      */
     @Test
     @SmallTest
@@ -1886,10 +1887,26 @@
         SimpleTelephonyConnection tc1 = createTestConnection(SUB1_HANDLE, 0, false);
         tcs.add(tc1);
         TelephonyConnectionService.maybeDisconnectCallsOnOtherSubs(
-                tcs, SUB2_HANDLE, mTelephonyManagerProxy);
+                tcs, SUB2_HANDLE, false, mTelephonyManagerProxy);
         assertFalse(tc1.wasDisconnected);
     }
 
+    /**
+     * Verifies that DSDA or virtual DSDA-enabled devices will disconnect the existing call when the
+     * call extra EXTRA_ANSWERING_DROPS_FG_CALL is set on the incoming call on a different sub.
+     */
+    @Test
+    @SmallTest
+    public void testDisconnectDifferentSubForVirtualDsdaDevice_ifCallExtraSet() {
+        when(mTelephonyManagerProxy.isConcurrentCallsPossible()).thenReturn(true);
+
+        ArrayList<android.telecom.Connection> tcs = new ArrayList<>();
+        SimpleTelephonyConnection tc1 = createTestConnection(SUB1_HANDLE, 0, false);
+        tcs.add(tc1);
+        TelephonyConnectionService.maybeDisconnectCallsOnOtherSubs(
+                tcs, SUB2_HANDLE, true, mTelephonyManagerProxy);
+        assertTrue(tc1.wasDisconnected);
+    }
 
     /**
      * For calls on the same sub, the Dialer implements the 'swap' functionality to perform hold and