Fixes for IMS Conferences via the RemoteConnectionService API.

IMS Conferences are problematic to Telecom when a connection manager is
used.  Both addExistingConnection and addConference will result in the
addition of duplicate conference participants and conferences to Telecom.
This is because Telecom does not have a means of de-duping the two, which
have new IDs created for them when they're added via a ConnectionService.

To fix this, added a workaround which packages the original existing
connection or conference id into the connection/conference extras which
are sent to the connection manager.  This way, the connection mgr can use
the same connection/conference ID was was used by the original
ConnectionService, and also perform some de-duping.

Its not optimal, and this should be fixed better in the future.

Bug: 31464792
Change-Id: I7b63f145c741c29e1735f50a473585f26ef70fc7
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index 98310bd..1227fce 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -26,6 +26,7 @@
 import android.os.RemoteException;
 import android.os.Trace;
 import android.provider.ContactsContract.Contacts;
+import android.telecom.Conference;
 import android.telecom.DisconnectCause;
 import android.telecom.Connection;
 import android.telecom.GatewayInfo;
@@ -383,6 +384,17 @@
     private PhoneNumberUtilsAdapter mPhoneNumberUtilsAdapter;
 
     /**
+     * For {@link Connection}s or {@link android.telecom.Conference}s added via a ConnectionManager
+     * using the {@link android.telecom.ConnectionService#addExistingConnection(PhoneAccountHandle,
+     * Connection)} or {@link android.telecom.ConnectionService#addConference(Conference)},
+     * indicates the ID of this call as it was referred to by the {@code ConnectionService} which
+     * originally created it.
+     *
+     * See {@link Connection#EXTRA_ORIGINAL_CONNECTION_ID} for more information.
+     */
+    private String mOriginalConnectionId;
+
+    /**
      * Persists the specified parameters and initializes the new instance.
      *
      * @param context The context.
@@ -1110,6 +1122,34 @@
     }
 
     /**
+     * Perform an in-place replacement of the {@link ConnectionServiceWrapper} for this Call.
+     * Removes the call from its former {@link ConnectionServiceWrapper}, ensuring that the
+     * ConnectionService is NOT unbound if the call count hits zero.
+     * This is used by the {@link ConnectionServiceWrapper} when handling {@link Connection} and
+     * {@link Conference} additions via a ConnectionManager.
+     * The original {@link android.telecom.ConnectionService} will directly add external calls and
+     * conferences to Telecom as well as the ConnectionManager, which will add to Telecom.  In these
+     * cases since its first added to via the original CS, we want to change the CS responsible for
+     * the call to the ConnectionManager rather than adding it again as another call/conference.
+     *
+     * @param service The new {@link ConnectionServiceWrapper}.
+     */
+    public void replaceConnectionService(ConnectionServiceWrapper service) {
+        Preconditions.checkNotNull(service);
+
+        if (mConnectionService != null) {
+            ConnectionServiceWrapper serviceTemp = mConnectionService;
+            mConnectionService = null;
+            serviceTemp.removeCall(this);
+            serviceTemp.decrementAssociatedCallCount(true /*isSuppressingUnbind*/);
+        }
+
+        service.incrementAssociatedCallCount();
+        mConnectionService = service;
+        mAnalytics.setCallConnectionService(service.getComponentName().flattenToShortString());
+    }
+
+    /**
      * Clears the associated connection service.
      */
     void clearConnectionService() {
@@ -2173,6 +2213,24 @@
         }
     }
 
