DSDA: Wait for call disconnect on other subs for ECC

Implements waiting logic in Telephony to disconnect calls on the other
subs when we try to place an emergency call. Performs a bulk transaction
and completes all the disconnects irregardless of individual failures.

This will help ensure that we don't have calls active/held across sims
when simultaneous calling is available and also address the shared data
call issue with not waiting for the disconnect to occur on the shared
data sim before placing the emergency call on the sim in service.

Bug: 392910450
Bug: 393240400
Flag: com.android.server.telecom.flags.enable_call_sequencing
Test: atest TelephonyConnectionServiceTest
Change-Id: I536d7334d908f0a7f349e10228bd08e188c621ae
diff --git a/src/com/android/services/telephony/TelephonyConnectionService.java b/src/com/android/services/telephony/TelephonyConnectionService.java
index 320e8be..457e8f8 100644
--- a/src/com/android/services/telephony/TelephonyConnectionService.java
+++ b/src/com/android/services/telephony/TelephonyConnectionService.java
@@ -126,6 +126,7 @@
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
 import java.util.function.Consumer;
 import java.util.regex.Pattern;
 import java.util.stream.Stream;
@@ -147,7 +148,7 @@
 
     // 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 = 2000;
+    private static final int DEFAULT_DSDA_CALL_STATE_CHANGE_TIMEOUT_MS = 5000;
 
     // 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.
@@ -745,6 +746,32 @@
         }
     }
 
+    private static class StateDisconnectListener extends
+            TelephonyConnection.TelephonyConnectionListener {
+        private final CompletableFuture<Boolean> mDisconnectFuture;
+
+        StateDisconnectListener(CompletableFuture<Boolean> future) {
+            mDisconnectFuture = future;
+        }
+
+        @Override
+        public void onStateChanged(
+                Connection connection, @Connection.ConnectionState int state) {
+            TelephonyConnection c = (TelephonyConnection) connection;
+            if (c != null) {
+                switch (c.getState()) {
+                    case Connection.STATE_DISCONNECTED: {
+                        Log.d(LOG_TAG, "Connection " + connection.getTelecomCallId()
+                                + " changed to STATE_DISCONNECTED!");
+                        mDisconnectFuture.complete(true);
+                        c.removeTelephonyConnectionListener(this);
+                    }
+                    break;
+                }
+            }
+        }
+    }
+
     private static class OnDisconnectListener extends
             com.android.internal.telephony.Connection.ListenerBase {
         private final CompletableFuture<Boolean> mFuture;
@@ -1369,44 +1396,61 @@
                     }
                 }
 
-                CompletableFuture<Void> maybeHoldFuture =
-                        checkAndHoldCallsOnOtherSubsForEmergencyCall(request,
+                CompletableFuture<Void> maybeHoldOrDisconnectOnOtherSubsFuture =
+                        checkAndHoldOrDisconnectCallsOnOtherSubsForEmergencyCall(request,
                                 resultConnection, phone);
                 Consumer<Boolean> ddsSwitchConsumer = (result) -> {
                     Log.i(this, "onCreateOutgoingConn emergency-"
                             + " delayDialForDdsSwitch result = " + result);
                     placeOutgoingConnection(request, resultConnection, phone);
                 };
-                maybeHoldFuture.thenRun(() -> delayDialForDdsSwitch(phone, ddsSwitchConsumer));
+                maybeHoldOrDisconnectOnOtherSubsFuture.thenRun(() -> delayDialForDdsSwitch(phone,
+                        ddsSwitchConsumer));
                 return resultConnection;
             }
         }
     }
 
