User Interaction for Call Redirection service in Telecom

- Show UX if Telecom gets user-defined service's timeout.
When Telecom does not receive a response from user-defined service, Telecom
will cancel the outgoing call.

- Show UX if Telecom gets confirmFirst request from user-defined service's
timeout.
When Telecom's Call Redirection service receives the redirected call
from user's application, it may show an UX to confirm with users if they
want to process with the call.

Design doc: go/telecom-outflow-ux

Bug: 64959558
Test: Treehugger; Manually Telecom test app and see two types of UX
 sucessfully show up
Change-Id: Ida0d46967bcaab3e4b621a5e635a0ac04c717033
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 6aac49e..d0b6fe1 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -235,6 +235,8 @@
                 <action android:name="com.android.server.telecom.ACTION_REJECT_FROM_NOTIFICATION" />
                 <action android:name="com.android.server.telecom.PROCEED_WITH_CALL" />
                 <action android:name="com.android.server.telecom.CANCEL_CALL" />
+                <action android:name="com.android.server.telecom.PROCEED_WITH_REDIRECTED_CALL" />
+                <action android:name="com.android.server.telecom.CANCEL_REDIRECTED_CALL" />
             </intent-filter>
         </receiver>
 
@@ -285,6 +287,22 @@
                 android:process=":ui">
         </activity>
 
+        <activity android:name=".ui.CallRedirectionConfirmDialogActivity"
+                  android:configChanges="orientation|screenSize|keyboardHidden"
+                  android:excludeFromRecents="true"
+                  android:launchMode="singleInstance"
+                  android:theme="@style/Theme.Telecomm.Transparent"
+                  android:process=":ui">
+        </activity>
+
+        <activity android:name=".ui.CallRedirectionTimeoutDialogActivity"
+                  android:configChanges="orientation|screenSize|keyboardHidden"
+                  android:excludeFromRecents="true"
+                  android:launchMode="singleInstance"
+                  android:theme="@style/Theme.Telecomm.Transparent"
+                  android:process=":ui">
+        </activity>
+
         <activity android:name=".components.ChangeDefaultDialerDialog"
                   android:label="@string/change_default_dialer_dialog_title"
                   android:excludeFromRecents="true"
diff --git a/res/values/strings.xml b/res/values/strings.xml
index eff68a0..de86f01 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -275,6 +275,12 @@
          ongoing call in the app "other_app". -->
     <string name="alert_outgoing_call">Placing this call will end your <xliff:g id="other_app">%1$s</xliff:g> call.</string>
 
+    <!-- Alert dialog content used to ask the user to confirm if they want to place a new outgoing call redirected by the app "other_app". -->
+    <string name="alert_redirect_outgoing_call">Allow <xliff:g id="other_app">%1$s</xliff:g> to place call using a different number or account.</string>
+
+    <!-- Alert dialog content used to tell the user the call is canceled because no response from the call redirection app "other_app". -->
+    <string name="alert_redirect_outgoing_call_timeout">Call can\'t be placed by <xliff:g id="other_app">%1$s</xliff:g>. Try using a different call redirecting app or contacting the developer for help.</string>
+
     <!-- The name of a feature available under the Call settings. -->
     <string name="phone_settings_call_blocking_txt">Call Blocking</string>
     <!-- Call type to be blocked. See the explanatory text "phone_settings_number_not_in_contact_summary_txt". -->
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index aa4cf08..2f95acd 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -66,6 +66,7 @@
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
 import android.util.Pair;
+import android.widget.Toast;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.telephony.AsyncEmergencyContactNotifier;
@@ -82,8 +83,11 @@
 import com.android.server.telecom.callfiltering.CallScreeningServiceController;
 import com.android.server.telecom.callfiltering.DirectToVoicemailCallFilter;
 import com.android.server.telecom.callfiltering.IncomingCallFilter;
+import com.android.server.telecom.callredirection.CallRedirectionProcessor;
 import com.android.server.telecom.components.ErrorDialogActivity;
 import com.android.server.telecom.settings.BlockedNumbersUtil;
