Handle disconnected participants which remain in CEP data.

Some carriers will report a disconnected participant in the CEP but then
never remove it from a subsequent CEP update.
This can render the "single participant emulation" inactive as the device
never thinks there is just a single participant in the conference.
This change updates the conference event package handling to check for
disconnected participants and ensure they're cleaned up appropriately.

Test: Add new unit tests for the bug scenario; verify they pass.
Bug: 111860217
Change-Id: I3fd8271368b0023badcfaec3f25564971fed5d06
Merged-In: I3fd8271368b0023badcfaec3f25564971fed5d06
(cherry picked from commit acd96bac6c10e190d95981dd582bb3e47dda4e43)
diff --git a/src/com/android/services/telephony/ImsConference.java b/src/com/android/services/telephony/ImsConference.java
index 509b2a8..bf0374f 100644
--- a/src/com/android/services/telephony/ImsConference.java
+++ b/src/com/android/services/telephony/ImsConference.java
@@ -750,6 +750,10 @@
             // Determine if the conference event package represents a single party conference.
             // A single party conference is one where there is no other participant other than the
             // conference host and one other participant.
+            // We purposely exclude participants which have a disconnected state in the conference
+            // event package; some carriers are known to keep a disconnected participant around in
+            // subsequent CEP updates with a state of disconnected, even though its no longer part
+            // of the conference.
             // Note: We consider 0 to still be a single party conference since some carriers will
             // send a conference event package with JUST the host in it when the conference is
             // disconnected.  We don't want to change back to conference mode prior to disconnection
@@ -757,7 +761,8 @@
             boolean isSinglePartyConference = participants.stream()
                     .filter(p -> {
                         Pair<Uri, Uri> pIdent = new Pair<>(p.getHandle(), p.getEndpoint());
-                        return !Objects.equals(mHostParticipantIdentity, pIdent);
+                        return !Objects.equals(mHostParticipantIdentity, pIdent)
+                                && p.getState() != Connection.STATE_DISCONNECTED;
                     })
                     .count() <= 1;
 
@@ -772,7 +777,14 @@
                     Pair<Uri, Uri> userEntity = new Pair<>(participant.getHandle(),
                             participant.getEndpoint());
 
