Update conference call APIs.

Clean up conference call APIs to use a distinct type separate from
Connection.  Also allow the addition of Conference calls at any point
using addConference() API method.

Bug:16844332
Bug:16449372
Change-Id: I34e45fde1aa43559f5f4e29b990929c188b16875
diff --git a/telecomm/java/android/telecomm/ConnectionService.java b/telecomm/java/android/telecomm/ConnectionService.java
index d2d4828..97a3102 100644
--- a/telecomm/java/android/telecomm/ConnectionService.java
+++ b/telecomm/java/android/telecomm/ConnectionService.java
@@ -39,6 +39,8 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
 
 /**
  * A {@link android.app.Service} that provides telephone connections to processes running on an
@@ -73,12 +75,14 @@
 
     private final Map<String, Connection> mConnectionById = new HashMap<>();
     private final Map<Connection, String> mIdByConnection = new HashMap<>();
+    private final Map<String, Conference> mConferenceById = new HashMap<>();
+    private final Map<Conference, String> mIdByConference = new HashMap<>();
     private final RemoteConnectionManager mRemoteConnectionManager = new RemoteConnectionManager();
-
-    private boolean mAreAccountsInitialized = false;
     private final List<Runnable> mPreInitializationConnectionRequests = new ArrayList<>();
     private final ConnectionServiceAdapter mAdapter = new ConnectionServiceAdapter();
 
+    private boolean mAreAccountsInitialized = false;
+
     private final IBinder mBinder = new IConnectionService.Stub() {
         @Override
         public void addConnectionServiceAdapter(IConnectionServiceAdapter adapter) {
@@ -155,10 +159,10 @@
         }
 
         @Override
-        public void conference(String conferenceCallId, String callId) {
+        public void conference(String callId1, String callId2) {
             SomeArgs args = SomeArgs.obtain();
-            args.arg1 = conferenceCallId;
-            args.arg2 = callId;
+            args.arg1 = callId1;
+            args.arg2 = callId2;
             mHandler.obtainMessage(MSG_CONFERENCE, args).sendToTarget();
         }
 
@@ -270,9 +274,9 @@
                 case MSG_CONFERENCE: {
                     SomeArgs args = (SomeArgs) msg.obj;
                     try {
-                        String conferenceCallId = (String) args.arg1;
-                        String callId = (String) args.arg2;
-                        conference(conferenceCallId, callId);
+                        String callId1 = (String) args.arg1;
+                        String callId2 = (String) args.arg2;
+                        conference(callId1, callId2);
                     } finally {
                         args.recycle();
                     }
@@ -301,6 +305,51 @@
         }
     };
 
+    private final Conference.Listener mConferenceListener = new Conference.Listener() {
+        @Override
+        public void onStateChanged(Conference conference, int oldState, int newState) {
+            String id = mIdByConference.get(conference);
+            switch (newState) {
+                case Connection.STATE_ACTIVE:
+                    mAdapter.setActive(id);
+                    break;
+                case Connection.STATE_HOLDING:
+                    mAdapter.setOnHold(id);
+                    break;
+                case Connection.STATE_DISCONNECTED:
+                    // handled by onDisconnected
+                    break;
+            }
+        }
+
+        @Override
+        public void onDisconnected(Conference conference, int cause, String message) {
+            String id = mIdByConference.get(conference);
+            mAdapter.setDisconnected(id, cause, message);
+        }
+
+        @Override
+        public void onConnectionAdded(Conference conference, Connection connection) {
+        }
+
+        @Override
+        public void onConnectionRemoved(Conference conference, Connection connection) {
+        }
+
+        @Override
+        public void onDestroyed(Conference conference) {
+            removeConference(conference);
+        }
+
+        @Override
+        public void onCapabilitiesChanged(Conference conference, int capabilities) {
+            String id = mIdByConference.get(conference);
+            Log.d(this, "call capabilities: conference: %s",
+                    PhoneCapabilities.toString(capabilities));
+            mAdapter.setCallCapabilities(id, capabilities);
+        }
+    };
+
     private final Connection.Listener mConnectionListener = new Connection.Listener() {
         @Override
         public void onStateChanged(Connection c, int state) {
@@ -383,13 +432,6 @@
         }
 
         @Override
-        public void onParentConnectionChanged(Connection c, Connection parent) {
-            String id = mIdByConnection.get(c);
-            String parentId = parent == null ? null : mIdByConnection.get(parent);
-            mAdapter.setIsConferenced(id, parentId);
-        }
-
-        @Override
         public void onVideoProviderChanged(Connection c, Connection.VideoProvider videoProvider) {
             String id = mIdByConnection.get(c);
             mAdapter.setVideoProvider(id, videoProvider);
@@ -426,6 +468,18 @@
             Collections.sort(conferenceableCallIds);
             mAdapter.setConferenceableConnections(id, conferenceableCallIds);
         }
+
+        @Override
+        public void onConferenceChanged(Connection connection, Conference conference) {
+            String id = mIdByConnection.get(connection);
+            if (id != null) {
+                String conferenceId = null;
+                if (conference != null) {
+                    conferenceId = mIdByConference.get(conference);
+                }
+                mAdapter.setIsConferenced(id, conferenceId);
+            }
+        }
     };
 
     /** {@inheritDoc} */
