Handoff: Implement handoff in Telecomm

See the following CLs for more info on how handoff is enabled and
triggered: changeid - I94c28b10c0e0a253450f14d31ecdc416d5b44ca4

Once a Call is handoff enabled it will have a non-null mHandoffHandle.

When handoff is triggered we create a new Call object and set
mOriginalCall.

At this point we have two call objects.
  1st call: Call1
        - mHandoffHandle: non-null
        - mOriginalCall: null
  2nd (invisible) call: Call2
        - mHandoffHandle: null
        - mOriginalCall: non-null

Once the new call's state changes to active we do the following:
      call1.disconnect() // hangup on the old call
      removeCall(call2) // stop tracking the new call
      // merge into call1
      call1.setCallService(call2.getCallService());
      call1.setState(call2.State());

At this point call2 is deleted and call1 has been fully handed off.

Bug: 13643568
Change-Id: I94c28b10c0e0a253450f14d31ecdc416d5b44ca4
diff --git a/src/com/android/telecomm/Call.java b/src/com/android/telecomm/Call.java
index b57ec80..f79a362 100644
--- a/src/com/android/telecomm/Call.java
+++ b/src/com/android/telecomm/Call.java
@@ -17,7 +17,9 @@
 package com.android.telecomm;
 
 import android.net.Uri;
+import android.os.Bundle;
 import android.telecomm.CallInfo;
+import android.telecomm.CallServiceDescriptor;
 import android.telecomm.CallState;
 import android.telecomm.GatewayInfo;
 import android.telephony.DisconnectCause;
@@ -91,6 +93,18 @@
      */
     private String mDisconnectMessage;
 
+    /** Info used by the call services. */
+    private Bundle mExtras;
+
+    /** The Uri to dial to perform the handoff. If this is null then handoff is not supported. */
+    private Uri mHandoffHandle;
+
+    /**
+     * References the call that is being handed off. This value is non-null for untracked calls
+     * that are being used to perform a handoff.
+     */
+    private Call mOriginalCall;
+
     /**
      * Creates an empty call object.
      *
@@ -116,6 +130,7 @@
         mIsIncoming = isIncoming;
         mCreationTime = new Date();
         mDisconnectCause = DisconnectCause.NOT_VALID;
+        mExtras = Bundle.EMPTY;
     }
 
     /** {@inheritDoc} */
@@ -242,6 +257,10 @@
         }
     }
 
+    CallServiceSelectorWrapper getCallServiceSelector() {
+        return mCallServiceSelector;
+    }
+
     void setCallServiceSelector(CallServiceSelectorWrapper selector) {
         Preconditions.checkNotNull(selector);
 
@@ -395,7 +414,13 @@
      * @return An object containing read-only information about this call.
      */
     CallInfo toCallInfo(String callId) {
-        return new CallInfo(callId, mState, mHandle, mGatewayInfo);
+        CallServiceDescriptor descriptor = null;
+        if (mCallService != null) {
+            descriptor = mCallService.getDescriptor();
+        } else if (mOriginalCall != null && mOriginalCall.mCallService != null) {
+            descriptor = mOriginalCall.mCallService.getDescriptor();
+        }
+        return new CallInfo(callId, mState, mHandle, mGatewayInfo, mExtras, descriptor);
     }
 
     /** Checks if this is a live call or not. */
@@ -411,6 +436,30 @@
         }
     }
 
+    Bundle getExtras() {
+        return mExtras;
+    }
+
+    void setExtras(Bundle extras) {
+        mExtras = extras;
+    }
+
+    Uri getHandoffHandle() {
+        return mHandoffHandle;
+    }
+
+    void setHandoffHandle(Uri handoffHandle) {
+        mHandoffHandle = handoffHandle;
+    }
+
+    Call getOriginalCall() {
+        return mOriginalCall;
+    }
+
+    void setOriginalCall(Call originalCall) {
+        mOriginalCall = originalCall;
+    }
+
     /**
      * @return True if the call is ringing, else logs the action name.
      */
diff --git a/src/com/android/telecomm/CallActivity.java b/src/com/android/telecomm/CallActivity.java
index 43c4cf3..77e64bc 100644
--- a/src/com/android/telecomm/CallActivity.java
+++ b/src/com/android/telecomm/CallActivity.java
@@ -22,7 +22,6 @@
 import android.os.Bundle;
 import android.telecomm.CallServiceDescriptor;
 import android.telecomm.TelecommConstants;
