Add prioritized enableCarMode API in UiModeManager

Per design doc (go/android-car-mode-design), added new system API to
enable car mode and specify a priority for the calling app.
Also modified UiModeManager to pass the package name of the caller to
UiModeManagerService.

Bug: 136109592
Test: Added new unit tests and CTS tests.
Test: Added Telecom test app functionality to verify.
Change-Id: I2848039c9ea18ba93e7694e04c4e5dc70759daa3
Merged-In: I2848039c9ea18ba93e7694e04c4e5dc70759daa3
diff --git a/api/system-current.txt b/api/system-current.txt
index 3393112..1d443ae 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -66,6 +66,7 @@
     field public static final String CRYPT_KEEPER = "android.permission.CRYPT_KEEPER";
     field public static final String DEVICE_POWER = "android.permission.DEVICE_POWER";
     field public static final String DISPATCH_PROVISIONING_MESSAGE = "android.permission.DISPATCH_PROVISIONING_MESSAGE";
+    field public static final String ENTER_CAR_MODE_PRIORITIZED = "android.permission.ENTER_CAR_MODE_PRIORITIZED";
     field public static final String FORCE_BACK = "android.permission.FORCE_BACK";
     field public static final String FORCE_STOP_PACKAGES = "android.permission.FORCE_STOP_PACKAGES";
     field public static final String GET_APP_OPS_STATS = "android.permission.GET_APP_OPS_STATS";
@@ -74,6 +75,7 @@
     field public static final String GET_TOP_ACTIVITY_INFO = "android.permission.GET_TOP_ACTIVITY_INFO";
     field public static final String GRANT_PROFILE_OWNER_DEVICE_IDS_ACCESS = "android.permission.GRANT_PROFILE_OWNER_DEVICE_IDS_ACCESS";
     field public static final String GRANT_RUNTIME_PERMISSIONS = "android.permission.GRANT_RUNTIME_PERMISSIONS";
+    field public static final String HANDLE_CAR_MODE_CHANGES = "android.permission.HANDLE_CAR_MODE_CHANGES";
     field public static final String HARDWARE_TEST = "android.permission.HARDWARE_TEST";
     field public static final String HDMI_CEC = "android.permission.HDMI_CEC";
     field public static final String HIDE_NON_SYSTEM_OVERLAY_WINDOWS = "android.permission.HIDE_NON_SYSTEM_OVERLAY_WINDOWS";
@@ -602,6 +604,15 @@
     method public boolean isStatusBarExpansionDisabled();
   }
 
+  public class UiModeManager {
+    method @RequiresPermission(android.Manifest.permission.ENTER_CAR_MODE_PRIORITIZED) public void enableCarMode(@IntRange(from=0) int, int);
+    field public static final String ACTION_ENTER_CAR_MODE_PRIORITIZED = "android.app.action.ENTER_CAR_MODE_PRIORITIZED";
+    field public static final String ACTION_EXIT_CAR_MODE_PRIORITIZED = "android.app.action.EXIT_CAR_MODE_PRIORITIZED";
+    field public static final int DEFAULT_PRIORITY = 0; // 0x0
+    field public static final String EXTRA_CALLING_PACKAGE = "android.app.extra.CALLING_PACKAGE";
+    field public static final String EXTRA_PRIORITY = "android.app.extra.PRIORITY";
+  }
+
   public final class Vr2dDisplayProperties implements android.os.Parcelable {
     ctor public Vr2dDisplayProperties(int, int, int);
     method public int describeContents();
diff --git a/api/test-current.txt b/api/test-current.txt
index 293ca55..8ff063f 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -376,6 +376,7 @@
   }
 
   public class UiModeManager {
+    method @RequiresPermission("android.permission.ENTER_CAR_MODE_PRIORITIZED") public void enableCarMode(@IntRange(from=0) int, int);
     method public boolean isNightModeLocked();
     method public boolean isUiModeLocked();
   }
diff --git a/core/java/android/app/IUiModeManager.aidl b/core/java/android/app/IUiModeManager.aidl
index f2c9f61..a3e0845 100644
--- a/core/java/android/app/IUiModeManager.aidl
+++ b/core/java/android/app/IUiModeManager.aidl
@@ -25,7 +25,7 @@
      * Enables the car mode. Only the system can do this.
      * @hide
      */
