Merge "Optimizing AppStandby.getIdleUidsForUser" into sc-dev
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
index 03d9a96..e63a7c4 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
@@ -59,7 +59,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
-import android.content.PermissionChecker;
+import android.content.pm.PackageManager;
 import android.content.pm.PackageManagerInternal;
 import android.net.Uri;
 import android.os.BatteryManager;
@@ -123,6 +123,7 @@
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
 import com.android.server.SystemServiceManager;
+import com.android.server.pm.permission.PermissionManagerServiceInternal;
 import com.android.server.usage.AppStandbyInternal;
 import com.android.server.usage.AppStandbyInternal.AppIdleStateChangeListener;
 
@@ -198,9 +199,13 @@
     private UsageStatsManagerInternal mUsageStatsManagerInternal;
     private ActivityManagerInternal mActivityManagerInternal;
     private PackageManagerInternal mPackageManagerInternal;
+    private PermissionManagerServiceInternal mLocalPermissionManager;
 
     final Object mLock = new Object();
 
+    /** Immutable set of app ids that have requested SCHEDULE_EXACT_ALARM permission.*/
+    @VisibleForTesting
+    volatile Set<Integer> mExactAlarmCandidates = Collections.emptySet();
     // List of alarms per uid deferred due to user applied background restrictions on the source app
     SparseArray<ArrayList<Alarm>> mPendingBackgroundAlarms = new SparseArray<>();
     private long mNextWakeup;
@@ -1540,6 +1545,21 @@
         publishBinderService(Context.ALARM_SERVICE, mService);
     }
 
