Don't start prefetch jobs for TOP apps.

If a prefetch job hasn't started by the time an app is opened, then we
decide not to run it until the app is closed again. The rationale being
that "prefetch" is to do something before the app launch and it wouldn't
be useful while the app is on TOP. Any prefetch job that is already
running when the app is launched will be allowed to finish.

Bug: 194532703
Test: atest FrameworksMockingServicesTests:PrefetchControllerTest
Change-Id: I485cd0170a0066a82ab66bf92e36206a9a78bd78
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/PrefetchController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/PrefetchController.java
index 393f368..788bfe4 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/PrefetchController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/PrefetchController.java
@@ -25,6 +25,7 @@
 import android.annotation.CurrentTimeMillisLong;
 import android.annotation.ElapsedRealtimeLong;
 import android.annotation.NonNull;
+import android.app.job.JobInfo;
 import android.app.usage.UsageStatsManagerInternal;
 import android.app.usage.UsageStatsManagerInternal.EstimatedLaunchTimeChangedListener;
 import android.content.Context;
@@ -38,6 +39,7 @@
 import android.util.Log;
 import android.util.Slog;
 import android.util.SparseArrayMap;
+import android.util.SparseBooleanArray;
 import android.util.TimeUtils;
 
 import com.android.internal.annotations.GuardedBy;
@@ -71,6 +73,9 @@
      */
     @GuardedBy("mLock")
     private final SparseArrayMap<String, Long> mEstimatedLaunchTimes = new SparseArrayMap<>();
+    /** Cached list of UIDs in the TOP state. */
+    @GuardedBy("mLock")
+    private final SparseBooleanArray mTopUids = new SparseBooleanArray();
     private final ThresholdAlarmListener mThresholdAlarmListener;
 
     /**
@@ -98,6 +103,7 @@
 
     private static final int MSG_RETRIEVE_ESTIMATED_LAUNCH_TIME = 0;
     private static final int MSG_PROCESS_UPDATED_ESTIMATED_LAUNCH_TIME = 1;
+    private static final int MSG_PROCESS_TOP_STATE_CHANGE = 2;
 
     public PrefetchController(JobSchedulerService service) {
         super(service);
@@ -165,6 +171,22 @@
         mThresholdAlarmListener.removeAlarmsForUserId(userId);
     }
 
+    @GuardedBy("mLock")
+    @Override
+    public void onUidBiasChangedLocked(int uid, int newBias) {
+        final boolean isNowTop = newBias == JobInfo.BIAS_TOP_APP;
+        final boolean wasTop = mTopUids.get(uid);
+        if (isNowTop) {
+            mTopUids.put(uid, true);
+        } else {
+            // Delete entries of non-top apps so the set doesn't get too large.
+            mTopUids.delete(uid);
+        }
+        if (isNowTop != wasTop) {
+            mHandler.obtainMessage(MSG_PROCESS_TOP_STATE_CHANGE, uid, 0).sendToTarget();
+        }
+    }
+
     /** Return the app's next estimated launch time. */
     @GuardedBy("mLock")
     @CurrentTimeMillisLong
@@ -205,6 +227,35 @@
         return changed;
     }
 
