Merge "Support Connection handover between ConnectionServices."
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;
+    }
 }