Prevent merging conference calls hosted on peer device.

With IMS, the "multiparty" bit on an ImsCall is set to "true" when a call
is merged into a conference.  This not only occurs on the device hosting
the conference call, but also on the devices of the callers merged into
the conference.

This CL changes the TelephonyConnection to listen to multipart state
changes reported by the ImsPhoneConnection.  It uses this to notify the
ImsConferenceController when a conference has started.  Note: the handler
is re-used to ensure the conference creation happens on the correct thread.
PhoneBase#checkCorrectThread would error out during creation of the
conference otherwise.

Modified ImsConferenceController to check for conferences which are not
conference hosts and to exclude them from the list of conferenceables.
We also exclude connections marked as multiparty which are not a conference
host (it is possible we have not yet turned these into a conference yet).

Bug: 19478784
Change-Id: I15c39e9c922c8a71e30f210f212e0ec779f5f939
diff --git a/src/com/android/services/telephony/ImsConference.java b/src/com/android/services/telephony/ImsConference.java
index e692a30..b63b75c 100644
--- a/src/com/android/services/telephony/ImsConference.java
+++ b/src/com/android/services/telephony/ImsConference.java
@@ -322,6 +322,26 @@
     }
 
     /**
+     * Determines if this conference is hosted on the current device or the peer device.
+     *
+     * @return {@code true} if this conference is hosted on the current device, {@code false} if it
+     *      is hosted on the peer device.
+     */
+    public boolean isConferenceHost() {
+        if (mConferenceHost == null) {
+            return false;
+        }
+        com.android.internal.telephony.Connection originalConnection =
+                mConferenceHost.getOriginalConnection();
+        if (!(originalConnection instanceof ImsPhoneConnection)) {
+            return false;
+        }
+
+        ImsPhoneConnection imsPhoneConnection = (ImsPhoneConnection) originalConnection;
+        return imsPhoneConnection.isMultiparty() && imsPhoneConnection.isConferenceHost();
+    }
+
+    /**
      * 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.
diff --git a/src/com/android/services/telephony/ImsConferenceController.java b/src/com/android/services/telephony/ImsConferenceController.java
index e93ebd4..21c61f8 100644
--- a/src/com/android/services/telephony/ImsConferenceController.java
+++ b/src/com/android/services/telephony/ImsConferenceController.java
@@ -16,6 +16,8 @@
 
 package com.android.services.telephony;
 
+import com.android.internal.telephony.imsphone.ImsPhoneConnection;
+
 import android.net.Uri;
 import android.telecom.Conference;
 import android.telecom.Connection;
@@ -155,6 +157,15 @@
                 Log.d(this, "recalc - %s %s", connection.getState(), connection);
             }
 
+            // If this connection is a member of a conference hosted on another device, it is not
+            // conferenceable with any other connections.
+            if (isMemberOfPeerConference(connection)) {
+                if (Log.VERBOSE) {
+                    Log.v(this, "Skipping connection in peer conference: %s", connection);
+                }
+                continue;
+            }
+
             switch (connection.getState()) {
                 case Connection.STATE_ACTIVE:
                     activeConnections.add(connection);
@@ -168,11 +179,18 @@
             connection.setConferenceableConnections(Collections.<Connection>emptyList());
         }
 
-        for (Conference conference : mImsConferences) {
+        for (ImsConference conference : mImsConferences) {
             if (Log.DEBUG) {
                 Log.d(this, "recalc - %s %s", conference.getState(), conference);
             }
 
+            if (!conference.isConferenceHost()) {
+                if (Log.VERBOSE) {
+                    Log.v(this, "skipping conference (not hosted on this device): %s", conference);
+                }
+                continue;
+            }
+
             switch (conference.getState()) {
                 case Connection.STATE_ACTIVE:
                     activeConnections.add(conference);
@@ -209,6 +227,16 @@
 
         // Set the conference as conferenceable with all the connections
         for (ImsConference conference : mImsConferences) {
+            // If this conference is not being hosted on the current device, we cannot conference it
+            // with any other connections.
+            if (!conference.isConferenceHost()) {
+                if (Log.VERBOSE) {
+                    Log.v(this, "skipping conference (not hosted on this device): %s",
+                            conference);
+                }
+                continue;
+            }
+
             List<Connection> nonConferencedConnections =
                 new ArrayList<>(mTelephonyConnections.size());
             for (Connection c : mTelephonyConnections) {
@@ -224,6 +252,27 @@
     }
 
     /**
+     * Determines if a connection is a member of a conference hosted on another device.
+     *
+     * @param connection The connection.
+     * @return {@code true} if the connection is a member of a conference hosted on another device.
+     */
+    private boolean isMemberOfPeerConference(Connection connection) {
+        if (!(connection instanceof TelephonyConnection)) {
+            return false;
+        }
+        TelephonyConnection telephonyConnection = (TelephonyConnection) connection;
+        com.android.internal.telephony.Connection originalConnection =
+                telephonyConnection.getOriginalConnection();
+        if (!(originalConnection instanceof ImsPhoneConnection)) {
+            return false;
+        }
+
+        ImsPhoneConnection imsPhoneConnection = (ImsPhoneConnection) originalConnection;
+        return imsPhoneConnection.isMultiparty() && !imsPhoneConnection.isConferenceHost();
+    }
+
+    /**
      * Starts a new ImsConference for a connection which just entered a multiparty state.
      */
     private void recalculateConference() {
diff --git a/src/com/android/services/telephony/TelephonyConnection.java b/src/com/android/services/telephony/TelephonyConnection.java
index ac384fd..d079df7 100644
--- a/src/com/android/services/telephony/TelephonyConnection.java
+++ b/src/com/android/services/telephony/TelephonyConnection.java
@@ -50,6 +50,7 @@
     private static final int MSG_RINGBACK_TONE = 2;
     private static final int MSG_HANDOVER_STATE_CHANGED = 3;
     private static final int MSG_DISCONNECT = 4;
+    private static final int MSG_MULTIPARTY_STATE_CHANGED = 5;
 
     private final Handler mHandler = new Handler() {
         @Override
@@ -87,6 +88,14 @@
                 case MSG_DISCONNECT:
                     updateState();
                     break;
+                case MSG_MULTIPARTY_STATE_CHANGED:
+                    boolean isMultiParty = (Boolean) msg.obj;
+                    Log.i(this, "Update multiparty state to %s", isMultiParty ? "Y" : "N");
+                    mIsMultiParty = isMultiParty;
+                    if (isMultiParty) {
+                        notifyConferenceStarted();
+                    }
+                    break;
             }
         }
     };
