Merge "Do not throttle EXEMPT apps on battery saver"
diff --git a/core/java/android/util/SparseSetArray.java b/core/java/android/util/SparseSetArray.java
new file mode 100644
index 0000000..d100f12
--- /dev/null
+++ b/core/java/android/util/SparseSetArray.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2018 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 android.util;
+
+/**
+ * A sparse array of ArraySets, which is suitable to hold userid->packages association.
+ *
+ * @hide
+ */
+public class SparseSetArray<T> {
+    private final SparseArray<ArraySet<T>> mData = new SparseArray<>();
+
+    public SparseSetArray() {
+    }
+
+    /**
+     * Add a value at index n.
+     * @return FALSE when the value already existed at the given index, TRUE otherwise.
+     */
+    public boolean add(int n, T value) {
+        ArraySet<T> set = mData.get(n);
+        if (set == null) {
+            set = new ArraySet<>();
+            mData.put(n, set);
+        }
+        if (set.contains(value)) {
+            return true;
+        }
+        set.add(value);
+        return false;
+    }
+
+    /**
+     * @return whether a value exists at index n.
+     */
+    public boolean contains(int n, T value) {
+        final ArraySet<T> set = mData.get(n);
+        if (set == null) {
+            return false;
+        }
+        return set.contains(value);
+    }
+
+    /**
+     * Remove a value from index n.
+     * @return TRUE when the value existed at the given index and removed, FALSE otherwise.
+     */
+    public boolean remove(int n, T value) {
+        final ArraySet<T> set = mData.get(n);
+        if (set == null) {
+            return false;
+        }
+        final boolean ret = set.remove(value);
+        if (set.size() == 0) {
+            mData.remove(n);
+        }
+        return ret;
+    }
+
+    /**
+     * Remove all values from index n.
+     */
+    public void remove(int n) {
+        mData.remove(n);
+    }
+    public int size() {
+        return mData.size();
+    }
+
+    public int keyAt(int index) {
+        return mData.keyAt(index);
+    }
+
+    public int sizeAt(int index) {
+        final ArraySet<T> set = mData.valueAt(index);
+        if (set == null) {
+            return 0;
+        }
+        return set.size();
+    }
+
+    public T valueAt(int intIndex, int valueIndex) {
+        return mData.valueAt(intIndex).valueAt(valueIndex);
+    }
+}
diff --git a/core/proto/android/server/forceappstandbytracker.proto b/core/proto/android/server/forceappstandbytracker.proto
index 5657f96..d0fd0b4 100644
--- a/core/proto/android/server/forceappstandbytracker.proto
+++ b/core/proto/android/server/forceappstandbytracker.proto
@@ -16,6 +16,8 @@
 
 syntax = "proto2";
 
+import "frameworks/base/core/proto/android/server/statlogger.proto";
+
 package com.android.server;
 
 option java_multiple_files = true;
@@ -53,6 +55,17 @@
   // Whether force app standby for small battery device setting is enabled
   optional bool force_all_apps_standby_for_small_battery = 7;
 
-  // Whether device is charging
-  optional bool is_charging = 8;
+  // Whether device is plugged in to the charger
+  optional bool is_plugged_in = 8;
+
+  // Performance stats.
+  optional StatLoggerProto stats = 9;
+
+  message ExemptedPackage {
+    optional int32 userId = 1;
+    optional string package_name = 2;
+  }
+
+  // Packages that are in the EXEMPT bucket.
+  repeated ExemptedPackage exempted_packages = 10;
 }
diff --git a/core/proto/android/server/statlogger.proto b/core/proto/android/server/statlogger.proto
new file mode 100644
index 0000000..fa430d8
--- /dev/null
+++ b/core/proto/android/server/statlogger.proto
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2018 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 StatLogger.
+message StatLoggerProto {
+  message Event {
+    optional int32 eventId = 1;
+    optional string label = 2;
+    optional int32 count = 3;
+    optional int64 total_duration_micros = 4;
+  }
+
+  repeated Event events = 1;
+}
diff --git a/services/core/java/com/android/server/ForceAppStandbyTracker.java b/services/core/java/com/android/server/ForceAppStandbyTracker.java
index 257845e..7604044 100644
--- a/services/core/java/com/android/server/ForceAppStandbyTracker.java
+++ b/services/core/java/com/android/server/ForceAppStandbyTracker.java
@@ -22,6 +22,9 @@
 import android.app.AppOpsManager.PackageOps;
 import android.app.IActivityManager;
 import android.app.IUidObserver;
