From d0bbe0b976fdefe412529cbf94a985699ed79453 Mon Sep 17 00:00:00 2001
From: Suprabh Shukla <suprabh@google.com>
Date: Fri, 29 Jan 2021 19:53:39 -0800
Subject: [PATCH] Adding a permission to protect exact alarms

Added a new permission to protect the alarm manager APIs setAlarmClock
and setExactAndAllowWhileIdle. The change will apply to apps targeting
API S and above.
- The permission will be granted by default to apps that request it.
- This will be a user facing permission as these APIs allow for exact
scheduling, and an app should generally only use them when scheduling
work on behalf of the user. This means the user can revoke it when they
want.
- Alarms set with this API will also be allowed to start FGS from the
background.
- Quota on exact, allow-while-idle alarms has been relaxed generously,
while alarm-clocks remain totally uninhibited.
- Apps that are exempted from battery via <allow-in-power-save> in the
sysconfig, can still use exact, allow-while-idle alarms without the
permission, but with lower quota.
- Inexact, allow-while-idle alarms do not require this permission, but
will *not* be allowed to start FGS from the background.
- A new API to query the state of this permission is included.

Test: atest FrameworksMockingServicesTests:com.android.server.alarm
atest CtsAlarmManagerTestCases

BYPASS_INCLUSIVE_LANGUAGE_REASON=Existing APIs

Bug: 171306433
Change-Id: Ifb6a3f3c42316b1c83fe6960920501f5e0ee51f2
---
 apex/jobscheduler/framework/Android.bp        |   1 +
 .../java/android/app/AlarmManager.java        |  47 ++
 .../java/android/app/IAlarmManager.aidl       |   1 +
 .../java/com/android/server/alarm/Alarm.java  |  10 +-
 .../server/alarm/AlarmManagerService.java     | 357 +++++++++++--
 core/api/current.txt                          |   6 +-
 core/java/android/app/AppOpsManager.java      |  31 +-
 core/res/AndroidManifest.xml                  |   6 +
 .../server/alarm/AlarmManagerServiceTest.java | 480 ++++++++++++++----
 .../android/server/alarm/AlarmStoreTest.java  |   4 +-
 .../com/android/server/alarm/AlarmTest.java   |  55 +-
 .../alarm/BackgroundRestrictedAlarmsTest.java |   2 +-
 12 files changed, 839 insertions(+), 161 deletions(-)

diff --git a/apex/jobscheduler/framework/Android.bp b/apex/jobscheduler/framework/Android.bp
index 6650e677544b..fd35537cd7b7 100644
--- a/apex/jobscheduler/framework/Android.bp
+++ b/apex/jobscheduler/framework/Android.bp
@@ -22,6 +22,7 @@ java_library {
         ],
     },
     libs: [
+        "app-compat-annotations",
         "framework-minus-apex",
         "unsupportedappusage",
     ],
diff --git a/apex/jobscheduler/framework/java/android/app/AlarmManager.java b/apex/jobscheduler/framework/java/android/app/AlarmManager.java
index 7851087bac19..7c7b21001c3b 100644
--- a/apex/jobscheduler/framework/java/android/app/AlarmManager.java
+++ b/apex/jobscheduler/framework/java/android/app/AlarmManager.java
@@ -16,11 +16,14 @@
 
 package android.app;
 
+import android.Manifest;
 import android.annotation.IntDef;
 import android.annotation.RequiresPermission;
 import android.annotation.SdkConstant;
 import android.annotation.SystemApi;
 import android.annotation.SystemService;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.Disabled;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
 import android.content.Intent;
