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
This commit is contained in:
Kweku Adams
2018-12-06 17:05:15 -08:00
parent fa1c305db0
commit cdbfcb9021
8 changed files with 1101 additions and 322 deletions

View File

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

View File

@@ -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,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<JobStatus> jobs = mRequestedWhitelistJobs.get(uid);
if (jobs == null) {
jobs = new ArraySet<JobStatus>();
mRequestedWhitelistJobs.put(uid, jobs);
}
if (!jobs.add(job) || isExceptionRequested) {
if (DEBUG) {
Slog.i(TAG, "requestStandbyExceptionLocked found exception already requested.");
}
return;
}
if (DEBUG) Slog.i(TAG, "Requesting standby exception for UID: " + uid);
mNetPolicyManagerInternal.setAppIdleWhitelist(uid, true);
}
/** Returns whether a standby exception has been requested for the UID. */
@VisibleForTesting
@GuardedBy("mLock")
boolean isStandbyExceptionRequestedLocked(final int uid) {
ArraySet jobs = mRequestedWhitelistJobs.get(uid);
return jobs != null && jobs.size() > 0;
}
@VisibleForTesting
@GuardedBy("mLock")
boolean wouldBeReadyWithConnectivityLocked(JobStatus jobStatus) {
final boolean networkAvailable = isNetworkAvailable(jobStatus);
if (DEBUG) {
Slog.v(TAG, "wouldBeReadyWithConnectivityLocked: " + jobStatus.toShortString()
+ " networkAvailable=" + networkAvailable);
}
// If the network isn't available, then requesting an exception won't help.
return networkAvailable && wouldBeReadyWithConstraintLocked(jobStatus,
JobStatus.CONSTRAINT_CONNECTIVITY);
}
/**
* Tell NetworkPolicyManager not to block a UID's network connection if that's the only
* thing stopping a job from running.
*/
@GuardedBy("mLock")
@Override
public void evaluateStateLocked(JobStatus jobStatus) {
if (mConstants.USE_HEARTBEATS) {
// This should only be used for the rolling quota system.
return;
}
if (!jobStatus.hasConnectivityConstraint()) {
return;
}
// Always check the full job readiness stat in case the component has been disabled.
if (wouldBeReadyWithConnectivityLocked(jobStatus)) {
if (DEBUG) {
Slog.i(TAG, "evaluateStateLocked finds job " + jobStatus + " would be ready.");
}
requestStandbyExceptionLocked(jobStatus);
} else {
if (DEBUG) {
Slog.i(TAG, "evaluateStateLocked finds job " + jobStatus + " would not be ready.");
}
maybeRevokeStandbyExceptionLocked(jobStatus);
}
}
@GuardedBy("mLock")
@Override
public void reevaluateStateLocked(final int uid) {
if (mConstants.USE_HEARTBEATS) {
return;
}
// Check if we still need a connectivity exception in case the JobService was disabled.
ArraySet<JobStatus> jobs = mTrackedJobs.get(uid);
if (jobs == null) {
return;
}
for (int i = jobs.size() - 1; i >= 0; i--) {
evaluateStateLocked(jobs.valueAt(i));
}
}
/** Cancel the requested standby exception if none of the jobs would be ready to run anyway. */
@VisibleForTesting
@GuardedBy("mLock")
void maybeRevokeStandbyExceptionLocked(final JobStatus job) {
final int uid = job.getSourceUid();
if (!isStandbyExceptionRequestedLocked(uid)) {
return;
}
ArraySet<JobStatus> jobs = mRequestedWhitelistJobs.get(uid);
if (jobs == null) {
Slog.wtf(TAG,
"maybeRevokeStandbyExceptionLocked found null jobs array even though a "
+ "standby exception has been requested.");
return;
}
if (!jobs.remove(job) || jobs.size() > 0) {
if (DEBUG) {
Slog.i(TAG,
"maybeRevokeStandbyExceptionLocked not revoking because there are still "
+ jobs.size() + " jobs left.");
}
return;
}
// No more jobs that need an exception.
revokeStandbyExceptionLocked(uid);
}
/**
* Tell NetworkPolicyManager to revoke any exception it granted from its standby policy chain
* for the uid.
*/
@GuardedBy("mLock")
private void revokeStandbyExceptionLocked(final int uid) {
if (DEBUG) Slog.i(TAG, "Revoking standby exception for UID: " + uid);
mNetPolicyManagerInternal.setAppIdleWhitelist(uid, false);
mRequestedWhitelistJobs.remove(uid);
}
/**
* Test to see if running the given job on the given network is insane.
* <p>
@@ -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<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++) {

View File

@@ -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);

View File

@@ -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,

View File

@@ -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);
}
}

View File

@@ -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());

View File

@@ -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));
}
}

View File

@@ -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);
}
}