Support conference calling. (2/4)

Bug: 15006702
Change-Id: I2764ea242f783ba478c9eae86618dd33e9fc792a
diff --git a/src/com/android/telecomm/Call.java b/src/com/android/telecomm/Call.java
index 66f9366..607aeb1 100644
--- a/src/com/android/telecomm/Call.java
+++ b/src/com/android/telecomm/Call.java
@@ -40,6 +40,9 @@
 import com.google.android.collect.Sets;
 import com.google.common.base.Preconditions;
 
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
 import java.util.Locale;
 import java.util.Set;
 
@@ -60,6 +63,11 @@
         void onFailedIncomingCall(Call call);
         void onRequestingRingback(Call call, boolean requestingRingback);
         void onPostDialWait(Call call, String remaining);
+        void onIsConferenceCapableChanged(Call call, boolean isConferenceCapable);
+        void onExpiredConferenceCall(Call call);
+        void onConfirmedConferenceCall(Call call);
+        void onParentChanged(Call call);
+        void onChildrenChanged(Call call);
     }
 
     private static final OnQueryCompleteListener sCallerInfoQueryListener =
@@ -180,13 +188,21 @@
     /** Incoming call-info to use when direct-to-voicemail query finishes. */
     private CallInfo mPendingDirectToVoicemailCallInfo;
 
+    private boolean mIsConferenceCapable = false;
+
+    private boolean mIsConference = false;
+
+    private Call mParentCall = null;
+
+    private List<Call> mChildCalls = new LinkedList<>();
+
     /**
      * Creates an empty call object.
      *
      * @param isIncoming True if this is an incoming call.
      */
