Fix issue with auto disconnect on empty conference.

Some carriers do not automatically disconnect a conference when the number
of participants drops to 0 and instead rely on the device to detect when
the CEP reports an empty conference and to then subsequently disconnect
the conference.

When the number of participants in a conference drops to 1, a device can
pretend that this is actually just a standalone call instead.  There was a
logic error in the code that auto-disconnects the conference; it was
looking at "newParticipantCount", which is a count of the number of
children we are tracking for the conference call.  This is a problem when
emulating a single party call as the "newParticipantCount" would be zero.

It also turns out that "testLocalDisconnectOnEmptySinglePartyConference"
had a bug where it was NOT actually using single party call emulation.
Hence the test that was specifically designed to test empty disconnect
was not even running.

This CL changes the logic used to perform the auto-disconnect to look
at the number of participants reported in the newest incoming CEP to
determine if an auto-disconnect is warranted, rather than relying on the
number of children this conference has.  This ensures that if we're in
single-party emulation mode we aren't going to disconnect the conference.

Also fixed the unit test that had a bug.

Test: Added new unit tests for this particular case; ran existing unit test suite.
Fixes: 269569833
Change-Id: I16127f16f3a79ad4b42e16996a94b1b5533d62a7
diff --git a/src/com/android/services/telephony/ImsConference.java b/src/com/android/services/telephony/ImsConference.java
index c62b4fa..654fb93 100644
--- a/src/com/android/services/telephony/ImsConference.java
+++ b/src/com/android/services/telephony/ImsConference.java
@@ -980,23 +980,25 @@
             // 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
-            // or we will not log the call.
-            boolean isSinglePartyConference = participants.stream()
+            final long numActiveCepParticipantsOtherThanHost = participants.stream()
                     .filter(p -> {
                         Pair<Uri, Uri> pIdent = new Pair<>(p.getHandle(), p.getEndpoint());
                         return !Objects.equals(mHostParticipantIdentity, pIdent)
                                 && p.getState() != Connection.STATE_DISCONNECTED;
                     })
-                    .count() <= 1;
+                    .count();
+            // 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 or we will not log the call.
+            final boolean isCepForSinglePartyConference =
+                    numActiveCepParticipantsOtherThanHost <= 1;
 
             // We will only process the CEP data if:
             // 1. We're not emulating a single party call.
             // 2. We're emulating a single party call and the CEP contains more than just the
             //    single party