-    private CompletableFuture<Void> checkAndHoldCallsOnOtherSubsForEmergencyCall(
+    private CompletableFuture<Void> checkAndHoldOrDisconnectCallsOnOtherSubsForEmergencyCall(
             ConnectionRequest request, Connection resultConnection, Phone phone) {
-        CompletableFuture<Void> maybeHoldFuture = CompletableFuture.completedFuture(null);
-        if (mTelephonyManagerProxy.isConcurrentCallsPossible()
-                && shouldHoldForEmergencyCall(phone)) {
+        CompletableFuture<Void> future = CompletableFuture.completedFuture(null);
+        if (mTelephonyManagerProxy.isConcurrentCallsPossible()) {
             // If the PhoneAccountHandle was adjusted on building the TelephonyConnection,
             // the relevant PhoneAccountHandle will be updated in resultConnection.
             PhoneAccountHandle phoneAccountHandle =
                     resultConnection.getPhoneAccountHandle() == null
-                    ? request.getAccountHandle() : resultConnection.getPhoneAccountHandle();
-            Conferenceable c = maybeHoldCallsOnOtherSubs(phoneAccountHandle);
-            if (c != null) {
-                maybeHoldFuture = delayDialForOtherSubHold(phone, c, (success) -> {
-                    Log.i(this, "checkAndHoldCallsOnOtherSubsForEmergencyCall"
-                            + " delayDialForOtherSubHold success = " + success);
-                    if (!success) {
-                        // Terminates the existing call to make way for the emergency call.
-                        hangup(c, android.telephony.DisconnectCause
-                                .OUTGOING_EMERGENCY_CALL_PLACED);
-                    }
-                });
+                            ? request.getAccountHandle()
+                            : resultConnection.getPhoneAccountHandle();
+            if (shouldHoldForEmergencyCall(phone) && !mTelecomFlags.enableCallSequencing()) {
+                Conferenceable c = maybeHoldCallsOnOtherSubs(phoneAccountHandle);
+                if (c != null) {
+                    future = delayDialForOtherSubHold(phone, c, (success) -> {
+                        Log.i(this, "checkAndHoldOrDisconnectCallsOnOtherSubsForEmergencyCall"
+                                + " delayDialForOtherSubHold success = " + success);
+                        if (!success) {
+                            // Terminates the existing call to make way for the emergency call.
+                            hangup(c, android.telephony.DisconnectCause
+                                    .OUTGOING_EMERGENCY_CALL_PLACED);
+                        }
+                    });
+                }
+            } else {
+                Log.i(this, "checkAndHoldOrDisconnectCallsOnOtherSubsForEmergencyCall"
+                        + " disconnectAllCallsOnOtherSubs, phoneAccountExcluded: "
+                        + phoneAccountHandle);
+                // Disconnect any calls on other subscription as part of call sequencing. This will
+                // cover the shared data call case too when we have a call on the shared data sim
+                // as the call will always try to be placed on the sim in service. Refer to
+                // #isAvailableForEmergencyCalls.
+                List<Conferenceable> disconnectedConferenceables =
+                        disconnectAllConferenceablesOnOtherSubs(phoneAccountHandle);
+                future = delayDialForOtherSubDisconnects(phone, disconnectedConferenceables,
+                        (success) -> Log.i(this,
+                                "checkAndHoldOrDisconnectCallsOnOtherSubsForEmergencyCall"
+                                        + " delayDialForOtherSubDisconnects success = " + success));
             }
         }
-        return maybeHoldFuture;
+        return future;
     }
 
     private Connection placeOutgoingConnection(ConnectionRequest request,
@@ -2679,10 +2723,12 @@
                     phone);
         }
 
-        CompletableFuture<Void> maybeHoldFuture =
-                checkAndHoldCallsOnOtherSubsForEmergencyCall(request, resultConnection, phone);
-        maybeHoldFuture.thenRun(() -> placeEmergencyConnectionInternal(resultConnection,
-                phone, request, numberToDial, isTestEmergencyNumber, needToTurnOnRadio));
+        CompletableFuture<Void> maybeHoldOrDisconnectOnOtherSubFuture =
+                checkAndHoldOrDisconnectCallsOnOtherSubsForEmergencyCall(request,
+                        resultConnection, phone);
+        maybeHoldOrDisconnectOnOtherSubFuture.thenRun(() -> placeEmergencyConnectionInternal(
+                resultConnection, phone, request, numberToDial, isTestEmergencyNumber,
+                needToTurnOnRadio));
 
         // Non TelephonyConnection type instance means dialing failure.
         return resultConnection;
@@ -3833,7 +3879,7 @@
             // 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);
+                    () -> timeout.complete(false), DEFAULT_DSDA_CALL_STATE_CHANGE_TIMEOUT_MS);
             // Ensure that the Consumer is completed on the main thread.
             return stateHoldingFuture.acceptEitherAsync(timeout, completeConsumer,
                     phone.getContext().getMainExecutor());
@@ -3846,6 +3892,63 @@
     }
 
     /**
+     * For DSDA devices, block until the connections passed in are disconnected (STATE_DISCONNECTED)
+     * or time out.
+     * @return {@link CompletableFuture} indicating the completion result after performing
+     * the bulk disconnect
+     */
+    private CompletableFuture<Void> delayDialForOtherSubDisconnects(Phone phone,
+            List<Conferenceable> conferenceables, Consumer<Boolean> completeConsumer) {
+        if (conferenceables.isEmpty()) {
+            completeConsumer.accept(true);
+            return CompletableFuture.completedFuture(null);
+        }
+        if (phone == null) {
+            // Unexpected inputs
+            completeConsumer.accept(false);
+            return CompletableFuture.completedFuture(null);
+        }
+        List<CompletableFuture<Void>> disconnectFutures = new ArrayList<>();
+        for (Conferenceable conferenceable : conferenceables) {
+            CompletableFuture<Void> disconnectFuture = CompletableFuture.completedFuture(null);
+            try {
+                if (conferenceable == null) {
+                    disconnectFuture = CompletableFuture.completedFuture(null);
+                } else {
+                    // Listen for each disconnect as part of an individual future.
+                    disconnectFuture = CompletableFuture.runAsync(() ->
+                            listenForDisconnectStateChanged(conferenceable)
+                                    .completeOnTimeout(false,
+                                            DEFAULT_DSDA_CALL_STATE_CHANGE_TIMEOUT_MS,
+                                            TimeUnit.MILLISECONDS),
+                            phone.getContext().getMainExecutor());
+                }
+            } catch (Exception e) {
+                Log.w(this, "delayDialForOtherSubDisconnects - exception= " + e.getMessage());
+                disconnectFuture = CompletableFuture.completedFuture(null);
+            } finally {
+                disconnectFutures.add(disconnectFuture);
+            }
+        }
+        // Return a future that waits for all the disconnect futures to complete.
+        return CompletableFuture.allOf(disconnectFutures.toArray(CompletableFuture[]::new));
+    }
+
+    /**
+     * Listen for the disconnect state change from the passed in {@link Conferenceable}.
+     * @param conferenceable
+     * @return {@link CompletableFuture} that provides the result of waiting on the
+     * disconnect state change.
+     */
+    private CompletableFuture<Boolean> listenForDisconnectStateChanged(
+            @NonNull Conferenceable conferenceable) {
+        CompletableFuture<Boolean> future = new CompletableFuture<>();
+        final StateDisconnectListener disconnectListener = new StateDisconnectListener(future);
+        addTelephonyConnectionListener(conferenceable, disconnectListener);
+        return future;
+    }
+
+    /**
      * 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
@@ -4779,8 +4882,47 @@
         return null;
     }
 
-    private void disconnectAllCallsOnOtherSubs (@NonNull PhoneAccountHandle handle) {
-        Collection<Connection>connections = getAllConnections();
+    /**
+     * For DSDA devices, disconnects all calls (and conferences) on other subs when placing an
+     * emergency call.
+     * @param handle The {@link PhoneAccountHandle} to exclude when disconnecting calls
+     * @return {@link List} compromised of the conferenceables that have been disconnected.
+     */
+    @VisibleForTesting
+    protected List<Conferenceable> disconnectAllConferenceablesOnOtherSubs(
+            @NonNull PhoneAccountHandle handle) {
+        List<Conferenceable> conferenceables = new ArrayList<>();
+        Collection<Conference> conferences = getAllConferences();
+        // Add the conferences
+        conferences.stream()
+                .filter(c ->
+                        (c.getState() == Connection.STATE_ACTIVE
+                                || c.getState() == Connection.STATE_HOLDING)
+                                // Include any calls not on same sub as current connection.
+                                && !Objects.equals(c.getPhoneAccountHandle(), handle))
+                .forEach(c -> {
+                    if (c instanceof TelephonyConference) {
+                        TelephonyConference tc = (TelephonyConference) c;
+                        Log.i(LOG_TAG, "disconnectAllConferenceablesOnOtherSubs: disconnect"
+                                        + " %s due to redial happened on other sub.",
+                                tc.getTelecomCallId());
+                        tc.onDisconnect();
+                        conferenceables.add(c);
+                    }
+                });
+        // Add the connections.
+        conferenceables.addAll(disconnectAllCallsOnOtherSubs(handle));
+        return conferenceables;
+    }
+
+    /**
+     * For DSDA devices, disconnects all calls on other subs when placing an emergency call.
+     * @param handle The {@link PhoneAccountHandle} to exclude when disconnecting calls
+     * @return {@link List} including compromised of the connections that have been disconnected.
+     */
+    private List<Connection> disconnectAllCallsOnOtherSubs(@NonNull PhoneAccountHandle handle) {
+        Collection<Connection> connections = getAllConnections();
+        List<Connection> disconnectedConnections = new ArrayList<>();
         connections.stream()
                 .filter(c ->
                         (c.getState() == Connection.STATE_ACTIVE
@@ -4794,8 +4936,10 @@
                                 " %s due to redial happened on other sub.",
                                 tc.getTelecomCallId());
                         tc.hangup(android.telephony.DisconnectCause.LOCAL);
+                        disconnectedConnections.add(c);
                     }
                 });
