diff options
| author | 2018-12-13 02:08:32 +0000 | |
|---|---|---|
| committer | 2018-12-13 02:08:32 +0000 | |
| commit | 0e2372d40d058dc54365763e2c951620c7674262 (patch) | |
| tree | eedb36ee358abf6c41a08feea7f493b052a96c4b | |
| parent | d92fd48b69ed4414e05ce7431ceaba6aa2a93eae (diff) | |
| parent | cdbfcb9021cd15fe764e184bd2f853d14df0cc83 (diff) | |
Merge "Requesting network exception for app idle jobs."
8 files changed, 1101 insertions, 322 deletions
diff --git a/services/core/java/com/android/server/job/JobSchedulerService.java b/services/core/java/com/android/server/job/JobSchedulerService.java index 611c8b728d0c..78e18e91ae73 100644 --- a/services/core/java/com/android/server/job/JobSchedulerService.java +++ b/services/core/java/com/android/server/job/JobSchedulerService.java @@ -839,6 +839,15 @@ 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); @@ -1042,6 +1051,8 @@ public class JobSchedulerService extends com.android.server.SystemService mJobPackageTracker.notePending(jobStatus); addOrderedItem(mPendingJobs, jobStatus, mEnqueueTimeComparator); maybeRunPendingJobsLocked(); + } else { + evaluateControllerStatesLocked(jobStatus); } } return JobScheduler.RESULT_SUCCESS; @@ -1884,6 +1895,8 @@ public class JobSchedulerService extends com.android.server.SystemService newReadyJobs = new ArrayList<JobStatus>(); } newReadyJobs.add(job); + } else { + evaluateControllerStatesLocked(job); } } @@ -1957,6 +1970,8 @@ public class JobSchedulerService extends com.android.server.SystemService runnableJobs = new ArrayList<>(); } runnableJobs.add(job); + } else { + evaluateControllerStatesLocked(job); } } @@ -2087,6 +2102,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. @@ -2219,6 +2243,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 6989c334d876..8f104e4a1525 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<ArraySet<JobStatus>> mTrackedJobs = new SparseArray<>(); + /** + * Keep track of all the UID's jobs that the controller has requested that NetworkPolicyManager + * grant an exception to in the app standby chain. + */ + @GuardedBy("mLock") + private final SparseArray<ArraySet<JobStatus>> mRequestedWhitelistJobs = new SparseArray<>(); + + /** List of currently available networks. */ + @GuardedBy("mLock") + private final ArraySet<Network> mAvailableNetworks = new ArraySet<>(); + public ConnectivityController(JobSchedulerService service) { super(service); mConnManager = mContext.getSystemService(ConnectivityManager.class); mNetPolicyManager = mContext.getSystemService(NetworkPolicyManager.class); + mNetPolicyManagerInternal = LocalServices.getService(NetworkPolicyManagerInternal.class); // We're interested in all network changes; internally we match these // network changes against the active network for each UID with jobs. @@ -109,7 +124,176 @@ 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<JobStatus> jobs = mRequestedWhitelistJobs.get(uid); + if (jobs == null) { + jobs = new ArraySet<JobStatus>(); + mRequestedWhitelistJobs.put(uid, jobs); + } + if (!jobs.add(job) || isExceptionRequested) { + if (DEBUG) { + Slog.i(TAG, "requestStandbyExceptionLocked found exception already requested."); + } + return; } + if (DEBUG) Slog.i(TAG, "Requesting standby exception for UID: " + uid); + mNetPolicyManagerInternal.setAppIdleWhitelist(uid, true); + } + + /** Returns whether a standby exception has been requested for the UID. */ + @VisibleForTesting + @GuardedBy("mLock") + boolean isStandbyExceptionRequestedLocked(final int uid) { + ArraySet jobs = mRequestedWhitelistJobs.get(uid); + return jobs != null && jobs.size() > 0; + } + + @VisibleForTesting + @GuardedBy("mLock") + boolean wouldBeReadyWithConnectivityLocked(JobStatus jobStatus) { + final boolean networkAvailable = isNetworkAvailable(jobStatus); + if (DEBUG) { + Slog.v(TAG, "wouldBeReadyWithConnectivityLocked: " + jobStatus.toShortString() + + " networkAvailable=" + networkAvailable); + } + // If the network isn't available, then requesting an exception won't help. + + return networkAvailable && wouldBeReadyWithConstraintLocked(jobStatus, + JobStatus.CONSTRAINT_CONNECTIVITY); + } + + /** + * Tell NetworkPolicyManager not to block a UID's network connection if that's the only + * thing stopping a job from running. + */ + @GuardedBy("mLock") + @Override + public void evaluateStateLocked(JobStatus jobStatus) { + if (mConstants.USE_HEARTBEATS) { + // This should only be used for the rolling quota system. + return; + } + + if (!jobStatus.hasConnectivityConstraint()) { + return; + } + + // Always check the full job readiness stat in case the component has been disabled. + if (wouldBeReadyWithConnectivityLocked(jobStatus)) { + if (DEBUG) { + Slog.i(TAG, "evaluateStateLocked finds job " + jobStatus + " would be ready."); + } + requestStandbyExceptionLocked(jobStatus); + } else { + if (DEBUG) { + Slog.i(TAG, "evaluateStateLocked finds job " + jobStatus + " would not be ready."); + } + maybeRevokeStandbyExceptionLocked(jobStatus); + } + } + + @GuardedBy("mLock") + @Override + public void reevaluateStateLocked(final int uid) { + if (mConstants.USE_HEARTBEATS) { + return; + } + // Check if we still need a connectivity exception in case the JobService was disabled. + ArraySet<JobStatus> jobs = mTrackedJobs.get(uid); + if (jobs == null) { + return; + } + for (int i = jobs.size() - 1; i >= 0; i--) { + evaluateStateLocked(jobs.valueAt(i)); + } + } + + /** Cancel the requested standby exception if none of the jobs would be ready to run anyway. */ + @VisibleForTesting + @GuardedBy("mLock") + void maybeRevokeStandbyExceptionLocked(final JobStatus job) { + final int uid = job.getSourceUid(); + if (!isStandbyExceptionRequestedLocked(uid)) { + return; + } + ArraySet<JobStatus> jobs = mRequestedWhitelistJobs.get(uid); + if (jobs == null) { + Slog.wtf(TAG, + "maybeRevokeStandbyExceptionLocked found null jobs array even though a " + + "standby exception has been requested."); + return; + } + if (!jobs.remove(job) || jobs.size() > 0) { + if (DEBUG) { + Slog.i(TAG, + "maybeRevokeStandbyExceptionLocked not revoking because there are still " + + jobs.size() + " jobs left."); + } + return; + } + // No more jobs that need an exception. + revokeStandbyExceptionLocked(uid); + } + + /** + * Tell NetworkPolicyManager to revoke any exception it granted from its standby policy chain + * for the uid. + */ + @GuardedBy("mLock") + private void revokeStandbyExceptionLocked(final int uid) { + if (DEBUG) Slog.i(TAG, "Revoking standby exception for UID: " + uid); + mNetPolicyManagerInternal.setAppIdleWhitelist(uid, false); + mRequestedWhitelistJobs.remove(uid); } /** @@ -326,6 +510,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) { Slog.v(TAG, "onCapabilitiesChanged: " + network); @@ -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<JobStatus> predicate) { + if (mRequestedWhitelistJobs.size() > 0) { + pw.print("Requested standby exceptions:"); + for (int i = 0; i < mRequestedWhitelistJobs.size(); i++) { + pw.print(" "); + pw.print(mRequestedWhitelistJobs.keyAt(i)); + pw.print(" ("); + pw.print(mRequestedWhitelistJobs.valueAt(i).size()); + pw.print(" jobs)"); + } + pw.println(); + } + if (mAvailableNetworks.size() > 0) { + pw.println("Available networks:"); + pw.increaseIndent(); + for (int i = 0; i < mAvailableNetworks.size(); i++) { + pw.println(mAvailableNetworks.valueAt(i)); + } + pw.decreaseIndent(); + } else { + pw.println("No available networks"); + } for (int i = 0; i < mTrackedJobs.size(); i++) { final ArraySet<JobStatus> jobs = mTrackedJobs.valueAt(i); for (int j = 0; j < jobs.size(); j++) { diff --git a/services/core/java/com/android/server/job/controllers/JobStatus.java b/services/core/java/com/android/server/job/controllers/JobStatus.java index 434158957c17..82bfa511507f 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 b439c0ddd028..61dc4799f221 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<JobStatus> predicate); public abstract void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId, diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java new file mode 100644 index 000000000000..8e78a5686b85 --- /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 b2ec83583eba..3fc4e89e80a8 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 000000000000..db69242538be --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/StateControllerTest.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.job.controllers; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyInt; + +import android.app.AlarmManager; +import android.app.job.JobInfo; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.PackageManagerInternal; +import android.os.SystemClock; +import android.util.proto.ProtoOutputStream; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.util.IndentingPrintWriter; +import com.android.server.LocalServices; +import com.android.server.job.JobSchedulerService; +import com.android.server.job.JobSchedulerService.Constants; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoSession; +import org.mockito.quality.Strictness; + +import java.time.Clock; +import java.time.ZoneOffset; +import java.util.function.Predicate; + +@RunWith(AndroidJUnit4.class) +public class StateControllerTest { + private static final long SECOND_IN_MILLIS = 1000L; + private static final long MINUTE_IN_MILLIS = 60 * SECOND_IN_MILLIS; + private static final long HOUR_IN_MILLIS = 60 * MINUTE_IN_MILLIS; + private static final int CALLING_UID = 1000; + private static final String SOURCE_PACKAGE = "com.android.frameworks.mockingservicestests"; + private static final int SOURCE_USER_ID = 0; + + private Constants mConstants; + private StateController mStateController; + + private MockitoSession mMockingSession; + @Mock + private AlarmManager mAlarmManager; + @Mock + private Context mContext; + @Mock + private JobSchedulerService mJobSchedulerService; + + private class TestStateController extends StateController { + TestStateController(JobSchedulerService service) { + super(service); + } + + public void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob) { + } + + public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob, + boolean forUpdate) { + } + + public void dumpControllerStateLocked(IndentingPrintWriter pw, + Predicate<JobStatus> predicate) { + } + + public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId, + Predicate<JobStatus> predicate) { + } + } + + @Before + public void setUp() { + mMockingSession = mockitoSession() + .initMocks(this) + .strictness(Strictness.LENIENT) + .mockStatic(LocalServices.class) + .startMocking(); + // Use default constants for now. + mConstants = new Constants(); + + // Called in StateController constructor. + when(mJobSchedulerService.getTestableContext()).thenReturn(mContext); + when(mJobSchedulerService.getLock()).thenReturn(mJobSchedulerService); + when(mJobSchedulerService.getConstants()).thenReturn(mConstants); + // Called in QuotaController constructor. + // Used in JobStatus. + doReturn(mock(PackageManagerInternal.class)) + .when(() -> LocalServices.getService(PackageManagerInternal.class)); + + // Freeze the clocks at this moment in time + JobSchedulerService.sSystemClock = + Clock.fixed(Clock.systemUTC().instant(), ZoneOffset.UTC); + JobSchedulerService.sUptimeMillisClock = + Clock.fixed(SystemClock.uptimeMillisClock().instant(), ZoneOffset.UTC); + JobSchedulerService.sElapsedRealtimeClock = + Clock.fixed(SystemClock.elapsedRealtimeClock().instant(), ZoneOffset.UTC); + + // Initialize real objects. + mStateController = new TestStateController(mJobSchedulerService); + } + + @After + public void tearDown() { + if (mMockingSession != null) { + mMockingSession.finishMocking(); + } + } + + private JobStatus createJobStatus(String testTag, int jobId) { + JobInfo jobInfo = new JobInfo.Builder(jobId, + new ComponentName(mContext, "TestQuotaJobService")) + .setMinimumLatency(Math.abs(jobId) + 1) + .build(); + return JobStatus.createFromJobInfo( + jobInfo, CALLING_UID, SOURCE_PACKAGE, SOURCE_USER_ID, testTag); + } + + @Test + public void testWouldBeReadyWithConstraintLocked() { + JobStatus job = spy(createJobStatus("testWouldBeReadyWithConstraintLocked", 1)); + + when(job.wouldBeReadyWithConstraint(anyInt())).thenReturn(false); + assertFalse(mStateController.wouldBeReadyWithConstraintLocked(job, 1)); + + when(job.wouldBeReadyWithConstraint(anyInt())).thenReturn(true); + when(mJobSchedulerService.areComponentsInPlaceLocked(job)).thenReturn(false); + assertFalse(mStateController.wouldBeReadyWithConstraintLocked(job, 1)); + + when(job.wouldBeReadyWithConstraint(anyInt())).thenReturn(true); + when(mJobSchedulerService.areComponentsInPlaceLocked(job)).thenReturn(true); + assertTrue(mStateController.wouldBeReadyWithConstraintLocked(job, 1)); + } +} diff --git a/services/tests/servicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java b/services/tests/servicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java deleted file mode 100644 index 5b59e607cba7..000000000000 --- a/services/tests/servicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java +++ /dev/null @@ -1,313 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.server.job.controllers; - -import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; -import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED; -import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED; -import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.when; - -import android.app.job.JobInfo; -import android.content.ComponentName; -import android.content.Context; -import android.content.pm.PackageManagerInternal; -import android.net.ConnectivityManager; -import android.net.ConnectivityManager.NetworkCallback; -import android.net.Network; -import android.net.NetworkCapabilities; -import android.net.NetworkInfo; -import android.net.NetworkInfo.DetailedState; -import android.net.NetworkPolicyManager; -import android.os.Build; -import android.os.SystemClock; -import android.util.DataUnit; - -import com.android.server.LocalServices; -import com.android.server.job.JobSchedulerService; -import com.android.server.job.JobSchedulerService.Constants; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; - -import java.time.Clock; -import java.time.ZoneOffset; - -@RunWith(MockitoJUnitRunner.class) -public class ConnectivityControllerTest { - - @Mock private Context mContext; - @Mock private ConnectivityManager mConnManager; - @Mock private NetworkPolicyManager mNetPolicyManager; - @Mock private JobSchedulerService mService; - - private Constants mConstants; - - private static final int UID_RED = 10001; - private static final int UID_BLUE = 10002; - - @Before - public void setUp() throws Exception { - // Assume all packages are current SDK - final PackageManagerInternal pm = mock(PackageManagerInternal.class); - when(pm.getPackageTargetSdkVersion(anyString())) - .thenReturn(Build.VERSION_CODES.CUR_DEVELOPMENT); - LocalServices.removeServiceForTest(PackageManagerInternal.class); - LocalServices.addService(PackageManagerInternal.class, pm); - - // Freeze the clocks at this moment in time - JobSchedulerService.sSystemClock = - Clock.fixed(Clock.systemUTC().instant(), ZoneOffset.UTC); - JobSchedulerService.sUptimeMillisClock = - Clock.fixed(SystemClock.uptimeMillisClock().instant(), ZoneOffset.UTC); - JobSchedulerService.sElapsedRealtimeClock = - Clock.fixed(SystemClock.elapsedRealtimeClock().instant(), ZoneOffset.UTC); - - // Assume default constants for now - mConstants = new Constants(); - - // Get our mocks ready - when(mContext.getSystemServiceName(ConnectivityManager.class)) - .thenReturn(Context.CONNECTIVITY_SERVICE); - when(mContext.getSystemService(Context.CONNECTIVITY_SERVICE)) - .thenReturn(mConnManager); - when(mContext.getSystemServiceName(NetworkPolicyManager.class)) - .thenReturn(Context.NETWORK_POLICY_SERVICE); - when(mContext.getSystemService(Context.NETWORK_POLICY_SERVICE)) - .thenReturn(mNetPolicyManager); - when(mService.getTestableContext()).thenReturn(mContext); - when(mService.getLock()).thenReturn(mService); - when(mService.getConstants()).thenReturn(mConstants); - } - - @Test - public void testInsane() throws Exception { - final Network net = new Network(101); - final JobInfo.Builder job = createJob() - .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1)) - .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY); - - // Slow network is too slow - assertFalse(ConnectivityController.isSatisfied(createJobStatus(job), net, - createCapabilities().setLinkUpstreamBandwidthKbps(1) - .setLinkDownstreamBandwidthKbps(1), mConstants)); - // Fast network looks great - assertTrue(ConnectivityController.isSatisfied(createJobStatus(job), net, - createCapabilities().setLinkUpstreamBandwidthKbps(1024) - .setLinkDownstreamBandwidthKbps(1024), mConstants)); - } - - @Test - public void testCongestion() throws Exception { - final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); - final JobInfo.Builder job = createJob() - .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1)) - .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY); - final JobStatus early = createJobStatus(job, now - 1000, now + 2000); - final JobStatus late = createJobStatus(job, now - 2000, now + 1000); - - // Uncongested network is whenever - { - final Network net = new Network(101); - final NetworkCapabilities caps = createCapabilities() - .addCapability(NET_CAPABILITY_NOT_CONGESTED); - assertTrue(ConnectivityController.isSatisfied(early, net, caps, mConstants)); - assertTrue(ConnectivityController.isSatisfied(late, net, caps, mConstants)); - } - - // Congested network is more selective - { - final Network net = new Network(101); - final NetworkCapabilities caps = createCapabilities(); - assertFalse(ConnectivityController.isSatisfied(early, net, caps, mConstants)); - assertTrue(ConnectivityController.isSatisfied(late, net, caps, mConstants)); - } - } - - @Test - public void testRelaxed() throws Exception { - final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); - final JobInfo.Builder job = createJob() - .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1)) - .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED); - final JobStatus early = createJobStatus(job, now - 1000, now + 2000); - final JobStatus late = createJobStatus(job, now - 2000, now + 1000); - - job.setIsPrefetch(true); - final JobStatus earlyPrefetch = createJobStatus(job, now - 1000, now + 2000); - final JobStatus latePrefetch = createJobStatus(job, now - 2000, now + 1000); - - // Unmetered network is whenever - { - final Network net = new Network(101); - final NetworkCapabilities caps = createCapabilities() - .addCapability(NET_CAPABILITY_NOT_CONGESTED) - .addCapability(NET_CAPABILITY_NOT_METERED); - assertTrue(ConnectivityController.isSatisfied(early, net, caps, mConstants)); - assertTrue(ConnectivityController.isSatisfied(late, net, caps, mConstants)); - assertTrue(ConnectivityController.isSatisfied(earlyPrefetch, net, caps, mConstants)); - assertTrue(ConnectivityController.isSatisfied(latePrefetch, net, caps, mConstants)); - } - - // Metered network is only when prefetching and late - { - final Network net = new Network(101); - final NetworkCapabilities caps = createCapabilities() - .addCapability(NET_CAPABILITY_NOT_CONGESTED); - assertFalse(ConnectivityController.isSatisfied(early, net, caps, mConstants)); - assertFalse(ConnectivityController.isSatisfied(late, net, caps, mConstants)); - assertFalse(ConnectivityController.isSatisfied(earlyPrefetch, net, caps, mConstants)); - assertTrue(ConnectivityController.isSatisfied(latePrefetch, net, caps, mConstants)); - } - } - - @Test - public void testUpdates() throws Exception { - final ArgumentCaptor<NetworkCallback> callback = ArgumentCaptor - .forClass(NetworkCallback.class); - doNothing().when(mConnManager).registerNetworkCallback(any(), callback.capture()); - - final ConnectivityController controller = new ConnectivityController(mService); - - final Network meteredNet = new Network(101); - final NetworkCapabilities meteredCaps = createCapabilities(); - final Network unmeteredNet = new Network(202); - final NetworkCapabilities unmeteredCaps = createCapabilities() - .addCapability(NET_CAPABILITY_NOT_METERED); - - final JobStatus red = createJobStatus(createJob() - .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1), 0) - .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED), UID_RED); - final JobStatus blue = createJobStatus(createJob() - .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1), 0) - .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY), UID_BLUE); - - // Pretend we're offline when job is added - { - reset(mConnManager); - answerNetwork(UID_RED, null, null); - answerNetwork(UID_BLUE, null, null); - - controller.maybeStartTrackingJobLocked(red, null); - controller.maybeStartTrackingJobLocked(blue, null); - - assertFalse(red.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); - assertFalse(blue.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); - } - - // Metered network - { - reset(mConnManager); - answerNetwork(UID_RED, meteredNet, meteredCaps); - answerNetwork(UID_BLUE, meteredNet, meteredCaps); - - callback.getValue().onCapabilitiesChanged(meteredNet, meteredCaps); - - assertFalse(red.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); - assertTrue(blue.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); - } - - // Unmetered network background - { - reset(mConnManager); - answerNetwork(UID_RED, meteredNet, meteredCaps); - answerNetwork(UID_BLUE, meteredNet, meteredCaps); - - callback.getValue().onCapabilitiesChanged(unmeteredNet, unmeteredCaps); - - assertFalse(red.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); - assertTrue(blue.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); - } - - // Lost metered network - { - reset(mConnManager); - answerNetwork(UID_RED, unmeteredNet, unmeteredCaps); - answerNetwork(UID_BLUE, unmeteredNet, unmeteredCaps); - - callback.getValue().onLost(meteredNet); - - assertTrue(red.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); - assertTrue(blue.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); - } - - // Specific UID was blocked - { - reset(mConnManager); - answerNetwork(UID_RED, null, null); - answerNetwork(UID_BLUE, unmeteredNet, unmeteredCaps); - - callback.getValue().onCapabilitiesChanged(unmeteredNet, unmeteredCaps); - - assertFalse(red.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); - assertTrue(blue.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); - } - } - - private void answerNetwork(int uid, Network net, NetworkCapabilities caps) { - when(mConnManager.getActiveNetworkForUid(eq(uid))).thenReturn(net); - when(mConnManager.getNetworkCapabilities(eq(net))).thenReturn(caps); - if (net != null) { - final NetworkInfo ni = new NetworkInfo(ConnectivityManager.TYPE_WIFI, 0, null, null); - ni.setDetailedState(DetailedState.CONNECTED, null, null); - when(mConnManager.getNetworkInfoForUid(eq(net), eq(uid), anyBoolean())).thenReturn(ni); - } - } - - private static NetworkCapabilities createCapabilities() { - return new NetworkCapabilities().addCapability(NET_CAPABILITY_INTERNET) - .addCapability(NET_CAPABILITY_VALIDATED); - } - - private static JobInfo.Builder createJob() { - return new JobInfo.Builder(101, new ComponentName("foo", "bar")); - } - - private static JobStatus createJobStatus(JobInfo.Builder job) { - return createJobStatus(job, android.os.Process.NOBODY_UID, 0, Long.MAX_VALUE); - } - - private static JobStatus createJobStatus(JobInfo.Builder job, int uid) { - return createJobStatus(job, uid, 0, Long.MAX_VALUE); - } - - private static JobStatus createJobStatus(JobInfo.Builder job, - long earliestRunTimeElapsedMillis, long latestRunTimeElapsedMillis) { - return createJobStatus(job, android.os.Process.NOBODY_UID, - earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis); - } - - private static JobStatus createJobStatus(JobInfo.Builder job, int uid, - long earliestRunTimeElapsedMillis, long latestRunTimeElapsedMillis) { - return new JobStatus(job.build(), uid, null, -1, 0, 0, null, - earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis, 0, 0, null, 0); - } -} |