Extreme battery saver: AlarmManager

- AlarmManagerService now uses ForceAppStandbyTracker.

- Now AlarmManagerService uses the system + user power-save whitelist,
rather than just the user whitelist.

Test: Manual test
Test: atest frameworks/base/services/tests/servicestests/src/com/android/server/ForceAppStandbyTrackerTest.java
Bug 68769804

Change-Id: Ie2bd17fe0c3cb8b09ec4c4a78f9254277be16926
diff --git a/core/proto/android/server/alarmmanagerservice.proto b/core/proto/android/server/alarmmanagerservice.proto
index d2cd190..d724437 100644
--- a/core/proto/android/server/alarmmanagerservice.proto
+++ b/core/proto/android/server/alarmmanagerservice.proto
@@ -20,6 +20,7 @@
 import "frameworks/base/core/proto/android/app/pendingintent.proto";
 import "frameworks/base/core/proto/android/internal/locallog.proto";
 import "frameworks/base/core/proto/android/os/worksource.proto";
+import "frameworks/base/core/proto/android/server/forceappstandbytracker.proto";
 
 package com.android.server;
 
@@ -32,10 +33,9 @@
   optional int64 last_time_change_realtime = 4;
   // Current settings
   optional ConstantsProto settings = 5;
-  // UIDs currently in the foreground.
-  repeated int32 foreground_uids = 6;
-  // Packages forced into app standby.
-  repeated string forced_app_standby_packages = 7;
+
+  // Dump from ForceAppStandbyTracker.
+  optional ForceAppStandbyTrackerProto force_app_standby_tracker = 6;
 
   optional bool is_interactive = 8;
   // Only valid if is_interactive is false.
diff --git a/core/proto/android/server/forceappstandbytracker.proto b/core/proto/android/server/forceappstandbytracker.proto
new file mode 100644
index 0000000..8753bf7
--- /dev/null
+++ b/core/proto/android/server/forceappstandbytracker.proto
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+syntax = "proto2";
+
+package com.android.server;
+
+option java_multiple_files = true;
+
+// Dump from ForceAppStandbyTracker.
+message ForceAppStandbyTrackerProto {
+  // Whether all apps are forced standby or not.
+  optional bool force_all_apps_standby = 1;
+
+  // UIDs currently in the foreground.
+  repeated int32 foreground_uids = 2;
+
+  // App ids that are in power-save whitelist.
+  repeated int32 power_save_whitelist_app_ids = 3;
+
+  // App ids that are in temporary power-save whitelist.
+  repeated int32 temp_power_save_whitelist_app_ids = 4;
+
+  message RunAnyInBackgroundRestrictedPackages {
+    optional int32 uid = 1;
+    optional string package_name = 2;
+  }
+
+  // Packages that are disallowed OP_RUN_ANY_IN_BACKGROUND.
+  repeated RunAnyInBackgroundRestrictedPackages run_any_in_background_restricted_packages = 5;
+}
diff --git a/services/core/java/com/android/server/AlarmManagerService.java b/services/core/java/com/android/server/AlarmManagerService.java
index 3904fc9..4330f5d 100644
--- a/services/core/java/com/android/server/AlarmManagerService.java
+++ b/services/core/java/com/android/server/AlarmManagerService.java
@@ -46,7 +46,6 @@
 import android.os.PowerManager;
 import android.os.Process;
 import android.os.RemoteException;
-import android.os.ServiceManager;
 import android.os.SystemClock;
 import android.os.SystemProperties;
 import android.os.UserHandle;
@@ -55,7 +54,6 @@
 import android.text.TextUtils;
 import android.text.format.DateFormat;
 import android.util.ArrayMap;
-import android.util.ArraySet;
 import android.util.KeyValueListParser;
 import android.util.Log;
 import android.util.Slog;
@@ -81,6 +79,7 @@
 import java.util.Random;
 import java.util.TimeZone;
 import java.util.TreeSet;
+import java.util.function.Predicate;
 
 import static android.app.AlarmManager.RTC_WAKEUP;
 import static android.app.AlarmManager.RTC;
@@ -88,11 +87,17 @@
 import static android.app.AlarmManager.ELAPSED_REALTIME;
 
 import com.android.internal.annotations.GuardedBy;
-import com.android.internal.app.IAppOpsCallback;
-import com.android.internal.app.IAppOpsService;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.LocalLog;
+import com.android.server.ForceAppStandbyTracker.Listener;
 
+/**
+ * Alarm manager implementaion.
+ *
+ * Unit test:
+ atest $ANDROID_BUILD_TOP/frameworks/base/services/tests/servicestests/src/com/android/server/AlarmManagerServiceTest.java
+ */
 class AlarmManagerService extends SystemService {
     private static final int RTC_WAKEUP_MASK = 1 << RTC_WAKEUP;
     private static final int RTC_MASK = 1 << RTC;
@@ -131,13 +136,10 @@
     final LocalLog mLog = new LocalLog(TAG);
 
     AppOpsManager mAppOps;
-    IAppOpsService mAppOpsService;
     DeviceIdleController.LocalService mLocalDeviceIdleController;
 
     final Object mLock = new Object();
 
-    ArraySet<String> mForcedAppStandbyPackages = new ArraySet<>();
-    SparseBooleanArray mForegroundUids = new SparseBooleanArray();
     // List of alarms per uid deferred due to user applied background restrictions on the source app
     SparseArray<ArrayList<Alarm>> mPendingBackgroundAlarms = new SparseArray<>();
     long mNativeData;
@@ -184,12 +186,6 @@
     int mSystemUiUid;
 
     /**
-     * The current set of user whitelisted apps for device idle mode, meaning these are allowed
-     * to freely schedule alarms.
-     */
-    int[] mDeviceIdleUserWhitelist = new int[0];
-
-    /**
      * For each uid, this is the last time we dispatched an "allow while idle" alarm,
      * used to determine the earliest we can dispatch the next such alarm. Times are in the
      * 'elapsed' timebase.
@@ -223,6 +219,8 @@
     private final SparseArray<AlarmManager.AlarmClockInfo> mHandlerSparseAlarmClockArray =
             new SparseArray<>();
 
+    private final ForceAppStandbyTracker mForceAppStandbyTracker;
+
     /**
      * All times are in milliseconds. These constants are kept synchronized with the system
      * global Settings. Any access to this class or its fields should be done while
@@ -757,6 +755,9 @@
     public AlarmManagerService(Context context) {
         super(context);
         mConstants = new Constants(mHandler);
+
+        mForceAppStandbyTracker = ForceAppStandbyTracker.getInstance(context);
+        mForceAppStandbyTracker.addListener(mForceAppStandbyListener);
     }
 
     static long convertToElapsed(long when, int type) {
@@ -894,17 +895,48 @@
         deliverPendingBackgroundAlarmsLocked(alarmsToDeliver, SystemClock.elapsedRealtime());
     }
 
-    void sendPendingBackgroundAlarmsForAppIdLocked(int appId) {
+    /**
+     * Check all alarms in {@link #mPendingBackgroundAlarms} and send the ones that are not
+     * restricted.
+     *
+     * This is only called when the global "force all apps-standby" flag changes or when the
+     * power save whitelist changes, so it's okay to be slow.
+     */
+    void sendAllUnrestrictedPendingBackgroundAlarmsLocked() {
         final ArrayList<Alarm> alarmsToDeliver = new ArrayList<>();
-        for (int i = mPendingBackgroundAlarms.size() - 1; i >= 0; i--) {
-            final int uid = mPendingBackgroundAlarms.keyAt(i);
-            final ArrayList<Alarm> alarmsForUid = mPendingBackgroundAlarms.valueAt(i);
-            if (UserHandle.getAppId(uid) == appId) {
-                alarmsToDeliver.addAll(alarmsForUid);
-                mPendingBackgroundAlarms.removeAt(i);
+
+        findAllUnrestrictedPendingBackgroundAlarmsLockedInner(
+                mPendingBackgroundAlarms, alarmsToDeliver, this::isBackgroundRestricted);
+
+        if (alarmsToDeliver.size() > 0) {
+            deliverPendingBackgroundAlarmsLocked(alarmsToDeliver, SystemClock.elapsedRealtime());
+        }
+    }
+
+    @VisibleForTesting
+    static void findAllUnrestrictedPendingBackgroundAlarmsLockedInner(
+            SparseArray<ArrayList<Alarm>> pendingAlarms, ArrayList<Alarm> unrestrictedAlarms,
+            Predicate<Alarm> isBackgroundRestricted) {
+
+        for (int uidIndex = pendingAlarms.size() - 1; uidIndex >= 0; uidIndex--) {
+            final int uid = pendingAlarms.keyAt(uidIndex);
+            final ArrayList<Alarm> alarmsForUid = pendingAlarms.valueAt(uidIndex);
+
+            for (int alarmIndex = alarmsForUid.size() - 1; alarmIndex >= 0; alarmIndex--) {
+                final Alarm alarm = alarmsForUid.get(alarmIndex);
+
+                if (isBackgroundRestricted.test(alarm)) {
+                    continue;
+                }
+
+                unrestrictedAlarms.add(alarm);
+                alarmsForUid.remove(alarmIndex);
+            }
+
+            if (alarmsForUid.size() == 0) {
+                pendingAlarms.removeAt(uidIndex);
             }
         }
-        deliverPendingBackgroundAlarmsLocked(alarmsToDeliver, SystemClock.elapsedRealtime());
     }
 
     private void deliverPendingBackgroundAlarmsLocked(ArrayList<Alarm> alarms, long nowELAPSED) {
@@ -1234,10 +1266,8 @@
         } catch (RemoteException e) {
             // ignored; both services live in system_server
         }
-        mAppOpsService = IAppOpsService.Stub.asInterface(
-                ServiceManager.getService(Context.APP_OPS_SERVICE));
         publishBinderService(Context.ALARM_SERVICE, mService);
-        publishLocalService(LocalService.class, new LocalService());
+        mForceAppStandbyTracker.start();
     }
 
     @Override
@@ -1247,13 +1277,6 @@
             mAppOps = (AppOpsManager) getContext().getSystemService(Context.APP_OPS_SERVICE);
             mLocalDeviceIdleController
                     = LocalServices.getService(DeviceIdleController.LocalService.class);
-            try {
-                mAppOpsService.startWatchingMode(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, null,
-                        new AppOpsWatcher());
-            } catch (RemoteException rexc) {
-                // Shouldn't happen as they are in the same process.
-                Slog.e(TAG, "AppOps service not reachable", rexc);
-            }
         }
     }
 
@@ -1582,8 +1605,7 @@
             // timing restrictions.
             } else if (workSource == null && (callingUid < Process.FIRST_APPLICATION_UID
                     || callingUid == mSystemUiUid
-                    || Arrays.binarySearch(mDeviceIdleUserWhitelist,
-                            UserHandle.getAppId(callingUid)) >= 0)) {
+                    || mForceAppStandbyTracker.isUidPowerSaveWhitelisted(callingUid))) {
                 flags |= AlarmManager.FLAG_ALLOW_WHILE_IDLE_UNRESTRICTED;
                 flags &= ~AlarmManager.FLAG_ALLOW_WHILE_IDLE;
             }
@@ -1660,24 +1682,14 @@
         }
     };
 