+    void refreshExactAlarmCandidates() {
+        final String[] candidates = mLocalPermissionManager.getAppOpPermissionPackages(
+                Manifest.permission.SCHEDULE_EXACT_ALARM);
+        final Set<Integer> appIds = new ArraySet<>(candidates.length);
+        for (final String candidate : candidates) {
+            final int uid = mPackageManagerInternal.getPackageUid(candidate,
+                    PackageManager.MATCH_ANY_USER, USER_SYSTEM);
+            if (uid > 0) {
+                appIds.add(UserHandle.getAppId(uid));
+            }
+        }
+        // No need to lock. Assignment is always atomic.
+        mExactAlarmCandidates = Collections.unmodifiableSet(appIds);
+    }
+
     @Override
     public void onBootPhase(int phase) {
         if (phase == PHASE_SYSTEM_SERVICES_READY) {
@@ -1569,6 +1589,11 @@
                         LocalServices.getService(DeviceIdleInternal.class);
                 mUsageStatsManagerInternal =
                         LocalServices.getService(UsageStatsManagerInternal.class);
+
+                mLocalPermissionManager = LocalServices.getService(
+                        PermissionManagerServiceInternal.class);
+                refreshExactAlarmCandidates();
+
                 AppStandbyInternal appStandbyInternal =
                         LocalServices.getService(AppStandbyInternal.class);
                 appStandbyInternal.addListener(new AppStandbyTracker());
@@ -2097,17 +2122,21 @@
 
     boolean hasScheduleExactAlarmInternal(String packageName, int uid) {
         final long start = mStatLogger.getTime();
-        // No locking needed as EXACT_ALARM_DENY_LIST is immutable.
-        final boolean isOnDenyList = mConstants.EXACT_ALARM_DENY_LIST.contains(packageName);
-        if (isOnDenyList && mAppOps.checkOpNoThrow(AppOpsManager.OP_SCHEDULE_EXACT_ALARM, uid,
-                packageName) != AppOpsManager.MODE_ALLOWED) {
-            return false;
+        final boolean hasPermission;
+        // No locking needed as all internal containers being queried are immutable.
+        if (!mExactAlarmCandidates.contains(UserHandle.getAppId(uid))) {
+            hasPermission = false;
+        } else {
+            final int mode = mAppOps.checkOpNoThrow(AppOpsManager.OP_SCHEDULE_EXACT_ALARM, uid,
+                    packageName);
+            if (mode == AppOpsManager.MODE_DEFAULT) {
+                hasPermission = !mConstants.EXACT_ALARM_DENY_LIST.contains(packageName);
+            } else {
+                hasPermission = (mode == AppOpsManager.MODE_ALLOWED);
+            }
         }
-        final boolean has = PermissionChecker.checkPermissionForPreflight(getContext(),
-                Manifest.permission.SCHEDULE_EXACT_ALARM, -1, uid, packageName)
-                == PermissionChecker.PERMISSION_GRANTED;
         mStatLogger.logDurationStat(Stats.HAS_SCHEDULE_EXACT_ALARM, start);
-        return has;
+        return hasPermission;
     }
 
     /**
@@ -2490,6 +2519,9 @@
             pw.println(mNumTimeChanged);
 
             pw.println();
+            pw.println("App ids requesting SCHEDULE_EXACT_ALARM: " + mExactAlarmCandidates);
+
+            pw.println();
             pw.println("Next alarm clock information: ");
             pw.increaseIndent();
             final TreeSet<Integer> users = new TreeSet<>();
@@ -3924,6 +3956,7 @@
         public static final int REMOVE_FOR_CANCELED = 7;
         public static final int REMOVE_EXACT_ALARMS = 8;
         public static final int EXACT_ALARM_DENY_LIST_CHANGED = 9;
+        public static final int REFRESH_EXACT_ALARM_CANDIDATES = 10;
 
         AlarmHandler() {
             super(Looper.myLooper());
@@ -4015,6 +4048,9 @@
                         handlePackagesAddedToExactAlarmsDenyListLocked((ArraySet<String>) msg.obj);
                     }
                     break;
+                case REFRESH_EXACT_ALARM_CANDIDATES:
+                    refreshExactAlarmCandidates();
+                    break;
                 default:
                     // nope, just ignore it
                     break;
@@ -4135,6 +4171,7 @@
         public UninstallReceiver() {
             IntentFilter filter = new IntentFilter();
             filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+            filter.addAction(Intent.ACTION_PACKAGE_ADDED);
             filter.addAction(Intent.ACTION_PACKAGE_RESTARTED);
             filter.addAction(Intent.ACTION_QUERY_PACKAGE_RESTART);
             filter.addDataScheme(IntentFilter.SCHEME_PACKAGE);
@@ -4179,8 +4216,11 @@
                     case Intent.ACTION_PACKAGE_REMOVED:
                         if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
                             // This package is being updated; don't kill its alarms.
+                            // We will refresh the exact alarm candidates on subsequent receipt of
+                            // PACKAGE_ADDED.
                             return;
                         }
+                        mHandler.sendEmptyMessage(AlarmHandler.REFRESH_EXACT_ALARM_CANDIDATES);
                         // Intentional fall-through.
                     case Intent.ACTION_PACKAGE_RESTARTED:
                         final Uri data = intent.getData();
@@ -4191,6 +4231,9 @@
                             }
                         }
                         break;
+                    case Intent.ACTION_PACKAGE_ADDED:
+                        mHandler.sendEmptyMessage(AlarmHandler.REFRESH_EXACT_ALARM_CANDIDATES);
+                        return;
                 }
                 if (pkgList != null && (pkgList.length > 0)) {
                     for (String pkg : pkgList) {
diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
index 97ee0e1..ebf4ed0 100644
--- a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
+++ b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
@@ -58,7 +58,6 @@
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
 import android.app.ActivityManager;
-import android.app.AppGlobals;
 import android.app.usage.AppStandbyInfo;
 import android.app.usage.UsageEvents;
 import android.app.usage.UsageStatsManager.StandbyBuckets;
@@ -75,7 +74,6 @@
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManagerInternal;
-import android.content.pm.ParceledListSlice;
 import android.database.ContentObserver;
 import android.hardware.display.DisplayManager;
 import android.net.NetworkScoreManager;
@@ -101,7 +99,7 @@
 import android.util.IndentingPrintWriter;
 import android.util.Slog;
 import android.util.SparseArray;
-import android.util.SparseIntArray;
+import android.util.SparseBooleanArray;
 import android.util.TimeUtils;
 import android.view.Display;
 import android.widget.Toast;
@@ -118,6 +116,8 @@
 import com.android.server.pm.parsing.pkg.AndroidPackage;
 import com.android.server.usage.AppIdleHistory.AppUsageHistory;
 
+import libcore.util.EmptyArray;
+
 import java.io.File;
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -1249,71 +1249,55 @@
     @Override
     public int[] getIdleUidsForUser(int userId) {
         if (!mAppIdleEnabled) {
-            return new int[0];
+            return EmptyArray.INT;
         }
 
         Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "getIdleUidsForUser");
 
         final long elapsedRealtime = mInjector.elapsedRealtime();
 
-        List<ApplicationInfo> apps;
-        try {
-            ParceledListSlice<ApplicationInfo> slice = AppGlobals.getPackageManager()
-                    .getInstalledApplications(/* flags= */ 0, userId);
-            if (slice == null) {
-                return new int[0];
-            }
-            apps = slice.getList();
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+        final PackageManagerInternal pmi = mInjector.getPackageManagerInternal();
+        final List<ApplicationInfo> apps = pmi.getInstalledApplications(0, userId, Process.myUid());
+        if (apps == null) {
+            return EmptyArray.INT;
         }
 
-        // State of each uid.  Key is the uid.  Value lower 16 bits is the number of apps
-        // associated with that uid, upper 16 bits is the number of those apps that is idle.
-        SparseIntArray uidStates = new SparseIntArray();
-
-        // Now resolve all app state.  Iterating over all apps, keeping track of how many
-        // we find for each uid and how many of those are idle.
+        // State of each uid: Key is the uid, value is whether all the apps in that uid are idle.
+        final SparseBooleanArray uidIdleStates = new SparseBooleanArray();
+        int notIdleCount = 0;
         for (int i = apps.size() - 1; i >= 0; i--) {
-            ApplicationInfo ai = apps.get(i);
+            final ApplicationInfo ai = apps.get(i);
+            final int index = uidIdleStates.indexOfKey(ai.uid);
 
-            // Check whether this app is idle.
-            boolean idle = isAppIdleFiltered(ai.packageName, UserHandle.getAppId(ai.uid),
-                    userId, elapsedRealtime);
+            final boolean currentIdle = (index < 0) ? true : uidIdleStates.valueAt(index);
 
-            int index = uidStates.indexOfKey(ai.uid);
+            final boolean newIdle = currentIdle && isAppIdleFiltered(ai.packageName,
+                    UserHandle.getAppId(ai.uid), userId, elapsedRealtime);
+
+            if (currentIdle && !newIdle) {
+                // This transition from true to false can happen at most once per uid in this loop.
+                notIdleCount++;
+            }
             if (index < 0) {
-                uidStates.put(ai.uid, 1 + (idle ? 1<<16 : 0));
+                uidIdleStates.put(ai.uid, newIdle);
             } else {
-                int value = uidStates.valueAt(index);
-                uidStates.setValueAt(index, value + 1 + (idle ? 1<<16 : 0));
+                uidIdleStates.setValueAt(index, newIdle);
             }
         }
 
+        int numIdleUids = uidIdleStates.size() - notIdleCount;
+        final int[] idleUids = new int[numIdleUids];
+        for (int i = uidIdleStates.size() - 1; i >= 0; i--) {
+            if (uidIdleStates.valueAt(i)) {
+                idleUids[--numIdleUids] = uidIdleStates.keyAt(i);
+            }
+        }
         if (DEBUG) {
             Slog.d(TAG, "getIdleUids took " + (mInjector.elapsedRealtime() - elapsedRealtime));
         }
-        int numIdle = 0;
-        for (int i = uidStates.size() - 1; i >= 0; i--) {
-            int value = uidStates.valueAt(i);
-            if ((value&0x7fff) == (value>>16)) {
-                numIdle++;
-            }
-        }
-
-        int[] res = new int[numIdle];
-        numIdle = 0;
-        for (int i = uidStates.size() - 1; i >= 0; i--) {
-            int value = uidStates.valueAt(i);
-            if ((value&0x7fff) == (value>>16)) {
-                res[numIdle] = uidStates.keyAt(i);
-                numIdle++;
-            }
-        }
-
         Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
 
-        return res;
+        return idleUids;
     }
 
     @Override
diff --git a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
index d55bbd1..683fbd1 100644
--- a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
@@ -15,6 +15,7 @@
  */
 package com.android.server.alarm;
 
+import static android.Manifest.permission.SCHEDULE_EXACT_ALARM;
 import static android.app.AlarmManager.ELAPSED_REALTIME;
 import static android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP;
 import static android.app.AlarmManager.FLAG_ALLOW_WHILE_IDLE;
@@ -54,6 +55,7 @@
 import static com.android.server.alarm.AlarmManagerService.AlarmHandler.APP_STANDBY_BUCKET_CHANGED;
 import static com.android.server.alarm.AlarmManagerService.AlarmHandler.CHARGING_STATUS_CHANGED;
 import static com.android.server.alarm.AlarmManagerService.AlarmHandler.EXACT_ALARM_DENY_LIST_CHANGED;
+import static com.android.server.alarm.AlarmManagerService.AlarmHandler.REFRESH_EXACT_ALARM_CANDIDATES;
 import static com.android.server.alarm.AlarmManagerService.AlarmHandler.REMOVE_EXACT_ALARMS;
 import static com.android.server.alarm.AlarmManagerService.AlarmHandler.REMOVE_FOR_CANCELED;
 import static com.android.server.alarm.AlarmManagerService.Constants.KEY_ALLOW_WHILE_IDLE_COMPAT_QUOTA;
@@ -99,7 +101,6 @@
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 
-import android.Manifest;
 import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
 import android.app.AlarmManager;
@@ -114,7 +115,6 @@
 import android.app.usage.UsageStatsManagerInternal;
 import android.content.Context;
 import android.content.Intent;
-import android.content.PermissionChecker;
 import android.content.pm.PackageManagerInternal;
 import android.os.BatteryManager;
 import android.os.Bundle;
@@ -139,14 +139,18 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.app.IAppOpsCallback;
 import com.android.internal.app.IAppOpsService;
+import com.android.internal.util.ArrayUtils;
 import com.android.server.AlarmManagerInternal;
 import com.android.server.AppStateTracker;
 import com.android.server.AppStateTrackerImpl;
 import com.android.server.DeviceIdleInternal;
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
+import com.android.server.pm.permission.PermissionManagerServiceInternal;
 import com.android.server.usage.AppStandbyInternal;
 
+import libcore.util.EmptyArray;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -160,6 +164,7 @@
 import org.mockito.stubbing.Answer;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.concurrent.Executor;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -176,6 +181,7 @@
     private long mAllowWhileIdleWindow;
     private AlarmManagerService mService;
     private AppStandbyInternal.AppIdleStateChangeListener mAppStandbyListener;
+    private AlarmManagerService.UninstallReceiver mPackageChangesReceiver;
     private AlarmManagerService.ChargingReceiver mChargingReceiver;
     private IAppOpsCallback mIAppOpsCallback;
     private IAlarmManager mBinder;
@@ -190,6 +196,8 @@
     @Mock
     private DeviceIdleInternal mDeviceIdleInternal;
     @Mock
+    private PermissionManagerServiceInternal mPermissionManagerInternal;
+    @Mock
     private UsageStatsManagerInternal mUsageStatsManagerInternal;
     @Mock
     private AppStandbyInternal mAppStandbyInternal;
@@ -351,7 +359,6 @@
                 .spyStatic(DeviceConfig.class)
                 .mockStatic(LocalServices.class)
                 .spyStatic(Looper.class)
-                .mockStatic(PermissionChecker.class)
                 .mockStatic(Settings.Global.class)
                 .mockStatic(ServiceManager.class)
                 .spyStatic(UserHandle.class)
@@ -361,6 +368,8 @@
         doReturn(mIActivityManager).when(ActivityManager::getService);
         doReturn(mDeviceIdleInternal).when(
                 () -> LocalServices.getService(DeviceIdleInternal.class));
+        doReturn(mPermissionManagerInternal).when(
+                () -> LocalServices.getService(PermissionManagerServiceInternal.class));
         doReturn(mActivityManagerInternal).when(
                 () -> LocalServices.getService(ActivityManagerInternal.class));
         doReturn(mPackageManagerInternal).when(
@@ -399,8 +408,10 @@
 
         when(mMockContext.getSystemService(Context.APP_OPS_SERVICE)).thenReturn(mAppOpsManager);
 
-        when(mPackageManagerInternal.getPackageUid(eq(TEST_CALLING_PACKAGE), anyInt(),
-                eq(TEST_CALLING_USER))).thenReturn(TEST_CALLING_UID);
+        registerAppIds(new String[]{TEST_CALLING_PACKAGE},
+                new Integer[]{UserHandle.getAppId(TEST_CALLING_UID)});
+        when(mPermissionManagerInternal.getAppOpPermissionPackages(
+                SCHEDULE_EXACT_ALARM)).thenReturn(EmptyArray.STRING);
 
         mInjector = new Injector(mMockContext);
         mService = new AlarmManagerService(mMockContext, mInjector);
@@ -424,13 +435,22 @@
         verify(mAppStandbyInternal).addListener(captor.capture());
         mAppStandbyListener = captor.getValue();
 
-        ArgumentCaptor<AlarmManagerService.ChargingReceiver> chargingReceiverCaptor =
+        final ArgumentCaptor<AlarmManagerService.ChargingReceiver> chargingReceiverCaptor =
                 ArgumentCaptor.forClass(AlarmManagerService.ChargingReceiver.class);
         verify(mMockContext).registerReceiver(chargingReceiverCaptor.capture(),
                 argThat((filter) -> filter.hasAction(BatteryManager.ACTION_CHARGING)
                         && filter.hasAction(BatteryManager.ACTION_DISCHARGING)));
         mChargingReceiver = chargingReceiverCaptor.getValue();
 
+        final ArgumentCaptor<AlarmManagerService.UninstallReceiver> packageReceiverCaptor =
+                ArgumentCaptor.forClass(AlarmManagerService.UninstallReceiver.class);
+        verify(mMockContext).registerReceiver(packageReceiverCaptor.capture(),
+                argThat((filter) -> filter.hasAction(Intent.ACTION_PACKAGE_ADDED)
+                        && filter.hasAction(Intent.ACTION_PACKAGE_REMOVED)));
+        mPackageChangesReceiver = packageReceiverCaptor.getValue();
+
+        assertEquals(mService.mExactAlarmCandidates, Collections.emptySet());
+
         ArgumentCaptor<IBinder> binderCaptor = ArgumentCaptor.forClass(IBinder.class);
         verify(() -> ServiceManager.addService(eq(Context.ALARM_SERVICE), binderCaptor.capture(),
                 anyBoolean(), anyInt()));
@@ -927,7 +947,8 @@
 
     private void assertAndHandleMessageSync(int what) {
         final ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
-        verify(mService.mHandler, atLeastOnce()).sendMessage(messageCaptor.capture());
+        verify(mService.mHandler, atLeastOnce()).sendMessageAtTime(messageCaptor.capture(),
+                anyLong());
         final Message lastMessage = messageCaptor.getValue();
         assertEquals("Unexpected message send to handler", lastMessage.what,
                 what);
@@ -1795,70 +1816,45 @@
     }
 
     @Test
-    public void hasScheduleExactAlarmBinderCallEmptyDenyList() throws RemoteException {
-        doReturn(PermissionChecker.PERMISSION_GRANTED).when(
-                () -> PermissionChecker.checkPermissionForPreflight(eq(mMockContext),
-                        eq(Manifest.permission.SCHEDULE_EXACT_ALARM), anyInt(),
-                        eq(TEST_CALLING_UID), eq(TEST_CALLING_PACKAGE)));
+    public void hasScheduleExactAlarmBinderCallNotDenyListed() throws RemoteException {
+        mockExactAlarmPermissionGrant(true, false, MODE_DEFAULT);
         assertTrue(mBinder.hasScheduleExactAlarm(TEST_CALLING_PACKAGE, TEST_CALLING_USER));
 
-        doReturn(PermissionChecker.PERMISSION_HARD_DENIED).when(
-                () -> PermissionChecker.checkPermissionForPreflight(eq(mMockContext),
-                        eq(Manifest.permission.SCHEDULE_EXACT_ALARM), anyInt(),
-                        eq(TEST_CALLING_UID), eq(TEST_CALLING_PACKAGE)));
+        mockExactAlarmPermissionGrant(true, false, MODE_ALLOWED);
+        assertTrue(mBinder.hasScheduleExactAlarm(TEST_CALLING_PACKAGE, TEST_CALLING_USER));
+
+        mockExactAlarmPermissionGrant(true, false, MODE_ERRORED);
+        assertFalse(mBinder.hasScheduleExactAlarm(TEST_CALLING_PACKAGE, TEST_CALLING_USER));
+
+        mockExactAlarmPermissionGrant(true, false, MODE_IGNORED);
         assertFalse(mBinder.hasScheduleExactAlarm(TEST_CALLING_PACKAGE, TEST_CALLING_USER));
     }
 
     @Test
-    public void hasScheduleExactAlarmBinderCallWithDenyList() throws RemoteException {
-        setDeviceConfigString(KEY_EXACT_ALARM_DENY_LIST, TEST_CALLING_PACKAGE);
-
-        when(mAppOpsManager.checkOpNoThrow(OP_SCHEDULE_EXACT_ALARM, TEST_CALLING_UID,
-                TEST_CALLING_PACKAGE)).thenReturn(MODE_ERRORED);
-
+    public void hasScheduleExactAlarmBinderCallDenyListed() throws RemoteException {
+        mockExactAlarmPermissionGrant(true, true, MODE_ERRORED);
         assertFalse(mBinder.hasScheduleExactAlarm(TEST_CALLING_PACKAGE, TEST_CALLING_USER));
-        verify(() -> PermissionChecker.checkPermissionForPreflight(eq(mMockContext),
-                eq(Manifest.permission.SCHEDULE_EXACT_ALARM), anyInt(),
-                eq(TEST_CALLING_UID), eq(TEST_CALLING_PACKAGE)), never());
 
-        when(mAppOpsManager.checkOpNoThrow(OP_SCHEDULE_EXACT_ALARM, TEST_CALLING_UID,
-                TEST_CALLING_PACKAGE)).thenReturn(MODE_DEFAULT);
-
+        mockExactAlarmPermissionGrant(true, true, MODE_DEFAULT);
         assertFalse(mBinder.hasScheduleExactAlarm(TEST_CALLING_PACKAGE, TEST_CALLING_USER));
-        verify(() -> PermissionChecker.checkPermissionForPreflight(eq(mMockContext),
-                eq(Manifest.permission.SCHEDULE_EXACT_ALARM), anyInt(),
-                eq(TEST_CALLING_UID), eq(TEST_CALLING_PACKAGE)), never());
 
-        when(mAppOpsManager.checkOpNoThrow(OP_SCHEDULE_EXACT_ALARM, TEST_CALLING_UID,
-                TEST_CALLING_PACKAGE)).thenReturn(MODE_IGNORED);
-
+        mockExactAlarmPermissionGrant(true, true, MODE_IGNORED);
         assertFalse(mBinder.hasScheduleExactAlarm(TEST_CALLING_PACKAGE, TEST_CALLING_USER));
-        verify(() -> PermissionChecker.checkPermissionForPreflight(eq(mMockContext),
-                eq(Manifest.permission.SCHEDULE_EXACT_ALARM), anyInt(),
-                eq(TEST_CALLING_UID), eq(TEST_CALLING_PACKAGE)), never());
 
-        when(mAppOpsManager.checkOpNoThrow(OP_SCHEDULE_EXACT_ALARM, TEST_CALLING_UID,
-                TEST_CALLING_PACKAGE)).thenReturn(MODE_ALLOWED);
-
-        doReturn(PermissionChecker.PERMISSION_HARD_DENIED).when(
-                () -> PermissionChecker.checkPermissionForPreflight(eq(mMockContext),
-                        eq(Manifest.permission.SCHEDULE_EXACT_ALARM), anyInt(),
-                        eq(TEST_CALLING_UID), eq(TEST_CALLING_PACKAGE)));
-
-        assertFalse(mBinder.hasScheduleExactAlarm(TEST_CALLING_PACKAGE, TEST_CALLING_USER));
-        verify(() -> PermissionChecker.checkPermissionForPreflight(eq(mMockContext),
-                eq(Manifest.permission.SCHEDULE_EXACT_ALARM), anyInt(),
-                eq(TEST_CALLING_UID), eq(TEST_CALLING_PACKAGE)));
-
-        doReturn(PermissionChecker.PERMISSION_GRANTED).when(
-                () -> PermissionChecker.checkPermissionForPreflight(eq(mMockContext),
-                        eq(Manifest.permission.SCHEDULE_EXACT_ALARM), anyInt(),
-                        eq(TEST_CALLING_UID), eq(TEST_CALLING_PACKAGE)));
-
+        mockExactAlarmPermissionGrant(true, true, MODE_ALLOWED);
         assertTrue(mBinder.hasScheduleExactAlarm(TEST_CALLING_PACKAGE, TEST_CALLING_USER));
-        verify(() -> PermissionChecker.checkPermissionForPreflight(eq(mMockContext),
-                eq(Manifest.permission.SCHEDULE_EXACT_ALARM), anyInt(),
-                eq(TEST_CALLING_UID), eq(TEST_CALLING_PACKAGE)), times(2));
+    }
+
+    @Test
+    public void hasScheduleExactAlarmBinderCallNotDeclared() throws RemoteException {
+        mockExactAlarmPermissionGrant(false, false, MODE_DEFAULT);
+        assertFalse(mBinder.hasScheduleExactAlarm(TEST_CALLING_PACKAGE, TEST_CALLING_USER));
+
+        mockExactAlarmPermissionGrant(false, false, MODE_ALLOWED);
+        assertFalse(mBinder.hasScheduleExactAlarm(TEST_CALLING_PACKAGE, TEST_CALLING_USER));
+
+        mockExactAlarmPermissionGrant(false, true, MODE_ALLOWED);
+        assertFalse(mBinder.hasScheduleExactAlarm(TEST_CALLING_PACKAGE, TEST_CALLING_USER));
     }
 
     @Test
@@ -1884,9 +1880,8 @@
         mBinder.set(TEST_CALLING_PACKAGE, ELAPSED_REALTIME_WAKEUP, 1234, WINDOW_HEURISTIC, 0,
                 FLAG_ALLOW_WHILE_IDLE, getNewMockPendingIntent(), null, null, null, null);
 
-        verify(() -> PermissionChecker.checkPermissionForPreflight(eq(mMockContext),
-                eq(Manifest.permission.SCHEDULE_EXACT_ALARM), anyInt(),
-                eq(TEST_CALLING_UID), eq(TEST_CALLING_PACKAGE)), never());
+        verify(mService, never()).hasScheduleExactAlarmInternal(TEST_CALLING_PACKAGE,
+                TEST_CALLING_UID);
         verify(mDeviceIdleInternal, never()).isAppOnWhitelist(anyInt());
     }
 
@@ -1969,10 +1964,7 @@
                 () -> CompatChanges.isChangeEnabled(eq(AlarmManager.REQUIRE_EXACT_ALARM_PERMISSION),
                         anyString(), any(UserHandle.class)));
 
-        doReturn(PermissionChecker.PERMISSION_GRANTED).when(
-                () -> PermissionChecker.checkPermissionForPreflight(eq(mMockContext),
-                        eq(Manifest.permission.SCHEDULE_EXACT_ALARM), anyInt(),
-                        eq(TEST_CALLING_UID), eq(TEST_CALLING_PACKAGE)));
+        mockExactAlarmPermissionGrant(true, false, MODE_ALLOWED);
 
         final PendingIntent alarmPi = getNewMockPendingIntent();
         final AlarmManager.AlarmClockInfo alarmClock = mock(AlarmManager.AlarmClockInfo.class);
@@ -1980,9 +1972,7 @@
                 alarmPi, null, null, null, alarmClock);
 
         // Correct permission checks are invoked.
-        verify(() -> PermissionChecker.checkPermissionForPreflight(eq(mMockContext),
-                eq(Manifest.permission.SCHEDULE_EXACT_ALARM), anyInt(),
-                eq(TEST_CALLING_UID), eq(TEST_CALLING_PACKAGE)));
+        verify(mService).hasScheduleExactAlarmInternal(TEST_CALLING_PACKAGE, TEST_CALLING_UID);
         verify(mDeviceIdleInternal, never()).isAppOnWhitelist(anyInt());
 
         final ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
@@ -1996,6 +1986,22 @@
         assertEquals(TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED, type);
     }
 