@@ -483,6 +537,13 @@
                         }
                         c.removeConnectionListener(this);
                     }
+
+                    @Override
+                    public void onDestroyed(Connection c) {
+                        // Listen to onDestroy in case the connection is destroyed before
+                        // transitioning to another state.
+                        c.removeConnectionListener(this);
+                    }
                 });
                 Log.d(this, "Connection created in state INITIALIZING");
                 connectionCreated(callId, request, createdConnection);
@@ -586,38 +647,22 @@
         findConnectionForAction(callId, "stopDtmfTone").onStopDtmfTone();
     }
 
-    private void conference(final String conferenceCallId, String callId) {
-        Log.d(this, "conference %s, %s", conferenceCallId, callId);
+    private void conference(String callId1, String callId2) {
+        Log.d(this, "conference %s, %s", callId1, callId2);
 
-        Connection connection = findConnectionForAction(callId, "conference");
-        if (connection == Connection.getNullConnection()) {
-            Log.w(this, "Connection missing in conference request %s.", callId);
+        Connection connection1 = findConnectionForAction(callId1, "conference");
+        if (connection1 == Connection.getNullConnection()) {
+            Log.w(this, "Connection1 missing in conference request %s.", callId1);
             return;
         }
 
-        onCreateConferenceConnection(conferenceCallId, connection,
-                new Response<String, Connection>() {
-                    /** ${inheritDoc} */
-                    @Override
-                    public void onResult(String ignored, Connection... result) {
-                        Log.d(this, "onCreateConference.Response %s", (Object[]) result);
-                        if (result != null && result.length == 1) {
-                            Connection conferenceConnection = result[0];
-                            if (!mIdByConnection.containsKey(conferenceConnection)) {
-                                Log.v(this, "sending new conference call %s", conferenceCallId);
-                                mAdapter.addConferenceCall(conferenceCallId);
-                                addConnection(conferenceCallId, conferenceConnection);
-                            }
-                        }
-                    }
+        Connection connection2 = findConnectionForAction(callId2, "conference");
+        if (connection2 == Connection.getNullConnection()) {
+            Log.w(this, "Connection2 missing in conference request %s.", callId2);
+            return;
+        }
 
-                    /** ${inheritDoc} */
-                    @Override
-                    public void onError(String request, int code, String reason) {
-                        // no-op
-                    }
-                }
-        );
+        onConference(connection1, connection2);
     }
 
     private void splitFromConference(String callId) {
@@ -712,6 +757,40 @@
     }
 
     /**
+     * Adds a new conference call. When a conference call is created either as a result of an
+     * explicit request via {@link #onConference} or otherwise, the connection service should supply
+     * an instance of {@link Conference} by invoking this method. A conference call provided by this
+     * method will persist until {@link Conference#destroy} is invoked on the conference instance.
+     *
+     * @param conference The new conference object.
+     */
+    public final void addConference(Conference conference) {
+        String id = addConferenceInternal(conference);
+        if (id != null) {
+            List<String> connectionIds = new ArrayList<>(2);
+            for (Connection connection : conference.getConnections()) {
+                if (mIdByConnection.containsKey(connection)) {
+                    connectionIds.add(mIdByConnection.get(connection));
+                }
+            }
+            ParcelableConference parcelableConference = new ParcelableConference(
+                    conference.getPhoneAccount(),
+                    conference.getState(),
+                    conference.getCapabilities(),
+                    connectionIds);
+            mAdapter.addConferenceCall(id, parcelableConference);
+
+            // Go through any child calls and set the parent.
+            for (Connection connection : conference.getConnections()) {
+                String connectionId = mIdByConnection.get(connection);
+                if (connectionId != null) {
+                    mAdapter.setIsConferenced(connectionId, id);
+                }
+            }
+        }
+    }
+
+    /**
      * Returns all the active {@code Connection}s for which this {@code ConnectionService}
      * has taken responsibility.
      *
@@ -767,22 +846,14 @@
     }
 
     /**
-     * Returns a new or existing conference connection when the the user elects to convert the
-     * specified connection into a conference call. The specified connection can be any connection
-     * which had previously specified itself as conference-capable including both simple connections
-     * and connections previously returned from this method.
-     * <p>
-     * TODO: To be refactored out with conference call re-engineering<br/>
-     * TODO: Also remove class {@link Response} once this method is removed
+     * Conference two specified connections. Invoked when the user has made a request to merge the
+     * specified connections into a conference call. In response, the connection service should
+     * create an instance of {@link Conference} and pass it into {@link #addConference}.
      *
-     * @param connection The connection from which the user opted to start a conference call.
-     * @param token The token to be passed into the response callback.
-     * @param callback The callback for providing the potentially-new conference connection.
+     * @param connection1 A connection to merge into a conference call.
+     * @param connection2 A connection to merge into a conference call.
      */
-    public void onCreateConferenceConnection(
-            String token,
-            Connection connection,
-            Response<String, Connection> callback) {}
+    public void onConference(Connection connection1, Connection connection2) {}
 
     /**
      * Notifies that a connection has been added to this connection service and sent to Telecomm.
@@ -798,6 +869,13 @@
      */
     public void onConnectionRemoved(Connection connection) {}
 
+    /**
+     * @hide
+     */
+    public boolean containsConference(Conference conference) {
+        return mIdByConference.containsKey(conference);
+    }
+
     private void onAccountsInitialized() {
         mAreAccountsInitialized = true;
         for (Runnable r : mPreInitializationConnectionRequests) {
@@ -810,11 +888,13 @@
         mConnectionById.put(callId, connection);
         mIdByConnection.put(connection, callId);
         connection.addConnectionListener(mConnectionListener);
+        connection.setConnectionService(this);
         onConnectionAdded(connection);
     }
 
     private void removeConnection(Connection connection) {
         String id = mIdByConnection.get(connection);
+        connection.unsetConnectionService(this);
         connection.removeConnectionListener(mConnectionListener);
         mConnectionById.remove(mIdByConnection.get(connection));
         mIdByConnection.remove(connection);
@@ -822,6 +902,31 @@
         mAdapter.removeCall(id);
     }
 
+    private String addConferenceInternal(Conference conference) {
+        if (mIdByConference.containsKey(conference)) {
+            Log.w(this, "Re-adding an existing conference: %s.", conference);
+        } else if (conference != null) {
+            String id = UUID.randomUUID().toString();
+            mConferenceById.put(id, conference);
+            mIdByConference.put(conference, id);
+            conference.addListener(mConferenceListener);
+            return id;
+        }
+
+        return null;
+    }
+
+    private void removeConference(Conference conference) {
+        if (mIdByConference.containsKey(conference)) {
+            conference.removeListener(mConferenceListener);
+
+            String id = mIdByConference.get(conference);
+            mConferenceById.remove(id);
+            mIdByConference.remove(conference);
+            mAdapter.removeCall(id);
+        }
+    }
+
     private Connection findConnectionForAction(String callId, String action) {
         if (mConnectionById.containsKey(callId)) {
             return mConnectionById.get(callId);