Merge "Remove obsolete LOCAL_JACK_FLAGS"
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 9202e74..17c9698 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -21,6 +21,8 @@
         android:sharedUserId="android.uid.system">
 
     <protected-broadcast android:name="android.intent.action.SHOW_MISSED_CALLS_NOTIFICATION" />
+    <protected-broadcast android:name="com.android.server.telecom.MESSAGE_SENT" />
+
 
     <!-- Prevents the activity manager from delaying any activity-start
          requests by this package, including requests immediately after
@@ -293,12 +295,6 @@
                 android:exported="false"
                 android:process=":ui" />
 
-        <receiver android:name=".components.PrimaryCallReceiver"
-                android:exported="true"
-                android:permission="android.permission.MODIFY_PHONE_STATE"
-                android:process="system">
-        </receiver>
-
         <service android:name=".components.BluetoothPhoneService"
                 android:singleUser="true"
                 android:process="system">
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 8c29a21..9065173 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -79,6 +79,10 @@
         a text response. [CHAR LIMIT=40] -->
     <string name="respond_via_sms_confirmation_format">Message sent to <xliff:g id="phone_number">%s</xliff:g>.</string>
 
+    <!-- "Respond via SMS": Error toast shown after failing to send
+        a text response. [CHAR LIMIT=40] -->
+    <string name="respond_via_sms_failure_format">Message failed to send to <xliff:g id="phone_number">%s</xliff:g>.</string>
+
     <!-- Title of settings screen that allows user to enable and disable phone-accounts.
          Each method for placing a call (SIM1, SIM2, SIP account, etc) has a phone-account.
          Phone-accounts that are created by third party apps can be disabled and enabled by user.
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index bb382a9..76eed40 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -50,6 +50,7 @@
 import android.text.TextUtils;
 import android.util.StatsLog;
 import android.os.UserHandle;
+import android.widget.Toast;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.telecom.IVideoProvider;
@@ -461,7 +462,7 @@
      * Indicates whether the {@link PhoneAccount} associated with this call supports video calling.
      * {@code True} if the phone account supports video calling, {@code false} otherwise.
      */
-    private boolean mIsVideoCallingSupported = false;
+    private boolean mIsVideoCallingSupportedByPhoneAccount = false;
 
     private PhoneNumberUtilsAdapter mPhoneNumberUtilsAdapter;
 
@@ -1092,12 +1093,23 @@
         return mDisconnectCause;
     }
 
+    /**
+     * @return {@code true} if this is an outgoing call to emergency services. An outgoing call is
+     * identified as an emergency call by the dialer phone number.
+     */
     @VisibleForTesting
     public boolean isEmergencyCall() {
         return mIsEmergencyCall;
     }
 
     /**
+     * @return {@code true} if the network has identified this call as an emergency call.
+     */
+    public boolean isNetworkIdentifiedEmergencyCall() {
+        return hasProperty(Connection.PROPERTY_NETWORK_IDENTIFIED_EMERGENCY_CALL);
+    }
+
+    /**
      * @return The original handle this call is associated with. In-call services should use this
      * handle when indicating in their UI the handle that is being called.
      */
@@ -1218,8 +1230,19 @@
         return mUseCallRecordingTone;
     }
 
