Implement new Conference APIs.

Bug:16844332
Bug:16449372
Change-Id: Id2e1e4996c19ca1fa4f37e1ec6597f3a15676aa8
diff --git a/sip/src/com/android/services/telephony/sip/SipConnection.java b/sip/src/com/android/services/telephony/sip/SipConnection.java
index 58db1f5..2603b98 100644
--- a/sip/src/com/android/services/telephony/sip/SipConnection.java
+++ b/sip/src/com/android/services/telephony/sip/SipConnection.java
@@ -180,11 +180,6 @@
     }
 
     @Override
-    public void onChildrenChanged(List<Connection> children) {
-        if (VERBOSE) log("onChildrenChanged, children: " + children);
-    }
-
-    @Override
     public void onPhoneAccountClicked() {
         if (VERBOSE) log("onPhoneAccountClicked");
     }
diff --git a/sip/src/com/android/services/telephony/sip/SipConnectionService.java b/sip/src/com/android/services/telephony/sip/SipConnectionService.java
index bda564f..2c8cd97 100644
--- a/sip/src/com/android/services/telephony/sip/SipConnectionService.java
+++ b/sip/src/com/android/services/telephony/sip/SipConnectionService.java
@@ -87,14 +87,6 @@
     }
 
     @Override
-    public void onCreateConferenceConnection(
-            String token,
-            Connection connection,
-            Response<String, Connection> response) {
-        if (VERBOSE) log("onCreateConferenceConnection, connection: " + connection);
-    }
-
-    @Override
     public Connection onCreateIncomingConnection(
             PhoneAccountHandle connectionManagerAccount,
             ConnectionRequest request) {
diff --git a/src/com/android/services/telephony/ConferenceConnection.java b/src/com/android/services/telephony/ConferenceConnection.java
deleted file mode 100644
index 33be0be..0000000
--- a/src/com/android/services/telephony/ConferenceConnection.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright (C) 2014 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.telecomm.Connection;
-import android.telephony.DisconnectCause;
-
-import com.android.internal.telephony.CallStateException;
-
-import java.util.List;
-
-/**
- * Manages state for a conference call.
- */
-final class ConferenceConnection extends Connection {
-    @Override
-    public void onChildrenChanged(List<Connection> children) {
-        if (children.isEmpty()) {
-            setDisconnected(DisconnectCause.LOCAL, "conference call disconnected.");
-            destroy();
-        }
-    }
-
-    /** ${inheritDoc} */
-    @Override
-    public void onDisconnect() {
-        // For conference-level disconnects, we need to make sure we disconnect the entire call,
-        // not just one of the connections. To do this, we go through the children and get a
-        // reference to the telephony-Call object and call hangup().
-        for (Connection connection : getChildConnections()) {
-            if (connection instanceof TelephonyConnection) {
-                com.android.internal.telephony.Connection origConnection =
-                        ((TelephonyConnection) connection).getOriginalConnection();
-                if (origConnection != null && origConnection.getCall() != null) {
-                    try {
-                        // getCall() returns what is the parent call of all conferenced conections
-                        // so we only need to call hangup on the main call object. Break once we've
-                        // done that.
-                        origConnection.getCall().hangup();
-                        break;
-                    } catch (CallStateException e) {
-                        Log.e(this, e, "Call state exception in conference hangup.");
-                    }
-                }
-            }
-        }
-    }
-
-    /** ${inheritDoc} */
-    @Override
-    public void onHold() {
-        for (Connection connection : getChildConnections()) {
-            if (connection instanceof TelephonyConnection) {
-                ((TelephonyConnection) connection).onHold();
-                // Hold only needs to be called on one of the children.
-                break;
-            }
-        }
-    }
-}
diff --git a/src/com/android/services/telephony/GsmConference.java b/src/com/android/services/telephony/GsmConference.java
new file mode 100644
index 0000000..d8bc11b
--- /dev/null
+++ b/src/com/android/services/telephony/GsmConference.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2014 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.telecomm.PhoneCapabilities;
+import android.telecomm.Conference;
+import android.telecomm.Connection;
+import android.telecomm.PhoneAccountHandle;
+
+import com.android.internal.telephony.Call;
+import com.android.internal.telephony.CallStateException;
+
+import java.util.List;
+
+/**
+ * GSM-based conference call.
+ */
+public class GsmConference extends Conference {
+
+    public GsmConference(PhoneAccountHandle phoneAccount) {
+        super(phoneAccount);
+        setCapabilities(
+                PhoneCapabilities.SUPPORT_HOLD |
+                PhoneCapabilities.HOLD |
+                PhoneCapabilities.MUTE |
+                PhoneCapabilities.SWAP_CALLS);
+        setActive();
+    }
+
+    /**
+     * Invoked when the Conference and all it's {@link Connection}s should be disconnected.
+     */
+    @Override
+    public void onDisconnect() {
+        for (Connection connection : getConnections()) {
+            Call call = getMultipartyCallForConnection(connection, "onDisconnect");
+            if (call != null) {
+                try {
+                    call.hangup();
+                } catch (CallStateException e) {
+                    Log.e(this, e, "Exception thrown trying to hangup conference");
+                }
+            }
+        }
+    }
+
+    /**
+     * Invoked when the specified {@link Connection} should be separated from the conference call.
+     *
+     * @param connection The connection to separate.
+     */
+    @Override
+    public void onSeparate(Connection connection) {
+        com.android.internal.telephony.Connection radioConnection =
+                getOriginalConnection(connection, "onSeparate");
+        try {
+            radioConnection.separate();
+        } catch (CallStateException e) {
+            Log.e(this, e, "Exception thrown trying to separate a conference call");
+        }
+    }
+
+    /**
+     * Invoked when the conference should be put on hold.
+     */
+    @Override
+    public void onHold() {
+        List<Connection> connections = getConnections();
+        if (connections.isEmpty()) {
+            return;
+        }
+        ((GsmConnection) connections.get(0)).performHold();
+    }
+
+    /**
+     * Invoked when the conference should be moved from hold to active.
+     */
+    @Override
+    public void onUnhold() {
+        List<Connection> connections = getConnections();
+        if (connections.isEmpty()) {
+            return;
+        }
+        ((GsmConnection) connections.get(0)).performUnhold();
+    }
+
+    private Call getMultipartyCallForConnection(Connection connection, String tag) {
+        com.android.internal.telephony.Connection radioConnection =
+                getOriginalConnection(connection, tag);
+        if (radioConnection != null) {
+            Call call = radioConnection.getCall();
+            if (call != null && call.isMultiparty()) {
+                return call;
+            }
+        }
+        return null;
+    }
+
+    private com.android.internal.telephony.Connection getOriginalConnection(
+            Connection connection, String tag) {
+
+        if (connection instanceof GsmConnection) {
+            return ((GsmConnection) connection).getOriginalConnection();
+        } else {
+            Log.e(this, null, "Non GSM connection found in a Gsm conference (%s)", tag);
+            return null;
+        }
+    }
+}
diff --git a/src/com/android/services/telephony/GsmConferenceController.java b/src/com/android/services/telephony/GsmConferenceController.java
index a69d1e6..e3d0bf5 100644
--- a/src/com/android/services/telephony/GsmConferenceController.java
+++ b/src/com/android/services/telephony/GsmConferenceController.java
@@ -17,82 +17,61 @@
 package com.android.services.telephony;
 
 import java.util.ArrayList;
-
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
+import android.telecomm.Conference;
 import android.telecomm.Connection;
 
+import com.android.internal.telephony.Call;
+
 /**
  * Maintains a list of all the known GSM connections and implements GSM-specific conference
  * call functionality.
  */
 final class GsmConferenceController {
-    private static GsmConferenceController sInstance;
-
     private final Connection.Listener mConnectionListener = new Connection.Listener() {
                 @Override
                 public void onStateChanged(Connection c, int state) {
-                    // No need to recalculate for conference calls, just traditional calls.
-                    if (c != mGsmConferenceConnection) {
-                        recalculate();
-                    }
+                    recalculate();
                 }
 
                 /** ${inheritDoc} */
                 @Override
                 public void onDisconnected(Connection c, int cause, String message) {
-                    // When a connection disconnects, make sure to release its parent reference
-                    // so that the parent can move to disconnected as well.
-                    c.setParentConnection(null);
+                    recalculate();
                 }
             };
 
     /** The known GSM connections. */
     private final List<GsmConnection> mGsmConnections = new ArrayList<>();
 
+    private final TelephonyConnectionService mConnectionService;
+
+    public GsmConferenceController(TelephonyConnectionService connectionService) {
+        mConnectionService = connectionService;
+    }
+
     /** The GSM conference connection object. */
-    private ConferenceConnection mGsmConferenceConnection;
+    private Conference mGsmConference;
 
-    static void add(GsmConnection connection) {
-        if (sInstance == null) {
-            sInstance = new GsmConferenceController();
-        }
-        connection.addConnectionListener(sInstance.mConnectionListener);
-        sInstance.mGsmConnections.add(connection);
-        sInstance.recalculate();
+    void add(GsmConnection connection) {
+        mGsmConnections.add(connection);
+        connection.addConnectionListener(mConnectionListener);
+        recalculate();
     }
 
-    static void remove(GsmConnection connection) {
-        if (sInstance != null) {
-            connection.removeConnectionListener(sInstance.mConnectionListener);
-            sInstance.mGsmConnections.remove(connection);
-            sInstance.recalculate();
-
-            if (sInstance.mGsmConnections.size() == 0 &&
-                    sInstance.mGsmConferenceConnection == null) {
-                sInstance = null;
-            }
-        }
-    }
-
-    static ConferenceConnection createConferenceConnection(Connection rootConnection) {
-        if (sInstance != null) {
-            if (sInstance.mGsmConferenceConnection == null) {
-                sInstance.mGsmConferenceConnection = new ConferenceConnection();
-                Log.d(sInstance, "creating the conference connection: %s",
-                        sInstance.mGsmConferenceConnection);
-            }
-            if (rootConnection instanceof GsmConnection) {
-                ((GsmConnection) rootConnection).performConference();
-            }
-            return sInstance.mGsmConferenceConnection;
-        }
-        return null;
+    void remove(GsmConnection connection) {
+        connection.removeConnectionListener(mConnectionListener);
+        mGsmConnections.remove(connection);
+        recalculate();
     }
 
     private void recalculate() {
         recalculateConferenceable();
+        recalculateConference();
     }
 
     /**
@@ -141,5 +120,60 @@
         for (Connection connection : backgroundConnections) {
             connection.setConferenceableConnections(activeConnections);
         }
+        // TODO: Do not allow conferencing of already conferenced connections.
+    }
+
+    private void recalculateConference() {
+        Set<GsmConnection> conferencedConnections = new HashSet<>();
+
+        for (GsmConnection connection : mGsmConnections) {
+            com.android.internal.telephony.Connection radioConnection =
+                connection.getOriginalConnection();
+
+            if (radioConnection != null) {
+                Call.State state = radioConnection.getState();
+                Call call = radioConnection.getCall();
+                if ((state == Call.State.ACTIVE || state == Call.State.HOLDING) &&
+                        (call != null && call.isMultiparty())) {
+                    conferencedConnections.add(connection);
+                }
+            }
+        }
+
+        Log.d(this, "Recalculate conference calls %s %s.",
+                mGsmConference, conferencedConnections);
+
+        if (conferencedConnections.size() < 2) {
+            Log.d(this, "less than two conference calls!");
+            // No more connections are conferenced, destroy any existing conference.
+            if (mGsmConference != null) {
+                Log.d(this, "with a conference to destroy!");
+                mGsmConference.destroy();
+                mGsmConference = null;
+            }
+        } else {
+            if (mGsmConference != null) {
+                List<Connection> existingConnections = mGsmConference.getConnections();
+                // Remove any that no longer exist
+                for (Connection connection : existingConnections) {
+                    if (!conferencedConnections.contains(connection)) {
+                        mGsmConference.removeConnection(connection);
+                    }
+                }
+
+                // Add any new ones
+                for (Connection connection : conferencedConnections) {
+                    if (!existingConnections.contains(connection)) {
+                        mGsmConference.addConnection(connection);
+                    }
+                }
+            } else {
+                mGsmConference = new GsmConference(null);
+                for (Connection connection : conferencedConnections) {
+                    mGsmConference.addConnection(connection);
+                }
+                mConnectionService.addConference(mGsmConference);
+            }
+        }
     }
 }
diff --git a/src/com/android/services/telephony/GsmConnection.java b/src/com/android/services/telephony/GsmConnection.java
index 5140574..99f2cde 100644
--- a/src/com/android/services/telephony/GsmConnection.java
+++ b/src/com/android/services/telephony/GsmConnection.java
@@ -29,7 +29,6 @@
 
     GsmConnection(Connection connection) {
         super(connection);
-        GsmConferenceController.add(this);
     }
 
     /** {@inheritDoc} */
@@ -55,10 +54,16 @@
         }
     }
 
-    void performConference() {
+    @Override
+    public void performConference(TelephonyConnection otherConnection) {
         Log.d(this, "performConference - %s", this);
         if (getPhone() != null) {
             try {
+                // We dont use the "other" connection because there is no concept of that in the
+                // implementation of calls inside telephony. Basically, you can "conference" and it
+                // will conference with the background call.  We know that otherConnection is the
+                // background call because it would never have called setConferenceableConnections()
+                // otherwise.
                 getPhone().conference();
             } catch (CallStateException e) {
                 Log.e(this, e, "Failed to conference call.");
@@ -81,6 +86,5 @@
     @Override
     void onRemovedFromCallService() {
         super.onRemovedFromCallService();
-        GsmConferenceController.remove(this);
     }
 }
diff --git a/src/com/android/services/telephony/TelephonyConnection.java b/src/com/android/services/telephony/TelephonyConnection.java
index 7ddb6d0..0ffb7c7 100644
--- a/src/com/android/services/telephony/TelephonyConnection.java
+++ b/src/com/android/services/telephony/TelephonyConnection.java
@@ -31,7 +31,6 @@
 import com.android.internal.telephony.Phone;
 
 import java.lang.Override;
-import java.util.List;
 import java.util.Objects;
 
 /**
@@ -204,51 +203,12 @@
 
     @Override
     public void onHold() {
-        Log.v(this, "onHold");
-        // TODO: Can dialing calls be put on hold as well since they take up the
-        // foreground call slot?
-        if (Call.State.ACTIVE == mOriginalConnectionState) {
-            Log.v(this, "Holding active call");
-            try {
-                Phone phone = mOriginalConnection.getCall().getPhone();
-                Call ringingCall = phone.getRingingCall();
-
-                // Although the method says switchHoldingAndActive, it eventually calls a RIL method
-                // called switchWaitingOrHoldingAndActive. What this means is that if we try to put
-                // a call on hold while a call-waiting call exists, it'll end up accepting the
-                // call-waiting call, which is bad if that was not the user's intention. We are
-                // cheating here and simply skipping it because we know any attempt to hold a call
-                // while a call-waiting call is happening is likely a request from Telecomm prior to
-                // accepting the call-waiting call.
-                // TODO: Investigate a better solution. It would be great here if we
-                // could "fake" hold by silencing the audio and microphone streams for this call
-                // instead of actually putting it on hold.
-                if (ringingCall.getState() != Call.State.WAITING) {
-                    phone.switchHoldingAndActive();
-                }
-
-                // TODO: Cdma calls are slightly different.
-            } catch (CallStateException e) {
-                Log.e(this, e, "Exception occurred while trying to put call on hold.");
-            }
-        } else {
-            Log.w(this, "Cannot put a call that is not currently active on hold.");
-        }
+        performHold();
     }
 
     @Override
     public void onUnhold() {
-        Log.v(this, "onUnhold");
-        if (Call.State.HOLDING == mOriginalConnectionState) {
-            try {
-                // TODO: This doesn't handle multiple calls across connection services yet
-                mOriginalConnection.getCall().getPhone().switchHoldingAndActive();
-            } catch (CallStateException e) {
-                Log.e(this, e, "Exception occurred while trying to release call from hold.");
-            }
-        } else {
-            Log.w(this, "Cannot release a call that is not already on hold from hold.");
-        }
+        performUnhold();
     }
 
     @Override
@@ -288,15 +248,59 @@
     }
 
     @Override
-    public void onChildrenChanged(List<Connection> children) {
-        Log.v(this, "onChildrenChanged, children: " + children);
-    }
-
-    @Override
     public void onPhoneAccountClicked() {
         Log.v(this, "onPhoneAccountClicked");
     }
 
+    public void performHold() {
+        Log.v(this, "performHold");
+        // TODO: Can dialing calls be put on hold as well since they take up the
+        // foreground call slot?
+        if (Call.State.ACTIVE == mOriginalConnectionState) {
+            Log.v(this, "Holding active call");
+            try {
+                Phone phone = mOriginalConnection.getCall().getPhone();
+                Call ringingCall = phone.getRingingCall();
+
+                // Although the method says switchHoldingAndActive, it eventually calls a RIL method
+                // called switchWaitingOrHoldingAndActive. What this means is that if we try to put
+                // a call on hold while a call-waiting call exists, it'll end up accepting the
+                // call-waiting call, which is bad if that was not the user's intention. We are
+                // cheating here and simply skipping it because we know any attempt to hold a call
+                // while a call-waiting call is happening is likely a request from Telecomm prior to
+                // accepting the call-waiting call.
+                // TODO: Investigate a better solution. It would be great here if we
+                // could "fake" hold by silencing the audio and microphone streams for this call
+                // instead of actually putting it on hold.
+                if (ringingCall.getState() != Call.State.WAITING) {
+                    phone.switchHoldingAndActive();
+                }
+
+                // TODO: Cdma calls are slightly different.
+            } catch (CallStateException e) {
+                Log.e(this, e, "Exception occurred while trying to put call on hold.");
+            }
+        } else {
+            Log.w(this, "Cannot put a call that is not currently active on hold.");
+        }
+    }
+
+    public void performUnhold() {
+        Log.v(this, "performUnhold");
+        if (Call.State.HOLDING == mOriginalConnectionState) {
+            try {
+                // TODO: This doesn't handle multiple calls across connection services yet
+                mOriginalConnection.getCall().getPhone().switchHoldingAndActive();
+            } catch (CallStateException e) {
+                Log.e(this, e, "Exception occurred while trying to release call from hold.");
+            }
+        } else {
+            Log.w(this, "Cannot release a call that is not already on hold from hold.");
+        }
+    }
+
+    public void performConference(TelephonyConnection otherConnection) {}
+
     protected abstract int buildCallCapabilities();
 
     protected final void updateCallCapabilities() {
@@ -356,12 +360,8 @@
     private void hangup(int disconnectCause) {
         if (mOriginalConnection != null) {
             try {
-                Call call = mOriginalConnection.getCall();
-                if (call != null && !call.isMultiparty()) {
-                    call.hangup();
-                } else {
-                    mOriginalConnection.hangup();
-                }
+                mOriginalConnection.hangup();
+
                 // Set state deliberately since we are going to close() and will no longer be
                 // listening to state updates from mOriginalConnection
                 setDisconnected(disconnectCause, null);
diff --git a/src/com/android/services/telephony/TelephonyConnectionService.java b/src/com/android/services/telephony/TelephonyConnectionService.java
index 04da3e7..742a7d2 100644
--- a/src/com/android/services/telephony/TelephonyConnectionService.java
+++ b/src/com/android/services/telephony/TelephonyConnectionService.java
@@ -47,6 +47,8 @@
 public class TelephonyConnectionService extends ConnectionService {
     static String SCHEME_TEL = "tel";
 
+    private final GsmConferenceController mGsmConferenceController =
+            new GsmConferenceController(this);
     private ComponentName mExpectedComponentName = null;
     private EmergencyCallHelper mEmergencyCallHelper;
 
@@ -153,20 +155,6 @@
     }
 
     @Override
-    public void onCreateConferenceConnection(
-            String token,
-            Connection connection,
-            Response<String, Connection> response) {
-        Log.v(this, "onCreateConferenceConnection, connection: " + connection);
-        if (connection instanceof GsmConnection || connection instanceof ConferenceConnection) {
-            if ((connection.getCallCapabilities() & PhoneCapabilities.MERGE_CALLS) != 0) {
-                response.onResult(token,
-                        GsmConferenceController.createConferenceConnection(connection));
-            }
-        }
-    }
-
-    @Override
     public Connection onCreateIncomingConnection(
             PhoneAccountHandle connectionManagerPhoneAccount,
             ConnectionRequest request) {
@@ -199,6 +187,30 @@
         }
     }
 
+    @Override
+    public void onConference(Connection connection1, Connection connection2) {
+        if (connection1 instanceof TelephonyConnection &&
+                connection2 instanceof TelephonyConnection) {
+            ((TelephonyConnection) connection1).performConference(
+                (TelephonyConnection) connection2);
+        }
+
+    }
+
+    @Override
+    public void onConnectionAdded(Connection connection) {
+        if (connection instanceof GsmConnection) {
+            mGsmConferenceController.add((GsmConnection) connection);
+        }
+    }
+
+    @Override
+    public void onConnectionRemoved(Connection connection) {
+        if (connection instanceof GsmConnection) {
+            mGsmConferenceController.remove((GsmConnection) connection);
+        }
+    }
+
     private void placeOutgoingConnection(
             TelephonyConnection connection, Phone phone, ConnectionRequest request) {
         String number = connection.getHandle().getSchemeSpecificPart();