summaryrefslogtreecommitdiff
path: root/apex
diff options
context:
space:
mode:
Diffstat (limited to 'apex')
-rw-r--r--apex/jobscheduler/framework/aconfig/job.aconfig7
-rw-r--r--apex/jobscheduler/framework/java/android/app/job/JobInfo.java169
-rw-r--r--apex/jobscheduler/framework/java/com/android/server/DeviceIdleInternal.java4
-rw-r--r--apex/jobscheduler/framework/java/com/android/server/job/JobSchedulerInternal.java6
-rw-r--r--apex/jobscheduler/service/Android.bp1
-rw-r--r--apex/jobscheduler/service/aconfig/Android.bp13
-rw-r--r--apex/jobscheduler/service/aconfig/alarm.aconfig19
-rw-r--r--apex/jobscheduler/service/aconfig/device_idle.aconfig2
-rw-r--r--apex/jobscheduler/service/aconfig/job.aconfig28
-rw-r--r--apex/jobscheduler/service/java/com/android/server/AppStateTrackerImpl.java3
-rw-r--r--apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java22
-rw-r--r--apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java110
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java1
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java707
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java73
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java22
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/JobStore.java2
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/PendingJobQueue.java5
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java4
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java76
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java301
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java914
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java112
-rw-r--r--apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java2
-rw-r--r--apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java10
-rw-r--r--apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java16
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);
}
}