-import android.widget.Toast;
 
 /**
  * Activity that handles system CALL actions and forwards them to {@link CallsManager}.
diff --git a/src/com/android/telecomm/CallServiceAdapter.java b/src/com/android/telecomm/CallServiceAdapter.java
index 195ac17..39de235 100644
--- a/src/com/android/telecomm/CallServiceAdapter.java
+++ b/src/com/android/telecomm/CallServiceAdapter.java
@@ -55,7 +55,7 @@
                         mOutgoingCallsManager.setIsCompatibleWith(call,
                                 msg.arg1 == 1 ? true : false);
                     } else {
-                        Log.w(this, "Unknown call: %s, id: %s", call, msg.obj);
+                        Log.w(this, "setIsCompatibleWith, unknown call: %s, id: %s", call, msg.obj);
                     }
                     break;
                 case MSG_NOTIFY_INCOMING_CALL:
@@ -66,7 +66,7 @@
                                 clientCallInfo.getHandle());
                         mIncomingCallsManager.handleSuccessfulIncomingCall(call, callInfo);
                     } else {
-                        Log.w(this, "Unknown incoming call: %s, id: %s", call,
+                        Log.w(this, "notifyIncomingCall, unknown incoming call: %s, id: %s", call,
                                 clientCallInfo.getId());
                     }
                     break;
@@ -76,7 +76,9 @@
                         mOutgoingCallsManager.handleSuccessfulCallAttempt(call);
                     } else {
                         // TODO(gilad): Figure out how to wire up the callService.abort() call.
-                        Log.w(this, "Unknown outgoing call: %s, id: %s", call, msg.obj);
+                        Log.w(this,
+                                "handleSuccessfulOutgoingCall, unknown outgoing call: %s, id: %s",
+                                call, msg.obj);
                     }
                     break;
                 case MSG_HANDLE_FAILED_OUTGOING_CALL: {
@@ -87,7 +89,9 @@
                         if (call != null && mPendingCalls.remove(call) && !call.isIncoming()) {
                             mOutgoingCallsManager.handleFailedCallAttempt(call, reason);
                         } else {
-                            Log.w(this, "Unknown outgoing call: %s, id: %s", call, args.arg1);
+                            Log.w(this,
+                                    "handleFailedOutgoingCall, unknown outgoing call: %s, id: %s",
+                                    call, args.arg1);
                         }
                     } finally {
                         args.recycle();
@@ -99,7 +103,7 @@
                     if (call != null) {
                         mCallsManager.markCallAsActive(call);
                     } else {
-                        Log.w(this, "Unknown call id: %s", msg.obj);
+                        Log.w(this, "setActive, unknown call id: %s", msg.obj);
                     }
                     break;
                 case MSG_SET_RINGING:
@@ -107,7 +111,7 @@
                     if (call != null) {
                         mCallsManager.markCallAsRinging(call);
                     } else {
-                        Log.w(this, "Unknown call id: %s", msg.obj);
+                        Log.w(this, "setRinging, unknown call id: %s", msg.obj);
                     }
                     break;
                 case MSG_SET_DIALING:
@@ -115,7 +119,7 @@
                     if (call != null) {
                         mCallsManager.markCallAsDialing(call);
                     } else {
-                        Log.w(this, "Unknown call id: %s", msg.obj);
+                        Log.w(this, "setDialing, unknown call id: %s", msg.obj);
                     }
                     break;
                 case MSG_SET_DISCONNECTED: {
@@ -128,7 +132,7 @@
                             mCallsManager.markCallAsDisconnected(call, disconnectCause,
                                     disconnectMessage);
                         } else {
-                            Log.w(this, "Unknown call id: %s", args.arg1);
+                            Log.w(this, "setDisconnected, unknown call id: %s", args.arg1);
                         }
                     } finally {
                         args.recycle();
@@ -140,7 +144,7 @@
                     if (call != null) {
                         mCallsManager.markCallAsOnHold(call);
                     } else {
-                        Log.w(this, "Unknown call id: %s", msg.obj);
+                        Log.w(this, "setOnHold, unknown call id: %s", msg.obj);
                     }
                     break;
             }
diff --git a/src/com/android/telecomm/CallServiceSelectorWrapper.java b/src/com/android/telecomm/CallServiceSelectorWrapper.java
index 40c399e..b5adda0 100644
--- a/src/com/android/telecomm/CallServiceSelectorWrapper.java
+++ b/src/com/android/telecomm/CallServiceSelectorWrapper.java
@@ -120,12 +120,51 @@
         mBinder.bind(callback);
     }
 
+    private void onCallUpdated(final CallInfo callInfo) {
+        BindCallback callback = new BindCallback() {
+            @Override
+            public void onSuccess() {
+                if (isServiceValid("onCallUpdated")) {
+                    try {
+                        mSelectorInterface.onCallUpdated(callInfo);
+                    } catch (RemoteException e) {
+                    }
+                }
+            }
+            @Override
+            public void onFailure() {
+            }
+        };
+        mBinder.bind(callback);
+    }
+
+    private void onCallRemoved(final String callId) {
+        BindCallback callback = new BindCallback() {
+            @Override
+            public void onSuccess() {
+                if (isServiceValid("onCallRemoved")) {
+                    try {
+                        mSelectorInterface.onCallRemoved(callId);
+                    } catch (RemoteException e) {
+                    }
+                }
+            }
+            @Override
+            public void onFailure() {
+            }
+        };
+        mBinder.bind(callback);
+    }
+
     void addCall(Call call) {
         mCallIdMapper.addCall(call);
+        onCallUpdated(call.toCallInfo(mCallIdMapper.getCallId(call)));
     }
 
     void removeCall(Call call) {
+        String callId = mCallIdMapper.getCallId(call);
         mCallIdMapper.removeCall(call);
+        onCallRemoved(callId);
     }
 
     /** {@inheritDoc} */