+import android.app.usage.UsageStatsManager;
+import android.app.usage.UsageStatsManagerInternal;
+import android.app.usage.UsageStatsManagerInternal.AppIdleStateChangeListener;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -42,6 +45,7 @@
 import android.util.Pair;
 import android.util.Slog;
 import android.util.SparseBooleanArray;
+import android.util.SparseSetArray;
 import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.annotations.GuardedBy;
@@ -50,6 +54,7 @@
 import com.android.internal.app.IAppOpsService;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.Preconditions;
+import com.android.server.ForceAppStandbyTrackerProto.ExemptedPackage;
 import com.android.server.ForceAppStandbyTrackerProto.RunAnyInBackgroundRestrictedPackages;
 
 import java.io.PrintWriter;
@@ -74,7 +79,7 @@
  */
 public class ForceAppStandbyTracker {
     private static final String TAG = "ForceAppStandbyTracker";
-    private static final boolean DEBUG = false;
+    private static final boolean DEBUG = true;
 
     @GuardedBy("ForceAppStandbyTracker.class")
     private static ForceAppStandbyTracker sInstance;
@@ -89,6 +94,8 @@
     AppOpsManager mAppOpsManager;
     IAppOpsService mAppOpsService;
     PowerManagerInternal mPowerManagerInternal;
+    StandbyTracker mStandbyTracker;
+    UsageStatsManagerInternal mUsageStatsManagerInternal;
 
     private final MyHandler mHandler;
 
@@ -113,6 +120,12 @@
     @GuardedBy("mLock")
     private int[] mTempWhitelistedAppIds = mPowerWhitelistedAllAppIds;
 
+    /**
+     * Per-user packages that are in the EXEMPT bucket.
+     */
+    @GuardedBy("mLock")
+    private final SparseSetArray<String> mExemptedPackages = new SparseSetArray<>();
+
     @GuardedBy("mLock")
     final ArraySet<Listener> mListeners = new ArraySet<>();
 
@@ -146,6 +159,28 @@
     @GuardedBy("mLock")
     boolean mForcedAppStandbyEnabled;
 
+    interface Stats {
+        int UID_STATE_CHANGED = 0;
+        int RUN_ANY_CHANGED = 1;
+        int ALL_UNWHITELISTED = 2;
+        int ALL_WHITELIST_CHANGED = 3;
+        int TEMP_WHITELIST_CHANGED = 4;
+        int EXEMPT_CHANGED = 5;
+        int FORCE_ALL_CHANGED = 6;
+        int FORCE_APP_STANDBY_FEATURE_FLAG_CHANGED = 7;
+    }
+
+    private final StatLogger mStatLogger = new StatLogger(new String[] {
+            "UID_STATE_CHANGED",
+            "RUN_ANY_CHANGED",
+            "ALL_UNWHITELISTED",
+            "ALL_WHITELIST_CHANGED",
+            "TEMP_WHITELIST_CHANGED",
+            "EXEMPT_CHANGED",
+            "FORCE_ALL_CHANGED",
+            "FORCE_APP_STANDBY_FEATURE_FLAG_CHANGED",
+    });
+
     @VisibleForTesting
     class FeatureFlagsObserver extends ContentObserver {
         FeatureFlagsObserver() {
@@ -162,12 +197,11 @@
         }
 
         boolean isForcedAppStandbyEnabled() {
-            return Settings.Global.getInt(mContext.getContentResolver(),
-                    Settings.Global.FORCED_APP_STANDBY_ENABLED, 1) == 1;
+            return injectGetGlobalSettingInt(Settings.Global.FORCED_APP_STANDBY_ENABLED, 1) == 1;
         }
 
         boolean isForcedAppStandbyForSmallBatteryEnabled() {
-            return Settings.Global.getInt(mContext.getContentResolver(),
+            return injectGetGlobalSettingInt(
                     Settings.Global.FORCED_APP_STANDBY_FOR_SMALL_BATTERY_ENABLED, 0) == 1;
         }
 
@@ -258,6 +292,17 @@
             // only for affected app-ids.
 
             updateAllJobs();
+
+            // Note when an app is just put in the temp whitelist, we do *not* drain pending alarms.
+        }
+
+        /**
+         * This is called when the EXEMPT bucket is updated.
+         */
+        private void onExemptChanged(ForceAppStandbyTracker sender) {
+            // This doesn't happen very often, so just re-evaluate all jobs / alarms.
+            updateAllJobs();
+            unblockAllUnrestrictedAlarms();
         }
 
         /**
@@ -346,11 +391,16 @@
             mAppOpsManager = Preconditions.checkNotNull(injectAppOpsManager());
             mAppOpsService = Preconditions.checkNotNull(injectIAppOpsService());
             mPowerManagerInternal = Preconditions.checkNotNull(injectPowerManagerInternal());
+            mUsageStatsManagerInternal = Preconditions.checkNotNull(
+                    injectUsageStatsManagerInternal());
+
             mFlagsObserver = new FeatureFlagsObserver();
             mFlagsObserver.register();
             mForcedAppStandbyEnabled = mFlagsObserver.isForcedAppStandbyEnabled();
             mForceAllAppStandbyForSmallBattery =
                     mFlagsObserver.isForcedAppStandbyForSmallBatteryEnabled();
+            mStandbyTracker = new StandbyTracker();
+            mUsageStatsManagerInternal.addAppIdleStateChangeListener(mStandbyTracker);
 
             try {
                 mIActivityManager.registerUidObserver(new UidObserver(),
@@ -408,10 +458,20 @@
     }
 
     @VisibleForTesting
+    UsageStatsManagerInternal injectUsageStatsManagerInternal() {
+        return LocalServices.getService(UsageStatsManagerInternal.class);
+    }
+
+    @VisibleForTesting
     boolean isSmallBatteryDevice() {
         return ActivityManager.isSmallBatteryDevice();
     }
 
+    @VisibleForTesting
+    int injectGetGlobalSettingInt(String key, int def) {
+        return Settings.Global.getInt(mContext.getContentResolver(), key, def);
+    }
+
     /**
      * Update {@link #mRunAnyRestrictedPackages} with the current app ops state.
      */
@@ -604,6 +664,30 @@
         }
     }
 
