Require MODIFY_PHONE_STATE for preferred TTY mode am: 266f2436b9

Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/services/Telecomm/+/12673305

Change-Id: I2a73bd57d38a489b275ee8017e762afcba94e4ef
diff --git a/src/com/android/server/telecom/CallIdMapper.java b/src/com/android/server/telecom/CallIdMapper.java
index da2c199..72a51f5 100644
--- a/src/com/android/server/telecom/CallIdMapper.java
+++ b/src/com/android/server/telecom/CallIdMapper.java
@@ -20,6 +20,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 
+import java.util.Collection;
 import java.util.Map;
 
 /** Utility to map {@link Call} objects to unique IDs. IDs are generated when a call is added. */
@@ -132,6 +133,10 @@
         return mCalls.getValue(callId);
     }
 
+    Collection<Call> getCalls() {
+        return mCalls.mPrimaryMap.values();
+    }
+
     void clear() {
         mCalls.clear();
     }
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index 1c46209..d785e47 100755
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -173,6 +173,7 @@
         void onConnectionTimeChanged(Call call);
         void onConferenceStateChanged(Call call, boolean isConference);
         void onCdmaConferenceSwap(Call call);
+        void onSetCamera(Call call, String cameraId);
     }
 
     /** Interface used to define the action which is executed delay under some condition. */
@@ -1011,6 +1012,18 @@
         }
     }
 
+    /**
+     * Handles a change to the currently active camera for a call by notifying listeners.
+     * @param call The call.
+     * @param cameraId The ID of the camera in use, or {@code null} if no camera is in use.
+     */
+    @Override
+    public void onSetCamera(Call call, String cameraId) {
+        for (CallsManagerListener listener : mListeners) {
+            listener.onSetCamera(call, cameraId);
+        }
+    }
+
     public Collection<Call> getCalls() {
         return Collections.unmodifiableCollection(mCalls);
     }
diff --git a/src/com/android/server/telecom/CallsManagerListenerBase.java b/src/com/android/server/telecom/CallsManagerListenerBase.java
index e0d2831..55c7b53 100644
--- a/src/com/android/server/telecom/CallsManagerListenerBase.java
+++ b/src/com/android/server/telecom/CallsManagerListenerBase.java
@@ -104,4 +104,8 @@
     @Override
     public void onCdmaConferenceSwap(Call call) {
     }
+
+    @Override
+    public void onSetCamera(Call call, String cameraId) {
+    }
 }
diff --git a/src/com/android/server/telecom/DeviceIdleControllerAdapter.java b/src/com/android/server/telecom/DeviceIdleControllerAdapter.java
new file mode 100644
index 0000000..d3a798a
--- /dev/null
+++ b/src/com/android/server/telecom/DeviceIdleControllerAdapter.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2020 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;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.os.PowerWhitelistManager;
+import android.os.RemoteException;
+import android.telecom.Log;
+
+import com.android.internal.telecom.IDeviceIdleControllerAdapter;
+
+/**
+ * Telecom is in the same process as the {@link PowerWhitelistManager}, so we can not make direct
+ * calls to the manager interface, since they will fail in the DeviceIdleController
+ * (see {@link Context#enforceCallingPermission(String, String)}). Instead, we must access it
+ * through SystemService#getLocalService, which is only accessible to the Telecom
+ * core loader service (TelecomLoaderService). Unfortunately, due to the architecture, this means
+ * we must use a Binder to allow services such as this to be accessible.
+ */
+public class DeviceIdleControllerAdapter {
+
+    private static final String TAG = "DeviceIdleControllerAdapter";
+
+    private final IDeviceIdleControllerAdapter mAdapter;
+
+    public DeviceIdleControllerAdapter(IDeviceIdleControllerAdapter adapter) {
+        mAdapter = adapter;
+    }
+
+    /**
+     * Exempts an application from power restrictions for the duration specified. See
+     * DeviceIdleController for more information on how this works.
+     */
+    public void exemptAppTemporarilyForEvent(@NonNull String packageName, long duration,
+            int userHandle, @NonNull String reason) {
+        try {
+            mAdapter.exemptAppTemporarilyForEvent(packageName, duration, userHandle, reason);
+        } catch (RemoteException e) {
+            Log.w(TAG, "exemptAppTemporarilyForEvent e=" + e.getMessage());
+        }
+    }
+}
diff --git a/src/com/android/server/telecom/InCallController.java b/src/com/android/server/telecom/InCallController.java
index 6de6dea..72a4a39 100644
--- a/src/com/android/server/telecom/InCallController.java
+++ b/src/com/android/server/telecom/InCallController.java
@@ -16,8 +16,11 @@
 
 package com.android.server.telecom;
 
+import static android.os.Process.myUid;
+
 import android.Manifest;
 import android.annotation.NonNull;
+import android.app.AppOpsManager;
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.content.ComponentName;
@@ -214,7 +217,7 @@
                 Log.startSession("ICSBC.oSD", Log.getPackageAbbreviation(name));
                 synchronized (mLock) {
                     try {
-                        Log.d(this, "onDisconnected: %s", name);
+                        Log.d(this, "onServiceDisconnected: %s", name);
                         mIsBound = false;
                         onDisconnected();
                     } finally {
@@ -335,6 +338,8 @@
                             mInCallServiceInfo.getDisconnectTime()
                                     - mInCallServiceInfo.getBindingStartTime(), mIsNullBinding);
                 }
+
+                InCallController.this.onDisconnected(mInCallServiceInfo);
             } else {
                 Log.i(InCallController.this, "ICSBC#disconnect: already disconnected; %s",
                         mInCallServiceInfo);
@@ -867,6 +872,7 @@
     private final CallIdMapper mCallIdMapper = new CallIdMapper(Call::getId);
 
     private final Context mContext;
+    private final AppOpsManager mAppOpsManager;
     private final TelecomSystem.SyncRoot mLock;
     private final CallsManager mCallsManager;
     private final SystemStateHelper mSystemStateHelper;
@@ -884,12 +890,24 @@
 
     private final CarModeTracker mCarModeTracker;
 
+    /**
+     * The package name of the app which is showing the calling UX.
+     */
+    private String mCurrentUserInterfacePackageName = null;
+
+    /**
+     * {@code true} if InCallController is tracking a managed, not external call which is using the
+     * microphone, {@code false} otherwise.
+     */
+    private boolean mIsCallUsingMicrophone = false;
+
     public InCallController(Context context, TelecomSystem.SyncRoot lock, CallsManager callsManager,
             SystemStateHelper systemStateHelper,
             DefaultDialerCache defaultDialerCache, Timeouts.Adapter timeoutsAdapter,
             EmergencyCallHelper emergencyCallHelper, CarModeTracker carModeTracker,
             ClockProxy clockProxy) {
         mContext = context;
+        mAppOpsManager = context.getSystemService(AppOpsManager.class);
         mLock = lock;
         mCallsManager = callsManager;
         mSystemStateHelper = systemStateHelper;
@@ -975,6 +993,7 @@
         }
         call.removeListener(mCallListener);
         mCallIdMapper.removeCall(call);
+        maybeTrackMicrophoneUse(isMuted());
     }
 
     @Override
@@ -1048,10 +1067,12 @@
             }
             Log.i(this, "External call removed from components: %s", componentsUpdated);
         }
+        maybeTrackMicrophoneUse(isMuted());
     }
 
     @Override
     public void onCallStateChanged(Call call, int oldState, int newState) {
+        maybeTrackMicrophoneUse(isMuted());
         updateCall(call);
     }
 