-    public final class LocalService {
-        public void setDeviceIdleUserWhitelist(int[] appids) {
-            setDeviceIdleUserWhitelistImpl(appids);
-        }
-    }
-
     void dumpImpl(PrintWriter pw) {
         synchronized (mLock) {
             pw.println("Current Alarm Manager state:");
             mConstants.dump(pw);
             pw.println();
 
-            pw.print("  Foreground uids: [");
-            for (int i = 0; i < mForegroundUids.size(); i++) {
-                if (mForegroundUids.valueAt(i)) pw.print(mForegroundUids.keyAt(i) + " ");
-            }
-            pw.println("]");
-            pw.println("  Forced app standby packages: " + mForcedAppStandbyPackages);
+            mForceAppStandbyTracker.dump(pw, "  ");
+
             final long nowRTC = System.currentTimeMillis();
             final long nowELAPSED = SystemClock.elapsedRealtime();
             SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@@ -1717,7 +1729,6 @@
             pw.print(" set at "); TimeUtils.formatDuration(mLastWakeupSet, nowELAPSED, pw);
             pw.println();
             pw.print("  Num time change events: "); pw.println(mNumTimeChanged);
-            pw.println("  mDeviceIdleUserWhitelist=" + Arrays.toString(mDeviceIdleUserWhitelist));
 
             pw.println();
             pw.println("  Next alarm clock information: ");
@@ -1990,15 +2001,8 @@
 
             mConstants.dumpProto(proto, AlarmManagerServiceProto.SETTINGS);
 
-            final int foregroundUidsSize = mForegroundUids.size();
-            for (int i = 0; i < foregroundUidsSize; i++) {
-                if (mForegroundUids.valueAt(i)) {
-                    proto.write(AlarmManagerServiceProto.FOREGROUND_UIDS, mForegroundUids.keyAt(i));
-                }
-            }
-            for (String pkg : mForcedAppStandbyPackages) {
-                proto.write(AlarmManagerServiceProto.FORCED_APP_STANDBY_PACKAGES, pkg);
-            }
+            mForceAppStandbyTracker.dumpProto(proto,
+                    AlarmManagerServiceProto.FORCE_APP_STANDBY_TRACKER);
 
             proto.write(AlarmManagerServiceProto.IS_INTERACTIVE, mInteractive);
             if (!mInteractive) {
@@ -2022,9 +2026,6 @@
             proto.write(AlarmManagerServiceProto.TIME_SINCE_LAST_WAKEUP_SET_MS,
                     nowElapsed - mLastWakeupSet);
             proto.write(AlarmManagerServiceProto.TIME_CHANGE_EVENT_COUNT, mNumTimeChanged);
-            for (int i : mDeviceIdleUserWhitelist) {
-                proto.write(AlarmManagerServiceProto.DEVICE_IDLE_USER_WHITELIST_APP_IDS, i);
-            }
 
             final TreeSet<Integer> users = new TreeSet<>();
             final int nextAlarmClockForUserSize = mNextAlarmClockForUser.size();
@@ -2266,28 +2267,6 @@
         }
     }
 
-    void setDeviceIdleUserWhitelistImpl(int[] appids) {
-        synchronized (mLock) {
-            // appids are sorted, just send pending alarms for any new appids added to the whitelist
-            int i = 0, j = 0;
-            while (i < appids.length) {
-                while (j < mDeviceIdleUserWhitelist.length
-                        && mDeviceIdleUserWhitelist[j] < appids[i]) {
-                    j++;
-                }
-                if (j < mDeviceIdleUserWhitelist.length
-                        && appids[i] != mDeviceIdleUserWhitelist[j]) {
-                    if (DEBUG_BG_LIMIT) {
-                        Slog.d(TAG, "Sending blocked alarms for whitelisted appid " + appids[j]);
-                    }
-                    sendPendingBackgroundAlarmsForAppIdLocked(appids[j]);
-                }
-                i++;
-            }
-            mDeviceIdleUserWhitelist = appids;
-        }
-    }
-
     AlarmManager.AlarmClockInfo getNextAlarmClockImpl(int userId) {
         synchronized (mLock) {
             return mNextAlarmClockForUser.get(userId);
@@ -2710,9 +2689,7 @@
         final String sourcePackage =
                 (alarm.operation != null) ? alarm.operation.getCreatorPackage() : alarm.packageName;
         final int sourceUid = alarm.creatorUid;
-        return mForcedAppStandbyPackages.contains(sourcePackage) && !mForegroundUids.get(sourceUid)
-                && Arrays.binarySearch(mDeviceIdleUserWhitelist, UserHandle.getAppId(sourceUid))
-                < 0;
+        return mForceAppStandbyTracker.areAlarmsRestricted(sourceUid, sourcePackage);
     }
 
     private native long init();
@@ -2859,7 +2836,8 @@
         }
     }
 
-    private static class Alarm {
+    @VisibleForTesting
+    static class Alarm {
         public final int type;
         public final long origWhen;
         public final boolean wakeup;
@@ -3476,17 +3454,10 @@
                 if (disabled) {
                     removeForStoppedLocked(uid);
                 }
-                mForegroundUids.delete(uid);
             }
         }
 
         @Override public void onUidActive(int uid) {
-            synchronized (mLock) {
-                if (!mForegroundUids.get(uid)) {
-                    mForegroundUids.put(uid, true);
-                    sendPendingBackgroundAlarmsLocked(uid, null);
-                }
-            }
         }
 
         @Override public void onUidIdle(int uid, boolean disabled) {
@@ -3494,7 +3465,6 @@
                 if (disabled) {
                     removeForStoppedLocked(uid);
                 }
-                mForegroundUids.delete(uid);
             }
         }
 
@@ -3502,27 +3472,29 @@
         }
     };
 
-    private final class AppOpsWatcher extends IAppOpsCallback.Stub {
+
+    private final Listener mForceAppStandbyListener = new Listener() {
         @Override
-        public void opChanged(int op, int uid, String packageName) throws RemoteException {
+        public void unblockAllUnrestrictedAlarms() {
             synchronized (mLock) {
-                final int mode = mAppOpsService.checkOperation(op, uid, packageName);
-                if (DEBUG_BG_LIMIT) {
-                    Slog.d(TAG,
-                            "Appop changed for " + uid + ", " + packageName + " to " + mode);
-                }
-                final boolean changed;
-                if (mode != AppOpsManager.MODE_ALLOWED) {
-                    changed = mForcedAppStandbyPackages.add(packageName);
-                } else {
-                    changed = mForcedAppStandbyPackages.remove(packageName);
-                }
-                if (changed && mode == AppOpsManager.MODE_ALLOWED) {
-                    sendPendingBackgroundAlarmsLocked(uid, packageName);
-                }
+                sendAllUnrestrictedPendingBackgroundAlarmsLocked();
             }
         }
-    }
+
+        @Override
+        public void unblockAlarmsForUid(int uid) {
+            synchronized (mLock) {
+                sendPendingBackgroundAlarmsLocked(uid, null);
+            }
+        }
+
+        @Override
+        public void unblockAlarmsForUidPackage(int uid, String packageName) {
+            synchronized (mLock) {
+                sendPendingBackgroundAlarmsLocked(uid, packageName);
+            }
+        }
+    };
 
     private final BroadcastStats getStatsLocked(PendingIntent pi) {
         String pkg = pi.getCreatorPackage();
diff --git a/services/core/java/com/android/server/DeviceIdleController.java b/services/core/java/com/android/server/DeviceIdleController.java
index 0921a00..d7aeb8c 100644
--- a/services/core/java/com/android/server/DeviceIdleController.java
+++ b/services/core/java/com/android/server/DeviceIdleController.java
@@ -119,7 +119,6 @@
     private PowerManagerInternal mLocalPowerManager;
     private PowerManager mPowerManager;
     private ConnectivityService mConnectivityService;
-    private AlarmManagerService.LocalService mLocalAlarmManager;
     private INetworkPolicyManager mNetworkPolicyManager;
     private SensorManager mSensorManager;
     private Sensor mMotionSensor;
@@ -1435,7 +1434,6 @@
                 mGoingIdleWakeLock.setReferenceCounted(true);
                 mConnectivityService = (ConnectivityService)ServiceManager.getService(
                         Context.CONNECTIVITY_SERVICE);
-                mLocalAlarmManager = getLocalService(AlarmManagerService.LocalService.class);
                 mNetworkPolicyManager = INetworkPolicyManager.Stub.asInterface(
                         ServiceManager.getService(Context.NETWORK_POLICY_SERVICE));
                 mNetworkPolicyManagerInternal = getLocalService(NetworkPolicyManagerInternal.class);
@@ -1500,8 +1498,8 @@
 
                 mLocalActivityManager.setDeviceIdleWhitelist(mPowerSaveWhitelistAllAppIdArray);
                 mLocalPowerManager.setDeviceIdleWhitelist(mPowerSaveWhitelistAllAppIdArray);
-                mLocalAlarmManager.setDeviceIdleUserWhitelist(mPowerSaveWhitelistUserAppIdArray);
 
+                passWhiteListToForceAppStandbyTrackerLocked();
                 updateInteractivityLocked();
             }
             updateConnectivityState(null);
@@ -2477,13 +2475,7 @@
             }
             mLocalPowerManager.setDeviceIdleWhitelist(mPowerSaveWhitelistAllAppIdArray);
         }
-        if (mLocalAlarmManager != null) {
-            if (DEBUG) {
-                Slog.d(TAG, "Setting alarm whitelist to "
-                        + Arrays.toString(mPowerSaveWhitelistUserAppIdArray));
-            }
-            mLocalAlarmManager.setDeviceIdleUserWhitelist(mPowerSaveWhitelistUserAppIdArray);
-        }
+        passWhiteListToForceAppStandbyTrackerLocked();
     }
 
     private void updateTempWhitelistAppIdsLocked(int appId, boolean adding) {
@@ -2509,6 +2501,7 @@
             }
             mLocalPowerManager.setDeviceIdleTempWhitelist(mTempWhitelistAppIdArray);
         }
+        passWhiteListToForceAppStandbyTrackerLocked();
     }
 
     private void reportPowerSaveWhitelistChangedLocked() {
@@ -2523,6 +2516,12 @@
         getContext().sendBroadcastAsUser(intent, UserHandle.SYSTEM);
     }
 