-    void enableCarMode(int flags);
+    void enableCarMode(int flags, int priority, String callingPackage);
 
     /**
      * Disables the car mode.
@@ -34,6 +34,12 @@
     void disableCarMode(int flags);
 
     /**
+     * Disables car mode (the original version is marked unsupported app usage so cannot be changed
+     * for the time being).
+     */
+    void disableCarModeByCallingPackage(int flags, String callingPackage);
+
+    /**
      * Return the current running mode.
      */
     int getCurrentModeType();
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index 646f0dc..2cb0794 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -671,7 +671,7 @@
                 new CachedServiceFetcher<UiModeManager>() {
             @Override
             public UiModeManager createService(ContextImpl ctx) throws ServiceNotFoundException {
-                return new UiModeManager();
+                return new UiModeManager(ctx.getOuterContext());
             }});
 
         registerService(Context.USB_SERVICE, UsbManager.class,
diff --git a/core/java/android/app/UiModeManager.java b/core/java/android/app/UiModeManager.java
index 46316e1..8324787 100644
--- a/core/java/android/app/UiModeManager.java
+++ b/core/java/android/app/UiModeManager.java
@@ -17,6 +17,10 @@
 package android.app;
 
 import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
 import android.annotation.SystemService;
 import android.annotation.TestApi;
 import android.annotation.UnsupportedAppUsage;
@@ -68,6 +72,25 @@
      * of the broadcast to {@link Activity#RESULT_CANCELED}.
      */
     public static String ACTION_ENTER_CAR_MODE = "android.app.action.ENTER_CAR_MODE";
+
+    /**
+     * Broadcast sent when the device's UI has switched to car mode, either by being placed in a car
+     * dock or explicit action of the user.
+     * <p>
+     * In addition to the behavior for {@link #ACTION_ENTER_CAR_MODE}, this broadcast includes the
+     * package name of the app which requested to enter car mode in the
+     * {@link #EXTRA_CALLING_PACKAGE}.  If an app requested to enter car mode using
+     * {@link #enableCarMode(int, int)} and specified a priority this will be specified in the
+     * {@link #EXTRA_PRIORITY}.
+     *
+     * This is primarily intended to be received by other components of the Android OS.
+     * <p>
+     * Receiver requires permission: {@link android.Manifest.permission.HANDLE_CAR_MODE_CHANGES}
+     * @hide
+     */
+    @SystemApi
+    public static final String ACTION_ENTER_CAR_MODE_PRIORITIZED =
+            "android.app.action.ENTER_CAR_MODE_PRIORITIZED";
     
     /**
      * Broadcast sent when the device's UI has switch away from car mode back
@@ -75,6 +98,28 @@
      * when the user exits car mode.
      */
     public static String ACTION_EXIT_CAR_MODE = "android.app.action.EXIT_CAR_MODE";
+
+    /**
+     * Broadcast sent when the device's UI has switched away from car mode back to normal mode.
+     * Typically used by a car mode app, to dismiss itself when the user exits car mode.
+     * <p>
+     * In addition to the behavior for {@link #ACTION_EXIT_CAR_MODE}, this broadcast includes the
+     * package name of the app which requested to exit car mode in {@link #EXTRA_CALLING_PACKAGE}.
+     * If an app requested to enter car mode using {@link #enableCarMode(int, int)} and specified a
+     * priority this will be specified in the {@link #EXTRA_PRIORITY} when exiting car mode.
+     * <p>
+     * If {@link #DISABLE_CAR_MODE_ALL_PRIORITIES} is used when disabling car mode (i.e. this is
+     * initiated by the user via the persistent car mode notification), this broadcast is sent once
+     * for each priority level for which car mode is being disabled.
+     * <p>
+     * This is primarily intended to be received by other components of the Android OS.
+     * <p>
+     * Receiver requires permission: {@link android.Manifest.permission.HANDLE_CAR_MODE_CHANGES}
+     * @hide
+     */
+    @SystemApi
+    public static final String ACTION_EXIT_CAR_MODE_PRIORITIZED =
+            "android.app.action.EXIT_CAR_MODE_PRIORITIZED";
     
     /**
      * Broadcast sent when the device's UI has switched to desk mode,
@@ -97,6 +142,24 @@
      */
     public static String ACTION_EXIT_DESK_MODE = "android.app.action.EXIT_DESK_MODE";
 
+    /**
+     * String extra used with {@link #ACTION_ENTER_CAR_MODE_PRIORITIZED} and
+     * {@link #ACTION_EXIT_CAR_MODE_PRIORITIZED} to indicate the package name of the app which
+     * requested to enter or exit car mode.
+     * @hide
+     */
+    @SystemApi
+    public static final String EXTRA_CALLING_PACKAGE = "android.app.extra.CALLING_PACKAGE";
+
+    /**
+     * Integer extra used with {@link #ACTION_ENTER_CAR_MODE_PRIORITIZED} and
+     * {@link #ACTION_EXIT_CAR_MODE_PRIORITIZED} to indicate the priority level at which car mode
+     * is being disabled.
+     * @hide
+     */
+    @SystemApi
+    public static final String EXTRA_PRIORITY = "android.app.extra.PRIORITY";
+
     /** @hide */
     @IntDef(prefix = { "MODE_" }, value = {
             MODE_NIGHT_AUTO,
@@ -126,10 +189,21 @@
 
     private IUiModeManager mService;
 
+    /**
+     * Context required for getting the opPackageName of API caller; maybe be {@code null} if the
+     * old constructor marked with UnSupportedAppUsage is used.
+     */
+    private @Nullable Context mContext;
+
     @UnsupportedAppUsage
     /*package*/ UiModeManager() throws ServiceNotFoundException {
+        this(null /* context */);
+    }
+
+    /*package*/ UiModeManager(Context context) throws ServiceNotFoundException {
         mService = IUiModeManager.Stub.asInterface(
                 ServiceManager.getServiceOrThrow(Context.UI_MODE_SERVICE));
+        mContext = context;
     }
 
     /**
@@ -152,6 +226,14 @@
      */
     public static final int ENABLE_CAR_MODE_ALLOW_SLEEP = 0x0002;
 
+    /** @hide */
+    @IntDef(prefix = { "ENABLE_CAR_MODE_" }, value = {
+            ENABLE_CAR_MODE_GO_CAR_HOME,
+            ENABLE_CAR_MODE_ALLOW_SLEEP
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface EnableCarMode {}
+
     /**
      * Force device into car mode, like it had been placed in the car dock.
      * This will cause the device to switch to the car home UI as part of
@@ -159,9 +241,54 @@
      * @param flags Must be 0.
      */
     public void enableCarMode(int flags) {
+        enableCarMode(DEFAULT_PRIORITY, flags);
+    }
+
+    /**
+     * Force device into car mode, like it had been placed in the car dock.  This will cause the
+     * device to switch to the car home UI as part of the mode switch.
+     * <p>
+     * An app may request to enter car mode when the system is already in car mode.  The app may
+     * specify a "priority" when entering car mode.  The device will remain in car mode
+     * (i.e. {@link #getCurrentModeType()} is {@link Configuration#UI_MODE_TYPE_CAR}) as long as
+     * there is a priority level at which car mode have been enabled.  For example assume app A
+     * enters car mode at priority level 100, and then app B enters car mode at the default priority
+     * (0).  If app A exits car mode, the device will remain in car mode until app B exits car mode.
+     * <p>
+     * Specifying a priority level when entering car mode is important in cases where multiple apps
+     * on a device implement a car-mode {@link android.telecom.InCallService} (see
+     * {@link android.telecom.TelecomManager#METADATA_IN_CALL_SERVICE_CAR_MODE_UI}).  The
+     * {@link android.telecom.InCallService} associated with the highest priority app which entered
+     * car mode will be bound to by Telecom and provided with information about ongoing calls on
+     * the device.
+     * <p>
+     * System apps holding the required permission can enable car mode when the app determines the
+     * correct conditions exist for that app to be in car mode.  The device maker should ensure that
+     * where multiple apps exist on the device which can potentially enter car mode, appropriate
+     * priorities are used to ensure that calls delivered by the
+     * {@link android.telecom.InCallService} API are delivered to the highest priority app.
+     * If app A and app B can both potentially enable car mode, and it is desired that app B is the
+     * one which should receive call information, the priority for app B should be higher than the
+     * one for app A.
+     * <p>
+     * When an app uses a priority to enable car mode, they can disable car mode at the specified
+     * priority level using {@link #disableCarMode(int)}.  An app may only enable car mode at a
+     * single priority.
+     * <p>
+     * Public apps are assumed to enter/exit car mode at {@link #DEFAULT_PRIORITY}.
+     *
+     * @param priority The declared priority for the caller.
+     * @param flags Car mode flags.
+     * @hide
+     */
+    @SystemApi
+    @TestApi
+    @RequiresPermission(android.Manifest.permission.ENTER_CAR_MODE_PRIORITIZED)
+    public void enableCarMode(@IntRange(from = 0) int priority, @EnableCarMode int flags) {
         if (mService != null) {
             try {
-                mService.enableCarMode(flags);
+                mService.enableCarMode(flags, priority,
+                        mContext == null ? null : mContext.getOpPackageName());
             } catch (RemoteException e) {
                 throw e.rethrowFromSystemServer();
             }
@@ -176,15 +303,44 @@
      * being in car mode).
      */
     public static final int DISABLE_CAR_MODE_GO_HOME = 0x0001;
+
+    /**
+     * Flag for use with {@link #disableCarMode(int)}: Disables car mode at ALL priority levels.
+     * Primarily intended for use from {@link com.android.internal.app.DisableCarModeActivity} to
+     * provide the user with a means to exit car mode at all priority levels.
+     * @hide
+     */
+    public static final int DISABLE_CAR_MODE_ALL_PRIORITIES = 0x0002;
+
+    /** @hide */
+    @IntDef(prefix = { "DISABLE_CAR_MODE_" }, value = {
+            DISABLE_CAR_MODE_GO_HOME
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface DisableCarMode {}
+
+    /**
+     * The default priority used for entering car mode.
+     * <p>
+     * Callers of the {@link UiModeManager#enableCarMode(int)} priority will be assigned the
+     * default priority.
+     * <p>
+     * System apps can specify a priority other than the default priority when using
+     * {@link UiModeManager#enableCarMode(int, int)} to enable car mode.
+     * @hide
+     */
+    @SystemApi
+    public static final int DEFAULT_PRIORITY = 0;
     
     /**
      * Turn off special mode if currently in car mode.
-     * @param flags May be 0 or {@link #DISABLE_CAR_MODE_GO_HOME}.
+     * @param flags One of the disable car mode flags.
      */
-    public void disableCarMode(int flags) {
+    public void disableCarMode(@DisableCarMode int flags) {
         if (mService != null) {
             try {
-                mService.disableCarMode(flags);
+                mService.disableCarModeByCallingPackage(flags,
+                        mContext == null ? null : mContext.getOpPackageName());
             } catch (RemoteException e) {
                 throw e.rethrowFromSystemServer();
             }
diff --git a/core/java/com/android/internal/app/DisableCarModeActivity.java b/core/java/com/android/internal/app/DisableCarModeActivity.java
index 7943c61..d44312b 100644
--- a/core/java/com/android/internal/app/DisableCarModeActivity.java
+++ b/core/java/com/android/internal/app/DisableCarModeActivity.java
@@ -33,7 +33,9 @@
         try {
             IUiModeManager uiModeManager = IUiModeManager.Stub.asInterface(
                     ServiceManager.getService("uimode"));
-            uiModeManager.disableCarMode(UiModeManager.DISABLE_CAR_MODE_GO_HOME);
+            uiModeManager.disableCarModeByCallingPackage(UiModeManager.DISABLE_CAR_MODE_GO_HOME
+                            | UiModeManager.DISABLE_CAR_MODE_ALL_PRIORITIES,
+                    getOpPackageName());
         } catch (RemoteException e) {
             Log.e(TAG, "Failed to disable car mode", e);
         }
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 1f20d7a..c889474 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -108,6 +108,8 @@
 
     <protected-broadcast android:name="android.app.action.ENTER_CAR_MODE" />
     <protected-broadcast android:name="android.app.action.EXIT_CAR_MODE" />
+    <protected-broadcast android:name="android.app.action.ENTER_CAR_MODE_PRIVILEGED" />
+    <protected-broadcast android:name="android.app.action.EXIT_CAR_MODE_PRIVILEGED" />
     <protected-broadcast android:name="android.app.action.ENTER_DESK_MODE" />
     <protected-broadcast android:name="android.app.action.EXIT_DESK_MODE" />
     <protected-broadcast android:name="android.app.action.NEXT_ALARM_CLOCK_CHANGED" />
@@ -4339,6 +4341,21 @@
          it will be ignored.
         @hide -->
     <permission android:name="android.permission.MODIFY_DAY_NIGHT_MODE"
+      android:protectionLevel="signature|privileged" />
+
+    <!-- @SystemApi Allows entering or exiting car mode using a specified priority.
+        This permission is required to use UiModeManager while specifying a priority for the calling
+        app.  A device manufacturer uses this permission to prioritize the apps which can
+        potentially request to enter car-mode on a device to help establish the correct behavior
+        where multiple such apps are active at the same time.
+        @hide -->
+    <permission android:name="android.permission.ENTER_CAR_MODE_PRIORITIZED"
+                android:protectionLevel="signature|privileged" />
+
+    <!-- @SystemApi Required to receive ACTION_ENTER_CAR_MODE_PRIVILEGED or
+        ACTION_EXIT_CAR_MODE_PRIVILEGED.
+        @hide -->
+    <permission android:name="android.permission.HANDLE_CAR_MODE_CHANGES"
                 android:protectionLevel="signature|privileged" />
 
     <!-- The system process is explicitly the only one allowed to launch the
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index 80f3cc6..9064abf 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -238,6 +238,7 @@
         <permission name="android.permission.BIND_CONNECTION_SERVICE"/>
         <permission name="android.permission.BIND_INCALL_SERVICE"/>
         <permission name="android.permission.CALL_PRIVILEGED"/>
+        <permission name="android.permission.HANDLE_CAR_MODE_CHANGES"/>
         <permission name="android.permission.INTERACT_ACROSS_USERS"/>
         <permission name="android.permission.MANAGE_USERS"/>
         <permission name="android.permission.MANAGE_ROLE_HOLDERS"/>
@@ -328,6 +329,8 @@
         <permission name="android.permission.REQUEST_NOTIFICATION_ASSISTANT_SERVICE" />
         <!-- Permission required to test ExplicitHealthCheckServiceImpl. -->
         <permission name="android.permission.BIND_EXPLICIT_HEALTH_CHECK_SERVICE"/>
+        <!-- Permission required for UiModeManager cts test. -->
+        <permission name="android.permission.ENTER_CAR_MODE_PRIORITIZED"/>
     </privapp-permissions>
 
     <privapp-permissions package="com.android.statementservice">
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 047ac59..13fc881 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -207,6 +207,9 @@
     <!-- Permission required for CTS test - CrossProfileAppsHostSideTest -->
     <uses-permission android:name="android.permission.INTERACT_ACROSS_PROFILES"/>
 
+    <!-- Permission requried for CTS test - UiModeManagerTest -->
+    <uses-permission android:name="android.permission.ENTER_CAR_MODE_PRIORITIZED"/>
+
     <application android:label="@string/app_label"
                 android:theme="@android:style/Theme.DeviceDefault.DayNight"
                 android:defaultToDeviceProtectedStorage="true"
diff --git a/services/core/java/com/android/server/UiModeManagerService.java b/services/core/java/com/android/server/UiModeManagerService.java
index 30a3563..d1e4231 100644
--- a/services/core/java/com/android/server/UiModeManagerService.java
+++ b/services/core/java/com/android/server/UiModeManagerService.java
@@ -16,6 +16,7 @@
 
 package com.android.server;
 
+import android.annotation.IntRange;
 import android.annotation.Nullable;
 import android.app.Activity;
 import android.app.ActivityManager;
@@ -41,6 +42,7 @@
 import android.os.PowerManager;
 import android.os.PowerManager.ServiceType;
 import android.os.PowerManagerInternal;
+import android.os.Process;
 import android.os.RemoteException;
 import android.os.ResultReceiver;
 import android.os.ServiceManager;
@@ -53,6 +55,7 @@
 import android.service.dreams.Sandman;
 import android.service.vr.IVrManager;
 import android.service.vr.IVrStateCallbacks;
+import android.util.ArraySet;
 import android.util.Slog;
 
 import com.android.internal.R;
@@ -66,6 +69,9 @@
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
 
 final class UiModeManagerService extends SystemService {
     private static final String TAG = UiModeManager.class.getSimpleName();
@@ -81,6 +87,7 @@
     private int mLastBroadcastState = Intent.EXTRA_DOCK_STATE_UNDOCKED;
     private int mNightMode = UiModeManager.MODE_NIGHT_NO;
 
+    private Map<Integer, String> mCarModePackagePriority = new HashMap<>();
     private boolean mCarModeEnabled = false;
     private boolean mCharging = false;
     private boolean mPowerSave = false;
@@ -338,15 +345,25 @@
 
     private final IUiModeManager.Stub mService = new IUiModeManager.Stub() {
         @Override
-        public void enableCarMode(int flags) {
+        public void enableCarMode(@UiModeManager.EnableCarMode int flags,
+                @IntRange(from = 0) int priority, String callingPackage) {
             if (isUiModeLocked()) {
                 Slog.e(TAG, "enableCarMode while UI mode is locked");
                 return;
             }
+
+            if (priority != UiModeManager.DEFAULT_PRIORITY
+                    && getContext().checkCallingOrSelfPermission(
+                    android.Manifest.permission.ENTER_CAR_MODE_PRIORITIZED)
+                    != PackageManager.PERMISSION_GRANTED) {
+                throw new SecurityException("Enabling car mode with a priority requires "
+                        + "permission ENTER_CAR_MODE_PRIORITIZED");
+            }
+
             final long ident = Binder.clearCallingIdentity();
             try {
                 synchronized (mLock) {
-                    setCarModeLocked(true, flags);
+                    setCarModeLocked(true, flags, priority, callingPackage);
                     if (mSystemReady) {
                         updateLocked(flags, 0);
                     }
@@ -356,16 +373,49 @@
             }
         }
 
+        /**
+         * This method is only kept around for the time being; the AIDL has an UnsupportedAppUsage
+         * tag which means this method is technically considered part of the greylist "API".
+         * @param flags
+         */
         @Override
-        public void disableCarMode(int flags) {
+        public void disableCarMode(@UiModeManager.DisableCarMode int flags) {
+            disableCarModeByCallingPackage(flags, null /* callingPackage */);
+        }
+
+        /**
+         * Handles requests to disable car mode.
+         * @param flags Disable car mode flags
+         * @param callingPackage
+         */
+        @Override
+        public void disableCarModeByCallingPackage(@UiModeManager.DisableCarMode int flags,
+                String callingPackage) {
             if (isUiModeLocked()) {
                 Slog.e(TAG, "disableCarMode while UI mode is locked");
                 return;
             }
+
+            // If the caller is the system, we will allow the DISABLE_CAR_MODE_ALL_PRIORITIES car
+            // mode flag to be specified; this is so that the user can disable car mode at all
+            // priorities using the persistent notification.
+            boolean isSystemCaller = Binder.getCallingUid() == Process.SYSTEM_UID;
+            final int carModeFlags =
+                    isSystemCaller ? flags : flags & ~UiModeManager.DISABLE_CAR_MODE_ALL_PRIORITIES;
+
             final long ident = Binder.clearCallingIdentity();
             try {
                 synchronized (mLock) {
-                    setCarModeLocked(false, 0);
+                    // Determine if the caller has enabled car mode at a priority other than the
+                    // default one.  If they have, then attempt to disable at that priority.
+                    int priority = mCarModePackagePriority.entrySet()
+                            .stream()
+                            .filter(e -> e.getValue().equals(callingPackage))
+                            .findFirst()
+                            .map(Map.Entry::getKey)
+                            .orElse(UiModeManager.DEFAULT_PRIORITY);
+
+                    setCarModeLocked(false, carModeFlags, priority, callingPackage);
                     if (mSystemReady) {
                         updateLocked(0, flags);
                     }
@@ -471,19 +521,32 @@
         synchronized (mLock) {
             pw.println("Current UI Mode Service state:");
             pw.print("  mDockState="); pw.print(mDockState);
-                    pw.print(" mLastBroadcastState="); pw.println(mLastBroadcastState);
+            pw.print(" mLastBroadcastState="); pw.println(mLastBroadcastState);
+
             pw.print("  mNightMode="); pw.print(mNightMode); pw.print(" (");
-                    pw.print(Shell.nightModeToStr(mNightMode)); pw.print(") ");
-                    pw.print(" mNightModeLocked="); pw.print(mNightModeLocked);
-                    pw.print(" mCarModeEnabled="); pw.print(mCarModeEnabled);
-                    pw.print(" mComputedNightMode="); pw.print(mComputedNightMode);
-                    pw.print(" mCarModeEnableFlags="); pw.print(mCarModeEnableFlags);
-                    pw.print(" mEnableCarDockLaunch="); pw.println(mEnableCarDockLaunch);
+            pw.print(Shell.nightModeToStr(mNightMode)); pw.print(") ");
+            pw.print(" mNightModeLocked="); pw.println(mNightModeLocked);
+
+            pw.print("  mCarModeEnabled="); pw.print(mCarModeEnabled);
+            pw.print(" (carModeApps=");
+            for (Map.Entry<Integer, String> entry : mCarModePackagePriority.entrySet()) {
+                pw.print(entry.getKey());
+                pw.print(":");
+                pw.print(entry.getValue());
+                pw.print(" ");
+            }
+            pw.println("");
+            pw.print(" mComputedNightMode="); pw.print(mComputedNightMode);
+            pw.print(" mCarModeEnableFlags="); pw.print(mCarModeEnableFlags);
+            pw.print(" mEnableCarDockLaunch="); pw.println(mEnableCarDockLaunch);
+
             pw.print("  mCurUiMode=0x"); pw.print(Integer.toHexString(mCurUiMode));
-                    pw.print(" mUiModeLocked="); pw.print(mUiModeLocked);
-                    pw.print(" mSetUiMode=0x"); pw.println(Integer.toHexString(mSetUiMode));
+            pw.print(" mUiModeLocked="); pw.print(mUiModeLocked);
+            pw.print(" mSetUiMode=0x"); pw.println(Integer.toHexString(mSetUiMode));
+
             pw.print("  mHoldingConfiguration="); pw.print(mHoldingConfiguration);
-                    pw.print(" mSystemReady="); pw.println(mSystemReady);
+            pw.print(" mSystemReady="); pw.println(mSystemReady);
+
             if (mTwilightManager != null) {
                 // We may not have a TwilightManager.
                 pw.print("  mTwilightService.getLastTwilightState()=");
@@ -506,12 +569,32 @@
         }
     }
 
-    void setCarModeLocked(boolean enabled, int flags) {
-        if (mCarModeEnabled != enabled) {
-            mCarModeEnabled = enabled;
+    /**
+     * Updates the global car mode state.
+     * The device is considered to be in car mode if there exists an app at any priority level which
+     * has entered car mode.
+     *
+     * @param enabled {@code true} if the caller wishes to enable car mode, {@code false} otherwise.
+     * @param flags Flags used when enabling/disabling car mode.
+     * @param priority The priority level for entering or exiting car mode; defaults to
+     *                 {@link UiModeManager#DEFAULT_PRIORITY} for callers using
+     *                 {@link UiModeManager#enableCarMode(int)}.  Callers using
+     *                 {@link UiModeManager#enableCarMode(int, int)} may specify a priority.
+     * @param packageName The package name of the app which initiated the request to enable or
+     *                    disable car mode.
+     */
+    void setCarModeLocked(boolean enabled, int flags, int priority, String packageName) {
+        if (enabled) {
+            enableCarMode(priority, packageName);
+        } else {
+            disableCarMode(flags, priority, packageName);
+        }
+        boolean isCarModeNowEnabled = isCarModeEnabled();
 
+        if (mCarModeEnabled != isCarModeNowEnabled) {
+            mCarModeEnabled = isCarModeNowEnabled;
             // When exiting car mode, restore night mode from settings
-            if (!mCarModeEnabled) {
+            if (!isCarModeNowEnabled) {
                 Context context = getContext();
                 updateNightModeFromSettings(context,
                         context.getResources(),
@@ -521,11 +604,102 @@
         mCarModeEnableFlags = flags;
     }
 
+    /**
+     * Handles disabling car mode.
+     * <p>
+     * Car mode can be disabled at a priority level if any of the following is true:
+     * 1. The priority being disabled is the {@link UiModeManager#DEFAULT_PRIORITY}.
+     * 2. The priority level is enabled and the caller is the app who originally enabled it.
+     * 3. The {@link UiModeManager#DISABLE_CAR_MODE_ALL_PRIORITIES} flag was specified, meaning all
+     *    car mode priorities are disabled.
+     *
+     * @param flags Car mode flags.
+     * @param priority The priority level at which to disable car mode.
+     * @param packageName The calling package which initiated the request.
+     */
+    private void disableCarMode(int flags, int priority, String packageName) {
+        boolean isDisableAll = (flags & UiModeManager.DISABLE_CAR_MODE_ALL_PRIORITIES) != 0;
+        boolean isPriorityTracked = mCarModePackagePriority.keySet().contains(priority);
+        boolean isDefaultPriority = priority == UiModeManager.DEFAULT_PRIORITY;
+        boolean isChangeAllowed =
+                // Anyone can disable the default priority.
+                isDefaultPriority
+                // If priority was enabled, only enabling package can disable it.
+                || isPriorityTracked && mCarModePackagePriority.get(priority).equals(packageName)
+                // Disable all priorities flag can disable all regardless.
+                || isDisableAll;
+        if (isChangeAllowed) {
+            Slog.d(TAG, "disableCarMode: disabling, priority=" + priority
+                    + ", packageName=" + packageName);
+            if (isDisableAll) {
+                Set<Map.Entry<Integer, String>> entries =
+                        new ArraySet<>(mCarModePackagePriority.entrySet());
+                mCarModePackagePriority.clear();
+
+                for (Map.Entry<Integer, String> entry : entries) {
+                    notifyCarModeDisabled(entry.getKey(), entry.getValue());
+                }
+            } else {
+                mCarModePackagePriority.remove(priority);
+                notifyCarModeDisabled(priority, packageName);
+            }
+        }
+    }
+
+    /**
+     * Handles enabling car mode.
+     * <p>
+     * Car mode can be enabled at any priority if it has not already been enabled at that priority.
+     * The calling package is tracked for the first app which enters priority at the
+     * {@link UiModeManager#DEFAULT_PRIORITY}, though any app can disable it at that priority.
+     *
+     * @param priority The priority for enabling car mode.
+     * @param packageName The calling package which initiated the request.
+     */
+    private void enableCarMode(int priority, String packageName) {
+        boolean isPriorityTracked = mCarModePackagePriority.containsKey(priority);
+        boolean isPackagePresent = mCarModePackagePriority.containsValue(packageName);
+        if (!isPriorityTracked && !isPackagePresent) {
+            Slog.d(TAG, "enableCarMode: enabled at priority=" + priority + ", packageName="
+                    + packageName);
+            mCarModePackagePriority.put(priority, packageName);
+            notifyCarModeEnabled(priority, packageName);
+        } else {
+            Slog.d(TAG, "enableCarMode: car mode at priority " + priority + " already enabled.");
+        }
+
+    }
+
+    private void notifyCarModeEnabled(int priority, String packageName) {
+        Intent intent = new Intent(UiModeManager.ACTION_ENTER_CAR_MODE_PRIORITIZED);
+        intent.putExtra(UiModeManager.EXTRA_CALLING_PACKAGE, packageName);
+        intent.putExtra(UiModeManager.EXTRA_PRIORITY, priority);
+        getContext().sendBroadcastAsUser(intent, UserHandle.ALL,
+                android.Manifest.permission.HANDLE_CAR_MODE_CHANGES);
+    }
+
+    private void notifyCarModeDisabled(int priority, String packageName) {
+        Intent intent = new Intent(UiModeManager.ACTION_EXIT_CAR_MODE_PRIORITIZED);
+        intent.putExtra(UiModeManager.EXTRA_CALLING_PACKAGE, packageName);
+        intent.putExtra(UiModeManager.EXTRA_PRIORITY, priority);
+        getContext().sendBroadcastAsUser(intent, UserHandle.ALL,
+                android.Manifest.permission.HANDLE_CAR_MODE_CHANGES);
+    }
+
+    /**
+     * Determines if car mode is enabled at any priority level.
+     * @return {@code true} if car mode is enabled, {@code false} otherwise.
+     */
+    private boolean isCarModeEnabled() {
+        return mCarModePackagePriority.size() > 0;
+    }
+
     private void updateDockState(int newState) {
         synchronized (mLock) {
             if (newState != mDockState) {
                 mDockState = newState;
-                setCarModeLocked(mDockState == Intent.EXTRA_DOCK_STATE_CAR, 0);
+                setCarModeLocked(mDockState == Intent.EXTRA_DOCK_STATE_CAR, 0,
+                        UiModeManager.DEFAULT_PRIORITY, "" /* packageName */);
                 if (mSystemReady) {
                     updateLocked(UiModeManager.ENABLE_CAR_MODE_GO_CAR_HOME, 0);
                 }