@@ -183,6 +186,25 @@ public class AlarmManager {
     @UnsupportedAppUsage
     public static final int FLAG_IDLE_UNTIL = 1<<4;
 
+    /**
+     * Flag for alarms: Used to provide backwards compatibility for apps with targetSdkVersion less
+     * than {@link Build.VERSION_CODES#S}
+     * @hide
+     */
+    public static final int FLAG_ALLOW_WHILE_IDLE_COMPAT = 1 << 5;
+
+    /**
+     * For apps targeting {@link Build.VERSION_CODES#S} or above, APIs
+     * {@link #setExactAndAllowWhileIdle(int, long, PendingIntent)} and
+     * {@link #setAlarmClock(AlarmClockInfo, PendingIntent)} will require holding a new
+     * permission {@link android.Manifest.permission#SCHEDULE_EXACT_ALARM}
+     *
+     * @hide
+     */
+    @ChangeId
+    @Disabled // TODO (b/171306433): Enable starting S.
+    public static final long REQUIRE_EXACT_ALARM_PERMISSION = 171306433L;
+
     @UnsupportedAppUsage
     private final IAlarmManager mService;
     private final Context mContext;
@@ -588,6 +610,11 @@ public class AlarmManager {
      * This method is like {@link #setExact(int, long, PendingIntent)}, but implies
      * {@link #RTC_WAKEUP}.
      *
+     * <p>
+     * Starting from API {@link Build.VERSION_CODES#S}, using this method requires the
+     * {@link Manifest.permission#SCHEDULE_EXACT_ALARM} permission. Alarms scheduled via this API
+     * will be allowed to start a foreground service even if the app is in the background.
+     *
      * @param info
      * @param operation Action to perform when the alarm goes off;
      *        typically comes from {@link PendingIntent#getBroadcast
@@ -603,6 +630,7 @@ public class AlarmManager {
      * @see android.content.Context#registerReceiver
      * @see android.content.Intent#filterEquals
      */
+    @RequiresPermission(Manifest.permission.SCHEDULE_EXACT_ALARM)
     public void setAlarmClock(AlarmClockInfo info, PendingIntent operation) {
         setImpl(RTC_WAKEUP, info.getTriggerTime(), WINDOW_EXACT, 0, 0, operation,
                 null, null, null, null, info);
@@ -876,6 +904,12 @@ public class AlarmManager {
      * device is idle it may take even more liberties with scheduling in order to optimize
      * for battery life.</p>
      *
+     * <p>
+     * Starting from API {@link Build.VERSION_CODES#S}, using this method requires the
+     * {@link Manifest.permission#SCHEDULE_EXACT_ALARM} permission, unless the app is exempt from
+     * battery restrictions. Alarms scheduled via this API will be allowed to start a foreground
+     * service even if the app is in the background.
+     *
      * @param type type of alarm.
      * @param triggerAtMillis time in milliseconds that the alarm should go
      *        off, using the appropriate clock (depending on the alarm type).
@@ -895,6 +929,7 @@ public class AlarmManager {
      * @see #RTC
      * @see #RTC_WAKEUP
      */
+    @RequiresPermission(value = Manifest.permission.SCHEDULE_EXACT_ALARM, conditional = true)
     public void setExactAndAllowWhileIdle(@AlarmType int type, long triggerAtMillis,
             PendingIntent operation) {
         setImpl(type, triggerAtMillis, WINDOW_EXACT, 0, FLAG_ALLOW_WHILE_IDLE, operation,
@@ -1017,6 +1052,18 @@ public class AlarmManager {
         }
     }
 
+    /**
+     * Called to check if the caller has permission to use alarms set via {@link }
+     * @return
+     */
+    public boolean canScheduleExactAlarms() {
+        try {
+            return mService.canScheduleExactAlarms();
+        } catch (RemoteException re) {
+            throw re.rethrowFromSystemServer();
+        }
+    }
+
     /**
      * Gets information about the next alarm clock currently scheduled.
      *
diff --git a/apex/jobscheduler/framework/java/android/app/IAlarmManager.aidl b/apex/jobscheduler/framework/java/android/app/IAlarmManager.aidl
index 2c51935dc446..2f21ce395df5 100644
--- a/apex/jobscheduler/framework/java/android/app/IAlarmManager.aidl
+++ b/apex/jobscheduler/framework/java/android/app/IAlarmManager.aidl
@@ -41,4 +41,5 @@ interface IAlarmManager {
     @UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553)
     AlarmManager.AlarmClockInfo getNextAlarmClock(int userId);
     long currentNetworkTimeMillis();
+    boolean canScheduleExactAlarms();
 }
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/Alarm.java b/apex/jobscheduler/service/java/com/android/server/alarm/Alarm.java
index 657c368d0aee..3bc7b307c334 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/Alarm.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/Alarm.java
@@ -26,6 +26,7 @@ import static com.android.server.alarm.AlarmManagerService.clampPositive;
 import android.app.AlarmManager;
 import android.app.IAlarmListener;
 import android.app.PendingIntent;
+import android.os.Bundle;
 import android.os.WorkSource;
 import android.util.IndentingPrintWriter;
 import android.util.TimeUtils;
@@ -92,10 +93,12 @@ class Alarm {
     private long mWhenElapsed;
     private long mMaxWhenElapsed;
     public AlarmManagerService.PriorityClass priorityClass;
+    /** Broadcast options to use when delivering this alarm */
+    public Bundle mIdleOptions;
 
     Alarm(int type, long when, long requestedWhenElapsed, long windowLength, long interval,
             PendingIntent op, IAlarmListener rec, String listenerTag, WorkSource ws, int flags,
-            AlarmManager.AlarmClockInfo info, int uid, String pkgName) {
+            AlarmManager.AlarmClockInfo info, int uid, String pkgName, Bundle idleOptions) {
         this.type = type;
         origWhen = when;
         wakeup = type == AlarmManager.ELAPSED_REALTIME_WAKEUP
@@ -115,6 +118,7 @@ class Alarm {
         alarmClock = info;
         this.uid = uid;
         packageName = pkgName;
+        mIdleOptions = idleOptions;
         sourcePackage = (operation != null) ? operation.getCreatorPackage() : packageName;
         creatorUid = (operation != null) ? operation.getCreatorUid() : this.uid;
     }
@@ -303,6 +307,10 @@ class Alarm {
             ipw.print("listener=");
             ipw.println(listener.asBinder());
         }
+        if (mIdleOptions != null) {
+            ipw.print("idle-options=");
+            ipw.println(mIdleOptions.toString());
+        }
     }
 
     public void dumpDebug(ProtoOutputStream proto, long fieldId, long nowElapsed) {
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
index f6a1b8af9e49..559a43491c7f 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
@@ -20,11 +20,15 @@ import static android.app.ActivityManagerInternal.ALLOW_NON_FULL;
 import static android.app.AlarmManager.ELAPSED_REALTIME;
 import static android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP;
 import static android.app.AlarmManager.FLAG_ALLOW_WHILE_IDLE;
+import static android.app.AlarmManager.FLAG_ALLOW_WHILE_IDLE_COMPAT;
 import static android.app.AlarmManager.FLAG_ALLOW_WHILE_IDLE_UNRESTRICTED;
 import static android.app.AlarmManager.FLAG_IDLE_UNTIL;
+import static android.app.AlarmManager.FLAG_WAKE_FROM_IDLE;
+import static android.app.AlarmManager.INTERVAL_HOUR;
 import static android.app.AlarmManager.RTC;
 import static android.app.AlarmManager.RTC_WAKEUP;
 import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY;
+import static android.os.PowerWhitelistManager.TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED;
 import static android.os.UserHandle.USER_SYSTEM;
 
 import static com.android.server.alarm.Alarm.APP_STANDBY_POLICY_INDEX;
@@ -32,6 +36,7 @@ import static com.android.server.alarm.Alarm.BATTERY_SAVER_POLICY_INDEX;
 import static com.android.server.alarm.Alarm.DEVICE_IDLE_POLICY_INDEX;
 import static com.android.server.alarm.Alarm.REQUESTER_POLICY_INDEX;
 
+import android.Manifest;
 import android.annotation.NonNull;
 import android.annotation.UserIdInt;
 import android.app.Activity;
@@ -43,12 +48,14 @@ import android.app.IAlarmCompleteListener;
 import android.app.IAlarmListener;
 import android.app.IAlarmManager;
 import android.app.PendingIntent;
+import android.app.compat.CompatChanges;
 import android.app.usage.UsageStatsManager;
 import android.app.usage.UsageStatsManagerInternal;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.PermissionChecker;
 import android.content.pm.PackageManagerInternal;
 import android.net.Uri;
 import android.os.BatteryManager;
@@ -65,6 +72,7 @@ import android.os.PowerManager;
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.ResultReceiver;
+import android.os.ServiceManager;
 import android.os.ShellCallback;
 import android.os.ShellCommand;
 import android.os.SystemClock;
@@ -95,6 +103,8 @@ import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.app.IAppOpsCallback;
+import com.android.internal.app.IAppOpsService;
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.internal.util.LocalLog;
@@ -177,6 +187,7 @@ public class AlarmManagerService extends SystemService {
     final LocalLog mLog = new LocalLog(TAG);
 
     AppOpsManager mAppOps;
+    IAppOpsService mAppOpsService;
     DeviceIdleInternal mLocalDeviceIdleController;
     private UsageStatsManagerInternal mUsageStatsManagerInternal;
     private ActivityManagerInternal mActivityManagerInternal;
@@ -253,10 +264,8 @@ public class AlarmManagerService extends SystemService {
             "REORDER_ALARMS_FOR_STANDBY",
     });
 
-    /**
-     * Broadcast options to use for FLAG_ALLOW_WHILE_IDLE.
-     */
-    Bundle mIdleOptions;
+    BroadcastOptions mOptsWithFgs = BroadcastOptions.makeBasic();
+    BroadcastOptions mOptsWithoutFgs = BroadcastOptions.makeBasic();
 
     // TODO(b/172085676): Move inside alarm store.
     private final SparseArray<AlarmManager.AlarmClockInfo> mNextAlarmClockForUser =
@@ -410,6 +419,10 @@ public class AlarmManagerService extends SystemService {
         @VisibleForTesting
         static final String KEY_ALLOW_WHILE_IDLE_QUOTA = "allow_while_idle_quota";
 
+        @VisibleForTesting
+        static final String KEY_ALLOW_WHILE_IDLE_COMPAT_QUOTA = "allow_while_idle_compat_quota";
+        private static final String KEY_ALLOW_WHILE_IDLE_WINDOW = "allow_while_idle_window";
+
         private static final long DEFAULT_MIN_FUTURITY = 5 * 1000;
         private static final long DEFAULT_MIN_INTERVAL = 60 * 1000;
         private static final long DEFAULT_MAX_INTERVAL = 365 * DateUtils.DAY_IN_MILLIS;
@@ -433,8 +446,14 @@ public class AlarmManagerService extends SystemService {
         private static final boolean DEFAULT_LAZY_BATCHING = true;
         private static final boolean DEFAULT_TIME_TICK_ALLOWED_WHILE_IDLE = true;
 
-        private static final int DEFAULT_ALLOW_WHILE_IDLE_QUOTA = 7;
-        public static final long ALLOW_WHILE_IDLE_WINDOW = 60 * 60 * 1000; // 1 hour.
+        /**
+         * Default quota for pre-S apps. Enough to accommodate the existing policy of an alarm
+         * every ALLOW_WHILE_IDLE_LONG_DELAY, which was 9 minutes.
+         */
+        private static final int DEFAULT_ALLOW_WHILE_IDLE_COMPAT_QUOTA = 7;
+        private static final int DEFAULT_ALLOW_WHILE_IDLE_QUOTA = 72;
+
+        private static final long DEFAULT_ALLOW_WHILE_IDLE_WINDOW = 60 * 60 * 1000; // 1 hour.
 
         // Minimum futurity of a new alarm
         public long MIN_FUTURITY = DEFAULT_MIN_FUTURITY;
@@ -463,6 +482,19 @@ public class AlarmManagerService extends SystemService {
 
         public int ALLOW_WHILE_IDLE_QUOTA = DEFAULT_ALLOW_WHILE_IDLE_QUOTA;
 
+        /**
+         * Used to provide backwards compatibility to pre-S apps with a quota equivalent to the
+         * earlier delay throttling mechanism.
+         */
+        public int ALLOW_WHILE_IDLE_COMPAT_QUOTA = DEFAULT_ALLOW_WHILE_IDLE_COMPAT_QUOTA;
+
+        /**
+         * The window used for enforcing {@link #ALLOW_WHILE_IDLE_QUOTA} and
+         * {@link #ALLOW_WHILE_IDLE_COMPAT_QUOTA}. Can be configured, but only recommended for
+         * testing.
+         */
+        public long ALLOW_WHILE_IDLE_WINDOW = DEFAULT_ALLOW_WHILE_IDLE_WINDOW;
+
         private long mLastAllowWhileIdleWhitelistDuration = -1;
 
         Constants() {
@@ -480,9 +512,11 @@ public class AlarmManagerService extends SystemService {
         public void updateAllowWhileIdleWhitelistDurationLocked() {
             if (mLastAllowWhileIdleWhitelistDuration != ALLOW_WHILE_IDLE_WHITELIST_DURATION) {
                 mLastAllowWhileIdleWhitelistDuration = ALLOW_WHILE_IDLE_WHITELIST_DURATION;
-                BroadcastOptions opts = BroadcastOptions.makeBasic();
-                opts.setTemporaryAppWhitelistDuration(ALLOW_WHILE_IDLE_WHITELIST_DURATION);
-                mIdleOptions = opts.toBundle();
+
+                mOptsWithFgs.setTemporaryAppWhitelistDuration(ALLOW_WHILE_IDLE_WHITELIST_DURATION);
+                mOptsWithoutFgs.setTemporaryAppWhitelistDuration(
+                        TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED,
+                        ALLOW_WHILE_IDLE_WHITELIST_DURATION);
             }
         }
 
@@ -516,6 +550,27 @@ public class AlarmManagerService extends SystemService {
                                 ALLOW_WHILE_IDLE_QUOTA = 1;
                             }
                             break;
+                        case KEY_ALLOW_WHILE_IDLE_COMPAT_QUOTA:
+                            ALLOW_WHILE_IDLE_COMPAT_QUOTA = properties.getInt(
+                                    KEY_ALLOW_WHILE_IDLE_COMPAT_QUOTA,
+                                    DEFAULT_ALLOW_WHILE_IDLE_COMPAT_QUOTA);
+                            if (ALLOW_WHILE_IDLE_COMPAT_QUOTA <= 0) {
+                                Slog.w(TAG, "Cannot have quota lower than 1.");
+                                ALLOW_WHILE_IDLE_COMPAT_QUOTA = 1;
+                            }
+                            break;
+                        case KEY_ALLOW_WHILE_IDLE_WINDOW:
+                            ALLOW_WHILE_IDLE_WINDOW = properties.getLong(
+                                    KEY_ALLOW_WHILE_IDLE_WINDOW, DEFAULT_ALLOW_WHILE_IDLE_WINDOW);
+                            if (ALLOW_WHILE_IDLE_WINDOW > DEFAULT_ALLOW_WHILE_IDLE_WINDOW) {
+                                Slog.w(TAG, "Cannot have allow_while_idle_window > "
+                                        + DEFAULT_ALLOW_WHILE_IDLE_WINDOW);
+                                ALLOW_WHILE_IDLE_WINDOW = DEFAULT_ALLOW_WHILE_IDLE_WINDOW;
+                            } else if (ALLOW_WHILE_IDLE_WINDOW < DEFAULT_ALLOW_WHILE_IDLE_WINDOW) {
+                                Slog.w(TAG, "Using a non-default allow_while_idle_window = "
+                                        + ALLOW_WHILE_IDLE_WINDOW);
+                            }
+                            break;
                         case KEY_ALLOW_WHILE_IDLE_WHITELIST_DURATION:
                             ALLOW_WHILE_IDLE_WHITELIST_DURATION = properties.getLong(
                                     KEY_ALLOW_WHILE_IDLE_WHITELIST_DURATION,
@@ -643,10 +698,14 @@ public class AlarmManagerService extends SystemService {
             TimeUtils.formatDuration(LISTENER_TIMEOUT, pw);
             pw.println();
 
-            pw.print("allow_while_idle_window=");
+            pw.print(KEY_ALLOW_WHILE_IDLE_WINDOW);
+            pw.print("=");
             TimeUtils.formatDuration(ALLOW_WHILE_IDLE_WINDOW, pw);
             pw.println();
 
+            pw.print(KEY_ALLOW_WHILE_IDLE_COMPAT_QUOTA, ALLOW_WHILE_IDLE_COMPAT_QUOTA);
+            pw.println();
+
             pw.print(KEY_ALLOW_WHILE_IDLE_QUOTA, ALLOW_WHILE_IDLE_QUOTA);
             pw.println();
 
@@ -830,7 +889,15 @@ public class AlarmManagerService extends SystemService {
         if (futurity < MIN_FUZZABLE_INTERVAL) {
             futurity = 0;
         }
-        return clampPositive(triggerAtTime + (long) (.75 * futurity));
+        long maxElapsed = triggerAtTime + (long) (0.75 * futurity);
+        // For non-repeating alarms, window is capped at a maximum of one hour from the requested
+        // delivery time. This allows for inexact-while-idle alarms to be slightly more reliable.
+        // In practice, the delivery window should generally be much smaller than that
+        // when the device is not idling.
+        if (interval == 0) {
+            maxElapsed = Math.min(maxElapsed, triggerAtTime + INTERVAL_HOUR);
+        }
+        return clampPositive(maxElapsed);
     }
 
     // The RTC clock has moved arbitrarily, so we need to recalculate all the RTC alarm deliveries.
@@ -998,7 +1065,7 @@ public class AlarmManagerService extends SystemService {
                 setImplLocked(alarm.type, alarm.origWhen + delta, nextElapsed,
                         nextMaxElapsed - nextElapsed, alarm.repeatInterval, alarm.operation, null,
                         null, alarm.flags, alarm.workSource, alarm.alarmClock, alarm.uid,
-                        alarm.packageName);
+                        alarm.packageName, null);
                 // Kernel alarms will be rescheduled as needed in setImplLocked
             }
         }
@@ -1241,7 +1308,8 @@ public class AlarmManagerService extends SystemService {
             mAlarmStore.setAlarmClockRemovalListener(mAlarmClockUpdater);
 
             mAppWakeupHistory = new AppWakeupHistory(Constants.DEFAULT_APP_STANDBY_WINDOW);
-            mAllowWhileIdleHistory = new AppWakeupHistory(Constants.ALLOW_WHILE_IDLE_WINDOW);
+            mAllowWhileIdleHistory = new AppWakeupHistory(
+                    Constants.DEFAULT_ALLOW_WHILE_IDLE_WINDOW);
 
             mNextWakeup = mNextNonWakeup = 0;
 
@@ -1327,6 +1395,28 @@ public class AlarmManagerService extends SystemService {
             synchronized (mLock) {
                 mConstants.start();
                 mAppOps = (AppOpsManager) getContext().getSystemService(Context.APP_OPS_SERVICE);
+                mAppOpsService = mInjector.getAppOpsService();
+                try {
+                    mAppOpsService.startWatchingMode(AppOpsManager.OP_SCHEDULE_EXACT_ALARM, null,
+                            new IAppOpsCallback.Stub() {
+                                @Override
+                                public void opChanged(int op, int uid, String packageName)
+                                        throws RemoteException {
+                                    if (op != AppOpsManager.OP_SCHEDULE_EXACT_ALARM) {
+                                        return;
+                                    }
+                                    final int mode = mAppOpsService.checkOperation(op, uid,
+                                            packageName);
+                                    if (mode != AppOpsManager.MODE_ALLOWED
+                                            && mode != AppOpsManager.MODE_DEFAULT) {
+                                        mHandler.obtainMessage(AlarmHandler.REMOVE_EXACT_ALARMS,
+                                                uid, 0, packageName).sendToTarget();
+                                    }
+                                }
+                            });
+                } catch (RemoteException e) {
+                }
+
                 mLocalDeviceIdleController =
                         LocalServices.getService(DeviceIdleInternal.class);
                 mUsageStatsManagerInternal =
@@ -1428,7 +1518,7 @@ public class AlarmManagerService extends SystemService {
     void setImpl(int type, long triggerAtTime, long windowLength, long interval,
             PendingIntent operation, IAlarmListener directReceiver, String listenerTag,
             int flags, WorkSource workSource, AlarmManager.AlarmClockInfo alarmClock,
-            int callingUid, String callingPackage) {
+            int callingUid, String callingPackage, Bundle idleOptions) {
         if ((operation == null && directReceiver == null)
                 || (operation != null && directReceiver != null)) {
             Slog.w(TAG, "Alarms must either supply a PendingIntent or an AlarmReceiver");
@@ -1519,17 +1609,18 @@ public class AlarmManagerService extends SystemService {
             }
             setImplLocked(type, triggerAtTime, triggerElapsed, windowLength, interval, operation,
                     directReceiver, listenerTag, flags, workSource, alarmClock, callingUid,
-                    callingPackage);
+                    callingPackage, idleOptions);
         }
     }
 
     private void setImplLocked(int type, long when, long whenElapsed, long windowLength,
             long interval, PendingIntent operation, IAlarmListener directReceiver,
             String listenerTag, int flags, WorkSource workSource,
-            AlarmManager.AlarmClockInfo alarmClock, int callingUid, String callingPackage) {
+            AlarmManager.AlarmClockInfo alarmClock, int callingUid, String callingPackage,
+            Bundle idleOptions) {
         final Alarm a = new Alarm(type, when, whenElapsed, windowLength, interval,
                 operation, directReceiver, listenerTag, workSource, flags, alarmClock,
-                callingUid, callingPackage);
+                callingUid, callingPackage, idleOptions);
         if (mActivityManagerInternal.isAppStartModeDisabled(callingUid, callingPackage)) {
             Slog.w(TAG, "Not setting alarm from " + callingUid + ":" + a
                     + " -- package not allowed to start");
@@ -1605,19 +1696,21 @@ public class AlarmManagerService extends SystemService {
             return false;
         }
 
-        if (!(mAppStateTracker != null && mAppStateTracker.areAlarmsRestrictedByBatterySaver(
-                alarm.creatorUid, alarm.sourcePackage))) {
+        if (mAppStateTracker == null || !mAppStateTracker.areAlarmsRestrictedByBatterySaver(
+                alarm.creatorUid, alarm.sourcePackage)) {
             return alarm.setPolicyElapsed(BATTERY_SAVER_POLICY_INDEX, nowElapsed);
         }
 
         final long batterySaverPolicyElapsed;
-        if ((alarm.flags & (AlarmManager.FLAG_ALLOW_WHILE_IDLE_UNRESTRICTED)) != 0) {
+        if ((alarm.flags & (FLAG_ALLOW_WHILE_IDLE_UNRESTRICTED)) != 0) {
             // Unrestricted.
             batterySaverPolicyElapsed = nowElapsed;
-        } else if ((alarm.flags & AlarmManager.FLAG_ALLOW_WHILE_IDLE) != 0) {
+        } else if (isAllowedWhileIdleRestricted(alarm)) {
             // Allowed but limited.
             final int userId = UserHandle.getUserId(alarm.creatorUid);
-            final int quota = mConstants.ALLOW_WHILE_IDLE_QUOTA;
+            final int quota = ((alarm.flags & FLAG_ALLOW_WHILE_IDLE) != 0)
+                    ? mConstants.ALLOW_WHILE_IDLE_QUOTA
+                    : mConstants.ALLOW_WHILE_IDLE_COMPAT_QUOTA;
             final int dispatchesInWindow = mAllowWhileIdleHistory.getTotalWakeupsInWindow(
                     alarm.sourcePackage, userId);
             if (dispatchesInWindow < quota) {
@@ -1625,7 +1718,7 @@ public class AlarmManagerService extends SystemService {
                 batterySaverPolicyElapsed = nowElapsed;
             } else {
                 batterySaverPolicyElapsed = mAllowWhileIdleHistory.getNthLastWakeupForPackage(
-                        alarm.sourcePackage, userId, quota) + Constants.ALLOW_WHILE_IDLE_WINDOW;
+                        alarm.sourcePackage, userId, quota) + mConstants.ALLOW_WHILE_IDLE_WINDOW;
             }
         } else {
             // Not allowed.
@@ -1634,6 +1727,16 @@ public class AlarmManagerService extends SystemService {
         return alarm.setPolicyElapsed(BATTERY_SAVER_POLICY_INDEX, batterySaverPolicyElapsed);
     }
 
+    /**
+     * Returns {@code true} if the given alarm has the flag
+     * {@link AlarmManager#FLAG_ALLOW_WHILE_IDLE} or
+     * {@link AlarmManager#FLAG_ALLOW_WHILE_IDLE_COMPAT}
+     *
+     */
+    private static boolean isAllowedWhileIdleRestricted(Alarm a) {
+        return (a.flags & (FLAG_ALLOW_WHILE_IDLE | FLAG_ALLOW_WHILE_IDLE_COMPAT)) != 0;
+    }
+
     /**
      * Adjusts the delivery time of the alarm based on device_idle (doze) rules.
      *
@@ -1647,14 +1750,15 @@ public class AlarmManagerService extends SystemService {
         }
 
         final long deviceIdlePolicyTime;
-        if ((alarm.flags & (AlarmManager.FLAG_ALLOW_WHILE_IDLE_UNRESTRICTED
-                | AlarmManager.FLAG_WAKE_FROM_IDLE)) != 0) {
+        if ((alarm.flags & (FLAG_ALLOW_WHILE_IDLE_UNRESTRICTED | FLAG_WAKE_FROM_IDLE)) != 0) {
             // Unrestricted.
             deviceIdlePolicyTime = nowElapsed;
-        } else if ((alarm.flags & AlarmManager.FLAG_ALLOW_WHILE_IDLE) != 0) {
+        } else if (isAllowedWhileIdleRestricted(alarm)) {
             // Allowed but limited.
             final int userId = UserHandle.getUserId(alarm.creatorUid);
-            final int quota = mConstants.ALLOW_WHILE_IDLE_QUOTA;
+            final int quota = ((alarm.flags & FLAG_ALLOW_WHILE_IDLE) != 0)
+                    ? mConstants.ALLOW_WHILE_IDLE_QUOTA
+                    : mConstants.ALLOW_WHILE_IDLE_COMPAT_QUOTA;
             final int dispatchesInWindow = mAllowWhileIdleHistory.getTotalWakeupsInWindow(
                     alarm.sourcePackage, userId);
             if (dispatchesInWindow < quota) {
@@ -1662,7 +1766,7 @@ public class AlarmManagerService extends SystemService {
                 deviceIdlePolicyTime = nowElapsed;
             } else {
                 final long whenInQuota = mAllowWhileIdleHistory.getNthLastWakeupForPackage(
-                        alarm.sourcePackage, userId, quota) + Constants.ALLOW_WHILE_IDLE_WINDOW;
+                        alarm.sourcePackage, userId, quota) + mConstants.ALLOW_WHILE_IDLE_WINDOW;
                 deviceIdlePolicyTime = Math.min(whenInQuota, mPendingIdleUntil.getWhenElapsed());
             }
         } else {
@@ -1749,7 +1853,7 @@ public class AlarmManagerService extends SystemService {
         } else if (mPendingIdleUntil != null) {
             adjustDeliveryTimeBasedOnDeviceIdle(a);
         }
-        if ((a.flags & AlarmManager.FLAG_WAKE_FROM_IDLE) != 0) {
+        if ((a.flags & FLAG_WAKE_FROM_IDLE) != 0) {
             if (mNextWakeFromIdle == null || mNextWakeFromIdle.getWhenElapsed()
                     > a.getWhenElapsed()) {
                 mNextWakeFromIdle = a;
@@ -1809,6 +1913,14 @@ public class AlarmManagerService extends SystemService {
         }
     }
 
+    /**
+     * Returns true if the given uid is on the system or user's power save exclusion list.
+     */
+    boolean isWhitelisted(int uid) {
+        return (mLocalDeviceIdleController == null || mLocalDeviceIdleController.isAppOnWhitelist(
+                UserHandle.getAppId(uid)));
+    }
+
     /**
      * Public-facing binder interface
      */
@@ -1824,6 +1936,54 @@ public class AlarmManagerService extends SystemService {
             // wakelock time spent in alarm delivery
             mAppOps.checkPackage(callingUid, callingPackage);
 
+            final boolean allowWhileIdle = (flags & FLAG_ALLOW_WHILE_IDLE) != 0;
+
+            Bundle idleOptions = null;
+            if (alarmClock != null || allowWhileIdle) {
+                // make sure the caller is allowed to use the requested kind of alarm, and also
+                // decide what broadcast options to use.
+                final boolean needsPermission;
+                boolean lowQuota;
+                if (CompatChanges.isChangeEnabled(AlarmManager.REQUIRE_EXACT_ALARM_PERMISSION,
+                        callingPackage, UserHandle.getUserHandleForUid(callingUid))) {
+                    if (windowLength != AlarmManager.WINDOW_EXACT) {
+                        needsPermission = false;
+                        lowQuota = true;
+                        idleOptions = isWhitelisted(callingUid) ? mOptsWithFgs.toBundle()
+                                : mOptsWithoutFgs.toBundle();
+                    } else if (alarmClock != null) {
+                        needsPermission = true;
+                        lowQuota = false;
+                        idleOptions = mOptsWithFgs.toBundle();
+                    } else {
+                        needsPermission = true;
+                        lowQuota = false;
+                        idleOptions = mOptsWithFgs.toBundle();
+                    }
+                } else {
+                    needsPermission = false;
+                    lowQuota = allowWhileIdle;
+                    idleOptions = allowWhileIdle ? mOptsWithFgs.toBundle() : null;
+                }
+                if (needsPermission && !canScheduleExactAlarms()) {
+                    if (alarmClock == null && isWhitelisted(callingUid)) {
+                        // If the app is on the full system allow-list (not except-idle), we still
+                        // allow the alarms, but with a lower quota to keep pre-S compatibility.
+                        lowQuota = true;
+                    } else {
+                        final String errorMessage = "Caller needs to hold "
+                                + Manifest.permission.SCHEDULE_EXACT_ALARM + " to set "
+                                + ((allowWhileIdle) ? "exact, allow-while-idle" : "alarm-clock")
+                                + " alarms.";
+                        throw new SecurityException(errorMessage);
+                    }
+                }
+                if (lowQuota) {
+                    flags &= ~FLAG_ALLOW_WHILE_IDLE;
+                    flags |= FLAG_ALLOW_WHILE_IDLE_COMPAT;
+                }
+            }
+
             // Repeating alarms must use PendingIntent, not direct listener
             if (interval != 0) {
                 if (directReceiver != null) {
@@ -1840,8 +2000,7 @@ public class AlarmManagerService extends SystemService {
 
             // No incoming callers can request either WAKE_FROM_IDLE or
             // ALLOW_WHILE_IDLE_UNRESTRICTED -- we will apply those later as appropriate.
-            flags &= ~(AlarmManager.FLAG_WAKE_FROM_IDLE
-                    | AlarmManager.FLAG_ALLOW_WHILE_IDLE_UNRESTRICTED);
+            flags &= ~(FLAG_WAKE_FROM_IDLE | FLAG_ALLOW_WHILE_IDLE_UNRESTRICTED);
 
             // Only the system can use FLAG_IDLE_UNTIL -- this is used to tell the alarm
             // manager when to come out of idle mode, which is only for DeviceIdleController.
@@ -1857,22 +2016,32 @@ public class AlarmManagerService extends SystemService {
             // If this alarm is for an alarm clock, then it must be standalone and we will
             // use it to wake early from idle if needed.
             if (alarmClock != null) {
-                flags |= AlarmManager.FLAG_WAKE_FROM_IDLE | AlarmManager.FLAG_STANDALONE;
+                flags |= FLAG_WAKE_FROM_IDLE | AlarmManager.FLAG_STANDALONE;
 
             // If the caller is a core system component or on the user's whitelist, and not calling
             // to do work on behalf of someone else, then always set ALLOW_WHILE_IDLE_UNRESTRICTED.
             // This means we will allow these alarms to go off as normal even while idle, with no
             // timing restrictions.
-            } else if (workSource == null && (callingUid < Process.FIRST_APPLICATION_UID
+            } else if (workSource == null && (UserHandle.isCore(callingUid)
                     || UserHandle.isSameApp(callingUid, mSystemUiUid)
                     || ((mAppStateTracker != null)
                         && mAppStateTracker.isUidPowerSaveUserExempt(callingUid)))) {
-                flags |= AlarmManager.FLAG_ALLOW_WHILE_IDLE_UNRESTRICTED;
-                flags &= ~AlarmManager.FLAG_ALLOW_WHILE_IDLE;
+                flags |= FLAG_ALLOW_WHILE_IDLE_UNRESTRICTED;
+                flags &= ~FLAG_ALLOW_WHILE_IDLE;
+                flags &= ~FLAG_ALLOW_WHILE_IDLE_COMPAT;
+                idleOptions = null;
             }
 
             setImpl(type, triggerAtTime, windowLength, interval, operation, directReceiver,
-                    listenerTag, flags, workSource, alarmClock, callingUid, callingPackage);
+                    listenerTag, flags, workSource, alarmClock, callingUid, callingPackage,
+                    idleOptions);
+        }
+
+        @Override
+        public boolean canScheduleExactAlarms() {
+            return PermissionChecker.checkCallingOrSelfPermissionForPreflight(getContext(),
+                    Manifest.permission.SCHEDULE_EXACT_ALARM)
+                    == PermissionChecker.PERMISSION_GRANTED;
         }
 
         @Override
@@ -2755,6 +2924,77 @@ public class AlarmManagerService extends SystemService {
         }
     }
 
+    /**
+     * Called when an app loses {@link Manifest.permission#SCHEDULE_EXACT_ALARM} to remove alarms
+     * that the app is no longer eligible to use.
+     * TODO (b/179541791): Revisit and write tests once UX is final.
+     */
+    void removeExactAlarmsOnPermissionRevokedLocked(int uid, String packageName) {
+        if (UserHandle.isCore(uid) || uid == mSystemUiUid) {
+            return;
+        }
+        if (isWhitelisted(uid)) {
+            return;
+        }
+        if (!CompatChanges.isChangeEnabled(
+                AlarmManager.REQUIRE_EXACT_ALARM_PERMISSION,
+                packageName, UserHandle.getUserHandleForUid(uid))) {
+            return;
+        }
+
+        final Predicate<Alarm> whichAlarms =
+                a -> (a.uid == uid && a.packageName.equals(packageName)
+                        && ((a.flags & FLAG_ALLOW_WHILE_IDLE) != 0 || a.alarmClock != null));
+        final ArrayList<Alarm> removed = mAlarmStore.remove(whichAlarms);
+        final boolean didRemove = !removed.isEmpty();
+        if (didRemove) {
+            decrementAlarmCount(uid, removed.size());
+        }
+
+        for (int i = mPendingBackgroundAlarms.size() - 1; i >= 0; i--) {
+            final ArrayList<Alarm> alarmsForUid = mPendingBackgroundAlarms.valueAt(i);
+            for (int j = alarmsForUid.size() - 1; j >= 0; j--) {
+                final Alarm alarm = alarmsForUid.get(j);
+                if (whichAlarms.test(alarm)) {
+                    // Don't set didRemove, since this doesn't impact the scheduled alarms.
+                    alarmsForUid.remove(j);
+                    decrementAlarmCount(alarm.uid, 1);
+                }
+            }
+            if (alarmsForUid.size() == 0) {
+                mPendingBackgroundAlarms.removeAt(i);
+            }
+        }
+        for (int i = mPendingNonWakeupAlarms.size() - 1; i >= 0; i--) {
+            final Alarm a = mPendingNonWakeupAlarms.get(i);
+            if (whichAlarms.test(a)) {
+                // Don't set didRemove, since this doesn't impact the scheduled alarms.
+                mPendingNonWakeupAlarms.remove(i);
+                decrementAlarmCount(a.uid, 1);
+            }
+        }
+
+        if (didRemove) {
+            if (mNextWakeFromIdle != null && whichAlarms.test(mNextWakeFromIdle)) {
+                mNextWakeFromIdle = mAlarmStore.getNextWakeFromIdleAlarm();
+                if (mPendingIdleUntil != null) {
+                    final boolean idleUntilUpdated = mAlarmStore.updateAlarmDeliveries(alarm -> {
+                        if (alarm != mPendingIdleUntil) {
+                            return false;
+                        }
+                        return adjustIdleUntilTime(alarm);
+                    });
+                    if (idleUntilUpdated) {
+                        mAlarmStore.updateAlarmDeliveries(
+                                alarm -> adjustDeliveryTimeBasedOnDeviceIdle(alarm));
+                    }
+                }
+            }
+            rescheduleKernelAlarmsLocked();
+            updateNextAlarmClockLocked();
+        }
+    }
+
     void removeLocked(PendingIntent operation, IAlarmListener directReceiver) {
         if (operation == null && directReceiver == null) {
             if (localLOGV) {
@@ -3082,7 +3322,7 @@ public class AlarmManagerService extends SystemService {
         }
     }
 
-    private boolean isExemptFromBatterySaver(Alarm alarm) {
+    private static boolean isExemptFromBatterySaver(Alarm alarm) {
         if (alarm.alarmClock != null) {
             return true;
         }
@@ -3142,7 +3382,7 @@ public class AlarmManagerService extends SystemService {
 
             alarm.count = 1;
             triggerList.add(alarm);
-            if ((alarm.flags & AlarmManager.FLAG_WAKE_FROM_IDLE) != 0) {
+            if ((alarm.flags & FLAG_WAKE_FROM_IDLE) != 0) {
                 EventLogTags.writeDeviceIdleWakeFromIdle(mPendingIdleUntil != null ? 1 : 0,
                         alarm.statsTag);
             }
@@ -3180,7 +3420,7 @@ public class AlarmManagerService extends SystemService {
                 setImplLocked(alarm.type, alarm.origWhen + delta, nextElapsed,
                         nextMaxElapsed - nextElapsed, alarm.repeatInterval, alarm.operation, null,
                         null, alarm.flags, alarm.workSource, alarm.alarmClock, alarm.uid,
-                        alarm.packageName);
+                        alarm.packageName, null);
             }
 
             if (alarm.wakeup) {
@@ -3257,7 +3497,6 @@ public class AlarmManagerService extends SystemService {
         mLastAlarmDeliveryTime = nowELAPSED;
         for (int i = 0; i < triggerList.size(); i++) {
             Alarm alarm = triggerList.get(i);
-            final boolean allowWhileIdle = (alarm.flags & AlarmManager.FLAG_ALLOW_WHILE_IDLE) != 0;
             if (alarm.wakeup) {
                 Trace.traceBegin(Trace.TRACE_TAG_POWER,
                         "Dispatch wakeup alarm to " + alarm.packageName);
@@ -3273,7 +3512,7 @@ public class AlarmManagerService extends SystemService {
                     mActivityManagerInternal.noteAlarmStart(alarm.operation, alarm.workSource,
                             alarm.uid, alarm.statsTag);
                 }
-                mDeliveryTracker.deliverLocked(alarm, nowELAPSED, allowWhileIdle);
+                mDeliveryTracker.deliverLocked(alarm, nowELAPSED);
             } catch (RuntimeException e) {
                 Slog.w(TAG, "Failure sending alarm.", e);
             }
@@ -3282,9 +3521,10 @@ public class AlarmManagerService extends SystemService {
         }
     }
 
-    private boolean isExemptFromAppStandby(Alarm a) {
+    @VisibleForTesting
+    static boolean isExemptFromAppStandby(Alarm a) {
         return a.alarmClock != null || UserHandle.isCore(a.creatorUid)
-                || (a.flags & FLAG_ALLOW_WHILE_IDLE_UNRESTRICTED) != 0;
+                || (a.flags & (FLAG_ALLOW_WHILE_IDLE_UNRESTRICTED | FLAG_ALLOW_WHILE_IDLE)) != 0;
     }
 
     @VisibleForTesting
@@ -3368,6 +3608,11 @@ public class AlarmManagerService extends SystemService {
                     MATCH_SYSTEM_ONLY, USER_SYSTEM);
         }
 
+        IAppOpsService getAppOpsService() {
+            return IAppOpsService.Stub.asInterface(
+                    ServiceManager.getService(Context.APP_OPS_SERVICE));
+        }
+
         ClockReceiver getClockReceiver(AlarmManagerService service) {
             return service.new ClockReceiver();
         }
@@ -3566,6 +3811,7 @@ public class AlarmManagerService extends SystemService {
         public static final int APP_STANDBY_BUCKET_CHANGED = 5;
         public static final int CHARGING_STATUS_CHANGED = 6;
         public static final int REMOVE_FOR_CANCELED = 7;
+        public static final int REMOVE_EXACT_ALARMS = 8;
 
         AlarmHandler() {
             super(Looper.myLooper());
@@ -3645,6 +3891,14 @@ public class AlarmManagerService extends SystemService {
                     }
                     break;
 
+                case REMOVE_EXACT_ALARMS:
+                    final int uid = msg.arg1;
+                    final String packageName = (String) msg.obj;
+                    synchronized (mLock) {
+                        removeExactAlarmsOnPermissionRevokedLocked(uid, packageName);
+                    }
+                    break;
+
                 default:
                     // nope, just ignore it
                     break;
@@ -3720,7 +3974,7 @@ public class AlarmManagerService extends SystemService {
 
             setImpl(ELAPSED_REALTIME, mInjector.getElapsedRealtime() + tickEventDelay, 0,
                     0, null, mTimeTickTrigger, TIME_TICK_TAG, flags, workSource, null,
-                    Process.myUid(), "android");
+                    Process.myUid(), "android", null);
 
             // Finally, remember when we set the tick alarm
             synchronized (mLock) {
@@ -3740,7 +3994,7 @@ public class AlarmManagerService extends SystemService {
             final WorkSource workSource = null; // Let system take blame for date change events.
             setImpl(RTC, calendar.getTimeInMillis(), 0, 0, mDateChangeSender, null, null,
                     AlarmManager.FLAG_STANDALONE, workSource, null,
-                    Process.myUid(), "android");
+                    Process.myUid(), "android", null);
         }
     }
 
@@ -4112,7 +4366,7 @@ public class AlarmManagerService extends SystemService {
          * Deliver an alarm and set up the post-delivery handling appropriately
          */
         @GuardedBy("mLock")
-        public void deliverLocked(Alarm alarm, long nowELAPSED, boolean allowWhileIdle) {
+        public void deliverLocked(Alarm alarm, long nowELAPSED) {
             final long workSourceToken = ThreadLocalWorkSource.setUid(
                     getAlarmAttributionUid(alarm));
             try {
@@ -4122,10 +4376,8 @@ public class AlarmManagerService extends SystemService {
 
                     try {
                         alarm.operation.send(getContext(), 0,
-                                mBackgroundIntent.putExtra(
-                                        Intent.EXTRA_ALARM_COUNT, alarm.count),
-                                mDeliveryTracker, mHandler, null,
-                                allowWhileIdle ? mIdleOptions : null);
+                                mBackgroundIntent.putExtra(Intent.EXTRA_ALARM_COUNT, alarm.count),
+                                mDeliveryTracker, mHandler, null, alarm.mIdleOptions);
                     } catch (PendingIntent.CanceledException e) {
                         if (alarm.repeatInterval > 0) {
                             // This IntentSender is no longer valid, but this
@@ -4194,7 +4446,7 @@ public class AlarmManagerService extends SystemService {
             if (inflight.isBroadcast()) {
                 notifyBroadcastAlarmPendingLocked(alarm.uid);
             }
-            if (allowWhileIdle) {
+            if (isAllowedWhileIdleRestricted(alarm)) {
                 final boolean doze = (mPendingIdleUntil != null);
                 final boolean batterySaver = (mAppStateTracker != null
                         && mAppStateTracker.isForceAllAppsStandbyEnabled());
@@ -4204,8 +4456,7 @@ public class AlarmManagerService extends SystemService {
                     mAllowWhileIdleHistory.recordAlarmForPackage(alarm.sourcePackage,
                             UserHandle.getUserId(alarm.creatorUid), nowELAPSED);
                     mAlarmStore.updateAlarmDeliveries(a -> {
-                        if (a.creatorUid != alarm.creatorUid
-                                || (a.flags & FLAG_ALLOW_WHILE_IDLE) == 0) {
+                        if (a.creatorUid != alarm.creatorUid || !isAllowedWhileIdleRestricted(a)) {
                             return false;
                         }
                         return (doze && adjustDeliveryTimeBasedOnDeviceIdle(a))
diff --git a/core/api/current.txt b/core/api/current.txt
index 53be53c62786..94b2283962cf 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -144,6 +144,7 @@ package android {
     field public static final String REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE = "android.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE";
     field public static final String REQUEST_PASSWORD_COMPLEXITY = "android.permission.REQUEST_PASSWORD_COMPLEXITY";
     field @Deprecated public static final String RESTART_PACKAGES = "android.permission.RESTART_PACKAGES";
+    field public static final String SCHEDULE_EXACT_ALARM = "android.permission.SCHEDULE_EXACT_ALARM";
     field public static final String SEND_RESPOND_VIA_MESSAGE = "android.permission.SEND_RESPOND_VIA_MESSAGE";
     field public static final String SEND_SMS = "android.permission.SEND_SMS";
     field public static final String SET_ALARM = "com.android.alarm.permission.SET_ALARM";
@@ -4299,16 +4300,17 @@ package android.app {
   }
 
   public class AlarmManager {
+    method public boolean canScheduleExactAlarms();
     method public void cancel(android.app.PendingIntent);
     method public void cancel(android.app.AlarmManager.OnAlarmListener);
     method public android.app.AlarmManager.AlarmClockInfo getNextAlarmClock();
     method public void set(int, long, android.app.PendingIntent);
     method public void set(int, long, String, android.app.AlarmManager.OnAlarmListener, android.os.Handler);
-    method public void setAlarmClock(android.app.AlarmManager.AlarmClockInfo, android.app.PendingIntent);
+    method @RequiresPermission(android.Manifest.permission.SCHEDULE_EXACT_ALARM) public void setAlarmClock(android.app.AlarmManager.AlarmClockInfo, android.app.PendingIntent);
     method public void setAndAllowWhileIdle(int, long, android.app.PendingIntent);
     method public void setExact(int, long, android.app.PendingIntent);
     method public void setExact(int, long, String, android.app.AlarmManager.OnAlarmListener, android.os.Handler);
-    method public void setExactAndAllowWhileIdle(int, long, android.app.PendingIntent);
+    method @RequiresPermission(value=android.Manifest.permission.SCHEDULE_EXACT_ALARM, conditional=true) public void setExactAndAllowWhileIdle(int, long, android.app.PendingIntent);
     method public void setInexactRepeating(int, long, long, android.app.PendingIntent);
     method public void setRepeating(int, long, long, android.app.PendingIntent);
     method @RequiresPermission(android.Manifest.permission.SET_TIME) public void setTime(long);
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index b85b1861aa02..1906ee4d85d2 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -795,7 +795,8 @@ public class AppOpsManager {
     // when adding one of these:
     //  - increment _NUM_OP
     //  - define an OPSTR_* constant (marked as @SystemApi)
-    //  - add rows to sOpToSwitch, sOpToString, sOpNames, sOpToPerms, sOpDefault
+    //  - add rows to sOpToSwitch, sOpToString, sOpNames, sOpPerms, sOpDefaultMode, sOpDisableReset,
+    //      sOpRestrictions, sOpAllowSystemRestrictionBypass
     //  - add descriptive strings to Settings/res/values/arrays.xml
     //  - add the op to the appropriate template in AppOpsState.OpsTemplate (settings app)
 
@@ -1174,13 +1175,19 @@ public class AppOpsManager {
      *
      * @hide
      */
-    // TODO: Add as AppProtoEnums
-    public static final int OP_RECORD_AUDIO_OUTPUT = 106;
+    public static final int OP_RECORD_AUDIO_OUTPUT = AppProtoEnums.APP_OP_RECORD_AUDIO_OUTPUT;
+
+    /**
+     * App can schedule exact alarm to perform timing based background work
+     *
+     * @hide
+     */
+    public static final int OP_SCHEDULE_EXACT_ALARM = AppProtoEnums.APP_OP_SCHEDULE_EXACT_ALARM;
 
 
     /** @hide */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
-    public static final int _NUM_OP = 107;
+    public static final int _NUM_OP = 108;
 
     /** Access to coarse location information. */
     public static final String OPSTR_COARSE_LOCATION = "android:coarse_location";
@@ -1553,6 +1560,13 @@ public class AppOpsManager {
      */
     public static final String OPSTR_RECORD_AUDIO_OUTPUT = "android:record_audio_output";
 
+    /**
+     * App can schedule exact alarm to perform timing based background work.
+     *
+     * @hide
+     */
+    public static final String OPSTR_SCHEDULE_EXACT_ALARM = "android:schedule_exact_alarm";
+
     /** {@link #sAppOpsToNote} not initialized yet for this op */
     private static final byte SHOULD_COLLECT_NOTE_OP_NOT_INITIALIZED = 0;
     /** Should not collect noting of this app-op in {@link #sAppOpsToNote} */
@@ -1633,6 +1647,7 @@ public class AppOpsManager {
             OP_LOADER_USAGE_STATS,
             OP_MANAGE_ONGOING_CALLS,
             OP_USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER,
+            OP_SCHEDULE_EXACT_ALARM,
     };
 
     /**
@@ -1751,6 +1766,7 @@ public class AppOpsManager {
             OP_MANAGE_CREDENTIALS,              // MANAGE_CREDENTIALS
             OP_USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER, // USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER
             OP_RECORD_AUDIO_OUTPUT,             // RECORD_AUDIO_OUTPUT
+            OP_SCHEDULE_EXACT_ALARM,            // SCHEDULE_EXACT_ALARM
     };
 
     /**
@@ -1864,6 +1880,7 @@ public class AppOpsManager {
             OPSTR_MANAGE_CREDENTIALS,
             OPSTR_USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER,
             OPSTR_RECORD_AUDIO_OUTPUT,
+            OPSTR_SCHEDULE_EXACT_ALARM,
     };
 
     /**
@@ -1978,6 +1995,7 @@ public class AppOpsManager {
             "MANAGE_CREDENTIALS",
             "USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER",
             "RECORD_AUDIO_OUTPUT",
+            "SCHEDULE_EXACT_ALARM",
     };
 
     /**
@@ -2093,6 +2111,7 @@ public class AppOpsManager {
             null, // no permission for OP_MANAGE_CREDENTIALS
             Manifest.permission.USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER,
             null, // no permission for OP_RECORD_AUDIO_OUTPUT
+            Manifest.permission.SCHEDULE_EXACT_ALARM,
     };
 
     /**
@@ -2208,6 +2227,7 @@ public class AppOpsManager {
             null, // MANAGE_CREDENTIALS
             null, // USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER
             null, // RECORD_AUDIO_OUTPUT
+            null, // SCHEDULE_EXACT_ALARM
     };
 
     /**
@@ -2322,6 +2342,7 @@ public class AppOpsManager {
             null, // MANAGE_CREDENTIALS
             null, // USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER
             null, // RECORD_AUDIO_OUTPUT
+            null, // SCHEDULE_EXACT_ALARM
     };
 
     /**
@@ -2435,6 +2456,7 @@ public class AppOpsManager {
             AppOpsManager.MODE_DEFAULT, // MANAGE_CREDENTIALS
             AppOpsManager.MODE_DEFAULT, // USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER
             AppOpsManager.MODE_ALLOWED, // RECORD_AUDIO_OUTPUT
+            AppOpsManager.MODE_DEFAULT, // SCHEDULE_EXACT_ALARM
     };
 
     /**
@@ -2552,6 +2574,7 @@ public class AppOpsManager {
             false, // MANAGE_CREDENTIALS
             true, // USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER
             false, // RECORD_AUDIO_OUTPUT
+            false, // SCHEDULE_EXACT_ALARM
     };
 
     /**
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index dc1cc329d460..d4d2c73bab25 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -3858,6 +3858,12 @@
     <permission android:name="android.permission.SET_KEYBOARD_LAYOUT"
         android:protectionLevel="signature" />
 
+    <!-- Allows an app to use exact alarm scheduling APIs to perform timing
+         sensitive background work.
+     -->
+    <permission android:name="android.permission.SCHEDULE_EXACT_ALARM"
+        android:protectionLevel="normal|appop"/>
+
     <!-- Allows an application to query tablet mode state and monitor changes
          in it.
          <p>Not for use by third-party applications.
diff --git a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
index 1254df95a1c0..91098813380e 100644
--- a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
@@ -18,6 +18,7 @@ package com.android.server.alarm;
 import static android.app.AlarmManager.ELAPSED_REALTIME;
 import static android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP;
 import static android.app.AlarmManager.FLAG_ALLOW_WHILE_IDLE;
+import static android.app.AlarmManager.FLAG_ALLOW_WHILE_IDLE_COMPAT;
 import static android.app.AlarmManager.FLAG_ALLOW_WHILE_IDLE_UNRESTRICTED;
 import static android.app.AlarmManager.FLAG_IDLE_UNTIL;
 import static android.app.AlarmManager.FLAG_STANDALONE;
@@ -25,11 +26,14 @@ import static android.app.AlarmManager.FLAG_WAKE_FROM_IDLE;
 import static android.app.AlarmManager.RTC;
 import static android.app.AlarmManager.RTC_WAKEUP;
 import static android.app.AlarmManager.WINDOW_EXACT;
+import static android.app.AlarmManager.WINDOW_HEURISTIC;
 import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_ACTIVE;
 import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_FREQUENT;
 import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_RARE;
 import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_RESTRICTED;
 import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_WORKING_SET;
+import static android.os.PowerWhitelistManager.TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED;
+import static android.os.PowerWhitelistManager.TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doCallRealMethod;
@@ -44,7 +48,7 @@ import static com.android.server.alarm.AlarmManagerService.ACTIVE_INDEX;
 import static com.android.server.alarm.AlarmManagerService.AlarmHandler.APP_STANDBY_BUCKET_CHANGED;
 import static com.android.server.alarm.AlarmManagerService.AlarmHandler.CHARGING_STATUS_CHANGED;
 import static com.android.server.alarm.AlarmManagerService.AlarmHandler.REMOVE_FOR_CANCELED;
-import static com.android.server.alarm.AlarmManagerService.Constants.ALLOW_WHILE_IDLE_WINDOW;
+import static com.android.server.alarm.AlarmManagerService.Constants.KEY_ALLOW_WHILE_IDLE_COMPAT_QUOTA;
 import static com.android.server.alarm.AlarmManagerService.Constants.KEY_ALLOW_WHILE_IDLE_QUOTA;
 import static com.android.server.alarm.AlarmManagerService.Constants.KEY_ALLOW_WHILE_IDLE_WHITELIST_DURATION;
 import static com.android.server.alarm.AlarmManagerService.Constants.KEY_LAZY_BATCHING;
@@ -61,6 +65,7 @@ import static com.android.server.alarm.Constants.TEST_CALLING_PACKAGE;
 import static com.android.server.alarm.Constants.TEST_CALLING_UID;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
@@ -75,20 +80,29 @@ import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.never;
 
+import android.Manifest;
 import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
+import android.app.AlarmManager;
+import android.app.AppOpsManager;
 import android.app.IActivityManager;
 import android.app.IAlarmCompleteListener;
 import android.app.IAlarmListener;
+import android.app.IAlarmManager;
 import android.app.PendingIntent;
+import android.app.compat.CompatChanges;
 import android.app.usage.UsageStatsManagerInternal;
 import android.content.Context;
 import android.content.Intent;
+import android.content.PermissionChecker;
 import android.os.BatteryManager;
+import android.os.Bundle;
 import android.os.Handler;
+import android.os.IBinder;
 import android.os.Looper;
 import android.os.Message;
 import android.os.PowerManager;
+import android.os.Process;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.UserHandle;
@@ -102,9 +116,12 @@ import androidx.test.runner.AndroidJUnit4;
 
 import com.android.dx.mockito.inline.extended.MockedVoidMethod;
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.app.IAppOpsCallback;
+import com.android.internal.app.IAppOpsService;
 import com.android.server.AlarmManagerInternal;
 import com.android.server.AppStateTracker;
 import com.android.server.AppStateTrackerImpl;
+import com.android.server.DeviceIdleInternal;
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
 import com.android.server.usage.AppStandbyInternal;
@@ -125,6 +142,7 @@ import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.concurrent.Executor;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.LongConsumer;
 
 @Presubmit
 @RunWith(AndroidJUnit4.class)
@@ -134,14 +152,21 @@ public class AlarmManagerServiceTest {
     private static final int TEST_CALLING_USER = UserHandle.getUserId(TEST_CALLING_UID);
 
     private long mAppStandbyWindow;
+    private long mAllowWhileIdleWindow;
     private AlarmManagerService mService;
     private AppStandbyInternal.AppIdleStateChangeListener mAppStandbyListener;
     private AlarmManagerService.ChargingReceiver mChargingReceiver;
+    private IAppOpsCallback mIAppOpsCallback;
+    private IAlarmManager mBinder;
     @Mock
     private Context mMockContext;
     @Mock
     private IActivityManager mIActivityManager;
     @Mock
+    private IAppOpsService mIAppOpsService;
+    @Mock
+    private DeviceIdleInternal mDeviceIdleInternal;
+    @Mock
     private UsageStatsManagerInternal mUsageStatsManagerInternal;
     @Mock
     private AppStandbyInternal mAppStandbyInternal;
@@ -280,6 +305,11 @@ public class AlarmManagerServiceTest {
             // "IllegalStateException: Querying activity state off main thread is not allowed."
             // when AlarmManager calls DeviceConfig.addOnPropertiesChangedListener().
         }
+
+        @Override
+        IAppOpsService getAppOpsService() {
+            return mIAppOpsService;
+        }
     }
 
     @Before
@@ -287,15 +317,20 @@ public class AlarmManagerServiceTest {
         mMockingSession = mockitoSession()
                 .initMocks(this)
                 .spyStatic(ActivityManager.class)
+                .mockStatic(CompatChanges.class)
                 .spyStatic(DeviceConfig.class)
                 .mockStatic(LocalServices.class)
                 .spyStatic(Looper.class)
+                .mockStatic(PermissionChecker.class)
                 .mockStatic(Settings.Global.class)
                 .mockStatic(ServiceManager.class)
                 .spyStatic(UserHandle.class)
                 .strictness(Strictness.WARN)
                 .startMocking();
+
         doReturn(mIActivityManager).when(ActivityManager::getService);
+        doReturn(mDeviceIdleInternal).when(
+                () -> LocalServices.getService(DeviceIdleInternal.class));
         doReturn(mActivityManagerInternal).when(
                 () -> LocalServices.getService(ActivityManagerInternal.class));
         doReturn(mAppStateTracker).when(() -> LocalServices.getService(AppStateTracker.class));
@@ -330,6 +365,9 @@ public class AlarmManagerServiceTest {
                 () -> DeviceConfig.getProperties(
                         eq(DeviceConfig.NAMESPACE_ALARM_MANAGER), ArgumentMatchers.<String>any()));
 
+        when(mMockContext.getSystemService(Context.APP_OPS_SERVICE)).thenReturn(
+                mock(AppOpsManager.class));
+
         mInjector = new Injector(mMockContext);
         mService = new AlarmManagerService(mMockContext, mInjector);
         spyOn(mService);
@@ -346,6 +384,7 @@ public class AlarmManagerServiceTest {
         // Other boot phases don't matter
         mService.onBootPhase(SystemService.PHASE_SYSTEM_SERVICES_READY);
         mAppStandbyWindow = mService.mConstants.APP_STANDBY_WINDOW;
+        mAllowWhileIdleWindow = mService.mConstants.ALLOW_WHILE_IDLE_WINDOW;
         ArgumentCaptor<AppStandbyInternal.AppIdleStateChangeListener> captor =
                 ArgumentCaptor.forClass(AppStandbyInternal.AppIdleStateChangeListener.class);
         verify(mAppStandbyInternal).addListener(captor.capture());
@@ -358,6 +397,20 @@ public class AlarmManagerServiceTest {
                         && filter.hasAction(BatteryManager.ACTION_DISCHARGING)));
         mChargingReceiver = chargingReceiverCaptor.getValue();
 
+        ArgumentCaptor<IBinder> binderCaptor = ArgumentCaptor.forClass(IBinder.class);
+        verify(() -> ServiceManager.addService(eq(Context.ALARM_SERVICE), binderCaptor.capture(),
+                anyBoolean(), anyInt()));
+        mBinder = IAlarmManager.Stub.asInterface(binderCaptor.getValue());
+
+        ArgumentCaptor<IAppOpsCallback> appOpsCallbackCaptor = ArgumentCaptor.forClass(
+                IAppOpsCallback.class);
+        try {
+            verify(mIAppOpsService).startWatchingMode(eq(AppOpsManager.OP_SCHEDULE_EXACT_ALARM),
+                    isNull(), appOpsCallbackCaptor.capture());
+        } catch (RemoteException e) {
+            // Not expected on a mock.
+        }
+        mIAppOpsCallback = appOpsCallbackCaptor.getValue();
         setTestableQuotas();
     }
 
@@ -389,13 +442,18 @@ public class AlarmManagerServiceTest {
 
     private void setTestAlarm(int type, long triggerTime, PendingIntent operation, long interval,
             int flags, int callingUid) {
+        setTestAlarm(type, triggerTime, operation, interval, flags, callingUid, null);
+    }
+
+    private void setTestAlarm(int type, long triggerTime, PendingIntent operation, long interval,
+            int flags, int callingUid, Bundle idleOptions) {
         mService.setImpl(type, triggerTime, WINDOW_EXACT, interval, operation, null, "test", flags,
-                null, null, callingUid, TEST_CALLING_PACKAGE);
+                null, null, callingUid, TEST_CALLING_PACKAGE, idleOptions);
     }
 
     private void setTestAlarmWithListener(int type, long triggerTime, IAlarmListener listener) {
         mService.setImpl(type, triggerTime, WINDOW_EXACT, 0, null, listener, "test",
-                FLAG_STANDALONE, null, null, TEST_CALLING_UID, TEST_CALLING_PACKAGE);
+                FLAG_STANDALONE, null, null, TEST_CALLING_UID, TEST_CALLING_PACKAGE, null);
     }
 
 
@@ -506,14 +564,27 @@ public class AlarmManagerServiceTest {
         setDeviceConfigLong(KEY_MIN_INTERVAL, 10);
         setDeviceConfigLong(KEY_MAX_INTERVAL, 15);
         setDeviceConfigInt(KEY_ALLOW_WHILE_IDLE_QUOTA, 20);
+        setDeviceConfigInt(KEY_ALLOW_WHILE_IDLE_COMPAT_QUOTA, 25);
         setDeviceConfigLong(KEY_ALLOW_WHILE_IDLE_WHITELIST_DURATION, 30);
         setDeviceConfigLong(KEY_LISTENER_TIMEOUT, 35);
         assertEquals(5, mService.mConstants.MIN_FUTURITY);
         assertEquals(10, mService.mConstants.MIN_INTERVAL);
         assertEquals(15, mService.mConstants.MAX_INTERVAL);
         assertEquals(20, mService.mConstants.ALLOW_WHILE_IDLE_QUOTA);
+        assertEquals(25, mService.mConstants.ALLOW_WHILE_IDLE_COMPAT_QUOTA);
         assertEquals(30, mService.mConstants.ALLOW_WHILE_IDLE_WHITELIST_DURATION);
         assertEquals(35, mService.mConstants.LISTENER_TIMEOUT);
+
+        // Test safeguards.
+        setDeviceConfigInt(KEY_ALLOW_WHILE_IDLE_QUOTA, -3);
+        assertEquals(1, mService.mConstants.ALLOW_WHILE_IDLE_QUOTA);
+        setDeviceConfigInt(KEY_ALLOW_WHILE_IDLE_QUOTA, 0);
+        assertEquals(1, mService.mConstants.ALLOW_WHILE_IDLE_QUOTA);
+
+        setDeviceConfigInt(KEY_ALLOW_WHILE_IDLE_COMPAT_QUOTA, -8);
+        assertEquals(1, mService.mConstants.ALLOW_WHILE_IDLE_COMPAT_QUOTA);
+        setDeviceConfigInt(KEY_ALLOW_WHILE_IDLE_COMPAT_QUOTA, 0);
+        assertEquals(1, mService.mConstants.ALLOW_WHILE_IDLE_COMPAT_QUOTA);
     }
 
     @Test
@@ -559,56 +630,45 @@ public class AlarmManagerServiceTest {
         assertEquals(mNowElapsedTest + 9, mTestTimer.getElapsed());
     }
 
-    private void testQuotasDeferralOnSet(int standbyBucket) throws Exception {
-        final int quota = mService.getQuotaForBucketLocked(standbyBucket);
-        when(mUsageStatsManagerInternal.getAppStandbyBucket(eq(TEST_CALLING_PACKAGE), anyInt(),
-                anyLong())).thenReturn(standbyBucket);
+    private void testQuotasDeferralOnSet(LongConsumer alarmSetter, int quota, long window)
+            throws Exception {
         final long firstTrigger = mNowElapsedTest + 10;
         for (int i = 0; i < quota; i++) {
-            setTestAlarm(ELAPSED_REALTIME_WAKEUP, firstTrigger + i,
-                    getNewMockPendingIntent());
+            alarmSetter.accept(firstTrigger + i);
             mNowElapsedTest = mTestTimer.getElapsed();
             mTestTimer.expire();
         }
         // This one should get deferred on set
-        setTestAlarm(ELAPSED_REALTIME_WAKEUP, firstTrigger + quota,
-                getNewMockPendingIntent());
-        final long expectedNextTrigger = firstTrigger + mAppStandbyWindow;
+        alarmSetter.accept(firstTrigger + quota);
+        final long expectedNextTrigger = firstTrigger + window;
         assertEquals("Incorrect next alarm trigger", expectedNextTrigger, mTestTimer.getElapsed());
     }
 
-    private void testQuotasDeferralOnExpiration(int standbyBucket) throws Exception {
-        final int quota = mService.getQuotaForBucketLocked(standbyBucket);
-        when(mUsageStatsManagerInternal.getAppStandbyBucket(eq(TEST_CALLING_PACKAGE), anyInt(),
-                anyLong())).thenReturn(standbyBucket);
+    private void testQuotasDeferralOnExpiration(LongConsumer alarmSetter, int quota, long window)
+            throws Exception {
         final long firstTrigger = mNowElapsedTest + 10;
         for (int i = 0; i < quota; i++) {
-            setTestAlarm(ELAPSED_REALTIME_WAKEUP, firstTrigger + i,
-                    getNewMockPendingIntent());
+            alarmSetter.accept(firstTrigger + i);
         }
-        // This one should get deferred after the latest alarm expires
-        setTestAlarm(ELAPSED_REALTIME_WAKEUP, firstTrigger + quota,
-                getNewMockPendingIntent());
+        // This one should get deferred after the latest alarm expires.
+        alarmSetter.accept(firstTrigger + quota);
         for (int i = 0; i < quota; i++) {
             mNowElapsedTest = mTestTimer.getElapsed();
             mTestTimer.expire();
         }
-        final long expectedNextTrigger = firstTrigger + mAppStandbyWindow;
+        final long expectedNextTrigger = firstTrigger + window;
         assertEquals("Incorrect next alarm trigger", expectedNextTrigger, mTestTimer.getElapsed());
     }
 
-    private void testQuotasNoDeferral(int standbyBucket) throws Exception {
-        final int quota = mService.getQuotaForBucketLocked(standbyBucket);
-        when(mUsageStatsManagerInternal.getAppStandbyBucket(eq(TEST_CALLING_PACKAGE), anyInt(),
-                anyLong())).thenReturn(standbyBucket);
+    private void testQuotasNoDeferral(LongConsumer alarmSetter, int quota, long window)
+            throws Exception {
         final long firstTrigger = mNowElapsedTest + 10;
         for (int i = 0; i < quota; i++) {
-            setTestAlarm(ELAPSED_REALTIME_WAKEUP, mNowElapsedTest + 10 + i,
-                    getNewMockPendingIntent());
+            alarmSetter.accept(firstTrigger + i);
         }
         // This delivery time maintains the quota invariant. Should not be deferred.
-        final long expectedNextTrigger = firstTrigger + mAppStandbyWindow + 5;
-        setTestAlarm(ELAPSED_REALTIME_WAKEUP, expectedNextTrigger, getNewMockPendingIntent());
+        final long expectedNextTrigger = firstTrigger + window + 5;
+        alarmSetter.accept(expectedNextTrigger);
         for (int i = 0; i < quota; i++) {
             mNowElapsedTest = mTestTimer.getElapsed();
             mTestTimer.expire();
@@ -616,64 +676,88 @@ public class AlarmManagerServiceTest {
         assertEquals("Incorrect next alarm trigger", expectedNextTrigger, mTestTimer.getElapsed());
     }
 
+    private void testStandbyQuotasDeferralOnSet(int standbyBucket) throws Exception {
+        final int quota = mService.getQuotaForBucketLocked(standbyBucket);
+        when(mUsageStatsManagerInternal.getAppStandbyBucket(eq(TEST_CALLING_PACKAGE), anyInt(),
+                anyLong())).thenReturn(standbyBucket);
+        testQuotasDeferralOnSet(trigger -> setTestAlarm(ELAPSED_REALTIME_WAKEUP, trigger,
+                getNewMockPendingIntent()), quota, mAppStandbyWindow);
+    }
+
+    private void testStandbyQuotasDeferralOnExpiration(int standbyBucket) throws Exception {
+        final int quota = mService.getQuotaForBucketLocked(standbyBucket);
+        when(mUsageStatsManagerInternal.getAppStandbyBucket(eq(TEST_CALLING_PACKAGE), anyInt(),
+                anyLong())).thenReturn(standbyBucket);
+        testQuotasDeferralOnExpiration(trigger -> setTestAlarm(ELAPSED_REALTIME_WAKEUP, trigger,
+                getNewMockPendingIntent()), quota, mAppStandbyWindow);
+    }
+
+    private void testStandbyQuotasNoDeferral(int standbyBucket) throws Exception {
+        final int quota = mService.getQuotaForBucketLocked(standbyBucket);
+        when(mUsageStatsManagerInternal.getAppStandbyBucket(eq(TEST_CALLING_PACKAGE), anyInt(),
+                anyLong())).thenReturn(standbyBucket);
+        testQuotasNoDeferral(trigger -> setTestAlarm(ELAPSED_REALTIME_WAKEUP, trigger,
+                getNewMockPendingIntent()), quota, mAppStandbyWindow);
+    }
+
     @Test
     public void testActiveQuota_deferredOnSet() throws Exception {
-        testQuotasDeferralOnSet(STANDBY_BUCKET_ACTIVE);
+        testStandbyQuotasDeferralOnSet(STANDBY_BUCKET_ACTIVE);
     }
 
     @Test
     public void testActiveQuota_deferredOnExpiration() throws Exception {
-        testQuotasDeferralOnExpiration(STANDBY_BUCKET_ACTIVE);
+        testStandbyQuotasDeferralOnExpiration(STANDBY_BUCKET_ACTIVE);
     }
 
     @Test
     public void testActiveQuota_notDeferred() throws Exception {
-        testQuotasNoDeferral(STANDBY_BUCKET_ACTIVE);
+        testStandbyQuotasNoDeferral(STANDBY_BUCKET_ACTIVE);
     }
 
     @Test
     public void testWorkingQuota_deferredOnSet() throws Exception {
-        testQuotasDeferralOnSet(STANDBY_BUCKET_WORKING_SET);
+        testStandbyQuotasDeferralOnSet(STANDBY_BUCKET_WORKING_SET);
     }
 
     @Test
     public void testWorkingQuota_deferredOnExpiration() throws Exception {
-        testQuotasDeferralOnExpiration(STANDBY_BUCKET_WORKING_SET);
+        testStandbyQuotasDeferralOnExpiration(STANDBY_BUCKET_WORKING_SET);
     }
 
     @Test
     public void testWorkingQuota_notDeferred() throws Exception {
-        testQuotasNoDeferral(STANDBY_BUCKET_WORKING_SET);
+        testStandbyQuotasNoDeferral(STANDBY_BUCKET_WORKING_SET);
     }
 
     @Test
     public void testFrequentQuota_deferredOnSet() throws Exception {
-        testQuotasDeferralOnSet(STANDBY_BUCKET_FREQUENT);
+        testStandbyQuotasDeferralOnSet(STANDBY_BUCKET_FREQUENT);
     }
 
     @Test
     public void testFrequentQuota_deferredOnExpiration() throws Exception {
-        testQuotasDeferralOnExpiration(STANDBY_BUCKET_FREQUENT);
+        testStandbyQuotasDeferralOnExpiration(STANDBY_BUCKET_FREQUENT);
     }
 
     @Test
     public void testFrequentQuota_notDeferred() throws Exception {
-        testQuotasNoDeferral(STANDBY_BUCKET_FREQUENT);
+        testStandbyQuotasNoDeferral(STANDBY_BUCKET_FREQUENT);
     }
 
     @Test
     public void testRareQuota_deferredOnSet() throws Exception {
-        testQuotasDeferralOnSet(STANDBY_BUCKET_RARE);
+        testStandbyQuotasDeferralOnSet(STANDBY_BUCKET_RARE);
     }
 
     @Test
     public void testRareQuota_deferredOnExpiration() throws Exception {
-        testQuotasDeferralOnExpiration(STANDBY_BUCKET_RARE);
+        testStandbyQuotasDeferralOnExpiration(STANDBY_BUCKET_RARE);
     }
 
     @Test
     public void testRareQuota_notDeferred() throws Exception {
-        testQuotasNoDeferral(STANDBY_BUCKET_RARE);
+        testStandbyQuotasNoDeferral(STANDBY_BUCKET_RARE);
     }
 
     @Test
@@ -731,7 +815,7 @@ public class AlarmManagerServiceTest {
     }
 
     @Test
-    public void testQuotaDowngrade() throws Exception {
+    public void testStandbyQuotaDowngrade() throws Exception {
         final int workingQuota = mService.getQuotaForBucketLocked(STANDBY_BUCKET_WORKING_SET);
         when(mUsageStatsManagerInternal.getAppStandbyBucket(eq(TEST_CALLING_PACKAGE), anyInt(),
                 anyLong())).thenReturn(STANDBY_BUCKET_WORKING_SET);
@@ -758,7 +842,7 @@ public class AlarmManagerServiceTest {
     }
 
     @Test
-    public void testQuotaUpgrade() throws Exception {
+    public void testStandbyQuotaUpgrade() throws Exception {
         final int frequentQuota = mService.getQuotaForBucketLocked(STANDBY_BUCKET_FREQUENT);
         when(mUsageStatsManagerInternal.getAppStandbyBucket(eq(TEST_CALLING_PACKAGE), anyInt(),
                 anyLong())).thenReturn(STANDBY_BUCKET_FREQUENT);
@@ -1308,7 +1392,7 @@ public class AlarmManagerServiceTest {
     public void allowWhileIdleAlarmsWhileDeviceIdle() throws Exception {
         doReturn(0).when(mService).fuzzForDuration(anyLong());
 
-        setIdleUntilAlarm(ELAPSED_REALTIME_WAKEUP, mNowElapsedTest + ALLOW_WHILE_IDLE_WINDOW + 1000,
+        setIdleUntilAlarm(ELAPSED_REALTIME_WAKEUP, mNowElapsedTest + mAllowWhileIdleWindow + 1000,
                 getNewMockPendingIntent());
         assertNotNull(mService.mPendingIdleUntil);
 
@@ -1323,7 +1407,7 @@ public class AlarmManagerServiceTest {
         // This one should get deferred on set.
         setAllowWhileIdleAlarm(ELAPSED_REALTIME_WAKEUP, firstTrigger + quota,
                 getNewMockPendingIntent(), false);
-        final long expectedNextTrigger = firstTrigger + ALLOW_WHILE_IDLE_WINDOW;
+        final long expectedNextTrigger = firstTrigger + mAllowWhileIdleWindow;
         assertEquals("Incorrect trigger when no quota left", expectedNextTrigger,
                 mTestTimer.getElapsed());
 
@@ -1449,61 +1533,24 @@ public class AlarmManagerServiceTest {
         when(mAppStateTracker.areAlarmsRestrictedByBatterySaver(TEST_CALLING_UID,
                 TEST_CALLING_PACKAGE)).thenReturn(true);
         when(mAppStateTracker.isForceAllAppsStandbyEnabled()).thenReturn(true);
-
         final int quota = mService.mConstants.ALLOW_WHILE_IDLE_QUOTA;
-        long firstTrigger = mNowElapsedTest + 10;
-        for (int i = 0; i < quota; i++) {
-            setAllowWhileIdleAlarm(ELAPSED_REALTIME_WAKEUP, firstTrigger + i,
-                    getNewMockPendingIntent(), false);
-            mNowElapsedTest = mTestTimer.getElapsed();
-            mTestTimer.expire();
-        }
-        // This one should get deferred on set.
-        setAllowWhileIdleAlarm(ELAPSED_REALTIME_WAKEUP, firstTrigger + quota,
-                getNewMockPendingIntent(), false);
-        long expectedNextTrigger = firstTrigger + ALLOW_WHILE_IDLE_WINDOW;
-        assertEquals("Incorrect trigger when no quota available", expectedNextTrigger,
-                mTestTimer.getElapsed());
+
+        testQuotasDeferralOnSet(trigger -> setAllowWhileIdleAlarm(ELAPSED_REALTIME_WAKEUP, trigger,
+                getNewMockPendingIntent(), false), quota, mAllowWhileIdleWindow);
 
         // Refresh the state
         mService.removeLocked(TEST_CALLING_UID);
         mService.mAllowWhileIdleHistory.removeForPackage(TEST_CALLING_PACKAGE, TEST_CALLING_USER);
 
-        firstTrigger = mNowElapsedTest + 10;
-        for (int i = 0; i < quota; i++) {
-            setAllowWhileIdleAlarm(ELAPSED_REALTIME_WAKEUP, firstTrigger + i,
-                    getNewMockPendingIntent(), false);
-        }
-        // This one should get deferred after the latest alarm expires.
-        setAllowWhileIdleAlarm(ELAPSED_REALTIME_WAKEUP, firstTrigger + quota,
-                getNewMockPendingIntent(), false);
-        for (int i = 0; i < quota; i++) {
-            mNowElapsedTest = mTestTimer.getElapsed();
-            mTestTimer.expire();
-        }
-        expectedNextTrigger = firstTrigger + ALLOW_WHILE_IDLE_WINDOW;
-        assertEquals("Incorrect trigger when no quota available", expectedNextTrigger,
-                mTestTimer.getElapsed());
+        testQuotasDeferralOnExpiration(trigger -> setAllowWhileIdleAlarm(ELAPSED_REALTIME_WAKEUP,
+                trigger, getNewMockPendingIntent(), false), quota, mAllowWhileIdleWindow);
 
         // Refresh the state
         mService.removeLocked(TEST_CALLING_UID);
         mService.mAllowWhileIdleHistory.removeForPackage(TEST_CALLING_PACKAGE, TEST_CALLING_USER);
 
-        firstTrigger = mNowElapsedTest + 10;
-        for (int i = 0; i < quota; i++) {
-            setAllowWhileIdleAlarm(ELAPSED_REALTIME_WAKEUP, firstTrigger + i,
-                    getNewMockPendingIntent(), false);
-        }
-        // This delivery time maintains the quota invariant. Should not be deferred.
-        expectedNextTrigger = firstTrigger + ALLOW_WHILE_IDLE_WINDOW + 5;
-        setAllowWhileIdleAlarm(ELAPSED_REALTIME_WAKEUP, expectedNextTrigger,
-                getNewMockPendingIntent(), false);
-        for (int i = 0; i < quota; i++) {
-            mNowElapsedTest = mTestTimer.getElapsed();
-            mTestTimer.expire();
-        }
-        assertEquals("Incorrect trigger when no quota available", expectedNextTrigger,
-                mTestTimer.getElapsed());
+        testQuotasNoDeferral(trigger -> setAllowWhileIdleAlarm(ELAPSED_REALTIME_WAKEUP, trigger,
+                getNewMockPendingIntent(), false), quota, mAllowWhileIdleWindow);
     }
 
     @Test
@@ -1544,6 +1591,259 @@ public class AlarmManagerServiceTest {
         }
     }
 
+    @Test
+    public void canScheduleExactAlarms() throws RemoteException {
+        doReturn(PermissionChecker.PERMISSION_GRANTED).when(
+                () -> PermissionChecker.checkCallingOrSelfPermissionForPreflight(mMockContext,
+                        Manifest.permission.SCHEDULE_EXACT_ALARM));
+        assertTrue(mBinder.canScheduleExactAlarms());
+
+        doReturn(PermissionChecker.PERMISSION_HARD_DENIED).when(
+                () -> PermissionChecker.checkCallingOrSelfPermissionForPreflight(mMockContext,
+                        Manifest.permission.SCHEDULE_EXACT_ALARM));
+        assertFalse(mBinder.canScheduleExactAlarms());
+
+        doReturn(PermissionChecker.PERMISSION_SOFT_DENIED).when(
+                () -> PermissionChecker.checkCallingOrSelfPermissionForPreflight(mMockContext,
+                        Manifest.permission.SCHEDULE_EXACT_ALARM));
+        assertFalse(mBinder.canScheduleExactAlarms());
+    }
+
+    @Test
+    public void noPermissionCheckWhenChangeDisabled() throws RemoteException {
+        doReturn(false).when(
+                () -> CompatChanges.isChangeEnabled(eq(AlarmManager.REQUIRE_EXACT_ALARM_PERMISSION),
+                        anyString(), any(UserHandle.class)));
+
+        // alarm clock
+        mBinder.set(TEST_CALLING_PACKAGE, ELAPSED_REALTIME_WAKEUP, 1234, WINDOW_EXACT, 0, 0,
+                getNewMockPendingIntent(), null, null, null,
+                mock(AlarmManager.AlarmClockInfo.class));
+
+        // exact, allow-while-idle
+        mBinder.set(TEST_CALLING_PACKAGE, ELAPSED_REALTIME_WAKEUP, 1234, WINDOW_EXACT, 0,
+                FLAG_ALLOW_WHILE_IDLE, getNewMockPendingIntent(), null, null, null, null);
+
+        // inexact, allow-while-idle
+        mBinder.set(TEST_CALLING_PACKAGE, ELAPSED_REALTIME_WAKEUP, 1234, WINDOW_HEURISTIC, 0,
+                FLAG_ALLOW_WHILE_IDLE, getNewMockPendingIntent(), null, null, null, null);
+
+        verify(() -> PermissionChecker.checkCallingOrSelfPermissionForPreflight(mMockContext,
+                Manifest.permission.SCHEDULE_EXACT_ALARM), never());
+        verify(mDeviceIdleInternal, never()).isAppOnWhitelist(anyInt());
+    }
+
+    @Test
+    public void exactAllowWhileIdleBinderCallWhenChangeDisabled() throws Exception {
+        doReturn(false).when(
+                () -> CompatChanges.isChangeEnabled(eq(AlarmManager.REQUIRE_EXACT_ALARM_PERMISSION),
+                        anyString(), any(UserHandle.class)));
+
+        final PendingIntent alarmPi = getNewMockPendingIntent();
+        mBinder.set(TEST_CALLING_PACKAGE, ELAPSED_REALTIME_WAKEUP, 1234, WINDOW_EXACT, 0,
+                FLAG_ALLOW_WHILE_IDLE, alarmPi, null, null, null, null);
+
+        final ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
+        verify(mService).setImpl(eq(ELAPSED_REALTIME_WAKEUP), eq(1234L), eq(WINDOW_EXACT), eq(0L),
+                eq(alarmPi), isNull(), isNull(),
+                eq(FLAG_ALLOW_WHILE_IDLE_COMPAT | FLAG_STANDALONE), isNull(), isNull(),
+                eq(Process.myUid()), eq(TEST_CALLING_PACKAGE), bundleCaptor.capture());
+
+        final Bundle idleOptions = bundleCaptor.getValue();
+        final int type = idleOptions.getInt("android:broadcast.temporaryAppWhitelistType");
+        assertEquals(TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED, type);
+    }
+
+    @Test
+    public void inexactAllowWhileIdleBinderCallWhenChangeDisabled() throws Exception {
+        doReturn(false).when(
+                () -> CompatChanges.isChangeEnabled(eq(AlarmManager.REQUIRE_EXACT_ALARM_PERMISSION),
+                        anyString(), any(UserHandle.class)));
+
+        final PendingIntent alarmPi = getNewMockPendingIntent();
+        mBinder.set(TEST_CALLING_PACKAGE, ELAPSED_REALTIME_WAKEUP, 1234, WINDOW_HEURISTIC, 0,
+                FLAG_ALLOW_WHILE_IDLE, alarmPi, null, null, null, null);
+
+        final ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
+        verify(mService).setImpl(eq(ELAPSED_REALTIME_WAKEUP), eq(1234L), anyLong(), eq(0L),
+                eq(alarmPi), isNull(), isNull(), eq(FLAG_ALLOW_WHILE_IDLE_COMPAT), isNull(),
+                isNull(), eq(Process.myUid()), eq(TEST_CALLING_PACKAGE), bundleCaptor.capture());
+
+        final Bundle idleOptions = bundleCaptor.getValue();
+        final int type = idleOptions.getInt("android:broadcast.temporaryAppWhitelistType");
+        assertEquals(TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED, type);
+    }
+
+    @Test
+    public void alarmClockBinderCallWhenChangeDisabled() throws Exception {
+        doReturn(false).when(
+                () -> CompatChanges.isChangeEnabled(eq(AlarmManager.REQUIRE_EXACT_ALARM_PERMISSION),
+                        anyString(), any(UserHandle.class)));
+
+        final PendingIntent alarmPi = getNewMockPendingIntent();
+        final AlarmManager.AlarmClockInfo alarmClock = mock(AlarmManager.AlarmClockInfo.class);
+        mBinder.set(TEST_CALLING_PACKAGE, RTC_WAKEUP, 1234, WINDOW_EXACT, 0, 0,
+                alarmPi, null, null, null, alarmClock);
+
+        verify(mService).setImpl(eq(RTC_WAKEUP), eq(1234L), eq(WINDOW_EXACT), eq(0L),
+                eq(alarmPi), isNull(), isNull(), eq(FLAG_STANDALONE | FLAG_WAKE_FROM_IDLE),
+                isNull(), eq(alarmClock), eq(Process.myUid()), eq(TEST_CALLING_PACKAGE), isNull());
+    }
+
+    @Test
+    public void alarmClockBinderCall() throws RemoteException {
+        doReturn(true).when(
+                () -> CompatChanges.isChangeEnabled(eq(AlarmManager.REQUIRE_EXACT_ALARM_PERMISSION),
+                        anyString(), any(UserHandle.class)));
+
+        doReturn(PermissionChecker.PERMISSION_GRANTED).when(
+                () -> PermissionChecker.checkCallingOrSelfPermissionForPreflight(mMockContext,
+                        Manifest.permission.SCHEDULE_EXACT_ALARM));
+
+        final PendingIntent alarmPi = getNewMockPendingIntent();
+        final AlarmManager.AlarmClockInfo alarmClock = mock(AlarmManager.AlarmClockInfo.class);
+        mBinder.set(TEST_CALLING_PACKAGE, RTC_WAKEUP, 1234, WINDOW_EXACT, 0, 0,
+                alarmPi, null, null, null, alarmClock);
+
+        // Correct permission checks are invoked.
+        verify(() -> PermissionChecker.checkCallingOrSelfPermissionForPreflight(mMockContext,
+                Manifest.permission.SCHEDULE_EXACT_ALARM));
+        verify(mDeviceIdleInternal, never()).isAppOnWhitelist(anyInt());
+
+        final ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
+        verify(mService).setImpl(eq(RTC_WAKEUP), eq(1234L), eq(WINDOW_EXACT), eq(0L),
+                eq(alarmPi), isNull(), isNull(), eq(FLAG_STANDALONE | FLAG_WAKE_FROM_IDLE),
+                isNull(), eq(alarmClock), eq(Process.myUid()), eq(TEST_CALLING_PACKAGE),
+                bundleCaptor.capture());
+
+        final Bundle idleOptions = bundleCaptor.getValue();
+        final int type = idleOptions.getInt("android:broadcast.temporaryAppWhitelistType");
+        assertEquals(TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED, type);
+    }
+
+    @Test
+    public void exactAllowWhileIdleBinderCallWithPermission() throws RemoteException {
+        doReturn(true).when(
+                () -> CompatChanges.isChangeEnabled(eq(AlarmManager.REQUIRE_EXACT_ALARM_PERMISSION),
+                        anyString(), any(UserHandle.class)));
+
+        // Permission check is granted by default by the mock.
+        final PendingIntent alarmPi = getNewMockPendingIntent();
+        mBinder.set(TEST_CALLING_PACKAGE, ELAPSED_REALTIME_WAKEUP, 1234, WINDOW_EXACT, 0,
+                FLAG_ALLOW_WHILE_IDLE, alarmPi, null, null, null, null);
+
+        verify(() -> PermissionChecker.checkCallingOrSelfPermissionForPreflight(mMockContext,
+                Manifest.permission.SCHEDULE_EXACT_ALARM));
+        verify(mDeviceIdleInternal, never()).isAppOnWhitelist(anyInt());
+
+        final ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
+        verify(mService).setImpl(eq(ELAPSED_REALTIME_WAKEUP), eq(1234L), eq(WINDOW_EXACT), eq(0L),
+                eq(alarmPi), isNull(), isNull(),
+                eq(FLAG_ALLOW_WHILE_IDLE | FLAG_STANDALONE), isNull(), isNull(),
+                eq(Process.myUid()), eq(TEST_CALLING_PACKAGE), bundleCaptor.capture());
+
+        final Bundle idleOptions = bundleCaptor.getValue();
+        final int type = idleOptions.getInt("android:broadcast.temporaryAppWhitelistType");
+        assertEquals(TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED, type);
+    }
+
+    @Test
+    public void exactAllowWhileIdleBinderCallWithAllowlist() throws RemoteException {
+        doReturn(true).when(
+                () -> CompatChanges.isChangeEnabled(eq(AlarmManager.REQUIRE_EXACT_ALARM_PERMISSION),
+                        anyString(), any(UserHandle.class)));
+        // If permission is denied, only then allowlist will be checked.
+        doReturn(PermissionChecker.PERMISSION_HARD_DENIED).when(
+                () -> PermissionChecker.checkCallingOrSelfPermissionForPreflight(mMockContext,
+                        Manifest.permission.SCHEDULE_EXACT_ALARM));
+        when(mDeviceIdleInternal.isAppOnWhitelist(anyInt())).thenReturn(true);
+
+        final PendingIntent alarmPi = getNewMockPendingIntent();
+        mBinder.set(TEST_CALLING_PACKAGE, ELAPSED_REALTIME_WAKEUP, 1234, WINDOW_EXACT, 0,
+                FLAG_ALLOW_WHILE_IDLE, alarmPi, null, null, null, null);
+
+        verify(() -> PermissionChecker.checkCallingOrSelfPermissionForPreflight(mMockContext,
+                Manifest.permission.SCHEDULE_EXACT_ALARM));
+        verify(mDeviceIdleInternal).isAppOnWhitelist(UserHandle.getAppId(Process.myUid()));
+
+        final ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
+        verify(mService).setImpl(eq(ELAPSED_REALTIME_WAKEUP), eq(1234L), eq(WINDOW_EXACT), eq(0L),
+                eq(alarmPi), isNull(), isNull(),
+                eq(FLAG_ALLOW_WHILE_IDLE_COMPAT | FLAG_STANDALONE), isNull(), isNull(),
+                eq(Process.myUid()), eq(TEST_CALLING_PACKAGE), bundleCaptor.capture());
+
+        final Bundle idleOptions = bundleCaptor.getValue();
+        final int type = idleOptions.getInt("android:broadcast.temporaryAppWhitelistType");
+        assertEquals(TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED, type);
+    }
+
+    @Test
+    public void inexactAllowWhileIdleBinderCallWithAllowlist() throws RemoteException {
+        doReturn(true).when(
+                () -> CompatChanges.isChangeEnabled(eq(AlarmManager.REQUIRE_EXACT_ALARM_PERMISSION),
+                        anyString(), any(UserHandle.class)));
+
+        when(mDeviceIdleInternal.isAppOnWhitelist(anyInt())).thenReturn(true);
+        final PendingIntent alarmPi = getNewMockPendingIntent();
+        mBinder.set(TEST_CALLING_PACKAGE, ELAPSED_REALTIME_WAKEUP, 4321, WINDOW_HEURISTIC, 0,
+                FLAG_ALLOW_WHILE_IDLE, alarmPi, null, null, null, null);
+
+        verify(() -> PermissionChecker.checkCallingOrSelfPermissionForPreflight(mMockContext,
+                Manifest.permission.SCHEDULE_EXACT_ALARM), never());
+        verify(mDeviceIdleInternal).isAppOnWhitelist(UserHandle.getAppId(Process.myUid()));
+
+        final ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
+        verify(mService).setImpl(eq(ELAPSED_REALTIME_WAKEUP), eq(4321L), anyLong(), eq(0L),
+                eq(alarmPi), isNull(), isNull(), eq(FLAG_ALLOW_WHILE_IDLE_COMPAT), isNull(),
+                isNull(), eq(Process.myUid()), eq(TEST_CALLING_PACKAGE), bundleCaptor.capture());
+
+        final Bundle idleOptions = bundleCaptor.getValue();
+        final int type = idleOptions.getInt("android:broadcast.temporaryAppWhitelistType");
+        assertEquals(TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED, type);
+    }
+
+    @Test
+    public void inexactAllowWhileIdleBinderCallWithoutAllowlist() throws RemoteException {
+        doReturn(true).when(
+                () -> CompatChanges.isChangeEnabled(eq(AlarmManager.REQUIRE_EXACT_ALARM_PERMISSION),
+                        anyString(), any(UserHandle.class)));
+
+        when(mDeviceIdleInternal.isAppOnWhitelist(anyInt())).thenReturn(false);
+        final PendingIntent alarmPi = getNewMockPendingIntent();
+        mBinder.set(TEST_CALLING_PACKAGE, ELAPSED_REALTIME_WAKEUP, 4321, WINDOW_HEURISTIC, 0,
+                FLAG_ALLOW_WHILE_IDLE, alarmPi, null, null, null, null);
+
+        verify(() -> PermissionChecker.checkCallingOrSelfPermissionForPreflight(mMockContext,
+                Manifest.permission.SCHEDULE_EXACT_ALARM), never());
+        verify(mDeviceIdleInternal).isAppOnWhitelist(UserHandle.getAppId(Process.myUid()));
+
+        final ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
+        verify(mService).setImpl(eq(ELAPSED_REALTIME_WAKEUP), eq(4321L), anyLong(), eq(0L),
+                eq(alarmPi), isNull(), isNull(), eq(FLAG_ALLOW_WHILE_IDLE_COMPAT), isNull(),
+                isNull(), eq(Process.myUid()), eq(TEST_CALLING_PACKAGE), bundleCaptor.capture());
+
+        final Bundle idleOptions = bundleCaptor.getValue();
+        final int type = idleOptions.getInt("android:broadcast.temporaryAppWhitelistType");
+        assertEquals(TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED, type);
+    }
+
+    @Test
+    public void idleOptionsSentOnExpiration() throws Exception {
+        final long triggerTime = mNowElapsedTest + 5000;
+        final PendingIntent alarmPi = getNewMockPendingIntent();
+        final Bundle idleOptions = new Bundle();
+        idleOptions.putChar("TEST_CHAR_KEY", 'x');
+        idleOptions.putInt("TEST_INT_KEY", 53);
+        setTestAlarm(ELAPSED_REALTIME_WAKEUP, triggerTime, alarmPi, 0, 0, TEST_CALLING_UID,
+                idleOptions);
+
+        mNowElapsedTest = mTestTimer.getElapsed();
+        mTestTimer.expire();
+
+        verify(alarmPi).send(eq(mMockContext), eq(0), any(Intent.class),
+                any(), any(Handler.class), isNull(), eq(idleOptions));
+    }
+
     @Test
     public void alarmStoreMigration() {
         setDeviceConfigBoolean(KEY_LAZY_BATCHING, false);
diff --git a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmStoreTest.java b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmStoreTest.java
index 42fa3d480046..12894ae4f75e 100644
--- a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmStoreTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmStoreTest.java
@@ -69,12 +69,12 @@ public class AlarmStoreTest {
                 mock(PendingIntent.class));
         return new Alarm(ELAPSED_REALTIME_WAKEUP, whenElapsed, whenElapsed, 0, 0,
                 mock(PendingIntent.class), null, null, null, 0, info, TEST_CALLING_UID,
-                TEST_CALLING_PACKAGE);
+                TEST_CALLING_PACKAGE, null);
     }
 
     private static Alarm createAlarm(int type, long whenElapsed, long windowLength, int flags) {
         return new Alarm(type, whenElapsed, whenElapsed, windowLength, 0, mock(PendingIntent.class),
-                null, null, null, flags, null, TEST_CALLING_UID, TEST_CALLING_PACKAGE);
+                null, null, null, flags, null, TEST_CALLING_UID, TEST_CALLING_PACKAGE, null);
     }
 
     private void addAlarmsToStore(Alarm... alarms) {
diff --git a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmTest.java b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmTest.java
index e80f0655e641..b64528c2f4d4 100644
--- a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmTest.java
@@ -17,10 +17,17 @@
 package com.android.server.alarm;
 
 import static android.app.AlarmManager.ELAPSED_REALTIME;
+import static android.app.AlarmManager.FLAG_ALLOW_WHILE_IDLE;
+import static android.app.AlarmManager.FLAG_ALLOW_WHILE_IDLE_COMPAT;
+import static android.app.AlarmManager.FLAG_ALLOW_WHILE_IDLE_UNRESTRICTED;
+import static android.app.AlarmManager.FLAG_STANDALONE;
+import static android.app.AlarmManager.FLAG_WAKE_FROM_IDLE;
+import static android.app.AlarmManager.RTC_WAKEUP;
 
 import static com.android.server.alarm.Alarm.APP_STANDBY_POLICY_INDEX;
 import static com.android.server.alarm.Alarm.NUM_POLICIES;
 import static com.android.server.alarm.Alarm.REQUESTER_POLICY_INDEX;
+import static com.android.server.alarm.AlarmManagerService.isExemptFromAppStandby;
 import static com.android.server.alarm.Constants.TEST_CALLING_PACKAGE;
 import static com.android.server.alarm.Constants.TEST_CALLING_UID;
 
@@ -28,7 +35,9 @@ import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
+import android.app.AlarmManager;
 import android.app.PendingIntent;
 import android.platform.test.annotations.Presubmit;
 
@@ -43,15 +52,29 @@ import java.util.Random;
 @RunWith(AndroidJUnit4.class)
 public class AlarmTest {
 
-    private Alarm createDefaultAlarm(long requestedElapsed, long windowLength) {
+    private Alarm createDefaultAlarm(long requestedElapsed, long windowLength, int flags) {
         return new Alarm(ELAPSED_REALTIME, 0, requestedElapsed, windowLength, 0,
-                mock(PendingIntent.class), null, null, null, 0, null, TEST_CALLING_UID,
-                TEST_CALLING_PACKAGE);
+                createAlarmSender(), null, null, null, flags, null, TEST_CALLING_UID,
+                TEST_CALLING_PACKAGE, null);
+    }
+
+    private Alarm createAlarmClock(long requestedRtc) {
+        final AlarmManager.AlarmClockInfo info = mock(AlarmManager.AlarmClockInfo.class);
+        return new Alarm(RTC_WAKEUP, requestedRtc, requestedRtc, 0, 0, createAlarmSender(),
+                null, null, null, FLAG_WAKE_FROM_IDLE | FLAG_STANDALONE, info, TEST_CALLING_UID,
+                TEST_CALLING_PACKAGE, null);
+    }
+
+    private PendingIntent createAlarmSender() {
+        final PendingIntent alarmPi = mock(PendingIntent.class);
+        when(alarmPi.getCreatorPackage()).thenReturn(TEST_CALLING_PACKAGE);
+        when(alarmPi.getCreatorUid()).thenReturn(TEST_CALLING_UID);
+        return alarmPi;
     }
 
     @Test
     public void initSetsOnlyRequesterPolicy() {
-        final Alarm a = createDefaultAlarm(4567, 2);
+        final Alarm a = createDefaultAlarm(4567, 2, 0);
 
         for (int i = 0; i < NUM_POLICIES; i++) {
             if (i == REQUESTER_POLICY_INDEX) {
@@ -86,7 +109,7 @@ public class AlarmTest {
 
     @Test
     public void whenElapsed() {
-        final Alarm a = createDefaultAlarm(0, 0);
+        final Alarm a = createDefaultAlarm(0, 0, 0);
 
         final long[][] uniqueData = generatePolicyTestMatrix(NUM_POLICIES);
         for (int i = 0; i < NUM_POLICIES; i++) {
@@ -104,7 +127,7 @@ public class AlarmTest {
 
     @Test
     public void maxWhenElapsed() {
-        final Alarm a = createDefaultAlarm(10, 12);
+        final Alarm a = createDefaultAlarm(10, 12, 0);
         assertEquals(22, a.getMaxWhenElapsed());
 
         a.setPolicyElapsed(REQUESTER_POLICY_INDEX, 15);
@@ -128,7 +151,7 @@ public class AlarmTest {
 
     @Test
     public void setPolicyElapsedExact() {
-        final Alarm exactAlarm = createDefaultAlarm(10, 0);
+        final Alarm exactAlarm = createDefaultAlarm(10, 0, 0);
 
         assertTrue(exactAlarm.setPolicyElapsed(REQUESTER_POLICY_INDEX, 4));
         assertTrue(exactAlarm.setPolicyElapsed(APP_STANDBY_POLICY_INDEX, 10));
@@ -143,7 +166,7 @@ public class AlarmTest {
 
     @Test
     public void setPolicyElapsedInexact() {
-        final Alarm inexactAlarm = createDefaultAlarm(10, 5);
+        final Alarm inexactAlarm = createDefaultAlarm(10, 5, 0);
 
         assertTrue(inexactAlarm.setPolicyElapsed(REQUESTER_POLICY_INDEX, 4));
         assertTrue(inexactAlarm.setPolicyElapsed(APP_STANDBY_POLICY_INDEX, 10));
@@ -154,4 +177,20 @@ public class AlarmTest {
 
         assertFalse(inexactAlarm.setPolicyElapsed(APP_STANDBY_POLICY_INDEX, 8));
     }
+
+    @Test
+    public void isExemptFromStandby() {
+        final long anything = 35412;    // Arbitrary number, doesn't matter for this test.
+
+        assertFalse("Basic alarm exempt", isExemptFromAppStandby(
+                createDefaultAlarm(anything, anything, 0)));
+        assertFalse("FLAG_ALLOW_WHILE_IDLE_COMPAT exempt", isExemptFromAppStandby(
+                createDefaultAlarm(anything, anything, FLAG_ALLOW_WHILE_IDLE_COMPAT)));
+
+        assertTrue("ALLOW_WHILE_IDLE not exempt", isExemptFromAppStandby(
+                createDefaultAlarm(anything, anything, FLAG_ALLOW_WHILE_IDLE)));
+        assertTrue("ALLOW_WHILE_IDLE_UNRESTRICTED not exempt", isExemptFromAppStandby(
+                createDefaultAlarm(anything, anything, FLAG_ALLOW_WHILE_IDLE_UNRESTRICTED)));
+        assertTrue("Alarm clock not exempt", isExemptFromAppStandby(createAlarmClock(anything)));
+    }
 }
diff --git a/services/tests/mockingservicestests/src/com/android/server/alarm/BackgroundRestrictedAlarmsTest.java b/services/tests/mockingservicestests/src/com/android/server/alarm/BackgroundRestrictedAlarmsTest.java
index 5bb6a42b2604..0e795a938d48 100644
--- a/services/tests/mockingservicestests/src/com/android/server/alarm/BackgroundRestrictedAlarmsTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/alarm/BackgroundRestrictedAlarmsTest.java
@@ -45,7 +45,7 @@ public class BackgroundRestrictedAlarmsTest {
         }
         uidAlarms.add(new Alarm(
                 removeIt ? RTC : RTC_WAKEUP,
-                0, 0, 0, 0, null, null, null, null, 0, null, uid, name));
+                0, 0, 0, 0, null, null, null, null, 0, null, uid, name, null));
         return all;
     }
 
-- 
GitLab