+    private void mockExactAlarmPermissionGrant(boolean declared, boolean denyList, int mode) {
+        String[] requesters = declared ? new String[]{TEST_CALLING_PACKAGE} : EmptyArray.STRING;
+        when(mPermissionManagerInternal.getAppOpPermissionPackages(SCHEDULE_EXACT_ALARM))
+                .thenReturn(requesters);
+        mService.refreshExactAlarmCandidates();
+
+        if (denyList) {
+            setDeviceConfigString(KEY_EXACT_ALARM_DENY_LIST, TEST_CALLING_PACKAGE);
+        } else {
+            setDeviceConfigString(KEY_EXACT_ALARM_DENY_LIST, "");
+        }
+
+        when(mAppOpsManager.checkOpNoThrow(OP_SCHEDULE_EXACT_ALARM, TEST_CALLING_UID,
+                TEST_CALLING_PACKAGE)).thenReturn(mode);
+    }
+
     @Test
     public void alarmClockBinderCallWithoutPermission() throws RemoteException {
         setDeviceConfigBoolean(KEY_CRASH_NON_CLOCK_APPS, true);
@@ -2003,10 +2009,7 @@
                 () -> CompatChanges.isChangeEnabled(eq(AlarmManager.REQUIRE_EXACT_ALARM_PERMISSION),
                         anyString(), any(UserHandle.class)));
 