diff --git a/src/com/android/telecomm/CallsManager.java b/src/com/android/telecomm/CallsManager.java
index c82b04d..a302c8d 100644
--- a/src/com/android/telecomm/CallsManager.java
+++ b/src/com/android/telecomm/CallsManager.java
@@ -53,6 +53,7 @@
         void onCallStateChanged(Call call, CallState oldState, CallState newState);
         void onIncomingCallAnswered(Call call);
         void onIncomingCallRejected(Call call);
+        void onCallHandoffHandleChanged(Call call, Uri oldHandle, Uri newHandle);
         void onForegroundCallChanged(Call oldForegroundCall, Call newForegroundCall);
         void onAudioStateChanged(CallAudioState oldAudioState, CallAudioState newAudioState);
     }
@@ -68,6 +69,12 @@
     private final Set<Call> mCalls = Sets.newLinkedHashSet();
 
     /**
+     * Set of new calls created to perform a handoff. The calls are added when handoff is initiated
+     * and removed when hadnoff is complete.
+     */
+    private final Set<Call> mPendingHandoffCalls = Sets.newLinkedHashSet();
+
+    /**
      * The call the user is currently interacting with. This is the call that should have audio
      * focus and be visible in the in-call UI.
      */
@@ -366,6 +373,33 @@
         mCallAudioManager.setAudioRoute(route);
     }
 