+import com.android.server.telecom.ui.CallRedirectionConfirmDialogActivity;
+import com.android.server.telecom.ui.CallRedirectionTimeoutDialogActivity;
 import com.android.server.telecom.ui.ConfirmCallDialogActivity;
 import com.android.server.telecom.ui.IncomingCallNotifier;
 
@@ -264,6 +268,18 @@
      * Used by {@link #startCallConfirmation}.
      */
     private Call mPendingCall;
+    /**
+     * Cached latest pending redirected call which requires user-intervention in order to be placed.
+     * Used by {@link #onCallRedirectionComplete}.
+     */
+    private Call mPendingRedirectedOutgoingCall;
+    /**
+     * Cached latest pending redirected call information which require user-intervention in order
+     * to be placed. Used by {@link #onCallRedirectionComplete}.
+     */
+    private final Map<String, Runnable> mPendingRedirectionOutgoingCallInfo =
+            new ConcurrentHashMap<>();
+
     private CompletableFuture<Call> mPendingCallConfirm;
     private CompletableFuture<Pair<Call, PhoneAccountHandle>> mPendingAccountSelection;
 
@@ -1691,11 +1707,24 @@
         boolean endEarly = false;
         String disconnectReason = "";
 
+        String callRedirectionApp = mRoleManagerAdapter.getDefaultCallRedirectionApp();
+
         if (shouldCancelCall) {
             Log.w(this, "onCallRedirectionComplete: call is canceled");
             endEarly = true;
             disconnectReason = "Canceled from Call Redirection Service";
-            // TODO show UI uiAction is CallRedirectionProcessor#UI_TYPE_USER_DEFINED_TIMEOUT
+            // Show UX when user-defined call redirection service does not response; the UX
+            // is not needed to show if the call is disconnected (e.g. by the user)
+            if (uiAction.equals(CallRedirectionProcessor.UI_TYPE_USER_DEFINED_TIMEOUT)
+                    && !call.isDisconnected()) {
+                Intent timeoutIntent = new Intent(mContext,
+                        CallRedirectionTimeoutDialogActivity.class);
+                timeoutIntent.putExtra(
+                        CallRedirectionTimeoutDialogActivity.EXTRA_REDIRECTION_APP_NAME,
+                        mRoleManagerAdapter.getApplicationLabelForPackageName(callRedirectionApp));
+                timeoutIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+                mContext.startActivityAsUser(timeoutIntent, UserHandle.CURRENT);
+            }
         } else if (handle == null) {
             Log.w(this, "onCallRedirectionComplete: handle is null");
             endEarly = true;
@@ -1725,10 +1754,66 @@
             return;
         }
 
-        // TODO show UI uiAction is CallRedirectionProcessor#UI_TYPE_USER_DEFINED_ASK_FOR_CONFIRM
+        if (uiAction.equals(CallRedirectionProcessor.UI_TYPE_USER_DEFINED_ASK_FOR_CONFIRM)) {
+            Log.addEvent(call, LogUtils.Events.REDIRECTION_USER_CONFIRMATION);
+            mPendingRedirectedOutgoingCall = call;
 
-        call.setTargetPhoneAccount(phoneAccountHandle);
-        placeOutgoingCall(call, handle, gatewayInfo, speakerphoneOn, videoState);
+            mPendingRedirectionOutgoingCallInfo.put(call.getId(),
+                    new Runnable("CM.oCRC", mLock) {
+                        @Override
+                        public void loggedRun() {
+                            Log.addEvent(call, LogUtils.Events.REDIRECTION_USER_CONFIRMED);
+                            call.setTargetPhoneAccount(phoneAccountHandle);
+                            placeOutgoingCall(call, handle, gatewayInfo, speakerphoneOn,
+                                    videoState);
+                        }
+                    });
+
+            Log.i(this, "onCallRedirectionComplete: UI_TYPE_USER_DEFINED_ASK_FOR_CONFIRM "
+                    + "callId=%s, callRedirectionAppName=%s",
+                    call.getId(), callRedirectionApp);
+
+            Intent confirmIntent = new Intent(mContext,
+                    CallRedirectionConfirmDialogActivity.class);
+            confirmIntent.putExtra(
+                    CallRedirectionConfirmDialogActivity.EXTRA_REDIRECTION_OUTGOING_CALL_ID,
+                    call.getId());
+            confirmIntent.putExtra(CallRedirectionConfirmDialogActivity.EXTRA_REDIRECTION_APP_NAME,
+                    mRoleManagerAdapter.getApplicationLabelForPackageName(callRedirectionApp));
+            confirmIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            mContext.startActivityAsUser(confirmIntent, UserHandle.CURRENT);
+        } else {
+            call.setTargetPhoneAccount(phoneAccountHandle);
+            placeOutgoingCall(call, handle, gatewayInfo, speakerphoneOn, videoState);
+        }
+    }
+
+    public void placeRedirectedOutgoingCallAfterUserInteraction(String callId) {
+        Log.i(this, "placeRedirectedOutgoingCallAfterUserInteraction for Call ID %s", callId);
+        if (mPendingRedirectedOutgoingCall != null && mPendingRedirectedOutgoingCall.getId()
+                .equals(callId)) {
+            mHandler.post(mPendingRedirectionOutgoingCallInfo.get(callId).prepare());
+            mPendingRedirectedOutgoingCall = null;
+            mPendingRedirectionOutgoingCallInfo.remove(callId);
+        } else {
+            Log.w(this, "placeRedirectedOutgoingCallAfterUserInteraction for non-matched Call ID "
+                    + " %s with handle %s and phoneAccountHandle %s", callId);
+        }
+    }
+
+    public void cancelRedirectedOutgoingCallAfterUserInteraction(String callId) {
+        Log.i(this, "cancelRedirectedOutgoingCallAfterUserInteraction for Call ID %s", callId);
+        if (mPendingRedirectedOutgoingCall != null && mPendingRedirectedOutgoingCall.getId()
+                .equals(callId)) {
+            Log.addEvent(mPendingRedirectedOutgoingCall,
+                    LogUtils.Events.REDIRECTION_USER_CANCELLED);
+            mPendingRedirectedOutgoingCall.disconnect("User canceled the redirected call.");
+            mPendingRedirectedOutgoingCall = null;
+            mPendingRedirectionOutgoingCallInfo.remove(callId);
+        } else {
+            Log.w(this, "cancelRedirectedOutgoingCallAfterUserInteraction for non-matched Call"
+                    + " ID ", callId);
+        }
     }
 
     /**
diff --git a/src/com/android/server/telecom/LogUtils.java b/src/com/android/server/telecom/LogUtils.java
index bf8dba7..3cc1f8f 100644
--- a/src/com/android/server/telecom/LogUtils.java
+++ b/src/com/android/server/telecom/LogUtils.java
@@ -148,6 +148,9 @@
         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 final String REDIRECTION_USER_CONFIRMATION = "REDIRECTION_USER_CONFIRMATION";
+        public static final String REDIRECTION_USER_CONFIRMED = "REDIRECTION_USER_CONFIRMED";
+        public static final String REDIRECTION_USER_CANCELLED = "REDIRECTION_USER_CANCELLED";
 
         public static class Timings {
             public static final String ACCEPT_TIMING = "accept";
diff --git a/src/com/android/server/telecom/RoleManagerAdapter.java b/src/com/android/server/telecom/RoleManagerAdapter.java
index f6cdc6c..b7e347e 100644
--- a/src/com/android/server/telecom/RoleManagerAdapter.java
+++ b/src/com/android/server/telecom/RoleManagerAdapter.java
@@ -94,4 +94,11 @@
      * @param currentUserHandle The new user handle.
      */
     void setCurrentUserHandle(UserHandle currentUserHandle);
