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 393f368920b73..788bfe4a0d5d2 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 static com.android.server.job.controllers.Package.packageToString; 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.IndentingPrintWriter; 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 @@ public class PrefetchController extends StateController { */ @GuardedBy("mLock") private final SparseArrayMap 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 @@ public class PrefetchController extends StateController { 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 @@ public class PrefetchController extends StateController { 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 @@ public class PrefetchController extends StateController { return changed; } + private void maybeUpdateConstraintForUid(int uid) { + synchronized (mLock) { + final ArraySet pkgs = mService.getPackagesForUidLocked(uid); + if (pkgs == null) { + return; + } + final int userId = UserHandle.getUserId(uid); + final ArraySet 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 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 @@ public class PrefetchController extends StateController { @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 @@ public class PrefetchController extends StateController { 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 e5b2d141ba103..b17ff53b166cc 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.ComponentName; 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 @@ public class PrefetchControllerTest { private PcConstants mPcConstants; private DeviceConfig.Properties.Builder mDeviceConfigPropertiesBuilder; private EstimatedLaunchTimeChangedListener mEstimatedLaunchTimeChangedListener; + private SparseArray> mPackagesForUid = new SparseArray<>(); private MockitoSession mMockingSession; @Mock @@ -125,6 +129,10 @@ public class PrefetchControllerTest { -> mDeviceConfigPropertiesBuilder.build()) .when(() -> DeviceConfig.getProperties( eq(DeviceConfig.NAMESPACE_JOB_SCHEDULER), ArgumentMatchers.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 @@ public class PrefetchControllerTest { 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 @@ public class PrefetchControllerTest { 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 @@ public class PrefetchControllerTest { private void trackJobs(JobStatus... jobs) { for (JobStatus job : jobs) { + ArraySet 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,9 +291,45 @@ public class PrefetchControllerTest { 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); @@ -285,6 +343,7 @@ public class PrefetchControllerTest { 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 @@ public class PrefetchControllerTest { 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)); } }