@@ -182,6 +191,7 @@
         public void onAudioQualityChanged(int audioQuality) {
             setAudioQuality(audioQuality);
         }
+
         /**
          * Handles a change in the state of conference participant(s), as reported by the
          * {@link com.android.internal.telephony.Connection}.
@@ -192,6 +202,17 @@
         public void onConferenceParticipantsChanged(List<ConferenceParticipant> participants) {
             updateConferenceParticipants(participants);
         }
+
+        /**
+         * Handles a change to the multiparty state for this connection.
+         *
+         * @param isMultiParty {@code true} if the call became multiparty, {@code false}
+         *      otherwise.
+         */
+        @Override
+        public void onMultipartyStateChanged(boolean isMultiParty) {
+            handleMultipartyStateChange(isMultiParty);
+        }
     };
 
     private com.android.internal.telephony.Connection mOriginalConnection;
@@ -693,6 +714,24 @@
         }
     }
 
+    /**
+     * Handles requests to update the multiparty state received via the
+     * {@link com.android.internal.telephony.Connection.Listener#onMultipartyStateChanged(boolean)}
+     * listener.
+     * <p>
+     * Note: We post this to the mHandler to ensure that if a conference must be created as a
+     * result of the multiparty state change, the conference creation happens on the correct
+     * thread.  This ensures that the thread check in
+     * {@link com.android.internal.telephony.PhoneBase#checkCorrectThread(android.os.Handler)}
+     * does not fire.
+     *
+     * @param isMultiParty {@code true} if this connection is multiparty, {@code false} otherwise.
+     */
+    private void handleMultipartyStateChange(boolean isMultiParty) {
+        Log.i(this, "Update multiparty state to %s", isMultiParty ? "Y" : "N");
+        mHandler.obtainMessage(MSG_MULTIPARTY_STATE_CHANGED, isMultiParty).sendToTarget();
+    }
+
     private void setActiveInternal() {
         if (getState() == STATE_ACTIVE) {
             Log.w(this, "Should not be called if this is already ACTIVE");