+
+    /**
+     * Returns the application label that corresponds to the given package name.
+     * @param packageName A valid package name.
+     * @return Application label for the given package name, or null if not found.
+     */
+    String getApplicationLabelForPackageName(String packageName);
 }
diff --git a/src/com/android/server/telecom/RoleManagerAdapterImpl.java b/src/com/android/server/telecom/RoleManagerAdapterImpl.java
index 51fe9b4..1200aa0 100644
--- a/src/com/android/server/telecom/RoleManagerAdapterImpl.java
+++ b/src/com/android/server/telecom/RoleManagerAdapterImpl.java
@@ -17,7 +17,11 @@
 package com.android.server.telecom;
 
 import android.app.role.RoleManager;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
 import android.os.UserHandle;
+import android.telecom.Log;
 
 import com.android.internal.util.IndentingPrintWriter;
 
@@ -35,10 +39,12 @@
     private String mOverrideDefaultCallScreeningApp = null;
     private String mOverrideDefaultCarModeApp = null;
     private List<String> mOverrideCallCompanionApps = new ArrayList<>();
+    private Context mContext;
     private RoleManager mRoleManager;
     private UserHandle mCurrentUserHandle;
 
-    public RoleManagerAdapterImpl(RoleManager roleManager) {
+    public RoleManagerAdapterImpl(Context context, RoleManager roleManager) {
+        mContext = context;
         mRoleManager = roleManager;
     }
 
@@ -136,6 +142,29 @@
     }
 
     /**
+     * Returns the application label that corresponds to the given package name
+     *
+     * @param packageName A valid package name.
+     *
+     * @return Application label for the given package name, or null if not found.
+     */
+    @Override
+    public String getApplicationLabelForPackageName(String packageName) {
+        PackageManager pm = mContext.getPackageManager();
+        ApplicationInfo info = null;
+        try {
+            info = pm.getApplicationInfo(packageName, 0);
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.d(this, "Application info not found for packageName " + packageName);
+        }
+        if (info == null) {
+            return packageName;
+        } else {
+            return info.loadLabel(pm).toString();
+        }
+    }
+
+    /**
      * Dumps the state of the {@link InCallController}.
      *
      * @param pw The {@code IndentingPrintWriter} to write the state to.
diff --git a/src/com/android/server/telecom/TelecomBroadcastIntentProcessor.java b/src/com/android/server/telecom/TelecomBroadcastIntentProcessor.java
index a51ef73..e5546b3 100644
--- a/src/com/android/server/telecom/TelecomBroadcastIntentProcessor.java
+++ b/src/com/android/server/telecom/TelecomBroadcastIntentProcessor.java
@@ -21,6 +21,8 @@
 import android.os.UserHandle;
 import android.telecom.Log;
 
+import android.telecom.VideoProfile;
+import com.android.server.telecom.ui.CallRedirectionConfirmDialogActivity;
 import com.android.server.telecom.ui.ConfirmCallDialogActivity;
 
 public final class TelecomBroadcastIntentProcessor {
@@ -64,6 +66,20 @@
     public static final String ACTION_CANCEL_CALL =
             "com.android.server.telecom.CANCEL_CALL";
 
+    /**
+     * The action used to proceed with a redirected call being confirmed via
+     * {@link com.android.server.telecom.ui.CallRedirectionConfirmDialogActivity}.
+     */
+    public static final String ACTION_PLACE_REDIRECTED_CALL =
+            "com.android.server.telecom.PROCEED_WITH_REDIRECTED_CALL";
+
+    /**
+     * The action used to cancel a redirected call being confirmed via
+     * {@link com.android.server.telecom.ui.CallRedirectionConfirmDialogActivity}.
+     */
+    public static final String ACTION_CANCEL_REDIRECTED_CALL =
+            "com.android.server.telecom.CANCEL_REDIRECTED_CALL";
+
     public static final String EXTRA_USERHANDLE = "userhandle";
 
     private final Context mContext;
