Merge "Requesting network exception for app idle jobs."
diff --git a/services/core/java/com/android/server/job/JobSchedulerService.java b/services/core/java/com/android/server/job/JobSchedulerService.java
index 611c8b7..78e18e9 100644
--- a/services/core/java/com/android/server/job/JobSchedulerService.java
+++ b/services/core/java/com/android/server/job/JobSchedulerService.java
@@ -839,6 +839,15 @@
                                 break;
                             }
                         }
+                        if (DEBUG) {
+                            Slog.d(TAG, "Something in " + pkgName
+                                    + " changed. Reevaluating controller states.");
+                        }
+                        synchronized (mLock) {
+                            for (int c = mControllers.size() - 1; c >= 0; --c) {
+                                mControllers.get(c).reevaluateStateLocked(pkgUid);
+                            }
+                        }
                     }
                 } else {
                     Slog.w(TAG, "PACKAGE_CHANGED for " + pkgName + " / uid " + pkgUid);
@@ -1042,6 +1051,8 @@
                 mJobPackageTracker.notePending(jobStatus);
                 addOrderedItem(mPendingJobs, jobStatus, mEnqueueTimeComparator);
                 maybeRunPendingJobsLocked();
+            } else {
+                evaluateControllerStatesLocked(jobStatus);
             }
         }
         return JobScheduler.RESULT_SUCCESS;
@@ -1884,6 +1895,8 @@
                     newReadyJobs = new ArrayList<JobStatus>();
                 }
                 newReadyJobs.add(job);
+            } else {
+                evaluateControllerStatesLocked(job);
             }
         }
 
@@ -1957,6 +1970,8 @@
                     runnableJobs = new ArrayList<>();
                 }
                 runnableJobs.add(job);
+            } else {
+                evaluateControllerStatesLocked(job);
             }
         }
 
@@ -2087,6 +2102,15 @@
                 HEARTBEAT_TAG, mHeartbeatAlarm, mHandler);
     }
 
+    /** Returns true if both the calling and source users for the job are started. */
+    private boolean areUsersStartedLocked(final JobStatus job) {
+        boolean sourceStarted = ArrayUtils.contains(mStartedUsers, job.getSourceUserId());
+        if (job.getUserId() == job.getSourceUserId()) {
+            return sourceStarted;
+        }
+        return sourceStarted && ArrayUtils.contains(mStartedUsers, job.getUserId());
+    }
+
     /**
      * Criteria for moving a job into the pending queue:
      *      - It's ready.
@@ -2219,6 +2243,61 @@
         return componentPresent;
     }
 
+    private void evaluateControllerStatesLocked(final JobStatus job) {
+        for (int c = mControllers.size() - 1; c >= 0; --c) {
+            final StateController sc = mControllers.get(c);
+            sc.evaluateStateLocked(job);
+        }
+    }
+
+    /**
+     * Returns true if non-job constraint components are in place -- if job.isReady() returns true
+     * and this method returns true, then the job is ready to be executed.
+     */
+    public boolean areComponentsInPlaceLocked(JobStatus job) {
+        // This code is very similar to the code in isReadyToBeExecutedLocked --- it uses the same
+        // conditions.
+
+        final boolean jobExists = mJobs.containsJob(job);
+        final boolean userStarted = areUsersStartedLocked(job);
+
+        if (DEBUG) {
+            Slog.v(TAG, "areComponentsInPlaceLocked: " + job.toShortString()
+                    + " exists=" + jobExists + " userStarted=" + userStarted);
+        }
+
+        // These are also fairly cheap to check, though they typically will not
+        // be conditions we fail.
+        if (!jobExists || !userStarted) {
+            return false;
+        }
+
+        // Job pending/active doesn't affect the readiness of a job.
+
+        // Skipping the hearbeat check as this will only come into play when using the rolling
+        // window quota management system.
+
+        // The expensive check last: validate that the defined package+service is
+        // still present & viable.
+        final boolean componentPresent;
+        try {
+            // TODO: cache result until we're notified that something in the package changed.
+            componentPresent = (AppGlobals.getPackageManager().getServiceInfo(
+                    job.getServiceComponent(), PackageManager.MATCH_DEBUG_TRIAGED_MISSING,
+                    job.getUserId()) != null);
+        } catch (RemoteException e) {
+            throw e.rethrowAsRuntimeException();
+        }
+
+        if (DEBUG) {
+            Slog.v(TAG, "areComponentsInPlaceLocked: " + job.toShortString()
+                    + " componentPresent=" + componentPresent);
+        }
+
+        // Everything else checked out so far, so this is the final yes/no check
+        return componentPresent;
+    }
+
     /**
      * Reconcile jobs in the pending queue against available execution contexts.
      * A controller can force a job into the pending queue even if it's already running, but
diff --git a/services/core/java/com/android/server/job/controllers/ConnectivityController.java b/services/core/java/com/android/server/job/controllers/ConnectivityController.java
index 6989c33..8f104e4 100644
--- a/services/core/java/com/android/server/job/controllers/ConnectivityController.java
+++ b/services/core/java/com/android/server/job/controllers/ConnectivityController.java
@@ -41,10 +41,12 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.LocalServices;
 import com.android.server.job.JobSchedulerService;
 import com.android.server.job.JobSchedulerService.Constants;
 import com.android.server.job.JobServiceContext;
 import com.android.server.job.StateControllerProto;
+import com.android.server.net.NetworkPolicyManagerInternal;
 
 import java.util.Objects;
 import java.util.function.Predicate;
@@ -66,16 +68,29 @@
 
     private final ConnectivityManager mConnManager;
     private final NetworkPolicyManager mNetPolicyManager;
+    private final NetworkPolicyManagerInternal mNetPolicyManagerInternal;
 
     /** List of tracked jobs keyed by source UID. */
     @GuardedBy("mLock")
     private final SparseArray<ArraySet<JobStatus>> mTrackedJobs = new SparseArray<>();
 
+    /**
+     * Keep track of all the UID's jobs that the controller has requested that NetworkPolicyManager
+     * grant an exception to in the app standby chain.
+     */
+    @GuardedBy("mLock")
+    private final SparseArray<ArraySet<JobStatus>> mRequestedWhitelistJobs = new SparseArray<>();
+
+    /** List of currently available networks. */
+    @GuardedBy("mLock")
+    private final ArraySet<Network> mAvailableNetworks = new ArraySet<>();
+
     public ConnectivityController(JobSchedulerService service) {
         super(service);
 
         mConnManager = mContext.getSystemService(ConnectivityManager.class);
         mNetPolicyManager = mContext.getSystemService(NetworkPolicyManager.class);
+        mNetPolicyManagerInternal = LocalServices.getService(NetworkPolicyManagerInternal.class);
 
         // We're interested in all network changes; internally we match these
         // network changes against the active network for each UID with jobs.
@@ -109,9 +124,178 @@
             if (jobs != null) {
                 jobs.remove(jobStatus);
             }
+            maybeRevokeStandbyExceptionLocked(jobStatus);
         }
     }
 
