Support notifying Telecom when a call is ended due to new emergency call

When there is a new outgoing emergency call, the telephony
ConnectionService may choose to end existing calls based on
carrier configurations. This change introduces a new DisconnectCause
reason, which allws Telecom to pop up a notification when calls
in this fasion.

Also restricts the possible PhoneAccounts that can have simultaneous
non-emergency+emergency calls to those with the
CAPABILITY_PLACE_EMERGENCY_CALLS capability to make sure 3rd party
PhoneAccounts do not perform badly and affect the outgoing emergency
call.

Bug: 138741228
Test: atest TelecomUnitTests; atest CtsTelecomTestCases
Change-Id: I1e1836367080e8235ebb4818a49f2aa26ad1d7a0
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 2c6a84a..93c5782 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -48,6 +48,14 @@
          [CHAR LIMIT=18] -->
     <string name="notification_missedCall_message">Message</string>
 
+    <!-- Disconnected call notification label, used when the call has been automatically
+         disconnected by telecom to make room for a higher priority call, such as an emergency
+         call.-->
+    <string name="notification_disconnectedCall_title">Disconnected call</string>
+    <!-- Body of the notification presented when an ongoing call is disconnected in favor of placing
+         an emergency call. This is required by some carriers. [CHAR LIMIT=NONE] -->
+    <string name="notification_disconnectedCall_body">The call to <xliff:g id="caller">%s</xliff:g> has been disconnected due to an emergency call being placed.</string>
+
     <!-- Title for the persistent notification presented when an app has requested that a call
          be put into the background so that the app can access the audio from the call
          [CHAR LIMIT=40] -->
@@ -290,6 +298,8 @@
     <string name="notification_channel_call_blocking">Call Blocking</string>
     <!-- Notification channel name for a channel containing background call notifications. -->
     <string name="notification_channel_background_calls">Background calls</string>
+    <!-- Notification channel name for a channel containing disconnected call notifications. -->
+    <string name="notification_channel_disconnected_calls">Disconnected calls</string>
 
     <!-- Alert dialog content used to inform the user that placing a new outgoing call will end the
          ongoing call in the app "other_app". -->
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index e48cc84..3d04e50 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -58,6 +58,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.telecom.IVideoProvider;
 import com.android.internal.util.Preconditions;
+import com.android.server.telecom.ui.DisconnectedCallNotifier;
 import com.android.server.telecom.ui.ToastFactory;
 
 import java.io.IOException;
@@ -366,9 +367,10 @@
 
     /**
      * Override the disconnect cause set by the connection service. Used for audio processing and
-     * simulated ringing calls.
+     * simulated ringing calls as well as the condition when an emergency call is ended due to
+     * an emergency call being placed.
      */
-    private int mOverrideDisconnectCauseCode = DisconnectCause.UNKNOWN;
+    private DisconnectCause mOverrideDisconnectCause = new DisconnectCause(DisconnectCause.UNKNOWN);
 
     private Bundle mIntentExtras = new Bundle();
 
@@ -1166,23 +1168,30 @@
     }
 
     /**
-     * @param disconnectCause The reason for the disconnection, represented by
-     *         {@link android.telecom.DisconnectCause}.
+     * @param cause The reason for the disconnection, represented by
+     * {@link android.telecom.DisconnectCause}.
      */