-    Call(boolean isIncoming) {
-        this(null, null, isIncoming);
+    Call(boolean isIncoming, boolean isConference) {
+        this(null, null, isIncoming, isConference);
     }
 
     /**
@@ -196,11 +212,12 @@
      * @param gatewayInfo Gateway information to use for the call.
      * @param isIncoming True if this is an incoming call.
      */
-    Call(Uri handle, GatewayInfo gatewayInfo, boolean isIncoming) {
-        mState = CallState.NEW;
+    Call(Uri handle, GatewayInfo gatewayInfo, boolean isIncoming, boolean isConference) {
+        mState = isConference ? CallState.ACTIVE : CallState.NEW;
         setHandle(handle);
         mGatewayInfo = gatewayInfo;
         mIsIncoming = isIncoming;
+        mIsConference = isConference;
     }
 
     void addListener(Listener listener) {
@@ -221,7 +238,15 @@
     }
 
     CallState getState() {
-        return mState;
+        if (mIsConference) {
+            if (!mChildCalls.isEmpty()) {
+                // If we have child calls, just return the child call.
+                return mChildCalls.get(0).getState();
+            }
+            return CallState.ACTIVE;
+        } else {
+            return mState;
+        }
     }
 
     /**
@@ -343,6 +368,27 @@
         mConnectTimeMillis = connectTimeMillis;
     }
 
+    boolean isConferenceCapable() {
+        return mIsConferenceCapable;
+    }
+
+    void setIsConferenceCapable(boolean isConferenceCapable) {
+        if (mIsConferenceCapable != isConferenceCapable) {
+            mIsConferenceCapable = isConferenceCapable;
+            for (Listener l : mListeners) {
+                l.onIsConferenceCapableChanged(this, mIsConferenceCapable);
+            }
+        }
+    }
+
+    Call getParentCall() {
+        return mParentCall;
+    }
+
+    List<Call> getChildCalls() {
+        return mChildCalls;
+    }
+
     CallServiceWrapper getCallService() {
         return mCallService;
     }
@@ -450,7 +496,7 @@
             public void run() {
                 processDirectToVoicemail();
             }
-        }, Timeouts.getDirectToVoicemail());
+        }, Timeouts.getDirectToVoicemailMillis());
     }
 
     void processDirectToVoicemail() {
@@ -740,6 +786,73 @@
         getCallService().onPostDialContinue(this, proceed);
     }
 
+    void conferenceInto(Call conferenceCall) {
+        if (mCallService == null) {
+            Log.w(this, "conference requested on a call without a call service.");
+        } else {
+            mCallService.conference(conferenceCall, this);
+        }
+    }
+
+    void expireConference() {
+        // The conference call expired before we got a confirmation of the conference from the
+        // call service...so start shutting down.
+        clearCallService();
+        for (Listener l : mListeners) {
+            l.onExpiredConferenceCall(this);
+        }
+    }
+
+    void confirmConference() {
+        Log.v(this, "confirming Conf call %s", mListeners);
+        for (Listener l : mListeners) {
+            l.onConfirmedConferenceCall(this);
+        }
+    }
+
+    void splitFromConference() {
+        // TODO(santoscordon): todo
+    }
+
+    void setParentCall(Call parentCall) {
+        if (parentCall == this) {
+            Log.e(this, new Exception(), "setting the parent to self");
+            return;
+        }
+        Preconditions.checkState(parentCall == null || mParentCall == null);
+
+        Call oldParent = mParentCall;
+        if (mParentCall != null) {
+            mParentCall.removeChildCall(this);
+        }
+        mParentCall = parentCall;
+        if (mParentCall != null) {
+            mParentCall.addChildCall(this);
+        }
+
+        for (Listener l : mListeners) {
+            l.onParentChanged(this);
+        }
+    }
+
+    private void addChildCall(Call call) {
+        if (!mChildCalls.contains(call)) {
+            mChildCalls.add(call);
+
+            for (Listener l : mListeners) {
+                l.onChildrenChanged(this);
+            }
+        }
+    }
+
+    private void removeChildCall(Call call) {
+        if (mChildCalls.remove(call)) {
+            for (Listener l : mListeners) {
+                l.onChildrenChanged(this);
+            }
+        }
+    }
+
     /**
      * @return True if the call is ringing, else logs the action name.
      */
diff --git a/src/com/android/telecomm/CallIdMapper.java b/src/com/android/telecomm/CallIdMapper.java
index 7f92067..e6b5c1f 100644
--- a/src/com/android/telecomm/CallIdMapper.java
+++ b/src/com/android/telecomm/CallIdMapper.java
@@ -38,12 +38,15 @@
         mCalls.put(callId, newCall);
     }
 
+    void addCall(Call call, String id) {
+        Preconditions.checkNotNull(call);
+        ThreadUtil.checkOnMainThread();
+        mCalls.put(id, call);
+    }
+
     void addCall(Call call) {
         ThreadUtil.checkOnMainThread();
-        Preconditions.checkNotNull(call);
-        sIdCount++;
-        String callId = mCallIdPrefix + sIdCount;
-        mCalls.put(callId, call);
+        addCall(call, getNewId());
     }
 
     void removeCall(Call call) {
@@ -91,4 +94,9 @@
         // Note, no need for thread check, this method is thread safe.
         return callId != null && callId.startsWith(mCallIdPrefix);
     }
+
+    String getNewId() {
+        sIdCount++;
+        return mCallIdPrefix + sIdCount;
+    }
 }