+    void startHandoffForCall(Call originalCall) {
+        if (!mCalls.contains(originalCall)) {
+            Log.w(this, "Unknown call %s asked to be handed off", originalCall);
+            return;
+        }
+
+        for (Call handoffCall : mPendingHandoffCalls) {
+            if (handoffCall.getOriginalCall() == originalCall) {
+                Log.w(this, "Call %s is already being handed off, skipping", originalCall);
+                return;
+            }
+        }
+
+        // Create a new call to be placed in the background. If handoff is successful then the
+        // 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.getContactInfo(),
+                originalCall.getGatewayInfo(), false);
+        tempCall.setOriginalCall(originalCall);
+        tempCall.setExtras(originalCall.getExtras());
+        tempCall.setCallServiceSelector(originalCall.getCallServiceSelector());
+        mPendingHandoffCalls.add(tempCall);
+        Log.d(this, "Placing handoff call");
+        mSwitchboard.placeOutgoingCall(tempCall);
+    }
+
     /** Called when the audio state changes. */
     void onAudioStateChanged(CallAudioState oldAudioState, CallAudioState newAudioState) {
         Log.v(this, "onAudioStateChanged, audioState: %s -> %s", oldAudioState, newAudioState);
@@ -384,6 +418,10 @@
 
     void markCallAsActive(Call call) {
         setCallState(call, CallState.ACTIVE);
+
+        if (mPendingHandoffCalls.contains(call)) {
+            completeHandoff(call);
+        }
     }
 
     void markCallAsOnHold(Call call) {
@@ -404,7 +442,25 @@
     }
 
     void setHandoffInfo(Call call, Uri handle, Bundle extras) {
-        // TODO(sail): Implement this.
+        if (!mCalls.contains(call)) {
+            Log.w(this, "Unknown call (%s) asked to set handoff info", call);
+            return;
+        }
+
+        if (extras == null) {
+            call.setExtras(Bundle.EMPTY);
+        } else {
+            call.setExtras(extras);
+        }
+
+        Uri oldHandle = call.getHandoffHandle();
+        Log.v(this, "set handoff handle %s -> %s, for call: %s", oldHandle, handle, call);
+        if (!areUriEqual(oldHandle, handle)) {
+            call.setHandoffHandle(handle);
+            for (CallsManagerListener listener : mListeners) {
+                listener.onCallHandoffHandleChanged(call, oldHandle, handle);
+            }
+        }
     }
 
     /**
@@ -443,6 +499,10 @@
         if (mCalls.contains(call)) {
             mCalls.remove(call);
             shouldNotify = true;
+            cleanUpHandoffCallsForOriginalCall(call);
+        } else if (mPendingHandoffCalls.contains(call)) {
+            Log.v(this, "silently removing handoff call %s", call);
+            mPendingHandoffCalls.remove(call);
         }
 
         // Only broadcast changes for calls that are being tracked.
@@ -509,4 +569,50 @@
             }
         }
     }
+
+    private void completeHandoff(Call handoffCall) {
+        Call originalCall = handoffCall.getOriginalCall();
+        Log.v(this, "complete handoff, %s -> %s", handoffCall, originalCall);
+
+        // Disconnect.
+        originalCall.disconnect();
+
+        // Synchronize.
+        originalCall.setCallService(handoffCall.getCallService());
+        setCallState(originalCall, handoffCall.getState());
+
+        // Remove the transient handoff call object (don't disconnect because the call is still
+        // live).
+        removeCall(handoffCall);
+
+        // Force the foreground call changed notification to be sent.
+        for (CallsManagerListener listener : mListeners) {
+            listener.onForegroundCallChanged(mForegroundCall, mForegroundCall);
+        }
+    }
+
+    /** Makes sure there are no dangling handoff calls. */
+    private void cleanUpHandoffCallsForOriginalCall(Call originalCall) {
+        for (Call handoffCall : ImmutableList.copyOf((mPendingHandoffCalls))) {
+            if (handoffCall.getOriginalCall() == originalCall) {
+                Log.d(this, "cancelling handoff call %s for originalCall: %s", handoffCall,
+                        originalCall);
+                if (handoffCall.getState() == CallState.NEW) {
+                    handoffCall.abort();
+                    handoffCall.setState(CallState.ABORTED);
+                } else {
+                    handoffCall.disconnect();
+                    handoffCall.setState(CallState.DISCONNECTED);
+                }
+                removeCall(handoffCall);
+            }
+        }
+    }
+
+    private static boolean areUriEqual(Uri handle1, Uri handle2) {
+        if (handle1 == null) {
+            return handle2 == null;
+        }
+        return handle1.equals(handle2);
+    }
 }
diff --git a/src/com/android/telecomm/CallsManagerListenerBase.java b/src/com/android/telecomm/CallsManagerListenerBase.java
index 04b78fe..6144651 100644
--- a/src/com/android/telecomm/CallsManagerListenerBase.java
+++ b/src/com/android/telecomm/CallsManagerListenerBase.java
@@ -16,6 +16,7 @@
 
 package com.android.telecomm;
 
+import android.net.Uri;
 import android.telecomm.CallAudioState;
 import android.telecomm.CallState;
 
@@ -44,6 +45,10 @@
     }
 
     @Override
+    public void onCallHandoffHandleChanged(Call call, Uri oldHandle, Uri newHandle) {
+    }
+
+    @Override
     public void onForegroundCallChanged(Call oldForegroundCall, Call newForegroundCall) {
     }
 