-        doReturn(PermissionChecker.PERMISSION_HARD_DENIED).when(
-                () -> PermissionChecker.checkPermissionForPreflight(eq(mMockContext),
-                        eq(Manifest.permission.SCHEDULE_EXACT_ALARM), anyInt(),
-                        eq(TEST_CALLING_UID), eq(TEST_CALLING_PACKAGE)));
+        mockExactAlarmPermissionGrant(true, false, MODE_ERRORED);
         when(mDeviceIdleInternal.isAppOnWhitelist(anyInt())).thenReturn(true);
 
         final PendingIntent alarmPi = getNewMockPendingIntent();
@@ -2018,9 +2021,6 @@
         } catch (SecurityException se) {
             // Expected.
         }
-        verify(() -> PermissionChecker.checkPermissionForPreflight(eq(mMockContext),
-                eq(Manifest.permission.SCHEDULE_EXACT_ALARM), anyInt(),
-                eq(TEST_CALLING_UID), eq(TEST_CALLING_PACKAGE)));
         verify(mDeviceIdleInternal, never()).isAppOnWhitelist(anyInt());
     }
 
@@ -2030,14 +2030,12 @@
                 () -> CompatChanges.isChangeEnabled(eq(AlarmManager.REQUIRE_EXACT_ALARM_PERMISSION),
                         anyString(), any(UserHandle.class)));
 