-            if ((!isMultiparty() && !isSinglePartyConference)
+            if ((!isMultiparty() && !isCepForSinglePartyConference)
                     || isMultiparty()) {
                 // Add any new participants and update existing.
                 for (ConferenceParticipant participant : participants) {
@@ -1082,15 +1084,17 @@
 
             int newParticipantCount = mConferenceParticipantConnections.size();
             Log.v(this, "handleConferenceParticipantsUpdate: oldParticipantCount=%d, "
-                            + "newParticipantcount=%d", oldParticipantCount, newParticipantCount);
-            // If the single party call emulation fature flag is enabled, we can potentially treat
+                            + "newParticipantCount=%d, isMultiPty=%b, cepParticipantCt=%d",
+                    oldParticipantCount, newParticipantCount, isMultiparty(),
+                    numActiveCepParticipantsOtherThanHost);
+            // If the single party call emulation feature flag is enabled, we can potentially treat
             // the conference as a single party call when there is just one participant.
             if (mFeatureFlagProxy.isUsingSinglePartyCallEmulation() &&
                     !mConferenceHost.isAdhocConferenceCall()) {
                 if (oldParticipantCount != 1 && newParticipantCount == 1) {
                     // If number of participants goes to 1, emulate a single party call.
                     startEmulatingSinglePartyCall();
-                } else if (!isMultiparty() && !isSinglePartyConference) {
+                } else if (!isMultiparty() && !isCepForSinglePartyConference) {
                     // Number of participants increased, so stop emulating a single party call.
                     stopEmulatingSinglePartyCall();
                 }
@@ -1108,8 +1112,8 @@
                     // OR if the conference had a single participant and is emulating a standalone
                     // call.
                     && (oldParticipantCount > 0 || !isMultiparty())
-                    // AND the CEP says there is nobody left any more.
-                    && newParticipantCount == 0) {
+                    // AND the CEP says there is nobody left anymore.
+                    && numActiveCepParticipantsOtherThanHost == 0) {
                 Log.i(this, "handleConferenceParticipantsUpdate: empty conference; "
                         + "local disconnect.");
                 onDisconnect();
diff --git a/tests/src/com/android/services/telephony/ImsConferenceTest.java b/tests/src/com/android/services/telephony/ImsConferenceTest.java
index 9d2f5ac..5123858 100644
--- a/tests/src/com/android/services/telephony/ImsConferenceTest.java
+++ b/tests/src/com/android/services/telephony/ImsConferenceTest.java
@@ -590,7 +590,7 @@
 
         ImsConference imsConference = new ImsConference(mMockTelecomAccountRegistry,
                 mMockTelephonyConnectionServiceProxy, mConferenceHost,
-                null /* phoneAccountHandle */, () -> false /* featureFlagProxy */,
+                null /* phoneAccountHandle */, () -> true /* isUsingSinglePartyCallEmulation */,
                 new ImsConference.CarrierConfiguration.Builder()
                         .setShouldLocalDisconnectEmptyConference(true)
                         .build());
@@ -622,6 +622,109 @@
     }
 
     /**
+     * Preconditions: both single party emulation and local disconnect of empty conferences is
+     * enabled.
+     * Tests the case where we receive a repeat with the same single-party data that caused a
+     * conference to be treated as a single party; we need to verify that we do not disconnect the
+     * conference locally in this case.
+     * @throws Exception
+     */
+    @Test
+    @SmallTest
+    public void testNoLocalDisconnectSinglePartyConferenceOnRepeatedCep() throws Exception {
+        when(mMockTelecomAccountRegistry.isUsingSimCallManager(any(PhoneAccountHandle.class)))
+                .thenReturn(false);
+
+        ImsConference imsConference = new ImsConference(mMockTelecomAccountRegistry,
+                mMockTelephonyConnectionServiceProxy, mConferenceHost,
+                null /* phoneAccountHandle */, () -> true /* isUsingSinglePartyCallEmulation */,
+                new ImsConference.CarrierConfiguration.Builder()
+                        .setShouldLocalDisconnectEmptyConference(true)
+                        .build());
+
+        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());
+
+        // Drop to 1 participant which enters single party mode.
+        imsConference.handleConferenceParticipantsUpdate(mConferenceHost,
+                Arrays.asList(participant1));
+        assertEquals(0, imsConference.getNumberOfParticipants());
+
+        // Get a repeat CEP with the same participant data; we should still be in single party mode
+        // but we should NOT disconnect the conference.
+        imsConference.handleConferenceParticipantsUpdate(mConferenceHost,
+                Arrays.asList(participant1));
+        assertEquals(0, imsConference.getNumberOfParticipants());
+        verify(mConferenceHost.mMockCall, never()).hangup();
+    }
+
+    /**
+     * An extension of {@link #testNoLocalDisconnectSinglePartyConferenceOnRepeatedCep()} where we
+     * get a repeated CEP with the same single party state, but then finally get a CEP with no
+     * participants anymore.  In this case we do expect a local disconnect as the final state.
+     * @throws Exception
+     */
+    @Test
+    @SmallTest
+    public void testLocalDisconnectSinglePartyConferenceOnRepeatedCep() throws Exception {
+        when(mMockTelecomAccountRegistry.isUsingSimCallManager(any(PhoneAccountHandle.class)))
+                .thenReturn(false);
+
+        ImsConference imsConference = new ImsConference(mMockTelecomAccountRegistry,
+                mMockTelephonyConnectionServiceProxy, mConferenceHost,
+                null /* phoneAccountHandle */, () -> true /* isUsingSinglePartyCallEmulation */,
+                new ImsConference.CarrierConfiguration.Builder()
+                        .setShouldLocalDisconnectEmptyConference(true)
+                        .build());
+
+        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());
+
+        // Drop to 1 participant which enters single party mode.
+        imsConference.handleConferenceParticipantsUpdate(mConferenceHost,
+                Arrays.asList(participant1));
+        assertEquals(0, imsConference.getNumberOfParticipants());
+
+        // Get a repeat CEP with the same participant data; we should still be in single party mode
+        // but we should NOT disconnect the conference.
+        imsConference.handleConferenceParticipantsUpdate(mConferenceHost,
+                Arrays.asList(participant1));
+        assertEquals(0, imsConference.getNumberOfParticipants());
+        verify(mConferenceHost.mMockCall, never()).hangup();
+
+        // Got another CEP that has no participants at all; we should disconnet in this case
+        imsConference.handleConferenceParticipantsUpdate(mConferenceHost, Collections.emptyList());
+        assertEquals(0, imsConference.getNumberOfParticipants());
+        verify(mConferenceHost.mMockCall).hangup();
+    }
+
+    /**
      * Tests a scenario where a handover connection arrives via
      * {@link TelephonyConnection#onOriginalConnectionRedialed(
      * com.android.internal.telephony.Connection)}.  During this process, the conference properties