Requesting network exception for app idle jobs.

When using the rolling quota system, jobs may have quota but not be
granted access to the network because of standard app idle restrictions.
This attempts to get the app access to the network only while the
relevant jobs are able to run. If there are no network-dependent jobs
for a UID that are able to run with connectivity granted, then the
exception is revoked.

Bug: 117846754
Bug: 111423978
Test: atest FrameworksMockingServicesTests
Change-Id: I9e3c4badd50dfdaa1c23f67534c0f02a4030a27c
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));
+    }
+}