-    public void setDisconnectCause(DisconnectCause disconnectCause) {
+    public void setDisconnectCause(DisconnectCause cause) {
         // TODO: Consider combining this method with a setDisconnected() method that is totally
         // separate from setState.
-        if (mOverrideDisconnectCauseCode != DisconnectCause.UNKNOWN) {
-            disconnectCause = new DisconnectCause(mOverrideDisconnectCauseCode,
-                    disconnectCause.getLabel(), disconnectCause.getDescription(),
-                    disconnectCause.getReason(), disconnectCause.getTone());
+
+        if (mOverrideDisconnectCause.getCode() != DisconnectCause.UNKNOWN) {
+            cause = new DisconnectCause(mOverrideDisconnectCause.getCode(),
+                    TextUtils.isEmpty(mOverrideDisconnectCause.getLabel()) ?
+                            cause.getLabel() : mOverrideDisconnectCause.getLabel(),
+                    (mOverrideDisconnectCause.getDescription() == null) ?
+                            cause.getDescription() :mOverrideDisconnectCause.getDescription(),
+                    TextUtils.isEmpty(mOverrideDisconnectCause.getReason()) ?
+                            cause.getReason() : mOverrideDisconnectCause.getReason(),
+                    (mOverrideDisconnectCause.getTone() == 0) ?
+                            cause.getTone() : mOverrideDisconnectCause.getTone());
         }
-        mAnalytics.setCallDisconnectCause(disconnectCause);
-        mDisconnectCause = disconnectCause;
+        mAnalytics.setCallDisconnectCause(cause);
+        mDisconnectCause = cause;
     }
 
-    public void setOverrideDisconnectCauseCode(int overrideDisconnectCauseCode) {
-        mOverrideDisconnectCauseCode = overrideDisconnectCauseCode;
+    public void setOverrideDisconnectCauseCode(DisconnectCause overrideDisconnectCause) {
+        mOverrideDisconnectCause = overrideDisconnectCause;
     }
 
 
@@ -1962,12 +1971,12 @@
             abort(disconnectionTimeout);
         } else if (mState != CallState.ABORTED && mState != CallState.DISCONNECTED) {
             if (mState == CallState.AUDIO_PROCESSING && !hasGoneActiveBefore()) {
-                mOverrideDisconnectCauseCode = DisconnectCause.REJECTED;
+                setOverrideDisconnectCauseCode(new DisconnectCause(DisconnectCause.REJECTED));
             } else if (mState == CallState.SIMULATED_RINGING) {
                 // This is the case where the dialer calls disconnect() because the call timed out
                 // or an emergency call was dialed while in this state.
                 // Override the disconnect cause to MISSED
-                mOverrideDisconnectCauseCode = DisconnectCause.MISSED;
+                setOverrideDisconnectCauseCode(new DisconnectCause(DisconnectCause.MISSED));
             }
             if (mConnectionService == null) {
                 Log.e(this, new Exception(), "disconnect() request on a call without a"
@@ -2128,7 +2137,7 @@
             // This handles the case where the user manually rejects a call that's in simulated
             // ringing. Since the call is already active on the connectionservice side, we want to
             // hangup, not reject.
-            mOverrideDisconnectCauseCode = DisconnectCause.REJECTED;
+            setOverrideDisconnectCauseCode(new DisconnectCause(DisconnectCause.REJECTED));
             if (mConnectionService != null) {
                 mConnectionService.disconnect(this);
             } else {
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index e136950..54adfc5 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -94,6 +94,7 @@
 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.DisconnectedCallNotifier;
 import com.android.server.telecom.ui.IncomingCallNotifier;
 import com.android.server.telecom.ui.ToastFactory;
 
@@ -327,6 +328,7 @@
     private final TelecomSystem.SyncRoot mLock;
     private final PhoneAccountRegistrar mPhoneAccountRegistrar;
     private final MissedCallNotifier mMissedCallNotifier;
+    private final DisconnectedCallNotifier mDisconnectedCallNotifier;
     private IncomingCallNotifier mIncomingCallNotifier;
     private final CallerInfoLookupHelper mCallerInfoLookupHelper;
     private final IncomingCallFilter.Factory mIncomingCallFilterFactory;
@@ -435,6 +437,7 @@
             TelecomSystem.SyncRoot lock,
             CallerInfoLookupHelper callerInfoLookupHelper,
             MissedCallNotifier missedCallNotifier,
+            DisconnectedCallNotifier.Factory disconnectedCallNotifierFactory,
             PhoneAccountRegistrar phoneAccountRegistrar,
             HeadsetMediaButtonFactory headsetMediaButtonFactory,
             ProximitySensorManagerFactory proximitySensorManagerFactory,
@@ -466,6 +469,7 @@
         mPhoneAccountRegistrar = phoneAccountRegistrar;
         mPhoneAccountRegistrar.addListener(mPhoneAccountListener);
         mMissedCallNotifier = missedCallNotifier;
+        mDisconnectedCallNotifier = disconnectedCallNotifierFactory.create(mContext, this);
         StatusBarNotifier statusBarNotifier = new StatusBarNotifier(context, this);
         mWiredHeadsetManager = wiredHeadsetManager;
         mSystemStateHelper = systemStateHelper;
@@ -545,6 +549,7 @@
         mListeners.add(mCallAudioManager);
         mListeners.add(mCallRecordingTonePlayer);
         mListeners.add(missedCallNotifier);
+        mListeners.add(mDisconnectedCallNotifier);
         mListeners.add(mHeadsetMediaButton);
         mListeners.add(mProximitySensorManager);
         mListeners.add(audioProcessingNotification);
@@ -2701,7 +2706,7 @@
                 && disconnectCause.getCode() == DisconnectCause.REMOTE) {
             // If the remote end hangs up while in SIMULATED_RINGING, the call should
             // be marked as missed.
-            call.setOverrideDisconnectCauseCode(DisconnectCause.MISSED);
+            call.setOverrideDisconnectCauseCode(new DisconnectCause(DisconnectCause.MISSED));
         }
         call.setDisconnectCause(disconnectCause);
         setCallState(call, CallState.DISCONNECTED, "disconnected set explicitly");
@@ -3100,6 +3105,14 @@
     }
 
     /**
+     * Retrieves the {@link DisconnectedCallNotifier}
+     * @return The {@link DisconnectedCallNotifier}.
+     */
+    DisconnectedCallNotifier getDisconnectedCallNotifier() {
+        return mDisconnectedCallNotifier;
+    }
+
+    /**
      * Retrieves the {@link MissedCallNotifier}
      * @return The {@link MissedCallNotifier}.
      */
@@ -3628,7 +3641,8 @@
                     // we will try to connect the first outgoing call.
                     call.getAnalytics().setCallIsAdditional(true);
                     outgoingCall.getAnalytics().setCallIsInterrupted(true);
-                    outgoingCall.disconnect();
+                    outgoingCall.disconnect("Disconnecting dialing call in favor of new dialing"
+                            + " emergency call.");
                     return true;
                 }
                 if (outgoingCall.getState() == CallState.SELECT_PHONE_ACCOUNT) {
@@ -3636,7 +3650,8 @@
                     // state, just disconnect it since the user has explicitly started a new call.
                     call.getAnalytics().setCallIsAdditional(true);
                     outgoingCall.getAnalytics().setCallIsInterrupted(true);
-                    outgoingCall.disconnect();
+                    outgoingCall.disconnect("Disconnecting call in SELECT_PHONE_ACCOUNT in favor"
+                            + " of new outgoing call.");
                     return true;
                 }
                 return false;
@@ -3675,6 +3690,23 @@
                         liveCallPhoneAccount);
             }
 
+            // We may not know which PhoneAccount the emergency call will be placed on yet, but if
+            // the liveCall PhoneAccount does not support placing emergency calls, then we know it
+            // will not be that one and we do not want multiple PhoneAccounts active during an
+            // emergency call if possible. Disconnect the active call in favor of the emergency call
+            // instead of trying to hold.
+            if (isEmergency && liveCall.getTargetPhoneAccount() != null) {
+                PhoneAccount pa = mPhoneAccountRegistrar.getPhoneAccountUnchecked(
+                        liveCall.getTargetPhoneAccount());
+                if((pa.getCapabilities() & PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS) == 0) {
+                    liveCall.setOverrideDisconnectCauseCode(new DisconnectCause(
+                            DisconnectCause.LOCAL, DisconnectCause.REASON_EMERGENCY_CALL_PLACED));
+                    liveCall.disconnect("outgoing call does not support emergency calls, "
+                            + "disconnecting.");
+                }
+                return true;
+            }
+
             // First thing, if we are trying to make a call with the same phone account as the live
             // call, then allow it so that the connection service can make its own decision about
             // how to handle the new call relative to the current one.
@@ -3725,7 +3757,8 @@
             } else { // normal incoming ringing call.
                 // Hang up the ringing call to make room for the emergency call and mark as missed,
                 // since the user did not reject.
-                ringingCall.setOverrideDisconnectCauseCode(DisconnectCause.MISSED);
+                ringingCall.setOverrideDisconnectCauseCode(
+                        new DisconnectCause(DisconnectCause.MISSED));
                 ringingCall.reject(false, null, "emergency call dialed during ringing.");
             }
             return true;
