Merge "Support conference calling. (3/4) [DO NOT MERGE]" into lmp-preview-dev
diff --git a/src/com/android/services/telephony/ConferenceConnection.java b/src/com/android/services/telephony/ConferenceConnection.java
new file mode 100644
index 0000000..43095b1
--- /dev/null
+++ b/src/com/android/services/telephony/ConferenceConnection.java
@@ -0,0 +1,73 @@
+/*
+ * 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.
+ */
+class ConferenceConnection extends Connection {
+    @Override
+    protected void onChildrenChanged(List<Connection> children) {
+        if (children.isEmpty()) {
+            setDisconnected(DisconnectCause.LOCAL, "conference call disconnected.");
+            setDestroyed();
+        }
+    }
+
+    /** ${inheritDoc} */
+    @Override
+    protected 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 ot 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
+    protected void onHold() {
+        List<Connection> children = getChildConnections();
+        if (!children.isEmpty()) {
+            // Hold only needs to be called on one of the children.
+            children.get(0).hold();
+        }
+    }
+
+}
diff --git a/src/com/android/services/telephony/GsmConnection.java b/src/com/android/services/telephony/GsmConnection.java
index 50ef154..d37accd 100644
--- a/src/com/android/services/telephony/GsmConnection.java
+++ b/src/com/android/services/telephony/GsmConnection.java
@@ -16,6 +16,7 @@
 
 package com.android.services.telephony;
 
+import com.android.internal.telephony.CallStateException;
 import com.android.internal.telephony.Connection;
 import com.android.internal.telephony.Phone;
 
@@ -41,4 +42,21 @@
         getPhone().stopDtmf();
         super.onStopDtmfTone();
     }
+
+    /** {@inheritDoc} */
+    @Override
+    public void onConference() {
+        try {
+            Log.d(this, "conference - %s", this);
+            getPhone().conference();
+        } catch (CallStateException e) {
+            Log.e(this, e, "Failed to conference call.");
+        }
+    }
+
+    @Override
+    public void setIsConferenceCapable(boolean isConferenceCapable) {
+        // This method increases access modifier.
+        super.setIsConferenceCapable(isConferenceCapable);
+    }
 }
diff --git a/src/com/android/services/telephony/GsmConnectionService.java b/src/com/android/services/telephony/GsmConnectionService.java
index 3fa59db..ae5dc22 100644
--- a/src/com/android/services/telephony/GsmConnectionService.java
+++ b/src/com/android/services/telephony/GsmConnectionService.java
@@ -20,16 +20,48 @@
 import android.net.Uri;
 import android.telephony.TelephonyManager;
 
+import com.android.internal.telephony.Call;
 import com.android.internal.telephony.Connection;
 import com.android.internal.telephony.Phone;
 import com.android.internal.telephony.PhoneFactory;
 import com.android.phone.Constants;
+
+import java.util.Collection;
+import java.util.HashSet;
+
+import android.telecomm.CallState;
 import android.telecomm.ConnectionRequest;
+import android.telecomm.Response;
 
 /**
  * Connnection service that uses GSM.
  */
 public class GsmConnectionService extends PstnConnectionService {
+
+    private final android.telecomm.Connection.Listener mConnectionListener =
+            new android.telecomm.Connection.ListenerBase() {
+                @Override
+                public void onStateChanged(android.telecomm.Connection c, int state) {
+                    // No need to recalculate for conference calls, just traditional calls.
+                    if (c != mConferenceConnection) {
+                        recalculateConferenceState();
+                    }
+                }
+
+                /** ${inheritDoc} */
+                @Override
+                public void onDisconnected(
+                        android.telecomm.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);
+                }
+
+            };
+
+    /** The conferenc connection object. */
+    private ConferenceConnection mConferenceConnection;
+
     /** {@inheritDoc} */
     @Override
     protected Phone getPhone() {
@@ -56,4 +88,73 @@
             ConnectionRequest request, Connection connection) {
         return new GsmConnection(getPhone(), connection);
     }
