diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java index cea19451f005e..e23e0670e5c5a 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -417,6 +417,9 @@ public class JobSchedulerService extends com.android.server.SystemService break; case Constants.KEY_CONN_CONGESTION_DELAY_FRAC: case Constants.KEY_CONN_PREFETCH_RELAX_FRAC: + case Constants.KEY_CONN_LOW_SIGNAL_STRENGTH_RELAX_FRAC: + case Constants.KEY_CONN_USE_CELL_SIGNAL_STRENGTH: + case Constants.KEY_CONN_UPDATE_ALL_JOBS_MIN_INTERVAL_MS: mConstants.updateConnectivityConstantsLocked(); break; case Constants.KEY_PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS: @@ -489,6 +492,12 @@ public class JobSchedulerService extends com.android.server.SystemService private static final String KEY_MIN_EXP_BACKOFF_TIME_MS = "min_exp_backoff_time_ms"; private static final String KEY_CONN_CONGESTION_DELAY_FRAC = "conn_congestion_delay_frac"; private static final String KEY_CONN_PREFETCH_RELAX_FRAC = "conn_prefetch_relax_frac"; + private static final String KEY_CONN_USE_CELL_SIGNAL_STRENGTH = + "conn_use_cell_signal_strength"; + private static final String KEY_CONN_UPDATE_ALL_JOBS_MIN_INTERVAL_MS = + "conn_update_all_jobs_min_interval_ms"; + private static final String KEY_CONN_LOW_SIGNAL_STRENGTH_RELAX_FRAC = + "conn_low_signal_strength_relax_frac"; private static final String KEY_PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS = "prefetch_force_batch_relax_threshold_ms"; private static final String KEY_ENABLE_API_QUOTAS = "enable_api_quotas"; @@ -514,6 +523,9 @@ public class JobSchedulerService extends com.android.server.SystemService private static final long DEFAULT_MIN_EXP_BACKOFF_TIME_MS = JobInfo.MIN_BACKOFF_MILLIS; private static final float DEFAULT_CONN_CONGESTION_DELAY_FRAC = 0.5f; private static final float DEFAULT_CONN_PREFETCH_RELAX_FRAC = 0.5f; + private static final boolean DEFAULT_CONN_USE_CELL_SIGNAL_STRENGTH = true; + private static final long DEFAULT_CONN_UPDATE_ALL_JOBS_MIN_INTERVAL_MS = MINUTE_IN_MILLIS; + private static final float DEFAULT_CONN_LOW_SIGNAL_STRENGTH_RELAX_FRAC = 0.5f; private static final long DEFAULT_PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS = HOUR_IN_MILLIS; private static final boolean DEFAULT_ENABLE_API_QUOTAS = true; private static final int DEFAULT_API_QUOTA_SCHEDULE_COUNT = 250; @@ -569,6 +581,23 @@ public class JobSchedulerService extends com.android.server.SystemService * we consider matching it against a metered network. */ public float CONN_PREFETCH_RELAX_FRAC = DEFAULT_CONN_PREFETCH_RELAX_FRAC; + /** + * Whether to use the cell signal strength to determine if a particular job is eligible to + * run. + */ + public boolean CONN_USE_CELL_SIGNAL_STRENGTH = DEFAULT_CONN_USE_CELL_SIGNAL_STRENGTH; + /** + * When throttling updating all tracked jobs, make sure not to update them more frequently + * than this value. + */ + public long CONN_UPDATE_ALL_JOBS_MIN_INTERVAL_MS = + DEFAULT_CONN_UPDATE_ALL_JOBS_MIN_INTERVAL_MS; + /** + * The fraction of a job's running window that must pass before we consider running it on + * low signal strength networks. + */ + public float CONN_LOW_SIGNAL_STRENGTH_RELAX_FRAC = + DEFAULT_CONN_LOW_SIGNAL_STRENGTH_RELAX_FRAC; /** * The amount of time within which we would consider the app to be launching relatively soon @@ -661,6 +690,18 @@ public class JobSchedulerService extends com.android.server.SystemService CONN_PREFETCH_RELAX_FRAC = DeviceConfig.getFloat(DeviceConfig.NAMESPACE_JOB_SCHEDULER, KEY_CONN_PREFETCH_RELAX_FRAC, DEFAULT_CONN_PREFETCH_RELAX_FRAC); + CONN_USE_CELL_SIGNAL_STRENGTH = DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_JOB_SCHEDULER, + KEY_CONN_USE_CELL_SIGNAL_STRENGTH, + DEFAULT_CONN_USE_CELL_SIGNAL_STRENGTH); + CONN_UPDATE_ALL_JOBS_MIN_INTERVAL_MS = DeviceConfig.getLong( + DeviceConfig.NAMESPACE_JOB_SCHEDULER, + KEY_CONN_UPDATE_ALL_JOBS_MIN_INTERVAL_MS, + DEFAULT_CONN_UPDATE_ALL_JOBS_MIN_INTERVAL_MS); + CONN_LOW_SIGNAL_STRENGTH_RELAX_FRAC = DeviceConfig.getFloat( + DeviceConfig.NAMESPACE_JOB_SCHEDULER, + KEY_CONN_LOW_SIGNAL_STRENGTH_RELAX_FRAC, + DEFAULT_CONN_LOW_SIGNAL_STRENGTH_RELAX_FRAC); } private void updatePrefetchConstantsLocked() { @@ -739,6 +780,11 @@ public class JobSchedulerService extends com.android.server.SystemService pw.print(KEY_MIN_EXP_BACKOFF_TIME_MS, MIN_EXP_BACKOFF_TIME_MS).println(); pw.print(KEY_CONN_CONGESTION_DELAY_FRAC, CONN_CONGESTION_DELAY_FRAC).println(); pw.print(KEY_CONN_PREFETCH_RELAX_FRAC, CONN_PREFETCH_RELAX_FRAC).println(); + pw.print(KEY_CONN_USE_CELL_SIGNAL_STRENGTH, CONN_USE_CELL_SIGNAL_STRENGTH).println(); + pw.print(KEY_CONN_UPDATE_ALL_JOBS_MIN_INTERVAL_MS, CONN_UPDATE_ALL_JOBS_MIN_INTERVAL_MS) + .println(); + pw.print(KEY_CONN_LOW_SIGNAL_STRENGTH_RELAX_FRAC, CONN_LOW_SIGNAL_STRENGTH_RELAX_FRAC) + .println(); pw.print(KEY_PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS, PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS).println(); 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 c678755f73dac..892e0c0280c0c 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 @@ -35,6 +35,10 @@ import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.UserHandle; +import android.telephony.CellSignalStrength; +import android.telephony.SignalStrength; +import android.telephony.TelephonyCallback; +import android.telephony.TelephonyManager; import android.text.format.DateUtils; import android.util.ArrayMap; import android.util.ArraySet; @@ -60,6 +64,7 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Objects; +import java.util.Set; import java.util.function.Predicate; /** @@ -213,9 +218,16 @@ public final class ConnectivityController extends RestrictingController implemen * is only done in {@link #maybeAdjustRegisteredCallbacksLocked()} and may sometimes be stale. */ private final List mSortedStats = new ArrayList<>(); + @GuardedBy("mLock") private long mLastCallbackAdjustmentTimeElapsed; + @GuardedBy("mLock") + private final SparseArray mSignalStrengths = new SparseArray<>(); + + @GuardedBy("mLock") + private long mLastAllJobUpdateTimeElapsed; private static final int MSG_ADJUST_CALLBACKS = 0; + private static final int MSG_UPDATE_ALL_TRACKED_JOBS = 1; private final Handler mHandler; @@ -529,11 +541,7 @@ public final class ConnectivityController extends RestrictingController implemen @GuardedBy("mLock") public void onBatteryStateChangedLocked() { // Update job bookkeeping out of band to avoid blocking broadcast progress. - JobSchedulerBackgroundThread.getHandler().post(() -> { - synchronized (mLock) { - updateTrackedJobsLocked(-1, null); - } - }); + mHandler.sendEmptyMessage(MSG_UPDATE_ALL_TRACKED_JOBS); } private boolean isUsable(NetworkCapabilities capabilities) { @@ -650,6 +658,82 @@ public final class ConnectivityController extends RestrictingController implemen } } + @GuardedBy("mLock") + private boolean isStrongEnough(JobStatus jobStatus, NetworkCapabilities capabilities, + Constants constants) { + final int priority = jobStatus.getEffectivePriority(); + if (priority >= JobInfo.PRIORITY_HIGH) { + return true; + } + if (!constants.CONN_USE_CELL_SIGNAL_STRENGTH) { + return true; + } + if (!capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { + return true; + } + if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) { + // Exclude VPNs because it's currently not possible to determine the VPN's underlying + // network, and thus the correct signal strength of the VPN's network. + // Transmitting data over a VPN is generally more battery-expensive than on the + // underlying network, so: + // TODO: find a good way to reduce job use of VPN when it'll be very expensive + // For now, we just pretend VPNs are always strong enough + return true; + } + + // VCNs running over WiFi will declare TRANSPORT_CELLULAR. When connected, a VCN will + // most likely be the default network. We ideally don't want this to restrict jobs when the + // VCN incorrectly declares the CELLULAR transport, but there's currently no way to + // determine if a network is a VCN. When there is: + // TODO(216127782): exclude VCN running over WiFi from this check + + int signalStrength = CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN; + // Use the best strength found. + final Set subscriptionIds = capabilities.getSubscriptionIds(); + for (int subId : subscriptionIds) { + CellSignalStrengthCallback callback = mSignalStrengths.get(subId); + if (callback != null) { + signalStrength = Math.max(signalStrength, callback.signalStrength); + } else { + Slog.wtf(TAG, + "Subscription ID " + subId + " doesn't have a registered callback"); + } + } + if (DEBUG) { + Slog.d(TAG, "Cell signal strength for job=" + signalStrength); + } + // Treat "NONE_OR_UNKNOWN" as "NONE". + if (signalStrength <= CellSignalStrength.SIGNAL_STRENGTH_POOR) { + // If signal strength is poor, don't run MIN or LOW priority jobs, and only + // run DEFAULT priority jobs if the device is charging or the job has been waiting + // long enough. + if (priority > JobInfo.PRIORITY_DEFAULT) { + return true; + } + if (priority < JobInfo.PRIORITY_DEFAULT) { + return false; + } + // DEFAULT job. + return (mService.isBatteryCharging() && mService.isBatteryNotLow()) + || jobStatus.getFractionRunTime() > constants.CONN_PREFETCH_RELAX_FRAC; + } + if (signalStrength <= CellSignalStrength.SIGNAL_STRENGTH_MODERATE) { + // If signal strength is moderate, only run MIN priority jobs when the device + // is charging, or the job is already running. + if (priority >= JobInfo.PRIORITY_LOW) { + return true; + } + // MIN job. + if (mService.isBatteryCharging() && mService.isBatteryNotLow()) { + return true; + } + final UidStats uidStats = getUidStats( + jobStatus.getSourceUid(), jobStatus.getSourcePackageName(), true); + return uidStats.runningJobs.contains(jobStatus); + } + return true; + } + private static NetworkCapabilities.Builder copyCapabilities( @NonNull final NetworkRequest request) { final NetworkCapabilities.Builder builder = new NetworkCapabilities.Builder(); @@ -717,10 +801,12 @@ public final class ConnectivityController extends RestrictingController implemen // Second, is the network congested? if (isCongestionDelayed(jobStatus, network, capabilities, constants)) return false; - // Third, is the network a strict match? + if (!isStrongEnough(jobStatus, capabilities, constants)) return false; + + // Is the network a strict match? if (isStrictSatisfied(jobStatus, network, capabilities, constants)) return true; - // Third, is the network a relaxed match? + // Is the network a relaxed match? if (isRelaxedSatisfied(jobStatus, network, capabilities, constants)) return true; return false; @@ -985,6 +1071,24 @@ public final class ConnectivityController extends RestrictingController implemen return changed; } + @GuardedBy("mLock") + private void updateAllTrackedJobsLocked(boolean allowThrottle) { + if (allowThrottle) { + final long throttleTimeLeftMs = + (mLastAllJobUpdateTimeElapsed + mConstants.CONN_UPDATE_ALL_JOBS_MIN_INTERVAL_MS) + - sElapsedRealtimeClock.millis(); + if (throttleTimeLeftMs > 0) { + Message msg = mHandler.obtainMessage(MSG_UPDATE_ALL_TRACKED_JOBS, 1, 0); + mHandler.sendMessageDelayed(msg, throttleTimeLeftMs); + return; + } + } + + mHandler.removeMessages(MSG_UPDATE_ALL_TRACKED_JOBS); + updateTrackedJobsLocked(-1, null); + mLastAllJobUpdateTimeElapsed = sElapsedRealtimeClock.millis(); + } + /** * Update any jobs tracked by this controller that match given filters. * @@ -1088,7 +1192,11 @@ public final class ConnectivityController extends RestrictingController implemen Slog.v(TAG, "onCapabilitiesChanged: " + network); } synchronized (mLock) { - mAvailableNetworks.put(network, capabilities); + final NetworkCapabilities oldCaps = mAvailableNetworks.put(network, capabilities); + if (oldCaps != null) { + maybeUnregisterSignalStrengthCallbackLocked(oldCaps); + } + maybeRegisterSignalStrengthCallbackLocked(capabilities); updateTrackedJobsLocked(-1, network); postAdjustCallbacks(); } @@ -1100,7 +1208,10 @@ public final class ConnectivityController extends RestrictingController implemen Slog.v(TAG, "onLost: " + network); } synchronized (mLock) { - mAvailableNetworks.remove(network); + final NetworkCapabilities capabilities = mAvailableNetworks.remove(network); + if (capabilities != null) { + maybeUnregisterSignalStrengthCallbackLocked(capabilities); + } for (int u = 0; u < mCurrentDefaultNetworkCallbacks.size(); ++u) { UidDefaultNetworkCallback callback = mCurrentDefaultNetworkCallbacks.valueAt(u); if (Objects.equals(callback.mDefaultNetwork, network)) { @@ -1111,6 +1222,63 @@ public final class ConnectivityController extends RestrictingController implemen postAdjustCallbacks(); } } + + @GuardedBy("mLock") + private void maybeRegisterSignalStrengthCallbackLocked( + @NonNull NetworkCapabilities capabilities) { + if (!capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { + return; + } + TelephonyManager telephonyManager = mContext.getSystemService(TelephonyManager.class); + final Set subscriptionIds = capabilities.getSubscriptionIds(); + for (int subId : subscriptionIds) { + if (mSignalStrengths.indexOfKey(subId) >= 0) { + continue; + } + TelephonyManager idTm = telephonyManager.createForSubscriptionId(subId); + CellSignalStrengthCallback callback = new CellSignalStrengthCallback(); + idTm.registerTelephonyCallback( + JobSchedulerBackgroundThread.getExecutor(), callback); + mSignalStrengths.put(subId, callback); + + final SignalStrength signalStrength = idTm.getSignalStrength(); + if (signalStrength != null) { + callback.signalStrength = signalStrength.getLevel(); + } + } + } + + @GuardedBy("mLock") + private void maybeUnregisterSignalStrengthCallbackLocked( + @NonNull NetworkCapabilities capabilities) { + if (!capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { + return; + } + ArraySet activeIds = new ArraySet<>(); + for (int i = 0, size = mAvailableNetworks.size(); i < size; ++i) { + NetworkCapabilities nc = mAvailableNetworks.valueAt(i); + if (nc.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { + activeIds.addAll(nc.getSubscriptionIds()); + } + } + if (DEBUG) { + Slog.d(TAG, "Active subscription IDs: " + activeIds); + } + TelephonyManager telephonyManager = mContext.getSystemService(TelephonyManager.class); + Set subscriptionIds = capabilities.getSubscriptionIds(); + for (int subId : subscriptionIds) { + if (activeIds.contains(subId)) { + continue; + } + TelephonyManager idTm = telephonyManager.createForSubscriptionId(subId); + CellSignalStrengthCallback callback = mSignalStrengths.removeReturnOld(subId); + if (callback != null) { + idTm.unregisterTelephonyCallback(callback); + } else { + Slog.wtf(TAG, "Callback for sub " + subId + " didn't exist?!?!"); + } + } + } }; private class CcHandler extends Handler { @@ -1127,6 +1295,13 @@ public final class ConnectivityController extends RestrictingController implemen maybeAdjustRegisteredCallbacksLocked(); } break; + + case MSG_UPDATE_ALL_TRACKED_JOBS: + synchronized (mLock) { + final boolean allowThrottle = msg.arg1 == 1; + updateAllTrackedJobsLocked(allowThrottle); + } + break; } } } @@ -1268,6 +1443,33 @@ public final class ConnectivityController extends RestrictingController implemen } } + private class CellSignalStrengthCallback extends TelephonyCallback + implements TelephonyCallback.SignalStrengthsListener { + @GuardedBy("mLock") + public int signalStrength = CellSignalStrength.SIGNAL_STRENGTH_GREAT; + + @Override + public void onSignalStrengthsChanged(@NonNull SignalStrength signalStrength) { + synchronized (mLock) { + final int newSignalStrength = signalStrength.getLevel(); + if (DEBUG) { + Slog.d(TAG, "Signal strength changing from " + + this.signalStrength + " to " + newSignalStrength); + for (CellSignalStrength css : signalStrength.getCellSignalStrengths()) { + Slog.d(TAG, "CSS: " + css.getLevel() + " " + css); + } + } + if (this.signalStrength == newSignalStrength) { + // This happens a lot. + return; + } + this.signalStrength = newSignalStrength; + // Update job bookkeeping out of band to avoid blocking callback progress. + mHandler.obtainMessage(MSG_UPDATE_ALL_TRACKED_JOBS, 1, 0).sendToTarget(); + } + } + } + @GuardedBy("mLock") @Override public void dumpControllerStateLocked(IndentingPrintWriter pw, @@ -1299,6 +1501,20 @@ public final class ConnectivityController extends RestrictingController implemen } pw.println(); + if (mSignalStrengths.size() > 0) { + pw.println("Subscription ID signal strengths:"); + pw.increaseIndent(); + for (int i = 0; i < mSignalStrengths.size(); ++i) { + pw.print(mSignalStrengths.keyAt(i)); + pw.print(": "); + pw.println(mSignalStrengths.valueAt(i).signalStrength); + } + pw.decreaseIndent(); + } else { + pw.println("No cached signal strengths"); + } + pw.println(); + pw.println("Current default network callbacks:"); pw.increaseIndent(); for (int i = 0; i < mCurrentDefaultNetworkCallbacks.size(); i++) { diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java index 0456a9bfeb2e1..acbb1c1e61ee6 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java @@ -1139,11 +1139,12 @@ public final class JobStatus { */ public float getFractionRunTime() { final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); - if (earliestRunTimeElapsedMillis == 0 && latestRunTimeElapsedMillis == Long.MAX_VALUE) { + if (earliestRunTimeElapsedMillis == NO_EARLIEST_RUNTIME + && latestRunTimeElapsedMillis == NO_LATEST_RUNTIME) { return 1; - } else if (earliestRunTimeElapsedMillis == 0) { + } else if (earliestRunTimeElapsedMillis == NO_EARLIEST_RUNTIME) { return now >= latestRunTimeElapsedMillis ? 1 : 0; - } else if (latestRunTimeElapsedMillis == Long.MAX_VALUE) { + } else if (latestRunTimeElapsedMillis == NO_LATEST_RUNTIME) { return now >= earliestRunTimeElapsedMillis ? 1 : 0; } else { if (now <= earliestRunTimeElapsedMillis) { 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 f61d6ca750cd4..e4c9f97ddf99c 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 @@ -23,6 +23,7 @@ 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; +import static android.net.NetworkCapabilities.TRANSPORT_VPN; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; @@ -59,6 +60,10 @@ import android.net.NetworkPolicyManager; import android.os.Build; import android.os.Looper; import android.os.SystemClock; +import android.telephony.CellSignalStrength; +import android.telephony.SignalStrength; +import android.telephony.TelephonyCallback; +import android.telephony.TelephonyManager; import android.util.DataUnit; import com.android.server.LocalServices; @@ -76,6 +81,7 @@ import org.mockito.junit.MockitoJUnitRunner; import java.time.Clock; import java.time.ZoneOffset; +import java.util.Set; @RunWith(MockitoJUnitRunner.class) public class ConnectivityControllerTest { @@ -301,6 +307,427 @@ public class ConnectivityControllerTest { } } + @Test + public void testStrongEnough_Cellular() { + mConstants.CONN_UPDATE_ALL_JOBS_MIN_INTERVAL_MS = 0; + + final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); + final TelephonyManager telephonyManager = mock(TelephonyManager.class); + when(mContext.getSystemService(TelephonyManager.class)) + .thenReturn(telephonyManager); + when(telephonyManager.createForSubscriptionId(anyInt())) + .thenReturn(telephonyManager); + final ArgumentCaptor signalStrengthsCaptor = + ArgumentCaptor.forClass(TelephonyCallback.class); + doNothing().when(telephonyManager) + .registerTelephonyCallback(any(), signalStrengthsCaptor.capture()); + final ArgumentCaptor callbackCaptor = + ArgumentCaptor.forClass(NetworkCallback.class); + doNothing().when(mConnManager).registerNetworkCallback(any(), callbackCaptor.capture()); + final Network net = mock(Network.class); + final NetworkCapabilities caps = createCapabilitiesBuilder() + .addTransportType(TRANSPORT_CELLULAR) + .addCapability(NET_CAPABILITY_NOT_CONGESTED) + .setSubscriptionIds(Set.of(7357)) + .build(); + final JobInfo.Builder baseJobBuilder = createJob() + .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1), + DataUnit.MEBIBYTES.toBytes(1)) + .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY); + final JobStatus jobExp = createJobStatus(baseJobBuilder.setExpedited(true)); + final JobStatus jobHigh = createJobStatus( + baseJobBuilder.setExpedited(false).setPriority(JobInfo.PRIORITY_HIGH)); + final JobStatus jobDefEarly = createJobStatus( + baseJobBuilder.setExpedited(false).setPriority(JobInfo.PRIORITY_DEFAULT), + now - 1000, now + 100_000); + final JobStatus jobDefLate = createJobStatus( + baseJobBuilder.setExpedited(false).setPriority(JobInfo.PRIORITY_DEFAULT), + now - 100_000, now + 1000); + final JobStatus jobLow = createJobStatus( + baseJobBuilder.setExpedited(false).setPriority(JobInfo.PRIORITY_LOW)); + final JobStatus jobMin = createJobStatus( + baseJobBuilder.setExpedited(false).setPriority(JobInfo.PRIORITY_MIN)); + final JobStatus jobMinRunner = createJobStatus( + baseJobBuilder.setExpedited(false).setPriority(JobInfo.PRIORITY_MIN)); + + final ConnectivityController controller = new ConnectivityController(mService); + + final NetworkCallback generalCallback = callbackCaptor.getValue(); + + when(mService.getMaxJobExecutionTimeMs(any())).thenReturn(10 * 60_000L); + + when(mService.isBatteryCharging()).thenReturn(false); + when(mService.isBatteryNotLow()).thenReturn(false); + answerNetwork(generalCallback, null, null, net, caps); + + final TelephonyCallback.SignalStrengthsListener signalStrengthsListener = + (TelephonyCallback.SignalStrengthsListener) signalStrengthsCaptor.getValue(); + + controller.maybeStartTrackingJobLocked(jobMinRunner, null); + controller.prepareForExecutionLocked(jobMinRunner); + + final SignalStrength signalStrength = mock(SignalStrength.class); + + when(signalStrength.getLevel()).thenReturn( + CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN); + signalStrengthsListener.onSignalStrengthsChanged(signalStrength); + + assertTrue(controller.isSatisfied(jobExp, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobHigh, net, caps, mConstants)); + assertFalse(controller.isSatisfied(jobDefEarly, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobDefLate, net, caps, mConstants)); + assertFalse(controller.isSatisfied(jobLow, net, caps, mConstants)); + assertFalse(controller.isSatisfied(jobMin, net, caps, mConstants)); + assertFalse(controller.isSatisfied(jobMinRunner, net, caps, mConstants)); + + when(signalStrength.getLevel()).thenReturn( + CellSignalStrength.SIGNAL_STRENGTH_POOR); + signalStrengthsListener.onSignalStrengthsChanged(signalStrength); + + assertTrue(controller.isSatisfied(jobExp, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobHigh, net, caps, mConstants)); + assertFalse(controller.isSatisfied(jobDefEarly, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobDefLate, net, caps, mConstants)); + assertFalse(controller.isSatisfied(jobLow, net, caps, mConstants)); + assertFalse(controller.isSatisfied(jobMin, net, caps, mConstants)); + assertFalse(controller.isSatisfied(jobMinRunner, net, caps, mConstants)); + + when(mService.isBatteryCharging()).thenReturn(true); + when(mService.isBatteryNotLow()).thenReturn(false); + + assertTrue(controller.isSatisfied(jobExp, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobHigh, net, caps, mConstants)); + assertFalse(controller.isSatisfied(jobDefEarly, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobDefLate, net, caps, mConstants)); + assertFalse(controller.isSatisfied(jobLow, net, caps, mConstants)); + assertFalse(controller.isSatisfied(jobMin, net, caps, mConstants)); + assertFalse(controller.isSatisfied(jobMinRunner, net, caps, mConstants)); + + when(mService.isBatteryCharging()).thenReturn(true); + when(mService.isBatteryNotLow()).thenReturn(true); + + assertTrue(controller.isSatisfied(jobExp, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobHigh, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobDefEarly, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobDefLate, net, caps, mConstants)); + assertFalse(controller.isSatisfied(jobLow, net, caps, mConstants)); + assertFalse(controller.isSatisfied(jobMin, net, caps, mConstants)); + assertFalse(controller.isSatisfied(jobMinRunner, net, caps, mConstants)); + + when(mService.isBatteryCharging()).thenReturn(false); + when(mService.isBatteryNotLow()).thenReturn(false); + when(signalStrength.getLevel()).thenReturn( + CellSignalStrength.SIGNAL_STRENGTH_MODERATE); + signalStrengthsListener.onSignalStrengthsChanged(signalStrength); + + assertTrue(controller.isSatisfied(jobExp, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobHigh, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobDefEarly, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobDefLate, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobLow, net, caps, mConstants)); + assertFalse(controller.isSatisfied(jobMin, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobMinRunner, net, caps, mConstants)); + + when(mService.isBatteryCharging()).thenReturn(false); + when(mService.isBatteryNotLow()).thenReturn(true); + + assertTrue(controller.isSatisfied(jobExp, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobHigh, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobDefEarly, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobDefLate, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobLow, net, caps, mConstants)); + assertFalse(controller.isSatisfied(jobMin, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobMinRunner, net, caps, mConstants)); + + when(mService.isBatteryCharging()).thenReturn(true); + when(mService.isBatteryNotLow()).thenReturn(true); + + assertTrue(controller.isSatisfied(jobExp, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobHigh, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobDefEarly, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobDefLate, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobLow, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobMin, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobMinRunner, net, caps, mConstants)); + + when(mService.isBatteryCharging()).thenReturn(false); + when(mService.isBatteryNotLow()).thenReturn(false); + when(signalStrength.getLevel()).thenReturn( + CellSignalStrength.SIGNAL_STRENGTH_GOOD); + signalStrengthsListener.onSignalStrengthsChanged(signalStrength); + + assertTrue(controller.isSatisfied(jobExp, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobHigh, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobDefEarly, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobDefLate, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobLow, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobMin, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobMinRunner, net, caps, mConstants)); + + when(mService.isBatteryCharging()).thenReturn(false); + when(mService.isBatteryNotLow()).thenReturn(false); + when(signalStrength.getLevel()).thenReturn( + CellSignalStrength.SIGNAL_STRENGTH_GREAT); + signalStrengthsListener.onSignalStrengthsChanged(signalStrength); + + assertTrue(controller.isSatisfied(jobExp, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobHigh, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobDefEarly, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobDefLate, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobLow, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobMin, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobMinRunner, net, caps, mConstants)); + } + + @Test + public void testStrongEnough_Cellular_CheckDisabled() { + mConstants.CONN_USE_CELL_SIGNAL_STRENGTH = false; + mConstants.CONN_UPDATE_ALL_JOBS_MIN_INTERVAL_MS = 0; + + final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); + final TelephonyManager telephonyManager = mock(TelephonyManager.class); + when(mContext.getSystemService(TelephonyManager.class)) + .thenReturn(telephonyManager); + when(telephonyManager.createForSubscriptionId(anyInt())) + .thenReturn(telephonyManager); + final ArgumentCaptor signalStrengthsCaptor = + ArgumentCaptor.forClass(TelephonyCallback.class); + doNothing().when(telephonyManager) + .registerTelephonyCallback(any(), signalStrengthsCaptor.capture()); + final ArgumentCaptor callbackCaptor = + ArgumentCaptor.forClass(NetworkCallback.class); + doNothing().when(mConnManager).registerNetworkCallback(any(), callbackCaptor.capture()); + final Network net = mock(Network.class); + final NetworkCapabilities caps = createCapabilitiesBuilder() + .addTransportType(TRANSPORT_CELLULAR) + .addCapability(NET_CAPABILITY_NOT_CONGESTED) + .setSubscriptionIds(Set.of(7357)) + .build(); + final JobInfo.Builder baseJobBuilder = createJob() + .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1), + DataUnit.MEBIBYTES.toBytes(1)) + .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY); + final JobStatus jobExp = createJobStatus(baseJobBuilder.setExpedited(true)); + final JobStatus jobHigh = createJobStatus( + baseJobBuilder.setExpedited(false).setPriority(JobInfo.PRIORITY_HIGH)); + final JobStatus jobDefEarly = createJobStatus( + baseJobBuilder.setExpedited(false).setPriority(JobInfo.PRIORITY_DEFAULT), + now - 1000, now + 100_000); + final JobStatus jobDefLate = createJobStatus( + baseJobBuilder.setExpedited(false).setPriority(JobInfo.PRIORITY_DEFAULT), + now - 100_000, now + 1000); + final JobStatus jobLow = createJobStatus( + baseJobBuilder.setExpedited(false).setPriority(JobInfo.PRIORITY_LOW)); + final JobStatus jobMin = createJobStatus( + baseJobBuilder.setExpedited(false).setPriority(JobInfo.PRIORITY_MIN)); + final JobStatus jobMinRunner = createJobStatus( + baseJobBuilder.setExpedited(false).setPriority(JobInfo.PRIORITY_MIN)); + + final ConnectivityController controller = new ConnectivityController(mService); + + final NetworkCallback generalCallback = callbackCaptor.getValue(); + + when(mService.getMaxJobExecutionTimeMs(any())).thenReturn(10 * 60_000L); + + when(mService.isBatteryCharging()).thenReturn(false); + when(mService.isBatteryNotLow()).thenReturn(false); + answerNetwork(generalCallback, null, null, net, caps); + + final TelephonyCallback.SignalStrengthsListener signalStrengthsListener = + (TelephonyCallback.SignalStrengthsListener) signalStrengthsCaptor.getValue(); + + controller.maybeStartTrackingJobLocked(jobMinRunner, null); + controller.prepareForExecutionLocked(jobMinRunner); + + final SignalStrength signalStrength = mock(SignalStrength.class); + + when(signalStrength.getLevel()).thenReturn( + CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN); + signalStrengthsListener.onSignalStrengthsChanged(signalStrength); + + assertTrue(controller.isSatisfied(jobExp, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobHigh, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobDefEarly, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobDefLate, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobLow, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobMin, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobMinRunner, net, caps, mConstants)); + + when(signalStrength.getLevel()).thenReturn( + CellSignalStrength.SIGNAL_STRENGTH_POOR); + signalStrengthsListener.onSignalStrengthsChanged(signalStrength); + + assertTrue(controller.isSatisfied(jobExp, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobHigh, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobDefEarly, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobDefLate, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobLow, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobMin, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobMinRunner, net, caps, mConstants)); + + when(mService.isBatteryCharging()).thenReturn(true); + when(mService.isBatteryNotLow()).thenReturn(false); + + assertTrue(controller.isSatisfied(jobExp, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobHigh, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobDefEarly, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobDefLate, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobLow, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobMin, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobMinRunner, net, caps, mConstants)); + + when(mService.isBatteryCharging()).thenReturn(true); + when(mService.isBatteryNotLow()).thenReturn(true); + + assertTrue(controller.isSatisfied(jobExp, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobHigh, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobDefEarly, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobDefLate, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobLow, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobMin, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobMinRunner, net, caps, mConstants)); + + when(mService.isBatteryCharging()).thenReturn(false); + when(mService.isBatteryNotLow()).thenReturn(false); + when(signalStrength.getLevel()).thenReturn( + CellSignalStrength.SIGNAL_STRENGTH_MODERATE); + signalStrengthsListener.onSignalStrengthsChanged(signalStrength); + + assertTrue(controller.isSatisfied(jobExp, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobHigh, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobDefEarly, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobDefLate, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobLow, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobMin, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobMinRunner, net, caps, mConstants)); + + when(mService.isBatteryCharging()).thenReturn(false); + when(mService.isBatteryNotLow()).thenReturn(true); + + assertTrue(controller.isSatisfied(jobExp, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobHigh, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobDefEarly, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobDefLate, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobLow, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobMin, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobMinRunner, net, caps, mConstants)); + + when(mService.isBatteryCharging()).thenReturn(true); + when(mService.isBatteryNotLow()).thenReturn(true); + + assertTrue(controller.isSatisfied(jobExp, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobHigh, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobDefEarly, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobDefLate, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobLow, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobMin, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobMinRunner, net, caps, mConstants)); + + when(mService.isBatteryCharging()).thenReturn(false); + when(mService.isBatteryNotLow()).thenReturn(false); + when(signalStrength.getLevel()).thenReturn( + CellSignalStrength.SIGNAL_STRENGTH_GOOD); + signalStrengthsListener.onSignalStrengthsChanged(signalStrength); + + assertTrue(controller.isSatisfied(jobExp, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobHigh, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobDefEarly, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobDefLate, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobLow, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobMin, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobMinRunner, net, caps, mConstants)); + + when(mService.isBatteryCharging()).thenReturn(false); + when(mService.isBatteryNotLow()).thenReturn(false); + when(signalStrength.getLevel()).thenReturn( + CellSignalStrength.SIGNAL_STRENGTH_GREAT); + signalStrengthsListener.onSignalStrengthsChanged(signalStrength); + + assertTrue(controller.isSatisfied(jobExp, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobHigh, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobDefEarly, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobDefLate, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobLow, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobMin, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobMinRunner, net, caps, mConstants)); + } + + @Test + public void testStrongEnough_Cellular_VPN() { + mConstants.CONN_UPDATE_ALL_JOBS_MIN_INTERVAL_MS = 0; + + final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); + final TelephonyManager telephonyManager = mock(TelephonyManager.class); + when(mContext.getSystemService(TelephonyManager.class)) + .thenReturn(telephonyManager); + when(telephonyManager.createForSubscriptionId(anyInt())) + .thenReturn(telephonyManager); + final ArgumentCaptor signalStrengthsCaptor = + ArgumentCaptor.forClass(TelephonyCallback.class); + doNothing().when(telephonyManager) + .registerTelephonyCallback(any(), signalStrengthsCaptor.capture()); + final ArgumentCaptor callbackCaptor = + ArgumentCaptor.forClass(NetworkCallback.class); + doNothing().when(mConnManager).registerNetworkCallback(any(), callbackCaptor.capture()); + final Network net = mock(Network.class); + final NetworkCapabilities caps = createCapabilitiesBuilder() + .addTransportType(TRANSPORT_CELLULAR) + .addTransportType(TRANSPORT_VPN) + .addCapability(NET_CAPABILITY_NOT_CONGESTED) + .setSubscriptionIds(Set.of(7357)) + .build(); + final JobInfo.Builder baseJobBuilder = createJob() + .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1), + DataUnit.MEBIBYTES.toBytes(1)) + .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY); + final JobStatus jobExp = createJobStatus(baseJobBuilder.setExpedited(true)); + final JobStatus jobHigh = createJobStatus( + baseJobBuilder.setExpedited(false).setPriority(JobInfo.PRIORITY_HIGH)); + final JobStatus jobDefEarly = createJobStatus( + baseJobBuilder.setExpedited(false).setPriority(JobInfo.PRIORITY_DEFAULT), + now - 1000, now + 100_000); + final JobStatus jobDefLate = createJobStatus( + baseJobBuilder.setExpedited(false).setPriority(JobInfo.PRIORITY_DEFAULT), + now - 100_000, now + 1000); + final JobStatus jobLow = createJobStatus( + baseJobBuilder.setExpedited(false).setPriority(JobInfo.PRIORITY_LOW)); + final JobStatus jobMin = createJobStatus( + baseJobBuilder.setExpedited(false).setPriority(JobInfo.PRIORITY_MIN)); + final JobStatus jobMinRunner = createJobStatus( + baseJobBuilder.setExpedited(false).setPriority(JobInfo.PRIORITY_MIN)); + + final ConnectivityController controller = new ConnectivityController(mService); + + final NetworkCallback generalCallback = callbackCaptor.getValue(); + + when(mService.getMaxJobExecutionTimeMs(any())).thenReturn(10 * 60_000L); + + when(mService.isBatteryCharging()).thenReturn(false); + when(mService.isBatteryNotLow()).thenReturn(false); + answerNetwork(generalCallback, null, null, net, caps); + + final TelephonyCallback.SignalStrengthsListener signalStrengthsListener = + (TelephonyCallback.SignalStrengthsListener) signalStrengthsCaptor.getValue(); + + controller.maybeStartTrackingJobLocked(jobMinRunner, null); + controller.prepareForExecutionLocked(jobMinRunner); + + final SignalStrength signalStrength = mock(SignalStrength.class); + + when(signalStrength.getLevel()).thenReturn( + CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN); + signalStrengthsListener.onSignalStrengthsChanged(signalStrength); + + // We don't restrict data via VPN over cellular. + assertTrue(controller.isSatisfied(jobExp, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobHigh, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobDefEarly, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobDefLate, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobLow, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobMin, net, caps, mConstants)); + assertTrue(controller.isSatisfied(jobMinRunner, net, caps, mConstants)); + } + @Test public void testRelaxed() throws Exception { final long now = JobSchedulerService.sElapsedRealtimeClock.millis();