diff --git a/src/com/android/server/telecom/TelecomBroadcastIntentProcessor.java b/src/com/android/server/telecom/TelecomBroadcastIntentProcessor.java
index 8d2a2c5..06aa174 100644
--- a/src/com/android/server/telecom/TelecomBroadcastIntentProcessor.java
+++ b/src/com/android/server/telecom/TelecomBroadcastIntentProcessor.java
@@ -26,6 +26,7 @@
 
 import com.android.server.telecom.ui.CallRedirectionConfirmDialogActivity;
 import com.android.server.telecom.ui.ConfirmCallDialogActivity;
+import com.android.server.telecom.ui.DisconnectedCallNotifier;
 
 import java.util.List;
 
@@ -38,6 +39,14 @@
     public static final String ACTION_CALL_BACK_FROM_NOTIFICATION =
             "com.android.server.telecom.ACTION_CALL_BACK_FROM_NOTIFICATION";
 
+    /** The action used to send SMS response for the disconnected call notification. */
+    public static final String ACTION_DISCONNECTED_SEND_SMS_FROM_NOTIFICATION =
+            "com.android.server.telecom.ACTION_DISCONNECTED_SEND_SMS_FROM_NOTIFICATION";
+
+    /** The action used to call a handle back for the disconnected call notification. */
+    public static final String ACTION_DISCONNECTED_CALL_BACK_FROM_NOTIFICATION =
+            "com.android.server.telecom.ACTION_DISCONNECTED_CALL_BACK_FROM_NOTIFICATION";
+
     /** The action used to clear missed calls. */
     public static final String ACTION_CLEAR_MISSED_CALLS =
             "com.android.server.telecom.ACTION_CLEAR_MISSED_CALLS";
@@ -121,34 +130,45 @@
                 // Close the notification shade and the notification itself.
                 closeSystemDialogs(mContext);
                 missedCallNotifier.clearMissedCalls(userHandle);
