diff --git a/apex/jobscheduler/framework/java/android/app/job/JobParameters.java b/apex/jobscheduler/framework/java/android/app/job/JobParameters.java index acc661e64a2ef..1429c45ef2d29 100644 --- a/apex/jobscheduler/framework/java/android/app/job/JobParameters.java +++ b/apex/jobscheduler/framework/java/android/app/job/JobParameters.java @@ -235,6 +235,14 @@ public class JobParameters implements Parcelable { public static final int STOP_REASON_USER = 13; /** The system is doing some processing that requires stopping this job. */ public static final int STOP_REASON_SYSTEM_PROCESSING = 14; + /** + * The system's estimate of when the app will be launched changed significantly enough to + * decide this job shouldn't be running right now. This will mostly apply to prefetch jobs. + * + * @see JobInfo#isPrefetch() + * @see JobInfo.Builder#setPrefetch(boolean) + */ + public static final int STOP_REASON_ESTIMATED_APP_LAUNCH_TIME_CHANGED = 15; /** @hide */ @IntDef(prefix = {"STOP_REASON_"}, value = { @@ -253,6 +261,7 @@ public class JobParameters implements Parcelable { STOP_REASON_APP_STANDBY, STOP_REASON_USER, STOP_REASON_SYSTEM_PROCESSING, + STOP_REASON_ESTIMATED_APP_LAUNCH_TIME_CHANGED, }) @Retention(RetentionPolicy.SOURCE) public @interface StopReason { diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java index 26237c41f5dc1..f73451d621ed1 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -18,6 +18,7 @@ package com.android.server.job; import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED; import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER; +import static android.text.format.DateUtils.HOUR_IN_MILLIS; import static android.text.format.DateUtils.MINUTE_IN_MILLIS; import android.annotation.NonNull; @@ -105,6 +106,7 @@ import com.android.server.job.controllers.ContentObserverController; import com.android.server.job.controllers.DeviceIdleJobsController; import com.android.server.job.controllers.IdleController; import com.android.server.job.controllers.JobStatus; +import com.android.server.job.controllers.PrefetchController; import com.android.server.job.controllers.QuotaController; import com.android.server.job.controllers.RestrictingController; import com.android.server.job.controllers.StateController; @@ -252,6 +254,8 @@ public class JobSchedulerService extends com.android.server.SystemService private final StorageController mStorageController; /** Need directly for sending uid state changes */ private final DeviceIdleJobsController mDeviceIdleJobsController; + /** Needed to get next estimated launch time. */ + private final PrefetchController mPrefetchController; /** Needed to get remaining quota time. */ private final QuotaController mQuotaController; /** Needed to get max execution time and expedited-job allowance. */ @@ -427,6 +431,9 @@ public class JobSchedulerService extends com.android.server.SystemService case Constants.KEY_CONN_PREFETCH_RELAX_FRAC: mConstants.updateConnectivityConstantsLocked(); break; + case Constants.KEY_PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS: + mConstants.updatePrefetchConstantsLocked(); + break; case Constants.KEY_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS: case Constants.KEY_RUNTIME_MIN_GUARANTEE_MS: case Constants.KEY_RUNTIME_MIN_EJ_GUARANTEE_MS: @@ -482,6 +489,8 @@ public class JobSchedulerService extends com.android.server.SystemService private static final String KEY_MIN_EXP_BACKOFF_TIME_MS = "min_exp_backoff_time_ms"; private static final String KEY_CONN_CONGESTION_DELAY_FRAC = "conn_congestion_delay_frac"; private static final String KEY_CONN_PREFETCH_RELAX_FRAC = "conn_prefetch_relax_frac"; + private static final String KEY_PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS = + "prefetch_force_batch_relax_threshold_ms"; private static final String KEY_ENABLE_API_QUOTAS = "enable_api_quotas"; private static final String KEY_API_QUOTA_SCHEDULE_COUNT = "aq_schedule_count"; private static final String KEY_API_QUOTA_SCHEDULE_WINDOW_MS = "aq_schedule_window_ms"; @@ -503,6 +512,7 @@ public class JobSchedulerService extends com.android.server.SystemService private static final long DEFAULT_MIN_EXP_BACKOFF_TIME_MS = JobInfo.MIN_BACKOFF_MILLIS; private static final float DEFAULT_CONN_CONGESTION_DELAY_FRAC = 0.5f; private static final float DEFAULT_CONN_PREFETCH_RELAX_FRAC = 0.5f; + private static final long DEFAULT_PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS = HOUR_IN_MILLIS; private static final boolean DEFAULT_ENABLE_API_QUOTAS = true; private static final int DEFAULT_API_QUOTA_SCHEDULE_COUNT = 250; private static final long DEFAULT_API_QUOTA_SCHEDULE_WINDOW_MS = MINUTE_IN_MILLIS; @@ -556,6 +566,14 @@ public class JobSchedulerService extends com.android.server.SystemService */ public float CONN_PREFETCH_RELAX_FRAC = DEFAULT_CONN_PREFETCH_RELAX_FRAC; + /** + * The amount of time within which we would consider the app to be launching relatively soon + * and will relax the force batching policy on prefetch jobs. If the app is not going to be + * launched within this amount of time from now, then we will force batch the prefetch job. + */ + public long PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS = + DEFAULT_PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS; + /** * Whether to enable quota limits on APIs. */ @@ -635,6 +653,13 @@ public class JobSchedulerService extends com.android.server.SystemService DEFAULT_CONN_PREFETCH_RELAX_FRAC); } + private void updatePrefetchConstantsLocked() { + PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS = DeviceConfig.getLong( + DeviceConfig.NAMESPACE_JOB_SCHEDULER, + KEY_PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS, + DEFAULT_PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS); + } + private void updateApiQuotaConstantsLocked() { ENABLE_API_QUOTAS = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_JOB_SCHEDULER, KEY_ENABLE_API_QUOTAS, DEFAULT_ENABLE_API_QUOTAS); @@ -700,6 +725,8 @@ public class JobSchedulerService extends com.android.server.SystemService pw.print(KEY_MIN_EXP_BACKOFF_TIME_MS, MIN_EXP_BACKOFF_TIME_MS).println(); pw.print(KEY_CONN_CONGESTION_DELAY_FRAC, CONN_CONGESTION_DELAY_FRAC).println(); pw.print(KEY_CONN_PREFETCH_RELAX_FRAC, CONN_PREFETCH_RELAX_FRAC).println(); + pw.print(KEY_PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS, + PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS).println(); pw.print(KEY_ENABLE_API_QUOTAS, ENABLE_API_QUOTAS).println(); pw.print(KEY_API_QUOTA_SCHEDULE_COUNT, API_QUOTA_SCHEDULE_COUNT).println(); @@ -1569,6 +1596,8 @@ public class JobSchedulerService extends com.android.server.SystemService mControllers.add(new ContentObserverController(this)); mDeviceIdleJobsController = new DeviceIdleJobsController(this); mControllers.add(mDeviceIdleJobsController); + mPrefetchController = new PrefetchController(this); + mControllers.add(mPrefetchController); mQuotaController = new QuotaController(this, backgroundJobsController, connectivityController); mControllers.add(mQuotaController); @@ -2325,6 +2354,15 @@ public class JobSchedulerService extends com.android.server.SystemService } else if (job.getEffectiveStandbyBucket() == RESTRICTED_INDEX) { // Restricted jobs must always be batched shouldForceBatchJob = true; + } else if (job.getJob().isPrefetch()) { + // Only relax batching on prefetch jobs if we expect the app to be launched + // relatively soon. PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS defines what + // "relatively soon" means. + final long relativelySoonCutoffTime = sSystemClock.millis() + + mConstants.PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS; + shouldForceBatchJob = + mPrefetchController.getNextEstimatedLaunchTimeLocked(job) + > relativelySoonCutoffTime; } else if (job.getNumFailures() > 0) { shouldForceBatchJob = false; } else { diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java index 3801885c9781e..d35c03dbef53f 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java @@ -93,6 +93,7 @@ public final class JobStatus { static final int CONSTRAINT_CONTENT_TRIGGER = 1<<26; static final int CONSTRAINT_DEVICE_NOT_DOZING = 1 << 25; // Implicit constraint static final int CONSTRAINT_WITHIN_QUOTA = 1 << 24; // Implicit constraint + static final int CONSTRAINT_PREFETCH = 1 << 23; static final int CONSTRAINT_BACKGROUND_NOT_RESTRICTED = 1 << 22; // Implicit constraint // The following set of dynamic constraints are for specific use cases (as explained in their @@ -147,6 +148,7 @@ public final class JobStatus { private static final int STATSD_CONSTRAINTS_TO_LOG = CONSTRAINT_CONTENT_TRIGGER | CONSTRAINT_DEADLINE | CONSTRAINT_IDLE + | CONSTRAINT_PREFETCH | CONSTRAINT_TARE_WEALTH | CONSTRAINT_TIMING_DELAY | CONSTRAINT_WITHIN_QUOTA; @@ -1175,6 +1177,11 @@ public final class JobStatus { return setConstraintSatisfied(CONSTRAINT_STORAGE_NOT_LOW, nowElapsed, state); } + /** @return true if the constraint was changed, false otherwise. */ + boolean setPrefetchConstraintSatisfied(final long nowElapsed, boolean state) { + return setConstraintSatisfied(CONSTRAINT_PREFETCH, nowElapsed, state); + } + /** @return true if the constraint was changed, false otherwise. */ boolean setTimingDelayConstraintSatisfied(final long nowElapsed, boolean state) { return setConstraintSatisfied(CONSTRAINT_TIMING_DELAY, nowElapsed, state); @@ -1387,6 +1394,9 @@ public final class JobStatus { case CONSTRAINT_DEVICE_NOT_DOZING: return JobParameters.STOP_REASON_DEVICE_STATE; + case CONSTRAINT_PREFETCH: + return JobParameters.STOP_REASON_ESTIMATED_APP_LAUNCH_TIME_CHANGED; + case CONSTRAINT_TARE_WEALTH: case CONSTRAINT_WITHIN_QUOTA: return JobParameters.STOP_REASON_QUOTA; @@ -1581,12 +1591,12 @@ public final class JobStatus { /** All constraints besides implicit and deadline. */ static final int CONSTRAINTS_OF_INTEREST = CONSTRAINT_CHARGING | CONSTRAINT_BATTERY_NOT_LOW | CONSTRAINT_STORAGE_NOT_LOW | CONSTRAINT_TIMING_DELAY | CONSTRAINT_CONNECTIVITY - | CONSTRAINT_IDLE | CONSTRAINT_CONTENT_TRIGGER; + | CONSTRAINT_IDLE | CONSTRAINT_CONTENT_TRIGGER | CONSTRAINT_PREFETCH; // Soft override covers all non-"functional" constraints static final int SOFT_OVERRIDE_CONSTRAINTS = CONSTRAINT_CHARGING | CONSTRAINT_BATTERY_NOT_LOW | CONSTRAINT_STORAGE_NOT_LOW - | CONSTRAINT_TIMING_DELAY | CONSTRAINT_IDLE; + | CONSTRAINT_TIMING_DELAY | CONSTRAINT_IDLE | CONSTRAINT_PREFETCH; /** Returns true whenever all dynamically set constraints are satisfied. */ public boolean areDynamicConstraintsSatisfied() { @@ -1775,6 +1785,9 @@ public final class JobStatus { if ((constraints&CONSTRAINT_BACKGROUND_NOT_RESTRICTED) != 0) { pw.print(" BACKGROUND_NOT_RESTRICTED"); } + if ((constraints & CONSTRAINT_PREFETCH) != 0) { + pw.print(" PREFETCH"); + } if ((constraints & CONSTRAINT_TARE_WEALTH) != 0) { pw.print(" TARE_WEALTH"); } 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 new file mode 100644 index 0000000000000..725092c328e55 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/PrefetchController.java @@ -0,0 +1,303 @@ +/* + * Copyright (C) 2021 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.job.controllers; + +import static android.text.format.DateUtils.HOUR_IN_MILLIS; + +import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; +import static com.android.server.job.JobSchedulerService.sSystemClock; + +import android.annotation.CurrentTimeMillisLong; +import android.annotation.NonNull; +import android.os.UserHandle; +import android.provider.DeviceConfig; +import android.util.ArraySet; +import android.util.IndentingPrintWriter; +import android.util.Log; +import android.util.Slog; +import android.util.SparseArrayMap; +import android.util.TimeUtils; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.JobSchedulerBackgroundThread; +import com.android.server.job.JobSchedulerService; + +import java.util.function.Predicate; + +/** + * Controller to delay prefetch jobs until we get close to an expected app launch. + */ +public class PrefetchController extends StateController { + private static final String TAG = "JobScheduler.Prefetch"; + private static final boolean DEBUG = JobSchedulerService.DEBUG + || Log.isLoggable(TAG, Log.DEBUG); + + private final PcConstants mPcConstants; + + @GuardedBy("mLock") + private final SparseArrayMap> mTrackedJobs = new SparseArrayMap<>(); + /** + * Cached set of the estimated next launch times of each app. Time are in the current time + * millis ({@link CurrentTimeMillisLong}) timebase. + */ + @GuardedBy("mLock") + private final SparseArrayMap mEstimatedLaunchTimes = new SparseArrayMap<>(); + + /** + * The cutoff point to decide if a prefetch job is worth running or not. If the app is expected + * to launch within this amount of time into the future, then we will let a prefetch job run. + */ + @GuardedBy("mLock") + @CurrentTimeMillisLong + private long mLaunchTimeThresholdMs = PcConstants.DEFAULT_LAUNCH_TIME_THRESHOLD_MS; + + public PrefetchController(JobSchedulerService service) { + super(service); + mPcConstants = new PcConstants(); + } + + @Override + @GuardedBy("mLock") + public void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob) { + if (jobStatus.getJob().isPrefetch()) { + final int userId = jobStatus.getSourceUserId(); + final String pkgName = jobStatus.getSourcePackageName(); + ArraySet jobs = mTrackedJobs.get(userId, pkgName); + if (jobs == null) { + jobs = new ArraySet<>(); + mTrackedJobs.add(userId, pkgName, jobs); + } + jobs.add(jobStatus); + updateConstraintLocked(jobStatus, + sSystemClock.millis(), sElapsedRealtimeClock.millis()); + } + } + + @Override + @GuardedBy("mLock") + public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob, + boolean forUpdate) { + final ArraySet jobs = mTrackedJobs.get(jobStatus.getSourceUserId(), + jobStatus.getSourcePackageName()); + if (jobs != null) { + jobs.remove(jobStatus); + } + } + + @Override + @GuardedBy("mLock") + public void onAppRemovedLocked(String packageName, int uid) { + if (packageName == null) { + Slog.wtf(TAG, "Told app removed but given null package name."); + return; + } + final int userId = UserHandle.getUserId(uid); + mTrackedJobs.delete(userId, packageName); + mEstimatedLaunchTimes.delete(userId, packageName); + } + + @Override + @GuardedBy("mLock") + public void onUserRemovedLocked(int userId) { + mTrackedJobs.delete(userId); + mEstimatedLaunchTimes.delete(userId); + } + + /** Return the app's next estimated launch time. */ + @GuardedBy("mLock") + @CurrentTimeMillisLong + public long getNextEstimatedLaunchTimeLocked(@NonNull JobStatus jobStatus) { + final int userId = jobStatus.getSourceUserId(); + final String pkgName = jobStatus.getSourcePackageName(); + Long nextEstimatedLaunchTime = mEstimatedLaunchTimes.get(userId, pkgName); + final long now = sSystemClock.millis(); + if (nextEstimatedLaunchTime == null || nextEstimatedLaunchTime < now) { + // TODO(194532703): get estimated time from UsageStats + nextEstimatedLaunchTime = now + 2 * HOUR_IN_MILLIS; + mEstimatedLaunchTimes.add(userId, pkgName, nextEstimatedLaunchTime); + } + return nextEstimatedLaunchTime; + } + + @GuardedBy("mLock") + private boolean maybeUpdateConstraintForPkgLocked(long now, long nowElapsed, int userId, + String pkgName) { + final ArraySet jobs = mTrackedJobs.get(userId, pkgName); + if (jobs == null) { + return false; + } + boolean changed = false; + for (int i = 0; i < jobs.size(); i++) { + final JobStatus js = jobs.valueAt(i); + changed |= updateConstraintLocked(js, now, nowElapsed); + } + return changed; + } + + @GuardedBy("mLock") + private boolean updateConstraintLocked(@NonNull JobStatus jobStatus, long now, + long nowElapsed) { + return jobStatus.setPrefetchConstraintSatisfied(nowElapsed, + getNextEstimatedLaunchTimeLocked(jobStatus) <= now + mLaunchTimeThresholdMs); + } + + @Override + @GuardedBy("mLock") + public void prepareForUpdatedConstantsLocked() { + mPcConstants.mShouldReevaluateConstraints = false; + } + + @Override + @GuardedBy("mLock") + public void processConstantLocked(DeviceConfig.Properties properties, String key) { + mPcConstants.processConstantLocked(properties, key); + } + + @Override + @GuardedBy("mLock") + public void onConstantsUpdatedLocked() { + if (mPcConstants.mShouldReevaluateConstraints) { + // Update job bookkeeping out of band. + JobSchedulerBackgroundThread.getHandler().post(() -> { + final ArraySet changedJobs = new ArraySet<>(); + synchronized (mLock) { + final long nowElapsed = sElapsedRealtimeClock.millis(); + final long now = sSystemClock.millis(); + for (int u = 0; u < mTrackedJobs.numMaps(); ++u) { + final int userId = mTrackedJobs.keyAt(u); + for (int p = 0; p < mTrackedJobs.numElementsForKey(userId); ++p) { + final String packageName = mTrackedJobs.keyAt(u, p); + if (maybeUpdateConstraintForPkgLocked( + now, nowElapsed, userId, packageName)) { + changedJobs.addAll(mTrackedJobs.valueAt(u, p)); + } + } + } + } + if (changedJobs.size() > 0) { + mStateChangedListener.onControllerStateChanged(changedJobs); + } + }); + } + } + + @VisibleForTesting + class PcConstants { + private boolean mShouldReevaluateConstraints = false; + + /** Prefix to use with all constant keys in order to "sub-namespace" the keys. */ + private static final String PC_CONSTANT_PREFIX = "pc_"; + + @VisibleForTesting + static final String KEY_LAUNCH_TIME_THRESHOLD_MS = + PC_CONSTANT_PREFIX + "launch_time_threshold_ms"; + + private static final long DEFAULT_LAUNCH_TIME_THRESHOLD_MS = 7 * HOUR_IN_MILLIS; + + /** How much time each app will have to run jobs within their standby bucket window. */ + public long LAUNCH_TIME_THRESHOLD_MS = DEFAULT_LAUNCH_TIME_THRESHOLD_MS; + + @GuardedBy("mLock") + public void processConstantLocked(@NonNull DeviceConfig.Properties properties, + @NonNull String key) { + switch (key) { + case KEY_LAUNCH_TIME_THRESHOLD_MS: + LAUNCH_TIME_THRESHOLD_MS = + properties.getLong(key, DEFAULT_LAUNCH_TIME_THRESHOLD_MS); + // Limit the threshold to the range [1, 24] hours. + long newLaunchTimeThresholdMs = Math.min(24 * HOUR_IN_MILLIS, + Math.max(HOUR_IN_MILLIS, LAUNCH_TIME_THRESHOLD_MS)); + if (mLaunchTimeThresholdMs != newLaunchTimeThresholdMs) { + mLaunchTimeThresholdMs = newLaunchTimeThresholdMs; + mShouldReevaluateConstraints = true; + } + break; + } + } + + private void dump(IndentingPrintWriter pw) { + pw.println(); + pw.print(PrefetchController.class.getSimpleName()); + pw.println(":"); + pw.increaseIndent(); + + pw.print(KEY_LAUNCH_TIME_THRESHOLD_MS, LAUNCH_TIME_THRESHOLD_MS).println(); + + pw.decreaseIndent(); + } + } + + //////////////////////// TESTING HELPERS ///////////////////////////// + + @VisibleForTesting + long getLaunchTimeThresholdMs() { + return mLaunchTimeThresholdMs; + } + + @VisibleForTesting + @NonNull + PcConstants getPcConstants() { + return mPcConstants; + } + + //////////////////////////// DATA DUMP ////////////////////////////// + + @Override + @GuardedBy("mLock") + public void dumpControllerStateLocked(IndentingPrintWriter pw, Predicate predicate) { + final long now = sSystemClock.millis(); + + pw.println("Cached launch times:"); + pw.increaseIndent(); + for (int u = 0; u < mEstimatedLaunchTimes.numMaps(); ++u) { + final int userId = mEstimatedLaunchTimes.keyAt(u); + for (int p = 0; p < mEstimatedLaunchTimes.numElementsForKey(userId); ++p) { + final String pkgName = mEstimatedLaunchTimes.keyAt(u, p); + final long estimatedLaunchTime = mEstimatedLaunchTimes.valueAt(u, p); + + pw.print("<" + userId + ">" + pkgName + ": "); + pw.print(estimatedLaunchTime); + pw.print(" ("); + TimeUtils.formatDuration(estimatedLaunchTime - now, pw, + TimeUtils.HUNDRED_DAY_FIELD_LEN); + pw.println(" from now)"); + } + } + pw.decreaseIndent(); + + pw.println(); + mTrackedJobs.forEach((jobs) -> { + for (int j = 0; j < jobs.size(); j++) { + final JobStatus js = jobs.valueAt(j); + if (!predicate.test(js)) { + continue; + } + pw.print("#"); + js.printUniqueId(pw); + pw.print(" from "); + UserHandle.formatUid(pw, js.getSourceUid()); + pw.println(); + } + }); + } + + @Override + public void dumpConstants(IndentingPrintWriter pw) { + mPcConstants.dump(pw); + } +} diff --git a/core/api/current.txt b/core/api/current.txt index da37669e60878..292fd4ca5d81a 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -8053,6 +8053,7 @@ package android.app.job { field public static final int STOP_REASON_CONSTRAINT_DEVICE_IDLE = 8; // 0x8 field public static final int STOP_REASON_CONSTRAINT_STORAGE_NOT_LOW = 9; // 0x9 field public static final int STOP_REASON_DEVICE_STATE = 4; // 0x4 + field public static final int STOP_REASON_ESTIMATED_APP_LAUNCH_TIME_CHANGED = 15; // 0xf field public static final int STOP_REASON_PREEMPT = 2; // 0x2 field public static final int STOP_REASON_QUOTA = 10; // 0xa field public static final int STOP_REASON_SYSTEM_PROCESSING = 14; // 0xe 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 new file mode 100644 index 0000000000000..98e089e0ab947 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java @@ -0,0 +1,150 @@ +/* + * 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.job.controllers; + +import static android.text.format.DateUtils.HOUR_IN_MILLIS; +import static android.text.format.DateUtils.MINUTE_IN_MILLIS; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.eq; + +import android.content.Context; +import android.os.SystemClock; +import android.provider.DeviceConfig; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.server.LocalServices; +import com.android.server.job.JobSchedulerService; +import com.android.server.job.controllers.PrefetchController.PcConstants; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentMatchers; +import org.mockito.Mock; +import org.mockito.MockitoSession; +import org.mockito.quality.Strictness; +import org.mockito.stubbing.Answer; + +import java.time.Clock; +import java.time.Duration; +import java.time.ZoneOffset; +import java.util.concurrent.Executor; + +@RunWith(AndroidJUnit4.class) +public class PrefetchControllerTest { + private PrefetchController mPrefetchController; + private PcConstants mPcConstants; + private DeviceConfig.Properties.Builder mDeviceConfigPropertiesBuilder; + + private MockitoSession mMockingSession; + @Mock + private Context mContext; + @Mock + private JobSchedulerService mJobSchedulerService; + + @Before + public void setUp() { + mMockingSession = mockitoSession() + .initMocks(this) + .strictness(Strictness.LENIENT) + .spyStatic(DeviceConfig.class) + .mockStatic(LocalServices.class) + .startMocking(); + + // Called in StateController constructor. + when(mJobSchedulerService.getTestableContext()).thenReturn(mContext); + when(mJobSchedulerService.getLock()).thenReturn(mJobSchedulerService); + // Used in PrefetchController.PcConstants + doAnswer((Answer) invocationOnMock -> null) + .when(() -> DeviceConfig.addOnPropertiesChangedListener( + anyString(), any(Executor.class), + any(DeviceConfig.OnPropertiesChangedListener.class))); + mDeviceConfigPropertiesBuilder = + new DeviceConfig.Properties.Builder(DeviceConfig.NAMESPACE_JOB_SCHEDULER); + doAnswer( + (Answer) invocationOnMock + -> mDeviceConfigPropertiesBuilder.build()) + .when(() -> DeviceConfig.getProperties( + eq(DeviceConfig.NAMESPACE_JOB_SCHEDULER), ArgumentMatchers.any())); + + // 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 + // causes sessions with negative timestamps, they will fail. + JobSchedulerService.sSystemClock = + getShiftedClock(Clock.fixed(Clock.systemUTC().instant(), ZoneOffset.UTC), + 24 * HOUR_IN_MILLIS); + JobSchedulerService.sUptimeMillisClock = getShiftedClock( + Clock.fixed(SystemClock.uptimeClock().instant(), ZoneOffset.UTC), + 24 * HOUR_IN_MILLIS); + JobSchedulerService.sElapsedRealtimeClock = getShiftedClock( + Clock.fixed(SystemClock.elapsedRealtimeClock().instant(), ZoneOffset.UTC), + 24 * HOUR_IN_MILLIS); + + // Initialize real objects. + // Capture the listeners. + mPrefetchController = new PrefetchController(mJobSchedulerService); + mPcConstants = mPrefetchController.getPcConstants(); + } + + @After + public void tearDown() { + if (mMockingSession != null) { + mMockingSession.finishMocking(); + } + } + + private Clock getShiftedClock(Clock clock, long incrementMs) { + return Clock.offset(clock, Duration.ofMillis(incrementMs)); + } + + private void setDeviceConfigLong(String key, long val) { + mDeviceConfigPropertiesBuilder.setLong(key, val); + synchronized (mPrefetchController.mLock) { + mPrefetchController.prepareForUpdatedConstantsLocked(); + mPcConstants.processConstantLocked(mDeviceConfigPropertiesBuilder.build(), key); + } + } + + @Test + public void testConstantsUpdating_ValidValues() { + setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_THRESHOLD_MS, 5 * HOUR_IN_MILLIS); + + assertEquals(5 * HOUR_IN_MILLIS, mPrefetchController.getLaunchTimeThresholdMs()); + } + + @Test + public void testConstantsUpdating_InvalidValues() { + // Test negatives/too low. + setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_THRESHOLD_MS, 4 * MINUTE_IN_MILLIS); + + assertEquals(HOUR_IN_MILLIS, mPrefetchController.getLaunchTimeThresholdMs()); + + // Test larger than a day. Controller should cap at one day. + setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_THRESHOLD_MS, 25 * HOUR_IN_MILLIS); + + assertEquals(24 * HOUR_IN_MILLIS, mPrefetchController.getLaunchTimeThresholdMs()); + } +}