@@ -1069,6 +1090,7 @@
         if (!mInCallServices.isEmpty()) {
             Log.i(this, "Calling onAudioStateChanged, audioState: %s -> %s", oldCallAudioState,
                     newCallAudioState);
+            maybeTrackMicrophoneUse(newCallAudioState.isMuted());
             for (IInCallService inCallService : mInCallServices.values()) {
                 try {
                     inCallService.onCallAudioStateChanged(newCallAudioState);
@@ -1133,6 +1155,23 @@
         updateCall(call);
     }
 
+    /**
+     * Track changes to camera usage for a call.
+     * @param call The call.
+     * @param cameraId The id of the camera to use, or {@code null} if camera is off.
+     */
+    @Override
+    public void onSetCamera(Call call, String cameraId) {
+        Log.i(this, "onSetCamera callId=%s, cameraId=%s", call.getId(), cameraId);
+        if (cameraId != null) {
+            mAppOpsManager.startOp(AppOpsManager.OP_PHONE_CALL_CAMERA, myUid(),
+                    mContext.getOpPackageName(), false, null, null);
+        } else {
+            mAppOpsManager.finishOp(AppOpsManager.OP_PHONE_CALL_CAMERA, myUid(),
+                    mContext.getOpPackageName(), null);
+        }
+    }
+
     void bringToForeground(boolean showDialpad) {
         if (!mInCallServices.isEmpty()) {
             for (IInCallService inCallService : mInCallServices.values()) {
@@ -1509,6 +1548,11 @@
     private boolean onConnected(InCallServiceInfo info, IBinder service) {
         Log.i(this, "onConnected to %s", info.getComponentName());
 
+        if (info.getType() == IN_CALL_SERVICE_TYPE_CAR_MODE_UI
+                || info.getType() == IN_CALL_SERVICE_TYPE_SYSTEM_UI
+                || info.getType() == IN_CALL_SERVICE_TYPE_DIALER_UI) {
+            trackCallingUserInterfaceStarted(info);
+        }
         IInCallService inCallService = IInCallService.Stub.asInterface(service);
         mInCallServices.put(info, inCallService);
 
@@ -1559,7 +1603,11 @@
             inCallService.onCanAddCallChanged(mCallsManager.canAddCall());
         } catch (RemoteException ignored) {
         }
-        mBindingFuture.complete(true);
+        // Don't complete the binding future for non-ui incalls
+        if (info.getType() != IN_CALL_SERVICE_TYPE_NON_UI) {
+            mBindingFuture.complete(true);
+        }
+
         Log.i(this, "%s calls sent to InCallService.", numCallsSent);
         return true;
     }
@@ -1571,7 +1619,11 @@
      */
     private void onDisconnected(InCallServiceInfo disconnectedInfo) {
         Log.i(this, "onDisconnected from %s", disconnectedInfo.getComponentName());
-
+        if (disconnectedInfo.getType() == IN_CALL_SERVICE_TYPE_CAR_MODE_UI
+                || disconnectedInfo.getType() == IN_CALL_SERVICE_TYPE_SYSTEM_UI
+                || disconnectedInfo.getType() == IN_CALL_SERVICE_TYPE_DIALER_UI) {
+            trackCallingUserInterfaceStopped(disconnectedInfo);
+        }
         mInCallServices.remove(disconnectedInfo);
     }
 
@@ -1637,6 +1689,7 @@
             mCallIdMapper.addCall(call);
             call.addListener(mCallListener);
         }
+        maybeTrackMicrophoneUse(isMuted());
     }
 
     /**
@@ -1811,6 +1864,69 @@
         }
     }
 
+    /**
+     * Tracks start of microphone use on binding to the current calling UX.
+     * @param info
+     */
+    private void trackCallingUserInterfaceStarted(InCallServiceInfo info) {
+        String packageName = info.getComponentName().getPackageName();
+        if (!Objects.equals(mCurrentUserInterfacePackageName, packageName)) {
+            Log.i(this, "trackCallingUserInterfaceStarted: %s is now calling UX.", packageName);
+            mCurrentUserInterfacePackageName = packageName;
+        }
+        maybeTrackMicrophoneUse(isMuted());
+    }
+
+    /**
+     * Tracks stop of microphone use on unbind from the current calling UX.
+     * @param info
+     */
+    private void trackCallingUserInterfaceStopped(InCallServiceInfo info) {
+        maybeTrackMicrophoneUse(isMuted());
+        mCurrentUserInterfacePackageName = null;
+        String packageName = info.getComponentName().getPackageName();
+        Log.i(this, "trackCallingUserInterfaceStopped: %s is no longer calling UX", packageName);
+    }
+
+    /**
+     * As calls are added, removed and change between external and non-external status, track
+     * whether the current active calling UX is using the microphone.  We assume if there is a
+     * managed call present and the mic is not muted that the microphone is in use.
+     */
+    private void maybeTrackMicrophoneUse(boolean isMuted) {
+        boolean wasTrackingManagedCall = mIsCallUsingMicrophone;
+        mIsCallUsingMicrophone = isTrackingManagedAliveCall() && !isMuted;
+        if (wasTrackingManagedCall != mIsCallUsingMicrophone) {
+            if (mIsCallUsingMicrophone) {
+                mAppOpsManager.startOp(AppOpsManager.OP_PHONE_CALL_MICROPHONE, myUid(),
+                        mContext.getOpPackageName(), false, null, null);
+            } else {
+                mAppOpsManager.finishOp(AppOpsManager.OP_PHONE_CALL_MICROPHONE, myUid(),
+                        mContext.getOpPackageName(), null);
+            }
+        }
+    }
+
+    /**
+     * @return {@code true} if InCallController is tracking a managed call (i.e. not self managed
+     * and not external) that is active.
+     */
+    private boolean isTrackingManagedAliveCall() {
+        return mCallIdMapper.getCalls().stream().anyMatch(c -> !c.isExternalCall()
+            && !c.isSelfManaged() && c.isAlive() && c.getState() != CallState.ON_HOLD
+                && c.getState() != CallState.AUDIO_PROCESSING);
+    }
+
+    /**
+     * @return {@code true} if the audio is currently muted, {@code false} otherwise.
+     */
+    private boolean isMuted() {
+        if (mCallsManager.getAudioState() == null) {
+            return false;
+        }
+        return mCallsManager.getAudioState().isMuted();
+    }
+
     private void sendCrashedInCallServiceNotification(String packageName) {
         PackageManager packageManager = mContext.getPackageManager();
         CharSequence appName;
diff --git a/src/com/android/server/telecom/InternalServiceRetrieverAdapter.java b/src/com/android/server/telecom/InternalServiceRetrieverAdapter.java
new file mode 100644
index 0000000..4a2c3d1
--- /dev/null
+++ b/src/com/android/server/telecom/InternalServiceRetrieverAdapter.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2020 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;
+
+import android.os.RemoteException;
+
+import com.android.internal.telecom.IInternalServiceRetriever;
+
+/**
+ * Contains all services that Telecom must access that are only accessible as a local service as
+ * part of the SYSTEM process.
+ */
+public class InternalServiceRetrieverAdapter {
+
+    private final IInternalServiceRetriever mRetriever;
+
+    public InternalServiceRetrieverAdapter(IInternalServiceRetriever retriever) {
+        mRetriever = retriever;
+    }
+
+    public DeviceIdleControllerAdapter getDeviceIdleController() {
+        try {
+            return new DeviceIdleControllerAdapter(mRetriever.getDeviceIdleController());
+        } catch (RemoteException e) {
+            // This should not happen - if it does, there is a bad configuration as this should
+            // all be local.
+            throw new IllegalStateException(e);
+        }
+    }
+}
diff --git a/src/com/android/server/telecom/TelecomSystem.java b/src/com/android/server/telecom/TelecomSystem.java
index 807cc2d..08389b9 100644
--- a/src/com/android/server/telecom/TelecomSystem.java
+++ b/src/com/android/server/telecom/TelecomSystem.java
@@ -207,7 +207,8 @@
             ClockProxy clockProxy,
             RoleManagerAdapter roleManagerAdapter,
             IncomingCallFilter.Factory incomingCallFilterFactory,
-            ContactsAsyncHelper.Factory contactsAsyncHelperFactory) {
+            ContactsAsyncHelper.Factory contactsAsyncHelperFactory,
+            DeviceIdleControllerAdapter deviceIdleControllerAdapter) {
         mContext = context.getApplicationContext();
         LogUtils.initLogging(mContext);
         DefaultDialerManagerAdapter defaultDialerAdapter =
@@ -241,7 +242,8 @@
         SystemStateHelper systemStateHelper = new SystemStateHelper(mContext);
 
         mMissedCallNotifier = missedCallNotifierImplFactory
-                .makeMissedCallNotifierImpl(mContext, mPhoneAccountRegistrar, defaultDialerCache);
+                .makeMissedCallNotifierImpl(mContext, mPhoneAccountRegistrar, defaultDialerCache,
+                        deviceIdleControllerAdapter);
         DisconnectedCallNotifier.Factory disconnectedCallNotifierFactory =
                 new DisconnectedCallNotifier.Default();
 
diff --git a/src/com/android/server/telecom/Timeouts.java b/src/com/android/server/telecom/Timeouts.java
index a701b88..2309596 100644
--- a/src/com/android/server/telecom/Timeouts.java
+++ b/src/com/android/server/telecom/Timeouts.java
@@ -211,4 +211,15 @@
     public static long getCallRecordingToneRepeatIntervalMillis(ContentResolver contentResolver) {
         return get(contentResolver, "call_recording_tone_repeat_interval", 15000L /* 15 seconds */);
     }
+
+    /**
+     * Returns the number of milliseconds for which the system should exempt the default dialer from
+     * power save restrictions due to the dialer needing to handle a missed call notification
+     * (update call log, check VVM, etc...).
+     */
+    public static long getDialerMissedCallPowerSaveExemptionTimeMillis(
+            ContentResolver contentResolver) {
+        return get(contentResolver, "dialer_missed_call_power_save_exemption_time_millis",
+                30000L /*30 seconds*/);
+    }
 }
diff --git a/src/com/android/server/telecom/VideoProviderProxy.java b/src/com/android/server/telecom/VideoProviderProxy.java
index 364e0f4..df11403 100644
--- a/src/com/android/server/telecom/VideoProviderProxy.java
+++ b/src/com/android/server/telecom/VideoProviderProxy.java
@@ -55,6 +55,7 @@
      */
     public interface Listener {
         void onSessionModifyRequestReceived(Call call, VideoProfile videoProfile);
+        void onSetCamera(Call call, String cameraId);
     }
 
     /**
@@ -346,6 +347,12 @@
                     return;
                 }
             }
+
+            // Inform other Telecom components of the change in camera status.
+            for (Listener listener : mListeners) {
+                listener.onSetCamera(mCall, cameraId);
+            }
+
             try {
                 mConectionServiceVideoProvider.setCamera(cameraId, callingPackage,
                         targetSdkVersion);
diff --git a/src/com/android/server/telecom/callfiltering/CallScreeningServiceFilter.java b/src/com/android/server/telecom/callfiltering/CallScreeningServiceFilter.java
index 1e52c5a..486cd8d 100644
--- a/src/com/android/server/telecom/callfiltering/CallScreeningServiceFilter.java
+++ b/src/com/android/server/telecom/callfiltering/CallScreeningServiceFilter.java
@@ -289,7 +289,12 @@
 
     public void unbindCallScreeningService() {
         if (mConnection != null) {
-            mContext.unbindService(mConnection);
+            try {
+                mContext.unbindService(mConnection);
+            } catch (IllegalArgumentException e) {
+                Log.i(this, "Exception when unbind service %s : %s", mConnection,
+                        e.getMessage());
+            }
         }
         mConnection = null;
     }
diff --git a/src/com/android/server/telecom/components/TelecomService.java b/src/com/android/server/telecom/components/TelecomService.java
index e20da80..b6705f4 100644
--- a/src/com/android/server/telecom/components/TelecomService.java
+++ b/src/com/android/server/telecom/components/TelecomService.java
@@ -30,6 +30,10 @@
 import android.telecom.Log;
 
 import android.telecom.CallerInfoAsyncQuery;
+
+import com.android.internal.telecom.IInternalServiceRetriever;
+import com.android.internal.telecom.ITelecomLoader;
+import com.android.internal.telecom.ITelecomService;
 import com.android.server.telecom.AsyncRingtonePlayer;
 import com.android.server.telecom.BluetoothAdapterProxy;
 import com.android.server.telecom.BluetoothPhoneServiceImpl;
@@ -41,16 +45,17 @@
 import com.android.server.telecom.ConnectionServiceFocusManager;
 import com.android.server.telecom.ContactsAsyncHelper;
 import com.android.server.telecom.DefaultDialerCache;
+import com.android.server.telecom.DeviceIdleControllerAdapter;
 import com.android.server.telecom.HeadsetMediaButton;
 import com.android.server.telecom.HeadsetMediaButtonFactory;
 import com.android.server.telecom.InCallWakeLockControllerFactory;
 import com.android.server.telecom.CallAudioManager;
+import com.android.server.telecom.InternalServiceRetrieverAdapter;
 import com.android.server.telecom.PhoneAccountRegistrar;
 import com.android.server.telecom.PhoneNumberUtilsAdapterImpl;
 import com.android.server.telecom.ProximitySensorManagerFactory;
 import com.android.server.telecom.InCallWakeLockController;
 import com.android.server.telecom.ProximitySensorManager;
-import com.android.server.telecom.R;
 import com.android.server.telecom.RoleManagerAdapterImpl;
 import com.android.server.telecom.TelecomSystem;
 import com.android.server.telecom.TelecomWakeLock;
@@ -68,10 +73,17 @@
     @Override
     public IBinder onBind(Intent intent) {
         Log.d(this, "onBind");
-        initializeTelecomSystem(this);
-        synchronized (getTelecomSystem().getLock()) {
-            return getTelecomSystem().getTelecomServiceImpl().getBinder();
-        }
+        return new ITelecomLoader.Stub() {
+            @Override
+            public ITelecomService createTelecomService(IInternalServiceRetriever retriever) {
+                InternalServiceRetrieverAdapter adapter =
+                        new InternalServiceRetrieverAdapter(retriever);
+                initializeTelecomSystem(TelecomService.this, adapter);
+                synchronized (getTelecomSystem().getLock()) {
+                    return getTelecomSystem().getTelecomServiceImpl().getBinder();
+                }
+            }
+        };
     }
 
     /**
@@ -84,7 +96,8 @@
      *
      * @param context
      */
-    static void initializeTelecomSystem(Context context) {
+    static void initializeTelecomSystem(Context context,
+            InternalServiceRetrieverAdapter internalServiceRetriever) {
         if (TelecomSystem.getInstance() == null) {
             NotificationChannelManager notificationChannelManager =
                     new NotificationChannelManager();
@@ -98,9 +111,11 @@
                                 public MissedCallNotifierImpl makeMissedCallNotifierImpl(
                                         Context context,
                                         PhoneAccountRegistrar phoneAccountRegistrar,
-                                        DefaultDialerCache defaultDialerCache) {
+                                        DefaultDialerCache defaultDialerCache,
+                                        DeviceIdleControllerAdapter idleControllerAdapter) {
                                     return new MissedCallNotifierImpl(context,
-                                            phoneAccountRegistrar, defaultDialerCache);
+                                            phoneAccountRegistrar, defaultDialerCache,
+                                            idleControllerAdapter);
                                 }
                             },
                             new CallerInfoAsyncQueryFactory() {
@@ -191,7 +206,8 @@
                             new RoleManagerAdapterImpl(context,
                                     (RoleManager) context.getSystemService(Context.ROLE_SERVICE)),
                             new IncomingCallFilter.Factory(),
-                            new ContactsAsyncHelper.Factory()));
+                            new ContactsAsyncHelper.Factory(),
+                            internalServiceRetriever.getDeviceIdleController()));
         }
         if (BluetoothAdapter.getDefaultAdapter() != null) {
             context.startService(new Intent(context, BluetoothPhoneService.class));
diff --git a/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java b/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java
index a0eca8f..e37fe6b 100644
--- a/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java
+++ b/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java
@@ -19,8 +19,10 @@
 import static android.Manifest.permission.READ_PHONE_STATE;
 
 import android.annotation.NonNull;
+import android.app.BroadcastOptions;
 import android.content.ContentProvider;
 import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.Bundle;
 import android.telecom.Logging.Runnable;
 import android.telecom.PhoneAccountHandle;
 import android.telecom.TelecomManager;
@@ -29,11 +31,13 @@
 import com.android.server.telecom.CallsManagerListenerBase;
 import com.android.server.telecom.Constants;
 import com.android.server.telecom.DefaultDialerCache;
+import com.android.server.telecom.DeviceIdleControllerAdapter;
 import com.android.server.telecom.MissedCallNotifier;
 import com.android.server.telecom.PhoneAccountRegistrar;
 import com.android.server.telecom.R;
 import com.android.server.telecom.TelecomBroadcastIntentProcessor;
 import com.android.server.telecom.TelecomSystem;
+import com.android.server.telecom.Timeouts;
 import com.android.server.telecom.components.TelecomBroadcastReceiver;
 
 import android.app.Notification;
@@ -86,7 +90,8 @@
     public interface MissedCallNotifierImplFactory {
         MissedCallNotifier makeMissedCallNotifierImpl(Context context,
                 PhoneAccountRegistrar phoneAccountRegistrar,
-                DefaultDialerCache defaultDialerCache);
+                DefaultDialerCache defaultDialerCache,
+                DeviceIdleControllerAdapter deviceIdleControllerAdapter);
     }
 
     public interface NotificationBuilderFactory {
@@ -124,12 +129,14 @@
 
     private static final int MISSED_CALL_NOTIFICATION_ID = 1;
     private static final String NOTIFICATION_TAG = MissedCallNotifierImpl.class.getSimpleName();
+    private static final String MISSED_CALL_POWER_SAVE_REASON = "missed-call";
 
     private final Context mContext;
     private final PhoneAccountRegistrar mPhoneAccountRegistrar;
     private final NotificationManager mNotificationManager;
     private final NotificationBuilderFactory mNotificationBuilderFactory;
     private final DefaultDialerCache mDefaultDialerCache;
+    private final DeviceIdleControllerAdapter mDeviceIdleControllerAdapter;
     private UserHandle mCurrentUserHandle;
 
     // Used to track the number of missed calls.
@@ -138,19 +145,22 @@
     private List<UserHandle> mUsersToLoadAfterBootComplete = new ArrayList<>();
 
     public MissedCallNotifierImpl(Context context, PhoneAccountRegistrar phoneAccountRegistrar,
-            DefaultDialerCache defaultDialerCache) {
+            DefaultDialerCache defaultDialerCache,
+            DeviceIdleControllerAdapter deviceIdleControllerAdapter) {
         this(context, phoneAccountRegistrar, defaultDialerCache,
-                new DefaultNotificationBuilderFactory());
+                new DefaultNotificationBuilderFactory(), deviceIdleControllerAdapter);
     }
 
     public MissedCallNotifierImpl(Context context,
             PhoneAccountRegistrar phoneAccountRegistrar,
             DefaultDialerCache defaultDialerCache,
-            NotificationBuilderFactory notificationBuilderFactory) {
+            NotificationBuilderFactory notificationBuilderFactory,
+            DeviceIdleControllerAdapter deviceIdleControllerAdapter) {
         mContext = context;
         mPhoneAccountRegistrar = phoneAccountRegistrar;
         mNotificationManager =
                 (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+        mDeviceIdleControllerAdapter = deviceIdleControllerAdapter;
         mDefaultDialerCache = defaultDialerCache;
 
         mNotificationBuilderFactory = notificationBuilderFactory;
@@ -162,7 +172,8 @@
     public void clearMissedCalls(UserHandle userHandle) {
         // If the default dialer is showing the missed call notification then it will modify the
         // call log and we don't have to do anything here.
-        if (!shouldManageNotificationThroughDefaultDialer(userHandle)) {
+        String dialerPackage = getDefaultDialerPackage(userHandle);
+        if (!shouldManageNotificationThroughDefaultDialer(dialerPackage, userHandle)) {
             markMissedCallsAsRead(userHandle);
         }
         cancelMissedCallNotification(userHandle);
@@ -194,6 +205,15 @@
         }.prepare());
     }
 
+    private String getDefaultDialerPackage(UserHandle userHandle) {
+        String dialerPackage = mDefaultDialerCache.getDefaultDialerApplication(
+                userHandle.getIdentifier());
+        if (TextUtils.isEmpty(dialerPackage)) {
+            return null;
+        }
+        return dialerPackage;
+    }
+
     /**
      * Returns the missed-call notification intent to send to the default dialer for the given user.
      * Note, the passed in userHandle is always the non-managed user for SIM calls (multi-user
@@ -204,18 +224,16 @@
      * handle of the phone account. This could be a managed user. In that case we return the default
      * dialer for the given user which could be a managed (work profile) dialer.
      */
-    private Intent getShowMissedCallIntentForDefaultDialer(UserHandle userHandle) {
-        String dialerPackage = mDefaultDialerCache.getDefaultDialerApplication(
-                userHandle.getIdentifier());
-        if (TextUtils.isEmpty(dialerPackage)) {
-            return null;
-        }
+    private Intent getShowMissedCallIntentForDefaultDialer(String dialerPackage) {
         return new Intent(TelecomManager.ACTION_SHOW_MISSED_CALLS_NOTIFICATION)
             .setPackage(dialerPackage);
     }
 
-    private boolean shouldManageNotificationThroughDefaultDialer(UserHandle userHandle) {
-        Intent intent = getShowMissedCallIntentForDefaultDialer(userHandle);
+    private boolean shouldManageNotificationThroughDefaultDialer(String dialerPackage,
+            UserHandle userHandle) {
+        if (TextUtils.isEmpty(dialerPackage)) return false;
+
+        Intent intent = getShowMissedCallIntentForDefaultDialer(dialerPackage);
         if (intent == null) {
             return false;
         }
@@ -225,9 +243,29 @@
         return receivers.size() > 0;
     }
 
-    private void sendNotificationThroughDefaultDialer(CallInfo callInfo, UserHandle userHandle) {
+    /**
+     * For dialers that manage missed call handling themselves, we must temporarily add them to the
+     * power save exemption list, as they must perform operations such as modifying the call log and
+     * power save restrictions can cause these types of operations to not complete (sometimes
+     * causing ANRs).
+     */
+    private Bundle exemptFromPowerSavingTemporarily(String dialerPackage, UserHandle handle) {
+        if (TextUtils.isEmpty(dialerPackage)) {
+            return null;
+        }
+        BroadcastOptions bopts = BroadcastOptions.makeBasic();
+        long duration = Timeouts.getDialerMissedCallPowerSaveExemptionTimeMillis(
+                mContext.getContentResolver());
+        mDeviceIdleControllerAdapter.exemptAppTemporarilyForEvent(dialerPackage, duration,
+                handle.getIdentifier(), MISSED_CALL_POWER_SAVE_REASON);
+        bopts.setTemporaryAppWhitelistDuration(duration);
+        return bopts.toBundle();
+    }
+
+    private void sendNotificationThroughDefaultDialer(String dialerPackage, CallInfo callInfo,
+            UserHandle userHandle) {
         int count = mMissedCallCounts.get(userHandle).get();
-        Intent intent = getShowMissedCallIntentForDefaultDialer(userHandle)
+        Intent intent = getShowMissedCallIntentForDefaultDialer(dialerPackage)
             .setFlags(Intent.FLAG_RECEIVER_FOREGROUND)
             .putExtra(TelecomManager.EXTRA_CLEAR_MISSED_CALLS_INTENT,
                     createClearMissedCallsPendingIntent(userHandle))
@@ -248,7 +286,8 @@
 
 
         Log.w(this, "Showing missed calls through default dialer.");
-        mContext.sendBroadcastAsUser(intent, userHandle, READ_PHONE_STATE);
+        Bundle options = exemptFromPowerSavingTemporarily(dialerPackage, userHandle);
+        mContext.sendBroadcastAsUser(intent, userHandle, READ_PHONE_STATE, options);
     }
 
     /**
@@ -276,8 +315,9 @@
         mMissedCallCounts.putIfAbsent(userHandle, new AtomicInteger(0));
         int missCallCounts = mMissedCallCounts.get(userHandle).incrementAndGet();
 
-        if (shouldManageNotificationThroughDefaultDialer(userHandle)) {
-            sendNotificationThroughDefaultDialer(callInfo, userHandle);
+        String dialerPackage = getDefaultDialerPackage(userHandle);
+        if (shouldManageNotificationThroughDefaultDialer(dialerPackage, userHandle)) {
+            sendNotificationThroughDefaultDialer(dialerPackage, callInfo, userHandle);
             return;
         }
 
@@ -393,8 +433,9 @@
         mMissedCallCounts.putIfAbsent(userHandle, new AtomicInteger(0));
         mMissedCallCounts.get(userHandle).set(0);
 
-        if (shouldManageNotificationThroughDefaultDialer(userHandle)) {
-            sendNotificationThroughDefaultDialer(null, userHandle);
+        String dialerPackage = getDefaultDialerPackage(userHandle);
+        if (shouldManageNotificationThroughDefaultDialer(dialerPackage, userHandle)) {
+            sendNotificationThroughDefaultDialer(dialerPackage, null, userHandle);
             return;
         }
 
diff --git a/tests/src/com/android/server/telecom/tests/CallScreeningServiceFilterTest.java b/tests/src/com/android/server/telecom/tests/CallScreeningServiceFilterTest.java
index 1345c01..c7b3a7e 100644
--- a/tests/src/com/android/server/telecom/tests/CallScreeningServiceFilterTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallScreeningServiceFilterTest.java
@@ -23,6 +23,7 @@
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.nullable;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -211,6 +212,19 @@
 
     @SmallTest
     @Test
+    public void testUnbindingException() {
+        // Make sure that exceptions when unbinding won't make the device crash.
+        doThrow(new IllegalArgumentException()).when(mContext)
+                .unbindService(nullable(ServiceConnection.class));
+        CallScreeningServiceFilter filter = new CallScreeningServiceFilter(mCall, PKG_NAME,
+                CallScreeningServiceFilter.PACKAGE_TYPE_CARRIER, mContext, mCallsManager,
+                mAppLabelProxy, mParcelableCallUtilsConverter);
+        filter.startFilterLookup(inputResult);
+        filter.unbindCallScreeningService();
+    }
+
+    @SmallTest
+    @Test
     public void testAllowCall() throws Exception {
         CallScreeningServiceFilter filter = new CallScreeningServiceFilter(mCall, PKG_NAME,
                 CallScreeningServiceFilter.PACKAGE_TYPE_CARRIER, mContext, mCallsManager,
diff --git a/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java b/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
index 7effc47..af062d7 100644
--- a/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
+++ b/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
@@ -39,6 +39,7 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.ServiceConnection;
+import android.content.pm.ActivityInfo;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
@@ -52,6 +53,7 @@
 import android.os.Handler;
 import android.os.IInterface;
 import android.os.PersistableBundle;
+import android.os.PowerWhitelistManager;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.telecom.CallAudioState;
@@ -320,6 +322,12 @@
         }
 
         @Override
+        public void sendBroadcastAsUser(Intent intent, UserHandle user, String receiverPermission,
+                Bundle options) {
+            // Override so that this can be verified via spy.
+        }
+
+        @Override
         public void sendOrderedBroadcastAsUser(Intent intent, UserHandle user,
                 String receiverPermission, BroadcastReceiver resultReceiver, Handler scheduler,
                 int initialCode, String initialData, Bundle initialExtras) {
@@ -434,6 +442,7 @@
             ArrayListMultimap.create();
     private final Map<ComponentName, IInterface> mServiceByComponentName = new HashMap<>();
     private final Map<ComponentName, ServiceInfo> mServiceInfoByComponentName = new HashMap<>();
+    private final Map<ComponentName, ActivityInfo> mActivityInfoByComponentName = new HashMap<>();
     private final Map<IInterface, ComponentName> mComponentNameByService = new HashMap<>();
     private final Map<ServiceConnection, IInterface> mServiceByServiceConnection = new HashMap<>();
 
@@ -507,6 +516,24 @@
             }
         }).when(mPackageManager).queryIntentServicesAsUser((Intent) any(), anyInt(), anyInt());
 
+        doAnswer(new Answer<List<ResolveInfo>>() {
+            @Override
+            public List<ResolveInfo> answer(InvocationOnMock invocation) throws Throwable {
+                return doQueryIntentReceivers(
+                        (Intent) invocation.getArguments()[0],
+                        (Integer) invocation.getArguments()[1]);
+            }
+        }).when(mPackageManager).queryBroadcastReceivers((Intent) any(), anyInt());
+
+        doAnswer(new Answer<List<ResolveInfo>>() {
+            @Override
+            public List<ResolveInfo> answer(InvocationOnMock invocation) throws Throwable {
+                return doQueryIntentReceivers(
+                        (Intent) invocation.getArguments()[0],
+                        (Integer) invocation.getArguments()[1]);
+            }
+        }).when(mPackageManager).queryBroadcastReceiversAsUser((Intent) any(), anyInt(), anyInt());
+
         // By default, tests use non-ui apps instead of 3rd party companion apps.
         when(mPackageManager.checkPermission(
                 matches(Manifest.permission.CALL_COMPANION_APP), anyString()))
@@ -585,6 +612,14 @@
                 eq(componentName.getPackageName()))).thenReturn(PackageManager.PERMISSION_GRANTED);
     }
 
+    public void addIntentReceiver(String action, ComponentName name) {
+        mComponentNamesByAction.put(action, name);
+        ActivityInfo activityInfo = new ActivityInfo();
+        activityInfo.packageName = name.getPackageName();
+        activityInfo.name = name.getClassName();
+        mActivityInfoByComponentName.put(name, activityInfo);
+    }
+
     public void putResource(int id, final String value) {
         when(mResources.getText(eq(id))).thenReturn(value);
         when(mResources.getString(eq(id))).thenReturn(value);
@@ -635,4 +670,14 @@
         }
         return result;
     }
+
+    private List<ResolveInfo> doQueryIntentReceivers(Intent intent, int flags) {
+        List<ResolveInfo> result = new ArrayList<>();
+        for (ComponentName componentName : mComponentNamesByAction.get(intent.getAction())) {
+            ResolveInfo resolveInfo = new ResolveInfo();
+            resolveInfo.activityInfo = mActivityInfoByComponentName.get(componentName);
+            result.add(resolveInfo);
+        }
+        return result;
+    }
 }
diff --git a/tests/src/com/android/server/telecom/tests/InCallControllerTests.java b/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
index df3bf82..78b3332 100644
--- a/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
+++ b/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
@@ -23,6 +23,7 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.matches;
 import static org.mockito.ArgumentMatchers.nullable;
@@ -39,6 +40,7 @@
 import static org.mockito.Mockito.when;
 
 import android.Manifest;
+import android.app.AppOpsManager;
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.UiModeManager;
@@ -58,8 +60,8 @@
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.UserHandle;
+import android.telecom.CallAudioState;
 import android.telecom.InCallService;
-import android.telecom.Log;
 import android.telecom.ParcelableCall;
 import android.telecom.PhoneAccountHandle;
 import android.telecom.TelecomManager;
@@ -98,6 +100,7 @@
 
 import java.util.Collections;
 import java.util.LinkedList;
+import java.util.List;
 import java.util.concurrent.CompletableFuture;
 
 @RunWith(JUnit4.class)
@@ -109,6 +112,7 @@
     @Mock PackageManager mMockPackageManager;
     @Mock Call mMockCall;
     @Mock Resources mMockResources;
+    @Mock AppOpsManager mMockAppOpsManager;
     @Mock MockContext mMockContext;
     @Mock Timeouts.Adapter mTimeoutsAdapter;
     @Mock DefaultDialerCache mDefaultDialerCache;
@@ -133,6 +137,10 @@
     private static final String CAR2_CLASS = "carcls";
     private static final int CAR_UID = 4;
     private static final int CAR2_UID = 5;
+    private static final String NONUI_PKG = "nonui_pkg";
+    private static final String NONUI_CLASS = "nonui_cls";
+    private static final int NONUI_UID = 6;
+
     private static final PhoneAccountHandle PA_HANDLE =
             new PhoneAccountHandle(new ComponentName("pa_pkg", "pa_cls"), "pa_id");
 
@@ -148,6 +156,7 @@
         MockitoAnnotations.initMocks(this);
         when(mMockCall.getAnalytics()).thenReturn(new Analytics.CallInfo());
         doReturn(mMockResources).when(mMockContext).getResources();
+        doReturn(mMockAppOpsManager).when(mMockContext).getSystemService(AppOpsManager.class);
         doReturn(SYS_PKG).when(mMockResources).getString(
                 com.android.internal.R.string.config_defaultDialer);
         doReturn(SYS_CLASS).when(mMockResources).getString(R.string.incall_default_class);
@@ -158,11 +167,11 @@
         mEmergencyCallHelper = new EmergencyCallHelper(mMockContext, mDefaultDialerCache,
                 mTimeoutsAdapter);
         when(mMockCallsManager.getRoleManagerAdapter()).thenReturn(mMockRoleManagerAdapter);
+        when(mMockContext.getSystemService(eq(Context.NOTIFICATION_SERVICE)))
+                .thenReturn(mNotificationManager);
         mInCallController = new InCallController(mMockContext, mLock, mMockCallsManager,
                 mMockSystemStateHelper, mDefaultDialerCache, mTimeoutsAdapter,
                 mEmergencyCallHelper, new CarModeTracker(), mClockProxy);
-        when(mMockContext.getSystemService(eq(Context.NOTIFICATION_SERVICE)))
-                .thenReturn(mNotificationManager);
         // Companion Apps don't have CONTROL_INCALL_EXPERIENCE permission.
         doAnswer(invocation -> {
             int uid = invocation.getArgument(0);
@@ -177,6 +186,8 @@
                     return new String[] { CAR_PKG };
                 case CAR2_UID:
                     return new String[] { CAR2_PKG };
+                case NONUI_UID:
+                    return new String[] { NONUI_PKG };
             }
             return null;
         }).when(mMockPackageManager).getPackagesForUid(anyInt());
@@ -189,6 +200,10 @@
         when(mMockPackageManager.checkPermission(
                 matches(Manifest.permission.CONTROL_INCALL_EXPERIENCE),
                 matches(CAR2_PKG))).thenReturn(PackageManager.PERMISSION_GRANTED);
+        when(mMockPackageManager.checkPermission(
+                matches(Manifest.permission.CONTROL_INCALL_EXPERIENCE),
+                matches(NONUI_PKG))).thenReturn(PackageManager.PERMISSION_GRANTED);
+        when(mMockCallsManager.getAudioState()).thenReturn(new CallAudioState(false, 0, 0));
     }
 
     @Override
@@ -886,13 +901,15 @@
                 nullable(ContentResolver.class))).thenReturn(500L);
 
         when(mMockCallsManager.getCalls()).thenReturn(Collections.singletonList(mMockCall));
-        setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
+        setupMockPackageManager(true /* default */, true /* nonui */, true /* system */,
+                false /* external calls */,
+                false /* self mgd in default*/, false /* self mgd in car*/);
         mInCallController.bindToServices(mMockCall);
 
         ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
         ArgumentCaptor<ServiceConnection> serviceConnectionCaptor =
                 ArgumentCaptor.forClass(ServiceConnection.class);
-        verify(mMockContext, times(1)).bindServiceAsUser(
+        verify(mMockContext, times(2)).bindServiceAsUser(
                 bindIntentCaptor.capture(),
                 serviceConnectionCaptor.capture(),
                 eq(Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE
@@ -905,13 +922,39 @@
 
         // Start the connection, make sure we don't unbind, and make sure that we don't send
         // anything to the in-call service yet.
-        ServiceConnection serviceConnection = serviceConnectionCaptor.getValue();
+        List<ServiceConnection> serviceConnections = serviceConnectionCaptor.getAllValues();
+        List<Intent> intents = bindIntentCaptor.getAllValues();
+
+        // Find the non-ui service and have it connect first.
+        int nonUiIdx = findFirstIndexMatching(intents,
+                i -> NONUI_PKG.equals(i.getComponent().getPackageName()));
+        if (nonUiIdx < 0) {
+            fail("Did not bind to non-ui incall");
+        }
+
+        {
+            ComponentName nonUiComponentName = new ComponentName(NONUI_PKG, NONUI_CLASS);
+            IBinder mockBinder = mock(IBinder.class);
+            IInCallService mockInCallService = mock(IInCallService.class);
+            when(mockBinder.queryLocalInterface(anyString())).thenReturn(mockInCallService);
+            serviceConnections.get(nonUiIdx).onServiceConnected(nonUiComponentName, mockBinder);
+
+            // Make sure the non-ui binding didn't trigger the future.
+            assertFalse(bindTimeout.isDone());
+        }
+
+        int defDialerIdx = findFirstIndexMatching(intents,
+                i -> DEF_PKG.equals(i.getComponent().getPackageName()));
+        if (defDialerIdx < 0) {
+            fail("Did not bind to default dialer incall");
+        }
+
         ComponentName defDialerComponentName = new ComponentName(DEF_PKG, DEF_CLASS);
         IBinder mockBinder = mock(IBinder.class);
         IInCallService mockInCallService = mock(IInCallService.class);
         when(mockBinder.queryLocalInterface(anyString())).thenReturn(mockInCallService);
 
-        serviceConnection.onServiceConnected(defDialerComponentName, mockBinder);
+        serviceConnections.get(defDialerIdx).onServiceConnected(defDialerComponentName, mockBinder);
         verify(mockInCallService).setInCallAdapter(nullable(IInCallAdapter.class));
 
         // Make sure that the future completed without timing out.
@@ -1103,9 +1146,21 @@
         }};
     }
 
+    private ResolveInfo getNonUiResolveinfo() {
+        return new ResolveInfo() {{
+            serviceInfo = new ServiceInfo();
+            serviceInfo.packageName = NONUI_PKG;
+            serviceInfo.name = NONUI_CLASS;
+            serviceInfo.applicationInfo = new ApplicationInfo();
+            serviceInfo.applicationInfo.uid = NONUI_UID;
+            serviceInfo.enabled = true;
+            serviceInfo.permission = Manifest.permission.BIND_INCALL_SERVICE;
+        }};
+    }
+
     private void setupMockPackageManager(final boolean useDefaultDialer,
             final boolean useSystemDialer, final boolean includeExternalCalls) {
-        setupMockPackageManager(useDefaultDialer, useSystemDialer, includeExternalCalls,
+        setupMockPackageManager(useDefaultDialer, false, useSystemDialer, includeExternalCalls,
                 false /* self mgd */, false /* self mgd */);
     }
 
@@ -1113,6 +1168,16 @@
             final boolean useSystemDialer, final boolean includeExternalCalls,
             final boolean includeSelfManagedCallsInDefaultDialer,
             final boolean includeSelfManagedCallsInCarModeDialer) {
+        setupMockPackageManager(useDefaultDialer, false /* nonui */, useSystemDialer,
+                includeExternalCalls, includeSelfManagedCallsInDefaultDialer,
+                includeSelfManagedCallsInCarModeDialer);
+    }
+
+    private void setupMockPackageManager(final boolean useDefaultDialer,
+            final boolean useNonUiInCalls,
+            final boolean useSystemDialer, final boolean includeExternalCalls,
+            final boolean includeSelfManagedCallsInDefaultDialer,
+            final boolean includeSelfManagedCallsInCarModeDialer) {
         doAnswer(new Answer() {
             @Override
             public Object answer(InvocationOnMock invocation) throws Throwable {
@@ -1147,7 +1212,13 @@
                         resolveInfo.add(getCarModeResolveinfo(CAR2_PKG, CAR2_CLASS,
                                 includeExternalCalls, includeSelfManagedCallsInCarModeDialer));
                     }
+                } else {
+                    // InCallController uses a blank package name when querying for non-ui incalls
+                    if (useNonUiInCalls) {
+                        resolveInfo.add(getNonUiResolveinfo());
+                    }
                 }
+
                 return resolveInfo;
             }
         }).when(mMockPackageManager).queryIntentServicesAsUser(
diff --git a/tests/src/com/android/server/telecom/tests/MissedCallNotifierImplTest.java b/tests/src/com/android/server/telecom/tests/MissedCallNotifierImplTest.java
index 10a0194..9dd90f5 100644
--- a/tests/src/com/android/server/telecom/tests/MissedCallNotifierImplTest.java
+++ b/tests/src/com/android/server/telecom/tests/MissedCallNotifierImplTest.java
@@ -16,6 +16,10 @@
 
 package com.android.server.telecom.tests;
 
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertTrue;
+
+import android.app.BroadcastOptions;
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
@@ -43,6 +47,7 @@
 import com.android.server.telecom.CallerInfoLookupHelper;
 import com.android.server.telecom.Constants;
 import com.android.server.telecom.DefaultDialerCache;
+import com.android.server.telecom.DeviceIdleControllerAdapter;
 import com.android.server.telecom.MissedCallNotifier;
 import com.android.server.telecom.PhoneAccountRegistrar;
 import com.android.server.telecom.TelecomBroadcastIntentProcessor;
@@ -67,6 +72,8 @@
 
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.nullable;
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.anyString;
@@ -143,6 +150,9 @@
     private static final UserHandle SECONARY_USER = UserHandle.of(12);
     private static final int NO_CAPABILITY = 0;
     private static final int TEST_TIMEOUT = 1000;
+    private static final long TEST_POWER_EXEMPT_TIME_MS = 1000;
+    private static final ComponentName COMPONENT_NAME = new ComponentName(
+            "com.anything", "com.whatever");
 
     @Mock
     private NotificationManager mNotificationManager;
@@ -155,6 +165,7 @@
 
     @Mock TelecomSystem mTelecomSystem;
     @Mock private DefaultDialerCache mDefaultDialerCache;
+    @Mock private DeviceIdleControllerAdapter mDeviceIdleControllerAdapter;
 
     @Override
     @Before
@@ -246,7 +257,8 @@
                 makeNotificationBuilderFactory(builders);
 
         MissedCallNotifier missedCallNotifier = new MissedCallNotifierImpl(mContext,
-                mPhoneAccountRegistrar, mDefaultDialerCache, fakeBuilderFactory);
+                mPhoneAccountRegistrar, mDefaultDialerCache, fakeBuilderFactory,
+                mDeviceIdleControllerAdapter);
 
         missedCallNotifier.showMissedCallNotification(fakeCall);
         missedCallNotifier.showMissedCallNotification(fakeCall);
@@ -401,7 +413,8 @@
                 makeNotificationBuilderFactory(builder1);
 
         MissedCallNotifier missedCallNotifier = new MissedCallNotifierImpl(mContext,
-                mPhoneAccountRegistrar, mDefaultDialerCache, fakeBuilderFactory);
+                mPhoneAccountRegistrar, mDefaultDialerCache, fakeBuilderFactory,
+                mDeviceIdleControllerAdapter);
         PhoneAccount phoneAccount = makePhoneAccount(PRIMARY_USER, NO_CAPABILITY);
 
         MissedCallNotifier.CallInfo fakeCall =
@@ -461,7 +474,8 @@
                 makeNotificationBuilderFactory(builder1);
 
         MissedCallNotifier missedCallNotifier = new MissedCallNotifierImpl(mContext,
-                mPhoneAccountRegistrar, mDefaultDialerCache, fakeBuilderFactory);
+                mPhoneAccountRegistrar, mDefaultDialerCache, fakeBuilderFactory,
+                mDeviceIdleControllerAdapter);
 
         // AsyncQueryHandler used in reloadFromDatabase interacts poorly with the below
         // timeout-verify, so run this in a new handler to mitigate that.