-        // Permission check is granted by default by the mock.
+        mockExactAlarmPermissionGrant(true, false, MODE_ALLOWED);
         final PendingIntent alarmPi = getNewMockPendingIntent();
         mBinder.set(TEST_CALLING_PACKAGE, ELAPSED_REALTIME_WAKEUP, 1234, WINDOW_EXACT, 0,
                 0, alarmPi, null, null, null, null);
 
-        verify(() -> PermissionChecker.checkPermissionForPreflight(eq(mMockContext),
-                eq(Manifest.permission.SCHEDULE_EXACT_ALARM), anyInt(),
-                eq(TEST_CALLING_UID), eq(TEST_CALLING_PACKAGE)));
+        verify(mService).hasScheduleExactAlarmInternal(TEST_CALLING_PACKAGE, TEST_CALLING_UID);
         verify(mDeviceIdleInternal, never()).isAppOnWhitelist(anyInt());
 
         final ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
@@ -2057,19 +2055,13 @@
                 () -> CompatChanges.isChangeEnabled(eq(AlarmManager.REQUIRE_EXACT_ALARM_PERMISSION),
                         anyString(), any(UserHandle.class)));
         // If permission is denied, only then allowlist will be checked.
-        doReturn(PermissionChecker.PERMISSION_HARD_DENIED).when(
-                () -> PermissionChecker.checkPermissionForPreflight(eq(mMockContext),
-                        eq(Manifest.permission.SCHEDULE_EXACT_ALARM), anyInt(),
-                        eq(TEST_CALLING_UID), eq(TEST_CALLING_PACKAGE)));
+        mockExactAlarmPermissionGrant(true, false, MODE_ERRORED);
         when(mDeviceIdleInternal.isAppOnWhitelist(anyInt())).thenReturn(true);
 
         final PendingIntent alarmPi = getNewMockPendingIntent();
         mBinder.set(TEST_CALLING_PACKAGE, ELAPSED_REALTIME_WAKEUP, 1234, WINDOW_EXACT, 0,
                 0, alarmPi, null, null, null, null);
 