-                    participantUserEntities.add(userEntity);
+                    // We will exclude disconnected participants from the hash set of tracked
+                    // participants.  Some carriers are known to leave disconnected participants in
+                    // the conference event package data which would cause them to be present in the
+                    // conference even though they're disconnected.  Removing them from the hash set
+                    // here means we'll clean them up below.
+                    if (participant.getState() != Connection.STATE_DISCONNECTED) {
+                        participantUserEntities.add(userEntity);
+                    }
                     if (!mConferenceParticipantConnections.containsKey(userEntity)) {
                         // Some carriers will also include the conference host in the CEP.  We will
                         // filter that out here.
diff --git a/tests/src/com/android/services/telephony/ImsConferenceTest.java b/tests/src/com/android/services/telephony/ImsConferenceTest.java
index 68b5b3b..eaec5b6 100644
--- a/tests/src/com/android/services/telephony/ImsConferenceTest.java
+++ b/tests/src/com/android/services/telephony/ImsConferenceTest.java
@@ -119,6 +119,189 @@
     }
 
     /**
+     * Tests CEPs with disconnected participants present with disconnected state.
+     */
+    @Test
+    @SmallTest
+    public void testDisconnectParticipantViaDisconnectState() {
+        when(mMockTelecomAccountRegistry.isUsingSimCallManager(any(PhoneAccountHandle.class)))
+                .thenReturn(false);
+
+        ImsConference imsConference = new ImsConference(mMockTelecomAccountRegistry,
+                mMockTelephonyConnectionServiceProxy, mConferenceHost,
+                null /* phoneAccountHandle */, () -> true /* featureFlagProxy */);
+
+        // Start off with 3 participants.
+        ConferenceParticipant participant1 = new ConferenceParticipant(
+                Uri.parse("tel:6505551212"),
+                "A",
+                Uri.parse("sip:6505551212@testims.com"),
+                Connection.STATE_ACTIVE,
+                Call.Details.DIRECTION_INCOMING);
+        ConferenceParticipant participant2 = new ConferenceParticipant(
+                Uri.parse("tel:6505551213"),
+                "A",
+                Uri.parse("sip:6505551213@testims.com"),
+                Connection.STATE_ACTIVE,
+                Call.Details.DIRECTION_INCOMING);
+
+        ConferenceParticipant participant3 = new ConferenceParticipant(
+                Uri.parse("tel:6505551214"),
+                "A",
+                Uri.parse("sip:6505551214@testims.com"),
+                Connection.STATE_ACTIVE,
+                Call.Details.DIRECTION_INCOMING);
+        imsConference.handleConferenceParticipantsUpdate(mConferenceHost,
+                Arrays.asList(participant1, participant2, participant3));
+        assertEquals(3, imsConference.getNumberOfParticipants());
+        verify(mMockTelephonyConnectionServiceProxy, times(3)).addExistingConnection(
+                any(PhoneAccountHandle.class), any(Connection.class),
+                eq(imsConference));
+
+
+        // Mark one participant as disconnected.
+        ConferenceParticipant participant3Disconnected = new ConferenceParticipant(
+                Uri.parse("tel:6505551214"),
+                "A",
+                Uri.parse("sip:6505551214@testims.com"),
+                Connection.STATE_DISCONNECTED,
+                Call.Details.DIRECTION_INCOMING);
+        imsConference.handleConferenceParticipantsUpdate(mConferenceHost,
+                Arrays.asList(participant1, participant2, participant3Disconnected));
+        assertEquals(2, imsConference.getNumberOfParticipants());
+        verify(mMockTelephonyConnectionServiceProxy, times(1)).removeConnection(
+                any(Connection.class));
+        reset(mMockTelephonyConnectionServiceProxy);
+
+        // Now remove it from another CEP update; should still be the same number of participants
+        // and no updates.
+        imsConference.handleConferenceParticipantsUpdate(mConferenceHost,
+                Arrays.asList(participant1, participant2));
+        assertEquals(2, imsConference.getNumberOfParticipants());
+        verify(mMockTelephonyConnectionServiceProxy, never()).removeConnection(
+                any(Connection.class));
+        verify(mMockTelephonyConnectionServiceProxy, never()).addExistingConnection(
+                any(PhoneAccountHandle.class), any(Connection.class),
+                any(Conference.class));
+    }
+
+    /**
+     * Tests CEPs with removed participants.
+     */
+    @Test
+    @SmallTest
+    public void testDisconnectParticipantViaRemoval() {
+        when(mMockTelecomAccountRegistry.isUsingSimCallManager(any(PhoneAccountHandle.class)))
+                .thenReturn(false);
+
+        ImsConference imsConference = new ImsConference(mMockTelecomAccountRegistry,
+                mMockTelephonyConnectionServiceProxy, mConferenceHost,
+                null /* phoneAccountHandle */, () -> true /* featureFlagProxy */);
+
+        // Start off with 3 participants.
+        ConferenceParticipant participant1 = new ConferenceParticipant(
+                Uri.parse("tel:6505551212"),
+                "A",
+                Uri.parse("sip:6505551212@testims.com"),
+                Connection.STATE_ACTIVE,
+                Call.Details.DIRECTION_INCOMING);
+        ConferenceParticipant participant2 = new ConferenceParticipant(
+                Uri.parse("tel:6505551213"),
+                "A",
+                Uri.parse("sip:6505551213@testims.com"),
+                Connection.STATE_ACTIVE,
+                Call.Details.DIRECTION_INCOMING);
+
+        ConferenceParticipant participant3 = new ConferenceParticipant(
+                Uri.parse("tel:6505551214"),
+                "A",
+                Uri.parse("sip:6505551214@testims.com"),
+                Connection.STATE_ACTIVE,
+                Call.Details.DIRECTION_INCOMING);
+        imsConference.handleConferenceParticipantsUpdate(mConferenceHost,
+                Arrays.asList(participant1, participant2, participant3));
+        assertEquals(3, imsConference.getNumberOfParticipants());
+        verify(mMockTelephonyConnectionServiceProxy, times(3)).addExistingConnection(
+                any(PhoneAccountHandle.class), any(Connection.class),
+                eq(imsConference));
+        reset(mMockTelephonyConnectionServiceProxy);
+
+        // Remove one from the CEP (don't disconnect first); should have 2 participants now.
+        imsConference.handleConferenceParticipantsUpdate(mConferenceHost,
+                Arrays.asList(participant1, participant2));
+        assertEquals(2, imsConference.getNumberOfParticipants());
+        verify(mMockTelephonyConnectionServiceProxy, times(1)).removeConnection(
+                any(Connection.class));
+        verify(mMockTelephonyConnectionServiceProxy, never()).addExistingConnection(
+                any(PhoneAccountHandle.class), any(Connection.class),
+                any(Conference.class));
+    }
+
+    /**
+     * Typically when a participant disconnects from a conference it is either:
+     * 1. Removed from a subsequent CEP update.
+     * 2. Marked as disconnected in a CEP update, and then removed from another CEP update.
+     *
+     * When a participant disconnects from a conference, some carriers will mark the disconnected
+     * participant as disconnected, but fail to send another CEP update with it removed.
+     *
+     * This test verifies that we can still enter single party emulation in this case.
+     */
+    @Test
+    @SmallTest
+    public void testSinglePartyEmulationEnterOnDisconnectParticipant() {
+        when(mMockTelecomAccountRegistry.isUsingSimCallManager(any(PhoneAccountHandle.class)))
+                .thenReturn(false);
+
+        ImsConference imsConference = new ImsConference(mMockTelecomAccountRegistry,
+                mMockTelephonyConnectionServiceProxy, mConferenceHost,
+                null /* phoneAccountHandle */, () -> true /* featureFlagProxy */);
+
+        // Setup the initial conference state with 2 participants.
+        ConferenceParticipant participant1 = new ConferenceParticipant(
+                Uri.parse("tel:6505551212"),
+                "A",
+                Uri.parse("sip:6505551212@testims.com"),
+                Connection.STATE_ACTIVE,
+                Call.Details.DIRECTION_INCOMING);
+        ConferenceParticipant participant2 = new ConferenceParticipant(
+                Uri.parse("tel:6505551213"),
+                "A",
+                Uri.parse("sip:6505551213@testims.com"),
+                Connection.STATE_ACTIVE,
+                Call.Details.DIRECTION_INCOMING);
+        imsConference.handleConferenceParticipantsUpdate(mConferenceHost,
+                Arrays.asList(participant1, participant2));
+        assertEquals(2, imsConference.getNumberOfParticipants());
+        verify(mMockTelephonyConnectionServiceProxy, times(2)).addExistingConnection(
+                any(PhoneAccountHandle.class), any(Connection.class),
+                eq(imsConference));
+
+        // Some carriers keep disconnected participants around in the CEP; this will cause problems
+        // when we want to enter single party conference mode. Verify that this case is handled.
+        ConferenceParticipant participant2Disconnected = new ConferenceParticipant(
+                Uri.parse("tel:6505551213"),
+                "A",
+                Uri.parse("sip:6505551213@testims.com"),
+                Connection.STATE_DISCONNECTED,
+                Call.Details.DIRECTION_INCOMING);
+        imsConference.handleConferenceParticipantsUpdate(mConferenceHost,
+                Arrays.asList(participant1, participant2Disconnected));
+        assertEquals(0, imsConference.getNumberOfParticipants());
+        verify(mMockTelephonyConnectionServiceProxy, times(2)).removeConnection(
+                any(Connection.class));
+        reset(mMockTelephonyConnectionServiceProxy);
+
+        // Pretend to merge someone else into the conference.
+        imsConference.handleConferenceParticipantsUpdate(mConferenceHost,
+                Arrays.asList(participant1, participant2));
+        assertEquals(2, imsConference.getNumberOfParticipants());
+        verify(mMockTelephonyConnectionServiceProxy, times(2)).addExistingConnection(
+                any(PhoneAccountHandle.class), any(Connection.class),
+                eq(imsConference));
+    }
+
+    /**
      * We have seen a scenario on a carrier where a conference event package comes in just prior to
      * the call disconnecting with only the conference host in it.  This caused a problem because
      * it triggered exiting single party conference mode (due to a bug) and caused the call to not