-
-                Intent callIntent = new Intent(Intent.ACTION_SENDTO, intent.getData());
-                callIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-                PackageManager packageManager = mContext.getPackageManager();
-                List<ResolveInfo> activities = packageManager.queryIntentActivitiesAsUser(
-                        callIntent, PackageManager.MATCH_DEFAULT_ONLY, userHandle.getIdentifier());
-                if (activities.size() > 0) {
-                    mContext.startActivityAsUser(callIntent, userHandle);
-                } else {
-                    Toast.makeText(mContext, com.android.internal.R.string.noApplications,
-                            Toast.LENGTH_SHORT).show();
-                }
+                sendSmsIntent(intent, userHandle);
 
                 // Call back recent caller from the missed call notification.
             } else if (ACTION_CALL_BACK_FROM_NOTIFICATION.equals(action)) {
                 // Close the notification shade and the notification itself.
                 closeSystemDialogs(mContext);
                 missedCallNotifier.clearMissedCalls(userHandle);
-
-                Intent callIntent = new Intent(Intent.ACTION_CALL, intent.getData());
-                callIntent.setFlags(
-                        Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
-                mContext.startActivityAsUser(callIntent, userHandle);
+                sendCallBackIntent(intent, userHandle);
 
                 // Clear the missed call notification and call log entries.
             } else if (ACTION_CLEAR_MISSED_CALLS.equals(action)) {
                 missedCallNotifier.clearMissedCalls(userHandle);
             }
+        } else if(ACTION_DISCONNECTED_SEND_SMS_FROM_NOTIFICATION.equals(action) ||
+                ACTION_DISCONNECTED_CALL_BACK_FROM_NOTIFICATION.equals(action)) {
+            Log.v(this, "Action received: %s.", action);
+            UserHandle userHandle = intent.getParcelableExtra(EXTRA_USERHANDLE);
+            if (userHandle == null) {
+                Log.d(this, "disconnect user handle can't be null, not processing the broadcast");
+                return;
+            }
+
+            DisconnectedCallNotifier disconnectedCallNotifier =
+                    mCallsManager.getDisconnectedCallNotifier();
+
+            // Send an SMS from the disconnected call notification.
+            if (ACTION_DISCONNECTED_SEND_SMS_FROM_NOTIFICATION.equals(action)) {
+                // Close the notification shade and the notification itself.
+                closeSystemDialogs(mContext);
+                disconnectedCallNotifier.clearNotification(userHandle);
+                sendSmsIntent(intent, userHandle);
+
+            // Call back recent caller from the disconnected call notification.
+            } else if (ACTION_DISCONNECTED_CALL_BACK_FROM_NOTIFICATION.equals(action)) {
+                // Close the notification shade and the notification itself.
+                closeSystemDialogs(mContext);
+                disconnectedCallNotifier.clearNotification(userHandle);
+                sendCallBackIntent(intent, userHandle);
+            }
         } else if (ACTION_ANSWER_FROM_NOTIFICATION.equals(action)) {
             Log.startSession("TBIP.aAFM");
             try {
@@ -230,4 +250,25 @@
         Intent intent = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
         context.sendBroadcastAsUser(intent, UserHandle.ALL);
     }
+
+    private void sendSmsIntent(Intent intent, UserHandle userHandle) {
+        Intent callIntent = new Intent(Intent.ACTION_SENDTO, intent.getData());
+        callIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        PackageManager packageManager = mContext.getPackageManager();
+        List<ResolveInfo> activities = packageManager.queryIntentActivitiesAsUser(
+                callIntent, PackageManager.MATCH_DEFAULT_ONLY, userHandle.getIdentifier());
+        if (activities.size() > 0) {
+            mContext.startActivityAsUser(callIntent, userHandle);
+        } else {
+            Toast.makeText(mContext, com.android.internal.R.string.noApplications,
+                    Toast.LENGTH_SHORT).show();
+        }
+    }
+
+    private void sendCallBackIntent(Intent intent, UserHandle userHandle) {
+        Intent callIntent = new Intent(Intent.ACTION_CALL, intent.getData());
+        callIntent.setFlags(
+                Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+        mContext.startActivityAsUser(callIntent, userHandle);
+    }
 }
diff --git a/src/com/android/server/telecom/TelecomSystem.java b/src/com/android/server/telecom/TelecomSystem.java
index 7700852..d28ddc4 100644
--- a/src/com/android/server/telecom/TelecomSystem.java
+++ b/src/com/android/server/telecom/TelecomSystem.java
@@ -24,6 +24,7 @@
 import com.android.server.telecom.components.UserCallIntentProcessor;
 import com.android.server.telecom.components.UserCallIntentProcessorFactory;
 import com.android.server.telecom.ui.AudioProcessingNotification;
+import com.android.server.telecom.ui.DisconnectedCallNotifier;
 import com.android.server.telecom.ui.IncomingCallNotifier;
 import com.android.server.telecom.ui.MissedCallNotifierImpl.MissedCallNotifierImplFactory;
 import com.android.server.telecom.BluetoothPhoneServiceImpl.BluetoothPhoneServiceImplFactory;
@@ -251,6 +252,8 @@
 
         mMissedCallNotifier = missedCallNotifierImplFactory
                 .makeMissedCallNotifierImpl(mContext, mPhoneAccountRegistrar, defaultDialerCache);
+        DisconnectedCallNotifier.Factory disconnectedCallNotifierFactory =
+                new DisconnectedCallNotifier.Default();
 
         CallerInfoLookupHelper callerInfoLookupHelper =
                 new CallerInfoLookupHelper(context, callerInfoAsyncQueryFactory,
@@ -291,6 +294,7 @@
                 mLock,
                 callerInfoLookupHelper,
                 mMissedCallNotifier,
+                disconnectedCallNotifierFactory,
                 mPhoneAccountRegistrar,
                 headsetMediaButtonFactory,
                 proximitySensorManagerFactory,
diff --git a/src/com/android/server/telecom/ui/DisconnectedCallNotifier.java b/src/com/android/server/telecom/ui/DisconnectedCallNotifier.java
new file mode 100644
index 0000000..aa05552
--- /dev/null
+++ b/src/com/android/server/telecom/ui/DisconnectedCallNotifier.java
@@ -0,0 +1,373 @@
+/*
+ * 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 android.annotation.NonNull;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.TaskStackBuilder;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.UserHandle;
+import android.provider.CallLog;
+import android.telecom.DisconnectCause;
+import android.telecom.Log;
+import android.telecom.PhoneAccount;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.TelephonyManager;
+import android.text.BidiFormatter;
+import android.text.TextDirectionHeuristics;
+import android.text.TextUtils;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallState;
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.CallsManagerListenerBase;
+import com.android.server.telecom.Constants;
+import com.android.server.telecom.R;
+import com.android.server.telecom.TelecomBroadcastIntentProcessor;
+import com.android.server.telecom.components.TelecomBroadcastReceiver;
+
+import java.util.Locale;
+
+/**
+ * Handles notifications generated by Telecom for the case that a call was disconnected in order to
+ * connect another "higher priority" emergency call and gives the user the choice to call or
+ * message that user back after, similar to the missed call notifier.
+ */
+public class DisconnectedCallNotifier extends CallsManagerListenerBase {
+
+    public interface Factory {
+        DisconnectedCallNotifier create(Context context, CallsManager manager);
+    }
+
+    public static class Default implements Factory {
+
+        @Override
+        public DisconnectedCallNotifier create(Context context, CallsManager manager) {
+            return new DisconnectedCallNotifier(context, manager);
+        }
+    }
+
+    private static class CallInfo {
+        public final UserHandle userHandle;
+        public final Uri handle;
+        public final long endTimeMs;
+        public final Bitmap callerInfoIcon;
+        public final Drawable callerInfoPhoto;
+        public final String callerInfoName;
+
+        public CallInfo(UserHandle userHandle, Uri handle, long endTimeMs, Bitmap callerInfoIcon,
+                Drawable callerInfoPhoto, String callerInfoName) {
+            this.userHandle = userHandle;
+            this.handle = handle;
+            this.endTimeMs = endTimeMs;
+            this.callerInfoIcon = callerInfoIcon;
+            this.callerInfoPhoto = callerInfoPhoto;
+            this.callerInfoName = callerInfoName;
+        }
+
+        @Override
+        public String toString() {
+            return "CallInfo{" +
+                    "userHandle=" + userHandle +
+                    ", handle=" + handle +
+                    ", endTimeMs=" + endTimeMs +
+                    ", callerInfoIcon=" + callerInfoIcon +
+                    ", callerInfoPhoto=" + callerInfoPhoto +
+                    ", callerInfoName='" + callerInfoName + '\'' +
+                    '}';
+        }
+    }
+
+    private static final String NOTIFICATION_TAG =
+            DisconnectedCallNotifier.class.getSimpleName();
+    private static final int DISCONNECTED_CALL_NOTIFICATION_ID = 1;
+
+    private final Context mContext;
+    private final CallsManager mCallsManager;
+    private final NotificationManager mNotificationManager;
+    // The pending info to display to the user after they have ended the emergency call.
+    private CallInfo mPendingCallNotification;
+
+    public DisconnectedCallNotifier(Context context, CallsManager callsManager) {
+        mContext = context;
+        mNotificationManager =
+                (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+        mCallsManager = callsManager;
+    }
+
+    @Override
+    public void onCallRemoved(Call call) {
+        // Wait until the emergency call is ended before showing the notification.
+        if (mCallsManager.getCalls().isEmpty() && mPendingCallNotification != null) {
+            showDisconnectedNotification(mPendingCallNotification);
+            mPendingCallNotification = null;
+        }
+    }
+
+    @Override
+    public void onCallStateChanged(Call call, int oldState, int newState) {
+        DisconnectCause cause = call.getDisconnectCause();
+        if (cause == null) {
+            Log.w(this, "onCallStateChanged: unexpected null disconnect cause.");
+            return;
+        }
+        // Call disconnected in favor of an emergency call. Place the call into a pending queue.
+        if ((newState == CallState.DISCONNECTED) && (cause.getCode() == DisconnectCause.LOCAL) &&
+                DisconnectCause.REASON_EMERGENCY_CALL_PLACED.equals(cause.getReason())) {
+            // Clear any existing notification.
+            clearNotification(mCallsManager.getCurrentUserHandle());
+            UserHandle userHandle = call.getTargetPhoneAccount() != null ?
+                    call.getTargetPhoneAccount().getUserHandle() : call.getInitiatingUser();
+            // As a last resort, use the current user to display the notification.
+            if (userHandle == null) userHandle = mCallsManager.getCurrentUserHandle();
+            mPendingCallNotification = new CallInfo(userHandle, call.getHandle(),
+                    call.getCreationTimeMillis() + call.getAgeMillis(), call.getPhotoIcon(),
+                    call.getPhoto(), call.getName());
+        }
+    }
+
+    private void showDisconnectedNotification(@NonNull CallInfo call) {
+        Log.i(this, "showDisconnectedNotification: userHandle=%d", call.userHandle.getIdentifier());
+
+        final int titleResId = R.string.notification_disconnectedCall_title;
+        final String expandedText = mContext.getString(R.string.notification_disconnectedCall_body,
+                getNameForCallNotification(call));
+
+        // Create a public viewable version of the notification, suitable for display when sensitive
+        // notification content is hidden.
+        // We use user's context here to make sure notification is badged if it is a managed user.
+        Context contextForUser = getContextForUser(call.userHandle);
+        Notification.Builder publicBuilder = new Notification.Builder(contextForUser,
+                NotificationChannelManager.CHANNEL_ID_DISCONNECTED_CALLS);
+        publicBuilder.setSmallIcon(android.R.drawable.stat_notify_error)
+                .setColor(mContext.getResources().getColor(R.color.theme_color, null /*theme*/))
+                // Set when the call was disconnected.
+                .setWhen(call.endTimeMs)
+                .setShowWhen(true)
+                // Show "Phone" for notification title.
+                .setContentTitle(mContext.getText(R.string.userCallActivityLabel))
+                // Notification details shows that there are disconnected call(s), but does not
+                // reveal the caller information.
+                .setContentText(mContext.getText(titleResId))
+                .setContentIntent(createCallLogPendingIntent(call.userHandle))
+                .setAutoCancel(true);
+
+        // Create the notification suitable for display when sensitive information is showing.
+        Notification.Builder builder = new Notification.Builder(contextForUser,
+                NotificationChannelManager.CHANNEL_ID_DISCONNECTED_CALLS);
+        builder.setSmallIcon(android.R.drawable.stat_notify_error)
+                .setColor(mContext.getResources().getColor(R.color.theme_color, null /*theme*/))
+                .setWhen(call.endTimeMs)
+                .setShowWhen(true)
+                .setContentTitle(mContext.getText(titleResId))
+                //Only show expanded text for sensitive information
+                .setStyle(new Notification.BigTextStyle().bigText(expandedText))
+                .setContentIntent(createCallLogPendingIntent(call.userHandle))
+                .setAutoCancel(true)
+                // Include a public version of the notification to be shown when the call
+                // notification is shown on the user's lock screen and they have chosen to hide
+                // sensitive notification information.
+                .setPublicVersion(publicBuilder.build())
+                .setChannelId(NotificationChannelManager.CHANNEL_ID_DISCONNECTED_CALLS);
+
+        String handle = call.handle != null ? call.handle.getSchemeSpecificPart() : null;
+
+        if (!TextUtils.isEmpty(handle)
+                && !TextUtils.equals(handle, mContext.getString(R.string.handle_restricted))) {
+            builder.addAction(new Notification.Action.Builder(
+                    Icon.createWithResource(contextForUser, R.drawable.ic_phone_24dp),
+                    // Reuse missed call "Call back"
+                    mContext.getString(R.string.notification_missedCall_call_back),
+                    createCallBackPendingIntent(call.handle, call.userHandle)).build());
+
+            if (canRespondViaSms(call)) {
+                builder.addAction(new Notification.Action.Builder(
+                        Icon.createWithResource(contextForUser, R.drawable.ic_message_24dp),
+                        // Reuse missed call "Call back"
+                        mContext.getString(R.string.notification_missedCall_message),
+                        createSendSmsFromNotificationPendingIntent(call.handle,
+                                call.userHandle)).build());
+            }
+        }
+
+        if (call.callerInfoIcon != null) {
+            builder.setLargeIcon(call.callerInfoIcon);
+        } else {
+            if (call.callerInfoPhoto instanceof BitmapDrawable) {
+                builder.setLargeIcon(((BitmapDrawable) call.callerInfoPhoto).getBitmap());
+            }
+        }
+
+        Notification notification = builder.build();
+
+        Log.i(this, "Adding missed call notification for %s.", Log.pii(call.handle));
+        long token = Binder.clearCallingIdentity();
+        try {
+            // TODO: Only support one notification right now, so if multiple are hung up, we only
+            // show the last one. Support multiple in the future.
+            mNotificationManager.notifyAsUser(NOTIFICATION_TAG, DISCONNECTED_CALL_NOTIFICATION_ID,
+                    notification, call.userHandle);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    /**
+     * Returns the name to use in the call notification.
+     */
+    private String getNameForCallNotification(@NonNull CallInfo call) {
+        String number = call.handle != null ? call.handle.getSchemeSpecificPart() : null;
+
+        if (!TextUtils.isEmpty(number)) {
+            String formattedNumber = PhoneNumberUtils.formatNumber(number,
+                    getCurrentCountryIso(mContext));
+
+            // The formatted number will be null if there was a problem formatting it, but we can
+            // default to using the unformatted number instead (e.g. a SIP URI may not be able to
+            // be formatted.
+            if (!TextUtils.isEmpty(formattedNumber)) {
+                number = formattedNumber;
+            }
+        }
+
+        if (!TextUtils.isEmpty(call.callerInfoName) && TextUtils.isGraphic(call.callerInfoName)) {
+            return call.callerInfoName;
+        }
+        if (!TextUtils.isEmpty(number)) {
+            // A handle should always be displayed LTR using {@link BidiFormatter} regardless of the
+            // content of the rest of the notification.
+            // TODO: Does this apply to SIP addresses?
+            BidiFormatter bidiFormatter = BidiFormatter.getInstance();
+            return bidiFormatter.unicodeWrap(number, TextDirectionHeuristics.LTR);
+        } else {
+            // Use "unknown" if the call is unidentifiable.
+            return mContext.getString(R.string.unknown);
+        }
+    }
+
+    /**
+     * @return The ISO 3166-1 two letters country code of the country the user is in based on the
+     *      network location.  If the network location does not exist, fall back to the locale
+     *      setting.
+     */
+    private String getCurrentCountryIso(Context context) {
+        // Without framework function calls, this seems to be the most accurate location service
+        // we can rely on.
+        final TelephonyManager telephonyManager =
+                (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+        String countryIso = telephonyManager.getNetworkCountryIso().toUpperCase();
+
+        if (countryIso == null) {
+            countryIso = Locale.getDefault().getCountry();
+            Log.w(this, "No CountryDetector; falling back to countryIso based on locale: "
+                    + countryIso);
+        }
+        return countryIso;
+    }
+
+    private Context getContextForUser(UserHandle user) {
+        try {
+            return mContext.createPackageContextAsUser(mContext.getPackageName(), 0, user);
+        } catch (PackageManager.NameNotFoundException e) {
+            // Default to mContext, not finding the package system is running as is unlikely.
+            return mContext;
+        }
+    }
+
+    /**
+     * Creates an intent to be invoked when the user opts to "call back" from the disconnected call
+     * notification.
+     *
+     * @param handle The handle to call back.
+     */
+    private PendingIntent createCallBackPendingIntent(Uri handle, UserHandle userHandle) {
+        return createTelecomPendingIntent(
+                TelecomBroadcastIntentProcessor.ACTION_DISCONNECTED_CALL_BACK_FROM_NOTIFICATION,
+                handle, userHandle);
+    }
+
+    /**
+     * Creates generic pending intent from the specified parameters to be received by
+     * {@link TelecomBroadcastIntentProcessor}.
+     *
+     * @param action The intent action.
+     * @param data The intent data.
+     */
+    private PendingIntent createTelecomPendingIntent(String action, Uri data,
+            UserHandle userHandle) {
+        Intent intent = new Intent(action, data, mContext, TelecomBroadcastReceiver.class);
+        intent.putExtra(TelecomBroadcastIntentProcessor.EXTRA_USERHANDLE, userHandle);
+        return PendingIntent.getBroadcast(mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+    }
+
+    private boolean canRespondViaSms(@NonNull CallInfo call) {
+        // Only allow respond-via-sms for "tel:" calls.
+        return call.handle != null &&
+                PhoneAccount.SCHEME_TEL.equals(call.handle.getScheme());
+    }
+
+    /**
+     * Creates a new pending intent that sends the user to the call log.
+     *
+     * @return The pending intent.
+     */
+    private PendingIntent createCallLogPendingIntent(UserHandle userHandle) {
+        Intent intent = new Intent(Intent.ACTION_VIEW, null);
+        intent.setType(CallLog.Calls.CONTENT_TYPE);
+
+        TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(mContext);
+        taskStackBuilder.addNextIntent(intent);
+
+        return taskStackBuilder.getPendingIntent(0, 0, null, userHandle);
+    }
+
+    /**
+     * Creates an intent to be invoked when the user opts to "send sms" from the missed call
+     * notification.
+     */
+    private PendingIntent createSendSmsFromNotificationPendingIntent(Uri handle,
+            UserHandle userHandle) {
+        return createTelecomPendingIntent(
+                TelecomBroadcastIntentProcessor.ACTION_DISCONNECTED_SEND_SMS_FROM_NOTIFICATION,
+                Uri.fromParts(Constants.SCHEME_SMSTO, handle.getSchemeSpecificPart(), null),
+                userHandle);
+    }
+
+    /**
+     * Clear any of the active notifications.
+     * @param userHandle The user to clear the notifications for.
+     */
+    public void clearNotification(UserHandle userHandle) {
+        long token = Binder.clearCallingIdentity();
+        try {
+            mNotificationManager.cancelAsUser(NOTIFICATION_TAG, DISCONNECTED_CALL_NOTIFICATION_ID,
+                    userHandle);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+}
diff --git a/src/com/android/server/telecom/ui/NotificationChannelManager.java b/src/com/android/server/telecom/ui/NotificationChannelManager.java
index d812ad8..360239b 100644
--- a/src/com/android/server/telecom/ui/NotificationChannelManager.java
+++ b/src/com/android/server/telecom/ui/NotificationChannelManager.java
@@ -38,6 +38,7 @@
     public static final String CHANNEL_ID_INCOMING_CALLS = "TelecomIncomingCalls";
     public static final String CHANNEL_ID_CALL_BLOCKING = "TelecomCallBlocking";
     public static final String CHANNEL_ID_AUDIO_PROCESSING = "TelecomBackgroundAudioProcessing";
+    public static final String CHANNEL_ID_DISCONNECTED_CALLS = "TelecomDisconnectedCalls";
 
     private BroadcastReceiver mLocaleChangeReceiver = new BroadcastReceiver() {
         @Override
@@ -59,6 +60,7 @@
         createOrUpdateChannel(context, CHANNEL_ID_INCOMING_CALLS);
         createOrUpdateChannel(context, CHANNEL_ID_CALL_BLOCKING);
         createOrUpdateChannel(context, CHANNEL_ID_AUDIO_PROCESSING);
+        createOrUpdateChannel(context, CHANNEL_ID_DISCONNECTED_CALLS);
     }
 
     private void createOrUpdateChannel(Context context, String channelId) {
@@ -108,6 +110,14 @@
                 vibration = false;
                 sound = null;
                 break;
+            case CHANNEL_ID_DISCONNECTED_CALLS:
+                name = context.getText(R.string.notification_channel_disconnected_calls);
+                importance = NotificationManager.IMPORTANCE_DEFAULT;
+                canShowBadge = true;
+                lights = true;
+                vibration = true;
+                sound = silentRingtone;
+                break;
         }
 
         NotificationChannel channel = new NotificationChannel(channelId, name, importance);
diff --git a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
index 110b58e..89383a4 100644
--- a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
@@ -88,6 +88,7 @@
 import com.android.server.telecom.bluetooth.BluetoothStateReceiver;
 import com.android.server.telecom.callfiltering.IncomingCallFilter;
 import com.android.server.telecom.ui.AudioProcessingNotification;
+import com.android.server.telecom.ui.DisconnectedCallNotifier;
 import com.android.server.telecom.ui.ToastFactory;
 
 import org.junit.After;
@@ -150,6 +151,8 @@
     private final TelecomSystem.SyncRoot mLock = new TelecomSystem.SyncRoot() { };
     @Mock private CallerInfoLookupHelper mCallerInfoLookupHelper;
     @Mock private MissedCallNotifier mMissedCallNotifier;
+    @Mock private DisconnectedCallNotifier.Factory mDisconnectedCallNotifierFactory;
+    @Mock private DisconnectedCallNotifier mDisconnectedCallNotifier;
     @Mock private PhoneAccountRegistrar mPhoneAccountRegistrar;
     @Mock private HeadsetMediaButton mHeadsetMediaButton;
     @Mock private HeadsetMediaButtonFactory mHeadsetMediaButtonFactory;
@@ -209,11 +212,14 @@
         when(mClockProxy.elapsedRealtime()).thenReturn(SystemClock.elapsedRealtime());
         when(mConnSvrFocusManagerFactory.create(any())).thenReturn(mConnectionSvrFocusMgr);
         doNothing().when(mRoleManagerAdapter).setCurrentUserHandle(any());
+        when(mDisconnectedCallNotifierFactory.create(any(Context.class),any(CallsManager.class)))
+                .thenReturn(mDisconnectedCallNotifier);
         mCallsManager = new CallsManager(
                 mComponentContextFixture.getTestDouble().getApplicationContext(),
                 mLock,
                 mCallerInfoLookupHelper,
                 mMissedCallNotifier,
+                mDisconnectedCallNotifierFactory,
                 mPhoneAccountRegistrar,
                 mHeadsetMediaButtonFactory,
                 mProximitySensorManagerFactory,
diff --git a/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java b/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
index dcd0607..b1e62b4 100644
--- a/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
+++ b/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
@@ -124,6 +124,11 @@
         }
 
         @Override
+        public Resources.Theme getTheme() {
+            return mResourcesTheme;
+        }
+
+        @Override
         public File getFilesDir() {
             try {
                 return File.createTempFile("temp", "temp").getParentFile();
@@ -446,6 +451,7 @@
     // We then create a spy on the application context allowing standard Mockito-style
     // when(...) logic to be used to add specific little responses where needed.
 
+    private final Resources.Theme mResourcesTheme = mock(Resources.Theme.class);
     private final Resources mResources = mock(Resources.class);
     private final Context mApplicationContextSpy = spy(mApplicationContext);
     private final PackageManager mPackageManager = mock(PackageManager.class);
diff --git a/tests/src/com/android/server/telecom/tests/DisconnectedCallNotifierTest.java b/tests/src/com/android/server/telecom/tests/DisconnectedCallNotifierTest.java
new file mode 100644
index 0000000..22963de
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/DisconnectedCallNotifierTest.java
@@ -0,0 +1,123 @@
+package com.android.server.telecom.tests;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.os.UserHandle;
+import android.telecom.DisconnectCause;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.TelephonyManager;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallState;
+import com.android.server.telecom.CallerInfoLookupHelper;
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.ui.DisconnectedCallNotifier;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+
+import java.util.Collections;
+
+public class DisconnectedCallNotifierTest extends TelecomTestCase {
+
+    private static final PhoneAccountHandle PHONE_ACCOUNT_HANDLE = new PhoneAccountHandle(
+            new ComponentName("com.android.server.telecom.tests", "DisconnectedCallNotifierTest"),
+            "testId");
+    private static final Uri TEL_CALL_HANDLE = Uri.parse("tel:+11915552620");
+
+    @Mock private CallsManager mCallsManager;
+    @Mock private CallerInfoLookupHelper mCallerInfoLookupHelper;
+
+    private NotificationManager mNotificationManager;
+
+    @Before
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+
+        mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
+
+        mNotificationManager = (NotificationManager) mContext.getSystemService(
+                Context.NOTIFICATION_SERVICE);
+        TelephonyManager fakeTelephonyManager = (TelephonyManager) mContext.getSystemService(
+                Context.TELEPHONY_SERVICE);
+        when(fakeTelephonyManager.getNetworkCountryIso()).thenReturn("US");
+        doReturn(mCallerInfoLookupHelper).when(mCallsManager).getCallerInfoLookupHelper();
+    }
+
+    @After
+    @Override
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @Test
+    @SmallTest
+    public void testNotificationShownAfterEmergencyCall() {
+        Call call = createCall(new DisconnectCause(DisconnectCause.LOCAL,
+                DisconnectCause.REASON_EMERGENCY_CALL_PLACED));
+
+        DisconnectedCallNotifier notifier = new DisconnectedCallNotifier(mContext, mCallsManager);
+        notifier.onCallStateChanged(call, CallState.NEW, CallState.DIALING);
+        notifier.onCallStateChanged(call, CallState.DIALING, CallState.DISCONNECTED);
+        verify(mNotificationManager, never()).notifyAsUser(anyString(), anyInt(),
+                any(Notification.class), any(UserHandle.class));
+
+        doReturn(Collections.EMPTY_LIST).when(mCallsManager).getCalls();
+        notifier.onCallRemoved(call);
+        verify(mNotificationManager).notifyAsUser(anyString(), anyInt(),
+                any(Notification.class), any(UserHandle.class));
+    }
+
+    @Test
+    @SmallTest
+    public void testNotificationNotShownAfterCall() {
+        Call call = createCall(new DisconnectCause(DisconnectCause.LOCAL));
+
+        DisconnectedCallNotifier notifier = new DisconnectedCallNotifier(mContext, mCallsManager);
+        notifier.onCallStateChanged(call, CallState.DIALING, CallState.DISCONNECTED);
+        verify(mNotificationManager, never()).notifyAsUser(anyString(), anyInt(),
+                any(Notification.class), any(UserHandle.class));
+
+        doReturn(Collections.EMPTY_LIST).when(mCallsManager).getCalls();
+        notifier.onCallRemoved(call);
+        verify(mNotificationManager, never()).notifyAsUser(anyString(), anyInt(),
+                any(Notification.class), any(UserHandle.class));
+    }
+
+    @Test
+    @SmallTest
+    public void testNotificationClearedForEmergencyCall() {
+        Call call = createCall(new DisconnectCause(DisconnectCause.LOCAL,
+                DisconnectCause.REASON_EMERGENCY_CALL_PLACED));
+
+        DisconnectedCallNotifier notifier = new DisconnectedCallNotifier(mContext, mCallsManager);
+        notifier.onCallStateChanged(call, CallState.DIALING, CallState.DISCONNECTED);
+        verify(mNotificationManager).cancelAsUser(anyString(), anyInt(), any());
+    }
+
+    private Call createCall(DisconnectCause cause) {
+        Call call = mock(Call.class);
+        when(call.getDisconnectCause()).thenReturn(cause);
+        when(call.getTargetPhoneAccount()).thenReturn(PHONE_ACCOUNT_HANDLE);
+        when(call.getHandle()).thenReturn(TEL_CALL_HANDLE);
+        return call;
+    }
+}