diff --git a/src/com/android/telecomm/CallServiceWrapper.java b/src/com/android/telecomm/CallServiceWrapper.java
index 6165804..4462cff 100644
--- a/src/com/android/telecomm/CallServiceWrapper.java
+++ b/src/com/android/telecomm/CallServiceWrapper.java
@@ -35,11 +35,11 @@
 import com.android.internal.telecomm.ICallServiceProvider;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Sets;
 
 import org.apache.http.conn.ClientConnectionRequest;
 
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -50,6 +50,7 @@
  * and instead should use this class to invoke methods of {@link ICallService}.
  */
 final class CallServiceWrapper extends ServiceBinder<ICallService> {
+    private static final String TAG = CallServiceWrapper.class.getSimpleName();
 
     private final class Adapter extends ICallServiceAdapter.Stub {
         private static final int MSG_NOTIFY_INCOMING_CALL = 1;
@@ -62,7 +63,10 @@
         private static final int MSG_SET_ON_HOLD = 8;
         private static final int MSG_SET_REQUESTING_RINGBACK = 9;
         private static final int MSG_ON_POST_DIAL_WAIT = 10;
-        private static final int MSG_HANDOFF_CALL = 11;
+        private static final int MSG_CAN_CONFERENCE = 11;
+        private static final int MSG_SET_IS_CONFERENCED = 12;
+        private static final int MSG_ADD_CONFERENCE_CALL = 13;
+        private static final int MSG_HANDOFF_CALL = 14;
 
         private final Handler mHandler = new Handler() {
             @Override
@@ -79,8 +83,7 @@
                             mIncomingCallsManager.handleSuccessfulIncomingCall(call, callInfo);
                         } else {
                             Log.w(this, "notifyIncomingCall, unknown incoming call: %s, id: %s",
-                                    call,
-                                    clientCallInfo.getId());
+                                    call, clientCallInfo.getId());
                         }
                         break;
                     case MSG_HANDLE_SUCCESSFUL_OUTGOING_CALL: {
@@ -176,7 +179,7 @@
                         }
                         break;
                     }
-                    case MSG_ON_POST_DIAL_WAIT:
+                    case MSG_ON_POST_DIAL_WAIT: {
                         SomeArgs args = (SomeArgs) msg.obj;
                         try {
                             call = mCallIdMapper.getCall(args.arg1);
@@ -189,6 +192,8 @@
                         } finally {
                             args.recycle();
                         }
+                        break;
+                    }
                     case MSG_HANDOFF_CALL:
                         call = mCallIdMapper.getCall(msg.obj);
                         if (call != null) {
@@ -197,6 +202,63 @@
                             Log.w(this, "handoffCall, unknown call id: %s", msg.obj);
                         }
                         break;
+                    case MSG_CAN_CONFERENCE: {
+                        call = mCallIdMapper.getCall(msg.obj);
+                        if (call != null) {
+                            call.setIsConferenceCapable(msg.arg1 == 1);
+                        } else {
+                            Log.w(CallServiceWrapper.this, "canConference, unknown call id: %s",
+                                    msg.obj);
+                        }
+                        break;
+                    }
+                    case MSG_SET_IS_CONFERENCED: {
+                        SomeArgs args = (SomeArgs) msg.obj;
+                        try {
+                            Call childCall = mCallIdMapper.getCall(args.arg1);
+                            if (childCall != null) {
+                                String conferenceCallId = (String) args.arg2;
+                                Log.d(this, "setIsConferenced %s, %s", childCall, conferenceCallId);
+
+                                if (conferenceCallId == null) {
+                                    childCall.setParentCall(null);
+                                } else {
+                                    Call conferenceCall = mCallIdMapper.getCall(conferenceCallId);
+                                    if (conferenceCall != null &&
+                                            !mPendingConferenceCalls.contains(conferenceCall)) {
+                                        childCall.setParentCall(conferenceCall);
+                                    } else {
+                                        Log.w(this, "setIsConferenced, unknown conference id %s",
+                                                conferenceCallId);
+                                    }
+                                }
+                            } else {
+                                Log.w(this, "setIsConferenced, unknown call id: %s", args.arg1);
+                            }
+                        } finally {
+                            args.recycle();
+                        }
+                        break;
+                    }
+                    case MSG_ADD_CONFERENCE_CALL: {
+                        SomeArgs args = (SomeArgs) msg.obj;
+                        try {
+                            String callId = (String) args.arg1;
+                            Log.d(this, "addConferenceCall attempt %s, %s",
+                                    callId, mPendingConferenceCalls);
+
+                            Call conferenceCall = mCallIdMapper.getCall(callId);
+                            if (mPendingConferenceCalls.remove(conferenceCall)) {
+                                Log.v(this, "confirming conf call %s", conferenceCall);
+                                conferenceCall.confirmConference();
+                            } else {
+                                Log.w(this, "addConference, unknown call id: %s", callId);
+                            }
+                        } finally {
+                            args.recycle();
+                        }
+                        break;
+                    }
                 }
             }
         };