-    public boolean isVideoCallingSupported() {
-        return mIsVideoCallingSupported;
+    /**
+     * @return {@code true} if the {@link Call}'s {@link #getTargetPhoneAccount()} supports video.
+     */
+    public boolean isVideoCallingSupportedByPhoneAccount() {
+        return mIsVideoCallingSupportedByPhoneAccount;
+    }
+
+    /**
+     * @return {@code true} if the {@link Call} locally supports video.
+     */
+    public boolean isLocallyVideoCapable() {
+        return (getConnectionCapabilities() & Connection.CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL)
+                == Connection.CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL;
     }
 
     public boolean isSelfManaged() {
@@ -1321,16 +1344,16 @@
         if (mTargetPhoneAccountHandle == null) {
             // If no target phone account handle is specified, assume we can potentially perform a
             // video call; once the phone account is set, we can confirm that it is video capable.
-            mIsVideoCallingSupported = true;
+            mIsVideoCallingSupportedByPhoneAccount = true;
             Log.d(this, "checkIfVideoCapable: no phone account selected; assume video capable.");
             return;
         }
         PhoneAccount phoneAccount =
                 phoneAccountRegistrar.getPhoneAccountUnchecked(mTargetPhoneAccountHandle);
-        mIsVideoCallingSupported = phoneAccount != null && phoneAccount.hasCapabilities(
+        mIsVideoCallingSupportedByPhoneAccount = phoneAccount != null && phoneAccount.hasCapabilities(
                     PhoneAccount.CAPABILITY_VIDEO_CALLING);
 
-        if (!mIsVideoCallingSupported && VideoProfile.isVideo(getVideoState())) {
+        if (!mIsVideoCallingSupportedByPhoneAccount && VideoProfile.isVideo(getVideoState())) {
             // The PhoneAccount for the Call was set to one which does not support video calling,
             // and the current call is configured to be a video call; downgrade to audio-only.
             setVideoState(VideoProfile.STATE_AUDIO_ONLY);
@@ -1433,7 +1456,8 @@
         if (forceUpdate || mConnectionCapabilities != connectionCapabilities) {
             // If the phone account does not support video calling, and the connection capabilities
             // passed in indicate that the call supports video, remove those video capabilities.
-            if (!isVideoCallingSupported() && doesCallSupportVideo(connectionCapabilities)) {
+            if (!isVideoCallingSupportedByPhoneAccount()
+                    && doesCallSupportVideo(connectionCapabilities)) {
                 Log.w(this, "setConnectionCapabilities: attempt to set connection as video " +
                         "capable when not supported by the phone account.");
                 connectionCapabilities = removeVideoCapabilities(connectionCapabilities);
@@ -1454,7 +1478,7 @@
         }
     }
 
-    void setConnectionProperties(int connectionProperties) {
+    public void setConnectionProperties(int connectionProperties) {
         Log.v(this, "setConnectionProperties: %s", Connection.propertiesToString(
                 connectionProperties));
 
@@ -1869,7 +1893,7 @@
         // Check to verify that the call is still in the ringing state. A call can change states
         // between the time the user hits 'answer' and Telecom receives the command.
         if (isRinging("answer")) {
-            if (!isVideoCallingSupported() && VideoProfile.isVideo(videoState)) {
+            if (!isVideoCallingSupportedByPhoneAccount() && VideoProfile.isVideo(videoState)) {
                 // Video calling is not supported, yet the InCallService is attempting to answer as
                 // video.  We will simply answer as audio-only.
                 videoState = VideoProfile.STATE_AUDIO_ONLY;
@@ -2249,10 +2273,20 @@
     public void sendCallEvent(String event, int targetSdkVer, Bundle extras) {
         if (mConnectionService != null) {
             if (android.telecom.Call.EVENT_REQUEST_HANDOVER.equals(event)) {
-                if (targetSdkVer > Build.VERSION_CODES.O_MR1) {
+                if (targetSdkVer > Build.VERSION_CODES.P) {
                     Log.e(this, new Exception(), "sendCallEvent failed. Use public api handoverTo" +
-                            " for API > 27(O-MR1)");
-                    // TODO: Add "return" after DUO team adds new API support for handover
+                            " for API > 28(P)");
+                    // Event-based Handover APIs are deprecated, so inform the user.
+                    mHandler.post(new Runnable() {
+                        @Override
+                        public void run() {
+                            Toast.makeText(mContext, "WARNING: Event-based handover APIs are deprecated "
+                                            + "and will no longer function in Android Q.",
+                                    Toast.LENGTH_LONG).show();
+                        }
+                    });
+
+                    // Uncomment and remove toast at feature complete: return;
                 }
 
                 // Handover requests are targeted at Telecom, not the ConnectionService.
@@ -2773,7 +2807,7 @@
     public void setVideoState(int videoState) {
         // If the phone account associated with this call does not support video calling, then we
         // will automatically set the video state to audio-only.
-        if (!isVideoCallingSupported()) {
+        if (!isVideoCallingSupportedByPhoneAccount()) {
             Log.d(this, "setVideoState: videoState=%s defaulted to audio (video not supported)",
                     VideoProfile.videoStateToString(videoState));
             videoState = VideoProfile.STATE_AUDIO_ONLY;
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index e078dee..7b318c3 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -577,9 +577,14 @@
                     rejectCallAndLog(incomingCall);
                 }
             } else if (hasMaximumManagedDialingCalls(incomingCall)) {
-                Log.i(this, "onCallFilteringCompleted: Call rejected! Exceeds maximum number of " +
-                        "dialing calls.");
-                rejectCallAndLog(incomingCall);
+                if (shouldSilenceInsteadOfReject(incomingCall)) {
+                    incomingCall.silence();
+                } else {
+
+                    Log.i(this, "onCallFilteringCompleted: Call rejected! Exceeds maximum number of " +
+                            "dialing calls.");
+                    rejectCallAndLog(incomingCall);
+                }
             } else {
                 addCall(incomingCall);
             }
@@ -604,8 +609,10 @@
     }
 
     /**
-     * Whether allow (silence rather than reject) the incoming call if it has a different source
-     * (connection service) from the existing ringing call when reaching maximum ringing calls.
+     * In the event that the maximum supported calls of a given type is reached, the
+     * default behavior is to reject any additional calls of that type.  This checks
+     * if the device is configured to silence instead of reject the call, provided
+     * that the incoming call is from a different source (connection service).
      */
     private boolean shouldSilenceInsteadOfReject(Call incomingCall) {
         if (!mContext.getResources().getBoolean(
@@ -613,8 +620,6 @@
             return false;
         }
 
-        Call ringingCall = null;
-
         for (Call call : mCalls) {
             // Only operate on top-level calls
             if (call.getParentCall() != null) {
@@ -625,8 +630,7 @@
                 continue;
             }
 
-            if (CallState.RINGING == call.getState() &&
-                    call.getConnectionService() == incomingCall.getConnectionService()) {
+            if (call.getConnectionService() == incomingCall.getConnectionService()) {
                 return false;
             }
         }
@@ -3559,6 +3563,10 @@
                 mCallAudioManager.getCallAudioState());
         Call handoverToCall = startOutgoingCall(handoverFromCall.getHandle(), handoverToHandle,
                 extras, getCurrentUserHandle(), null /* originalIntent */);
+        if (handoverToCall == null) {
+            handoverFromCall.sendCallEvent(android.telecom.Call.EVENT_HANDOVER_FAILED, null);
+            return;
+        }
         Log.addEvent(handoverFromCall, LogUtils.Events.START_HANDOVER,
                 "handOverFrom=%s, handOverTo=%s", handoverFromCall.getId(), handoverToCall.getId());
         handoverFromCall.setHandoverDestinationCall(handoverToCall);
@@ -3979,4 +3987,14 @@
             }
         }
     }
+
+    /**
+     * Determines if there is an ongoing emergency call. This can be either an outgoing emergency
+     * call, or a number which has been identified by the number as an emergency call.
+     * @return {@code true} if there is an ongoing emergency call, {@code false} otherwise.
+     */
+    public boolean isInEmergencyCall() {
+        return mCalls.stream().filter(c -> c.isEmergencyCall()
+                || c.isNetworkIdentifiedEmergencyCall()).count() > 0;
+    }
 }
diff --git a/src/com/android/server/telecom/InCallController.java b/src/com/android/server/telecom/InCallController.java
index a413814..1ba23a5 100644
--- a/src/com/android/server/telecom/InCallController.java
+++ b/src/com/android/server/telecom/InCallController.java
@@ -716,11 +716,6 @@
     /** The in-call app implementations, see {@link IInCallService}. */
     private final Map<InCallServiceInfo, IInCallService> mInCallServices = new ArrayMap<>();
 
-    /**
-     * The {@link ComponentName} of the bound In-Call UI Service.
-     */
-    private ComponentName mInCallUIComponentName;
-
     private final CallIdMapper mCallIdMapper = new CallIdMapper(Call::getId);
 
     /** The {@link ComponentName} of the default InCall UI. */
@@ -1463,18 +1458,42 @@
         pw.decreaseIndent();
     }
 
+    /**
+     * @return The package name of the UI which is currently bound, or null if none.
+     */
+    private ComponentName getConnectedUi() {
+        InCallServiceInfo connectedUi = mInCallServices.keySet().stream().filter(
+                i -> i.getType() == IN_CALL_SERVICE_TYPE_DIALER_UI
+                        || i.getType() == IN_CALL_SERVICE_TYPE_SYSTEM_UI)
+                .findAny()
+                .orElse(null);
+        if (connectedUi != null) {
+            return connectedUi.mComponentName;
+        }
+        return null;
+    }
+
     public boolean doesConnectedDialerSupportRinging() {
         String ringingPackage =  null;
-        if (mInCallUIComponentName != null) {
-            ringingPackage = mInCallUIComponentName.getPackageName().trim();
+
+        ComponentName connectedPackage = getConnectedUi();
+        if (connectedPackage != null) {
+            ringingPackage = connectedPackage.getPackageName().trim();
+            Log.d(this, "doesConnectedDialerSupportRinging: alreadyConnectedPackage=%s",
+                    ringingPackage);
         }
 
         if (TextUtils.isEmpty(ringingPackage)) {
             // The current in-call UI returned nothing, so lets use the default dialer.
-            ringingPackage = DefaultDialerManager.getDefaultDialerApplication(
-                    mContext, UserHandle.USER_CURRENT);
+            ringingPackage = mDefaultDialerCache.getDefaultDialerApplication(
+                    mCallsManager.getCurrentUserHandle().getIdentifier());
+            if (ringingPackage != null) {
+                Log.d(this, "doesConnectedDialerSupportRinging: notCurentlyConnectedPackage=%s",
+                        ringingPackage);
+            }
         }
         if (TextUtils.isEmpty(ringingPackage)) {
+            Log.w(this, "doesConnectedDialerSupportRinging: no default dialer found; oh no!");
             return false;
         }
 
@@ -1484,11 +1503,15 @@
                 intent, PackageManager.GET_META_DATA,
                 mCallsManager.getCurrentUserHandle().getIdentifier());
         if (entries.isEmpty()) {
+            Log.w(this, "doesConnectedDialerSupportRinging: couldn't find dialer's package info"
+                    + " <sad trombone>");
             return false;
         }
 
         ResolveInfo info = entries.get(0);
         if (info.serviceInfo == null || info.serviceInfo.metaData == null) {
+            Log.w(this, "doesConnectedDialerSupportRinging: couldn't find dialer's metadata"
+                    + " <even sadder trombone>");
             return false;
         }
 
diff --git a/src/com/android/server/telecom/LogUtils.java b/src/com/android/server/telecom/LogUtils.java
index 4e966f0..9015f8a 100644
--- a/src/com/android/server/telecom/LogUtils.java
+++ b/src/com/android/server/telecom/LogUtils.java
@@ -137,6 +137,14 @@
         public static final String HANDOVER_FAILED = "HANDOVER_FAILED";
         public static final String START_RINBACK = "START_RINGBACK";
         public static final String STOP_RINGBACK = "STOP_RINGBACK";
+        public static final String REDIRECTION_BOUND_USER = "REDIRECTION_BOUND_USER";
+        public static final String REDIRECTION_BOUND_CARRIER = "REDIRECTION_BOUND_CARRIER";
+        public static final String REDIRECTION_SENT_USER = "REDIRECTION_SENT_USER";
+        public static final String REDIRECTION_SENT_CARRIER = "REDIRECTION_SENT_CARRIER";
+        public static final String REDIRECTION_COMPLETED_USER = "REDIRECTION_COMPLETED_USER";
+        public static final String REDIRECTION_COMPLETED_CARRIER = "REDIRECTION_COMPLETED_CARRIER";
+        public static final String REDIRECTION_TIMED_OUT_USER = "REDIRECTION_TIMED_OUT_USER";
+        public static final String REDIRECTION_TIMED_OUT_CARRIER = "REDIRECTION_TIMED_OUT_CARRIER";
 
         public static class Timings {
             public static final String ACCEPT_TIMING = "accept";
diff --git a/src/com/android/server/telecom/ParcelableCallUtils.java b/src/com/android/server/telecom/ParcelableCallUtils.java
index dc43095..80a8b5a 100644
--- a/src/com/android/server/telecom/ParcelableCallUtils.java
+++ b/src/com/android/server/telecom/ParcelableCallUtils.java
@@ -347,7 +347,10 @@
         android.telecom.Call.Details.PROPERTY_ASSISTED_DIALING_USED,
 
         Connection.PROPERTY_IS_RTT,
-        android.telecom.Call.Details.PROPERTY_RTT
+        android.telecom.Call.Details.PROPERTY_RTT,
+
+        Connection.PROPERTY_NETWORK_IDENTIFIED_EMERGENCY_CALL,
+        android.telecom.Call.Details.PROPERTY_NETWORK_IDENTIFIED_EMERGENCY_CALL
     };
 
     private static int convertConnectionToCallProperties(int connectionProperties) {
diff --git a/src/com/android/server/telecom/RespondViaSmsManager.java b/src/com/android/server/telecom/RespondViaSmsManager.java
index fadc6b5..4c13222 100644
--- a/src/com/android/server/telecom/RespondViaSmsManager.java
+++ b/src/com/android/server/telecom/RespondViaSmsManager.java
@@ -18,10 +18,14 @@
 
 // TODO: Needed for move to system service: import com.android.internal.R;
 import com.android.internal.os.SomeArgs;
-import com.android.internal.telephony.SmsApplication;
 
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.SharedPreferences;
 import android.content.res.Resources;
 import android.os.Handler;
@@ -45,30 +49,37 @@
  * Helper class to manage the "Respond via Message" feature for incoming calls.
  */
 public class RespondViaSmsManager extends CallsManagerListenerBase {
-    private static final int MSG_SHOW_SENT_TOAST = 2;
+    private static final String ACTION_MESSAGE_SENT = "com.android.server.telecom.MESSAGE_SENT";
+
+    private static final class MessageSentReceiver extends BroadcastReceiver {
+        private final String mContactName;
+        private final int mNumMessageParts;
+        private int mNumMessagesSent = 0;
+        MessageSentReceiver(String contactName, int numMessageParts) {
+            mContactName = contactName;
+            mNumMessageParts = numMessageParts;
+        }
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (getResultCode() == Activity.RESULT_OK) {
+                mNumMessagesSent++;
+                if (mNumMessagesSent == mNumMessageParts) {
+                    showMessageResultToast(mContactName, context, true);
+                    context.unregisterReceiver(this);
+                }
+            } else {
+                context.unregisterReceiver(this);
+                showMessageResultToast(mContactName, context, false);
+                Log.w(RespondViaSmsManager.class.getSimpleName(),
+                        "Message failed with error %s", getResultCode());
+            }
+        }
+    }
 
     private final CallsManager mCallsManager;
     private final TelecomSystem.SyncRoot mLock;
 
-    private final Handler mHandler = new Handler(Looper.getMainLooper()) {
-        @Override
-        public void handleMessage(Message msg) {
-            switch (msg.what) {
-                case MSG_SHOW_SENT_TOAST: {
-                    SomeArgs args = (SomeArgs) msg.obj;
-                    try {
-                        String toastMessage = (String) args.arg1;
-                        Context context = (Context) args.arg2;
-                        showMessageSentToast(toastMessage, context);
-                    } finally {
-                        args.recycle();
-                    }
-                    break;
-                }
-            }
-        }
-    };
-
     public RespondViaSmsManager(CallsManager callsManager, TelecomSystem.SyncRoot lock) {
         mCallsManager = callsManager;
         mLock = lock;
@@ -144,13 +155,15 @@
         }
     }
 
-    private void showMessageSentToast(final String phoneNumber, final Context context) {
+    private static void showMessageResultToast(final String phoneNumber,
+            final Context context, boolean success) {
         // ...and show a brief confirmation to the user (since
         // otherwise it's hard to be sure that anything actually
         // happened.)
         final Resources res = context.getResources();
-        final String formatString = res.getString(
-                R.string.respond_via_sms_confirmation_format);
+        final String formatString = res.getString(success
+                ? R.string.respond_via_sms_confirmation_format
+                : R.string.respond_via_sms_failure_format);
         final String confirmationMsg = String.format(formatString, phoneNumber);
         int startingPosition = confirmationMsg.indexOf(phoneNumber);
         int endingPosition = startingPosition + phoneNumber.length();
@@ -192,13 +205,20 @@
 
         SmsManager smsManager = SmsManager.getSmsManagerForSubscriptionId(subId);
         try {
-            smsManager.sendTextMessage(phoneNumber, null, textMessage, null /*sentIntent*/,
-                    null /*deliveryIntent*/);
-
-            SomeArgs args = SomeArgs.obtain();
-            args.arg1 = !TextUtils.isEmpty(contactName) ? contactName : phoneNumber;
-            args.arg2 = context;
-            mHandler.obtainMessage(MSG_SHOW_SENT_TOAST, args).sendToTarget();
+            ArrayList<String> messageParts = smsManager.divideMessage(textMessage);
+            ArrayList<PendingIntent> sentIntents = new ArrayList<>(messageParts.size());
+            for (int i = 0; i < messageParts.size(); i++) {
+                Intent intent = new Intent(ACTION_MESSAGE_SENT);
+                PendingIntent pendingIntent = PendingIntent.getBroadcast(context, i, intent,
+                        PendingIntent.FLAG_ONE_SHOT);
+                sentIntents.add(pendingIntent);
+            }
+            MessageSentReceiver receiver = new MessageSentReceiver(
+                    !TextUtils.isEmpty(contactName) ? contactName : phoneNumber,
+                    messageParts.size());
+            context.registerReceiver(receiver, new IntentFilter(ACTION_MESSAGE_SENT));
+            smsManager.sendMultipartTextMessage(phoneNumber, null, messageParts,
+                    sentIntents/*sentIntent*/, null /*deliveryIntent*/);
         } catch (IllegalArgumentException e) {
             Log.w(RespondViaSmsManager.this, "Couldn't send SMS message: " +
                     e.getMessage());
diff --git a/src/com/android/server/telecom/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java
index 81d1700..ab36585 100644
--- a/src/com/android/server/telecom/TelecomServiceImpl.java
+++ b/src/com/android/server/telecom/TelecomServiceImpl.java
@@ -1449,6 +1449,56 @@
                 Log.endSession();
             }
         }
+
+        /**
+         * See {@link TelecomManager#isInEmergencyCall()}
+         */
+        @Override
+        public boolean isInEmergencyCall() {
+            try {
+                Log.startSession("TSI.iIEC");
+                enforceModifyPermission();
+                synchronized (mLock) {
+                    long token = Binder.clearCallingIdentity();
+                    try {
+                        boolean isInEmergencyCall = mCallsManager.isInEmergencyCall();
+                        Log.i(this, "isInEmergencyCall: %b", isInEmergencyCall);
+                        return isInEmergencyCall;
+                    } finally {
+                        Binder.restoreCallingIdentity(token);
+                    }
+                }
+            } finally {
+                Log.endSession();
+            }
+        }
+
+        /**
+         * See {@link TelecomManager#handleCallIntent(Intent)} ()}
+         */
+        @Override
+        public void handleCallIntent(Intent intent) {
+            try {
+                Log.startSession("TSI.hCI");
+                synchronized (mLock) {
+                    int callingUid = Binder.getCallingUid();
+
+                    long token = Binder.clearCallingIdentity();
+                    if (callingUid != Process.myUid()) {
+                        throw new SecurityException("handleCallIntent is for Telecom only");
+                    }
+                    try {
+                        Log.i(this, "handleCallIntent: handling call intent");
+                        mCallIntentProcessorAdapter.processOutgoingCallIntent(mContext,
+                                mCallsManager, intent);
+                    } finally {
+                        Binder.restoreCallingIdentity(token);
+                    }
+                }
+            } finally {
+                Log.endSession();
+            }
+        }
     };
 
     /**
diff --git a/src/com/android/server/telecom/Timeouts.java b/src/com/android/server/telecom/Timeouts.java
index 5187641..9257654 100644
--- a/src/com/android/server/telecom/Timeouts.java
+++ b/src/com/android/server/telecom/Timeouts.java
@@ -18,6 +18,7 @@
 
 import android.content.ContentResolver;
 import android.provider.Settings;
+import android.telecom.CallRedirectionService;
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -49,6 +50,14 @@
         public long getEmergencyCallbackWindowMillis(ContentResolver cr) {
             return Timeouts.getEmergencyCallbackWindowMillis(cr);
         }
+
+        public long getUserDefinedCallRedirectionTimeoutMillis(ContentResolver cr) {
+            return Timeouts.getUserDefinedCallRedirectionTimeoutMillis(cr);
+        }
+
+        public long getCarrierCallRedirectionTimeoutMillis(ContentResolver cr) {
+            return Timeouts.getCarrierCallRedirectionTimeoutMillis(cr);
+        }
     }
 
     /** A prefix to use for all keys so to not clobber the global namespace. */
@@ -158,4 +167,23 @@
       return get(contentResolver, "emergency_callback_window_millis",
           TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES));
     }
+
+    /**
+     * Returns the amount of time for an user-defined {@link CallRedirectionService}.
+     *
+     * @param contentResolver The content resolved.
+     */
+    public static long getUserDefinedCallRedirectionTimeoutMillis(ContentResolver contentResolver) {
+        return get(contentResolver, "user_defined_call_redirection_timeout",
+            3000L /* 3 seconds */);
+    }
+
+    /**
+     * Returns the amount of time for a carrier {@link CallRedirectionService}.
+     *
+     * @param contentResolver The content resolved.
+     */
+    public static long getCarrierCallRedirectionTimeoutMillis(ContentResolver contentResolver) {
+        return get(contentResolver, "carrier_call_redirection_timeout", 3000L /* 3 seconds */);
+    }
 }
diff --git a/src/com/android/server/telecom/VideoProviderProxy.java b/src/com/android/server/telecom/VideoProviderProxy.java
index 7d90a6d..6e1f01d 100644
--- a/src/com/android/server/telecom/VideoProviderProxy.java
+++ b/src/com/android/server/telecom/VideoProviderProxy.java
@@ -20,7 +20,6 @@
 import android.app.AppOpsManager;
 import android.content.Context;
 import android.net.Uri;
-import android.os.Binder;
 import android.os.Build;
 import android.os.IBinder;
 import android.os.Looper;
@@ -33,6 +32,7 @@
 import android.text.TextUtils;
 import android.view.Surface;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.telecom.IVideoCallback;
 import com.android.internal.telecom.IVideoProvider;
 
@@ -40,8 +40,6 @@
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 
-import static android.Manifest.permission.CALL_PHONE;
-
 /**
  * Proxies video provider messages from {@link InCallService.VideoCall}
  * implementations to the underlying {@link Connection.VideoProvider} implementation.  Also proxies
@@ -55,7 +53,7 @@
     /**
      * Listener for Telecom components interested in callbacks from the video provider.
      */
-    interface Listener {
+    public interface Listener {
         void onSessionModifyRequestReceived(Call call, VideoProfile videoProfile);
     }
 
@@ -112,7 +110,7 @@
      * @param call The current call.
      * @throws RemoteException Remote exception.
      */
-    VideoProviderProxy(TelecomSystem.SyncRoot lock,
+    public VideoProviderProxy(TelecomSystem.SyncRoot lock,
             IVideoProvider videoProvider, Call call, CurrentUserProxy currentUserProxy)
             throws RemoteException {
 
@@ -136,11 +134,16 @@
         }
     }
 
+    @VisibleForTesting
+    public VideoCallListenerBinder getVideoCallListenerBinder() {
+        return mVideoCallListenerBinder;
+    }
+
     /**
      * IVideoCallback stub implementation.  An instance of this class receives callbacks from the
      * {@code ConnectionService}'s video provider.
      */
-    private final class VideoCallListenerBinder extends IVideoCallback.Stub {
+    public final class VideoCallListenerBinder extends IVideoCallback.Stub {
         /**
          * Proxies a request from the {@link #mConectionServiceVideoProvider} to the
          * {@link InCallService} when a session modification request is received.
@@ -160,13 +163,14 @@
                             Analytics.RECEIVE_REMOTE_SESSION_MODIFY_REQUEST,
                             videoProfile.getVideoState());
 
-                    if (!mCall.isVideoCallingSupported() &&
-                            VideoProfile.isVideo(videoProfile.getVideoState())) {
-                        // If video calling is not supported by the phone account, and we receive
-                        // a request to upgrade to video, automatically reject it without informing
-                        // the InCallService.
-
-                        Log.addEvent(mCall, LogUtils.Events.SEND_VIDEO_RESPONSE, "video not supported");
+                    if ((!mCall.isVideoCallingSupportedByPhoneAccount()
+                            || !mCall.isLocallyVideoCapable())
+                            && VideoProfile.isVideo(videoProfile.getVideoState())) {
+                        // If video calling is not supported by the phone account, or is not
+                        // locally video capable and we receive a request to upgrade to video,
+                        // automatically reject it without informing the InCallService.
+                        Log.addEvent(mCall, LogUtils.Events.SEND_VIDEO_RESPONSE,
+                                "video not supported");
                         VideoProfile responseProfile = new VideoProfile(
                                 VideoProfile.STATE_AUDIO_ONLY);
                         try {
diff --git a/src/com/android/server/telecom/callredirection/CallRedirectionCallback.java b/src/com/android/server/telecom/callredirection/CallRedirectionCallback.java
new file mode 100644
index 0000000..24ac326
--- /dev/null
+++ b/src/com/android/server/telecom/callredirection/CallRedirectionCallback.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2018 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.callredirection;
+
+import com.android.server.telecom.Call;
+
+public interface CallRedirectionCallback {
+    void onCallRedirectionComplete(Call call);
+}
diff --git a/src/com/android/server/telecom/callredirection/CallRedirectionProcessor.java b/src/com/android/server/telecom/callredirection/CallRedirectionProcessor.java
new file mode 100644
index 0000000..abb87e1
--- /dev/null
+++ b/src/com/android/server/telecom/callredirection/CallRedirectionProcessor.java
@@ -0,0 +1,409 @@
+/*
+ * Copyright (C) 2018 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.callredirection;
+
+import android.Manifest;
+import android.app.AppOpsManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.PersistableBundle;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.telecom.CallRedirectionService;
+import android.telecom.Log;
+import android.telecom.Logging.Runnable;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.CarrierConfigManager;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telecom.ICallRedirectionAdapter;
+import com.android.internal.telecom.ICallRedirectionService;
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.LogUtils;
+import com.android.server.telecom.PhoneAccountRegistrar;
+import com.android.server.telecom.TelecomSystem;
+import com.android.server.telecom.Timeouts;
+
+import java.util.List;
+
+/**
+ * A single instance of call redirection processor that handles the call redirection with
+ * user-defined {@link CallRedirectionService} and carrier {@link CallRedirectionService} for a
+ * single call.
+ *
+ * A user-defined call redirection will be performed firstly and a carrier call redirection will be
+ * performed after that; there will be a total of two call redirection cycles.
+ *
+ * A call redirection cycle is a cycle:
+ * 1) Telecom requests a call redirection of a call with a specific {@link CallRedirectionService},
+ * 2) Telecom receives the response either from a specific {@link CallRedirectionService} or from
+ * the timeout.
+ *
+ * Telecom should return to {@link CallsManager} at the end of current call redirection
+ * cycle, if
+ * 1) {@link CallRedirectionService} sends {@link CallRedirectionService#cancelCall()} response
+ * before timeout;
+ * or 2) Telecom finishes call redirection with carrier {@link CallRedirectionService}.
+ */
+public class CallRedirectionProcessor implements CallRedirectionCallback {
+
+    private class CallRedirectionAttempt {
+        private final ComponentName mComponentName;
+        private final String mServiceType;
+        private ServiceConnection mConnection;
+        private ICallRedirectionService mService;
+
+        private CallRedirectionAttempt(ComponentName componentName, String serviceType) {
+            mComponentName = componentName;
+            mServiceType = serviceType;
+        }
+
+        private void process() {
+            Intent intent = new Intent(CallRedirectionService.SERVICE_INTERFACE)
+                    .setComponent(mComponentName);
+            ServiceConnection connection = new CallRedirectionServiceConnection();
+            if (mContext.bindServiceAsUser(
+                    intent,
+                    connection,
+                    Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE,
+                    UserHandle.CURRENT)) {
+                Log.d(this, "bindService, found " + mServiceType + " call redirection service,"
+                        + " waiting for it to connect");
+                mConnection = connection;
+            }
+        }
+
+        private void onServiceBound(ICallRedirectionService service) {
+            mService = service;
+            try {
+                mService.placeCall(new CallRedirectionAdapter(), mHandle, mPhoneAccountHandle);
+                Log.addEvent(mCall, mServiceType.equals(SERVICE_TYPE_USER_DEFINED)
+                        ? LogUtils.Events.REDIRECTION_SENT_USER
+                        : LogUtils.Events.REDIRECTION_SENT_CARRIER);
+                Log.d(this, "Requested placeCall with [handle]" + Log.pii(mHandle)
+                        + " [phoneAccountHandle]" + mPhoneAccountHandle);
+            } catch (RemoteException e) {
+                Log.e(this, e, "Failed to request with the found " + mServiceType + " call"
+                        + " redirection service");
+                finishCallRedirection();
+            }
+        }
+
+        private void finishCallRedirection() {
+            if (((mServiceType.equals(SERVICE_TYPE_CARRIER)) && mIsCarrierRedirectionPending)
+                || ((mServiceType.equals(SERVICE_TYPE_USER_DEFINED))
+                    && mIsUserDefinedRedirectionPending)) {
+                if (mConnection != null) {
+                    // We still need to call unbind even if the service disconnected.
+                    mContext.unbindService(mConnection);
+                    mConnection = null;
+                }
+                mService = null;
+                onCallRedirectionComplete(mCall);
+            }
+        }
+
+        private class CallRedirectionServiceConnection implements ServiceConnection {
+            @Override
+            public void onServiceConnected(ComponentName componentName, IBinder service) {
+                Log.startSession("CRSC.oSC");
+                try {
+                    synchronized (mTelecomLock) {
+                        Log.addEvent(mCall, mServiceType.equals(SERVICE_TYPE_USER_DEFINED)
+                                ? LogUtils.Events.REDIRECTION_BOUND_USER
+                                : LogUtils.Events.REDIRECTION_BOUND_CARRIER, componentName);
+                        onServiceBound(ICallRedirectionService.Stub.asInterface(service));
+                    }
+                } finally {
+                    Log.endSession();
+                }
+            }
+
+            @Override
+            public void onServiceDisconnected(ComponentName componentName) {
+                Log.startSession("CRSC.oSD");
+                try {
+                    synchronized (mTelecomLock) {
+                        finishCallRedirection();
+                    }
+                } finally {
+                    Log.endSession();
+                }
+            }
+        }
+
+        private class CallRedirectionAdapter extends ICallRedirectionAdapter.Stub {
+            @Override
+            public void cancelCall() {
+                Log.startSession("CRA.cC");
+                long token = Binder.clearCallingIdentity();
+                try {
+                    synchronized (mTelecomLock) {
+                        Log.d(this, "Received cancelCall from " +  mServiceType + " call"
+                                + " redirection service");
+                        mShouldCancelCall = true;
+                        finishCallRedirection();
+                    }
+                } finally {
+                    Binder.restoreCallingIdentity(token);
+                    Log.endSession();
+                }
+            }
+
+            @Override
+            public void placeCallUnmodified() {
+                Log.startSession("CRA.pCU");
+                long token = Binder.clearCallingIdentity();
+                try {
+                    synchronized (mTelecomLock) {
+                        Log.d(this, "Received placeCallUnmodified from " +  mServiceType + " call"
+                                + " redirection service");
+                        finishCallRedirection();
+                    }
+                } finally {
+                    Binder.restoreCallingIdentity(token);
+                    Log.endSession();
+                }
+            }
+
+            @Override
+            public void redirectCall(Uri handle, PhoneAccountHandle targetPhoneAccount) {
+                Log.startSession("CRA.rC");
+                long token = Binder.clearCallingIdentity();
+                try {
+                    synchronized (mTelecomLock) {
+                        mHandle = handle;
+                        mPhoneAccountHandle = targetPhoneAccount;
+                        Log.d(this, "Received redirectCall with [handle]" + Log.pii(mHandle)
+                                + " [phoneAccountHandle]" + mPhoneAccountHandle + " from "
+                                + mServiceType + " call" + " redirection service");
+                        finishCallRedirection();
+                    }
+                } finally {
+                    Binder.restoreCallingIdentity(token);
+                    Log.endSession();
+                }
+            }
+        }
+    }
+
+    private final Context mContext;
+    private final CallsManager mCallsManager;
+    private final Call mCall;
+    private final PhoneAccountRegistrar mPhoneAccountRegistrar;
+    private final Timeouts.Adapter mTimeoutsAdapter;
+    private final TelecomSystem.SyncRoot mTelecomLock;
+    private final Handler mHandler = new Handler(Looper.getMainLooper());
+
+    private CallRedirectionAttempt mAttempt;
+    public static final String SERVICE_TYPE_CARRIER = "carrier";
+    public static final String SERVICE_TYPE_USER_DEFINED = "user_defined";
+
+    private PhoneAccountHandle mPhoneAccountHandle;
+    private Uri mHandle;
+
+    /**
+     * Indicates if Telecom should cancel the call when the whole call redirection finishes.
+     */
+    private boolean mShouldCancelCall = false;
+    /**
+     * Indicates if Telecom is waiting for a callback from a user-defined
+     * {@link CallRedirectionService}.
+     */
+    private boolean mIsUserDefinedRedirectionPending = false;
+    /**
+     * Indicates if Telecom is waiting for a callback from a carrier
+     * {@link CallRedirectionService}.
+     */
+    private boolean mIsCarrierRedirectionPending = false;
+
+    public CallRedirectionProcessor(
+            Context context,
+            CallsManager callsManager,
+            Call call,
+            PhoneAccountRegistrar phoneAccountRegistrar,
+            Uri handle,
+            PhoneAccountHandle phoneAccountHandle,
+            Timeouts.Adapter timeoutsAdapter,
+            TelecomSystem.SyncRoot lock) {
+        mContext = context;
+        mCallsManager = callsManager;
+        mCall = call;
+        mPhoneAccountRegistrar = phoneAccountRegistrar;
+        mHandle = handle;
+        mPhoneAccountHandle = phoneAccountHandle;
+        mTimeoutsAdapter = timeoutsAdapter;
+        mTelecomLock = lock;
+    }
+
+    @Override
+    public void onCallRedirectionComplete(Call call) {
+        // synchronized on mTelecomLock to enter into Telecom.
+        mHandler.post(new Runnable("CRP.oCRC", mTelecomLock) {
+            @Override
+            public void loggedRun() {
+                if (mIsUserDefinedRedirectionPending) {
+                    Log.addEvent(mCall, LogUtils.Events.REDIRECTION_COMPLETED_USER);
+                    mIsUserDefinedRedirectionPending = false;
+                    if (mShouldCancelCall) {
+                        // TODO mCallsManager.onCallRedirectionComplete
+                    } else {
+                        performCarrierCallRedirection();
+                    }
+                }
+                if (mIsCarrierRedirectionPending) {
+                    Log.addEvent(mCall, LogUtils.Events.REDIRECTION_COMPLETED_CARRIER);
+                    mIsCarrierRedirectionPending = false;
+                    // TODO mCallsManager.onCallRedirectionComplete
+                }
+            }
+        }.prepare());
+    }
+
+    /*
+     * The entry to perform call redirection of the call from (@link CallsManager)
+     */
+    public void performCallRedirection() {
+        performUserDefinedCallRedirection();
+    }
+
+    private void performUserDefinedCallRedirection() {
+        Log.d(this, "performUserDefinedCallRedirection");
+        ComponentName componentName = getUserDefinedCallRedirectionService(mContext);
+        if (componentName != null && canBindToCallRedirectionService(mContext, componentName)) {
+            mAttempt = new CallRedirectionAttempt(componentName, SERVICE_TYPE_USER_DEFINED);
+            mAttempt.process();
+            mIsUserDefinedRedirectionPending = true;
+            processTimeoutForCallRedirection(SERVICE_TYPE_USER_DEFINED);
+        } else {
+            Log.i(this, "There are no user-defined call redirection services installed on this"
+                    + " device.");
+            performCarrierCallRedirection();
+        }
+    }
+
+    private void performCarrierCallRedirection() {
+        Log.d(this, "performCarrierCallRedirection");
+        ComponentName componentName = getCarrierCallRedirectionService(
+            mContext, mPhoneAccountHandle);
+        if (componentName != null && canBindToCallRedirectionService(mContext, componentName)) {
+            mAttempt = new CallRedirectionAttempt(componentName, SERVICE_TYPE_CARRIER);
+            mAttempt.process();
+            mIsCarrierRedirectionPending = true;
+            processTimeoutForCallRedirection(SERVICE_TYPE_CARRIER);
+        } else {
+            Log.i(this, "There are no carrier call redirection services installed on this"
+                    + " device.");
+            // TODO return to CallsManager.onCallRedirectionComplete
+        }
+    }
+
+    private void processTimeoutForCallRedirection(String serviceType) {
+        long timeout = serviceType.equals(SERVICE_TYPE_USER_DEFINED) ?
+            mTimeoutsAdapter.getUserDefinedCallRedirectionTimeoutMillis(
+                mContext.getContentResolver()) : mTimeoutsAdapter
+            .getCarrierCallRedirectionTimeoutMillis(mContext.getContentResolver());
+
+        mHandler.postDelayed(new Runnable("CRP.pTFCR", null) {
+            @Override
+            public void loggedRun() {
+                boolean isCurrentRedirectionPending =
+                        serviceType.equals(SERVICE_TYPE_USER_DEFINED) ?
+                                mIsUserDefinedRedirectionPending : mIsCarrierRedirectionPending;
+                if (isCurrentRedirectionPending) {
+                    Log.i(CallRedirectionProcessor.this,
+                            serviceType + "call redirection has timed out.");
+                    Log.addEvent(mCall, serviceType.equals(SERVICE_TYPE_USER_DEFINED)
+                            ? LogUtils.Events.REDIRECTION_TIMED_OUT_USER
+                            : LogUtils.Events.REDIRECTION_TIMED_OUT_CARRIER);
+                    onCallRedirectionComplete(mCall);
+                }
+            }
+        }.prepare(), timeout);
+    }
+
+    private ComponentName getUserDefinedCallRedirectionService(Context context) {
+        // TODO get service component name from settings default value:
+        // android.provider.Settings#CALL_REDIRECTION_DEFAULT_APPLICATION
+        return null;
+    }
+
+    private ComponentName getCarrierCallRedirectionService(Context context, PhoneAccountHandle
+            targetPhoneAccountHandle) {
+        CarrierConfigManager configManager = (CarrierConfigManager)
+                context.getSystemService(Context.CARRIER_CONFIG_SERVICE);
+        if (configManager == null) {
+            Log.i(this, "Cannot get CarrierConfigManager.");
+            return null;
+        }
+        PersistableBundle pb = configManager.getConfigForSubId(mPhoneAccountRegistrar
+                .getSubscriptionIdForPhoneAccount(targetPhoneAccountHandle));
+        if (pb == null) {
+            Log.i(this, "Cannot get PersistableBundle.");
+            return null;
+        }
+        String componentNameString = pb.getString(
+            CarrierConfigManager.KEY_CALL_REDIRECTION_SERVICE_COMPONENT_NAME_STRING);
+        return new ComponentName(context, componentNameString);
+    }
+
+    private boolean canBindToCallRedirectionService(Context context, ComponentName componentName) {
+        Intent intent = new Intent(CallRedirectionService.SERVICE_INTERFACE);
+        intent.setComponent(componentName);
+        List<ResolveInfo> entries = mContext.getPackageManager().queryIntentServicesAsUser(
+                intent, 0, mCallsManager.getCurrentUserHandle().getIdentifier());
+        if (entries.isEmpty()) {
+            Log.i(this, "There are no call redirection services installed on this device.");
+            return false;
+        } else if (entries.size() != 1) {
+            Log.i(this, "There are multiple call redirection services installed on this device.");
+            return false;
+        } else {
+            ResolveInfo entry = entries.get(0);
+            if (entry.serviceInfo.permission == null || !entry.serviceInfo.permission.equals(
+                    Manifest.permission.BIND_CALL_REDIRECTION_SERVICE)) {
+                Log.w(this, "CallRedirectionService must require BIND_CALL_REDIRECTION_SERVICE"
+                        + " permission: " + entry.serviceInfo.packageName);
+                return false;
+            }
+            AppOpsManager appOps = (AppOpsManager) context.getSystemService(
+                Context.APP_OPS_SERVICE);
+            if (appOps.noteOp(AppOpsManager.OP_PROCESS_OUTGOING_CALLS, Binder.getCallingUid(),
+                    entry.serviceInfo.packageName) != AppOpsManager.MODE_ALLOWED) {
+                Log.w(this, "App Ops does not allow " + entry.serviceInfo.packageName);
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Returns the handler for testing purposes.
+     */
+    @VisibleForTesting
+    public Handler getHandler() {
+        return mHandler;
+    }
+}
diff --git a/src/com/android/server/telecom/components/PrimaryCallReceiver.java b/src/com/android/server/telecom/components/PrimaryCallReceiver.java
deleted file mode 100644
index f19a243..0000000
--- a/src/com/android/server/telecom/components/PrimaryCallReceiver.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package com.android.server.telecom.components;
-
-import com.android.server.telecom.TelecomSystem;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.telecom.Log;
-
-/**
- * Single point of entry for all outgoing and incoming calls. {@link UserCallIntentProcessor} serves
- * as a trampoline that captures call intents for individual users and forwards it to
- * the {@link PrimaryCallReceiver} which interacts with the rest of Telecom, both of which run only as
- * the primary user.
- */
-public class PrimaryCallReceiver extends BroadcastReceiver implements TelecomSystem.Component {
-
-    @Override
-    public void onReceive(Context context, Intent intent) {
-        Log.startSession("PCR.oR");
-        synchronized (getTelecomSystem().getLock()) {
-            getTelecomSystem().getCallIntentProcessor().processIntent(intent);
-        }
-        Log.endSession();
-    }
-
-    @Override
-    public TelecomSystem getTelecomSystem() {
-        return TelecomSystem.getInstance();
-    }
-}
diff --git a/src/com/android/server/telecom/components/UserCallIntentProcessor.java b/src/com/android/server/telecom/components/UserCallIntentProcessor.java
index 0c8525f..6a8f8c8 100644
--- a/src/com/android/server/telecom/components/UserCallIntentProcessor.java
+++ b/src/com/android/server/telecom/components/UserCallIntentProcessor.java
@@ -189,14 +189,13 @@
     }
 
     /**
-     * Potentially trampolines the intent to the broadcast receiver that runs only as the primary
-     * user.  If the caller is local to the Telecom service, we send the intent to Telecom without
-     * rebroadcasting it.
+     * Potentially trampolines the intent to Telecom via TelecomServiceImpl.
+     * If the caller is local to the Telecom service, we send the intent to Telecom without
+     * sending it through TelecomServiceImpl.
      */
     private boolean sendIntentToDestination(Intent intent, boolean isLocalInvocation) {
         intent.putExtra(CallIntentProcessor.KEY_IS_INCOMING_CALL, false);
         intent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND);
-        intent.setClass(mContext, PrimaryCallReceiver.class);
         if (isLocalInvocation) {
             // We are invoking this from TelecomServiceImpl, so TelecomSystem is available.  Don't
             // bother trampolining the intent, just sent it directly to the call intent processor.
@@ -209,7 +208,8 @@
             // We're calling from the UserCallActivity, so the TelecomSystem is not in the same
             // process; we need to trampoline to TelecomSystem in the system server process.
             Log.i(this, "sendIntentToDestination: trampoline to Telecom.");
-            mContext.sendBroadcastAsUser(intent, UserHandle.SYSTEM);
+            TelecomManager tm = (TelecomManager) mContext.getSystemService(Context.TELECOM_SERVICE);
+            tm.handleCallIntent(intent);
         }
         return true;
     }
diff --git a/testapps/AndroidManifest.xml b/testapps/AndroidManifest.xml
index 48451d1..02443ba 100644
--- a/testapps/AndroidManifest.xml
+++ b/testapps/AndroidManifest.xml
@@ -19,9 +19,10 @@
           package="com.android.server.telecom.testapps">
 
     <uses-sdk
-        android:minSdkVersion="23"
-        android:targetSdkVersion="23" />
+        android:minSdkVersion="28"
+        android:targetSdkVersion="28" />
 
+    <uses-permission android:name="android.permission.ACCEPT_HANDOVER" />
     <uses-permission android:name="android.permission.BLUETOOTH" />
     <uses-permission android:name="android.permission.CAMERA" />
     <uses-permission android:name="android.permission.CALL_PHONE" />
diff --git a/testapps/src/com/android/server/telecom/testapps/SelfManagedCallingActivity.java b/testapps/src/com/android/server/telecom/testapps/SelfManagedCallingActivity.java
index 93a2c7f..959b855 100644
--- a/testapps/src/com/android/server/telecom/testapps/SelfManagedCallingActivity.java
+++ b/testapps/src/com/android/server/telecom/testapps/SelfManagedCallingActivity.java
@@ -119,12 +119,12 @@
         mPlaceIncomingCallButton.setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View v) {
-                placeIncomingCall(false /* isHandoverFrom */);
+                placeIncomingCall();
             }
         });
         mHandoverFrom = (Button) findViewById(R.id.handoverFrom);
         mHandoverFrom.setOnClickListener((v -> {
-            placeIncomingCall(true /* isHandoverFrom */);
+            initiateHandover();
         }));
 
         mUseAcct1Button = findViewById(R.id.useAcct1Button);
@@ -176,7 +176,14 @@
         tm.placeCall(Uri.parse(mNumber.getText().toString()), extras);
     }
 
-    private void placeIncomingCall(boolean isHandoverFrom) {
+    private void initiateHandover() {
+        TelecomManager tm = TelecomManager.from(this);
+        PhoneAccountHandle phoneAccountHandle = getSelectedPhoneAccountHandle();
+        Uri address = Uri.parse(mNumber.getText().toString());
+        tm.acceptHandover(address, VideoProfile.STATE_BIDIRECTIONAL, phoneAccountHandle);
+    }
+
+    private void placeIncomingCall() {
         TelecomManager tm = TelecomManager.from(this);
         PhoneAccountHandle phoneAccountHandle = getSelectedPhoneAccountHandle();
 
@@ -196,9 +203,6 @@
             extras.putInt(TelecomManager.EXTRA_INCOMING_VIDEO_STATE,
                     VideoProfile.STATE_BIDIRECTIONAL);
         }
-        if (isHandoverFrom) {
-            extras.putBoolean(TelecomManager.EXTRA_IS_HANDOVER, true);
-        }
         tm.addNewIncomingCall(getSelectedPhoneAccountHandle(), extras);
     }
 
diff --git a/testapps/src/com/android/server/telecom/testapps/SelfManagedConnectionService.java b/testapps/src/com/android/server/telecom/testapps/SelfManagedConnectionService.java
index fca05dc..f2b6496 100644
--- a/testapps/src/com/android/server/telecom/testapps/SelfManagedConnectionService.java
+++ b/testapps/src/com/android/server/telecom/testapps/SelfManagedConnectionService.java
@@ -24,9 +24,7 @@
 import android.telecom.Log;
 import android.telecom.PhoneAccountHandle;
 import android.telecom.TelecomManager;
-import android.telecom.VideoProfile;
 
-import java.util.Objects;
 import java.util.Random;
 
 /**
@@ -45,13 +43,25 @@
             PhoneAccountHandle connectionManagerAccount,
             final ConnectionRequest request) {
 
-        return createSelfManagedConnection(request, false);
+        return createSelfManagedConnection(request, false, false /* isHandover */);
     }
 
     @Override
     public Connection onCreateIncomingConnection(PhoneAccountHandle connectionManagerPhoneAccount,
             ConnectionRequest request) {
-        return createSelfManagedConnection(request, true);
+        return createSelfManagedConnection(request, true, false /* isHandover */);
+    }
+
+    @Override
+    public Connection onCreateOutgoingHandoverConnection(PhoneAccountHandle fromPhoneAccountHandle,
+            ConnectionRequest request) {
+        return createSelfManagedConnection(request, false, true /* isHandover */);
+    }
+
+    @Override
+    public Connection onCreateIncomingHandoverConnection(PhoneAccountHandle fromPhoneAccountHandle,
+            ConnectionRequest request) {
+        return createSelfManagedConnection(request, true, true /* isHandover */);
     }
 
     @Override
@@ -77,7 +87,8 @@
         mCallList.notifyConnectionServiceFocusGained();
     }
 
-    private Connection createSelfManagedConnection(ConnectionRequest request, boolean isIncoming) {
+    private Connection createSelfManagedConnection(ConnectionRequest request, boolean isIncoming,
+            boolean isHandover) {
         SelfManagedConnection connection = new SelfManagedConnection(mCallList,
                 getApplicationContext(), isIncoming);
         connection.setListener(mCallList.getConnectionListener());
@@ -98,11 +109,10 @@
         if (requestExtras != null) {
             boolean isHoldable = requestExtras.getBoolean(EXTRA_HOLDABLE, false);
             Log.i(this, "createConnection: isHandover=%b, handoverFrom=%s, holdable=%b",
-                    requestExtras.getBoolean(TelecomManager.EXTRA_IS_HANDOVER),
+                    isHandover,
                     requestExtras.getString(TelecomManager.EXTRA_HANDOVER_FROM_PHONE_ACCOUNT),
                     isHoldable);
-            connection.setIsHandover(requestExtras.getBoolean(TelecomManager.EXTRA_IS_HANDOVER,
-                    false));
+            connection.setIsHandover(isHandover);
             if (isHoldable) {
                 connection.setConnectionCapabilities(connection.getConnectionCapabilities() |
                         Connection.CAPABILITY_HOLD | Connection.CAPABILITY_SUPPORT_HOLD);
diff --git a/testapps/src/com/android/server/telecom/testapps/TestInCallUI.java b/testapps/src/com/android/server/telecom/testapps/TestInCallUI.java
index 9851253..2a5b33a 100644
--- a/testapps/src/com/android/server/telecom/testapps/TestInCallUI.java
+++ b/testapps/src/com/android/server/telecom/testapps/TestInCallUI.java
@@ -18,6 +18,7 @@
 
 import android.app.Activity;
 import android.bluetooth.BluetoothDevice;
+import android.content.ComponentName;
 import android.content.Intent;
 import android.os.Bundle;
 import android.telecom.Call;
@@ -209,11 +210,8 @@
 
         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);
+            call.handoverTo(getHandoverToPhoneAccountHandle(), VideoProfile.STATE_BIDIRECTIONAL,
+                    null);
         });
     }
 
@@ -263,17 +261,8 @@
     }
 
     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;
+        return new PhoneAccountHandle(new ComponentName(
+                SelfManagedCallList.class.getPackage().getName(),
+                SelfManagedConnectionService.class.getName()), "1");
     }
 }
diff --git a/tests/src/com/android/server/telecom/tests/BasicCallTests.java b/tests/src/com/android/server/telecom/tests/BasicCallTests.java
index 9240199..e863d27 100644
--- a/tests/src/com/android/server/telecom/tests/BasicCallTests.java
+++ b/tests/src/com/android/server/telecom/tests/BasicCallTests.java
@@ -26,7 +26,6 @@
 import static org.mockito.Matchers.anyString;
 import static org.mockito.Matchers.eq;
 import static org.mockito.Matchers.isNull;
-import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.times;
@@ -921,12 +920,12 @@
                 Process.myUserHandle(), VideoProfile.STATE_BIDIRECTIONAL);
         com.android.server.telecom.Call call = mTelecomSystem.getCallsManager().getCalls()
                 .iterator().next();
-        assert(call.isVideoCallingSupported());
+        assert(call.isVideoCallingSupportedByPhoneAccount());
         assertEquals(VideoProfile.STATE_BIDIRECTIONAL, call.getVideoState());
 
         // Change the phone account to one which supports video calling.
         call.setTargetPhoneAccount(mPhoneAccountA1.getAccountHandle());
-        assert(call.isVideoCallingSupported());
+        assert(call.isVideoCallingSupportedByPhoneAccount());
         assertEquals(VideoProfile.STATE_BIDIRECTIONAL, call.getVideoState());
     }
 
@@ -944,12 +943,12 @@
                 Process.myUserHandle(), VideoProfile.STATE_BIDIRECTIONAL);
         com.android.server.telecom.Call call = mTelecomSystem.getCallsManager().getCalls()
                 .iterator().next();
