Support local disconnect of empty IMS Conference.
Add support for locally disconnecting an IMS conference when the
participant count goes to 0.
Refactor carrier config access in ImsConference to use a builder passed
in, facilitating easier testing and abstraction of carrier config from
the ImsConference class.
Test: Add unit test to verify local disconnect when participant count is
zero.
Test: Use conference event package test cmd/intents to inject test CEP to
simulate a conference dropped to 0 participants. Verify IMS conference is
disconnected.
Bug: 151707520
Fixes: 154245549
Merged-In: Ie88bf2f6cfc6b7146b3d04cbeab377f9322e349d
Change-Id: I024b6e8705640460c88d8418256b59beaa90362f
diff --git a/src/com/android/services/telephony/ImsConference.java b/src/com/android/services/telephony/ImsConference.java
index c11a1ca..9f52e63 100644
--- a/src/com/android/services/telephony/ImsConference.java
+++ b/src/com/android/services/telephony/ImsConference.java
@@ -20,7 +20,6 @@
import android.graphics.drawable.Icon;
import android.net.Uri;
import android.os.Bundle;
-import android.os.PersistableBundle;
import android.telecom.Connection;
import android.telecom.Connection.VideoProvider;
import android.telecom.DisconnectCause;
@@ -28,7 +27,6 @@
import android.telecom.StatusHints;
import android.telecom.TelecomManager;
import android.telecom.VideoProfile;
-import android.telephony.CarrierConfigManager;
import android.telephony.PhoneNumberUtils;
import android.util.Pair;
@@ -38,7 +36,6 @@
import com.android.internal.telephony.CallStateException;
import com.android.internal.telephony.Phone;
import com.android.internal.telephony.PhoneConstants;
-import com.android.phone.PhoneGlobals;
import com.android.phone.PhoneUtils;
import com.android.phone.R;
import com.android.telephony.Rlog;
@@ -79,6 +76,126 @@
}
/**
+ * Abstracts out carrier configuration items specific to the conference.
+ */
+ public static class CarrierConfiguration {
+ /**
+ * Builds and instance of {@link CarrierConfiguration}.
+ */
+ public static class Builder {
+ private boolean mIsMaximumConferenceSizeEnforced = false;
+ private int mMaximumConferenceSize = 5;
+ private boolean mShouldLocalDisconnectEmptyConference = false;
+ private boolean mIsHoldAllowed = false;
+
+ /**
+ * Sets whether the maximum size of the conference is enforced.
+ * @param isMaximumConferenceSizeEnforced {@code true} if conference size enforced.
+ * @return builder instance.
+ */
+ public Builder setIsMaximumConferenceSizeEnforced(
+ boolean isMaximumConferenceSizeEnforced) {
+ mIsMaximumConferenceSizeEnforced = isMaximumConferenceSizeEnforced;
+ return this;
+ }
+
+ /**
+ * Sets the maximum size of an IMS conference.
+ * @param maximumConferenceSize Max conference size.
+ * @return builder instance.
+ */
+ public Builder setMaximumConferenceSize(int maximumConferenceSize) {
+ mMaximumConferenceSize = maximumConferenceSize;
+ return this;
+ }
+
+ /**
+ * Sets whether an empty conference should be locally disconnected.
+ * @param shouldLocalDisconnectEmptyConference {@code true} if conference should be
+ * locally disconnected if empty.
+ * @return builder instance.
+ */
+ public Builder setShouldLocalDisconnectEmptyConference(
+ boolean shouldLocalDisconnectEmptyConference) {
+ mShouldLocalDisconnectEmptyConference = shouldLocalDisconnectEmptyConference;
+ return this;
+ }
+
+ /**
+ * Sets whether holding the conference is allowed.
+ * @param isHoldAllowed {@code true} if holding is allowed.
+ * @return builder instance.
+ */
+ public Builder setIsHoldAllowed(boolean isHoldAllowed) {
+ mIsHoldAllowed = isHoldAllowed;
+ return this;
+ }
+
+ /**
+ * Build instance of {@link CarrierConfiguration}.
+ * @return carrier config instance.
+ */
+ public ImsConference.CarrierConfiguration build() {
+ return new ImsConference.CarrierConfiguration(mIsMaximumConferenceSizeEnforced,
+ mMaximumConferenceSize, mShouldLocalDisconnectEmptyConference,
+ mIsHoldAllowed);
+ }
+ }
+
+ private CarrierConfiguration(boolean isMaximumConferenceSizeEnforced,
+ int maximumConferenceSize, boolean shouldLocalDisconnectEmptyConference,
+ boolean isHoldAllowed) {
+ mIsMaximumConferenceSizeEnforced = isMaximumConferenceSizeEnforced;
+ mMaximumConferenceSize = maximumConferenceSize;
+ mShouldLocalDisconnectEmptyConference = shouldLocalDisconnectEmptyConference;
+ mIsHoldAllowed = isHoldAllowed;
+ }
+
+ private boolean mIsMaximumConferenceSizeEnforced;
+
+ private int mMaximumConferenceSize;
+
+ private boolean mShouldLocalDisconnectEmptyConference;
+
+ private boolean mIsHoldAllowed;
+
+ /**
+ * Determines whether the {@link ImsConference} should enforce a size limit based on
+ * {@link #getMaximumConferenceSize()}.
+ * {@code true} if maximum size limit should be enforced, {@code false} otherwise.
+ */
+ public boolean isMaximumConferenceSizeEnforced() {
+ return mIsMaximumConferenceSizeEnforced;
+ }
+
+ /**
+ * Determines the maximum number of participants (not including the host) in a conference
+ * which is enforced when {@link #isMaximumConferenceSizeEnforced()} is {@code true}.
+ */
+ public int getMaximumConferenceSize() {
+ return mMaximumConferenceSize;
+ }
+
+ /**
+ * Determines whether this {@link ImsConference} should locally disconnect itself when the
+ * number of participants in the conference drops to zero.
+ * {@code true} if empty conference should be locally disconnected, {@code false}
+ * otherwise.
+ */
+ public boolean shouldLocalDisconnectEmptyConference() {
+ return mShouldLocalDisconnectEmptyConference;
+ }
+
+ /**
+ * Determines whether holding the conference is permitted or not.
+ * {@code true} if hold is permitted, {@code false} otherwise.
+ */
+ public boolean isHoldAllowed() {
+ return mIsHoldAllowed;
+ }
+ }
+
+ /**
* Listener used to respond to changes to the underlying radio connection for the conference
* host connection. Used to respond to SRVCC changes.
*/
@@ -260,6 +377,7 @@
private boolean mIsHoldable;
private boolean mCouldManageConference;
private FeatureFlagProxy mFeatureFlagProxy;
+ private final CarrierConfiguration mCarrierConfig;
private boolean mIsEmulatingSinglePartyCall = false;
private boolean mIsUsingSimCallManager = false;
@@ -301,12 +419,13 @@
public ImsConference(TelecomAccountRegistry telecomAccountRegistry,
TelephonyConnectionServiceProxy telephonyConnectionService,
TelephonyConnection conferenceHost, PhoneAccountHandle phoneAccountHandle,
- FeatureFlagProxy featureFlagProxy) {
+ FeatureFlagProxy featureFlagProxy, CarrierConfiguration carrierConfig) {
super(phoneAccountHandle);
mTelecomAccountRegistry = telecomAccountRegistry;
mFeatureFlagProxy = featureFlagProxy;
+ mCarrierConfig = carrierConfig;
// Specify the connection time of the conference to be the connection time of the original
// connection.
@@ -323,7 +442,7 @@
int capabilities = Connection.CAPABILITY_MUTE |
Connection.CAPABILITY_CONFERENCE_HAS_NO_CHILDREN;
- if (canHoldImsCalls()) {
+ if (mCarrierConfig.isHoldAllowed()) {
capabilities |= Connection.CAPABILITY_SUPPORT_HOLD | Connection.CAPABILITY_HOLD;
mIsHoldable = true;
}
@@ -491,6 +610,8 @@
} catch (CallStateException e) {
Log.e(this, e, "Exception thrown trying to hangup conference");
}
+ } else {
+ Log.w(this, "onDisconnect - null call");
}
}
@@ -935,6 +1056,14 @@
if (newParticipantsAdded || oldParticipantsRemoved) {
updateManageConference();
}
+
+ // If the conference is empty and we're supposed to do a local disconnect, do so now.
+ if (mCarrierConfig.shouldLocalDisconnectEmptyConference()
+ && oldParticipantCount > 0 && newParticipantCount == 0) {
+ Log.i(this, "handleConferenceParticipantsUpdate: empty conference; "
+ + "local disconnect.");
+ onDisconnect();
+ }
}
}
@@ -1346,51 +1475,6 @@
return sb.toString();
}
- private boolean canHoldImsCalls() {
- PersistableBundle b = getCarrierConfig();
- // Return true if the CarrierConfig is unavailable
- return b == null || b.getBoolean(CarrierConfigManager.KEY_ALLOW_HOLD_IN_IMS_CALL_BOOL);
- }
-
- private PersistableBundle getCarrierConfig() {
- if (mConferenceHost == null) {
- return null;
- }
-
- Phone phone = mConferenceHost.getPhone();
- if (phone == null) {
- return null;
- }
- return PhoneGlobals.getInstance().getCarrierConfigForSubId(phone.getSubId());
- }
-
- /**
- * @return {@code true} if the carrier associated with the conference requires that the maximum
- * size of the conference is enforced, {@code false} otherwise.
- */
- public boolean isMaximumConferenceSizeEnforced() {
- PersistableBundle b = getCarrierConfig();
- // Return false if the CarrierConfig is unavailable
- return b != null && b.getBoolean(
- CarrierConfigManager.KEY_IS_IMS_CONFERENCE_SIZE_ENFORCED_BOOL);
- }
-
- /**
- * @return The maximum size of a conference call where
- * {@link #isMaximumConferenceSizeEnforced()} is true.
- */
- public int getMaximumConferenceSize() {
- PersistableBundle b = getCarrierConfig();
-
- // If there is no carrier config its really a problem, but we'll still define a sane limit
- // of 5 so that we can still make a conference.
- if (b == null) {
- Log.w(this, "getMaximumConferenceSize - failed to get conference size");
- return 5;
- }
- return b.getInt(CarrierConfigManager.KEY_IMS_CONFERENCE_SIZE_LIMIT_INT);
- }
-
/**
* @return The number of participants in the conference.
*/
@@ -1403,8 +1487,8 @@
* participants in the conference has reached the limit, {@code false} otherwise.
*/
public boolean isFullConference() {
- return isMaximumConferenceSizeEnforced()
- && getNumberOfParticipants() >= getMaximumConferenceSize();
+ return mCarrierConfig.isMaximumConferenceSizeEnforced()
+ && getNumberOfParticipants() >= mCarrierConfig.getMaximumConferenceSize();
}
/**
diff --git a/src/com/android/services/telephony/ImsConferenceController.java b/src/com/android/services/telephony/ImsConferenceController.java
index 8789ba8..1d1c5d8 100644
--- a/src/com/android/services/telephony/ImsConferenceController.java
+++ b/src/com/android/services/telephony/ImsConferenceController.java
@@ -16,12 +16,16 @@
package com.android.services.telephony;
+import android.content.Context;
+import android.os.PersistableBundle;
import android.telecom.Conference;
import android.telecom.Conferenceable;
import android.telecom.Connection;
import android.telecom.ConnectionService;
import android.telecom.DisconnectCause;
import android.telecom.PhoneAccountHandle;
+import android.telephony.CarrierConfigManager;
+
import com.android.telephony.Rlog;
import com.android.internal.telephony.Phone;
@@ -403,6 +407,7 @@
PhoneAccountHandle phoneAccountHandle = null;
// Attempt to determine the phone account associated with the conference host connection.
+ ImsConference.CarrierConfiguration carrierConfig = null;
if (connection.getPhone() != null &&
connection.getPhone().getPhoneType() == PhoneConstants.PHONE_TYPE_IMS) {
Phone imsPhone = connection.getPhone();
@@ -410,10 +415,11 @@
// base GSM or CDMA phone, not on the ImsPhone itself).
phoneAccountHandle =
PhoneUtils.makePstnPhoneAccountHandle(imsPhone.getDefaultPhone());
+ carrierConfig = getCarrierConfig(imsPhone);
}
ImsConference conference = new ImsConference(mTelecomAccountRegistry, mConnectionService,
- conferenceHostConnection, phoneAccountHandle, mFeatureFlagProxy);
+ conferenceHostConnection, phoneAccountHandle, mFeatureFlagProxy, carrierConfig);
conference.setState(conferenceHostConnection.getState());
conference.addTelephonyConferenceListener(mConferenceListener);
conference.updateConferenceParticipantsAfterCreation();
@@ -433,4 +439,33 @@
// conferenceable connections for the conference to show merge calls option.
recalculateConferenceable();
}
+
+ public static ImsConference.CarrierConfiguration getCarrierConfig(Phone phone) {
+ ImsConference.CarrierConfiguration.Builder config =
+ new ImsConference.CarrierConfiguration.Builder();
+ if (phone == null) {
+ return config.build();
+ }
+
+ CarrierConfigManager cfgManager = (CarrierConfigManager)
+ phone.getContext().getSystemService(Context.CARRIER_CONFIG_SERVICE);
+ if (cfgManager != null) {
+ PersistableBundle bundle = cfgManager.getConfigForSubId(phone.getSubId());
+ boolean isMaximumConferenceSizeEnforced = bundle.getBoolean(
+ CarrierConfigManager.KEY_IS_IMS_CONFERENCE_SIZE_ENFORCED_BOOL);
+ int maximumConferenceSize = bundle.getInt(
+ CarrierConfigManager.KEY_IMS_CONFERENCE_SIZE_LIMIT_INT);
+ boolean isHoldAllowed = bundle.getBoolean(
+ CarrierConfigManager.KEY_ALLOW_HOLD_IN_IMS_CALL_BOOL);
+ boolean shouldLocalDisconnectOnEmptyConference = bundle.getBoolean(
+ CarrierConfigManager.KEY_LOCAL_DISCONNECT_EMPTY_IMS_CONFERENCE_BOOL);
+
+ config.setIsMaximumConferenceSizeEnforced(isMaximumConferenceSizeEnforced)
+ .setMaximumConferenceSize(maximumConferenceSize)
+ .setIsHoldAllowed(isHoldAllowed)
+ .setShouldLocalDisconnectEmptyConference(
+ shouldLocalDisconnectOnEmptyConference);
+ }
+ return config.build();
+ }
}
diff --git a/src/com/android/services/telephony/TelephonyConnectionService.java b/src/com/android/services/telephony/TelephonyConnectionService.java
index eb6482f..63d74d1 100644
--- a/src/com/android/services/telephony/TelephonyConnectionService.java
+++ b/src/com/android/services/telephony/TelephonyConnectionService.java
@@ -585,9 +585,11 @@
}
TelephonyConnection connection = (TelephonyConnection)conn;
+
ImsConference conference = new ImsConference(TelecomAccountRegistry.getInstance(this),
mTelephonyConnectionServiceProxy, connection,
- phoneAccountHandle, () -> true);
+ phoneAccountHandle, () -> true,
+ ImsConferenceController.getCarrierConfig(connection.getPhone()));
mImsConferenceController.addConference(conference);
conference.setVideoState(connection,
connection.getVideoState());
diff --git a/tests/src/com/android/services/telephony/ImsConferenceTest.java b/tests/src/com/android/services/telephony/ImsConferenceTest.java
index 7251402..3e7f541 100644
--- a/tests/src/com/android/services/telephony/ImsConferenceTest.java
+++ b/tests/src/com/android/services/telephony/ImsConferenceTest.java
@@ -43,6 +43,7 @@
import org.mockito.MockitoAnnotations;
import java.util.Arrays;
+import java.util.Collections;
public class ImsConferenceTest {
@Mock
@@ -73,7 +74,8 @@
ImsConference imsConference = new ImsConference(mMockTelecomAccountRegistry,
mMockTelephonyConnectionServiceProxy, mConferenceHost,
- null /* phoneAccountHandle */, () -> true /* featureFlagProxy */);
+ null /* phoneAccountHandle */, () -> true /* featureFlagProxy */,
+ new ImsConference.CarrierConfiguration.Builder().build());
ConferenceParticipant participant1 = new ConferenceParticipant(
Uri.parse("tel:6505551212"),
@@ -120,7 +122,8 @@
ImsConference imsConference = new ImsConference(mMockTelecomAccountRegistry,
mMockTelephonyConnectionServiceProxy, mConferenceHost,
- null /* phoneAccountHandle */, () -> true /* featureFlagProxy */);
+ null /* phoneAccountHandle */, () -> true /* featureFlagProxy */,
+ new ImsConference.CarrierConfiguration.Builder().build());
// Start off with 3 participants.
ConferenceParticipant participant1 = new ConferenceParticipant(
@@ -183,7 +186,8 @@
ImsConference imsConference = new ImsConference(mMockTelecomAccountRegistry,
mMockTelephonyConnectionServiceProxy, mConferenceHost,
- null /* phoneAccountHandle */, () -> true /* featureFlagProxy */);
+ null /* phoneAccountHandle */, () -> true /* featureFlagProxy */,
+ new ImsConference.CarrierConfiguration.Builder().build());
// Start off with 3 participants.
ConferenceParticipant participant1 = new ConferenceParticipant(
@@ -240,7 +244,8 @@
ImsConference imsConference = new ImsConference(mMockTelecomAccountRegistry,
mMockTelephonyConnectionServiceProxy, mConferenceHost,
- null /* phoneAccountHandle */, () -> true /* featureFlagProxy */);
+ null /* phoneAccountHandle */, () -> true /* featureFlagProxy */,
+ new ImsConference.CarrierConfiguration.Builder().build());
// Setup the initial conference state with 2 participants.
ConferenceParticipant participant1 = new ConferenceParticipant(
@@ -298,7 +303,8 @@
ImsConference imsConference = new ImsConference(mMockTelecomAccountRegistry,
mMockTelephonyConnectionServiceProxy, mConferenceHost,
- null /* phoneAccountHandle */, () -> true /* featureFlagProxy */);
+ null /* phoneAccountHandle */, () -> true /* featureFlagProxy */,
+ new ImsConference.CarrierConfiguration.Builder().build());
final boolean[] isConferenceState = new boolean[1];
TelephonyConferenceBase.TelephonyConferenceListener conferenceListener =
@@ -354,7 +360,8 @@
ImsConference imsConference = new ImsConference(mMockTelecomAccountRegistry,
mMockTelephonyConnectionServiceProxy, mConferenceHost,
- null /* phoneAccountHandle */, () -> true /* featureFlagProxy */);
+ null /* phoneAccountHandle */, () -> true /* featureFlagProxy */,
+ new ImsConference.CarrierConfiguration.Builder().build());
ConferenceParticipant participant1 = new ConferenceParticipant(
Uri.parse("tel:6505551212"),
@@ -379,7 +386,8 @@
ImsConference imsConference = new ImsConference(mMockTelecomAccountRegistry,
mMockTelephonyConnectionServiceProxy, mConferenceHost,
- null /* phoneAccountHandle */, () -> true /* featureFlagProxy */);
+ null /* phoneAccountHandle */, () -> true /* featureFlagProxy */,
+ new ImsConference.CarrierConfiguration.Builder().build());
ConferenceParticipant participant1 = new ConferenceParticipant(
Uri.parse("tel:6505551212"),
@@ -410,13 +418,14 @@
@Test
@SmallTest
- public void testNormalConference() {
+ public void testNormalConference() throws Exception {
when(mMockTelecomAccountRegistry.isUsingSimCallManager(any(PhoneAccountHandle.class)))
.thenReturn(false);
ImsConference imsConference = new ImsConference(mMockTelecomAccountRegistry,
mMockTelephonyConnectionServiceProxy, mConferenceHost,
- null /* phoneAccountHandle */, () -> false /* featureFlagProxy */);
+ null /* phoneAccountHandle */, () -> false /* featureFlagProxy */,
+ new ImsConference.CarrierConfiguration.Builder().build());
ConferenceParticipant participant1 = new ConferenceParticipant(
Uri.parse("tel:6505551212"),
@@ -438,5 +447,45 @@
imsConference.handleConferenceParticipantsUpdate(mConferenceHost,
Arrays.asList(participant1));
assertEquals(1, imsConference.getNumberOfParticipants());
+
+ // Drop to 0 participants; should not hangup the conf now
+ imsConference.handleConferenceParticipantsUpdate(mConferenceHost, Collections.emptyList());
+ assertEquals(0, imsConference.getNumberOfParticipants());
+ verify(mConferenceHost.mMockCall, never()).hangup();
+ }
+
+ @Test
+ @SmallTest
+ public void testLocalDisconnectOnEmptyConference() throws Exception {
+ when(mMockTelecomAccountRegistry.isUsingSimCallManager(any(PhoneAccountHandle.class)))
+ .thenReturn(false);
+
+ ImsConference imsConference = new ImsConference(mMockTelecomAccountRegistry,
+ mMockTelephonyConnectionServiceProxy, mConferenceHost,
+ null /* phoneAccountHandle */, () -> false /* featureFlagProxy */,
+ 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 0 participants; should have a hangup request.
+ imsConference.handleConferenceParticipantsUpdate(mConferenceHost, Collections.emptyList());
+ assertEquals(0, imsConference.getNumberOfParticipants());
+ verify(mConferenceHost.mMockCall).hangup();
}
}
diff --git a/tests/src/com/android/services/telephony/TestTelephonyConnection.java b/tests/src/com/android/services/telephony/TestTelephonyConnection.java
index 5b31c0f..8b7a477 100644
--- a/tests/src/com/android/services/telephony/TestTelephonyConnection.java
+++ b/tests/src/com/android/services/telephony/TestTelephonyConnection.java
@@ -30,6 +30,7 @@
import static org.mockito.Mockito.when;
import com.android.internal.telephony.Call;
+import com.android.internal.telephony.CallStateException;
import com.android.internal.telephony.Connection;
import com.android.internal.telephony.Phone;
import com.android.internal.telephony.PhoneConstants;
@@ -72,6 +73,11 @@
return mMockRadioConnection;
}
+ @Override
+ protected Call getCall() {
+ return mMockCall;
+ }
+
public TestTelephonyConnection() {
super(null, null, false);
MockitoAnnotations.initMocks(this);
@@ -100,6 +106,11 @@
when(mMockPhone.getPhoneType()).thenReturn(PhoneConstants.PHONE_TYPE_IMS);
when(mMockCall.getState()).thenReturn(Call.State.ACTIVE);
when(mMockCall.getPhone()).thenReturn(mMockPhone);
+ try {
+ doNothing().when(mMockCall).hangup();
+ } catch (CallStateException e) {
+ e.printStackTrace();
+ }
}
@Override