@@ -294,12 +356,29 @@
 
         /** ${inheritDoc} */
         @Override
-        public void setCanConferenceWith(String callId, List<String> conferenceCapableCallIds) {
+        public void setCanConference(String callId, boolean canConference) {
+            Log.d(this, "setCanConference(%s, %b)", callId, canConference);
+            mHandler.obtainMessage(MSG_CAN_CONFERENCE, canConference ? 1 : 0, 0, callId)
+                    .sendToTarget();
         }
 
         /** ${inheritDoc} */
         @Override
-        public void setIsConferenced(String conferenceCallId, String callId, boolean isConferenced) {
+        public void setIsConferenced(String callId, String conferenceCallId) {
+            SomeArgs args = SomeArgs.obtain();
+            args.arg1 = callId;
+            args.arg2 = conferenceCallId;
+            mHandler.obtainMessage(MSG_SET_IS_CONFERENCED, args).sendToTarget();
+        }
+
+        /** ${InheritDoc} */
+        @Override
+        public void addConferenceCall(String callId, CallInfo callInfo) {
+            mCallIdMapper.checkValidCallId(callId);
+            SomeArgs args = SomeArgs.obtain();
+            args.arg1 = callId;
+            args.arg2 = callInfo;
+            mHandler.obtainMessage(MSG_ADD_CONFERENCE_CALL, args).sendToTarget();
         }
 
         @Override
@@ -321,11 +400,13 @@
 
     private final Adapter mAdapter = new Adapter();
     private final CallsManager mCallsManager = CallsManager.getInstance();
-    private final Set<Call> mPendingIncomingCalls = Sets.newHashSet();
+    private final Set<Call> mPendingIncomingCalls = new HashSet<>();
+    private final Set<Call> mPendingConferenceCalls = new HashSet<>();
     private final CallServiceDescriptor mDescriptor;
     private final CallIdMapper mCallIdMapper = new CallIdMapper("CallService");
     private final IncomingCallsManager mIncomingCallsManager;
     private final Map<String, AsyncResultCallback<Boolean>> mPendingOutgoingCalls = new HashMap<>();
+    private final Handler mHandler = new Handler();
 
     private Binder mBinder = new Binder();
     private ICallService mServiceInterface;
@@ -521,7 +602,9 @@
     }
 
     void addCall(Call call) {
-        mCallIdMapper.addCall(call);
+        if (mCallIdMapper.getCallId(call) == null) {
+            mCallIdMapper.addCall(call);
+        }
     }
 
     /**
@@ -553,6 +636,37 @@
         }
     }
 
+    void conference(final Call conferenceCall, Call call) {
+        if (isServiceValid("conference")) {
+            try {
+                conferenceCall.setCallService(this);
+                mPendingConferenceCalls.add(conferenceCall);
+                mHandler.postDelayed(new Runnable() {
+                    @Override public void run() {
+                        if (mPendingConferenceCalls.remove(conferenceCall)) {
+                            conferenceCall.expireConference();
+                            Log.i(this, "Conference call expired: %s", conferenceCall);
+                        }
+                    }
+                }, Timeouts.getConferenceCallExpireMillis());
+
+                mServiceInterface.conference(
+                        mCallIdMapper.getCallId(conferenceCall),
+                        mCallIdMapper.getCallId(call));
+            } catch (RemoteException ignored) {
+            }
+        }
+    }
+
+    void splitFromConference(Call call) {
+        if (isServiceValid("splitFromConference")) {
+            try {
+                mServiceInterface.splitFromConference(mCallIdMapper.getCallId(call));
+            } catch (RemoteException ignored) {
+            }
+        }
+    }
+
     /** {@inheritDoc} */
     @Override
     protected void setServiceInterface(IBinder binder) {
@@ -589,12 +703,12 @@
                 mIncomingCallsManager.handleFailedIncomingCall(call);
             }
 
-      if (!mPendingIncomingCalls.isEmpty()) {
-        Log.wtf(this, "Pending calls did not get cleared.");
-        mPendingIncomingCalls.clear();
-      }
-    }
+            if (!mPendingIncomingCalls.isEmpty()) {
+                Log.wtf(this, "Pending calls did not get cleared.");
+                mPendingIncomingCalls.clear();
+            }
+        }
 
