[TelephonyService] Improve hold capability signal The CAPABILITY_HOLD perpority is not set properly in telephony call(If there are two top calls, both of them still holdable). This add a hold tracker to track all telephony connections/conference and set the holdable state of them. The connections/conference itself can use the holdable state and the call state to determine if CAPABILITY_HOLD should be set. Test: manully test and unit test Bug: 66949982 Change-Id: I43fde6fcbc047c09ba375c6c8fd5ac374bf7fb70
diff --git a/src/com/android/services/telephony/CdmaConference.java b/src/com/android/services/telephony/CdmaConference.java index 19572e9..69ff2a4 100755 --- a/src/com/android/services/telephony/CdmaConference.java +++ b/src/com/android/services/telephony/CdmaConference.java
@@ -26,16 +26,16 @@ import com.android.internal.telephony.Call; import com.android.internal.telephony.CallStateException; import com.android.phone.PhoneGlobals; -import com.android.phone.common.R; import java.util.List; /** * CDMA-based conference call. */ -public class CdmaConference extends Conference { +public class CdmaConference extends Conference implements Holdable { private int mCapabilities; private int mProperties; + private boolean mIsHoldable; public CdmaConference(PhoneAccountHandle phoneAccount) { super(phoneAccount); @@ -43,6 +43,8 @@ mProperties = Connection.PROPERTY_GENERIC_CONFERENCE; setConnectionProperties(mProperties); + + mIsHoldable = false; } public void updateCapabilities(int capabilities) { @@ -199,4 +201,17 @@ } return (CdmaConnection) connections.get(0); } + + @Override + public void setHoldable(boolean isHoldable) { + // Since the CDMA-based conference can't not be held, dont update the capability when this + // method called. + mIsHoldable = isHoldable; + } + + @Override + public boolean isChildHoldable() { + // The conference can not be a child of other conference. + return false; + } }
diff --git a/src/com/android/services/telephony/GsmConnection.java b/src/com/android/services/telephony/GsmConnection.java index ca547fa..0a58fba 100644 --- a/src/com/android/services/telephony/GsmConnection.java +++ b/src/com/android/services/telephony/GsmConnection.java
@@ -76,7 +76,7 @@ // hold for IMS calls. if (!shouldTreatAsEmergencyCall()) { capabilities |= CAPABILITY_SUPPORT_HOLD; - if (getState() == STATE_ACTIVE || getState() == STATE_HOLDING) { + if (isHoldable() && (getState() == STATE_ACTIVE || getState() == STATE_HOLDING)) { capabilities |= CAPABILITY_HOLD; } }
diff --git a/src/com/android/services/telephony/HoldTracker.java b/src/com/android/services/telephony/HoldTracker.java new file mode 100644 index 0000000..805802f --- /dev/null +++ b/src/com/android/services/telephony/HoldTracker.java
@@ -0,0 +1,88 @@ +/* + * Copyright (C) 2017 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 android.telecom.Log; +import android.telecom.PhoneAccountHandle; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @hide + */ +public class HoldTracker { + private final Map<PhoneAccountHandle, List<Holdable>> mHoldables; + + public HoldTracker() { + mHoldables = new HashMap<>(); + } + + /** + * Adds the holdable associated with the {@code phoneAccountHandle}, this method may update + * the hold state for all holdable associated with the {@code phoneAccountHandle}. + */ + public void addHoldable(PhoneAccountHandle phoneAccountHandle, Holdable holdable) { + if (!mHoldables.containsKey(phoneAccountHandle)) { + mHoldables.put(phoneAccountHandle, new ArrayList<>(1)); + } + List<Holdable> holdables = mHoldables.get(phoneAccountHandle); + if (!holdables.contains(holdable)) { + holdables.add(holdable); + updateHoldCapability(phoneAccountHandle); + } + } + + /** + * Removes the holdable associated with the {@code phoneAccountHandle}, this method may update + * the hold state for all holdable associated with the {@code phoneAccountHandle}. + */ + public void removeHoldable(PhoneAccountHandle phoneAccountHandle, Holdable holdable) { + if (!mHoldables.containsKey(phoneAccountHandle)) { + return; + } + + if (mHoldables.get(phoneAccountHandle).remove(holdable)) { + updateHoldCapability(phoneAccountHandle); + } + } + + /** + * Updates the hold capability for all holdables associated with the {@code phoneAccountHandle}. + */ + public void updateHoldCapability(PhoneAccountHandle phoneAccountHandle) { + if (!mHoldables.containsKey(phoneAccountHandle)) { + return; + } + + List<Holdable> holdables = mHoldables.get(phoneAccountHandle); + int topHoldableCount = 0; + for (Holdable holdable : holdables) { + if (!holdable.isChildHoldable()) { + ++topHoldableCount; + } + } + + Log.d(this, "topHoldableCount = " + topHoldableCount); + boolean isHoldable = topHoldableCount < 2; + for (Holdable holdable : holdables) { + holdable.setHoldable(holdable.isChildHoldable() ? false : isHoldable); + } + } +}
diff --git a/src/com/android/services/telephony/Holdable.java b/src/com/android/services/telephony/Holdable.java new file mode 100644 index 0000000..4002d30 --- /dev/null +++ b/src/com/android/services/telephony/Holdable.java
@@ -0,0 +1,32 @@ +/* + * Copyright (C) 2017 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; + +/** The inference used to track the hold state of a holdable object. */ +public interface Holdable { + + /** Returns true if this holdable is a child node of other holdable. */ + boolean isChildHoldable(); + + /** + * Sets the holdable property for a holdable object. + * + * @param isHoldable true means this holdable object can be held. + */ + void setHoldable(boolean isHoldable); +} +
diff --git a/src/com/android/services/telephony/ImsConference.java b/src/com/android/services/telephony/ImsConference.java index 06bc06f..61c7a72 100644 --- a/src/com/android/services/telephony/ImsConference.java +++ b/src/com/android/services/telephony/ImsConference.java
@@ -65,7 +65,7 @@ * connection and is responsible for managing the conference participant connections which represent * the participants. */ -public class ImsConference extends Conference { +public class ImsConference extends Conference implements Holdable { /** * Listener used to respond to changes to conference participants. At the conference level we @@ -240,6 +240,8 @@ */ private final Object mUpdateSyncRoot = new Object(); + private boolean mIsHoldable; + public void updateConferenceParticipantsAfterCreation() { if (mConferenceHost != null) { Log.v(this, "updateConferenceStateAfterCreation :: process participant update"); @@ -283,6 +285,7 @@ Connection.CAPABILITY_CONFERENCE_HAS_NO_CHILDREN; if (canHoldImsCalls()) { capabilities |= Connection.CAPABILITY_SUPPORT_HOLD | Connection.CAPABILITY_HOLD; + mIsHoldable = true; } capabilities = applyHostCapabilities(capabilities, mConferenceHost.getConnectionCapabilities(), @@ -508,6 +511,22 @@ // No-op } + @Override + public void setHoldable(boolean isHoldable) { + mIsHoldable = isHoldable; + if (!mIsHoldable) { + removeCapability(Connection.CAPABILITY_HOLD); + } else { + addCapability(Connection.CAPABILITY_HOLD); + } + } + + @Override + public boolean isChildHoldable() { + // The conference should not be a child of other conference. + return false; + } + /** * Changes a bit-mask to add or remove a bit-field. *
diff --git a/src/com/android/services/telephony/TelephonyConference.java b/src/com/android/services/telephony/TelephonyConference.java index e379f38..c66d6f2 100644 --- a/src/com/android/services/telephony/TelephonyConference.java +++ b/src/com/android/services/telephony/TelephonyConference.java
@@ -30,7 +30,9 @@ * TelephonyConnection-based conference call for GSM conferences and IMS conferences (which may * be either GSM-based or CDMA-based). */ -public class TelephonyConference extends Conference { +public class TelephonyConference extends Conference implements Holdable { + + private boolean mIsHoldable; public TelephonyConference(PhoneAccountHandle phoneAccount) { super(phoneAccount); @@ -40,6 +42,7 @@ Connection.CAPABILITY_MUTE | Connection.CAPABILITY_MANAGE_CONFERENCE); setActive(); + mIsHoldable = true; } /** @@ -176,6 +179,22 @@ return primaryConnection; } + @Override + public void setHoldable(boolean isHoldable) { + mIsHoldable = isHoldable; + if (!mIsHoldable) { + removeCapability(Connection.CAPABILITY_HOLD); + } else { + addCapability(Connection.CAPABILITY_HOLD); + } + } + + @Override + public boolean isChildHoldable() { + // The conference should not be a child of other conference. + return false; + } + private Call getMultipartyCallForConnection(Connection connection, String tag) { com.android.internal.telephony.Connection radioConnection = getOriginalConnection(connection);
diff --git a/src/com/android/services/telephony/TelephonyConnection.java b/src/com/android/services/telephony/TelephonyConnection.java index d5ff043..c1f65dd 100644 --- a/src/com/android/services/telephony/TelephonyConnection.java +++ b/src/com/android/services/telephony/TelephonyConnection.java
@@ -33,7 +33,6 @@ import android.telecom.TelecomManager; import android.telecom.VideoProfile; import android.telephony.CarrierConfigManager; -import android.telephony.DisconnectCause; import android.telephony.PhoneNumberUtils; import android.telephony.TelephonyManager; import android.util.Pair; @@ -68,7 +67,7 @@ /** * Base class for CDMA and GSM connections. */ -abstract class TelephonyConnection extends Connection { +abstract class TelephonyConnection extends Connection implements Holdable { private static final int MSG_PRECISE_CALL_STATE_CHANGED = 1; private static final int MSG_RINGBACK_TONE = 2; private static final int MSG_HANDOVER_STATE_CHANGED = 3; @@ -515,6 +514,13 @@ protected final boolean mIsOutgoing; /** + * Indicates whether the connection can be held. This filed combined with the state of the + * connection can determine whether {@link Connection#CAPABILITY_HOLD} should be added to the + * connection. + */ + private boolean mIsHoldable; + + /** * Listeners to our TelephonyConnection specific callbacks */ private final Set<TelephonyConnectionListener> mTelephonyListeners = Collections.newSetFromMap( @@ -768,11 +774,14 @@ } if (!shouldTreatAsEmergencyCall() && isImsConnection() && canHoldImsCalls()) { callCapabilities |= CAPABILITY_SUPPORT_HOLD; - if (getState() == STATE_ACTIVE || getState() == STATE_HOLDING) { + if (mIsHoldable && (getState() == STATE_ACTIVE || getState() == STATE_HOLDING)) { callCapabilities |= CAPABILITY_HOLD; } } + Log.d(this, "buildConnectionCapabilities: isHoldable = " + + mIsHoldable + " State = " + getState() + " capabilities = " + callCapabilities); + return callCapabilities; } @@ -1473,9 +1482,7 @@ * @return {@code true} if the connection is external, {@code false} otherwise. */ private boolean isExternalConnection() { - return can(mOriginalConnectionCapabilities, Capability.IS_EXTERNAL_CONNECTION) - && can(mOriginalConnectionCapabilities, - Capability.IS_EXTERNAL_CONNECTION); + return can(mOriginalConnectionCapabilities, Capability.IS_EXTERNAL_CONNECTION); } /** @@ -1737,6 +1744,21 @@ return this; } + @Override + public void setHoldable(boolean isHoldable) { + mIsHoldable = isHoldable; + buildConnectionCapabilities(); + } + + @Override + public boolean isChildHoldable() { + return getConference() != null; + } + + public boolean isHoldable() { + return mIsHoldable; + } + /** * Fire a callback to the various listeners for when the original connection is * set in this {@link TelephonyConnection}
diff --git a/src/com/android/services/telephony/TelephonyConnectionService.java b/src/com/android/services/telephony/TelephonyConnectionService.java index ded2468..6b3fe65 100644 --- a/src/com/android/services/telephony/TelephonyConnectionService.java +++ b/src/com/android/services/telephony/TelephonyConnectionService.java
@@ -111,6 +111,13 @@ } }; + private final Connection.Listener mConnectionListener = new Connection.Listener() { + @Override + public void onConferenceChanged(Connection connection, Conference conference) { + mHoldTracker.updateHoldCapability(connection.getPhoneAccountHandle()); + } + }; + private final TelephonyConferenceController mTelephonyConferenceController = new TelephonyConferenceController(mTelephonyConnectionServiceProxy); private final CdmaConferenceController mCdmaConferenceController = @@ -122,6 +129,7 @@ private ComponentName mExpectedComponentName = null; private RadioOnHelper mRadioOnHelper; private EmergencyTonePlayer mEmergencyTonePlayer; + private HoldTracker mHoldTracker; // Contains one TelephonyConnection that has placed a call and a memory of which Phones it has // already tried to connect with. There should be only one TelephonyConnection trying to place a @@ -253,6 +261,7 @@ mExpectedComponentName = new ComponentName(this, this.getClass()); mEmergencyTonePlayer = new EmergencyTonePlayer(this); TelecomAccountRegistry.getInstance(this).setTelephonyConnectionService(this); + mHoldTracker = new HoldTracker(); } @Override @@ -860,6 +869,41 @@ } } + @Override + public void onConnectionAdded(Connection connection) { + if (connection instanceof Holdable && !isExternalConnection(connection)) { + connection.addConnectionListener(mConnectionListener); + mHoldTracker.addHoldable( + connection.getPhoneAccountHandle(), (Holdable) connection); + } + } + + @Override + public void onConnectionRemoved(Connection connection) { + if (connection instanceof Holdable && !isExternalConnection(connection)) { + mHoldTracker.removeHoldable(connection.getPhoneAccountHandle(), (Holdable) connection); + } + } + + @Override + public void onConferenceAdded(Conference conference) { + if (conference instanceof Holdable) { + mHoldTracker.addHoldable(conference.getPhoneAccountHandle(), (Holdable) conference); + } + } + + @Override + public void onConferenceRemoved(Conference conference) { + if (conference instanceof Holdable) { + mHoldTracker.removeHoldable(conference.getPhoneAccountHandle(), (Holdable) conference); + } + } + + private boolean isExternalConnection(Connection connection) { + return (connection.getConnectionProperties() & Connection.PROPERTY_IS_EXTERNAL_CALL) + == Connection.PROPERTY_IS_EXTERNAL_CALL; + } + private boolean blockCallForwardingNumberWhileRoaming(Phone phone, String number) { if (phone == null || TextUtils.isEmpty(number) || !phone.getServiceState().getRoaming()) { return false; @@ -969,7 +1013,7 @@ // on which phone account ECall can be placed. After deciding, we should notify Telecom of // the change so that the proper PhoneAccount can be displayed. Log.i(this, "updatePhoneAccount setPhoneAccountHandle, account = " + pHandle); - connection.notifyPhoneAccountChanged(pHandle); + connection.setPhoneAccountHandle(pHandle); } private void placeOutgoingConnection(
diff --git a/tests/src/com/android/services/telephony/HoldTrackerTest.java b/tests/src/com/android/services/telephony/HoldTrackerTest.java new file mode 100644 index 0000000..0db10e4 --- /dev/null +++ b/tests/src/com/android/services/telephony/HoldTrackerTest.java
@@ -0,0 +1,129 @@ +/* + * Copyright (C) 2017 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 junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; + +import android.content.ComponentName; +import android.support.test.runner.AndroidJUnit4; +import android.telecom.PhoneAccountHandle; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class HoldTrackerTest { + + private HoldTracker mHoldTrackerUT; + private PhoneAccountHandle mPhoneAccountHandle1; + private PhoneAccountHandle mPhoneAccountHandle2; + + @Before + public void setUp() throws Exception { + mHoldTrackerUT = new HoldTracker(); + mPhoneAccountHandle1 = + new PhoneAccountHandle(new ComponentName("pkg1", "cls1"), "0"); + mPhoneAccountHandle2 = + new PhoneAccountHandle(new ComponentName("pkg2", "cls2"), "1"); + } + + @Test + public void oneTopHoldableCanBeHeld() { + FakeHoldable topHoldable = createHoldable(false); + mHoldTrackerUT.addHoldable(mPhoneAccountHandle1, topHoldable); + + assertTrue(topHoldable.canBeHeld()); + } + + @Test + public void childHoldableCanNotBeHeld() { + FakeHoldable topHoldable = createHoldable(false); + FakeHoldable childHoldable = createHoldable(true); + mHoldTrackerUT.addHoldable(mPhoneAccountHandle1, topHoldable); + mHoldTrackerUT.addHoldable(mPhoneAccountHandle1, childHoldable); + + assertTrue(topHoldable.canBeHeld()); + assertFalse(childHoldable.canBeHeld()); + } + + @Test + public void twoTopHoldableWithTheSamePhoneAccountCanNotBeHeld() { + FakeHoldable topHoldable1 = createHoldable(false); + FakeHoldable topHoldable2 = createHoldable(false); + mHoldTrackerUT.addHoldable(mPhoneAccountHandle1, topHoldable1); + mHoldTrackerUT.addHoldable(mPhoneAccountHandle1, topHoldable2); + + mHoldTrackerUT.updateHoldCapability(mPhoneAccountHandle1); + assertFalse(topHoldable1.canBeHeld()); + assertFalse(topHoldable2.canBeHeld()); + } + + @Test + public void holdableWithDifferentPhoneAccountDoesNotAffectEachOther() { + FakeHoldable topHoldable1 = createHoldable(false); + FakeHoldable topHoldable2 = createHoldable(false); + mHoldTrackerUT.addHoldable(mPhoneAccountHandle1, topHoldable1); + mHoldTrackerUT.addHoldable(mPhoneAccountHandle2, topHoldable2); + + // Both phones account have only one top holdable, so the holdable of each phone account can + // be held. + assertTrue(topHoldable1.canBeHeld()); + assertTrue(topHoldable2.canBeHeld()); + } + + @Test + public void removeOneTopHoldableAndUpdateHoldCapabilityCorrectly() { + FakeHoldable topHoldable1 = createHoldable(false); + FakeHoldable topHoldable2 = createHoldable(false); + mHoldTrackerUT.addHoldable(mPhoneAccountHandle1, topHoldable1); + mHoldTrackerUT.addHoldable(mPhoneAccountHandle1, topHoldable2); + assertFalse(topHoldable1.canBeHeld()); + assertFalse(topHoldable2.canBeHeld()); + + mHoldTrackerUT.removeHoldable(mPhoneAccountHandle1, topHoldable1); + assertTrue(topHoldable2.canBeHeld()); + } + + public FakeHoldable createHoldable(boolean isChildHoldable) { + return new FakeHoldable(isChildHoldable); + } + + private class FakeHoldable implements Holdable { + private boolean mIsChildHoldable; + private boolean mIsHoldable; + + FakeHoldable(boolean isChildHoldable) { + mIsChildHoldable = isChildHoldable; + } + + @Override + public boolean isChildHoldable() { + return mIsChildHoldable; + } + + @Override + public void setHoldable(boolean isHoldable) { + mIsHoldable = isHoldable; + } + + public boolean canBeHeld() { + return mIsHoldable; + } + } +}