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/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);
+ }
}