-    mCallIdMapper.clear();
-  }
+        mCallIdMapper.clear();
+    }
 }
diff --git a/src/com/android/telecomm/CallsManager.java b/src/com/android/telecomm/CallsManager.java
index 9a9e235..6057350 100644
--- a/src/com/android/telecomm/CallsManager.java
+++ b/src/com/android/telecomm/CallsManager.java
@@ -61,6 +61,8 @@
         void onForegroundCallChanged(Call oldForegroundCall, Call newForegroundCall);
         void onAudioStateChanged(CallAudioState oldAudioState, CallAudioState newAudioState);
         void onRequestingRingback(Call call, boolean ringback);
+        void onIsConferenceCapableChanged(Call call, boolean isConferenceCapable);
+        void onIsConferencedChanged(Call call);
     }
 
     private static final CallsManager INSTANCE = new CallsManager();
@@ -165,6 +167,13 @@
     }
 
     @Override
+    public void onIsConferenceCapableChanged(Call call, boolean isConferenceCapable) {
+        for (CallsManagerListener listener : mListeners) {
+            listener.onIsConferenceCapableChanged(call, isConferenceCapable);
+        }
+    }
+
+    @Override
     public void onRequestingRingback(Call call, boolean ringback) {
         for (CallsManagerListener listener : mListeners) {
             listener.onRequestingRingback(call, ringback);
@@ -176,6 +185,34 @@
         mInCallController.onPostDialWait(call, remaining);
     }
 
+    @Override
+    public void onExpiredConferenceCall(Call call) {
+        call.removeListener(this);
+    }
+
+    @Override
+    public void onConfirmedConferenceCall(Call call) {
+        addCall(call);
+        Log.v(this, "confirming Conf call %s", call);
+        for (CallsManagerListener listener : mListeners) {
+            listener.onIsConferencedChanged(call);
+        }
+    }
+
+    @Override
+    public void onParentChanged(Call call) {
+        for (CallsManagerListener listener : mListeners) {
+            listener.onIsConferencedChanged(call);
+        }
+    }
+
+    @Override
+    public void onChildrenChanged(Call call) {
+        for (CallsManagerListener listener : mListeners) {
+            listener.onIsConferencedChanged(call);
+        }
+    }
+
     ImmutableCollection<Call> getCalls() {
         return ImmutableList.copyOf(mCalls);
     }
@@ -218,7 +255,7 @@
         // Create a call with no handle. Eventually, switchboard will update the call with
         // additional information from the call service, but for now we just need one to pass
         // around.
-        Call call = new Call(true /* isIncoming */);
+        Call call = new Call(true /* isIncoming */, false /* isConference */);
         // TODO(santoscordon): Move this to be a part of addCall()
         call.addListener(this);
 
@@ -243,7 +280,8 @@
                     Log.pii(uriHandle), Log.pii(handle));
         }
 