+    public void setOriginalConnectionId(String originalConnectionId) {
+        mOriginalConnectionId = originalConnectionId;
+    }
+
+    /**
+     * For calls added via a ConnectionManager using the
+     * {@link android.telecom.ConnectionService#addExistingConnection(PhoneAccountHandle,
+     * Connection)}, or {@link android.telecom.ConnectionService#addConference(Conference)} APIS,
+     * indicates the ID of this call as it was referred to by the {@code ConnectionService} which
+     * originally created it.
+     *
+     * See {@link Connection#EXTRA_ORIGINAL_CONNECTION_ID}.
+     * @return The original connection ID.
+     */
+    public String getOriginalConnectionId() {
+        return mOriginalConnectionId;
+    }
+
     /**
      * Determines if a {@link Call}'s capabilities bitmask indicates that video is supported either
      * remotely or locally.
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index fefca78..d0f062a 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -74,6 +74,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 
@@ -1664,6 +1665,12 @@
         call.setVideoProvider(parcelableConference.getVideoProvider());
         call.setStatusHints(parcelableConference.getStatusHints());
         call.putExtras(Call.SOURCE_CONNECTION_SERVICE, parcelableConference.getExtras());
+        // In case this Conference was added via a ConnectionManager, keep track of the original
+        // Connection ID as created by the originating ConnectionService.
+        Bundle extras = parcelableConference.getExtras();
+        if (extras != null && extras.containsKey(Connection.EXTRA_ORIGINAL_CONNECTION_ID)) {
+            call.setOriginalConnectionId(extras.getString(Connection.EXTRA_ORIGINAL_CONNECTION_ID));
+        }
 
         // TODO: Move this to be a part of addCall()
         call.addListener(this);
@@ -2067,14 +2074,44 @@
         call.setConnectionProperties(connection.getConnectionProperties());
         call.setCallerDisplayName(connection.getCallerDisplayName(),
                 connection.getCallerDisplayNamePresentation());
-
         call.addListener(this);
+
+        // In case this connection was added via a ConnectionManager, keep track of the original
+        // Connection ID as created by the originating ConnectionService.
+        Bundle extras = connection.getExtras();
+        if (extras != null && extras.containsKey(Connection.EXTRA_ORIGINAL_CONNECTION_ID)) {
+            call.setOriginalConnectionId(extras.getString(Connection.EXTRA_ORIGINAL_CONNECTION_ID));
+        }
         addCall(call);
 
         return call;
     }
 
     /**
+     * Determines whether Telecom already knows about a Connection added via the
+     * {@link android.telecom.ConnectionService#addExistingConnection(PhoneAccountHandle,
+     * Connection)} API via a ConnectionManager.
+     *
+     * See {@link Connection#EXTRA_ORIGINAL_CONNECTION_ID}.
+     * @param originalConnectionId The new connection ID to check.
+     * @return {@code true} if this connection is already known by Telecom.
+     */
+    Call getAlreadyAddedConnection(String originalConnectionId) {
+        Optional<Call> existingCall = mCalls.stream()
+                .filter(call -> originalConnectionId.equals(call.getOriginalConnectionId()) ||
+                            originalConnectionId.equals(call.getId()))
+                .findFirst();
+
+        if (existingCall.isPresent()) {
+            Log.i(this, "isExistingConnectionAlreadyAdded - call %s already added with id %s",
+                    originalConnectionId, existingCall.get().getId());
+            return existingCall.get();
+        }
+
+        return null;
+    }
+
+    /**
      * @return A new unique telecom call Id.
      */
     private String getNextCallId() {
diff --git a/src/com/android/server/telecom/ConnectionServiceWrapper.java b/src/com/android/server/telecom/ConnectionServiceWrapper.java
index bf82a99..6ec8945 100644
--- a/src/com/android/server/telecom/ConnectionServiceWrapper.java
+++ b/src/com/android/server/telecom/ConnectionServiceWrapper.java
@@ -33,7 +33,6 @@
 import android.telecom.GatewayInfo;
 import android.telecom.ParcelableConference;
 import android.telecom.ParcelableConnection;
-import android.telecom.PhoneAccount;
 import android.telecom.PhoneAccountHandle;
 import android.telecom.StatusHints;
 import android.telecom.TelecomManager;
@@ -361,6 +360,8 @@
                                 "call id %s", callId);
                         return;
                     }
+                    logIncoming("addConferenceCall %s %s [%s]", callId, parcelableConference,
+                            parcelableConference.getConnectionIds());
 
                     // Make sure that there's at least one valid call. For remote connections
                     // we'll get a add conference msg from both the remote connection service
@@ -378,16 +379,46 @@
                         return;
                     }
 
-                    // need to create a new Call
                     PhoneAccountHandle phAcc = null;
                     if (parcelableConference != null &&
                             parcelableConference.getPhoneAccount() != null) {
                         phAcc = parcelableConference.getPhoneAccount();
                     }
-                    Call conferenceCall = mCallsManager.createConferenceCall(callId,
-                            phAcc, parcelableConference);
-                    mCallIdMapper.addCall(conferenceCall, callId);
-                    conferenceCall.setConnectionService(ConnectionServiceWrapper.this);
+
+                    Bundle connectionExtras = parcelableConference.getExtras();
+
+                    String connectIdToCheck = null;
+                    if (connectionExtras != null && connectionExtras
+                            .containsKey(Connection.EXTRA_ORIGINAL_CONNECTION_ID)) {
+                        // Conference was added via a connection manager, see if its original id is
+                        // known.
+                        connectIdToCheck = connectionExtras
+                                .getString(Connection.EXTRA_ORIGINAL_CONNECTION_ID);
+                    } else {
+                        connectIdToCheck = callId;
+                    }
+
+                    Call conferenceCall;
+                    // Check to see if this conference has already been added.
+                    Call alreadyAddedConnection = mCallsManager
+                            .getAlreadyAddedConnection(connectIdToCheck);
+                    if (alreadyAddedConnection != null && mCallIdMapper.getCall(callId) == null) {
+                        // We are currently attempting to add the conference via a connection mgr,
+                        // and the originating ConnectionService has already added it.  Instead of
+                        // making a new Telecom call, we will simply add it to the ID mapper here,
+                        // and replace the ConnectionService on the call.
+                        mCallIdMapper.addCall(alreadyAddedConnection, callId);
+                        alreadyAddedConnection.replaceConnectionService(
+                                ConnectionServiceWrapper.this);
+                        conferenceCall = alreadyAddedConnection;
+                    } else {
+                        // need to create a new Call
+                        Call newConferenceCall = mCallsManager.createConferenceCall(callId,
+                                phAcc, parcelableConference);
+                        mCallIdMapper.addCall(newConferenceCall, callId);
+                        newConferenceCall.setConnectionService(ConnectionServiceWrapper.this);
+                        conferenceCall = newConferenceCall;
+                    }
 
                     Log.d(this, "adding children to conference %s phAcc %s",
                             parcelableConference.getConnectionIds(), phAcc);