+    final class StandbyTracker extends AppIdleStateChangeListener {
+        @Override
+        public void onAppIdleStateChanged(String packageName, int userId, boolean idle,
+                int bucket) {
+            if (DEBUG) {
+                Slog.d(TAG,"onAppIdleStateChanged: " + packageName + " u" + userId
+                        + (idle ? " idle" : " active") + " " + bucket);
+            }
+            final boolean changed;
+            if (bucket == UsageStatsManager.STANDBY_BUCKET_EXEMPTED) {
+                changed = mExemptedPackages.add(userId, packageName);
+            } else {
+                changed = mExemptedPackages.remove(userId, packageName);
+            }
+            if (changed) {
+                mHandler.notifyExemptChanged();
+            }
+        }
+
+        @Override
+        public void onParoleStateChanged(boolean isParoleOn) {
+        }
+    }
+
     private Listener[] cloneListeners() {
         synchronized (mLock) {
             return mListeners.toArray(new Listener[mListeners.size()]);
@@ -619,6 +703,7 @@
         private static final int MSG_FORCE_ALL_CHANGED = 6;
         private static final int MSG_USER_REMOVED = 7;
         private static final int MSG_FORCE_APP_STANDBY_FEATURE_FLAG_CHANGED = 8;
+        private static final int MSG_EXEMPT_CHANGED = 9;
 
         public MyHandler(Looper looper) {
             super(looper);
@@ -652,6 +737,10 @@
             obtainMessage(MSG_FORCE_APP_STANDBY_FEATURE_FLAG_CHANGED).sendToTarget();
         }
 
+        public void notifyExemptChanged() {
+            obtainMessage(MSG_EXEMPT_CHANGED).sendToTarget();
+        }
+
         public void doUserRemoved(int userId) {
             obtainMessage(MSG_USER_REMOVED, userId, 0).sendToTarget();
         }
@@ -672,37 +761,57 @@
             }
             final ForceAppStandbyTracker sender = ForceAppStandbyTracker.this;
 
+            long start = mStatLogger.getTime();
             switch (msg.what) {
                 case MSG_UID_STATE_CHANGED:
                     for (Listener l : cloneListeners()) {
                         l.onUidForegroundStateChanged(sender, msg.arg1);
                     }
+                    mStatLogger.logDurationStat(Stats.UID_STATE_CHANGED, start);
                     return;
+
                 case MSG_RUN_ANY_CHANGED:
                     for (Listener l : cloneListeners()) {
                         l.onRunAnyAppOpsChanged(sender, msg.arg1, (String) msg.obj);
                     }
+                    mStatLogger.logDurationStat(Stats.RUN_ANY_CHANGED, start);
                     return;
+
                 case MSG_ALL_UNWHITELISTED:
                     for (Listener l : cloneListeners()) {
                         l.onPowerSaveUnwhitelisted(sender);
                     }
+                    mStatLogger.logDurationStat(Stats.ALL_UNWHITELISTED, start);
                     return;
+
                 case MSG_ALL_WHITELIST_CHANGED:
                     for (Listener l : cloneListeners()) {
                         l.onPowerSaveWhitelistedChanged(sender);
                     }
+                    mStatLogger.logDurationStat(Stats.ALL_WHITELIST_CHANGED, start);
                     return;
+
                 case MSG_TEMP_WHITELIST_CHANGED:
                     for (Listener l : cloneListeners()) {
                         l.onTempPowerSaveWhitelistChanged(sender);
                     }
+                    mStatLogger.logDurationStat(Stats.TEMP_WHITELIST_CHANGED, start);
                     return;
+
+                case MSG_EXEMPT_CHANGED:
+                    for (Listener l : cloneListeners()) {
+                        l.onExemptChanged(sender);
+                    }
+                    mStatLogger.logDurationStat(Stats.EXEMPT_CHANGED, start);
+                    return;
+
                 case MSG_FORCE_ALL_CHANGED:
                     for (Listener l : cloneListeners()) {
                         l.onForceAllAppsStandbyChanged(sender);
                     }
+                    mStatLogger.logDurationStat(Stats.FORCE_ALL_CHANGED, start);
                     return;
+
                 case MSG_FORCE_APP_STANDBY_FEATURE_FLAG_CHANGED:
                     // Feature flag for forced app standby changed.
                     final boolean unblockAlarms;
@@ -715,7 +824,10 @@
                             l.unblockAllUnrestrictedAlarms();
                         }
                     }
