diff options
Diffstat (limited to 'apex')
26 files changed, 2262 insertions, 367 deletions
diff --git a/apex/jobscheduler/framework/aconfig/job.aconfig b/apex/jobscheduler/framework/aconfig/job.aconfig index 6dbb96974bd3..e4799657616d 100644 --- a/apex/jobscheduler/framework/aconfig/job.aconfig +++ b/apex/jobscheduler/framework/aconfig/job.aconfig @@ -14,3 +14,10 @@ flag { description: "Add APIs to let apps attach debug information to jobs" bug: "293491637" } + +flag { + name: "backup_jobs_exemption" + namespace: "backstage_power" + description: "Introduce a new RUN_BACKUP_JOBS permission and exemption logic allowing for longer running jobs for apps whose primary purpose is to backup or sync content." + bug: "318731461" +} diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java index 4bc73130db29..60eb4ac61076 100644 --- a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java +++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java @@ -23,7 +23,6 @@ import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING; import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN; import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED; import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; -import static android.text.format.DateUtils.HOUR_IN_MILLIS; import static android.util.TimeUtils.formatDuration; import android.annotation.BytesLong; @@ -50,9 +49,7 @@ import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.os.PersistableBundle; -import android.os.Process; import android.os.Trace; -import android.os.UserHandle; import android.util.ArraySet; import android.util.Log; @@ -127,6 +124,15 @@ public class JobInfo implements Parcelable { @Overridable // Aid in testing public static final long ENFORCE_MINIMUM_TIME_WINDOWS = 311402873L; + /** + * Require that minimum latencies and override deadlines are nonnegative. + * + * @hide + */ + @ChangeId + @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + public static final long REJECT_NEGATIVE_DELAYS_AND_DEADLINES = 323349338L; + /** @hide */ @IntDef(prefix = { "NETWORK_TYPE_" }, value = { NETWORK_TYPE_NONE, @@ -206,6 +212,8 @@ public class JobInfo implements Parcelable { /* Minimum flex for a periodic job, in milliseconds. */ private static final long MIN_FLEX_MILLIS = 5 * 60 * 1000L; // 5 minutes + private static final long MIN_ALLOWED_TIME_WINDOW_MILLIS = MIN_PERIOD_MILLIS; + /** * Minimum backoff interval for a job, in milliseconds * @hide @@ -288,12 +296,16 @@ public class JobInfo implements Parcelable { public static final int PRIORITY_HIGH = 400; /** - * This task should be run ahead of all other tasks. Only Expedited Jobs - * {@link Builder#setExpedited(boolean)} can have this priority and as such, - * are subject to the same execution time details noted in - * {@link Builder#setExpedited(boolean)}. - * A sample task of max priority: receiving a text message and processing it to - * show a notification + * This task is critical to user experience or functionality + * and should be run ahead of all other tasks. Only + * {@link Builder#setExpedited(boolean) expedited jobs} and + * {@link Builder#setUserInitiated(boolean) user-initiated jobs} can have this priority. + * <p> + * Example tasks of max priority: + * <ul> + * <li>Receiving a text message and processing it to show a notification</li> + * <li>Downloading or uploading some content the user requested to transfer immediately</li> + * </ul> */ public static final int PRIORITY_MAX = 500; @@ -689,14 +701,14 @@ public class JobInfo implements Parcelable { * @see JobInfo.Builder#setMinimumLatency(long) */ public long getMinLatencyMillis() { - return minLatencyMillis; + return Math.max(0, minLatencyMillis); } /** * @see JobInfo.Builder#setOverrideDeadline(long) */ public long getMaxExecutionDelayMillis() { - return maxExecutionDelayMillis; + return Math.max(0, maxExecutionDelayMillis); } /** @@ -1866,6 +1878,13 @@ public class JobInfo implements Parcelable { * Because it doesn't make sense setting this property on a periodic job, doing so will * throw an {@link java.lang.IllegalArgumentException} when * {@link android.app.job.JobInfo.Builder#build()} is called. + * + * Negative latencies also don't make sense for a job and are indicative of an error, + * so starting in Android version {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM}, + * setting a negative deadline will result in + * {@link android.app.job.JobInfo.Builder#build()} throwing an + * {@link java.lang.IllegalArgumentException}. + * * @param minLatencyMillis Milliseconds before which this job will not be considered for * execution. * @see JobInfo#getMinLatencyMillis() @@ -1877,43 +1896,44 @@ public class JobInfo implements Parcelable { } /** - * Set deadline which is the maximum scheduling latency. The job will be run by this - * deadline even if other requirements (including a delay set through - * {@link #setMinimumLatency(long)}) are not met. + * Set a deadline after which all other functional requested constraints will be ignored. + * After the deadline has passed, the job can run even if other requirements (including + * a delay set through {@link #setMinimumLatency(long)}) are not met. * {@link JobParameters#isOverrideDeadlineExpired()} will return {@code true} if the job's - * deadline has passed. + * deadline has passed. The job's execution may be delayed beyond the set deadline by + * other factors such as Doze mode and system health signals. * * <p> * Because it doesn't make sense setting this property on a periodic job, doing so will * throw an {@link java.lang.IllegalArgumentException} when * {@link android.app.job.JobInfo.Builder#build()} is called. * + * <p> + * Negative deadlines also don't make sense for a job and are indicative of an error, + * so starting in Android version {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM}, + * setting a negative deadline will result in + * {@link android.app.job.JobInfo.Builder#build()} throwing an + * {@link java.lang.IllegalArgumentException}. + * * <p class="note"> * Since a job will run once the deadline has passed regardless of the status of other - * constraints, setting a deadline of 0 with other constraints makes those constraints - * meaningless when it comes to execution decisions. Avoid doing this. - * </p> - * - * <p> - * Short deadlines hinder the system's ability to optimize scheduling behavior and may - * result in running jobs at inopportune times. Therefore, starting in Android version - * {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM}, minimum time windows will be - * enforced to help make it easier to better optimize job execution. Time windows are + * constraints, setting a deadline of 0 (or a {@link #setMinimumLatency(long) delay} equal + * to the deadline) with other constraints makes those constraints + * meaningless when it comes to execution decisions. Since doing so is indicative of an + * error in the logic, starting in Android version + * {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM}, jobs with extremely short + * time windows will fail to build. Time windows are * defined as the time between a job's {@link #setMinimumLatency(long) minimum latency} * and its deadline. If the minimum latency is not set, it is assumed to be 0. - * The following minimums will be enforced: - * <ul> - * <li> - * Jobs with {@link #PRIORITY_DEFAULT} or higher priorities have a minimum time - * window of one hour. - * </li> - * <li>Jobs with {@link #PRIORITY_LOW} have a minimum time window of 6 hours.</li> - * <li>Jobs with {@link #PRIORITY_MIN} have a minimum time window of 12 hours.</li> - * </ul> * * Work that must happen immediately should use {@link #setExpedited(boolean)} or * {@link #setUserInitiated(boolean)} in the appropriate manner. * + * <p> + * This API aimed to guarantee execution of the job by the deadline only on Android version + * {@link android.os.Build.VERSION_CODES#LOLLIPOP}. That aim and guarantee has not existed + * since {@link android.os.Build.VERSION_CODES#M}. + * * @see JobInfo#getMaxExecutionDelayMillis() */ public Builder setOverrideDeadline(long maxExecutionDelayMillis) { @@ -1969,6 +1989,9 @@ public class JobInfo implements Parcelable { * </ol> * * <p> + * Expedited jobs are given {@link #PRIORITY_MAX} by default. + * + * <p> * Since these jobs have stronger guarantees than regular jobs, they will be subject to * stricter quotas. As long as an app has available expedited quota, jobs scheduled with * this set to true will run with these guarantees. If an app has run out of available @@ -2059,6 +2082,7 @@ public class JobInfo implements Parcelable { * <p> * These jobs will not be subject to quotas and will be started immediately once scheduled * if all constraints are met and the device system health allows for additional tasks. + * They are also given {@link #PRIORITY_MAX} by default, and the priority cannot be changed. * * @see JobInfo#isUserInitiated() */ @@ -2188,13 +2212,15 @@ public class JobInfo implements Parcelable { public JobInfo build() { return build(Compatibility.isChangeEnabled(DISALLOW_DEADLINES_FOR_PREFETCH_JOBS), Compatibility.isChangeEnabled(REJECT_NEGATIVE_NETWORK_ESTIMATES), - Compatibility.isChangeEnabled(ENFORCE_MINIMUM_TIME_WINDOWS)); + Compatibility.isChangeEnabled(ENFORCE_MINIMUM_TIME_WINDOWS), + Compatibility.isChangeEnabled(REJECT_NEGATIVE_DELAYS_AND_DEADLINES)); } /** @hide */ public JobInfo build(boolean disallowPrefetchDeadlines, boolean rejectNegativeNetworkEstimates, - boolean enforceMinimumTimeWindows) { + boolean enforceMinimumTimeWindows, + boolean rejectNegativeDelaysAndDeadlines) { // This check doesn't need to be inside enforceValidity. It's an unnecessary legacy // check that would ideally be phased out instead. if (mBackoffPolicySet && (mConstraintFlags & CONSTRAINT_FLAG_DEVICE_IDLE) != 0) { @@ -2204,7 +2230,7 @@ public class JobInfo implements Parcelable { } JobInfo jobInfo = new JobInfo(this); jobInfo.enforceValidity(disallowPrefetchDeadlines, rejectNegativeNetworkEstimates, - enforceMinimumTimeWindows); + enforceMinimumTimeWindows, rejectNegativeDelaysAndDeadlines); return jobInfo; } @@ -2224,7 +2250,8 @@ public class JobInfo implements Parcelable { */ public final void enforceValidity(boolean disallowPrefetchDeadlines, boolean rejectNegativeNetworkEstimates, - boolean enforceMinimumTimeWindows) { + boolean enforceMinimumTimeWindows, + boolean rejectNegativeDelaysAndDeadlines) { // Check that network estimates require network type and are reasonable values. if ((networkDownloadBytes > 0 || networkUploadBytes > 0 || minimumNetworkChunkBytes > 0) && networkRequest == null) { @@ -2258,6 +2285,17 @@ public class JobInfo implements Parcelable { throw new IllegalArgumentException("Minimum chunk size must be positive"); } + if (rejectNegativeDelaysAndDeadlines) { + if (minLatencyMillis < 0) { + throw new IllegalArgumentException( + "Minimum latency is negative: " + minLatencyMillis); + } + if (maxExecutionDelayMillis < 0) { + throw new IllegalArgumentException( + "Override deadline is negative: " + maxExecutionDelayMillis); + } + } + final boolean hasDeadline = maxExecutionDelayMillis != 0L; // Check that a deadline was not set on a periodic job. if (isPeriodic) { @@ -2339,35 +2377,36 @@ public class JobInfo implements Parcelable { throw new IllegalArgumentException("Invalid priority level provided: " + mPriority); } - if (enforceMinimumTimeWindows - && Flags.enforceMinimumTimeWindows() - // TODO(312197030): remove exemption for the system - && !UserHandle.isCore(Process.myUid()) - && hasLateConstraint && !isPeriodic) { - final long windowStart = hasEarlyConstraint ? minLatencyMillis : 0; - if (mPriority >= PRIORITY_DEFAULT) { - if (maxExecutionDelayMillis - windowStart < HOUR_IN_MILLIS) { - throw new IllegalArgumentException( - getPriorityString(mPriority) - + " cannot have a time window less than 1 hour." - + " Delay=" + windowStart - + ", deadline=" + maxExecutionDelayMillis); - } - } else if (mPriority >= PRIORITY_LOW) { - if (maxExecutionDelayMillis - windowStart < 6 * HOUR_IN_MILLIS) { - throw new IllegalArgumentException( - getPriorityString(mPriority) - + " cannot have a time window less than 6 hours." - + " Delay=" + windowStart - + ", deadline=" + maxExecutionDelayMillis); - } + final boolean hasFunctionalConstraint = networkRequest != null + || constraintFlags != 0 + || (triggerContentUris != null && triggerContentUris.length > 0); + if (hasLateConstraint && !isPeriodic) { + if (!hasFunctionalConstraint) { + Log.w(TAG, "Job '" + service.flattenToShortString() + "#" + jobId + "'" + + " has a deadline with no functional constraints." + + " The deadline won't improve job execution latency." + + " Consider removing the deadline."); } else { - if (maxExecutionDelayMillis - windowStart < 12 * HOUR_IN_MILLIS) { - throw new IllegalArgumentException( - getPriorityString(mPriority) - + " cannot have a time window less than 12 hours." - + " Delay=" + windowStart - + ", deadline=" + maxExecutionDelayMillis); + final long windowStart = hasEarlyConstraint ? minLatencyMillis : 0; + if (maxExecutionDelayMillis - windowStart < MIN_ALLOWED_TIME_WINDOW_MILLIS) { + if (enforceMinimumTimeWindows + && Flags.enforceMinimumTimeWindows()) { + throw new IllegalArgumentException("Time window too short. Constraints" + + " unlikely to be satisfied. Increase deadline to a reasonable" + + " duration." + + " Job '" + service.flattenToShortString() + "#" + jobId + "'" + + " has delay=" + windowStart + + ", deadline=" + maxExecutionDelayMillis); + } else { + Log.w(TAG, "Job '" + service.flattenToShortString() + "#" + jobId + "'" + + " has a deadline with functional constraints and an extremely" + + " short time window of " + + (maxExecutionDelayMillis - windowStart) + " ms" + + " (delay=" + windowStart + + ", deadline=" + maxExecutionDelayMillis + ")." + + " The functional constraints are not likely to be satisfied when" + + " the job runs."); + } } } } diff --git a/apex/jobscheduler/framework/java/com/android/server/DeviceIdleInternal.java b/apex/jobscheduler/framework/java/com/android/server/DeviceIdleInternal.java index caf7e7f4a4ed..1fc888b06ffd 100644 --- a/apex/jobscheduler/framework/java/com/android/server/DeviceIdleInternal.java +++ b/apex/jobscheduler/framework/java/com/android/server/DeviceIdleInternal.java @@ -16,6 +16,7 @@ package com.android.server; +import android.annotation.NonNull; import android.annotation.Nullable; import android.os.PowerExemptionManager; import android.os.PowerExemptionManager.ReasonCode; @@ -77,6 +78,9 @@ public interface DeviceIdleInternal { int[] getPowerSaveTempWhitelistAppIds(); + @NonNull + String[] getFullPowerWhitelistExceptIdle(); + /** * Listener to be notified when DeviceIdleController determines that the device has moved or is * stationary. diff --git a/apex/jobscheduler/framework/java/com/android/server/job/JobSchedulerInternal.java b/apex/jobscheduler/framework/java/com/android/server/job/JobSchedulerInternal.java index 6c8af39015f5..ae98fe14fbe6 100644 --- a/apex/jobscheduler/framework/java/com/android/server/job/JobSchedulerInternal.java +++ b/apex/jobscheduler/framework/java/com/android/server/job/JobSchedulerInternal.java @@ -77,6 +77,12 @@ public interface JobSchedulerInternal { @NonNull String notificationChannel, int userId, @NonNull String packageName); /** + * @return {@code true} if the given package holds the + * {@link android.Manifest.permission.RUN_BACKUP_JOBS} permission. + */ + boolean hasRunBackupJobsPermission(@NonNull String packageName, int packageUid); + + /** * Report a snapshot of sync-related jobs back to the sync manager */ JobStorePersistStats getPersistStats(); diff --git a/apex/jobscheduler/service/Android.bp b/apex/jobscheduler/service/Android.bp index a654f7a2d974..ace56d42ddd1 100644 --- a/apex/jobscheduler/service/Android.bp +++ b/apex/jobscheduler/service/Android.bp @@ -30,6 +30,7 @@ java_library { static_libs: [ "modules-utils-fastxmlserializer", + "service-jobscheduler-alarm.flags-aconfig-java", "service-jobscheduler-job.flags-aconfig-java", ], diff --git a/apex/jobscheduler/service/aconfig/Android.bp b/apex/jobscheduler/service/aconfig/Android.bp index 7f1fd47afcba..859c67ad8910 100644 --- a/apex/jobscheduler/service/aconfig/Android.bp +++ b/apex/jobscheduler/service/aconfig/Android.bp @@ -29,3 +29,16 @@ java_aconfig_library { aconfig_declarations: "service-job.flags-aconfig", visibility: ["//frameworks/base:__subpackages__"], } + +// Alarm +aconfig_declarations { + name: "alarm_flags", + package: "com.android.server.alarm", + container: "system", + srcs: ["alarm.aconfig"], +} + +java_aconfig_library { + name: "service-jobscheduler-alarm.flags-aconfig-java", + aconfig_declarations: "alarm_flags", +} diff --git a/apex/jobscheduler/service/aconfig/alarm.aconfig b/apex/jobscheduler/service/aconfig/alarm.aconfig new file mode 100644 index 000000000000..d3068d7d37e8 --- /dev/null +++ b/apex/jobscheduler/service/aconfig/alarm.aconfig @@ -0,0 +1,19 @@ +package: "com.android.server.alarm" +container: "system" + +flag { + name: "use_frozen_state_to_drop_listener_alarms" + namespace: "backstage_power" + description: "Use frozen state callback to drop listener alarms for cached apps" + bug: "324470945" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "start_user_before_scheduled_alarms" + namespace: "multiuser" + description: "Persist list of users with alarms scheduled and wakeup stopped users before alarms are due" + bug: "314907186" +} diff --git a/apex/jobscheduler/service/aconfig/device_idle.aconfig b/apex/jobscheduler/service/aconfig/device_idle.aconfig index 7a5e4bfd4c4a..e8c99b12828f 100644 --- a/apex/jobscheduler/service/aconfig/device_idle.aconfig +++ b/apex/jobscheduler/service/aconfig/device_idle.aconfig @@ -5,5 +5,5 @@ flag { name: "disable_wakelocks_in_light_idle" namespace: "backstage_power" description: "Disable wakelocks for background apps while Light Device Idle is active" - bug: "299329948" + bug: "326607666" } diff --git a/apex/jobscheduler/service/aconfig/job.aconfig b/apex/jobscheduler/service/aconfig/job.aconfig index db8124eb0a23..75e2efd2ec99 100644 --- a/apex/jobscheduler/service/aconfig/job.aconfig +++ b/apex/jobscheduler/service/aconfig/job.aconfig @@ -2,15 +2,29 @@ package: "com.android.server.job" container: "system" flag { - name: "relax_prefetch_connectivity_constraint_only_on_charger" + name: "batch_active_bucket_jobs" namespace: "backstage_power" - description: "Only relax a prefetch job's connectivity constraint when the device is charging and battery is not low" - bug: "299329948" + description: "Include jobs in the ACTIVE bucket in the job batching effort. Don't let them run as freely as they're ready." + bug: "326607666" +} + +flag { + name: "batch_connectivity_jobs_per_network" + namespace: "backstage_power" + description: "Have JobScheduler attempt to delay the start of some connectivity jobs until there are several ready or the network is active" + bug: "28382445" } flag { - name: "throw_on_unsupported_bias_usage" + name: "do_not_force_rush_execution_at_boot" namespace: "backstage_power" - description: "Throw an exception if an unsupported app uses JobInfo.setBias" - bug: "300477393" -}
\ No newline at end of file + description: "Don't force rush job execution right after boot completion" + bug: "321598070" +} + +flag { + name: "relax_prefetch_connectivity_constraint_only_on_charger" + namespace: "backstage_power" + description: "Only relax a prefetch job's connectivity constraint when the device is charging and battery is not low" + bug: "299329948" +} diff --git a/apex/jobscheduler/service/java/com/android/server/AppStateTrackerImpl.java b/apex/jobscheduler/service/java/com/android/server/AppStateTrackerImpl.java index e08200b055d8..33f6899239c6 100644 --- a/apex/jobscheduler/service/java/com/android/server/AppStateTrackerImpl.java +++ b/apex/jobscheduler/service/java/com/android/server/AppStateTrackerImpl.java @@ -743,7 +743,8 @@ public class AppStateTrackerImpl implements AppStateTracker { private final class AppOpsWatcher extends IAppOpsCallback.Stub { @Override - public void opChanged(int op, int uid, String packageName) throws RemoteException { + public void opChanged(int op, int uid, String packageName, + String persistentDeviceId) throws RemoteException { boolean restricted = false; try { restricted = mAppOpsService.checkOperation(TARGET_OP, diff --git a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java index a49ad9863c5f..11d3e96ccb7e 100644 --- a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java +++ b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java @@ -894,8 +894,9 @@ public class DeviceIdleController extends SystemService } // Fall through when quick doze is not requested. - if (!mIsOffBody) { - // Quick doze was not requested and device is on body so turn the device active. + if (!mIsOffBody && !mForceIdle) { + // Quick doze wasn't requested, doze wasn't forced and device is on body + // so turn the device active. mActiveReason = ACTIVE_REASON_ONBODY; becomeActiveLocked("on_body", Process.myUid()); } @@ -2375,6 +2376,11 @@ public class DeviceIdleController extends SystemService return DeviceIdleController.this.isAppOnWhitelistInternal(appid); } + @Override + public String[] getFullPowerWhitelistExceptIdle() { + return DeviceIdleController.this.getFullPowerWhitelistInternalUnchecked(); + } + /** * Returns the array of app ids whitelisted by user. Take care not to * modify this, as it is a reference to the original copy. But the reference @@ -3101,10 +3107,14 @@ public class DeviceIdleController extends SystemService } private String[] getFullPowerWhitelistInternal(final int callingUid, final int callingUserId) { - final String[] apps; + return ArrayUtils.filter(getFullPowerWhitelistInternalUnchecked(), String[]::new, + (pkg) -> !mPackageManagerInternal.filterAppAccess(pkg, callingUid, callingUserId)); + } + + private String[] getFullPowerWhitelistInternalUnchecked() { synchronized (this) { int size = mPowerSaveWhitelistApps.size() + mPowerSaveWhitelistUserApps.size(); - apps = new String[size]; + final String[] apps = new String[size]; int cur = 0; for (int i = 0; i < mPowerSaveWhitelistApps.size(); i++) { apps[cur] = mPowerSaveWhitelistApps.keyAt(i); @@ -3114,9 +3124,8 @@ public class DeviceIdleController extends SystemService apps[cur] = mPowerSaveWhitelistUserApps.keyAt(i); cur++; } + return apps; } - return ArrayUtils.filter(apps, String[]::new, - (pkg) -> !mPackageManagerInternal.filterAppAccess(pkg, callingUid, callingUserId)); } public boolean isPowerSaveWhitelistExceptIdleAppInternal(String packageName) { @@ -3912,6 +3921,7 @@ public class DeviceIdleController extends SystemService if (locationManager != null && locationManager.getProvider(LocationManager.FUSED_PROVIDER) != null) { + mHasFusedLocation = true; locationManager.requestLocationUpdates(LocationManager.FUSED_PROVIDER, mLocationRequest, AppSchedulingModuleThread.getExecutor(), diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java index 5a32a02ca8ce..d0a1b027ec48 100644 --- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java +++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java @@ -16,6 +16,7 @@ package com.android.server.alarm; +import static android.app.ActivityManager.UidFrozenStateChangedCallback.UID_FROZEN_STATE_FROZEN; import static android.app.ActivityManagerInternal.ALLOW_NON_FULL; import static android.app.AlarmManager.ELAPSED_REALTIME; import static android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP; @@ -75,6 +76,7 @@ import android.annotation.NonNull; import android.annotation.SuppressLint; import android.annotation.UserIdInt; import android.app.Activity; +import android.app.ActivityManager; import android.app.ActivityManagerInternal; import android.app.ActivityOptions; import android.app.AlarmManager; @@ -103,6 +105,7 @@ import android.os.Binder; import android.os.Build; import android.os.Bundle; import android.os.Handler; +import android.os.HandlerExecutor; import android.os.IBinder; import android.os.Looper; import android.os.Message; @@ -115,6 +118,7 @@ import android.os.ServiceManager; import android.os.ShellCallback; import android.os.ShellCommand; import android.os.SystemClock; +import android.os.SystemProperties; import android.os.ThreadLocalWorkSource; import android.os.Trace; import android.os.UserHandle; @@ -144,6 +148,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.IAppOpsCallback; import com.android.internal.app.IAppOpsService; +import com.android.internal.util.ArrayUtils; import com.android.internal.util.DumpUtils; import com.android.internal.util.FrameworkStatsLog; import com.android.internal.util.LocalLog; @@ -178,6 +183,9 @@ import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.text.SimpleDateFormat; +import java.time.Instant; +import java.time.zone.ZoneOffsetTransition; +import java.time.zone.ZoneRules; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; @@ -189,6 +197,7 @@ import java.util.Set; import java.util.TimeZone; import java.util.TreeSet; import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; import java.util.function.Predicate; /** @@ -229,6 +238,13 @@ public class AlarmManagerService extends SystemService { private static final long TEMPORARY_QUOTA_DURATION = INTERVAL_DAY; + // System properties read on some device configurations to initialize time properly and + // perform DST transitions at the bootloader level. + private static final String TIMEOFFSET_PROPERTY = "persist.sys.time.offset"; + private static final String DST_TRANSITION_PROPERTY = "persist.sys.time.dst_transition"; + private static final String DST_OFFSET_PROPERTY = "persist.sys.time.dst_offset"; + + private final Intent mBackgroundIntent = new Intent().addFlags(Intent.FLAG_FROM_BACKGROUND); @@ -289,6 +305,7 @@ public class AlarmManagerService extends SystemService { private final Injector mInjector; int mBroadcastRefCount = 0; + boolean mUseFrozenStateToDropListenerAlarms; MetricsHelper mMetricsHelper; PowerManager.WakeLock mWakeLock; SparseIntArray mAlarmsPerUid = new SparseIntArray(); @@ -1852,15 +1869,47 @@ public class AlarmManagerService extends SystemService { @Override public void onStart() { mInjector.init(); + mHandler = new AlarmHandler(); + mOptsWithFgs.setPendingIntentBackgroundActivityLaunchAllowed(false); mOptsWithFgsForAlarmClock.setPendingIntentBackgroundActivityLaunchAllowed(false); mOptsWithoutFgs.setPendingIntentBackgroundActivityLaunchAllowed(false); mOptsTimeBroadcast.setPendingIntentBackgroundActivityLaunchAllowed(false); mActivityOptsRestrictBal.setPendingIntentBackgroundActivityLaunchAllowed(false); mBroadcastOptsRestrictBal.setPendingIntentBackgroundActivityLaunchAllowed(false); + mMetricsHelper = new MetricsHelper(getContext(), mLock); mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class); + mUseFrozenStateToDropListenerAlarms = Flags.useFrozenStateToDropListenerAlarms(); + if (mUseFrozenStateToDropListenerAlarms) { + final ActivityManager.UidFrozenStateChangedCallback callback = (uids, frozenStates) -> { + final int size = frozenStates.length; + if (uids.length != size) { + Slog.wtf(TAG, "Got different length arrays in frozen state callback!" + + " uids.length: " + uids.length + " frozenStates.length: " + size); + // Cannot process received data in any meaningful way. + return; + } + final IntArray affectedUids = new IntArray(); + for (int i = 0; i < size; i++) { + if (frozenStates[i] != UID_FROZEN_STATE_FROZEN) { + continue; + } + if (!CompatChanges.isChangeEnabled(EXACT_LISTENER_ALARMS_DROPPED_ON_CACHED, + uids[i])) { + continue; + } + affectedUids.add(uids[i]); + } + if (affectedUids.size() > 0) { + removeExactListenerAlarms(affectedUids.toArray()); + } + }; + final ActivityManager am = getContext().getSystemService(ActivityManager.class); + am.registerUidFrozenStateChangedCallback(new HandlerExecutor(mHandler), callback); + } + mListenerDeathRecipient = new IBinder.DeathRecipient() { @Override public void binderDied() { @@ -1876,7 +1925,6 @@ public class AlarmManagerService extends SystemService { }; synchronized (mLock) { - mHandler = new AlarmHandler(); mConstants = new Constants(mHandler); mAlarmStore = new LazyAlarmStore(); @@ -1956,6 +2004,21 @@ public class AlarmManagerService extends SystemService { publishBinderService(Context.ALARM_SERVICE, mService); } + private void removeExactListenerAlarms(int... whichUids) { + synchronized (mLock) { + removeAlarmsInternalLocked(a -> { + if (!ArrayUtils.contains(whichUids, a.uid) || a.listener == null + || a.windowLength != 0) { + return false; + } + Slog.w(TAG, "Alarm " + a.listenerTag + " being removed for " + + UserHandle.formatUid(a.uid) + ":" + a.packageName + + " because the app got frozen"); + return true; + }, REMOVE_REASON_LISTENER_CACHED); + } + } + void refreshExactAlarmCandidates() { final String[] candidates = mLocalPermissionManager.getAppOpPermissionPackages( Manifest.permission.SCHEDULE_EXACT_ALARM); @@ -2022,8 +2085,8 @@ public class AlarmManagerService extends SystemService { iAppOpsService.startWatchingMode(AppOpsManager.OP_SCHEDULE_EXACT_ALARM, null, new IAppOpsCallback.Stub() { @Override - public void opChanged(int op, int uid, String packageName) - throws RemoteException { + public void opChanged(int op, int uid, String packageName, + String persistentDeviceId) throws RemoteException { final int userId = UserHandle.getUserId(uid); if (op != AppOpsManager.OP_SCHEDULE_EXACT_ALARM || !isExactAlarmChangeEnabled(packageName, userId)) { @@ -2142,6 +2205,22 @@ public class AlarmManagerService extends SystemService { // "GMT" if the ID is unrecognized). The parameter ID is used here rather than // newZone.getId(). It will be rejected if it is invalid. timeZoneWasChanged = SystemTimeZone.setTimeZoneId(tzId, confidence, logInfo); + + final int gmtOffset = newZone.getOffset(mInjector.getCurrentTimeMillis()); + SystemProperties.set(TIMEOFFSET_PROPERTY, String.valueOf(gmtOffset)); + + final ZoneRules rules = newZone.toZoneId().getRules(); + final ZoneOffsetTransition transition = rules.nextTransition(Instant.now()); + if (null != transition) { + // Get the offset between the time after the DST transition and before. + final long transitionOffset = TimeUnit.SECONDS.toMillis(( + transition.getOffsetAfter().getTotalSeconds() + - transition.getOffsetBefore().getTotalSeconds())); + // Time when the next DST transition is programmed. + final long nextTransition = TimeUnit.SECONDS.toMillis(transition.toEpochSecond()); + SystemProperties.set(DST_TRANSITION_PROPERTY, String.valueOf(nextTransition)); + SystemProperties.set(DST_OFFSET_PROPERTY, String.valueOf(transitionOffset)); + } } // Clear the default time zone in the system server process. This forces the next call @@ -3067,6 +3146,14 @@ public class AlarmManagerService extends SystemService { mConstants.dump(pw); pw.println(); + pw.println("Feature Flags:"); + pw.increaseIndent(); + pw.print(Flags.FLAG_USE_FROZEN_STATE_TO_DROP_LISTENER_ALARMS, + mUseFrozenStateToDropListenerAlarms); + pw.decreaseIndent(); + pw.println(); + pw.println(); + if (mConstants.USE_TARE_POLICY == EconomyManager.ENABLED_MODE_ON) { pw.println("TARE details:"); pw.increaseIndent(); @@ -4952,18 +5039,7 @@ public class AlarmManagerService extends SystemService { break; case REMOVE_EXACT_LISTENER_ALARMS_ON_CACHED: uid = (Integer) msg.obj; - synchronized (mLock) { - removeAlarmsInternalLocked(a -> { - if (a.uid != uid || a.listener == null || a.windowLength != 0) { - return false; - } - // TODO (b/265195908): Change to .w once we have some data on breakages. - Slog.wtf(TAG, "Alarm " + a.listenerTag + " being removed for " - + UserHandle.formatUid(a.uid) + ":" + a.packageName - + " because the app went into cached state"); - return true; - }, REMOVE_REASON_LISTENER_CACHED); - } + removeExactListenerAlarms(uid); break; default: // nope, just ignore it @@ -5315,6 +5391,10 @@ public class AlarmManagerService extends SystemService { @Override public void handleUidCachedChanged(int uid, boolean cached) { + if (mUseFrozenStateToDropListenerAlarms) { + // Use ActivityManager#UidFrozenStateChangedCallback instead. + return; + } if (!CompatChanges.isChangeEnabled(EXACT_LISTENER_ALARMS_DROPPED_ON_CACHED, uid)) { return; } diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java index 6550f26436d4..012ede274bc1 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java @@ -96,7 +96,6 @@ class JobConcurrencyManager { static final String CONFIG_KEY_PREFIX_CONCURRENCY = "concurrency_"; private static final String KEY_CONCURRENCY_LIMIT = CONFIG_KEY_PREFIX_CONCURRENCY + "limit"; - @VisibleForTesting static final int DEFAULT_CONCURRENCY_LIMIT; static { 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 83db4cbb7e43..bd00c03741f3 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -65,6 +65,7 @@ import android.content.pm.ParceledListSlice; import android.content.pm.ProviderInfo; import android.content.pm.ServiceInfo; import android.net.Network; +import android.net.NetworkCapabilities; import android.net.Uri; import android.os.BatteryManager; import android.os.BatteryManagerInternal; @@ -89,6 +90,7 @@ import android.text.format.DateUtils; import android.util.ArrayMap; import android.util.ArraySet; import android.util.IndentingPrintWriter; +import android.util.KeyValueListParser; import android.util.Log; import android.util.Pair; import android.util.Slog; @@ -159,6 +161,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.function.Predicate; @@ -315,7 +318,8 @@ public class JobSchedulerService extends com.android.server.SystemService private final List<JobRestriction> mJobRestrictions; @GuardedBy("mLock") - private final BatteryStateTracker mBatteryStateTracker; + @VisibleForTesting + final BatteryStateTracker mBatteryStateTracker; @GuardedBy("mLock") private final SparseArray<String> mCloudMediaProviderPackages = new SparseArray<>(); @@ -481,6 +485,32 @@ public class JobSchedulerService extends com.android.server.SystemService private class ConstantsObserver implements DeviceConfig.OnPropertiesChangedListener, EconomyManagerInternal.TareStateChangeListener { + @Nullable + @GuardedBy("mLock") + private DeviceConfig.Properties mLastPropertiesPulled; + @GuardedBy("mLock") + private boolean mCacheConfigChanges = false; + + @Nullable + @GuardedBy("mLock") + public String getValueLocked(String key) { + if (mLastPropertiesPulled == null) { + return null; + } + return mLastPropertiesPulled.getString(key, null); + } + + @GuardedBy("mLock") + public void setCacheConfigChangesLocked(boolean enabled) { + if (enabled && !mCacheConfigChanges) { + mLastPropertiesPulled = + DeviceConfig.getProperties(DeviceConfig.NAMESPACE_JOB_SCHEDULER); + } else { + mLastPropertiesPulled = null; + } + mCacheConfigChanges = enabled; + } + public void start() { DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_JOB_SCHEDULER, AppSchedulingModuleThread.getExecutor(), this); @@ -509,10 +539,18 @@ public class JobSchedulerService extends com.android.server.SystemService } synchronized (mLock) { + if (mCacheConfigChanges) { + mLastPropertiesPulled = + DeviceConfig.getProperties(DeviceConfig.NAMESPACE_JOB_SCHEDULER); + } for (String name : properties.getKeyset()) { if (name == null) { continue; } + if (DEBUG || mCacheConfigChanges) { + Slog.d(TAG, "DeviceConfig " + name + + " changed to " + properties.getString(name, null)); + } switch (name) { case Constants.KEY_ENABLE_API_QUOTAS: case Constants.KEY_ENABLE_EXECUTION_SAFEGUARDS_UDC: @@ -533,7 +571,9 @@ public class JobSchedulerService extends com.android.server.SystemService apiQuotaScheduleUpdated = true; } break; + case Constants.KEY_MIN_READY_CPU_ONLY_JOBS_COUNT: case Constants.KEY_MIN_READY_NON_ACTIVE_JOBS_COUNT: + case Constants.KEY_MAX_CPU_ONLY_JOB_BATCH_DELAY_MS: case Constants.KEY_MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS: mConstants.updateBatchingConstantsLocked(); break; @@ -549,6 +589,8 @@ public class JobSchedulerService extends com.android.server.SystemService 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_MAX_CONNECTIVITY_JOB_BATCH_DELAY_MS: + case Constants.KEY_CONN_TRANSPORT_BATCH_THRESHOLD: case Constants.KEY_CONN_USE_CELL_SIGNAL_STRENGTH: case Constants.KEY_CONN_UPDATE_ALL_JOBS_MIN_INTERVAL_MS: mConstants.updateConnectivityConstantsLocked(); @@ -597,6 +639,8 @@ public class JobSchedulerService extends com.android.server.SystemService sc.onConstantsUpdatedLocked(); } } + + mHandler.sendEmptyMessage(MSG_CHECK_JOB); } @Override @@ -641,8 +685,12 @@ public class JobSchedulerService extends com.android.server.SystemService */ public static class Constants { // Key names stored in the settings value. + private static final String KEY_MIN_READY_CPU_ONLY_JOBS_COUNT = + "min_ready_cpu_only_jobs_count"; private static final String KEY_MIN_READY_NON_ACTIVE_JOBS_COUNT = "min_ready_non_active_jobs_count"; + private static final String KEY_MAX_CPU_ONLY_JOB_BATCH_DELAY_MS = + "max_cpu_only_job_batch_delay_ms"; private static final String KEY_MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS = "max_non_active_job_batch_delay_ms"; private static final String KEY_HEAVY_USE_FACTOR = "heavy_use_factor"; @@ -660,6 +708,10 @@ public class JobSchedulerService extends com.android.server.SystemService "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_CONN_TRANSPORT_BATCH_THRESHOLD = + "conn_transport_batch_threshold"; + private static final String KEY_CONN_MAX_CONNECTIVITY_JOB_BATCH_DELAY_MS = + "conn_max_connectivity_job_batch_delay_ms"; private static final String KEY_PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS = "prefetch_force_batch_relax_threshold_ms"; // This has been enabled for 3+ full releases. We're unlikely to disable it. @@ -708,7 +760,11 @@ public class JobSchedulerService extends com.android.server.SystemService private static final String KEY_MAX_NUM_PERSISTED_JOB_WORK_ITEMS = "max_num_persisted_job_work_items"; - private static final int DEFAULT_MIN_READY_NON_ACTIVE_JOBS_COUNT = 5; + private static final int DEFAULT_MIN_READY_CPU_ONLY_JOBS_COUNT = + Math.min(3, JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT / 3); + private static final int DEFAULT_MIN_READY_NON_ACTIVE_JOBS_COUNT = + Math.min(5, JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT / 3); + private static final long DEFAULT_MAX_CPU_ONLY_JOB_BATCH_DELAY_MS = 31 * MINUTE_IN_MILLIS; private static final long DEFAULT_MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS = 31 * MINUTE_IN_MILLIS; private static final float DEFAULT_HEAVY_USE_FACTOR = .9f; private static final float DEFAULT_MODERATE_USE_FACTOR = .5f; @@ -720,6 +776,15 @@ public class JobSchedulerService extends com.android.server.SystemService 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 SparseIntArray DEFAULT_CONN_TRANSPORT_BATCH_THRESHOLD = + new SparseIntArray(); + private static final long DEFAULT_CONN_MAX_CONNECTIVITY_JOB_BATCH_DELAY_MS = + 31 * MINUTE_IN_MILLIS; + static { + DEFAULT_CONN_TRANSPORT_BATCH_THRESHOLD.put( + NetworkCapabilities.TRANSPORT_CELLULAR, + Math.min(3, JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT / 3)); + } 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; @@ -757,11 +822,23 @@ public class JobSchedulerService extends com.android.server.SystemService static final int DEFAULT_MAX_NUM_PERSISTED_JOB_WORK_ITEMS = 100_000; /** - * Minimum # of non-ACTIVE jobs for which the JMS will be happy running some work early. + * Minimum # of jobs that have to be ready for JS to be happy running work. + * Only valid if {@link Flags#batchActiveBucketJobs()} is true. + */ + int MIN_READY_CPU_ONLY_JOBS_COUNT = DEFAULT_MIN_READY_CPU_ONLY_JOBS_COUNT; + + /** + * Minimum # of non-ACTIVE jobs that have to be ready for JS to be happy running work. */ int MIN_READY_NON_ACTIVE_JOBS_COUNT = DEFAULT_MIN_READY_NON_ACTIVE_JOBS_COUNT; /** + * Don't batch a CPU-only job if it's been delayed due to force batching attempts for + * at least this amount of time. + */ + long MAX_CPU_ONLY_JOB_BATCH_DELAY_MS = DEFAULT_MAX_CPU_ONLY_JOB_BATCH_DELAY_MS; + + /** * Don't batch a non-ACTIVE job if it's been delayed due to force batching attempts for * at least this amount of time. */ @@ -817,6 +894,17 @@ public class JobSchedulerService extends com.android.server.SystemService */ public float CONN_LOW_SIGNAL_STRENGTH_RELAX_FRAC = DEFAULT_CONN_LOW_SIGNAL_STRENGTH_RELAX_FRAC; + /** + * The minimum batch requirement per each transport type before allowing a network to run + * on a network with that transport. + */ + public SparseIntArray CONN_TRANSPORT_BATCH_THRESHOLD = new SparseIntArray(); + /** + * Don't batch a connectivity job if it's been delayed due to force batching attempts for + * at least this amount of time. + */ + public long CONN_MAX_CONNECTIVITY_JOB_BATCH_DELAY_MS = + DEFAULT_CONN_MAX_CONNECTIVITY_JOB_BATCH_DELAY_MS; /** * The amount of time within which we would consider the app to be launching relatively soon @@ -967,11 +1055,31 @@ public class JobSchedulerService extends com.android.server.SystemService public boolean USE_TARE_POLICY = EconomyManager.DEFAULT_ENABLE_POLICY_JOB_SCHEDULER && EconomyManager.DEFAULT_ENABLE_TARE_MODE == EconomyManager.ENABLED_MODE_ON; + public Constants() { + copyTransportBatchThresholdDefaults(); + } + private void updateBatchingConstantsLocked() { - MIN_READY_NON_ACTIVE_JOBS_COUNT = DeviceConfig.getInt( + // The threshold should be in the range + // [0, DEFAULT_CONCURRENCY_LIMIT / 3]. + MIN_READY_CPU_ONLY_JOBS_COUNT = + Math.max(0, Math.min(JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT / 3, + DeviceConfig.getInt( + DeviceConfig.NAMESPACE_JOB_SCHEDULER, + KEY_MIN_READY_CPU_ONLY_JOBS_COUNT, + DEFAULT_MIN_READY_CPU_ONLY_JOBS_COUNT))); + // The threshold should be in the range + // [0, DEFAULT_CONCURRENCY_LIMIT / 3]. + MIN_READY_NON_ACTIVE_JOBS_COUNT = + Math.max(0, Math.min(JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT / 3, + DeviceConfig.getInt( + DeviceConfig.NAMESPACE_JOB_SCHEDULER, + KEY_MIN_READY_NON_ACTIVE_JOBS_COUNT, + DEFAULT_MIN_READY_NON_ACTIVE_JOBS_COUNT))); + MAX_CPU_ONLY_JOB_BATCH_DELAY_MS = DeviceConfig.getLong( DeviceConfig.NAMESPACE_JOB_SCHEDULER, - KEY_MIN_READY_NON_ACTIVE_JOBS_COUNT, - DEFAULT_MIN_READY_NON_ACTIVE_JOBS_COUNT); + KEY_MAX_CPU_ONLY_JOB_BATCH_DELAY_MS, + DEFAULT_MAX_CPU_ONLY_JOB_BATCH_DELAY_MS); MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS = DeviceConfig.getLong( DeviceConfig.NAMESPACE_JOB_SCHEDULER, KEY_MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS, @@ -1019,6 +1127,46 @@ public class JobSchedulerService extends com.android.server.SystemService DeviceConfig.NAMESPACE_JOB_SCHEDULER, KEY_CONN_LOW_SIGNAL_STRENGTH_RELAX_FRAC, DEFAULT_CONN_LOW_SIGNAL_STRENGTH_RELAX_FRAC); + final String batchThresholdConfigString = DeviceConfig.getString( + DeviceConfig.NAMESPACE_JOB_SCHEDULER, + KEY_CONN_TRANSPORT_BATCH_THRESHOLD, + null); + final KeyValueListParser parser = new KeyValueListParser(','); + CONN_TRANSPORT_BATCH_THRESHOLD.clear(); + try { + parser.setString(batchThresholdConfigString); + + for (int t = parser.size() - 1; t >= 0; --t) { + final String transportString = parser.keyAt(t); + try { + final int transport = Integer.parseInt(transportString); + // The threshold should be in the range + // [0, DEFAULT_CONCURRENCY_LIMIT / 3]. + CONN_TRANSPORT_BATCH_THRESHOLD.put(transport, Math.max(0, + Math.min(JobConcurrencyManager.DEFAULT_CONCURRENCY_LIMIT / 3, + parser.getInt(transportString, 1)))); + } catch (NumberFormatException e) { + Slog.e(TAG, "Bad transport string", e); + } + } + } catch (IllegalArgumentException e) { + Slog.wtf(TAG, "Bad string for " + KEY_CONN_TRANSPORT_BATCH_THRESHOLD, e); + // Use the defaults. + copyTransportBatchThresholdDefaults(); + } + CONN_MAX_CONNECTIVITY_JOB_BATCH_DELAY_MS = Math.max(0, Math.min(24 * HOUR_IN_MILLIS, + DeviceConfig.getLong( + DeviceConfig.NAMESPACE_JOB_SCHEDULER, + KEY_CONN_MAX_CONNECTIVITY_JOB_BATCH_DELAY_MS, + DEFAULT_CONN_MAX_CONNECTIVITY_JOB_BATCH_DELAY_MS))); + } + + private void copyTransportBatchThresholdDefaults() { + for (int i = DEFAULT_CONN_TRANSPORT_BATCH_THRESHOLD.size() - 1; i >= 0; --i) { + CONN_TRANSPORT_BATCH_THRESHOLD.put( + DEFAULT_CONN_TRANSPORT_BATCH_THRESHOLD.keyAt(i), + DEFAULT_CONN_TRANSPORT_BATCH_THRESHOLD.valueAt(i)); + } } private void updatePersistingConstantsLocked() { @@ -1163,8 +1311,11 @@ public class JobSchedulerService extends com.android.server.SystemService void dump(IndentingPrintWriter pw) { pw.println("Settings:"); pw.increaseIndent(); + pw.print(KEY_MIN_READY_CPU_ONLY_JOBS_COUNT, MIN_READY_CPU_ONLY_JOBS_COUNT).println(); pw.print(KEY_MIN_READY_NON_ACTIVE_JOBS_COUNT, MIN_READY_NON_ACTIVE_JOBS_COUNT).println(); + pw.print(KEY_MAX_CPU_ONLY_JOB_BATCH_DELAY_MS, + MAX_CPU_ONLY_JOB_BATCH_DELAY_MS).println(); pw.print(KEY_MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS, MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS).println(); pw.print(KEY_HEAVY_USE_FACTOR, HEAVY_USE_FACTOR).println(); @@ -1180,6 +1331,10 @@ public class JobSchedulerService extends com.android.server.SystemService .println(); pw.print(KEY_CONN_LOW_SIGNAL_STRENGTH_RELAX_FRAC, CONN_LOW_SIGNAL_STRENGTH_RELAX_FRAC) .println(); + pw.print(KEY_CONN_TRANSPORT_BATCH_THRESHOLD, CONN_TRANSPORT_BATCH_THRESHOLD.toString()) + .println(); + pw.print(KEY_CONN_MAX_CONNECTIVITY_JOB_BATCH_DELAY_MS, + CONN_MAX_CONNECTIVITY_JOB_BATCH_DELAY_MS).println(); pw.print(KEY_PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS, PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS).println(); @@ -1828,7 +1983,16 @@ public class JobSchedulerService extends com.android.server.SystemService /* system_measured_source_download_bytes */0, /* system_measured_source_upload_bytes */ 0, /* system_measured_calling_download_bytes */0, - /* system_measured_calling_upload_bytes */ 0); + /* system_measured_calling_upload_bytes */ 0, + jobStatus.getJob().getIntervalMillis(), + jobStatus.getJob().getFlexMillis(), + jobStatus.hasFlexibilityConstraint(), + /* isFlexConstraintSatisfied */ false, + jobStatus.canApplyTransportAffinities(), + jobStatus.getNumAppliedFlexibleConstraints(), + jobStatus.getNumDroppedFlexibleConstraints(), + jobStatus.getFilteredTraceTag(), + jobStatus.getFilteredDebugTags()); // If the job is immediately ready to run, then we can just immediately // put it in the pending list and try to schedule it. This is especially @@ -2269,7 +2433,16 @@ public class JobSchedulerService extends com.android.server.SystemService /* system_measured_source_download_bytes */ 0, /* system_measured_source_upload_bytes */ 0, /* system_measured_calling_download_bytes */0, - /* system_measured_calling_upload_bytes */ 0); + /* system_measured_calling_upload_bytes */ 0, + cancelled.getJob().getIntervalMillis(), + cancelled.getJob().getFlexMillis(), + cancelled.hasFlexibilityConstraint(), + cancelled.isConstraintSatisfied(JobStatus.CONSTRAINT_FLEXIBLE), + cancelled.canApplyTransportAffinities(), + cancelled.getNumAppliedFlexibleConstraints(), + cancelled.getNumDroppedFlexibleConstraints(), + cancelled.getFilteredTraceTag(), + cancelled.getFilteredDebugTags()); } // If this is a replacement, bring in the new version of the job if (incomingJob != null) { @@ -2701,8 +2874,10 @@ public class JobSchedulerService extends com.android.server.SystemService sc.maybeStartTrackingJobLocked(job, null); } }); - // GO GO GO! - mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget(); + if (!Flags.doNotForceRushExecutionAtBoot()) { + // GO GO GO! + mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget(); + } } } } @@ -2810,9 +2985,9 @@ public class JobSchedulerService extends com.android.server.SystemService mJobPackageTracker.notePending(job); } - void noteJobsPending(List<JobStatus> jobs) { - for (int i = jobs.size() - 1; i >= 0; i--) { - noteJobPending(jobs.get(i)); + void noteJobsPending(ArraySet<JobStatus> jobs) { + for (int i = jobs.size() - 1; i >= 0; --i) { + noteJobPending(jobs.valueAt(i)); } } @@ -3438,7 +3613,7 @@ public class JobSchedulerService extends com.android.server.SystemService } final class ReadyJobQueueFunctor implements Consumer<JobStatus> { - final ArrayList<JobStatus> newReadyJobs = new ArrayList<>(); + final ArraySet<JobStatus> newReadyJobs = new ArraySet<>(); @Override public void accept(JobStatus job) { @@ -3466,9 +3641,27 @@ public class JobSchedulerService extends com.android.server.SystemService * policies on when we want to execute jobs. */ final class MaybeReadyJobQueueFunctor implements Consumer<JobStatus> { - int forceBatchedCount; - int unbatchedCount; + /** + * Set of jobs that will be force batched, mapped by network. A {@code null} network is + * reserved/intended for CPU-only (non-networked) jobs. + * The set may include already running jobs. + */ + @VisibleForTesting + final ArrayMap<Network, ArraySet<JobStatus>> mBatches = new ArrayMap<>(); + /** List of all jobs that could run if allowed. Already running jobs are excluded. */ + @VisibleForTesting final List<JobStatus> runnableJobs = new ArrayList<>(); + /** + * Convenience holder of all jobs ready to run that won't be force batched. + * Already running jobs are excluded. + */ + final ArraySet<JobStatus> mUnbatchedJobs = new ArraySet<>(); + /** + * Count of jobs that won't be force batched, mapped by network. A {@code null} network is + * reserved/intended for CPU-only (non-networked) jobs. + * The set may include already running jobs. + */ + final ArrayMap<Network, Integer> mUnbatchedJobCount = new ArrayMap<>(); public MaybeReadyJobQueueFunctor() { reset(); @@ -3493,7 +3686,10 @@ public class JobSchedulerService extends com.android.server.SystemService } final boolean shouldForceBatchJob; - if (job.shouldTreatAsExpeditedJob() || job.shouldTreatAsUserInitiatedJob()) { + if (job.overrideState > JobStatus.OVERRIDE_NONE) { + // The job should run for some test. Don't force batch it. + shouldForceBatchJob = false; + } else if (job.shouldTreatAsExpeditedJob() || job.shouldTreatAsUserInitiatedJob()) { // Never batch expedited or user-initiated jobs, even for RESTRICTED apps. shouldForceBatchJob = false; } else if (job.getEffectiveStandbyBucket() == RESTRICTED_INDEX) { @@ -3512,27 +3708,77 @@ public class JobSchedulerService extends com.android.server.SystemService shouldForceBatchJob = false; } else { final long nowElapsed = sElapsedRealtimeClock.millis(); - final boolean batchDelayExpired = job.getFirstForceBatchedTimeElapsed() > 0 - && nowElapsed - job.getFirstForceBatchedTimeElapsed() - >= mConstants.MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS; - shouldForceBatchJob = - mConstants.MIN_READY_NON_ACTIVE_JOBS_COUNT > 1 - && job.getEffectiveStandbyBucket() != ACTIVE_INDEX - && job.getEffectiveStandbyBucket() != EXEMPTED_INDEX - && !batchDelayExpired; + final long timeUntilDeadlineMs = job.hasDeadlineConstraint() + ? job.getLatestRunTimeElapsed() - nowElapsed + : Long.MAX_VALUE; + // Differentiate behavior based on whether the job needs network or not. + if (Flags.batchConnectivityJobsPerNetwork() + && job.hasConnectivityConstraint()) { + // For connectivity jobs, let them run immediately if the network is already + // active (in a state for job run), otherwise, only run them if there are + // enough to meet the batching requirement or the job has been waiting + // long enough. + final boolean batchDelayExpired = + job.getFirstForceBatchedTimeElapsed() > 0 + && nowElapsed - job.getFirstForceBatchedTimeElapsed() + >= mConstants.CONN_MAX_CONNECTIVITY_JOB_BATCH_DELAY_MS; + shouldForceBatchJob = !batchDelayExpired + && job.getEffectiveStandbyBucket() != EXEMPTED_INDEX + && timeUntilDeadlineMs + > mConstants.CONN_MAX_CONNECTIVITY_JOB_BATCH_DELAY_MS / 2 + && !mConnectivityController.isNetworkInStateForJobRunLocked(job); + } else { + final boolean batchDelayExpired; + final boolean batchingEnabled; + if (Flags.batchActiveBucketJobs()) { + batchingEnabled = mConstants.MIN_READY_CPU_ONLY_JOBS_COUNT > 1 + && timeUntilDeadlineMs + > mConstants.MAX_CPU_ONLY_JOB_BATCH_DELAY_MS / 2 + // Active UIDs' jobs were by default treated as in the ACTIVE + // bucket, so we must explicitly exclude them when batching + // ACTIVE jobs. + && !job.uidActive + && !job.getJob().isExemptedFromAppStandby(); + batchDelayExpired = job.getFirstForceBatchedTimeElapsed() > 0 + && nowElapsed - job.getFirstForceBatchedTimeElapsed() + >= mConstants.MAX_CPU_ONLY_JOB_BATCH_DELAY_MS; + } else { + batchingEnabled = mConstants.MIN_READY_NON_ACTIVE_JOBS_COUNT > 1 + && job.getEffectiveStandbyBucket() != ACTIVE_INDEX; + batchDelayExpired = job.getFirstForceBatchedTimeElapsed() > 0 + && nowElapsed - job.getFirstForceBatchedTimeElapsed() + >= mConstants.MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS; + } + shouldForceBatchJob = batchingEnabled + && job.getEffectiveStandbyBucket() != EXEMPTED_INDEX + && !batchDelayExpired; + } + } + + // If connectivity job batching isn't enabled, treat every job as + // a non-connectivity job since that mimics the old behavior. + final Network network = + Flags.batchConnectivityJobsPerNetwork() ? job.network : null; + ArraySet<JobStatus> batch = mBatches.get(network); + if (batch == null) { + batch = new ArraySet<>(); + mBatches.put(network, batch); } + batch.add(job); if (shouldForceBatchJob) { - // Force batching non-ACTIVE jobs. Don't include them in the other counts. - forceBatchedCount++; if (job.getFirstForceBatchedTimeElapsed() == 0) { job.setFirstForceBatchedTimeElapsed(sElapsedRealtimeClock.millis()); } } else { - unbatchedCount++; + mUnbatchedJobCount.put(network, + mUnbatchedJobCount.getOrDefault(job.network, 0) + 1); } if (!isRunning) { runnableJobs.add(job); + if (!shouldForceBatchJob) { + mUnbatchedJobs.add(job); + } } } else { if (isRunning) { @@ -3572,34 +3818,135 @@ public class JobSchedulerService extends com.android.server.SystemService @GuardedBy("mLock") @VisibleForTesting void postProcessLocked() { - if (unbatchedCount > 0 - || forceBatchedCount >= mConstants.MIN_READY_NON_ACTIVE_JOBS_COUNT) { + final ArraySet<JobStatus> jobsToRun = mUnbatchedJobs; + + if (DEBUG) { + Slog.d(TAG, "maybeQueueReadyJobsForExecutionLocked: " + + mUnbatchedJobs.size() + " unbatched jobs."); + } + + int unbatchedCount = 0; + + for (int n = mBatches.size() - 1; n >= 0; --n) { + final Network network = mBatches.keyAt(n); + + // Count all of the unbatched jobs, including the ones without a network. + final Integer unbatchedJobCountObj = mUnbatchedJobCount.get(network); + final int unbatchedJobCount; + if (unbatchedJobCountObj != null) { + unbatchedJobCount = unbatchedJobCountObj; + unbatchedCount += unbatchedJobCount; + } else { + unbatchedJobCount = 0; + } + + // Skip the non-networked jobs here. They'll be handled after evaluating + // everything else. + if (network == null) { + continue; + } + + final ArraySet<JobStatus> batchedJobs = mBatches.valueAt(n); + if (unbatchedJobCount > 0) { + // Some job is going to activate the network anyway. Might as well run all + // the other jobs that will use this network. + if (DEBUG) { + Slog.d(TAG, "maybeQueueReadyJobsForExecutionLocked: piggybacking " + + (batchedJobs.size() - unbatchedJobCount) + " jobs on " + network + + " because of unbatched job"); + } + jobsToRun.addAll(batchedJobs); + continue; + } + + final NetworkCapabilities networkCapabilities = + mConnectivityController.getNetworkCapabilities(network); + if (networkCapabilities == null) { + Slog.e(TAG, "Couldn't get NetworkCapabilities for network " + network); + continue; + } + + final int[] transports = networkCapabilities.getTransportTypes(); + int maxNetworkBatchReq = 1; + for (int transport : transports) { + maxNetworkBatchReq = Math.max(maxNetworkBatchReq, + mConstants.CONN_TRANSPORT_BATCH_THRESHOLD.get(transport)); + } + + if (batchedJobs.size() >= maxNetworkBatchReq) { + if (DEBUG) { + Slog.d(TAG, "maybeQueueReadyJobsForExecutionLocked: " + + batchedJobs.size() + + " batched network jobs meet requirement for " + network); + } + jobsToRun.addAll(batchedJobs); + } + } + + final ArraySet<JobStatus> batchedNonNetworkedJobs = mBatches.get(null); + if (batchedNonNetworkedJobs != null) { + final int minReadyCount = Flags.batchActiveBucketJobs() + ? mConstants.MIN_READY_CPU_ONLY_JOBS_COUNT + : mConstants.MIN_READY_NON_ACTIVE_JOBS_COUNT; + if (jobsToRun.size() > 0) { + // Some job is going to use the CPU anyway. Might as well run all the other + // CPU-only jobs. + if (DEBUG) { + final Integer unbatchedJobCountObj = mUnbatchedJobCount.get(null); + final int unbatchedJobCount = + unbatchedJobCountObj == null ? 0 : unbatchedJobCountObj; + Slog.d(TAG, "maybeQueueReadyJobsForExecutionLocked: piggybacking " + + (batchedNonNetworkedJobs.size() - unbatchedJobCount) + + " non-network jobs"); + } + jobsToRun.addAll(batchedNonNetworkedJobs); + } else if (batchedNonNetworkedJobs.size() >= minReadyCount) { + if (DEBUG) { + Slog.d(TAG, "maybeQueueReadyJobsForExecutionLocked: adding " + + batchedNonNetworkedJobs.size() + " batched non-network jobs."); + } + jobsToRun.addAll(batchedNonNetworkedJobs); + } + } + + // In order to properly determine an accurate batch count, the running jobs must be + // included in the earlier lists and can only be removed after checking if the batch + // count requirement is satisfied. + jobsToRun.removeIf(JobSchedulerService.this::isCurrentlyRunningLocked); + + if (unbatchedCount > 0 || jobsToRun.size() > 0) { if (DEBUG) { - Slog.d(TAG, "maybeQueueReadyJobsForExecutionLocked: Running jobs."); + Slog.d(TAG, "maybeQueueReadyJobsForExecutionLocked: Running " + + jobsToRun + " jobs."); } - noteJobsPending(runnableJobs); - mPendingJobQueue.addAll(runnableJobs); + noteJobsPending(jobsToRun); + mPendingJobQueue.addAll(jobsToRun); } else { if (DEBUG) { Slog.d(TAG, "maybeQueueReadyJobsForExecutionLocked: Not running anything."); } - final int numRunnableJobs = runnableJobs.size(); - if (numRunnableJobs > 0) { - synchronized (mPendingJobReasonCache) { - for (int i = 0; i < numRunnableJobs; ++i) { - final JobStatus job = runnableJobs.get(i); - SparseIntArray reasons = - mPendingJobReasonCache.get(job.getUid(), job.getNamespace()); - if (reasons == null) { - reasons = new SparseIntArray(); - mPendingJobReasonCache - .add(job.getUid(), job.getNamespace(), reasons); - } - // We're force batching these jobs, so consider it an optimization - // policy reason. - reasons.put(job.getJobId(), - JobScheduler.PENDING_JOB_REASON_JOB_SCHEDULER_OPTIMIZATION); + } + + // Update the pending reason for any jobs that aren't going to be run. + final int numRunnableJobs = runnableJobs.size(); + if (numRunnableJobs > 0 && numRunnableJobs != jobsToRun.size()) { + synchronized (mPendingJobReasonCache) { + for (int i = 0; i < numRunnableJobs; ++i) { + final JobStatus job = runnableJobs.get(i); + if (jobsToRun.contains(job)) { + // We're running this job. Skip updating the pending reason. + continue; } + SparseIntArray reasons = + mPendingJobReasonCache.get(job.getUid(), job.getNamespace()); + if (reasons == null) { + reasons = new SparseIntArray(); + mPendingJobReasonCache.add(job.getUid(), job.getNamespace(), reasons); + } + // We're force batching these jobs, so consider it an optimization + // policy reason. + reasons.put(job.getJobId(), + JobScheduler.PENDING_JOB_REASON_JOB_SCHEDULER_OPTIMIZATION); } } } @@ -3610,9 +3957,10 @@ public class JobSchedulerService extends com.android.server.SystemService @VisibleForTesting void reset() { - forceBatchedCount = 0; - unbatchedCount = 0; runnableJobs.clear(); + mBatches.clear(); + mUnbatchedJobs.clear(); + mUnbatchedJobCount.clear(); } } @@ -3948,20 +4296,34 @@ public class JobSchedulerService extends com.android.server.SystemService .sendToTarget(); } - private final class BatteryStateTracker extends BroadcastReceiver { + @VisibleForTesting + final class BatteryStateTracker extends BroadcastReceiver + implements BatteryManagerInternal.ChargingPolicyChangeListener { + private final BatteryManagerInternal mBatteryManagerInternal; + + /** Last reported battery level. */ + private int mBatteryLevel; + /** Keep track of whether the battery is charged enough that we want to do work. */ + private boolean mBatteryNotLow; /** - * Track whether we're "charging", where charging means that we're ready to commit to - * doing work. + * Charging status based on {@link BatteryManager#ACTION_CHARGING} and + * {@link BatteryManager#ACTION_DISCHARGING}. */ private boolean mCharging; - /** Keep track of whether the battery is charged enough that we want to do work. */ - private boolean mBatteryNotLow; + /** + * The most recently acquired value of + * {@link BatteryManager#BATTERY_PROPERTY_CHARGING_POLICY}. + */ + private int mChargingPolicy; + /** Track whether there is power connected. It doesn't mean the device is charging. */ + private boolean mPowerConnected; /** Sequence number of last broadcast. */ private int mLastBatterySeq = -1; private BroadcastReceiver mMonitor; BatteryStateTracker() { + mBatteryManagerInternal = LocalServices.getService(BatteryManagerInternal.class); } public void startTracking() { @@ -3973,13 +4335,18 @@ public class JobSchedulerService extends com.android.server.SystemService // Charging/not charging. filter.addAction(BatteryManager.ACTION_CHARGING); filter.addAction(BatteryManager.ACTION_DISCHARGING); + filter.addAction(Intent.ACTION_BATTERY_LEVEL_CHANGED); + filter.addAction(Intent.ACTION_POWER_CONNECTED); + filter.addAction(Intent.ACTION_POWER_DISCONNECTED); getTestableContext().registerReceiver(this, filter); + mBatteryManagerInternal.registerChargingPolicyChangeListener(this); + // Initialise tracker state. - BatteryManagerInternal batteryManagerInternal = - LocalServices.getService(BatteryManagerInternal.class); - mBatteryNotLow = !batteryManagerInternal.getBatteryLevelLow(); - mCharging = batteryManagerInternal.isPowered(BatteryManager.BATTERY_PLUGGED_ANY); + mBatteryLevel = mBatteryManagerInternal.getBatteryLevel(); + mBatteryNotLow = !mBatteryManagerInternal.getBatteryLevelLow(); + mCharging = mBatteryManagerInternal.isPowered(BatteryManager.BATTERY_PLUGGED_ANY); + mChargingPolicy = mBatteryManagerInternal.getChargingPolicy(); } public void setMonitorBatteryLocked(boolean enabled) { @@ -4002,7 +4369,7 @@ public class JobSchedulerService extends com.android.server.SystemService } public boolean isCharging() { - return mCharging; + return isConsideredCharging(); } public boolean isBatteryNotLow() { @@ -4013,17 +4380,42 @@ public class JobSchedulerService extends com.android.server.SystemService return mMonitor != null; } + public boolean isPowerConnected() { + return mPowerConnected; + } + public int getSeq() { return mLastBatterySeq; } @Override + public void onChargingPolicyChanged(int newPolicy) { + synchronized (mLock) { + if (mChargingPolicy == newPolicy) { + return; + } + if (DEBUG) { + Slog.i(TAG, + "Charging policy changed from " + mChargingPolicy + " to " + newPolicy); + } + + final boolean wasConsideredCharging = isConsideredCharging(); + mChargingPolicy = newPolicy; + + if (isConsideredCharging() != wasConsideredCharging) { + for (int c = mControllers.size() - 1; c >= 0; --c) { + mControllers.get(c).onBatteryStateChangedLocked(); + } + } + } + } + + @Override public void onReceive(Context context, Intent intent) { onReceiveInternal(intent); } - @VisibleForTesting - public void onReceiveInternal(Intent intent) { + private void onReceiveInternal(Intent intent) { synchronized (mLock) { final String action = intent.getAction(); boolean changed = false; @@ -4043,21 +4435,49 @@ public class JobSchedulerService extends com.android.server.SystemService mBatteryNotLow = true; changed = true; } + } else if (Intent.ACTION_BATTERY_LEVEL_CHANGED.equals(action)) { + if (DEBUG) { + Slog.d(TAG, "Battery level changed @ " + + sElapsedRealtimeClock.millis()); + } + final boolean wasConsideredCharging = isConsideredCharging(); + mBatteryLevel = mBatteryManagerInternal.getBatteryLevel(); + changed = isConsideredCharging() != wasConsideredCharging; + } else if (Intent.ACTION_POWER_CONNECTED.equals(action)) { + if (DEBUG) { + Slog.d(TAG, "Power connected @ " + sElapsedRealtimeClock.millis()); + } + if (mPowerConnected) { + return; + } + mPowerConnected = true; + changed = true; + } else if (Intent.ACTION_POWER_DISCONNECTED.equals(action)) { + if (DEBUG) { + Slog.d(TAG, "Power disconnected @ " + sElapsedRealtimeClock.millis()); + } + if (!mPowerConnected) { + return; + } + mPowerConnected = false; + changed = true; } else if (BatteryManager.ACTION_CHARGING.equals(action)) { if (DEBUG) { Slog.d(TAG, "Battery charging @ " + sElapsedRealtimeClock.millis()); } if (!mCharging) { + final boolean wasConsideredCharging = isConsideredCharging(); mCharging = true; - changed = true; + changed = isConsideredCharging() != wasConsideredCharging; } } else if (BatteryManager.ACTION_DISCHARGING.equals(action)) { if (DEBUG) { Slog.d(TAG, "Battery discharging @ " + sElapsedRealtimeClock.millis()); } if (mCharging) { + final boolean wasConsideredCharging = isConsideredCharging(); mCharging = false; - changed = true; + changed = isConsideredCharging() != wasConsideredCharging; } } mLastBatterySeq = @@ -4069,6 +4489,30 @@ public class JobSchedulerService extends com.android.server.SystemService } } } + + private boolean isConsideredCharging() { + if (mCharging) { + return true; + } + // BatteryService (or Health HAL or whatever central location makes sense) + // should ideally hold this logic so that everyone has a consistent + // idea of when the device is charging (or an otherwise stable charging/plugged state). + // TODO(304512874): move this determination to BatteryService + if (!mPowerConnected) { + return false; + } + + if (mChargingPolicy == Integer.MIN_VALUE) { + // Property not supported on this device. + return false; + } + // Adaptive charging policies don't expose their target battery level, but 80% is a + // commonly used threshold for battery health, so assume that's what's being used by + // the policies and use 70%+ as the threshold here for charging in case some + // implementations choose to discharge the device slightly before recharging back up + // to the target level. + return mBatteryLevel >= 70 && BatteryManager.isAdaptiveChargingPolicy(mChargingPolicy); + } } final class LocalService implements JobSchedulerInternal { @@ -4169,6 +4613,11 @@ public class JobSchedulerService extends com.android.server.SystemService } @Override + public boolean hasRunBackupJobsPermission(@NonNull String packageName, int packageUid) { + return JobSchedulerService.this.hasRunBackupJobsPermission(packageName, packageUid); + } + + @Override public JobStorePersistStats getPersistStats() { synchronized (mLock) { return new JobStorePersistStats(mJobs.getPersistStats()); @@ -4331,6 +4780,22 @@ public class JobSchedulerService extends com.android.server.SystemService } /** + * Returns whether the app holds the {@link Manifest.permission.RUN_BACKUP_JOBS} permission. + */ + private boolean hasRunBackupJobsPermission(@NonNull String packageName, int packageUid) { + if (packageName == null) { + Slog.wtfStack(TAG, + "Expected a non-null package name when calling hasRunBackupJobsPermission"); + return false; + } + + return PermissionChecker.checkPermissionForPreflight(getTestableContext(), + android.Manifest.permission.RUN_BACKUP_JOBS, + PermissionChecker.PID_UNKNOWN, packageUid, packageName) + == PermissionChecker.PERMISSION_GRANTED; + } + + /** * Binder stub trampoline implementation */ final class JobSchedulerStub extends IJobScheduler.Stub { @@ -4381,8 +4846,7 @@ public class JobSchedulerService extends com.android.server.SystemService private JobInfo enforceBuilderApiPermissions(int uid, int pid, JobInfo job) { if (job.getBias() != JobInfo.BIAS_DEFAULT && !hasPermission(uid, pid, Manifest.permission.UPDATE_DEVICE_STATS)) { - if (CompatChanges.isChangeEnabled(THROW_ON_UNSUPPORTED_BIAS_USAGE, uid) - && Flags.throwOnUnsupportedBiasUsage()) { + if (CompatChanges.isChangeEnabled(THROW_ON_UNSUPPORTED_BIAS_USAGE, uid)) { throw new SecurityException("Apps may not call setBias()"); } else { // We can't throw the exception. Log the issue and modify the job to remove @@ -4390,7 +4854,7 @@ public class JobSchedulerService extends com.android.server.SystemService Slog.w(TAG, "Uid " + uid + " set bias on its job"); return new JobInfo.Builder(job) .setBias(JobInfo.BIAS_DEFAULT) - .build(false, false, false); + .build(false, false, false, false); } } @@ -4414,7 +4878,9 @@ public class JobSchedulerService extends com.android.server.SystemService JobInfo.DISALLOW_DEADLINES_FOR_PREFETCH_JOBS, callingUid), rejectNegativeNetworkEstimates, CompatChanges.isChangeEnabled( - JobInfo.ENFORCE_MINIMUM_TIME_WINDOWS, callingUid)); + JobInfo.ENFORCE_MINIMUM_TIME_WINDOWS, callingUid), + CompatChanges.isChangeEnabled( + JobInfo.REJECT_NEGATIVE_DELAYS_AND_DEADLINES, callingUid)); if ((job.getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0) { getContext().enforceCallingOrSelfPermission( android.Manifest.permission.CONNECTIVITY_INTERNAL, TAG); @@ -4946,6 +5412,8 @@ public class JobSchedulerService extends com.android.server.SystemService Slog.d(TAG, "executeRunCommand(): " + pkgName + "/" + namespace + "/" + userId + " " + jobId + " s=" + satisfied + " f=" + force); + final CountDownLatch delayLatch = new CountDownLatch(1); + final JobStatus js; try { final int uid = AppGlobals.getPackageManager().getPackageUid(pkgName, 0, userId != UserHandle.USER_ALL ? userId : UserHandle.USER_SYSTEM); @@ -4954,7 +5422,7 @@ public class JobSchedulerService extends com.android.server.SystemService } synchronized (mLock) { - final JobStatus js = mJobs.getJobByUidAndJobId(uid, namespace, jobId); + js = mJobs.getJobByUidAndJobId(uid, namespace, jobId); if (js == null) { return JobSchedulerShellCommand.CMD_ERR_NO_JOB; } @@ -4965,23 +5433,71 @@ public class JobSchedulerService extends com.android.server.SystemService // Re-evaluate constraints after the override is set in case one of the overridden // constraints was preventing another constraint from thinking it needed to update. for (int c = mControllers.size() - 1; c >= 0; --c) { - mControllers.get(c).reevaluateStateLocked(uid); + mControllers.get(c).evaluateStateLocked(js); } if (!js.isConstraintsSatisfied()) { - js.overrideState = JobStatus.OVERRIDE_NONE; - return JobSchedulerShellCommand.CMD_ERR_CONSTRAINTS; + if (js.hasConnectivityConstraint() + && !js.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY) + && js.wouldBeReadyWithConstraint(JobStatus.CONSTRAINT_CONNECTIVITY)) { + // Because of how asynchronous the connectivity signals are, JobScheduler + // may not get the connectivity satisfaction signal immediately. In this + // case, wait a few seconds to see if it comes in before saying the + // connectivity constraint isn't satisfied. + mHandler.postDelayed( + checkConstraintRunnableForTesting( + mHandler, js, delayLatch, 5, 1000), + 1000); + } else { + // There's no asynchronous signal to wait for. We can immediately say the + // job's constraints aren't satisfied and return. + js.overrideState = JobStatus.OVERRIDE_NONE; + return JobSchedulerShellCommand.CMD_ERR_CONSTRAINTS; + } + } else { + delayLatch.countDown(); } - - queueReadyJobsForExecutionLocked(); - maybeRunPendingJobsLocked(); } } catch (RemoteException e) { // can't happen + return 0; + } + + // Choose to block the return until we're sure about the state of the connectivity job + // so that tests can expect a reliable state after calling the run command. + try { + delayLatch.await(7L, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Slog.e(TAG, "Couldn't wait for asynchronous constraint change", e); + } + + synchronized (mLock) { + if (!js.isConstraintsSatisfied()) { + js.overrideState = JobStatus.OVERRIDE_NONE; + return JobSchedulerShellCommand.CMD_ERR_CONSTRAINTS; + } + + queueReadyJobsForExecutionLocked(); + maybeRunPendingJobsLocked(); } return 0; } + private static Runnable checkConstraintRunnableForTesting(@NonNull final Handler handler, + @NonNull final JobStatus js, @NonNull final CountDownLatch latch, + final int remainingAttempts, final long delayMs) { + return () -> { + if (remainingAttempts <= 0 || js.isConstraintsSatisfied()) { + latch.countDown(); + return; + } + handler.postDelayed( + checkConstraintRunnableForTesting( + handler, js, latch, remainingAttempts - 1, delayMs), + delayMs); + }; + } + // Shell command infrastructure: immediately timeout currently executing jobs int executeStopCommand(PrintWriter pw, String pkgName, int userId, @Nullable String namespace, boolean hasJobId, int jobId, @@ -5066,6 +5582,25 @@ public class JobSchedulerService extends com.android.server.SystemService } } + /** Return {@code true} if the device is connected to power. */ + public boolean isPowerConnected() { + synchronized (mLock) { + return mBatteryStateTracker.isPowerConnected(); + } + } + + void setCacheConfigChanges(boolean enabled) { + synchronized (mLock) { + mConstantsObserver.setCacheConfigChangesLocked(enabled); + } + } + + String getConfigValue(String key) { + synchronized (mLock) { + return mConstantsObserver.getValueLocked(key); + } + } + int getStorageSeq() { synchronized (mLock) { return mStorageController.getTracker().getSeq(); @@ -5369,8 +5904,16 @@ public class JobSchedulerService extends com.android.server.SystemService pw.println("Aconfig flags:"); pw.increaseIndent(); - pw.print(Flags.FLAG_THROW_ON_UNSUPPORTED_BIAS_USAGE, - Flags.throwOnUnsupportedBiasUsage()); + pw.print(Flags.FLAG_BATCH_ACTIVE_BUCKET_JOBS, Flags.batchActiveBucketJobs()); + pw.println(); + pw.print(Flags.FLAG_BATCH_CONNECTIVITY_JOBS_PER_NETWORK, + Flags.batchConnectivityJobsPerNetwork()); + pw.println(); + pw.print(Flags.FLAG_DO_NOT_FORCE_RUSH_EXECUTION_AT_BOOT, + Flags.doNotForceRushExecutionAtBoot()); + pw.println(); + pw.print(android.app.job.Flags.FLAG_BACKUP_JOBS_EXEMPTION, + android.app.job.Flags.backupJobsExemption()); pw.println(); pw.decreaseIndent(); pw.println(); @@ -5383,8 +5926,14 @@ public class JobSchedulerService extends com.android.server.SystemService mQuotaTracker.dump(pw); pw.println(); + pw.print("Power connected: "); + pw.println(mBatteryStateTracker.isPowerConnected()); pw.print("Battery charging: "); - pw.println(mBatteryStateTracker.isCharging()); + pw.println(mBatteryStateTracker.mCharging); + pw.print("Considered charging: "); + pw.println(mBatteryStateTracker.isConsideredCharging()); + pw.print("Battery level: "); + pw.println(mBatteryStateTracker.mBatteryLevel); pw.print("Battery not low: "); pw.println(mBatteryStateTracker.isBatteryNotLow()); if (mBatteryStateTracker.isMonitoring()) { diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java index c14efae3fa62..af7b27e51e20 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java @@ -16,6 +16,7 @@ package com.android.server.job; +import android.Manifest; import android.annotation.Nullable; import android.app.ActivityManager; import android.app.AppGlobals; @@ -66,6 +67,8 @@ public final class JobSchedulerShellCommand extends BasicShellCommandHandler { return getBatteryCharging(pw); case "get-battery-not-low": return getBatteryNotLow(pw); + case "get-config-value": + return getConfigValue(pw); case "get-estimated-download-bytes": return getEstimatedNetworkBytes(pw, BYTE_OPTION_DOWNLOAD); case "get-estimated-upload-bytes": @@ -82,6 +85,8 @@ public final class JobSchedulerShellCommand extends BasicShellCommandHandler { return getJobState(pw); case "heartbeat": return doHeartbeat(pw); + case "cache-config-changes": + return cacheConfigChanges(pw); case "reset-execution-quota": return resetExecutionQuota(pw); case "reset-schedule-quota": @@ -100,13 +105,16 @@ public final class JobSchedulerShellCommand extends BasicShellCommandHandler { } private void checkPermission(String operation) throws Exception { + checkPermission(operation, Manifest.permission.CHANGE_APP_IDLE_STATE); + } + + private void checkPermission(String operation, String permission) throws Exception { final int uid = Binder.getCallingUid(); if (uid == 0) { // Root can do anything. return; } - final int perm = mPM.checkUidPermission( - "android.permission.CHANGE_APP_IDLE_STATE", uid); + final int perm = mPM.checkUidPermission(permission, uid); if (perm != PackageManager.PERMISSION_GRANTED) { throw new SecurityException("Uid " + uid + " not permitted to " + operation); @@ -339,19 +347,28 @@ public final class JobSchedulerShellCommand extends BasicShellCommandHandler { } private int getAconfigFlagState(PrintWriter pw) throws Exception { - checkPermission("get aconfig flag state"); + checkPermission("get aconfig flag state", Manifest.permission.DUMP); final String flagName = getNextArgRequired(); switch (flagName) { + case android.app.job.Flags.FLAG_ENFORCE_MINIMUM_TIME_WINDOWS: + pw.println(android.app.job.Flags.enforceMinimumTimeWindows()); + break; case android.app.job.Flags.FLAG_JOB_DEBUG_INFO_APIS: pw.println(android.app.job.Flags.jobDebugInfoApis()); break; - case android.app.job.Flags.FLAG_ENFORCE_MINIMUM_TIME_WINDOWS: - pw.println(android.app.job.Flags.enforceMinimumTimeWindows()); + case com.android.server.job.Flags.FLAG_BATCH_ACTIVE_BUCKET_JOBS: + pw.println(com.android.server.job.Flags.batchActiveBucketJobs()); + break; + case com.android.server.job.Flags.FLAG_BATCH_CONNECTIVITY_JOBS_PER_NETWORK: + pw.println(com.android.server.job.Flags.batchConnectivityJobsPerNetwork()); + break; + case com.android.server.job.Flags.FLAG_DO_NOT_FORCE_RUSH_EXECUTION_AT_BOOT: + pw.println(com.android.server.job.Flags.doNotForceRushExecutionAtBoot()); break; - case com.android.server.job.Flags.FLAG_THROW_ON_UNSUPPORTED_BIAS_USAGE: - pw.println(com.android.server.job.Flags.throwOnUnsupportedBiasUsage()); + case android.app.job.Flags.FLAG_BACKUP_JOBS_EXEMPTION: + pw.println(android.app.job.Flags.backupJobsExemption()); break; default: pw.println("Unknown flag: " + flagName); @@ -378,6 +395,20 @@ public final class JobSchedulerShellCommand extends BasicShellCommandHandler { return 0; } + private int getConfigValue(PrintWriter pw) throws Exception { + checkPermission("get device config value", Manifest.permission.DUMP); + + final String key = getNextArgRequired(); + + final long ident = Binder.clearCallingIdentity(); + try { + pw.println(mInternal.getConfigValue(key)); + return 0; + } finally { + Binder.restoreCallingIdentity(ident); + } + } + private int getEstimatedNetworkBytes(PrintWriter pw, int byteOption) throws Exception { checkPermission("get estimated bytes"); @@ -528,6 +559,28 @@ public final class JobSchedulerShellCommand extends BasicShellCommandHandler { return -1; } + private int cacheConfigChanges(PrintWriter pw) throws Exception { + checkPermission("change config caching", Manifest.permission.DUMP); + String opt = getNextArgRequired(); + boolean enabled; + if ("on".equals(opt)) { + enabled = true; + } else if ("off".equals(opt)) { + enabled = false; + } else { + getErrPrintWriter().println("Error: unknown option " + opt); + return 1; + } + final long ident = Binder.clearCallingIdentity(); + try { + mInternal.setCacheConfigChanges(enabled); + pw.println("Config caching " + (enabled ? "enabled" : "disabled")); + } finally { + Binder.restoreCallingIdentity(ident); + } + return 0; + } + private int resetExecutionQuota(PrintWriter pw) throws Exception { checkPermission("reset execution quota"); @@ -714,6 +767,9 @@ public final class JobSchedulerShellCommand extends BasicShellCommandHandler { pw.println(" is null (no namespace)."); pw.println(" heartbeat [num]"); pw.println(" No longer used."); + pw.println(" cache-config-changes [on|off]"); + pw.println(" Control caching the set of most recently processed config flags."); + pw.println(" Off by default. Turning on makes get-config-value useful."); pw.println(" monitor-battery [on|off]"); pw.println(" Control monitoring of all battery changes. Off by default. Turning"); pw.println(" on makes get-battery-seq useful."); @@ -726,6 +782,9 @@ public final class JobSchedulerShellCommand extends BasicShellCommandHandler { pw.println(" Return whether the battery is currently considered to be charging."); pw.println(" get-battery-not-low"); pw.println(" Return whether the battery is currently considered to not be low."); + pw.println(" get-config-value KEY"); + pw.println(" Return the most recently processed and cached config value for the KEY."); + pw.println(" Only useful if caching is turned on with cache-config-changes."); pw.println(" get-estimated-download-bytes [-u | --user USER_ID]" + " [-n | --namespace NAMESPACE] PACKAGE JOB_ID"); pw.println(" Return the most recent estimated download bytes for the job."); diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java index 6449edcd3103..8ab7d2fae49f 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java @@ -534,7 +534,16 @@ public final class JobServiceContext implements ServiceConnection { /* system_measured_source_download_bytes */ 0, /* system_measured_source_upload_bytes */ 0, /* system_measured_calling_download_bytes */ 0, - /* system_measured_calling_upload_bytes */ 0); + /* system_measured_calling_upload_bytes */ 0, + job.getJob().getIntervalMillis(), + job.getJob().getFlexMillis(), + job.hasFlexibilityConstraint(), + job.isConstraintSatisfied(JobStatus.CONSTRAINT_FLEXIBLE), + job.canApplyTransportAffinities(), + job.getNumAppliedFlexibleConstraints(), + job.getNumDroppedFlexibleConstraints(), + job.getFilteredTraceTag(), + job.getFilteredDebugTags()); sEnqueuedJwiAtJobStart.logSampleWithUid(job.getUid(), job.getWorkCount()); final String sourcePackage = job.getSourcePackageName(); if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) { @@ -1616,7 +1625,16 @@ public final class JobServiceContext implements ServiceConnection { TrafficStats.getUidRxBytes(completedJob.getUid()) - mInitialDownloadedBytesFromCalling, TrafficStats.getUidTxBytes(completedJob.getUid()) - - mInitialUploadedBytesFromCalling); + - mInitialUploadedBytesFromCalling, + completedJob.getJob().getIntervalMillis(), + completedJob.getJob().getFlexMillis(), + completedJob.hasFlexibilityConstraint(), + completedJob.isConstraintSatisfied(JobStatus.CONSTRAINT_FLEXIBLE), + completedJob.canApplyTransportAffinities(), + completedJob.getNumAppliedFlexibleConstraints(), + completedJob.getNumDroppedFlexibleConstraints(), + completedJob.getFilteredTraceTag(), + completedJob.getFilteredDebugTags()); if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) { Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_SYSTEM_SERVER, "JobScheduler", getId()); diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java index 53b14d616ecc..d8934d8f83b8 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java @@ -1495,7 +1495,7 @@ public final class JobStore { // return value), the deadline is dropped. Periodic jobs require all constraints // to be met, so there's no issue with their deadlines. // The same logic applies for other target SDK-based validation checks. - builtJob = jobBuilder.build(false, false, false); + builtJob = jobBuilder.build(false, false, false, false); } catch (Exception e) { Slog.w(TAG, "Unable to build job from XML, ignoring: " + jobBuilder.summarize(), e); return null; diff --git a/apex/jobscheduler/service/java/com/android/server/job/PendingJobQueue.java b/apex/jobscheduler/service/java/com/android/server/job/PendingJobQueue.java index 4f4096f69ad5..813cf8710ab1 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/PendingJobQueue.java +++ b/apex/jobscheduler/service/java/com/android/server/job/PendingJobQueue.java @@ -18,6 +18,7 @@ package com.android.server.job; import android.annotation.NonNull; import android.annotation.Nullable; +import android.util.ArraySet; import android.util.Pools; import android.util.SparseArray; @@ -96,10 +97,10 @@ class PendingJobQueue { } } - void addAll(@NonNull List<JobStatus> jobs) { + void addAll(@NonNull ArraySet<JobStatus> jobs) { final SparseArray<List<JobStatus>> jobsByUid = new SparseArray<>(); for (int i = jobs.size() - 1; i >= 0; --i) { - final JobStatus job = jobs.get(i); + final JobStatus job = jobs.valueAt(i); List<JobStatus> appJobs = jobsByUid.get(job.getSourceUid()); if (appJobs == null) { appJobs = new ArrayList<>(); diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java index e3ba50dc635b..e3ac780abf09 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java @@ -318,6 +318,10 @@ public final class BackgroundJobsController extends StateController { try { final boolean isStopped = mPackageManagerInternal.isPackageStopped(packageName, uid); + if (DEBUG) { + Slog.d(TAG, + "Pulled stopped state of " + packageName + " (" + uid + "): " + isStopped); + } mPackageStoppedState.add(uid, packageName, isStopped); return isStopped; } catch (PackageManager.NameNotFoundException e) { diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java index ddbc2ecf5e3e..e9f9b14daed3 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java @@ -20,12 +20,6 @@ import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; import android.annotation.NonNull; import android.app.job.JobInfo; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.BatteryManager; -import android.os.BatteryManagerInternal; import android.os.UserHandle; import android.util.ArraySet; import android.util.IndentingPrintWriter; @@ -36,7 +30,6 @@ import android.util.proto.ProtoOutputStream; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.server.AppSchedulingModuleThread; -import com.android.server.LocalServices; import com.android.server.job.JobSchedulerService; import com.android.server.job.StateControllerProto; @@ -60,8 +53,6 @@ public final class BatteryController extends RestrictingController { @GuardedBy("mLock") private final ArraySet<JobStatus> mTopStartedJobs = new ArraySet<>(); - private final PowerTracker mPowerTracker; - private final FlexibilityController mFlexibilityController; /** * Helper set to avoid too much GC churn from frequent calls to @@ -77,16 +68,10 @@ public final class BatteryController extends RestrictingController { public BatteryController(JobSchedulerService service, FlexibilityController flexibilityController) { super(service); - mPowerTracker = new PowerTracker(); mFlexibilityController = flexibilityController; } @Override - public void startTrackingLocked() { - mPowerTracker.startTracking(); - } - - @Override public void maybeStartTrackingJobLocked(JobStatus taskStatus, JobStatus lastJob) { if (taskStatus.hasPowerConstraint()) { final long nowElapsed = sElapsedRealtimeClock.millis(); @@ -95,7 +80,7 @@ public final class BatteryController extends RestrictingController { if (taskStatus.hasChargingConstraint()) { if (hasTopExemptionLocked(taskStatus)) { taskStatus.setChargingConstraintSatisfied(nowElapsed, - mPowerTracker.isPowerConnected()); + mService.isPowerConnected()); } else { taskStatus.setChargingConstraintSatisfied(nowElapsed, mService.isBatteryCharging() && mService.isBatteryNotLow()); @@ -178,7 +163,7 @@ public final class BatteryController extends RestrictingController { @GuardedBy("mLock") private void maybeReportNewChargingStateLocked() { - final boolean powerConnected = mPowerTracker.isPowerConnected(); + final boolean powerConnected = mService.isPowerConnected(); final boolean stablePower = mService.isBatteryCharging() && mService.isBatteryNotLow(); final boolean batteryNotLow = mService.isBatteryNotLow(); if (DEBUG) { @@ -239,62 +224,6 @@ public final class BatteryController extends RestrictingController { mChangedJobs.clear(); } - private final class PowerTracker extends BroadcastReceiver { - /** - * Track whether there is power connected. It doesn't mean the device is charging. - * Use {@link JobSchedulerService#isBatteryCharging()} to determine if the device is - * charging. - */ - private boolean mPowerConnected; - - PowerTracker() { - } - - void startTracking() { - IntentFilter filter = new IntentFilter(); - - filter.addAction(Intent.ACTION_POWER_CONNECTED); - filter.addAction(Intent.ACTION_POWER_DISCONNECTED); - mContext.registerReceiver(this, filter); - - // Initialize tracker state. - BatteryManagerInternal batteryManagerInternal = - LocalServices.getService(BatteryManagerInternal.class); - mPowerConnected = batteryManagerInternal.isPowered(BatteryManager.BATTERY_PLUGGED_ANY); - } - - boolean isPowerConnected() { - return mPowerConnected; - } - - @Override - public void onReceive(Context context, Intent intent) { - synchronized (mLock) { - final String action = intent.getAction(); - - if (Intent.ACTION_POWER_CONNECTED.equals(action)) { - if (DEBUG) { - Slog.d(TAG, "Power connected @ " + sElapsedRealtimeClock.millis()); - } - if (mPowerConnected) { - return; - } - mPowerConnected = true; - } else if (Intent.ACTION_POWER_DISCONNECTED.equals(action)) { - if (DEBUG) { - Slog.d(TAG, "Power disconnected @ " + sElapsedRealtimeClock.millis()); - } - if (!mPowerConnected) { - return; - } - mPowerConnected = false; - } - - maybeReportNewChargingStateLocked(); - } - } - } - @VisibleForTesting ArraySet<JobStatus> getTrackedJobs() { return mTrackedTasks; @@ -308,7 +237,6 @@ public final class BatteryController extends RestrictingController { @Override public void dumpControllerStateLocked(IndentingPrintWriter pw, Predicate<JobStatus> predicate) { - pw.println("Power connected: " + mPowerTracker.isPowerConnected()); pw.println("Stable power: " + (mService.isBatteryCharging() && mService.isBatteryNotLow())); pw.println("Not low: " + mService.isBatteryNotLow()); 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 f40508302ee3..3219f7e5ce20 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 @@ -21,12 +21,13 @@ import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED; import static android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED; import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET; +import static android.net.NetworkCapabilities.TRANSPORT_SATELLITE; import static android.net.NetworkCapabilities.TRANSPORT_WIFI; +import static android.text.format.DateUtils.MINUTE_IN_MILLIS; import static com.android.server.job.JobSchedulerService.RESTRICTED_INDEX; import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; import static com.android.server.job.Flags.FLAG_RELAX_PREFETCH_CONNECTIVITY_CONSTRAINT_ONLY_ON_CHARGER; -import static com.android.server.job.Flags.relaxPrefetchConnectivityConstraintOnlyOnCharger; import android.annotation.NonNull; import android.annotation.Nullable; @@ -66,6 +67,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.server.AppSchedulingModuleThread; import com.android.server.LocalServices; +import com.android.server.job.Flags; import com.android.server.job.JobSchedulerService; import com.android.server.job.JobSchedulerService.Constants; import com.android.server.job.StateControllerProto; @@ -138,8 +140,9 @@ public final class ConnectivityController extends RestrictingController implemen static final SparseIntArray sNetworkTransportAffinities = new SparseIntArray(); static { sNetworkTransportAffinities.put(TRANSPORT_CELLULAR, TRANSPORT_AFFINITY_AVOID); - sNetworkTransportAffinities.put(TRANSPORT_WIFI, TRANSPORT_AFFINITY_PREFER); sNetworkTransportAffinities.put(TRANSPORT_ETHERNET, TRANSPORT_AFFINITY_PREFER); + sNetworkTransportAffinities.put(TRANSPORT_SATELLITE, TRANSPORT_AFFINITY_AVOID); + sNetworkTransportAffinities.put(TRANSPORT_WIFI, TRANSPORT_AFFINITY_PREFER); } private final CcConfig mCcConfig; @@ -166,6 +169,10 @@ public final class ConnectivityController extends RestrictingController implemen @GuardedBy("mLock") private final ArrayMap<Network, CachedNetworkMetadata> mAvailableNetworks = new ArrayMap<>(); + @GuardedBy("mLock") + @Nullable + private Network mSystemDefaultNetwork; + private final SparseArray<UidDefaultNetworkCallback> mCurrentDefaultNetworkCallbacks = new SparseArray<>(); private final Comparator<UidStats> mUidStatsComparator = new Comparator<UidStats>() { @@ -286,6 +293,7 @@ public final class ConnectivityController extends RestrictingController implemen private static final int MSG_UPDATE_ALL_TRACKED_JOBS = 1; private static final int MSG_DATA_SAVER_TOGGLED = 2; private static final int MSG_UID_POLICIES_CHANGED = 3; + private static final int MSG_PROCESS_ACTIVE_NETWORK = 4; private final Handler mHandler; @@ -313,6 +321,14 @@ public final class ConnectivityController extends RestrictingController implemen } } + @Override + public void startTrackingLocked() { + if (Flags.batchConnectivityJobsPerNetwork()) { + mConnManager.registerSystemDefaultNetworkCallback(mDefaultNetworkCallback, mHandler); + mConnManager.addDefaultNetworkActiveListener(this); + } + } + @GuardedBy("mLock") @Override public void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob) { @@ -911,8 +927,8 @@ public final class ConnectivityController extends RestrictingController implemen 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. + // VPNs may have multiple underlying networks and determining the correct strength + // may not be straightforward. // 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 @@ -1007,7 +1023,7 @@ public final class ConnectivityController extends RestrictingController implemen // Need to at least know the estimated download bytes for a prefetch job. return false; } - if (relaxPrefetchConnectivityConstraintOnlyOnCharger()) { + if (Flags.relaxPrefetchConnectivityConstraintOnlyOnCharger()) { // Since the constraint relaxation isn't required by the job, only do it when the // device is charging and the battery level is above the "low battery" threshold. if (!mService.isBatteryCharging() || !mService.isBatteryNotLow()) { @@ -1309,7 +1325,7 @@ public final class ConnectivityController extends RestrictingController implemen } @Nullable - private NetworkCapabilities getNetworkCapabilities(@Nullable Network network) { + public NetworkCapabilities getNetworkCapabilities(@Nullable Network network) { final CachedNetworkMetadata metadata = getNetworkMetadata(network); return metadata == null ? null : metadata.networkCapabilities; } @@ -1527,26 +1543,138 @@ public final class ConnectivityController extends RestrictingController implemen } /** + * Returns {@code true} if the job's assigned network is active or otherwise considered to be + * in a good state to run the job now. + */ + @GuardedBy("mLock") + public boolean isNetworkInStateForJobRunLocked(@NonNull JobStatus jobStatus) { + if (jobStatus.network == null) { + return false; + } + if (jobStatus.shouldTreatAsExpeditedJob() || jobStatus.shouldTreatAsUserInitiatedJob() + || mService.getUidProcState(jobStatus.getSourceUid()) + <= ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE) { + // EJs, UIJs, and BFGS+ jobs should be able to activate the network. + return true; + } + return isNetworkInStateForJobRunLocked(jobStatus.network); + } + + @GuardedBy("mLock") + @VisibleForTesting + boolean isNetworkInStateForJobRunLocked(@NonNull Network network) { + if (!Flags.batchConnectivityJobsPerNetwork()) { + // Active network batching isn't enabled. We don't care about the network state. + return true; + } + + CachedNetworkMetadata cachedNetworkMetadata = mAvailableNetworks.get(network); + if (cachedNetworkMetadata == null) { + return false; + } + + final long nowElapsed = sElapsedRealtimeClock.millis(); + if (cachedNetworkMetadata.defaultNetworkActivationLastConfirmedTimeElapsed + + mCcConfig.NETWORK_ACTIVATION_EXPIRATION_MS > nowElapsed) { + // Network is still presumed to be active. + return true; + } + + final boolean inactiveForTooLong = + cachedNetworkMetadata.capabilitiesFirstAcquiredTimeElapsed + < nowElapsed - mCcConfig.NETWORK_ACTIVATION_MAX_WAIT_TIME_MS + && cachedNetworkMetadata.defaultNetworkActivationLastConfirmedTimeElapsed + < nowElapsed - mCcConfig.NETWORK_ACTIVATION_MAX_WAIT_TIME_MS; + // We can only know the state of the system default network. If that's not available + // or the network in question isn't the system default network, + // then return true if we haven't gotten an active signal in a long time. + if (mSystemDefaultNetwork == null) { + return inactiveForTooLong; + } + + if (!mSystemDefaultNetwork.equals(network)) { + final NetworkCapabilities capabilities = cachedNetworkMetadata.networkCapabilities; + if (capabilities != null + && capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) { + // VPNs won't have an active signal sent for them. Check their underlying networks + // instead, prioritizing the system default if it's one of them. + final List<Network> underlyingNetworks = capabilities.getUnderlyingNetworks(); + if (underlyingNetworks == null) { + return inactiveForTooLong; + } + + if (underlyingNetworks.contains(mSystemDefaultNetwork)) { + if (DEBUG) { + Slog.i(TAG, "Substituting system default network " + + mSystemDefaultNetwork + " for VPN " + network); + } + return isNetworkInStateForJobRunLocked(mSystemDefaultNetwork); + } + + for (int i = underlyingNetworks.size() - 1; i >= 0; --i) { + if (isNetworkInStateForJobRunLocked(underlyingNetworks.get(i))) { + return true; + } + } + } + return inactiveForTooLong; + } + + if (cachedNetworkMetadata.defaultNetworkActivationLastCheckTimeElapsed + + mCcConfig.NETWORK_ACTIVATION_EXPIRATION_MS < nowElapsed) { + // We haven't checked the state recently enough. Let's check if the network is active. + // However, if we checked after the last confirmed active time and it wasn't active, + // then the network is still not active (we would be told when it becomes active + // via onNetworkActive()). + if (cachedNetworkMetadata.defaultNetworkActivationLastCheckTimeElapsed + > cachedNetworkMetadata.defaultNetworkActivationLastConfirmedTimeElapsed) { + return inactiveForTooLong; + } + // We need to explicitly check because there's no callback telling us when the network + // leaves the high power state. + cachedNetworkMetadata.defaultNetworkActivationLastCheckTimeElapsed = nowElapsed; + final boolean isActive = mConnManager.isDefaultNetworkActive(); + if (isActive) { + cachedNetworkMetadata.defaultNetworkActivationLastConfirmedTimeElapsed = nowElapsed; + return true; + } + return inactiveForTooLong; + } + + // We checked the state recently enough, but the network wasn't active. Assume it still + // isn't active. + return false; + } + + /** * We know the network has just come up. We want to run any jobs that are ready. */ @Override public void onNetworkActive() { synchronized (mLock) { - for (int i = mTrackedJobs.size()-1; i >= 0; i--) { - final ArraySet<JobStatus> jobs = mTrackedJobs.valueAt(i); - for (int j = jobs.size() - 1; j >= 0; j--) { - final JobStatus js = jobs.valueAt(j); - if (js.isReady()) { - if (DEBUG) { - Slog.d(TAG, "Running " + js + " due to network activity."); - } - mStateChangedListener.onRunJobNow(js); - } - } + if (mSystemDefaultNetwork == null) { + Slog.wtf(TAG, "System default network is unknown but active"); + return; + } + + CachedNetworkMetadata cachedNetworkMetadata = + mAvailableNetworks.get(mSystemDefaultNetwork); + if (cachedNetworkMetadata == null) { + Slog.wtf(TAG, "System default network capabilities are unknown but active"); + return; } + + // This method gets called on the system's main thread (not the + // AppSchedulingModuleThread), so shift the processing work to a handler to avoid + // blocking important operations on the main thread. + cachedNetworkMetadata.defaultNetworkActivationLastConfirmedTimeElapsed = + cachedNetworkMetadata.defaultNetworkActivationLastCheckTimeElapsed = + sElapsedRealtimeClock.millis(); + mHandler.sendEmptyMessage(MSG_PROCESS_ACTIVE_NETWORK); } } + /** NetworkCallback to track all network changes. */ private final NetworkCallback mNetworkCallback = new NetworkCallback() { @Override public void onAvailable(Network network) { @@ -1565,6 +1693,7 @@ public final class ConnectivityController extends RestrictingController implemen CachedNetworkMetadata cnm = mAvailableNetworks.get(network); if (cnm == null) { cnm = new CachedNetworkMetadata(); + cnm.capabilitiesFirstAcquiredTimeElapsed = sElapsedRealtimeClock.millis(); mAvailableNetworks.put(network, cnm); } else { final NetworkCapabilities oldCaps = cnm.networkCapabilities; @@ -1700,6 +1829,29 @@ public final class ConnectivityController extends RestrictingController implemen } }; + /** NetworkCallback to track only changes to the default network. */ + private final NetworkCallback mDefaultNetworkCallback = new NetworkCallback() { + @Override + public void onAvailable(Network network) { + if (DEBUG) Slog.v(TAG, "systemDefault-onAvailable: " + network); + synchronized (mLock) { + mSystemDefaultNetwork = network; + } + } + + @Override + public void onLost(Network network) { + if (DEBUG) { + Slog.v(TAG, "systemDefault-onLost: " + network); + } + synchronized (mLock) { + if (network.equals(mSystemDefaultNetwork)) { + mSystemDefaultNetwork = null; + } + } + } + }; + private final INetworkPolicyListener mNetPolicyListener = new NetworkPolicyManager.Listener() { @Override public void onRestrictBackgroundChanged(boolean restrictBackground) { @@ -1762,6 +1914,66 @@ public final class ConnectivityController extends RestrictingController implemen } } break; + + case MSG_PROCESS_ACTIVE_NETWORK: + removeMessages(MSG_PROCESS_ACTIVE_NETWORK); + synchronized (mLock) { + if (mSystemDefaultNetwork == null) { + break; + } + if (!Flags.batchConnectivityJobsPerNetwork()) { + break; + } + if (!isNetworkInStateForJobRunLocked(mSystemDefaultNetwork)) { + break; + } + + final ArrayMap<Network, Boolean> includeInProcessing = new ArrayMap<>(); + // Try to get the jobs to piggyback on the active network. + for (int u = mTrackedJobs.size() - 1; u >= 0; --u) { + final ArraySet<JobStatus> jobs = mTrackedJobs.valueAt(u); + for (int j = jobs.size() - 1; j >= 0; --j) { + final JobStatus js = jobs.valueAt(j); + if (!mSystemDefaultNetwork.equals(js.network)) { + final NetworkCapabilities capabilities = + getNetworkCapabilities(js.network); + if (capabilities == null + || !capabilities.hasTransport( + NetworkCapabilities.TRANSPORT_VPN)) { + includeInProcessing.put(js.network, Boolean.FALSE); + continue; + } + if (includeInProcessing.containsKey(js.network)) { + if (!includeInProcessing.get(js.network)) { + continue; + } + } else { + // VPNs most likely use the system default network as + // their underlying network. If so, process the job. + final List<Network> underlyingNetworks = + capabilities.getUnderlyingNetworks(); + final boolean isSystemDefaultInUnderlying = + underlyingNetworks != null + && underlyingNetworks.contains( + mSystemDefaultNetwork); + includeInProcessing.put(js.network, + isSystemDefaultInUnderlying); + if (!isSystemDefaultInUnderlying) { + continue; + } + } + } + if (js.isReady()) { + if (DEBUG) { + Slog.d(TAG, "Potentially running " + js + + " due to network activity"); + } + mStateChangedListener.onRunJobNow(js); + } + } + } + } + break; } } } @@ -1782,8 +1994,15 @@ public final class ConnectivityController extends RestrictingController implemen @VisibleForTesting static final String KEY_AVOID_UNDEFINED_TRANSPORT_AFFINITY = CC_CONFIG_PREFIX + "avoid_undefined_transport_affinity"; + private static final String KEY_NETWORK_ACTIVATION_EXPIRATION_MS = + CC_CONFIG_PREFIX + "network_activation_expiration_ms"; + private static final String KEY_NETWORK_ACTIVATION_MAX_WAIT_TIME_MS = + CC_CONFIG_PREFIX + "network_activation_max_wait_time_ms"; private static final boolean DEFAULT_AVOID_UNDEFINED_TRANSPORT_AFFINITY = false; + private static final long DEFAULT_NETWORK_ACTIVATION_EXPIRATION_MS = 10000L; + private static final long DEFAULT_NETWORK_ACTIVATION_MAX_WAIT_TIME_MS = + 31 * MINUTE_IN_MILLIS; /** * If true, will avoid network transports that don't have an explicitly defined affinity. @@ -1791,6 +2010,19 @@ public final class ConnectivityController extends RestrictingController implemen public boolean AVOID_UNDEFINED_TRANSPORT_AFFINITY = DEFAULT_AVOID_UNDEFINED_TRANSPORT_AFFINITY; + /** + * Amount of time that needs to pass before needing to determine if the network is still + * active. + */ + public long NETWORK_ACTIVATION_EXPIRATION_MS = DEFAULT_NETWORK_ACTIVATION_EXPIRATION_MS; + + /** + * Max time to wait since the network was last activated before deciding to allow jobs to + * run even if the network isn't active + */ + public long NETWORK_ACTIVATION_MAX_WAIT_TIME_MS = + DEFAULT_NETWORK_ACTIVATION_MAX_WAIT_TIME_MS; + @GuardedBy("mLock") public void processConstantLocked(@NonNull DeviceConfig.Properties properties, @NonNull String key) { @@ -1803,6 +2035,22 @@ public final class ConnectivityController extends RestrictingController implemen mShouldReprocessNetworkCapabilities = true; } break; + case KEY_NETWORK_ACTIVATION_EXPIRATION_MS: + final long gracePeriodMs = properties.getLong(key, + DEFAULT_NETWORK_ACTIVATION_EXPIRATION_MS); + if (NETWORK_ACTIVATION_EXPIRATION_MS != gracePeriodMs) { + NETWORK_ACTIVATION_EXPIRATION_MS = gracePeriodMs; + // This doesn't need to trigger network capability reprocessing. + } + break; + case KEY_NETWORK_ACTIVATION_MAX_WAIT_TIME_MS: + final long maxWaitMs = properties.getLong(key, + DEFAULT_NETWORK_ACTIVATION_MAX_WAIT_TIME_MS); + if (NETWORK_ACTIVATION_MAX_WAIT_TIME_MS != maxWaitMs) { + NETWORK_ACTIVATION_MAX_WAIT_TIME_MS = maxWaitMs; + mShouldReprocessNetworkCapabilities = true; + } + break; } } @@ -1814,6 +2062,10 @@ public final class ConnectivityController extends RestrictingController implemen pw.print(KEY_AVOID_UNDEFINED_TRANSPORT_AFFINITY, AVOID_UNDEFINED_TRANSPORT_AFFINITY).println(); + pw.print(KEY_NETWORK_ACTIVATION_EXPIRATION_MS, + NETWORK_ACTIVATION_EXPIRATION_MS).println(); + pw.print(KEY_NETWORK_ACTIVATION_MAX_WAIT_TIME_MS, + NETWORK_ACTIVATION_MAX_WAIT_TIME_MS).println(); pw.decreaseIndent(); } @@ -1925,11 +2177,24 @@ public final class ConnectivityController extends RestrictingController implemen private static class CachedNetworkMetadata { public NetworkCapabilities networkCapabilities; public boolean satisfiesTransportAffinities; + /** + * Track the first time ConnectivityController was informed about the capabilities of the + * network after it became available. + */ + public long capabilitiesFirstAcquiredTimeElapsed; + public long defaultNetworkActivationLastCheckTimeElapsed; + public long defaultNetworkActivationLastConfirmedTimeElapsed; public String toString() { return "CNM{" + networkCapabilities.toString() + ", satisfiesTransportAffinities=" + satisfiesTransportAffinities + + ", capabilitiesFirstAcquiredTimeElapsed=" + + capabilitiesFirstAcquiredTimeElapsed + + ", defaultNetworkActivationLastCheckTimeElapsed=" + + defaultNetworkActivationLastCheckTimeElapsed + + ", defaultNetworkActivationLastConfirmedTimeElapsed=" + + defaultNetworkActivationLastConfirmedTimeElapsed + "}"; } } @@ -2017,7 +2282,7 @@ public final class ConnectivityController extends RestrictingController implemen pw.println("Aconfig flags:"); pw.increaseIndent(); pw.print(FLAG_RELAX_PREFETCH_CONNECTIVITY_CONSTRAINT_ONLY_ON_CHARGER, - relaxPrefetchConnectivityConstraintOnlyOnCharger()); + Flags.relaxPrefetchConnectivityConstraintOnlyOnCharger()); pw.println(); pw.decreaseIndent(); pw.println(); diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java index 0e67b9ac944f..e96d07f44b34 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java @@ -16,6 +16,11 @@ package com.android.server.job.controllers; +import static android.app.job.JobInfo.PRIORITY_DEFAULT; +import static android.app.job.JobInfo.PRIORITY_HIGH; +import static android.app.job.JobInfo.PRIORITY_LOW; +import static android.app.job.JobInfo.PRIORITY_MAX; +import static android.app.job.JobInfo.PRIORITY_MIN; import static android.text.format.DateUtils.DAY_IN_MILLIS; import static android.text.format.DateUtils.HOUR_IN_MILLIS; import static android.text.format.DateUtils.MINUTE_IN_MILLIS; @@ -30,24 +35,34 @@ import android.annotation.ElapsedRealtimeLong; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.job.JobInfo; +import android.content.BroadcastReceiver; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.content.pm.PackageManager; import android.os.Handler; import android.os.Looper; import android.os.Message; +import android.os.PowerManager; import android.os.UserHandle; import android.provider.DeviceConfig; import android.util.ArraySet; import android.util.IndentingPrintWriter; +import android.util.KeyValueListParser; import android.util.Log; import android.util.Slog; +import android.util.SparseArray; import android.util.SparseArrayMap; +import android.util.SparseIntArray; import android.util.SparseLongArray; +import android.util.SparseSetArray; import android.util.TimeUtils; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.server.AppSchedulingModuleThread; +import com.android.server.DeviceIdleInternal; +import com.android.server.LocalServices; import com.android.server.job.JobSchedulerService; import com.android.server.utils.AlarmQueue; @@ -85,6 +100,23 @@ public final class FlexibilityController extends StateController { */ private long mFallbackFlexibilityDeadlineMs = FcConfig.DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS; + /** + * The default deadline that all flexible constraints should be dropped by if a job lacks + * a deadline, keyed by job priority. + */ + private SparseLongArray mFallbackFlexibilityDeadlines = + FcConfig.DEFAULT_FALLBACK_FLEXIBILITY_DEADLINES; + /** + * The scores to use for each job, keyed by job priority. + */ + private SparseIntArray mFallbackFlexibilityDeadlineScores = + FcConfig.DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_SCORES; + /** + * The amount of time to add (scaled by job run score) to the fallback flexibility deadline, + * keyed by job priority. + */ + private SparseLongArray mFallbackFlexibilityAdditionalScoreTimeFactors = + FcConfig.DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS; private long mRescheduledJobDeadline = FcConfig.DEFAULT_RESCHEDULED_JOB_DEADLINE_MS; private long mMaxRescheduledDeadline = FcConfig.DEFAULT_MAX_RESCHEDULED_DEADLINE_MS; @@ -111,10 +143,10 @@ public final class FlexibilityController extends StateController { /** * The percent of a job's lifecycle to drop number of required constraints. - * mPercentToDropConstraints[i] denotes that at x% of a Jobs lifecycle, - * the controller should have i+1 constraints dropped. + * mPercentsToDropConstraints[i] denotes that at x% of a Jobs lifecycle, + * the controller should have i+1 constraints dropped. Keyed by job priority. */ - private int[] mPercentToDropConstraints; + private SparseArray<int[]> mPercentsToDropConstraints; /** * Keeps track of what flexible constraints are satisfied at the moment. @@ -138,6 +170,7 @@ public final class FlexibilityController extends StateController { private final FcHandler mHandler; @VisibleForTesting final PrefetchController mPrefetchController; + private final SpecialAppTracker mSpecialAppTracker; /** * Stores the beginning of prefetch jobs lifecycle per app as a maximum of @@ -180,8 +213,115 @@ public final class FlexibilityController extends StateController { } }; - private static final int MSG_UPDATE_JOBS = 0; - private static final int MSG_UPDATE_JOB = 1; + /** Helper object to track job run score for each app. */ + private static class JobScoreTracker { + private static class JobScoreBucket { + @ElapsedRealtimeLong + public long startTimeElapsed; + public int score; + + private void reset() { + startTimeElapsed = 0; + score = 0; + } + } + + private static final int NUM_SCORE_BUCKETS = 24; + private static final long MAX_TIME_WINDOW_MS = 24 * HOUR_IN_MILLIS; + private final JobScoreBucket[] mScoreBuckets = new JobScoreBucket[NUM_SCORE_BUCKETS]; + private int mScoreBucketIndex = 0; + private long mCachedScoreExpirationTimeElapsed; + private int mCachedScore; + + public void addScore(int add, long nowElapsed) { + JobScoreBucket bucket = mScoreBuckets[mScoreBucketIndex]; + if (bucket == null) { + bucket = new JobScoreBucket(); + bucket.startTimeElapsed = nowElapsed; + mScoreBuckets[mScoreBucketIndex] = bucket; + // Brand new bucket, there's nothing to remove from the score, + // so just update the expiration time if needed. + mCachedScoreExpirationTimeElapsed = Math.min(mCachedScoreExpirationTimeElapsed, + nowElapsed + MAX_TIME_WINDOW_MS); + } else if (bucket.startTimeElapsed < nowElapsed - MAX_TIME_WINDOW_MS) { + // The bucket is too old. + bucket.reset(); + bucket.startTimeElapsed = nowElapsed; + // Force a recalculation of the cached score instead of just updating the cached + // value and time in case there are multiple stale buckets. + mCachedScoreExpirationTimeElapsed = nowElapsed; + } else if (bucket.startTimeElapsed + < nowElapsed - MAX_TIME_WINDOW_MS / NUM_SCORE_BUCKETS) { + // The current bucket's duration has completed. Move on to the next bucket. + mScoreBucketIndex = (mScoreBucketIndex + 1) % NUM_SCORE_BUCKETS; + addScore(add, nowElapsed); + return; + } + + bucket.score += add; + mCachedScore += add; + } + + public int getScore(long nowElapsed) { + if (nowElapsed < mCachedScoreExpirationTimeElapsed) { + return mCachedScore; + } + int score = 0; + final long earliestElapsed = nowElapsed - MAX_TIME_WINDOW_MS; + long earliestValidBucketTimeElapsed = Long.MAX_VALUE; + for (JobScoreBucket bucket : mScoreBuckets) { + if (bucket != null && bucket.startTimeElapsed >= earliestElapsed) { + score += bucket.score; + if (earliestValidBucketTimeElapsed > bucket.startTimeElapsed) { + earliestValidBucketTimeElapsed = bucket.startTimeElapsed; + } + } + } + mCachedScore = score; + mCachedScoreExpirationTimeElapsed = earliestValidBucketTimeElapsed + MAX_TIME_WINDOW_MS; + return score; + } + + public void dump(@NonNull IndentingPrintWriter pw, long nowElapsed) { + pw.print("{"); + + boolean printed = false; + for (int x = 0; x < mScoreBuckets.length; ++x) { + final int idx = (mScoreBucketIndex + 1 + x) % mScoreBuckets.length; + final JobScoreBucket jsb = mScoreBuckets[idx]; + if (jsb == null || jsb.startTimeElapsed == 0) { + continue; + } + if (printed) { + pw.print(", "); + } + TimeUtils.formatDuration(jsb.startTimeElapsed, nowElapsed, pw); + pw.print("="); + pw.print(jsb.score); + printed = true; + } + + pw.print("}"); + } + } + + /** + * Set of {@link JobScoreTracker JobScoreTrackers} for each app. + * Keyed by source UID -> source package. + **/ + private final SparseArrayMap<String, JobScoreTracker> mJobScoreTrackers = + new SparseArrayMap<>(); + + private static final int MSG_CHECK_ALL_JOBS = 0; + /** Check the jobs in {@link #mJobsToCheck} */ + private static final int MSG_CHECK_JOBS = 1; + /** Check the jobs of packages in {@link #mPackagesToCheck} */ + private static final int MSG_CHECK_PACKAGES = 2; + + @GuardedBy("mLock") + private final ArraySet<JobStatus> mJobsToCheck = new ArraySet<>(); + @GuardedBy("mLock") + private final ArraySet<String> mPackagesToCheck = new ArraySet<>(); public FlexibilityController( JobSchedulerService service, PrefetchController prefetchController) { @@ -201,9 +341,19 @@ public final class FlexibilityController extends StateController { mFcConfig = new FcConfig(); mFlexibilityAlarmQueue = new FlexibilityAlarmQueue( mContext, AppSchedulingModuleThread.get().getLooper()); - mPercentToDropConstraints = - mFcConfig.DEFAULT_PERCENT_TO_DROP_FLEXIBLE_CONSTRAINTS; + mPercentsToDropConstraints = + FcConfig.DEFAULT_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS; mPrefetchController = prefetchController; + mSpecialAppTracker = new SpecialAppTracker(); + + if (mFlexibilityEnabled) { + mSpecialAppTracker.startTracking(); + } + } + + @Override + public void onSystemServicesReady() { + mSpecialAppTracker.onSystemServicesReady(); } @Override @@ -235,12 +385,55 @@ public final class FlexibilityController extends StateController { } @Override + public void prepareForExecutionLocked(JobStatus jobStatus) { + if (jobStatus.lastEvaluatedBias == JobInfo.BIAS_TOP_APP) { + // Don't include jobs for the TOP app in the score calculation. + return; + } + // Use the job's requested priority to determine its score since that is what the developer + // selected and it will be stable across job runs. + final int priority = jobStatus.getJob().getPriority(); + final int score = mFallbackFlexibilityDeadlineScores.get(priority, + FcConfig.DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_SCORES + .get(priority, priority / 100)); + JobScoreTracker jobScoreTracker = + mJobScoreTrackers.get(jobStatus.getSourceUid(), jobStatus.getSourcePackageName()); + if (jobScoreTracker == null) { + jobScoreTracker = new JobScoreTracker(); + mJobScoreTrackers.add(jobStatus.getSourceUid(), jobStatus.getSourcePackageName(), + jobScoreTracker); + } + jobScoreTracker.addScore(score, sElapsedRealtimeClock.millis()); + } + + @Override + public void unprepareFromExecutionLocked(JobStatus jobStatus) { + if (jobStatus.lastEvaluatedBias == JobInfo.BIAS_TOP_APP) { + // Jobs for the TOP app are excluded from the score calculation. + return; + } + // The job didn't actually start. Undo the score increase. + JobScoreTracker jobScoreTracker = + mJobScoreTrackers.get(jobStatus.getSourceUid(), jobStatus.getSourcePackageName()); + if (jobScoreTracker == null) { + Slog.e(TAG, "Unprepared a job that didn't result in a score change"); + return; + } + final int priority = jobStatus.getJob().getPriority(); + final int score = mFallbackFlexibilityDeadlineScores.get(priority, + FcConfig.DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_SCORES + .get(priority, priority / 100)); + jobScoreTracker.addScore(-score, sElapsedRealtimeClock.millis()); + } + + @Override @GuardedBy("mLock") public void maybeStopTrackingJobLocked(JobStatus js, JobStatus incomingJob) { if (js.clearTrackingController(JobStatus.TRACKING_FLEXIBILITY)) { mFlexibilityAlarmQueue.removeAlarmForKey(js); mFlexibilityTracker.remove(js); } + mJobsToCheck.remove(js); } @Override @@ -248,12 +441,35 @@ public final class FlexibilityController extends StateController { public void onAppRemovedLocked(String packageName, int uid) { final int userId = UserHandle.getUserId(uid); mPrefetchLifeCycleStart.delete(userId, packageName); + mJobScoreTrackers.delete(uid, packageName); + mSpecialAppTracker.onAppRemoved(userId, packageName); + for (int i = mJobsToCheck.size() - 1; i >= 0; --i) { + final JobStatus js = mJobsToCheck.valueAt(i); + if ((js.getSourceUid() == uid && js.getSourcePackageName().equals(packageName)) + || (js.getUid() == uid && js.getCallingPackageName().equals(packageName))) { + mJobsToCheck.removeAt(i); + } + } } @Override @GuardedBy("mLock") public void onUserRemovedLocked(int userId) { mPrefetchLifeCycleStart.delete(userId); + mSpecialAppTracker.onUserRemoved(userId); + for (int u = mJobScoreTrackers.numMaps() - 1; u >= 0; --u) { + final int uid = mJobScoreTrackers.keyAt(u); + if (UserHandle.getUserId(uid) == userId) { + mJobScoreTrackers.deleteAt(u); + } + } + for (int i = mJobsToCheck.size() - 1; i >= 0; --i) { + final JobStatus js = mJobsToCheck.valueAt(i); + if (UserHandle.getUserId(js.getSourceUid()) == userId + || UserHandle.getUserId(js.getUid()) == userId) { + mJobsToCheck.removeAt(i); + } + } } boolean isEnabled() { @@ -266,7 +482,15 @@ public final class FlexibilityController extends StateController { @GuardedBy("mLock") boolean isFlexibilitySatisfiedLocked(JobStatus js) { return !mFlexibilityEnabled + // Exclude all jobs of the TOP app || mService.getUidBias(js.getSourceUid()) == JobInfo.BIAS_TOP_APP + // Only exclude DEFAULT+ priority jobs for BFGS+ apps + || (mService.getUidBias(js.getSourceUid()) >= JobInfo.BIAS_BOUND_FOREGROUND_SERVICE + && js.getEffectivePriority() >= PRIORITY_DEFAULT) + // For special/privileged apps, automatically exclude DEFAULT+ priority jobs. + || (js.getEffectivePriority() >= PRIORITY_DEFAULT + && mSpecialAppTracker.isSpecialApp( + js.getSourceUserId(), js.getSourcePackageName())) || hasEnoughSatisfiedConstraintsLocked(js) || mService.isCurrentlyRunningLocked(js); } @@ -371,7 +595,7 @@ public final class FlexibilityController extends StateController { // Push the job update to the handler to avoid blocking other controllers and // potentially batch back-to-back controller state updates together. - mHandler.obtainMessage(MSG_UPDATE_JOBS).sendToTarget(); + mHandler.obtainMessage(MSG_CHECK_ALL_JOBS).sendToTarget(); } } } @@ -417,7 +641,14 @@ public final class FlexibilityController extends StateController { @VisibleForTesting @GuardedBy("mLock") - long getLifeCycleEndElapsedLocked(JobStatus js, long earliest) { + int getScoreLocked(int uid, @NonNull String pkgName, long nowElapsed) { + final JobScoreTracker scoreTracker = mJobScoreTrackers.get(uid, pkgName); + return scoreTracker == null ? 0 : scoreTracker.getScore(nowElapsed); + } + + @VisibleForTesting + @GuardedBy("mLock") + long getLifeCycleEndElapsedLocked(JobStatus js, long nowElapsed, long earliest) { if (js.getJob().isPrefetch()) { final long estimatedLaunchTime = mPrefetchController.getNextEstimatedLaunchTimeLocked(js); @@ -441,15 +672,31 @@ public final class FlexibilityController extends StateController { (long) Math.scalb(mRescheduledJobDeadline, js.getNumPreviousAttempts() - 2), mMaxRescheduledDeadline); } - return js.getLatestRunTimeElapsed() == JobStatus.NO_LATEST_RUNTIME - ? earliest + mFallbackFlexibilityDeadlineMs : js.getLatestRunTimeElapsed(); + + // Intentionally use the effective priority here. If a job's priority was effectively + // lowered, it will be less likely to run quickly given other policies in JobScheduler. + // Thus, there's no need to further delay the job based on flex policy. + final int jobPriority = js.getEffectivePriority(); + final int jobScore = + getScoreLocked(js.getSourceUid(), js.getSourcePackageName(), nowElapsed); + // Set an upper limit on the fallback deadline so that the delay doesn't become extreme. + final long fallbackDurationMs = Math.min(3 * mFallbackFlexibilityDeadlineMs, + mFallbackFlexibilityDeadlines.get(jobPriority, mFallbackFlexibilityDeadlineMs) + + mFallbackFlexibilityAdditionalScoreTimeFactors + .get(jobPriority, MINUTE_IN_MILLIS) * jobScore); + final long fallbackDeadlineMs = earliest + fallbackDurationMs; + + if (js.getLatestRunTimeElapsed() == JobStatus.NO_LATEST_RUNTIME) { + return fallbackDeadlineMs; + } + return Math.max(fallbackDeadlineMs, js.getLatestRunTimeElapsed()); } @VisibleForTesting @GuardedBy("mLock") int getCurPercentOfLifecycleLocked(JobStatus js, long nowElapsed) { final long earliest = getLifeCycleBeginningElapsedLocked(js); - final long latest = getLifeCycleEndElapsedLocked(js, earliest); + final long latest = getLifeCycleEndElapsedLocked(js, nowElapsed, earliest); if (latest == NO_LIFECYCLE_END || earliest >= nowElapsed) { return 0; } @@ -465,7 +712,8 @@ public final class FlexibilityController extends StateController { @GuardedBy("mLock") long getNextConstraintDropTimeElapsedLocked(JobStatus js) { final long earliest = getLifeCycleBeginningElapsedLocked(js); - final long latest = getLifeCycleEndElapsedLocked(js, earliest); + final long latest = + getLifeCycleEndElapsedLocked(js, sElapsedRealtimeClock.millis(), earliest); return getNextConstraintDropTimeElapsedLocked(js, earliest, latest); } @@ -473,19 +721,33 @@ public final class FlexibilityController extends StateController { @ElapsedRealtimeLong @GuardedBy("mLock") long getNextConstraintDropTimeElapsedLocked(JobStatus js, long earliest, long latest) { + final int[] percentsToDropConstraints = + getPercentsToDropConstraints(js.getEffectivePriority()); if (latest == NO_LIFECYCLE_END - || js.getNumDroppedFlexibleConstraints() == mPercentToDropConstraints.length) { + || js.getNumDroppedFlexibleConstraints() == percentsToDropConstraints.length) { return NO_LIFECYCLE_END; } - final int percent = mPercentToDropConstraints[js.getNumDroppedFlexibleConstraints()]; + final int percent = percentsToDropConstraints[js.getNumDroppedFlexibleConstraints()]; final long percentInTime = ((latest - earliest) * percent) / 100; return earliest + percentInTime; } + @NonNull + private int[] getPercentsToDropConstraints(int priority) { + int[] percentsToDropConstraints = mPercentsToDropConstraints.get(priority); + if (percentsToDropConstraints == null) { + Slog.wtf(TAG, "No %-to-drop for priority " + JobInfo.getPriorityString(priority)); + return new int[]{50, 60, 70, 80}; + } + return percentsToDropConstraints; + } + @Override @GuardedBy("mLock") public void onUidBiasChangedLocked(int uid, int prevBias, int newBias) { - if (prevBias != JobInfo.BIAS_TOP_APP && newBias != JobInfo.BIAS_TOP_APP) { + if (prevBias < JobInfo.BIAS_BOUND_FOREGROUND_SERVICE + && newBias < JobInfo.BIAS_BOUND_FOREGROUND_SERVICE) { + // All changes are below BFGS. There's no significant change to care about. return; } final long nowElapsed = sElapsedRealtimeClock.millis(); @@ -601,10 +863,12 @@ public final class FlexibilityController extends StateController { Integer.bitCount(getRelevantAppliedConstraintsLocked(js)); js.setNumAppliedFlexibleConstraints(numAppliedConstraints); + final int[] percentsToDropConstraints = + getPercentsToDropConstraints(js.getEffectivePriority()); final int curPercent = getCurPercentOfLifecycleLocked(js, nowElapsed); int toDrop = 0; for (int i = 0; i < numAppliedConstraints; i++) { - if (curPercent >= mPercentToDropConstraints[i]) { + if (curPercent >= percentsToDropConstraints[i]) { toDrop++; } } @@ -625,8 +889,10 @@ public final class FlexibilityController extends StateController { final int curPercent = getCurPercentOfLifecycleLocked(js, nowElapsed); int toDrop = 0; final int jsMaxFlexibleConstraints = js.getNumAppliedFlexibleConstraints(); + final int[] percentsToDropConstraints = + getPercentsToDropConstraints(js.getEffectivePriority()); for (int i = 0; i < jsMaxFlexibleConstraints; i++) { - if (curPercent >= mPercentToDropConstraints[i]) { + if (curPercent >= percentsToDropConstraints[i]) { toDrop++; } } @@ -653,7 +919,7 @@ public final class FlexibilityController extends StateController { return mTrackedJobs.size(); } - public void dump(IndentingPrintWriter pw, Predicate<JobStatus> predicate) { + public void dump(IndentingPrintWriter pw, Predicate<JobStatus> predicate, long nowElapsed) { for (int i = 0; i < mTrackedJobs.size(); i++) { ArraySet<JobStatus> jobs = mTrackedJobs.get(i); for (int j = 0; j < jobs.size(); j++) { @@ -664,8 +930,18 @@ public final class FlexibilityController extends StateController { js.printUniqueId(pw); pw.print(" from "); UserHandle.formatUid(pw, js.getSourceUid()); - pw.print(" Num Required Constraints: "); + pw.print("-> Num Required Constraints: "); pw.print(js.getNumRequiredFlexibleConstraints()); + + pw.print(", lifecycle=["); + final long earliest = getLifeCycleBeginningElapsedLocked(js); + pw.print(earliest); + pw.print(", ("); + pw.print(getCurPercentOfLifecycleLocked(js, nowElapsed)); + pw.print("%), "); + pw.print(getLifeCycleEndElapsedLocked(js, nowElapsed, earliest)); + pw.print("]"); + pw.println(); } } @@ -688,13 +964,28 @@ public final class FlexibilityController extends StateController { public void scheduleDropNumConstraintsAlarm(JobStatus js, long nowElapsed) { synchronized (mLock) { final long earliest = getLifeCycleBeginningElapsedLocked(js); - final long latest = getLifeCycleEndElapsedLocked(js, earliest); + final long latest = getLifeCycleEndElapsedLocked(js, nowElapsed, earliest); + if (latest <= earliest) { + // Something has gone horribly wrong. This has only occurred on incorrectly + // configured tests, but add a check here for safety. + Slog.wtf(TAG, "Got invalid latest when scheduling alarm." + + " prefetch=" + js.getJob().isPrefetch() + + " periodic=" + js.getJob().isPeriodic()); + // Since things have gone wrong, the safest and most reliable thing to do is + // stop applying flex policy to the job. + mFlexibilityTracker.setNumDroppedFlexibleConstraints(js, + js.getNumAppliedFlexibleConstraints()); + mJobsToCheck.add(js); + mHandler.sendEmptyMessage(MSG_CHECK_JOBS); + return; + } + final long nextTimeElapsed = getNextConstraintDropTimeElapsedLocked(js, earliest, latest); if (DEBUG) { Slog.d(TAG, "scheduleDropNumConstraintsAlarm: " - + js.getSourcePackageName() + " " + js.getSourceUserId() + + js.toShortString() + " numApplied: " + js.getNumAppliedFlexibleConstraints() + " numRequired: " + js.getNumRequiredFlexibleConstraints() + " numSatisfied: " + Integer.bitCount( @@ -710,7 +1001,8 @@ public final class FlexibilityController extends StateController { } mFlexibilityTracker.setNumDroppedFlexibleConstraints(js, js.getNumAppliedFlexibleConstraints()); - mHandler.obtainMessage(MSG_UPDATE_JOB, js).sendToTarget(); + mJobsToCheck.add(js); + mHandler.sendEmptyMessage(MSG_CHECK_JOBS); return; } if (nextTimeElapsed == NO_LIFECYCLE_END) { @@ -761,10 +1053,12 @@ public final class FlexibilityController extends StateController { @Override public void handleMessage(Message msg) { switch (msg.what) { - case MSG_UPDATE_JOBS: - removeMessages(MSG_UPDATE_JOBS); + case MSG_CHECK_ALL_JOBS: + removeMessages(MSG_CHECK_ALL_JOBS); synchronized (mLock) { + mJobsToCheck.clear(); + mPackagesToCheck.clear(); final long nowElapsed = sElapsedRealtimeClock.millis(); final ArraySet<JobStatus> changedJobs = new ArraySet<>(); @@ -790,19 +1084,50 @@ public final class FlexibilityController extends StateController { } break; - case MSG_UPDATE_JOB: + case MSG_CHECK_JOBS: synchronized (mLock) { - final JobStatus js = (JobStatus) msg.obj; - if (DEBUG) { - Slog.d("blah", "Checking on " + js.toShortString()); + final long nowElapsed = sElapsedRealtimeClock.millis(); + ArraySet<JobStatus> changedJobs = new ArraySet<>(); + + for (int i = mJobsToCheck.size() - 1; i >= 0; --i) { + final JobStatus js = mJobsToCheck.valueAt(i); + if (DEBUG) { + Slog.d(TAG, "Checking on " + js.toShortString()); + } + if (js.setFlexibilityConstraintSatisfied( + nowElapsed, isFlexibilitySatisfiedLocked(js))) { + changedJobs.add(js); + } + } + + mJobsToCheck.clear(); + if (changedJobs.size() > 0) { + mStateChangedListener.onControllerStateChanged(changedJobs); } + } + break; + + case MSG_CHECK_PACKAGES: + synchronized (mLock) { final long nowElapsed = sElapsedRealtimeClock.millis(); - if (js.setFlexibilityConstraintSatisfied( - nowElapsed, isFlexibilitySatisfiedLocked(js))) { - // TODO(141645789): add method that will take a single job - ArraySet<JobStatus> changedJob = new ArraySet<>(); - changedJob.add(js); - mStateChangedListener.onControllerStateChanged(changedJob); + final ArraySet<JobStatus> changedJobs = new ArraySet<>(); + + mService.getJobStore().forEachJob( + (js) -> mPackagesToCheck.contains(js.getSourcePackageName()) + || mPackagesToCheck.contains(js.getCallingPackageName()), + (js) -> { + if (DEBUG) { + Slog.d(TAG, "Checking on " + js.toShortString()); + } + if (js.setFlexibilityConstraintSatisfied( + nowElapsed, isFlexibilitySatisfiedLocked(js))) { + changedJobs.add(js); + } + }); + + mPackagesToCheck.clear(); + if (changedJobs.size() > 0) { + mStateChangedListener.onControllerStateChanged(changedJobs); } } break; @@ -822,10 +1147,16 @@ public final class FlexibilityController extends StateController { FC_CONFIG_PREFIX + "flexibility_deadline_proximity_limit_ms"; static final String KEY_FALLBACK_FLEXIBILITY_DEADLINE = FC_CONFIG_PREFIX + "fallback_flexibility_deadline_ms"; + static final String KEY_FALLBACK_FLEXIBILITY_DEADLINES = + FC_CONFIG_PREFIX + "fallback_flexibility_deadlines"; + static final String KEY_FALLBACK_FLEXIBILITY_DEADLINE_SCORES = + FC_CONFIG_PREFIX + "fallback_flexibility_deadline_scores"; + static final String KEY_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS = + FC_CONFIG_PREFIX + "fallback_flexibility_deadline_additional_score_time_factors"; static final String KEY_MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS = FC_CONFIG_PREFIX + "min_time_between_flexibility_alarms_ms"; - static final String KEY_PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS = - FC_CONFIG_PREFIX + "percents_to_drop_num_flexible_constraints"; + static final String KEY_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS = + FC_CONFIG_PREFIX + "percents_to_drop_flexible_constraints"; static final String KEY_MAX_RESCHEDULED_DEADLINE_MS = FC_CONFIG_PREFIX + "max_rescheduled_deadline_ms"; static final String KEY_RESCHEDULED_JOB_DEADLINE_MS = @@ -837,12 +1168,53 @@ public final class FlexibilityController extends StateController { @VisibleForTesting static final long DEFAULT_DEADLINE_PROXIMITY_LIMIT_MS = 15 * MINUTE_IN_MILLIS; @VisibleForTesting - static final long DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS = 72 * HOUR_IN_MILLIS; - private static final long DEFAULT_MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS = MINUTE_IN_MILLIS; + static final long DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS = 24 * HOUR_IN_MILLIS; + static final SparseLongArray DEFAULT_FALLBACK_FLEXIBILITY_DEADLINES = new SparseLongArray(); + static final SparseIntArray DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_SCORES = + new SparseIntArray(); + static final SparseLongArray + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS = + new SparseLongArray(); @VisibleForTesting - final int[] DEFAULT_PERCENT_TO_DROP_FLEXIBLE_CONSTRAINTS = {50, 60, 70, 80}; + static final SparseArray<int[]> DEFAULT_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS = + new SparseArray<>(); + + static { + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINES.put(PRIORITY_MAX, HOUR_IN_MILLIS); + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINES.put(PRIORITY_HIGH, 6 * HOUR_IN_MILLIS); + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINES.put(PRIORITY_DEFAULT, 12 * HOUR_IN_MILLIS); + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINES.put(PRIORITY_LOW, 24 * HOUR_IN_MILLIS); + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINES.put(PRIORITY_MIN, 48 * HOUR_IN_MILLIS); + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_SCORES.put(PRIORITY_MAX, 5); + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_SCORES.put(PRIORITY_HIGH, 4); + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_SCORES.put(PRIORITY_DEFAULT, 3); + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_SCORES.put(PRIORITY_LOW, 2); + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_SCORES.put(PRIORITY_MIN, 1); + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS + .put(PRIORITY_MAX, 0); + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS + .put(PRIORITY_HIGH, 3 * MINUTE_IN_MILLIS); + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS + .put(PRIORITY_DEFAULT, 2 * MINUTE_IN_MILLIS); + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS + .put(PRIORITY_LOW, 1 * MINUTE_IN_MILLIS); + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS + .put(PRIORITY_MIN, 1 * MINUTE_IN_MILLIS); + DEFAULT_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS + .put(PRIORITY_MAX, new int[]{1, 2, 3, 4}); + DEFAULT_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS + .put(PRIORITY_HIGH, new int[]{33, 50, 60, 75}); + DEFAULT_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS + .put(PRIORITY_DEFAULT, new int[]{50, 60, 70, 80}); + DEFAULT_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS + .put(PRIORITY_LOW, new int[]{50, 60, 70, 80}); + DEFAULT_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS + .put(PRIORITY_MIN, new int[]{55, 65, 75, 85}); + } + + private static final long DEFAULT_MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS = MINUTE_IN_MILLIS; private static final long DEFAULT_RESCHEDULED_JOB_DEADLINE_MS = HOUR_IN_MILLIS; - private static final long DEFAULT_MAX_RESCHEDULED_DEADLINE_MS = 5 * DAY_IN_MILLIS; + private static final long DEFAULT_MAX_RESCHEDULED_DEADLINE_MS = DAY_IN_MILLIS; @VisibleForTesting static final long DEFAULT_UNSEEN_CONSTRAINT_GRACE_PERIOD_MS = 3 * DAY_IN_MILLIS; @@ -854,9 +1226,11 @@ public final class FlexibilityController extends StateController { public long FALLBACK_FLEXIBILITY_DEADLINE_MS = DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS; public long MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS = DEFAULT_MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS; - /** The percentages of a jobs' lifecycle to drop the number of required constraints. */ - public int[] PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS = - DEFAULT_PERCENT_TO_DROP_FLEXIBLE_CONSTRAINTS; + /** + * The percentages of a jobs' lifecycle to drop the number of required constraints. + * Keyed by job priority. + */ + public SparseArray<int[]> PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS = new SparseArray<>(); /** Initial fallback flexible deadline for rescheduled jobs. */ public long RESCHEDULED_JOB_DEADLINE_MS = DEFAULT_RESCHEDULED_JOB_DEADLINE_MS; /** The max deadline for rescheduled jobs. */ @@ -866,10 +1240,56 @@ public final class FlexibilityController extends StateController { * it in order to run jobs. */ public long UNSEEN_CONSTRAINT_GRACE_PERIOD_MS = DEFAULT_UNSEEN_CONSTRAINT_GRACE_PERIOD_MS; + /** + * The base fallback deadlines to use if a job doesn't have its own deadline. Values are in + * milliseconds and keyed by job priority. + */ + public final SparseLongArray FALLBACK_FLEXIBILITY_DEADLINES = new SparseLongArray(); + /** + * The score to ascribe to each job, keyed by job priority. + */ + public final SparseIntArray FALLBACK_FLEXIBILITY_DEADLINE_SCORES = new SparseIntArray(); + /** + * How much additional time to increase the fallback deadline by based on the app's current + * job run score. Values are in + * milliseconds and keyed by job priority. + */ + public final SparseLongArray FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS = + new SparseLongArray(); + + FcConfig() { + // Copy the values from the DEFAULT_* data structures to avoid accidentally modifying + // the DEFAULT_* data structures in other parts of the code. + for (int i = 0; i < DEFAULT_FALLBACK_FLEXIBILITY_DEADLINES.size(); ++i) { + FALLBACK_FLEXIBILITY_DEADLINES.put( + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINES.keyAt(i), + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINES.valueAt(i)); + } + for (int i = 0; i < DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_SCORES.size(); ++i) { + FALLBACK_FLEXIBILITY_DEADLINE_SCORES.put( + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_SCORES.keyAt(i), + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_SCORES.valueAt(i)); + } + for (int i = 0; + i < DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS.size(); + ++i) { + FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS.put( + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS + .keyAt(i), + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS + .valueAt(i)); + } + for (int i = 0; i < DEFAULT_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS.size(); ++i) { + PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS.put( + DEFAULT_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS.keyAt(i), + DEFAULT_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS.valueAt(i)); + } + } @GuardedBy("mLock") public void processConstantLocked(@NonNull DeviceConfig.Properties properties, @NonNull String key) { + // TODO(257322915): add appropriate minimums and maximums to constants when parsing switch (key) { case KEY_APPLIED_CONSTRAINTS: APPLIED_CONSTRAINTS = @@ -882,10 +1302,12 @@ public final class FlexibilityController extends StateController { mFlexibilityEnabled = true; mPrefetchController .registerPrefetchChangedListener(mPrefetchChangedListener); + mSpecialAppTracker.startTracking(); } else { mFlexibilityEnabled = false; mPrefetchController .unRegisterPrefetchChangedListener(mPrefetchChangedListener); + mSpecialAppTracker.stopTracking(); } } break; @@ -918,6 +1340,33 @@ public final class FlexibilityController extends StateController { properties.getLong(key, DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS); if (mFallbackFlexibilityDeadlineMs != FALLBACK_FLEXIBILITY_DEADLINE_MS) { mFallbackFlexibilityDeadlineMs = FALLBACK_FLEXIBILITY_DEADLINE_MS; + } + break; + case KEY_FALLBACK_FLEXIBILITY_DEADLINES: + if (parsePriorityToLongKeyValueString( + properties.getString(key, null), + FALLBACK_FLEXIBILITY_DEADLINES, + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINES)) { + mFallbackFlexibilityDeadlines = FALLBACK_FLEXIBILITY_DEADLINES; + mShouldReevaluateConstraints = true; + } + break; + case KEY_FALLBACK_FLEXIBILITY_DEADLINE_SCORES: + if (parsePriorityToIntKeyValueString( + properties.getString(key, null), + FALLBACK_FLEXIBILITY_DEADLINE_SCORES, + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_SCORES)) { + mFallbackFlexibilityDeadlineScores = FALLBACK_FLEXIBILITY_DEADLINE_SCORES; + mShouldReevaluateConstraints = true; + } + break; + case KEY_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS: + if (parsePriorityToLongKeyValueString( + properties.getString(key, null), + FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS, + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS)) { + mFallbackFlexibilityAdditionalScoreTimeFactors = + FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS; mShouldReevaluateConstraints = true; } break; @@ -940,25 +1389,69 @@ public final class FlexibilityController extends StateController { mShouldReevaluateConstraints = true; } break; - case KEY_PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS: - String dropPercentString = properties.getString(key, ""); - PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS = - parsePercentToDropString(dropPercentString); - if (PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS != null - && !Arrays.equals(mPercentToDropConstraints, - PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS)) { - mPercentToDropConstraints = PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS; + case KEY_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS: + if (parsePercentToDropKeyValueString( + properties.getString(key, null), + PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS, + DEFAULT_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS)) { + mPercentsToDropConstraints = PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS; mShouldReevaluateConstraints = true; } break; } } - private int[] parsePercentToDropString(String s) { - String[] dropPercentString = s.split(","); + private boolean parsePercentToDropKeyValueString(@Nullable String s, + SparseArray<int[]> into, SparseArray<int[]> defaults) { + final KeyValueListParser priorityParser = new KeyValueListParser(','); + try { + priorityParser.setString(s); + } catch (IllegalArgumentException e) { + Slog.wtf(TAG, "Bad percent to drop key value string given", e); + // Clear the string and continue with the defaults. + priorityParser.setString(null); + } + + final int[] oldMax = into.get(PRIORITY_MAX); + final int[] oldHigh = into.get(PRIORITY_HIGH); + final int[] oldDefault = into.get(PRIORITY_DEFAULT); + final int[] oldLow = into.get(PRIORITY_LOW); + final int[] oldMin = into.get(PRIORITY_MIN); + + final int[] newMax = parsePercentToDropString(priorityParser.getString( + String.valueOf(PRIORITY_MAX), null)); + final int[] newHigh = parsePercentToDropString(priorityParser.getString( + String.valueOf(PRIORITY_HIGH), null)); + final int[] newDefault = parsePercentToDropString(priorityParser.getString( + String.valueOf(PRIORITY_DEFAULT), null)); + final int[] newLow = parsePercentToDropString(priorityParser.getString( + String.valueOf(PRIORITY_LOW), null)); + final int[] newMin = parsePercentToDropString(priorityParser.getString( + String.valueOf(PRIORITY_MIN), null)); + + into.put(PRIORITY_MAX, newMax == null ? defaults.get(PRIORITY_MAX) : newMax); + into.put(PRIORITY_HIGH, newHigh == null ? defaults.get(PRIORITY_HIGH) : newHigh); + into.put(PRIORITY_DEFAULT, + newDefault == null ? defaults.get(PRIORITY_DEFAULT) : newDefault); + into.put(PRIORITY_LOW, newLow == null ? defaults.get(PRIORITY_LOW) : newLow); + into.put(PRIORITY_MIN, newMin == null ? defaults.get(PRIORITY_MIN) : newMin); + + return !Arrays.equals(oldMax, into.get(PRIORITY_MAX)) + || !Arrays.equals(oldHigh, into.get(PRIORITY_HIGH)) + || !Arrays.equals(oldDefault, into.get(PRIORITY_DEFAULT)) + || !Arrays.equals(oldLow, into.get(PRIORITY_LOW)) + || !Arrays.equals(oldMin, into.get(PRIORITY_MIN)); + } + + @Nullable + private int[] parsePercentToDropString(@Nullable String s) { + if (s == null || s.isEmpty()) { + return null; + } + final String[] dropPercentString = s.split("\\|"); int[] dropPercentInt = new int[Integer.bitCount(FLEXIBLE_CONSTRAINTS)]; if (dropPercentInt.length != dropPercentString.length) { - return DEFAULT_PERCENT_TO_DROP_FLEXIBLE_CONSTRAINTS; + return null; } int prevPercent = 0; for (int i = 0; i < dropPercentString.length; i++) { @@ -967,11 +1460,15 @@ public final class FlexibilityController extends StateController { Integer.parseInt(dropPercentString[i]); } catch (NumberFormatException ex) { Slog.e(TAG, "Provided string was improperly formatted.", ex); - return DEFAULT_PERCENT_TO_DROP_FLEXIBLE_CONSTRAINTS; + return null; } if (dropPercentInt[i] < prevPercent) { Slog.wtf(TAG, "Percents to drop constraints were not in increasing order."); - return DEFAULT_PERCENT_TO_DROP_FLEXIBLE_CONSTRAINTS; + return null; + } + if (dropPercentInt[i] > 100) { + Slog.e(TAG, "Found % over 100"); + return null; } prevPercent = dropPercentInt[i]; } @@ -979,19 +1476,127 @@ public final class FlexibilityController extends StateController { return dropPercentInt; } + /** + * Parses the input string, expecting it to a key-value string where the keys are job + * priorities, and replaces everything in {@code into} with the values from the string, + * or the default values if the string contains none. + * + * Returns true if any values changed. + */ + private boolean parsePriorityToIntKeyValueString(@Nullable String s, + SparseIntArray into, SparseIntArray defaults) { + final KeyValueListParser parser = new KeyValueListParser(','); + try { + parser.setString(s); + } catch (IllegalArgumentException e) { + Slog.wtf(TAG, "Bad string given", e); + // Clear the string and continue with the defaults. + parser.setString(null); + } + + final int oldMax = into.get(PRIORITY_MAX); + final int oldHigh = into.get(PRIORITY_HIGH); + final int oldDefault = into.get(PRIORITY_DEFAULT); + final int oldLow = into.get(PRIORITY_LOW); + final int oldMin = into.get(PRIORITY_MIN); + + final int newMax = parser.getInt(String.valueOf(PRIORITY_MAX), + defaults.get(PRIORITY_MAX)); + final int newHigh = parser.getInt(String.valueOf(PRIORITY_HIGH), + defaults.get(PRIORITY_HIGH)); + final int newDefault = parser.getInt(String.valueOf(PRIORITY_DEFAULT), + defaults.get(PRIORITY_DEFAULT)); + final int newLow = parser.getInt(String.valueOf(PRIORITY_LOW), + defaults.get(PRIORITY_LOW)); + final int newMin = parser.getInt(String.valueOf(PRIORITY_MIN), + defaults.get(PRIORITY_MIN)); + + into.put(PRIORITY_MAX, newMax); + into.put(PRIORITY_HIGH, newHigh); + into.put(PRIORITY_DEFAULT, newDefault); + into.put(PRIORITY_LOW, newLow); + into.put(PRIORITY_MIN, newMin); + + return oldMax != newMax + || oldHigh != newHigh + || oldDefault != newDefault + || oldLow != newLow + || oldMin != newMin; + } + + /** + * Parses the input string, expecting it to a key-value string where the keys are job + * priorities, and replaces everything in {@code into} with the values from the string, + * or the default values if the string contains none. + * + * Returns true if any values changed. + */ + private boolean parsePriorityToLongKeyValueString(@Nullable String s, + SparseLongArray into, SparseLongArray defaults) { + final KeyValueListParser parser = new KeyValueListParser(','); + try { + parser.setString(s); + } catch (IllegalArgumentException e) { + Slog.wtf(TAG, "Bad string given", e); + // Clear the string and continue with the defaults. + parser.setString(null); + } + + final long oldMax = into.get(PRIORITY_MAX); + final long oldHigh = into.get(PRIORITY_HIGH); + final long oldDefault = into.get(PRIORITY_DEFAULT); + final long oldLow = into.get(PRIORITY_LOW); + final long oldMin = into.get(PRIORITY_MIN); + + final long newMax = parser.getLong(String.valueOf(PRIORITY_MAX), + defaults.get(PRIORITY_MAX)); + final long newHigh = parser.getLong(String.valueOf(PRIORITY_HIGH), + defaults.get(PRIORITY_HIGH)); + final long newDefault = parser.getLong(String.valueOf(PRIORITY_DEFAULT), + defaults.get(PRIORITY_DEFAULT)); + final long newLow = parser.getLong(String.valueOf(PRIORITY_LOW), + defaults.get(PRIORITY_LOW)); + final long newMin = parser.getLong(String.valueOf(PRIORITY_MIN), + defaults.get(PRIORITY_MIN)); + + into.put(PRIORITY_MAX, newMax); + into.put(PRIORITY_HIGH, newHigh); + into.put(PRIORITY_DEFAULT, newDefault); + into.put(PRIORITY_LOW, newLow); + into.put(PRIORITY_MIN, newMin); + + return oldMax != newMax + || oldHigh != newHigh + || oldDefault != newDefault + || oldLow != newLow + || oldMin != newMin; + } + private void dump(IndentingPrintWriter pw) { pw.println(); pw.print(FlexibilityController.class.getSimpleName()); pw.println(":"); pw.increaseIndent(); - pw.print(KEY_APPLIED_CONSTRAINTS, APPLIED_CONSTRAINTS).println(); + pw.print(KEY_APPLIED_CONSTRAINTS, APPLIED_CONSTRAINTS); + pw.print("("); + if (APPLIED_CONSTRAINTS != 0) { + JobStatus.dumpConstraints(pw, APPLIED_CONSTRAINTS); + } else { + pw.print("nothing"); + } + pw.println(")"); pw.print(KEY_DEADLINE_PROXIMITY_LIMIT, DEADLINE_PROXIMITY_LIMIT_MS).println(); pw.print(KEY_FALLBACK_FLEXIBILITY_DEADLINE, FALLBACK_FLEXIBILITY_DEADLINE_MS).println(); + pw.print(KEY_FALLBACK_FLEXIBILITY_DEADLINES, FALLBACK_FLEXIBILITY_DEADLINES).println(); + pw.print(KEY_FALLBACK_FLEXIBILITY_DEADLINE_SCORES, + FALLBACK_FLEXIBILITY_DEADLINE_SCORES).println(); + pw.print(KEY_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS, + FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS).println(); pw.print(KEY_MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS, MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS).println(); - pw.print(KEY_PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS, - PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS).println(); + pw.print(KEY_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS, + PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS).println(); pw.print(KEY_RESCHEDULED_JOB_DEADLINE_MS, RESCHEDULED_JOB_DEADLINE_MS).println(); pw.print(KEY_MAX_RESCHEDULED_DEADLINE_MS, MAX_RESCHEDULED_DEADLINE_MS).println(); pw.print(KEY_UNSEEN_CONSTRAINT_GRACE_PERIOD_MS, UNSEEN_CONSTRAINT_GRACE_PERIOD_MS) @@ -1007,6 +1612,176 @@ public final class FlexibilityController extends StateController { return mFcConfig; } + private class SpecialAppTracker { + /** + * Lock for objects inside this class. This should never be held when attempting to acquire + * {@link #mLock}. It is fine to acquire this if already holding {@link #mLock}. + */ + private final Object mSatLock = new Object(); + + private DeviceIdleInternal mDeviceIdleInternal; + + /** Set of all apps that have been deemed special, keyed by user ID. */ + private final SparseSetArray<String> mSpecialApps = new SparseSetArray<>(); + @GuardedBy("mSatLock") + private final ArraySet<String> mPowerAllowlistedApps = new ArraySet<>(); + + private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + switch (intent.getAction()) { + case PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED: + mHandler.post(SpecialAppTracker.this::updatePowerAllowlistCache); + break; + } + } + }; + + public boolean isSpecialApp(final int userId, @NonNull String packageName) { + synchronized (mSatLock) { + if (mSpecialApps.contains(UserHandle.USER_ALL, packageName)) { + return true; + } + if (mSpecialApps.contains(userId, packageName)) { + return true; + } + } + return false; + } + + private boolean isSpecialAppInternal(final int userId, @NonNull String packageName) { + synchronized (mSatLock) { + if (mPowerAllowlistedApps.contains(packageName)) { + return true; + } + } + return false; + } + + private void onAppRemoved(final int userId, String packageName) { + synchronized (mSatLock) { + // Don't touch the USER_ALL set here. If the app is completely removed from the + // device, any list that affects USER_ALL should update and this would eventually + // be updated with those lists no longer containing the app. + mSpecialApps.remove(userId, packageName); + } + } + + private void onSystemServicesReady() { + mDeviceIdleInternal = LocalServices.getService(DeviceIdleInternal.class); + + synchronized (mLock) { + if (mFlexibilityEnabled) { + mHandler.post(SpecialAppTracker.this::updatePowerAllowlistCache); + } + } + } + + private void onUserRemoved(final int userId) { + synchronized (mSatLock) { + mSpecialApps.remove(userId); + } + } + + private void startTracking() { + IntentFilter filter = new IntentFilter( + PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED); + mContext.registerReceiver(mBroadcastReceiver, filter); + + updatePowerAllowlistCache(); + } + + private void stopTracking() { + mContext.unregisterReceiver(mBroadcastReceiver); + + synchronized (mSatLock) { + mPowerAllowlistedApps.clear(); + mSpecialApps.clear(); + } + } + + /** + * Update the processed special app set for the specified user ID, only looking at the + * specified set of apps. This method must <b>NEVER</b> be called while holding + * {@link #mSatLock}. + */ + private void updateSpecialAppSetUnlocked(final int userId, @NonNull ArraySet<String> pkgs) { + // This method may need to acquire mLock, so ensure that mSatLock isn't held to avoid + // lock inversion. + if (Thread.holdsLock(mSatLock)) { + throw new IllegalStateException("Must never hold local mSatLock"); + } + if (pkgs.size() == 0) { + return; + } + final ArraySet<String> changedPkgs = new ArraySet<>(); + + synchronized (mSatLock) { + for (int i = pkgs.size() - 1; i >= 0; --i) { + final String pkgName = pkgs.valueAt(i); + if (isSpecialAppInternal(userId, pkgName)) { + if (mSpecialApps.add(userId, pkgName)) { + changedPkgs.add(pkgName); + } + } else if (mSpecialApps.remove(userId, pkgName)) { + changedPkgs.add(pkgName); + } + } + } + + if (changedPkgs.size() > 0) { + synchronized (mLock) { + mPackagesToCheck.addAll(changedPkgs); + mHandler.sendEmptyMessage(MSG_CHECK_PACKAGES); + } + } + } + + private void updatePowerAllowlistCache() { + if (mDeviceIdleInternal == null) { + return; + } + + // Don't call out to DeviceIdleController with the lock held. + final String[] allowlistedPkgs = mDeviceIdleInternal.getFullPowerWhitelistExceptIdle(); + final ArraySet<String> changedPkgs = new ArraySet<>(); + synchronized (mSatLock) { + changedPkgs.addAll(mPowerAllowlistedApps); + mPowerAllowlistedApps.clear(); + for (String pkgName : allowlistedPkgs) { + mPowerAllowlistedApps.add(pkgName); + if (!changedPkgs.remove(pkgName)) { + // The package wasn't in the previous set of allowlisted apps. Add it + // since its state has changed. + changedPkgs.add(pkgName); + } + } + } + + // The full allowlist is currently user-agnostic, so use USER_ALL for these packages. + updateSpecialAppSetUnlocked(UserHandle.USER_ALL, changedPkgs); + } + + public void dump(@NonNull IndentingPrintWriter pw) { + pw.println("Special apps:"); + pw.increaseIndent(); + + synchronized (mSatLock) { + for (int u = 0; u < mSpecialApps.size(); ++u) { + pw.print(mSpecialApps.keyAt(u)); + pw.print(": "); + pw.println(mSpecialApps.valuesAt(u)); + } + + pw.println(); + pw.print("Power allowlisted packages: "); + pw.println(mPowerAllowlistedApps); + } + + pw.decreaseIndent(); + } + } + @Override @GuardedBy("mLock") public void dumpConstants(IndentingPrintWriter pw) { @@ -1044,7 +1819,24 @@ public final class FlexibilityController extends StateController { pw.decreaseIndent(); pw.println(); - mFlexibilityTracker.dump(pw, predicate); + mSpecialAppTracker.dump(pw); + + pw.println(); + mFlexibilityTracker.dump(pw, predicate, nowElapsed); + + pw.println(); + pw.println("Job scores:"); + pw.increaseIndent(); + mJobScoreTrackers.forEach((uid, pkgName, jobScoreTracker) -> { + pw.print(uid); + pw.print("/"); + pw.print(pkgName); + pw.print(": "); + jobScoreTracker.dump(pw, nowElapsed); + pw.println(); + }); + pw.decreaseIndent(); + pw.println(); mFlexibilityAlarmQueue.dump(pw); } 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 0d5d11e98860..edd86e3454a5 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 @@ -16,8 +16,6 @@ package com.android.server.job.controllers; -import static android.text.format.DateUtils.HOUR_IN_MILLIS; - import static com.android.server.job.JobSchedulerService.ACTIVE_INDEX; import static com.android.server.job.JobSchedulerService.EXEMPTED_INDEX; import static com.android.server.job.JobSchedulerService.NEVER_INDEX; @@ -48,6 +46,7 @@ import android.util.ArrayMap; import android.util.ArraySet; import android.util.IndentingPrintWriter; import android.util.Pair; +import android.util.Patterns; import android.util.Range; import android.util.Slog; import android.util.TimeUtils; @@ -76,6 +75,7 @@ import java.util.Collections; import java.util.Objects; import java.util.Random; import java.util.function.Predicate; +import java.util.regex.Pattern; /** * Uniquely identifies a job internally. @@ -106,11 +106,8 @@ public final class JobStatus { public static final long NO_LATEST_RUNTIME = Long.MAX_VALUE; public static final long NO_EARLIEST_RUNTIME = 0L; - @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) public static final int CONSTRAINT_CHARGING = JobInfo.CONSTRAINT_FLAG_CHARGING; // 1 < 0 - @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) public static final int CONSTRAINT_IDLE = JobInfo.CONSTRAINT_FLAG_DEVICE_IDLE; // 1 << 2 - @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) public static final int CONSTRAINT_BATTERY_NOT_LOW = JobInfo.CONSTRAINT_FLAG_BATTERY_NOT_LOW; // 1 << 1 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) @@ -125,7 +122,7 @@ public final class JobStatus { static final int CONSTRAINT_WITHIN_QUOTA = 1 << 24; // Implicit constraint static final int CONSTRAINT_PREFETCH = 1 << 23; static final int CONSTRAINT_BACKGROUND_NOT_RESTRICTED = 1 << 22; // Implicit constraint - static final int CONSTRAINT_FLEXIBLE = 1 << 21; // Implicit constraint + public static final int CONSTRAINT_FLEXIBLE = 1 << 21; // Implicit constraint private static final int IMPLICIT_CONSTRAINTS = 0 | CONSTRAINT_BACKGROUND_NOT_RESTRICTED @@ -206,6 +203,17 @@ public final class JobStatus { // TODO(b/129954980): ensure this doesn't spam statsd, especially at boot private static final boolean STATS_LOG_ENABLED = false; + /** + * Simple patterns to match some common forms of PII. This is not intended all-encompassing and + * any clients should aim to do additional filtering. + */ + private static final ArrayMap<Pattern, String> BASIC_PII_FILTERS = new ArrayMap<>(); + + static { + BASIC_PII_FILTERS.put(Patterns.EMAIL_ADDRESS, "[EMAIL]"); + BASIC_PII_FILTERS.put(Patterns.PHONE, "[PHONE]"); + } + // No override. public static final int OVERRIDE_NONE = 0; // Override to improve sorting order. Does not affect constraint evaluation. @@ -253,6 +261,18 @@ public final class JobStatus { private final long mLoggingJobId; /** + * List of tags from {@link JobInfo#getDebugTags()}, filtered using {@link #BASIC_PII_FILTERS}. + * Lazily loaded in {@link #getFilteredDebugTags()}. + */ + @Nullable + private String[] mFilteredDebugTags; + /** + * Trace tag from {@link JobInfo#getTraceTag()}, filtered using {@link #BASIC_PII_FILTERS}. + * Lazily loaded in {@link #getFilteredTraceTag()}. + */ + @Nullable + private String mFilteredTraceTag; + /** * Tag to identify the wakelock held for this job. Lazily loaded in * {@link #getWakelockTag()} since it's not typically needed until the job is about to run. */ @@ -408,9 +428,6 @@ public final class JobStatus { */ public static final int INTERNAL_FLAG_DEMOTED_BY_SYSTEM_UIJ = 1 << 2; - /** Minimum difference between start and end time to have flexible constraint */ - @VisibleForTesting - static final long MIN_WINDOW_FOR_FLEXIBILITY_MS = HOUR_IN_MILLIS; /** * Versatile, persistable flags for a job that's updated within the system server, * as opposed to {@link JobInfo#flags} that's set by callers. @@ -635,7 +652,7 @@ public final class JobStatus { .build()); // Don't perform validation checks at this point since we've already passed the // initial validation check. - job = builder.build(false, false, false); + job = builder.build(false, false, false, false); } this.job = job; @@ -686,14 +703,10 @@ public final class JobStatus { final boolean lacksSomeFlexibleConstraints = ((~requiredConstraints) & SYSTEM_WIDE_FLEXIBLE_CONSTRAINTS) != 0 || mCanApplyTransportAffinities; - final boolean satisfiesMinWindowException = - (latestRunTimeElapsedMillis - earliestRunTimeElapsedMillis) - >= MIN_WINDOW_FOR_FLEXIBILITY_MS; // The first time a job is rescheduled it will not be subject to flexible constraints. // Otherwise, every consecutive reschedule increases a jobs' flexibility deadline. if (!isRequestedExpeditedJob() && !job.isUserInitiated() - && satisfiesMinWindowException && (numFailures + numSystemStops) != 1 && lacksSomeFlexibleConstraints) { requiredConstraints |= CONSTRAINT_FLEXIBLE; @@ -1200,21 +1213,25 @@ public final class JobStatus { return ACTIVE_INDEX; } - final int bucketWithMediaExemption; - if (actualBucket != RESTRICTED_INDEX && actualBucket != NEVER_INDEX - && mHasMediaBackupExemption) { + final boolean isEligibleAsBackupJob = job.getTriggerContentUris() != null + && job.getRequiredNetwork() != null + && !job.hasLateConstraint() + && mJobSchedulerInternal.hasRunBackupJobsPermission(sourcePackageName, sourceUid); + final boolean isBackupExempt = mHasMediaBackupExemption || isEligibleAsBackupJob; + final int bucketWithBackupExemption; + if (actualBucket != RESTRICTED_INDEX && actualBucket != NEVER_INDEX && isBackupExempt) { // Treat it as if it's at most WORKING_INDEX (lower index grants higher quota) since // media backup jobs are important to the user, and the source package may not have // been used directly in a while. - bucketWithMediaExemption = Math.min(WORKING_INDEX, actualBucket); + bucketWithBackupExemption = Math.min(WORKING_INDEX, actualBucket); } else { - bucketWithMediaExemption = actualBucket; + bucketWithBackupExemption = actualBucket; } // If the app is considered buggy, but hasn't yet been put in the RESTRICTED bucket // (potentially because it's used frequently by the user), limit its effective bucket // so that it doesn't get to run as much as a normal ACTIVE app. - if (isBuggy && bucketWithMediaExemption < WORKING_INDEX) { + if (isBuggy && bucketWithBackupExemption < WORKING_INDEX) { if (!mIsDowngradedDueToBuggyApp) { // Safety check to avoid logging multiple times for the same job. Counter.logIncrementWithUid( @@ -1224,7 +1241,7 @@ public final class JobStatus { } return WORKING_INDEX; } - return bucketWithMediaExemption; + return bucketWithBackupExemption; } /** Returns the real standby bucket of the job. */ @@ -1328,6 +1345,47 @@ public final class JobStatus { return batteryName; } + @VisibleForTesting + @NonNull + static String applyBasicPiiFilters(@NonNull String val) { + for (int i = BASIC_PII_FILTERS.size() - 1; i >= 0; --i) { + val = BASIC_PII_FILTERS.keyAt(i).matcher(val).replaceAll(BASIC_PII_FILTERS.valueAt(i)); + } + return val; + } + + /** + * List of tags from {@link JobInfo#getDebugTags()}, filtered using a basic set of PII filters. + */ + @NonNull + public String[] getFilteredDebugTags() { + if (mFilteredDebugTags != null) { + return mFilteredDebugTags; + } + final ArraySet<String> debugTags = job.getDebugTagsArraySet(); + mFilteredDebugTags = new String[debugTags.size()]; + for (int i = 0; i < mFilteredDebugTags.length; ++i) { + mFilteredDebugTags[i] = applyBasicPiiFilters(debugTags.valueAt(i)); + } + return mFilteredDebugTags; + } + + /** + * Trace tag from {@link JobInfo#getTraceTag()}, filtered using a basic set of PII filters. + */ + @Nullable + public String getFilteredTraceTag() { + if (mFilteredTraceTag != null) { + return mFilteredTraceTag; + } + final String rawTag = job.getTraceTag(); + if (rawTag == null) { + return null; + } + mFilteredTraceTag = applyBasicPiiFilters(rawTag); + return mFilteredTraceTag; + } + /** Return the String to be used as the tag for the wakelock held for this job. */ @NonNull public String getWakelockTag() { @@ -1534,7 +1592,7 @@ public final class JobStatus { /** * Returns the number of required flexible job constraints that have been dropped with time. - * The lower this number is the easier it is for the flexibility constraint to be satisfied. + * The higher this number is the easier it is for the flexibility constraint to be satisfied. */ public int getNumDroppedFlexibleConstraints() { return mNumDroppedFlexibleConstraints; @@ -1600,7 +1658,8 @@ public final class JobStatus { mTransportAffinitiesSatisfied = isSatisfied; } - boolean canApplyTransportAffinities() { + /** Whether transport affinities can be applied to the job in flex scheduling. */ + public boolean canApplyTransportAffinities() { return mCanApplyTransportAffinities; } @@ -1985,6 +2044,11 @@ public final class JobStatus { case CONSTRAINT_WITHIN_QUOTA: return JobParameters.STOP_REASON_QUOTA; + // This can change from true to false, but should never change when a job is already + // running, so there's no reason to log a message or create a new stop reason. + case CONSTRAINT_FLEXIBLE: + return JobParameters.STOP_REASON_UNDEFINED; + // These should never be stop reasons since they can never go from true to false. case CONSTRAINT_CONTENT_TRIGGER: case CONSTRAINT_DEADLINE: @@ -2194,7 +2258,7 @@ public final class JobStatus { * @return Whether or not this job would be ready to run if it had the specified constraint * granted, based on its requirements. */ - boolean wouldBeReadyWithConstraint(int constraint) { + public boolean wouldBeReadyWithConstraint(int constraint) { return readinessStatusWithConstraint(constraint, true); } diff --git a/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java b/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java index 357e139617ef..6635484b20b9 100644 --- a/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java +++ b/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java @@ -227,7 +227,7 @@ public class InternalResourceService extends SystemService { private final IAppOpsCallback mApbListener = new IAppOpsCallback.Stub() { @Override - public void opChanged(int op, int uid, String packageName) { + public void opChanged(int op, int uid, String packageName, String persistentDeviceId) { boolean restricted = false; try { restricted = mAppOpsService.checkOperation( diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java b/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java index 913a76a65026..4d4e3407a3c3 100644 --- a/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java +++ b/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java @@ -591,6 +591,16 @@ public class AppIdleHistory { if (idle) { newBucket = IDLE_BUCKET_CUTOFF; reason = REASON_MAIN_FORCED_BY_USER; + final AppUsageHistory appHistory = getAppUsageHistory(packageName, userId, + elapsedRealtime); + // Wipe all expiry times that could raise the bucket on reevaluation. + if (appHistory.bucketExpiryTimesMs != null) { + for (int i = appHistory.bucketExpiryTimesMs.size() - 1; i >= 0; --i) { + if (appHistory.bucketExpiryTimesMs.keyAt(i) < newBucket) { + appHistory.bucketExpiryTimesMs.removeAt(i); + } + } + } } else { newBucket = STANDBY_BUCKET_ACTIVE; // This is to pretend that the app was just used, don't freeze the state anymore. diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java index 12f455ad0144..19bc7160e16a 100644 --- a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java +++ b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java @@ -59,6 +59,7 @@ import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY; import static com.android.server.usage.AppIdleHistory.STANDBY_BUCKET_UNKNOWN; import android.annotation.CurrentTimeMillisLong; +import android.annotation.DurationMillisLong; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; @@ -670,7 +671,8 @@ public class AppStandbyController /*packageName=*/ null, new IAppOpsCallback.Stub() { @Override - public void opChanged(int op, int uid, String packageName) { + public void opChanged(int op, int uid, String packageName, + String persistentDeviceId) { final int userId = UserHandle.getUserId(uid); synchronized (mSystemExemptionAppOpMode) { mSystemExemptionAppOpMode.delete(uid); @@ -2146,6 +2148,15 @@ public class AppStandbyController } } + /** + * Flush the handler. + * Returns true if successfully flushed within the timeout, otherwise return false. + */ + @VisibleForTesting + boolean flushHandler(@DurationMillisLong long timeoutMillis) { + return mHandler.runWithScissors(() -> {}, timeoutMillis); + } + @Override public void flushToDisk() { synchronized (mAppIdleLock) { @@ -2258,7 +2269,8 @@ public class AppStandbyController } synchronized (mSystemExemptionAppOpMode) { if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) { - mSystemExemptionAppOpMode.delete(UserHandle.getUid(userId, getAppId(pkgName))); + final int uid = intent.getIntExtra(Intent.EXTRA_UID, Process.INVALID_UID); + mSystemExemptionAppOpMode.delete(uid); } } |