@@ -155,6 +171,25 @@
             } finally {
                 Log.endSession();
             }
+        } else if (ACTION_PLACE_REDIRECTED_CALL.equals(action)) {
+            Log.startSession("TBIP.aPRC");
+            try {
+                mCallsManager.placeRedirectedOutgoingCallAfterUserInteraction(
+                        intent.getStringExtra(CallRedirectionConfirmDialogActivity
+                                .EXTRA_REDIRECTION_OUTGOING_CALL_ID));
+            } finally {
+                Log.endSession();
+            }
+        } else if (ACTION_CANCEL_REDIRECTED_CALL.equals(action)) {
+            Log.startSession("TBIP.aCRC");
+            try {
+                mCallsManager.cancelRedirectedOutgoingCallAfterUserInteraction(
+                        intent.getStringExtra(CallRedirectionConfirmDialogActivity
+                                .EXTRA_REDIRECTION_OUTGOING_CALL_ID)
+                );
+            } finally {
+                Log.endSession();
+            }
         }
     }
 
diff --git a/src/com/android/server/telecom/callredirection/CallRedirectionProcessorHelper.java b/src/com/android/server/telecom/callredirection/CallRedirectionProcessorHelper.java
index 5e3f0da..9f9cd28 100644
--- a/src/com/android/server/telecom/callredirection/CallRedirectionProcessorHelper.java
+++ b/src/com/android/server/telecom/callredirection/CallRedirectionProcessorHelper.java
@@ -25,7 +25,6 @@
 import android.net.Uri;
 import android.os.Binder;
 import android.os.PersistableBundle;