+    private void passWhiteListToForceAppStandbyTrackerLocked() {
+        ForceAppStandbyTracker.getInstance(getContext()).setPowerSaveWhitelistAppIds(
+                mPowerSaveWhitelistAllAppIdArray,
+                mTempWhitelistAppIdArray);
+    }
+
     void readConfigFileLocked() {
         if (DEBUG) Slog.d(TAG, "Reading config from " + mConfigFile.getBaseFile());
         mPowerSaveWhitelistUserApps.clear();
diff --git a/services/core/java/com/android/server/ForceAppStandbyTracker.java b/services/core/java/com/android/server/ForceAppStandbyTracker.java
index 5dd3ee0..61d3833 100644
--- a/services/core/java/com/android/server/ForceAppStandbyTracker.java
+++ b/services/core/java/com/android/server/ForceAppStandbyTracker.java
@@ -16,13 +16,18 @@
 package com.android.server;
 
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.app.AppOpsManager;
 import android.app.AppOpsManager.PackageOps;
+import android.app.IActivityManager;
 import android.app.IUidObserver;
+import android.content.BroadcastReceiver;
 import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
 import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
 import android.os.PowerManager.ServiceType;
 import android.os.PowerManagerInternal;
 import android.os.RemoteException;
@@ -30,21 +35,36 @@
 import android.os.UserHandle;
 import android.util.ArraySet;
 import android.util.Pair;
-import android.util.Slog;
 import android.util.SparseBooleanArray;
+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.ArrayUtils;
 import com.android.internal.util.Preconditions;
+import com.android.server.ForceAppStandbyTrackerProto.RunAnyInBackgroundRestrictedPackages;
 
+import java.io.PrintWriter;
+import java.util.Arrays;
 import java.util.List;
 
 /**
- * Class to track OP_RUN_ANY_IN_BACKGROUND, UID foreground state and "force all app standby".
+ * Class to keep track of the information related to "force app standby", which includes:
+ * - OP_RUN_ANY_IN_BACKGROUND for each package
+ * - UID foreground state
+ * - User+system power save whitelist
+ * - Temporary power save whitelist
+ * - Global "force all apps standby" mode enforced by battery saver.
  *
- * TODO Clean up cache when a user is deleted.
- * TODO Add unit tests. b/68769804.
+ * TODO: In general, we can reduce the number of callbacks by checking all signals before sending
+ *    each callback. For example, even when an UID comes into the foreground, if it wasn't
+ *    originally restricted, then there's no need to send an event.
+ *    Doing this would be error-prone, so we punt it for now, but we should revisit it later.
+ *
+ * Test:
+   atest $ANDROID_BUILD_TOP/frameworks/base/services/tests/servicestests/src/com/android/server/ForceAppStandbyTrackerTest.java
  */
 public class ForceAppStandbyTracker {
     private static final String TAG = "ForceAppStandbyTracker";
@@ -55,22 +75,32 @@
     private final Object mLock = new Object();
     private final Context mContext;
 
+    @VisibleForTesting
+    static final int TARGET_OP = AppOpsManager.OP_RUN_ANY_IN_BACKGROUND;
+
+    IActivityManager mIActivityManager;
     AppOpsManager mAppOpsManager;
     IAppOpsService mAppOpsService;
     PowerManagerInternal mPowerManagerInternal;
 
-    private final Handler mCallbackHandler;
+    private final MyHandler mHandler;
 
     /**
      * Pair of (uid (not user-id), packageName) with OP_RUN_ANY_IN_BACKGROUND *not* allowed.
      */
     @GuardedBy("mLock")
-    final ArraySet<Pair<Integer, String>> mForcedAppStandbyUidPackages = new ArraySet<>();
+    final ArraySet<Pair<Integer, String>> mRunAnyRestrictedPackages = new ArraySet<>();
 
     @GuardedBy("mLock")
     final SparseBooleanArray mForegroundUids = new SparseBooleanArray();
 
     @GuardedBy("mLock")
+    private int[] mPowerWhitelistedAllAppIds = new int[0];
+
+    @GuardedBy("mLock")
+    private int[] mTempWhitelistedAppIds = mPowerWhitelistedAllAppIds;
+
+    @GuardedBy("mLock")
     final ArraySet<Listener> mListeners = new ArraySet<>();
 
     @GuardedBy("mLock")
@@ -80,16 +110,116 @@
     boolean mForceAllAppsStandby;
 
     public static abstract class Listener {
-        public void onRestrictionChanged(int uid, @Nullable String packageName) {
+        /**
+         * This is called when the OP_RUN_ANY_IN_BACKGROUND appops changed for a package.
+         */
+        private void onRunAnyAppOpsChanged(ForceAppStandbyTracker sender,
+                int uid, @NonNull String packageName) {
+            updateJobsForUidPackage(uid, packageName);
+
+            if (!sender.areAlarmsRestricted(uid, packageName)) {
+                unblockAlarmsForUidPackage(uid, packageName);
+            }
         }
 
-        public void onGlobalRestrictionChanged() {
+        /**
+         * This is called when the foreground state changed for a UID.
+         */
+        private void onUidForegroundStateChanged(ForceAppStandbyTracker sender, int uid) {
+            updateJobsForUid(uid);
+
+            if (sender.isInForeground(uid)) {
+                unblockAlarmsForUid(uid);
+            }
+        }
+
+        /**
+         * This is called when an app-id(s) is removed from the power save whitelist.
+         */
+        private void onPowerSaveUnwhitelisted(ForceAppStandbyTracker sender) {
+            updateAllJobs();
+            unblockAllUnrestrictedAlarms();
+        }
+
+        /**
+         * This is called when the power save whitelist changes, excluding the
+         * {@link #onPowerSaveUnwhitelisted} case.
+         */
+        private void onPowerSaveWhitelistedChanged(ForceAppStandbyTracker sender) {
+            updateAllJobs();
+        }
+
+        /**
+         * This is called when the temp whitelist changes.
+         */
+        private void onTempPowerSaveWhitelistChanged(ForceAppStandbyTracker sender) {
+
+            // TODO This case happens rather frequently; consider optimizing and update jobs
+            // only for affected app-ids.
+
+            updateAllJobs();
+        }
+
+        /**
+         * This is called when the global "force all apps standby" flag changes.
+         */
+        private void onForceAllAppsStandbyChanged(ForceAppStandbyTracker sender) {
+            updateAllJobs();
+
+            if (!sender.isForceAllAppsStandbyEnabled()) {
+                unblockAllUnrestrictedAlarms();
+            }
+        }
+
+        /**
+         * Called when the job restrictions for multiple UIDs might have changed, so the job
+         * scheduler should re-evaluate all restrictions for all jobs.
+         */
+        public void updateAllJobs() {
+        }
+
+        /**
+         * Called when the job restrictions for a UID might have changed, so the job
+         * scheduler should re-evaluate all restrictions for all jobs.
+         */
+        public void updateJobsForUid(int uid) {
+        }
+
+        /**
+         * Called when the job restrictions for a UID - package might have changed, so the job
+         * scheduler should re-evaluate all restrictions for all jobs.
+         */
+        public void updateJobsForUidPackage(int uid, String packageName) {
+        }
+
+        /**
+         * Called when the job restrictions for multiple UIDs might have changed, so the alarm
+         * manager should re-evaluate all restrictions for all blocked jobs.
+         */
+        public void unblockAllUnrestrictedAlarms() {
+        }
+
+        /**
+         * Called when all jobs for a specific UID are unblocked.
+         */
+        public void unblockAlarmsForUid(int uid) {
+        }
+
+        /**
+         * Called when all alarms for a specific UID - package are unblocked.
+         */
+        public void unblockAlarmsForUidPackage(int uid, String packageName) {
         }
     }
 
-    private ForceAppStandbyTracker(Context context) {
+    @VisibleForTesting
+    ForceAppStandbyTracker(Context context, Looper looper) {
         mContext = context;
-        mCallbackHandler = FgThread.getHandler();
+        mHandler = new MyHandler(looper);
+    }
+
+    private ForceAppStandbyTracker(Context context) {
+        this(context, FgThread.get().getLooper());
     }
 
     /**
@@ -112,45 +242,65 @@
             }
             mStarted = true;
 
-            mAppOpsManager = Preconditions.checkNotNull(
-                    mContext.getSystemService(AppOpsManager.class));
-            mAppOpsService = Preconditions.checkNotNull(
-                    IAppOpsService.Stub.asInterface(
-                            ServiceManager.getService(Context.APP_OPS_SERVICE)));
-            mPowerManagerInternal = Preconditions.checkNotNull(
-                    LocalServices.getService(PowerManagerInternal.class));
+            mIActivityManager = Preconditions.checkNotNull(injectIActivityManager());
+            mAppOpsManager = Preconditions.checkNotNull(injectAppOpsManager());
+            mAppOpsService = Preconditions.checkNotNull(injectIAppOpsService());
+            mPowerManagerInternal = Preconditions.checkNotNull(injectPowerManagerInternal());
 
             try {
-                ActivityManager.getService().registerUidObserver(new UidObserver(),
+                mIActivityManager.registerUidObserver(new UidObserver(),
                         ActivityManager.UID_OBSERVER_GONE | ActivityManager.UID_OBSERVER_IDLE
                                 | ActivityManager.UID_OBSERVER_ACTIVE,
                         ActivityManager.PROCESS_STATE_UNKNOWN, null);
-                mAppOpsService.startWatchingMode(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, null,
+                mAppOpsService.startWatchingMode(TARGET_OP, null,
                         new AppOpsWatcher());
             } catch (RemoteException e) {
                 // shouldn't happen.
             }
 
-            mPowerManagerInternal.registerLowPowerModeObserver(
-                    ServiceType.FORCE_ALL_APPS_STANDBY,
-                    state -> updateForceAllAppsStandby(state.batterySaverEnabled));
-
-            updateForceAllAppsStandby(
-                    mPowerManagerInternal.getLowPowerState(ServiceType.FORCE_ALL_APPS_STANDBY)
-                            .batterySaverEnabled);
+            IntentFilter filter = new IntentFilter();
+            filter.addAction(Intent.ACTION_USER_REMOVED);
+            mContext.registerReceiver(new MyReceiver(), filter);
 
             refreshForcedAppStandbyUidPackagesLocked();
+
+            mPowerManagerInternal.registerLowPowerModeObserver(
+                    ServiceType.FORCE_ALL_APPS_STANDBY,
+                    (state) -> updateForceAllAppsStandby(state.batterySaverEnabled));
+
+            updateForceAllAppsStandby(mPowerManagerInternal.getLowPowerState(
+                    ServiceType.FORCE_ALL_APPS_STANDBY).batterySaverEnabled);
         }
     }
 
+    @VisibleForTesting
+    AppOpsManager injectAppOpsManager() {
+        return mContext.getSystemService(AppOpsManager.class);
+    }
+
+    @VisibleForTesting
+    IAppOpsService injectIAppOpsService() {
+        return IAppOpsService.Stub.asInterface(
+                ServiceManager.getService(Context.APP_OPS_SERVICE));
+    }
+
+    @VisibleForTesting
+    IActivityManager injectIActivityManager() {
+        return ActivityManager.getService();
+    }
+
+    @VisibleForTesting
+    PowerManagerInternal injectPowerManagerInternal() {
+        return LocalServices.getService(PowerManagerInternal.class);
+    }
+
     /**
-     * Update {@link #mForcedAppStandbyUidPackages} with the current app ops state.
+     * Update {@link #mRunAnyRestrictedPackages} with the current app ops state.
      */
     private void refreshForcedAppStandbyUidPackagesLocked() {
-        final int op = AppOpsManager.OP_RUN_ANY_IN_BACKGROUND;
-
-        mForcedAppStandbyUidPackages.clear();
-        final List<PackageOps> ops = mAppOpsManager.getPackagesForOps(new int[] {op});
+        mRunAnyRestrictedPackages.clear();
+        final List<PackageOps> ops = mAppOpsManager.getPackagesForOps(
+                new int[] {TARGET_OP});
 
         if (ops == null) {
             return;
@@ -162,31 +312,38 @@
 
             for (int j = 0; j < entries.size(); j++) {
                 AppOpsManager.OpEntry ent = entries.get(j);
-                if (ent.getOp() != op) {
+                if (ent.getOp() != TARGET_OP) {
                     continue;
                 }
                 if (ent.getMode() != AppOpsManager.MODE_ALLOWED) {
-                    mForcedAppStandbyUidPackages.add(Pair.create(
+                    mRunAnyRestrictedPackages.add(Pair.create(
                             pkg.getUid(), pkg.getPackageName()));
                 }
             }
         }
     }
 
-    boolean isRunAnyInBackgroundAppOpRestricted(int uid, @NonNull String packageName) {
-        try {
-            return mAppOpsService.checkOperation(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND,
-                    uid, packageName) != AppOpsManager.MODE_ALLOWED;
-        } catch (RemoteException e) {
-            return false; // shouldn't happen.
+    /**
+     * Update {@link #mForceAllAppsStandby} and notifies the listeners.
+     */
+    void updateForceAllAppsStandby(boolean enable) {
+        synchronized (mLock) {
+            if (enable == mForceAllAppsStandby) {
+                return;
+            }
+            mForceAllAppsStandby = enable;
+
+            mHandler.notifyForceAllAppsStandbyChanged();
         }
     }
 
     private int findForcedAppStandbyUidPackageIndexLocked(int uid, @NonNull String packageName) {
-        // TODO Maybe we should switch to indexOf(Pair.create()) if the array size is too big.
-        final int size = mForcedAppStandbyUidPackages.size();
+        final int size = mRunAnyRestrictedPackages.size();
+        if (size > 8) {
+            return mRunAnyRestrictedPackages.indexOf(Pair.create(uid, packageName));
+        }
         for (int i = 0; i < size; i++) {
-            final Pair<Integer, String> pair = mForcedAppStandbyUidPackages.valueAt(i);
+            final Pair<Integer, String> pair = mRunAnyRestrictedPackages.valueAt(i);
 
             if ((pair.first == uid) && packageName.equals(pair.second)) {
                 return i;
@@ -196,13 +353,16 @@
     }
 
     /**
-     * @return whether a uid package-name pair is in mForcedAppStandbyUidPackages.
+     * @return whether a uid package-name pair is in mRunAnyRestrictedPackages.
      */
-    boolean isUidPackageRestrictedLocked(int uid, @NonNull String packageName) {
+    boolean isRunAnyRestrictedLocked(int uid, @NonNull String packageName) {
         return findForcedAppStandbyUidPackageIndexLocked(uid, packageName) >= 0;
     }
 
-    boolean updateRestrictedUidPackageLocked(int uid, @NonNull String packageName,
+    /**
+     * Add to / remove from {@link #mRunAnyRestrictedPackages}.
+     */
+    boolean updateForcedAppStandbyUidPackageLocked(int uid, @NonNull String packageName,
             boolean restricted) {
         final int index =  findForcedAppStandbyUidPackageIndexLocked(uid, packageName);
         final boolean wasRestricted = index >= 0;
@@ -210,13 +370,16 @@
             return false;
         }
         if (restricted) {
-            mForcedAppStandbyUidPackages.add(Pair.create(uid, packageName));
+            mRunAnyRestrictedPackages.add(Pair.create(uid, packageName));
         } else {
-            mForcedAppStandbyUidPackages.removeAt(index);
+            mRunAnyRestrictedPackages.removeAt(index);
         }
         return true;
     }
 
+    /**
+     * Puts a UID to {@link #mForegroundUids}.
+     */
     void uidToForeground(int uid) {
         synchronized (mLock) {
             if (!UserHandle.isApp(uid)) {
@@ -228,10 +391,13 @@
                 return;
             }
             mForegroundUids.put(uid, true);
-            notifyForUidPackage(uid, null);
+            mHandler.notifyUidForegroundStateChanged(uid);
         }
     }
 
+    /**
+     * Sets false for a UID {@link #mForegroundUids}, or remove it when {@code remove} is true.
+     */
     void uidToBackground(int uid, boolean remove) {
         synchronized (mLock) {
             if (!UserHandle.isApp(uid)) {
@@ -247,13 +413,11 @@
             } else {
                 mForegroundUids.put(uid, false);
             }
-            notifyForUidPackage(uid, null);
+            mHandler.notifyUidForegroundStateChanged(uid);
         }
     }
 
-    // Event handlers
-
-    final class UidObserver extends IUidObserver.Stub {
+    private final class UidObserver extends IUidObserver.Stub {
         @Override public void onUidStateChanged(int uid, int procState, long procStateSeq) {
         }
 
@@ -277,11 +441,28 @@
     private final class AppOpsWatcher extends IAppOpsCallback.Stub {
         @Override
         public void opChanged(int op, int uid, String packageName) throws RemoteException {
+            boolean restricted = false;
+            try {
+                restricted = mAppOpsService.checkOperation(TARGET_OP,
+                        uid, packageName) != AppOpsManager.MODE_ALLOWED;
+            } catch (RemoteException e) {
+                // Shouldn't happen
+            }
             synchronized (mLock) {
-                final boolean restricted = isRunAnyInBackgroundAppOpRestricted(uid, packageName);
+                if (updateForcedAppStandbyUidPackageLocked(uid, packageName, restricted)) {
+                    mHandler.notifyRunAnyAppOpsChanged(uid, packageName);
+                }
+            }
+        }
+    }
 
-                if (updateRestrictedUidPackageLocked(uid, packageName, restricted)) {
-                    notifyForUidPackage(uid, packageName);
+    private final class MyReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (Intent.ACTION_USER_REMOVED.equals(intent.getAction())) {
+                final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
+                if (userId > 0) {
+                    mHandler.doUserRemoved(userId);
                 }
             }
         }
@@ -293,31 +474,183 @@
         }
     }
 
-    void notifyForUidPackage(int uid, String packageName) {
-        mCallbackHandler.post(() -> {
-            for (Listener l : cloneListeners()) {
-                l.onRestrictionChanged(uid, packageName);
-            }
-        });
-    }
+    private class MyHandler extends Handler {
+        private static final int MSG_UID_STATE_CHANGED = 1;
+        private static final int MSG_RUN_ANY_CHANGED = 2;
+        private static final int MSG_ALL_UNWHITELISTED = 3;
+        private static final int MSG_ALL_WHITELIST_CHANGED = 4;
+        private static final int MSG_TEMP_WHITELIST_CHANGED = 5;
+        private static final int MSG_FORCE_ALL_CHANGED = 6;
 
-    void notifyGlobal() {
-        mCallbackHandler.post(() -> {
-            for (Listener l : cloneListeners()) {
-                l.onGlobalRestrictionChanged();
-            }
-        });
-    }
+        private static final int MSG_USER_REMOVED = 7;
 
-    void updateForceAllAppsStandby(boolean forceAllAppsStandby) {
-        synchronized (mLock) {
-            if (mForceAllAppsStandby == forceAllAppsStandby) {
-                return;
-            }
-            mForceAllAppsStandby = forceAllAppsStandby;
-            Slog.i(TAG, "Force all app standby: " + mForceAllAppsStandby);
-            notifyGlobal();
+        public MyHandler(Looper looper) {
+            super(looper);
         }
+
+        public void notifyUidForegroundStateChanged(int uid) {
+            obtainMessage(MSG_UID_STATE_CHANGED, uid, 0).sendToTarget();
+        }
+        public void notifyRunAnyAppOpsChanged(int uid, @NonNull String packageName) {
+            obtainMessage(MSG_RUN_ANY_CHANGED, uid, 0, packageName).sendToTarget();
+        }
+
+        public void notifyAllUnwhitelisted() {
+            obtainMessage(MSG_ALL_UNWHITELISTED).sendToTarget();
+        }
+
+        public void notifyAllWhitelistChanged() {
+            obtainMessage(MSG_ALL_WHITELIST_CHANGED).sendToTarget();
+        }
+
+        public void notifyTempWhitelistChanged() {
+            obtainMessage(MSG_TEMP_WHITELIST_CHANGED).sendToTarget();
+        }
+
+        public void notifyForceAllAppsStandbyChanged() {
+            obtainMessage(MSG_FORCE_ALL_CHANGED).sendToTarget();
+        }
+
+        public void doUserRemoved(int userId) {
+            obtainMessage(MSG_USER_REMOVED, userId, 0).sendToTarget();
+        }
+
+        @Override
+        public void dispatchMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_USER_REMOVED:
+                    handleUserRemoved(msg.arg1);
+                    return;
+            }
+
+            // Only notify the listeners when started.
+            synchronized (mLock) {
+                if (!mStarted) {
+                    return;
+                }
+            }
+            final ForceAppStandbyTracker sender = ForceAppStandbyTracker.this;
+
+            switch (msg.what) {
+                case MSG_UID_STATE_CHANGED:
+                    for (Listener l : cloneListeners()) {
+                        l.onUidForegroundStateChanged(sender, msg.arg1);
+                    }
+                    return;
+                case MSG_RUN_ANY_CHANGED:
+                    for (Listener l : cloneListeners()) {
+                        l.onRunAnyAppOpsChanged(sender, msg.arg1, (String) msg.obj);
+                    }
+                    return;
+                case MSG_ALL_UNWHITELISTED:
+                    for (Listener l : cloneListeners()) {
+                        l.onPowerSaveUnwhitelisted(sender);
+                    }
+                    return;
+                case MSG_ALL_WHITELIST_CHANGED:
+                    for (Listener l : cloneListeners()) {
+                        l.onPowerSaveWhitelistedChanged(sender);
+                    }
+                    return;
+                case MSG_TEMP_WHITELIST_CHANGED:
+                    for (Listener l : cloneListeners()) {
+                        l.onTempPowerSaveWhitelistChanged(sender);
+                    }
+                    return;
+                case MSG_FORCE_ALL_CHANGED:
+                    for (Listener l : cloneListeners()) {
+                        l.onForceAllAppsStandbyChanged(sender);
+                    }
+                    return;
+                case MSG_USER_REMOVED:
+                    handleUserRemoved(msg.arg1);
+                    return;
+            }
+        }
+    }
+
+    void handleUserRemoved(int removedUserId) {
+        synchronized (mLock) {
+            for (int i = mRunAnyRestrictedPackages.size() - 1; i >= 0; i--) {
+                final Pair<Integer, String> pair = mRunAnyRestrictedPackages.valueAt(i);
+                final int uid = pair.first;
+                final int userId = UserHandle.getUserId(uid);
+
+                if (userId == removedUserId) {
+                    mRunAnyRestrictedPackages.removeAt(i);
+                }
+            }
+            for (int i = mForegroundUids.size() - 1; i >= 0; i--) {
+                final int uid = mForegroundUids.keyAt(i);
+                final int userId = UserHandle.getUserId(uid);
+
+                if (userId == removedUserId) {
+                    mForegroundUids.removeAt(i);
+                }
+            }
+        }
+    }
+
+    /**
+     * Called by device idle controller to update the power save whitelists.
+     */
+    public void setPowerSaveWhitelistAppIds(
+            int[] powerSaveWhitelistAllAppIdArray, int[] tempWhitelistAppIdArray) {
+        synchronized (mLock) {
+            final int[] previousWhitelist = mPowerWhitelistedAllAppIds;
+            final int[] previousTempWhitelist = mTempWhitelistedAppIds;
+
+            mPowerWhitelistedAllAppIds = powerSaveWhitelistAllAppIdArray;
+            mTempWhitelistedAppIds = tempWhitelistAppIdArray;
+
+            if (isAnyAppIdUnwhitelisted(previousWhitelist, mPowerWhitelistedAllAppIds)) {
+                mHandler.notifyAllUnwhitelisted();
+            } else if (!Arrays.equals(previousWhitelist, mPowerWhitelistedAllAppIds)) {
+                mHandler.notifyAllWhitelistChanged();
+            }
+
+            if (!Arrays.equals(previousTempWhitelist, mTempWhitelistedAppIds)) {
+                mHandler.notifyTempWhitelistChanged();
+            }
+
+        }
+    }
+
+    /**
+     * @retunr true if a sorted app-id array {@code prevArray} has at least one element
+     * that's not in a sorted app-id array {@code newArray}.
+     */
+    @VisibleForTesting
+    static boolean isAnyAppIdUnwhitelisted(int[] prevArray, int[] newArray) {
+        int i1 = 0;
+        int i2 = 0;
+        boolean prevFinished;
+        boolean newFinished;
+
+        for (;;) {
+            prevFinished = i1 >= prevArray.length;
+            newFinished = i2 >= newArray.length;
+            if (prevFinished || newFinished) {
+                break;
+            }
+            int a1 = prevArray[i1];
+            int a2 = newArray[i2];
+
+            if (a1 == a2) {
+                i1++;
+                i2++;
+                continue;
+            }
+            if (a1 < a2) {
+                // prevArray has an element that's not in a2.
+                return true;
+            }
+            i2++;
+        }
+        if (prevFinished) {
+            return false;
+        }
+        return newFinished;
     }
 
     // Public interface.
@@ -332,21 +665,51 @@
     }
 
     /**
-     * Whether force-app-standby is effective for a UID package-name.
+     * @return whether alarms should be restricted for a UID package-name.
      */
-    public boolean isRestricted(int uid, @NonNull String packageName) {
+    public boolean areAlarmsRestricted(int uid, @NonNull String packageName) {
+        return isRestricted(uid, packageName, /*useTempWhitelistToo=*/ false);
+    }
+
+    /**
+     * @return whether jobs should be restricted for a UID package-name.
+     */
+    public boolean areJobsRestricted(int uid, @NonNull String packageName) {
+        return isRestricted(uid, packageName, /*useTempWhitelistToo=*/ true);
+    }
+
+    /**
+     * @return whether force-app-standby is effective for a UID package-name.
+     */
+    private boolean isRestricted(int uid, @NonNull String packageName,
+            boolean useTempWhitelistToo) {
         if (isInForeground(uid)) {
             return false;
         }
         synchronized (mLock) {
+            // Whitelisted?
+            final int appId = UserHandle.getAppId(uid);
+            if (ArrayUtils.contains(mPowerWhitelistedAllAppIds, appId)) {
+                return false;
+            }
+            if (useTempWhitelistToo &&
+                    ArrayUtils.contains(mTempWhitelistedAppIds, appId)) {
+                return false;
+            }
+
             if (mForceAllAppsStandby) {
                 return true;
             }
-            return isUidPackageRestrictedLocked(uid, packageName);
+
+            return isRunAnyRestrictedLocked(uid, packageName);
         }
     }
 
-    /** For dumpsys -- otherwise the callers don't need to know it. */
+    /**
+     * @return whether a UID is in the foreground or not.
+     *
+     * Note clients normally shouldn't need to access it. It's only for dumpsys.
+     */
     public boolean isInForeground(int uid) {
         if (!UserHandle.isApp(uid)) {
             return true;
@@ -356,31 +719,120 @@
         }
     }
 
-    /** For dumpsys -- otherwise the callers don't need to know it. */
-    public boolean isForceAllAppsStandbyEnabled() {
+    /**
+     * @return whether force all apps standby is enabled or not.
+     *
+     * Note clients normally shouldn't need to access it.
+     */
+    boolean isForceAllAppsStandbyEnabled() {
         synchronized (mLock) {
             return mForceAllAppsStandby;
         }
     }
 
-    /** For dumpsys -- otherwise the callers don't need to know it. */
+    /**
+     * @return whether a UID/package has {@code OP_RUN_ANY_IN_BACKGROUND} allowed or not.
+     *
+     * Note clients normally shouldn't need to access it. It's only for dumpsys.
+     */
     public boolean isRunAnyInBackgroundAppOpsAllowed(int uid, @NonNull String packageName) {
         synchronized (mLock) {
-            return !isUidPackageRestrictedLocked(uid, packageName);
+            return !isRunAnyRestrictedLocked(uid, packageName);
         }
     }
 
-    /** For dumpsys -- otherwise the callers don't need to know it. */
-    public SparseBooleanArray getForegroudUids() {
+    /**
+     * @return whether a UID is in the user / system defined power-save whitelist or not.
+     *
+     * Note clients normally shouldn't need to access it. It's only for dumpsys.
+     */
+    public boolean isUidPowerSaveWhitelisted(int uid) {
         synchronized (mLock) {
-            return mForegroundUids.clone();
+            return ArrayUtils.contains(mPowerWhitelistedAllAppIds, UserHandle.getAppId(uid));
         }
     }
 
-    /** For dumpsys -- otherwise the callers don't need to know it. */
-    public ArraySet<Pair<Integer, String>> getRestrictedUidPackages() {
+    /**
+     * @return whether a UID is in the temp power-save whitelist or not.
+     *
+     * Note clients normally shouldn't need to access it. It's only for dumpsys.
+     */
+    public boolean isUidTempPowerSaveWhitelisted(int uid) {
         synchronized (mLock) {
-            return new ArraySet(mForcedAppStandbyUidPackages);
+            return ArrayUtils.contains(mTempWhitelistedAppIds, UserHandle.getAppId(uid));
+        }
+    }
+
+    public void dump(PrintWriter pw, String indent) {
+        synchronized (mLock) {
+            pw.print(indent);
+            pw.print("Force all apps standby: ");
+            pw.println(isForceAllAppsStandbyEnabled());
+
+            pw.print(indent);
+            pw.print("Foreground uids: [");
+
+            String sep = "";
+            for (int i = 0; i < mForegroundUids.size(); i++) {
+                if (mForegroundUids.valueAt(i)) {
+                    pw.print(sep);
+                    pw.print(UserHandle.formatUid(mForegroundUids.keyAt(i)));
+                    sep = " ";
+                }
+            }
+            pw.println("]");
+
+            pw.print(indent);
+            pw.print("Whitelist appids: ");
+            pw.println(Arrays.toString(mPowerWhitelistedAllAppIds));
+
+            pw.print(indent);
+            pw.print("Temp whitelist appids: ");
+            pw.println(Arrays.toString(mTempWhitelistedAppIds));
+
+            pw.print(indent);
+            pw.println("Restricted packages:");
+            for (Pair<Integer, String> uidAndPackage : mRunAnyRestrictedPackages) {
+                pw.print(indent);
+                pw.print("  ");
+                pw.print(UserHandle.formatUid(uidAndPackage.first));
+                pw.print(" ");
+                pw.print(uidAndPackage.second);
+                pw.println();
+            }
+        }
+    }
+
+    public void dumpProto(ProtoOutputStream proto, long fieldId) {
+        synchronized (mLock) {
+            final long token = proto.start(fieldId);
+
+            proto.write(ForceAppStandbyTrackerProto.FORCE_ALL_APPS_STANDBY, mForceAllAppsStandby);
+
+            for (int i = 0; i < mForegroundUids.size(); i++) {
+                if (mForegroundUids.valueAt(i)) {
+                    proto.write(ForceAppStandbyTrackerProto.FOREGROUND_UIDS,
+                            mForegroundUids.keyAt(i));
+                }
+            }
+
+            for (int appId : mPowerWhitelistedAllAppIds) {
+                proto.write(ForceAppStandbyTrackerProto.POWER_SAVE_WHITELIST_APP_IDS, appId);
+            }
+
+            for (int appId : mTempWhitelistedAppIds) {
+                proto.write(ForceAppStandbyTrackerProto.TEMP_POWER_SAVE_WHITELIST_APP_IDS, appId);
+            }
+
+            for (Pair<Integer, String> uidAndPackage : mRunAnyRestrictedPackages) {
+                final long token2 = proto.start(
+                        ForceAppStandbyTrackerProto.RUN_ANY_IN_BACKGROUND_RESTRICTED_PACKAGES);
+                proto.write(RunAnyInBackgroundRestrictedPackages.UID, uidAndPackage.first);
+                proto.write(RunAnyInBackgroundRestrictedPackages.PACKAGE_NAME,
+                        uidAndPackage.second);
+                proto.end(token2);
+            }
+            proto.end(token);
         }
     }
 }
diff --git a/services/core/java/com/android/server/job/controllers/BackgroundJobsController.java b/services/core/java/com/android/server/job/controllers/BackgroundJobsController.java
index f67bd04..fc4015d 100644
--- a/services/core/java/com/android/server/job/controllers/BackgroundJobsController.java
+++ b/services/core/java/com/android/server/job/controllers/BackgroundJobsController.java
@@ -16,19 +16,12 @@
 
 package com.android.server.job.controllers;
 
-import android.content.BroadcastReceiver;
 import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
 import android.os.IDeviceIdleController;
-import android.os.PowerManager;
-import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.SystemClock;
 import android.os.UserHandle;
-import android.util.Pair;
 import android.util.Slog;
-import android.util.SparseBooleanArray;
 
 import com.android.internal.util.ArrayUtils;
 import com.android.server.ForceAppStandbyTracker;
@@ -37,7 +30,6 @@
 import com.android.server.job.JobStore;
 
 import java.io.PrintWriter;
-import java.util.function.Predicate;
 
 public final class BackgroundJobsController extends StateController {
 
@@ -51,9 +43,6 @@
     private final JobSchedulerService mJobSchedulerService;
     private final IDeviceIdleController mDeviceIdleController;
 
-    private int[] mPowerWhitelistedUserAppIds;
-    private int[] mTempWhitelistedAppIds;
-
     private final ForceAppStandbyTracker mForceAppStandbyTracker;
 
 
@@ -67,28 +56,6 @@
         }
     }
 
-    private BroadcastReceiver mDozeWhitelistReceiver = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            synchronized (mLock) {
-                try {
-                    switch (intent.getAction()) {
-                        case PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED:
-                            mPowerWhitelistedUserAppIds =
-                                    mDeviceIdleController.getAppIdUserWhitelist();
-                            break;
-                        case PowerManager.ACTION_POWER_SAVE_TEMP_WHITELIST_CHANGED:
-                            mTempWhitelistedAppIds = mDeviceIdleController.getAppIdTempWhitelist();
-                            break;
-                    }
-                } catch (RemoteException rexc) {
-                    Slog.e(LOG_TAG, "Device idle controller not reachable");
-                }
-                updateAllJobRestrictionsLocked();
-            }
-        }
-    };
-
     private BackgroundJobsController(JobSchedulerService service, Context context, Object lock) {
         super(service, context, lock);
         mJobSchedulerService = service;
@@ -97,19 +64,6 @@
 
         mForceAppStandbyTracker = ForceAppStandbyTracker.getInstance(context);
 
-        try {
-            mPowerWhitelistedUserAppIds = mDeviceIdleController.getAppIdUserWhitelist();
-            mTempWhitelistedAppIds = mDeviceIdleController.getAppIdTempWhitelist();
-        } catch (RemoteException rexc) {
-            // Shouldn't happen as they are in the same process.
-            Slog.e(LOG_TAG, "AppOps or DeviceIdle service not reachable", rexc);
-        }
-        IntentFilter powerWhitelistFilter = new IntentFilter();
-        powerWhitelistFilter.addAction(PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED);
-        powerWhitelistFilter.addAction(PowerManager.ACTION_POWER_SAVE_TEMP_WHITELIST_CHANGED);
-        context.registerReceiverAsUser(mDozeWhitelistReceiver, UserHandle.ALL, powerWhitelistFilter,
-                null, null);
-
         mForceAppStandbyTracker.addListener(mForceAppStandbyListener);
         mForceAppStandbyTracker.start();
     }
@@ -128,31 +82,7 @@
     public void dumpControllerStateLocked(final PrintWriter pw, final int filterUid) {
         pw.println("BackgroundJobsController");
 
-        pw.print("Force all apps standby: ");
-        pw.println(mForceAppStandbyTracker.isForceAllAppsStandbyEnabled());
-
-        pw.print("Foreground uids: [");
-        final SparseBooleanArray foregroundUids = mForceAppStandbyTracker.getForegroudUids();
-
-        String sep = "";
-        for (int i = 0; i < foregroundUids.size(); i++) {
-            if (foregroundUids.valueAt(i)) {
-                pw.print(sep);
-                pw.print(UserHandle.formatUid(foregroundUids.keyAt(i)));
-                sep = " ";
-            }
-        }
-        pw.println("]");
-
-        pw.println("Restricted packages:");
-        for (Pair<Integer, String> uidAndPackage
-                : mForceAppStandbyTracker.getRestrictedUidPackages()) {
-            pw.print("  ");
-            pw.print(UserHandle.formatUid(uidAndPackage.first));
-            pw.print(" ");
-            pw.print(uidAndPackage.second);
-            pw.println();
-        }
+        mForceAppStandbyTracker.dump(pw, "");
 
         pw.println("Job state:");
         mJobSchedulerService.getJobStore().forEachJob((jobStatus) -> {
@@ -165,15 +95,17 @@
             pw.print(" from ");
             UserHandle.formatUid(pw, uid);
             pw.print(mForceAppStandbyTracker.isInForeground(uid) ? " foreground" : " background");
-            if (isWhitelistedLocked(uid)) {
+            if (mForceAppStandbyTracker.isUidPowerSaveWhitelisted(uid) ||
+                    mForceAppStandbyTracker.isUidTempPowerSaveWhitelisted(uid)) {
                 pw.print(", whitelisted");
             }
             pw.print(": ");
             pw.print(jobStatus.getSourcePackageName());
 
-            pw.print(" [background restrictions ");
+            pw.print(" [RUN_ANY_IN_BACKGROUND ");
             pw.print(mForceAppStandbyTracker.isRunAnyInBackgroundAppOpsAllowed(
-                    jobStatus.getSourceUid(), jobStatus.getSourcePackageName()) ? "off]" : "on]");
+                    jobStatus.getSourceUid(), jobStatus.getSourcePackageName())
+                    ? "allowed]" : "disallowed]");
 
             if ((jobStatus.satisfiedConstraints
                     & JobStatus.CONSTRAINT_BACKGROUND_NOT_RESTRICTED) != 0) {
@@ -218,19 +150,12 @@
         }
     }
 
-    private boolean isWhitelistedLocked(int uid) {
-        final int appId = UserHandle.getAppId(uid);
-        return ArrayUtils.contains(mTempWhitelistedAppIds, appId)
-                || ArrayUtils.contains(mPowerWhitelistedUserAppIds, appId);
-    }
-
     boolean updateSingleJobRestrictionLocked(JobStatus jobStatus) {
 
         final int uid = jobStatus.getSourceUid();
         final String packageName = jobStatus.getSourcePackageName();
 
-        final boolean canRun = isWhitelistedLocked(uid)
-                || !mForceAppStandbyTracker.isRestricted(uid, packageName);
+        final boolean canRun = !mForceAppStandbyTracker.areJobsRestricted(uid, packageName);
 
         return jobStatus.setBackgroundNotRestrictedConstraintSatisfied(canRun);
     }
@@ -261,13 +186,18 @@
 
     private final Listener mForceAppStandbyListener = new Listener() {
         @Override
-        public void onRestrictionChanged(int uid, String packageName) {
+        public void updateAllJobs() {
+            updateAllJobRestrictionsLocked();
+        }
+
+        @Override
+        public void updateJobsForUid(int uid) {
             updateJobRestrictionsForUidLocked(uid);
         }
 
         @Override
-        public void onGlobalRestrictionChanged() {
-            updateAllJobRestrictionsLocked();
+        public void updateJobsForUidPackage(int uid, String packageName) {
+            updateJobRestrictionsForUidLocked(uid);
         }
     };
 }
diff --git a/services/tests/servicestests/src/com/android/server/AlarmManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/AlarmManagerServiceTest.java
new file mode 100644
index 0000000..918807d
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/AlarmManagerServiceTest.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server;
+
+import static android.app.AlarmManager.RTC;
+import static android.app.AlarmManager.RTC_WAKEUP;
+
+import static org.junit.Assert.assertEquals;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.SparseArray;
+
+import com.android.internal.util.ObjectUtils;
+import com.android.server.AlarmManagerService.Alarm;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class AlarmManagerServiceTest {
+    private SparseArray<ArrayList<Alarm>> addPendingAlarm(
+            SparseArray<ArrayList<Alarm>> all, int uid, String name, boolean removeIt) {
+        ArrayList<Alarm> uidAlarms = all.get(uid);
+        if (uidAlarms == null) {
+            all.put(uid, uidAlarms = new ArrayList<>());
+        }
+        // Details don't matter.
+        uidAlarms.add(new Alarm(
+                removeIt ? RTC : RTC_WAKEUP,
+                0, 0, 0, 0, 0, null, null, null, null, 0, null, uid, name));
+        return all;
+    }
+
+    private static String toString(SparseArray<ArrayList<Alarm>> pendingAlarms) {
+        final StringBuilder sb = new StringBuilder();
+
+        String sep = "";
+        for (int i = 0; i < pendingAlarms.size(); i++) {
+            sb.append(sep);
+            sep = ", ";
+            sb.append("[");
+            sb.append(pendingAlarms.keyAt(i));
+            sb.append(": ");
+            sb.append(toString(pendingAlarms.valueAt(i)));
+            sb.append("]");
+        }
+        return sb.toString();
+    }
+
+    private static String toString(ArrayList<Alarm> alarms) {
+        final StringBuilder sb = new StringBuilder();
+
+        alarms.sort((a, b) -> ObjectUtils.compare(a.packageName, b.packageName));
+
+        String sep = "";
+        for (Alarm a : alarms) {
+            sb.append(sep);
+            sep = ", ";
+            sb.append(a.packageName);
+        }
+        return sb.toString();
+    }
+
+    private void runCheckAllPendingAlarms(
+            SparseArray<ArrayList<Alarm>> pending, ArrayList<Alarm> alarmsToDeliver) {
+        // RTC_WAKEUP alarms are restricted.
+        AlarmManagerService.findAllUnrestrictedPendingBackgroundAlarmsLockedInner(pending,
+                alarmsToDeliver, alarm -> alarm.type == RTC_WAKEUP);
+    }
+
+    @Test
+    public void findAllUnrestrictedPendingBackgroundAlarmsLockedInner_empty() {
+        SparseArray<ArrayList<Alarm>> pending = new SparseArray<>();
+
+        final ArrayList<Alarm> alarmsToDeliver = new ArrayList<>();
+
+        runCheckAllPendingAlarms(pending, alarmsToDeliver);
+
+        assertEquals("", toString(pending));
+        assertEquals("", toString(alarmsToDeliver));
+    }
+
+    @Test
+    public void findAllUnrestrictedPendingBackgroundAlarmsLockedInner_single_remove() {
+        SparseArray<ArrayList<Alarm>> pending = new SparseArray<>();
+
+        addPendingAlarm(pending, 100001, "a1", false);
+
+        final ArrayList<Alarm> alarmsToDeliver = new ArrayList<>();
+
+        runCheckAllPendingAlarms(pending, alarmsToDeliver);
+
+        assertEquals("[100001: a1]", toString(pending));
+        assertEquals("", toString(alarmsToDeliver));
+    }
+
+    @Test
+    public void findAllUnrestrictedPendingBackgroundAlarmsLockedInner_single_nonremove() {
+        SparseArray<ArrayList<Alarm>> pending = new SparseArray<>();
+
+        addPendingAlarm(pending, 100001, "a1", true);
+
+        final ArrayList<Alarm> alarmsToDeliver = new ArrayList<>();
+        runCheckAllPendingAlarms(pending, alarmsToDeliver);
+
+
+        assertEquals("", toString(pending));
+        assertEquals("a1", toString(alarmsToDeliver));
+    }
+
+    @Test
+    public void findAllUnrestrictedPendingBackgroundAlarmsLockedInner_complex() {
+        SparseArray<ArrayList<Alarm>> pending = new SparseArray<>();
+
+        addPendingAlarm(pending, 100001, "a11", false);
+        addPendingAlarm(pending, 100001, "a12", true);
+        addPendingAlarm(pending, 100001, "a13", false);
+        addPendingAlarm(pending, 100001, "a14", true);
+
+        addPendingAlarm(pending, 100002, "a21", false);
+
+        addPendingAlarm(pending, 100003, "a31", true);
+
+        addPendingAlarm(pending, 100004, "a41", false);
+        addPendingAlarm(pending, 100004, "a42", false);
+
+        addPendingAlarm(pending, 100005, "a51", true);
+        addPendingAlarm(pending, 100005, "a52", true);
+
+        addPendingAlarm(pending, 100006, "a61", true);
+        addPendingAlarm(pending, 100006, "a62", false);
+        addPendingAlarm(pending, 100006, "a63", true);
+        addPendingAlarm(pending, 100006, "a64", false);
+
+        final ArrayList<Alarm> alarmsToDeliver = new ArrayList<>();
+        runCheckAllPendingAlarms(pending, alarmsToDeliver);
+
+
+        assertEquals("[100001: a11, a13], [100002: a21], [100004: a41, a42], [100006: a62, a64]",
+                toString(pending));
+        assertEquals("a12, a14, a31, a51, a52, a61, a63", toString(alarmsToDeliver));
+    }
+
+    @Test
+    public void findAllUnrestrictedPendingBackgroundAlarmsLockedInner_complex_allRemove() {
+        SparseArray<ArrayList<Alarm>> pending = new SparseArray<>();
+
+        addPendingAlarm(pending, 100001, "a11", true);
+        addPendingAlarm(pending, 100001, "a12", true);
+        addPendingAlarm(pending, 100001, "a13", true);
+        addPendingAlarm(pending, 100001, "a14", true);
+
+        addPendingAlarm(pending, 100002, "a21", true);
+
+        addPendingAlarm(pending, 100003, "a31", true);
+
+        addPendingAlarm(pending, 100004, "a41", true);
+        addPendingAlarm(pending, 100004, "a42", true);
+
+        addPendingAlarm(pending, 100005, "a51", true);
+        addPendingAlarm(pending, 100005, "a52", true);
+
+        addPendingAlarm(pending, 100006, "a61", true);
+        addPendingAlarm(pending, 100006, "a62", true);
+        addPendingAlarm(pending, 100006, "a63", true);
+        addPendingAlarm(pending, 100006, "a64", true);
+
+        final ArrayList<Alarm> alarmsToDeliver = new ArrayList<>();
+        runCheckAllPendingAlarms(pending, alarmsToDeliver);
+
+
+        assertEquals("", toString(pending));
+        assertEquals("a11, a12, a13, a14, a21, a31, a41, a42, a51, a52, a61, a62, a63, a64",
+                toString(alarmsToDeliver));
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/ForceAppStandbyTrackerTest.java b/services/tests/servicestests/src/com/android/server/ForceAppStandbyTrackerTest.java
new file mode 100644
index 0000000..66d0da1
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/ForceAppStandbyTrackerTest.java
@@ -0,0 +1,900 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server;
+
+import static com.android.server.ForceAppStandbyTracker.TARGET_OP;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityManager;
+import android.app.AppOpsManager;
+import android.app.AppOpsManager.OpEntry;
+import android.app.AppOpsManager.PackageOps;
+import android.app.IActivityManager;
+import android.app.IUidObserver;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.PowerManager.ServiceType;
+import android.os.PowerManagerInternal;
+import android.os.PowerSaveState;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.ArraySet;
+import android.util.Pair;
+
+import com.android.internal.app.IAppOpsCallback;
+import com.android.internal.app.IAppOpsService;
+import com.android.server.ForceAppStandbyTracker.Listener;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ForceAppStandbyTrackerTest {
+
+    private class ForceAppStandbyTrackerTestable extends ForceAppStandbyTracker {
+        ForceAppStandbyTrackerTestable() {
+            super(mMockContext, Looper.getMainLooper());
+        }
+
+        @Override
+        AppOpsManager injectAppOpsManager() {
+            return mMockAppOpsManager;
+        }
+
+        @Override
+        IAppOpsService injectIAppOpsService() {
+            return mMockIAppOpsService;
+        }
+
+        @Override
+        IActivityManager injectIActivityManager() {
+            return mMockIActivityManager;
+        }
+
+        @Override
+        PowerManagerInternal injectPowerManagerInternal() {
+            return mMockPowerManagerInternal;
+        }
+    }
+
+    private static final int UID_1 = Process.FIRST_APPLICATION_UID + 1;
+    private static final int UID_2 = Process.FIRST_APPLICATION_UID + 2;
+    private static final int UID_3 = Process.FIRST_APPLICATION_UID + 3;
+    private static final int UID_10_1 = UserHandle.getUid(10, UID_1);
+    private static final int UID_10_2 = UserHandle.getUid(10, UID_2);
+    private static final int UID_10_3 = UserHandle.getUid(10, UID_3);
+    private static final String PACKAGE_1 = "package1";
+    private static final String PACKAGE_2 = "package2";
+    private static final String PACKAGE_3 = "package3";
+    private static final String PACKAGE_SYSTEM = "android";
+
+    private Handler mMainHandler;
+
+    @Mock
+    private Context mMockContext;
+
+    @Mock
+    private IActivityManager mMockIActivityManager;
+
+    @Mock
+    private AppOpsManager mMockAppOpsManager;
+
+    @Mock
+    private IAppOpsService mMockIAppOpsService;
+
+    @Mock
+    private PowerManagerInternal mMockPowerManagerInternal;
+
+    private IUidObserver mIUidObserver;
+    private IAppOpsCallback.Stub mAppOpsCallback;
+    private Consumer<PowerSaveState> mPowerSaveObserver;
+    private BroadcastReceiver mReceiver;
+
+    private boolean mPowerSaveMode;
+
+    private final ArraySet<Pair<Integer, String>> mRestrictedPackages = new ArraySet();
+
+    @Before
+    public void setUp() {
+        mMainHandler = new Handler(Looper.getMainLooper());
+    }
+
+    private void waitUntilMainHandlerDrain() throws Exception {
+        final CountDownLatch l = new CountDownLatch(1);
+        mMainHandler.post(() -> {
+            l.countDown();
+        });
+        assertTrue(l.await(5, TimeUnit.SECONDS));
+    }
+
+    private PowerSaveState getPowerSaveState() {
+        return new PowerSaveState.Builder().setBatterySaverEnabled(mPowerSaveMode).build();
+    }
+
+    private ForceAppStandbyTrackerTestable newInstance() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        when(mMockIAppOpsService.checkOperation(eq(TARGET_OP), anyInt(), anyString()))
+                .thenAnswer(inv -> {
+                    return mRestrictedPackages.indexOf(
+                            Pair.create(inv.getArgument(1), inv.getArgument(2))) >= 0 ?
+                            AppOpsManager.MODE_IGNORED : AppOpsManager.MODE_ALLOWED;
+                });
+
+        final ForceAppStandbyTrackerTestable instance = new ForceAppStandbyTrackerTestable();
+
+        return instance;
+    }
+
+    private void callStart(ForceAppStandbyTrackerTestable instance) throws RemoteException {
+
+        // Set up functions that start() calls.
+        when(mMockPowerManagerInternal.getLowPowerState(eq(ServiceType.FORCE_ALL_APPS_STANDBY)))
+                .thenAnswer(inv -> getPowerSaveState());
+        when(mMockAppOpsManager.getPackagesForOps(
+                any(int[].class)
+                )).thenAnswer(inv -> new ArrayList<AppOpsManager.PackageOps>());
+
+        // Call start.
+        instance.start();
+
+        // Capture the listeners.
+        ArgumentCaptor<IUidObserver> uidObserverArgumentCaptor =
+                ArgumentCaptor.forClass(IUidObserver.class);
+        ArgumentCaptor<IAppOpsCallback.Stub> appOpsCallbackCaptor =
+                ArgumentCaptor.forClass(IAppOpsCallback.Stub.class);
+        ArgumentCaptor<Consumer<PowerSaveState>> powerSaveObserverCaptor =
+                ArgumentCaptor.forClass(Consumer.class);
+        ArgumentCaptor<BroadcastReceiver> receiverCaptor =
+                ArgumentCaptor.forClass(BroadcastReceiver.class);
+
+        verify(mMockIActivityManager).registerUidObserver(
+                uidObserverArgumentCaptor.capture(),
+                eq(ActivityManager.UID_OBSERVER_GONE | ActivityManager.UID_OBSERVER_IDLE
+                        | ActivityManager.UID_OBSERVER_ACTIVE),
+                eq(ActivityManager.PROCESS_STATE_UNKNOWN),
+                isNull());
+        verify(mMockIAppOpsService).startWatchingMode(
+                eq(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND),
+                isNull(),
+                appOpsCallbackCaptor.capture());
+        verify(mMockPowerManagerInternal).registerLowPowerModeObserver(
+                eq(ServiceType.FORCE_ALL_APPS_STANDBY),
+                powerSaveObserverCaptor.capture());
+
+        verify(mMockContext).registerReceiver(
+                receiverCaptor.capture(), any(IntentFilter.class));
+
+        mIUidObserver = uidObserverArgumentCaptor.getValue();
+        mAppOpsCallback = appOpsCallbackCaptor.getValue();
+        mPowerSaveObserver = powerSaveObserverCaptor.getValue();
+        mReceiver = receiverCaptor.getValue();
+
+        assertNotNull(mIUidObserver);
+        assertNotNull(mAppOpsCallback);
+        assertNotNull(mPowerSaveObserver);
+        assertNotNull(mReceiver);
+    }
+
+    private void setAppOps(int uid, String packageName, boolean restrict) throws RemoteException {
+        final Pair p = Pair.create(uid, packageName);
+        if (restrict) {
+            mRestrictedPackages.add(p);
+        } else {
+            mRestrictedPackages.remove(p);
+        }
+        if (mAppOpsCallback != null) {
+            mAppOpsCallback.opChanged(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, uid, packageName);
+        }
+    }
+
+    private static final int NONE = 0;
+    private static final int ALARMS_ONLY = 1 << 0;
+    private static final int JOBS_ONLY = 1 << 1;
+    private static final int JOBS_AND_ALARMS = ALARMS_ONLY | JOBS_ONLY;
+
+    private void areRestricted(ForceAppStandbyTrackerTestable instance, int uid, String packageName,
+            int restrictionTypes) {
+        assertEquals(((restrictionTypes & JOBS_ONLY) != 0),
+                instance.areJobsRestricted(uid, packageName));
+        assertEquals(((restrictionTypes & ALARMS_ONLY) != 0),
+                instance.areAlarmsRestricted(uid, packageName));
+    }
+
+    @Test
+    public void testAll() throws Exception {
+        final ForceAppStandbyTrackerTestable instance = newInstance();
+        callStart(instance);
+
+        assertFalse(instance.isForceAllAppsStandbyEnabled());
+        areRestricted(instance, UID_1, PACKAGE_1, NONE);
+        areRestricted(instance, UID_2, PACKAGE_2, NONE);
+        areRestricted(instance, Process.SYSTEM_UID, PACKAGE_SYSTEM, NONE);
+
+        mPowerSaveMode = true;
+        mPowerSaveObserver.accept(getPowerSaveState());
+
+        assertTrue(instance.isForceAllAppsStandbyEnabled());
+
+        areRestricted(instance, UID_1, PACKAGE_1, JOBS_AND_ALARMS);
+        areRestricted(instance, UID_2, PACKAGE_2, JOBS_AND_ALARMS);
+        areRestricted(instance, Process.SYSTEM_UID, PACKAGE_SYSTEM, NONE);
+
+        // Toggle the foreground state.
+        mPowerSaveMode = true;
+        mPowerSaveObserver.accept(getPowerSaveState());
+
+        assertFalse(instance.isInForeground(UID_1));
+        assertFalse(instance.isInForeground(UID_2));
+        assertTrue(instance.isInForeground(Process.SYSTEM_UID));
+
+        mIUidObserver.onUidActive(UID_1);
+        areRestricted(instance, UID_1, PACKAGE_1, NONE);
+        areRestricted(instance, UID_2, PACKAGE_2, JOBS_AND_ALARMS);
+        areRestricted(instance, Process.SYSTEM_UID, PACKAGE_SYSTEM, NONE);
+        assertTrue(instance.isInForeground(UID_1));
+        assertFalse(instance.isInForeground(UID_2));
+
+        mIUidObserver.onUidGone(UID_1, /*disable=*/ false);
+        areRestricted(instance, UID_1, PACKAGE_1, JOBS_AND_ALARMS);
+        areRestricted(instance, UID_2, PACKAGE_2, JOBS_AND_ALARMS);
+        areRestricted(instance, Process.SYSTEM_UID, PACKAGE_SYSTEM, NONE);
+        assertFalse(instance.isInForeground(UID_1));
+        assertFalse(instance.isInForeground(UID_2));
+
+        mIUidObserver.onUidActive(UID_1);
+        areRestricted(instance, UID_1, PACKAGE_1, NONE);
+        areRestricted(instance, UID_2, PACKAGE_2, JOBS_AND_ALARMS);
+        areRestricted(instance, Process.SYSTEM_UID, PACKAGE_SYSTEM, NONE);
+
+        mIUidObserver.onUidIdle(UID_1, /*disable=*/ false);
+        areRestricted(instance, UID_1, PACKAGE_1, JOBS_AND_ALARMS);
+        areRestricted(instance, UID_2, PACKAGE_2, JOBS_AND_ALARMS);
+        areRestricted(instance, Process.SYSTEM_UID, PACKAGE_SYSTEM, NONE);
+        assertFalse(instance.isInForeground(UID_1));
+        assertFalse(instance.isInForeground(UID_2));
+
+        // Toggle the app ops.
+        mPowerSaveMode = false;
+        mPowerSaveObserver.accept(getPowerSaveState());
+
+        assertTrue(instance.isRunAnyInBackgroundAppOpsAllowed(UID_1, PACKAGE_1));
+        assertTrue(instance.isRunAnyInBackgroundAppOpsAllowed(UID_10_1, PACKAGE_1));
+        assertTrue(instance.isRunAnyInBackgroundAppOpsAllowed(UID_2, PACKAGE_2));
+        assertTrue(instance.isRunAnyInBackgroundAppOpsAllowed(UID_10_2, PACKAGE_2));
+
+        areRestricted(instance, UID_1, PACKAGE_1, NONE);
+        areRestricted(instance, UID_10_1, PACKAGE_1, NONE);
+        areRestricted(instance, UID_2, PACKAGE_2, NONE);
+        areRestricted(instance, UID_10_2, PACKAGE_2, NONE);
+        areRestricted(instance, Process.SYSTEM_UID, PACKAGE_SYSTEM, NONE);
+
+        setAppOps(UID_1, PACKAGE_1, true);
+        setAppOps(UID_10_2, PACKAGE_2, true);
+        assertFalse(instance.isRunAnyInBackgroundAppOpsAllowed(UID_1, PACKAGE_1));
+        assertTrue(instance.isRunAnyInBackgroundAppOpsAllowed(UID_10_1, PACKAGE_1));
+        assertTrue(instance.isRunAnyInBackgroundAppOpsAllowed(UID_2, PACKAGE_2));
+        assertFalse(instance.isRunAnyInBackgroundAppOpsAllowed(UID_10_2, PACKAGE_2));
+
+        areRestricted(instance, UID_1, PACKAGE_1, JOBS_AND_ALARMS);
+        areRestricted(instance, UID_10_1, PACKAGE_1, NONE);
+        areRestricted(instance, UID_2, PACKAGE_2, NONE);
+        areRestricted(instance, UID_10_2, PACKAGE_2, JOBS_AND_ALARMS);
+        areRestricted(instance, Process.SYSTEM_UID, PACKAGE_SYSTEM, NONE);
+
+        // Toggle power saver, should still be the same.
+        mPowerSaveMode = true;
+        mPowerSaveObserver.accept(getPowerSaveState());
+
+        mPowerSaveMode = false;
+        mPowerSaveObserver.accept(getPowerSaveState());
+
+        areRestricted(instance, UID_1, PACKAGE_1, JOBS_AND_ALARMS);
+        areRestricted(instance, UID_10_1, PACKAGE_1, NONE);
+        areRestricted(instance, UID_2, PACKAGE_2, NONE);
+        areRestricted(instance, UID_10_2, PACKAGE_2, JOBS_AND_ALARMS);
+        areRestricted(instance, Process.SYSTEM_UID, PACKAGE_SYSTEM, NONE);
+
+        // Clear the app ops and update the whitelist.
+        setAppOps(UID_1, PACKAGE_1, false);
+        setAppOps(UID_10_2, PACKAGE_2, false);
+
+        mPowerSaveMode = true;
+        mPowerSaveObserver.accept(getPowerSaveState());
+
+        areRestricted(instance, UID_1, PACKAGE_1, JOBS_AND_ALARMS);
+        areRestricted(instance, UID_10_1, PACKAGE_1, JOBS_AND_ALARMS);
+        areRestricted(instance, UID_2, PACKAGE_2, JOBS_AND_ALARMS);
+        areRestricted(instance, UID_10_2, PACKAGE_2, JOBS_AND_ALARMS);
+        areRestricted(instance, UID_3, PACKAGE_3, JOBS_AND_ALARMS);
+        areRestricted(instance, UID_10_3, PACKAGE_3, JOBS_AND_ALARMS);
+        areRestricted(instance, Process.SYSTEM_UID, PACKAGE_SYSTEM, NONE);
+
+        instance.setPowerSaveWhitelistAppIds(new int[] {UID_1}, new int[] {UID_2});
+
+        areRestricted(instance, UID_1, PACKAGE_1, NONE);
+        areRestricted(instance, UID_10_1, PACKAGE_1, NONE);
+        areRestricted(instance, UID_2, PACKAGE_2, ALARMS_ONLY);
+        areRestricted(instance, UID_10_2, PACKAGE_2, ALARMS_ONLY);
+        areRestricted(instance, UID_3, PACKAGE_3, JOBS_AND_ALARMS);
+        areRestricted(instance, UID_10_3, PACKAGE_3, JOBS_AND_ALARMS);
+        areRestricted(instance, Process.SYSTEM_UID, PACKAGE_SYSTEM, NONE);
+
+        // Again, make sure toggling the global state doesn't change it.
+        mPowerSaveMode = false;
+        mPowerSaveObserver.accept(getPowerSaveState());
+
+        mPowerSaveMode = true;
+        mPowerSaveObserver.accept(getPowerSaveState());
+
+        areRestricted(instance, UID_1, PACKAGE_1, NONE);
+        areRestricted(instance, UID_10_1, PACKAGE_1, NONE);
+        areRestricted(instance, UID_2, PACKAGE_2, ALARMS_ONLY);
+        areRestricted(instance, UID_10_2, PACKAGE_2, ALARMS_ONLY);
+        areRestricted(instance, UID_3, PACKAGE_3, JOBS_AND_ALARMS);
+        areRestricted(instance, UID_10_3, PACKAGE_3, JOBS_AND_ALARMS);
+        areRestricted(instance, Process.SYSTEM_UID, PACKAGE_SYSTEM, NONE);
+
+        assertTrue(instance.isUidPowerSaveWhitelisted(UID_1));
+        assertTrue(instance.isUidPowerSaveWhitelisted(UID_10_1));
+        assertFalse(instance.isUidPowerSaveWhitelisted(UID_2));
+        assertFalse(instance.isUidPowerSaveWhitelisted(UID_10_2));
+
+        assertFalse(instance.isUidTempPowerSaveWhitelisted(UID_1));
+        assertFalse(instance.isUidTempPowerSaveWhitelisted(UID_10_1));
+        assertTrue(instance.isUidTempPowerSaveWhitelisted(UID_2));
+        assertTrue(instance.isUidTempPowerSaveWhitelisted(UID_10_2));
+    }
+
+    public void loadPersistedAppOps() throws Exception {
+        final ForceAppStandbyTrackerTestable instance = newInstance();
+
+        final List<PackageOps> ops = new ArrayList<>();
+
+        //--------------------------------------------------
+        List<OpEntry> entries = new ArrayList<>();
+        entries.add(new AppOpsManager.OpEntry(
+                AppOpsManager.OP_ACCESS_NOTIFICATIONS,
+                AppOpsManager.MODE_IGNORED, 0, 0, 0, 0, null));
+        entries.add(new AppOpsManager.OpEntry(
+                ForceAppStandbyTracker.TARGET_OP,
+                AppOpsManager.MODE_IGNORED, 0, 0, 0, 0, null));
+
+        ops.add(new PackageOps(PACKAGE_1, UID_1, entries));
+
+        //--------------------------------------------------
+        entries = new ArrayList<>();
+        entries.add(new AppOpsManager.OpEntry(
+                ForceAppStandbyTracker.TARGET_OP,
+                AppOpsManager.MODE_IGNORED, 0, 0, 0, 0, null));
+
+        ops.add(new PackageOps(PACKAGE_2, UID_2, entries));
+
+        //--------------------------------------------------
+        entries = new ArrayList<>();
+        entries.add(new AppOpsManager.OpEntry(
+                ForceAppStandbyTracker.TARGET_OP,
+                AppOpsManager.MODE_ALLOWED, 0, 0, 0, 0, null));
+
+        ops.add(new PackageOps(PACKAGE_1, UID_10_1, entries));
+
+        //--------------------------------------------------
+        entries = new ArrayList<>();
+        entries.add(new AppOpsManager.OpEntry(
+                ForceAppStandbyTracker.TARGET_OP,
+                AppOpsManager.MODE_IGNORED, 0, 0, 0, 0, null));
+        entries.add(new AppOpsManager.OpEntry(
+                AppOpsManager.OP_ACCESS_NOTIFICATIONS,
+                AppOpsManager.MODE_IGNORED, 0, 0, 0, 0, null));
+
+        ops.add(new PackageOps(PACKAGE_3, UID_10_3, entries));
+
+        callStart(instance);
+
+        assertFalse(instance.isRunAnyInBackgroundAppOpsAllowed(UID_1, PACKAGE_1));
+        assertFalse(instance.isRunAnyInBackgroundAppOpsAllowed(UID_2, PACKAGE_2));
+        assertTrue(instance.isRunAnyInBackgroundAppOpsAllowed(UID_3, PACKAGE_3));
+
+        assertTrue(instance.isRunAnyInBackgroundAppOpsAllowed(UID_10_1, PACKAGE_1));
+        assertTrue(instance.isRunAnyInBackgroundAppOpsAllowed(UID_10_2, PACKAGE_2));
+        assertFalse(instance.isRunAnyInBackgroundAppOpsAllowed(UID_10_3, PACKAGE_3));
+    }
+
+    private void assertNoCallbacks(Listener l) throws Exception {
+        waitUntilMainHandlerDrain();
+        verify(l, times(0)).updateAllJobs();
+        verify(l, times(0)).updateJobsForUid(anyInt());
+        verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString());
+
+        verify(l, times(0)).unblockAllUnrestrictedAlarms();
+        verify(l, times(0)).unblockAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
+        reset(l);
+    }
+
+    @Test
+    public void testPowerSaveListener() throws Exception {
+        final ForceAppStandbyTrackerTestable instance = newInstance();
+        callStart(instance);
+
+        ForceAppStandbyTracker.Listener l = mock(ForceAppStandbyTracker.Listener.class);
+        instance.addListener(l);
+
+        // Power save on.
+        mPowerSaveMode = true;
+        mPowerSaveObserver.accept(getPowerSaveState());
+
+        waitUntilMainHandlerDrain();
+        verify(l, times(1)).updateAllJobs();
+        verify(l, times(0)).updateJobsForUid(anyInt());
+        verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString());
+
+        verify(l, times(0)).unblockAllUnrestrictedAlarms();
+        verify(l, times(0)).unblockAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
+        reset(l);
+
+        // Power save off.
+        mPowerSaveMode = false;
+        mPowerSaveObserver.accept(getPowerSaveState());
+
+        waitUntilMainHandlerDrain();
+        verify(l, times(1)).updateAllJobs();
+        verify(l, times(0)).updateJobsForUid(anyInt());
+        verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString());
+
+        verify(l, times(1)).unblockAllUnrestrictedAlarms();
+        verify(l, times(0)).unblockAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
+        reset(l);
+
+        // Power save on.
+        mPowerSaveMode = true;
+        mPowerSaveObserver.accept(getPowerSaveState());
+
+        assertNoCallbacks(l);
+    }
+
+    @Test
+    public void testAllListeners() throws Exception {
+        final ForceAppStandbyTrackerTestable instance = newInstance();
+        callStart(instance);
+
+        ForceAppStandbyTracker.Listener l = mock(ForceAppStandbyTracker.Listener.class);
+        instance.addListener(l);
+
+        // -------------------------------------------------------------------------
+        // Test with apppops.
+
+        setAppOps(UID_10_2, PACKAGE_2, true);
+
+        waitUntilMainHandlerDrain();
+        verify(l, times(0)).updateAllJobs();
+        verify(l, times(0)).updateJobsForUid(anyInt());
+        verify(l, times(1)).updateJobsForUidPackage(eq(UID_10_2), eq(PACKAGE_2));
+
+        verify(l, times(0)).unblockAllUnrestrictedAlarms();
+        verify(l, times(0)).unblockAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
+        reset(l);
+
+        setAppOps(UID_10_2, PACKAGE_2, false);
+
+        waitUntilMainHandlerDrain();
+        verify(l, times(0)).updateAllJobs();
+        verify(l, times(0)).updateJobsForUid(anyInt());
+        verify(l, times(1)).updateJobsForUidPackage(eq(UID_10_2), eq(PACKAGE_2));
+
+        verify(l, times(0)).unblockAllUnrestrictedAlarms();
+        verify(l, times(0)).unblockAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAlarmsForUidPackage(eq(UID_10_2), eq(PACKAGE_2));
+        reset(l);
+
+        setAppOps(UID_10_2, PACKAGE_2, false);
+
+        verify(l, times(0)).updateAllJobs();
+        verify(l, times(0)).updateJobsForUid(anyInt());
+        verify(l, times(1)).updateJobsForUidPackage(eq(UID_10_2), eq(PACKAGE_2));
+
+        verify(l, times(0)).unblockAllUnrestrictedAlarms();
+        verify(l, times(0)).unblockAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
+
+        // Unrestrict while battery saver is on. Shouldn't fire.
+        mPowerSaveMode = true;
+        mPowerSaveObserver.accept(getPowerSaveState());
+
+        // Note toggling appops while BS is on will suppress unblockAlarmsForUidPackage().
+        setAppOps(UID_10_2, PACKAGE_2, true);
+
+        waitUntilMainHandlerDrain();
+        verify(l, times(1)).updateAllJobs();
+        verify(l, times(0)).updateJobsForUid(anyInt());
+        verify(l, times(1)).updateJobsForUidPackage(eq(UID_10_2), eq(PACKAGE_2));
+
+        verify(l, times(0)).unblockAllUnrestrictedAlarms();
+        verify(l, times(0)).unblockAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
+        reset(l);
+
+        // Battery saver off.
+        mPowerSaveMode = false;
+        mPowerSaveObserver.accept(getPowerSaveState());
+
+        waitUntilMainHandlerDrain();
+        verify(l, times(1)).updateAllJobs();
+        verify(l, times(0)).updateJobsForUid(anyInt());
+        verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString());
+
+        verify(l, times(1)).unblockAllUnrestrictedAlarms();
+        verify(l, times(0)).unblockAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
+        reset(l);
+
+        // -------------------------------------------------------------------------
+        // Tests with system/user/temp whitelist.
+
+        instance.setPowerSaveWhitelistAppIds(new int[] {UID_1, UID_2}, new int[] {});
+
+        waitUntilMainHandlerDrain();
+        verify(l, times(1)).updateAllJobs();
+        verify(l, times(0)).updateJobsForUid(anyInt());
+        verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString());
+
+        verify(l, times(0)).unblockAllUnrestrictedAlarms();
+        verify(l, times(0)).unblockAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
+        reset(l);
+
+        instance.setPowerSaveWhitelistAppIds(new int[] {UID_2}, new int[] {});
+
+        waitUntilMainHandlerDrain();
+        verify(l, times(1)).updateAllJobs();
+        verify(l, times(0)).updateJobsForUid(anyInt());
+        verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString());
+
+        verify(l, times(1)).unblockAllUnrestrictedAlarms();
+        verify(l, times(0)).unblockAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
+        reset(l);
+
+        // Update temp whitelist.
+        instance.setPowerSaveWhitelistAppIds(new int[] {UID_2}, new int[] {UID_1, UID_3});
+
+        waitUntilMainHandlerDrain();
+        verify(l, times(1)).updateAllJobs();
+        verify(l, times(0)).updateJobsForUid(anyInt());
+        verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString());
+
+        verify(l, times(0)).unblockAllUnrestrictedAlarms();
+        verify(l, times(0)).unblockAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
+        reset(l);
+
+        instance.setPowerSaveWhitelistAppIds(new int[] {UID_2}, new int[] {UID_3});
+
+        waitUntilMainHandlerDrain();
+        verify(l, times(1)).updateAllJobs();
+        verify(l, times(0)).updateJobsForUid(anyInt());
+        verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString());
+
+        verify(l, times(0)).unblockAllUnrestrictedAlarms();
+        verify(l, times(0)).unblockAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
+        reset(l);
+
+        // Do the same thing with battery saver on. (Currently same callbacks are called.)
+        mPowerSaveMode = true;
+        mPowerSaveObserver.accept(getPowerSaveState());
+
+        instance.setPowerSaveWhitelistAppIds(new int[] {UID_1, UID_2}, new int[] {});
+
+        waitUntilMainHandlerDrain();
+        verify(l, times(1)).updateAllJobs();
+        verify(l, times(0)).updateJobsForUid(anyInt());
+        verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString());
+
+        verify(l, times(0)).unblockAllUnrestrictedAlarms();
+        verify(l, times(0)).unblockAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
+        reset(l);
+
+        instance.setPowerSaveWhitelistAppIds(new int[] {UID_2}, new int[] {});
+
+        waitUntilMainHandlerDrain();
+        verify(l, times(1)).updateAllJobs();
+        verify(l, times(0)).updateJobsForUid(anyInt());
+        verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString());
+
+        verify(l, times(1)).unblockAllUnrestrictedAlarms();
+        verify(l, times(0)).unblockAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
+        reset(l);
+
+        // Update temp whitelist.
+        instance.setPowerSaveWhitelistAppIds(new int[] {UID_2}, new int[] {UID_1, UID_3});
+
+        waitUntilMainHandlerDrain();
+        verify(l, times(1)).updateAllJobs();
+        verify(l, times(0)).updateJobsForUid(anyInt());
+        verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString());
+
+        verify(l, times(0)).unblockAllUnrestrictedAlarms();
+        verify(l, times(0)).unblockAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
+        reset(l);
+
+        instance.setPowerSaveWhitelistAppIds(new int[] {UID_2}, new int[] {UID_3});
+
+        waitUntilMainHandlerDrain();
+        verify(l, times(1)).updateAllJobs();
+        verify(l, times(0)).updateJobsForUid(anyInt());
+        verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString());
+
+        verify(l, times(0)).unblockAllUnrestrictedAlarms();
+        verify(l, times(0)).unblockAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
+        reset(l);
+
+
+        // -------------------------------------------------------------------------
+        // Tests with proc state changes.
+
+        // With battery save.
+        mPowerSaveMode = true;
+        mPowerSaveObserver.accept(getPowerSaveState());
+
+        mIUidObserver.onUidActive(UID_10_1);
+
+        waitUntilMainHandlerDrain();
+        verify(l, times(0)).updateAllJobs();
+        verify(l, times(1)).updateJobsForUid(eq(UID_10_1));
+        verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString());
+
+        verify(l, times(0)).unblockAllUnrestrictedAlarms();
+        verify(l, times(0)).unblockAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
+        reset(l);
+
+        mIUidObserver.onUidGone(UID_10_1, true);
+
+        waitUntilMainHandlerDrain();
+        verify(l, times(0)).updateAllJobs();
+        verify(l, times(1)).updateJobsForUid(eq(UID_10_1));
+        verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString());
+
+        verify(l, times(0)).unblockAllUnrestrictedAlarms();
+        verify(l, times(0)).unblockAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
+        reset(l);
+
+        mIUidObserver.onUidActive(UID_10_1);
+
+        waitUntilMainHandlerDrain();
+        verify(l, times(0)).updateAllJobs();
+        verify(l, times(1)).updateJobsForUid(eq(UID_10_1));
+        verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString());
+
+        verify(l, times(0)).unblockAllUnrestrictedAlarms();
+        verify(l, times(0)).unblockAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
+        reset(l);
+
+        mIUidObserver.onUidIdle(UID_10_1, true);
+
+        waitUntilMainHandlerDrain();
+        verify(l, times(0)).updateAllJobs();
+        verify(l, times(1)).updateJobsForUid(eq(UID_10_1));
+        verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString());
+
+        verify(l, times(0)).unblockAllUnrestrictedAlarms();
+        verify(l, times(0)).unblockAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
+        reset(l);
+
+        // Without battery save.
+        mPowerSaveMode = false;
+        mPowerSaveObserver.accept(getPowerSaveState());
+
+        mIUidObserver.onUidActive(UID_10_1);
+
+        waitUntilMainHandlerDrain();
+        verify(l, times(0)).updateAllJobs();
+        verify(l, times(1)).updateJobsForUid(eq(UID_10_1));
+        verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString());
+
+        verify(l, times(0)).unblockAllUnrestrictedAlarms();
+        verify(l, times(0)).unblockAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
+        reset(l);
+
+        mIUidObserver.onUidGone(UID_10_1, true);
+
+        waitUntilMainHandlerDrain();
+        verify(l, times(0)).updateAllJobs();
+        verify(l, times(1)).updateJobsForUid(eq(UID_10_1));
+        verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString());
+
+        verify(l, times(0)).unblockAllUnrestrictedAlarms();
+        verify(l, times(0)).unblockAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
+        reset(l);
+
+        mIUidObserver.onUidActive(UID_10_1);
+
+        waitUntilMainHandlerDrain();
+        verify(l, times(0)).updateAllJobs();
+        verify(l, times(1)).updateJobsForUid(eq(UID_10_1));
+        verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString());
+
+        verify(l, times(0)).unblockAllUnrestrictedAlarms();
+        verify(l, times(0)).unblockAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
+        reset(l);
+
+        mIUidObserver.onUidIdle(UID_10_1, true);
+
+        waitUntilMainHandlerDrain();
+        verify(l, times(0)).updateAllJobs();
+        verify(l, times(1)).updateJobsForUid(eq(UID_10_1));
+        verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString());
+
+        verify(l, times(0)).unblockAllUnrestrictedAlarms();
+        verify(l, times(0)).unblockAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
+        reset(l);
+    }
+
+    @Test
+    public void testUserRemoved() throws Exception {
+        final ForceAppStandbyTrackerTestable instance = newInstance();
+        callStart(instance);
+
+        mIUidObserver.onUidActive(UID_1);
+        mIUidObserver.onUidActive(UID_10_1);
+
+        setAppOps(UID_2, PACKAGE_2, true);
+        setAppOps(UID_10_2, PACKAGE_2, true);
+
+        assertTrue(instance.isInForeground(UID_1));
+        assertTrue(instance.isInForeground(UID_10_1));
+
+        assertFalse(instance.isRunAnyInBackgroundAppOpsAllowed(UID_2, PACKAGE_2));
+        assertFalse(instance.isRunAnyInBackgroundAppOpsAllowed(UID_10_2, PACKAGE_2));
+
+        final Intent intent = new Intent(Intent.ACTION_USER_REMOVED);
+        intent.putExtra(Intent.EXTRA_USER_HANDLE, 10);
+        mReceiver.onReceive(mMockContext, intent);
+
+        waitUntilMainHandlerDrain();
+
+        assertTrue(instance.isInForeground(UID_1));
+        assertFalse(instance.isInForeground(UID_10_1));
+
+        assertFalse(instance.isRunAnyInBackgroundAppOpsAllowed(UID_2, PACKAGE_2));
+        assertTrue(instance.isRunAnyInBackgroundAppOpsAllowed(UID_10_2, PACKAGE_2));
+    }
+
+    static int[] array(int... appIds) {
+        Arrays.sort(appIds);
+        return appIds;
+    }
+
+    private final Random mRandom = new Random();
+
+    int[] makeRandomArray() {
+        final ArrayList<Integer> list = new ArrayList<>();
+        for (int i = 0; i < 5; i++) {
+            if (mRandom.nextDouble() < 0.5) {
+                list.add(i);
+            }
+        }
+        return Arrays.stream(list.toArray(new Integer[list.size()]))
+                .mapToInt(Integer::intValue).toArray();
+    }
+
+    static boolean isAnyAppIdUnwhitelistedSlow(int[] prevArray, int[] newArray) {
+        Arrays.sort(newArray); // Just in case...
+        for (int p : prevArray) {
+            if (Arrays.binarySearch(newArray, p) < 0) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private void checkAnyAppIdUnwhitelisted(int[] prevArray, int[] newArray, boolean expected) {
+        assertEquals("Input: " + Arrays.toString(prevArray) + " " + Arrays.toString(newArray),
+                expected, ForceAppStandbyTracker.isAnyAppIdUnwhitelisted(prevArray, newArray));
+
+        // Also test isAnyAppIdUnwhitelistedSlow.
+        assertEquals("Input: " + Arrays.toString(prevArray) + " " + Arrays.toString(newArray),
+                expected, isAnyAppIdUnwhitelistedSlow(prevArray, newArray));
+    }
+
+    @Test
+    public void isAnyAppIdUnwhitelisted() {
+        checkAnyAppIdUnwhitelisted(array(), array(), false);
+
+        checkAnyAppIdUnwhitelisted(array(1), array(), true);
+        checkAnyAppIdUnwhitelisted(array(1), array(1), false);
+        checkAnyAppIdUnwhitelisted(array(1), array(0, 1), false);
+        checkAnyAppIdUnwhitelisted(array(1), array(0, 1, 2), false);
+        checkAnyAppIdUnwhitelisted(array(1), array(0, 1, 2), false);
+
+        checkAnyAppIdUnwhitelisted(array(1, 2, 10), array(), true);
+        checkAnyAppIdUnwhitelisted(array(1, 2, 10), array(1, 2), true);
+        checkAnyAppIdUnwhitelisted(array(1, 2, 10), array(1, 2, 10), false);
+        checkAnyAppIdUnwhitelisted(array(1, 2, 10), array(2, 10), true);
+        checkAnyAppIdUnwhitelisted(array(1, 2, 10), array(0, 1, 2, 4, 3, 10), false);
+        checkAnyAppIdUnwhitelisted(array(1, 2, 10), array(0, 0, 1, 2, 10), false);
+
+        // Random test
+        int trueCount = 0;
+        final int count = 10000;
+        for (int i = 0; i < count; i++) {
+            final int[] array1 = makeRandomArray();
+            final int[] array2 = makeRandomArray();
+
+            final boolean expected = isAnyAppIdUnwhitelistedSlow(array1, array2);
+            final boolean actual = ForceAppStandbyTracker.isAnyAppIdUnwhitelisted(array1, array2);
+
+            assertEquals("Input: " + Arrays.toString(array1) + " " + Arrays.toString(array2),
+                    expected, actual);
+            if (expected) {
+                trueCount++;
+            }
+        }
+
+        // Make sure makeRandomArray() didn't generate all same arrays by accident.
+        assertTrue(trueCount > 0);
+        assertTrue(trueCount < count);
+    }
+}