Merge "Adds Flexibility Controller and its supporting JobStatus functions."
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java
new file mode 100644
index 0000000..687693c
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java
@@ -0,0 +1,312 @@
+/*
+ * Copyright (C) 2022 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.server.job.JobSchedulerService.sElapsedRealtimeClock;
+
+import android.annotation.ElapsedRealtimeLong;
+import android.annotation.NonNull;
+import android.app.job.JobInfo;
+import android.content.Context;
+import android.os.Looper;
+import android.os.UserHandle;
+import android.util.ArraySet;
+import android.util.IndentingPrintWriter;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.JobSchedulerBackgroundThread;
+import com.android.server.job.JobSchedulerService;
+import com.android.server.utils.AlarmQueue;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Predicate;
+
+/**
+ * Controller that tracks the number of flexible constraints being actively satisfied.
+ * Drops constraint for TOP apps and lowers number of required constraints with time.
+ *
+ * TODO: Plug in to other controllers (b/239047584), handle prefetch (b/238887951)
+ */
+public final class FlexibilityController extends StateController {
+ /**
+ * List of all potential flexible constraints
+ */
+ @VisibleForTesting
+ static final int FLEXIBLE_CONSTRAINTS = JobStatus.CONSTRAINT_BATTERY_NOT_LOW
+ | JobStatus.CONSTRAINT_CHARGING
+ | JobStatus.CONSTRAINT_CONNECTIVITY
+ | JobStatus.CONSTRAINT_IDLE;
+
+ /** Hard cutoff to remove flexible constraints */
+ private static final long DEADLINE_PROXIMITY_LIMIT_MS = 15 * MINUTE_IN_MILLIS;
+
+ /**
+ * Keeps track of what flexible constraints are satisfied at the moment.
+ * Is updated by the other controllers.
+ */
+ private int mSatisfiedFlexibleConstraints;
+
+ @VisibleForTesting
+ @GuardedBy("mLock")
+ final FlexibilityTracker mFlexibilityTracker;
+
+ private final FlexibilityAlarmQueue mFlexibilityAlarmQueue;
+ private final long mMinTimeBetweenAlarmsMs = MINUTE_IN_MILLIS;
+
+ /**
+ * The percent of a Jobs lifecycle to drop number of required constraints.
+ * mPercentToDropConstraints[i] denotes that at x% of a Jobs lifecycle,
+ * the controller should have i+1 constraints dropped.
+ */
+ private final int[] mPercentToDropConstraints = {50, 60, 70, 80};
+
+ /** The default deadline that all flexible constraints should be dropped by. */
+ private final long mDefaultFlexibleDeadline = 72 * HOUR_IN_MILLIS;
+
+ public FlexibilityController(JobSchedulerService service) {
+ super(service);
+ mFlexibilityTracker = new FlexibilityTracker(FLEXIBLE_CONSTRAINTS);
+ mFlexibilityAlarmQueue = new FlexibilityAlarmQueue(
+ mContext, JobSchedulerBackgroundThread.get().getLooper());
+ }
+
+ /**
+ * StateController interface
+ */
+ @Override
+ public void maybeStartTrackingJobLocked(JobStatus js, JobStatus lastJob) {
+ if (js.hasFlexibilityConstraint()) {
+ mFlexibilityTracker.add(js);
+ js.setTrackingController(JobStatus.TRACKING_FLEXIBILITY);
+ final long nowElapsed = sElapsedRealtimeClock.millis();
+ js.setFlexibilityConstraintSatisfied(nowElapsed, isFlexibilitySatisfiedLocked(js));
+ mFlexibilityAlarmQueue.addAlarm(js, getNextConstraintDropTimeElapsed(js));
+ }
+ }
+
+ @Override
+ public void maybeStopTrackingJobLocked(JobStatus js, JobStatus incomingJob, boolean forUpdate) {
+ if (js.clearTrackingController(JobStatus.TRACKING_FLEXIBILITY)) {
+ mFlexibilityAlarmQueue.removeAlarmForKey(js);
+ mFlexibilityTracker.remove(js);
+ }
+ }
+
+ /** Checks if the flexibility constraint is actively satisfied for a given job. */
+ @VisibleForTesting
+ boolean isFlexibilitySatisfiedLocked(JobStatus js) {
+ synchronized (mLock) {
+ return mService.getUidBias(js.getUid()) == JobInfo.BIAS_TOP_APP
+ || mService.isCurrentlyRunningLocked(js)
+ || getNumSatisfiedRequiredConstraintsLocked(js)
+ >= js.getNumRequiredFlexibleConstraints();
+ }
+ }
+
+ @VisibleForTesting
+ @GuardedBy("mLock")
+ int getNumSatisfiedRequiredConstraintsLocked(JobStatus js) {
+ return Integer.bitCount(js.getFlexibleConstraints() & mSatisfiedFlexibleConstraints);
+ }
+
+ /**
+ * Sets the controller's constraint to a given state.
+ * Changes flexibility constraint satisfaction for affected jobs.
+ */
+ @VisibleForTesting
+ void setConstraintSatisfied(int constraint, boolean state) {
+ synchronized (mLock) {
+ final boolean old = (mSatisfiedFlexibleConstraints & constraint) != 0;
+ if (old == state) {
+ return;
+ }
+
+ final int prevSatisfied = Integer.bitCount(mSatisfiedFlexibleConstraints);
+ mSatisfiedFlexibleConstraints =
+ (mSatisfiedFlexibleConstraints & ~constraint) | (state ? constraint : 0);
+ final int curSatisfied = Integer.bitCount(mSatisfiedFlexibleConstraints);
+
+ // Only the max of the number of required flexible constraints will need to be updated
+ // The rest did not have a change in state and are still satisfied or unsatisfied.
+ final int numConstraintsToUpdate = Math.max(curSatisfied, prevSatisfied);
+
+ final ArraySet<JobStatus> jobs = mFlexibilityTracker.getJobsByNumRequiredConstraints(
+ numConstraintsToUpdate);
+ final long nowElapsed = sElapsedRealtimeClock.millis();
+
+ for (int i = 0; i < jobs.size(); i++) {
+ JobStatus js = jobs.valueAt(i);
+ js.setFlexibilityConstraintSatisfied(nowElapsed, isFlexibilitySatisfiedLocked(js));
+ }
+ }
+ }
+
+ /** Checks if the given constraint is satisfied in the flexibility controller. */
+ @VisibleForTesting
+ boolean isConstraintSatisfied(int constraint) {
+ return (mSatisfiedFlexibleConstraints & constraint) != 0;
+ }
+
+ /** The elapsed time that marks when the next constraint should be dropped. */
+ @VisibleForTesting
+ @ElapsedRealtimeLong
+ long getNextConstraintDropTimeElapsed(JobStatus js) {
+ final long earliest = js.getEarliestRunTime() == JobStatus.NO_EARLIEST_RUNTIME
+ ? js.enqueueTime : js.getEarliestRunTime();
+ final long latest = js.getLatestRunTimeElapsed() == JobStatus.NO_LATEST_RUNTIME
+ ? earliest + mDefaultFlexibleDeadline
+ : js.getLatestRunTimeElapsed();
+ final int percent = mPercentToDropConstraints[js.getNumDroppedFlexibleConstraints()];
+ final long percentInTime = ((latest - earliest) * percent) / 100;
+ return earliest + percentInTime;
+ }
+
+ @Override
+ @GuardedBy("mLock")
+ public void onUidBiasChangedLocked(int uid, int prevBias, int newBias) {
+ if (prevBias != JobInfo.BIAS_TOP_APP && newBias != JobInfo.BIAS_TOP_APP) {
+ return;
+ }
+ final long nowElapsed = sElapsedRealtimeClock.millis();
+ List<JobStatus> jobsByUid = mService.getJobStore().getJobsByUid(uid);
+ for (int i = 0; i < jobsByUid.size(); i++) {
+ JobStatus js = jobsByUid.get(i);
+ if (js.hasFlexibilityConstraint()) {
+ js.setFlexibilityConstraintSatisfied(nowElapsed, isFlexibilitySatisfiedLocked(js));
+ }
+ }
+ }
+
+ @VisibleForTesting
+ class FlexibilityTracker {
+ final ArrayList<ArraySet<JobStatus>> mTrackedJobs;
+
+ FlexibilityTracker(int flexibleConstraints) {
+ mTrackedJobs = new ArrayList<>();
+ int numFlexibleConstraints = Integer.bitCount(flexibleConstraints);
+ for (int i = 0; i <= numFlexibleConstraints; i++) {
+ mTrackedJobs.add(new ArraySet<JobStatus>());
+ }
+ }
+
+ /** Gets every tracked job with a given number of required constraints. */
+ public ArraySet<JobStatus> getJobsByNumRequiredConstraints(int numRequired) {
+ return mTrackedJobs.get(numRequired - 1);
+ }
+
+ /** adds a JobStatus object based on number of required flexible constraints. */
+ public void add(JobStatus js) {
+ if (js.getNumRequiredFlexibleConstraints() <= 0) {
+ return;
+ }
+ mTrackedJobs.get(js.getNumRequiredFlexibleConstraints() - 1).add(js);
+ }
+
+ /** Removes a JobStatus object. */
+ public void remove(JobStatus js) {
+ if (js.getNumRequiredFlexibleConstraints() == 0) {
+ return;
+ }
+ mTrackedJobs.get(js.getNumRequiredFlexibleConstraints() - 1).remove(js);
+ }
+
+ /** Returns all tracked jobs. */
+ public ArrayList<ArraySet<JobStatus>> getArrayList() {
+ return mTrackedJobs;
+ }
+
+ /**
+ * Adjusts number of required flexible constraints and sorts it into the tracker.
+ * Returns false if the job status's number of flexible constraints is now 0.
+ * Jobs with 0 required flexible constraints are removed from the tracker.
+ */
+ public boolean adjustJobsRequiredConstraints(JobStatus js, int n) {
+ remove(js);
+ js.adjustNumRequiredFlexibleConstraints(n);
+ final long nowElapsed = sElapsedRealtimeClock.millis();
+ js.setFlexibilityConstraintSatisfied(nowElapsed, isFlexibilitySatisfiedLocked(js));
+ if (js.getNumRequiredFlexibleConstraints() <= 0) {
+ maybeStopTrackingJobLocked(js, null, false);
+ return false;
+ }
+ add(js);
+ return true;
+ }
+
+ public void dump(IndentingPrintWriter pw, Predicate<JobStatus> predicate) {
+ for (int i = 0; i < mTrackedJobs.size(); i++) {
+ ArraySet<JobStatus> jobs = mTrackedJobs.get(i);
+ for (int j = 0; j < mTrackedJobs.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();
+ }
+ }
+ }
+ }
+
+ private class FlexibilityAlarmQueue extends AlarmQueue<JobStatus> {
+ private FlexibilityAlarmQueue(Context context, Looper looper) {
+ super(context, looper, "*job.flexibility_check*",
+ "Flexible Constraint Check", false, mMinTimeBetweenAlarmsMs);
+ }
+
+ @Override
+ protected boolean isForUser(@NonNull JobStatus js, int userId) {
+ return js.getSourceUserId() == userId;
+ }
+
+ @Override
+ protected void processExpiredAlarms(@NonNull ArraySet<JobStatus> expired) {
+ synchronized (mLock) {
+ JobStatus js;
+ for (int i = 0; i < expired.size(); i++) {
+ js = expired.valueAt(i);
+ long time = getNextConstraintDropTimeElapsed(js);
+ if (js.getLatestRunTimeElapsed() - time < DEADLINE_PROXIMITY_LIMIT_MS) {
+ mFlexibilityTracker.adjustJobsRequiredConstraints(js,
+ -js.getNumRequiredFlexibleConstraints());
+ continue;
+ }
+ if (mFlexibilityTracker.adjustJobsRequiredConstraints(js, -1)) {
+ mFlexibilityAlarmQueue.addAlarm(js, time);
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ @GuardedBy("mLock")
+ public void dumpControllerStateLocked(IndentingPrintWriter pw, Predicate<JobStatus> predicate) {
+ pw.println("# Constraints Satisfied: " + Integer.bitCount(mSatisfiedFlexibleConstraints));
+ pw.println();
+
+ mFlexibilityTracker.dump(pw, predicate);
+ }
+}
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 e0f58e3..41cf4212 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
@@ -16,6 +16,9 @@
package com.android.server.job.controllers;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
+import static android.text.format.DateUtils.HOUR_IN_MILLIS;
+
import static com.android.server.job.JobSchedulerService.ACTIVE_INDEX;
import static com.android.server.job.JobSchedulerService.EXEMPTED_INDEX;
import static com.android.server.job.JobSchedulerService.NEVER_INDEX;
@@ -99,6 +102,7 @@
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
+ static final int CONSTRAINT_FLEXIBLE = 1 << 21; // Implicit constraint
// The following set of dynamic constraints are for specific use cases (as explained in their
// relative naming and comments). Right now, they apply different constraints, which is fine,
@@ -118,6 +122,22 @@
| CONSTRAINT_IDLE;
/**
+ * The set of constraints that are required to satisfy flexible constraints.
+ * Constraints explicitly requested by the job will not be added to the set.
+ */
+ private int mFlexibleConstraints;
+
+ /**
+ * Keeps track of how many flexible constraints must be satisfied for the job to execute.
+ */
+ private int mNumRequiredFlexibleConstraints;
+
+ /**
+ * Number of required flexible constraints that have been dropped.
+ */
+ private int mNumDroppedFlexibleConstraints;
+
+ /**
* The additional set of dynamic constraints that must be met if this is an expedited job that
* had a long enough run while the device was Dozing or in battery saver.
*/
@@ -305,6 +325,12 @@
public static final int TRACKING_QUOTA = 1 << 6;
/**
+ * Flag for {@link #trackingControllers}: the flexibility controller is currently tracking this
+ * job.
+ */
+ public static final int TRACKING_FLEXIBILITY = 1 << 7;
+
+ /**
* Bit mask of controllers that are currently tracking the job.
*/
private int trackingControllers;
@@ -318,6 +344,8 @@
*/
public static final int INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION = 1 << 0;
+ /** Minimum difference between start and end time to have flexible constraint */
+ private static final long MIN_WINDOW_FOR_FLEXIBILITY_MS = HOUR_IN_MILLIS;
/**
* Versatile, persistable flags for a job that's updated within the system server,
* as opposed to {@link JobInfo#flags} that's set by callers.
@@ -525,6 +553,33 @@
}
}
mHasExemptedMediaUrisOnly = exemptedMediaUrisOnly;
+
+ if (isRequestedExpeditedJob()
+ || ((latestRunTimeElapsedMillis - earliestRunTimeElapsedMillis)
+ < MIN_WINDOW_FOR_FLEXIBILITY_MS)
+ || job.isPrefetch()) {
+ mFlexibleConstraints = 0;
+ } else {
+ if ((requiredConstraints & CONSTRAINT_CHARGING) == 0) {
+ mFlexibleConstraints |= CONSTRAINT_CHARGING;
+ }
+ if ((requiredConstraints & CONSTRAINT_BATTERY_NOT_LOW) == 0) {
+ mFlexibleConstraints |= CONSTRAINT_BATTERY_NOT_LOW;
+ }
+ if ((requiredConstraints & CONSTRAINT_IDLE) == 0) {
+ mFlexibleConstraints |= CONSTRAINT_IDLE;
+ }
+ if (job.getRequiredNetwork() != null
+ && !job.getRequiredNetwork().hasCapability(NET_CAPABILITY_NOT_METERED)) {
+ mFlexibleConstraints |= CONSTRAINT_CONNECTIVITY;
+ }
+ }
+ if (mFlexibleConstraints != 0) {
+ // TODO(b/239047584): Uncomment once Flexibility Controller is plugged in.
+ // requiredConstraints |= CONSTRAINT_FLEXIBLE;
+ mNumRequiredFlexibleConstraints = Integer.bitCount(mFlexibleConstraints);
+ }
+
this.requiredConstraints = requiredConstraints;
mRequiredConstraintsOfInterest = requiredConstraints & CONSTRAINTS_OF_INTEREST;
addDynamicConstraints(dynamicConstraints);
@@ -1082,12 +1137,35 @@
return hasConstraint(CONSTRAINT_IDLE);
}
+ /** Returns true if the job has a prefetch constraint */
+ public boolean hasPrefetchConstraint() {
+ return hasConstraint(CONSTRAINT_PREFETCH);
+ }
+
public boolean hasContentTriggerConstraint() {
// No need to check mDynamicConstraints since content trigger will only be in that list if
// it's already in the requiredConstraints list.
return (requiredConstraints&CONSTRAINT_CONTENT_TRIGGER) != 0;
}
+ /** Returns true if the job has flexible job constraints enabled */
+ public boolean hasFlexibilityConstraint() {
+ return (requiredConstraints & CONSTRAINT_FLEXIBLE) != 0;
+ }
+
+ /** Returns the number of flexible job constraints required to be satisfied to execute */
+ public int getNumRequiredFlexibleConstraints() {
+ return mNumRequiredFlexibleConstraints;
+ }
+
+ /**
+ * Returns the number of required flexible job constraints that have been dropped with time.
+ * The lower this number is the easier it is for the flexibility constraint to be satisfied.
+ */
+ public int getNumDroppedFlexibleConstraints() {
+ return mNumDroppedFlexibleConstraints;
+ }
+
/**
* Checks both {@link #requiredConstraints} and {@link #mDynamicConstraints} to see if this job
* requires the specified constraint.
@@ -1128,6 +1206,10 @@
return mOriginalLatestRunTimeElapsedMillis;
}
+ public int getFlexibleConstraints() {
+ return mFlexibleConstraints;
+ }
+
public void setOriginalLatestRunTimeElapsed(long latestRunTimeElapsed) {
mOriginalLatestRunTimeElapsedMillis = latestRunTimeElapsed;
}
@@ -1301,6 +1383,11 @@
return false;
}
+ /** @return true if the constraint was changed, false otherwise. */
+ boolean setFlexibilityConstraintSatisfied(final long nowElapsed, boolean state) {
+ return setConstraintSatisfied(CONSTRAINT_FLEXIBLE, nowElapsed, state);
+ }
+
/**
* Sets whether or not this job is approved to be treated as an expedited job based on quota
* policy.
@@ -1490,6 +1577,18 @@
trackingControllers |= which;
}
+ /** Adjusts the number of required flexible constraints by the given number */
+ public void adjustNumRequiredFlexibleConstraints(int adjustment) {
+ mNumRequiredFlexibleConstraints += adjustment;
+ if (mNumRequiredFlexibleConstraints < 0) {
+ mNumRequiredFlexibleConstraints = 0;
+ }
+ mNumDroppedFlexibleConstraints -= adjustment;
+ if (mNumDroppedFlexibleConstraints < 0) {
+ mNumDroppedFlexibleConstraints = 0;
+ }
+ }
+
/**
* Add additional constraints to prevent this job from running when doze or battery saver are
* active.
@@ -1650,12 +1749,14 @@
/** 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_PREFETCH;
+ | CONSTRAINT_IDLE | CONSTRAINT_CONTENT_TRIGGER | CONSTRAINT_PREFETCH
+ | CONSTRAINT_FLEXIBLE;
// 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_PREFETCH;
+ | CONSTRAINT_TIMING_DELAY | CONSTRAINT_IDLE | CONSTRAINT_PREFETCH
+ | CONSTRAINT_FLEXIBLE;
/** Returns true whenever all dynamically set constraints are satisfied. */
public boolean areDynamicConstraintsSatisfied() {
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java
new file mode 100644
index 0000000..c7ccef2
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright (C) 2022 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 com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.app.AlarmManager;
+import android.app.job.JobInfo;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageManagerInternal;
+import android.os.Looper;
+import android.util.ArraySet;
+
+import com.android.server.LocalServices;
+import com.android.server.job.JobSchedulerService;
+import com.android.server.job.JobStore;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
+
+import java.time.Clock;
+import java.time.Instant;
+import java.time.ZoneOffset;
+import java.util.ArrayList;
+
+public class FlexibilityControllerTest {
+ private static final String SOURCE_PACKAGE = "com.android.frameworks.mockingservicestests";
+ private static final int SOURCE_USER_ID = 0;
+
+ private MockitoSession mMockingSession;
+ private FlexibilityController mFlexibilityController;
+ @Mock
+ private AlarmManager mAlarmManager;
+ @Mock
+ private Context mContext;
+ @Mock
+ private JobSchedulerService mJobSchedulerService;
+ private JobStore mJobStore;
+
+ @Before
+ public void setup() {
+ mMockingSession = mockitoSession()
+ .initMocks(this)
+ .strictness(Strictness.LENIENT)
+ .mockStatic(LocalServices.class)
+ .startMocking();
+ // Called in StateController constructor.
+ when(mJobSchedulerService.getTestableContext()).thenReturn(mContext);
+ when(mJobSchedulerService.getLock()).thenReturn(mJobSchedulerService);
+ when(mJobSchedulerService.getConstants()).thenReturn(
+ mock(JobSchedulerService.Constants.class));
+ // Called in FlexibilityController constructor.
+ when(mContext.getMainLooper()).thenReturn(Looper.getMainLooper());
+ when(mContext.getSystemService(Context.ALARM_SERVICE)).thenReturn(mAlarmManager);
+ //used to get jobs by UID
+ mJobStore = JobStore.initAndGetForTesting(mContext, mContext.getFilesDir());
+ when(mJobSchedulerService.getJobStore()).thenReturn(mJobStore);
+ // Used in JobStatus.
+ doReturn(mock(PackageManagerInternal.class))
+ .when(() -> LocalServices.getService(PackageManagerInternal.class));
+ // Freeze the clocks at a moment in time
+ JobSchedulerService.sSystemClock =
+ Clock.fixed(Instant.ofEpochMilli(100L), ZoneOffset.UTC);
+ JobSchedulerService.sElapsedRealtimeClock =
+ Clock.fixed(Instant.ofEpochMilli(100L), ZoneOffset.UTC);
+ // Initialize real objects.
+ mFlexibilityController = new FlexibilityController(mJobSchedulerService);
+ }
+
+ @After
+ public void teardown() {
+ if (mMockingSession != null) {
+ mMockingSession.finishMocking();
+ }
+ }
+
+ private static JobInfo.Builder createJob(int id) {
+ return new JobInfo.Builder(id, new ComponentName("foo", "bar"));
+ }
+
+ private JobStatus createJobStatus(String testTag, JobInfo.Builder job) {
+ JobInfo jobInfo = job.build();
+ return JobStatus.createFromJobInfo(
+ jobInfo, 1000, SOURCE_PACKAGE, SOURCE_USER_ID, testTag);
+ }
+
+ @Test
+ public void testGetNextConstraintDropTimeElapsed() {
+ long nextTimeToDropNumConstraints;
+
+ // no delay, deadline
+ JobInfo.Builder jb = createJob(0).setOverrideDeadline(1000);
+ JobStatus js = createJobStatus("time", jb);
+ js.enqueueTime = 100L;
+
+ assertEquals(0, js.getEarliestRunTime());
+ assertEquals(1100L, js.getLatestRunTimeElapsed());
+ assertEquals(100L, js.enqueueTime);
+
+ nextTimeToDropNumConstraints = mFlexibilityController.getNextConstraintDropTimeElapsed(js);
+ assertEquals(600L, nextTimeToDropNumConstraints);
+ js.adjustNumRequiredFlexibleConstraints(-1);
+ nextTimeToDropNumConstraints = mFlexibilityController.getNextConstraintDropTimeElapsed(js);
+ assertEquals(700L, nextTimeToDropNumConstraints);
+ js.adjustNumRequiredFlexibleConstraints(-1);
+ nextTimeToDropNumConstraints = mFlexibilityController.getNextConstraintDropTimeElapsed(js);
+ assertEquals(800L, nextTimeToDropNumConstraints);
+
+ // delay, no deadline
+ jb = createJob(0).setMinimumLatency(800000L);
+ js = createJobStatus("time", jb);
+ js.enqueueTime = 100L;
+
+ nextTimeToDropNumConstraints = mFlexibilityController.getNextConstraintDropTimeElapsed(js);
+ assertEquals(130400100, nextTimeToDropNumConstraints);
+ js.adjustNumRequiredFlexibleConstraints(-1);
+ nextTimeToDropNumConstraints = mFlexibilityController.getNextConstraintDropTimeElapsed(js);
+ assertEquals(156320100L, nextTimeToDropNumConstraints);
+ js.adjustNumRequiredFlexibleConstraints(-1);
+ nextTimeToDropNumConstraints = mFlexibilityController.getNextConstraintDropTimeElapsed(js);
+ assertEquals(182240100L, nextTimeToDropNumConstraints);
+
+ // no delay, no deadline
+ jb = createJob(0);
+ js = createJobStatus("time", jb);
+ js.enqueueTime = 100L;
+
+ nextTimeToDropNumConstraints = mFlexibilityController.getNextConstraintDropTimeElapsed(js);
+ assertEquals(129600100, nextTimeToDropNumConstraints);
+ js.adjustNumRequiredFlexibleConstraints(-1);
+ nextTimeToDropNumConstraints = mFlexibilityController.getNextConstraintDropTimeElapsed(js);
+ assertEquals(155520100L, nextTimeToDropNumConstraints);
+ js.adjustNumRequiredFlexibleConstraints(-1);
+ nextTimeToDropNumConstraints = mFlexibilityController.getNextConstraintDropTimeElapsed(js);
+ assertEquals(181440100L, nextTimeToDropNumConstraints);
+
+ // delay, deadline
+ jb = createJob(0).setOverrideDeadline(1100).setMinimumLatency(100);
+ js = createJobStatus("time", jb);
+ js.enqueueTime = 100L;
+
+ nextTimeToDropNumConstraints = mFlexibilityController.getNextConstraintDropTimeElapsed(js);
+ assertEquals(700L, nextTimeToDropNumConstraints);
+ js.adjustNumRequiredFlexibleConstraints(-1);
+ nextTimeToDropNumConstraints = mFlexibilityController.getNextConstraintDropTimeElapsed(js);
+ assertEquals(800L, nextTimeToDropNumConstraints);
+ js.adjustNumRequiredFlexibleConstraints(-1);
+ nextTimeToDropNumConstraints = mFlexibilityController.getNextConstraintDropTimeElapsed(js);
+ assertEquals(900L, nextTimeToDropNumConstraints);
+ }
+
+ @Test
+ public void testWontStopJobFromRunning() {
+ JobStatus js = createJobStatus("testWontStopJobFromRunning", createJob(101));
+ // Stop satisfied constraints from causing a false positive.
+ js.adjustNumRequiredFlexibleConstraints(100);
+ synchronized (mFlexibilityController.mLock) {
+ when(mJobSchedulerService.isCurrentlyRunningLocked(js)).thenReturn(true);
+ assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(js));
+ }
+ }
+
+ @Test
+ public void testFlexibilityTracker() {
+ FlexibilityController.FlexibilityTracker flexTracker =
+ mFlexibilityController.new
+ FlexibilityTracker(FlexibilityController.FLEXIBLE_CONSTRAINTS);
+
+ JobStatus[] jobs = new JobStatus[4];
+ JobInfo.Builder jb;
+ for (int i = 0; i < jobs.length; i++) {
+ jb = createJob(i);
+ if (i > 0) {
+ jb.setRequiresDeviceIdle(true);
+ }
+ if (i > 1) {
+ jb.setRequiresBatteryNotLow(true);
+ }
+ if (i > 2) {
+ jb.setRequiresCharging(true);
+ }
+ jobs[i] = createJobStatus("", jb);
+ flexTracker.add(jobs[i]);
+
+ }
+
+ ArrayList<ArraySet<JobStatus>> trackedJobs = flexTracker.getArrayList();
+ assertEquals(1, trackedJobs.get(0).size());
+ assertEquals(1, trackedJobs.get(1).size());
+ assertEquals(1, trackedJobs.get(2).size());
+ assertEquals(0, trackedJobs.get(3).size());
+
+ flexTracker.adjustJobsRequiredConstraints(jobs[0], -1);
+ assertEquals(1, trackedJobs.get(0).size());
+ assertEquals(2, trackedJobs.get(1).size());
+ assertEquals(0, trackedJobs.get(2).size());
+ assertEquals(0, trackedJobs.get(3).size());
+
+ flexTracker.adjustJobsRequiredConstraints(jobs[0], -1);
+ assertEquals(2, trackedJobs.get(0).size());
+ assertEquals(1, trackedJobs.get(1).size());
+ assertEquals(0, trackedJobs.get(2).size());
+ assertEquals(0, trackedJobs.get(3).size());
+
+ flexTracker.adjustJobsRequiredConstraints(jobs[0], -1);
+ assertEquals(1, trackedJobs.get(0).size());
+ assertEquals(1, trackedJobs.get(1).size());
+ assertEquals(0, trackedJobs.get(2).size());
+ assertEquals(0, trackedJobs.get(3).size());
+
+ flexTracker.remove(jobs[1]);
+ assertEquals(1, trackedJobs.get(0).size());
+ assertEquals(0, trackedJobs.get(1).size());
+ assertEquals(0, trackedJobs.get(2).size());
+ assertEquals(0, trackedJobs.get(3).size());
+ }
+}