-import android.provider.Settings;
 import android.telecom.CallRedirectionService;
 import android.telecom.Log;
 import android.telecom.PhoneAccountHandle;
@@ -56,19 +55,15 @@
     }
 
     @VisibleForTesting
-    // TODO integarte with RoleManager functions
     public ComponentName getUserDefinedCallRedirectionService() {
-        String componentNameString = Settings.Secure.getStringForUser(
-                mContext.getContentResolver(),
-                Settings.Secure.CALL_REDIRECTION_DEFAULT_APPLICATION,
-                mCallsManager.getCurrentUserHandle().getIdentifier());
-        if (TextUtils.isEmpty(componentNameString)) {
-            Log.i(this, "Default user-defined call redirection is empty. Not performing call"
-                    + " redirection.");
+        String packageName = mCallsManager.getRoleManagerAdapter().getDefaultCallRedirectionApp();
+        if (TextUtils.isEmpty(packageName)) {
+            Log.i(this, "PackageName is empty. Not performing user-defined call redirection.");
             return null;
         }
-        return getComponentName(componentNameString,
-                CallRedirectionProcessor.SERVICE_TYPE_USER_DEFINED);
+        Intent intent = new Intent(CallRedirectionService.SERVICE_INTERFACE)
+                .setPackage(packageName);
+        return getComponentName(intent, CallRedirectionProcessor.SERVICE_TYPE_CARRIER);
     }
 
     @VisibleForTesting
@@ -92,11 +87,6 @@
             Log.i(this, "Cannot get carrier componentNameString.");
             return null;
         }
