From cdbfcb9021cd15fe764e184bd2f853d14df0cc83 Mon Sep 17 00:00:00 2001 From: Kweku Adams Date: Thu, 6 Dec 2018 17:05:15 -0800 Subject: [PATCH] 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 --- .../server/job/JobSchedulerService.java | 79 +++ .../controllers/ConnectivityController.java | 216 +++++++ .../server/job/controllers/JobStatus.java | 20 +- .../job/controllers/StateController.java | 36 ++ .../ConnectivityControllerTest.java | 581 ++++++++++++++++++ .../job/controllers/QuotaControllerTest.java | 19 +- .../job/controllers/StateControllerTest.java | 159 +++++ .../ConnectivityControllerTest.java | 313 ---------- 8 files changed, 1101 insertions(+), 322 deletions(-) create mode 100644 services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java create mode 100644 services/tests/mockingservicestests/src/com/android/server/job/controllers/StateControllerTest.java delete mode 100644 services/tests/servicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java diff --git a/services/core/java/com/android/server/job/JobSchedulerService.java b/services/core/java/com/android/server/job/JobSchedulerService.java index ea295de5909fa..4d07b205d6688 100644 --- a/services/core/java/com/android/server/job/JobSchedulerService.java +++ b/services/core/java/com/android/server/job/JobSchedulerService.java @@ -834,6 +834,15 @@ public class JobSchedulerService extends com.android.server.SystemService 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); @@ -1037,6 +1046,8 @@ public class JobSchedulerService extends com.android.server.SystemService mJobPackageTracker.notePending(jobStatus); addOrderedItem(mPendingJobs, jobStatus, mEnqueueTimeComparator); maybeRunPendingJobsLocked(); + } else { + evaluateControllerStatesLocked(jobStatus); } } return JobScheduler.RESULT_SUCCESS; @@ -1853,6 +1864,8 @@ public class JobSchedulerService extends com.android.server.SystemService newReadyJobs = new ArrayList(); } newReadyJobs.add(job); + } else { + evaluateControllerStatesLocked(job); } } @@ -1926,6 +1939,8 @@ public class JobSchedulerService extends com.android.server.SystemService runnableJobs = new ArrayList<>(); } runnableJobs.add(job); + } else { + evaluateControllerStatesLocked(job); } } @@ -2056,6 +2071,15 @@ public class JobSchedulerService extends com.android.server.SystemService 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. @@ -2184,6 +2208,61 @@ public class JobSchedulerService extends com.android.server.SystemService 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 6989c334d8767..8f104e4a1525b 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 android.util.proto.ProtoOutputStream; 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 @@ public final class ConnectivityController extends StateController implements 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> 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> mRequestedWhitelistJobs = new SparseArray<>(); + + /** List of currently available networks. */ + @GuardedBy("mLock") + private final ArraySet 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 @@ public final class ConnectivityController extends StateController implements 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 jobs = mRequestedWhitelistJobs.get(uid); + if (jobs == null) { + jobs = new ArraySet(); + 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 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 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. *

@@ -325,6 +509,14 @@ public final class ConnectivityController extends StateController implements } 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) { @@ -338,6 +530,9 @@ public final class ConnectivityController extends StateController implements if (DEBUG) { Slog.v(TAG, "onLost: " + network); } + synchronized (mLock) { + mAvailableNetworks.remove(network); + } updateTrackedJobs(-1, network); } }; @@ -356,6 +551,27 @@ public final class ConnectivityController extends StateController implements @Override public void dumpControllerStateLocked(IndentingPrintWriter pw, Predicate 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 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 6deecbd9a83b5..42cb4c389911f 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 @@ public final class JobStatus { * @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 @@ public final class JobStatus { // 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 @@ public final class JobStatus { * @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 b439c0ddd0280..61dc4799f2210 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 @@ import java.util.function.Predicate; * 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 abstract class StateController { 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 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 0000000000000..8e78a5686b85c --- /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 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 b2ec83583eba6..3fc4e89e80a87 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 @@ public class QuotaControllerTest { 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 @@ public class QuotaControllerTest { 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 @@ public class QuotaControllerTest { // 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 @@ public class QuotaControllerTest { // 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 @@ public class QuotaControllerTest { // 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 0000000000000..db69242538be5 --- /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 predicate) { + } + + public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId, + Predicate 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 5b59e607cba7c..0000000000000 --- 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 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); - } -}