diff --git a/src/com/android/telecomm/InCallAdapter.java b/src/com/android/telecomm/InCallAdapter.java
index 14669c6..da7cf18 100644
--- a/src/com/android/telecomm/InCallAdapter.java
+++ b/src/com/android/telecomm/InCallAdapter.java
@@ -35,8 +35,9 @@
     private static final int MSG_DISCONNECT_CALL = 5;
     private static final int MSG_HOLD_CALL = 6;
     private static final int MSG_UNHOLD_CALL = 7;
-    private static final int MSG_MUTE = 8;
-    private static final int MSG_SET_AUDIO_ROUTE = 9;
+    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 final class InCallAdapterHandler extends Handler {
         @Override
@@ -75,6 +76,9 @@
                 case MSG_UNHOLD_CALL:
                     mCallsManager.unholdCall(call);
                     break;
+                case MSG_HANDOFF_CALL:
+                    mCallsManager.startHandoffForCall(call);
+                    break;
                 case MSG_MUTE:
                     mCallsManager.mute(msg.arg1 == 1 ? true : false);
                     break;
@@ -160,6 +164,13 @@
 
     /** {@inheritDoc} */
     @Override
+    public void handoffCall(String callId) {
+        mCallIdMapper.checkValidCallId(callId);
+        mHandler.obtainMessage(MSG_HANDOFF_CALL, callId).sendToTarget();
+    }
+
+    /** {@inheritDoc} */
+    @Override
     public void mute(boolean shouldMute) {
         mHandler.obtainMessage(MSG_MUTE, shouldMute ? 1 : 0, 0).sendToTarget();
     }
diff --git a/src/com/android/telecomm/InCallController.java b/src/com/android/telecomm/InCallController.java
index d975b0b..beac0d5 100644
--- a/src/com/android/telecomm/InCallController.java
+++ b/src/com/android/telecomm/InCallController.java
@@ -20,6 +20,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.ServiceConnection;
+import android.net.Uri;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.telecomm.CallAudioState;
@@ -136,6 +137,17 @@
     }
 
     @Override
+    public void onCallHandoffHandleChanged(Call call, Uri oldHandle, Uri newHandle) {
+        if (mInCallService != null) {
+            try {
+                mInCallService.setHandoffEnabled(mCallIdMapper.getCallId(call), newHandle != null);
+            } catch (RemoteException e) {
+                Log.e(this, e, "Exception attempting to call setHandoffEnabled.");
+            }
+        }
+    }
+
+    @Override
     public void onAudioStateChanged(CallAudioState oldAudioState, CallAudioState newAudioState) {
         if (mInCallService != null) {
             Log.i(this, "Calling onAudioStateChanged, audioState: %s -> %s", oldAudioState,
@@ -207,6 +219,7 @@
         if (!calls.isEmpty()) {
             for (Call call : calls) {
                 onCallAdded(call);
+                onCallHandoffHandleChanged(call, null, call.getHandoffHandle());
             }
             onAudioStateChanged(null, CallsManager.getInstance().getAudioState());
         } else {
diff --git a/src/com/android/telecomm/Switchboard.java b/src/com/android/telecomm/Switchboard.java
index ccb6b80..c2df63e 100644
--- a/src/com/android/telecomm/Switchboard.java
+++ b/src/com/android/telecomm/Switchboard.java
@@ -293,7 +293,14 @@
      * @param call The call to place.
      */
     private void processNewOutgoingCall(Call call) {
-        Collection<CallServiceSelectorWrapper> selectors = mSelectors;
+        Collection<CallServiceSelectorWrapper> selectors;
+
+        // Use the call's selector if it's already tied to one. This is the case for handoff calls.
+        if (call.getCallServiceSelector() != null) {
+            selectors = ImmutableList.of(call.getCallServiceSelector());
+        } else {
+            selectors = mSelectors;
+        }
 
         boolean useEmergencySelector =
                 EmergencyCallServiceSelector.shouldUseSelector(call.getHandle());
@@ -315,7 +322,7 @@
                             mOutgoingCallsManager);
 
             selectorsBuilder.add(emergencySelector);
-            selectorsBuilder.addAll(mSelectors);
+            selectorsBuilder.addAll(selectors);
             selectors = selectorsBuilder.build();
         }