+    private void maybeUpdateConstraintForUid(int uid) {
+        synchronized (mLock) {
+            final ArraySet<String> pkgs = mService.getPackagesForUidLocked(uid);
+            if (pkgs == null) {
+                return;
+            }
+            final int userId = UserHandle.getUserId(uid);
+            final ArraySet<JobStatus> changedJobs = new ArraySet<>();
+            final long now = sSystemClock.millis();
+            final long nowElapsed = sElapsedRealtimeClock.millis();
+            for (int p = pkgs.size() - 1; p >= 0; --p) {
+                final String pkgName = pkgs.valueAt(p);
+                final ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, pkgName);
+                if (jobs == null) {
+                    continue;
+                }
+                for (int i = 0; i < jobs.size(); i++) {
+                    final JobStatus js = jobs.valueAt(i);
+                    if (updateConstraintLocked(js, now, nowElapsed)) {
+                        changedJobs.add(js);
+                    }
+                }
+            }
+            if (changedJobs.size() > 0) {
+                mStateChangedListener.onControllerStateChanged(changedJobs);
+            }
+        }
+    }
+
     private void processUpdatedEstimatedLaunchTime(int userId, @NonNull String pkgName,
             @CurrentTimeMillisLong long newEstimatedLaunchTime) {
         if (DEBUG) {
@@ -244,9 +295,18 @@
     @GuardedBy("mLock")
     private boolean updateConstraintLocked(@NonNull JobStatus jobStatus,
             @CurrentTimeMillisLong long now, @ElapsedRealtimeLong long nowElapsed) {
-        return jobStatus.setPrefetchConstraintSatisfied(nowElapsed,
-                willBeLaunchedSoonLocked(
-                        jobStatus.getSourceUserId(), jobStatus.getSourcePackageName(), now));
+        // Mark a prefetch constraint as satisfied in the following scenarios:
+        //   1. The app is not open but it will be launched soon
+        //   2. The app is open and the job is already running (so we let it finish)
+        final boolean appIsOpen = mTopUids.get(jobStatus.getSourceUid());
+        final boolean satisfied;
+        if (!appIsOpen) {
+            satisfied = willBeLaunchedSoonLocked(
+                    jobStatus.getSourceUserId(), jobStatus.getSourcePackageName(), now);
+        } else {
+            satisfied = mService.isCurrentlyRunningLocked(jobStatus);
+        }
+        return jobStatus.setPrefetchConstraintSatisfied(nowElapsed, satisfied);
     }
 
     @GuardedBy("mLock")
@@ -399,6 +459,11 @@
                     processUpdatedEstimatedLaunchTime(args.argi1, (String) args.arg1, args.argl1);
                     args.recycle();
                     break;
+
+                case MSG_PROCESS_TOP_STATE_CHANGE:
+                    final int uid = msg.arg1;
+                    maybeUpdateConstraintForUid(uid);
+                    break;
             }
         }
     }
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java
index e5b2d14..b17ff53b 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java
@@ -47,8 +47,11 @@
 import android.content.Context;
 import android.content.pm.ServiceInfo;
 import android.os.Looper;
+import android.os.Process;
 import android.os.SystemClock;
 import android.provider.DeviceConfig;
+import android.util.ArraySet;
+import android.util.SparseArray;
 
 import androidx.test.runner.AndroidJUnit4;
 
@@ -85,6 +88,7 @@
     private PcConstants mPcConstants;
     private DeviceConfig.Properties.Builder mDeviceConfigPropertiesBuilder;
     private EstimatedLaunchTimeChangedListener mEstimatedLaunchTimeChangedListener;
+    private SparseArray<ArraySet<String>> mPackagesForUid = new SparseArray<>();
 
     private MockitoSession mMockingSession;
     @Mock
@@ -125,6 +129,10 @@
                         -> mDeviceConfigPropertiesBuilder.build())
                 .when(() -> DeviceConfig.getProperties(
                         eq(DeviceConfig.NAMESPACE_JOB_SCHEDULER), ArgumentMatchers.<String>any()));
+        // Used in PrefetchController.maybeUpdateConstraintForUid
+        when(mJobSchedulerService.getPackagesForUidLocked(anyInt()))
+                .thenAnswer(invocationOnMock
+                        -> mPackagesForUid.get(invocationOnMock.getArgument(0)));
 
         // Freeze the clocks at 24 hours after this moment in time. Several tests create sessions
         // in the past, and PrefetchController sometimes floors values at 0, so if the test time
@@ -146,6 +154,8 @@
         mPrefetchController = new PrefetchController(mJobSchedulerService);
         mPcConstants = mPrefetchController.getPcConstants();
 
+        setUidBias(Process.myUid(), JobInfo.BIAS_DEFAULT);
+
         verify(mUsageStatsManagerInternal)
                 .registerLaunchTimeChangedListener(eltListenerCaptor.capture());
         mEstimatedLaunchTimeChangedListener = eltListenerCaptor.getValue();
@@ -185,6 +195,12 @@
         return Clock.offset(clock, Duration.ofMillis(incrementMs));
     }
 