-        verify(() -> PermissionChecker.checkPermissionForPreflight(eq(mMockContext),
-                eq(Manifest.permission.SCHEDULE_EXACT_ALARM), anyInt(),
-                eq(TEST_CALLING_UID), eq(TEST_CALLING_PACKAGE)));
         verify(mDeviceIdleInternal).isAppOnWhitelist(UserHandle.getAppId(TEST_CALLING_UID));
 
         verify(mService).setImpl(eq(ELAPSED_REALTIME_WAKEUP), eq(1234L), eq(WINDOW_EXACT), eq(0L),
@@ -2084,14 +2076,11 @@
                 () -> CompatChanges.isChangeEnabled(eq(AlarmManager.REQUIRE_EXACT_ALARM_PERMISSION),
                         anyString(), any(UserHandle.class)));
 
-        // Permission check is granted by default by the mock.
+        mockExactAlarmPermissionGrant(true, false, MODE_ALLOWED);
         final PendingIntent alarmPi = getNewMockPendingIntent();
         mBinder.set(TEST_CALLING_PACKAGE, ELAPSED_REALTIME_WAKEUP, 1234, WINDOW_EXACT, 0,
                 FLAG_ALLOW_WHILE_IDLE, alarmPi, null, null, null, null);
 
-        verify(() -> PermissionChecker.checkPermissionForPreflight(eq(mMockContext),
-                eq(Manifest.permission.SCHEDULE_EXACT_ALARM), anyInt(),
-                eq(TEST_CALLING_UID), eq(TEST_CALLING_PACKAGE)));
         verify(mDeviceIdleInternal, never()).isAppOnWhitelist(anyInt());
 
         final ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
@@ -2111,19 +2100,13 @@
                 () -> CompatChanges.isChangeEnabled(eq(AlarmManager.REQUIRE_EXACT_ALARM_PERMISSION),
                         anyString(), any(UserHandle.class)));
         // If permission is denied, only then allowlist will be checked.