+
+    /** {@inheritDoc} */
+    @Override
+    public void onConnectionAdded(android.telecomm.Connection connection) {
+        connection.addConnectionListener(mConnectionListener);
+        recalculateConferenceState();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void onConnectionRemoved(android.telecomm.Connection connection) {
+        connection.removeConnectionListener(mConnectionListener);
+        recalculateConferenceState();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void onCreateConferenceConnection(
+            String token,
+            android.telecomm.Connection telecommConnection,
+            Response<String, android.telecomm.Connection> callback) {
+        if (mConferenceConnection == null) {
+            mConferenceConnection = new ConferenceConnection();
+            Log.d(this, "creating the conference connection: %s", mConferenceConnection);
+        }
+        callback.onResult(token, mConferenceConnection);
+        telecommConnection.conference();
+    }
+
+    /**
+     * Calculates the conference-capable state of all connections in this connection service.
+     */
+    private void recalculateConferenceState() {
+        Log.v(this, "recalculateConferenceState");
+        Collection<android.telecomm.Connection> allConnections = this.getAllConnections();
+        for (android.telecomm.Connection connection : new HashSet<>(allConnections)) {
+            Log.d(this, "recalc - %s", connection);
+            if (connection instanceof GsmConnection) {
+                boolean isConferenceCapable = false;
+                Connection radioConnection = ((GsmConnection) connection).getOriginalConnection();
+                if (radioConnection != null) {
+
+                    // First calculate to see if we are in the conference call. We only support a
+                    // single active conference call on PSTN, which makes things a little easier.
+                    if (mConferenceConnection != null) {
+                        if (radioConnection.getCall().isMultiparty()) {
+                            connection.setParentConnection(mConferenceConnection);
+                        } else {
+                            connection.setParentConnection(null);
+                        }
+                    }
+
+                    boolean callIsActive = radioConnection.getState() == Call.State.ACTIVE;
+                    boolean isConferenced =
+                            callIsActive && radioConnection.getCall().isMultiparty();
+                    boolean hasBackgroundCall = getPhone().getBackgroundCall().hasConnections();
+                    Log.d(this, "recalc: active: %b, is_conf: %b, has_bkgd: %b",
+                            callIsActive, isConferenced, hasBackgroundCall);
+                    // We only set conference capable on:
+                    // 1) Active calls,
+                    // 2) which are not already part of a conference call
+                    // 3) and there exists a call on HOLD
+                    isConferenceCapable = callIsActive && !isConferenced && hasBackgroundCall;
+                }
+
+                ((GsmConnection) connection).setIsConferenceCapable(isConferenceCapable);
+            }
+        }
+    }
 }
diff --git a/src/com/android/services/telephony/TelephonyConnection.java b/src/com/android/services/telephony/TelephonyConnection.java
index 0d84134..43e72c8 100644
--- a/src/com/android/services/telephony/TelephonyConnection.java
+++ b/src/com/android/services/telephony/TelephonyConnection.java
@@ -61,6 +61,18 @@
     }
 
     @Override
+    protected void onSeparate() {
+        if (mOriginalConnection != null) {
+            try {
+                mOriginalConnection.separate();
+            } catch (CallStateException e) {
+                Log.e(this, e, "Call to Connection.separate failed with exception");
+            }
+        }
+        super.onSeparate();
+    }
+
+    @Override
     protected void onHold() {
         Log.d(this, "Attempting to put call on hold");
         // TODO(santoscordon): Can dialing calls be put on hold as well since they take up the
@@ -127,8 +139,10 @@
         if (mOriginalConnection != null) {
             try {
                 Call call = mOriginalConnection.getCall();
-                if (call != null) {
+                if (call != null && !call.isMultiparty()) {
                     call.hangup();
+                } else {
+                    mOriginalConnection.hangup();
                 }
                 // Set state deliberately since we are going to close() and will no longer be
                 // listening to state updates from mOriginalConnection
@@ -146,35 +160,34 @@
         }
 
         Call.State newState = mOriginalConnection.getState();
-        if (mState == newState) {
-            return;
-        }
+        Log.v(this, "Update state from %s to %s for %s", mState, newState, this);
+        if (mState != newState) {
+            Log.d(this, "mOriginalConnection new state = %s", newState);
 
-        Log.d(this, "mOriginalConnection new state = %s", newState);
-
-        mState = newState;
-        switch (newState) {
-            case IDLE:
-                break;
-            case ACTIVE:
-                setActive();
-                break;
-            case HOLDING:
-                setOnHold();
-                break;
-            case DIALING:
-            case ALERTING:
-                setDialing();
-                break;
-            case INCOMING:
-            case WAITING:
-                setRinging();
-                break;
-            case DISCONNECTED:
-                setDisconnected(mOriginalConnection.getDisconnectCause(), null);
-                break;
-            case DISCONNECTING:
-                break;
+            mState = newState;
+            switch (newState) {
+                case IDLE:
+                    break;
+                case ACTIVE:
+                    setActive();
+                    break;
+                case HOLDING:
+                    setOnHold();
+                    break;
+                case DIALING:
+                case ALERTING:
+                    setDialing();
+                    break;
+                case INCOMING:
+                case WAITING:
+                    setRinging();
+                    break;
+                case DISCONNECTED:
+                    setDisconnected(mOriginalConnection.getDisconnectCause(), null);
+                    break;
+                case DISCONNECTING:
+                    break;
+            }
         }
     }
 
@@ -185,6 +198,7 @@
                 call.getPhone().unregisterForPreciseCallStateChanged(mHandler);
             }
             mOriginalConnection = null;
+            setDestroyed();
         }
     }