+    private void setUidBias(int uid, int bias) {
+        synchronized (mPrefetchController.mLock) {
+            mPrefetchController.onUidBiasChangedLocked(uid, bias);
+        }
+    }
+
     private void setDeviceConfigLong(String key, long val) {
         mDeviceConfigPropertiesBuilder.setLong(key, val);
         synchronized (mPrefetchController.mLock) {
@@ -196,6 +212,12 @@
 
     private void trackJobs(JobStatus... jobs) {
         for (JobStatus job : jobs) {
+            ArraySet<String> pkgs = mPackagesForUid.get(job.getSourceUid());
+            if (pkgs == null) {
+                pkgs = new ArraySet<>();
+                mPackagesForUid.put(job.getSourceUid(), pkgs);
+            }
+            pkgs.add(job.getSourcePackageName());
             synchronized (mPrefetchController.mLock) {
                 mPrefetchController.maybeStartTrackingJobLocked(job, null);
             }
@@ -269,10 +291,46 @@
         trackJobs(job);
         verify(mUsageStatsManagerInternal, timeout(DEFAULT_WAIT_MS))
                 .getEstimatedPackageLaunchTime(SOURCE_PACKAGE, SOURCE_USER_ID);
+        verify(mJobSchedulerService, timeout(DEFAULT_WAIT_MS)).onControllerStateChanged(any());
         assertTrue(job.isConstraintSatisfied(JobStatus.CONSTRAINT_PREFETCH));
     }
 
     @Test
+    public void testConstraintSatisfiedWhenTop() {
+        final JobStatus jobPending = createJobStatus("testConstraintSatisfiedWhenTop", 1);
+        final JobStatus jobRunning = createJobStatus("testConstraintSatisfiedWhenTop", 2);
+        final int uid = jobPending.getSourceUid();
+
+        when(mJobSchedulerService.isCurrentlyRunningLocked(jobPending)).thenReturn(false);
+        when(mJobSchedulerService.isCurrentlyRunningLocked(jobRunning)).thenReturn(true);
+
+        InOrder inOrder = inOrder(mJobSchedulerService);
+
+        when(mUsageStatsManagerInternal
+                .getEstimatedPackageLaunchTime(SOURCE_PACKAGE, SOURCE_USER_ID))
+                .thenReturn(sSystemClock.millis() + 10 * MINUTE_IN_MILLIS);
+        trackJobs(jobPending, jobRunning);
+        verify(mUsageStatsManagerInternal, timeout(DEFAULT_WAIT_MS))
+                .getEstimatedPackageLaunchTime(SOURCE_PACKAGE, SOURCE_USER_ID);
+        inOrder.verify(mJobSchedulerService, timeout(DEFAULT_WAIT_MS))
+                .onControllerStateChanged(any());
+        assertTrue(jobPending.isConstraintSatisfied(JobStatus.CONSTRAINT_PREFETCH));
+        assertTrue(jobRunning.isConstraintSatisfied(JobStatus.CONSTRAINT_PREFETCH));
+        setUidBias(uid, JobInfo.BIAS_TOP_APP);
+        // Processing happens on the handler, so wait until we're sure the change has been processed
+        inOrder.verify(mJobSchedulerService, timeout(DEFAULT_WAIT_MS))
+                .onControllerStateChanged(any());
+        // Already running job should continue but pending job must wait.
+        assertFalse(jobPending.isConstraintSatisfied(JobStatus.CONSTRAINT_PREFETCH));
+        assertTrue(jobRunning.isConstraintSatisfied(JobStatus.CONSTRAINT_PREFETCH));
+        setUidBias(uid, JobInfo.BIAS_DEFAULT);
+        inOrder.verify(mJobSchedulerService, timeout(DEFAULT_WAIT_MS))
+                .onControllerStateChanged(any());
+        assertTrue(jobPending.isConstraintSatisfied(JobStatus.CONSTRAINT_PREFETCH));
+        assertTrue(jobRunning.isConstraintSatisfied(JobStatus.CONSTRAINT_PREFETCH));
+    }
+
+    @Test
     public void testEstimatedLaunchTimeChangedToLate() {
         setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_THRESHOLD_MS, 7 * HOUR_IN_MILLIS);
         when(mUsageStatsManagerInternal
@@ -285,6 +343,7 @@
         trackJobs(jobStatus);
         inOrder.verify(mUsageStatsManagerInternal, timeout(DEFAULT_WAIT_MS))
                 .getEstimatedPackageLaunchTime(SOURCE_PACKAGE, SOURCE_USER_ID);
+        verify(mJobSchedulerService, timeout(DEFAULT_WAIT_MS)).onControllerStateChanged(any());
         assertTrue(jobStatus.isConstraintSatisfied(JobStatus.CONSTRAINT_PREFETCH));
 
         mEstimatedLaunchTimeChangedListener.onEstimatedLaunchTimeChanged(SOURCE_USER_ID,
@@ -315,6 +374,7 @@
 
         inOrder.verify(mUsageStatsManagerInternal, timeout(DEFAULT_WAIT_MS).times(0))
                 .getEstimatedPackageLaunchTime(SOURCE_PACKAGE, SOURCE_USER_ID);
+        verify(mJobSchedulerService, timeout(DEFAULT_WAIT_MS)).onControllerStateChanged(any());
         assertTrue(jobStatus.isConstraintSatisfied(JobStatus.CONSTRAINT_PREFETCH));
     }
 }