diff options
3 files changed, 657 insertions, 2 deletions
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 000000000000..687693ce0438 --- /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 e0f58e3fa6e3..41cf42125d70 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 @@ public final class JobStatus { 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 @@ public final class JobStatus { | 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 final class JobStatus { 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 final class JobStatus { */ 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 @@ public final class JobStatus { } } 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 @@ public final class JobStatus { 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 @@ public final class JobStatus { return mOriginalLatestRunTimeElapsedMillis; } + public int getFlexibleConstraints() { + return mFlexibleConstraints; + } + public void setOriginalLatestRunTimeElapsed(long latestRunTimeElapsed) { mOriginalLatestRunTimeElapsedMillis = latestRunTimeElapsed; } @@ -1301,6 +1383,11 @@ public final class JobStatus { 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 @@ public final class JobStatus { 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 @@ 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_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 000000000000..c7ccef279bf7 --- /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()); + } +} |