+                    mStatLogger.logDurationStat(
+                            Stats.FORCE_APP_STANDBY_FEATURE_FLAG_CHANGED, start);
                     return;
+
                 case MSG_USER_REMOVED:
                     handleUserRemoved(msg.arg1);
                     return;
@@ -742,6 +854,7 @@
                     mForegroundUids.removeAt(i);
                 }
             }
+            mExemptedPackages.remove(removedUserId);
         }
     }
 
@@ -860,6 +973,10 @@
             if (exemptOnBatterySaver) {
                 return false;
             }
+            final int userId = UserHandle.getUserId(uid);
+            if (mExemptedPackages.contains(userId, packageName)) {
+                return false;
+            }
             return mForceAllAppsStandby;
         }
     }
@@ -966,6 +1083,23 @@
             pw.println(Arrays.toString(mTempWhitelistedAppIds));
 
             pw.print(indent);
+            pw.println("Exempted packages:");
+            for (int i = 0; i < mExemptedPackages.size(); i++) {
+                pw.print(indent);
+                pw.print("  User ");
+                pw.print(mExemptedPackages.keyAt(i));
+                pw.println();
+
+                for (int j = 0; j < mExemptedPackages.sizeAt(i); j++) {
+                    pw.print(indent);
+                    pw.print("    ");
+                    pw.print(mExemptedPackages.valueAt(i, j));
+                    pw.println();
+                }
+            }
+            pw.println();
+
+            pw.print(indent);
             pw.println("Restricted packages:");
             for (Pair<Integer, String> uidAndPackage : mRunAnyRestrictedPackages) {
                 pw.print(indent);
@@ -975,6 +1109,8 @@
                 pw.print(uidAndPackage.second);
                 pw.println();
             }