-        return getComponentName(componentNameString,
-                CallRedirectionProcessor.SERVICE_TYPE_CARRIER);
-    }
-
-    protected ComponentName getComponentName(String componentNameString, String serviceType) {
         ComponentName componentName = ComponentName.unflattenFromString(componentNameString);
         if (componentName == null) {
             Log.w(this, "ComponentName is null from string: " + componentNameString);
@@ -104,6 +94,10 @@
         }
         Intent intent = new Intent(CallRedirectionService.SERVICE_INTERFACE);
         intent.setComponent(componentName);
+        return getComponentName(intent, CallRedirectionProcessor.SERVICE_TYPE_CARRIER);
+    }
+
+    protected ComponentName getComponentName(Intent intent, String serviceType) {
         List<ResolveInfo> entries = mContext.getPackageManager().queryIntentServicesAsUser(
                 intent, 0, mCallsManager.getCurrentUserHandle().getIdentifier());
         if (entries.isEmpty()) {
@@ -134,7 +128,7 @@
             Log.w(this, "App Ops does not allow " + entry.serviceInfo.packageName);
             return null;
         }
-        return componentName;
+        return new ComponentName(entry.serviceInfo.packageName, entry.serviceInfo.name);
     }
 
     /**
diff --git a/src/com/android/server/telecom/components/TelecomService.java b/src/com/android/server/telecom/components/TelecomService.java
index b331d5c..956274a 100644
--- a/src/com/android/server/telecom/components/TelecomService.java
+++ b/src/com/android/server/telecom/components/TelecomService.java
@@ -189,7 +189,7 @@
                                     return SystemClock.elapsedRealtime();
                                 }
                             },
-                            new RoleManagerAdapterImpl(
+                            new RoleManagerAdapterImpl(context,
                                     (RoleManager) context.getSystemService(Context.ROLE_SERVICE))));
         }
         if (BluetoothAdapter.getDefaultAdapter() != null) {
diff --git a/src/com/android/server/telecom/ui/CallRedirectionConfirmDialogActivity.java b/src/com/android/server/telecom/ui/CallRedirectionConfirmDialogActivity.java
new file mode 100644
index 0000000..562c433
--- /dev/null
+++ b/src/com/android/server/telecom/ui/CallRedirectionConfirmDialogActivity.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2019 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.ui;
+
+import com.android.server.telecom.R;
+import com.android.server.telecom.TelecomBroadcastIntentProcessor;
+import com.android.server.telecom.components.TelecomBroadcastReceiver;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.telecom.Log;
+
+/**
+ * Dialog activity used when there is an ongoing call redirected by the call redirection service.
+ * The dialog prompts the user to see if they want to place the redirected outgoing call.
+ */
+public class CallRedirectionConfirmDialogActivity extends Activity {
+    public static final String EXTRA_REDIRECTION_OUTGOING_CALL_ID =
+            "android.telecom.extra.REDIRECTION_OUTGOING_CALL_ID";
+    public static final String EXTRA_REDIRECTION_APP_NAME =
+            "android.telecom.extra.REDIRECTION_APP_NAME";
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        Log.i(this, "CallRedirectionConfirmDialogActivity onCreate.");
+        final CharSequence redirectionAppName = getIntent().getStringExtra(
+                EXTRA_REDIRECTION_APP_NAME);
+        showDialog(redirectionAppName);
+    }
+
+    private void showDialog(final CharSequence redirectionAppName) {
+        Log.i(this, "showDialog: confirming redirection with %s", redirectionAppName);
+        CharSequence message = getString(
+                R.string.alert_redirect_outgoing_call, redirectionAppName);
+        final AlertDialog confirmDialog = new AlertDialog.Builder(this)
+                .setMessage(message)
+                .setPositiveButton("Call", new DialogInterface.OnClickListener() {
+                    @Override
+                    public void onClick(DialogInterface dialog, int which) {
+                        Intent proceedWithRedirectedCall = new Intent(
+                                TelecomBroadcastIntentProcessor
+                                        .ACTION_PLACE_REDIRECTED_CALL, null,
+                                CallRedirectionConfirmDialogActivity.this,
+                                TelecomBroadcastReceiver.class);
+                        proceedWithRedirectedCall.putExtra(EXTRA_REDIRECTION_OUTGOING_CALL_ID,
+                                getIntent().getStringExtra(EXTRA_REDIRECTION_OUTGOING_CALL_ID));
+                        sendBroadcast(proceedWithRedirectedCall);
+                        dialog.dismiss();
+                        finish();
+                    }
+                })
+                .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
+                    @Override
+                    public void onClick(DialogInterface dialog, int which) {
+                        Intent cancelRedirectedCall = new Intent(
+                                TelecomBroadcastIntentProcessor.ACTION_CANCEL_REDIRECTED_CALL,
+                                null, CallRedirectionConfirmDialogActivity.this,
+                                TelecomBroadcastReceiver.class);
+                        cancelRedirectedCall.putExtra(EXTRA_REDIRECTION_OUTGOING_CALL_ID,
+                                getIntent().getStringExtra(EXTRA_REDIRECTION_OUTGOING_CALL_ID));
+                        sendBroadcast(cancelRedirectedCall);
+                        dialog.dismiss();
+                        finish();
+                    }
+                })
+                .setOnCancelListener(new DialogInterface.OnCancelListener() {
+                    @Override
+                    public void onCancel(DialogInterface dialog) {
+                        Intent cancelRedirectedCall = new Intent(
+                                TelecomBroadcastIntentProcessor.ACTION_CANCEL_REDIRECTED_CALL,
+                                null, CallRedirectionConfirmDialogActivity.this,
+                                TelecomBroadcastReceiver.class);
+                        cancelRedirectedCall.putExtra(EXTRA_REDIRECTION_OUTGOING_CALL_ID,
+                                getIntent().getStringExtra(EXTRA_REDIRECTION_OUTGOING_CALL_ID));
+                        sendBroadcast(cancelRedirectedCall);
+                        dialog.dismiss();
+                        finish();
+                    }
+                })
+                .create();
+
+        confirmDialog.show();
+    }
+}
diff --git a/src/com/android/server/telecom/ui/CallRedirectionTimeoutDialogActivity.java b/src/com/android/server/telecom/ui/CallRedirectionTimeoutDialogActivity.java
new file mode 100644
index 0000000..5aa80c6
--- /dev/null
+++ b/src/com/android/server/telecom/ui/CallRedirectionTimeoutDialogActivity.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2019 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.ui;
+
+import com.android.server.telecom.R;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.telecom.Log;
+
+/**
+ * Dialog activity used when there is an ongoing call redirected by the call redirection service.
+ * The dialog prompts the user to inform the redirected outgoing call is canceled due to timeout.
+ */
+public class CallRedirectionTimeoutDialogActivity extends Activity {
+    public static final String EXTRA_REDIRECTION_APP_NAME =
+            "android.telecom.extra.REDIRECTION_APP_NAME";
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        Log.i(this, "CallRedirectionTimeoutDialogActivity onCreate.");
+        final CharSequence redirectionAppName = getIntent().getStringExtra(
+                EXTRA_REDIRECTION_APP_NAME);
+        showDialog(redirectionAppName);
+    }
+
+    private void showDialog(final CharSequence redirectionAppName) {
+        Log.i(this, "showDialog: timeout redirection with %s", redirectionAppName);
+        CharSequence message = getString(
+                R.string.alert_redirect_outgoing_call_timeout, redirectionAppName);
+        final AlertDialog errorDialog = new AlertDialog.Builder(this)
+                .setMessage(message)
+                .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+                    @Override
+                    public void onClick(DialogInterface dialog, int which) {
+                        dialog.dismiss();
+                        finish();
+                    }
+                })
+                .setOnCancelListener(new DialogInterface.OnCancelListener() {
+                    @Override
+                    public void onCancel(DialogInterface dialog) {
+                        dialog.dismiss();
+                        finish();
+                    }
+                })
+                .create();
+
+        errorDialog.show();
+    }
+}
diff --git a/testapps/AndroidManifest.xml b/testapps/AndroidManifest.xml
index 9cb1992..b3ecd27 100644
--- a/testapps/AndroidManifest.xml
+++ b/testapps/AndroidManifest.xml
@@ -252,5 +252,19 @@
                   android:excludeFromRecents="true"
                   android:launchMode="singleInstance">
         </activity>
