Support Connection handover between ConnectionServices.

Call:
- added support for initiation of handover on receipt of the
EVENT_REQUEST_HANDOVER call event.
CallAudioManager:
- suppress disconnect tone when handover is in process for a call.
CallsManager:
- when the ConnectionService adds a new incoming call with
EXTRA_IS_HANDOVER, this indicates that the call is being added as the
destination for the handover.  Adding logic to find the ongoing call on
the device which will be handed over to this new call, and confirm that
handover is supported by both ConnectionService.
- on call removal, clean up handover call references.
- When calls change state, handle completion or failure of overall
handover.
- ensure it is possible to add handover calls when there are other ongoing
calls which would normally prevent them from being added.
TestApps:
- Added ability to initiate handover from Test Incall UI.
- Added ability to receive handover from test self-mgd calling app.

Test: Manual
Bug: 37102939
Change-Id: Idfa4325bb1aee34abad5cdb3d8edb48f0186692e
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index 2eeb96c..0dd2cca 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -24,11 +24,13 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.os.ParcelFileDescriptor;
+import android.os.Parcelable;
 import android.os.RemoteException;
 import android.os.Trace;
 import android.provider.ContactsContract.Contacts;
 import android.telecom.CallAudioState;
 import android.telecom.Conference;
+import android.telecom.ConnectionService;
 import android.telecom.DisconnectCause;
 import android.telecom.Connection;
 import android.telecom.GatewayInfo;
@@ -126,6 +128,7 @@
         void onExternalCallChanged(Call call, boolean isExternalCall);
         void onRttInitiationFailure(Call call, int reason);
         void onRemoteRttRequest(Call call, int requestId);
+        void onHandoverRequested(Call call, PhoneAccountHandle handoverTo, int videoState);
     }
 
     public abstract static class ListenerBase implements Listener {
@@ -197,6 +200,8 @@
         public void onRttInitiationFailure(Call call, int reason) {}
         @Override
         public void onRemoteRttRequest(Call call, int requestId) {}
+        @Override
+        public void onHandoverRequested(Call call, PhoneAccountHandle handoverTo, int videoState) {}
     }
 
     private final CallerInfoLookupHelper.OnQueryCompleteListener mCallerInfoQueryListener =
@@ -439,6 +444,18 @@
     private int mPendingRttRequestId = INVALID_RTT_REQUEST_ID;
 
     /**
+     * When a call handover has been initiated via {@link #requestHandover(PhoneAccountHandle)},
+     * contains the call which this call is being handed over to.
+     */
+    private Call mHandoverToCall = null;
+
+    /**
+     * When a call handover has been initiated via {@link #requestHandover(PhoneAccountHandle)},
+     * contains the call which this call is being handed over from.
+     */
+    private Call mHandoverFromCall = null;
+
+    /**
      * Persists the specified parameters and initializes the new instance.
      *
      * @param context The context.
@@ -1027,6 +1044,22 @@
         setConnectionProperties(getConnectionProperties());
     }
 
+    public Call getHandoverToCall() {
+        return mHandoverToCall;
+    }
+
+    public void setHandoverToCall(Call call) {
+        mHandoverToCall = call;
+    }
+
+    public Call getHandoverFromCall() {
+        return mHandoverFromCall;
+    }
+
+    public void setHandoverFromCall(Call call) {
+        mHandoverFromCall = call;
+    }
+
     private void configureIsWorkCall() {
         PhoneAccountRegistrar phoneAccountRegistrar = mCallsManager.getPhoneAccountRegistrar();
         boolean isWorkCall = false;
@@ -1841,7 +1874,32 @@
      */
     public void sendCallEvent(String event, Bundle extras) {
         if (mConnectionService != null) {
-            mConnectionService.sendCallEvent(this, event, extras);
+            if (android.telecom.Call.EVENT_REQUEST_HANDOVER.equals(event)) {
+                // Handover requests are targeted at Telecom, not the ConnectionService.
+                if (extras == null) {
+                    Log.w(this, "sendCallEvent: %s event received with null extras.",
+                            android.telecom.Call.EVENT_REQUEST_HANDOVER);
+                    mConnectionService.sendCallEvent(this,
+                            android.telecom.Call.EVENT_HANDOVER_FAILED, null);
+                    return;
+                }
+                Parcelable parcelable = extras.getParcelable(
+                        android.telecom.Call.EXTRA_HANDOVER_PHONE_ACCOUNT_HANDLE);
+                if (!(parcelable instanceof PhoneAccountHandle) || parcelable == null) {
+                    Log.w(this, "sendCallEvent: %s event received with invalid handover acct.",
+                            android.telecom.Call.EVENT_REQUEST_HANDOVER);
+                    mConnectionService.sendCallEvent(this,
+                            android.telecom.Call.EVENT_HANDOVER_FAILED, null);
+                    return;
+                }
+                PhoneAccountHandle phoneAccountHandle = (PhoneAccountHandle) parcelable;
+                int videoState = extras.getInt(android.telecom.Call.EXTRA_HANDOVER_VIDEO_STATE,
+                        VideoProfile.STATE_AUDIO_ONLY);
+                requestHandover(phoneAccountHandle, videoState);
+            } else {
+                Log.addEvent(this, LogUtils.Events.CALL_EVENT, event);
+                mConnectionService.sendCallEvent(this, event, extras);
+            }
         } else {
             Log.e(this, new NullPointerException(),
                     "sendCallEvent failed due to null CS callId=%s", getId());
@@ -2520,4 +2578,15 @@
         return capabilities & ~(Connection.CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL |
                 Connection.CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL);
     }
+
+    /**
+     * Initiates a handover of this {@link Call} to another {@link PhoneAccount}.
+     * @param handoverToHandle The {@link PhoneAccountHandle} to handover to.
+     * @param videoState The video state of the call when handed over.
+     */
+    private void requestHandover(PhoneAccountHandle handoverToHandle, int videoState) {
+        for (Listener l : mListeners) {
+            l.onHandoverRequested(this, handoverToHandle, videoState);
+        }
+    }
 }