-        Call call = new Call(uriHandle, gatewayInfo, false /* isIncoming */);
+        Call call = new Call(
+                uriHandle, gatewayInfo, false /* isIncoming */, false /* isConference */);
 
         // TODO(santoscordon): Move this to be a part of addCall()
         call.addListener(this);
@@ -253,6 +291,17 @@
     }
 
     /**
+     * Attempts to start a conference call for the specified call.
+     *
+     * @param call The call to conference with.
+     */
+    void conference(Call call) {
+        Call conferenceCall = new Call(false, true);
+        conferenceCall.addListener(this);
+        call.conferenceInto(conferenceCall);
+    }
+
+    /**
      * Instructs Telecomm to answer the specified call. Intended to be invoked by the in-call
      * app through {@link InCallAdapter} after Telecomm notifies it of an incoming call followed by
      * the user opting to answer said call.
@@ -410,8 +459,8 @@
         // original call will live on but its state will be updated to the new call's state. In
         // particular the original call's call service will be updated to the new call's call
         // service.
-        Call tempCall =
-                new Call(originalCall.getHandoffHandle(), originalCall.getGatewayInfo(), false);
+        Call tempCall = new Call(
+                originalCall.getHandoffHandle(), originalCall.getGatewayInfo(), false, false);
         tempCall.setOriginalCall(originalCall);
         tempCall.setExtras(originalCall.getExtras());
         tempCall.setCallServiceSelector(originalCall.getCallServiceSelector());
diff --git a/src/com/android/telecomm/CallsManagerListenerBase.java b/src/com/android/telecomm/CallsManagerListenerBase.java
index d3ea758..60fdbf6 100644
--- a/src/com/android/telecomm/CallsManagerListenerBase.java
+++ b/src/com/android/telecomm/CallsManagerListenerBase.java
@@ -74,4 +74,12 @@
     @Override
     public void onRequestingRingback(Call call, boolean ringback) {
     }
+
+    @Override
+    public void onIsConferenceCapableChanged(Call call, boolean isConferenceCapable) {
+    }
+
+    @Override
+    public void onIsConferencedChanged(Call call) {
+    }
 }
diff --git a/src/com/android/telecomm/InCallAdapter.java b/src/com/android/telecomm/InCallAdapter.java
index 0dfcae8..7ae12ab 100644
--- a/src/com/android/telecomm/InCallAdapter.java
+++ b/src/com/android/telecomm/InCallAdapter.java
@@ -39,6 +39,8 @@
     private static final int MSG_HANDOFF_CALL = 8;
     private static final int MSG_MUTE = 9;
     private static final int MSG_SET_AUDIO_ROUTE = 10;
+    private static final int MSG_CONFERENCE = 11;
+    private static final int MSG_SPLIT_FROM_CONFERENCE = 12;
 
     private final class InCallAdapterHandler extends Handler {
         @Override
@@ -86,6 +88,12 @@
                 case MSG_SET_AUDIO_ROUTE:
                     mCallsManager.setAudioRoute(msg.arg1);
                     break;
+                case MSG_CONFERENCE:
+                    mCallsManager.conference(call);
+                    break;
+                case MSG_SPLIT_FROM_CONFERENCE:
+                    call.splitFromConference();
+                    break;
             }
         }
     }
@@ -184,11 +192,13 @@
 
     /** ${inheritDoc} */
     @Override
