Merge "Update sendSatelliteDatagram API." into udc-dev
diff --git a/src/com/android/services/telephony/ImsConferenceController.java b/src/com/android/services/telephony/ImsConferenceController.java
index 7cf9415..4200904 100644
--- a/src/com/android/services/telephony/ImsConferenceController.java
+++ b/src/com/android/services/telephony/ImsConferenceController.java
@@ -16,6 +16,7 @@
 
 package com.android.services.telephony;
 
+import android.annotation.NonNull;
 import android.content.Context;
 import android.os.PersistableBundle;
 import android.telecom.Conference;
@@ -26,17 +27,17 @@
 import android.telecom.PhoneAccountHandle;
 import android.telephony.CarrierConfigManager;
 
-import com.android.telephony.Rlog;
-
 import com.android.internal.telephony.Phone;
 import com.android.internal.telephony.PhoneConstants;
 import com.android.phone.PhoneUtils;
+import com.android.telephony.Rlog;
 
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Objects;
 import java.util.stream.Collectors;
 
 /**
@@ -220,14 +221,33 @@
         recalculateConference();
     }
 
+    private PhoneAccountHandle getPhoneAccountHandle(@NonNull Conferenceable c) {
+        if (c instanceof Connection) {
+            Connection connection = (Connection) c;
+            return connection.getPhoneAccountHandle();
+        } else if (c instanceof Conference) {
+            Conference conference = (Conference) c;
+            return conference.getPhoneAccountHandle();
+        }
+        throw new IllegalArgumentException("Unrecognized Conferenceable!" + c);
+    }
+
+    private boolean isSamePhoneAccountHandle(
+            @NonNull Conferenceable left, @NonNull Conferenceable right) {
+        PhoneAccountHandle leftHandle = getPhoneAccountHandle(left);
+        PhoneAccountHandle rightHandle = getPhoneAccountHandle(right);
+        return Objects.equals(leftHandle, rightHandle);
+    }
+
     /**
      * Calculates the conference-capable state of all GSM connections in this connection service.
+     * Connections from different {@link PhoneAccountHandle}s shall not be conferenceable.
      */
     private void recalculateConferenceable() {
         Log.v(this, "recalculateConferenceable : %d", mTelephonyConnections.size());
         HashSet<Conferenceable> conferenceableSet = new HashSet<>(mTelephonyConnections.size() +
                 mImsConferences.size());
-        HashSet<Conferenceable> conferenceParticipantsSet = new HashSet<>();
+        HashSet<Connection> conferenceParticipantsSet = new HashSet<>();
 
         // Loop through and collect all calls which are active or holding
         for (TelephonyConnection connection : mTelephonyConnections) {
@@ -300,11 +320,6 @@
 
         for (Conferenceable c : conferenceableSet) {
             if (c instanceof Connection) {
-                // Remove this connection from the Set and add all others
-                List<Conferenceable> conferenceables = conferenceableSet
-                        .stream()
-                        .filter(conferenceable -> c != conferenceable)
-                        .collect(Collectors.toList());
                 // TODO: Remove this once RemoteConnection#setConferenceableConnections is fixed.
                 // Add all conference participant connections as conferenceable with a standalone
                 // Connection.  We need to do this to ensure that RemoteConnections work properly.
@@ -313,7 +328,18 @@
                 // into the conference.
                 // We should add support for RemoteConnection#setConferenceables, which accepts a
                 // list of remote conferences and connections in the future.
-                conferenceables.addAll(conferenceParticipantsSet);
+                List<Conferenceable> conferenceables = conferenceParticipantsSet
+                        .stream()
+                        // Removes conference participants from different PhoneAccountHandles.
+                        .filter(connection -> isSamePhoneAccountHandle(c, connection))
+                        .collect(Collectors.toCollection(ArrayList::new));
+
+                // Removes this connection from the Set and add all others. Removes conferenceables
+                // from different PhoneAccountHandles.
+                conferenceables.addAll(conferenceableSet
+                        .stream()
+                        .filter(conferenceable -> c != conferenceable
+                                && isSamePhoneAccountHandle(c, conferenceable)).toList());
 
                 ((Connection) c).setConferenceables(conferenceables);
             } else if (c instanceof ImsConference) {
@@ -325,10 +351,11 @@
                 }
 
                 // Remove all conferences from the set, since we can not conference a conference
-                // to another conference.
+                // to another conference. Removes connections from different PhoneAccountHandles.
                 List<Connection> connections = conferenceableSet
                         .stream()
-                        .filter(conferenceable -> conferenceable instanceof Connection)
+                        .filter(conferenceable -> conferenceable instanceof Connection
+                                && isSamePhoneAccountHandle(c, conferenceable))
                         .map(conferenceable -> (Connection) conferenceable)
                         .collect(Collectors.toList());
                 // Conference equivalent to setConferenceables that only accepts Connections
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/ImsConferenceControllerTest.java b/tests/src/com/android/services/telephony/ImsConferenceControllerTest.java
index 33c8b8a..a9207e6 100644
--- a/tests/src/com/android/services/telephony/ImsConferenceControllerTest.java
+++ b/tests/src/com/android/services/telephony/ImsConferenceControllerTest.java
@@ -24,7 +24,9 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.content.ComponentName;
 import android.os.Looper;
+import android.telecom.PhoneAccountHandle;
 import android.test.suitebuilder.annotation.SmallTest;
 
 import org.junit.Before;
@@ -46,6 +48,14 @@
     @Mock
     private TelecomAccountRegistry mMockTelecomAccountRegistry;
 
+    private static final ComponentName TEST_COMPONENT_NAME = new ComponentName(
+            "com.android.phone.tests", ImsConferenceControllerTest.class.getName());
+    private static final String TEST_ACCOUNT_ID1 = "id1";
+    private static final String TEST_ACCOUNT_ID2 = "id2";
+    private static final PhoneAccountHandle PHONE_ACCOUNT_HANDLE_1 = new PhoneAccountHandle(
+            TEST_COMPONENT_NAME, TEST_ACCOUNT_ID1);
+    private static final PhoneAccountHandle PHONE_ACCOUNT_HANDLE_2 = new PhoneAccountHandle(
+            TEST_COMPONENT_NAME, TEST_ACCOUNT_ID2);
     private TestTelephonyConnection mTestTelephonyConnectionA;
     private TestTelephonyConnection mTestTelephonyConnectionB;
 
@@ -61,6 +71,9 @@
         mTestTelephonyConnectionA = new TestTelephonyConnection();
         mTestTelephonyConnectionB = new TestTelephonyConnection();
 
+        mTestTelephonyConnectionA.setPhoneAccountHandle(PHONE_ACCOUNT_HANDLE_1);
+        mTestTelephonyConnectionB.setPhoneAccountHandle(PHONE_ACCOUNT_HANDLE_1);
+
         mControllerTest = new ImsConferenceController(mTelecomAccountRegistry,
                 mMockTelephonyConnectionServiceProxy, () -> false);
     }
@@ -99,6 +112,27 @@
     }
 
     /**
+     * Behavior: add telephony connections A and B to conference controller;
+     * Assumption: Connection A and B have different PhoneAccountHandles, belong to different subs;
+     * Expected: Connection A and Connection B are not conferenceable with each other;
+     */
+    @Test
+    @SmallTest
+    public void testCallsOnDifferentSubsNotConferenceable() {
+        mTestTelephonyConnectionB.setPhoneAccountHandle(PHONE_ACCOUNT_HANDLE_2);
+        mControllerTest.add(mTestTelephonyConnectionA);
+        mControllerTest.add(mTestTelephonyConnectionB);
+
+        mTestTelephonyConnectionA.setActive();
+        mTestTelephonyConnectionB.setTelephonyConnectionOnHold();
+
+        assertFalse(mTestTelephonyConnectionA.getConferenceables()
+                .contains(mTestTelephonyConnectionB));
+        assertFalse(mTestTelephonyConnectionB.getConferenceables()
+                .contains(mTestTelephonyConnectionA));
+    }
+
+    /**
      * Behavior: add telephony connection B and A to conference controller,
      *           set status for merged connections
      * Assumption: after performing the behaviors, the status of Connection A is STATE_ACTIVE;
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()}.