diff --git a/src/com/android/server/telecom/CallAudioManager.java b/src/com/android/server/telecom/CallAudioManager.java
index 9eb8aea..8c75b63 100644
--- a/src/com/android/server/telecom/CallAudioManager.java
+++ b/src/com/android/server/telecom/CallAudioManager.java
@@ -634,6 +634,13 @@
     }
 
     private void playToneForDisconnectedCall(Call call) {
+        // If this call is being disconnected as a result of being handed over to another call,
+        // we will not play a disconnect tone.
+        if (call.getHandoverToCall() != null || call.getHandoverFromCall() != null) {
+            Log.i(LOG_TAG, "Omitting tone because %s is being handed over.", call);
+            return;
+        }
+
         if (mForegroundCall != null && call != mForegroundCall && mCalls.size() > 1) {
             Log.v(LOG_TAG, "Omitting tone because we are not foreground" +
                     " and there is another call.");
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index 45cd488..d26b098 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -656,6 +656,18 @@
         }
     }
 
+    /**
+     * A {@link Call} managed by the {@link CallsManager} has requested a handover to another
+     * {@link PhoneAccount}.
+     * @param call The call.
+     * @param handoverTo The {@link PhoneAccountHandle} to handover the call to.
+     * @param videoState The desired video state of the call after handover.
+     */
+    @Override
+    public void onHandoverRequested(Call call, PhoneAccountHandle handoverTo, int videoState) {
+        requestHandover(call, handoverTo, videoState);
+    }
+
     @VisibleForTesting
     public Call getForegroundCall() {
         if (mCallAudioManager == null) {
@@ -741,6 +753,7 @@
      */
     void processIncomingCallIntent(PhoneAccountHandle phoneAccountHandle, Bundle extras) {
         Log.d(this, "processIncomingCallIntent");
+        boolean isHandover = extras.getBoolean(TelecomManager.EXTRA_IS_HANDOVER);
         Uri handle = extras.getParcelable(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS);
         if (handle == null) {
             // Required for backwards compatibility
@@ -818,7 +831,37 @@
         // TODO: Move this to be a part of addCall()
         call.addListener(this);
 
-        if (call.isSelfManaged() && !isIncomingCallPermitted(call, call.getTargetPhoneAccount())) {
+        boolean isHandoverAllowed = true;
+        if (isHandover) {
+            if (!isHandoverInProgress() &&
+                    isHandoverToPhoneAccountSupported(phoneAccountHandle)) {
+                Log.w(this, "processIncomingCallIntent: To account doesn't support handover.");
+                final String handleScheme = handle.getSchemeSpecificPart();
+                Call fromCall = mCalls.stream()
+                        .filter((c) -> mPhoneNumberUtilsAdapter.isSamePhoneNumber(
+                                c.getHandle().getSchemeSpecificPart(), handleScheme))
+                        .findFirst()
+                        .orElse(null);
+                if (fromCall != null) {
+                    if (!isHandoverFromPhoneAccountSupported(fromCall.getTargetPhoneAccount())) {
+                        Log.w(this, "processIncomingCallIntent: From account doesn't support " +
+                                        "handover.");
+                        isHandoverAllowed = false;
+                    }
+                } else {
+                    Log.w(this, "processIncomingCallIntent: handover fail; can't find from call.");
+                    isHandoverAllowed = false;
+                }
+
+                if (isHandoverAllowed) {
+                    // Link the calls so we know we're handing over.
+                    fromCall.setHandoverToCall(call);
+                    call.setHandoverFromCall(fromCall);
+                }
+            }
+        }
+        if (!isHandoverAllowed || (call.isSelfManaged() && !isIncomingCallPermitted(call,
+                call.getTargetPhoneAccount()))) {
             notifyCreateConnectionFailed(phoneAccountHandle, call);
         } else {
             call.startCreateConnection(mPhoneAccountRegistrar);
@@ -1621,6 +1664,9 @@
      * Removes an existing disconnected call, and notifies the in-call app.
      */
     void markCallAsRemoved(Call call) {
+        call.setHandoverToCall(null);
+        call.setHandoverFromCall(null);
+
         removeCall(call);
         Call foregroundCall = mCallAudioManager.getPossiblyHeldForegroundCall();
         if (mLocallyDisconnectingCalls.contains(call)) {
@@ -2047,6 +2093,31 @@
             maybeShowErrorDialogOnDisconnect(call);
 
             Trace.beginSection("onCallStateChanged");
+
+            // If this call became active because it is being handed over from another Call, the
+            // call which was being handed over from can be disconnected at this point.
+            if (call.getHandoverFromCall() != null) {
+                if (newState == CallState.ACTIVE) {
+                    Call handoverFrom = call.getHandoverFromCall();
+                    Log.addEvent(call, LogUtils.Events.HANDOVER_COMPLETE, "from=%s, to=%s",
+                            call.getId(), handoverFrom.getId());
+                    Log.addEvent(handoverFrom, LogUtils.Events.HANDOVER_COMPLETE, "from=%s, to=%s",
+                            call.getId(), handoverFrom.getId());
+                    markCallAsDisconnected(handoverFrom,
+                            new DisconnectCause(DisconnectCause.LOCAL));
+                    markCallAsRemoved(handoverFrom);
+                    call.sendCallEvent(android.telecom.Call.EVENT_HANDOVER_COMPLETE, null);
+                } else if (newState == CallState.DISCONNECTED) {
+                    Call handoverFrom = call.getHandoverFromCall();
+                    Log.i(this, "Call %s failed to handover from %s.",
+                            call.getId(), handoverFrom.getId());
+                    Log.addEvent(handoverFrom, LogUtils.Events.HANDOVER_FAILED, "from=%s, to=%s",
+                            call.getId(), handoverFrom.getId());
+                    handoverFrom.sendCallEvent(
+                            android.telecom.Call.EVENT_HANDOVER_FAILED, null);
+                }
+            }
+
             // Only broadcast state change for calls that are being tracked.
             if (mCalls.contains(call)) {
                 updateCanAddCall();
@@ -2232,7 +2303,8 @@
      */
     public boolean shouldShowSystemIncomingCallUi(Call incomingCall) {
         return incomingCall.isIncoming() && incomingCall.isSelfManaged() &&
-                hasCallsForOtherPhoneAccount(incomingCall.getTargetPhoneAccount());
+                hasCallsForOtherPhoneAccount(incomingCall.getTargetPhoneAccount()) &&
+                incomingCall.getHandoverFromCall() == null;
     }
 
     private boolean makeRoomForOutgoingCall(Call call, boolean isEmergency) {
@@ -2538,10 +2610,11 @@
         } else {
             // Only permit outgoing calls if there is no ongoing emergency calls and all other calls
             // are associated with the current PhoneAccountHandle.
-            return !hasEmergencyCall() &&
-                    !hasMaximumSelfManagedCalls(excludeCall, phoneAccountHandle) &&
-                    !hasCallsForOtherPhoneAccount(phoneAccountHandle) &&
-                    !hasManagedCalls();
+            return !hasEmergencyCall() && (
+                    excludeCall.getHandoverFromCall() != null ||
+                            (!hasMaximumSelfManagedCalls(excludeCall, phoneAccountHandle) &&
+                            !hasCallsForOtherPhoneAccount(phoneAccountHandle) &&
+                            !hasManagedCalls()));
         }
     }
 
@@ -2672,4 +2745,94 @@
             service.createConnectionFailed(call);
         }
     }
+
+    /**
+     * Called in response to a {@link Call} receiving a {@link Call#sendCallEvent(String, Bundle)}
+     * of type {@link android.telecom.Call#EVENT_REQUEST_HANDOVER} indicating the
+     * {@link android.telecom.InCallService} has requested a handover to another
+     * {@link android.telecom.ConnectionService}.
+     *
+     * We will explicitly disallow a handover when there is an emergency call present.
+     *
+     * @param handoverFromCall The {@link Call} to be handed over.
+     * @param handoverToHandle The {@link PhoneAccountHandle} to hand over the call to.
+     * @param videoState The desired video state of {@link Call} after handover.
+     */
+    private void requestHandover(Call handoverFromCall, PhoneAccountHandle handoverToHandle,
+                                 int videoState) {
+
+        boolean isHandoverFromSupported = isHandoverFromPhoneAccountSupported(
+                handoverFromCall.getTargetPhoneAccount());
+        boolean isHandoverToSupported = isHandoverToPhoneAccountSupported(handoverToHandle);
+
+        if (!isHandoverFromSupported || !isHandoverToSupported || hasEmergencyCall()) {
+            handoverFromCall.sendCallEvent(android.telecom.Call.EVENT_HANDOVER_FAILED, null);
+            return;
+        }
+
+        Log.addEvent(handoverFromCall, LogUtils.Events.HANDOVER_REQUEST, handoverToHandle);
+
+        Bundle extras = new Bundle();
+        extras.putBoolean(TelecomManager.EXTRA_IS_HANDOVER, true);
+        extras.putInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE, videoState);
+        Call handoverToCall = startOutgoingCall(handoverFromCall.getHandle(), handoverToHandle,
+                extras, getCurrentUserHandle());
+        Log.addEvent(handoverFromCall, LogUtils.Events.START_HANDOVER,
+                "handOverFrom=%s, handOverTo=%s", handoverFromCall.getId(), handoverToCall.getId());
+        handoverFromCall.setHandoverToCall(handoverToCall);
+        handoverToCall.setHandoverFromCall(handoverFromCall);
+        handoverToCall.setNewOutgoingCallIntentBroadcastIsDone();
+        placeOutgoingCall(handoverToCall, handoverToCall.getHandle(), null /* gatewayInfo */,
+                false /* startwithSpeaker */,
+                videoState);
+    }
+
+    /**
+     * Determines if handover from the specified {@link PhoneAccountHandle} is supported.
+     *
+     * @param from The {@link PhoneAccountHandle} the handover originates from.
+     * @return {@code true} if handover is currently allowed, {@code false} otherwise.
+     */
+    private boolean isHandoverFromPhoneAccountSupported(PhoneAccountHandle from) {
+        return getBooleanPhoneAccountExtra(from, PhoneAccount.EXTRA_SUPPORTS_HANDOVER_TO);
+    }
+
+    /**
+     * Determines if handover to the specified {@link PhoneAccountHandle} is supported.
+     *
+     * @param to The {@link PhoneAccountHandle} the handover it to.
+     * @return {@code true} if handover is currently allowed, {@code false} otherwise.
+     */
+    private boolean isHandoverToPhoneAccountSupported(PhoneAccountHandle to) {
+        return getBooleanPhoneAccountExtra(to, PhoneAccount.EXTRA_SUPPORTS_HANDOVER_TO);
+    }
+
+    /**
+     * Retrieves a boolean phone account extra.
+     * @param handle the {@link PhoneAccountHandle} to retrieve the extra for.
+     * @param key The extras key.
+     * @return {@code true} if the extra {@link PhoneAccount} extra is true, {@code false}
+     *      otherwise.
+     */
+    private boolean getBooleanPhoneAccountExtra(PhoneAccountHandle handle, String key) {
+        PhoneAccount phoneAccount = getPhoneAccountRegistrar().getPhoneAccountUnchecked(handle);
+        if (phoneAccount == null) {
+            return false;
+        }
+
+        Bundle fromExtras = phoneAccount.getExtras();
+        if (fromExtras == null) {
+            return false;
+        }
+        return fromExtras.getBoolean(key);
+    }
+
+    /**
+     * Determines if there is an existing handover in process.
+     * @return {@code true} if a call in the process of handover exists, {@code false} otherwise.
+     */
+    private boolean isHandoverInProgress() {
+        return mCalls.stream().filter(c -> c.getHandoverFromCall() != null ||
+                c.getHandoverToCall() != null).count() > 0;
+    }
 }
diff --git a/src/com/android/server/telecom/LogUtils.java b/src/com/android/server/telecom/LogUtils.java
index 121abcf..c61ff65 100644
--- a/src/com/android/server/telecom/LogUtils.java
+++ b/src/com/android/server/telecom/LogUtils.java
@@ -123,6 +123,11 @@
         public static final String PROPERTY_CHANGE = "PROPERTY_CHANGE";
         public static final String CAPABILITY_CHANGE = "CAPABILITY_CHANGE";
         public static final String CONNECTION_EVENT = "CONNECTION_EVENT";
+        public static final String CALL_EVENT = "CALL_EVENT";
+        public static final String HANDOVER_REQUEST = "HANDOVER_REQUEST";
+        public static final String START_HANDOVER = "START_HANDOVER";
+        public static final String HANDOVER_COMPLETE = "HANDOVER_COMPLETE";
+        public static final String HANDOVER_FAILED = "HANDOVER_FAILED";
 
         public static class Timings {
             public static final String ACCEPT_TIMING = "accept";
diff --git a/src/com/android/server/telecom/PhoneNumberUtilsAdapter.java b/src/com/android/server/telecom/PhoneNumberUtilsAdapter.java
index 8e59a64..aa568a9 100644
--- a/src/com/android/server/telecom/PhoneNumberUtilsAdapter.java
+++ b/src/com/android/server/telecom/PhoneNumberUtilsAdapter.java
@@ -27,6 +27,7 @@
     boolean isLocalEmergencyNumber(Context context, String number);
     boolean isPotentialLocalEmergencyNumber(Context context, String number);
     boolean isUriNumber(String number);
+    boolean isSamePhoneNumber(String number1, String number2);
     String getNumberFromIntent(Intent intent, Context context);
     String convertKeypadLettersToDigits(String number);
     String stripSeparators(String number);
diff --git a/src/com/android/server/telecom/PhoneNumberUtilsAdapterImpl.java b/src/com/android/server/telecom/PhoneNumberUtilsAdapterImpl.java
index fa316a5..7ff854e 100644
--- a/src/com/android/server/telecom/PhoneNumberUtilsAdapterImpl.java
+++ b/src/com/android/server/telecom/PhoneNumberUtilsAdapterImpl.java
@@ -37,6 +37,11 @@
     }
 
     @Override
+    public boolean isSamePhoneNumber(String number1, String number2) {
+        return PhoneNumberUtils.compare(number1, number2);
+    }
+
+    @Override
     public String getNumberFromIntent(Intent intent, Context context) {
         return PhoneNumberUtils.getNumberFromIntent(intent, context);
     }
diff --git a/testapps/AndroidManifest.xml b/testapps/AndroidManifest.xml
index a57b158..6980a12 100644
--- a/testapps/AndroidManifest.xml
+++ b/testapps/AndroidManifest.xml
@@ -196,6 +196,14 @@
           </intent-filter>
         </activity>
 
+        <activity android:name="com.android.server.telecom.testapps.HandoverActivity"
+                  android:label="@string/selfManagedCallingActivityLabel"
+                  android:process="com.android.server.telecom.testapps.SelfMangingCallingApp">
+          <intent-filter>
+              <action android:name="android.intent.action.MAIN" />
+          </intent-filter>
+        </activity>
+
         <service android:name="com.android.server.telecom.testapps.SelfManagedConnectionService"
                  android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
                  android:process="com.android.server.telecom.testapps.SelfMangingCallingApp">
diff --git a/testapps/res/layout/incall_screen.xml b/testapps/res/layout/incall_screen.xml
index 502bdf4..36ffb27 100644
--- a/testapps/res/layout/incall_screen.xml
+++ b/testapps/res/layout/incall_screen.xml
@@ -27,7 +27,7 @@
             android:dividerHeight="4px">
     </ListView>
     <GridLayout
-        android:columnCount="4"
+        android:columnCount="3"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:orientation="horizontal">
@@ -66,5 +66,10 @@
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:text="@string/acceptRttButton"/>
+        <Button
+            android:id="@+id/request_handover_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/handoverButton"/>
     </GridLayout>
 </LinearLayout>
diff --git a/testapps/res/layout/self_managed_handover.xml b/testapps/res/layout/self_managed_handover.xml
new file mode 100644
index 0000000..4524370
--- /dev/null
+++ b/testapps/res/layout/self_managed_handover.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 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
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical" android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <TextView
+        android:text="Do you want to handover your call?"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:id="@+id/incomingCallText" />
+
+    <Button
+        android:text="No Way"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:id="@+id/rejectUpgradeButton" />
+
+    <Button
+        android:text="Yes Definitely"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:id="@+id/acceptUpgradeButton" />
+</LinearLayout>
\ No newline at end of file
diff --git a/testapps/res/values/donottranslate_strings.xml b/testapps/res/values/donottranslate_strings.xml
index 5a7e500..bfe7550 100644
--- a/testapps/res/values/donottranslate_strings.xml
+++ b/testapps/res/values/donottranslate_strings.xml
@@ -56,6 +56,8 @@
 
     <string name="holdButton">Hold</string>
 
+    <string name="handoverButton">Handover</string>
+
     <string name="inCallUiAppLabel">Test InCall UI</string>
 
     <string name="UssdUiAppLabel">Test Ussd UI</string>
diff --git a/testapps/src/com/android/server/telecom/testapps/HandoverActivity.java b/testapps/src/com/android/server/telecom/testapps/HandoverActivity.java
new file mode 100644
index 0000000..f33022c
--- /dev/null
+++ b/testapps/src/com/android/server/telecom/testapps/HandoverActivity.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2017 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.server.telecom.testapps;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.telecom.Log;
+import android.telecom.TelecomManager;
+import android.telephony.DisconnectCause;
+import android.view.View;
+import android.widget.Button;
+
+/**
+ * Displays a UX to the user confirming whether they want to handover a call to the self-managed CS.
+ */
+public class HandoverActivity extends Activity {
+    public static final String EXTRA_CALL_ID = "com.android.server.telecom.testapps.extra.CALL_ID";
+
+    private Button mAcceptHandoverButton;
+    private Button mRejectHandoverButton;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        Intent launchingIntent = getIntent();
+        int callId = launchingIntent.getIntExtra(EXTRA_CALL_ID, 0);
+        Log.i(this, "showing fullscreen upgrade ux for call id %d", callId);
+
+        setContentView(R.layout.self_managed_handover);
+        final SelfManagedConnection connection = SelfManagedCallList.getInstance()
+                .getConnectionById(callId);
+        mAcceptHandoverButton = (Button) findViewById(R.id.acceptUpgradeButton);
+        mAcceptHandoverButton.setOnClickListener((View v) -> {
+            if (connection != null) {
+                connection.setConnectionActive();
+                Intent intent = new Intent(Intent.ACTION_MAIN, null);
+                intent.setFlags(Intent.FLAG_ACTIVITY_NO_USER_ACTION |
+                        Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
+                intent.setClass(this, SelfManagedCallingActivity.class);
+                startActivity(intent);
+            }
+            finish();
+        });
+        mRejectHandoverButton = (Button) findViewById(R.id.rejectUpgradeButton);
+        mRejectHandoverButton.setOnClickListener((View v) -> {
+            if (connection != null) {
+                connection.setConnectionDisconnected(DisconnectCause.INCOMING_REJECTED);
+                connection.destroy();
+                TelecomManager tm = TelecomManager.from(this);
+                tm.showInCallScreen(false);
+            }
+            finish();
+        });
+    }
+}
diff --git a/testapps/src/com/android/server/telecom/testapps/SelfManagedCallList.java b/testapps/src/com/android/server/telecom/testapps/SelfManagedCallList.java
index 4275079..83efba4 100644
--- a/testapps/src/com/android/server/telecom/testapps/SelfManagedCallList.java
+++ b/testapps/src/com/android/server/telecom/testapps/SelfManagedCallList.java
@@ -19,6 +19,7 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.net.Uri;
+import android.os.Bundle;
 import android.telecom.ConnectionRequest;
 import android.telecom.Log;
 import android.telecom.PhoneAccount;
@@ -100,6 +101,8 @@
     public void registerPhoneAccount(Context context, String id, Uri address, String name) {
         PhoneAccountHandle handle = new PhoneAccountHandle(COMPONENT_NAME, id);
         mPhoneAccounts.put(id, handle);
+        Bundle extras = new Bundle();
+        extras.putBoolean(PhoneAccount.EXTRA_SUPPORTS_HANDOVER_TO, true);
         PhoneAccount.Builder builder = PhoneAccount.builder(handle, name)
                 .addSupportedUriScheme(PhoneAccount.SCHEME_TEL)
                 .addSupportedUriScheme(PhoneAccount.SCHEME_SIP)
@@ -107,6 +110,7 @@
                 .setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED |
                         PhoneAccount.CAPABILITY_VIDEO_CALLING |
                         PhoneAccount.CAPABILITY_SUPPORTS_VIDEO_CALLING)
+                .setExtras(extras)
                 .setShortDescription(name);
 
         TelecomManager.from(context).registerPhoneAccount(builder.build());
diff --git a/testapps/src/com/android/server/telecom/testapps/SelfManagedConnection.java b/testapps/src/com/android/server/telecom/testapps/SelfManagedConnection.java
index 766efa5..72a6184 100644
--- a/testapps/src/com/android/server/telecom/testapps/SelfManagedConnection.java
+++ b/testapps/src/com/android/server/telecom/testapps/SelfManagedConnection.java
@@ -23,6 +23,8 @@
 import android.content.Intent;
 import android.graphics.drawable.Icon;
 import android.media.MediaPlayer;
+import android.os.Bundle;
+import android.telecom.Call;
 import android.telecom.CallAudioState;
 import android.telecom.Connection;
 import android.telecom.ConnectionService;
@@ -55,6 +57,7 @@
     private final boolean mIsIncomingCall;
     private boolean mIsIncomingCallUiShowing;
     private Listener mListener;
+    private boolean mIsHandover;
 
     SelfManagedConnection(SelfManagedCallList callList, Context context, boolean isIncoming) {
         mCallList = callList;
@@ -201,6 +204,14 @@
         return mCallId;
     }
 
+    public void setIsHandover(boolean isHandover) {
+        mIsHandover = isHandover;
+    }
+
+    public boolean isHandover() {
+        return mIsHandover;
+    }
+
     private MediaPlayer createMediaPlayer(Context context) {
         int audioToPlay = (Math.random() > 0.5f) ? R.raw.sample_audio : R.raw.sample_audio2;
         MediaPlayer mediaPlayer = MediaPlayer.create(context, audioToPlay);
diff --git a/testapps/src/com/android/server/telecom/testapps/SelfManagedConnectionService.java b/testapps/src/com/android/server/telecom/testapps/SelfManagedConnectionService.java
index 1d52a3b..4f28848 100644
--- a/testapps/src/com/android/server/telecom/testapps/SelfManagedConnectionService.java
+++ b/testapps/src/com/android/server/telecom/testapps/SelfManagedConnectionService.java
@@ -16,6 +16,7 @@
 
 package com.android.server.telecom.testapps;
 
+import android.content.Intent;
 import android.os.Bundle;
 import android.telecom.Connection;
 import android.telecom.ConnectionRequest;
@@ -79,6 +80,16 @@
         if (isIncoming) {
             connection.setIsIncomingCallUiShowing(request.shouldShowIncomingCallUi());
         }
+        Bundle requestExtras = request.getExtras();
+        if (requestExtras != null) {
+            connection.setIsHandover(requestExtras.getBoolean(TelecomManager.EXTRA_IS_HANDOVER,
+                    false));
+            Intent intent = new Intent(Intent.ACTION_MAIN, null);
+            intent.setFlags(Intent.FLAG_ACTIVITY_NO_USER_ACTION | Intent.FLAG_ACTIVITY_NEW_TASK);
+            intent.setClass(this, HandoverActivity.class);
+            intent.putExtra(HandoverActivity.EXTRA_CALL_ID, connection.getCallId());
+            startActivity(intent);
+        }
 
         // Track the phone account handle which created this connection so we can distinguish them
         // in the sample call list later.
diff --git a/testapps/src/com/android/server/telecom/testapps/TestCallList.java b/testapps/src/com/android/server/telecom/testapps/TestCallList.java
index 1b32690..322c94c 100644
--- a/testapps/src/com/android/server/telecom/testapps/TestCallList.java
+++ b/testapps/src/com/android/server/telecom/testapps/TestCallList.java
@@ -151,7 +151,9 @@
         call.unregisterCallback(this);
 
         for (Listener l : mListeners) {
-            l.onCallRemoved(call);
+            if (l != null) {
+                l.onCallRemoved(call);
+            }
         }
     }
 
diff --git a/testapps/src/com/android/server/telecom/testapps/TestInCallUI.java b/testapps/src/com/android/server/telecom/testapps/TestInCallUI.java
index 2920ca7..d99798c 100644
--- a/testapps/src/com/android/server/telecom/testapps/TestInCallUI.java
+++ b/testapps/src/com/android/server/telecom/testapps/TestInCallUI.java
@@ -20,6 +20,9 @@
 import android.content.Intent;
 import android.os.Bundle;
 import android.telecom.Call;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
 import android.telecom.VideoProfile;
 import android.util.Log;
 import android.view.View;
@@ -27,6 +30,9 @@
 import android.widget.ListView;
 import android.widget.Toast;
 
+import java.util.List;
+import java.util.Optional;
+
 public class TestInCallUI extends Activity {
 
     private ListView mListView;
@@ -83,6 +89,7 @@
         View answerButton = findViewById(R.id.answer_button);
         View startRttButton = findViewById(R.id.start_rtt_button);
         View acceptRttButton = findViewById(R.id.accept_rtt_button);
+        View handoverButton = findViewById(R.id.request_handover_button);
 
         endCallButton.setOnClickListener(new OnClickListener() {
             @Override
@@ -145,6 +152,15 @@
                 call.respondToRttRequest(mCallList.getLastRttRequestId(), true);
             }
         });
+
+        handoverButton.setOnClickListener((v) -> {
+            Call call = mCallList.getCall(0);
+            Bundle extras = new Bundle();
+            extras.putParcelable(Call.EXTRA_HANDOVER_PHONE_ACCOUNT_HANDLE,
+                    getHandoverToPhoneAccountHandle());
+            extras.putInt(Call.EXTRA_HANDOVER_VIDEO_STATE, VideoProfile.STATE_BIDIRECTIONAL);
+            call.sendCallEvent(Call.EVENT_REQUEST_HANDOVER, extras);
+        });
     }
 
     /** ${inheritDoc} */
@@ -167,4 +183,19 @@
     protected void onResume() {
         super.onResume();
     }
+
+    private PhoneAccountHandle getHandoverToPhoneAccountHandle() {
+        TelecomManager tm = TelecomManager.from(this);
+
+        List<PhoneAccountHandle> handles = tm.getAllPhoneAccountHandles();
+        Optional<PhoneAccountHandle> found = handles.stream().filter(h -> {
+            PhoneAccount account = tm.getPhoneAccount(h);
+            Bundle extras = account.getExtras();
+            return extras != null && extras.getBoolean(PhoneAccount.EXTRA_SUPPORTS_HANDOVER_TO);
+        }).findFirst();
+        PhoneAccountHandle foundHandle = found.orElse(null);
+        Log.i(TestInCallUI.class.getSimpleName(), "getHandoverToPhoneAccountHandle() = " +
+            foundHandle);
+        return foundHandle;
+    }
 }