@@ -530,7 +544,8 @@
                 makeNotificationBuilderFactory(builder1);
 
         MissedCallNotifier missedCallNotifier = new MissedCallNotifierImpl(mContext,
-                mPhoneAccountRegistrar, mDefaultDialerCache, fakeBuilderFactory);
+                mPhoneAccountRegistrar, mDefaultDialerCache, fakeBuilderFactory,
+                mDeviceIdleControllerAdapter);
 
         // AsyncQueryHandler used in reloadFromDatabase interacts poorly with the below
         // timeout-verify, so run this in a new handler to mitigate that.
@@ -560,6 +575,49 @@
                 nullable(Notification.class), eq(PRIMARY_USER));
     }
 
+    @SmallTest
+    @Test
+    public void testDialerHandleMissedCall() {
+        // Configure Notifier to send missed call intent and let dialer handle
+        enableDialerHandlesMissedCall();
+
+        Notification.Builder builder1 = makeNotificationBuilder("builder1");
+        MissedCallNotifierImpl.NotificationBuilderFactory fakeBuilderFactory =
+                makeNotificationBuilderFactory(builder1);
+
+        MissedCallNotifier missedCallNotifier = new MissedCallNotifierImpl(mContext,
+                mPhoneAccountRegistrar, mDefaultDialerCache, fakeBuilderFactory,
+                mDeviceIdleControllerAdapter);
+        PhoneAccount phoneAccount = makePhoneAccount(PRIMARY_USER, NO_CAPABILITY);
+
+        MissedCallNotifier.CallInfo fakeCall =
+                makeFakeCallInfo(SIP_CALL_HANDLE, CALLER_NAME, CALL_TIMESTAMP,
+                        phoneAccount.getAccountHandle());
+        missedCallNotifier.showMissedCallNotification(fakeCall);
+
+        ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
+        ArgumentCaptor<Bundle> bundleCaptor =
+                ArgumentCaptor.forClass(Bundle.class);
+        verify(mDeviceIdleControllerAdapter).exemptAppTemporarilyForEvent(
+                eq(COMPONENT_NAME.getPackageName()), anyLong(), anyInt(), any());
+        verify(mContext).sendBroadcastAsUser(
+                intentCaptor.capture(),
+                any(),
+                eq(android.Manifest.permission.READ_PHONE_STATE), bundleCaptor.capture());
+        assertNotNull("Not expecting null intent", intentCaptor.getValue());
+        assertEquals("Incorrect intent received",
+                TelecomManager.ACTION_SHOW_MISSED_CALLS_NOTIFICATION,
+                intentCaptor.getValue().getAction());
+        assertNotNull("Not expecting null options bundle", bundleCaptor.getValue());
+        BroadcastOptions options = new BroadcastOptions(bundleCaptor.getValue());
+        assertTrue("App must have a temporary exemption set.",
+                options.getTemporaryAppWhitelistDuration() > 0);
+
+        // A notification should never be posted by Telecom
+        verify(mNotificationManager, never()).notifyAsUser(nullable(String.class), anyInt(),
+                nullable(Notification.class), eq(PRIMARY_USER));
+    }
+
     private Notification.Builder makeNotificationBuilder(String label) {
         Notification.Builder builder = spy(new Notification.Builder(mContext));
         Notification notification = mock(Notification.class);
@@ -592,14 +650,14 @@
     private MissedCallNotifier makeMissedCallNotifier(
             NotificationBuilderFactory fakeBuilderFactory, UserHandle currentUser) {
         MissedCallNotifier missedCallNotifier = new MissedCallNotifierImpl(mContext,
-                mPhoneAccountRegistrar, mDefaultDialerCache, fakeBuilderFactory);
+                mPhoneAccountRegistrar, mDefaultDialerCache, fakeBuilderFactory,
+                mDeviceIdleControllerAdapter);
         missedCallNotifier.setCurrentUserHandle(currentUser);
         return missedCallNotifier;
     }
 
     private PhoneAccount makePhoneAccount(UserHandle userHandle, int capability) {
-        ComponentName componentName = new ComponentName("com.anything", "com.whatever");
-        PhoneAccountHandle phoneAccountHandle = new PhoneAccountHandle(componentName, "id",
+        PhoneAccountHandle phoneAccountHandle = new PhoneAccountHandle(COMPONENT_NAME, "id",
                 userHandle);
         PhoneAccount.Builder builder = new PhoneAccount.Builder(phoneAccountHandle, "test");
         builder.setCapabilities(capability);
@@ -609,6 +667,13 @@
         return phoneAccount;
     }
 
+    private void enableDialerHandlesMissedCall() {
+        doReturn(COMPONENT_NAME.getPackageName()).when(mDefaultDialerCache).
+                getDefaultDialerApplication(anyInt());
+        mComponentContextFixture.addIntentReceiver(
+                TelecomManager.ACTION_SHOW_MISSED_CALLS_NOTIFICATION, COMPONENT_NAME);
+    }
+
     private IContentProvider getContentProviderForUser(int userId) {
         return mContext.getContentResolver().acquireProvider(userId + "@call_log");
     }
diff --git a/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java b/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
index 442c310..68b10c6 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
@@ -38,6 +38,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.app.AppOpsManager;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.ContentResolver;
@@ -78,6 +79,7 @@
 import com.android.server.telecom.ClockProxy;
 import com.android.server.telecom.ConnectionServiceFocusManager;
 import com.android.server.telecom.ContactsAsyncHelper;
+import com.android.server.telecom.DeviceIdleControllerAdapter;
 import com.android.server.telecom.HeadsetMediaButton;
 import com.android.server.telecom.HeadsetMediaButtonFactory;
 import com.android.server.telecom.InCallWakeLockController;
@@ -207,6 +209,7 @@
     @Mock ClockProxy mClockProxy;
     @Mock RoleManagerAdapter mRoleManagerAdapter;
     @Mock ToneGenerator mToneGenerator;
+    @Mock DeviceIdleControllerAdapter mDeviceIdleControllerAdapter;
 
     final ComponentName mInCallServiceComponentNameX =
             new ComponentName(
@@ -348,6 +351,8 @@
         doReturn(mSpyContext).when(mSpyContext).getApplicationContext();
         doNothing().when(mSpyContext).sendBroadcastAsUser(any(), any(), any());
 
+        doReturn(mock(AppOpsManager.class)).when(mSpyContext).getSystemService(AppOpsManager.class);
+
         mHandlerThread = new HandlerThread("TelecomHandlerThread");
         mHandlerThread.start();
 
@@ -475,7 +480,8 @@
         when(mRoleManagerAdapter.getDefaultCallScreeningApp()).thenReturn(null);
         mTelecomSystem = new TelecomSystem(
                 mComponentContextFixture.getTestDouble(),
-                (context, phoneAccountRegistrar, defaultDialerCache) -> mMissedCallNotifier,
+                (context, phoneAccountRegistrar, defaultDialerCache, mDeviceIdleControllerAdapter)
+                        -> mMissedCallNotifier,
                 mCallerInfoAsyncQueryFactoryFixture.getTestDouble(),
                 headsetMediaButtonFactory,
                 proximitySensorManagerFactory,
@@ -535,7 +541,7 @@
                             ContactsAsyncHelper.ContentResolverAdapter adapter) {
                         return new ContactsAsyncHelper(adapter, mHandlerThread.getLooper());
                     }
-                });
+                }, mDeviceIdleControllerAdapter);
 
         mComponentContextFixture.setTelecomManager(new TelecomManager(
                 mComponentContextFixture.getTestDouble(),
diff --git a/tests/src/com/android/server/telecom/tests/TelecomTestCase.java b/tests/src/com/android/server/telecom/tests/TelecomTestCase.java
index b0b1ec0..264e087 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomTestCase.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomTestCase.java
@@ -25,8 +25,10 @@
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
+import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
+import java.util.function.Predicate;
 
 public abstract class TelecomTestCase {
     protected static final String TESTING_TAG = "Telecom-TEST";
@@ -75,4 +77,13 @@
             }
         }
     }
+
+    protected static <T> int findFirstIndexMatching(List<T> items, Predicate<T> matcher) {
+        for (int i = 0; i < items.size(); i++) {
+            if (matcher.test(items.get(i))) {
+                return i;
+            }
+        }
+        return -1;
+    }
 }