+    @GuardedBy("mLock")
+    @Override
+    public void onConstantsUpdatedLocked() {
+        if (mConstants.USE_HEARTBEATS) {
+            // App idle exceptions are only requested for the rolling quota system.
+            if (DEBUG) Slog.i(TAG, "Revoking all standby exceptions");
+            for (int i = 0; i < mRequestedWhitelistJobs.size(); ++i) {
+                int uid = mRequestedWhitelistJobs.keyAt(i);
+                mNetPolicyManagerInternal.setAppIdleWhitelist(uid, false);
+            }
+            mRequestedWhitelistJobs.clear();
+        }
+    }
+
+    /**
+     * Returns true if the job's requested network is available. This DOES NOT necesarilly mean
+     * that the UID has been granted access to the network.
+     */
+    public boolean isNetworkAvailable(JobStatus job) {
+        synchronized (mLock) {
+            for (int i = 0; i < mAvailableNetworks.size(); ++i) {
+                final Network network = mAvailableNetworks.valueAt(i);
+                final NetworkCapabilities capabilities = mConnManager.getNetworkCapabilities(
+                        network);
+                final boolean satisfied = isSatisfied(job, network, capabilities, mConstants);
+                if (DEBUG) {
+                    Slog.v(TAG, "isNetworkAvailable(" + job + ") with network " + network
+                            + " and capabilities " + capabilities + ". Satisfied=" + satisfied);
+                }
+                if (satisfied) {
+                    return true;
+                }
+            }
+            return false;
+        }
+    }
+
+    /**
+     * Request that NetworkPolicyManager grant an exception to the uid from its standby policy
+     * chain.
+     */
+    @VisibleForTesting
+    @GuardedBy("mLock")
+    void requestStandbyExceptionLocked(JobStatus job) {
+        final int uid = job.getSourceUid();
+        // Need to call this before adding the job.
+        final boolean isExceptionRequested = isStandbyExceptionRequestedLocked(uid);
+        ArraySet<JobStatus> jobs = mRequestedWhitelistJobs.get(uid);
+        if (jobs == null) {
+            jobs = new ArraySet<JobStatus>();
+            mRequestedWhitelistJobs.put(uid, jobs);
+        }
+        if (!jobs.add(job) || isExceptionRequested) {
+            if (DEBUG) {
+                Slog.i(TAG, "requestStandbyExceptionLocked found exception already requested.");
+            }
+            return;
+        }
+        if (DEBUG) Slog.i(TAG, "Requesting standby exception for UID: " + uid);
+        mNetPolicyManagerInternal.setAppIdleWhitelist(uid, true);
+    }
+
+    /** Returns whether a standby exception has been requested for the UID. */
+    @VisibleForTesting
+    @GuardedBy("mLock")
+    boolean isStandbyExceptionRequestedLocked(final int uid) {
+        ArraySet jobs = mRequestedWhitelistJobs.get(uid);
+        return jobs != null && jobs.size() > 0;
+    }
+
+    @VisibleForTesting
+    @GuardedBy("mLock")
+    boolean wouldBeReadyWithConnectivityLocked(JobStatus jobStatus) {
+        final boolean networkAvailable = isNetworkAvailable(jobStatus);
+        if (DEBUG) {
+            Slog.v(TAG, "wouldBeReadyWithConnectivityLocked: " + jobStatus.toShortString()
+                    + " networkAvailable=" + networkAvailable);
+        }
+        // If the network isn't available, then requesting an exception won't help.
+
+        return networkAvailable && wouldBeReadyWithConstraintLocked(jobStatus,
+                JobStatus.CONSTRAINT_CONNECTIVITY);
+    }
+
+    /**
+     * Tell NetworkPolicyManager not to block a UID's network connection if that's the only
+     * thing stopping a job from running.
+     */
+    @GuardedBy("mLock")
+    @Override
+    public void evaluateStateLocked(JobStatus jobStatus) {
+        if (mConstants.USE_HEARTBEATS) {
+            // This should only be used for the rolling quota system.
+            return;
+        }
+
+        if (!jobStatus.hasConnectivityConstraint()) {
+            return;
+        }
+
+        // Always check the full job readiness stat in case the component has been disabled.
+        if (wouldBeReadyWithConnectivityLocked(jobStatus)) {
+            if (DEBUG) {
+                Slog.i(TAG, "evaluateStateLocked finds job " + jobStatus + " would be ready.");
+            }
+            requestStandbyExceptionLocked(jobStatus);
+        } else {
+            if (DEBUG) {
+                Slog.i(TAG, "evaluateStateLocked finds job " + jobStatus + " would not be ready.");
+            }
+            maybeRevokeStandbyExceptionLocked(jobStatus);
+        }
+    }
+
+    @GuardedBy("mLock")
+    @Override
+    public void reevaluateStateLocked(final int uid) {
+        if (mConstants.USE_HEARTBEATS) {
+            return;
+        }
+        // Check if we still need a connectivity exception in case the JobService was disabled.
+        ArraySet<JobStatus> jobs = mTrackedJobs.get(uid);
+        if (jobs == null) {
+            return;
+        }
+        for (int i = jobs.size() - 1; i >= 0; i--) {
+            evaluateStateLocked(jobs.valueAt(i));
+        }
+    }
+
+    /** Cancel the requested standby exception if none of the jobs would be ready to run anyway. */
+    @VisibleForTesting
+    @GuardedBy("mLock")
+    void maybeRevokeStandbyExceptionLocked(final JobStatus job) {
+        final int uid = job.getSourceUid();
+        if (!isStandbyExceptionRequestedLocked(uid)) {
+            return;
+        }
+        ArraySet<JobStatus> jobs = mRequestedWhitelistJobs.get(uid);
+        if (jobs == null) {
+            Slog.wtf(TAG,
+                    "maybeRevokeStandbyExceptionLocked found null jobs array even though a "
+                            + "standby exception has been requested.");
+            return;
+        }
+        if (!jobs.remove(job) || jobs.size() > 0) {
+            if (DEBUG) {
+                Slog.i(TAG,
+                        "maybeRevokeStandbyExceptionLocked not revoking because there are still "
+                                + jobs.size() + " jobs left.");
+            }
+            return;
+        }
+        // No more jobs that need an exception.
+        revokeStandbyExceptionLocked(uid);
+    }
+
+    /**
+     * Tell NetworkPolicyManager to revoke any exception it granted from its standby policy chain
+     * for the uid.
+     */
+    @GuardedBy("mLock")
+    private void revokeStandbyExceptionLocked(final int uid) {
+        if (DEBUG) Slog.i(TAG, "Revoking standby exception for UID: " + uid);
+        mNetPolicyManagerInternal.setAppIdleWhitelist(uid, false);
+        mRequestedWhitelistJobs.remove(uid);
+    }
+
     /**
      * Test to see if running the given job on the given network is insane.
      * <p>
@@ -326,6 +510,14 @@
 
     private final NetworkCallback mNetworkCallback = new NetworkCallback() {
         @Override
+        public void onAvailable(Network network) {
+            if (DEBUG) Slog.v(TAG, "onAvailable: " + network);
+            synchronized (mLock) {
+                mAvailableNetworks.add(network);
+            }
+        }
+
+        @Override
         public void onCapabilitiesChanged(Network network, NetworkCapabilities capabilities) {
             if (DEBUG) {
                 Slog.v(TAG, "onCapabilitiesChanged: " + network);
@@ -338,6 +530,9 @@
             if (DEBUG) {
                 Slog.v(TAG, "onLost: " + network);
             }
+            synchronized (mLock) {
+                mAvailableNetworks.remove(network);
+            }
             updateTrackedJobs(-1, network);
         }
     };
@@ -356,6 +551,27 @@
     @Override
     public void dumpControllerStateLocked(IndentingPrintWriter pw,
             Predicate<JobStatus> predicate) {
+        if (mRequestedWhitelistJobs.size() > 0) {
+            pw.print("Requested standby exceptions:");
+            for (int i = 0; i < mRequestedWhitelistJobs.size(); i++) {
+                pw.print(" ");
+                pw.print(mRequestedWhitelistJobs.keyAt(i));
+                pw.print(" (");
+                pw.print(mRequestedWhitelistJobs.valueAt(i).size());
+                pw.print(" jobs)");
+            }
+            pw.println();
+        }
+        if (mAvailableNetworks.size() > 0) {
+            pw.println("Available networks:");
+            pw.increaseIndent();
+            for (int i = 0; i < mAvailableNetworks.size(); i++) {
+                pw.println(mAvailableNetworks.valueAt(i));
+            }
+            pw.decreaseIndent();
+        } else {
+            pw.println("No available networks");
+        }
         for (int i = 0; i < mTrackedJobs.size(); i++) {
             final ArraySet<JobStatus> jobs = mTrackedJobs.valueAt(i);
             for (int j = 0; j < jobs.size(); j++) {
diff --git a/services/core/java/com/android/server/job/controllers/JobStatus.java b/services/core/java/com/android/server/job/controllers/JobStatus.java
index 4341589..82bfa51 100644
--- a/services/core/java/com/android/server/job/controllers/JobStatus.java
+++ b/services/core/java/com/android/server/job/controllers/JobStatus.java
@@ -1007,6 +1007,18 @@
      * @return Whether or not this job is ready to run, based on its requirements.
      */
     public boolean isReady() {
+        return isReady(mSatisfiedConstraintsOfInterest);
+    }
+
+    /**
+     * @return Whether or not this job would be ready to run if it had the specified constraint
+     * granted, based on its requirements.
+     */
+    public boolean wouldBeReadyWithConstraint(int constraint) {
+        return isReady(mSatisfiedConstraintsOfInterest | constraint);
+    }
+
+    private boolean isReady(int satisfiedConstraints) {
         // Quota constraints trumps all other constraints.
         if (!mReadyWithinQuota) {
             return false;
@@ -1017,7 +1029,7 @@
         // DeviceNotDozing implicit constraint must be satisfied
         // NotRestrictedInBackground implicit constraint must be satisfied
         return mReadyNotDozing && mReadyNotRestrictedInBg && (mReadyDeadlineSatisfied
-                || isConstraintsSatisfied());
+                || isConstraintsSatisfied(satisfiedConstraints));
     }
 
     static final int CONSTRAINTS_OF_INTEREST = CONSTRAINT_CHARGING | CONSTRAINT_BATTERY_NOT_LOW