-    public void conferenceWith(String arg0, String arg1) {
+    public void conference(String callId) {
+        mHandler.obtainMessage(MSG_CONFERENCE, callId).sendToTarget();
     }
 
     /** ${inheritDoc} */
     @Override
-    public void splitFromConference(String arg0) {
+    public void splitFromConference(String callId) {
+        mHandler.obtainMessage(MSG_SPLIT_FROM_CONFERENCE, callId).sendToTarget();
     }
 }
diff --git a/src/com/android/telecomm/InCallController.java b/src/com/android/telecomm/InCallController.java
index 637e03e..434c709 100644
--- a/src/com/android/telecomm/InCallController.java
+++ b/src/com/android/telecomm/InCallController.java
@@ -34,6 +34,10 @@
 import com.android.internal.telecomm.IInCallService;
 import com.google.common.collect.ImmutableCollection;
 
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
 /**
  * Binds to {@link IInCallService} and provides the service to {@link CallsManager} through which it
  * can send updates to the in-call app. This class is created and owned by CallsManager and retains
@@ -153,6 +157,17 @@
         }
     }
 
+    @Override
+    public void onIsConferenceCapableChanged(Call call, boolean isConferenceCapable) {
+        updateCall(call);
+    }
+
+    @Override
+    public void onIsConferencedChanged(Call call) {
+        Log.v(this, "onIsConferencedChanged %s", call);
+        updateCall(call);
+    }
+
     void bringToForeground(boolean showDialpad) {
         if (mInCallService != null) {
             try {
@@ -243,7 +258,9 @@
     private void updateCall(Call call) {
         if (mInCallService != null) {
             try {
-                mInCallService.updateCall(toInCallCall(call));
+                InCallCall inCallCall = toInCallCall(call);
+                Log.v(this, "updateCall %s ==> %s", call, inCallCall);
+                mInCallService.updateCall(inCallCall);
             } catch (RemoteException ignored) {
             }
         }
@@ -262,6 +279,9 @@
         if (CallsManager.getInstance().isAddCallCapable(call)) {
             capabilities |= CallCapabilities.ADD_CALL;
         }
+        if (call.isConferenceCapable()) {
+            capabilities |= CallCapabilities.MERGE_CALLS;
+        }
         CallState state = call.getState();
         if (state == CallState.ABORTED) {
             state = CallState.DISCONNECTED;
@@ -270,9 +290,27 @@
         if (state == CallState.DISCONNECTED && call.getHandoffCallServiceDescriptor() != null) {
             state = CallState.ACTIVE;
         }
+
+        String parentCallId = null;
+        Call parentCall = call.getParentCall();
+        if (parentCall != null) {
+            parentCallId = mCallIdMapper.getCallId(parentCall);
+        }
+
+        long connectTimeMillis = call.getConnectTimeMillis();
+        List<Call> childCalls = call.getChildCalls();
+        List<String> childCallIds = new ArrayList<>();
+        if (!childCalls.isEmpty()) {
+            connectTimeMillis = Long.MAX_VALUE;
+            for (Call child : childCalls) {
+                connectTimeMillis = Math.min(child.getConnectTimeMillis(), connectTimeMillis);
+                childCallIds.add(mCallIdMapper.getCallId(child));
+            }
+        }
+
         return new InCallCall(callId, state, call.getDisconnectCause(), call.getDisconnectMessage(),
-                capabilities, call.getConnectTimeMillis(), call.getHandle(), call.getGatewayInfo(),
-                descriptor, call.getHandoffCallServiceDescriptor());
+                capabilities, connectTimeMillis, call.getHandle(), call.getGatewayInfo(),
+                descriptor, call.getHandoffCallServiceDescriptor(), parentCallId, childCallIds);
     }
 
 }
diff --git a/src/com/android/telecomm/Timeouts.java b/src/com/android/telecomm/Timeouts.java
index d13091f..3e5899c 100644
--- a/src/com/android/telecomm/Timeouts.java
+++ b/src/com/android/telecomm/Timeouts.java
@@ -65,7 +65,14 @@
      * to complete. If the query goes beyond this timeout, the incoming call screen is shown to the
      * user.
      */
-    public static long getDirectToVoicemail() {
+    public static long getDirectToVoicemailMillis() {
         return get("direct_to_voicemail_ms", 500L);
     }
+
+    /**
+     * Returns the amount of time that a connection service has to respond to a "conference" action.
+     */
+    public static long getConferenceCallExpireMillis() {
+        return get("conference_call_expire_ms", 15 * 1000L);
+    }
 }