+
+            mStatLogger.dump(pw, indent);
         }
     }
 
@@ -987,7 +1123,7 @@
                     isSmallBatteryDevice());
             proto.write(ForceAppStandbyTrackerProto.FORCE_ALL_APPS_STANDBY_FOR_SMALL_BATTERY,
                     mForceAllAppStandbyForSmallBattery);
-            proto.write(ForceAppStandbyTrackerProto.IS_CHARGING, mIsPluggedIn);
+            proto.write(ForceAppStandbyTrackerProto.IS_PLUGGED_IN, mIsPluggedIn);
 
             for (int i = 0; i < mForegroundUids.size(); i++) {
                 if (mForegroundUids.valueAt(i)) {
@@ -1004,6 +1140,18 @@
                 proto.write(ForceAppStandbyTrackerProto.TEMP_POWER_SAVE_WHITELIST_APP_IDS, appId);
             }
 
+            for (int i = 0; i < mExemptedPackages.size(); i++) {
+                for (int j = 0; j < mExemptedPackages.sizeAt(i); j++) {
+                    final long token2 = proto.start(
+                            ForceAppStandbyTrackerProto.EXEMPTED_PACKAGES);
+
+                    proto.write(ExemptedPackage.USER_ID, mExemptedPackages.keyAt(i));
+                    proto.write(ExemptedPackage.PACKAGE_NAME, mExemptedPackages.valueAt(i, j));
+
+                    proto.end(token2);
+                }
+            }
+
             for (Pair<Integer, String> uidAndPackage : mRunAnyRestrictedPackages) {
                 final long token2 = proto.start(
                         ForceAppStandbyTrackerProto.RUN_ANY_IN_BACKGROUND_RESTRICTED_PACKAGES);
@@ -1012,6 +1160,9 @@
                         uidAndPackage.second);
                 proto.end(token2);
             }
+
+            mStatLogger.dumpProto(proto, ForceAppStandbyTrackerProto.STATS);
+
             proto.end(token);
         }
     }
