Merge "Add basic prefetch job handling."

This commit is contained in:
TreeHugger Robot
2021-10-05 20:17:14 +00:00
committed by Android (Google) Code Review
6 changed files with 516 additions and 2 deletions

View File

@@ -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 {

View File

@@ -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();
@@ -1577,6 +1604,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);
@@ -2333,6 +2362,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 {

View File

@@ -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");
}

View File

@@ -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<String, ArraySet<JobStatus>> 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<String, Long> 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<JobStatus> 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<JobStatus> 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<JobStatus> 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<JobStatus> 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<JobStatus> 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);
}
}

View File

@@ -8055,6 +8055,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

View File

@@ -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<Void>) invocationOnMock -> null)
.when(() -> DeviceConfig.addOnPropertiesChangedListener(
anyString(), any(Executor.class),
any(DeviceConfig.OnPropertiesChangedListener.class)));
mDeviceConfigPropertiesBuilder =
new DeviceConfig.Properties.Builder(DeviceConfig.NAMESPACE_JOB_SCHEDULER);
doAnswer(
(Answer<DeviceConfig.Properties>) invocationOnMock
-> mDeviceConfigPropertiesBuilder.build())
.when(() -> DeviceConfig.getProperties(
eq(DeviceConfig.NAMESPACE_JOB_SCHEDULER), ArgumentMatchers.<String>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());
}
}