+
+        <service
+                android:name=".TestCallRedirectionService"
+                android:permission="android.permission.BIND_CALL_REDIRECTION_SERVICE">
+            <intent-filter>
+                <action android:name="android.telecom.CallRedirectionService"/>
+            </intent-filter>
+        </service>
+
+        <activity android:name=".CallRedirectionActivity"
+                  android:configChanges="orientation|screenSize|keyboardHidden"
+                  android:excludeFromRecents="true"
+                  android:launchMode="singleInstance">
+        </activity>
     </application>
 </manifest>
diff --git a/testapps/src/com/android/server/telecom/testapps/CallRedirectionActivity.java b/testapps/src/com/android/server/telecom/testapps/CallRedirectionActivity.java
new file mode 100644
index 0000000..044e271
--- /dev/null
+++ b/testapps/src/com/android/server/telecom/testapps/CallRedirectionActivity.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2019 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.app.AlertDialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.telecom.Log;
+
+public class CallRedirectionActivity extends Activity {
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        Log.i(this, "onCreate: CallRedirectionActivity");
+
+        AlertDialog alertDialog = new AlertDialog.Builder(this)
+                .setTitle("Test Call Redirection")
+                .setMessage("Decision for call redirection?")
+                .setNegativeButton("Timeout", new DialogInterface.OnClickListener() {
+                    @Override
+                    public void onClick(DialogInterface dialog, int which) {
+                        // No action is needed for timeout
+                        finish();
+                    }
+                })
+                .setPositiveButton("Redirect and confirm", new DialogInterface.OnClickListener() {
+                    @Override
+                    public void onClick(DialogInterface dialog, int which) {
+                        if (TestCallRedirectionService.getInstance() != null) {
+                            TestCallRedirectionService.getInstance().tryRedirectCallAndAskToConfirm();
+                        }
+                        finish();
+                    }
+                }).create();
+        alertDialog.show();
+    }
+}
\ No newline at end of file
diff --git a/testapps/src/com/android/server/telecom/testapps/TestCallRedirectionService.java b/testapps/src/com/android/server/telecom/testapps/TestCallRedirectionService.java
new file mode 100644
index 0000000..b7a033c
--- /dev/null
+++ b/testapps/src/com/android/server/telecom/testapps/TestCallRedirectionService.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2019 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.annotation.NonNull;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.net.Uri;
+import android.telecom.CallRedirectionService;
+import android.telecom.Log;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+
+public class TestCallRedirectionService extends CallRedirectionService {
+
+    public static TestCallRedirectionService getInstance() {
+        return sTestCallRedirectionService;
+    }
+
+    private static TestCallRedirectionService sTestCallRedirectionService;
+
+    private static final Uri SAMPLE_HANDLE = Uri.fromParts(PhoneAccount.SCHEME_TEL, "0001112222",
+            null);
+
+    private static final PhoneAccountHandle SAMPLE_PHONE_ACCOUNT = new PhoneAccountHandle(
+            new ComponentName("com.android.server.telecom.testapps",
+                    "com.android.server.telecom.testapps.TestCallRedirectionService"),
+            "TELECOM_TEST_APP_PHONE_ACCOUNT_ID");
+
+    /**
+     * Handles request from the system to redirect an outgoing call.
+     */
+    @Override
+    public void onPlaceCall(@NonNull Uri handle, @NonNull PhoneAccountHandle initialPhoneAccount,
+                            boolean allowInteractiveResponse) {
+        Log.i(this, "onPlaceCall: received call %s", handle);
+        sTestCallRedirectionService = this;
+
+        Intent intent = new Intent(this, CallRedirectionActivity.class);
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        startActivity(intent);
+    }
+
+    public void tryRedirectCallAndAskToConfirm() {
+        // Provide call identification
+        redirectCall(SAMPLE_HANDLE, SAMPLE_PHONE_ACCOUNT, true);
+    }
+}
diff --git a/tests/src/com/android/server/telecom/tests/NewOutgoingCallIntentBroadcasterTest.java b/tests/src/com/android/server/telecom/tests/NewOutgoingCallIntentBroadcasterTest.java
index cfbcfcf..9d363e9 100644
--- a/tests/src/com/android/server/telecom/tests/NewOutgoingCallIntentBroadcasterTest.java
+++ b/tests/src/com/android/server/telecom/tests/NewOutgoingCallIntentBroadcasterTest.java
@@ -37,6 +37,7 @@
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallsManager;
 import com.android.server.telecom.NewOutgoingCallIntentBroadcaster;