diff --git a/services/core/java/com/android/server/StatLogger.java b/services/core/java/com/android/server/StatLogger.java
new file mode 100644
index 0000000..f211731
--- /dev/null
+++ b/services/core/java/com/android/server/StatLogger.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2018 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 android.os.SystemClock;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.server.StatLoggerProto.Event;
+
+import java.io.PrintWriter;
+
+/**
+ * Simple class to keep track of the number of times certain events happened and their durations for
+ * benchmarking.
+ *
+ * TODO Update shortcut service to switch to it.
+ *
+ * @hide
+ */
+public class StatLogger {
+    private final Object mLock = new Object();
+
+    private final int SIZE;
+
+    @GuardedBy("mLock")
+    private final int[] mCountStats;
+
+    @GuardedBy("mLock")
+    private final long[] mDurationStats;
+
+    private final String[] mLabels;
+
+    public StatLogger(String[] eventLabels) {
+        SIZE = eventLabels.length;
+        mCountStats = new int[SIZE];
+        mDurationStats = new long[SIZE];
+        mLabels = eventLabels;
+    }
+
+    /**
+     * Return the current time in the internal time unit.
+     * Call it before an event happens, and
+     * give it back to the {@link #logDurationStat(int, long)}} after the event.
+     */
+    public long getTime() {
+        return SystemClock.elapsedRealtimeNanos() / 1000;
+    }
+
+    /**
+     * @see {@link #getTime()}
+     */
+    public void logDurationStat(int eventId, long start) {
+        synchronized (mLock) {
+            mCountStats[eventId]++;
+            mDurationStats[eventId] += (getTime() - start);
+        }
+    }
+
+    public void dump(PrintWriter pw, String prefix) {
+        synchronized (mLock) {
+            pw.print(prefix);
+            pw.println("Stats:");
+            for (int i = 0; i < SIZE; i++) {
+                pw.print(prefix);
+                pw.print("  ");
+                final int count = mCountStats[i];
+                final double durationMs = mDurationStats[i] / 1000.0;
+                pw.println(String.format("%s: count=%d, total=%.1fms, avg=%.3fms",
+                        mLabels[i], count, durationMs,
+                        (count == 0 ? 0 : ((double) durationMs) / count)));
+            }
+        }
+    }
+
+    public void dumpProto(ProtoOutputStream proto, long fieldId) {
+        synchronized (mLock) {
+            final long outer = proto.start(fieldId);
+
+            for (int i = 0; i < mLabels.length; i++) {
+                final long inner = proto.start(StatLoggerProto.EVENTS);
+
+                proto.write(Event.EVENT_ID, i);
+                proto.write(Event.LABEL, mLabels[i]);
+                proto.write(Event.COUNT, mCountStats[i]);
+                proto.write(Event.TOTAL_DURATION_MICROS, mDurationStats[i]);
+
+                proto.end(inner);
+            }
+
+            proto.end(outer);
+        }
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/ForceAppStandbyTrackerTest.java b/services/tests/servicestests/src/com/android/server/ForceAppStandbyTrackerTest.java
index b68bf2d..a16f118 100644
--- a/services/tests/servicestests/src/com/android/server/ForceAppStandbyTrackerTest.java
+++ b/services/tests/servicestests/src/com/android/server/ForceAppStandbyTrackerTest.java
@@ -15,8 +15,6 @@
  */
 package com.android.server;
 
-import static android.app.ActivityManager.PROCESS_STATE_CACHED_EMPTY;
-
 import static com.android.server.ForceAppStandbyTracker.TARGET_OP;
 
 import static org.junit.Assert.assertEquals;
@@ -35,12 +33,14 @@
 import static org.mockito.Mockito.when;
 
 import android.app.ActivityManager;
-import android.app.ActivityManagerInternal;
 import android.app.AppOpsManager;
 import android.app.AppOpsManager.OpEntry;
 import android.app.AppOpsManager.PackageOps;
 import android.app.IActivityManager;
 import android.app.IUidObserver;
+import android.app.usage.UsageStatsManager;
+import android.app.usage.UsageStatsManagerInternal;
+import android.app.usage.UsageStatsManagerInternal.AppIdleStateChangeListener;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -76,6 +76,7 @@
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Random;
 import java.util.concurrent.CountDownLatch;
@@ -112,6 +113,18 @@
         }
 
         @Override
+        UsageStatsManagerInternal injectUsageStatsManagerInternal() {
+            return mMockUsageStatsManagerInternal;
+        }
+
+        @Override
+        int injectGetGlobalSettingInt(String key, int def) {
+            Integer val = mGlobalSettings.get(key);
+
+            return (val == null) ? def : val;
+        }
+
+        @Override
         boolean isSmallBatteryDevice() { return mIsSmallBatteryDevice; };
     }
 
@@ -143,19 +156,24 @@
     @Mock
     private PowerManagerInternal mMockPowerManagerInternal;
 
+    @Mock
+    private UsageStatsManagerInternal mMockUsageStatsManagerInternal;
+
+    private MockContentResolver mMockContentResolver;
+
     private IUidObserver mIUidObserver;
     private IAppOpsCallback.Stub mAppOpsCallback;
     private Consumer<PowerSaveState> mPowerSaveObserver;
     private BroadcastReceiver mReceiver;
-
-    private MockContentResolver mMockContentResolver;
-    private FakeSettingsProvider mFakeSettingsProvider;
+    private AppIdleStateChangeListener mAppIdleStateChangeListener;
 
     private boolean mPowerSaveMode;
     private boolean mIsSmallBatteryDevice;
 
     private final ArraySet<Pair<Integer, String>> mRestrictedPackages = new ArraySet();
 
+    private final HashMap<String, Integer> mGlobalSettings = new HashMap<>();
+
     @Before
     public void setUp() {
         mMainHandler = new Handler(Looper.getMainLooper());
@@ -198,9 +216,7 @@
                 )).thenAnswer(inv -> new ArrayList<AppOpsManager.PackageOps>());
 
         mMockContentResolver = new MockContentResolver();
