Treat single party IMS conference as a standalone call.

Where an IMS conference drops to a single participant, remove that child
from the conference and tell telecom to no longer treat the conference as
if it is a conference.  Also pass through the participant name/number to
telecom.  When recalculating whether manage conference is supported, do
not allow manage conference if there is just a single participant.

Test: manual
Bug: 75975913
Change-Id: Iba9a65bc37df5df6472c51183410b774219c9eb2
diff --git a/src/com/android/services/telephony/ImsConference.java b/src/com/android/services/telephony/ImsConference.java
index 5722834..d5af25b 100644
--- a/src/com/android/services/telephony/ImsConference.java
+++ b/src/com/android/services/telephony/ImsConference.java
@@ -29,11 +29,14 @@
 import android.telecom.Log;
 import android.telecom.PhoneAccountHandle;
 import android.telecom.StatusHints;
+import android.telecom.TelecomManager;
 import android.telecom.VideoProfile;
 import android.telephony.CarrierConfigManager;
 import android.telephony.PhoneNumberUtils;
+import android.util.FeatureFlagUtils;
 import android.util.Pair;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.telephony.Call;
 import com.android.internal.telephony.CallStateException;
 import com.android.internal.telephony.Phone;
@@ -44,11 +47,13 @@
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 
 /**
  * Represents an IMS conference call.
@@ -68,6 +73,13 @@
 public class ImsConference extends Conference implements Holdable {
 
     /**
+     * Abstracts out fetching a feature flag.  Makes testing easier.
+     */
+    public interface FeatureFlagProxy {
+        boolean isUsingSinglePartyCallEmulation();
+    }
+
+    /**
      * Listener used to respond to changes to conference participants.  At the conference level we
      * are most concerned with handling destruction of a conference participant.
      */
@@ -241,6 +253,25 @@
     private final Object mUpdateSyncRoot = new Object();
 
     private boolean mIsHoldable;
