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();
}