-        assert(call.isVideoCallingSupported());
+        assert(call.isVideoCallingSupportedByPhoneAccount());
         assertEquals(VideoProfile.STATE_BIDIRECTIONAL, call.getVideoState());
 
         // Change the phone account to one which does not support video calling.
         call.setTargetPhoneAccount(mPhoneAccountA2.getAccountHandle());
-        assert(!call.isVideoCallingSupported());
+        assert(!call.isVideoCallingSupportedByPhoneAccount());
         assertEquals(VideoProfile.STATE_AUDIO_ONLY, call.getVideoState());
     }
 
@@ -1054,8 +1053,13 @@
                 mConnectionServiceFixtureA);
 
         // Should have reverted back to earpiece.
-        assertEquals(CallAudioState.ROUTE_EARPIECE,
-                mInCallServiceFixtureX.mCallAudioState.getRoute());
+        assertTrueWithTimeout(new Predicate<Void>() {
+            @Override
+            public boolean apply(Void aVoid) {
+                return mInCallServiceFixtureX.mCallAudioState.getRoute()
+                        == CallAudioState.ROUTE_EARPIECE;
+            }
+        });
     }
 
     /**
diff --git a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
index bc9cfc3..7bfd82f 100644
--- a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
@@ -17,6 +17,7 @@
 package com.android.server.telecom.tests;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
@@ -37,6 +38,7 @@
 import android.telecom.Connection;
 import android.telecom.PhoneAccount;
 import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
 import android.telecom.VideoProfile;
 import android.telephony.TelephonyManager;
 import android.test.suitebuilder.annotation.MediumTest;
@@ -868,6 +870,32 @@
         verify(incomingCall).setIsUsingCallFiltering(eq(false));
     }
 
+    @SmallTest
+    @Test
+    public void testIsInEmergencyCallNetwork() {
+        // Setup a call which the network identified as an emergency call.
+        Call ongoingCall = addSpyCall();
+        ongoingCall.setConnectionProperties(Connection.PROPERTY_NETWORK_IDENTIFIED_EMERGENCY_CALL);
+
+        assertFalse(ongoingCall.isEmergencyCall());
+        assertTrue(ongoingCall.isNetworkIdentifiedEmergencyCall());
+        assertTrue(mCallsManager.isInEmergencyCall());
+    }
+
+    @SmallTest
+    @Test
+    public void testIsInEmergencyCallLocal() {
+        // Setup a call which is considered emergency based on its phone number.
+        Call ongoingCall = addSpyCall();
+        when(mPhoneNumberUtilsAdapter.isLocalEmergencyNumber(any(), any())).thenReturn(true);
+        ongoingCall.setHandle(Uri.fromParts("tel", "5551212", null),
+                TelecomManager.PRESENTATION_ALLOWED);
+
+        assertTrue(ongoingCall.isEmergencyCall());
+        assertFalse(ongoingCall.isNetworkIdentifiedEmergencyCall());
+        assertTrue(mCallsManager.isInEmergencyCall());
+    }
+
     private Call addSpyCallWithConnectionService(ConnectionServiceWrapper connSvr) {
         Call call = addSpyCall();
         doReturn(connSvr).when(call).getConnectionService();
diff --git a/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java b/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
index ad00456..25110e8 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
@@ -682,11 +682,6 @@
         Context localAppContext = mComponentContextFixture.getTestDouble().getApplicationContext();
         new UserCallIntentProcessor(localAppContext, userHandle).processIntent(
                 actionCallIntent, null, true /* hasCallAppOp*/, false /* isLocal */);
