[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;
+        }
+    }
+}