-        doReturn(PermissionChecker.PERMISSION_HARD_DENIED).when(
-                () -> PermissionChecker.checkPermissionForPreflight(eq(mMockContext),
-                        eq(Manifest.permission.SCHEDULE_EXACT_ALARM), anyInt(),
-                        eq(TEST_CALLING_UID), eq(TEST_CALLING_PACKAGE)));
+        mockExactAlarmPermissionGrant(true, false, MODE_ERRORED);
         when(mDeviceIdleInternal.isAppOnWhitelist(anyInt())).thenReturn(true);
 
         final PendingIntent alarmPi = getNewMockPendingIntent();
         mBinder.set(TEST_CALLING_PACKAGE, ELAPSED_REALTIME_WAKEUP, 1234, WINDOW_EXACT, 0,
                 FLAG_ALLOW_WHILE_IDLE, alarmPi, null, null, null, null);
 
-        verify(() -> PermissionChecker.checkPermissionForPreflight(eq(mMockContext),
-                eq(Manifest.permission.SCHEDULE_EXACT_ALARM), anyInt(),
-                eq(TEST_CALLING_UID), eq(TEST_CALLING_PACKAGE)));
         verify(mDeviceIdleInternal).isAppOnWhitelist(UserHandle.getAppId(TEST_CALLING_UID));
 
         final ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
@@ -2145,10 +2128,7 @@
                 () -> CompatChanges.isChangeEnabled(eq(AlarmManager.REQUIRE_EXACT_ALARM_PERMISSION),
                         anyString(), any(UserHandle.class)));
 
-        doReturn(PermissionChecker.PERMISSION_HARD_DENIED).when(
-                () -> PermissionChecker.checkPermissionForPreflight(eq(mMockContext),
-                        eq(Manifest.permission.SCHEDULE_EXACT_ALARM), anyInt(),
-                        eq(TEST_CALLING_UID), eq(TEST_CALLING_PACKAGE)));
+        mockExactAlarmPermissionGrant(true, false, MODE_ERRORED);
         when(mDeviceIdleInternal.isAppOnWhitelist(anyInt())).thenReturn(false);
 
         final PendingIntent alarmPi = getNewMockPendingIntent();
@@ -2166,9 +2146,6 @@
         } catch (SecurityException se) {
             // Expected.
         }
-        verify(() -> PermissionChecker.checkPermissionForPreflight(eq(mMockContext),
-                eq(Manifest.permission.SCHEDULE_EXACT_ALARM), anyInt(),
-                eq(TEST_CALLING_UID), eq(TEST_CALLING_PACKAGE)), times(2));
         verify(mDeviceIdleInternal, times(2)).isAppOnWhitelist(anyInt());
     }
 
@@ -2184,9 +2161,7 @@
         mBinder.set(TEST_CALLING_PACKAGE, ELAPSED_REALTIME_WAKEUP, 4321, WINDOW_HEURISTIC, 0,
                 FLAG_ALLOW_WHILE_IDLE, alarmPi, null, null, null, null);
 
-        verify(() -> PermissionChecker.checkPermissionForPreflight(eq(mMockContext),
-                eq(Manifest.permission.SCHEDULE_EXACT_ALARM), anyInt(),
-                eq(TEST_CALLING_UID), eq(TEST_CALLING_PACKAGE)), never());
+        verify(mService, never()).hasScheduleExactAlarmInternal(anyString(), anyInt());
         verify(mDeviceIdleInternal, never()).isAppOnWhitelist(anyInt());
 
         final ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
@@ -2205,10 +2180,7 @@
                 () -> CompatChanges.isChangeEnabled(eq(AlarmManager.REQUIRE_EXACT_ALARM_PERMISSION),
                         anyString(), any(UserHandle.class)));
 
-        doReturn(PermissionChecker.PERMISSION_HARD_DENIED).when(
-                () -> PermissionChecker.checkPermissionForPreflight(eq(mMockContext),
-                        eq(Manifest.permission.SCHEDULE_EXACT_ALARM), anyInt(),
-                        eq(TEST_CALLING_UID), eq(TEST_CALLING_PACKAGE)));
+        mockExactAlarmPermissionGrant(true, false, MODE_ERRORED);
         when(mDeviceIdleInternal.isAppOnWhitelist(anyInt())).thenReturn(true);
         when(mAppStateTracker.isUidPowerSaveUserExempt(TEST_CALLING_UID)).thenReturn(true);
 
