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());
+    }
+}