+    private boolean mCouldManageConference;
+    private FeatureFlagProxy mFeatureFlagProxy;
+    private boolean mIsEmulatingSinglePartyCall = false;
+    /**
+     * Where {@link #mIsEmulatingSinglePartyCall} is {@code true}, contains the
+     * {@link ConferenceParticipantConnection#getUserEntity()} and
+     * {@link ConferenceParticipantConnection#getEndpoint()} of the single participant which this
+     * conference pretends to be.
+     */
+    private Pair<Uri, Uri> mLoneParticipantIdentity = null;
+
+    /**
+     * The {@link ConferenceParticipantConnection#getUserEntity()} and
+     * {@link ConferenceParticipantConnection#getEndpoint()} of the conference host as they appear
+     * in the CEP.  This is determined when we scan the first conference event package.
+     * It is possible that this will be {@code null} for carriers which do not include the host
+     * in the CEP.
+     */
+    private Pair<Uri, Uri> mHostParticipantIdentity = null;
 
     public void updateConferenceParticipantsAfterCreation() {
         if (mConferenceHost != null) {
@@ -254,19 +285,21 @@
 
     /**
      * Initializes a new {@link ImsConference}.
-     *
-     * @param telephonyConnectionService The connection service responsible for adding new
+     *  @param telephonyConnectionService The connection service responsible for adding new
      *                                   conferene participants.
      * @param conferenceHost The telephony connection hosting the conference.
      * @param phoneAccountHandle The phone account handle associated with the conference.
+     * @param featureFlagProxy
      */
     public ImsConference(TelecomAccountRegistry telecomAccountRegistry,
-                         TelephonyConnectionServiceProxy telephonyConnectionService,
-            TelephonyConnection conferenceHost, PhoneAccountHandle phoneAccountHandle) {
+            TelephonyConnectionServiceProxy telephonyConnectionService,
+            TelephonyConnection conferenceHost, PhoneAccountHandle phoneAccountHandle,
+            FeatureFlagProxy featureFlagProxy) {
 
         super(phoneAccountHandle);
 
         mTelecomAccountRegistry = telecomAccountRegistry;
+        mFeatureFlagProxy = featureFlagProxy;
 
         // Specify the connection time of the conference to be the connection time of the original
         // connection.
@@ -561,16 +594,25 @@
     }
 
     /**
-     * Updates the manage conference capability of the conference.  Where there are one or more
-     * conference event package participants, the conference management is permitted.  Where there
-     * are no conference event package participants, conference management is not permitted.
+     * Updates the manage conference capability of the conference.
+     *
+     * The following cases are handled:
+     * <ul>
+     *     <li>There is only a single participant in the conference -- manage conference is
+     *     disabled.</li>
+     *     <li>There is more than one participant in the conference -- manage conference is
+     *     enabled.</li>
+     *     <li>No conference event package data is available -- manage conference is disabled.</li>
+     * </ul>
      * <p>
      * Note: We add and remove {@link Connection#CAPABILITY_CONFERENCE_HAS_NO_CHILDREN} to ensure
      * that the conference is represented appropriately on Bluetooth devices.
      */
     private void updateManageConference() {
         boolean couldManageConference = can(Connection.CAPABILITY_MANAGE_CONFERENCE);
-        boolean canManageConference = !mConferenceParticipantConnections.isEmpty();
+        boolean canManageConference = mFeatureFlagProxy.isUsingSinglePartyCallEmulation()
+                ? mConferenceParticipantConnections.size() > 1
+                : mConferenceParticipantConnections.size() != 0;
         Log.v(this, "updateManageConference was :%s is:%s", couldManageConference ? "Y" : "N",
                 canManageConference ? "Y" : "N");
 
@@ -649,7 +691,8 @@
      * @param parent The connection which was notified of the conference participant.
      * @param participants The conference participant information.
      */
-    private void handleConferenceParticipantsUpdate(
+    @VisibleForTesting
+    public void handleConferenceParticipantsUpdate(
             TelephonyConnection parent, List<ConferenceParticipant> participants) {
 
         if (participants == null) {
@@ -668,64 +711,102 @@
         // update adds new participants, and the second does something like update the status of one
         // of the participants, we can get into a situation where the participant is added twice.
         synchronized (mUpdateSyncRoot) {
+            int oldParticipantCount = mConferenceParticipantConnections.size();
             boolean newParticipantsAdded = false;
             boolean oldParticipantsRemoved = false;
             ArrayList<ConferenceParticipant> newParticipants = new ArrayList<>(participants.size());
             HashSet<Pair<Uri,Uri>> participantUserEntities = new HashSet<>(participants.size());
 
-            // Add any new participants and update existing.
-            for (ConferenceParticipant participant : participants) {
-                Pair<Uri,Uri> userEntity = new Pair<>(participant.getHandle(),
-                        participant.getEndpoint());
+            // 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.
+            boolean isSinglePartyConference = participants.stream()
+                    .filter(p -> {
+                        Pair<Uri, Uri> pIdent = new Pair<>(p.getHandle(), p.getEndpoint());
+                        return !Objects.equals(mHostParticipantIdentity, pIdent);
+                    })
+                    .count() == 1;
 
-                participantUserEntities.add(userEntity);
-                if (!mConferenceParticipantConnections.containsKey(userEntity)) {
-                    // Some carriers will also include the conference host in the CEP.  We will
-                    // filter that out here.
-                    if (!isParticipantHost(mConferenceHostAddress, participant.getHandle())) {
-                        createConferenceParticipantConnection(parent, participant);
-                        newParticipants.add(participant);
-                        newParticipantsAdded = true;
+            // 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 ((mIsEmulatingSinglePartyCall && !isSinglePartyConference) ||
+                !mIsEmulatingSinglePartyCall) {
+                // Add any new participants and update existing.
+                for (ConferenceParticipant participant : participants) {
+                    Pair<Uri, Uri> userEntity = new Pair<>(participant.getHandle(),
+                            participant.getEndpoint());
+
+                    participantUserEntities.add(userEntity);
+                    if (!mConferenceParticipantConnections.containsKey(userEntity)) {
+                        // Some carriers will also include the conference host in the CEP.  We will
+                        // filter that out here.
+                        if (!isParticipantHost(mConferenceHostAddress, participant.getHandle())) {
+                            createConferenceParticipantConnection(parent, participant);
+                            newParticipants.add(participant);
+                            newParticipantsAdded = true;
+                        } else {
+                            // Track the identity of the conference host; its useful to know when
+                            // we look at the CEP in the future.
+                            mHostParticipantIdentity = userEntity;
+                        }
+                    } else {
+                        ConferenceParticipantConnection connection =
+                                mConferenceParticipantConnections.get(userEntity);
+                        Log.i(this,
+                                "handleConferenceParticipantsUpdate: updateState, participant = %s",
+                                participant);
+                        connection.updateState(participant.getState());
+                        connection.setVideoState(parent.getVideoState());
                     }
-                } else {
-                    ConferenceParticipantConnection connection =
-                            mConferenceParticipantConnections.get(userEntity);
-                    Log.i(this, "handleConferenceParticipantsUpdate: updateState, participant = %s",
-                            participant);
-                    connection.updateState(participant.getState());
-                    connection.setVideoState(parent.getVideoState());
+                }
+
+                // Set state of new participants.
+                if (newParticipantsAdded) {
+                    // Set the state of the new participants at once and add to the conference
+                    for (ConferenceParticipant newParticipant : newParticipants) {
+                        ConferenceParticipantConnection connection =
+                                mConferenceParticipantConnections.get(new Pair<>(
+                                        newParticipant.getHandle(),
+                                        newParticipant.getEndpoint()));
+                        connection.updateState(newParticipant.getState());
+                        connection.setVideoState(parent.getVideoState());
+                    }
+                }
+
+                // Finally, remove any participants from the conference that no longer exist in the
+                // conference event package data.
+                Iterator<Map.Entry<Pair<Uri, Uri>, ConferenceParticipantConnection>> entryIterator =
+                        mConferenceParticipantConnections.entrySet().iterator();
+                while (entryIterator.hasNext()) {
+                    Map.Entry<Pair<Uri, Uri>, ConferenceParticipantConnection> entry =
+                            entryIterator.next();
+
+                    if (!participantUserEntities.contains(entry.getKey())) {
+                        ConferenceParticipantConnection participant = entry.getValue();
+                        participant.setDisconnected(new DisconnectCause(DisconnectCause.CANCELED));
+                        participant.removeConnectionListener(mParticipantListener);
+                        mTelephonyConnectionService.removeConnection(participant);
+                        removeConnection(participant);
+                        entryIterator.remove();
+                        oldParticipantsRemoved = true;
+                    }
                 }
             }
 
-            // Set state of new participants.
-            if (newParticipantsAdded) {
-                // Set the state of the new participants at once and add to the conference
-                for (ConferenceParticipant newParticipant : newParticipants) {
-                    ConferenceParticipantConnection connection =
-                            mConferenceParticipantConnections.get(new Pair<>(
-                                    newParticipant.getHandle(),
-                                    newParticipant.getEndpoint()));
-                    connection.updateState(newParticipant.getState());
-                    connection.setVideoState(parent.getVideoState());
-                }
-            }
-
-            // Finally, remove any participants from the conference that no longer exist in the
-            // conference event package data.
-            Iterator<Map.Entry<Pair<Uri, Uri>, ConferenceParticipantConnection>> entryIterator =
-                    mConferenceParticipantConnections.entrySet().iterator();
-            while (entryIterator.hasNext()) {
-                Map.Entry<Pair<Uri, Uri>, ConferenceParticipantConnection> entry =
-                        entryIterator.next();
-
-                if (!participantUserEntities.contains(entry.getKey())) {
-                    ConferenceParticipantConnection participant = entry.getValue();
-                    participant.setDisconnected(new DisconnectCause(DisconnectCause.CANCELED));
-                    participant.removeConnectionListener(mParticipantListener);
-                    mTelephonyConnectionService.removeConnection(participant);
-                    removeConnection(participant);
-                    entryIterator.remove();
-                    oldParticipantsRemoved = true;
+            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
+            // the conference as a single party call when there is just one participant.
+            if (mFeatureFlagProxy.isUsingSinglePartyCallEmulation()) {
+                if (oldParticipantCount > 1 && newParticipantCount == 1) {
+                    // If number of participants goes to 1, emulate a single party call.
+                    startEmulatingSinglePartyCall();
+                } else if (mIsEmulatingSinglePartyCall && !isSinglePartyConference) {
+                    // Number of participants increased, so stop emulating a single party call.
+                    stopEmulatingSinglePartyCall();
                 }
             }
 
@@ -738,6 +819,89 @@
     }
 
     /**
+     * Called after {@link #startEmulatingSinglePartyCall()} to cause the conference to appear as
+     * if it is a conference again.
+     * 1. Tell telecom we're a conference again.
+     * 2. Restore {@link Connection#CAPABILITY_MANAGE_CONFERENCE} capability.
+     * 3. Null out the name/address.
+     */
+    private void stopEmulatingSinglePartyCall() {
+        Log.i(this, "stopEmulatingSinglePartyCall: conference now has more than one"
+                + " participant; make it look conference-like again.");
+        mIsEmulatingSinglePartyCall = false;
+
+        if (mCouldManageConference) {
+            int currentCapabilities = getConnectionCapabilities();
+            currentCapabilities |= Connection.CAPABILITY_MANAGE_CONFERENCE;
+            setConnectionCapabilities(currentCapabilities);
+        }
+
+        // Null out the address/name so it doesn't look like a single party call
+        setAddress(null, TelecomManager.PRESENTATION_UNKNOWN);
+        setCallerDisplayName(null, TelecomManager.PRESENTATION_UNKNOWN);
+
+        // Copy the conference connect time back to the previous lone participant.
+        ConferenceParticipantConnection loneParticipant =
+                mConferenceParticipantConnections.get(mLoneParticipantIdentity);
+        if (loneParticipant != null) {
+            Log.d(this,
+                    "stopEmulatingSinglePartyCall: restored lone participant connect time");
+            loneParticipant.setConnectTimeMillis(getConnectionTime());
+            loneParticipant.setConnectionStartElapsedRealTime(getConnectionStartElapsedRealTime());
+        }
+
+        // Tell Telecom its a conference again.
+        setConferenceState(true);
+    }
+
+    /**
+     * Called when a conference drops to a single participant. Causes this conference to present
+     * itself to Telecom as if it was a single party call.
+     * 1. Remove the participant from Telecom and from local tracking; when we get a new CEP in
+     *    the future we'll just re-add the participant anyways.
+     * 2. Tell telecom we're not a conference.
+     * 3. Remove {@link Connection#CAPABILITY_MANAGE_CONFERENCE} capability.
+     * 4. Set the name/address to that of the single participant.
+     */
+    private void startEmulatingSinglePartyCall() {
+        Log.i(this, "startEmulatingSinglePartyCall: conference has a single "
+                + "participant; downgrade to single party call.");
+
+        mIsEmulatingSinglePartyCall = true;
+        Iterator<ConferenceParticipantConnection> valueIterator =
+                mConferenceParticipantConnections.values().iterator();
+        if (valueIterator.hasNext()) {
+            ConferenceParticipantConnection entry = valueIterator.next();
+
+            // Set the conference name/number to that of the remaining participant.
+            setAddress(entry.getAddress(), entry.getAddressPresentation());
+            setCallerDisplayName(entry.getCallerDisplayName(),
+                    entry.getCallerDisplayNamePresentation());
+            setConnectionStartElapsedRealTime(entry.getConnectElapsedTimeMillis());
+            setConnectionTime(entry.getConnectTimeMillis());
+            mLoneParticipantIdentity = new Pair<>(entry.getUserEntity(), entry.getEndpoint());
+
+            // Remove the participant from Telecom.  It'll get picked up in a future CEP update
+            // again anyways.
+            entry.setDisconnected(new DisconnectCause(DisconnectCause.CANCELED,
+                    "EMULATING_SINGLE_CALL"));
+            entry.removeConnectionListener(mParticipantListener);
+            mTelephonyConnectionService.removeConnection(entry);
+            removeConnection(entry);
+            valueIterator.remove();
+        }
+
+        // Have Telecom pretend its not a conference.
+        setConferenceState(false);
+
+        // Remove manage conference capability.
+        mCouldManageConference = can(Connection.CAPABILITY_MANAGE_CONFERENCE);
+        int currentCapabilities = getConnectionCapabilities();
+        currentCapabilities &= ~Connection.CAPABILITY_MANAGE_CONFERENCE;
+        setConnectionCapabilities(currentCapabilities);
+    }
+
+    /**
      * Creates a new {@link ConferenceParticipantConnection} to represent a
      * {@link ConferenceParticipant}.
      * <p>
diff --git a/src/com/android/services/telephony/ImsConferenceController.java b/src/com/android/services/telephony/ImsConferenceController.java
index 971dd7b..9902700 100644
--- a/src/com/android/services/telephony/ImsConferenceController.java
+++ b/src/com/android/services/telephony/ImsConferenceController.java
@@ -93,6 +93,8 @@
      */
     private final TelephonyConnectionServiceProxy mConnectionService;
 
+    private final ImsConference.FeatureFlagProxy mFeatureFlagProxy;
+
     /**
      * List of known {@link TelephonyConnection}s.
      */
@@ -110,11 +112,14 @@
      * Creates a new instance of the Ims conference controller.
      *
      * @param connectionService The current connection service.
+     * @param featureFlagProxy
      */
     public ImsConferenceController(TelecomAccountRegistry telecomAccountRegistry,
-                                   TelephonyConnectionServiceProxy connectionService) {
+            TelephonyConnectionServiceProxy connectionService,
+            ImsConference.FeatureFlagProxy featureFlagProxy) {
         mConnectionService = connectionService;
         mTelecomAccountRegistry = telecomAccountRegistry;
+        mFeatureFlagProxy = featureFlagProxy;
     }
 
     /**
@@ -372,7 +377,7 @@
         }
 
         ImsConference conference = new ImsConference(mTelecomAccountRegistry, mConnectionService,
-                conferenceHostConnection, phoneAccountHandle);
+                conferenceHostConnection, phoneAccountHandle, mFeatureFlagProxy);
         conference.setState(conferenceHostConnection.getState());
         conference.addListener(mConferenceListener);
         conference.updateConferenceParticipantsAfterCreation();
diff --git a/src/com/android/services/telephony/TelecomAccountRegistry.java b/src/com/android/services/telephony/TelecomAccountRegistry.java
index c267835..37c5d7c 100644
--- a/src/com/android/services/telephony/TelecomAccountRegistry.java
+++ b/src/com/android/services/telephony/TelecomAccountRegistry.java
@@ -67,7 +67,7 @@
  * Owns all data we have registered with Telecom including handling dynamic addition and
  * removal of SIMs and SIP accounts.
  */
-public final class TelecomAccountRegistry {
+public class TelecomAccountRegistry {
     private static final boolean DBG = false; /* STOP SHIP if true */
 
     // This icon is the one that is used when the Slot ID that we have for a particular SIM
diff --git a/src/com/android/services/telephony/TelephonyConnectionService.java b/src/com/android/services/telephony/TelephonyConnectionService.java
index 00b24e9..ab9e211 100644
--- a/src/com/android/services/telephony/TelephonyConnectionService.java
+++ b/src/com/android/services/telephony/TelephonyConnectionService.java
@@ -41,6 +41,7 @@
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
+import android.util.FeatureFlagUtils;
 import android.util.Pair;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -145,7 +146,10 @@
             new CdmaConferenceController(this);
     private final ImsConferenceController mImsConferenceController =
             new ImsConferenceController(TelecomAccountRegistry.getInstance(this),
-                    mTelephonyConnectionServiceProxy);
+                    mTelephonyConnectionServiceProxy,
+                    // FeatureFlagProxy; used to determine if standalone call emulation is enabled.
+                    // TODO: Move to carrier config
+                    () -> true);
 
     private ComponentName mExpectedComponentName = null;
     private RadioOnHelper mRadioOnHelper;
diff --git a/tests/src/com/android/services/telephony/ImsConferenceControllerTest.java b/tests/src/com/android/services/telephony/ImsConferenceControllerTest.java
index 229bdee..aa832aa 100644
--- a/tests/src/com/android/services/telephony/ImsConferenceControllerTest.java
+++ b/tests/src/com/android/services/telephony/ImsConferenceControllerTest.java
@@ -47,6 +47,9 @@
 
     private TelecomAccountRegistry mTelecomAccountRegistry;
 
+    @Mock
+    private TelecomAccountRegistry mMockTelecomAccountRegistry;
+
     private TestTelephonyConnection mTestTelephonyConnectionA;
     private TestTelephonyConnection mTestTelephonyConnectionB;
 
@@ -63,7 +66,7 @@
         mTestTelephonyConnectionB = new TestTelephonyConnection();
 
         mControllerTest = new ImsConferenceController(mTelecomAccountRegistry,
-                mMockTelephonyConnectionServiceProxy);
+                mMockTelephonyConnectionServiceProxy, () -> false);
     }
 
     /**
@@ -78,7 +81,6 @@
     @Test
     @SmallTest
     public void testConferenceable() {
-
         mControllerTest.add(mTestTelephonyConnectionB);
         mControllerTest.add(mTestTelephonyConnectionA);
 
@@ -112,7 +114,6 @@
     @Test
     @SmallTest
     public void testMergeMultiPartyCalls() {
-
         when(mTestTelephonyConnectionA.mMockRadioConnection.getPhoneType())
                 .thenReturn(PhoneConstants.PHONE_TYPE_IMS);
         when(mTestTelephonyConnectionB.mMockRadioConnection.getPhoneType())
diff --git a/tests/src/com/android/services/telephony/ImsConferenceTest.java b/tests/src/com/android/services/telephony/ImsConferenceTest.java
new file mode 100644
index 0000000..56a6240
--- /dev/null
+++ b/tests/src/com/android/services/telephony/ImsConferenceTest.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.services.telephony;
+
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.times;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+import android.net.Uri;
+import android.os.Looper;
+import android.telecom.ConferenceParticipant;
+import android.telecom.Connection;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.internal.telephony.PhoneConstants;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.util.Arrays;
+
+public class ImsConferenceTest {
+    @Mock
+    private TelephonyConnectionServiceProxy mMockTelephonyConnectionServiceProxy;
+
+    @Mock
+    private TelecomAccountRegistry mMockTelecomAccountRegistry;
+
+    @Mock
+    private com.android.internal.telephony.Connection mOriginalConnection;
+
+    private TestTelephonyConnection mConferenceHost;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        if (Looper.myLooper() == null) {
+            Looper.prepare();
+        }
+        mConferenceHost = new TestTelephonyConnection();
+        mConferenceHost.setManageImsConferenceCallSupported(true);
+    }
+
+    @Test
+    @SmallTest
+    public void testSinglePartyEmulation() {
+        ImsConference imsConference = new ImsConference(mMockTelecomAccountRegistry,
+                mMockTelephonyConnectionServiceProxy, mConferenceHost,
+                null /* phoneAccountHandle */, () -> true /* featureFlagProxy */);
+
+        ConferenceParticipant participant1 = new ConferenceParticipant(
+                Uri.parse("tel:6505551212"),
+                "A",
+                Uri.parse("sip:6505551212@testims.com"),
+                Connection.STATE_ACTIVE);
+        ConferenceParticipant participant2 = new ConferenceParticipant(
+                Uri.parse("tel:6505551213"),
+                "A",
+                Uri.parse("sip:6505551213@testims.com"),
+                Connection.STATE_ACTIVE);
+        imsConference.handleConferenceParticipantsUpdate(mConferenceHost,
+                Arrays.asList(participant1, participant2));
+        assertEquals(2, imsConference.getNumberOfParticipants());
+
+        // Because we're pretending its a single party, there should be no participants any more.
+        imsConference.handleConferenceParticipantsUpdate(mConferenceHost,
+                Arrays.asList(participant1));
+        assertEquals(0, imsConference.getNumberOfParticipants());
+
+        // Back to 2!
+        imsConference.handleConferenceParticipantsUpdate(mConferenceHost,
+                Arrays.asList(participant1, participant2));
+        assertEquals(2, imsConference.getNumberOfParticipants());
+    }
+
+    @Test
+    @SmallTest
+    public void testNormalConference() {
+        ImsConference imsConference = new ImsConference(mMockTelecomAccountRegistry,
+                mMockTelephonyConnectionServiceProxy, mConferenceHost,
+                null /* phoneAccountHandle */, () -> false /* featureFlagProxy */);
+
+        ConferenceParticipant participant1 = new ConferenceParticipant(
+                Uri.parse("tel:6505551212"),
+                "A",
+                Uri.parse("sip:6505551212@testims.com"),
+                Connection.STATE_ACTIVE);
+        ConferenceParticipant participant2 = new ConferenceParticipant(
+                Uri.parse("tel:6505551213"),
+                "A",
+                Uri.parse("sip:6505551213@testims.com"),
+                Connection.STATE_ACTIVE);
+        imsConference.handleConferenceParticipantsUpdate(mConferenceHost,
+                Arrays.asList(participant1, participant2));
+        assertEquals(2, imsConference.getNumberOfParticipants());
+
+        // Not emulating single party; should still be one.
+        imsConference.handleConferenceParticipantsUpdate(mConferenceHost,
+                Arrays.asList(participant1));
+        assertEquals(1, imsConference.getNumberOfParticipants());
+    }
+}
diff --git a/tests/src/com/android/services/telephony/TestTelephonyConnection.java b/tests/src/com/android/services/telephony/TestTelephonyConnection.java
index 39e4579..c064ef6 100644
--- a/tests/src/com/android/services/telephony/TestTelephonyConnection.java
+++ b/tests/src/com/android/services/telephony/TestTelephonyConnection.java
@@ -17,10 +17,12 @@
 package com.android.services.telephony;
 
 import android.content.Context;
+import android.content.res.Resources;
 import android.os.Bundle;
 import android.telecom.PhoneAccountHandle;
 
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
@@ -28,6 +30,7 @@
 import com.android.internal.telephony.Call;
 import com.android.internal.telephony.Connection;
 import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
 
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
@@ -50,6 +53,9 @@
     @Mock
     Context mMockContext;
 
+    @Mock
+    Resources mMockResources;
+
     private Phone mMockPhone;
     private int mNotifyPhoneAccountChangedCount = 0;
     private List<String> mLastConnectionEvents = new ArrayList<>();
@@ -66,14 +72,18 @@
 
         mMockPhone = mock(Phone.class);
         mMockContext = mock(Context.class);
+        mOriginalConnection = mock(Connection.class);
         // Set up mMockRadioConnection and mMockPhone to contain an active call
         when(mMockRadioConnection.getState()).thenReturn(Call.State.ACTIVE);
         when(mMockRadioConnection.getCall()).thenReturn(mMockCall);
+        when(mMockRadioConnection.getPhoneType()).thenReturn(PhoneConstants.PHONE_TYPE_IMS);
         doNothing().when(mMockRadioConnection).addListener(any(Connection.Listener.class));
         doNothing().when(mMockRadioConnection).addPostDialListener(
                 any(Connection.PostDialListener.class));
         when(mMockPhone.getRingingCall()).thenReturn(mMockCall);
-        when(mMockPhone.getContext()).thenReturn(null);
+        when(mMockPhone.getContext()).thenReturn(mMockContext);
+        when(mMockContext.getResources()).thenReturn(mMockResources);
+        when(mMockResources.getBoolean(anyInt())).thenReturn(false);
         when(mMockPhone.getDefaultPhone()).thenReturn(mMockPhone);
         when(mMockCall.getState()).thenReturn(Call.State.ACTIVE);
         when(mMockCall.getPhone()).thenReturn(mMockPhone);