+import com.android.server.telecom.RoleManagerAdapter;
 import com.android.server.telecom.PhoneAccountRegistrar;
 import com.android.server.telecom.PhoneNumberUtilsAdapter;
 import com.android.server.telecom.PhoneNumberUtilsAdapterImpl;
@@ -84,6 +85,7 @@
     @Mock private SystemStateHelper mSystemStateHelper;
     @Mock private UserHandle mUserHandle;
     @Mock private PhoneAccountRegistrar mPhoneAccountRegistrar;
+    @Mock private RoleManagerAdapter mRoleManagerAdapter;
 
     private PhoneNumberUtilsAdapter mPhoneNumberUtilsAdapterSpy;
 
@@ -98,6 +100,7 @@
         when(mCallsManager.getSystemStateHelper()).thenReturn(mSystemStateHelper);
         when(mCallsManager.getCurrentUserHandle()).thenReturn(mUserHandle);
         when(mCallsManager.getPhoneAccountRegistrar()).thenReturn(mPhoneAccountRegistrar);
+        when(mCallsManager.getRoleManagerAdapter()).thenReturn(mRoleManagerAdapter);
         when(mPhoneAccountRegistrar.getSubscriptionIdForPhoneAccount(
                 any(PhoneAccountHandle.class))).thenReturn(-1);
         when(mSystemStateHelper.isCarMode()).thenReturn(false);