@@ -2355,10 +2327,7 @@
 
     @Test
     public void opScheduleExactAlarmRevoked() throws Exception {
-        doReturn(PermissionChecker.PERMISSION_HARD_DENIED).when(
-                () -> PermissionChecker.checkPermissionForPreflight(eq(mMockContext),
-                        eq(Manifest.permission.SCHEDULE_EXACT_ALARM), anyInt(),
-                        eq(TEST_CALLING_UID), eq(TEST_CALLING_PACKAGE)));
+        mockExactAlarmPermissionGrant(true, false, MODE_ERRORED);
         mIAppOpsCallback.opChanged(OP_SCHEDULE_EXACT_ALARM, TEST_CALLING_UID, TEST_CALLING_PACKAGE);
         assertAndHandleMessageSync(REMOVE_EXACT_ALARMS);
         verify(mService).removeExactAlarmsOnPermissionRevokedLocked(TEST_CALLING_UID,
@@ -2456,6 +2425,73 @@
         assertEquals(LazyAlarmStore.TAG, mService.mAlarmStore.getName());
     }
 
+    private void registerAppIds(String[] packages, Integer[] ids) {
+        assertEquals(packages.length, ids.length);
+
+        when(mPackageManagerInternal.getPackageUid(anyString(), anyInt(), anyInt())).thenAnswer(
+                invocation -> {
+                    final String pkg = invocation.getArgument(0);
+                    final int index = ArrayUtils.indexOf(packages, pkg);
+                    if (index < 0) {
+                        return index;
+                    }
+                    final int userId = invocation.getArgument(2);
+                    return UserHandle.getUid(userId, ids[index]);
+                });
+    }
+
+    @Test
+    public void refreshExactAlarmCandidatesOnPackageAdded() {
+        final String[] exactAlarmRequesters = new String[]{"p11", "p2", "p9"};
+        final Integer[] appIds = new Integer[]{11, 2, 9};
+        registerAppIds(exactAlarmRequesters, appIds);
+
+        when(mPermissionManagerInternal.getAppOpPermissionPackages(
+                SCHEDULE_EXACT_ALARM)).thenReturn(exactAlarmRequesters);
+
+        final Intent packageAdded = new Intent(Intent.ACTION_PACKAGE_ADDED)
+                .setPackage(TEST_CALLING_PACKAGE);
+        mPackageChangesReceiver.onReceive(mMockContext, packageAdded);
+
+        assertAndHandleMessageSync(REFRESH_EXACT_ALARM_CANDIDATES);
+        assertEquals(new ArraySet<>(appIds), mService.mExactAlarmCandidates);
+    }
+
+    @Test
+    public void refreshExactAlarmCandidatesOnPackageReplaced() {
+        final String[] exactAlarmRequesters = new String[]{"p15", "p21", "p3"};
+        final Integer[] appIds = new Integer[]{15, 21, 3};
+        registerAppIds(exactAlarmRequesters, appIds);
+
+        when(mPermissionManagerInternal.getAppOpPermissionPackages(
+                SCHEDULE_EXACT_ALARM)).thenReturn(exactAlarmRequesters);
+
+        final Intent packageAdded = new Intent(Intent.ACTION_PACKAGE_ADDED)
+                .setPackage(TEST_CALLING_PACKAGE)
+                .putExtra(Intent.EXTRA_REPLACING, true);
+        mPackageChangesReceiver.onReceive(mMockContext, packageAdded);
+
+        assertAndHandleMessageSync(REFRESH_EXACT_ALARM_CANDIDATES);
+        assertEquals(new ArraySet<>(appIds), mService.mExactAlarmCandidates);
+    }
+
+    @Test
+    public void refreshExactAlarmCandidatesOnPackageRemoved() {
+        final String[] exactAlarmRequesters = new String[]{"p99", "p1", "p19"};
+        final Integer[] appIds = new Integer[]{99, 1, 19};
+        registerAppIds(exactAlarmRequesters, appIds);
+
+        when(mPermissionManagerInternal.getAppOpPermissionPackages(
+                SCHEDULE_EXACT_ALARM)).thenReturn(exactAlarmRequesters);
+
+        final Intent packageRemoved = new Intent(Intent.ACTION_PACKAGE_REMOVED)
+                .setPackage(TEST_CALLING_PACKAGE);
+        mPackageChangesReceiver.onReceive(mMockContext, packageRemoved);
+
+        assertAndHandleMessageSync(REFRESH_EXACT_ALARM_CANDIDATES);
+        assertEquals(new ArraySet<>(appIds), mService.mExactAlarmCandidates);
+    }
+
     @After
     public void tearDown() {
         if (mMockingSession != null) {
diff --git a/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java b/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java
index 916a278..a246917 100644
--- a/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java
+++ b/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java
@@ -59,11 +59,14 @@
 import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeTrue;
 import static org.mockito.AdditionalMatchers.not;
+import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Matchers.anyInt;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
 
 import android.annotation.NonNull;
 import android.app.ActivityManager;
@@ -77,6 +80,7 @@
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
+import android.content.pm.PackageManagerInternal;
 import android.hardware.display.DisplayManager;
 import android.os.Handler;
 import android.os.Looper;
@@ -92,6 +96,7 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.internal.util.ArrayUtils;
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
 import com.android.server.usage.AppStandbyInternal.AppIdleStateChangeListener;
@@ -101,6 +106,8 @@
 import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
 
 import java.io.File;
 import java.util.ArrayList;
@@ -194,6 +201,8 @@
     }
 
     static class MyInjector extends AppStandbyController.Injector {
+        @Mock
+        private PackageManagerInternal mPackageManagerInternal;
         long mElapsedRealtime;
         boolean mIsAppIdleEnabled = true;
         boolean mIsCharging;
@@ -222,6 +231,7 @@
 
         MyInjector(Context context, Looper looper) {
             super(context, looper);
+            MockitoAnnotations.initMocks(this);
         }
 
         @Override
@@ -269,6 +279,11 @@
         }
 
         @Override
+        PackageManagerInternal getPackageManagerInternal() {
+            return mPackageManagerInternal;
+        }
+
+        @Override
         void updatePowerWhitelistCache() {
         }
 
@@ -491,6 +506,37 @@
                         mInjector.mElapsedRealtime, false));
     }
 
+    @Test
+    public void testGetIdleUidsForUser() {
+        final AppStandbyController controllerUnderTest = spy(mController);
+
+        final int userIdForTest = 325;
+        final int[] uids = new int[]{129, 23, 129, 129, 44, 23, 41, 751};
+        final boolean[] idle = new boolean[]{true, true, false, true, false, true, false, true};
+        // Based on uids[] and idle[], the only two uids that have all true's in idle[].
+        final int[] expectedIdleUids = new int[]{23, 751};
+
+        final List<ApplicationInfo> installedApps = new ArrayList<>();
+        for (int i = 0; i < uids.length; i++) {
+            final ApplicationInfo ai = mock(ApplicationInfo.class);
+            ai.uid = uids[i];
+            ai.packageName = "example.package.name." + i;
+            installedApps.add(ai);
+            when(controllerUnderTest.isAppIdleFiltered(eq(ai.packageName),
+                    eq(UserHandle.getAppId(ai.uid)), eq(userIdForTest), anyLong()))
+                    .thenReturn(idle[i]);
+        }
+        when(mInjector.mPackageManagerInternal.getInstalledApplications(anyInt(), eq(userIdForTest),
+                anyInt())).thenReturn(installedApps);
+        final int[] returnedIdleUids = controllerUnderTest.getIdleUidsForUser(userIdForTest);
+
+        assertEquals(expectedIdleUids.length, returnedIdleUids.length);
+        for (final int uid : expectedIdleUids) {
+            assertTrue("Idle uid: " + uid + " not found in result: " + Arrays.toString(
+                    returnedIdleUids), ArrayUtils.contains(returnedIdleUids, uid));
+        }
+    }
+
     private static class TestParoleListener extends AppIdleStateChangeListener {
         private boolean mIsParoleOn = false;
         private CountDownLatch mLatch;