@@ -597,10 +628,11 @@
             long token = Binder.clearCallingIdentity();
             try {
                 synchronized (mLock) {
-                    logIncoming("setConferenceableConnections %s %s", callId,
-                            conferenceableCallIds);
+
                     Call call = mCallIdMapper.getCall(callId);
                     if (call != null) {
+                        logIncoming("setConferenceableConnections %s %s", callId,
+                                conferenceableCallIds);
                         List<Call> conferenceableCalls =
                                 new ArrayList<>(conferenceableCallIds.size());
                         for (String otherId : conferenceableCallIds) {
@@ -643,8 +675,37 @@
                             phoneAccountHandle = accountHandle;
                         }
                     }
+                    // Allow the Sim call manager account as well, even if its disabled.
+                    if (phoneAccountHandle == null && callingPhoneAccountHandle != null) {
+                        if (callingPhoneAccountHandle.equals(
+                                mPhoneAccountRegistrar.getSimCallManager(userHandle))) {
+                            phoneAccountHandle = callingPhoneAccountHandle;
+                        }
+                    }
                     if (phoneAccountHandle != null) {
-                        logIncoming("addExistingConnection  %s %s", callId, connection);
+                        logIncoming("addExistingConnection %s %s", callId, connection);
+
+                        Bundle connectionExtras = connection.getExtras();
+                        String connectIdToCheck = null;
+                        if (connectionExtras != null && connectionExtras
+                                .containsKey(Connection.EXTRA_ORIGINAL_CONNECTION_ID)) {
+                            connectIdToCheck = connectionExtras
+                                    .getString(Connection.EXTRA_ORIGINAL_CONNECTION_ID);
+                        } else {
+                            connectIdToCheck = callId;
+                        }
+                        // Check to see if this Connection has already been added.
+                        Call alreadyAddedConnection = mCallsManager
+                                .getAlreadyAddedConnection(connectIdToCheck);
+
+                        if (alreadyAddedConnection != null
+                                && mCallIdMapper.getCall(callId) == null) {
+                            mCallIdMapper.addCall(alreadyAddedConnection, callId);
+                            alreadyAddedConnection
+                                    .replaceConnectionService(ConnectionServiceWrapper.this);
+                            return;
+                        }
+
                         Call existingCall = mCallsManager
                                 .createCallForExistingConnection(callId, connection);
                         mCallIdMapper.addCall(existingCall, callId);
@@ -1117,11 +1178,13 @@
     }
 
     private void logIncoming(String msg, Object... params) {
-        Log.d(this, "ConnectionService -> Telecom: " + msg, params);
+        Log.d(this, "ConnectionService -> Telecom[" + mComponentName.flattenToShortString() + "]: "
+                + msg, params);
     }
 
     private void logOutgoing(String msg, Object... params) {
-        Log.d(this, "Telecom -> ConnectionService: " + msg, params);
+        Log.d(this, "Telecom -> ConnectionService[" + mComponentName.flattenToShortString() + "]: "
+                + msg, params);
     }
 
     private void queryRemoteConnectionServices(final UserHandle userHandle,
diff --git a/src/com/android/server/telecom/ServiceBinder.java b/src/com/android/server/telecom/ServiceBinder.java
index 9a0f7b4..05f0bcb 100644
--- a/src/com/android/server/telecom/ServiceBinder.java
+++ b/src/com/android/server/telecom/ServiceBinder.java
@@ -169,7 +169,7 @@
     private final String mServiceAction;
 
     /** The component name of the service to bind to. */
-    private final ComponentName mComponentName;
+    protected final ComponentName mComponentName;
 
     /** The set of callbacks waiting for notification of the binding's success or failure. */
     private final Set<BindCallback> mCallbacks = new ArraySet<>();
@@ -227,12 +227,16 @@
     }
 
     final void decrementAssociatedCallCount() {
+        decrementAssociatedCallCount(false /*isSuppressingUnbind*/);
+    }
+
+    final void decrementAssociatedCallCount(boolean isSuppressingUnbind) {
         if (mAssociatedCallCount > 0) {
             mAssociatedCallCount--;
             Log.v(this, "Call count decrement %d, %s", mAssociatedCallCount,
                     mComponentName.flattenToShortString());
 
-            if (mAssociatedCallCount == 0) {
+            if (!isSuppressingUnbind && mAssociatedCallCount == 0) {
                 unbind();
             }
         } else {