+        return disconnectedConnections;
     }
 
     private @NetworkRegistrationInfo.Domain int getActiveCallDomain(int subId) {
diff --git a/tests/src/com/android/services/telephony/TelephonyConnectionServiceTest.java b/tests/src/com/android/services/telephony/TelephonyConnectionServiceTest.java
index 3710d6a..e27bea5 100644
--- a/tests/src/com/android/services/telephony/TelephonyConnectionServiceTest.java
+++ b/tests/src/com/android/services/telephony/TelephonyConnectionServiceTest.java
@@ -2279,6 +2279,35 @@
     }
 
     /**
+     * For DSDA devices, verifies that calls on other subs are disconnected based on the passed in
+     * phone account
+     */
+    @Test
+    @SmallTest
+    public void testDisconnectCallsOnOtherSubs() throws Exception {
+        setupForCallTest();
+        when(mTelephonyManagerProxy.isConcurrentCallsPossible()).thenReturn(true);
+        doNothing().when(mContext).startActivityAsUser(any(), any());
+
+        mBinderStub.createConnection(PHONE_ACCOUNT_HANDLE_1, "TC@1",
+                new ConnectionRequest(PHONE_ACCOUNT_HANDLE_1, Uri.parse("tel:16505551212"),
+                        new Bundle()),
+                true, false, null);
+        waitForHandlerAction(mTestConnectionService.getHandler(), TIMEOUT_MS);
+        assertEquals(1, mTestConnectionService.getAllConnections().size());
+
+        TelephonyConnection cn = (TelephonyConnection)
+                mTestConnectionService.getAllConnections().toArray()[0];
+        cn.setActive();
+
+        List<Conferenceable> conferenceables = mTestConnectionService
+                .disconnectAllConferenceablesOnOtherSubs(PHONE_ACCOUNT_HANDLE_2);
+        assertFalse(conferenceables.isEmpty());
+        assertEquals(conferenceables.getFirst(), cn);
+        assertEquals(cn.getState(), android.telecom.Connection.STATE_DISCONNECTED);
+    }
+
+    /**
      * Verifies that TelephonyManager is used to determine whether a connection is Emergency when
      * creating an outgoing connection.
      */