-        mFakeSettingsProvider = new FakeSettingsProvider();
         when(mMockContext.getContentResolver()).thenReturn(mMockContentResolver);
-        mMockContentResolver.addProvider(Settings.AUTHORITY, mFakeSettingsProvider);
 
         // Call start.
         instance.start();
@@ -214,6 +230,8 @@
                 ArgumentCaptor.forClass(Consumer.class);
         ArgumentCaptor<BroadcastReceiver> receiverCaptor =
                 ArgumentCaptor.forClass(BroadcastReceiver.class);
+        ArgumentCaptor<AppIdleStateChangeListener> appIdleStateChangeListenerCaptor =
+                ArgumentCaptor.forClass(AppIdleStateChangeListener.class);
 
         verify(mMockIActivityManager).registerUidObserver(
                 uidObserverArgumentCaptor.capture(),
@@ -231,11 +249,14 @@
 
         verify(mMockContext).registerReceiver(
                 receiverCaptor.capture(), any(IntentFilter.class));
+        verify(mMockUsageStatsManagerInternal).addAppIdleStateChangeListener(
+                appIdleStateChangeListenerCaptor.capture());
 
         mIUidObserver = uidObserverArgumentCaptor.getValue();
         mAppOpsCallback = appOpsCallbackCaptor.getValue();
         mPowerSaveObserver = powerSaveObserverCaptor.getValue();
         mReceiver = receiverCaptor.getValue();
+        mAppIdleStateChangeListener = appIdleStateChangeListenerCaptor.getValue();
 
         assertNotNull(mIUidObserver);
         assertNotNull(mAppOpsCallback);
@@ -275,6 +296,12 @@
                 /*exemptFromBatterySaver=*/ false);
     }
 
+    private void areRestrictedWithExemption(ForceAppStandbyTrackerTestable instance,
+            int uid, String packageName, int restrictionTypes) {
+        areRestricted(instance, uid, packageName, restrictionTypes,
+                /*exemptFromBatterySaver=*/ true);
+    }
+
     @Test
     public void testAll() throws Exception {
         final ForceAppStandbyTrackerTestable instance = newInstance();
@@ -285,6 +312,10 @@
         areRestricted(instance, UID_2, PACKAGE_2, NONE);
         areRestricted(instance, Process.SYSTEM_UID, PACKAGE_SYSTEM, NONE);
 
+        areRestrictedWithExemption(instance, UID_1, PACKAGE_1, NONE);
+        areRestrictedWithExemption(instance, UID_2, PACKAGE_2, NONE);
+        areRestrictedWithExemption(instance, Process.SYSTEM_UID, PACKAGE_SYSTEM, NONE);
+
         mPowerSaveMode = true;
         mPowerSaveObserver.accept(getPowerSaveState());
 
@@ -294,6 +325,10 @@
         areRestricted(instance, UID_2, PACKAGE_2, JOBS_AND_ALARMS);
         areRestricted(instance, Process.SYSTEM_UID, PACKAGE_SYSTEM, NONE);
 
+        areRestrictedWithExemption(instance, UID_1, PACKAGE_1, NONE);
+        areRestrictedWithExemption(instance, UID_2, PACKAGE_2, NONE);
+        areRestrictedWithExemption(instance, Process.SYSTEM_UID, PACKAGE_SYSTEM, NONE);
+
         // Toggle the foreground state.
         mPowerSaveMode = true;
         mPowerSaveObserver.accept(getPowerSaveState());
@@ -420,6 +455,73 @@
         assertTrue(instance.isUidTempPowerSaveWhitelisted(UID_10_2));
     }
 
