diff options
2 files changed, 562 insertions, 80 deletions
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java index df21d753ea1f..370b3c9e8f88 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java @@ -30,7 +30,6 @@ import android.net.ConnectivityManager.NetworkCallback; import android.net.INetworkPolicyListener; import android.net.Network; import android.net.NetworkCapabilities; -import android.net.NetworkInfo; import android.net.NetworkPolicyManager; import android.net.NetworkRequest; import android.os.Handler; @@ -43,8 +42,10 @@ import android.util.ArraySet; import android.util.DataUnit; import android.util.IndentingPrintWriter; import android.util.Log; +import android.util.Pools; import android.util.Slog; import android.util.SparseArray; +import android.util.TimeUtils; import android.util.proto.ProtoOutputStream; import com.android.internal.annotations.GuardedBy; @@ -55,6 +56,9 @@ import com.android.server.job.JobSchedulerService.Constants; import com.android.server.job.StateControllerProto; import com.android.server.net.NetworkPolicyManagerInternal; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; import java.util.Objects; import java.util.function.Predicate; @@ -73,6 +77,14 @@ public final class ConnectivityController extends RestrictingController implemen private static final boolean DEBUG = JobSchedulerService.DEBUG || Log.isLoggable(TAG, Log.DEBUG); + // The networking stack has a hard limit so we can't make this configurable. + private static final int MAX_NETWORK_CALLBACKS = 50; + /** + * Minimum amount of time that should have elapsed before we'll update a {@link UidStats} + * instance. + */ + private static final long MIN_STATS_UPDATE_INTERVAL_MS = 30_000L; + private final ConnectivityManager mConnManager; private final NetworkPolicyManager mNetPolicyManager; private final NetworkPolicyManagerInternal mNetPolicyManagerInternal; @@ -95,6 +107,69 @@ public final class ConnectivityController extends RestrictingController implemen @GuardedBy("mLock") private final ArrayMap<Network, NetworkCapabilities> mAvailableNetworks = new ArrayMap<>(); + private final SparseArray<UidDefaultNetworkCallback> mCurrentDefaultNetworkCallbacks = + new SparseArray<>(); + private final Comparator<UidStats> mUidStatsComparator = new Comparator<UidStats>() { + private int prioritizeExistence(int v1, int v2) { + if (v1 > 0 && v2 > 0) { + return 0; + } + return v2 - v1; + } + + @Override + public int compare(UidStats us1, UidStats us2) { + // TODO: build a better prioritization scheme + // Some things to use: + // * Proc state + // * IMPORTANT_WHILE_IN_FOREGROUND bit + final int runningPriority = prioritizeExistence(us1.numRunning, us2.numRunning); + if (runningPriority != 0) { + return runningPriority; + } + // Prioritize any UIDs that have jobs that would be ready ahead of UIDs that don't. + final int readyWithConnPriority = + prioritizeExistence(us1.numReadyWithConnectivity, us2.numReadyWithConnectivity); + if (readyWithConnPriority != 0) { + return readyWithConnPriority; + } + // They both have jobs that would be ready. Prioritize the UIDs whose requested + // network is available ahead of UIDs that don't have their requested network available. + final int reqAvailPriority = prioritizeExistence( + us1.numRequestedNetworkAvailable, us2.numRequestedNetworkAvailable); + if (reqAvailPriority != 0) { + return reqAvailPriority; + } + // They both have jobs with available networks. Prioritize based on: + // 1. (eventually) proc state + // 2. Existence of runnable EJs (not just requested) + // 3. Enqueue time + // TODO: maybe consider number of jobs + final int ejPriority = prioritizeExistence(us1.numEJs, us2.numEJs); + if (ejPriority != 0) { + return ejPriority; + } + // They both have EJs. Order them by EJ enqueue time to help provide low EJ latency. + if (us1.earliestEJEnqueueTime < us2.earliestEJEnqueueTime) { + return -1; + } else if (us1.earliestEJEnqueueTime > us2.earliestEJEnqueueTime) { + return 1; + } + if (us1.earliestEnqueueTime < us2.earliestEnqueueTime) { + return -1; + } + return us1.earliestEnqueueTime > us2.earliestEnqueueTime ? 1 : 0; + } + }; + private final SparseArray<UidStats> mUidStats = new SparseArray<>(); + private final Pools.Pool<UidDefaultNetworkCallback> mDefaultNetworkCallbackPool = + new Pools.SimplePool<>(MAX_NETWORK_CALLBACKS); + /** + * List of UidStats, sorted by priority as defined in {@link #mUidStatsComparator}. The sorting + * is only done in {@link #maybeAdjustRegisteredCallbacksLocked()} and may sometimes be stale. + */ + private final List<UidStats> mSortedStats = new ArrayList<>(); + private static final int MSG_DATA_SAVER_TOGGLED = 0; private static final int MSG_UID_RULES_CHANGES = 1; private static final int MSG_REEVALUATE_JOBS = 2; @@ -121,7 +196,14 @@ public final class ConnectivityController extends RestrictingController implemen @Override public void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob) { if (jobStatus.hasConnectivityConstraint()) { - updateConstraintsSatisfied(jobStatus); + UidStats uidStats = mUidStats.get(jobStatus.getSourceUid()); + if (uidStats == null) { + uidStats = new UidStats(jobStatus.getSourceUid()); + mUidStats.append(jobStatus.getSourceUid(), uidStats); + } + if (wouldBeReadyWithConstraintLocked(jobStatus, JobStatus.CONSTRAINT_CONNECTIVITY)) { + uidStats.numReadyWithConnectivity++; + } ArraySet<JobStatus> jobs = mTrackedJobs.get(jobStatus.getSourceUid()); if (jobs == null) { jobs = new ArraySet<>(); @@ -129,6 +211,16 @@ public final class ConnectivityController extends RestrictingController implemen } jobs.add(jobStatus); jobStatus.setTrackingController(JobStatus.TRACKING_CONNECTIVITY); + updateConstraintsSatisfied(jobStatus); + } + } + + @GuardedBy("mLock") + @Override + public void prepareForExecutionLocked(JobStatus jobStatus) { + if (jobStatus.hasConnectivityConstraint()) { + UidStats uidStats = mUidStats.get(jobStatus.getSourceUid()); + uidStats.numRunning++; } } @@ -141,7 +233,13 @@ public final class ConnectivityController extends RestrictingController implemen if (jobs != null) { jobs.remove(jobStatus); } + UidStats us = mUidStats.get(jobStatus.getSourceUid()); + us.numReadyWithConnectivity--; + if (jobStatus.madeActive != 0) { + us.numRunning--; + } maybeRevokeStandbyExceptionLocked(jobStatus); + maybeAdjustRegisteredCallbacksLocked(); } } @@ -229,6 +327,8 @@ public final class ConnectivityController extends RestrictingController implemen return; } + UidStats uidStats = mUidStats.get(jobStatus.getSourceUid()); + if (jobStatus.shouldTreatAsExpeditedJob()) { if (!jobStatus.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)) { // Don't request a direct hole through any of the firewalls. Instead, mark the @@ -252,11 +352,15 @@ public final class ConnectivityController extends RestrictingController implemen if (DEBUG) { Slog.i(TAG, "evaluateStateLocked finds job " + jobStatus + " would be ready."); } + uidStats.numReadyWithConnectivity++; requestStandbyExceptionLocked(jobStatus); } else { if (DEBUG) { Slog.i(TAG, "evaluateStateLocked finds job " + jobStatus + " would not be ready."); } + // Don't decrement numReadyWithConnectivity here because we don't know if it was + // incremented for this job. The count will be set properly in + // maybeAdjustRegisteredCallbacksLocked(). maybeRevokeStandbyExceptionLocked(jobStatus); } } @@ -316,6 +420,30 @@ public final class ConnectivityController extends RestrictingController implemen @Override public void onAppRemovedLocked(String pkgName, int uid) { mTrackedJobs.delete(uid); + UidStats uidStats = mUidStats.removeReturnOld(uid); + unregisterDefaultNetworkCallbackLocked(uid, sElapsedRealtimeClock.millis()); + mSortedStats.remove(uidStats); + registerPendingUidCallbacksLocked(); + } + + @GuardedBy("mLock") + @Override + public void onUserRemovedLocked(int userId) { + final long nowElapsed = sElapsedRealtimeClock.millis(); + for (int u = mUidStats.size() - 1; u >= 0; --u) { + UidStats uidStats = mUidStats.valueAt(u); + if (UserHandle.getUserId(uidStats.uid) == userId) { + unregisterDefaultNetworkCallbackLocked(uidStats.uid, nowElapsed); + mSortedStats.remove(uidStats); + mUidStats.removeAt(u); + } + } + maybeAdjustRegisteredCallbacksLocked(); + } + + private boolean isUsable(NetworkCapabilities capabilities) { + return capabilities != null + && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED); } /** @@ -428,6 +556,8 @@ public final class ConnectivityController extends RestrictingController implemen // Zeroth, we gotta have a network to think about being satisfied if (network == null || capabilities == null) return false; + if (!isUsable(capabilities)) return false; + // First, are we insane? if (isInsane(jobStatus, network, capabilities, constants)) return false; @@ -443,6 +573,156 @@ public final class ConnectivityController extends RestrictingController implemen return false; } + @GuardedBy("mLock") + private void maybeRegisterDefaultNetworkCallbackLocked(JobStatus jobStatus) { + final int sourceUid = jobStatus.getSourceUid(); + if (mCurrentDefaultNetworkCallbacks.contains(sourceUid)) { + return; + } + UidStats uidStats = mUidStats.get(sourceUid); + if (!mSortedStats.contains(uidStats)) { + mSortedStats.add(uidStats); + } + if (mCurrentDefaultNetworkCallbacks.size() >= MAX_NETWORK_CALLBACKS) { + // TODO: offload to handler + maybeAdjustRegisteredCallbacksLocked(); + return; + } + registerPendingUidCallbacksLocked(); + } + + /** + * Register UID callbacks for UIDs that are next in line, based on the current order in {@link + * #mSortedStats}. This assumes that there are only registered callbacks for UIDs in the top + * {@value #MAX_NETWORK_CALLBACKS} UIDs and that the only UIDs missing callbacks are the lower + * priority ones. + */ + @GuardedBy("mLock") + private void registerPendingUidCallbacksLocked() { + final int numCallbacks = mCurrentDefaultNetworkCallbacks.size(); + final int numPending = mSortedStats.size(); + if (numPending < numCallbacks) { + // This means there's a bug in the code >.< + Slog.wtf(TAG, "There are more registered callbacks than sorted UIDs: " + + numCallbacks + " vs " + numPending); + } + for (int i = numCallbacks; i < numPending && i < MAX_NETWORK_CALLBACKS; ++i) { + UidStats uidStats = mSortedStats.get(i); + UidDefaultNetworkCallback callback = mDefaultNetworkCallbackPool.acquire(); + if (callback == null) { + callback = new UidDefaultNetworkCallback(); + } + callback.setUid(uidStats.uid); + mCurrentDefaultNetworkCallbacks.append(uidStats.uid, callback); + mConnManager.registerDefaultNetworkCallbackAsUid(uidStats.uid, callback, mHandler); + } + } + + @GuardedBy("mLock") + private void maybeAdjustRegisteredCallbacksLocked() { + final int count = mUidStats.size(); + if (count == mCurrentDefaultNetworkCallbacks.size()) { + // All of them are registered and there are no blocked UIDs. + // No point evaluating all UIDs. + return; + } + final long nowElapsed = sElapsedRealtimeClock.millis(); + mSortedStats.clear(); + + for (int u = 0; u < mUidStats.size(); ++u) { + UidStats us = mUidStats.valueAt(u); + ArraySet<JobStatus> jobs = mTrackedJobs.get(us.uid); + if (jobs == null || jobs.size() == 0) { + unregisterDefaultNetworkCallbackLocked(us.uid, nowElapsed); + continue; + } + + // We won't evaluate stats in the first 30 seconds after boot...That's probably okay. + if (us.lastUpdatedElapsed + MIN_STATS_UPDATE_INTERVAL_MS < nowElapsed) { + us.earliestEnqueueTime = Long.MAX_VALUE; + us.earliestEJEnqueueTime = Long.MAX_VALUE; + us.numReadyWithConnectivity = 0; + us.numRequestedNetworkAvailable = 0; + us.numRegular = 0; + us.numEJs = 0; + + for (int j = 0; j < jobs.size(); ++j) { + JobStatus job = jobs.valueAt(j); + us.earliestEnqueueTime = Math.min(us.earliestEnqueueTime, job.enqueueTime); + if (wouldBeReadyWithConstraintLocked(job, JobStatus.CONSTRAINT_CONNECTIVITY)) { + us.numReadyWithConnectivity++; + if (isNetworkAvailable(job)) { + us.numRequestedNetworkAvailable++; + } + } + if (job.shouldTreatAsExpeditedJob() || job.startedAsExpeditedJob) { + us.numEJs++; + us.earliestEJEnqueueTime = + Math.min(us.earliestEJEnqueueTime, job.enqueueTime); + } else { + us.numRegular++; + } + } + + us.lastUpdatedElapsed = nowElapsed; + } + mSortedStats.add(us); + } + + mSortedStats.sort(mUidStatsComparator); + + boolean changed = false; + // Iterate in reverse order to remove existing callbacks before adding new ones. + for (int i = mSortedStats.size() - 1; i >= 0; --i) { + UidStats us = mSortedStats.get(i); + if (i >= MAX_NETWORK_CALLBACKS) { + changed |= unregisterDefaultNetworkCallbackLocked(us.uid, nowElapsed); + } else { + UidDefaultNetworkCallback defaultNetworkCallback = + mCurrentDefaultNetworkCallbacks.get(us.uid); + if (defaultNetworkCallback == null) { + // Not already registered. + defaultNetworkCallback = mDefaultNetworkCallbackPool.acquire(); + if (defaultNetworkCallback == null) { + defaultNetworkCallback = new UidDefaultNetworkCallback(); + } + defaultNetworkCallback.setUid(us.uid); + mCurrentDefaultNetworkCallbacks.append(us.uid, defaultNetworkCallback); + mConnManager.registerDefaultNetworkCallbackAsUid( + us.uid, defaultNetworkCallback, mHandler); + } + } + } + if (changed) { + mStateChangedListener.onControllerStateChanged(); + } + } + + @GuardedBy("mLock") + private boolean unregisterDefaultNetworkCallbackLocked(int uid, long nowElapsed) { + UidDefaultNetworkCallback defaultNetworkCallback = mCurrentDefaultNetworkCallbacks.get(uid); + if (defaultNetworkCallback == null) { + return false; + } + mCurrentDefaultNetworkCallbacks.remove(uid); + mConnManager.unregisterNetworkCallback(defaultNetworkCallback); + mDefaultNetworkCallbackPool.release(defaultNetworkCallback); + defaultNetworkCallback.clear(); + + boolean changed = false; + final ArraySet<JobStatus> jobs = mTrackedJobs.get(uid); + if (jobs != null) { + // Since we're unregistering the callback, we can no longer monitor + // changes to the app's network and so we should just mark the + // connectivity constraint as not satisfied. + for (int j = jobs.size() - 1; j >= 0; --j) { + changed |= updateConstraintsSatisfied( + jobs.valueAt(j), nowElapsed, null, null); + } + } + return changed; + } + @Nullable private NetworkCapabilities getNetworkCapabilities(@Nullable Network network) { if (network == null) { @@ -451,43 +731,34 @@ public final class ConnectivityController extends RestrictingController implemen synchronized (mLock) { // There is technically a race here if the Network object is reused. This can happen // only if that Network disconnects and the auto-incrementing network ID in - // ConnectivityService wraps. This should no longer be a concern if/when we only make + // ConnectivityService wraps. This shouldn't be a concern since we only make // use of asynchronous calls. - if (mAvailableNetworks.get(network) != null) { - return mAvailableNetworks.get(network); - } - - // This should almost never happen because any time a new network connects, the - // NetworkCallback would populate mAvailableNetworks. However, it's currently necessary - // because we also call synchronous methods such as getActiveNetworkForUid. - // TODO(134978280): remove after switching to callback-based APIs - final NetworkCapabilities capabilities = mConnManager.getNetworkCapabilities(network); - mAvailableNetworks.put(network, capabilities); - return capabilities; + return mAvailableNetworks.get(network); } } private boolean updateConstraintsSatisfied(JobStatus jobStatus) { - final Network network = mConnManager.getActiveNetworkForUid( - jobStatus.getSourceUid(), jobStatus.shouldIgnoreNetworkBlocking()); + final long nowElapsed = sElapsedRealtimeClock.millis(); + final UidDefaultNetworkCallback defaultNetworkCallback = + mCurrentDefaultNetworkCallbacks.get(jobStatus.getSourceUid()); + if (defaultNetworkCallback == null) { + maybeRegisterDefaultNetworkCallbackLocked(jobStatus); + return updateConstraintsSatisfied(jobStatus, nowElapsed, null, null); + } + final Network network = + (jobStatus.shouldIgnoreNetworkBlocking() || !defaultNetworkCallback.mBlocked) + ? defaultNetworkCallback.mDefaultNetwork : null; final NetworkCapabilities capabilities = getNetworkCapabilities(network); - return updateConstraintsSatisfied(jobStatus, sElapsedRealtimeClock.millis(), - network, capabilities); + return updateConstraintsSatisfied(jobStatus, nowElapsed, network, capabilities); } private boolean updateConstraintsSatisfied(JobStatus jobStatus, final long nowElapsed, Network network, NetworkCapabilities capabilities) { - // TODO: consider matching against non-active networks - - final boolean ignoreBlocked = jobStatus.shouldIgnoreNetworkBlocking(); - final NetworkInfo info = mConnManager.getNetworkInfoForUid(network, - jobStatus.getSourceUid(), ignoreBlocked); + // TODO: consider matching against non-default networks - final boolean connected = (info != null) && info.isConnected(); final boolean satisfied = isSatisfied(jobStatus, network, capabilities, mConstants); - final boolean changed = jobStatus - .setConnectivityConstraintSatisfied(nowElapsed, connected && satisfied); + final boolean changed = jobStatus.setConnectivityConstraintSatisfied(nowElapsed, satisfied); // Pass along the evaluated network for job to use; prevents race // conditions as default routes change over time, and opens the door to @@ -496,7 +767,7 @@ public final class ConnectivityController extends RestrictingController implemen if (DEBUG) { Slog.i(TAG, "Connectivity " + (changed ? "CHANGED" : "unchanged") - + " for " + jobStatus + ": connected=" + connected + + " for " + jobStatus + ": usable=" + isUsable(capabilities) + " satisfied=" + satisfied); } return changed; @@ -531,15 +802,24 @@ public final class ConnectivityController extends RestrictingController implemen return false; } - final Network network = - mConnManager.getActiveNetworkForUid(jobs.valueAt(0).getSourceUid(), false); + UidDefaultNetworkCallback defaultNetworkCallback = + mCurrentDefaultNetworkCallbacks.get(jobs.valueAt(0).getSourceUid()); + if (defaultNetworkCallback == null) { + maybeRegisterDefaultNetworkCallbackLocked(jobs.valueAt(0)); + return false; + } + + final Network network = defaultNetworkCallback.mBlocked + ? null : defaultNetworkCallback.mDefaultNetwork; final NetworkCapabilities capabilities = getNetworkCapabilities(network); final boolean networkMatch = (filterNetwork == null || Objects.equals(filterNetwork, network)); - boolean exemptedLoaded = false; - Network exemptedNetwork = null; - NetworkCapabilities exemptedNetworkCapabilities = null; - boolean exemptedNetworkMatch = false; + // Ignore blocked + final Network exemptedNetwork = defaultNetworkCallback.mDefaultNetwork; + final NetworkCapabilities exemptedNetworkCapabilities = + getNetworkCapabilities(exemptedNetwork); + final boolean exemptedNetworkMatch = + (filterNetwork == null || Objects.equals(filterNetwork, exemptedNetwork)); final long nowElapsed = sElapsedRealtimeClock.millis(); boolean changed = false; @@ -551,13 +831,6 @@ public final class ConnectivityController extends RestrictingController implemen boolean match = networkMatch; if (js.shouldIgnoreNetworkBlocking()) { - if (!exemptedLoaded) { - exemptedLoaded = true; - exemptedNetwork = mConnManager.getActiveNetworkForUid(js.getSourceUid(), true); - exemptedNetworkCapabilities = getNetworkCapabilities(exemptedNetwork); - exemptedNetworkMatch = (filterNetwork == null - || Objects.equals(filterNetwork, exemptedNetwork)); - } net = exemptedNetwork; netCap = exemptedNetworkCapabilities; match = exemptedNetworkMatch; @@ -612,6 +885,7 @@ public final class ConnectivityController extends RestrictingController implemen mAvailableNetworks.put(network, capabilities); } updateTrackedJobs(-1, network); + maybeAdjustRegisteredCallbacksLocked(); } @Override @@ -621,8 +895,15 @@ public final class ConnectivityController extends RestrictingController implemen } synchronized (mLock) { mAvailableNetworks.remove(network); + for (int u = 0; u < mCurrentDefaultNetworkCallbacks.size(); ++u) { + UidDefaultNetworkCallback callback = mCurrentDefaultNetworkCallbacks.valueAt(u); + if (Objects.equals(callback.mDefaultNetwork, network)) { + callback.mDefaultNetwork = null; + } + } } updateTrackedJobs(-1, network); + maybeAdjustRegisteredCallbacksLocked(); } }; @@ -665,12 +946,142 @@ public final class ConnectivityController extends RestrictingController implemen } } } - }; + } + + private class UidDefaultNetworkCallback extends NetworkCallback { + private int mUid; + @Nullable + private Network mDefaultNetwork; + private boolean mBlocked; + + private void setUid(int uid) { + mUid = uid; + mDefaultNetwork = null; + } + + private void clear() { + mDefaultNetwork = null; + mUid = UserHandle.USER_NULL; + } + + @Override + public void onAvailable(Network network) { + if (DEBUG) Slog.v(TAG, "default-onAvailable(" + mUid + "): " + network); + } + + @Override + public void onBlockedStatusChanged(Network network, boolean blocked) { + if (DEBUG) { + Slog.v(TAG, "default-onBlockedStatusChanged(" + mUid + "): " + + network + " -> " + blocked); + } + if (mUid == UserHandle.USER_NULL) { + return; + } + synchronized (mLock) { + mDefaultNetwork = network; + mBlocked = blocked; + } + updateTrackedJobs(mUid, network); + } + + // Network transitions have some complicated behavior that JS doesn't handle very well. + // + // * If the default network changes from A to B without A disconnecting, then we'll only + // get onAvailable(B) (and the subsequent onBlockedStatusChanged() call). Since we get + // the onBlockedStatusChanged() call, we re-evaluate the job, but keep it running + // (assuming the new network satisfies constraints). The app continues to use the old + // network (if they use the network object provided through JobParameters.getNetwork()) + // because we don't notify them of the default network change. If the old network no + // longer satisfies requested constraints, then we have a problem. Depending on the order + // of calls, if the per-UID callback gets notified of the network change before the + // general callback gets notified of the capabilities change, then the job's network + // object will point to the new network and we won't stop the job, even though we told it + // to use the old network that no longer satisfies its constraints. This is the behavior + // we loosely had (ignoring race conditions between asynchronous and synchronous + // connectivity calls) when we were calling the synchronous getActiveNetworkForUid() API. + // However, we should fix it. + // TODO: stop jobs when the existing capabilities change after default network change + // + // * If the default network changes from A to B because A disconnected, then we'll get + // onLost(A) and then onAvailable(B). In this case, there will be a short period where JS + // doesn't think there's an available network for the job, so we'll stop the job even + // though onAvailable(B) will be called soon. One on hand, the app would have gotten a + // network error as well because of A's disconnect, and this will allow JS to provide the + // job with the new default network. On the other hand, we have to stop the job even + // though it could have continued running with the new network and the job has to deal + // with whatever backoff policy is set. For now, the current behavior is fine, but we may + // want to see if there's a way to have a smoother transition. + + @Override + public void onLost(Network network) { + if (DEBUG) { + Slog.v(TAG, "default-onLost(" + mUid + "): " + network); + } + if (mUid == UserHandle.USER_NULL) { + return; + } + synchronized (mLock) { + if (Objects.equals(mDefaultNetwork, network)) { + mDefaultNetwork = null; + } + } + updateTrackedJobs(mUid, network); + } + + private void dumpLocked(IndentingPrintWriter pw) { + pw.print("UID: "); + pw.print(mUid); + pw.print("; "); + if (mDefaultNetwork == null) { + pw.print("No network"); + } else { + pw.print("Network: "); + pw.print(mDefaultNetwork); + pw.print(" (blocked="); + pw.print(mBlocked); + pw.print(")"); + } + pw.println(); + } + } + + private static class UidStats { + public final int uid; + public int numRunning; + public int numReadyWithConnectivity; + public int numRequestedNetworkAvailable; + public int numEJs; + public int numRegular; + public long earliestEnqueueTime; + public long earliestEJEnqueueTime; + public long lastUpdatedElapsed; + + private UidStats(int uid) { + this.uid = uid; + } + + private void dumpLocked(IndentingPrintWriter pw, final long nowElapsed) { + pw.print("UidStats{"); + pw.print("uid", uid); + pw.print("#run", numRunning); + pw.print("#readyWithConn", numReadyWithConnectivity); + pw.print("#netAvail", numRequestedNetworkAvailable); + pw.print("#EJs", numEJs); + pw.print("#reg", numRegular); + pw.print("earliestEnqueue", earliestEnqueueTime); + pw.print("earliestEJEnqueue", earliestEJEnqueueTime); + pw.print("updated="); + TimeUtils.formatDuration(lastUpdatedElapsed - nowElapsed, pw); + pw.println("}"); + } + } @GuardedBy("mLock") @Override public void dumpControllerStateLocked(IndentingPrintWriter pw, Predicate<JobStatus> predicate) { + final long nowElapsed = sElapsedRealtimeClock.millis(); if (mRequestedWhitelistJobs.size() > 0) { pw.print("Requested standby exceptions:"); @@ -695,6 +1106,26 @@ public final class ConnectivityController extends RestrictingController implemen } else { pw.println("No available networks"); } + pw.println(); + + pw.println("Current default network callbacks:"); + pw.increaseIndent(); + for (int i = 0; i < mCurrentDefaultNetworkCallbacks.size(); i++) { + mCurrentDefaultNetworkCallbacks.valueAt(i).dumpLocked(pw); + } + pw.decreaseIndent(); + pw.println(); + + pw.println("UID Pecking Order:"); + pw.increaseIndent(); + for (int i = 0; i < mSortedStats.size(); ++i) { + pw.print(i); + pw.print(": "); + mSortedStats.get(i).dumpLocked(pw, nowElapsed); + } + pw.decreaseIndent(); + pw.println(); + 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/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java index 3870b02ba37c..1b8f9c767e3e 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java @@ -19,6 +19,7 @@ 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_NOT_SUSPENDED; import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED; import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED; import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; @@ -41,10 +42,11 @@ 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.annotation.NonNull; +import android.annotation.Nullable; import android.app.job.JobInfo; import android.content.ComponentName; import android.content.Context; @@ -53,8 +55,6 @@ 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.Looper; @@ -136,7 +136,7 @@ public class ConnectivityControllerTest { } @Test - public void testInsane() throws Exception { + public void testUsable() throws Exception { final Network net = new Network(101); final JobInfo.Builder job = createJob() .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1), @@ -192,6 +192,30 @@ public class ConnectivityControllerTest { } @Test + public void testInsane() throws Exception { + final Network net = new Network(101); + final JobInfo.Builder job = createJob() + .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1), + DataUnit.MEBIBYTES.toBytes(1)) + .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY); + + final ConnectivityController controller = new ConnectivityController(mService); + when(mService.getMaxJobExecutionTimeMs(any())).thenReturn(10 * 60_000L); + + + // Suspended networks aren't usable. + assertFalse(controller.isSatisfied(createJobStatus(job), net, + createCapabilities().removeCapability(NET_CAPABILITY_NOT_SUSPENDED) + .setLinkUpstreamBandwidthKbps(1024).setLinkDownstreamBandwidthKbps(1024), + mConstants)); + + // Not suspended networks are usable. + assertTrue(controller.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() @@ -263,9 +287,17 @@ public class ConnectivityControllerTest { @Test public void testUpdates() throws Exception { - final ArgumentCaptor<NetworkCallback> callback = ArgumentCaptor - .forClass(NetworkCallback.class); - doNothing().when(mConnManager).registerNetworkCallback(any(), callback.capture()); + final ArgumentCaptor<NetworkCallback> callbackCaptor = + ArgumentCaptor.forClass(NetworkCallback.class); + doNothing().when(mConnManager).registerNetworkCallback(any(), callbackCaptor.capture()); + final ArgumentCaptor<NetworkCallback> redCallbackCaptor = + ArgumentCaptor.forClass(NetworkCallback.class); + doNothing().when(mConnManager).registerDefaultNetworkCallbackAsUid( + eq(UID_RED), redCallbackCaptor.capture(), any()); + final ArgumentCaptor<NetworkCallback> blueCallbackCaptor = + ArgumentCaptor.forClass(NetworkCallback.class); + doNothing().when(mConnManager).registerDefaultNetworkCallbackAsUid( + eq(UID_BLUE), blueCallbackCaptor.capture(), any()); final ConnectivityController controller = new ConnectivityController(mService); @@ -281,15 +313,16 @@ public class ConnectivityControllerTest { final JobStatus blue = createJobStatus(createJob() .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1), 0) .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY), UID_BLUE); + controller.maybeStartTrackingJobLocked(red, null); + controller.maybeStartTrackingJobLocked(blue, null); + final NetworkCallback generalCallback = callbackCaptor.getValue(); + final NetworkCallback redCallback = redCallbackCaptor.getValue(); + final NetworkCallback blueCallback = blueCallbackCaptor.getValue(); // 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); + answerNetwork(generalCallback, redCallback, null, null, null); + answerNetwork(generalCallback, blueCallback, null, null, null); assertFalse(red.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); assertFalse(blue.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); @@ -297,11 +330,10 @@ public class ConnectivityControllerTest { // Metered network { - reset(mConnManager); - answerNetwork(UID_RED, meteredNet, meteredCaps); - answerNetwork(UID_BLUE, meteredNet, meteredCaps); + answerNetwork(generalCallback, redCallback, null, meteredNet, meteredCaps); + answerNetwork(generalCallback, blueCallback, null, meteredNet, meteredCaps); - callback.getValue().onCapabilitiesChanged(meteredNet, meteredCaps); + generalCallback.onCapabilitiesChanged(meteredNet, meteredCaps); assertFalse(red.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); assertTrue(blue.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); @@ -309,11 +341,10 @@ public class ConnectivityControllerTest { // Unmetered network background { - reset(mConnManager); - answerNetwork(UID_RED, meteredNet, meteredCaps); - answerNetwork(UID_BLUE, meteredNet, meteredCaps); + answerNetwork(generalCallback, redCallback, meteredNet, meteredNet, meteredCaps); + answerNetwork(generalCallback, blueCallback, meteredNet, meteredNet, meteredCaps); - callback.getValue().onCapabilitiesChanged(unmeteredNet, unmeteredCaps); + generalCallback.onCapabilitiesChanged(unmeteredNet, unmeteredCaps); assertFalse(red.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); assertTrue(blue.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); @@ -321,11 +352,10 @@ public class ConnectivityControllerTest { // Lost metered network { - reset(mConnManager); - answerNetwork(UID_RED, unmeteredNet, unmeteredCaps); - answerNetwork(UID_BLUE, unmeteredNet, unmeteredCaps); + answerNetwork(generalCallback, redCallback, meteredNet, unmeteredNet, unmeteredCaps); + answerNetwork(generalCallback, blueCallback, meteredNet, unmeteredNet, unmeteredCaps); - callback.getValue().onLost(meteredNet); + generalCallback.onLost(meteredNet); assertTrue(red.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); assertTrue(blue.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); @@ -333,11 +363,10 @@ public class ConnectivityControllerTest { // Specific UID was blocked { - reset(mConnManager); - answerNetwork(UID_RED, null, null); - answerNetwork(UID_BLUE, unmeteredNet, unmeteredCaps); + answerNetwork(generalCallback, redCallback, unmeteredNet, null, null); + answerNetwork(generalCallback, blueCallback, unmeteredNet, unmeteredNet, unmeteredCaps); - callback.getValue().onCapabilitiesChanged(unmeteredNet, unmeteredCaps); + generalCallback.onCapabilitiesChanged(unmeteredNet, unmeteredCaps); assertFalse(red.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); assertTrue(blue.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); @@ -404,6 +433,8 @@ public class ConnectivityControllerTest { final JobStatus blue = createJobStatus(createJob() .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1), 0) .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY), UID_BLUE); + controller.maybeStartTrackingJobLocked(red, null); + controller.maybeStartTrackingJobLocked(blue, null); InOrder inOrder = inOrder(mNetPolicyManagerInternal); @@ -560,6 +591,18 @@ public class ConnectivityControllerTest { @Test public void testRestrictedJobTracking() { + final ArgumentCaptor<NetworkCallback> callback = + ArgumentCaptor.forClass(NetworkCallback.class); + doNothing().when(mConnManager).registerNetworkCallback(any(), callback.capture()); + final ArgumentCaptor<NetworkCallback> redCallback = + ArgumentCaptor.forClass(NetworkCallback.class); + doNothing().when(mConnManager).registerDefaultNetworkCallbackAsUid( + eq(UID_RED), redCallback.capture(), any()); + final ArgumentCaptor<NetworkCallback> blueCallback = + ArgumentCaptor.forClass(NetworkCallback.class); + doNothing().when(mConnManager).registerDefaultNetworkCallbackAsUid( + eq(UID_BLUE), blueCallback.capture(), any()); + final JobStatus networked = createJobStatus(createJob() .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1), 0) .setRequiredNetworkType(JobInfo.NETWORK_TYPE_CELLULAR), UID_RED); @@ -570,13 +613,11 @@ public class ConnectivityControllerTest { final Network cellularNet = new Network(101); final NetworkCapabilities cellularCaps = createCapabilities().addTransportType(TRANSPORT_CELLULAR); - reset(mConnManager); - answerNetwork(UID_RED, cellularNet, cellularCaps); - answerNetwork(UID_BLUE, cellularNet, cellularCaps); final ConnectivityController controller = new ConnectivityController(mService); controller.maybeStartTrackingJobLocked(networked, null); controller.maybeStartTrackingJobLocked(unnetworked, null); + answerNetwork(callback.getValue(), redCallback.getValue(), null, cellularNet, cellularCaps); assertTrue(networked.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); assertFalse(unnetworked.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); @@ -600,18 +641,28 @@ public class ConnectivityControllerTest { assertFalse(unnetworked.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); } - private void answerNetwork(int uid, Network net, NetworkCapabilities caps) { - when(mConnManager.getActiveNetworkForUid(eq(uid), anyBoolean())).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 void answerNetwork(@NonNull NetworkCallback generalCallback, + @Nullable NetworkCallback uidCallback, @Nullable Network lastNetwork, + @Nullable Network net, @Nullable NetworkCapabilities caps) { + if (net == null) { + generalCallback.onLost(lastNetwork); + if (uidCallback != null) { + uidCallback.onLost(lastNetwork); + } + } else { + generalCallback.onAvailable(net); + generalCallback.onCapabilitiesChanged(net, caps); + if (uidCallback != null) { + uidCallback.onAvailable(net); + uidCallback.onBlockedStatusChanged(net, false); + uidCallback.onCapabilitiesChanged(net, caps); + } } } private static NetworkCapabilities createCapabilities() { return new NetworkCapabilities().addCapability(NET_CAPABILITY_INTERNET) + .addCapability(NET_CAPABILITY_NOT_SUSPENDED) .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED) .addCapability(NET_CAPABILITY_VALIDATED); } |