@@ -1033,12 +1045,16 @@
      * @return Whether the constraints set on this job are satisfied.
      */
     public boolean isConstraintsSatisfied() {
+        return isConstraintsSatisfied(mSatisfiedConstraintsOfInterest);
+    }
+
+    private boolean isConstraintsSatisfied(int satisfiedConstraints) {
         if (overrideState == OVERRIDE_FULL) {
             // force override: the job is always runnable
             return true;
         }
 
-        int sat = mSatisfiedConstraintsOfInterest;
+        int sat = satisfiedConstraints;
         if (overrideState == OVERRIDE_SOFT) {
             // override: pretend all 'soft' requirements are satisfied
             sat |= (requiredConstraints & SOFT_OVERRIDE_CONSTRAINTS);
diff --git a/services/core/java/com/android/server/job/controllers/StateController.java b/services/core/java/com/android/server/job/controllers/StateController.java
index b439c0d..61dc479 100644
--- a/services/core/java/com/android/server/job/controllers/StateController.java
+++ b/services/core/java/com/android/server/job/controllers/StateController.java
@@ -16,7 +16,10 @@
 
 package com.android.server.job.controllers;
 
+import static com.android.server.job.JobSchedulerService.DEBUG;
+
 import android.content.Context;
+import android.util.Slog;
 import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.util.IndentingPrintWriter;
@@ -32,6 +35,8 @@
  * are ready to run, or whether they must be stopped.
  */
 public abstract class StateController {
+    private static final String TAG = "JobScheduler.SC";
+
     protected final JobSchedulerService mService;
     protected final StateChangedListener mStateChangedListener;
     protected final Context mContext;
@@ -78,6 +83,37 @@
     public void onConstantsUpdatedLocked() {
     }
 
+    protected boolean wouldBeReadyWithConstraintLocked(JobStatus jobStatus, int constraint) {
+        // This is very cheap to check (just a few conditions on data in JobStatus).
+        final boolean jobWouldBeReady = jobStatus.wouldBeReadyWithConstraint(constraint);
+        if (DEBUG) {
+            Slog.v(TAG, "wouldBeReadyWithConstraintLocked: " + jobStatus.toShortString()
+                    + " readyWithConstraint=" + jobWouldBeReady);
+        }
+        if (!jobWouldBeReady) {
+            // If the job wouldn't be ready, nothing to do here.
+            return false;
+        }
+
+        // This is potentially more expensive since JSS may have to query component
+        // presence.
+        return mService.areComponentsInPlaceLocked(jobStatus);
+    }
+
+    /**
+     * Called when JobSchedulerService has determined that the job is not ready to be run. The
+     * Controller can evaluate if it can or should do something to promote this job's readiness.
+     */
+    public void evaluateStateLocked(JobStatus jobStatus) {
+    }
+
+    /**
+     * Called when something with the UID has changed. The controller should re-evaluate any
+     * internal state tracking dependent on this UID.
+     */
+    public void reevaluateStateLocked(int uid) {
+    }
+
     public abstract void dumpControllerStateLocked(IndentingPrintWriter pw,
             Predicate<JobStatus> predicate);
     public abstract void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId,
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java
new file mode 100644
index 0000000..8e78a56
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java
@@ -0,0 +1,581 @@
+/*
+ * 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.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.inOrder;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.app.job.JobInfo;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageManagerInternal;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkInfo;
+import android.net.NetworkInfo.DetailedState;
+import android.net.NetworkPolicyManager;
+import android.os.Build;
+import android.os.SystemClock;
+import android.util.DataUnit;
+
+import com.android.server.LocalServices;
+import com.android.server.job.JobSchedulerService;
+import com.android.server.job.JobSchedulerService.Constants;
+import com.android.server.net.NetworkPolicyManagerInternal;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.time.Clock;
+import java.time.ZoneOffset;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ConnectivityControllerTest {
+
+    @Mock
+    private Context mContext;
+    @Mock
+    private ConnectivityManager mConnManager;
+    @Mock
+    private NetworkPolicyManager mNetPolicyManager;
+    @Mock
+    private NetworkPolicyManagerInternal mNetPolicyManagerInternal;
+    @Mock
+    private JobSchedulerService mService;
+
+    private Constants mConstants;
+
+    private static final int UID_RED = 10001;
+    private static final int UID_BLUE = 10002;
+
+    @Before
+    public void setUp() throws Exception {
+        // Assume all packages are current SDK
+        final PackageManagerInternal pm = mock(PackageManagerInternal.class);
+        when(pm.getPackageTargetSdkVersion(anyString()))
+                .thenReturn(Build.VERSION_CODES.CUR_DEVELOPMENT);
+        LocalServices.removeServiceForTest(PackageManagerInternal.class);
+        LocalServices.addService(PackageManagerInternal.class, pm);
+
+        LocalServices.removeServiceForTest(NetworkPolicyManagerInternal.class);
+        LocalServices.addService(NetworkPolicyManagerInternal.class, mNetPolicyManagerInternal);
+
+        // Freeze the clocks at this moment in time
+        JobSchedulerService.sSystemClock =
+                Clock.fixed(Clock.systemUTC().instant(), ZoneOffset.UTC);
+        JobSchedulerService.sUptimeMillisClock =
+                Clock.fixed(SystemClock.uptimeMillisClock().instant(), ZoneOffset.UTC);
+        JobSchedulerService.sElapsedRealtimeClock =
+                Clock.fixed(SystemClock.elapsedRealtimeClock().instant(), ZoneOffset.UTC);
+
+        // Assume default constants for now
+        mConstants = new Constants();
+
+        // Get our mocks ready
+        when(mContext.getSystemServiceName(ConnectivityManager.class))
+                .thenReturn(Context.CONNECTIVITY_SERVICE);
+        when(mContext.getSystemService(ConnectivityManager.class))
+                .thenReturn(mConnManager);
+        when(mContext.getSystemServiceName(NetworkPolicyManager.class))
+                .thenReturn(Context.NETWORK_POLICY_SERVICE);
+        when(mContext.getSystemService(NetworkPolicyManager.class))
+                .thenReturn(mNetPolicyManager);
+        when(mService.getTestableContext()).thenReturn(mContext);
+        when(mService.getLock()).thenReturn(mService);
+        when(mService.getConstants()).thenReturn(mConstants);
+    }
+
+    @Test
+    public void testInsane() throws Exception {
+        final Network net = new Network(101);
+        final JobInfo.Builder job = createJob()
+                .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1))
+                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
+
+        // Slow network is too slow
+        assertFalse(ConnectivityController.isSatisfied(createJobStatus(job), net,
+                createCapabilities().setLinkUpstreamBandwidthKbps(1)
+                        .setLinkDownstreamBandwidthKbps(1), mConstants));
+        // Fast network looks great
+        assertTrue(ConnectivityController.isSatisfied(createJobStatus(job), net,
+                createCapabilities().setLinkUpstreamBandwidthKbps(1024)
+                        .setLinkDownstreamBandwidthKbps(1024), mConstants));
+    }
+
+    @Test
+    public void testCongestion() throws Exception {
+        final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+        final JobInfo.Builder job = createJob()
+                .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1))
+                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
+        final JobStatus early = createJobStatus(job, now - 1000, now + 2000);
+        final JobStatus late = createJobStatus(job, now - 2000, now + 1000);
+
+        // Uncongested network is whenever
+        {
+            final Network net = new Network(101);
+            final NetworkCapabilities caps = createCapabilities()
+                    .addCapability(NET_CAPABILITY_NOT_CONGESTED);
+            assertTrue(ConnectivityController.isSatisfied(early, net, caps, mConstants));
+            assertTrue(ConnectivityController.isSatisfied(late, net, caps, mConstants));
+        }
+
+        // Congested network is more selective
+        {
+            final Network net = new Network(101);
+            final NetworkCapabilities caps = createCapabilities();
+            assertFalse(ConnectivityController.isSatisfied(early, net, caps, mConstants));
+            assertTrue(ConnectivityController.isSatisfied(late, net, caps, mConstants));
+        }
+    }
+
+    @Test
+    public void testRelaxed() throws Exception {
+        final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+        final JobInfo.Builder job = createJob()
+                .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1))
+                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED);
+        final JobStatus early = createJobStatus(job, now - 1000, now + 2000);
+        final JobStatus late = createJobStatus(job, now - 2000, now + 1000);
+
+        job.setIsPrefetch(true);
+        final JobStatus earlyPrefetch = createJobStatus(job, now - 1000, now + 2000);
+        final JobStatus latePrefetch = createJobStatus(job, now - 2000, now + 1000);
+
+        // Unmetered network is whenever
+        {
+            final Network net = new Network(101);
+            final NetworkCapabilities caps = createCapabilities()
+                    .addCapability(NET_CAPABILITY_NOT_CONGESTED)
+                    .addCapability(NET_CAPABILITY_NOT_METERED);
+            assertTrue(ConnectivityController.isSatisfied(early, net, caps, mConstants));
+            assertTrue(ConnectivityController.isSatisfied(late, net, caps, mConstants));
+            assertTrue(ConnectivityController.isSatisfied(earlyPrefetch, net, caps, mConstants));
+            assertTrue(ConnectivityController.isSatisfied(latePrefetch, net, caps, mConstants));
+        }
+
+        // Metered network is only when prefetching and late
+        {
+            final Network net = new Network(101);
+            final NetworkCapabilities caps = createCapabilities()
+                    .addCapability(NET_CAPABILITY_NOT_CONGESTED);
+            assertFalse(ConnectivityController.isSatisfied(early, net, caps, mConstants));
+            assertFalse(ConnectivityController.isSatisfied(late, net, caps, mConstants));
+            assertFalse(ConnectivityController.isSatisfied(earlyPrefetch, net, caps, mConstants));
+            assertTrue(ConnectivityController.isSatisfied(latePrefetch, net, caps, mConstants));
+        }
+    }
+
+    @Test
+    public void testUpdates() throws Exception {
+        final ArgumentCaptor<NetworkCallback> callback = ArgumentCaptor
+                .forClass(NetworkCallback.class);
+        doNothing().when(mConnManager).registerNetworkCallback(any(), callback.capture());
+
+        final ConnectivityController controller = new ConnectivityController(mService);
+
+        final Network meteredNet = new Network(101);
+        final NetworkCapabilities meteredCaps = createCapabilities();
+        final Network unmeteredNet = new Network(202);
+        final NetworkCapabilities unmeteredCaps = createCapabilities()
+                .addCapability(NET_CAPABILITY_NOT_METERED);
+
+        final JobStatus red = createJobStatus(createJob()
+                .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1), 0)
+                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED), UID_RED);
+        final JobStatus blue = createJobStatus(createJob()
+                .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1), 0)
+                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY), UID_BLUE);
+
+        // Pretend we're offline when job is added
+        {
+            reset(mConnManager);
+            answerNetwork(UID_RED, null, null);
+            answerNetwork(UID_BLUE, null, null);
+
+            controller.maybeStartTrackingJobLocked(red, null);
+            controller.maybeStartTrackingJobLocked(blue, null);
+
+            assertFalse(red.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
+            assertFalse(blue.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
+        }
+
+        // Metered network
+        {
+            reset(mConnManager);
+            answerNetwork(UID_RED, meteredNet, meteredCaps);
+            answerNetwork(UID_BLUE, meteredNet, meteredCaps);
+
+            callback.getValue().onCapabilitiesChanged(meteredNet, meteredCaps);
+
+            assertFalse(red.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
+            assertTrue(blue.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
+        }
+
+        // Unmetered network background
+        {
+            reset(mConnManager);
+            answerNetwork(UID_RED, meteredNet, meteredCaps);
+            answerNetwork(UID_BLUE, meteredNet, meteredCaps);
+
+            callback.getValue().onCapabilitiesChanged(unmeteredNet, unmeteredCaps);
+
+            assertFalse(red.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
+            assertTrue(blue.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
+        }
+
+        // Lost metered network
+        {
+            reset(mConnManager);
+            answerNetwork(UID_RED, unmeteredNet, unmeteredCaps);
+            answerNetwork(UID_BLUE, unmeteredNet, unmeteredCaps);
+
+            callback.getValue().onLost(meteredNet);
+
+            assertTrue(red.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
+            assertTrue(blue.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
+        }
+
+        // Specific UID was blocked
+        {
+            reset(mConnManager);
+            answerNetwork(UID_RED, null, null);
+            answerNetwork(UID_BLUE, unmeteredNet, unmeteredCaps);
+
+            callback.getValue().onCapabilitiesChanged(unmeteredNet, unmeteredCaps);
+
+            assertFalse(red.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
+            assertTrue(blue.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
+        }
+    }
+
+    @Test
+    public void testRequestStandbyExceptionLocked() {
+        final ConnectivityController controller = new ConnectivityController(mService);
+        final JobStatus red = createJobStatus(createJob()
+                .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1), 0)
+                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED), UID_RED);
+        final JobStatus blue = createJobStatus(createJob()
+                .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1), 0)
+                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY), UID_BLUE);
+
+        InOrder inOrder = inOrder(mNetPolicyManagerInternal);
+
+        controller.requestStandbyExceptionLocked(red);
+        inOrder.verify(mNetPolicyManagerInternal, times(1))
+                .setAppIdleWhitelist(eq(UID_RED), eq(true));
+        inOrder.verify(mNetPolicyManagerInternal, never())
+                .setAppIdleWhitelist(eq(UID_BLUE), anyBoolean());
+        assertTrue(controller.isStandbyExceptionRequestedLocked(UID_RED));
+        assertFalse(controller.isStandbyExceptionRequestedLocked(UID_BLUE));
+        // Whitelisting doesn't need to be requested again.
+        controller.requestStandbyExceptionLocked(red);
+        inOrder.verify(mNetPolicyManagerInternal, never())
+                .setAppIdleWhitelist(eq(UID_RED), anyBoolean());
+        inOrder.verify(mNetPolicyManagerInternal, never())
+                .setAppIdleWhitelist(eq(UID_BLUE), anyBoolean());
+        assertTrue(controller.isStandbyExceptionRequestedLocked(UID_RED));
+        assertFalse(controller.isStandbyExceptionRequestedLocked(UID_BLUE));
+
+        controller.requestStandbyExceptionLocked(blue);
+        inOrder.verify(mNetPolicyManagerInternal, never())
+                .setAppIdleWhitelist(eq(UID_RED), anyBoolean());
+        inOrder.verify(mNetPolicyManagerInternal, times(1))
+                .setAppIdleWhitelist(eq(UID_BLUE), eq(true));
+        assertTrue(controller.isStandbyExceptionRequestedLocked(UID_RED));
+        assertTrue(controller.isStandbyExceptionRequestedLocked(UID_BLUE));
+    }
+
+    @Test
+    public void testWouldBeReadyWithConnectivityLocked() {
+        final ConnectivityController controller = spy(new ConnectivityController(mService));
+        final JobStatus red = createJobStatus(createJob()
+                .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1), 0)
+                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED), UID_RED);
+
+        doReturn(false).when(controller).isNetworkAvailable(any());
+        assertFalse(controller.wouldBeReadyWithConnectivityLocked(red));
+
+        doReturn(true).when(controller).isNetworkAvailable(any());
+        doReturn(false).when(controller).wouldBeReadyWithConstraintLocked(any(),
+                eq(JobStatus.CONSTRAINT_CONNECTIVITY));
+        assertFalse(controller.wouldBeReadyWithConnectivityLocked(red));
+
+        doReturn(true).when(controller).isNetworkAvailable(any());
+        doReturn(true).when(controller).wouldBeReadyWithConstraintLocked(any(),
+                eq(JobStatus.CONSTRAINT_CONNECTIVITY));
+        assertTrue(controller.wouldBeReadyWithConnectivityLocked(red));
+    }
+
+    @Test
+    public void testEvaluateStateLocked_HeartbeatsOn() {
+        mConstants.USE_HEARTBEATS = true;
+        final ConnectivityController controller = new ConnectivityController(mService);
+        final JobStatus red = createJobStatus(createJob()
+                .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1), 0)
+                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED), UID_RED);
+
+        controller.evaluateStateLocked(red);
+        assertFalse(controller.isStandbyExceptionRequestedLocked(UID_RED));
+        verify(mNetPolicyManagerInternal, never())
+                .setAppIdleWhitelist(eq(UID_RED), anyBoolean());
+    }
+
+    @Test
+    public void testEvaluateStateLocked_JobWithoutConnectivity() {
+        mConstants.USE_HEARTBEATS = false;
+        final ConnectivityController controller = new ConnectivityController(mService);
+        final JobStatus red = createJobStatus(createJob().setMinimumLatency(1));
+
+        controller.evaluateStateLocked(red);
+        assertFalse(controller.isStandbyExceptionRequestedLocked(UID_RED));
+        verify(mNetPolicyManagerInternal, never())
+                .setAppIdleWhitelist(eq(UID_RED), anyBoolean());
+    }
+
+    @Test
+    public void testEvaluateStateLocked_JobWouldBeReady() {
+        mConstants.USE_HEARTBEATS = false;
+        final ConnectivityController controller = spy(new ConnectivityController(mService));
+        doReturn(true).when(controller).wouldBeReadyWithConnectivityLocked(any());
+        final JobStatus red = createJobStatus(createJob()
+                .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1), 0)
+                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED), UID_RED);
+        final JobStatus blue = createJobStatus(createJob()
+                .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1), 0)
+                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY), UID_BLUE);
+
+        InOrder inOrder = inOrder(mNetPolicyManagerInternal);
+
+        controller.evaluateStateLocked(red);
+        inOrder.verify(mNetPolicyManagerInternal, times(1))
+                .setAppIdleWhitelist(eq(UID_RED), eq(true));
+        inOrder.verify(mNetPolicyManagerInternal, never())
+                .setAppIdleWhitelist(eq(UID_BLUE), anyBoolean());
+        assertTrue(controller.isStandbyExceptionRequestedLocked(UID_RED));
+        assertFalse(controller.isStandbyExceptionRequestedLocked(UID_BLUE));
+        // Whitelisting doesn't need to be requested again.
+        controller.evaluateStateLocked(red);
+        inOrder.verify(mNetPolicyManagerInternal, never())
+                .setAppIdleWhitelist(eq(UID_RED), anyBoolean());
+        inOrder.verify(mNetPolicyManagerInternal, never())
+                .setAppIdleWhitelist(eq(UID_BLUE), anyBoolean());
+        assertTrue(controller.isStandbyExceptionRequestedLocked(UID_RED));
+        assertFalse(controller.isStandbyExceptionRequestedLocked(UID_BLUE));
+
+        controller.evaluateStateLocked(blue);
+        inOrder.verify(mNetPolicyManagerInternal, never())
+                .setAppIdleWhitelist(eq(UID_RED), anyBoolean());
+        inOrder.verify(mNetPolicyManagerInternal, times(1))
+                .setAppIdleWhitelist(eq(UID_BLUE), eq(true));
+        assertTrue(controller.isStandbyExceptionRequestedLocked(UID_RED));
+        assertTrue(controller.isStandbyExceptionRequestedLocked(UID_BLUE));
+    }
+
+    @Test
+    public void testEvaluateStateLocked_JobWouldNotBeReady() {
+        mConstants.USE_HEARTBEATS = false;
+        final ConnectivityController controller = spy(new ConnectivityController(mService));
+        doReturn(false).when(controller).wouldBeReadyWithConnectivityLocked(any());
+        final JobStatus red = createJobStatus(createJob()
+                .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1), 0)
+                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED), UID_RED);
+        final JobStatus blue = createJobStatus(createJob()
+                .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1), 0)
+                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY), UID_BLUE);
+
+        InOrder inOrder = inOrder(mNetPolicyManagerInternal);
+
+        controller.evaluateStateLocked(red);
+        inOrder.verify(mNetPolicyManagerInternal, never())
+                .setAppIdleWhitelist(eq(UID_RED), anyBoolean());
+        inOrder.verify(mNetPolicyManagerInternal, never())
+                .setAppIdleWhitelist(eq(UID_BLUE), anyBoolean());
+        assertFalse(controller.isStandbyExceptionRequestedLocked(UID_RED));
+        assertFalse(controller.isStandbyExceptionRequestedLocked(UID_BLUE));
+
+        // Test that a currently whitelisted uid is now removed.
+        controller.requestStandbyExceptionLocked(blue);
+        inOrder.verify(mNetPolicyManagerInternal, times(1))
+                .setAppIdleWhitelist(eq(UID_BLUE), eq(true));
+        assertTrue(controller.isStandbyExceptionRequestedLocked(UID_BLUE));
+        controller.evaluateStateLocked(blue);
+        inOrder.verify(mNetPolicyManagerInternal, never())
+                .setAppIdleWhitelist(eq(UID_RED), anyBoolean());
+        inOrder.verify(mNetPolicyManagerInternal, times(1))
+                .setAppIdleWhitelist(eq(UID_BLUE), eq(false));
+        assertFalse(controller.isStandbyExceptionRequestedLocked(UID_RED));
+        assertFalse(controller.isStandbyExceptionRequestedLocked(UID_BLUE));
+    }
+
+    @Test
+    public void testReevaluateStateLocked() {
+        mConstants.USE_HEARTBEATS = false;
+        final ConnectivityController controller = spy(new ConnectivityController(mService));
+        final JobStatus redOne = createJobStatus(createJob(1)
+                .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1), 0)
+                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED), UID_RED);
+        final JobStatus redTwo = createJobStatus(createJob(2)
+                .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1), 0)
+                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED), UID_RED);
+        final JobStatus blue = createJobStatus(createJob()
+                .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1), 0)
+                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY), UID_BLUE);
+        controller.maybeStartTrackingJobLocked(redOne, null);
+        controller.maybeStartTrackingJobLocked(redTwo, null);
+        controller.maybeStartTrackingJobLocked(blue, null);
+
+        InOrder inOrder = inOrder(mNetPolicyManagerInternal);
+        controller.requestStandbyExceptionLocked(redOne);
+        controller.requestStandbyExceptionLocked(redTwo);
+        inOrder.verify(mNetPolicyManagerInternal, times(1))
+                .setAppIdleWhitelist(eq(UID_RED), eq(true));
+        assertTrue(controller.isStandbyExceptionRequestedLocked(UID_RED));
+
+        // Make sure nothing happens if an exception hasn't been requested.
+        assertFalse(controller.isStandbyExceptionRequestedLocked(UID_BLUE));
+        controller.reevaluateStateLocked(UID_BLUE);
+        inOrder.verify(mNetPolicyManagerInternal, never())
+                .setAppIdleWhitelist(eq(UID_BLUE), anyBoolean());
+
+        // Make sure a job that isn't being tracked doesn't cause issues.
+        assertFalse(controller.isStandbyExceptionRequestedLocked(12345));
+        controller.reevaluateStateLocked(12345);
+        inOrder.verify(mNetPolicyManagerInternal, never())
+                .setAppIdleWhitelist(eq(12345), anyBoolean());
+
+        // Both jobs would still be ready. Exception should not be revoked.
+        doReturn(true).when(controller).wouldBeReadyWithConnectivityLocked(any());
+        controller.reevaluateStateLocked(UID_RED);
+        inOrder.verify(mNetPolicyManagerInternal, never())
+                .setAppIdleWhitelist(eq(UID_RED), anyBoolean());
+
+        // One job is still ready. Exception should not be revoked.
+        doReturn(true).when(controller).wouldBeReadyWithConnectivityLocked(eq(redOne));
+        doReturn(false).when(controller).wouldBeReadyWithConnectivityLocked(eq(redTwo));
+        controller.reevaluateStateLocked(UID_RED);
+        inOrder.verify(mNetPolicyManagerInternal, never())
+                .setAppIdleWhitelist(eq(UID_RED), anyBoolean());
+        assertTrue(controller.isStandbyExceptionRequestedLocked(UID_RED));
+
+        // Both jobs are not ready. Exception should be revoked.
+        doReturn(false).when(controller).wouldBeReadyWithConnectivityLocked(any());
+        controller.reevaluateStateLocked(UID_RED);
+        inOrder.verify(mNetPolicyManagerInternal, times(1))
+                .setAppIdleWhitelist(eq(UID_RED), eq(false));
+        assertFalse(controller.isStandbyExceptionRequestedLocked(UID_RED));
+    }
+
+    @Test
+    public void testMaybeRevokeStandbyExceptionLocked() {
+        final ConnectivityController controller = new ConnectivityController(mService);
+        final JobStatus red = createJobStatus(createJob()
+                .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1), 0)
+                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED), UID_RED);
+        final JobStatus blue = createJobStatus(createJob()
+                .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1), 0)
+                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY), UID_BLUE);
+
+        InOrder inOrder = inOrder(mNetPolicyManagerInternal);
+        controller.requestStandbyExceptionLocked(red);
+        inOrder.verify(mNetPolicyManagerInternal, times(1))
+                .setAppIdleWhitelist(eq(UID_RED), eq(true));
+
+        // Try revoking for blue instead of red. Red should still have an exception requested.
+        controller.maybeRevokeStandbyExceptionLocked(blue);
+        inOrder.verify(mNetPolicyManagerInternal, never())
+                .setAppIdleWhitelist(anyInt(), anyBoolean());
+        assertTrue(controller.isStandbyExceptionRequestedLocked(UID_RED));
+
+        // Now revoke for red.
+        controller.maybeRevokeStandbyExceptionLocked(red);
+        inOrder.verify(mNetPolicyManagerInternal, times(1))
+                .setAppIdleWhitelist(eq(UID_RED), eq(false));
+        assertFalse(controller.isStandbyExceptionRequestedLocked(UID_RED));
+    }
+
+    private void answerNetwork(int uid, Network net, NetworkCapabilities caps) {
+        when(mConnManager.getActiveNetworkForUid(eq(uid))).thenReturn(net);
+        when(mConnManager.getNetworkCapabilities(eq(net))).thenReturn(caps);
+        if (net != null) {
+            final NetworkInfo ni = new NetworkInfo(ConnectivityManager.TYPE_WIFI, 0, null, null);
+            ni.setDetailedState(DetailedState.CONNECTED, null, null);
+            when(mConnManager.getNetworkInfoForUid(eq(net), eq(uid), anyBoolean())).thenReturn(ni);
+        }
+    }
+
+    private static NetworkCapabilities createCapabilities() {
+        return new NetworkCapabilities().addCapability(NET_CAPABILITY_INTERNET)
+                .addCapability(NET_CAPABILITY_VALIDATED);
+    }
+
+    private static JobInfo.Builder createJob() {
+        return createJob(101);
+    }
+
+    private static JobInfo.Builder createJob(int jobId) {
+        return new JobInfo.Builder(jobId, new ComponentName("foo", "bar"));
+    }
+
+    private static JobStatus createJobStatus(JobInfo.Builder job) {
+        return createJobStatus(job, android.os.Process.NOBODY_UID, 0, Long.MAX_VALUE);
+    }
+
+    private static JobStatus createJobStatus(JobInfo.Builder job, int uid) {
+        return createJobStatus(job, uid, 0, Long.MAX_VALUE);
+    }
+
+    private static JobStatus createJobStatus(JobInfo.Builder job,
+            long earliestRunTimeElapsedMillis, long latestRunTimeElapsedMillis) {
+        return createJobStatus(job, android.os.Process.NOBODY_UID,
+                earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis);
+    }
+
+    private static JobStatus createJobStatus(JobInfo.Builder job, int uid,
+            long earliestRunTimeElapsedMillis, long latestRunTimeElapsedMillis) {
+        return new JobStatus(job.build(), uid, null, -1, 0, 0, null,
+                earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis, 0, 0, null, 0);
+    }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
index b2ec835..3fc4e89 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
@@ -87,7 +87,6 @@
     private static final long HOUR_IN_MILLIS = 60 * MINUTE_IN_MILLIS;
     private static final String TAG_CLEANUP = "*job.cleanup*";
     private static final String TAG_QUOTA_CHECK = "*job.quota_check*";
-    private static final long IN_QUOTA_BUFFER_MILLIS = 30 * SECOND_IN_MILLIS;
     private static final int CALLING_UID = 1000;
     private static final String SOURCE_PACKAGE = "com.android.frameworks.mockingservicestests";
     private static final int SOURCE_USER_ID = 0;
@@ -365,7 +364,8 @@
         final long end = now - (2 * HOUR_IN_MILLIS - 5 * MINUTE_IN_MILLIS);
         // Counting backwards, the quota will come back one minute before the end.
         final long expectedAlarmTime =
-                end - MINUTE_IN_MILLIS + 2 * HOUR_IN_MILLIS + IN_QUOTA_BUFFER_MILLIS;
+                end - MINUTE_IN_MILLIS + 2 * HOUR_IN_MILLIS
+                        + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS;
         mQuotaController.saveTimingSession(0, "com.android.test",
                 new TimingSession(now - 2 * HOUR_IN_MILLIS, end, 1));
         mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
@@ -415,7 +415,8 @@
 
         // Test with timing sessions in window but still in quota.
         final long start = now - (6 * HOUR_IN_MILLIS);
-        final long expectedAlarmTime = start + 8 * HOUR_IN_MILLIS + IN_QUOTA_BUFFER_MILLIS;
+        final long expectedAlarmTime =
+                start + 8 * HOUR_IN_MILLIS + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS;
         mQuotaController.saveTimingSession(0, "com.android.test",
                 createTimingSession(start, 5 * MINUTE_IN_MILLIS, 1));
         mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
@@ -468,7 +469,8 @@
         // Counting backwards, the first minute in the session is over the allowed time, so it
         // needs to be excluded.
         final long expectedAlarmTime =
-                start + MINUTE_IN_MILLIS + 24 * HOUR_IN_MILLIS + IN_QUOTA_BUFFER_MILLIS;
+                start + MINUTE_IN_MILLIS + 24 * HOUR_IN_MILLIS
+                        + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS;
         mQuotaController.saveTimingSession(0, "com.android.test",
                 createTimingSession(start, 5 * MINUTE_IN_MILLIS, 1));
         mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
@@ -529,19 +531,22 @@
 
         // And down from there.
         final long expectedWorkingAlarmTime =
-                outOfQuotaTime + (2 * HOUR_IN_MILLIS) + IN_QUOTA_BUFFER_MILLIS;
+                outOfQuotaTime + (2 * HOUR_IN_MILLIS)
+                        + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS;
         mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", WORKING_INDEX);
         inOrder.verify(mAlarmManager, times(1))
                 .set(anyInt(), eq(expectedWorkingAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
 
         final long expectedFrequentAlarmTime =
-                outOfQuotaTime + (8 * HOUR_IN_MILLIS) + IN_QUOTA_BUFFER_MILLIS;
+                outOfQuotaTime + (8 * HOUR_IN_MILLIS)
+                        + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS;
         mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", FREQUENT_INDEX);
         inOrder.verify(mAlarmManager, times(1))
                 .set(anyInt(), eq(expectedFrequentAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
 
         final long expectedRareAlarmTime =
-                outOfQuotaTime + (24 * HOUR_IN_MILLIS) + IN_QUOTA_BUFFER_MILLIS;
+                outOfQuotaTime + (24 * HOUR_IN_MILLIS)
+                        + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS;
         mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", RARE_INDEX);
         inOrder.verify(mAlarmManager, times(1))
                 .set(anyInt(), eq(expectedRareAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/StateControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/StateControllerTest.java
new file mode 100644
index 0000000..db69242
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/StateControllerTest.java
@@ -0,0 +1,159 @@
+/*
+ * 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 com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
+
+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.SystemClock;
+import android.util.proto.ProtoOutputStream;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.LocalServices;
+import com.android.server.job.JobSchedulerService;
+import com.android.server.job.JobSchedulerService.Constants;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
+
+import java.time.Clock;
+import java.time.ZoneOffset;
+import java.util.function.Predicate;
+
+@RunWith(AndroidJUnit4.class)
+public class StateControllerTest {
+    private static final long SECOND_IN_MILLIS = 1000L;
+    private static final long MINUTE_IN_MILLIS = 60 * SECOND_IN_MILLIS;
+    private static final long HOUR_IN_MILLIS = 60 * MINUTE_IN_MILLIS;
+    private static final int CALLING_UID = 1000;
+    private static final String SOURCE_PACKAGE = "com.android.frameworks.mockingservicestests";
+    private static final int SOURCE_USER_ID = 0;
+
+    private Constants mConstants;
+    private StateController mStateController;
+
+    private MockitoSession mMockingSession;
+    @Mock
+    private AlarmManager mAlarmManager;
+    @Mock
+    private Context mContext;
+    @Mock
+    private JobSchedulerService mJobSchedulerService;
+
+    private class TestStateController extends StateController {
+        TestStateController(JobSchedulerService service) {
+            super(service);
+        }
+
+        public void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob) {
+        }
+
+        public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob,
+                boolean forUpdate) {
+        }
+
+        public void dumpControllerStateLocked(IndentingPrintWriter pw,
+                Predicate<JobStatus> predicate) {
+        }
+
+        public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId,
+                Predicate<JobStatus> predicate) {
+        }
+    }
+
+    @Before
+    public void setUp() {
+        mMockingSession = mockitoSession()
+                .initMocks(this)
+                .strictness(Strictness.LENIENT)
+                .mockStatic(LocalServices.class)
+                .startMocking();
+        // Use default constants for now.
+        mConstants = new Constants();
+
+        // Called in StateController constructor.
+        when(mJobSchedulerService.getTestableContext()).thenReturn(mContext);
+        when(mJobSchedulerService.getLock()).thenReturn(mJobSchedulerService);
+        when(mJobSchedulerService.getConstants()).thenReturn(mConstants);
+        // Called in QuotaController constructor.
+        // Used in JobStatus.
+        doReturn(mock(PackageManagerInternal.class))
+                .when(() -> LocalServices.getService(PackageManagerInternal.class));
+
+        // Freeze the clocks at this moment in time
+        JobSchedulerService.sSystemClock =
+                Clock.fixed(Clock.systemUTC().instant(), ZoneOffset.UTC);
+        JobSchedulerService.sUptimeMillisClock =
+                Clock.fixed(SystemClock.uptimeMillisClock().instant(), ZoneOffset.UTC);
+        JobSchedulerService.sElapsedRealtimeClock =
+                Clock.fixed(SystemClock.elapsedRealtimeClock().instant(), ZoneOffset.UTC);
+
+        // Initialize real objects.
+        mStateController = new TestStateController(mJobSchedulerService);
+    }
+
+    @After
+    public void tearDown() {
+        if (mMockingSession != null) {
+            mMockingSession.finishMocking();
+        }
+    }
+
+    private JobStatus createJobStatus(String testTag, int jobId) {
+        JobInfo jobInfo = new JobInfo.Builder(jobId,
+                new ComponentName(mContext, "TestQuotaJobService"))
+                .setMinimumLatency(Math.abs(jobId) + 1)
+                .build();
+        return JobStatus.createFromJobInfo(
+                jobInfo, CALLING_UID, SOURCE_PACKAGE, SOURCE_USER_ID, testTag);
+    }
+
+    @Test
+    public void testWouldBeReadyWithConstraintLocked() {
+        JobStatus job = spy(createJobStatus("testWouldBeReadyWithConstraintLocked", 1));
+
+        when(job.wouldBeReadyWithConstraint(anyInt())).thenReturn(false);
+        assertFalse(mStateController.wouldBeReadyWithConstraintLocked(job, 1));
+
+        when(job.wouldBeReadyWithConstraint(anyInt())).thenReturn(true);
+        when(mJobSchedulerService.areComponentsInPlaceLocked(job)).thenReturn(false);
+        assertFalse(mStateController.wouldBeReadyWithConstraintLocked(job, 1));
+
+        when(job.wouldBeReadyWithConstraint(anyInt())).thenReturn(true);
+        when(mJobSchedulerService.areComponentsInPlaceLocked(job)).thenReturn(true);
+        assertTrue(mStateController.wouldBeReadyWithConstraintLocked(job, 1));
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java b/services/tests/servicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java
deleted file mode 100644
index 5b59e60..0000000
--- a/services/tests/servicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java
+++ /dev/null
@@ -1,313 +0,0 @@
-/*
- * 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.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
-import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED;
-import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
-import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
-
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.when;
-
-import android.app.job.JobInfo;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.pm.PackageManagerInternal;
-import android.net.ConnectivityManager;
-import android.net.ConnectivityManager.NetworkCallback;
-import android.net.Network;
-import android.net.NetworkCapabilities;
-import android.net.NetworkInfo;
-import android.net.NetworkInfo.DetailedState;
-import android.net.NetworkPolicyManager;
-import android.os.Build;
-import android.os.SystemClock;
-import android.util.DataUnit;
-
-import com.android.server.LocalServices;
-import com.android.server.job.JobSchedulerService;
-import com.android.server.job.JobSchedulerService.Constants;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnitRunner;
-
-import java.time.Clock;
-import java.time.ZoneOffset;
-
-@RunWith(MockitoJUnitRunner.class)
-public class ConnectivityControllerTest {
-
-    @Mock private Context mContext;
-    @Mock private ConnectivityManager mConnManager;
-    @Mock private NetworkPolicyManager mNetPolicyManager;
-    @Mock private JobSchedulerService mService;
-
-    private Constants mConstants;
-
-    private static final int UID_RED = 10001;
-    private static final int UID_BLUE = 10002;
-
-    @Before
-    public void setUp() throws Exception {
-        // Assume all packages are current SDK
-        final PackageManagerInternal pm = mock(PackageManagerInternal.class);
-        when(pm.getPackageTargetSdkVersion(anyString()))
-                .thenReturn(Build.VERSION_CODES.CUR_DEVELOPMENT);
-        LocalServices.removeServiceForTest(PackageManagerInternal.class);
-        LocalServices.addService(PackageManagerInternal.class, pm);
-
-        // Freeze the clocks at this moment in time
-        JobSchedulerService.sSystemClock =
-                Clock.fixed(Clock.systemUTC().instant(), ZoneOffset.UTC);
-        JobSchedulerService.sUptimeMillisClock =
-                Clock.fixed(SystemClock.uptimeMillisClock().instant(), ZoneOffset.UTC);
-        JobSchedulerService.sElapsedRealtimeClock =
-                Clock.fixed(SystemClock.elapsedRealtimeClock().instant(), ZoneOffset.UTC);
-
-        // Assume default constants for now
-        mConstants = new Constants();
-
-        // Get our mocks ready
-        when(mContext.getSystemServiceName(ConnectivityManager.class))
-                .thenReturn(Context.CONNECTIVITY_SERVICE);
-        when(mContext.getSystemService(Context.CONNECTIVITY_SERVICE))
-                .thenReturn(mConnManager);
-        when(mContext.getSystemServiceName(NetworkPolicyManager.class))
-                .thenReturn(Context.NETWORK_POLICY_SERVICE);
-        when(mContext.getSystemService(Context.NETWORK_POLICY_SERVICE))
-                .thenReturn(mNetPolicyManager);
-        when(mService.getTestableContext()).thenReturn(mContext);
-        when(mService.getLock()).thenReturn(mService);
-        when(mService.getConstants()).thenReturn(mConstants);
-    }
-
-    @Test
-    public void testInsane() throws Exception {
-        final Network net = new Network(101);
-        final JobInfo.Builder job = createJob()
-                .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1))
-                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
-
-        // Slow network is too slow
-        assertFalse(ConnectivityController.isSatisfied(createJobStatus(job), net,
-                createCapabilities().setLinkUpstreamBandwidthKbps(1)
-                        .setLinkDownstreamBandwidthKbps(1), mConstants));
-        // Fast network looks great
-        assertTrue(ConnectivityController.isSatisfied(createJobStatus(job), net,
-                createCapabilities().setLinkUpstreamBandwidthKbps(1024)
-                        .setLinkDownstreamBandwidthKbps(1024), mConstants));
-    }
-
-    @Test
-    public void testCongestion() throws Exception {
-        final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
-        final JobInfo.Builder job = createJob()
-                .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1))
-                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
-        final JobStatus early = createJobStatus(job, now - 1000, now + 2000);
-        final JobStatus late = createJobStatus(job, now - 2000, now + 1000);
-
-        // Uncongested network is whenever
-        {
-            final Network net = new Network(101);
-            final NetworkCapabilities caps = createCapabilities()
-                    .addCapability(NET_CAPABILITY_NOT_CONGESTED);
-            assertTrue(ConnectivityController.isSatisfied(early, net, caps, mConstants));
-            assertTrue(ConnectivityController.isSatisfied(late, net, caps, mConstants));
-        }
-
-        // Congested network is more selective
-        {
-            final Network net = new Network(101);
-            final NetworkCapabilities caps = createCapabilities();
-            assertFalse(ConnectivityController.isSatisfied(early, net, caps, mConstants));
-            assertTrue(ConnectivityController.isSatisfied(late, net, caps, mConstants));
-        }
-    }
-
-    @Test
-    public void testRelaxed() throws Exception {
-        final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
-        final JobInfo.Builder job = createJob()
-                .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1))
-                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED);
-        final JobStatus early = createJobStatus(job, now - 1000, now + 2000);
-        final JobStatus late = createJobStatus(job, now - 2000, now + 1000);
-
-        job.setIsPrefetch(true);
-        final JobStatus earlyPrefetch = createJobStatus(job, now - 1000, now + 2000);
-        final JobStatus latePrefetch = createJobStatus(job, now - 2000, now + 1000);
-
-        // Unmetered network is whenever
-        {
-            final Network net = new Network(101);
-            final NetworkCapabilities caps = createCapabilities()
-                    .addCapability(NET_CAPABILITY_NOT_CONGESTED)
-                    .addCapability(NET_CAPABILITY_NOT_METERED);
-            assertTrue(ConnectivityController.isSatisfied(early, net, caps, mConstants));
-            assertTrue(ConnectivityController.isSatisfied(late, net, caps, mConstants));
-            assertTrue(ConnectivityController.isSatisfied(earlyPrefetch, net, caps, mConstants));
-            assertTrue(ConnectivityController.isSatisfied(latePrefetch, net, caps, mConstants));
-        }
-
-        // Metered network is only when prefetching and late
-        {
-            final Network net = new Network(101);
-            final NetworkCapabilities caps = createCapabilities()
-                    .addCapability(NET_CAPABILITY_NOT_CONGESTED);
-            assertFalse(ConnectivityController.isSatisfied(early, net, caps, mConstants));
-            assertFalse(ConnectivityController.isSatisfied(late, net, caps, mConstants));
-            assertFalse(ConnectivityController.isSatisfied(earlyPrefetch, net, caps, mConstants));
-            assertTrue(ConnectivityController.isSatisfied(latePrefetch, net, caps, mConstants));
-        }
-    }
-
-    @Test
-    public void testUpdates() throws Exception {
-        final ArgumentCaptor<NetworkCallback> callback = ArgumentCaptor
-                .forClass(NetworkCallback.class);
-        doNothing().when(mConnManager).registerNetworkCallback(any(), callback.capture());
-
-        final ConnectivityController controller = new ConnectivityController(mService);
-
-        final Network meteredNet = new Network(101);
-        final NetworkCapabilities meteredCaps = createCapabilities();
-        final Network unmeteredNet = new Network(202);
-        final NetworkCapabilities unmeteredCaps = createCapabilities()
-                .addCapability(NET_CAPABILITY_NOT_METERED);
-
-        final JobStatus red = createJobStatus(createJob()
-                .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1), 0)
-                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED), UID_RED);
-        final JobStatus blue = createJobStatus(createJob()
-                .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1), 0)
-                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY), UID_BLUE);
-
-        // Pretend we're offline when job is added
-        {
-            reset(mConnManager);
-            answerNetwork(UID_RED, null, null);
-            answerNetwork(UID_BLUE, null, null);
-
-            controller.maybeStartTrackingJobLocked(red, null);
-            controller.maybeStartTrackingJobLocked(blue, null);
-
-            assertFalse(red.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
-            assertFalse(blue.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
-        }
-
-        // Metered network
-        {
-            reset(mConnManager);
-            answerNetwork(UID_RED, meteredNet, meteredCaps);
-            answerNetwork(UID_BLUE, meteredNet, meteredCaps);
-
-            callback.getValue().onCapabilitiesChanged(meteredNet, meteredCaps);
-
-            assertFalse(red.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
-            assertTrue(blue.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
-        }
-
-        // Unmetered network background
-        {
-            reset(mConnManager);
-            answerNetwork(UID_RED, meteredNet, meteredCaps);
-            answerNetwork(UID_BLUE, meteredNet, meteredCaps);
-
-            callback.getValue().onCapabilitiesChanged(unmeteredNet, unmeteredCaps);
-
-            assertFalse(red.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
-            assertTrue(blue.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
-        }
-
-        // Lost metered network
-        {
-            reset(mConnManager);
-            answerNetwork(UID_RED, unmeteredNet, unmeteredCaps);
-            answerNetwork(UID_BLUE, unmeteredNet, unmeteredCaps);
-
-            callback.getValue().onLost(meteredNet);
-
-            assertTrue(red.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
-            assertTrue(blue.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
-        }
-
-        // Specific UID was blocked
-        {
-            reset(mConnManager);
-            answerNetwork(UID_RED, null, null);
-            answerNetwork(UID_BLUE, unmeteredNet, unmeteredCaps);
-
-            callback.getValue().onCapabilitiesChanged(unmeteredNet, unmeteredCaps);
-
-            assertFalse(red.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
-            assertTrue(blue.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
-        }
-    }
-
-    private void answerNetwork(int uid, Network net, NetworkCapabilities caps) {
-        when(mConnManager.getActiveNetworkForUid(eq(uid))).thenReturn(net);
-        when(mConnManager.getNetworkCapabilities(eq(net))).thenReturn(caps);
-        if (net != null) {
-            final NetworkInfo ni = new NetworkInfo(ConnectivityManager.TYPE_WIFI, 0, null, null);
-            ni.setDetailedState(DetailedState.CONNECTED, null, null);
-            when(mConnManager.getNetworkInfoForUid(eq(net), eq(uid), anyBoolean())).thenReturn(ni);
-        }
-    }
-
-    private static NetworkCapabilities createCapabilities() {
-        return new NetworkCapabilities().addCapability(NET_CAPABILITY_INTERNET)
-                .addCapability(NET_CAPABILITY_VALIDATED);
-    }
-
-    private static JobInfo.Builder createJob() {
-        return new JobInfo.Builder(101, new ComponentName("foo", "bar"));
-    }
-
-    private static JobStatus createJobStatus(JobInfo.Builder job) {
-        return createJobStatus(job, android.os.Process.NOBODY_UID, 0, Long.MAX_VALUE);
-    }
-
-    private static JobStatus createJobStatus(JobInfo.Builder job, int uid) {
-        return createJobStatus(job, uid, 0, Long.MAX_VALUE);
-    }
-
-    private static JobStatus createJobStatus(JobInfo.Builder job,
-            long earliestRunTimeElapsedMillis, long latestRunTimeElapsedMillis) {
-        return createJobStatus(job, android.os.Process.NOBODY_UID,
-                earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis);
-    }
-
-    private static JobStatus createJobStatus(JobInfo.Builder job, int uid,
-            long earliestRunTimeElapsedMillis, long latestRunTimeElapsedMillis) {
-        return new JobStatus(job.build(), uid, null, -1, 0, 0, null,
-                earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis, 0, 0, null, 0);
-    }
-}