-        // UserCallIntentProcessor's mContext.sendBroadcastAsUser(...) will call to an empty method
-        // as to not actually try to send an intent to PrimaryCallReceiver. We verify that it was
-        // called correctly in order to continue.
-        verify(localAppContext).sendBroadcastAsUser(actionCallIntent, UserHandle.SYSTEM);
-        mTelecomSystem.getCallIntentProcessor().processIntent(actionCallIntent);
         // Wait for handler to start CallerInfo lookup.
         waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT);
         // Send the CallerInfo lookup reply.
diff --git a/tests/src/com/android/server/telecom/tests/VideoProviderProxyTest.java b/tests/src/com/android/server/telecom/tests/VideoProviderProxyTest.java
new file mode 100644
index 0000000..a6eecf7
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/VideoProviderProxyTest.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2018 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.tests;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.os.IBinder;
+import android.telecom.VideoProfile;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.internal.telecom.IVideoProvider;
+import com.android.server.telecom.Analytics;
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CurrentUserProxy;
+import com.android.server.telecom.TelecomSystem;
+import com.android.server.telecom.VideoProviderProxy;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+public class VideoProviderProxyTest extends TelecomTestCase {
+
+    private TelecomSystem.SyncRoot mLock;
+    private VideoProviderProxy mVideoProviderProxy;
+    @Mock private IVideoProvider mVideoProvider;
+    @Mock private IBinder mIBinder;
+    @Mock private Call mCall;
+    @Mock private Analytics.CallInfo mCallInfo;
+    @Mock private CurrentUserProxy mCurrentUserProxy;
+    @Mock private VideoProviderProxy.Listener mListener;
+
+    @Override
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        MockitoAnnotations.initMocks(this);
+        mLock = new TelecomSystem.SyncRoot() { };
+
+        when(mVideoProvider.asBinder()).thenReturn(mIBinder);
+        doNothing().when(mIBinder).linkToDeath(any(), anyInt());
+        when(mCall.getAnalytics()).thenReturn(mCallInfo);
+        doNothing().when(mCallInfo).addVideoEvent(anyInt(), anyInt());
+        mVideoProviderProxy = new VideoProviderProxy(mLock, mVideoProvider, mCall,
+                mCurrentUserProxy);
+        mVideoProviderProxy.addListener(mListener);
+    }
+
+    /**
+     * Tests the case where we receive a request to upgrade to video, except:
+     * 1. Phone account says we support video.
+     * 2. Call says we don't support video.
+     *
+     * Ensures that we send back a response immediately to indicate the call should remain as
+     * audio-only.
+     * @throws Exception
+     */
+    @SmallTest
+    @Test
+    public void testReceiveUpgradeRequestWhenLocalDoesntSupportVideo() throws Exception {
+        // Given a call which supports video at the phone account level, but is not currently
+        // marked as supporting video locally.
+        when(mCall.isLocallyVideoCapable()).thenReturn(false);
+        when(mCall.isVideoCallingSupportedByPhoneAccount()).thenReturn(true);
+
+        // Simulate receiving a request to upgrade to video.
+        mVideoProviderProxy.getVideoCallListenerBinder().receiveSessionModifyRequest(
+                new VideoProfile(VideoProfile.STATE_BIDIRECTIONAL));
+
+        // Make sure that we send back a response rejecting the request.
+        ArgumentCaptor<VideoProfile> capturedProfile = ArgumentCaptor.forClass(VideoProfile.class);
+        verify(mVideoProvider).sendSessionModifyResponse(capturedProfile.capture());
+        assertEquals(VideoProfile.STATE_AUDIO_ONLY, capturedProfile.getValue().getVideoState());
+    }
+
+    /**
+     * Tests the case where we receive a request to upgrade to video and video is supported.
+     * @throws Exception
+     */
+    @SmallTest
+    @Test
+    public void testReceiveUpgradeRequestWhenVideoIsSupported() throws Exception {
+        // Given a call which supports video at the phone account level, and is currently marked as
+        // supporting video locally.
+        when(mCall.isLocallyVideoCapable()).thenReturn(true);
+        when(mCall.isVideoCallingSupportedByPhoneAccount()).thenReturn(true);
+
+        // Simulate receiving a request to upgrade to video.
+        mVideoProviderProxy.getVideoCallListenerBinder().receiveSessionModifyRequest(
+                new VideoProfile(VideoProfile.STATE_BIDIRECTIONAL));
+
+        // Ensure it gets proxied back to the caller.
+
+        ArgumentCaptor<VideoProfile> capturedProfile = ArgumentCaptor.forClass(VideoProfile.class);
+        verify(mListener).onSessionModifyRequestReceived(any(), capturedProfile.capture());
+        assertEquals(VideoProfile.STATE_BIDIRECTIONAL, capturedProfile.getValue().getVideoState());
+    }
+}
diff --git a/tests/src/com/android/server/telecom/tests/VideoProviderTest.java b/tests/src/com/android/server/telecom/tests/VideoProviderTest.java
index 75dc36f..eacecf9 100644
--- a/tests/src/com/android/server/telecom/tests/VideoProviderTest.java
+++ b/tests/src/com/android/server/telecom/tests/VideoProviderTest.java
@@ -34,6 +34,7 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.os.UserHandle;
+import android.telecom.Connection;
 import android.telecom.Connection.VideoProvider;
 import android.telecom.InCallService;
 import android.telecom.InCallService.VideoCall;
@@ -96,7 +97,8 @@
         super.setUp();
         mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
         mAppOpsManager = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
-
+        mConnectionServiceFixtureA.mConnectionServiceDelegate.mCapabilities
+                |= Connection.CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL;
         mCallIds = startAndMakeActiveOutgoingCall(
                 "650-555-1212",
                 mPhoneAccountA0.getAccountHandle(),