+    @Test
+    public void testExempt() 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, UID_10_2, PACKAGE_2, JOBS_AND_ALARMS);
+        areRestricted(instance, Process.SYSTEM_UID, PACKAGE_SYSTEM, NONE);
+
+        // Exempt package 2 on user-10.
+        mAppIdleStateChangeListener.onAppIdleStateChanged(PACKAGE_2, /*user=*/ 10, false,
+                UsageStatsManager.STANDBY_BUCKET_EXEMPTED);
+
+        areRestricted(instance, UID_1, PACKAGE_1, JOBS_AND_ALARMS);
+        areRestricted(instance, UID_2, PACKAGE_2, JOBS_AND_ALARMS);
+        areRestricted(instance, UID_10_2, PACKAGE_2, NONE);
+
+        areRestrictedWithExemption(instance, UID_1, PACKAGE_1, NONE);
+        areRestrictedWithExemption(instance, UID_2, PACKAGE_2, NONE);
+        areRestrictedWithExemption(instance, UID_10_2, PACKAGE_2, NONE);
+
+        // Exempt package 1 on user-0.
+        mAppIdleStateChangeListener.onAppIdleStateChanged(PACKAGE_1, /*user=*/ 0, false,
+                UsageStatsManager.STANDBY_BUCKET_EXEMPTED);
+
+        areRestricted(instance, UID_1, PACKAGE_1, NONE);
+        areRestricted(instance, UID_2, PACKAGE_2, JOBS_AND_ALARMS);
+        areRestricted(instance, UID_10_2, PACKAGE_2, NONE);
+
+        // Unexempt package 2 on user-10.
+        mAppIdleStateChangeListener.onAppIdleStateChanged(PACKAGE_2, /*user=*/ 10, false,
+                UsageStatsManager.STANDBY_BUCKET_ACTIVE);
+
+        areRestricted(instance, UID_1, PACKAGE_1, NONE);
+        areRestricted(instance, UID_2, PACKAGE_2, JOBS_AND_ALARMS);
+        areRestricted(instance, UID_10_2, PACKAGE_2, JOBS_AND_ALARMS);
+
+        // Check force-app-standby.
+        // EXEMPT doesn't exempt from force-app-standby.
+        mPowerSaveMode = false;
+        mPowerSaveObserver.accept(getPowerSaveState());
+
+        mAppIdleStateChangeListener.onAppIdleStateChanged(PACKAGE_1, /*user=*/ 0, false,
+                UsageStatsManager.STANDBY_BUCKET_EXEMPTED);
+        mAppIdleStateChangeListener.onAppIdleStateChanged(PACKAGE_2, /*user=*/ 0, false,
+                UsageStatsManager.STANDBY_BUCKET_EXEMPTED);
+
+        setAppOps(UID_1, PACKAGE_1, true);
+
+        areRestricted(instance, UID_1, PACKAGE_1, JOBS_AND_ALARMS);
+        areRestricted(instance, UID_2, PACKAGE_2, NONE);
+
+        areRestrictedWithExemption(instance, UID_1, PACKAGE_1, JOBS_AND_ALARMS);
+        areRestrictedWithExemption(instance, UID_2, PACKAGE_2, NONE);
+    }
+
     public void loadPersistedAppOps() throws Exception {
         final ForceAppStandbyTrackerTestable instance = newInstance();
 
@@ -661,6 +763,7 @@
         mPowerSaveMode = true;
         mPowerSaveObserver.accept(getPowerSaveState());
 
+        waitUntilMainHandlerDrain();
         verify(l, times(1)).updateAllJobs();
         verify(l, times(0)).updateJobsForUid(anyInt());
         verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString());
@@ -780,6 +883,7 @@
         mPowerSaveMode = false;
         mPowerSaveObserver.accept(getPowerSaveState());
 
+        waitUntilMainHandlerDrain();
         verify(l, times(1)).updateAllJobs();
         verify(l, times(0)).updateJobsForUid(eq(UID_10_1));
         verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString());
@@ -878,7 +982,7 @@
         assertFalse(instance.isForceAllAppsStandbyEnabled());
 
         // Setting/experiment for all app standby for small battery is enabled
-        Global.putInt(mMockContentResolver, Global.FORCED_APP_STANDBY_FOR_SMALL_BATTERY_ENABLED, 1);
+        mGlobalSettings.put(Global.FORCED_APP_STANDBY_FOR_SMALL_BATTERY_ENABLED, 1);
         instance.mFlagsObserver.onChange(true,
                 Global.getUriFor(Global.FORCED_APP_STANDBY_FOR_SMALL_BATTERY_ENABLED));
         assertTrue(instance.isForceAllAppsStandbyEnabled());