summaryrefslogtreecommitdiff
path: root/apex
diff options
context:
space:
mode:
Diffstat (limited to 'apex')
-rw-r--r--apex/blobstore/service/java/com/android/server/blob/BlobAccessMode.java26
-rw-r--r--apex/blobstore/service/java/com/android/server/blob/BlobMetadata.java4
-rw-r--r--apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java28
-rw-r--r--apex/jobscheduler/framework/java/android/app/AlarmManager.java218
-rw-r--r--apex/jobscheduler/framework/java/android/app/IAlarmManager.aidl2
-rw-r--r--apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java125
-rw-r--r--apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl53
-rw-r--r--apex/jobscheduler/framework/java/android/app/job/IJobScheduler.aidl26
-rw-r--r--apex/jobscheduler/framework/java/android/app/job/IJobService.aidl7
-rw-r--r--apex/jobscheduler/framework/java/android/app/job/JobInfo.java333
-rw-r--r--apex/jobscheduler/framework/java/android/app/job/JobParameters.java84
-rw-r--r--apex/jobscheduler/framework/java/android/app/job/JobScheduler.java275
-rw-r--r--apex/jobscheduler/framework/java/android/app/job/JobSchedulerFrameworkInitializer.java8
-rw-r--r--apex/jobscheduler/framework/java/android/app/job/JobService.java293
-rw-r--r--apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java306
-rw-r--r--apex/jobscheduler/framework/java/android/app/job/JobWorkItem.java214
-rw-r--r--apex/jobscheduler/framework/java/android/app/job/UserVisibleJobSummary.java37
-rw-r--r--apex/jobscheduler/framework/java/android/app/tare/EconomyManager.java122
-rw-r--r--apex/jobscheduler/framework/java/android/app/tare/IEconomyManager.aidl1
-rw-r--r--apex/jobscheduler/framework/java/android/os/PowerExemptionManager.java34
-rw-r--r--apex/jobscheduler/framework/java/com/android/server/job/JobSchedulerInternal.java28
-rw-r--r--apex/jobscheduler/service/java/com/android/server/AppSchedulingModuleThread.java (renamed from apex/jobscheduler/service/java/com/android/server/JobSchedulerBackgroundThread.java)27
-rw-r--r--apex/jobscheduler/service/java/com/android/server/AppStateTrackerImpl.java123
-rw-r--r--apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java595
-rw-r--r--apex/jobscheduler/service/java/com/android/server/alarm/Alarm.java43
-rw-r--r--apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java785
-rw-r--r--apex/jobscheduler/service/java/com/android/server/alarm/BatchingAlarmStore.java408
-rw-r--r--apex/jobscheduler/service/java/com/android/server/alarm/MetricsHelper.java10
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/JobCompletedListener.java12
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java1113
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/JobNotificationCoordinator.java350
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java2250
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java276
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java667
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/JobStore.java794
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/PendingJobQueue.java24
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/StateChangedListener.java14
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java35
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java21
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/ComponentController.java53
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java323
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/ContentObserverController.java6
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java6
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java861
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java12
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java780
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/Package.java59
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/PrefetchController.java58
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java104
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java10
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/StorageController.java3
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/TareController.java50
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/TimeController.java14
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/idle/CarIdlenessTracker.java5
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/idle/DeviceIdlenessTracker.java14
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/restrictions/JobRestriction.java25
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java39
-rw-r--r--apex/jobscheduler/service/java/com/android/server/tare/Agent.java316
-rw-r--r--apex/jobscheduler/service/java/com/android/server/tare/AlarmManagerEconomicPolicy.java121
-rw-r--r--apex/jobscheduler/service/java/com/android/server/tare/Analyst.java141
-rw-r--r--apex/jobscheduler/service/java/com/android/server/tare/CompleteEconomicPolicy.java151
-rw-r--r--apex/jobscheduler/service/java/com/android/server/tare/EconomicPolicy.java99
-rw-r--r--apex/jobscheduler/service/java/com/android/server/tare/EconomyManagerInternal.java15
-rw-r--r--apex/jobscheduler/service/java/com/android/server/tare/InstalledPackageInfo.java106
-rw-r--r--apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java960
-rw-r--r--apex/jobscheduler/service/java/com/android/server/tare/JobSchedulerEconomicPolicy.java161
-rw-r--r--apex/jobscheduler/service/java/com/android/server/tare/Ledger.java241
-rw-r--r--apex/jobscheduler/service/java/com/android/server/tare/ProcessStateModifier.java19
-rw-r--r--apex/jobscheduler/service/java/com/android/server/tare/README.md21
-rw-r--r--apex/jobscheduler/service/java/com/android/server/tare/Scribe.java246
-rw-r--r--apex/jobscheduler/service/java/com/android/server/tare/TareShellCommand.java112
-rw-r--r--apex/jobscheduler/service/java/com/android/server/tare/TareUtils.java11
-rw-r--r--apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java152
-rw-r--r--apex/jobscheduler/service/jni/com_android_server_alarm_AlarmManagerService.cpp80
74 files changed, 12314 insertions, 2831 deletions
diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobAccessMode.java b/apex/blobstore/service/java/com/android/server/blob/BlobAccessMode.java
index 0d17bbc7bbff..b0c295c331d7 100644
--- a/apex/blobstore/service/java/com/android/server/blob/BlobAccessMode.java
+++ b/apex/blobstore/service/java/com/android/server/blob/BlobAccessMode.java
@@ -24,12 +24,16 @@ import android.annotation.IntDef;
import android.annotation.NonNull;
import android.content.Context;
import android.content.pm.PackageManager;
+import android.content.pm.PackageManagerInternal;
+import android.os.Binder;
+import android.os.UserHandle;
import android.util.ArraySet;
import android.util.Base64;
import android.util.DebugUtils;
import android.util.IndentingPrintWriter;
import com.android.internal.util.XmlUtils;
+import com.android.server.LocalServices;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -100,20 +104,21 @@ class BlobAccessMode {
}
boolean isAccessAllowedForCaller(Context context,
- @NonNull String callingPackage, @NonNull String committerPackage) {
+ @NonNull String callingPackage, int callingUid, int committerUid) {
if ((mAccessType & ACCESS_TYPE_PUBLIC) != 0) {
return true;
}
- final PackageManager pm = context.getPackageManager();
if ((mAccessType & ACCESS_TYPE_SAME_SIGNATURE) != 0) {
- if (pm.checkSignatures(committerPackage, callingPackage)
- == PackageManager.SIGNATURE_MATCH) {
+ if (checkSignatures(callingUid, committerUid)) {
return true;
}
}
if ((mAccessType & ACCESS_TYPE_ALLOWLIST) != 0) {
+ final UserHandle callingUser = UserHandle.of(UserHandle.getUserId(callingUid));
+ final PackageManager pm =
+ context.createContextAsUser(callingUser, 0 /* flags */).getPackageManager();
for (int i = 0; i < mAllowedPackages.size(); ++i) {
final PackageIdentifier packageIdentifier = mAllowedPackages.valueAt(i);
if (packageIdentifier.packageName.equals(callingPackage)
@@ -127,6 +132,19 @@ class BlobAccessMode {
return false;
}
+ /**
+ * Compare signatures for two packages of different users.
+ */
+ private boolean checkSignatures(int uid1, int uid2) {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ return LocalServices.getService(PackageManagerInternal.class)
+ .checkUidSignaturesForAllUsers(uid1, uid2) == PackageManager.SIGNATURE_MATCH;
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
int getAccessType() {
return mAccessType;
}
diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobMetadata.java b/apex/blobstore/service/java/com/android/server/blob/BlobMetadata.java
index 7638f059b47e..d5315daec11a 100644
--- a/apex/blobstore/service/java/com/android/server/blob/BlobMetadata.java
+++ b/apex/blobstore/service/java/com/android/server/blob/BlobMetadata.java
@@ -293,7 +293,7 @@ class BlobMetadata {
// Check if the caller is allowed access as per the access mode specified
// by the committer.
if (committer.blobAccessMode.isAccessAllowedForCaller(mContext,
- callingPackage, committer.packageName)) {
+ callingPackage, callingUid, committer.uid)) {
return true;
}
}
@@ -316,7 +316,7 @@ class BlobMetadata {
// Check if the caller is allowed access as per the access mode specified
// by the committer.
if (committer.blobAccessMode.isAccessAllowedForCaller(mContext,
- callingPackage, committer.packageName)) {
+ callingPackage, callingUid, committer.uid)) {
return true;
}
}
diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java b/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java
index 9ac3e412b1e4..9d363c806f5f 100644
--- a/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java
+++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java
@@ -80,6 +80,7 @@ import android.os.Process;
import android.os.RemoteCallback;
import android.os.SystemClock;
import android.os.UserHandle;
+import android.os.UserManager;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.AtomicFile;
@@ -619,7 +620,7 @@ public class BlobStoreManagerService extends SystemService {
return blobInfos;
}
- private void deleteBlobInternal(long blobId, int callingUid) {
+ private void deleteBlobInternal(long blobId) {
synchronized (mBlobsLock) {
mBlobsMap.entrySet().removeIf(entry -> {
final BlobMetadata blobMetadata = entry.getValue();
@@ -1612,10 +1613,7 @@ public class BlobStoreManagerService extends SystemService {
@Override
@NonNull
public List<BlobInfo> queryBlobsForUser(@UserIdInt int userId) {
- if (Binder.getCallingUid() != Process.SYSTEM_UID) {
- throw new SecurityException("Only system uid is allowed to call "
- + "queryBlobsForUser()");
- }
+ verifyCallerIsSystemUid("queryBlobsForUser");
final int resolvedUserId = userId == USER_CURRENT
? ActivityManager.getCurrentUser() : userId;
@@ -1629,13 +1627,9 @@ public class BlobStoreManagerService extends SystemService {
@Override
public void deleteBlob(long blobId) {
- final int callingUid = Binder.getCallingUid();
- if (callingUid != Process.SYSTEM_UID) {
- throw new SecurityException("Only system uid is allowed to call "
- + "deleteBlob()");
- }
+ verifyCallerIsSystemUid("deleteBlob");
- deleteBlobInternal(blobId, callingUid);
+ deleteBlobInternal(blobId);
}
@Override
@@ -1716,6 +1710,18 @@ public class BlobStoreManagerService extends SystemService {
return new BlobStoreManagerShellCommand(BlobStoreManagerService.this).exec(this,
in.getFileDescriptor(), out.getFileDescriptor(), err.getFileDescriptor(), args);
}
+
+ /**
+ * Verify if the caller is an admin user's app with system uid
+ */
+ private void verifyCallerIsSystemUid(final String operation) {
+ if (UserHandle.getCallingAppId() != Process.SYSTEM_UID
+ || !mContext.getSystemService(UserManager.class)
+ .isUserAdmin(UserHandle.getCallingUserId())) {
+ throw new SecurityException("Only admin user's app with system uid"
+ + "are allowed to call #" + operation);
+ }
+ }
}
static final class DumpArgs {
diff --git a/apex/jobscheduler/framework/java/android/app/AlarmManager.java b/apex/jobscheduler/framework/java/android/app/AlarmManager.java
index 21ed1eb6bef8..ec6a8b8af899 100644
--- a/apex/jobscheduler/framework/java/android/app/AlarmManager.java
+++ b/apex/jobscheduler/framework/java/android/app/AlarmManager.java
@@ -27,7 +27,6 @@ import android.annotation.SystemApi;
import android.annotation.SystemService;
import android.annotation.TestApi;
import android.compat.annotation.ChangeId;
-import android.compat.annotation.Disabled;
import android.compat.annotation.EnabledSince;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
@@ -295,13 +294,32 @@ public class AlarmManager {
* The permission {@link Manifest.permission#SCHEDULE_EXACT_ALARM} will be denied, unless the
* user explicitly allows it from Settings.
*
- * TODO (b/226439802): Either enable it in the next SDK or replace it with a better alternative.
* @hide
*/
@ChangeId
- @Disabled
+ @EnabledSince(targetSdkVersion = Build.VERSION_CODES.TIRAMISU)
public static final long SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT = 226439802L;
+ /**
+ * Holding the permission {@link Manifest.permission#SCHEDULE_EXACT_ALARM} will no longer pin
+ * the standby-bucket of the app to
+ * {@link android.app.usage.UsageStatsManager#STANDBY_BUCKET_WORKING_SET} or better.
+ *
+ * @hide
+ */
+ @ChangeId
+ @EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public static final long SCHEDULE_EXACT_ALARM_DOES_NOT_ELEVATE_BUCKET = 262645982L;
+
+ /**
+ * Exact alarms expecting a {@link OnAlarmListener} callback will be dropped when the calling
+ * app goes into cached state.
+ *
+ * @hide
+ */
+ @ChangeId
+ public static final long EXACT_LISTENER_ALARMS_DROPPED_ON_CACHED = 265195908L;
+
@UnsupportedAppUsage
private final IAlarmManager mService;
private final Context mContext;
@@ -319,7 +337,7 @@ public class AlarmManager {
/**
* Callback method that is invoked by the system when the alarm time is reached.
*/
- public void onAlarm();
+ void onAlarm();
}
final class ListenerWrapper extends IAlarmListener.Stub implements Runnable {
@@ -463,7 +481,7 @@ public class AlarmManager {
* @see #RTC
* @see #RTC_WAKEUP
*/
- public void set(@AlarmType int type, long triggerAtMillis, PendingIntent operation) {
+ public void set(@AlarmType int type, long triggerAtMillis, @NonNull PendingIntent operation) {
setImpl(type, triggerAtMillis, legacyExactLength(), 0, 0, operation, null, null,
(Handler) null, null, null);
}
@@ -490,8 +508,8 @@ public class AlarmManager {
* @param targetHandler {@link Handler} on which to execute the listener's onAlarm()
* callback, or {@code null} to run that callback on the main looper.
*/
- public void set(@AlarmType int type, long triggerAtMillis, String tag, OnAlarmListener listener,
- Handler targetHandler) {
+ public void set(@AlarmType int type, long triggerAtMillis, @Nullable String tag,
+ @NonNull OnAlarmListener listener, @Nullable Handler targetHandler) {
setImpl(type, triggerAtMillis, legacyExactLength(), 0, 0, null, listener, tag,
targetHandler, null, null);
}
@@ -556,7 +574,7 @@ public class AlarmManager {
* @see Intent#EXTRA_ALARM_COUNT
*/
public void setRepeating(@AlarmType int type, long triggerAtMillis,
- long intervalMillis, PendingIntent operation) {
+ long intervalMillis, @NonNull PendingIntent operation) {
setImpl(type, triggerAtMillis, legacyExactLength(), intervalMillis, 0, operation,
null, null, (Handler) null, null, null);
}
@@ -612,7 +630,7 @@ public class AlarmManager {
* @see #RTC_WAKEUP
*/
public void setWindow(@AlarmType int type, long windowStartMillis, long windowLengthMillis,
- PendingIntent operation) {
+ @NonNull PendingIntent operation) {
setImpl(type, windowStartMillis, windowLengthMillis, 0, 0, operation,
null, null, (Handler) null, null, null);
}
@@ -635,12 +653,62 @@ public class AlarmManager {
* @see #setWindow(int, long, long, PendingIntent)
*/
public void setWindow(@AlarmType int type, long windowStartMillis, long windowLengthMillis,
- String tag, OnAlarmListener listener, Handler targetHandler) {
+ @Nullable String tag, @NonNull OnAlarmListener listener,
+ @Nullable Handler targetHandler) {
setImpl(type, windowStartMillis, windowLengthMillis, 0, 0, null, listener, tag,
targetHandler, null, null);
}
/**
+ * Direct callback version of {@link #setWindow(int, long, long, PendingIntent)}. Rather
+ * than supplying a PendingIntent to be sent when the alarm time is reached, this variant
+ * supplies an {@link OnAlarmListener} instance that will be invoked at that time.
+ * <p>
+ * The OnAlarmListener {@link OnAlarmListener#onAlarm() onAlarm()} method will be
+ * invoked via the specified target Executor.
+ *
+ * <p>
+ * Note: Starting with API {@link Build.VERSION_CODES#S}, apps should not pass in a window of
+ * less than 10 minutes. The system will try its best to accommodate smaller windows if the
+ * alarm is supposed to fire in the near future, but there are no guarantees and the app should
+ * expect any window smaller than 10 minutes to get elongated to 10 minutes.
+ *
+ * @see #setWindow(int, long, long, PendingIntent)
+ */
+ public void setWindow(@AlarmType int type, long windowStartMillis, long windowLengthMillis,
+ @Nullable String tag, @NonNull Executor executor, @NonNull OnAlarmListener listener) {
+ setImpl(type, windowStartMillis, windowLengthMillis, 0, 0, null, listener, tag,
+ executor, null, null);
+ }
+
+ /**
+ * Direct callback version of {@link #setWindow(int, long, long, PendingIntent)}. Rather
+ * than supplying a PendingIntent to be sent when the alarm time is reached, this variant
+ * supplies an {@link OnAlarmListener} instance that will be invoked at that time.
+ * <p>
+ * The OnAlarmListener's {@link OnAlarmListener#onAlarm() onAlarm()} method will be
+ * invoked via the specified target Executor.
+ *
+ * <p>
+ * Note: Starting with API {@link Build.VERSION_CODES#S}, apps should not pass in a window of
+ * less than 10 minutes. The system will try its best to accommodate smaller windows if the
+ * alarm is supposed to fire in the near future, but there are no guarantees and the app should
+ * expect any window smaller than 10 minutes to get elongated to 10 minutes.
+ *
+ * @see #setWindow(int, long, long, PendingIntent)
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS)
+ public void setWindow(@AlarmType int type, long windowStartMillis, long windowLengthMillis,
+ @Nullable String tag, @NonNull Executor executor, @Nullable WorkSource workSource,
+ @NonNull OnAlarmListener listener) {
+ setImpl(type, windowStartMillis, windowLengthMillis, 0, 0, null, listener, tag,
+ executor, workSource, null);
+ }
+
+ /**
* Schedule an alarm that is prioritized by the system while the device is in power saving modes
* such as battery saver and device idle (doze).
*
@@ -735,7 +803,8 @@ public class AlarmManager {
* @see Manifest.permission#SCHEDULE_EXACT_ALARM SCHEDULE_EXACT_ALARM
*/
@RequiresPermission(value = Manifest.permission.SCHEDULE_EXACT_ALARM, conditional = true)
- public void setExact(@AlarmType int type, long triggerAtMillis, PendingIntent operation) {
+ public void setExact(@AlarmType int type, long triggerAtMillis,
+ @NonNull PendingIntent operation) {
setImpl(type, triggerAtMillis, WINDOW_EXACT, 0, 0, operation, null, null, (Handler) null,
null, null);
}
@@ -748,26 +817,27 @@ public class AlarmManager {
* The OnAlarmListener's {@link OnAlarmListener#onAlarm() onAlarm()} method will be
* invoked via the specified target Handler, or on the application's main looper
* if {@code null} is passed as the {@code targetHandler} parameter.
+ * <p>
+ * This API should only be used to set alarms that are relevant in the context of the app's
+ * current lifecycle, as the {@link OnAlarmListener} instance supplied is only valid as long as
+ * the process is alive, and the system can clean up the app process as soon as it is out of
+ * lifecycle. To schedule alarms that fire reliably even after the current lifecycle completes,
+ * and wakes up the app if required, use any of the other scheduling APIs that accept a
+ * {@link PendingIntent} instance.
*
- * <p class="note"><strong>Note:</strong>
- * Starting with {@link Build.VERSION_CODES#S}, apps targeting SDK level 31 or higher
- * need to request the
- * {@link Manifest.permission#SCHEDULE_EXACT_ALARM SCHEDULE_EXACT_ALARM} permission to use this
- * API, unless the app is exempt from battery restrictions.
- * The user and the system can revoke this permission via the special app access screen in
- * Settings.
+ * <p>
+ * On previous android versions {@link Build.VERSION_CODES#S} and
+ * {@link Build.VERSION_CODES#TIRAMISU}, apps targeting SDK level 31 or higher needed to hold
+ * the {@link Manifest.permission#SCHEDULE_EXACT_ALARM SCHEDULE_EXACT_ALARM} permission to use
+ * this API, unless the app was exempt from battery restrictions.
*
* <p class="note"><strong>Note:</strong>
- * Exact alarms should only be used for user-facing features.
- * For more details, see <a
- * href="{@docRoot}about/versions/12/behavior-changes-12#exact-alarm-permission">
- * Exact alarm permission</a>.
+ * Starting with android version {@link Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, the system will
+ * explicitly drop any alarms set via this API when the calling app goes out of lifecycle.
*
- * @see Manifest.permission#SCHEDULE_EXACT_ALARM SCHEDULE_EXACT_ALARM
*/
- @RequiresPermission(value = Manifest.permission.SCHEDULE_EXACT_ALARM, conditional = true)
- public void setExact(@AlarmType int type, long triggerAtMillis, String tag,
- OnAlarmListener listener, Handler targetHandler) {
+ public void setExact(@AlarmType int type, long triggerAtMillis, @Nullable String tag,
+ @NonNull OnAlarmListener listener, @Nullable Handler targetHandler) {
setImpl(type, triggerAtMillis, WINDOW_EXACT, 0, 0, null, listener, tag,
targetHandler, null, null);
}
@@ -777,8 +847,8 @@ public class AlarmManager {
* the given time.
* @hide
*/
- public void setIdleUntil(@AlarmType int type, long triggerAtMillis, String tag,
- OnAlarmListener listener, Handler targetHandler) {
+ public void setIdleUntil(@AlarmType int type, long triggerAtMillis, @Nullable String tag,
+ @NonNull OnAlarmListener listener, @Nullable Handler targetHandler) {
setImpl(type, triggerAtMillis, WINDOW_EXACT, 0, FLAG_IDLE_UNTIL, null,
listener, tag, targetHandler, null, null);
}
@@ -838,7 +908,7 @@ public class AlarmManager {
* @see Manifest.permission#SCHEDULE_EXACT_ALARM SCHEDULE_EXACT_ALARM
*/
@RequiresPermission(Manifest.permission.SCHEDULE_EXACT_ALARM)
- public void setAlarmClock(AlarmClockInfo info, PendingIntent operation) {
+ public void setAlarmClock(@NonNull AlarmClockInfo info, @NonNull PendingIntent operation) {
setImpl(RTC_WAKEUP, info.getTriggerTime(), WINDOW_EXACT, 0, 0, operation,
null, null, (Handler) null, null, info);
}
@@ -847,7 +917,8 @@ public class AlarmManager {
@SystemApi
@RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS)
public void set(@AlarmType int type, long triggerAtMillis, long windowMillis,
- long intervalMillis, PendingIntent operation, WorkSource workSource) {
+ long intervalMillis, @NonNull PendingIntent operation,
+ @Nullable WorkSource workSource) {
setImpl(type, triggerAtMillis, windowMillis, intervalMillis, 0, operation, null, null,
(Handler) null, workSource, null);
}
@@ -864,8 +935,8 @@ public class AlarmManager {
*/
@UnsupportedAppUsage
public void set(@AlarmType int type, long triggerAtMillis, long windowMillis,
- long intervalMillis, String tag, OnAlarmListener listener, Handler targetHandler,
- WorkSource workSource) {
+ long intervalMillis, @Nullable String tag, @NonNull OnAlarmListener listener,
+ @Nullable Handler targetHandler, @Nullable WorkSource workSource) {
setImpl(type, triggerAtMillis, windowMillis, intervalMillis, 0, null, listener, tag,
targetHandler, workSource, null);
}
@@ -896,13 +967,24 @@ public class AlarmManager {
* invoked via the specified target Handler, or on the application's main looper
* if {@code null} is passed as the {@code targetHandler} parameter.
*
+ * <p>The behavior of this API when {@code windowMillis < 0} is undefined.
+ *
+ * @deprecated Better alternative APIs exist for setting an alarm with this method:
+ * <ul>
+ * <li>For alarms with {@code windowMillis > 0}, use
+ * {@link #setWindow(int, long, long, String, Executor, WorkSource, OnAlarmListener)}</li>
+ * <li>For alarms with {@code windowMillis = 0}, use
+ * {@link #setExact(int, long, String, Executor, WorkSource, OnAlarmListener)}</li>
+ * </ul>
+ *
* @hide
*/
+ @Deprecated
@SystemApi
@RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS)
public void set(@AlarmType int type, long triggerAtMillis, long windowMillis,
- long intervalMillis, OnAlarmListener listener, Handler targetHandler,
- WorkSource workSource) {
+ long intervalMillis, @NonNull OnAlarmListener listener, @Nullable Handler targetHandler,
+ @Nullable WorkSource workSource) {
setImpl(type, triggerAtMillis, windowMillis, intervalMillis, 0, null, listener,
makeTag(triggerAtMillis, workSource), targetHandler, workSource, null);
}
@@ -916,11 +998,16 @@ public class AlarmManager {
* {@link #setExact(int, long, String, OnAlarmListener, Handler)} instead.
*
* <p>
- * Note that using this API requires you to hold
+ * Note that on previous Android versions {@link Build.VERSION_CODES#S} and
+ * {@link Build.VERSION_CODES#TIRAMISU}, using this API required you to hold
* {@link Manifest.permission#SCHEDULE_EXACT_ALARM}, unless you are on the system's power
* allowlist. This can be set, for example, by marking the app as {@code <allow-in-power-save>}
* within the system config.
*
+ * <p class="note"><strong>Note:</strong>
+ * Starting with android version {@link Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, the system will
+ * explicitly drop any alarms set via this API when the calling app goes out of lifecycle.
+ *
* @param type type of alarm
* @param triggerAtMillis The exact time in milliseconds, that the alarm should be delivered,
* expressed in the appropriate clock's units (depending on the alarm
@@ -937,9 +1024,7 @@ public class AlarmManager {
* @hide
*/
@SystemApi
- @RequiresPermission(allOf = {
- Manifest.permission.UPDATE_DEVICE_STATS,
- Manifest.permission.SCHEDULE_EXACT_ALARM}, conditional = true)
+ @RequiresPermission(Manifest.permission.UPDATE_DEVICE_STATS)
public void setExact(@AlarmType int type, long triggerAtMillis, @Nullable String tag,
@NonNull Executor executor, @NonNull WorkSource workSource,
@NonNull OnAlarmListener listener) {
@@ -1100,7 +1185,7 @@ public class AlarmManager {
* @see Intent#EXTRA_ALARM_COUNT
*/
public void setInexactRepeating(@AlarmType int type, long triggerAtMillis,
- long intervalMillis, PendingIntent operation) {
+ long intervalMillis, @NonNull PendingIntent operation) {
setImpl(type, triggerAtMillis, WINDOW_HEURISTIC, intervalMillis, 0, operation, null,
null, (Handler) null, null, null);
}
@@ -1150,7 +1235,7 @@ public class AlarmManager {
* @see #RTC_WAKEUP
*/
public void setAndAllowWhileIdle(@AlarmType int type, long triggerAtMillis,
- PendingIntent operation) {
+ @NonNull PendingIntent operation) {
setImpl(type, triggerAtMillis, WINDOW_HEURISTIC, 0, FLAG_ALLOW_WHILE_IDLE,
operation, null, null, (Handler) null, null, null);
}
@@ -1223,12 +1308,48 @@ public class AlarmManager {
*/
@RequiresPermission(value = Manifest.permission.SCHEDULE_EXACT_ALARM, conditional = true)
public void setExactAndAllowWhileIdle(@AlarmType int type, long triggerAtMillis,
- PendingIntent operation) {
+ @NonNull PendingIntent operation) {
setImpl(type, triggerAtMillis, WINDOW_EXACT, 0, FLAG_ALLOW_WHILE_IDLE, operation,
null, null, (Handler) null, null, null);
}
/**
+ * Like {@link #setExact(int, long, String, Executor, WorkSource, OnAlarmListener)}, but this
+ * alarm will be allowed to execute even when the system is in low-power idle modes.
+ *
+ * <p> See {@link #setExactAndAllowWhileIdle(int, long, PendingIntent)} for more details.
+ *
+ * <p class="note"><strong>Note:</strong>
+ * Starting with android version {@link Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, the system will
+ * explicitly drop any alarms set via this API when the calling app goes out of lifecycle.
+ *
+ * @param type type of alarm
+ * @param triggerAtMillis The exact time in milliseconds, that the alarm should be delivered,
+ * expressed in the appropriate clock's units (depending on the alarm
+ * type).
+ * @param listener {@link OnAlarmListener} instance whose
+ * {@link OnAlarmListener#onAlarm() onAlarm()} method will be called when
+ * the alarm time is reached.
+ * @param executor The {@link Executor} on which to execute the listener's onAlarm()
+ * callback.
+ * @param tag Optional. A string tag used to identify this alarm in logs and
+ * battery-attribution.
+ * @param workSource A {@link WorkSource} object to attribute this alarm to the app that
+ * requested this work.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(Manifest.permission.UPDATE_DEVICE_STATS)
+ public void setExactAndAllowWhileIdle(@AlarmType int type, long triggerAtMillis,
+ @Nullable String tag, @NonNull Executor executor, @Nullable WorkSource workSource,
+ @NonNull OnAlarmListener listener) {
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(listener);
+ setImpl(type, triggerAtMillis, WINDOW_EXACT, 0, FLAG_ALLOW_WHILE_IDLE, null, listener, tag,
+ executor, workSource, null);
+ }
+
+ /**
* Remove any alarms with a matching {@link Intent}.
* Any alarm, of any type, whose Intent matches this one (as defined by
* {@link Intent#filterEquals}), will be canceled.
@@ -1238,7 +1359,7 @@ public class AlarmManager {
*
* @see #set
*/
- public void cancel(PendingIntent operation) {
+ public void cancel(@NonNull PendingIntent operation) {
if (operation == null) {
final String msg = "cancel() called with a null PendingIntent";
if (mTargetSdkVersion >= Build.VERSION_CODES.N) {
@@ -1261,7 +1382,7 @@ public class AlarmManager {
*
* @param listener OnAlarmListener instance that is the target of a currently-set alarm.
*/
- public void cancel(OnAlarmListener listener) {
+ public void cancel(@NonNull OnAlarmListener listener) {
if (listener == null) {
throw new NullPointerException("cancel() called with a null OnAlarmListener");
}
@@ -1285,6 +1406,17 @@ public class AlarmManager {
}
/**
+ * Remove all alarms previously set by the caller, if any.
+ */
+ public void cancelAll() {
+ try {
+ mService.removeAll(mContext.getOpPackageName());
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Set the system wall clock time.
* Requires the permission android.permission.SET_TIME.
*
diff --git a/apex/jobscheduler/framework/java/android/app/IAlarmManager.aidl b/apex/jobscheduler/framework/java/android/app/IAlarmManager.aidl
index 9d11ca470397..a46e69796abd 100644
--- a/apex/jobscheduler/framework/java/android/app/IAlarmManager.aidl
+++ b/apex/jobscheduler/framework/java/android/app/IAlarmManager.aidl
@@ -37,10 +37,10 @@ interface IAlarmManager {
boolean setTime(long millis);
void setTimeZone(String zone);
void remove(in PendingIntent operation, in IAlarmListener listener);
+ void removeAll(String packageName);
long getNextWakeFromIdleTime();
@UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553)
AlarmManager.AlarmClockInfo getNextAlarmClock(int userId);
- long currentNetworkTimeMillis();
boolean canScheduleExactAlarms(String packageName);
boolean hasScheduleExactAlarm(String packageName, int userId);
int getConfigVersion();
diff --git a/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java b/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java
index 25d258c817b6..3cfddc6d8e2b 100644
--- a/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java
+++ b/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java
@@ -17,6 +17,7 @@
package android.app;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.app.job.IJobScheduler;
import android.app.job.IUserVisibleJobObserver;
@@ -24,10 +25,14 @@ import android.app.job.JobInfo;
import android.app.job.JobScheduler;
import android.app.job.JobSnapshot;
import android.app.job.JobWorkItem;
+import android.content.Context;
+import android.content.pm.ParceledListSlice;
import android.os.RemoteException;
+import android.util.ArrayMap;
import java.util.List;
-
+import java.util.Map;
+import java.util.Set;
/**
* Concrete implementation of the JobScheduler interface
@@ -35,19 +40,51 @@ import java.util.List;
* Note android.app.job is the better package to put this class, but we can't move it there
* because that'd break robolectric. Grr.
*
- * @hide
+ * @hide
*/
public class JobSchedulerImpl extends JobScheduler {
IJobScheduler mBinder;
+ private final Context mContext;
+ private final String mNamespace;
+
+ public JobSchedulerImpl(@NonNull Context context, IJobScheduler binder) {
+ this(context, binder, null);
+ }
- public JobSchedulerImpl(IJobScheduler binder) {
+ private JobSchedulerImpl(@NonNull Context context, IJobScheduler binder,
+ @Nullable String namespace) {
+ mContext = context;
mBinder = binder;
+ mNamespace = namespace;
+ }
+
+ private JobSchedulerImpl(JobSchedulerImpl jsi, @Nullable String namespace) {
+ this(jsi.mContext, jsi.mBinder, namespace);
+ }
+
+ @NonNull
+ @Override
+ public JobScheduler forNamespace(@NonNull String namespace) {
+ namespace = sanitizeNamespace(namespace);
+ if (namespace == null) {
+ throw new NullPointerException("namespace cannot be null");
+ }
+ if (namespace.isEmpty()) {
+ throw new IllegalArgumentException("namespace cannot be empty");
+ }
+ return new JobSchedulerImpl(this, namespace);
+ }
+
+ @Nullable
+ @Override
+ public String getNamespace() {
+ return mNamespace;
}
@Override
public int schedule(JobInfo job) {
try {
- return mBinder.schedule(job);
+ return mBinder.schedule(mNamespace, job);
} catch (RemoteException e) {
return JobScheduler.RESULT_FAILURE;
}
@@ -56,7 +93,7 @@ public class JobSchedulerImpl extends JobScheduler {
@Override
public int enqueue(JobInfo job, JobWorkItem work) {
try {
- return mBinder.enqueue(job, work);
+ return mBinder.enqueue(mNamespace, job, work);
} catch (RemoteException e) {
return JobScheduler.RESULT_FAILURE;
}
@@ -65,7 +102,7 @@ public class JobSchedulerImpl extends JobScheduler {
@Override
public int scheduleAsPackage(JobInfo job, String packageName, int userId, String tag) {
try {
- return mBinder.scheduleAsPackage(job, packageName, userId, tag);
+ return mBinder.scheduleAsPackage(mNamespace, job, packageName, userId, tag);
} catch (RemoteException e) {
return JobScheduler.RESULT_FAILURE;
}
@@ -74,23 +111,44 @@ public class JobSchedulerImpl extends JobScheduler {
@Override
public void cancel(int jobId) {
try {
- mBinder.cancel(jobId);
+ mBinder.cancel(mNamespace, jobId);
} catch (RemoteException e) {}
-
}
@Override
public void cancelAll() {
try {
- mBinder.cancelAll();
+ mBinder.cancelAllInNamespace(mNamespace);
} catch (RemoteException e) {}
+ }
+ @Override
+ public void cancelInAllNamespaces() {
+ try {
+ mBinder.cancelAll();
+ } catch (RemoteException e) {}
}
@Override
public List<JobInfo> getAllPendingJobs() {
try {
- return mBinder.getAllPendingJobs().getList();
+ return mBinder.getAllPendingJobsInNamespace(mNamespace).getList();
+ } catch (RemoteException e) {
+ return null;
+ }
+ }
+
+ @Override
+ public Map<String, List<JobInfo>> getPendingJobsInAllNamespaces() {
+ try {
+ final Map<String, ParceledListSlice<JobInfo>> parceledList =
+ mBinder.getAllPendingJobs();
+ final ArrayMap<String, List<JobInfo>> jobMap = new ArrayMap<>();
+ final Set<String> keys = parceledList.keySet();
+ for (String key : keys) {
+ jobMap.put(key, parceledList.get(key).getList());
+ }
+ return jobMap;
} catch (RemoteException e) {
return null;
}
@@ -99,13 +157,40 @@ public class JobSchedulerImpl extends JobScheduler {
@Override
public JobInfo getPendingJob(int jobId) {
try {
- return mBinder.getPendingJob(jobId);
+ return mBinder.getPendingJob(mNamespace, jobId);
} catch (RemoteException e) {
return null;
}
}
@Override
+ public int getPendingJobReason(int jobId) {
+ try {
+ return mBinder.getPendingJobReason(mNamespace, jobId);
+ } catch (RemoteException e) {
+ return PENDING_JOB_REASON_UNDEFINED;
+ }
+ }
+
+ @Override
+ public boolean canRunUserInitiatedJobs() {
+ try {
+ return mBinder.canRunUserInitiatedJobs(mContext.getOpPackageName());
+ } catch (RemoteException e) {
+ return false;
+ }
+ }
+
+ @Override
+ public boolean hasRunUserInitiatedJobsPermission(String packageName, int userId) {
+ try {
+ return mBinder.hasRunUserInitiatedJobsPermission(packageName, userId);
+ } catch (RemoteException e) {
+ return false;
+ }
+ }
+
+ @Override
public List<JobInfo> getStartedJobs() {
try {
return mBinder.getStartedJobs();
@@ -128,7 +213,10 @@ public class JobSchedulerImpl extends JobScheduler {
android.Manifest.permission.INTERACT_ACROSS_USERS_FULL})
@Override
public void registerUserVisibleJobObserver(@NonNull IUserVisibleJobObserver observer) {
- // TODO(255767350): implement
+ try {
+ mBinder.registerUserVisibleJobObserver(observer);
+ } catch (RemoteException e) {
+ }
}
@RequiresPermission(allOf = {
@@ -136,14 +224,21 @@ public class JobSchedulerImpl extends JobScheduler {
android.Manifest.permission.INTERACT_ACROSS_USERS_FULL})
@Override
public void unregisterUserVisibleJobObserver(@NonNull IUserVisibleJobObserver observer) {
- // TODO(255767350): implement
+ try {
+ mBinder.unregisterUserVisibleJobObserver(observer);
+ } catch (RemoteException e) {
+ }
}
@RequiresPermission(allOf = {
android.Manifest.permission.MANAGE_ACTIVITY_TASKS,
android.Manifest.permission.INTERACT_ACROSS_USERS_FULL})
@Override
- public void stopUserVisibleJobsForUser(@NonNull String packageName, int userId) {
- // TODO(255767350): implement
+ public void notePendingUserRequestedAppStop(@NonNull String packageName, int userId,
+ @Nullable String debugReason) {
+ try {
+ mBinder.notePendingUserRequestedAppStop(packageName, userId, debugReason);
+ } catch (RemoteException e) {
+ }
}
}
diff --git a/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl b/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl
index d281da037fde..96494ec28204 100644
--- a/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl
+++ b/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl
@@ -16,6 +16,7 @@
package android.app.job;
+import android.app.Notification;
import android.app.job.JobWorkItem;
/**
@@ -30,6 +31,25 @@ import android.app.job.JobWorkItem;
*/
interface IJobCallback {
/**
+ * Immediate callback to the system after sending a data transfer download progress request
+ * signal; used to quickly detect ANR.
+ *
+ * @param jobId Unique integer used to identify this job.
+ * @param workId Unique integer used to identify a specific work item.
+ * @param transferredBytes How much data has been downloaded, in bytes.
+ */
+ void acknowledgeGetTransferredDownloadBytesMessage(int jobId, int workId,
+ long transferredBytes);
+ /**
+ * Immediate callback to the system after sending a data transfer upload progress request
+ * signal; used to quickly detect ANR.
+ *
+ * @param jobId Unique integer used to identify this job.
+ * @param workId Unique integer used to identify a specific work item.
+ * @param transferredBytes How much data has been uploaded, in bytes.
+ */
+ void acknowledgeGetTransferredUploadBytesMessage(int jobId, int workId, long transferredBytes);
+ /**
* Immediate callback to the system after sending a start signal, used to quickly detect ANR.
*
* @param jobId Unique integer used to identify this job.
@@ -65,4 +85,37 @@ interface IJobCallback {
*/
@UnsupportedAppUsage
void jobFinished(int jobId, boolean reschedule);
+ /*
+ * Inform JobScheduler of a change in the estimated transfer payload.
+ *
+ * @param jobId Unique integer used to identify this job.
+ * @param item The particular JobWorkItem this progress is associated with, if any.
+ * @param downloadBytes How many bytes the app expects to download.
+ * @param uploadBytes How many bytes the app expects to upload.
+ */
+ void updateEstimatedNetworkBytes(int jobId, in JobWorkItem item,
+ long downloadBytes, long uploadBytes);
+ /*
+ * Update JobScheduler of how much data the job has successfully transferred.
+ *
+ * @param jobId Unique integer used to identify this job.
+ * @param item The particular JobWorkItem this progress is associated with, if any.
+ * @param transferredDownloadBytes The number of bytes that have successfully been downloaded.
+ * @param transferredUploadBytes The number of bytes that have successfully been uploaded.
+ */
+ void updateTransferredNetworkBytes(int jobId, in JobWorkItem item,
+ long transferredDownloadBytes, long transferredUploadBytes);
+ /**
+ * Provide JobScheduler with a notification to post and tie to this job's
+ * lifecycle.
+ * This is required for all user-initiated job and optional for other jobs.
+ *
+ * @param jobId Unique integer used to identify this job.
+ * @param notificationId The ID for this notification, as per
+ * {@link android.app.NotificationManager#notify(int, Notification)}.
+ * @param notification The notification to be displayed.
+ * @param jobEndNotificationPolicy The policy to apply to the notification when the job stops.
+ */
+ void setNotification(int jobId, int notificationId,
+ in Notification notification, int jobEndNotificationPolicy);
}
diff --git a/apex/jobscheduler/framework/java/android/app/job/IJobScheduler.aidl b/apex/jobscheduler/framework/java/android/app/job/IJobScheduler.aidl
index 3006f50e54fc..416a2d8c0002 100644
--- a/apex/jobscheduler/framework/java/android/app/job/IJobScheduler.aidl
+++ b/apex/jobscheduler/framework/java/android/app/job/IJobScheduler.aidl
@@ -16,23 +16,37 @@
package android.app.job;
+import android.app.job.IUserVisibleJobObserver;
import android.app.job.JobInfo;
import android.app.job.JobSnapshot;
import android.app.job.JobWorkItem;
import android.content.pm.ParceledListSlice;
+import java.util.Map;
/**
* IPC interface that supports the app-facing {@link #JobScheduler} api.
* {@hide}
*/
interface IJobScheduler {
- int schedule(in JobInfo job);
- int enqueue(in JobInfo job, in JobWorkItem work);
- int scheduleAsPackage(in JobInfo job, String packageName, int userId, String tag);
- void cancel(int jobId);
+ int schedule(String namespace, in JobInfo job);
+ int enqueue(String namespace, in JobInfo job, in JobWorkItem work);
+ int scheduleAsPackage(String namespace, in JobInfo job, String packageName, int userId, String tag);
+ void cancel(String namespace, int jobId);
void cancelAll();
- ParceledListSlice getAllPendingJobs();
- JobInfo getPendingJob(int jobId);
+ void cancelAllInNamespace(String namespace);
+ // Returns Map<String, ParceledListSlice>, where the keys are the namespaces.
+ Map<String, ParceledListSlice<JobInfo>> getAllPendingJobs();
+ ParceledListSlice<JobInfo> getAllPendingJobsInNamespace(String namespace);
+ JobInfo getPendingJob(String namespace, int jobId);
+ int getPendingJobReason(String namespace, int jobId);
+ boolean canRunUserInitiatedJobs(String packageName);
+ boolean hasRunUserInitiatedJobsPermission(String packageName, int userId);
List<JobInfo> getStartedJobs();
ParceledListSlice getAllJobSnapshots();
+ @EnforcePermission(allOf={"MANAGE_ACTIVITY_TASKS", "INTERACT_ACROSS_USERS_FULL"})
+ void registerUserVisibleJobObserver(in IUserVisibleJobObserver observer);
+ @EnforcePermission(allOf={"MANAGE_ACTIVITY_TASKS", "INTERACT_ACROSS_USERS_FULL"})
+ void unregisterUserVisibleJobObserver(in IUserVisibleJobObserver observer);
+ @EnforcePermission(allOf={"MANAGE_ACTIVITY_TASKS", "INTERACT_ACROSS_USERS_FULL"})
+ void notePendingUserRequestedAppStop(String packageName, int userId, String debugReason);
}
diff --git a/apex/jobscheduler/framework/java/android/app/job/IJobService.aidl b/apex/jobscheduler/framework/java/android/app/job/IJobService.aidl
index 22ad252b9639..f8dc3b01162c 100644
--- a/apex/jobscheduler/framework/java/android/app/job/IJobService.aidl
+++ b/apex/jobscheduler/framework/java/android/app/job/IJobService.aidl
@@ -17,6 +17,7 @@
package android.app.job;
import android.app.job.JobParameters;
+import android.app.job.JobWorkItem;
/**
* Interface that the framework uses to communicate with application code that implements a
@@ -31,4 +32,10 @@ oneway interface IJobService {
/** Stop execution of application's job. */
@UnsupportedAppUsage
void stopJob(in JobParameters jobParams);
+ /** Inform the job of a change in the network it should use. */
+ void onNetworkChanged(in JobParameters jobParams);
+ /** Update JS of how much data has been downloaded. */
+ void getTransferredDownloadBytes(in JobParameters jobParams, in JobWorkItem jobWorkItem);
+ /** Update JS of how much data has been uploaded. */
+ void getTransferredUploadBytes(in JobParameters jobParams, in JobWorkItem jobWorkItem);
}
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
index f49cdbf403f0..526e63cf7e29 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
@@ -30,8 +30,10 @@ import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
+import android.app.Notification;
import android.compat.Compatibility;
import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledAfter;
import android.compat.annotation.EnabledSince;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.ClipData;
@@ -97,6 +99,15 @@ public class JobInfo implements Parcelable {
@EnabledSince(targetSdkVersion = Build.VERSION_CODES.TIRAMISU)
public static final long THROW_ON_INVALID_PRIORITY_VALUE = 140852299L;
+ /**
+ * Require that estimated network bytes are nonnegative.
+ *
+ * @hide
+ */
+ @ChangeId
+ @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.TIRAMISU)
+ public static final long REJECT_NEGATIVE_NETWORK_ESTIMATES = 253665015L;
+
/** @hide */
@IntDef(prefix = { "NETWORK_TYPE_" }, value = {
NETWORK_TYPE_NONE,
@@ -386,6 +397,13 @@ public class JobInfo implements Parcelable {
public static final int FLAG_EXPEDITED = 1 << 4;
/**
+ * Whether it's a user initiated job or not.
+ *
+ * @hide
+ */
+ public static final int FLAG_USER_INITIATED = 1 << 5;
+
+ /**
* @hide
*/
public static final int CONSTRAINT_FLAG_CHARGING = 1 << 0;
@@ -414,6 +432,7 @@ public class JobInfo implements Parcelable {
@UnsupportedAppUsage
private final ComponentName service;
private final int constraintFlags;
+ private final int mPreferredConstraintFlags;
private final TriggerContentUri[] triggerContentUris;
private final long triggerContentUpdateDelay;
private final long triggerContentMaxDelay;
@@ -504,6 +523,30 @@ public class JobInfo implements Parcelable {
}
/**
+ * @hide
+ * @see JobInfo.Builder#setPrefersBatteryNotLow(boolean)
+ */
+ public boolean isPreferBatteryNotLow() {
+ return (mPreferredConstraintFlags & CONSTRAINT_FLAG_BATTERY_NOT_LOW) != 0;
+ }
+
+ /**
+ * @hide
+ * @see JobInfo.Builder#setPrefersCharging(boolean)
+ */
+ public boolean isPreferCharging() {
+ return (mPreferredConstraintFlags & CONSTRAINT_FLAG_CHARGING) != 0;
+ }
+
+ /**
+ * @hide
+ * @see JobInfo.Builder#setPrefersDeviceIdle(boolean)
+ */
+ public boolean isPreferDeviceIdle() {
+ return (mPreferredConstraintFlags & CONSTRAINT_FLAG_DEVICE_IDLE) != 0;
+ }
+
+ /**
* @see JobInfo.Builder#setRequiresCharging(boolean)
*/
public boolean isRequireCharging() {
@@ -539,6 +582,13 @@ public class JobInfo implements Parcelable {
}
/**
+ * @hide
+ */
+ public int getPreferredConstraintFlags() {
+ return mPreferredConstraintFlags;
+ }
+
+ /**
* Which content: URIs must change for the job to be scheduled. Returns null
* if there are none required.
* @see JobInfo.Builder#addTriggerContentUri(TriggerContentUri)
@@ -713,6 +763,13 @@ public class JobInfo implements Parcelable {
}
/**
+ * @see JobInfo.Builder#setUserInitiated(boolean)
+ */
+ public boolean isUserInitiated() {
+ return (flags & FLAG_USER_INITIATED) != 0;
+ }
+
+ /**
* @see JobInfo.Builder#setImportantWhileForeground(boolean)
*/
public boolean isImportantWhileForeground() {
@@ -775,6 +832,9 @@ public class JobInfo implements Parcelable {
if (constraintFlags != j.constraintFlags) {
return false;
}
+ if (mPreferredConstraintFlags != j.mPreferredConstraintFlags) {
+ return false;
+ }
if (!Arrays.equals(triggerContentUris, j.triggerContentUris)) {
return false;
}
@@ -855,6 +915,7 @@ public class JobInfo implements Parcelable {
hashCode = 31 * hashCode + service.hashCode();
}
hashCode = 31 * hashCode + constraintFlags;
+ hashCode = 31 * hashCode + mPreferredConstraintFlags;
if (triggerContentUris != null) {
hashCode = 31 * hashCode + Arrays.hashCode(triggerContentUris);
}
@@ -885,7 +946,8 @@ public class JobInfo implements Parcelable {
@SuppressWarnings("UnsafeParcelApi")
private JobInfo(Parcel in) {
jobId = in.readInt();
- extras = in.readPersistableBundle();
+ final PersistableBundle persistableExtras = in.readPersistableBundle();
+ extras = persistableExtras != null ? persistableExtras : PersistableBundle.EMPTY;
transientExtras = in.readBundle();
if (in.readInt() != 0) {
clipData = ClipData.CREATOR.createFromParcel(in);
@@ -896,6 +958,7 @@ public class JobInfo implements Parcelable {
}
service = in.readParcelable(null);
constraintFlags = in.readInt();
+ mPreferredConstraintFlags = in.readInt();
triggerContentUris = in.createTypedArray(TriggerContentUri.CREATOR);
triggerContentUpdateDelay = in.readLong();
triggerContentMaxDelay = in.readLong();
@@ -930,6 +993,7 @@ public class JobInfo implements Parcelable {
clipGrantFlags = b.mClipGrantFlags;
service = b.mJobService;
constraintFlags = b.mConstraintFlags;
+ mPreferredConstraintFlags = b.mPreferredConstraintFlags;
triggerContentUris = b.mTriggerContentUris != null
? b.mTriggerContentUris.toArray(new TriggerContentUri[b.mTriggerContentUris.size()])
: null;
@@ -973,6 +1037,7 @@ public class JobInfo implements Parcelable {
}
out.writeParcelable(service, flags);
out.writeInt(constraintFlags);
+ out.writeInt(mPreferredConstraintFlags);
out.writeTypedArray(triggerContentUris, flags);
out.writeLong(triggerContentUpdateDelay);
out.writeLong(triggerContentMaxDelay);
@@ -1120,6 +1185,7 @@ public class JobInfo implements Parcelable {
private int mFlags;
// Requirements.
private int mConstraintFlags;
+ private int mPreferredConstraintFlags;
private NetworkRequest mNetworkRequest;
private long mNetworkDownloadBytes = NETWORK_BYTES_UNKNOWN;
private long mNetworkUploadBytes = NETWORK_BYTES_UNKNOWN;
@@ -1173,6 +1239,7 @@ public class JobInfo implements Parcelable {
mBias = job.getBias();
mFlags = job.getFlags();
mConstraintFlags = job.getConstraintFlags();
+ mPreferredConstraintFlags = job.getPreferredConstraintFlags();
mNetworkRequest = job.getRequiredNetwork();
mNetworkDownloadBytes = job.getEstimatedNetworkDownloadBytes();
mNetworkUploadBytes = job.getEstimatedNetworkUploadBytes();
@@ -1212,6 +1279,9 @@ public class JobInfo implements Parcelable {
* in them all being treated the same. The priorities each have slightly different
* behaviors, as noted in their relevant javadoc.
*
+ * Starting in Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE},
+ * the priority will only affect sorting order within the job's namespace.
+ *
* <b>NOTE:</b> Setting all of your jobs to high priority will not be
* beneficial to your app and in fact may hurt its performance in the
* long run.
@@ -1312,6 +1382,12 @@ public class JobInfo implements Parcelable {
* Calling this method will override any requirements previously defined
* by {@link #setRequiredNetwork(NetworkRequest)}; you typically only
* want to call one of these methods.
+ *
+ * Starting in Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE},
+ * an app must hold the
+ * {@link android.Manifest.permission#ACCESS_NETWORK_STATE} permission to
+ * schedule a job that requires a network.
+ *
* <p class="note">
* When your job executes in
* {@link JobService#onStartJob(JobParameters)}, be sure to use the
@@ -1368,6 +1444,11 @@ public class JobInfo implements Parcelable {
* otherwise you'll use the default network which may not meet this
* constraint.
*
+ * Starting in Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE},
+ * an app must hold the
+ * {@link android.Manifest.permission#ACCESS_NETWORK_STATE} permission to
+ * schedule a job that requires a network.
+ *
* @param networkRequest The detailed description of the kind of network
* this job requires, or {@code null} if no specific kind of
* network is required. Defining a {@link NetworkSpecifier}
@@ -1431,6 +1512,7 @@ public class JobInfo implements Parcelable {
* @see JobInfo#getEstimatedNetworkUploadBytes()
* @see JobWorkItem#JobWorkItem(android.content.Intent, long, long)
*/
+ // TODO(b/255371817): update documentation to reflect how this data will be used
public Builder setEstimatedNetworkBytes(@BytesLong long downloadBytes,
@BytesLong long uploadBytes) {
mNetworkDownloadBytes = downloadBytes;
@@ -1472,10 +1554,105 @@ public class JobInfo implements Parcelable {
}
/**
- * Specify that to run this job, the device must be charging (or be a
+ * Specify that this job would prefer to be run when the device's battery is not low.
+ * This defaults to {@code false}.
+ *
+ * <p>The system may attempt to delay this job until the device's battery is not low,
+ * but may choose to run it even if the device's battery is low. JobScheduler will not stop
+ * this job if this constraint is no longer satisfied after the job has started running.
+ * If this job must only run when the device's battery is not low,
+ * use {@link #setRequiresBatteryNotLow(boolean)} instead.
+ *
+ * <p>
+ * Because it doesn't make sense for a constraint to be both preferred and required,
+ * calling both this and {@link #setRequiresBatteryNotLow(boolean)} with {@code true}
+ * will result in an {@link java.lang.IllegalArgumentException} when
+ * {@link android.app.job.JobInfo.Builder#build()} is called.
+ *
+ * @param prefersBatteryNotLow Pass {@code true} to prefer that the device's battery level
+ * not be low in order to run the job.
+ * @return This object for method chaining
+ * @see JobInfo#isPreferBatteryNotLow()
+ * @hide
+ */
+ @NonNull
+ public Builder setPrefersBatteryNotLow(boolean prefersBatteryNotLow) {
+ mPreferredConstraintFlags =
+ (mPreferredConstraintFlags & ~CONSTRAINT_FLAG_BATTERY_NOT_LOW)
+ | (prefersBatteryNotLow ? CONSTRAINT_FLAG_BATTERY_NOT_LOW : 0);
+ return this;
+ }
+
+ /**
+ * Specify that this job would prefer to be run when the device is charging (or be a
* non-battery-powered device connected to permanent power, such as Android TV
* devices). This defaults to {@code false}.
*
+ * <p>
+ * The system may attempt to delay this job until the device is charging, but may
+ * choose to run it even if the device is not charging. JobScheduler will not stop
+ * this job if this constraint is no longer satisfied after the job has started running.
+ * If this job must only run when the device is charging,
+ * use {@link #setRequiresCharging(boolean)} instead.
+ *
+ * <p>
+ * Because it doesn't make sense for a constraint to be both preferred and required,
+ * calling both this and {@link #setRequiresCharging(boolean)} with {@code true}
+ * will result in an {@link java.lang.IllegalArgumentException} when
+ * {@link android.app.job.JobInfo.Builder#build()} is called.
+ *
+ * @param prefersCharging Pass {@code true} to prefer that the device be
+ * charging in order to run the job.
+ * @return This object for method chaining
+ * @see JobInfo#isPreferCharging()
+ * @hide
+ */
+ @NonNull
+ public Builder setPrefersCharging(boolean prefersCharging) {
+ mPreferredConstraintFlags = (mPreferredConstraintFlags & ~CONSTRAINT_FLAG_CHARGING)
+ | (prefersCharging ? CONSTRAINT_FLAG_CHARGING : 0);
+ return this;
+ }
+
+ /**
+ * Specify that this job would prefer to be run when the device is not in active use.
+ * This defaults to {@code false}.
+ *
+ * <p>The system may attempt to delay this job until the device is not in active use,
+ * but may choose to run it even if the device is not idle. JobScheduler will not stop
+ * this job if this constraint is no longer satisfied after the job has started running.
+ * If this job must only run when the device is not in active use,
+ * use {@link #setRequiresDeviceIdle(boolean)} instead.
+ *
+ * <p>
+ * Because it doesn't make sense for a constraint to be both preferred and required,
+ * calling both this and {@link #setRequiresDeviceIdle(boolean)} with {@code true}
+ * will result in an {@link java.lang.IllegalArgumentException} when
+ * {@link android.app.job.JobInfo.Builder#build()} is called.
+ *
+ * <p class="note">Despite the similar naming, this job constraint is <em>not</em>
+ * related to the system's "device idle" or "doze" states. This constraint only
+ * determines whether a job is allowed to run while the device is directly in use.
+ *
+ * @param prefersDeviceIdle Pass {@code true} to prefer that the device not be in active
+ * use when running this job.
+ * @return This object for method chaining
+ * @see JobInfo#isRequireDeviceIdle()
+ * @hide
+ */
+ @NonNull
+ public Builder setPrefersDeviceIdle(boolean prefersDeviceIdle) {
+ mPreferredConstraintFlags = (mPreferredConstraintFlags & ~CONSTRAINT_FLAG_DEVICE_IDLE)
+ | (prefersDeviceIdle ? CONSTRAINT_FLAG_DEVICE_IDLE : 0);
+ return this;
+ }
+
+ /**
+ * Specify that to run this job, the device must be charging (or be a
+ * non-battery-powered device connected to permanent power, such as Android TV
+ * devices). This defaults to {@code false}. Setting this to {@code false} <b>DOES NOT</b>
+ * mean the job will only run when the device is not charging.
+ *
* <p class="note">For purposes of running jobs, a battery-powered device
* "charging" is not quite the same as simply being connected to power. If the
* device is so busy that the battery is draining despite a power connection, jobs
@@ -1497,7 +1674,9 @@ public class JobInfo implements Parcelable {
* Specify that to run this job, the device's battery level must not be low.
* This defaults to false. If true, the job will only run when the battery level
* is not low, which is generally the point where the user is given a "low battery"
- * warning.
+ * warning. Setting this to {@code false} <b>DOES NOT</b> mean the job will only run
+ * when the battery is low.
+ *
* @param batteryNotLow Whether or not the device's battery level must not be low.
* @see JobInfo#isRequireBatteryNotLow()
*/
@@ -1510,7 +1689,8 @@ public class JobInfo implements Parcelable {
/**
* When set {@code true}, ensure that this job will not run if the device is in active use.
* The default state is {@code false}: that is, the for the job to be runnable even when
- * someone is interacting with the device.
+ * someone is interacting with the device. Setting this to {@code false} <b>DOES NOT</b>
+ * mean the job will only run when the device is not idle.
*
* <p>This state is a loose definition provided by the system. In general, it means that
* the device is not currently being used interactively, and has not been in use for some
@@ -1742,6 +1922,7 @@ public class JobInfo implements Parcelable {
* <li>Bypass Doze, app standby, and battery saver network restrictions</li>
* <li>Be less likely to be killed than regular jobs</li>
* <li>Be subject to background location throttling</li>
+ * <li>Be exempt from delay to optimize job execution</li>
* </ol>
*
* <p>
@@ -1802,6 +1983,64 @@ public class JobInfo implements Parcelable {
}
/**
+ * Indicates that this job is being scheduled to fulfill an explicit user request.
+ * As such, user-initiated jobs can only be scheduled when the app is in the foreground
+ * or in a state where launching an activity is allowed, as defined
+ * <a href=
+ * "https://developer.android.com/guide/components/activities/background-starts#exceptions">
+ * here</a>. Attempting to schedule one outside of these conditions will throw a
+ * {@link SecurityException}.
+ *
+ * <p>
+ * This should <b>NOT</b> be used for automatic features.
+ *
+ * <p>
+ * All user-initiated jobs must have an associated notification, set via
+ * {@link JobService#setNotification(JobParameters, int, Notification, int)}, and will be
+ * shown in the Task Manager when running. These jobs cannot be rescheduled by the app
+ * if the user stops the job via system provided affordance (such as the Task Manager).
+ * Thus, it is best practice and recommended to provide action buttons in the
+ * associated notification to allow the user to stop the job gracefully
+ * and allow for rescheduling.
+ *
+ * <p>
+ * If the app doesn't hold the {@link android.Manifest.permission#RUN_USER_INITIATED_JOBS}
+ * permission when scheduling a user-initiated job, JobScheduler will throw a
+ * {@link SecurityException}.
+ *
+ * <p>
+ * In {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, user-initiated jobs can only
+ * be used for network data transfers. As such, they must specify a required network via
+ * {@link #setRequiredNetwork(NetworkRequest)} or {@link #setRequiredNetworkType(int)}.
+ *
+ * <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.
+ *
+ * @see JobInfo#isUserInitiated()
+ */
+ @RequiresPermission(android.Manifest.permission.RUN_USER_INITIATED_JOBS)
+ @NonNull
+ public Builder setUserInitiated(boolean userInitiated) {
+ if (userInitiated) {
+ mFlags |= FLAG_USER_INITIATED;
+ if (mPriority == PRIORITY_DEFAULT) {
+ // The default priority for UIJs is MAX, but only change this if .setPriority()
+ // hasn't been called yet.
+ mPriority = PRIORITY_MAX;
+ }
+ } else {
+ if (mPriority == PRIORITY_MAX && (mFlags & FLAG_USER_INITIATED) != 0) {
+ // Reset the priority for the job, but only change this if .setPriority()
+ // hasn't been called yet.
+ mPriority = PRIORITY_DEFAULT;
+ }
+ mFlags &= (~FLAG_USER_INITIATED);
+ }
+ return this;
+ }
+
+ /**
* Setting this to true indicates that this job is important while the scheduling app
* is in the foreground or on the temporary whitelist for background restrictions.
* This means that the system will relax doze restrictions on this job during this time.
@@ -1886,11 +2125,13 @@ public class JobInfo implements Parcelable {
* @return The job object to hand to the JobScheduler. This object is immutable.
*/
public JobInfo build() {
- return build(Compatibility.isChangeEnabled(DISALLOW_DEADLINES_FOR_PREFETCH_JOBS));
+ return build(Compatibility.isChangeEnabled(DISALLOW_DEADLINES_FOR_PREFETCH_JOBS),
+ Compatibility.isChangeEnabled(REJECT_NEGATIVE_NETWORK_ESTIMATES));
}
/** @hide */
- public JobInfo build(boolean disallowPrefetchDeadlines) {
+ public JobInfo build(boolean disallowPrefetchDeadlines,
+ boolean rejectNegativeNetworkEstimates) {
// 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) {
@@ -1899,7 +2140,7 @@ public class JobInfo implements Parcelable {
" setRequiresDeviceIdle is an error.");
}
JobInfo jobInfo = new JobInfo(this);
- jobInfo.enforceValidity(disallowPrefetchDeadlines);
+ jobInfo.enforceValidity(disallowPrefetchDeadlines, rejectNegativeNetworkEstimates);
return jobInfo;
}
@@ -1917,13 +2158,24 @@ public class JobInfo implements Parcelable {
/**
* @hide
*/
- public final void enforceValidity(boolean disallowPrefetchDeadlines) {
+ public final void enforceValidity(boolean disallowPrefetchDeadlines,
+ boolean rejectNegativeNetworkEstimates) {
// Check that network estimates require network type and are reasonable values.
if ((networkDownloadBytes > 0 || networkUploadBytes > 0 || minimumNetworkChunkBytes > 0)
&& networkRequest == null) {
throw new IllegalArgumentException(
"Can't provide estimated network usage without requiring a network");
}
+ if (networkRequest != null && rejectNegativeNetworkEstimates) {
+ if (networkUploadBytes != NETWORK_BYTES_UNKNOWN && networkUploadBytes < 0) {
+ throw new IllegalArgumentException(
+ "Invalid network upload bytes: " + networkUploadBytes);
+ }
+ if (networkDownloadBytes != NETWORK_BYTES_UNKNOWN && networkDownloadBytes < 0) {
+ throw new IllegalArgumentException(
+ "Invalid network download bytes: " + networkDownloadBytes);
+ }
+ }
final long estimatedTransfer;
if (networkUploadBytes == NETWORK_BYTES_UNKNOWN) {
estimatedTransfer = networkDownloadBytes;
@@ -1998,10 +2250,12 @@ public class JobInfo implements Parcelable {
}
final boolean isExpedited = (flags & FLAG_EXPEDITED) != 0;
+ final boolean isUserInitiated = (flags & FLAG_USER_INITIATED) != 0;
switch (mPriority) {
case PRIORITY_MAX:
- if (!isExpedited) {
- throw new IllegalArgumentException("Only expedited jobs can have max priority");
+ if (!(isExpedited || isUserInitiated)) {
+ throw new IllegalArgumentException(
+ "Only expedited or user-initiated jobs can have max priority");
}
break;
case PRIORITY_HIGH:
@@ -2030,6 +2284,9 @@ public class JobInfo implements Parcelable {
if (isPeriodic) {
throw new IllegalArgumentException("An expedited job cannot be periodic");
}
+ if (isUserInitiated) {
+ throw new IllegalArgumentException("An expedited job cannot be user-initiated");
+ }
if (mPriority != PRIORITY_MAX && mPriority != PRIORITY_HIGH) {
throw new IllegalArgumentException(
"An expedited job must be high or max priority. Don't use expedited jobs"
@@ -2045,6 +2302,62 @@ public class JobInfo implements Parcelable {
"Can't call addTriggerContentUri() on an expedited job");
}
}
+
+ if ((constraintFlags & mPreferredConstraintFlags) != 0) {
+ // Something is marked as both preferred and required. Try to give a clear exception
+ // reason.
+ if ((constraintFlags & CONSTRAINT_FLAG_BATTERY_NOT_LOW) != 0
+ && (mPreferredConstraintFlags & CONSTRAINT_FLAG_BATTERY_NOT_LOW) != 0) {
+ throw new IllegalArgumentException(
+ "battery-not-low constraint cannot be both preferred and required");
+ }
+ if ((constraintFlags & CONSTRAINT_FLAG_CHARGING) != 0
+ && (mPreferredConstraintFlags & CONSTRAINT_FLAG_CHARGING) != 0) {
+ throw new IllegalArgumentException(
+ "charging constraint cannot be both preferred and required");
+ }
+ if ((constraintFlags & CONSTRAINT_FLAG_DEVICE_IDLE) != 0
+ && (mPreferredConstraintFlags & CONSTRAINT_FLAG_DEVICE_IDLE) != 0) {
+ throw new IllegalArgumentException(
+ "device idle constraint cannot be both preferred and required");
+ }
+ // Couldn't figure out what the overlap was. Just use a generic message.
+ throw new IllegalArgumentException(
+ "constraints cannot be both preferred and required");
+ }
+
+ if (isUserInitiated) {
+ if (hasEarlyConstraint) {
+ throw new IllegalArgumentException("A user-initiated job cannot have a time delay");
+ }
+ if (hasLateConstraint) {
+ throw new IllegalArgumentException("A user-initiated job cannot have a deadline");
+ }
+ if (isPeriodic) {
+ throw new IllegalArgumentException("A user-initiated job cannot be periodic");
+ }
+ if ((flags & FLAG_PREFETCH) != 0) {
+ throw new IllegalArgumentException(
+ "A user-initiated job cannot also be a prefetch job");
+ }
+ if (mPriority != PRIORITY_MAX) {
+ throw new IllegalArgumentException("A user-initiated job must be max priority.");
+ }
+ if ((constraintFlags & CONSTRAINT_FLAG_DEVICE_IDLE) != 0
+ || (mPreferredConstraintFlags & CONSTRAINT_FLAG_DEVICE_IDLE) != 0) {
+ throw new IllegalArgumentException(
+ "A user-initiated job cannot have a device-idle constraint");
+ }
+ if (triggerContentUris != null && triggerContentUris.length > 0) {
+ throw new IllegalArgumentException(
+ "Can't call addTriggerContentUri() on a user-initiated job");
+ }
+ // UIDTs
+ if (networkRequest == null) {
+ throw new IllegalArgumentException(
+ "A user-initiated data transfer job must specify a valid network type");
+ }
+ }
}
/**
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobParameters.java b/apex/jobscheduler/framework/java/android/app/job/JobParameters.java
index ed72530d8608..bf4f9a83b99c 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobParameters.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobParameters.java
@@ -19,6 +19,7 @@ package android.app.job;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.TestApi;
import android.app.ActivityManager;
import android.app.usage.UsageStatsManager;
import android.compat.annotation.UnsupportedAppUsage;
@@ -98,6 +99,19 @@ public class JobParameters implements Parcelable {
*/
public static final int INTERNAL_STOP_REASON_SUCCESSFUL_FINISH =
JobProtoEnums.INTERNAL_STOP_REASON_SUCCESSFUL_FINISH; // 10.
+ /**
+ * The user stopped the job via some UI (eg. Task Manager).
+ * @hide
+ */
+ @TestApi
+ public static final int INTERNAL_STOP_REASON_USER_UI_STOP =
+ JobProtoEnums.INTERNAL_STOP_REASON_USER_UI_STOP; // 11.
+ /**
+ * The app didn't respond quickly enough from JobScheduler's perspective.
+ * @hide
+ */
+ public static final int INTERNAL_STOP_REASON_ANR =
+ JobProtoEnums.INTERNAL_STOP_REASON_ANR; // 12.
/**
* All the stop reason codes. This should be regarded as an immutable array at runtime.
@@ -121,6 +135,8 @@ public class JobParameters implements Parcelable {
INTERNAL_STOP_REASON_DATA_CLEARED,
INTERNAL_STOP_REASON_RTC_UPDATED,
INTERNAL_STOP_REASON_SUCCESSFUL_FINISH,
+ INTERNAL_STOP_REASON_USER_UI_STOP,
+ INTERNAL_STOP_REASON_ANR,
};
/**
@@ -141,6 +157,8 @@ public class JobParameters implements Parcelable {
case INTERNAL_STOP_REASON_DATA_CLEARED: return "data_cleared";
case INTERNAL_STOP_REASON_RTC_UPDATED: return "rtc_updated";
case INTERNAL_STOP_REASON_SUCCESSFUL_FINISH: return "successful_finish";
+ case INTERNAL_STOP_REASON_USER_UI_STOP: return "user_ui_stop";
+ case INTERNAL_STOP_REASON_ANR: return "anr";
default: return "unknown:" + reasonCode;
}
}
@@ -230,7 +248,7 @@ public class JobParameters implements Parcelable {
public static final int STOP_REASON_APP_STANDBY = 12;
/**
* The user stopped the job. This can happen either through force-stop, adb shell commands,
- * or uninstalling.
+ * uninstalling, or some other UI.
*/
public static final int STOP_REASON_USER = 13;
/** The system is doing some processing that requires stopping this job. */
@@ -269,6 +287,8 @@ public class JobParameters implements Parcelable {
@UnsupportedAppUsage
private final int jobId;
+ @Nullable
+ private final String mJobNamespace;
private final PersistableBundle extras;
private final Bundle transientExtras;
private final ClipData clipData;
@@ -277,18 +297,21 @@ public class JobParameters implements Parcelable {
private final IBinder callback;
private final boolean overrideDeadlineExpired;
private final boolean mIsExpedited;
+ private final boolean mIsUserInitiated;
private final Uri[] mTriggeredContentUris;
private final String[] mTriggeredContentAuthorities;
- private final Network network;
+ @Nullable
+ private Network mNetwork;
private int mStopReason = STOP_REASON_UNDEFINED;
private int mInternalStopReason = INTERNAL_STOP_REASON_UNKNOWN;
private String debugStopReason; // Human readable stop reason for debugging.
/** @hide */
- public JobParameters(IBinder callback, int jobId, PersistableBundle extras,
+ public JobParameters(IBinder callback, String namespace, int jobId, PersistableBundle extras,
Bundle transientExtras, ClipData clipData, int clipGrantFlags,
- boolean overrideDeadlineExpired, boolean isExpedited, Uri[] triggeredContentUris,
+ boolean overrideDeadlineExpired, boolean isExpedited,
+ boolean isUserInitiated, Uri[] triggeredContentUris,
String[] triggeredContentAuthorities, Network network) {
this.jobId = jobId;
this.extras = extras;
@@ -298,9 +321,11 @@ public class JobParameters implements Parcelable {
this.callback = callback;
this.overrideDeadlineExpired = overrideDeadlineExpired;
this.mIsExpedited = isExpedited;
+ this.mIsUserInitiated = isUserInitiated;
this.mTriggeredContentUris = triggeredContentUris;
this.mTriggeredContentAuthorities = triggeredContentAuthorities;
- this.network = network;
+ this.mNetwork = network;
+ this.mJobNamespace = namespace;
}
/**
@@ -311,6 +336,18 @@ public class JobParameters implements Parcelable {
}
/**
+ * Get the namespace this job was placed in.
+ *
+ * @see JobScheduler#forNamespace(String)
+ * @return The namespace this job was scheduled in. Will be {@code null} if there was no
+ * explicit namespace set and this job is therefore in the default namespace.
+ */
+ @Nullable
+ public String getJobNamespace() {
+ return mJobNamespace;
+ }
+
+ /**
* @return The reason {@link JobService#onStopJob(JobParameters)} was called on this job. Will
* be {@link #STOP_REASON_UNDEFINED} if {@link JobService#onStopJob(JobParameters)} has not
* yet been called.
@@ -384,6 +421,20 @@ public class JobParameters implements Parcelable {
}
/**
+ * @return Whether this job is running as a user-initiated job or not. A job is guaranteed to
+ * have all user-initiated job guarantees for the duration of the job execution if this returns
+ * {@code true}. This will return {@code false} if the job wasn't requested to run as a
+ * user-initiated job, or if it was requested to run as a user-initiated job but the app didn't
+ * meet any of the requirements at the time of execution, such as having the
+ * {@link android.Manifest.permission#RUN_USER_INITIATED_JOBS} permission.
+ *
+ * @see JobInfo.Builder#setUserInitiated(boolean)
+ */
+ public boolean isUserInitiatedJob() {
+ return mIsUserInitiated;
+ }
+
+ /**
* For jobs with {@link android.app.job.JobInfo.Builder#setOverrideDeadline(long)} set, this
* provides an easy way to tell whether the job is being executed due to the deadline
* expiring. Note: If the job is running because its deadline expired, it implies that its
@@ -430,6 +481,10 @@ public class JobParameters implements Parcelable {
* such as allowing a {@link JobInfo#NETWORK_TYPE_UNMETERED} job to run over
* a metered network when there is a surplus of metered data available.
*
+ * Starting in Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE},
+ * this will return {@code null} if the app does not hold the permissions specified in
+ * {@link JobInfo.Builder#setRequiredNetwork(NetworkRequest)}.
+ *
* @return the network that should be used to perform any network requests
* for this job, or {@code null} if this job didn't set any required
* network type or if the job executed when there was no available network to use.
@@ -437,7 +492,7 @@ public class JobParameters implements Parcelable {
* @see JobInfo.Builder#setRequiredNetwork(NetworkRequest)
*/
public @Nullable Network getNetwork() {
- return network;
+ return mNetwork;
}
/**
@@ -515,6 +570,7 @@ public class JobParameters implements Parcelable {
private JobParameters(Parcel in) {
jobId = in.readInt();
+ mJobNamespace = in.readString();
extras = in.readPersistableBundle();
transientExtras = in.readBundle();
if (in.readInt() != 0) {
@@ -527,12 +583,13 @@ public class JobParameters implements Parcelable {
callback = in.readStrongBinder();
overrideDeadlineExpired = in.readInt() == 1;
mIsExpedited = in.readBoolean();
+ mIsUserInitiated = in.readBoolean();
mTriggeredContentUris = in.createTypedArray(Uri.CREATOR);
mTriggeredContentAuthorities = in.createStringArray();
if (in.readInt() != 0) {
- network = Network.CREATOR.createFromParcel(in);
+ mNetwork = Network.CREATOR.createFromParcel(in);
} else {
- network = null;
+ mNetwork = null;
}
mStopReason = in.readInt();
mInternalStopReason = in.readInt();
@@ -540,6 +597,11 @@ public class JobParameters implements Parcelable {
}
/** @hide */
+ public void setNetwork(@Nullable Network network) {
+ mNetwork = network;
+ }
+
+ /** @hide */
public void setStopReason(@StopReason int reason, int internalStopReason,
String debugStopReason) {
mStopReason = reason;
@@ -555,6 +617,7 @@ public class JobParameters implements Parcelable {
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(jobId);
+ dest.writeString(mJobNamespace);
dest.writePersistableBundle(extras);
dest.writeBundle(transientExtras);
if (clipData != null) {
@@ -567,11 +630,12 @@ public class JobParameters implements Parcelable {
dest.writeStrongBinder(callback);
dest.writeInt(overrideDeadlineExpired ? 1 : 0);
dest.writeBoolean(mIsExpedited);
+ dest.writeBoolean(mIsUserInitiated);
dest.writeTypedArray(mTriggeredContentUris, flags);
dest.writeStringArray(mTriggeredContentAuthorities);
- if (network != null) {
+ if (mNetwork != null) {
dest.writeInt(1);
- network.writeToParcel(dest, flags);
+ mNetwork.writeToParcel(dest, flags);
} else {
dest.writeInt(0);
}
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
index acbf2c49628c..d59d430e0b78 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
@@ -22,14 +22,23 @@ import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.annotation.SystemService;
+import android.annotation.UserIdInt;
+import android.app.ActivityManager;
+import android.app.usage.UsageStatsManager;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledAfter;
import android.content.ClipData;
import android.content.Context;
+import android.content.pm.PackageManager;
+import android.net.NetworkRequest;
+import android.os.Build;
import android.os.Bundle;
import android.os.PersistableBundle;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;
+import java.util.Map;
/**
* This is an API for scheduling various types of jobs against the framework that will be executed
@@ -45,8 +54,23 @@ import java.util.List;
* </p>
* <p>
* The framework will be intelligent about when it executes jobs, and attempt to batch
- * and defer them as much as possible. Typically if you don't specify a deadline on a job, it
+ * and defer them as much as possible. Typically, if you don't specify a deadline on a job, it
* can be run at any moment depending on the current state of the JobScheduler's internal queue.
+ * </p>
+ * <p>
+ * Starting in Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE},
+ * JobScheduler may try to optimize job execution by shifting execution to times with more available
+ * system resources in order to lower user impact. Factors in system health include sufficient
+ * battery, idle, charging, and access to an un-metered network. Jobs will initially be treated as
+ * if they have all these requirements, but as their deadlines approach, restrictions will become
+ * less strict. Requested requirements will not be affected by this change.
+ * </p>
+ *
+ * {@see android.app.job.JobInfo.Builder#setRequiresBatteryNotLow(boolean)}
+ * {@see android.app.job.JobInfo.Builder#setRequiresDeviceIdle(boolean)}
+ * {@see android.app.job.JobInfo.Builder#setRequiresCharging(boolean)}
+ * {@see android.app.job.JobInfo.Builder#setRequiredNetworkType(int)}
+ *
* <p>
* While a job is running, the system holds a wakelock on behalf of your app. For this reason,
* you do not need to take any action to guarantee that the device stays awake for the
@@ -78,6 +102,16 @@ import java.util.List;
*/
@SystemService(Context.JOB_SCHEDULER_SERVICE)
public abstract class JobScheduler {
+ /**
+ * Whether to throw an exception when an app doesn't properly implement all the necessary
+ * data transfer APIs.
+ *
+ * @hide
+ */
+ @ChangeId
+ @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.TIRAMISU)
+ public static final long THROW_ON_INVALID_DATA_TRANSFER_IMPLEMENTATION = 255371817L;
+
/** @hide */
@IntDef(prefix = { "RESULT_" }, value = {
RESULT_FAILURE,
@@ -104,6 +138,167 @@ public abstract class JobScheduler {
*/
public static final int RESULT_SUCCESS = 1;
+ /** The job doesn't exist. */
+ public static final int PENDING_JOB_REASON_INVALID_JOB_ID = -2;
+ /** The job is currently running and is therefore not pending. */
+ public static final int PENDING_JOB_REASON_EXECUTING = -1;
+ /**
+ * There is no known reason why the job is pending.
+ * If additional reasons are added on newer Android versions, the system may return this reason
+ * to apps whose target SDK is not high enough to expect that reason.
+ */
+ public static final int PENDING_JOB_REASON_UNDEFINED = 0;
+ /**
+ * The app is in a state that prevents the job from running
+ * (eg. the {@link JobService} component is disabled).
+ */
+ public static final int PENDING_JOB_REASON_APP = 1;
+ /**
+ * The current standby bucket prevents the job from running.
+ *
+ * @see UsageStatsManager#STANDBY_BUCKET_RESTRICTED
+ */
+ public static final int PENDING_JOB_REASON_APP_STANDBY = 2;
+ /**
+ * The app is restricted from running in the background.
+ *
+ * @see ActivityManager#isBackgroundRestricted()
+ * @see PackageManager#isInstantApp()
+ */
+ public static final int PENDING_JOB_REASON_BACKGROUND_RESTRICTION = 3;
+ /**
+ * The requested battery-not-low constraint is not satisfied.
+ *
+ * @see JobInfo.Builder#setRequiresBatteryNotLow(boolean)
+ */
+ public static final int PENDING_JOB_REASON_CONSTRAINT_BATTERY_NOT_LOW = 4;
+ /**
+ * The requested charging constraint is not satisfied.
+ *
+ * @see JobInfo.Builder#setRequiresCharging(boolean)
+ */
+ public static final int PENDING_JOB_REASON_CONSTRAINT_CHARGING = 5;
+ /**
+ * The requested connectivity constraint is not satisfied.
+ *
+ * @see JobInfo.Builder#setRequiredNetwork(NetworkRequest)
+ * @see JobInfo.Builder#setRequiredNetworkType(int)
+ */
+ public static final int PENDING_JOB_REASON_CONSTRAINT_CONNECTIVITY = 6;
+ /**
+ * The requested content trigger constraint is not satisfied.
+ *
+ * @see JobInfo.Builder#addTriggerContentUri(JobInfo.TriggerContentUri)
+ */
+ public static final int PENDING_JOB_REASON_CONSTRAINT_CONTENT_TRIGGER = 7;
+ /**
+ * The requested idle constraint is not satisfied.
+ *
+ * @see JobInfo.Builder#setRequiresDeviceIdle(boolean)
+ */
+ public static final int PENDING_JOB_REASON_CONSTRAINT_DEVICE_IDLE = 8;
+ /**
+ * The minimum latency has not transpired.
+ *
+ * @see JobInfo.Builder#setMinimumLatency(long)
+ */
+ public static final int PENDING_JOB_REASON_CONSTRAINT_MINIMUM_LATENCY = 9;
+ /**
+ * The system's estimate of when the app will be launched is far away enough to warrant delaying
+ * this job.
+ *
+ * @see JobInfo#isPrefetch()
+ * @see JobInfo.Builder#setPrefetch(boolean)
+ */
+ public static final int PENDING_JOB_REASON_CONSTRAINT_PREFETCH = 10;
+ /**
+ * The requested storage-not-low constraint is not satisfied.
+ *
+ * @see JobInfo.Builder#setRequiresStorageNotLow(boolean)
+ */
+ public static final int PENDING_JOB_REASON_CONSTRAINT_STORAGE_NOT_LOW = 11;
+ /**
+ * The job is being deferred due to the device state (eg. Doze, battery saver, memory usage,
+ * thermal status, etc.).
+ */
+ public static final int PENDING_JOB_REASON_DEVICE_STATE = 12;
+ /**
+ * JobScheduler thinks it can defer this job to a more optimal running time.
+ */
+ public static final int PENDING_JOB_REASON_JOB_SCHEDULER_OPTIMIZATION = 13;
+ /**
+ * The app has consumed all of its current quota.
+ *
+ * @see UsageStatsManager#getAppStandbyBucket()
+ * @see JobParameters#STOP_REASON_QUOTA
+ */
+ public static final int PENDING_JOB_REASON_QUOTA = 14;
+ /**
+ * JobScheduler is respecting one of the user's actions (eg. force stop or adb shell commands)
+ * to defer this job.
+ */
+ public static final int PENDING_JOB_REASON_USER = 15;
+
+ /** @hide */
+ @IntDef(prefix = {"PENDING_JOB_REASON_"}, value = {
+ PENDING_JOB_REASON_UNDEFINED,
+ PENDING_JOB_REASON_APP,
+ PENDING_JOB_REASON_APP_STANDBY,
+ PENDING_JOB_REASON_BACKGROUND_RESTRICTION,
+ PENDING_JOB_REASON_CONSTRAINT_BATTERY_NOT_LOW,
+ PENDING_JOB_REASON_CONSTRAINT_CHARGING,
+ PENDING_JOB_REASON_CONSTRAINT_CONNECTIVITY,
+ PENDING_JOB_REASON_CONSTRAINT_CONTENT_TRIGGER,
+ PENDING_JOB_REASON_CONSTRAINT_DEVICE_IDLE,
+ PENDING_JOB_REASON_CONSTRAINT_MINIMUM_LATENCY,
+ PENDING_JOB_REASON_CONSTRAINT_PREFETCH,
+ PENDING_JOB_REASON_CONSTRAINT_STORAGE_NOT_LOW,
+ PENDING_JOB_REASON_DEVICE_STATE,
+ PENDING_JOB_REASON_EXECUTING,
+ PENDING_JOB_REASON_INVALID_JOB_ID,
+ PENDING_JOB_REASON_JOB_SCHEDULER_OPTIMIZATION,
+ PENDING_JOB_REASON_QUOTA,
+ PENDING_JOB_REASON_USER,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface PendingJobReason {
+ }
+
+ /**
+ * Get a JobScheduler instance that is dedicated to a specific namespace. Any API calls using
+ * this instance will interact with jobs in that namespace, unless the API documentation says
+ * otherwise. Attempting to update a job scheduled in another namespace will not be possible
+ * but will instead create or update the job inside the current namespace. A JobScheduler
+ * instance dedicated to a namespace must be used to schedule or update jobs in that namespace.
+ *
+ * <p class="note">Since leading and trailing whitespace can lead to hard-to-debug issues,
+ * they will be {@link String#trim() trimmed}. An empty String (after trimming) is not allowed.
+ * @see #getNamespace()
+ */
+ @NonNull
+ public JobScheduler forNamespace(@NonNull String namespace) {
+ throw new RuntimeException("Not implemented. Must override in a subclass.");
+ }
+
+ /**
+ * Get the namespace this JobScheduler instance is operating in. A {@code null} value means
+ * that the app has not specified a namespace for this instance, and it is therefore using the
+ * default namespace.
+ */
+ @Nullable
+ public String getNamespace() {
+ throw new RuntimeException("Not implemented. Must override in a subclass.");
+ }
+
+ /** @hide */
+ @Nullable
+ public static String sanitizeNamespace(@Nullable String namespace) {
+ if (namespace == null) {
+ return null;
+ }
+ return namespace.trim().intern();
+ }
+
/**
* Schedule a job to be executed. Will replace any currently scheduled job with the same
* ID with the new information in the {@link JobInfo}. If a job with the given ID is currently
@@ -152,7 +347,7 @@ public abstract class JobScheduler {
* but there are situations where it may get this wrong and count the JobInfo as changing.
* (That said, you should be relatively safe with a simple set of consistent data in these
* fields.) You should never use {@link JobInfo.Builder#setClipData(ClipData, int)} with
- * work you are enqueue, since currently this will always be treated as a different JobInfo,
+ * work you are enqueuing, since currently this will always be treated as a different JobInfo,
* even if the ClipData contents are exactly the same.</p>
*
* <p class="caution"><strong>Note:</strong> Scheduling a job can have a high cost, even if it's
@@ -160,6 +355,16 @@ public abstract class JobScheduler {
* version {@link android.os.Build.VERSION_CODES#Q}. As such, the system may throttle calls to
* this API if calls are made too frequently in a short amount of time.
*
+ * <p class="caution"><strong>Note:</strong> Prior to Android version
+ * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, JobWorkItems could not be persisted.
+ * Apps were not allowed to enqueue JobWorkItems with persisted jobs and the system would throw
+ * an {@link IllegalArgumentException} if they attempted to do so. Starting with
+ * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE},
+ * JobWorkItems can be persisted alongside the hosting job.
+ * However, Intents cannot be persisted. Set a {@link PersistableBundle} using
+ * {@link JobWorkItem.Builder#setExtras(PersistableBundle)} for any information that needs
+ * to be persisted.
+ *
* <p>Note: The JobService component needs to be enabled in order to successfully schedule a
* job.
*
@@ -200,11 +405,27 @@ public abstract class JobScheduler {
public abstract void cancel(int jobId);
/**
- * Cancel <em>all</em> jobs that have been scheduled by the calling application.
+ * Cancel all jobs that have been scheduled in the current namespace by the
+ * calling application.
+ *
+ * <p>
+ * Starting with Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, this
+ * will only cancel within the current namespace. If a namespace hasn't been explicitly set
+ * with {@link #forNamespace(String)}, then this will cancel jobs in the default namespace.
+ * To cancel all jobs scheduled by the application,
+ * use {@link #cancelInAllNamespaces()} instead.
*/
public abstract void cancelAll();
/**
+ * Cancel <em>all</em> jobs that have been scheduled by the calling application, regardless of
+ * namespace.
+ */
+ public void cancelInAllNamespaces() {
+ throw new RuntimeException("Not implemented. Must override in a subclass.");
+ }
+
+ /**
* Retrieve all jobs that have been scheduled by the calling application.
*
* @return a list of all of the app's scheduled jobs. This includes jobs that are
@@ -213,6 +434,20 @@ public abstract class JobScheduler {
public abstract @NonNull List<JobInfo> getAllPendingJobs();
/**
+ * Retrieve all jobs that have been scheduled by the calling application within the current
+ * namespace.
+ *
+ * @return a list of all of the app's scheduled jobs scheduled with the current namespace.
+ * If a namespace hasn't been explicitly set with {@link #forNamespace(String)},
+ * then this will return jobs in the default namespace.
+ * This includes jobs that are currently started as well as those that are still waiting to run.
+ */
+ @NonNull
+ public Map<String, List<JobInfo>> getPendingJobsInAllNamespaces() {
+ throw new RuntimeException("Not implemented. Must override in a subclass.");
+ }
+
+ /**
* Look up the description of a scheduled job.
*
* @return The {@link JobInfo} description of the given scheduled job, or {@code null}
@@ -221,6 +456,37 @@ public abstract class JobScheduler {
public abstract @Nullable JobInfo getPendingJob(int jobId);
/**
+ * Returns a reason why the job is pending and not currently executing. If there are multiple
+ * reasons why a job may be pending, this will only return one of them.
+ */
+ @PendingJobReason
+ public int getPendingJobReason(int jobId) {
+ return PENDING_JOB_REASON_UNDEFINED;
+ }
+
+ /**
+ * Returns {@code true} if the calling app currently holds the
+ * {@link android.Manifest.permission#RUN_USER_INITIATED_JOBS} permission, allowing it to run
+ * user-initiated jobs.
+ */
+ public boolean canRunUserInitiatedJobs() {
+ return false;
+ }
+
+ /**
+ * Returns {@code true} if the app currently holds the
+ * {@link android.Manifest.permission#RUN_USER_INITIATED_JOBS} permission, allowing it to run
+ * user-initiated jobs.
+ * @hide
+ * TODO(255371817): consider exposing this to apps who could call
+ * {@link #scheduleAsPackage(JobInfo, String, int, String)}
+ */
+ public boolean hasRunUserInitiatedJobsPermission(@NonNull String packageName,
+ @UserIdInt int userId) {
+ return false;
+ }
+
+ /**
* <b>For internal system callers only!</b>
* Returns a list of all currently-executing jobs.
* @hide
@@ -264,5 +530,6 @@ public abstract class JobScheduler {
android.Manifest.permission.MANAGE_ACTIVITY_TASKS,
android.Manifest.permission.INTERACT_ACROSS_USERS_FULL})
@SuppressWarnings("HiddenAbstractMethod")
- public abstract void stopUserVisibleJobsForUser(@NonNull String packageName, int userId);
+ public abstract void notePendingUserRequestedAppStop(@NonNull String packageName, int userId,
+ @Nullable String debugReason);
}
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobSchedulerFrameworkInitializer.java b/apex/jobscheduler/framework/java/android/app/job/JobSchedulerFrameworkInitializer.java
index 7b287d5f9d15..36174c6aad3d 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobSchedulerFrameworkInitializer.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobSchedulerFrameworkInitializer.java
@@ -20,6 +20,7 @@ import android.annotation.SystemApi;
import android.app.JobSchedulerImpl;
import android.app.SystemServiceRegistry;
import android.app.tare.EconomyManager;
+import android.app.tare.IEconomyManager;
import android.content.Context;
import android.os.DeviceIdleManager;
import android.os.IDeviceIdleController;
@@ -44,9 +45,9 @@ public class JobSchedulerFrameworkInitializer {
* <p>If this is called from other places, it throws a {@link IllegalStateException).
*/
public static void registerServiceWrappers() {
- SystemServiceRegistry.registerStaticService(
+ SystemServiceRegistry.registerContextAwareService(
Context.JOB_SCHEDULER_SERVICE, JobScheduler.class,
- (b) -> new JobSchedulerImpl(IJobScheduler.Stub.asInterface(b)));
+ (context, b) -> new JobSchedulerImpl(context, IJobScheduler.Stub.asInterface(b)));
SystemServiceRegistry.registerContextAwareService(
Context.DEVICE_IDLE_CONTROLLER, DeviceIdleManager.class,
(context, b) -> new DeviceIdleManager(
@@ -58,6 +59,7 @@ public class JobSchedulerFrameworkInitializer {
Context.POWER_EXEMPTION_SERVICE, PowerExemptionManager.class,
PowerExemptionManager::new);
SystemServiceRegistry.registerStaticService(
- Context.RESOURCE_ECONOMY_SERVICE, EconomyManager.class, EconomyManager::new);
+ Context.RESOURCE_ECONOMY_SERVICE, EconomyManager.class,
+ (b) -> new EconomyManager(IEconomyManager.Stub.asInterface(b)));
}
}
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobService.java b/apex/jobscheduler/framework/java/android/app/job/JobService.java
index d184d44239ed..3b5f11bea6c8 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobService.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobService.java
@@ -16,9 +16,21 @@
package android.app.job;
+import static android.app.job.JobScheduler.THROW_ON_INVALID_DATA_TRANSFER_IMPLEMENTATION;
+
+import android.annotation.BytesLong;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Notification;
import android.app.Service;
+import android.compat.Compatibility;
import android.content.Intent;
import android.os.IBinder;
+import android.util.Log;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
/**
* <p>Entry point for the callback from the {@link android.app.job.JobScheduler}.</p>
@@ -57,6 +69,28 @@ public abstract class JobService extends Service {
public static final String PERMISSION_BIND =
"android.permission.BIND_JOB_SERVICE";
+ /**
+ * Detach the notification supplied to
+ * {@link #setNotification(JobParameters, int, Notification, int)} when the job ends.
+ * The notification will remain shown even after JobScheduler stops the job.
+ */
+ public static final int JOB_END_NOTIFICATION_POLICY_DETACH = 0;
+ /**
+ * Cancel and remove the notification supplied to
+ * {@link #setNotification(JobParameters, int, Notification, int)} when the job ends.
+ * The notification will be removed from the notification shade.
+ */
+ public static final int JOB_END_NOTIFICATION_POLICY_REMOVE = 1;
+
+ /** @hide */
+ @IntDef(prefix = {"JOB_END_NOTIFICATION_POLICY_"}, value = {
+ JOB_END_NOTIFICATION_POLICY_DETACH,
+ JOB_END_NOTIFICATION_POLICY_REMOVE,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface JobEndNotificationPolicy {
+ }
+
private JobServiceEngine mEngine;
/** @hide */
@@ -72,6 +106,33 @@ public abstract class JobService extends Service {
public boolean onStopJob(JobParameters params) {
return JobService.this.onStopJob(params);
}
+
+ @Override
+ @BytesLong
+ public long getTransferredDownloadBytes(@NonNull JobParameters params,
+ @Nullable JobWorkItem item) {
+ if (item == null) {
+ return JobService.this.getTransferredDownloadBytes(params);
+ } else {
+ return JobService.this.getTransferredDownloadBytes(params, item);
+ }
+ }
+
+ @Override
+ @BytesLong
+ public long getTransferredUploadBytes(@NonNull JobParameters params,
+ @Nullable JobWorkItem item) {
+ if (item == null) {
+ return JobService.this.getTransferredUploadBytes(params);
+ } else {
+ return JobService.this.getTransferredUploadBytes(params, item);
+ }
+ }
+
+ @Override
+ public void onNetworkChanged(@NonNull JobParameters params) {
+ JobService.this.onNetworkChanged(params);
+ }
};
}
return mEngine.getBinder();
@@ -95,11 +156,18 @@ public abstract class JobService extends Service {
* a future idle maintenance window.
* </p>
*
+ * <p class="note">
+ * Any {@link JobInfo.Builder#setUserInitiated(boolean) user-initiated job}
+ * cannot be rescheduled when the user has asked to stop the app
+ * via a system provided affordance (such as the Task Manager).
+ * In such situations, the value of {@code wantsReschedule} is always treated as {@code false}.
+ *
* @param params The parameters identifying this job, as supplied to
* the job in the {@link #onStartJob(JobParameters)} callback.
* @param wantsReschedule {@code true} if this job should be rescheduled according
* to the back-off criteria specified when it was first scheduled; {@code false}
- * otherwise.
+ * otherwise. When {@code false} is returned for a periodic job,
+ * the job will be rescheduled according to its periodic policy.
*/
public final void jobFinished(JobParameters params, boolean wantsReschedule) {
mEngine.jobFinished(params, wantsReschedule);
@@ -150,7 +218,7 @@ public abstract class JobService extends Service {
* {@link android.app.job.JobInfo.Builder#setRequiredNetworkType(int)}, yet while your
* job was executing the user toggled WiFi. Another example is if you had specified
* {@link android.app.job.JobInfo.Builder#setRequiresDeviceIdle(boolean)}, and the phone left
- * its idle maintenance window. There are many other reasons a job can be stopped early besides
+ * its idle state. There are many other reasons a job can be stopped early besides
* constraints no longer being satisfied. {@link JobParameters#getStopReason()} will return the
* reason this method was called. You are solely responsible for the behavior of your
* application upon receipt of this message; your app will likely start to misbehave if you
@@ -159,6 +227,12 @@ public abstract class JobService extends Service {
* Once this method returns (or times out), the system releases the wakelock that it is holding
* on behalf of the job.</p>
*
+ * <p class="note">
+ * Any {@link JobInfo.Builder#setUserInitiated(boolean) user-initiated job}
+ * cannot be rescheduled when stopped by the user via a system provided affordance (such as
+ * the Task Manager). In such situations, the returned value from this method call is always
+ * treated as {@code false}.
+ *
* <p class="caution"><strong>Note:</strong> When a job is stopped and rescheduled via this
* method call, the deadline constraint is excluded from the rescheduled job's constraint set.
* The rescheduled job will run again once all remaining constraints are satisfied.
@@ -168,7 +242,220 @@ public abstract class JobService extends Service {
* included.
* @return {@code true} to indicate to the JobManager whether you'd like to reschedule
* this job based on the retry criteria provided at job creation-time; or {@code false}
- * to end the job entirely. Regardless of the value returned, your job must stop executing.
+ * to end the job entirely (or, for a periodic job, to reschedule it according to its
+ * requested periodic criteria). Regardless of the value returned, your job must stop executing.
*/
public abstract boolean onStopJob(JobParameters params);
+
+ /**
+ * This method is called that for a job that has a network constraint when the network
+ * to be used by the job changes. The new network object will be available via
+ * {@link JobParameters#getNetwork()}. Any network that results in this method call will
+ * match the job's requested network constraints.
+ *
+ * <p>
+ * For example, if a device is on a metered mobile network and then connects to an
+ * unmetered WiFi network, and the job has indicated that both networks satisfy its
+ * network constraint, then this method will be called to notify the job of the new
+ * unmetered WiFi network.
+ *
+ * @param params The parameters identifying this job, similar to what was supplied to the job in
+ * the {@link #onStartJob(JobParameters)} callback, but with an updated network.
+ * @see JobInfo.Builder#setRequiredNetwork(android.net.NetworkRequest)
+ * @see JobInfo.Builder#setRequiredNetworkType(int)
+ */
+ public void onNetworkChanged(@NonNull JobParameters params) {
+ Log.w(TAG, "onNetworkChanged() not implemented in " + getClass().getName()
+ + ". Must override in a subclass.");
+ }
+
+ /**
+ * Update the amount of data this job is estimated to transfer after the job has started.
+ *
+ * @see JobInfo.Builder#setEstimatedNetworkBytes(long, long)
+ */
+ public final void updateEstimatedNetworkBytes(@NonNull JobParameters params,
+ @BytesLong long downloadBytes, @BytesLong long uploadBytes) {
+ mEngine.updateEstimatedNetworkBytes(params, null, downloadBytes, uploadBytes);
+ }
+
+ /**
+ * Update the amount of data this JobWorkItem is estimated to transfer after the job has
+ * started.
+ *
+ * @see JobInfo.Builder#setEstimatedNetworkBytes(long, long)
+ */
+ public final void updateEstimatedNetworkBytes(@NonNull JobParameters params,
+ @NonNull JobWorkItem jobWorkItem,
+ @BytesLong long downloadBytes, @BytesLong long uploadBytes) {
+ mEngine.updateEstimatedNetworkBytes(params, jobWorkItem, downloadBytes, uploadBytes);
+ }
+
+ /**
+ * Tell JobScheduler how much data has successfully been transferred for the data transfer job.
+ */
+ public final void updateTransferredNetworkBytes(@NonNull JobParameters params,
+ @BytesLong long transferredDownloadBytes, @BytesLong long transferredUploadBytes) {
+ mEngine.updateTransferredNetworkBytes(params, null,
+ transferredDownloadBytes, transferredUploadBytes);
+ }
+
+ /**
+ * Tell JobScheduler how much data has been transferred for the data transfer
+ * {@link JobWorkItem}.
+ */
+ public final void updateTransferredNetworkBytes(@NonNull JobParameters params,
+ @NonNull JobWorkItem item,
+ @BytesLong long transferredDownloadBytes, @BytesLong long transferredUploadBytes) {
+ mEngine.updateTransferredNetworkBytes(params, item,
+ transferredDownloadBytes, transferredUploadBytes);
+ }
+
+ /**
+ * Get the number of bytes the app has successfully downloaded for this job. JobScheduler
+ * will call this if the job has specified positive estimated download bytes and
+ * {@link #updateTransferredNetworkBytes(JobParameters, long, long)}
+ * hasn't been called recently.
+ *
+ * <p>
+ * This must be implemented for all data transfer jobs.
+ *
+ * @hide
+ * @see JobInfo.Builder#setEstimatedNetworkBytes(long, long)
+ * @see JobInfo#NETWORK_BYTES_UNKNOWN
+ */
+ // TODO(255371817): specify the actual time JS will wait for progress before requesting
+ @BytesLong
+ public long getTransferredDownloadBytes(@NonNull JobParameters params) {
+ if (Compatibility.isChangeEnabled(THROW_ON_INVALID_DATA_TRANSFER_IMPLEMENTATION)) {
+ // Regular jobs don't have to implement this and JobScheduler won't call this API for
+ // non-data transfer jobs.
+ throw new RuntimeException("Not implemented. Must override in a subclass.");
+ }
+ return 0;
+ }
+
+ /**
+ * Get the number of bytes the app has successfully downloaded for this job. JobScheduler
+ * will call this if the job has specified positive estimated upload bytes and
+ * {@link #updateTransferredNetworkBytes(JobParameters, long, long)}
+ * hasn't been called recently.
+ *
+ * <p>
+ * This must be implemented for all data transfer jobs.
+ *
+ * @hide
+ * @see JobInfo.Builder#setEstimatedNetworkBytes(long, long)
+ * @see JobInfo#NETWORK_BYTES_UNKNOWN
+ */
+ // TODO(255371817): specify the actual time JS will wait for progress before requesting
+ @BytesLong
+ public long getTransferredUploadBytes(@NonNull JobParameters params) {
+ if (Compatibility.isChangeEnabled(THROW_ON_INVALID_DATA_TRANSFER_IMPLEMENTATION)) {
+ // Regular jobs don't have to implement this and JobScheduler won't call this API for
+ // non-data transfer jobs.
+ throw new RuntimeException("Not implemented. Must override in a subclass.");
+ }
+ return 0;
+ }
+
+ /**
+ * Get the number of bytes the app has successfully downloaded for this job. JobScheduler
+ * will call this if the job has specified positive estimated download bytes and
+ * {@link #updateTransferredNetworkBytes(JobParameters, JobWorkItem, long, long)}
+ * hasn't been called recently and the job has
+ * {@link JobWorkItem JobWorkItems} that have been
+ * {@link JobParameters#dequeueWork dequeued} but not
+ * {@link JobParameters#completeWork(JobWorkItem) completed}.
+ *
+ * <p>
+ * This must be implemented for all data transfer jobs.
+ *
+ * @hide
+ * @see JobInfo#NETWORK_BYTES_UNKNOWN
+ */
+ // TODO(255371817): specify the actual time JS will wait for progress before requesting
+ @BytesLong
+ public long getTransferredDownloadBytes(@NonNull JobParameters params,
+ @NonNull JobWorkItem item) {
+ if (item == null) {
+ return getTransferredDownloadBytes(params);
+ }
+ if (Compatibility.isChangeEnabled(THROW_ON_INVALID_DATA_TRANSFER_IMPLEMENTATION)) {
+ // Regular jobs don't have to implement this and JobScheduler won't call this API for
+ // non-data transfer jobs.
+ throw new RuntimeException("Not implemented. Must override in a subclass.");
+ }
+ return 0;
+ }
+
+ /**
+ * Get the number of bytes the app has successfully downloaded for this job. JobScheduler
+ * will call this if the job has specified positive estimated upload bytes and
+ * {@link #updateTransferredNetworkBytes(JobParameters, JobWorkItem, long, long)}
+ * hasn't been called recently and the job has
+ * {@link JobWorkItem JobWorkItems} that have been
+ * {@link JobParameters#dequeueWork dequeued} but not
+ * {@link JobParameters#completeWork(JobWorkItem) completed}.
+ *
+ * <p>
+ * This must be implemented for all data transfer jobs.
+ *
+ * @hide
+ * @see JobInfo#NETWORK_BYTES_UNKNOWN
+ */
+ // TODO(255371817): specify the actual time JS will wait for progress before requesting
+ @BytesLong
+ public long getTransferredUploadBytes(@NonNull JobParameters params,
+ @NonNull JobWorkItem item) {
+ if (item == null) {
+ return getTransferredUploadBytes(params);
+ }
+ if (Compatibility.isChangeEnabled(THROW_ON_INVALID_DATA_TRANSFER_IMPLEMENTATION)) {
+ // Regular jobs don't have to implement this and JobScheduler won't call this API for
+ // non-data transfer jobs.
+ throw new RuntimeException("Not implemented. Must override in a subclass.");
+ }
+ return 0;
+ }
+
+ /**
+ * Provide JobScheduler with a notification to post and tie to this job's lifecycle.
+ * This is only required for those user-initiated jobs which return {@code true} via
+ * {@link JobParameters#isUserInitiatedJob()}.
+ * If the app does not call this method for a required notification within
+ * 10 seconds after {@link #onStartJob(JobParameters)} is called,
+ * the system will trigger an ANR and stop this job.
+ *
+ * The notification must provide an accurate description of the work that the job is doing
+ * and, if possible, the state of the work.
+ *
+ * <p>
+ * Note that certain types of jobs
+ * (e.g. {@link JobInfo.Builder#setEstimatedNetworkBytes(long, long) data transfer jobs})
+ * may require the notification to have certain characteristics
+ * and their documentation will state any such requirements.
+ *
+ * <p>
+ * JobScheduler will not remember this notification after the job has finished running,
+ * so apps must call this every time the job is started (if required or desired).
+ *
+ * <p>
+ * If separate jobs use the same notification ID with this API, the most recently provided
+ * notification will be shown to the user, and the
+ * {@code jobEndNotificationPolicy} of the last job to stop will be applied.
+ *
+ * @param params The parameters identifying this job, as supplied to
+ * the job in the {@link #onStartJob(JobParameters)} callback.
+ * @param notificationId The ID for this notification, as per
+ * {@link android.app.NotificationManager#notify(int,
+ * Notification)}.
+ * @param notification The notification to be displayed.
+ * @param jobEndNotificationPolicy The policy to apply to the notification when the job stops.
+ */
+ public final void setNotification(@NonNull JobParameters params, int notificationId,
+ @NonNull Notification notification,
+ @JobEndNotificationPolicy int jobEndNotificationPolicy) {
+ mEngine.setNotification(params, notificationId, notification, jobEndNotificationPolicy);
+ }
}
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java b/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java
index 3d43d20e7955..79d87edff9b2 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java
@@ -16,7 +16,14 @@
package android.app.job;
+import static android.app.job.JobScheduler.THROW_ON_INVALID_DATA_TRANSFER_IMPLEMENTATION;
+
+import android.annotation.BytesLong;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Notification;
import android.app.Service;
+import android.compat.Compatibility;
import android.content.Intent;
import android.os.Handler;
import android.os.IBinder;
@@ -25,6 +32,8 @@ import android.os.Message;
import android.os.RemoteException;
import android.util.Log;
+import com.android.internal.os.SomeArgs;
+
import java.lang.ref.WeakReference;
/**
@@ -51,6 +60,24 @@ public abstract class JobServiceEngine {
* Message that the client has completed execution of this job.
*/
private static final int MSG_JOB_FINISHED = 2;
+ /**
+ * Message that will result in a call to
+ * {@link #getTransferredDownloadBytes(JobParameters, JobWorkItem)}.
+ */
+ private static final int MSG_GET_TRANSFERRED_DOWNLOAD_BYTES = 3;
+ /**
+ * Message that will result in a call to
+ * {@link #getTransferredUploadBytes(JobParameters, JobWorkItem)}.
+ */
+ private static final int MSG_GET_TRANSFERRED_UPLOAD_BYTES = 4;
+ /** Message that the client wants to update JobScheduler of the data transfer progress. */
+ private static final int MSG_UPDATE_TRANSFERRED_NETWORK_BYTES = 5;
+ /** Message that the client wants to update JobScheduler of the estimated transfer size. */
+ private static final int MSG_UPDATE_ESTIMATED_NETWORK_BYTES = 6;
+ /** Message that the client wants to give JobScheduler a notification to tie to the job. */
+ private static final int MSG_SET_NOTIFICATION = 7;
+ /** Message that the network to use has changed. */
+ private static final int MSG_INFORM_OF_NETWORK_CHANGE = 8;
private final IJobService mBinder;
@@ -68,6 +95,32 @@ public abstract class JobServiceEngine {
}
@Override
+ public void getTransferredDownloadBytes(@NonNull JobParameters jobParams,
+ @Nullable JobWorkItem jobWorkItem) throws RemoteException {
+ JobServiceEngine service = mService.get();
+ if (service != null) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = jobParams;
+ args.arg2 = jobWorkItem;
+ service.mHandler.obtainMessage(MSG_GET_TRANSFERRED_DOWNLOAD_BYTES, args)
+ .sendToTarget();
+ }
+ }
+
+ @Override
+ public void getTransferredUploadBytes(@NonNull JobParameters jobParams,
+ @Nullable JobWorkItem jobWorkItem) throws RemoteException {
+ JobServiceEngine service = mService.get();
+ if (service != null) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = jobParams;
+ args.arg2 = jobWorkItem;
+ service.mHandler.obtainMessage(MSG_GET_TRANSFERRED_UPLOAD_BYTES, args)
+ .sendToTarget();
+ }
+ }
+
+ @Override
public void startJob(JobParameters jobParams) throws RemoteException {
JobServiceEngine service = mService.get();
if (service != null) {
@@ -77,6 +130,16 @@ public abstract class JobServiceEngine {
}
@Override
+ public void onNetworkChanged(JobParameters jobParams) throws RemoteException {
+ JobServiceEngine service = mService.get();
+ if (service != null) {
+ service.mHandler.removeMessages(MSG_INFORM_OF_NETWORK_CHANGE);
+ service.mHandler.obtainMessage(MSG_INFORM_OF_NETWORK_CHANGE, jobParams)
+ .sendToTarget();
+ }
+ }
+
+ @Override
public void stopJob(JobParameters jobParams) throws RemoteException {
JobServiceEngine service = mService.get();
if (service != null) {
@@ -98,9 +161,9 @@ public abstract class JobServiceEngine {
@Override
public void handleMessage(Message msg) {
- final JobParameters params = (JobParameters) msg.obj;
switch (msg.what) {
- case MSG_EXECUTE_JOB:
+ case MSG_EXECUTE_JOB: {
+ final JobParameters params = (JobParameters) msg.obj;
try {
boolean workOngoing = JobServiceEngine.this.onStartJob(params);
ackStartMessage(params, workOngoing);
@@ -109,7 +172,9 @@ public abstract class JobServiceEngine {
throw new RuntimeException(e);
}
break;
- case MSG_STOP_JOB:
+ }
+ case MSG_STOP_JOB: {
+ final JobParameters params = (JobParameters) msg.obj;
try {
boolean ret = JobServiceEngine.this.onStopJob(params);
ackStopMessage(params, ret);
@@ -118,7 +183,9 @@ public abstract class JobServiceEngine {
throw new RuntimeException(e);
}
break;
- case MSG_JOB_FINISHED:
+ }
+ case MSG_JOB_FINISHED: {
+ final JobParameters params = (JobParameters) msg.obj;
final boolean needsReschedule = (msg.arg2 == 1);
IJobCallback callback = params.getCallback();
if (callback != null) {
@@ -132,19 +199,145 @@ public abstract class JobServiceEngine {
Log.e(TAG, "finishJob() called for a nonexistent job id.");
}
break;
+ }
+ case MSG_GET_TRANSFERRED_DOWNLOAD_BYTES: {
+ final SomeArgs args = (SomeArgs) msg.obj;
+ final JobParameters params = (JobParameters) args.arg1;
+ final JobWorkItem item = (JobWorkItem) args.arg2;
+ try {
+ long ret = JobServiceEngine.this.getTransferredDownloadBytes(params, item);
+ ackGetTransferredDownloadBytesMessage(params, item, ret);
+ } catch (Exception e) {
+ Log.e(TAG, "Application unable to handle getTransferredDownloadBytes.", e);
+ throw new RuntimeException(e);
+ }
+ args.recycle();
+ break;
+ }
+ case MSG_GET_TRANSFERRED_UPLOAD_BYTES: {
+ final SomeArgs args = (SomeArgs) msg.obj;
+ final JobParameters params = (JobParameters) args.arg1;
+ final JobWorkItem item = (JobWorkItem) args.arg2;
+ try {
+ long ret = JobServiceEngine.this.getTransferredUploadBytes(params, item);
+ ackGetTransferredUploadBytesMessage(params, item, ret);
+ } catch (Exception e) {
+ Log.e(TAG, "Application unable to handle getTransferredUploadBytes.", e);
+ throw new RuntimeException(e);
+ }
+ args.recycle();
+ break;
+ }
+ case MSG_UPDATE_TRANSFERRED_NETWORK_BYTES: {
+ final SomeArgs args = (SomeArgs) msg.obj;
+ final JobParameters params = (JobParameters) args.arg1;
+ IJobCallback callback = params.getCallback();
+ if (callback != null) {
+ try {
+ callback.updateTransferredNetworkBytes(params.getJobId(),
+ (JobWorkItem) args.arg2, args.argl1, args.argl2);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error updating data transfer progress to system:"
+ + " binder has gone away.");
+ }
+ } else {
+ Log.e(TAG, "updateDataTransferProgress() called for a nonexistent job id.");
+ }
+ args.recycle();
+ break;
+ }
+ case MSG_UPDATE_ESTIMATED_NETWORK_BYTES: {
+ final SomeArgs args = (SomeArgs) msg.obj;
+ final JobParameters params = (JobParameters) args.arg1;
+ IJobCallback callback = params.getCallback();
+ if (callback != null) {
+ try {
+ callback.updateEstimatedNetworkBytes(params.getJobId(),
+ (JobWorkItem) args.arg2, args.argl1, args.argl2);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error updating estimated transfer size to system:"
+ + " binder has gone away.");
+ }
+ } else {
+ Log.e(TAG,
+ "updateEstimatedNetworkBytes() called for a nonexistent job id.");
+ }
+ args.recycle();
+ break;
+ }
+ case MSG_SET_NOTIFICATION: {
+ final SomeArgs args = (SomeArgs) msg.obj;
+ final JobParameters params = (JobParameters) args.arg1;
+ final Notification notification = (Notification) args.arg2;
+ IJobCallback callback = params.getCallback();
+ if (callback != null) {
+ try {
+ callback.setNotification(params.getJobId(),
+ args.argi1, notification, args.argi2);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error providing notification: binder has gone away.");
+ }
+ } else {
+ Log.e(TAG, "setNotification() called for a nonexistent job.");
+ }
+ args.recycle();
+ break;
+ }
+ case MSG_INFORM_OF_NETWORK_CHANGE: {
+ final JobParameters params = (JobParameters) msg.obj;
+ try {
+ JobServiceEngine.this.onNetworkChanged(params);
+ } catch (Exception e) {
+ Log.e(TAG, "Error while executing job: " + params.getJobId());
+ throw new RuntimeException(e);
+ }
+ break;
+ }
default:
Log.e(TAG, "Unrecognised message received.");
break;
}
}
+ private void ackGetTransferredDownloadBytesMessage(@NonNull JobParameters params,
+ @Nullable JobWorkItem item, long progress) {
+ final IJobCallback callback = params.getCallback();
+ final int jobId = params.getJobId();
+ final int workId = item == null ? -1 : item.getWorkId();
+ if (callback != null) {
+ try {
+ callback.acknowledgeGetTransferredDownloadBytesMessage(jobId, workId, progress);
+ } catch (RemoteException e) {
+ Log.e(TAG, "System unreachable for returning progress.");
+ }
+ } else if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Attempting to ack a job that has already been processed.");
+ }
+ }
+
+ private void ackGetTransferredUploadBytesMessage(@NonNull JobParameters params,
+ @Nullable JobWorkItem item, long progress) {
+ final IJobCallback callback = params.getCallback();
+ final int jobId = params.getJobId();
+ final int workId = item == null ? -1 : item.getWorkId();
+ if (callback != null) {
+ try {
+ callback.acknowledgeGetTransferredUploadBytesMessage(jobId, workId, progress);
+ } catch (RemoteException e) {
+ Log.e(TAG, "System unreachable for returning progress.");
+ }
+ } else if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Attempting to ack a job that has already been processed.");
+ }
+ }
+
private void ackStartMessage(JobParameters params, boolean workOngoing) {
final IJobCallback callback = params.getCallback();
final int jobId = params.getJobId();
if (callback != null) {
try {
callback.acknowledgeStartMessage(jobId, workOngoing);
- } catch(RemoteException e) {
+ } catch (RemoteException e) {
Log.e(TAG, "System unreachable for starting job.");
}
} else {
@@ -213,4 +406,105 @@ public abstract class JobServiceEngine {
m.arg2 = needsReschedule ? 1 : 0;
m.sendToTarget();
}
-} \ No newline at end of file
+
+ /**
+ * Engine's report that the network for the job has changed.
+ *
+ * @see JobService#onNetworkChanged(JobParameters)
+ */
+ public void onNetworkChanged(@NonNull JobParameters params) {
+ Log.w(TAG, "onNetworkChanged() not implemented. Must override in a subclass.");
+ }
+
+ /**
+ * Engine's request to get how much data has been downloaded.
+ *
+ * @hide
+ * @see JobService#getTransferredDownloadBytes()
+ */
+ @BytesLong
+ public long getTransferredDownloadBytes(@NonNull JobParameters params,
+ @Nullable JobWorkItem item) {
+ if (Compatibility.isChangeEnabled(THROW_ON_INVALID_DATA_TRANSFER_IMPLEMENTATION)) {
+ throw new RuntimeException("Not implemented. Must override in a subclass.");
+ }
+ return 0;
+ }
+
+ /**
+ * Engine's request to get how much data has been uploaded.
+ *
+ * @hide
+ * @see JobService#getTransferredUploadBytes()
+ */
+ @BytesLong
+ public long getTransferredUploadBytes(@NonNull JobParameters params,
+ @Nullable JobWorkItem item) {
+ if (Compatibility.isChangeEnabled(THROW_ON_INVALID_DATA_TRANSFER_IMPLEMENTATION)) {
+ throw new RuntimeException("Not implemented. Must override in a subclass.");
+ }
+ return 0;
+ }
+
+ /**
+ * Call in to engine to report data transfer progress.
+ *
+ * @see JobService#updateTransferredNetworkBytes(JobParameters, long, long)
+ * @see JobService#updateTransferredNetworkBytes(JobParameters, JobWorkItem, long, long)
+ */
+ public void updateTransferredNetworkBytes(@NonNull JobParameters params,
+ @Nullable JobWorkItem item,
+ @BytesLong long downloadBytes, @BytesLong long uploadBytes) {
+ if (params == null) {
+ throw new NullPointerException("params");
+ }
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = params;
+ args.arg2 = item;
+ args.argl1 = downloadBytes;
+ args.argl2 = uploadBytes;
+ mHandler.obtainMessage(MSG_UPDATE_TRANSFERRED_NETWORK_BYTES, args).sendToTarget();
+ }
+
+ /**
+ * Call in to engine to report data transfer progress.
+ *
+ * @see JobService#updateEstimatedNetworkBytes(JobParameters, long, long)
+ * @see JobService#updateEstimatedNetworkBytes(JobParameters, JobWorkItem, long, long)
+ */
+ public void updateEstimatedNetworkBytes(@NonNull JobParameters params,
+ @Nullable JobWorkItem item,
+ @BytesLong long downloadBytes, @BytesLong long uploadBytes) {
+ if (params == null) {
+ throw new NullPointerException("params");
+ }
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = params;
+ args.arg2 = item;
+ args.argl1 = downloadBytes;
+ args.argl2 = uploadBytes;
+ mHandler.obtainMessage(MSG_UPDATE_ESTIMATED_NETWORK_BYTES, args).sendToTarget();
+ }
+
+ /**
+ * Give JobScheduler a notification to tie to this job's lifecycle.
+ *
+ * @see JobService#setNotification(JobParameters, int, Notification, int)
+ */
+ public void setNotification(@NonNull JobParameters params, int notificationId,
+ @NonNull Notification notification,
+ @JobService.JobEndNotificationPolicy int jobEndNotificationPolicy) {
+ if (params == null) {
+ throw new NullPointerException("params");
+ }
+ if (notification == null) {
+ throw new NullPointerException("notification");
+ }
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = params;
+ args.arg2 = notification;
+ args.argi1 = notificationId;
+ args.argi2 = jobEndNotificationPolicy;
+ mHandler.obtainMessage(MSG_SET_NOTIFICATION, args).sendToTarget();
+ }
+}
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobWorkItem.java b/apex/jobscheduler/framework/java/android/app/job/JobWorkItem.java
index 372f9faacd5a..18167e2a67dc 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobWorkItem.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobWorkItem.java
@@ -19,19 +19,33 @@ package android.app.job;
import static android.app.job.JobInfo.NETWORK_BYTES_UNKNOWN;
import android.annotation.BytesLong;
+import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.compat.Compatibility;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Intent;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
+import android.os.PersistableBundle;
/**
* A unit of work that can be enqueued for a job using
* {@link JobScheduler#enqueue JobScheduler.enqueue}. See
* {@link JobParameters#dequeueWork() JobParameters.dequeueWork} for more details.
+ *
+ * <p class="caution"><strong>Note:</strong> Prior to Android version
+ * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, JobWorkItems could not be persisted.
+ * Apps were not allowed to enqueue JobWorkItems with persisted jobs and the system would throw
+ * an {@link IllegalArgumentException} if they attempted to do so. Starting with
+ * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, JobWorkItems can be persisted alongside
+ * the hosting job. However, Intents cannot be persisted. Set a {@link PersistableBundle} using
+ * {@link Builder#setExtras(PersistableBundle)} for any information that needs to be persisted.
*/
final public class JobWorkItem implements Parcelable {
+ @NonNull
+ private final PersistableBundle mExtras;
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
final Intent mIntent;
private final long mNetworkDownloadBytes;
@@ -48,6 +62,10 @@ final public class JobWorkItem implements Parcelable {
* Create a new piece of work, which can be submitted to
* {@link JobScheduler#enqueue JobScheduler.enqueue}.
*
+ * <p>
+ * Intents cannot be used for persisted JobWorkItems.
+ * Use {@link Builder#setExtras(PersistableBundle)} instead for persisted JobWorkItems.
+ *
* @param intent The general Intent describing this work.
*/
public JobWorkItem(Intent intent) {
@@ -61,6 +79,10 @@ final public class JobWorkItem implements Parcelable {
* See {@link JobInfo.Builder#setEstimatedNetworkBytes(long, long)} for
* details about how to estimate network traffic.
*
+ * <p>
+ * Intents cannot be used for persisted JobWorkItems.
+ * Use {@link Builder#setExtras(PersistableBundle)} instead for persisted JobWorkItems.
+ *
* @param intent The general Intent describing this work.
* @param downloadBytes The estimated size of network traffic that will be
* downloaded by this job work item, in bytes.
@@ -78,6 +100,10 @@ final public class JobWorkItem implements Parcelable {
* See {@link JobInfo.Builder#setEstimatedNetworkBytes(long, long)} for
* details about how to estimate network traffic.
*
+ * <p>
+ * Intents cannot be used for persisted JobWorkItems.
+ * Use {@link Builder#setExtras(PersistableBundle)} instead for persisted JobWorkItems.
+ *
* @param intent The general Intent describing this work.
* @param downloadBytes The estimated size of network traffic that will be
* downloaded by this job work item, in bytes.
@@ -88,25 +114,31 @@ final public class JobWorkItem implements Parcelable {
*/
public JobWorkItem(@Nullable Intent intent, @BytesLong long downloadBytes,
@BytesLong long uploadBytes, @BytesLong long minimumChunkBytes) {
- if (minimumChunkBytes != NETWORK_BYTES_UNKNOWN && minimumChunkBytes <= 0) {
- throw new IllegalArgumentException("Minimum chunk size must be positive");
- }
- final long estimatedTransfer;
- if (uploadBytes == NETWORK_BYTES_UNKNOWN) {
- estimatedTransfer = downloadBytes;
- } else {
- estimatedTransfer = uploadBytes
- + (downloadBytes == NETWORK_BYTES_UNKNOWN ? 0 : downloadBytes);
- }
- if (minimumChunkBytes != NETWORK_BYTES_UNKNOWN && estimatedTransfer != NETWORK_BYTES_UNKNOWN
- && minimumChunkBytes > estimatedTransfer) {
- throw new IllegalArgumentException(
- "Minimum chunk size can't be greater than estimated network usage");
- }
+ mExtras = PersistableBundle.EMPTY;
mIntent = intent;
mNetworkDownloadBytes = downloadBytes;
mNetworkUploadBytes = uploadBytes;
mMinimumChunkBytes = minimumChunkBytes;
+ enforceValidity(Compatibility.isChangeEnabled(JobInfo.REJECT_NEGATIVE_NETWORK_ESTIMATES));
+ }
+
+ private JobWorkItem(@NonNull Builder builder) {
+ mDeliveryCount = builder.mDeliveryCount;
+ mExtras = builder.mExtras.deepCopy();
+ mIntent = builder.mIntent;
+ mNetworkDownloadBytes = builder.mNetworkDownloadBytes;
+ mNetworkUploadBytes = builder.mNetworkUploadBytes;
+ mMinimumChunkBytes = builder.mMinimumNetworkChunkBytes;
+ }
+
+ /**
+ * Return the extras associated with this work.
+ *
+ * @see Builder#setExtras(PersistableBundle)
+ */
+ @NonNull
+ public PersistableBundle getExtras() {
+ return mExtras;
}
/**
@@ -189,6 +221,7 @@ final public class JobWorkItem implements Parcelable {
/**
* @hide
*/
+ @Nullable
public Object getGrants() {
return mGrants;
}
@@ -199,6 +232,8 @@ final public class JobWorkItem implements Parcelable {
sb.append(mWorkId);
sb.append(" intent=");
sb.append(mIntent);
+ sb.append(" extras=");
+ sb.append(mExtras);
if (mNetworkDownloadBytes != NETWORK_BYTES_UNKNOWN) {
sb.append(" downloadBytes=");
sb.append(mNetworkDownloadBytes);
@@ -220,9 +255,153 @@ final public class JobWorkItem implements Parcelable {
}
/**
+ * Builder class for constructing {@link JobWorkItem} objects.
+ */
+ public static final class Builder {
+ private int mDeliveryCount;
+ private PersistableBundle mExtras = PersistableBundle.EMPTY;
+ private Intent mIntent;
+ private long mNetworkDownloadBytes = NETWORK_BYTES_UNKNOWN;
+ private long mNetworkUploadBytes = NETWORK_BYTES_UNKNOWN;
+ private long mMinimumNetworkChunkBytes = NETWORK_BYTES_UNKNOWN;
+
+ /**
+ * Initialize a new Builder to construct a {@link JobWorkItem} object.
+ */
+ public Builder() {
+ }
+
+ /**
+ * @see JobWorkItem#getDeliveryCount()
+ * @return This object for method chaining
+ * @hide
+ */
+ @NonNull
+ public Builder setDeliveryCount(int deliveryCount) {
+ mDeliveryCount = deliveryCount;
+ return this;
+ }
+
+ /**
+ * Set optional extras. This can be persisted, so we only allow primitive types.
+ * @param extras Bundle containing extras you want the scheduler to hold on to for you.
+ * @return This object for method chaining
+ * @see JobWorkItem#getExtras()
+ */
+ @NonNull
+ public Builder setExtras(@NonNull PersistableBundle extras) {
+ if (extras == null) {
+ throw new IllegalArgumentException("extras cannot be null");
+ }
+ mExtras = extras;
+ return this;
+ }
+
+ /**
+ * Set an intent with information relevant to this work item.
+ *
+ * <p>
+ * Intents cannot be used for persisted JobWorkItems.
+ * Use {@link #setExtras(PersistableBundle)} instead for persisted JobWorkItems.
+ *
+ * @return This object for method chaining
+ * @see JobWorkItem#getIntent()
+ */
+ @NonNull
+ public Builder setIntent(@NonNull Intent intent) {
+ mIntent = intent;
+ return this;
+ }
+
+ /**
+ * Set the estimated size of network traffic that will be performed for this work item,
+ * in bytes.
+ *
+ * See {@link JobInfo.Builder#setEstimatedNetworkBytes(long, long)} for
+ * details about how to estimate network traffic.
+ *
+ * @param downloadBytes The estimated size of network traffic that will be
+ * downloaded for this work item, in bytes.
+ * @param uploadBytes The estimated size of network traffic that will be
+ * uploaded for this work item, in bytes.
+ * @return This object for method chaining
+ * @see JobInfo.Builder#setEstimatedNetworkBytes(long, long)
+ * @see JobWorkItem#getEstimatedNetworkDownloadBytes()
+ * @see JobWorkItem#getEstimatedNetworkUploadBytes()
+ */
+ @NonNull
+ @SuppressLint("MissingGetterMatchingBuilder")
+ public Builder setEstimatedNetworkBytes(@BytesLong long downloadBytes,
+ @BytesLong long uploadBytes) {
+ if (downloadBytes != NETWORK_BYTES_UNKNOWN && downloadBytes < 0) {
+ throw new IllegalArgumentException(
+ "Invalid network download bytes: " + downloadBytes);
+ }
+ if (uploadBytes != NETWORK_BYTES_UNKNOWN && uploadBytes < 0) {
+ throw new IllegalArgumentException("Invalid network upload bytes: " + uploadBytes);
+ }
+ mNetworkDownloadBytes = downloadBytes;
+ mNetworkUploadBytes = uploadBytes;
+ return this;
+ }
+
+ /**
+ * Set the minimum size of non-resumable network traffic this work item requires, in bytes.
+ * When the upload or download can be easily paused and resumed, use this to set the
+ * smallest size that must be transmitted between start and stop events to be considered
+ * successful. If the transfer cannot be paused and resumed, then this should be the sum
+ * of the values provided to {@link #setEstimatedNetworkBytes(long, long)}.
+ *
+ * See {@link JobInfo.Builder#setMinimumNetworkChunkBytes(long)} for
+ * details about how to set the minimum chunk.
+ *
+ * @param chunkSizeBytes The smallest piece of data that cannot be easily paused and
+ * resumed, in bytes.
+ * @return This object for method chaining
+ * @see JobInfo.Builder#setMinimumNetworkChunkBytes(long)
+ * @see JobWorkItem#getMinimumNetworkChunkBytes()
+ * @see JobWorkItem#JobWorkItem(android.content.Intent, long, long, long)
+ */
+ @NonNull
+ public Builder setMinimumNetworkChunkBytes(@BytesLong long chunkSizeBytes) {
+ if (chunkSizeBytes != NETWORK_BYTES_UNKNOWN && chunkSizeBytes <= 0) {
+ throw new IllegalArgumentException("Minimum chunk size must be positive");
+ }
+ mMinimumNetworkChunkBytes = chunkSizeBytes;
+ return this;
+ }
+
+ /**
+ * @return The JobWorkItem object to hand to the JobScheduler. This object is immutable.
+ */
+ @NonNull
+ public JobWorkItem build() {
+ return build(Compatibility.isChangeEnabled(JobInfo.REJECT_NEGATIVE_NETWORK_ESTIMATES));
+ }
+
+ /** @hide */
+ @NonNull
+ public JobWorkItem build(boolean rejectNegativeNetworkEstimates) {
+ JobWorkItem jobWorkItem = new JobWorkItem(this);
+ jobWorkItem.enforceValidity(rejectNegativeNetworkEstimates);
+ return jobWorkItem;
+ }
+ }
+
+ /**
* @hide
*/
- public void enforceValidity() {
+ public void enforceValidity(boolean rejectNegativeNetworkEstimates) {
+ if (rejectNegativeNetworkEstimates) {
+ if (mNetworkUploadBytes != NETWORK_BYTES_UNKNOWN && mNetworkUploadBytes < 0) {
+ throw new IllegalArgumentException(
+ "Invalid network upload bytes: " + mNetworkUploadBytes);
+ }
+ if (mNetworkDownloadBytes != NETWORK_BYTES_UNKNOWN && mNetworkDownloadBytes < 0) {
+ throw new IllegalArgumentException(
+ "Invalid network download bytes: " + mNetworkDownloadBytes);
+ }
+ }
final long estimatedTransfer;
if (mNetworkUploadBytes == NETWORK_BYTES_UNKNOWN) {
estimatedTransfer = mNetworkDownloadBytes;
@@ -252,6 +431,7 @@ final public class JobWorkItem implements Parcelable {
} else {
out.writeInt(0);
}
+ out.writePersistableBundle(mExtras);
out.writeLong(mNetworkDownloadBytes);
out.writeLong(mNetworkUploadBytes);
out.writeLong(mMinimumChunkBytes);
@@ -277,6 +457,8 @@ final public class JobWorkItem implements Parcelable {
} else {
mIntent = null;
}
+ final PersistableBundle extras = in.readPersistableBundle();
+ mExtras = extras != null ? extras : PersistableBundle.EMPTY;
mNetworkDownloadBytes = in.readLong();
mNetworkUploadBytes = in.readLong();
mMinimumChunkBytes = in.readLong();
diff --git a/apex/jobscheduler/framework/java/android/app/job/UserVisibleJobSummary.java b/apex/jobscheduler/framework/java/android/app/job/UserVisibleJobSummary.java
index afcbe7d8eb3d..ba79c307395c 100644
--- a/apex/jobscheduler/framework/java/android/app/job/UserVisibleJobSummary.java
+++ b/apex/jobscheduler/framework/java/android/app/job/UserVisibleJobSummary.java
@@ -17,9 +17,12 @@
package android.app.job;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.os.Parcel;
import android.os.Parcelable;
+import java.util.Objects;
+
/**
* Summary of a scheduled job that the user is meant to be aware of.
*
@@ -27,26 +30,40 @@ import android.os.Parcelable;
*/
public class UserVisibleJobSummary implements Parcelable {
private final int mCallingUid;
+ @NonNull
+ private final String mCallingPackageName;
private final int mSourceUserId;
@NonNull
private final String mSourcePackageName;
+ @Nullable
+ private final String mNamespace;
private final int mJobId;
- public UserVisibleJobSummary(int callingUid, int sourceUserId,
- @NonNull String sourcePackageName, int jobId) {
+ public UserVisibleJobSummary(int callingUid, @NonNull String callingPackageName,
+ int sourceUserId, @NonNull String sourcePackageName,
+ @Nullable String namespace, int jobId) {
mCallingUid = callingUid;
+ mCallingPackageName = callingPackageName;
mSourceUserId = sourceUserId;
mSourcePackageName = sourcePackageName;
+ mNamespace = namespace;
mJobId = jobId;
}
protected UserVisibleJobSummary(Parcel in) {
mCallingUid = in.readInt();
+ mCallingPackageName = in.readString();
mSourceUserId = in.readInt();
mSourcePackageName = in.readString();
+ mNamespace = in.readString();
mJobId = in.readInt();
}
+ @NonNull
+ public String getCallingPackageName() {
+ return mCallingPackageName;
+ }
+
public int getCallingUid() {
return mCallingUid;
}
@@ -55,10 +72,16 @@ public class UserVisibleJobSummary implements Parcelable {
return mJobId;
}
+ @Nullable
+ public String getNamespace() {
+ return mNamespace;
+ }
+
public int getSourceUserId() {
return mSourceUserId;
}
+ @NonNull
public String getSourcePackageName() {
return mSourcePackageName;
}
@@ -69,8 +92,10 @@ public class UserVisibleJobSummary implements Parcelable {
if (!(o instanceof UserVisibleJobSummary)) return false;
UserVisibleJobSummary that = (UserVisibleJobSummary) o;
return mCallingUid == that.mCallingUid
+ && mCallingPackageName.equals(that.mCallingPackageName)
&& mSourceUserId == that.mSourceUserId
&& mSourcePackageName.equals(that.mSourcePackageName)
+ && Objects.equals(mNamespace, that.mNamespace)
&& mJobId == that.mJobId;
}
@@ -78,8 +103,12 @@ public class UserVisibleJobSummary implements Parcelable {
public int hashCode() {
int result = 0;
result = 31 * result + mCallingUid;
+ result = 31 * result + mCallingPackageName.hashCode();
result = 31 * result + mSourceUserId;
result = 31 * result + mSourcePackageName.hashCode();
+ if (mNamespace != null) {
+ result = 31 * result + mNamespace.hashCode();
+ }
result = 31 * result + mJobId;
return result;
}
@@ -88,8 +117,10 @@ public class UserVisibleJobSummary implements Parcelable {
public String toString() {
return "UserVisibleJobSummary{"
+ "callingUid=" + mCallingUid
+ + ", callingPackageName='" + mCallingPackageName + "'"
+ ", sourceUserId=" + mSourceUserId
+ ", sourcePackageName='" + mSourcePackageName + "'"
+ + ", namespace=" + mNamespace
+ ", jobId=" + mJobId
+ "}";
}
@@ -102,8 +133,10 @@ public class UserVisibleJobSummary implements Parcelable {
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(mCallingUid);
+ dest.writeString(mCallingPackageName);
dest.writeInt(mSourceUserId);
dest.writeString(mSourcePackageName);
+ dest.writeString(mNamespace);
dest.writeInt(mJobId);
}
diff --git a/apex/jobscheduler/framework/java/android/app/tare/EconomyManager.java b/apex/jobscheduler/framework/java/android/app/tare/EconomyManager.java
index c3fc7b16ebdf..0bea028e6f50 100644
--- a/apex/jobscheduler/framework/java/android/app/tare/EconomyManager.java
+++ b/apex/jobscheduler/framework/java/android/app/tare/EconomyManager.java
@@ -16,16 +16,23 @@
package android.app.tare;
+import android.annotation.IntDef;
import android.annotation.Nullable;
import android.annotation.SystemService;
+import android.annotation.TestApi;
import android.content.Context;
+import android.os.RemoteException;
import android.util.Log;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
/**
* Provides access to the resource economy service.
*
* @hide
*/
+@TestApi
@SystemService(Context.RESOURCE_ECONOMY_SERVICE)
public class EconomyManager {
private static final String TAG = "TARE-" + EconomyManager.class.getSimpleName();
@@ -91,6 +98,53 @@ public class EconomyManager {
}
}
+ /** @hide */
+ @TestApi
+ public static final int ENABLED_MODE_OFF = 0;
+ /** @hide */
+ public static final int ENABLED_MODE_ON = 1;
+ /**
+ * Go through the motions, tracking events, updating balances and other TARE state values,
+ * but don't use TARE to affect actual device behavior.
+ * @hide
+ */
+ @TestApi
+ public static final int ENABLED_MODE_SHADOW = 2;
+
+ /** @hide */
+ @IntDef(prefix = {"ENABLED_MODE_"}, value = {
+ ENABLED_MODE_OFF,
+ ENABLED_MODE_ON,
+ ENABLED_MODE_SHADOW,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface EnabledMode {
+ }
+
+ /** @hide */
+ public static String enabledModeToString(@EnabledMode int mode) {
+ switch (mode) {
+ case ENABLED_MODE_OFF: return "ENABLED_MODE_OFF";
+ case ENABLED_MODE_ON: return "ENABLED_MODE_ON";
+ case ENABLED_MODE_SHADOW: return "ENABLED_MODE_SHADOW";
+ default: return "ENABLED_MODE_" + mode;
+ }
+ }
+
+ /** @hide */
+ @TestApi
+ public static final String KEY_ENABLE_TARE_MODE = "enable_tare_mode";
+ /** @hide */
+ public static final String KEY_ENABLE_POLICY_ALARM = "enable_policy_alarm";
+ /** @hide */
+ public static final String KEY_ENABLE_POLICY_JOB_SCHEDULER = "enable_policy_job";
+ /** @hide */
+ public static final int DEFAULT_ENABLE_TARE_MODE = ENABLED_MODE_OFF;
+ /** @hide */
+ public static final boolean DEFAULT_ENABLE_POLICY_ALARM = true;
+ /** @hide */
+ public static final boolean DEFAULT_ENABLE_POLICY_JOB_SCHEDULER = true;
+
// Keys for AlarmManager TARE factors
/** @hide */
public static final String KEY_AM_MIN_SATIATED_BALANCE_EXEMPTED =
@@ -106,7 +160,9 @@ public class EconomyManager {
/** @hide */
public static final String KEY_AM_INITIAL_CONSUMPTION_LIMIT = "am_initial_consumption_limit";
/** @hide */
- public static final String KEY_AM_HARD_CONSUMPTION_LIMIT = "am_hard_consumption_limit";
+ public static final String KEY_AM_MIN_CONSUMPTION_LIMIT = "am_minimum_consumption_limit";
+ /** @hide */
+ public static final String KEY_AM_MAX_CONSUMPTION_LIMIT = "am_maximum_consumption_limit";
// TODO: Add AlarmManager modifier keys
/** @hide */
public static final String KEY_AM_REWARD_TOP_ACTIVITY_INSTANT =
@@ -227,14 +283,28 @@ public class EconomyManager {
public static final String KEY_JS_MIN_SATIATED_BALANCE_OTHER_APP =
"js_min_satiated_balance_other_app";
/** @hide */
+ public static final String KEY_JS_MIN_SATIATED_BALANCE_INCREMENT_APP_UPDATER =
+ "js_min_satiated_balance_increment_updater";
+ /** @hide */
public static final String KEY_JS_MAX_SATIATED_BALANCE =
"js_max_satiated_balance";
/** @hide */
public static final String KEY_JS_INITIAL_CONSUMPTION_LIMIT = "js_initial_consumption_limit";
/** @hide */
- public static final String KEY_JS_HARD_CONSUMPTION_LIMIT = "js_hard_consumption_limit";
+ public static final String KEY_JS_MIN_CONSUMPTION_LIMIT = "js_minimum_consumption_limit";
+ /** @hide */
+ public static final String KEY_JS_MAX_CONSUMPTION_LIMIT = "js_maximum_consumption_limit";
// TODO: Add JobScheduler modifier keys
/** @hide */
+ public static final String KEY_JS_REWARD_APP_INSTALL_INSTANT =
+ "js_reward_app_install_instant";
+ /** @hide */
+ public static final String KEY_JS_REWARD_APP_INSTALL_ONGOING =
+ "js_reward_app_install_ongoing";
+ /** @hide */
+ public static final String KEY_JS_REWARD_APP_INSTALL_MAX =
+ "js_reward_app_install_max";
+ /** @hide */
public static final String KEY_JS_REWARD_TOP_ACTIVITY_INSTANT =
"js_reward_top_activity_instant";
/** @hide */
@@ -352,7 +422,9 @@ public class EconomyManager {
/** @hide */
public static final long DEFAULT_AM_INITIAL_CONSUMPTION_LIMIT_CAKES = arcToCake(2880);
/** @hide */
- public static final long DEFAULT_AM_HARD_CONSUMPTION_LIMIT_CAKES = arcToCake(15_000);
+ public static final long DEFAULT_AM_MIN_CONSUMPTION_LIMIT_CAKES = arcToCake(1440);
+ /** @hide */
+ public static final long DEFAULT_AM_MAX_CONSUMPTION_LIMIT_CAKES = arcToCake(15_000);
// TODO: add AlarmManager modifier default values
/** @hide */
public static final long DEFAULT_AM_REWARD_TOP_ACTIVITY_INSTANT_CAKES = arcToCake(0);
@@ -459,10 +531,18 @@ public class EconomyManager {
/** @hide */
public static final long DEFAULT_JS_INITIAL_CONSUMPTION_LIMIT_CAKES = arcToCake(29_000);
/** @hide */
- // TODO: set hard limit based on device type (phone vs tablet vs etc) + battery size
- public static final long DEFAULT_JS_HARD_CONSUMPTION_LIMIT_CAKES = arcToCake(250_000);
+ public static final long DEFAULT_JS_MIN_CONSUMPTION_LIMIT_CAKES = arcToCake(17_000);
+ /** @hide */
+ // TODO: set maximum limit based on device type (phone vs tablet vs etc) + battery size
+ public static final long DEFAULT_JS_MAX_CONSUMPTION_LIMIT_CAKES = arcToCake(250_000);
// TODO: add JobScheduler modifier default values
/** @hide */
+ public static final long DEFAULT_JS_REWARD_APP_INSTALL_INSTANT_CAKES = arcToCake(408);
+ /** @hide */
+ public static final long DEFAULT_JS_REWARD_APP_INSTALL_ONGOING_CAKES = arcToCake(0);
+ /** @hide */
+ public static final long DEFAULT_JS_REWARD_APP_INSTALL_MAX_CAKES = arcToCake(4000);
+ /** @hide */
public static final long DEFAULT_JS_REWARD_TOP_ACTIVITY_INSTANT_CAKES = arcToCake(0);
/** @hide */
public static final long DEFAULT_JS_REWARD_TOP_ACTIVITY_ONGOING_CAKES = CAKE_IN_ARC / 2;
@@ -494,6 +574,15 @@ public class EconomyManager {
public static final long DEFAULT_JS_REWARD_OTHER_USER_INTERACTION_ONGOING_CAKES = arcToCake(0);
/** @hide */
public static final long DEFAULT_JS_REWARD_OTHER_USER_INTERACTION_MAX_CAKES = arcToCake(5000);
+ /**
+ * How many credits to increase the updating app's min satiated balance by for each app that it
+ * is responsible for updating.
+ * @hide
+ */
+ public static final long DEFAULT_JS_MIN_SATIATED_BALANCE_INCREMENT_APP_UPDATER_CAKES =
+ // Research indicates that the median time between popular app updates is 13-14 days,
+ // so adjust by 14 to amortize over that time.
+ DEFAULT_JS_REWARD_APP_INSTALL_INSTANT_CAKES / 14;
/** @hide */
public static final long DEFAULT_JS_ACTION_JOB_MAX_START_CTP_CAKES = arcToCake(3);
/** @hide */
@@ -538,4 +627,27 @@ public class EconomyManager {
public static final long DEFAULT_JS_ACTION_JOB_MIN_RUNNING_BASE_PRICE_CAKES = arcToCake(1);
/** @hide */
public static final long DEFAULT_JS_ACTION_JOB_TIMEOUT_PENALTY_BASE_PRICE_CAKES = arcToCake(60);
+
+ //////// APIs below ////////
+
+ private final IEconomyManager mService;
+
+ /** @hide */
+ public EconomyManager(IEconomyManager service) {
+ mService = service;
+ }
+
+ /**
+ * Returns the current enabled status of TARE.
+ * @hide
+ */
+ @EnabledMode
+ @TestApi
+ public int getEnabledMode() {
+ try {
+ return mService.getEnabledMode();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
}
diff --git a/apex/jobscheduler/framework/java/android/app/tare/IEconomyManager.aidl b/apex/jobscheduler/framework/java/android/app/tare/IEconomyManager.aidl
index bb150118ebb1..2be0db7a4c9f 100644
--- a/apex/jobscheduler/framework/java/android/app/tare/IEconomyManager.aidl
+++ b/apex/jobscheduler/framework/java/android/app/tare/IEconomyManager.aidl
@@ -21,4 +21,5 @@ package android.app.tare;
* {@hide}
*/
interface IEconomyManager {
+ int getEnabledMode();
}
diff --git a/apex/jobscheduler/framework/java/android/os/PowerExemptionManager.java b/apex/jobscheduler/framework/java/android/os/PowerExemptionManager.java
index 78214dc27a9f..17076bc4eea4 100644
--- a/apex/jobscheduler/framework/java/android/os/PowerExemptionManager.java
+++ b/apex/jobscheduler/framework/java/android/os/PowerExemptionManager.java
@@ -98,6 +98,15 @@ public class PowerExemptionManager {
public static final int TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED = 1;
/**
+ * Delay freezing the app when the broadcast is delivered. This flag is not required if
+ * TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED or
+ * TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED are specified, as those will
+ * already defer freezing during the allowlist duration.
+ * @hide temporarily until the next release
+ */
+ public static final int TEMPORARY_ALLOW_LIST_TYPE_APP_FREEZING_DELAYED = 1 << 2;
+
+ /**
* The list of temp allow list types.
* @hide
*/
@@ -105,6 +114,7 @@ public class PowerExemptionManager {
TEMPORARY_ALLOW_LIST_TYPE_NONE,
TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED,
TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED,
+ TEMPORARY_ALLOW_LIST_TYPE_APP_FREEZING_DELAYED
})
@Retention(RetentionPolicy.SOURCE)
public @interface TempAllowListType {}
@@ -216,6 +226,11 @@ public class PowerExemptionManager {
* Set temp-allow-list for transferring accounts between users.
*/
public static final int REASON_ACCOUNT_TRANSFER = 104;
+ /**
+ * Set temp-allow-list for server push messaging that can be deferred.
+ * @hide temporarily until the next release
+ */
+ public static final int REASON_PUSH_MESSAGING_DEFERRABLE = 105;
/* Reason code range 200-299 are reserved for broadcast actions */
/**
@@ -391,6 +406,19 @@ public class PowerExemptionManager {
*/
public static final int REASON_MEDIA_NOTIFICATION_TRANSFER = 325;
+ /**
+ * Package installer.
+ * @hide
+ */
+ public static final int REASON_PACKAGE_INSTALLER = 326;
+
+ /**
+ * {@link android.app.AppOpsManager#OP_SYSTEM_EXEMPT_FROM_POWER_RESTRICTIONS}
+ * set to MODE_ALLOWED
+ * @hide
+ */
+ public static final int REASON_SYSTEM_EXEMPT_APP_OP = 327;
+
/** @hide The app requests out-out. */
public static final int REASON_OPT_OUT_REQUESTED = 1000;
@@ -436,6 +464,7 @@ public class PowerExemptionManager {
REASON_PUSH_MESSAGING_OVER_QUOTA,
REASON_ACTIVITY_RECOGNITION,
REASON_ACCOUNT_TRANSFER,
+ REASON_PUSH_MESSAGING_DEFERRABLE,
REASON_BOOT_COMPLETED,
REASON_PRE_BOOT_COMPLETED,
REASON_LOCKED_BOOT_COMPLETED,
@@ -472,6 +501,7 @@ public class PowerExemptionManager {
REASON_DISALLOW_APPS_CONTROL,
REASON_ACTIVE_DEVICE_ADMIN,
REASON_MEDIA_NOTIFICATION_TRANSFER,
+ REASON_PACKAGE_INSTALLER,
})
@Retention(RetentionPolicy.SOURCE)
public @interface ReasonCode {}
@@ -767,6 +797,8 @@ public class PowerExemptionManager {
return "ACTIVITY_RECOGNITION";
case REASON_ACCOUNT_TRANSFER:
return "REASON_ACCOUNT_TRANSFER";
+ case REASON_PUSH_MESSAGING_DEFERRABLE:
+ return "PUSH_MESSAGING_DEFERRABLE";
case REASON_BOOT_COMPLETED:
return "BOOT_COMPLETED";
case REASON_PRE_BOOT_COMPLETED:
@@ -839,6 +871,8 @@ public class PowerExemptionManager {
return "REASON_OPT_OUT_REQUESTED";
case REASON_MEDIA_NOTIFICATION_TRANSFER:
return "REASON_MEDIA_NOTIFICATION_TRANSFER";
+ case REASON_PACKAGE_INSTALLER:
+ return "REASON_PACKAGE_INSTALLER";
default:
return "(unknown:" + reasonCode + ")";
}
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 2682dd75ca73..6c8af39015f5 100644
--- a/apex/jobscheduler/framework/java/com/android/server/job/JobSchedulerInternal.java
+++ b/apex/jobscheduler/framework/java/com/android/server/job/JobSchedulerInternal.java
@@ -16,6 +16,7 @@
package com.android.server.job;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.job.JobInfo;
import android.app.job.JobParameters;
@@ -30,15 +31,17 @@ import java.util.List;
public interface JobSchedulerInternal {
/**
- * Returns a list of pending jobs scheduled by the system service.
+ * Returns a list of jobs scheduled by the system service for itself.
*/
- List<JobInfo> getSystemScheduledPendingJobs();
+ List<JobInfo> getSystemScheduledOwnJobs(@Nullable String namespace);
/**
* Cancel the jobs for a given uid (e.g. when app data is cleared)
+ *
+ * @param includeProxiedJobs Include jobs scheduled for this UID by other apps
*/
- void cancelJobsForUid(int uid, @JobParameters.StopReason int reason, int debugReasonCode,
- String debugReason);
+ void cancelJobsForUid(int uid, boolean includeProxiedJobs,
+ @JobParameters.StopReason int reason, int debugReasonCode, String debugReason);
/**
* These are for activity manager to communicate to use what is currently performing backups.
@@ -56,6 +59,23 @@ public interface JobSchedulerInternal {
*/
void reportAppUsage(String packageName, int userId);
+ /** @return {@code true} if the app is considered buggy from JobScheduler's perspective. */
+ boolean isAppConsideredBuggy(int callingUserId, @NonNull String callingPackageName,
+ int timeoutBlameUserId, @NonNull String timeoutBlamePackageName);
+
+ /**
+ * @return {@code true} if the given notification is associated with any user-initiated jobs.
+ */
+ boolean isNotificationAssociatedWithAnyUserInitiatedJobs(int notificationId,
+ int userId, @NonNull String packageName);
+
+ /**
+ * @return {@code true} if the given notification channel is associated with any user-initiated
+ * jobs.
+ */
+ boolean isNotificationChannelAssociatedWithAnyUserInitiatedJobs(
+ @NonNull String notificationChannel, int userId, @NonNull String packageName);
+
/**
* Report a snapshot of sync-related jobs back to the sync manager
*/
diff --git a/apex/jobscheduler/service/java/com/android/server/JobSchedulerBackgroundThread.java b/apex/jobscheduler/service/java/com/android/server/AppSchedulingModuleThread.java
index a413f7b1f3ca..d36f44fad81b 100644
--- a/apex/jobscheduler/service/java/com/android/server/JobSchedulerBackgroundThread.java
+++ b/apex/jobscheduler/service/java/com/android/server/AppSchedulingModuleThread.java
@@ -20,29 +20,30 @@ import android.os.Handler;
import android.os.HandlerExecutor;
import android.os.HandlerThread;
import android.os.Looper;
+import android.os.Process;
import android.os.Trace;
import java.util.concurrent.Executor;
/**
- * Shared singleton background thread.
+ * Shared singleton default priority thread for the app scheduling module.
*
* @see com.android.internal.os.BackgroundThread
*/
-public final class JobSchedulerBackgroundThread extends HandlerThread {
+public final class AppSchedulingModuleThread extends HandlerThread {
private static final long SLOW_DISPATCH_THRESHOLD_MS = 10_000;
private static final long SLOW_DELIVERY_THRESHOLD_MS = 30_000;
- private static JobSchedulerBackgroundThread sInstance;
+ private static AppSchedulingModuleThread sInstance;
private static Handler sHandler;
private static Executor sHandlerExecutor;
- private JobSchedulerBackgroundThread() {
- super("jobscheduler.bg", android.os.Process.THREAD_PRIORITY_BACKGROUND);
+ private AppSchedulingModuleThread() {
+ super("appscheduling.default", Process.THREAD_PRIORITY_DEFAULT);
}
private static void ensureThreadLocked() {
if (sInstance == null) {
- sInstance = new JobSchedulerBackgroundThread();
+ sInstance = new AppSchedulingModuleThread();
sInstance.start();
final Looper looper = sInstance.getLooper();
looper.setTraceTag(Trace.TRACE_TAG_SYSTEM_SERVER);
@@ -53,25 +54,25 @@ public final class JobSchedulerBackgroundThread extends HandlerThread {
}
}
- /** Returns the JobSchedulerBackgroundThread singleton */
- public static JobSchedulerBackgroundThread get() {
- synchronized (JobSchedulerBackgroundThread.class) {
+ /** Returns the AppSchedulingModuleThread singleton */
+ public static AppSchedulingModuleThread get() {
+ synchronized (AppSchedulingModuleThread.class) {
ensureThreadLocked();
return sInstance;
}
}
- /** Returns the singleton handler for JobSchedulerBackgroundThread */
+ /** Returns the singleton handler for AppSchedulingModuleThread */
public static Handler getHandler() {
- synchronized (JobSchedulerBackgroundThread.class) {
+ synchronized (AppSchedulingModuleThread.class) {
ensureThreadLocked();
return sHandler;
}
}
- /** Returns the singleton handler executor for JobSchedulerBackgroundThread */
+ /** Returns the singleton handler executor for AppSchedulingModuleThread */
public static Executor getExecutor() {
- synchronized (JobSchedulerBackgroundThread.class) {
+ synchronized (AppSchedulingModuleThread.class) {
ensureThreadLocked();
return sHandlerExecutor;
}
diff --git a/apex/jobscheduler/service/java/com/android/server/AppStateTrackerImpl.java b/apex/jobscheduler/service/java/com/android/server/AppStateTrackerImpl.java
index fab5b5fd6933..e08200b055d8 100644
--- a/apex/jobscheduler/service/java/com/android/server/AppStateTrackerImpl.java
+++ b/apex/jobscheduler/service/java/com/android/server/AppStateTrackerImpl.java
@@ -22,7 +22,6 @@ import android.app.ActivityManagerInternal.AppBackgroundRestrictionListener;
import android.app.AppOpsManager;
import android.app.AppOpsManager.PackageOps;
import android.app.IActivityManager;
-import android.app.IUidObserver;
import android.app.usage.UsageStatsManager;
import android.content.BroadcastReceiver;
import android.content.Context;
@@ -54,6 +53,7 @@ import com.android.internal.app.IAppOpsCallback;
import com.android.internal.app.IAppOpsService;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.StatLogger;
+import com.android.modules.expresslog.Counter;
import com.android.server.AppStateTrackerProto.ExemptedPackage;
import com.android.server.AppStateTrackerProto.RunAnyInBackgroundRestrictedPackages;
import com.android.server.usage.AppStandbyInternal;
@@ -79,6 +79,9 @@ import java.util.Set;
public class AppStateTrackerImpl implements AppStateTracker {
private static final boolean DEBUG = false;
+ private static final String APP_RESTRICTION_COUNTER_METRIC_ID =
+ "battery.value_app_background_restricted";
+
private final Object mLock = new Object();
private final Context mContext;
@@ -158,17 +161,11 @@ public class AppStateTrackerImpl implements AppStateTracker {
boolean mForceAllAppStandbyForSmallBattery;
/**
- * True if the forced app standby feature is enabled in settings
- */
- @GuardedBy("mLock")
- boolean mForcedAppStandbyEnabled;
-
- /**
* A lock-free set of (uid, packageName) pairs in background restricted mode.
*
* <p>
- * It's bascially shadowing the {@link #mRunAnyRestrictedPackages} together with
- * the {@link #mForcedAppStandbyEnabled} - mutations on them would result in copy-on-write.
+ * It's basically shadowing the {@link #mRunAnyRestrictedPackages}, any mutations on it would
+ * result in copy-on-write.
* </p>
*/
volatile Set<Pair<Integer, String>> mBackgroundRestrictedUidPackages = Collections.emptySet();
@@ -200,10 +197,9 @@ public class AppStateTrackerImpl implements AppStateTracker {
int TEMP_EXEMPTION_LIST_CHANGED = 5;
int EXEMPTED_BUCKET_CHANGED = 6;
int FORCE_ALL_CHANGED = 7;
- int FORCE_APP_STANDBY_FEATURE_FLAG_CHANGED = 8;
- int IS_UID_ACTIVE_CACHED = 9;
- int IS_UID_ACTIVE_RAW = 10;
+ int IS_UID_ACTIVE_CACHED = 8;
+ int IS_UID_ACTIVE_RAW = 9;
}
private final StatLogger mStatLogger = new StatLogger(new String[] {
@@ -215,7 +211,6 @@ public class AppStateTrackerImpl implements AppStateTracker {
"TEMP_EXEMPTION_LIST_CHANGED",
"EXEMPTED_BUCKET_CHANGED",
"FORCE_ALL_CHANGED",
- "FORCE_APP_STANDBY_FEATURE_FLAG_CHANGED",
"IS_UID_ACTIVE_CACHED",
"IS_UID_ACTIVE_RAW",
@@ -228,18 +223,10 @@ public class AppStateTrackerImpl implements AppStateTracker {
}
void register() {
- mContext.getContentResolver().registerContentObserver(
- Settings.Global.getUriFor(Settings.Global.FORCED_APP_STANDBY_ENABLED),
- false, this);
-
mContext.getContentResolver().registerContentObserver(Settings.Global.getUriFor(
Settings.Global.FORCED_APP_STANDBY_FOR_SMALL_BATTERY_ENABLED), false, this);
}
- boolean isForcedAppStandbyEnabled() {
- return injectGetGlobalSettingInt(Settings.Global.FORCED_APP_STANDBY_ENABLED, 1) == 1;
- }
-
boolean isForcedAppStandbyForSmallBatteryEnabled() {
return injectGetGlobalSettingInt(
Settings.Global.FORCED_APP_STANDBY_FOR_SMALL_BATTERY_ENABLED, 0) == 1;
@@ -247,21 +234,7 @@ public class AppStateTrackerImpl implements AppStateTracker {
@Override
public void onChange(boolean selfChange, Uri uri) {
- if (Settings.Global.getUriFor(Settings.Global.FORCED_APP_STANDBY_ENABLED).equals(uri)) {
- final boolean enabled = isForcedAppStandbyEnabled();
- synchronized (mLock) {
- if (mForcedAppStandbyEnabled == enabled) {
- return;
- }
- mForcedAppStandbyEnabled = enabled;
- updateBackgroundRestrictedUidPackagesLocked();
- if (DEBUG) {
- Slog.d(TAG, "Forced app standby feature flag changed: "
- + mForcedAppStandbyEnabled);
- }
- }
- mHandler.notifyForcedAppStandbyFeatureFlagChanged();
- } else if (Settings.Global.getUriFor(
+ if (Settings.Global.getUriFor(
Settings.Global.FORCED_APP_STANDBY_FOR_SMALL_BATTERY_ENABLED).equals(uri)) {
final boolean enabled = isForcedAppStandbyForSmallBatteryEnabled();
synchronized (mLock) {
@@ -455,6 +428,12 @@ public class AppStateTrackerImpl implements AppStateTracker {
*/
public void removeAlarmsForUid(int uid) {
}
+
+ /**
+ * Called when a uid goes into cached, so its alarms using a listener should be removed.
+ */
+ public void handleUidCachedChanged(int uid, boolean cached) {
+ }
}
public AppStateTrackerImpl(Context context, Looper looper) {
@@ -515,7 +494,6 @@ public class AppStateTrackerImpl implements AppStateTracker {
mFlagsObserver = new FeatureFlagsObserver();
mFlagsObserver.register();
- mForcedAppStandbyEnabled = mFlagsObserver.isForcedAppStandbyEnabled();
mForceAllAppStandbyForSmallBattery =
mFlagsObserver.isForcedAppStandbyForSmallBatteryEnabled();
mStandbyTracker = new StandbyTracker();
@@ -527,7 +505,8 @@ public class AppStateTrackerImpl implements AppStateTracker {
mIActivityManager.registerUidObserver(new UidObserver(),
ActivityManager.UID_OBSERVER_GONE
| ActivityManager.UID_OBSERVER_IDLE
- | ActivityManager.UID_OBSERVER_ACTIVE,
+ | ActivityManager.UID_OBSERVER_ACTIVE
+ | ActivityManager.UID_OBSERVER_CACHED,
ActivityManager.PROCESS_STATE_UNKNOWN, null);
mAppOpsService.startWatchingMode(TARGET_OP, null,
new AppOpsWatcher());
@@ -636,14 +615,10 @@ public class AppStateTrackerImpl implements AppStateTracker {
/**
* Update the {@link #mBackgroundRestrictedUidPackages} upon mutations on
- * {@link #mRunAnyRestrictedPackages} or {@link #mForcedAppStandbyEnabled}.
+ * {@link #mRunAnyRestrictedPackages}.
*/
@GuardedBy("mLock")
private void updateBackgroundRestrictedUidPackagesLocked() {
- if (!mForcedAppStandbyEnabled) {
- mBackgroundRestrictedUidPackages = Collections.emptySet();
- return;
- }
Set<Pair<Integer, String>> fasUidPkgs = new ArraySet<>();
for (int i = 0, size = mRunAnyRestrictedPackages.size(); i < size; i++) {
fasUidPkgs.add(mRunAnyRestrictedPackages.valueAt(i));
@@ -744,11 +719,7 @@ public class AppStateTrackerImpl implements AppStateTracker {
return true;
}
- private final class UidObserver extends IUidObserver.Stub {
- @Override
- public void onUidStateChanged(int uid, int procState, long procStateSeq, int capability) {
- }
-
+ private final class UidObserver extends android.app.UidObserver {
@Override
public void onUidActive(int uid) {
mHandler.onUidActive(uid);
@@ -766,10 +737,7 @@ public class AppStateTrackerImpl implements AppStateTracker {
@Override
public void onUidCachedChanged(int uid, boolean cached) {
- }
-
- @Override
- public void onUidProcAdjChanged(int uid) {
+ mHandler.onUidCachedChanged(uid, cached);
}
}
@@ -783,6 +751,9 @@ public class AppStateTrackerImpl implements AppStateTracker {
} catch (RemoteException e) {
// Shouldn't happen
}
+ if (restricted) {
+ Counter.logIncrementWithUid(APP_RESTRICTION_COUNTER_METRIC_ID, uid);
+ }
synchronized (mLock) {
if (updateForcedAppStandbyUidPackageLocked(uid, packageName, restricted)) {
mHandler.notifyRunAnyAppOpsChanged(uid, packageName);
@@ -821,19 +792,21 @@ public class AppStateTrackerImpl implements AppStateTracker {
private class MyHandler extends Handler {
private static final int MSG_UID_ACTIVE_STATE_CHANGED = 0;
+ // Unused ids 1, 2.
private static final int MSG_RUN_ANY_CHANGED = 3;
private static final int MSG_ALL_UNEXEMPTED = 4;
private static final int MSG_ALL_EXEMPTION_LIST_CHANGED = 5;
private static final int MSG_TEMP_EXEMPTION_LIST_CHANGED = 6;
private static final int MSG_FORCE_ALL_CHANGED = 7;
private static final int MSG_USER_REMOVED = 8;
- private static final int MSG_FORCE_APP_STANDBY_FEATURE_FLAG_CHANGED = 9;
+ // Unused id 9.
private static final int MSG_EXEMPTED_BUCKET_CHANGED = 10;
private static final int MSG_AUTO_RESTRICTED_BUCKET_FEATURE_FLAG_CHANGED = 11;
private static final int MSG_ON_UID_ACTIVE = 12;
private static final int MSG_ON_UID_GONE = 13;
private static final int MSG_ON_UID_IDLE = 14;
+ private static final int MSG_ON_UID_CACHED = 15;
MyHandler(Looper looper) {
super(looper);
@@ -867,11 +840,6 @@ public class AppStateTrackerImpl implements AppStateTracker {
obtainMessage(MSG_FORCE_ALL_CHANGED).sendToTarget();
}
- public void notifyForcedAppStandbyFeatureFlagChanged() {
- removeMessages(MSG_FORCE_APP_STANDBY_FEATURE_FLAG_CHANGED);
- obtainMessage(MSG_FORCE_APP_STANDBY_FEATURE_FLAG_CHANGED).sendToTarget();
- }
-
public void notifyExemptedBucketChanged() {
removeMessages(MSG_EXEMPTED_BUCKET_CHANGED);
obtainMessage(MSG_EXEMPTED_BUCKET_CHANGED).sendToTarget();
@@ -899,6 +867,10 @@ public class AppStateTrackerImpl implements AppStateTracker {
obtainMessage(MSG_ON_UID_IDLE, uid, disabled ? 1 : 0).sendToTarget();
}
+ public void onUidCachedChanged(int uid, boolean cached) {
+ obtainMessage(MSG_ON_UID_CACHED, uid, cached ? 1 : 0).sendToTarget();
+ }
+
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
@@ -966,22 +938,6 @@ public class AppStateTrackerImpl implements AppStateTracker {
mStatLogger.logDurationStat(Stats.FORCE_ALL_CHANGED, start);
return;
- case MSG_FORCE_APP_STANDBY_FEATURE_FLAG_CHANGED:
- // Feature flag for forced app standby changed.
- final boolean unblockAlarms;
- synchronized (mLock) {
- unblockAlarms = !mForcedAppStandbyEnabled;
- }
- for (Listener l : cloneListeners()) {
- l.updateAllJobs();
- if (unblockAlarms) {
- l.unblockAllUnrestrictedAlarms();
- }
- }
- mStatLogger.logDurationStat(
- Stats.FORCE_APP_STANDBY_FEATURE_FLAG_CHANGED, start);
- return;
-
case MSG_USER_REMOVED:
handleUserRemoved(msg.arg1);
return;
@@ -1008,6 +964,15 @@ public class AppStateTrackerImpl implements AppStateTracker {
handleUidDisabled(msg.arg1);
}
return;
+ case MSG_ON_UID_CACHED:
+ handleUidCached(msg.arg1, (msg.arg2 != 0));
+ return;
+ }
+ }
+
+ private void handleUidCached(int uid, boolean cached) {
+ for (Listener l : cloneListeners()) {
+ l.handleUidCachedChanged(uid, cached);
}
}
@@ -1164,8 +1129,7 @@ public class AppStateTrackerImpl implements AppStateTracker {
// If apps will be put into restricted standby bucket automatically on user-forced
// app standby, instead of blocking alarms completely, let the restricted standby bucket
// policy take care of it.
- return (mForcedAppStandbyEnabled
- && !mActivityManagerInternal.isBgAutoRestrictedBucketFeatureFlagEnabled()
+ return (!mActivityManagerInternal.isBgAutoRestrictedBucketFeatureFlagEnabled()
&& isRunAnyRestrictedLocked(uid, packageName));
}
}
@@ -1210,8 +1174,7 @@ public class AppStateTrackerImpl implements AppStateTracker {
// If apps will be put into restricted standby bucket automatically on user-forced
// app standby, instead of blocking jobs completely, let the restricted standby bucket
// policy take care of it.
- if (mForcedAppStandbyEnabled
- && !mActivityManagerInternal.isBgAutoRestrictedBucketFeatureFlagEnabled()
+ if (!mActivityManagerInternal.isBgAutoRestrictedBucketFeatureFlagEnabled()
&& isRunAnyRestrictedLocked(uid, packageName)) {
return true;
}
@@ -1321,8 +1284,6 @@ public class AppStateTrackerImpl implements AppStateTracker {
pw.println("Current AppStateTracker State:");
pw.increaseIndent();
- pw.println("Forced App Standby Feature enabled: " + mForcedAppStandbyEnabled);
-
pw.print("Force all apps standby: ");
pw.println(isForceAllAppsStandbyEnabled());
@@ -1400,8 +1361,6 @@ public class AppStateTrackerImpl implements AppStateTracker {
synchronized (mLock) {
final long token = proto.start(fieldId);
- proto.write(AppStateTrackerProto.FORCED_APP_STANDBY_FEATURE_ENABLED,
- mForcedAppStandbyEnabled);
proto.write(AppStateTrackerProto.FORCE_ALL_APPS_STANDBY,
isForceAllAppsStandbyEnabled());
proto.write(AppStateTrackerProto.IS_SMALL_BATTERY_DEVICE, isSmallBatteryDevice());
diff --git a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java
index bd475e9dd734..8316a2625ccd 100644
--- a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java
+++ b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java
@@ -29,8 +29,10 @@ import android.annotation.SuppressLint;
import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
import android.app.AlarmManager;
+import android.app.BroadcastOptions;
import android.content.BroadcastReceiver;
import android.content.Context;
+import android.content.IIntentReceiver;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
@@ -77,6 +79,9 @@ import android.os.SystemClock;
import android.os.Trace;
import android.os.UserHandle;
import android.provider.DeviceConfig;
+import android.telephony.TelephonyCallback;
+import android.telephony.TelephonyManager;
+import android.telephony.emergency.EmergencyNumber;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.AtomicFile;
@@ -95,6 +100,7 @@ import com.android.internal.util.ArrayUtils;
import com.android.internal.util.DumpUtils;
import com.android.internal.util.FastXmlSerializer;
import com.android.internal.util.XmlUtils;
+import com.android.modules.expresslog.Counter;
import com.android.server.am.BatteryStatsService;
import com.android.server.deviceidle.ConstraintController;
import com.android.server.deviceidle.DeviceIdleConstraintTracker;
@@ -144,15 +150,17 @@ import java.util.stream.Collectors;
label="deep";
STATE_ACTIVE [
- label="STATE_ACTIVE\nScreen on OR Charging OR Alarm going off soon",
+ label="STATE_ACTIVE\nScreen on OR charging OR alarm going off soon\n"
+ + "OR active emergency call",
color=black,shape=diamond
]
STATE_INACTIVE [
- label="STATE_INACTIVE\nScreen off AND Not charging",color=black,shape=diamond
+ label="STATE_INACTIVE\nScreen off AND not charging AND no active emergency call",
+ color=black,shape=diamond
]
STATE_QUICK_DOZE_DELAY [
label="STATE_QUICK_DOZE_DELAY\n"
- + "Screen off AND Not charging\n"
+ + "Screen off AND not charging AND no active emergency call\n"
+ "Location, motion detection, and significant motion monitoring turned off",
color=black,shape=diamond
]
@@ -235,14 +243,15 @@ import java.util.stream.Collectors;
label="light"
LIGHT_STATE_ACTIVE [
- label="LIGHT_STATE_ACTIVE\nScreen on OR Charging OR Alarm going off soon",
+ label="LIGHT_STATE_ACTIVE\n"
+ + "Screen on OR charging OR alarm going off soon OR active emergency call",
color=black,shape=diamond
]
LIGHT_STATE_INACTIVE [
- label="LIGHT_STATE_INACTIVE\nScreen off AND Not charging",
+ label="LIGHT_STATE_INACTIVE\nScreen off AND not charging AND no active emergency call",
color=black,shape=diamond
]
- LIGHT_STATE_IDLE [label="LIGHT_STATE_IDLE\n",color=blue,shape=oval]
+ LIGHT_STATE_IDLE [label="LIGHT_STATE_IDLE\n",color=red,shape=box]
LIGHT_STATE_WAITING_FOR_NETWORK [
label="LIGHT_STATE_WAITING_FOR_NETWORK\n"
+ "Coming out of LIGHT_STATE_IDLE, waiting for network",
@@ -292,6 +301,12 @@ public class DeviceIdleController extends SystemService
implements AnyMotionDetector.DeviceIdleCallback {
private static final String TAG = "DeviceIdleController";
+ private static final String USER_ALLOWLIST_ADDITION_METRIC_ID =
+ "battery.value_app_added_to_power_allowlist";
+
+ private static final String USER_ALLOWLIST_REMOVAL_METRIC_ID =
+ "battery.value_app_removed_from_power_allowlist";
+
private static final boolean DEBUG = false;
private static final boolean COMPRESS_TIME = false;
@@ -311,9 +326,17 @@ public class DeviceIdleController extends SystemService
private SensorManager mSensorManager;
private final boolean mUseMotionSensor;
private Sensor mMotionSensor;
+ private final boolean mIsLocationPrefetchEnabled;
+ @Nullable
private LocationRequest mLocationRequest;
private Intent mIdleIntent;
+ private Bundle mIdleIntentOptions;
private Intent mLightIdleIntent;
+ private Bundle mLightIdleIntentOptions;
+ private Intent mPowerSaveWhitelistChangedIntent;
+ private Bundle mPowerSaveWhitelistChangedOptions;
+ private Intent mPowerSaveTempWhitelistChangedIntent;
+ private Bundle mPowerSaveTempWhilelistChangedOptions;
private AnyMotionDetector mAnyMotionDetector;
private final AppStateTrackerImpl mAppStateTracker;
@GuardedBy("this")
@@ -341,7 +364,7 @@ public class DeviceIdleController extends SystemService
@GuardedBy("this")
private boolean mHasGps;
@GuardedBy("this")
- private boolean mHasNetworkLocation;
+ private boolean mHasFusedLocation;
@GuardedBy("this")
private Location mLastGenericLocation;
@GuardedBy("this")
@@ -403,6 +426,7 @@ public class DeviceIdleController extends SystemService
private static final int ACTIVE_REASON_FROM_BINDER_CALL = 5;
private static final int ACTIVE_REASON_FORCED = 6;
private static final int ACTIVE_REASON_ALARM = 7;
+ private static final int ACTIVE_REASON_EMERGENCY_CALL = 8;
@VisibleForTesting
static final int SET_IDLE_FACTOR_RESULT_UNINIT = -1;
@VisibleForTesting
@@ -482,9 +506,9 @@ public class DeviceIdleController extends SystemService
@GuardedBy("this")
private long mNextLightIdleDelay;
@GuardedBy("this")
- private long mNextLightAlarmTime;
+ private long mNextLightIdleDelayFlex;
@GuardedBy("this")
- private long mNextLightMaintenanceAlarmTime;
+ private long mNextLightAlarmTime;
@GuardedBy("this")
private long mNextSensingTimeoutAlarmTime;
@@ -681,15 +705,6 @@ public class DeviceIdleController extends SystemService
}
};
- private final AlarmManager.OnAlarmListener mLightMaintenanceAlarmListener = () -> {
- if (DEBUG) {
- Slog.d(TAG, "Light maintenance alarm fired");
- }
- synchronized (DeviceIdleController.this) {
- stepLightIdleStateLocked("s:alarm");
- }
- };
-
/** AlarmListener to start monitoring motion if there are registered stationary listeners. */
private final AlarmManager.OnAlarmListener mMotionRegistrationAlarmListener = () -> {
synchronized (DeviceIdleController.this) {
@@ -740,8 +755,10 @@ public class DeviceIdleController extends SystemService
}
};
- private final BroadcastReceiver mIdleStartedDoneReceiver = new BroadcastReceiver() {
- @Override public void onReceive(Context context, Intent intent) {
+ private final IIntentReceiver mIdleStartedDoneReceiver = new IIntentReceiver.Stub() {
+ @Override
+ public void performReceive(Intent intent, int resultCode, String data, Bundle extras,
+ boolean ordered, boolean sticky, int sendingUser) {
// When coming out of a deep idle, we will add in some delay before we allow
// the system to settle down and finish the maintenance window. This is
// to give a chance for any pending work to be scheduled.
@@ -764,6 +781,8 @@ public class DeviceIdleController extends SystemService
}
};
+ private final EmergencyCallListener mEmergencyCallListener = new EmergencyCallListener();
+
/** Post stationary status only to this listener. */
private void postStationaryStatus(DeviceIdleInternal.StationaryListener listener) {
mHandler.obtainMessage(MSG_REPORT_STATIONARY_STATUS, listener).sendToTarget();
@@ -952,6 +971,9 @@ public class DeviceIdleController extends SystemService
private static final String KEY_LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT =
"light_after_inactive_to";
private static final String KEY_LIGHT_IDLE_TIMEOUT = "light_idle_to";
+ private static final String KEY_LIGHT_IDLE_TIMEOUT_INITIAL_FLEX =
+ "light_idle_to_initial_flex";
+ private static final String KEY_LIGHT_IDLE_TIMEOUT_MAX_FLEX = "light_max_idle_to_flex";
private static final String KEY_LIGHT_IDLE_FACTOR = "light_idle_factor";
private static final String KEY_LIGHT_MAX_IDLE_TIMEOUT = "light_max_idle_to";
private static final String KEY_LIGHT_IDLE_MAINTENANCE_MIN_BUDGET =
@@ -1000,6 +1022,10 @@ public class DeviceIdleController extends SystemService
!COMPRESS_TIME ? 4 * 60 * 1000L : 30 * 1000L;
private long mDefaultLightIdleTimeout =
!COMPRESS_TIME ? 5 * 60 * 1000L : 15 * 1000L;
+ private long mDefaultLightIdleTimeoutInitialFlex =
+ !COMPRESS_TIME ? 60 * 1000L : 5 * 1000L;
+ private long mDefaultLightIdleTimeoutMaxFlex =
+ !COMPRESS_TIME ? 15 * 60 * 1000L : 60 * 1000L;
private float mDefaultLightIdleFactor = 2f;
private long mDefaultLightMaxIdleTimeout =
!COMPRESS_TIME ? 15 * 60 * 1000L : 60 * 1000L;
@@ -1074,6 +1100,22 @@ public class DeviceIdleController extends SystemService
public long LIGHT_IDLE_TIMEOUT = mDefaultLightIdleTimeout;
/**
+ * This is the initial alarm window size that we will tolerate for light idle maintenance
+ * timing.
+ *
+ * @see #KEY_LIGHT_IDLE_TIMEOUT_MAX_FLEX
+ * @see #mNextLightIdleDelayFlex
+ */
+ public long LIGHT_IDLE_TIMEOUT_INITIAL_FLEX = mDefaultLightIdleTimeoutInitialFlex;
+
+ /**
+ * This is the maximum value that {@link #mNextLightIdleDelayFlex} should take.
+ *
+ * @see #KEY_LIGHT_IDLE_TIMEOUT_INITIAL_FLEX
+ */
+ public long LIGHT_IDLE_TIMEOUT_MAX_FLEX = mDefaultLightIdleTimeoutMaxFlex;
+
+ /**
* Scaling factor to apply to the light idle mode time each time we complete a cycle.
*
* @see #KEY_LIGHT_IDLE_FACTOR
@@ -1294,7 +1336,7 @@ public class DeviceIdleController extends SystemService
IDLE_AFTER_INACTIVE_TIMEOUT = DEFAULT_IDLE_AFTER_INACTIVE_TIMEOUT_SMALL_BATTERY;
}
DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_DEVICE_IDLE,
- JobSchedulerBackgroundThread.getExecutor(), this);
+ AppSchedulingModuleThread.getExecutor(), this);
// Load all the constants.
onPropertiesChanged(DeviceConfig.getProperties(DeviceConfig.NAMESPACE_DEVICE_IDLE));
}
@@ -1311,6 +1353,14 @@ public class DeviceIdleController extends SystemService
mDefaultLightIdleTimeout = getTimeout(
res.getInteger(com.android.internal.R.integer.device_idle_light_idle_to_ms),
mDefaultLightIdleTimeout);
+ mDefaultLightIdleTimeoutInitialFlex = getTimeout(
+ res.getInteger(
+ com.android.internal.R.integer.device_idle_light_idle_to_init_flex_ms),
+ mDefaultLightIdleTimeoutInitialFlex);
+ mDefaultLightIdleTimeoutMaxFlex = getTimeout(
+ res.getInteger(
+ com.android.internal.R.integer.device_idle_light_idle_to_max_flex_ms),
+ mDefaultLightIdleTimeoutMaxFlex);
mDefaultLightIdleFactor = res.getFloat(
com.android.internal.R.integer.device_idle_light_idle_factor);
mDefaultLightMaxIdleTimeout = getTimeout(
@@ -1390,6 +1440,8 @@ public class DeviceIdleController extends SystemService
FLEX_TIME_SHORT = mDefaultFlexTimeShort;
LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT = mDefaultLightIdleAfterInactiveTimeout;
LIGHT_IDLE_TIMEOUT = mDefaultLightIdleTimeout;
+ LIGHT_IDLE_TIMEOUT_INITIAL_FLEX = mDefaultLightIdleTimeoutInitialFlex;
+ LIGHT_IDLE_TIMEOUT_MAX_FLEX = mDefaultLightIdleTimeoutMaxFlex;
LIGHT_IDLE_FACTOR = mDefaultLightIdleFactor;
LIGHT_MAX_IDLE_TIMEOUT = mDefaultLightMaxIdleTimeout;
LIGHT_IDLE_MAINTENANCE_MIN_BUDGET = mDefaultLightIdleMaintenanceMinBudget;
@@ -1447,6 +1499,16 @@ public class DeviceIdleController extends SystemService
LIGHT_IDLE_TIMEOUT = properties.getLong(
KEY_LIGHT_IDLE_TIMEOUT, mDefaultLightIdleTimeout);
break;
+ case KEY_LIGHT_IDLE_TIMEOUT_INITIAL_FLEX:
+ LIGHT_IDLE_TIMEOUT_INITIAL_FLEX = properties.getLong(
+ KEY_LIGHT_IDLE_TIMEOUT_INITIAL_FLEX,
+ mDefaultLightIdleTimeoutInitialFlex);
+ break;
+ case KEY_LIGHT_IDLE_TIMEOUT_MAX_FLEX:
+ LIGHT_IDLE_TIMEOUT_MAX_FLEX = properties.getLong(
+ KEY_LIGHT_IDLE_TIMEOUT_MAX_FLEX,
+ mDefaultLightIdleTimeoutMaxFlex);
+ break;
case KEY_LIGHT_IDLE_FACTOR:
LIGHT_IDLE_FACTOR = Math.max(1, properties.getFloat(
KEY_LIGHT_IDLE_FACTOR, mDefaultLightIdleFactor));
@@ -1603,6 +1665,14 @@ public class DeviceIdleController extends SystemService
TimeUtils.formatDuration(LIGHT_IDLE_TIMEOUT, pw);
pw.println();
+ pw.print(" "); pw.print(KEY_LIGHT_IDLE_TIMEOUT_INITIAL_FLEX); pw.print("=");
+ TimeUtils.formatDuration(LIGHT_IDLE_TIMEOUT_INITIAL_FLEX, pw);
+ pw.println();
+
+ pw.print(" "); pw.print(KEY_LIGHT_IDLE_TIMEOUT_MAX_FLEX); pw.print("=");
+ TimeUtils.formatDuration(LIGHT_IDLE_TIMEOUT_MAX_FLEX, pw);
+ pw.println();
+
pw.print(" "); pw.print(KEY_LIGHT_IDLE_FACTOR); pw.print("=");
pw.print(LIGHT_IDLE_FACTOR);
pw.println();
@@ -1795,10 +1865,12 @@ public class DeviceIdleController extends SystemService
} catch (RemoteException e) {
}
if (deepChanged) {
- getContext().sendBroadcastAsUser(mIdleIntent, UserHandle.ALL);
+ getContext().sendBroadcastAsUser(mIdleIntent, UserHandle.ALL,
+ null /* receiverPermission */, mIdleIntentOptions);
}
if (lightChanged) {
- getContext().sendBroadcastAsUser(mLightIdleIntent, UserHandle.ALL);
+ getContext().sendBroadcastAsUser(mLightIdleIntent, UserHandle.ALL,
+ null /* receiverPermission */, mLightIdleIntentOptions);
}
EventLogTags.writeDeviceIdleOnComplete();
mGoingIdleWakeLock.release();
@@ -1816,13 +1888,15 @@ public class DeviceIdleController extends SystemService
}
if (deepChanged) {
incActiveIdleOps();
- getContext().sendOrderedBroadcastAsUser(mIdleIntent, UserHandle.ALL,
- null, mIdleStartedDoneReceiver, null, 0, null, null);
+ mLocalActivityManager.broadcastIntentWithCallback(mIdleIntent,
+ mIdleStartedDoneReceiver, null, UserHandle.USER_ALL,
+ null, null, mIdleIntentOptions);
}
if (lightChanged) {
incActiveIdleOps();
- getContext().sendOrderedBroadcastAsUser(mLightIdleIntent, UserHandle.ALL,
- null, mIdleStartedDoneReceiver, null, 0, null, null);
+ mLocalActivityManager.broadcastIntentWithCallback(mLightIdleIntent,
+ mIdleStartedDoneReceiver, null, UserHandle.USER_ALL,
+ null, null, mLightIdleIntentOptions);
}
// Always start with one active op for the message being sent here.
// Now we are done!
@@ -1844,10 +1918,12 @@ public class DeviceIdleController extends SystemService
} catch (RemoteException e) {
}
if (deepChanged) {
- getContext().sendBroadcastAsUser(mIdleIntent, UserHandle.ALL);
+ getContext().sendBroadcastAsUser(mIdleIntent, UserHandle.ALL,
+ null /* receiverPermission */, mIdleIntentOptions);
}
if (lightChanged) {
- getContext().sendBroadcastAsUser(mLightIdleIntent, UserHandle.ALL);
+ getContext().sendBroadcastAsUser(mLightIdleIntent, UserHandle.ALL,
+ null /* receiverPermission */, mLightIdleIntentOptions);
}
EventLogTags.writeDeviceIdleOffComplete();
} break;
@@ -1903,7 +1979,7 @@ public class DeviceIdleController extends SystemService
} break;
case MSG_RESET_PRE_IDLE_TIMEOUT_FACTOR: {
updatePreIdleFactor();
- maybeDoImmediateMaintenance();
+ maybeDoImmediateMaintenance("idle factor");
} break;
case MSG_REPORT_STATIONARY_STATUS: {
final DeviceIdleInternal.StationaryListener newListener =
@@ -2265,6 +2341,39 @@ public class DeviceIdleController extends SystemService
}
}
+ private class EmergencyCallListener extends TelephonyCallback implements
+ TelephonyCallback.OutgoingEmergencyCallListener,
+ TelephonyCallback.CallStateListener {
+ private volatile boolean mIsEmergencyCallActive;
+
+ @Override
+ public void onOutgoingEmergencyCall(EmergencyNumber placedEmergencyNumber,
+ int subscriptionId) {
+ mIsEmergencyCallActive = true;
+ if (DEBUG) Slog.d(TAG, "onOutgoingEmergencyCall(): subId = " + subscriptionId);
+ synchronized (DeviceIdleController.this) {
+ mActiveReason = ACTIVE_REASON_EMERGENCY_CALL;
+ becomeActiveLocked("emergency call", Process.myUid());
+ }
+ }
+
+ @Override
+ public void onCallStateChanged(int state) {
+ if (DEBUG) Slog.d(TAG, "onCallStateChanged(): state is " + state);
+ // An emergency call just finished
+ if (state == TelephonyManager.CALL_STATE_IDLE && mIsEmergencyCallActive) {
+ mIsEmergencyCallActive = false;
+ synchronized (DeviceIdleController.this) {
+ becomeInactiveIfAppropriateLocked();
+ }
+ }
+ }
+
+ boolean isEmergencyCallActive() {
+ return mIsEmergencyCallActive;
+ }
+ }
+
static class Injector {
private final Context mContext;
private ConnectivityManager mConnectivityManager;
@@ -2302,7 +2411,6 @@ public class DeviceIdleController extends SystemService
return mConstants;
}
-
/** Returns the current elapsed realtime in milliseconds. */
long getElapsedRealtime() {
return SystemClock.elapsedRealtime();
@@ -2316,7 +2424,7 @@ public class DeviceIdleController extends SystemService
}
MyHandler getHandler(DeviceIdleController controller) {
- return controller.new MyHandler(JobSchedulerBackgroundThread.getHandler().getLooper());
+ return controller.new MyHandler(AppSchedulingModuleThread.getHandler().getLooper());
}
Sensor getMotionSensor() {
@@ -2348,6 +2456,10 @@ public class DeviceIdleController extends SystemService
return mContext.getSystemService(SensorManager.class);
}
+ TelephonyManager getTelephonyManager() {
+ return mContext.getSystemService(TelephonyManager.class);
+ }
+
ConstraintController getConstraintController(Handler handler,
DeviceIdleInternal localService) {
if (mContext.getPackageManager()
@@ -2357,6 +2469,11 @@ public class DeviceIdleController extends SystemService
return null;
}
+ boolean isLocationPrefetchEnabled() {
+ return mContext.getResources().getBoolean(
+ com.android.internal.R.bool.config_autoPowerModePrefetchLocation);
+ }
+
boolean useMotionSensor() {
return mContext.getResources().getBoolean(
com.android.internal.R.bool.config_autoPowerModeUseMotionSensor);
@@ -2384,8 +2501,9 @@ public class DeviceIdleController extends SystemService
mConfigFile = new AtomicFile(new File(getSystemDir(), "deviceidle.xml"));
mHandler = mInjector.getHandler(this);
mAppStateTracker = mInjector.getAppStateTracker(context,
- JobSchedulerBackgroundThread.get().getLooper());
+ AppSchedulingModuleThread.get().getLooper());
LocalServices.addService(AppStateTracker.class, mAppStateTracker);
+ mIsLocationPrefetchEnabled = mInjector.isLocationPrefetchEnabled();
mUseMotionSensor = mInjector.useMotionSensor();
}
@@ -2499,8 +2617,7 @@ public class DeviceIdleController extends SystemService
mMotionSensor = mInjector.getMotionSensor();
}
- if (getContext().getResources().getBoolean(
- com.android.internal.R.bool.config_autoPowerModePrefetchLocation)) {
+ if (mIsLocationPrefetchEnabled) {
mLocationRequest = new LocationRequest.Builder(/*intervalMillis=*/ 0)
.setQuality(LocationRequest.QUALITY_HIGH_ACCURACY)
.setMaxUpdates(1)
@@ -2520,12 +2637,27 @@ public class DeviceIdleController extends SystemService
mAppStateTracker.onSystemServicesReady();
+ final Bundle mostRecentDeliveryOptions = BroadcastOptions.makeBasic()
+ .setDeliveryGroupPolicy(BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT)
+ .setDeferralPolicy(BroadcastOptions.DEFERRAL_POLICY_UNTIL_ACTIVE)
+ .toBundle();
+
mIdleIntent = new Intent(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED);
mIdleIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY
| Intent.FLAG_RECEIVER_FOREGROUND);
mLightIdleIntent = new Intent(PowerManager.ACTION_LIGHT_DEVICE_IDLE_MODE_CHANGED);
mLightIdleIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY
| Intent.FLAG_RECEIVER_FOREGROUND);
+ mIdleIntentOptions = mLightIdleIntentOptions = mostRecentDeliveryOptions;
+
+ mPowerSaveWhitelistChangedIntent = new Intent(
+ PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED);
+ mPowerSaveWhitelistChangedIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
+ mPowerSaveTempWhitelistChangedIntent = new Intent(
+ PowerManager.ACTION_POWER_SAVE_TEMP_WHITELIST_CHANGED);
+ mPowerSaveTempWhitelistChangedIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
+ mPowerSaveWhitelistChangedOptions = mostRecentDeliveryOptions;
+ mPowerSaveTempWhilelistChangedOptions = mostRecentDeliveryOptions;
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_BATTERY_CHANGED);
@@ -2561,6 +2693,9 @@ public class DeviceIdleController extends SystemService
mLocalActivityTaskManager.registerScreenObserver(mScreenObserver);
+ mInjector.getTelephonyManager().registerTelephonyCallback(
+ AppSchedulingModuleThread.getExecutor(), mEmergencyCallListener);
+
passWhiteListsToForceAppStandbyTrackerLocked();
updateInteractivityLocked();
}
@@ -2677,6 +2812,7 @@ public class DeviceIdleController extends SystemService
if (mPowerSaveWhitelistUserApps.put(name, UserHandle.getAppId(ai.uid))
== null) {
numAdded++;
+ Counter.logIncrement(USER_ALLOWLIST_ADDITION_METRIC_ID);
}
} catch (PackageManager.NameNotFoundException e) {
Slog.e(TAG, "Tried to add unknown package to power save whitelist: " + name);
@@ -2698,6 +2834,7 @@ public class DeviceIdleController extends SystemService
reportPowerSaveWhitelistChangedLocked();
updateWhitelistAppIdsLocked();
writeConfigFileLocked();
+ Counter.logIncrement(USER_ALLOWLIST_REMOVAL_METRIC_ID);
return true;
}
}
@@ -3178,7 +3315,7 @@ public class DeviceIdleController extends SystemService
if (conn != mNetworkConnected) {
mNetworkConnected = conn;
if (conn && mLightState == LIGHT_STATE_WAITING_FOR_NETWORK) {
- stepLightIdleStateLocked("network", /* forceProgression */ true);
+ stepLightIdleStateLocked("network");
}
}
}
@@ -3362,6 +3499,7 @@ public class DeviceIdleController extends SystemService
final boolean isScreenBlockingInactive =
mScreenOn && (!mConstants.WAIT_FOR_UNLOCK || !mScreenLocked);
+ final boolean isEmergencyCallActive = mEmergencyCallListener.isEmergencyCallActive();
if (DEBUG) {
Slog.d(TAG, "becomeInactiveIfAppropriateLocked():"
+ " isScreenBlockingInactive=" + isScreenBlockingInactive
@@ -3369,10 +3507,11 @@ public class DeviceIdleController extends SystemService
+ ", WAIT_FOR_UNLOCK=" + mConstants.WAIT_FOR_UNLOCK
+ ", mScreenLocked=" + mScreenLocked + ")"
+ " mCharging=" + mCharging
+ + " emergencyCall=" + isEmergencyCallActive
+ " mForceIdle=" + mForceIdle
);
}
- if (!mForceIdle && (mCharging || isScreenBlockingInactive)) {
+ if (!mForceIdle && (mCharging || isScreenBlockingInactive || isEmergencyCallActive)) {
return;
}
// Become inactive and determine if we will ultimately go idle.
@@ -3394,11 +3533,11 @@ public class DeviceIdleController extends SystemService
// doze alarm to after the upcoming AlarmClock alarm.
scheduleAlarmLocked(
mAlarmManager.getNextWakeFromIdleTime() - mInjector.getElapsedRealtime()
- + mConstants.QUICK_DOZE_DELAY_TIMEOUT, false);
+ + mConstants.QUICK_DOZE_DELAY_TIMEOUT);
} else {
// Wait a small amount of time in case something (eg: background service from
// recently closed app) needs to finish running.
- scheduleAlarmLocked(mConstants.QUICK_DOZE_DELAY_TIMEOUT, false);
+ scheduleAlarmLocked(mConstants.QUICK_DOZE_DELAY_TIMEOUT);
}
} else if (mState == STATE_ACTIVE) {
moveToStateLocked(STATE_INACTIVE, "no activity");
@@ -3413,9 +3552,9 @@ public class DeviceIdleController extends SystemService
// alarm to after the upcoming AlarmClock alarm.
scheduleAlarmLocked(
mAlarmManager.getNextWakeFromIdleTime() - mInjector.getElapsedRealtime()
- + delay, false);
+ + delay);
} else {
- scheduleAlarmLocked(delay, false);
+ scheduleAlarmLocked(delay);
}
}
}
@@ -3423,11 +3562,7 @@ public class DeviceIdleController extends SystemService
moveToLightStateLocked(LIGHT_STATE_INACTIVE, "no activity");
resetLightIdleManagementLocked();
scheduleLightAlarmLocked(mConstants.LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT,
- mConstants.FLEX_TIME_SHORT);
- // After moving in INACTIVE, the maintenance window should start the time inactive
- // timeout and a single light idle period.
- scheduleLightMaintenanceAlarmLocked(
- mConstants.LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT + mConstants.LIGHT_IDLE_TIMEOUT);
+ mConstants.FLEX_TIME_SHORT, true);
}
}
@@ -3449,8 +3584,9 @@ public class DeviceIdleController extends SystemService
private void resetLightIdleManagementLocked() {
mNextLightIdleDelay = mConstants.LIGHT_IDLE_TIMEOUT;
mMaintenanceStartTime = 0;
+ mNextLightIdleDelayFlex = mConstants.LIGHT_IDLE_TIMEOUT_INITIAL_FLEX;
mCurLightIdleBudget = mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET;
- cancelAllLightAlarmsLocked();
+ cancelLightAlarmLocked();
}
@GuardedBy("this")
@@ -3484,14 +3620,9 @@ public class DeviceIdleController extends SystemService
}
@GuardedBy("this")
- private void stepLightIdleStateLocked(String reason) {
- stepLightIdleStateLocked(reason, false);
- }
-
- @GuardedBy("this")
@VisibleForTesting
@SuppressLint("WakelockTimeout")
- void stepLightIdleStateLocked(String reason, boolean forceProgression) {
+ void stepLightIdleStateLocked(String reason) {
if (mLightState == LIGHT_STATE_ACTIVE || mLightState == LIGHT_STATE_OVERRIDE) {
// If we are already in deep device idle mode, then
// there is nothing left to do for light mode.
@@ -3499,91 +3630,78 @@ public class DeviceIdleController extends SystemService
}
if (DEBUG) {
- Slog.d(TAG, "stepLightIdleStateLocked: mLightState=" + lightStateToString(mLightState)
- + " force=" + forceProgression);
+ Slog.d(TAG, "stepLightIdleStateLocked: mLightState=" + lightStateToString(mLightState));
}
EventLogTags.writeDeviceIdleLightStep();
- final long nowElapsed = mInjector.getElapsedRealtime();
- final boolean crossedMaintenanceTime =
- mNextLightMaintenanceAlarmTime > 0 && nowElapsed >= mNextLightMaintenanceAlarmTime;
- final boolean crossedProgressionTime =
- mNextLightAlarmTime > 0 && nowElapsed >= mNextLightAlarmTime;
- final boolean enterMaintenance;
- if (crossedMaintenanceTime) {
- if (crossedProgressionTime) {
- enterMaintenance = (mNextLightAlarmTime <= mNextLightMaintenanceAlarmTime);
- } else {
- enterMaintenance = true;
- }
- } else if (crossedProgressionTime) {
- enterMaintenance = false;
- } else if (forceProgression) {
- // This will happen for adb commands, unit tests,
- // and when we're in WAITING_FOR_NETWORK and the network connects.
- enterMaintenance =
- mLightState == LIGHT_STATE_IDLE
- || mLightState == LIGHT_STATE_WAITING_FOR_NETWORK;
- } else {
- Slog.wtfStack(TAG, "stepLightIdleStateLocked called in invalid state: " + mLightState);
+ if (mEmergencyCallListener.isEmergencyCallActive()) {
+ // The emergency call should have raised the state to ACTIVE and kept it there,
+ // so this method shouldn't be called. Don't proceed further.
+ Slog.wtf(TAG, "stepLightIdleStateLocked called when emergency call is active");
+ if (mLightState != LIGHT_STATE_ACTIVE) {
+ mActiveReason = ACTIVE_REASON_EMERGENCY_CALL;
+ becomeActiveLocked("emergency", Process.myUid());
+ }
return;
}
- if (enterMaintenance) {
- if (mNetworkConnected || mLightState == LIGHT_STATE_WAITING_FOR_NETWORK) {
- // We have been idling long enough, now it is time to do some work.
- mActiveIdleOpCount = 1;
- mActiveIdleWakeLock.acquire();
- mMaintenanceStartTime = SystemClock.elapsedRealtime();
- if (mCurLightIdleBudget < mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET) {
- mCurLightIdleBudget = mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET;
- } else if (mCurLightIdleBudget > mConstants.LIGHT_IDLE_MAINTENANCE_MAX_BUDGET) {
- mCurLightIdleBudget = mConstants.LIGHT_IDLE_MAINTENANCE_MAX_BUDGET;
+ switch (mLightState) {
+ case LIGHT_STATE_INACTIVE:
+ mCurLightIdleBudget = mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET;
+ // Reset the upcoming idle delays.
+ mNextLightIdleDelay = mConstants.LIGHT_IDLE_TIMEOUT;
+ mNextLightIdleDelayFlex = mConstants.LIGHT_IDLE_TIMEOUT_INITIAL_FLEX;
+ mMaintenanceStartTime = 0;
+ // Fall through to immediately idle.
+ case LIGHT_STATE_IDLE_MAINTENANCE:
+ if (mMaintenanceStartTime != 0) {
+ long duration = SystemClock.elapsedRealtime() - mMaintenanceStartTime;
+ if (duration < mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET) {
+ // We didn't use up all of our minimum budget; add this to the reserve.
+ mCurLightIdleBudget +=
+ (mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET - duration);
+ } else {
+ // We used more than our minimum budget; this comes out of the reserve.
+ mCurLightIdleBudget -=
+ (duration - mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET);
+ }
}
+ mMaintenanceStartTime = 0;
+ scheduleLightAlarmLocked(mNextLightIdleDelay, mNextLightIdleDelayFlex, true);
mNextLightIdleDelay = Math.min(mConstants.LIGHT_MAX_IDLE_TIMEOUT,
(long) (mNextLightIdleDelay * mConstants.LIGHT_IDLE_FACTOR));
- // We're entering MAINTENANCE. It should end curLightIdleBudget time from now.
- // The next maintenance window should be curLightIdleBudget + nextLightIdleDelay
- // time from now.
- scheduleLightAlarmLocked(mCurLightIdleBudget, mConstants.FLEX_TIME_SHORT);
- scheduleLightMaintenanceAlarmLocked(mCurLightIdleBudget + mNextLightIdleDelay);
- moveToLightStateLocked(LIGHT_STATE_IDLE_MAINTENANCE, reason);
- addEvent(EVENT_LIGHT_MAINTENANCE, null);
- mHandler.sendEmptyMessage(MSG_REPORT_IDLE_OFF);
- } else {
- // We'd like to do maintenance, but currently don't have network
- // connectivity... let's try to wait until the network comes back.
- // We'll only wait for another full idle period, however, and then give up.
- scheduleLightMaintenanceAlarmLocked(mNextLightIdleDelay);
- cancelLightAlarmLocked();
- moveToLightStateLocked(LIGHT_STATE_WAITING_FOR_NETWORK, reason);
- }
- } else {
- if (mMaintenanceStartTime != 0) {
- // Cap duration at budget since the non-wakeup alarm to exit maintenance may
- // not fire at the exact intended time, but once the system is up, we will stop
- // more ongoing work.
- long duration = Math.min(mCurLightIdleBudget,
- SystemClock.elapsedRealtime() - mMaintenanceStartTime);
- if (duration < mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET) {
- // We didn't use up all of our minimum budget; add this to the reserve.
- mCurLightIdleBudget +=
- (mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET - duration);
+ mNextLightIdleDelayFlex = Math.min(mConstants.LIGHT_IDLE_TIMEOUT_MAX_FLEX,
+ (long) (mNextLightIdleDelayFlex * mConstants.LIGHT_IDLE_FACTOR));
+ moveToLightStateLocked(LIGHT_STATE_IDLE, reason);
+ addEvent(EVENT_LIGHT_IDLE, null);
+ mGoingIdleWakeLock.acquire();
+ mHandler.sendEmptyMessage(MSG_REPORT_IDLE_ON_LIGHT);
+ break;
+ case LIGHT_STATE_IDLE:
+ case LIGHT_STATE_WAITING_FOR_NETWORK:
+ if (mNetworkConnected || mLightState == LIGHT_STATE_WAITING_FOR_NETWORK) {
+ // We have been idling long enough, now it is time to do some work.
+ mActiveIdleOpCount = 1;
+ mActiveIdleWakeLock.acquire();
+ mMaintenanceStartTime = SystemClock.elapsedRealtime();
+ if (mCurLightIdleBudget < mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET) {
+ mCurLightIdleBudget = mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET;
+ } else if (mCurLightIdleBudget > mConstants.LIGHT_IDLE_MAINTENANCE_MAX_BUDGET) {
+ mCurLightIdleBudget = mConstants.LIGHT_IDLE_MAINTENANCE_MAX_BUDGET;
+ }
+ scheduleLightAlarmLocked(mCurLightIdleBudget, mConstants.FLEX_TIME_SHORT, true);
+ moveToLightStateLocked(LIGHT_STATE_IDLE_MAINTENANCE, reason);
+ addEvent(EVENT_LIGHT_MAINTENANCE, null);
+ mHandler.sendEmptyMessage(MSG_REPORT_IDLE_OFF);
} else {
- // We used more than our minimum budget; this comes out of the reserve.
- mCurLightIdleBudget -=
- (duration - mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET);
+ // We'd like to do maintenance, but currently don't have network
+ // connectivity... let's try to wait until the network comes back.
+ // We'll only wait for another full idle period, however, and then give up.
+ scheduleLightAlarmLocked(mNextLightIdleDelay,
+ mNextLightIdleDelayFlex / 2, true);
+ moveToLightStateLocked(LIGHT_STATE_WAITING_FOR_NETWORK, reason);
}
- }
- mMaintenanceStartTime = 0;
- // We're entering IDLE. We may have used less than curLightIdleBudget for the
- // maintenance window, so reschedule the alarm starting from now.
- scheduleLightMaintenanceAlarmLocked(mNextLightIdleDelay);
- cancelLightAlarmLocked();
- moveToLightStateLocked(LIGHT_STATE_IDLE, reason);
- addEvent(EVENT_LIGHT_IDLE, null);
- mGoingIdleWakeLock.acquire();
- mHandler.sendEmptyMessage(MSG_REPORT_IDLE_ON_LIGHT);
+ break;
}
}
@@ -3609,6 +3727,17 @@ public class DeviceIdleController extends SystemService
if (DEBUG) Slog.d(TAG, "stepIdleStateLocked: mState=" + mState);
EventLogTags.writeDeviceIdleStep();
+ if (mEmergencyCallListener.isEmergencyCallActive()) {
+ // The emergency call should have raised the state to ACTIVE and kept it there,
+ // so this method shouldn't be called. Don't proceed further.
+ Slog.wtf(TAG, "stepIdleStateLocked called when emergency call is active");
+ if (mState != STATE_ACTIVE) {
+ mActiveReason = ACTIVE_REASON_EMERGENCY_CALL;
+ becomeActiveLocked("emergency", Process.myUid());
+ }
+ return;
+ }
+
if (isUpcomingAlarmClock()) {
// Whoops, there is an upcoming alarm. We don't actually want to go idle.
if (mState != STATE_ACTIVE) {
@@ -3640,7 +3769,7 @@ public class DeviceIdleController extends SystemService
if (shouldUseIdleTimeoutFactorLocked()) {
delay = (long) (mPreIdleFactor * delay);
}
- scheduleAlarmLocked(delay, false);
+ scheduleAlarmLocked(delay);
moveToStateLocked(STATE_IDLE_PENDING, reason);
break;
case STATE_IDLE_PENDING:
@@ -3666,32 +3795,40 @@ public class DeviceIdleController extends SystemService
case STATE_SENSING:
cancelSensingTimeoutAlarmLocked();
moveToStateLocked(STATE_LOCATING, reason);
- scheduleAlarmLocked(mConstants.LOCATING_TIMEOUT, false);
- LocationManager locationManager = mInjector.getLocationManager();
- if (locationManager != null
- && locationManager.getProvider(LocationManager.NETWORK_PROVIDER) != null) {
- locationManager.requestLocationUpdates(mLocationRequest,
- mGenericLocationListener, mHandler.getLooper());
- mLocating = true;
- } else {
- mHasNetworkLocation = false;
- }
- if (locationManager != null
- && locationManager.getProvider(LocationManager.GPS_PROVIDER) != null) {
- mHasGps = true;
- locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1000, 5,
- mGpsLocationListener, mHandler.getLooper());
- mLocating = true;
+ if (mIsLocationPrefetchEnabled) {
+ scheduleAlarmLocked(mConstants.LOCATING_TIMEOUT);
+ LocationManager locationManager = mInjector.getLocationManager();
+ if (locationManager != null
+ && locationManager.getProvider(LocationManager.FUSED_PROVIDER)
+ != null) {
+ locationManager.requestLocationUpdates(LocationManager.FUSED_PROVIDER,
+ mLocationRequest,
+ AppSchedulingModuleThread.getExecutor(),
+ mGenericLocationListener);
+ mLocating = true;
+ } else {
+ mHasFusedLocation = false;
+ }
+ if (locationManager != null
+ && locationManager.getProvider(LocationManager.GPS_PROVIDER) != null) {
+ mHasGps = true;
+ locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER,
+ 1000, 5, mGpsLocationListener, mHandler.getLooper());
+ mLocating = true;
+ } else {
+ mHasGps = false;
+ }
+ // If we have a location provider, we're all set, the listeners will move state
+ // forward.
+ if (mLocating) {
+ break;
+ }
+ // Otherwise, we have to move from locating into idle maintenance.
} else {
- mHasGps = false;
- }
- // If we have a location provider, we're all set, the listeners will move state
- // forward.
- if (mLocating) {
- break;
+ mLocating = false;
}
- // Otherwise, we have to move from locating into idle maintenance.
+ // We're not doing any locating work, so move on to the next state.
case STATE_LOCATING:
cancelAlarmLocked();
cancelLocatingLocked();
@@ -3705,7 +3842,8 @@ public class DeviceIdleController extends SystemService
// Everything is in place to go into IDLE state.
case STATE_IDLE_MAINTENANCE:
- scheduleAlarmLocked(mNextIdleDelay, true);
+ moveToStateLocked(STATE_IDLE, reason);
+ scheduleAlarmLocked(mNextIdleDelay);
if (DEBUG) Slog.d(TAG, "Moved to STATE_IDLE. Next alarm in " + mNextIdleDelay +
" ms.");
mNextIdleDelay = (long)(mNextIdleDelay * mConstants.IDLE_FACTOR);
@@ -3715,10 +3853,9 @@ public class DeviceIdleController extends SystemService
if (mNextIdleDelay < mConstants.IDLE_TIMEOUT) {
mNextIdleDelay = mConstants.IDLE_TIMEOUT;
}
- moveToStateLocked(STATE_IDLE, reason);
if (mLightState != LIGHT_STATE_OVERRIDE) {
moveToLightStateLocked(LIGHT_STATE_OVERRIDE, "deep");
- cancelAllLightAlarmsLocked();
+ cancelLightAlarmLocked();
}
addEvent(EVENT_DEEP_IDLE, null);
mGoingIdleWakeLock.acquire();
@@ -3728,7 +3865,8 @@ public class DeviceIdleController extends SystemService
// We have been idling long enough, now it is time to do some work.
mActiveIdleOpCount = 1;
mActiveIdleWakeLock.acquire();
- scheduleAlarmLocked(mNextIdlePendingDelay, false);
+ moveToStateLocked(STATE_IDLE_MAINTENANCE, reason);
+ scheduleAlarmLocked(mNextIdlePendingDelay);
if (DEBUG) Slog.d(TAG, "Moved from STATE_IDLE to STATE_IDLE_MAINTENANCE. " +
"Next alarm in " + mNextIdlePendingDelay + " ms.");
mMaintenanceStartTime = SystemClock.elapsedRealtime();
@@ -3737,7 +3875,6 @@ public class DeviceIdleController extends SystemService
if (mNextIdlePendingDelay < mConstants.IDLE_PENDING_TIMEOUT) {
mNextIdlePendingDelay = mConstants.IDLE_PENDING_TIMEOUT;
}
- moveToStateLocked(STATE_IDLE_MAINTENANCE, reason);
addEvent(EVENT_DEEP_MAINTENANCE, null);
mHandler.sendEmptyMessage(MSG_REPORT_IDLE_OFF);
break;
@@ -3900,19 +4037,18 @@ public class DeviceIdleController extends SystemService
if (Math.abs(delay - newDelay) < MIN_STATE_STEP_ALARM_CHANGE) {
return;
}
- scheduleAlarmLocked(newDelay, false);
+ scheduleAlarmLocked(newDelay);
}
}
}
- private void maybeDoImmediateMaintenance() {
+ private void maybeDoImmediateMaintenance(String reason) {
synchronized (this) {
if (mState == STATE_IDLE) {
long duration = SystemClock.elapsedRealtime() - mIdleStartTime;
- /* Let's trgger a immediate maintenance,
- * if it has been idle for a long time */
+ // Trigger an immediate maintenance window if it has been IDLE for long enough.
if (duration > mConstants.IDLE_TIMEOUT) {
- scheduleAlarmLocked(0, false);
+ stepIdleStateLocked(reason);
}
}
}
@@ -3932,7 +4068,7 @@ public class DeviceIdleController extends SystemService
void setIdleStartTimeForTest(long idleStartTime) {
synchronized (this) {
mIdleStartTime = idleStartTime;
- maybeDoImmediateMaintenance();
+ maybeDoImmediateMaintenance("testing");
}
}
@@ -3943,6 +4079,11 @@ public class DeviceIdleController extends SystemService
}
}
+ @VisibleForTesting
+ boolean isEmergencyCallActive() {
+ return mEmergencyCallListener.isEmergencyCallActive();
+ }
+
@GuardedBy("this")
boolean isOpsInactiveLocked() {
return mActiveIdleOpCount <= 0 && !mJobsActive && !mAlarmsActive;
@@ -3964,7 +4105,7 @@ public class DeviceIdleController extends SystemService
if (mState == STATE_IDLE_MAINTENANCE) {
stepIdleStateLocked("s:early");
} else {
- stepLightIdleStateLocked("s:early", /* forceProgression */ true);
+ stepLightIdleStateLocked("s:early");
}
}
}
@@ -4072,12 +4213,6 @@ public class DeviceIdleController extends SystemService
}
@GuardedBy("this")
- private void cancelAllLightAlarmsLocked() {
- cancelLightAlarmLocked();
- cancelLightMaintenanceAlarmLocked();
- }
-
- @GuardedBy("this")
private void cancelLightAlarmLocked() {
if (mNextLightAlarmTime != 0) {
mNextLightAlarmTime = 0;
@@ -4086,14 +4221,6 @@ public class DeviceIdleController extends SystemService
}
@GuardedBy("this")
- private void cancelLightMaintenanceAlarmLocked() {
- if (mNextLightMaintenanceAlarmTime != 0) {
- mNextLightMaintenanceAlarmTime = 0;
- mAlarmManager.cancel(mLightMaintenanceAlarmListener);
- }
- }
-
- @GuardedBy("this")
void cancelLocatingLocked() {
if (mLocating) {
LocationManager locationManager = mInjector.getLocationManager();
@@ -4120,8 +4247,9 @@ public class DeviceIdleController extends SystemService
}
@GuardedBy("this")
- void scheduleAlarmLocked(long delay, boolean idleUntil) {
- if (DEBUG) Slog.d(TAG, "scheduleAlarmLocked(" + delay + ", " + idleUntil + ")");
+ @VisibleForTesting
+ void scheduleAlarmLocked(long delay) {
+ if (DEBUG) Slog.d(TAG, "scheduleAlarmLocked(" + delay + ", " + stateToString(mState) + ")");
if (mUseMotionSensor && mMotionSensor == null
&& mState != STATE_QUICK_DOZE_DELAY
@@ -4137,7 +4265,7 @@ public class DeviceIdleController extends SystemService
return;
}
mNextAlarmTime = SystemClock.elapsedRealtime() + delay;
- if (idleUntil) {
+ if (mState == STATE_IDLE) {
mAlarmManager.setIdleUntil(AlarmManager.ELAPSED_REALTIME_WAKEUP,
mNextAlarmTime, "DeviceIdleController.deep", mDeepAlarmListener, mHandler);
} else if (mState == STATE_LOCATING) {
@@ -4157,40 +4285,26 @@ public class DeviceIdleController extends SystemService
}
@GuardedBy("this")
- @VisibleForTesting
- void scheduleLightAlarmLocked(long delay, long flex) {
+ void scheduleLightAlarmLocked(long delay, long flex, boolean wakeup) {
if (DEBUG) {
Slog.d(TAG, "scheduleLightAlarmLocked(" + delay
+ (mConstants.USE_WINDOW_ALARMS ? "/" + flex : "")
- + ")");
+ + ", wakeup=" + wakeup + ")");
}
mNextLightAlarmTime = mInjector.getElapsedRealtime() + delay;
if (mConstants.USE_WINDOW_ALARMS) {
mAlarmManager.setWindow(
- AlarmManager.ELAPSED_REALTIME,
+ wakeup ? AlarmManager.ELAPSED_REALTIME_WAKEUP : AlarmManager.ELAPSED_REALTIME,
mNextLightAlarmTime, flex,
"DeviceIdleController.light", mLightAlarmListener, mHandler);
} else {
mAlarmManager.set(
- AlarmManager.ELAPSED_REALTIME,
+ wakeup ? AlarmManager.ELAPSED_REALTIME_WAKEUP : AlarmManager.ELAPSED_REALTIME,
mNextLightAlarmTime,
"DeviceIdleController.light", mLightAlarmListener, mHandler);
}
}
- @GuardedBy("this")
- @VisibleForTesting
- void scheduleLightMaintenanceAlarmLocked(long delay) {
- if (DEBUG) {
- Slog.d(TAG, "scheduleLightMaintenanceAlarmLocked(" + delay + ")");
- }
- mNextLightMaintenanceAlarmTime = mInjector.getElapsedRealtime() + delay;
- mAlarmManager.setWindow(
- AlarmManager.ELAPSED_REALTIME_WAKEUP,
- mNextLightMaintenanceAlarmTime, mConstants.FLEX_TIME_SHORT,
- "DeviceIdleController.light", mLightMaintenanceAlarmListener, mHandler);
- }
-
@VisibleForTesting
long getNextLightAlarmTimeForTesting() {
synchronized (this) {
@@ -4198,13 +4312,6 @@ public class DeviceIdleController extends SystemService
}
}
- @VisibleForTesting
- long getNextLightMaintenanceAlarmTimeForTesting() {
- synchronized (this) {
- return mNextLightMaintenanceAlarmTime;
- }
- }
-
private void scheduleMotionRegistrationAlarmLocked() {
if (DEBUG) Slog.d(TAG, "scheduleMotionRegistrationAlarmLocked");
long nextMotionRegistrationAlarmTime =
@@ -4335,17 +4442,17 @@ public class DeviceIdleController extends SystemService
}
private void reportPowerSaveWhitelistChangedLocked() {
- Intent intent = new Intent(PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED);
- intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
- getContext().sendBroadcastAsUser(intent, UserHandle.SYSTEM);
+ getContext().sendBroadcastAsUser(mPowerSaveWhitelistChangedIntent, UserHandle.SYSTEM,
+ null /* receiverPermission */,
+ mPowerSaveWhitelistChangedOptions);
}
private void reportTempWhitelistChangedLocked(final int uid, final boolean added) {
mHandler.obtainMessage(MSG_REPORT_TEMP_APP_WHITELIST_CHANGED, uid, added ? 1 : 0)
.sendToTarget();
- Intent intent = new Intent(PowerManager.ACTION_POWER_SAVE_TEMP_WHITELIST_CHANGED);
- intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
- getContext().sendBroadcastAsUser(intent, UserHandle.SYSTEM);
+ getContext().sendBroadcastAsUser(mPowerSaveTempWhitelistChangedIntent, UserHandle.SYSTEM,
+ null /* receiverPermission */,
+ mPowerSaveTempWhilelistChangedOptions);
}
private void passWhiteListsToForceAppStandbyTrackerLocked() {
@@ -4574,7 +4681,7 @@ public class DeviceIdleController extends SystemService
pw.print("Stepped to deep: ");
pw.println(stateToString(mState));
} else if ("light".equals(arg)) {
- stepLightIdleStateLocked("s:shell", /* forceProgression */ true);
+ stepLightIdleStateLocked("s:shell");
pw.print("Stepped to light: "); pw.println(lightStateToString(mLightState));
} else {
pw.println("Unknown idle mode: " + arg);
@@ -4583,6 +4690,22 @@ public class DeviceIdleController extends SystemService
Binder.restoreCallingIdentity(token);
}
}
+ } else if ("force-active".equals(cmd)) {
+ getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER,
+ null);
+ synchronized (this) {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ mForceIdle = true;
+ becomeActiveLocked("force-active", Process.myUid());
+ pw.print("Light state: ");
+ pw.print(lightStateToString(mLightState));
+ pw.print(", deep state: ");
+ pw.println(stateToString(mState));
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
} else if ("force-idle".equals(cmd)) {
getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER,
null);
@@ -4614,7 +4737,7 @@ public class DeviceIdleController extends SystemService
becomeInactiveIfAppropriateLocked();
int curLightState = mLightState;
while (curLightState != LIGHT_STATE_IDLE) {
- stepLightIdleStateLocked("s:shell", /* forceProgression */ true);
+ stepLightIdleStateLocked("s:shell");
if (curLightState == mLightState) {
pw.print("Unable to go light idle; stopped at ");
pw.println(lightStateToString(mLightState));
@@ -4806,6 +4929,9 @@ public class DeviceIdleController extends SystemService
Binder.restoreCallingIdentity(token);
}
} else {
+ if (!DumpUtils.checkDumpPermission(getContext(), TAG, pw)) {
+ return -1;
+ }
synchronized (this) {
for (int j=0; j<mPowerSaveWhitelistAppsExceptIdle.size(); j++) {
pw.print("system-excidle,");
@@ -4867,6 +4993,9 @@ public class DeviceIdleController extends SystemService
pw.println("[-r] requires a package name");
return -1;
} else {
+ if (!DumpUtils.checkDumpPermission(getContext(), TAG, pw)) {
+ return -1;
+ }
dumpTempWhitelistSchedule(pw, false);
}
} else if ("except-idle-whitelist".equals(cmd)) {
@@ -4942,6 +5071,9 @@ public class DeviceIdleController extends SystemService
Binder.restoreCallingIdentity(token);
}
} else {
+ if (!DumpUtils.checkDumpPermission(getContext(), TAG, pw)) {
+ return -1;
+ }
synchronized (this) {
for (int j = 0; j < mPowerSaveWhitelistApps.size(); j++) {
pw.print(mPowerSaveWhitelistApps.keyAt(j));
@@ -5168,6 +5300,8 @@ public class DeviceIdleController extends SystemService
pw.print(" mScreenLocked="); pw.println(mScreenLocked);
pw.print(" mNetworkConnected="); pw.println(mNetworkConnected);
pw.print(" mCharging="); pw.println(mCharging);
+ pw.print(" activeEmergencyCall=");
+ pw.println(mEmergencyCallListener.isEmergencyCallActive());
if (mConstraints.size() != 0) {
pw.println(" mConstraints={");
for (int i = 0; i < mConstraints.size(); i++) {
@@ -5191,14 +5325,19 @@ public class DeviceIdleController extends SystemService
pw.print(" "); pw.print(mStationaryListeners.size());
pw.println(" stationary listeners registered");
}
- pw.print(" mLocating="); pw.print(mLocating); pw.print(" mHasGps=");
- pw.print(mHasGps); pw.print(" mHasNetwork=");
- pw.print(mHasNetworkLocation); pw.print(" mLocated="); pw.println(mLocated);
- if (mLastGenericLocation != null) {
- pw.print(" mLastGenericLocation="); pw.println(mLastGenericLocation);
- }
- if (mLastGpsLocation != null) {
- pw.print(" mLastGpsLocation="); pw.println(mLastGpsLocation);
+ if (mIsLocationPrefetchEnabled) {
+ pw.print(" mLocating="); pw.print(mLocating);
+ pw.print(" mHasGps="); pw.print(mHasGps);
+ pw.print(" mHasFused="); pw.print(mHasFusedLocation);
+ pw.print(" mLocated="); pw.println(mLocated);
+ if (mLastGenericLocation != null) {
+ pw.print(" mLastGenericLocation="); pw.println(mLastGenericLocation);
+ }
+ if (mLastGpsLocation != null) {
+ pw.print(" mLastGpsLocation="); pw.println(mLastGpsLocation);
+ }
+ } else {
+ pw.println(" Location prefetching disabled");
}
pw.print(" mState="); pw.print(stateToString(mState));
pw.print(" mLightState=");
@@ -5226,19 +5365,19 @@ public class DeviceIdleController extends SystemService
if (mNextLightIdleDelay != 0) {
pw.print(" mNextLightIdleDelay=");
TimeUtils.formatDuration(mNextLightIdleDelay, pw);
- pw.println();
+ if (mConstants.USE_WINDOW_ALARMS) {
+ pw.print(" (flex=");
+ TimeUtils.formatDuration(mNextLightIdleDelayFlex, pw);
+ pw.println(")");
+ } else {
+ pw.println();
+ }
}
if (mNextLightAlarmTime != 0) {
pw.print(" mNextLightAlarmTime=");
TimeUtils.formatDuration(mNextLightAlarmTime, SystemClock.elapsedRealtime(), pw);
pw.println();
}
- if (mNextLightMaintenanceAlarmTime != 0) {
- pw.print(" mNextLightMaintenanceAlarmTime=");
- TimeUtils.formatDuration(
- mNextLightMaintenanceAlarmTime, SystemClock.elapsedRealtime(), pw);
- pw.println();
- }
if (mCurLightIdleBudget != 0) {
pw.print(" mCurLightIdleBudget=");
TimeUtils.formatDuration(mCurLightIdleBudget, pw);
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/Alarm.java b/apex/jobscheduler/service/java/com/android/server/alarm/Alarm.java
index fd2bb1347fd3..4d646de2e529 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/Alarm.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/Alarm.java
@@ -35,6 +35,7 @@ import android.util.proto.ProtoOutputStream;
import com.android.internal.annotations.VisibleForTesting;
import java.text.SimpleDateFormat;
+import java.util.Arrays;
import java.util.Date;
/**
@@ -92,6 +93,14 @@ class Alarm {
* Caller had USE_EXACT_ALARM permission.
*/
static final int EXACT_ALLOW_REASON_POLICY_PERMISSION = 3;
+ /**
+ * Caller used a listener alarm, which does not need permission to be exact.
+ */
+ static final int EXACT_ALLOW_REASON_LISTENER = 4;
+ /**
+ * Caller used a prioritized alarm, which does not need permission to be exact.
+ */
+ static final int EXACT_ALLOW_REASON_PRIORITIZED = 5;
public final int type;
/**
@@ -256,7 +265,7 @@ class Alarm {
return sb.toString();
}
- private static String policyIndexToString(int index) {
+ static String policyIndexToString(int index) {
switch (index) {
case REQUESTER_POLICY_INDEX:
return "requester";
@@ -283,6 +292,10 @@ class Alarm {
return "permission";
case EXACT_ALLOW_REASON_POLICY_PERMISSION:
return "policy_permission";
+ case EXACT_ALLOW_REASON_LISTENER:
+ return "listener";
+ case EXACT_ALLOW_REASON_PRIORITIZED:
+ return "prioritized";
case EXACT_ALLOW_REASON_NOT_APPLICABLE:
return "N/A";
default:
@@ -392,4 +405,32 @@ class Alarm {
proto.end(token);
}
+
+ /**
+ * Stores a snapshot of an alarm at any given time to be used for logging and diagnostics.
+ * This should intentionally avoid holding pointers to objects like {@link Alarm#operation}.
+ */
+ static class Snapshot {
+ final int mType;
+ final String mTag;
+ final long[] mPolicyWhenElapsed;
+
+ Snapshot(Alarm a) {
+ mType = a.type;
+ mTag = a.statsTag;
+ mPolicyWhenElapsed = Arrays.copyOf(a.mPolicyWhenElapsed, NUM_POLICIES);
+ }
+
+ void dump(IndentingPrintWriter pw, long nowElapsed) {
+ pw.print("type", typeToString(mType));
+ pw.print("tag", mTag);
+ pw.println();
+ pw.print("policyWhenElapsed:");
+ for (int i = 0; i < NUM_POLICIES; i++) {
+ pw.print(" " + policyIndexToString(i) + "=");
+ TimeUtils.formatDuration(mPolicyWhenElapsed[i], nowElapsed, pw);
+ }
+ pw.println();
+ }
+ }
}
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 342465138ac7..220aa2762305 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
@@ -19,6 +19,7 @@ package com.android.server.alarm;
import static android.app.ActivityManagerInternal.ALLOW_NON_FULL;
import static android.app.AlarmManager.ELAPSED_REALTIME;
import static android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP;
+import static android.app.AlarmManager.EXACT_LISTENER_ALARMS_DROPPED_ON_CACHED;
import static android.app.AlarmManager.FLAG_ALLOW_WHILE_IDLE;
import static android.app.AlarmManager.FLAG_ALLOW_WHILE_IDLE_COMPAT;
import static android.app.AlarmManager.FLAG_ALLOW_WHILE_IDLE_UNRESTRICTED;
@@ -29,6 +30,9 @@ import static android.app.AlarmManager.INTERVAL_DAY;
import static android.app.AlarmManager.INTERVAL_HOUR;
import static android.app.AlarmManager.RTC;
import static android.app.AlarmManager.RTC_WAKEUP;
+import static android.content.PermissionChecker.PERMISSION_GRANTED;
+import static android.content.PermissionChecker.PID_UNKNOWN;
+import static android.content.PermissionChecker.checkPermissionForPreflight;
import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY;
import static android.os.PowerExemptionManager.REASON_ALARM_MANAGER_ALARM_CLOCK;
import static android.os.PowerExemptionManager.REASON_DENIED;
@@ -39,23 +43,33 @@ import static android.os.PowerWhitelistManager.TEMPORARY_ALLOWLIST_TYPE_FOREGROU
import static android.os.PowerWhitelistManager.TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED;
import static android.os.UserHandle.USER_SYSTEM;
+import static com.android.server.SystemClockTime.TIME_CONFIDENCE_HIGH;
+import static com.android.server.SystemTimeZone.TIME_ZONE_CONFIDENCE_HIGH;
+import static com.android.server.SystemTimeZone.getTimeZoneId;
import static com.android.server.alarm.Alarm.APP_STANDBY_POLICY_INDEX;
import static com.android.server.alarm.Alarm.BATTERY_SAVER_POLICY_INDEX;
import static com.android.server.alarm.Alarm.DEVICE_IDLE_POLICY_INDEX;
import static com.android.server.alarm.Alarm.EXACT_ALLOW_REASON_ALLOW_LIST;
import static com.android.server.alarm.Alarm.EXACT_ALLOW_REASON_COMPAT;
+import static com.android.server.alarm.Alarm.EXACT_ALLOW_REASON_LISTENER;
import static com.android.server.alarm.Alarm.EXACT_ALLOW_REASON_NOT_APPLICABLE;
import static com.android.server.alarm.Alarm.EXACT_ALLOW_REASON_PERMISSION;
import static com.android.server.alarm.Alarm.EXACT_ALLOW_REASON_POLICY_PERMISSION;
+import static com.android.server.alarm.Alarm.EXACT_ALLOW_REASON_PRIORITIZED;
import static com.android.server.alarm.Alarm.REQUESTER_POLICY_INDEX;
import static com.android.server.alarm.Alarm.TARE_POLICY_INDEX;
+import static com.android.server.alarm.AlarmManagerService.AlarmHandler.REMOVE_EXACT_LISTENER_ALARMS_ON_CACHED;
import static com.android.server.alarm.AlarmManagerService.RemovedAlarm.REMOVE_REASON_ALARM_CANCELLED;
import static com.android.server.alarm.AlarmManagerService.RemovedAlarm.REMOVE_REASON_DATA_CLEARED;
import static com.android.server.alarm.AlarmManagerService.RemovedAlarm.REMOVE_REASON_EXACT_PERMISSION_REVOKED;
+import static com.android.server.alarm.AlarmManagerService.RemovedAlarm.REMOVE_REASON_LISTENER_BINDER_DIED;
+import static com.android.server.alarm.AlarmManagerService.RemovedAlarm.REMOVE_REASON_LISTENER_CACHED;
import static com.android.server.alarm.AlarmManagerService.RemovedAlarm.REMOVE_REASON_PI_CANCELLED;
import static com.android.server.alarm.AlarmManagerService.RemovedAlarm.REMOVE_REASON_UNDEFINED;
import android.Manifest;
+import android.annotation.CurrentTimeMillisLong;
+import android.annotation.ElapsedRealtimeLong;
import android.annotation.NonNull;
import android.annotation.UserIdInt;
import android.app.Activity;
@@ -70,27 +84,26 @@ import android.app.IAlarmManager;
import android.app.PendingIntent;
import android.app.compat.CompatChanges;
import android.app.role.RoleManager;
+import android.app.tare.EconomyManager;
import android.app.usage.UsageStatsManager;
import android.app.usage.UsageStatsManagerInternal;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
-import android.content.PermissionChecker;
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
-import android.database.ContentObserver;
+import android.content.pm.UserPackage;
import android.net.Uri;
import android.os.BatteryManager;
+import android.os.BatteryStatsInternal;
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
-import android.os.Environment;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
-import android.os.ParcelableException;
import android.os.PowerExemptionManager;
import android.os.PowerManager;
import android.os.Process;
@@ -100,7 +113,6 @@ 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;
@@ -114,10 +126,9 @@ import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.EventLog;
import android.util.IndentingPrintWriter;
+import android.util.IntArray;
import android.util.Log;
import android.util.LongArrayQueue;
-import android.util.NtpTrustedTime;
-import android.util.Pair;
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseArrayMap;
@@ -128,6 +139,7 @@ import android.util.TimeUtils;
import android.util.proto.ProtoOutputStream;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.Keep;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.IAppOpsCallback;
import com.android.internal.app.IAppOpsService;
@@ -137,18 +149,22 @@ import com.android.internal.util.LocalLog;
import com.android.internal.util.RingBuffer;
import com.android.internal.util.StatLogger;
import com.android.server.AlarmManagerInternal;
+import com.android.server.AppSchedulingModuleThread;
import com.android.server.AppStateTracker;
import com.android.server.AppStateTrackerImpl;
import com.android.server.AppStateTrackerImpl.Listener;
import com.android.server.DeviceIdleInternal;
import com.android.server.EventLogTags;
-import com.android.server.JobSchedulerBackgroundThread;
import com.android.server.LocalServices;
+import com.android.server.SystemClockTime;
+import com.android.server.SystemClockTime.TimeConfidence;
import com.android.server.SystemService;
import com.android.server.SystemServiceManager;
-import com.android.server.pm.parsing.pkg.AndroidPackage;
+import com.android.server.SystemTimeZone;
+import com.android.server.SystemTimeZone.TimeZoneConfidence;
import com.android.server.pm.permission.PermissionManagerService;
import com.android.server.pm.permission.PermissionManagerServiceInternal;
+import com.android.server.pm.pkg.AndroidPackage;
import com.android.server.tare.AlarmManagerEconomicPolicy;
import com.android.server.tare.EconomyManagerInternal;
import com.android.server.usage.AppStandbyInternal;
@@ -161,7 +177,6 @@ import libcore.util.EmptyArray;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
-import java.time.DateTimeException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
@@ -169,7 +184,6 @@ import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
-import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.TimeZone;
@@ -202,7 +216,6 @@ public class AlarmManagerService extends SystemService {
static final boolean DEBUG_TARE = localLOGV || false;
static final boolean RECORD_ALARMS_IN_HISTORY = true;
static final boolean RECORD_DEVICE_IDLE_ALARMS = false;
- static final String TIMEZONE_PROPERTY = "persist.sys.timezone";
static final int TICK_HISTORY_DEPTH = 10;
static final long INDEFINITE_DELAY = 365 * INTERVAL_DAY;
@@ -216,6 +229,19 @@ public class AlarmManagerService extends SystemService {
private static final long TEMPORARY_QUOTA_DURATION = INTERVAL_DAY;
+ /*
+ * b/246256335: This compile-time constant controls whether Android attempts to sync the Kernel
+ * time zone offset via settimeofday(null, tz). For <= Android T behavior is the same as
+ * {@code true}, the state for future releases is the same as {@code false}.
+ * It is unlikely anything depends on this, but a compile-time constant has been used to limit
+ * the size of the revert if this proves to be invorrect. The guarded code and associated
+ * methods / native code can be removed after release testing has proved that removing the
+ * behavior doesn't break anything.
+ * TODO(b/246256335): After this change has soaked for a release, remove this constant and
+ * everything it affects.
+ */
+ private static final boolean KERNEL_TIME_ZONE_SYNC_ENABLED = false;
+
private final Intent mBackgroundIntent
= new Intent().addFlags(Intent.FLAG_FROM_BACKGROUND);
@@ -232,6 +258,7 @@ public class AlarmManagerService extends SystemService {
private ActivityManagerInternal mActivityManagerInternal;
private final EconomyManagerInternal mEconomyManagerInternal;
private PackageManagerInternal mPackageManagerInternal;
+ private BatteryStatsInternal mBatteryStatsInternal;
private RoleManager mRoleManager;
private volatile PermissionManagerServiceInternal mLocalPermissionManager;
@@ -243,7 +270,8 @@ public class AlarmManagerService extends SystemService {
/**
* A map from uid to the last op-mode we have seen for
- * {@link AppOpsManager#OP_SCHEDULE_EXACT_ALARM}
+ * {@link AppOpsManager#OP_SCHEDULE_EXACT_ALARM}. Used for evaluating permission state change
+ * when the denylist changes.
*/
@VisibleForTesting
@GuardedBy("mLock")
@@ -292,6 +320,7 @@ public class AlarmManagerService extends SystemService {
final DeliveryTracker mDeliveryTracker = new DeliveryTracker();
IBinder.DeathRecipient mListenerDeathRecipient;
Intent mTimeTickIntent;
+ Bundle mTimeTickOptions;
IAlarmListener mTimeTickTrigger;
PendingIntent mDateChangeSender;
boolean mInteractive = true;
@@ -381,7 +410,7 @@ public class AlarmManagerService extends SystemService {
public long lastUsage;
}
/** Map of {package, user} -> {quotaInfo} */
- private final ArrayMap<Pair<String, Integer>, QuotaInfo> mQuotaBuffer = new ArrayMap<>();
+ private final ArrayMap<UserPackage, QuotaInfo> mQuotaBuffer = new ArrayMap<>();
private long mMaxDuration;
@@ -393,11 +422,11 @@ public class AlarmManagerService extends SystemService {
if (quota <= 0) {
return;
}
- final Pair<String, Integer> packageUser = Pair.create(packageName, userId);
- QuotaInfo currentQuotaInfo = mQuotaBuffer.get(packageUser);
+ final UserPackage userPackage = UserPackage.of(userId, packageName);
+ QuotaInfo currentQuotaInfo = mQuotaBuffer.get(userPackage);
if (currentQuotaInfo == null) {
currentQuotaInfo = new QuotaInfo();
- mQuotaBuffer.put(packageUser, currentQuotaInfo);
+ mQuotaBuffer.put(userPackage, currentQuotaInfo);
}
currentQuotaInfo.remainingQuota = quota;
currentQuotaInfo.expirationTime = nowElapsed + mMaxDuration;
@@ -405,8 +434,8 @@ public class AlarmManagerService extends SystemService {
/** Returns if the supplied package has reserve quota to fire at the given time. */
boolean hasQuota(String packageName, int userId, long triggerElapsed) {
- final Pair<String, Integer> packageUser = Pair.create(packageName, userId);
- final QuotaInfo quotaInfo = mQuotaBuffer.get(packageUser);
+ final UserPackage userPackage = UserPackage.of(userId, packageName);
+ final QuotaInfo quotaInfo = mQuotaBuffer.get(userPackage);
return quotaInfo != null && quotaInfo.remainingQuota > 0
&& triggerElapsed <= quotaInfo.expirationTime;
@@ -417,8 +446,8 @@ public class AlarmManagerService extends SystemService {
* required.
*/
void recordUsage(String packageName, int userId, long nowElapsed) {
- final Pair<String, Integer> packageUser = Pair.create(packageName, userId);
- final QuotaInfo quotaInfo = mQuotaBuffer.get(packageUser);
+ final UserPackage userPackage = UserPackage.of(userId, packageName);
+ final QuotaInfo quotaInfo = mQuotaBuffer.get(userPackage);
if (quotaInfo == null) {
Slog.wtf(TAG, "Temporary quota being consumed at " + nowElapsed
@@ -458,26 +487,26 @@ public class AlarmManagerService extends SystemService {
void removeForUser(int userId) {
for (int i = mQuotaBuffer.size() - 1; i >= 0; i--) {
- final Pair<String, Integer> packageUserKey = mQuotaBuffer.keyAt(i);
- if (packageUserKey.second == userId) {
+ final UserPackage userPackageKey = mQuotaBuffer.keyAt(i);
+ if (userPackageKey.userId == userId) {
mQuotaBuffer.removeAt(i);
}
}
}
void removeForPackage(String packageName, int userId) {
- final Pair<String, Integer> packageUser = Pair.create(packageName, userId);
- mQuotaBuffer.remove(packageUser);
+ final UserPackage userPackage = UserPackage.of(userId, packageName);
+ mQuotaBuffer.remove(userPackage);
}
void dump(IndentingPrintWriter pw, long nowElapsed) {
pw.increaseIndent();
for (int i = 0; i < mQuotaBuffer.size(); i++) {
- final Pair<String, Integer> packageUser = mQuotaBuffer.keyAt(i);
+ final UserPackage userPackage = mQuotaBuffer.keyAt(i);
final QuotaInfo quotaInfo = mQuotaBuffer.valueAt(i);
- pw.print(packageUser.first);
+ pw.print(userPackage.packageName);
pw.print(", u");
- pw.print(packageUser.second);
+ pw.print(userPackage.userId);
pw.print(": ");
if (quotaInfo == null) {
pw.print("--");
@@ -501,8 +530,7 @@ public class AlarmManagerService extends SystemService {
*/
@VisibleForTesting
static class AppWakeupHistory {
- private ArrayMap<Pair<String, Integer>, LongArrayQueue> mPackageHistory =
- new ArrayMap<>();
+ private final ArrayMap<UserPackage, LongArrayQueue> mPackageHistory = new ArrayMap<>();
private long mWindowSize;
AppWakeupHistory(long windowSize) {
@@ -510,11 +538,11 @@ public class AlarmManagerService extends SystemService {
}
void recordAlarmForPackage(String packageName, int userId, long nowElapsed) {
- final Pair<String, Integer> packageUser = Pair.create(packageName, userId);
- LongArrayQueue history = mPackageHistory.get(packageUser);
+ final UserPackage userPackage = UserPackage.of(userId, packageName);
+ LongArrayQueue history = mPackageHistory.get(userPackage);
if (history == null) {
history = new LongArrayQueue();
- mPackageHistory.put(packageUser, history);
+ mPackageHistory.put(userPackage, history);
}
if (history.size() == 0 || history.peekLast() < nowElapsed) {
history.addLast(nowElapsed);
@@ -524,16 +552,16 @@ public class AlarmManagerService extends SystemService {
void removeForUser(int userId) {
for (int i = mPackageHistory.size() - 1; i >= 0; i--) {
- final Pair<String, Integer> packageUserKey = mPackageHistory.keyAt(i);
- if (packageUserKey.second == userId) {
+ final UserPackage userPackageKey = mPackageHistory.keyAt(i);
+ if (userPackageKey.userId == userId) {
mPackageHistory.removeAt(i);
}
}
}
void removeForPackage(String packageName, int userId) {
- final Pair<String, Integer> packageUser = Pair.create(packageName, userId);
- mPackageHistory.remove(packageUser);
+ final UserPackage userPackage = UserPackage.of(userId, packageName);
+ mPackageHistory.remove(userPackage);
}
private void snapToWindow(LongArrayQueue history) {
@@ -543,7 +571,7 @@ public class AlarmManagerService extends SystemService {
}
int getTotalWakeupsInWindow(String packageName, int userId) {
- final LongArrayQueue history = mPackageHistory.get(Pair.create(packageName, userId));
+ final LongArrayQueue history = mPackageHistory.get(UserPackage.of(userId, packageName));
return (history == null) ? 0 : history.size();
}
@@ -552,7 +580,7 @@ public class AlarmManagerService extends SystemService {
* (1=1st-last=the ultimate wakeup and 2=2nd-last=the penultimate wakeup)
*/
long getNthLastWakeupForPackage(String packageName, int userId, int n) {
- final LongArrayQueue history = mPackageHistory.get(Pair.create(packageName, userId));
+ final LongArrayQueue history = mPackageHistory.get(UserPackage.of(userId, packageName));
if (history == null) {
return 0;
}
@@ -563,11 +591,11 @@ public class AlarmManagerService extends SystemService {
void dump(IndentingPrintWriter pw, long nowElapsed) {
pw.increaseIndent();
for (int i = 0; i < mPackageHistory.size(); i++) {
- final Pair<String, Integer> packageUser = mPackageHistory.keyAt(i);
+ final UserPackage userPackage = mPackageHistory.keyAt(i);
final LongArrayQueue timestamps = mPackageHistory.valueAt(i);
- pw.print(packageUser.first);
+ pw.print(userPackage.packageName);
pw.print(", u");
- pw.print(packageUser.second);
+ pw.print(userPackage.userId);
pw.print(": ");
// limit dumping to a max of 100 values
final int lastIdx = Math.max(0, timestamps.size() - 100);
@@ -587,14 +615,16 @@ public class AlarmManagerService extends SystemService {
static final int REMOVE_REASON_EXACT_PERMISSION_REVOKED = 2;
static final int REMOVE_REASON_DATA_CLEARED = 3;
static final int REMOVE_REASON_PI_CANCELLED = 4;
+ static final int REMOVE_REASON_LISTENER_BINDER_DIED = 5;
+ static final int REMOVE_REASON_LISTENER_CACHED = 6;
- final String mTag;
+ final Alarm.Snapshot mAlarmSnapshot;
final long mWhenRemovedElapsed;
final long mWhenRemovedRtc;
final int mRemoveReason;
RemovedAlarm(Alarm a, int removeReason, long nowRtc, long nowElapsed) {
- mTag = a.statsTag;
+ mAlarmSnapshot = new Alarm.Snapshot(a);
mRemoveReason = removeReason;
mWhenRemovedRtc = nowRtc;
mWhenRemovedElapsed = nowElapsed;
@@ -616,19 +646,31 @@ public class AlarmManagerService extends SystemService {
return "data_cleared";
case REMOVE_REASON_PI_CANCELLED:
return "pi_cancelled";
+ case REMOVE_REASON_LISTENER_BINDER_DIED:
+ return "listener_binder_died";
+ case REMOVE_REASON_LISTENER_CACHED:
+ return "listener_cached";
default:
return "unknown:" + reason;
}
}
void dump(IndentingPrintWriter pw, long nowElapsed, SimpleDateFormat sdf) {
- pw.print("[tag", mTag);
- pw.print("reason", removeReasonToString(mRemoveReason));
+ pw.increaseIndent();
+
+ pw.print("Reason", removeReasonToString(mRemoveReason));
pw.print("elapsed=");
TimeUtils.formatDuration(mWhenRemovedElapsed, nowElapsed, pw);
pw.print(" rtc=");
pw.print(sdf.format(new Date(mWhenRemovedRtc)));
- pw.println("]");
+ pw.println();
+
+ pw.println("Snapshot:");
+ pw.increaseIndent();
+ mAlarmSnapshot.dump(pw, nowElapsed);
+ pw.decreaseIndent();
+
+ pw.decreaseIndent();
}
}
@@ -676,12 +718,12 @@ public class AlarmManagerService extends SystemService {
private static final String KEY_APP_STANDBY_RESTRICTED_WINDOW =
"app_standby_restricted_window";
- @VisibleForTesting
- static final String KEY_LAZY_BATCHING = "lazy_batching";
-
private static final String KEY_TIME_TICK_ALLOWED_WHILE_IDLE =
"time_tick_allowed_while_idle";
+ private static final String KEY_DELAY_NONWAKEUP_ALARMS_WHILE_SCREEN_OFF =
+ "delay_nonwakeup_alarms_while_screen_off";
+
@VisibleForTesting
static final String KEY_ALLOW_WHILE_IDLE_QUOTA = "allow_while_idle_quota";
@@ -694,8 +736,6 @@ public class AlarmManagerService extends SystemService {
static final String KEY_ALLOW_WHILE_IDLE_COMPAT_WINDOW = "allow_while_idle_compat_window";
@VisibleForTesting
- static final String KEY_CRASH_NON_CLOCK_APPS = "crash_non_clock_apps";
- @VisibleForTesting
static final String KEY_PRIORITY_ALARM_DELAY = "priority_alarm_delay";
@VisibleForTesting
static final String KEY_EXACT_ALARM_DENY_LIST = "exact_alarm_deny_list";
@@ -708,6 +748,8 @@ public class AlarmManagerService extends SystemService {
"kill_on_schedule_exact_alarm_revoked";
@VisibleForTesting
static final String KEY_TEMPORARY_QUOTA_BUMP = "temporary_quota_bump";
+ @VisibleForTesting
+ static final String KEY_CACHED_LISTENER_REMOVAL_DELAY = "cached_listener_removal_delay";
private static final long DEFAULT_MIN_FUTURITY = 5 * 1000;
private static final long DEFAULT_MIN_INTERVAL = 60 * 1000;
@@ -730,20 +772,17 @@ public class AlarmManagerService extends SystemService {
private static final int DEFAULT_APP_STANDBY_RESTRICTED_QUOTA = 1;
private static final long DEFAULT_APP_STANDBY_RESTRICTED_WINDOW = INTERVAL_DAY;
- private static final boolean DEFAULT_LAZY_BATCHING = true;
private static final boolean DEFAULT_TIME_TICK_ALLOWED_WHILE_IDLE = true;
/**
* Default quota for pre-S apps. The same as allowing an alarm slot once
* every ALLOW_WHILE_IDLE_LONG_DELAY, which was 9 minutes.
*/
- private static final int DEFAULT_ALLOW_WHILE_IDLE_COMPAT_QUOTA = 1;
+ private static final int DEFAULT_ALLOW_WHILE_IDLE_COMPAT_QUOTA = 7;
private static final int DEFAULT_ALLOW_WHILE_IDLE_QUOTA = 72;
private static final long DEFAULT_ALLOW_WHILE_IDLE_WINDOW = 60 * 60 * 1000; // 1 hour.
- private static final long DEFAULT_ALLOW_WHILE_IDLE_COMPAT_WINDOW = 9 * 60 * 1000; // 9 mins.
-
- private static final boolean DEFAULT_CRASH_NON_CLOCK_APPS = true;
+ private static final long DEFAULT_ALLOW_WHILE_IDLE_COMPAT_WINDOW = 60 * 60 * 1000;
private static final long DEFAULT_PRIORITY_ALARM_DELAY = 9 * 60_000;
@@ -754,6 +793,10 @@ public class AlarmManagerService extends SystemService {
private static final int DEFAULT_TEMPORARY_QUOTA_BUMP = 0;
+ private static final boolean DEFAULT_DELAY_NONWAKEUP_ALARMS_WHILE_SCREEN_OFF = true;
+
+ private static final long DEFAULT_CACHED_LISTENER_REMOVAL_DELAY = 10_000;
+
// Minimum futurity of a new alarm
public long MIN_FUTURITY = DEFAULT_MIN_FUTURITY;
@@ -779,7 +822,6 @@ public class AlarmManagerService extends SystemService {
public int APP_STANDBY_RESTRICTED_QUOTA = DEFAULT_APP_STANDBY_RESTRICTED_QUOTA;
public long APP_STANDBY_RESTRICTED_WINDOW = DEFAULT_APP_STANDBY_RESTRICTED_WINDOW;
- public boolean LAZY_BATCHING = DEFAULT_LAZY_BATCHING;
public boolean TIME_TICK_ALLOWED_WHILE_IDLE = DEFAULT_TIME_TICK_ALLOWED_WHILE_IDLE;
public int ALLOW_WHILE_IDLE_QUOTA = DEFAULT_ALLOW_WHILE_IDLE_QUOTA;
@@ -803,13 +845,6 @@ public class AlarmManagerService extends SystemService {
public long ALLOW_WHILE_IDLE_WINDOW = DEFAULT_ALLOW_WHILE_IDLE_WINDOW;
/**
- * Whether or not to crash callers that use setExactAndAllowWhileIdle or setAlarmClock
- * but don't hold the required permission. This is useful to catch broken
- * apps and reverting to a softer failure in case of broken apps.
- */
- public boolean CRASH_NON_CLOCK_APPS = DEFAULT_CRASH_NON_CLOCK_APPS;
-
- /**
* Minimum delay between two slots that an app can get for their prioritized alarms, while
* the device is in doze.
*/
@@ -841,7 +876,8 @@ public class AlarmManagerService extends SystemService {
public boolean KILL_ON_SCHEDULE_EXACT_ALARM_REVOKED =
DEFAULT_KILL_ON_SCHEDULE_EXACT_ALARM_REVOKED;
- public boolean USE_TARE_POLICY = Settings.Global.DEFAULT_ENABLE_TARE == 1;
+ public int USE_TARE_POLICY = EconomyManager.DEFAULT_ENABLE_POLICY_ALARM
+ ? EconomyManager.DEFAULT_ENABLE_TARE_MODE : EconomyManager.ENABLED_MODE_OFF;
/**
* The amount of temporary reserve quota to give apps on receiving the
@@ -854,6 +890,16 @@ public class AlarmManagerService extends SystemService {
*/
public int TEMPORARY_QUOTA_BUMP = DEFAULT_TEMPORARY_QUOTA_BUMP;
+ public boolean DELAY_NONWAKEUP_ALARMS_WHILE_SCREEN_OFF =
+ DEFAULT_DELAY_NONWAKEUP_ALARMS_WHILE_SCREEN_OFF;
+
+ /**
+ * Exact listener alarms for apps that get cached are removed after this duration. This is
+ * a grace period to allow for transient procstate changes, e.g., when the app switches
+ * between different lifecycles.
+ */
+ public long CACHED_LISTENER_REMOVAL_DELAY = DEFAULT_CACHED_LISTENER_REMOVAL_DELAY;
+
private long mLastAllowWhileIdleWhitelistDuration = -1;
private int mVersion = 0;
@@ -874,9 +920,11 @@ public class AlarmManagerService extends SystemService {
mInjector.registerDeviceConfigListener(this);
final EconomyManagerInternal economyManagerInternal =
LocalServices.getService(EconomyManagerInternal.class);
- economyManagerInternal.registerTareStateChangeListener(this);
+ economyManagerInternal.registerTareStateChangeListener(this,
+ AlarmManagerEconomicPolicy.POLICY_ALARM);
onPropertiesChanged(DeviceConfig.getProperties(DeviceConfig.NAMESPACE_ALARM_MANAGER));
- updateTareSettings(economyManagerInternal.isEnabled());
+ updateTareSettings(
+ economyManagerInternal.getEnabledMode(AlarmManagerEconomicPolicy.POLICY_ALARM));
}
public void updateAllowWhileIdleWhitelistDurationLocked() {
@@ -990,23 +1038,11 @@ public class AlarmManagerService extends SystemService {
case KEY_APP_STANDBY_RESTRICTED_WINDOW:
updateStandbyWindowsLocked();
break;
- case KEY_LAZY_BATCHING:
- final boolean oldLazyBatching = LAZY_BATCHING;
- LAZY_BATCHING = properties.getBoolean(
- KEY_LAZY_BATCHING, DEFAULT_LAZY_BATCHING);
- if (oldLazyBatching != LAZY_BATCHING) {
- migrateAlarmsToNewStoreLocked();
- }
- break;
case KEY_TIME_TICK_ALLOWED_WHILE_IDLE:
TIME_TICK_ALLOWED_WHILE_IDLE = properties.getBoolean(
KEY_TIME_TICK_ALLOWED_WHILE_IDLE,
DEFAULT_TIME_TICK_ALLOWED_WHILE_IDLE);
break;
- case KEY_CRASH_NON_CLOCK_APPS:
- CRASH_NON_CLOCK_APPS = properties.getBoolean(KEY_CRASH_NON_CLOCK_APPS,
- DEFAULT_CRASH_NON_CLOCK_APPS);
- break;
case KEY_PRIORITY_ALARM_DELAY:
PRIORITY_ALARM_DELAY = properties.getLong(KEY_PRIORITY_ALARM_DELAY,
DEFAULT_PRIORITY_ALARM_DELAY);
@@ -1042,6 +1078,16 @@ public class AlarmManagerService extends SystemService {
TEMPORARY_QUOTA_BUMP = properties.getInt(KEY_TEMPORARY_QUOTA_BUMP,
DEFAULT_TEMPORARY_QUOTA_BUMP);
break;
+ case KEY_DELAY_NONWAKEUP_ALARMS_WHILE_SCREEN_OFF:
+ DELAY_NONWAKEUP_ALARMS_WHILE_SCREEN_OFF = properties.getBoolean(
+ KEY_DELAY_NONWAKEUP_ALARMS_WHILE_SCREEN_OFF,
+ DEFAULT_DELAY_NONWAKEUP_ALARMS_WHILE_SCREEN_OFF);
+ break;
+ case KEY_CACHED_LISTENER_REMOVAL_DELAY:
+ CACHED_LISTENER_REMOVAL_DELAY = properties.getLong(
+ KEY_CACHED_LISTENER_REMOVAL_DELAY,
+ DEFAULT_CACHED_LISTENER_REMOVAL_DELAY);
+ break;
default:
if (name.startsWith(KEY_PREFIX_STANDBY_QUOTA) && !standbyQuotaUpdated) {
// The quotas need to be updated in order, so we can't just rely
@@ -1056,18 +1102,19 @@ public class AlarmManagerService extends SystemService {
}
@Override
- public void onTareEnabledStateChanged(boolean isTareEnabled) {
- updateTareSettings(isTareEnabled);
+ public void onTareEnabledModeChanged(@EconomyManager.EnabledMode int enabledMode) {
+ updateTareSettings(enabledMode);
}
- private void updateTareSettings(boolean isTareEnabled) {
+ private void updateTareSettings(int enabledMode) {
synchronized (mLock) {
- if (USE_TARE_POLICY != isTareEnabled) {
- USE_TARE_POLICY = isTareEnabled;
+ if (USE_TARE_POLICY != enabledMode) {
+ USE_TARE_POLICY = enabledMode;
final boolean changed = mAlarmStore.updateAlarmDeliveries(alarm -> {
final boolean standbyChanged = adjustDeliveryTimeBasedOnBucketLocked(alarm);
final boolean tareChanged = adjustDeliveryTimeBasedOnTareLocked(alarm);
- if (USE_TARE_POLICY) {
+ if (USE_TARE_POLICY == EconomyManager.ENABLED_MODE_ON) {
+ // Only register listeners if we're going to be acting on the policy.
registerTareListener(alarm);
} else {
mEconomyManagerInternal.unregisterAffordabilityChangeListener(
@@ -1077,7 +1124,7 @@ public class AlarmManagerService extends SystemService {
}
return standbyChanged || tareChanged;
});
- if (!USE_TARE_POLICY) {
+ if (USE_TARE_POLICY != EconomyManager.ENABLED_MODE_ON) {
// Remove the cached values so we don't accidentally use them when TARE is
// re-enabled.
mAffordabilityCache.clear();
@@ -1112,15 +1159,6 @@ public class AlarmManagerService extends SystemService {
}
}
- private void migrateAlarmsToNewStoreLocked() {
- final AlarmStore newStore = LAZY_BATCHING ? new LazyAlarmStore()
- : new BatchingAlarmStore();
- final ArrayList<Alarm> allAlarms = mAlarmStore.remove((unused) -> true);
- newStore.addAll(allAlarms);
- mAlarmStore = newStore;
- mAlarmStore.setAlarmClockRemovalListener(mAlarmClockUpdater);
- }
-
private void updateDeviceIdleFuzzBoundaries() {
final DeviceConfig.Properties properties = DeviceConfig.getProperties(
DeviceConfig.NAMESPACE_ALARM_MANAGER,
@@ -1258,15 +1296,9 @@ public class AlarmManagerService extends SystemService {
TimeUtils.formatDuration(APP_STANDBY_RESTRICTED_WINDOW, pw);
pw.println();
- pw.print(KEY_LAZY_BATCHING, LAZY_BATCHING);
- pw.println();
-
pw.print(KEY_TIME_TICK_ALLOWED_WHILE_IDLE, TIME_TICK_ALLOWED_WHILE_IDLE);
pw.println();
- pw.print(KEY_CRASH_NON_CLOCK_APPS, CRASH_NON_CLOCK_APPS);
- pw.println();
-
pw.print(KEY_PRIORITY_ALARM_DELAY);
pw.print("=");
TimeUtils.formatDuration(PRIORITY_ALARM_DELAY, pw);
@@ -1289,12 +1321,22 @@ public class AlarmManagerService extends SystemService {
KILL_ON_SCHEDULE_EXACT_ALARM_REVOKED);
pw.println();
- pw.print(Settings.Global.ENABLE_TARE, USE_TARE_POLICY);
+ pw.print(Settings.Global.ENABLE_TARE,
+ EconomyManager.enabledModeToString(USE_TARE_POLICY));
pw.println();
pw.print(KEY_TEMPORARY_QUOTA_BUMP, TEMPORARY_QUOTA_BUMP);
pw.println();
+ pw.print(KEY_DELAY_NONWAKEUP_ALARMS_WHILE_SCREEN_OFF,
+ DELAY_NONWAKEUP_ALARMS_WHILE_SCREEN_OFF);
+ pw.println();
+
+ pw.print(KEY_CACHED_LISTENER_REMOVAL_DELAY);
+ pw.print("=");
+ TimeUtils.formatDuration(CACHED_LISTENER_REMOVAL_DELAY, pw);
+ pw.println();
+
pw.decreaseIndent();
}
@@ -1426,7 +1468,7 @@ public class AlarmManagerService extends SystemService {
private long convertToElapsed(long when, int type) {
if (isRtc(type)) {
- when -= mInjector.getCurrentTimeMillis() - mInjector.getElapsedRealtime();
+ when -= mInjector.getCurrentTimeMillis() - mInjector.getElapsedRealtimeMillis();
}
return when;
}
@@ -1504,13 +1546,13 @@ public class AlarmManagerService extends SystemService {
* null indicates all
* @return True if there was any reordering done to the current list.
*/
- boolean reorderAlarmsBasedOnStandbyBuckets(ArraySet<Pair<String, Integer>> targetPackages) {
+ boolean reorderAlarmsBasedOnStandbyBuckets(ArraySet<UserPackage> targetPackages) {
final long start = mStatLogger.getTime();
final boolean changed = mAlarmStore.updateAlarmDeliveries(a -> {
- final Pair<String, Integer> packageUser =
- Pair.create(a.sourcePackage, UserHandle.getUserId(a.creatorUid));
- if (targetPackages != null && !targetPackages.contains(packageUser)) {
+ final UserPackage userPackage =
+ UserPackage.of(UserHandle.getUserId(a.creatorUid), a.sourcePackage);
+ if (targetPackages != null && !targetPackages.contains(userPackage)) {
return false;
}
return adjustDeliveryTimeBasedOnBucketLocked(a);
@@ -1527,13 +1569,13 @@ public class AlarmManagerService extends SystemService {
* null indicates all
* @return True if there was any reordering done to the current list.
*/
- boolean reorderAlarmsBasedOnTare(ArraySet<Pair<String, Integer>> targetPackages) {
+ boolean reorderAlarmsBasedOnTare(ArraySet<UserPackage> targetPackages) {
final long start = mStatLogger.getTime();
final boolean changed = mAlarmStore.updateAlarmDeliveries(a -> {
- final Pair<String, Integer> packageUser =
- Pair.create(a.sourcePackage, UserHandle.getUserId(a.creatorUid));
- if (targetPackages != null && !targetPackages.contains(packageUser)) {
+ final UserPackage userPackage =
+ UserPackage.of(UserHandle.getUserId(a.creatorUid), a.sourcePackage);
+ if (targetPackages != null && !targetPackages.contains(userPackage)) {
return false;
}
return adjustDeliveryTimeBasedOnTareLocked(a);
@@ -1586,7 +1628,7 @@ public class AlarmManagerService extends SystemService {
alarmsToDeliver = alarmsForUid;
mPendingBackgroundAlarms.remove(uid);
}
- deliverPendingBackgroundAlarmsLocked(alarmsToDeliver, mInjector.getElapsedRealtime());
+ deliverPendingBackgroundAlarmsLocked(alarmsToDeliver, mInjector.getElapsedRealtimeMillis());
}
/**
@@ -1603,7 +1645,8 @@ public class AlarmManagerService extends SystemService {
mPendingBackgroundAlarms, alarmsToDeliver, this::isBackgroundRestricted);
if (alarmsToDeliver.size() > 0) {
- deliverPendingBackgroundAlarmsLocked(alarmsToDeliver, mInjector.getElapsedRealtime());
+ deliverPendingBackgroundAlarmsLocked(
+ alarmsToDeliver, mInjector.getElapsedRealtimeMillis());
}
}
@@ -1892,7 +1935,9 @@ public class AlarmManagerService extends SystemService {
@Override
public void binderDied(IBinder who) {
final IAlarmListener listener = IAlarmListener.Stub.asInterface(who);
- removeImpl(null, listener);
+ synchronized (mLock) {
+ removeLocked(null, listener, REMOVE_REASON_LISTENER_BINDER_DIED);
+ }
}
};
@@ -1900,8 +1945,7 @@ public class AlarmManagerService extends SystemService {
mHandler = new AlarmHandler();
mConstants = new Constants(mHandler);
- mAlarmStore = mConstants.LAZY_BATCHING ? new LazyAlarmStore()
- : new BatchingAlarmStore();
+ mAlarmStore = new LazyAlarmStore();
mAlarmStore.setAlarmClockRemovalListener(mAlarmClockUpdater);
mAppWakeupHistory = new AppWakeupHistory(Constants.DEFAULT_APP_STANDBY_WINDOW);
@@ -1912,22 +1956,16 @@ public class AlarmManagerService extends SystemService {
mNextWakeup = mNextNonWakeup = 0;
- // We have to set current TimeZone info to kernel
- // because kernel doesn't keep this after reboot
- setTimeZoneImpl(SystemProperties.get(TIMEZONE_PROPERTY));
-
- // Ensure that we're booting with a halfway sensible current time. Use the
- // most recent of Build.TIME, the root file system's timestamp, and the
- // value of the ro.build.date.utc system property (which is in seconds).
- final long systemBuildTime = Long.max(
- 1000L * SystemProperties.getLong("ro.build.date.utc", -1L),
- Long.max(Environment.getRootDirectory().lastModified(), Build.TIME));
- if (mInjector.getCurrentTimeMillis() < systemBuildTime) {
- Slog.i(TAG, "Current time only " + mInjector.getCurrentTimeMillis()
- + ", advancing to build time " + systemBuildTime);
- mInjector.setKernelTime(systemBuildTime);
+ if (KERNEL_TIME_ZONE_SYNC_ENABLED) {
+ // We set the current offset in kernel because the kernel doesn't keep this after a
+ // reboot. Keeping the kernel time zone in sync is "best effort" and can be wrong
+ // for a period after daylight savings transitions.
+ mInjector.syncKernelTimeZoneOffset();
}
+ // Ensure that we're booting with a halfway sensible current time.
+ mInjector.initializeTimeIfRequired();
+
mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
// Determine SysUI's uid
mSystemUiUid = mInjector.getSystemUiUid(mPackageManagerInternal);
@@ -1940,7 +1978,10 @@ public class AlarmManagerService extends SystemService {
Intent.FLAG_RECEIVER_REGISTERED_ONLY
| Intent.FLAG_RECEIVER_FOREGROUND
| Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS);
-
+ mTimeTickOptions = BroadcastOptions.makeBasic()
+ .setDeliveryGroupPolicy(BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT)
+ .setDeferralPolicy(BroadcastOptions.DEFERRAL_POLICY_UNTIL_ACTIVE)
+ .toBundle();
mTimeTickTrigger = new IAlarmListener.Stub() {
@Override
public void doAlarm(final IAlarmCompleteListener callback) throws RemoteException {
@@ -1952,8 +1993,8 @@ public class AlarmManagerService extends SystemService {
// takes care of this automatically, but we're using the direct internal
// interface here rather than that client-side wrapper infrastructure.
mHandler.post(() -> {
- getContext().sendBroadcastAsUser(mTimeTickIntent, UserHandle.ALL);
-
+ getContext().sendBroadcastAsUser(mTimeTickIntent, UserHandle.ALL, null,
+ mTimeTickOptions);
try {
callback.alarmComplete(this);
} catch (RemoteException e) { /* local method call */ }
@@ -2087,20 +2128,31 @@ public class AlarmManagerService extends SystemService {
if (oldMode == newMode) {
return;
}
- final boolean allowedByDefault =
- isScheduleExactAlarmAllowedByDefault(packageName, uid);
+ final boolean deniedByDefault = isScheduleExactAlarmDeniedByDefault(
+ packageName, UserHandle.getUserId(uid));
final boolean hadPermission;
- if (oldMode != AppOpsManager.MODE_DEFAULT) {
- hadPermission = (oldMode == AppOpsManager.MODE_ALLOWED);
- } else {
- hadPermission = allowedByDefault;
- }
final boolean hasPermission;
- if (newMode != AppOpsManager.MODE_DEFAULT) {
- hasPermission = (newMode == AppOpsManager.MODE_ALLOWED);
+
+ if (deniedByDefault) {
+ final boolean permissionState = getContext().checkPermission(
+ Manifest.permission.SCHEDULE_EXACT_ALARM, PID_UNKNOWN,
+ uid) == PackageManager.PERMISSION_GRANTED;
+ hadPermission = (oldMode == AppOpsManager.MODE_DEFAULT)
+ ? permissionState
+ : (oldMode == AppOpsManager.MODE_ALLOWED);
+ hasPermission = (newMode == AppOpsManager.MODE_DEFAULT)
+ ? permissionState
+ : (newMode == AppOpsManager.MODE_ALLOWED);
} else {
- hasPermission = allowedByDefault;
+ final boolean allowedByDefault =
+ !mConstants.EXACT_ALARM_DENY_LIST.contains(packageName);
+ hadPermission = (oldMode == AppOpsManager.MODE_DEFAULT)
+ ? allowedByDefault
+ : (oldMode == AppOpsManager.MODE_ALLOWED);
+ hasPermission = (newMode == AppOpsManager.MODE_DEFAULT)
+ ? allowedByDefault
+ : (newMode == AppOpsManager.MODE_ALLOWED);
}
if (hadPermission && !hasPermission) {
@@ -2123,6 +2175,8 @@ public class AlarmManagerService extends SystemService {
LocalServices.getService(AppStandbyInternal.class);
appStandbyInternal.addListener(new AppStandbyTracker());
+ mBatteryStatsInternal = LocalServices.getService(BatteryStatsInternal.class);
+
mRoleManager = getContext().getSystemService(RoleManager.class);
mMetricsHelper.registerPuller(() -> mAlarmStore);
@@ -2138,22 +2192,24 @@ public class AlarmManagerService extends SystemService {
}
}
- boolean setTimeImpl(long millis) {
- if (!mInjector.isAlarmDriverPresent()) {
- Slog.w(TAG, "Not setting time since no alarm driver is available.");
- return false;
- }
-
+ boolean setTimeImpl(
+ @CurrentTimeMillisLong long newSystemClockTimeMillis, @TimeConfidence int confidence,
+ @NonNull String logMsg) {
synchronized (mLock) {
- final long currentTimeMillis = mInjector.getCurrentTimeMillis();
- mInjector.setKernelTime(millis);
- final TimeZone timeZone = TimeZone.getDefault();
- final int currentTzOffset = timeZone.getOffset(currentTimeMillis);
- final int newTzOffset = timeZone.getOffset(millis);
- if (currentTzOffset != newTzOffset) {
- Slog.i(TAG, "Timezone offset has changed, updating kernel timezone");
- mInjector.setKernelTimezone(-(newTzOffset / 60000));
+ final long oldSystemClockTimeMillis = mInjector.getCurrentTimeMillis();
+ mInjector.setCurrentTimeMillis(newSystemClockTimeMillis, confidence, logMsg);
+
+ if (KERNEL_TIME_ZONE_SYNC_ENABLED) {
+ // Changing the time may cross a DST transition; sync the kernel offset if needed.
+ final TimeZone timeZone = TimeZone.getTimeZone(SystemTimeZone.getTimeZoneId());
+ final int currentTzOffset = timeZone.getOffset(oldSystemClockTimeMillis);
+ final int newTzOffset = timeZone.getOffset(newSystemClockTimeMillis);
+ if (currentTzOffset != newTzOffset) {
+ Slog.i(TAG, "Timezone offset has changed, updating kernel timezone");
+ mInjector.setKernelTimeZoneOffset(newTzOffset);
+ }
}
+
// The native implementation of setKernelTime can return -1 even when the kernel
// time was set correctly, so assume setting kernel time was successful and always
// return true.
@@ -2161,31 +2217,30 @@ public class AlarmManagerService extends SystemService {
}
}
- void setTimeZoneImpl(String tz) {
- if (TextUtils.isEmpty(tz)) {
+ void setTimeZoneImpl(String tzId, @TimeZoneConfidence int confidence, String logInfo) {
+ if (TextUtils.isEmpty(tzId)) {
return;
}
- TimeZone zone = TimeZone.getTimeZone(tz);
+ TimeZone newZone = TimeZone.getTimeZone(tzId);
// Prevent reentrant calls from stepping on each other when writing
// the time zone property
- boolean timeZoneWasChanged = false;
+ boolean timeZoneWasChanged;
synchronized (this) {
- String current = SystemProperties.get(TIMEZONE_PROPERTY);
- if (current == null || !current.equals(zone.getID())) {
- if (localLOGV) {
- Slog.v(TAG, "timezone changed: " + current + ", new=" + zone.getID());
- }
- timeZoneWasChanged = true;
- SystemProperties.set(TIMEZONE_PROPERTY, zone.getID());
- }
+ // TimeZone.getTimeZone() can return a time zone with a different ID (e.g. it can return
+ // "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);
- // Update the kernel timezone information
- // Kernel tracks time offsets as 'minutes west of GMT'
- int gmtOffset = zone.getOffset(mInjector.getCurrentTimeMillis());
- mInjector.setKernelTimezone(-(gmtOffset / 60000));
+ if (KERNEL_TIME_ZONE_SYNC_ENABLED) {
+ // Update the kernel timezone information
+ int utcOffsetMillis = newZone.getOffset(mInjector.getCurrentTimeMillis());
+ mInjector.setKernelTimeZoneOffset(utcOffsetMillis);
+ }
}
+ // Clear the default time zone in the system server process. This forces the next call
+ // to TimeZone.getDefault() to re-read the device settings.
TimeZone.setDefault(null);
if (timeZoneWasChanged) {
@@ -2198,7 +2253,7 @@ public class AlarmManagerService extends SystemService {
| Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND
| Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
| Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS);
- intent.putExtra(Intent.EXTRA_TIMEZONE, zone.getID());
+ intent.putExtra(Intent.EXTRA_TIMEZONE, newZone.getID());
mOptsTimeBroadcast.setTemporaryAppAllowlist(
mActivityManagerInternal.getBootTimeTempAllowListDuration(),
TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED,
@@ -2261,7 +2316,7 @@ public class AlarmManagerService extends SystemService {
triggerAtTime = 0;
}
- final long nowElapsed = mInjector.getElapsedRealtime();
+ final long nowElapsed = mInjector.getElapsedRealtimeMillis();
final long nominalTrigger = convertToElapsed(triggerAtTime, type);
// Try to prevent spamming by making sure apps aren't firing alarms in the immediate future
final long minTrigger = nowElapsed
@@ -2384,7 +2439,7 @@ public class AlarmManagerService extends SystemService {
// No need to fuzz as this is already earlier than the coming wake-from-idle.
return changedBeforeFuzz;
}
- final long nowElapsed = mInjector.getElapsedRealtime();
+ final long nowElapsed = mInjector.getElapsedRealtimeMillis();
final long futurity = upcomingWakeFromIdle - nowElapsed;
if (futurity <= mConstants.MIN_DEVICE_IDLE_FUZZ) {
@@ -2406,7 +2461,7 @@ public class AlarmManagerService extends SystemService {
* @return {@code true} if the alarm delivery time was updated.
*/
private boolean adjustDeliveryTimeBasedOnBatterySaver(Alarm alarm) {
- final long nowElapsed = mInjector.getElapsedRealtime();
+ final long nowElapsed = mInjector.getElapsedRealtimeMillis();
if (isExemptFromBatterySaver(alarm)) {
return false;
}
@@ -2473,7 +2528,7 @@ public class AlarmManagerService extends SystemService {
* @return {@code true} if the alarm delivery time was updated.
*/
private boolean adjustDeliveryTimeBasedOnDeviceIdle(Alarm alarm) {
- final long nowElapsed = mInjector.getElapsedRealtime();
+ final long nowElapsed = mInjector.getElapsedRealtimeMillis();
if (mPendingIdleUntil == null || mPendingIdleUntil == alarm) {
return alarm.setPolicyElapsed(DEVICE_IDLE_POLICY_INDEX, nowElapsed);
}
@@ -2528,8 +2583,9 @@ public class AlarmManagerService extends SystemService {
* adjustments made in this call.
*/
private boolean adjustDeliveryTimeBasedOnBucketLocked(Alarm alarm) {
- final long nowElapsed = mInjector.getElapsedRealtime();
- if (mConstants.USE_TARE_POLICY || isExemptFromAppStandby(alarm) || mAppStandbyParole) {
+ final long nowElapsed = mInjector.getElapsedRealtimeMillis();
+ if (mConstants.USE_TARE_POLICY == EconomyManager.ENABLED_MODE_ON
+ || isExemptFromAppStandby(alarm) || mAppStandbyParole) {
return alarm.setPolicyElapsed(APP_STANDBY_POLICY_INDEX, nowElapsed);
}
@@ -2588,8 +2644,8 @@ public class AlarmManagerService extends SystemService {
* adjustments made in this call.
*/
private boolean adjustDeliveryTimeBasedOnTareLocked(Alarm alarm) {
- final long nowElapsed = mInjector.getElapsedRealtime();
- if (!mConstants.USE_TARE_POLICY
+ final long nowElapsed = mInjector.getElapsedRealtimeMillis();
+ if (mConstants.USE_TARE_POLICY != EconomyManager.ENABLED_MODE_ON
|| isExemptFromTare(alarm) || hasEnoughWealthLocked(alarm)) {
return alarm.setPolicyElapsed(TARE_POLICY_INDEX, nowElapsed);
}
@@ -2599,7 +2655,8 @@ public class AlarmManagerService extends SystemService {
}
private void registerTareListener(Alarm alarm) {
- if (!mConstants.USE_TARE_POLICY) {
+ if (mConstants.USE_TARE_POLICY != EconomyManager.ENABLED_MODE_ON) {
+ // Only register listeners if we're going to be acting on the policy.
return;
}
mEconomyManagerInternal.registerAffordabilityChangeListener(
@@ -2610,7 +2667,7 @@ public class AlarmManagerService extends SystemService {
/** Unregister the TARE listener associated with the alarm if it's no longer needed. */
@GuardedBy("mLock")
private void maybeUnregisterTareListenerLocked(Alarm alarm) {
- if (!mConstants.USE_TARE_POLICY) {
+ if (mConstants.USE_TARE_POLICY != EconomyManager.ENABLED_MODE_ON) {
return;
}
final EconomyManagerInternal.ActionBill bill = TareBill.getAppropriateBill(alarm);
@@ -2644,7 +2701,7 @@ public class AlarmManagerService extends SystemService {
ent.pkg = a.sourcePackage;
ent.tag = a.statsTag;
ent.op = "START IDLE";
- ent.elapsedRealtime = mInjector.getElapsedRealtime();
+ ent.elapsedRealtime = mInjector.getElapsedRealtimeMillis();
ent.argRealtime = a.getWhenElapsed();
mAllowWhileIdleDispatches.add(ent);
}
@@ -2709,9 +2766,24 @@ public class AlarmManagerService extends SystemService {
}
@Override
- public boolean hasExactAlarmPermission(String packageName, int uid) {
- return hasScheduleExactAlarmInternal(packageName, uid)
- || hasUseExactAlarmInternal(packageName, uid);
+ public boolean shouldGetBucketElevation(String packageName, int uid) {
+ return hasUseExactAlarmInternal(packageName, uid) || (!CompatChanges.isChangeEnabled(
+ AlarmManager.SCHEDULE_EXACT_ALARM_DOES_NOT_ELEVATE_BUCKET, packageName,
+ UserHandle.getUserHandleForUid(uid)) && hasScheduleExactAlarmInternal(
+ packageName, uid));
+ }
+
+ @Override
+ public void setTimeZone(String tzId, @TimeZoneConfidence int confidence,
+ String logInfo) {
+ setTimeZoneImpl(tzId, confidence, logInfo);
+ }
+
+ @Override
+ public void setTime(
+ @CurrentTimeMillisLong long unixEpochTimeMillis, int confidence,
+ String logMsg) {
+ setTimeImpl(unixEpochTimeMillis, confidence, logMsg);
}
@Override
@@ -2724,41 +2796,13 @@ public class AlarmManagerService extends SystemService {
boolean hasUseExactAlarmInternal(String packageName, int uid) {
return isUseExactAlarmEnabled(packageName, UserHandle.getUserId(uid))
- && (PermissionChecker.checkPermissionForPreflight(getContext(),
- Manifest.permission.USE_EXACT_ALARM, PermissionChecker.PID_UNKNOWN, uid,
- packageName) == PermissionChecker.PERMISSION_GRANTED);
- }
-
- /**
- * Returns whether SCHEDULE_EXACT_ALARM is allowed by default.
- */
- boolean isScheduleExactAlarmAllowedByDefault(String packageName, int uid) {
- if (isScheduleExactAlarmDeniedByDefault(packageName, UserHandle.getUserId(uid))) {
-
- // This is essentially like changing the protection level of the permission to
- // (privileged|signature|role|appop), but have to implement this logic to maintain
- // compatibility for older apps.
- if (mPackageManagerInternal.isPlatformSigned(packageName)
- || mPackageManagerInternal.isUidPrivileged(uid)) {
- return true;
- }
- final long token = Binder.clearCallingIdentity();
- try {
- final List<String> wellbeingHolders = (mRoleManager != null)
- ? mRoleManager.getRoleHolders(RoleManager.ROLE_SYSTEM_WELLBEING)
- : Collections.emptyList();
- return wellbeingHolders.contains(packageName);
- } finally {
- Binder.restoreCallingIdentity(token);
- }
- }
- return !mConstants.EXACT_ALARM_DENY_LIST.contains(packageName);
+ && (checkPermissionForPreflight(getContext(), Manifest.permission.USE_EXACT_ALARM,
+ PID_UNKNOWN, uid, packageName) == PERMISSION_GRANTED);
}
boolean hasScheduleExactAlarmInternal(String packageName, int uid) {
final long start = mStatLogger.getTime();
- // Not using getScheduleExactAlarmState as this can avoid some calls to AppOpsService.
// Not using #mLastOpScheduleExactAlarm as it may contain stale values.
// No locking needed as all internal containers being queried are immutable.
final boolean hasPermission;
@@ -2766,11 +2810,16 @@ public class AlarmManagerService extends SystemService {
hasPermission = false;
} else if (!isExactAlarmChangeEnabled(packageName, UserHandle.getUserId(uid))) {
hasPermission = false;
+ } else if (isScheduleExactAlarmDeniedByDefault(packageName, UserHandle.getUserId(uid))) {
+ hasPermission = (checkPermissionForPreflight(getContext(),
+ Manifest.permission.SCHEDULE_EXACT_ALARM, PID_UNKNOWN, uid, packageName)
+ == PERMISSION_GRANTED);
} else {
+ // Compatibility permission check for older apps.
final int mode = mAppOps.checkOpNoThrow(AppOpsManager.OP_SCHEDULE_EXACT_ALARM, uid,
packageName);
if (mode == AppOpsManager.MODE_DEFAULT) {
- hasPermission = isScheduleExactAlarmAllowedByDefault(packageName, uid);
+ hasPermission = !mConstants.EXACT_ALARM_DENY_LIST.contains(packageName);
} else {
hasPermission = (mode == AppOpsManager.MODE_ALLOWED);
}
@@ -2880,12 +2929,23 @@ public class AlarmManagerService extends SystemService {
// The API doesn't allow using both together.
flags &= ~FLAG_ALLOW_WHILE_IDLE;
// Prioritized alarms don't need any extra permission to be exact.
+ if (exact) {
+ exactAllowReason = EXACT_ALLOW_REASON_PRIORITIZED;
+ }
} else if (exact || allowWhileIdle) {
final boolean needsPermission;
boolean lowerQuota;
if (isExactAlarmChangeEnabled(callingPackage, callingUserId)) {
- needsPermission = exact;
- lowerQuota = !exact;
+ if (directReceiver == null) {
+ needsPermission = exact;
+ lowerQuota = !exact;
+ } else {
+ needsPermission = false;
+ lowerQuota = allowWhileIdle;
+ if (exact) {
+ exactAllowReason = EXACT_ALLOW_REASON_LISTENER;
+ }
+ }
if (exact) {
idleOptions = (alarmClock != null) ? mOptsWithFgsForAlarmClock.toBundle()
: mOptsWithFgs.toBundle();
@@ -2918,18 +2978,12 @@ public class AlarmManagerService extends SystemService {
+ Manifest.permission.SCHEDULE_EXACT_ALARM + " or "
+ Manifest.permission.USE_EXACT_ALARM + " to set "
+ "exact alarms.";
- if (mConstants.CRASH_NON_CLOCK_APPS) {
- throw new SecurityException(errorMessage);
- } else {
- Slog.wtf(TAG, errorMessage);
- }
+ throw new SecurityException(errorMessage);
}
// If the app is on the full system power allow-list (not except-idle),
- // or the user-elected allow-list, or we're in a soft failure mode, we still
- // allow the alarms.
- // In both cases, ALLOW_WHILE_IDLE alarms get a lower quota equivalent to
- // what pre-S apps got. Note that user-allow-listed apps don't use the flag
- // ALLOW_WHILE_IDLE.
+ // or the user-elected allow-list, we allow exact alarms.
+ // ALLOW_WHILE_IDLE alarms get a lower quota equivalent to what pre-S apps
+ // got. Note that user-allow-listed apps don't use FLAG_ALLOW_WHILE_IDLE.
// We grant temporary allow-list to allow-while-idle alarms but without FGS
// capability. AlarmClock alarms do not get the temporary allow-list.
// This is consistent with pre-S behavior. Note that apps that are in
@@ -2987,12 +3041,16 @@ public class AlarmManagerService extends SystemService {
}
@Override
- public boolean setTime(long millis) {
+ public boolean setTime(@CurrentTimeMillisLong long millis) {
getContext().enforceCallingOrSelfPermission(
"android.permission.SET_TIME",
"setTime");
- return setTimeImpl(millis);
+ // The public API (and the shell command that also uses this method) have no concept
+ // of confidence, but since the time should come either from apps working on behalf of
+ // the user or a developer, confidence is assumed "high".
+ final int timeConfidence = TIME_CONFIDENCE_HIGH;
+ return setTimeImpl(millis, timeConfidence, "AlarmManager.setTime() called");
}
@Override
@@ -3003,7 +3061,11 @@ public class AlarmManagerService extends SystemService {
final long oldId = Binder.clearCallingIdentity();
try {
- setTimeZoneImpl(tz);
+ // The public API (and the shell command that also uses this method) have no concept
+ // of confidence, but since the time zone ID should come either from apps working on
+ // behalf of the user or a developer, confidence is assumed "high".
+ final int timeZoneConfidence = TIME_ZONE_CONFIDENCE_HIGH;
+ setTimeZoneImpl(tz, timeZoneConfidence, "AlarmManager.setTimeZone() called");
} finally {
Binder.restoreCallingIdentity(oldId);
}
@@ -3021,6 +3083,26 @@ public class AlarmManagerService extends SystemService {
}
@Override
+ public void removeAll(String callingPackage) {
+ final int callingUid = mInjector.getCallingUid();
+ if (callingUid == Process.SYSTEM_UID) {
+ Slog.wtfStack(TAG, "Attempt to remove all alarms from the system uid package: "
+ + callingPackage);
+ return;
+ }
+ if (callingUid != mPackageManagerInternal.getPackageUid(callingPackage, 0,
+ UserHandle.getUserId(callingUid))) {
+ throw new SecurityException("Package " + callingPackage
+ + " does not belong to the calling uid " + callingUid);
+ }
+ synchronized (mLock) {
+ removeAlarmsInternalLocked(
+ a -> (a.matches(callingPackage) && a.creatorUid == callingUid),
+ REMOVE_REASON_ALARM_CANCELLED);
+ }
+ }
+
+ @Override
public long getNextWakeFromIdleTime() {
return getNextWakeFromIdleTimeImpl();
}
@@ -3034,17 +3116,6 @@ public class AlarmManagerService extends SystemService {
}
@Override
- public long currentNetworkTimeMillis() {
- final NtpTrustedTime time = NtpTrustedTime.getInstance(getContext());
- NtpTrustedTime.TimeResult ntpResult = time.getCachedTimeResult();
- if (ntpResult != null) {
- return ntpResult.currentTimeMillis();
- } else {
- throw new ParcelableException(new DateTimeException("Missing NTP fix"));
- }
- }
-
- @Override
public int getConfigVersion() {
getContext().enforceCallingOrSelfPermission(Manifest.permission.DUMP,
"getConfigVersion");
@@ -3094,7 +3165,7 @@ public class AlarmManagerService extends SystemService {
mConstants.dump(pw);
pw.println();
- if (mConstants.USE_TARE_POLICY) {
+ if (mConstants.USE_TARE_POLICY == EconomyManager.ENABLED_MODE_ON) {
pw.println("TARE details:");
pw.increaseIndent();
@@ -3122,16 +3193,16 @@ public class AlarmManagerService extends SystemService {
pw.decreaseIndent();
pw.println();
} else {
- if (mAppStateTracker != null) {
- mAppStateTracker.dump(pw);
- pw.println();
- }
-
pw.println("App Standby Parole: " + mAppStandbyParole);
pw.println();
}
- final long nowELAPSED = mInjector.getElapsedRealtime();
+ if (mAppStateTracker != null) {
+ mAppStateTracker.dump(pw);
+ pw.println();
+ }
+
+ final long nowELAPSED = mInjector.getElapsedRealtimeMillis();
final long nowUPTIME = SystemClock.uptimeMillis();
final long nowRTC = mInjector.getCurrentTimeMillis();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
@@ -3442,15 +3513,16 @@ public class AlarmManagerService extends SystemService {
}
if (mRemovalHistory.size() > 0) {
- pw.println("Removal history: ");
+ pw.println("Removal history:");
pw.increaseIndent();
for (int i = 0; i < mRemovalHistory.size(); i++) {
UserHandle.formatUid(pw, mRemovalHistory.keyAt(i));
pw.println(":");
pw.increaseIndent();
final RemovedAlarm[] historyForUid = mRemovalHistory.valueAt(i).toArray();
- for (final RemovedAlarm removedAlarm : historyForUid) {
- removedAlarm.dump(pw, nowELAPSED, sdf);
+ for (int index = historyForUid.length - 1; index >= 0; index--) {
+ pw.print("#" + (historyForUid.length - index) + ": ");
+ historyForUid[index].dump(pw, nowELAPSED, sdf);
}
pw.decreaseIndent();
}
@@ -3607,7 +3679,7 @@ public class AlarmManagerService extends SystemService {
synchronized (mLock) {
final long nowRTC = mInjector.getCurrentTimeMillis();
- final long nowElapsed = mInjector.getElapsedRealtime();
+ final long nowElapsed = mInjector.getElapsedRealtimeMillis();
proto.write(AlarmManagerServiceDumpProto.CURRENT_TIME, nowRTC);
proto.write(AlarmManagerServiceDumpProto.ELAPSED_REALTIME, nowElapsed);
proto.write(AlarmManagerServiceDumpProto.LAST_TIME_CHANGE_CLOCK_TIME,
@@ -3945,7 +4017,7 @@ public class AlarmManagerService extends SystemService {
void rescheduleKernelAlarmsLocked() {
// Schedule the next upcoming wakeup alarm. If there is a deliverable batch
// prior to that which contains no wakeups, we schedule that as well.
- final long nowElapsed = mInjector.getElapsedRealtime();
+ final long nowElapsed = mInjector.getElapsedRealtimeMillis();
long nextNonWakeup = 0;
if (mAlarmStore.size() > 0) {
final long firstWakeup = mAlarmStore.getNextWakeupDeliveryTime();
@@ -4058,7 +4130,7 @@ public class AlarmManagerService extends SystemService {
@GuardedBy("mLock")
private void removeAlarmsInternalLocked(Predicate<Alarm> whichAlarms, int reason) {
final long nowRtc = mInjector.getCurrentTimeMillis();
- final long nowElapsed = mInjector.getElapsedRealtime();
+ final long nowElapsed = mInjector.getElapsedRealtimeMillis();
final ArrayList<Alarm> removedAlarms = mAlarmStore.remove(whichAlarms);
final boolean removedFromStore = !removedAlarms.isEmpty();
@@ -4143,7 +4215,7 @@ public class AlarmManagerService extends SystemService {
}
@GuardedBy("mLock")
- void removeLocked(final String packageName) {
+ void removeLocked(final String packageName, int reason) {
if (packageName == null) {
if (localLOGV) {
Slog.w(TAG, "requested remove() of null packageName",
@@ -4151,7 +4223,7 @@ public class AlarmManagerService extends SystemService {
}
return;
}
- removeAlarmsInternalLocked(a -> a.matches(packageName), REMOVE_REASON_UNDEFINED);
+ removeAlarmsInternalLocked(a -> a.matches(packageName), reason);
}
// Only called for ephemeral apps
@@ -4197,7 +4269,7 @@ public class AlarmManagerService extends SystemService {
void interactiveStateChangedLocked(boolean interactive) {
if (mInteractive != interactive) {
mInteractive = interactive;
- final long nowELAPSED = mInjector.getElapsedRealtime();
+ final long nowELAPSED = mInjector.getElapsedRealtimeMillis();
if (interactive) {
if (mPendingNonWakeupAlarms.size() > 0) {
final long thisDelayTime = nowELAPSED - mStartCurrentDelayTime;
@@ -4216,18 +4288,33 @@ public class AlarmManagerService extends SystemService {
}
}
// And send a TIME_TICK right now, since it is important to get the UI updated.
- mHandler.post(() ->
- getContext().sendBroadcastAsUser(mTimeTickIntent, UserHandle.ALL));
+ mHandler.post(() -> getContext().sendBroadcastAsUser(mTimeTickIntent,
+ UserHandle.ALL, null, mTimeTickOptions));
} else {
mNonInteractiveStartTime = nowELAPSED;
}
}
}
- boolean lookForPackageLocked(String packageName) {
- final ArrayList<Alarm> allAlarms = mAlarmStore.asList();
- for (final Alarm alarm : allAlarms) {
- if (alarm.matches(packageName)) {
+ @GuardedBy("mLock")
+ boolean lookForPackageLocked(String packageName, int uid) {
+ // This is called extremely rarely, e.g. when the user opens the force-stop page in settings
+ // so the loops using an iterator should be fine.
+ for (final Alarm alarm : mAlarmStore.asList()) {
+ if (alarm.matches(packageName) && alarm.creatorUid == uid) {
+ return true;
+ }
+ }
+ final ArrayList<Alarm> alarmsForUid = mPendingBackgroundAlarms.get(uid);
+ if (alarmsForUid != null) {
+ for (final Alarm alarm : alarmsForUid) {
+ if (alarm.matches(packageName)) {
+ return true;
+ }
+ }
+ }
+ for (final Alarm alarm : mPendingNonWakeupAlarms) {
+ if (alarm.matches(packageName) && alarm.creatorUid == uid) {
return true;
}
}
@@ -4299,7 +4386,15 @@ public class AlarmManagerService extends SystemService {
private static native void close(long nativeData);
private static native int set(long nativeData, int type, long seconds, long nanoseconds);
private static native int waitForAlarm(long nativeData);
- private static native int setKernelTime(long nativeData, long millis);
+
+ /*
+ * b/246256335: The @Keep ensures that the native definition is kept even when the optimizer can
+ * tell no calls will be made due to a compile-time constant. Allowing this definition to be
+ * optimized away breaks loadLibrary("alarm_jni") at boot time.
+ * TODO(b/246256335): Remove this native method and the associated native code when it is no
+ * longer needed.
+ */
+ @Keep
private static native int setKernelTimezone(long nativeData, int minuteswest);
private static native long getNextAlarm(long nativeData, int type);
@@ -4337,7 +4432,7 @@ public class AlarmManagerService extends SystemService {
ent.pkg = alarm.sourcePackage;
ent.tag = alarm.statsTag;
ent.op = "END IDLE";
- ent.elapsedRealtime = mInjector.getElapsedRealtime();
+ ent.elapsedRealtime = mInjector.getElapsedRealtimeMillis();
ent.argRealtime = alarm.getWhenElapsed();
mAllowWhileIdleDispatches.add(ent);
}
@@ -4404,7 +4499,11 @@ public class AlarmManagerService extends SystemService {
}
}
+ @GuardedBy("mLock")
boolean checkAllowNonWakeupDelayLocked(long nowELAPSED) {
+ if (!mConstants.DELAY_NONWAKEUP_ALARMS_WHILE_SCREEN_OFF) {
+ return false;
+ }
if (mInteractive) {
return false;
}
@@ -4456,7 +4555,8 @@ public class AlarmManagerService extends SystemService {
}
private void reportAlarmEventToTare(Alarm alarm) {
- if (!mConstants.USE_TARE_POLICY) {
+ // Don't bother reporting events if TARE is completely off.
+ if (mConstants.USE_TARE_POLICY == EconomyManager.ENABLED_MODE_OFF) {
return;
}
final boolean allowWhileIdle =
@@ -4564,24 +4664,41 @@ public class AlarmManagerService extends SystemService {
return AlarmManagerService.getNextAlarm(mNativeData, type);
}
- void setKernelTimezone(int minutesWest) {
- AlarmManagerService.setKernelTimezone(mNativeData, minutesWest);
+ void setKernelTimeZoneOffset(int utcOffsetMillis) {
+ // Kernel tracks time offsets as 'minutes west of GMT'
+ AlarmManagerService.setKernelTimezone(mNativeData, -(utcOffsetMillis / 60000));
}
- void setKernelTime(long millis) {
- if (mNativeData != 0) {
- AlarmManagerService.setKernelTime(mNativeData, millis);
- }
+ void syncKernelTimeZoneOffset() {
+ long currentTimeMillis = getCurrentTimeMillis();
+ TimeZone currentTimeZone = TimeZone.getTimeZone(getTimeZoneId());
+ // If the time zone ID is invalid, GMT will be returned and this will set a kernel
+ // offset of zero.
+ int utcOffsetMillis = currentTimeZone.getOffset(currentTimeMillis);
+ setKernelTimeZoneOffset(utcOffsetMillis);
+ }
+
+ void initializeTimeIfRequired() {
+ SystemClockTime.initializeIfRequired();
+ }
+
+ void setCurrentTimeMillis(
+ @CurrentTimeMillisLong long unixEpochMillis,
+ @TimeConfidence int confidence,
+ @NonNull String logMsg) {
+ SystemClockTime.setTimeAndConfidence(unixEpochMillis, confidence, logMsg);
}
void close() {
AlarmManagerService.close(mNativeData);
}
- long getElapsedRealtime() {
+ @ElapsedRealtimeLong
+ long getElapsedRealtimeMillis() {
return SystemClock.elapsedRealtime();
}
+ @CurrentTimeMillisLong
long getCurrentTimeMillis() {
return System.currentTimeMillis();
}
@@ -4605,13 +4722,9 @@ public class AlarmManagerService extends SystemService {
return service.new ClockReceiver();
}
- void registerContentObserver(ContentObserver contentObserver, Uri uri) {
- mContext.getContentResolver().registerContentObserver(uri, false, contentObserver);
- }
-
void registerDeviceConfigListener(DeviceConfig.OnPropertiesChangedListener listener) {
DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_ALARM_MANAGER,
- JobSchedulerBackgroundThread.getExecutor(), listener);
+ AppSchedulingModuleThread.getExecutor(), listener);
}
}
@@ -4631,7 +4744,7 @@ public class AlarmManagerService extends SystemService {
while (true) {
int result = mInjector.waitForAlarm();
final long nowRTC = mInjector.getCurrentTimeMillis();
- final long nowELAPSED = mInjector.getElapsedRealtime();
+ final long nowELAPSED = mInjector.getElapsedRealtimeMillis();
synchronized (mLock) {
mLastWakeup = nowELAPSED;
}
@@ -4737,30 +4850,35 @@ public class AlarmManagerService extends SystemService {
}
}
}
- final ArraySet<Pair<String, Integer>> triggerPackages =
- new ArraySet<>();
+ final ArraySet<UserPackage> triggerPackages = new ArraySet<>();
+ final IntArray wakeupUids = new IntArray();
final SparseIntArray countsPerUid = new SparseIntArray();
final SparseIntArray wakeupCountsPerUid = new SparseIntArray();
for (int i = 0; i < triggerList.size(); i++) {
final Alarm a = triggerList.get(i);
increment(countsPerUid, a.uid);
if (a.wakeup) {
+ wakeupUids.add(a.uid);
increment(wakeupCountsPerUid, a.uid);
}
- if (mConstants.USE_TARE_POLICY) {
+ if (mConstants.USE_TARE_POLICY == EconomyManager.ENABLED_MODE_ON) {
if (!isExemptFromTare(a)) {
- triggerPackages.add(Pair.create(
- a.sourcePackage,
- UserHandle.getUserId(a.creatorUid)));
+ triggerPackages.add(UserPackage.of(
+ UserHandle.getUserId(a.creatorUid),
+ a.sourcePackage));
}
} else if (!isExemptFromAppStandby(a)) {
- triggerPackages.add(Pair.create(
- a.sourcePackage, UserHandle.getUserId(a.creatorUid)));
+ triggerPackages.add(UserPackage.of(
+ UserHandle.getUserId(a.creatorUid), a.sourcePackage));
}
}
+ if (wakeupUids.size() > 0 && mBatteryStatsInternal != null) {
+ mBatteryStatsInternal.noteWakingAlarmBatch(nowELAPSED,
+ wakeupUids.toArray());
+ }
deliverAlarmsLocked(triggerList, nowELAPSED);
mTemporaryQuotaReserve.cleanUpExpiredQuotas(nowELAPSED);
- if (mConstants.USE_TARE_POLICY) {
+ if (mConstants.USE_TARE_POLICY == EconomyManager.ENABLED_MODE_ON) {
reorderAlarmsBasedOnTare(triggerPackages);
} else {
reorderAlarmsBasedOnStandbyBuckets(triggerPackages);
@@ -4897,6 +5015,7 @@ public class AlarmManagerService extends SystemService {
public static final int TARE_AFFORDABILITY_CHANGED = 12;
public static final int CHECK_EXACT_ALARM_PERMISSION_ON_UPDATE = 13;
public static final int TEMPORARY_QUOTA_CHANGED = 14;
+ public static final int REMOVE_EXACT_LISTENER_ALARMS_ON_CACHED = 15;
AlarmHandler() {
super(Looper.myLooper());
@@ -4912,7 +5031,7 @@ public class AlarmManagerService extends SystemService {
// this way, so WAKE_UP alarms will be delivered only when the device is awake.
ArrayList<Alarm> triggerList = new ArrayList<Alarm>();
synchronized (mLock) {
- final long nowELAPSED = mInjector.getElapsedRealtime();
+ final long nowELAPSED = mInjector.getElapsedRealtimeMillis();
triggerAlarmsLocked(triggerList, nowELAPSED);
updateNextAlarmClockLocked();
}
@@ -4965,8 +5084,8 @@ public class AlarmManagerService extends SystemService {
case TEMPORARY_QUOTA_CHANGED:
case APP_STANDBY_BUCKET_CHANGED:
synchronized (mLock) {
- final ArraySet<Pair<String, Integer>> filterPackages = new ArraySet<>();
- filterPackages.add(Pair.create((String) msg.obj, msg.arg1));
+ final ArraySet<UserPackage> filterPackages = new ArraySet<>();
+ filterPackages.add(UserPackage.of(msg.arg1, (String) msg.obj));
if (reorderAlarmsBasedOnStandbyBuckets(filterPackages)) {
rescheduleKernelAlarmsLocked();
updateNextAlarmClockLocked();
@@ -4979,8 +5098,8 @@ public class AlarmManagerService extends SystemService {
final int userId = msg.arg1;
final String packageName = (String) msg.obj;
- final ArraySet<Pair<String, Integer>> filterPackages = new ArraySet<>();
- filterPackages.add(Pair.create(packageName, userId));
+ final ArraySet<UserPackage> filterPackages = new ArraySet<>();
+ filterPackages.add(UserPackage.of(userId, packageName));
if (reorderAlarmsBasedOnTare(filterPackages)) {
rescheduleKernelAlarmsLocked();
updateNextAlarmClockLocked();
@@ -5017,6 +5136,21 @@ public class AlarmManagerService extends SystemService {
removeExactAlarmsOnPermissionRevoked(uid, packageName, /*killUid = */false);
}
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);
+ }
+ break;
default:
// nope, just ignore it
break;
@@ -5065,13 +5199,12 @@ public class AlarmManagerService extends SystemService {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(Intent.ACTION_DATE_CHANGED)) {
- // Since the kernel does not keep track of DST, we need to
- // reset the TZ information at the beginning of each day
- // based off of the current Zone gmt offset + userspace tracked
- // daylight savings information.
- TimeZone zone = TimeZone.getTimeZone(SystemProperties.get(TIMEZONE_PROPERTY));
- int gmtOffset = zone.getOffset(mInjector.getCurrentTimeMillis());
- mInjector.setKernelTimezone(-(gmtOffset / 60000));
+ if (KERNEL_TIME_ZONE_SYNC_ENABLED) {
+ // Since the kernel does not keep track of DST, we reset the TZ information at
+ // the beginning of each day. This may miss a DST transition, but it will
+ // correct itself within 24 hours.
+ mInjector.syncKernelTimeZoneOffset();
+ }
scheduleDateChangedEvent();
}
}
@@ -5090,7 +5223,7 @@ public class AlarmManagerService extends SystemService {
flags |= mConstants.TIME_TICK_ALLOWED_WHILE_IDLE ? FLAG_ALLOW_WHILE_IDLE_UNRESTRICTED
: 0;
- setImpl(ELAPSED_REALTIME, mInjector.getElapsedRealtime() + tickEventDelay, 0,
+ setImpl(ELAPSED_REALTIME, mInjector.getElapsedRealtimeMillis() + tickEventDelay, 0,
0, null, mTimeTickTrigger, TIME_TICK_TAG, flags, workSource, null,
Process.myUid(), "android", null, EXACT_ALLOW_REASON_ALLOW_LIST);
@@ -5161,7 +5294,7 @@ public class AlarmManagerService extends SystemService {
case Intent.ACTION_QUERY_PACKAGE_RESTART:
pkgList = intent.getStringArrayExtra(Intent.EXTRA_PACKAGES);
for (String packageName : pkgList) {
- if (lookForPackageLocked(packageName)) {
+ if (lookForPackageLocked(packageName, uid)) {
setResultCode(Activity.RESULT_OK);
return;
}
@@ -5227,7 +5360,7 @@ public class AlarmManagerService extends SystemService {
removeLocked(uid, REMOVE_REASON_UNDEFINED);
} else {
// external-applications-unavailable case
- removeLocked(pkg);
+ removeLocked(pkg, REMOVE_REASON_UNDEFINED);
}
mPriorities.remove(pkg);
for (int i = mBroadcastStats.size() - 1; i >= 0; i--) {
@@ -5277,7 +5410,7 @@ public class AlarmManagerService extends SystemService {
}
synchronized (mLock) {
mTemporaryQuotaReserve.replenishQuota(packageName, userId, quotaBump,
- mInjector.getElapsedRealtime());
+ mInjector.getElapsedRealtimeMillis());
}
mHandler.obtainMessage(AlarmHandler.TEMPORARY_QUOTA_CHANGED, userId, -1,
packageName).sendToTarget();
@@ -5344,9 +5477,7 @@ public class AlarmManagerService extends SystemService {
@Override
public void unblockAllUnrestrictedAlarms() {
- // Called when:
- // 1. Power exemption list changes,
- // 2. User FAS feature is disabled.
+ // Called when the power exemption list changes.
synchronized (mLock) {
sendAllUnrestrictedPendingBackgroundAlarmsLocked();
}
@@ -5374,6 +5505,34 @@ public class AlarmManagerService extends SystemService {
removeForStoppedLocked(uid);
}
}
+
+ @Override
+ public void handleUidCachedChanged(int uid, boolean cached) {
+ if (!CompatChanges.isChangeEnabled(EXACT_LISTENER_ALARMS_DROPPED_ON_CACHED, uid)) {
+ return;
+ }
+ // Apps can quickly get frozen after being cached, breaking the exactness guarantee on
+ // listener alarms. So going forward, the contract of exact listener alarms explicitly
+ // states that they will be removed as soon as the app goes out of lifecycle. We still
+ // allow a short grace period for quick shuffling of proc-states that may happen
+ // unexpectedly when switching between different lifecycles and is generally hard for
+ // apps to avoid.
+
+ final long delay;
+ synchronized (mLock) {
+ delay = mConstants.CACHED_LISTENER_REMOVAL_DELAY;
+ }
+ final Integer uidObj = uid;
+
+ if (cached && !mHandler.hasEqualMessages(REMOVE_EXACT_LISTENER_ALARMS_ON_CACHED,
+ uidObj)) {
+ mHandler.sendMessageDelayed(
+ mHandler.obtainMessage(REMOVE_EXACT_LISTENER_ALARMS_ON_CACHED, uidObj),
+ delay);
+ } else {
+ mHandler.removeEqualMessages(REMOVE_EXACT_LISTENER_ALARMS_ON_CACHED, uidObj);
+ }
+ }
};
private final BroadcastStats getStatsLocked(PendingIntent pi) {
@@ -5439,7 +5598,7 @@ public class AlarmManagerService extends SystemService {
}
private void updateStatsLocked(InFlight inflight) {
- final long nowELAPSED = mInjector.getElapsedRealtime();
+ final long nowELAPSED = mInjector.getElapsedRealtimeMillis();
BroadcastStats bs = inflight.mBroadcastStats;
bs.nesting--;
if (bs.nesting <= 0) {
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/BatchingAlarmStore.java b/apex/jobscheduler/service/java/com/android/server/alarm/BatchingAlarmStore.java
deleted file mode 100644
index 1a4efb8ffcd9..000000000000
--- a/apex/jobscheduler/service/java/com/android/server/alarm/BatchingAlarmStore.java
+++ /dev/null
@@ -1,408 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.alarm;
-
-import static com.android.server.alarm.AlarmManagerService.DEBUG_BATCH;
-import static com.android.server.alarm.AlarmManagerService.clampPositive;
-import static com.android.server.alarm.AlarmManagerService.dumpAlarmList;
-import static com.android.server.alarm.AlarmManagerService.isTimeTickAlarm;
-
-import android.app.AlarmManager;
-import android.util.IndentingPrintWriter;
-import android.util.Slog;
-import android.util.proto.ProtoOutputStream;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.util.StatLogger;
-
-import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.function.Predicate;
-
-/**
- * Batching implementation of an Alarm Store.
- * This keeps the alarms in batches, which are sorted on the start time of their delivery window.
- */
-public class BatchingAlarmStore implements AlarmStore {
- @VisibleForTesting
- static final String TAG = BatchingAlarmStore.class.getSimpleName();
-
- private final ArrayList<Batch> mAlarmBatches = new ArrayList<>();
- private int mSize;
- private Runnable mOnAlarmClockRemoved;
-
- interface Stats {
- int REBATCH_ALL_ALARMS = 0;
- int GET_COUNT = 1;
- }
-
- final StatLogger mStatLogger = new StatLogger(TAG + " stats", new String[]{
- "REBATCH_ALL_ALARMS",
- "GET_COUNT",
- });
-
- private static final Comparator<Batch> sBatchOrder = Comparator.comparingLong(b -> b.mStart);
-
- private static final Comparator<Alarm> sIncreasingTimeOrder = Comparator.comparingLong(
- Alarm::getWhenElapsed);
-
- @Override
- public void add(Alarm a) {
- insertAndBatchAlarm(a);
- mSize++;
- }
-
- @Override
- public void addAll(ArrayList<Alarm> alarms) {
- if (alarms == null) {
- return;
- }
- for (final Alarm a : alarms) {
- add(a);
- }
- }
-
- @Override
- public ArrayList<Alarm> remove(Predicate<Alarm> whichAlarms) {
- final ArrayList<Alarm> removed = new ArrayList<>();
- for (int i = mAlarmBatches.size() - 1; i >= 0; i--) {
- final Batch b = mAlarmBatches.get(i);
- removed.addAll(b.remove(whichAlarms));
- if (b.size() == 0) {
- mAlarmBatches.remove(i);
- }
- }
- if (!removed.isEmpty()) {
- mSize -= removed.size();
- // Not needed if only whole batches were removed, but keeping existing behavior.
- rebatchAllAlarms();
- }
- return removed;
- }
-
- @Override
- public void setAlarmClockRemovalListener(Runnable listener) {
- mOnAlarmClockRemoved = listener;
- }
-
- @Override
- public Alarm getNextWakeFromIdleAlarm() {
- for (final Batch batch : mAlarmBatches) {
- if ((batch.mFlags & AlarmManager.FLAG_WAKE_FROM_IDLE) == 0) {
- continue;
- }
- for (int i = 0; i < batch.size(); i++) {
- final Alarm a = batch.get(i);
- if ((a.flags & AlarmManager.FLAG_WAKE_FROM_IDLE) != 0) {
- return a;
- }
- }
- }
- return null;
- }
-
- private void rebatchAllAlarms() {
- final long start = mStatLogger.getTime();
- final ArrayList<Batch> oldBatches = (ArrayList<Batch>) mAlarmBatches.clone();
- mAlarmBatches.clear();
- for (final Batch batch : oldBatches) {
- for (int i = 0; i < batch.size(); i++) {
- insertAndBatchAlarm(batch.get(i));
- }
- }
- mStatLogger.logDurationStat(Stats.REBATCH_ALL_ALARMS, start);
- }
-
- @Override
- public int size() {
- return mSize;
- }
-
- @Override
- public long getNextWakeupDeliveryTime() {
- for (Batch b : mAlarmBatches) {
- if (b.hasWakeups()) {
- return b.mStart;
- }
- }
- return 0;
- }
-
- @Override
- public long getNextDeliveryTime() {
- if (mAlarmBatches.size() > 0) {
- return mAlarmBatches.get(0).mStart;
- }
- return 0;
- }
-
- @Override
- public ArrayList<Alarm> removePendingAlarms(long nowElapsed) {
- final ArrayList<Alarm> removedAlarms = new ArrayList<>();
- while (mAlarmBatches.size() > 0) {
- final Batch batch = mAlarmBatches.get(0);
- if (batch.mStart > nowElapsed) {
- break;
- }
- mAlarmBatches.remove(0);
- for (int i = 0; i < batch.size(); i++) {
- removedAlarms.add(batch.get(i));
- }
- }
- mSize -= removedAlarms.size();
- return removedAlarms;
- }
-
- @Override
- public boolean updateAlarmDeliveries(AlarmDeliveryCalculator deliveryCalculator) {
- boolean changed = false;
- for (final Batch b : mAlarmBatches) {
- for (int i = 0; i < b.size(); i++) {
- changed |= deliveryCalculator.updateAlarmDelivery(b.get(i));
- }
- }
- if (changed) {
- rebatchAllAlarms();
- }
- return changed;
- }
-
- @Override
- public ArrayList<Alarm> asList() {
- final ArrayList<Alarm> allAlarms = new ArrayList<>();
- for (final Batch batch : mAlarmBatches) {
- for (int i = 0; i < batch.size(); i++) {
- allAlarms.add(batch.get(i));
- }
- }
- return allAlarms;
- }
-
- @Override
- public void dump(IndentingPrintWriter ipw, long nowElapsed, SimpleDateFormat sdf) {
- ipw.print("Pending alarm batches: ");
- ipw.println(mAlarmBatches.size());
- for (Batch b : mAlarmBatches) {
- ipw.print(b);
- ipw.println(':');
- ipw.increaseIndent();
- dumpAlarmList(ipw, b.mAlarms, nowElapsed, sdf);
- ipw.decreaseIndent();
- }
- mStatLogger.dump(ipw);
- }
-
- @Override
- public void dumpProto(ProtoOutputStream pos, long nowElapsed) {
- for (Batch b : mAlarmBatches) {
- b.dumpDebug(pos, AlarmManagerServiceDumpProto.PENDING_ALARM_BATCHES, nowElapsed);
- }
- }
-
- @Override
- public String getName() {
- return TAG;
- }
-
- @Override
- public int getCount(Predicate<Alarm> condition) {
- long start = mStatLogger.getTime();
-
- int count = 0;
- for (Batch b : mAlarmBatches) {
- for (int i = 0; i < b.size(); i++) {
- if (condition.test(b.get(i))) {
- count++;
- }
- }
- }
- mStatLogger.logDurationStat(Stats.GET_COUNT, start);
- return count;
- }
-
- private void insertAndBatchAlarm(Alarm alarm) {
- final int whichBatch = ((alarm.flags & AlarmManager.FLAG_STANDALONE) != 0) ? -1
- : attemptCoalesce(alarm.getWhenElapsed(), alarm.getMaxWhenElapsed());
-
- if (whichBatch < 0) {
- addBatch(mAlarmBatches, new Batch(alarm));
- } else {
- final Batch batch = mAlarmBatches.get(whichBatch);
- if (batch.add(alarm)) {
- // The start time of this batch advanced, so batch ordering may
- // have just been broken. Move it to where it now belongs.
- mAlarmBatches.remove(whichBatch);
- addBatch(mAlarmBatches, batch);
- }
- }
- }
-
- static void addBatch(ArrayList<Batch> list, Batch newBatch) {
- int index = Collections.binarySearch(list, newBatch, sBatchOrder);
- if (index < 0) {
- index = 0 - index - 1;
- }
- list.add(index, newBatch);
- }
-
- // Return the index of the matching batch, or -1 if none found.
- private int attemptCoalesce(long whenElapsed, long maxWhen) {
- final int n = mAlarmBatches.size();
- for (int i = 0; i < n; i++) {
- Batch b = mAlarmBatches.get(i);
- if ((b.mFlags & AlarmManager.FLAG_STANDALONE) == 0 && b.canHold(whenElapsed, maxWhen)) {
- return i;
- }
- }
- return -1;
- }
-
- final class Batch {
- long mStart; // These endpoints are always in ELAPSED
- long mEnd;
- int mFlags; // Flags for alarms, such as FLAG_STANDALONE.
-
- final ArrayList<Alarm> mAlarms = new ArrayList<>();
-
- Batch(Alarm seed) {
- mStart = seed.getWhenElapsed();
- mEnd = clampPositive(seed.getMaxWhenElapsed());
- mFlags = seed.flags;
- mAlarms.add(seed);
- }
-
- int size() {
- return mAlarms.size();
- }
-
- Alarm get(int index) {
- return mAlarms.get(index);
- }
-
- boolean canHold(long whenElapsed, long maxWhen) {
- return (mEnd >= whenElapsed) && (mStart <= maxWhen);
- }
-
- boolean add(Alarm alarm) {
- boolean newStart = false;
- // narrows the batch if necessary; presumes that canHold(alarm) is true
- int index = Collections.binarySearch(mAlarms, alarm, sIncreasingTimeOrder);
- if (index < 0) {
- index = 0 - index - 1;
- }
- mAlarms.add(index, alarm);
- if (DEBUG_BATCH) {
- Slog.v(TAG, "Adding " + alarm + " to " + this);
- }
- if (alarm.getWhenElapsed() > mStart) {
- mStart = alarm.getWhenElapsed();
- newStart = true;
- }
- if (alarm.getMaxWhenElapsed() < mEnd) {
- mEnd = alarm.getMaxWhenElapsed();
- }
- mFlags |= alarm.flags;
-
- if (DEBUG_BATCH) {
- Slog.v(TAG, " => now " + this);
- }
- return newStart;
- }
-
- ArrayList<Alarm> remove(Predicate<Alarm> predicate) {
- final ArrayList<Alarm> removed = new ArrayList<>();
- long newStart = 0; // recalculate endpoints as we go
- long newEnd = Long.MAX_VALUE;
- int newFlags = 0;
- for (int i = 0; i < mAlarms.size(); ) {
- Alarm alarm = mAlarms.get(i);
- if (predicate.test(alarm)) {
- removed.add(mAlarms.remove(i));
- if (alarm.alarmClock != null && mOnAlarmClockRemoved != null) {
- mOnAlarmClockRemoved.run();
- }
- if (isTimeTickAlarm(alarm)) {
- // This code path is not invoked when delivering alarms, only when removing
- // alarms due to the caller cancelling it or getting uninstalled, etc.
- Slog.wtf(TAG, "Removed TIME_TICK alarm");
- }
- } else {
- if (alarm.getWhenElapsed() > newStart) {
- newStart = alarm.getWhenElapsed();
- }
- if (alarm.getMaxWhenElapsed() < newEnd) {
- newEnd = alarm.getMaxWhenElapsed();
- }
- newFlags |= alarm.flags;
- i++;
- }
- }
- if (!removed.isEmpty()) {
- // commit the new batch bounds
- mStart = newStart;
- mEnd = newEnd;
- mFlags = newFlags;
- }
- return removed;
- }
-
- boolean hasWakeups() {
- final int n = mAlarms.size();
- for (int i = 0; i < n; i++) {
- Alarm a = mAlarms.get(i);
- if (a.wakeup) {
- return true;
- }
- }
- return false;
- }
-
- @Override
- public String toString() {
- StringBuilder b = new StringBuilder(40);
- b.append("Batch{");
- b.append(Integer.toHexString(this.hashCode()));
- b.append(" num=");
- b.append(size());
- b.append(" start=");
- b.append(mStart);
- b.append(" end=");
- b.append(mEnd);
- if (mFlags != 0) {
- b.append(" flgs=0x");
- b.append(Integer.toHexString(mFlags));
- }
- b.append('}');
- return b.toString();
- }
-
- public void dumpDebug(ProtoOutputStream proto, long fieldId, long nowElapsed) {
- final long token = proto.start(fieldId);
-
- proto.write(BatchProto.START_REALTIME, mStart);
- proto.write(BatchProto.END_REALTIME, mEnd);
- proto.write(BatchProto.FLAGS, mFlags);
- for (Alarm a : mAlarms) {
- a.dumpDebug(proto, BatchProto.ALARMS, nowElapsed);
- }
-
- proto.end(token);
- }
- }
-}
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/MetricsHelper.java b/apex/jobscheduler/service/java/com/android/server/alarm/MetricsHelper.java
index 2923cfd8e22c..eb1848d666f0 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/MetricsHelper.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/MetricsHelper.java
@@ -18,9 +18,11 @@ package com.android.server.alarm;
import static com.android.internal.util.FrameworkStatsLog.ALARM_SCHEDULED__EXACT_ALARM_ALLOWED_REASON__ALLOW_LIST;
import static com.android.internal.util.FrameworkStatsLog.ALARM_SCHEDULED__EXACT_ALARM_ALLOWED_REASON__CHANGE_DISABLED;
+import static com.android.internal.util.FrameworkStatsLog.ALARM_SCHEDULED__EXACT_ALARM_ALLOWED_REASON__LISTENER;
import static com.android.internal.util.FrameworkStatsLog.ALARM_SCHEDULED__EXACT_ALARM_ALLOWED_REASON__NOT_APPLICABLE;
import static com.android.internal.util.FrameworkStatsLog.ALARM_SCHEDULED__EXACT_ALARM_ALLOWED_REASON__PERMISSION;
import static com.android.internal.util.FrameworkStatsLog.ALARM_SCHEDULED__EXACT_ALARM_ALLOWED_REASON__POLICY_PERMISSION;
+import static com.android.internal.util.FrameworkStatsLog.ALARM_SCHEDULED__EXACT_ALARM_ALLOWED_REASON__PRIORITIZED;
import static com.android.server.alarm.AlarmManagerService.INDEFINITE_DELAY;
import android.app.ActivityManager;
@@ -84,14 +86,18 @@ class MetricsHelper {
private static int reasonToStatsReason(int reasonCode) {
switch (reasonCode) {
- case Alarm.EXACT_ALLOW_REASON_ALLOW_LIST:
- return ALARM_SCHEDULED__EXACT_ALARM_ALLOWED_REASON__ALLOW_LIST;
case Alarm.EXACT_ALLOW_REASON_PERMISSION:
return ALARM_SCHEDULED__EXACT_ALARM_ALLOWED_REASON__PERMISSION;
+ case Alarm.EXACT_ALLOW_REASON_ALLOW_LIST:
+ return ALARM_SCHEDULED__EXACT_ALARM_ALLOWED_REASON__ALLOW_LIST;
case Alarm.EXACT_ALLOW_REASON_COMPAT:
return ALARM_SCHEDULED__EXACT_ALARM_ALLOWED_REASON__CHANGE_DISABLED;
case Alarm.EXACT_ALLOW_REASON_POLICY_PERMISSION:
return ALARM_SCHEDULED__EXACT_ALARM_ALLOWED_REASON__POLICY_PERMISSION;
+ case Alarm.EXACT_ALLOW_REASON_LISTENER:
+ return ALARM_SCHEDULED__EXACT_ALARM_ALLOWED_REASON__LISTENER;
+ case Alarm.EXACT_ALLOW_REASON_PRIORITIZED:
+ return ALARM_SCHEDULED__EXACT_ALARM_ALLOWED_REASON__PRIORITIZED;
default:
return ALARM_SCHEDULED__EXACT_ALARM_ALLOWED_REASON__NOT_APPLICABLE;
}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobCompletedListener.java b/apex/jobscheduler/service/java/com/android/server/job/JobCompletedListener.java
index 862d8b7cac50..3f46cc44d379 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobCompletedListener.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobCompletedListener.java
@@ -16,6 +16,8 @@
package com.android.server.job;
+import android.app.job.JobParameters;
+
import com.android.server.job.controllers.JobStatus;
/**
@@ -26,8 +28,12 @@ public interface JobCompletedListener {
/**
* Callback for when a job is completed.
*
- * @param stopReason The stop reason provided to JobParameters.
- * @param needsReschedule Whether the implementing class should reschedule this job.
+ * @param stopReason The stop reason returned from
+ * {@link JobParameters#getStopReason()}.
+ * @param internalStopReason The stop reason returned from
+ * {@link JobParameters#getInternalStopReasonCode()}.
+ * @param needsReschedule Whether the implementing class should reschedule this job.
*/
- void onJobCompletedLocked(JobStatus jobStatus, int stopReason, boolean needsReschedule);
+ void onJobCompletedLocked(JobStatus jobStatus, int stopReason, int internalStopReason,
+ boolean needsReschedule);
}
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 28116a8df180..dc608e7fddfd 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
@@ -16,6 +16,9 @@
package com.android.server.job;
+import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
+import static android.util.DataUnit.GIGABYTES;
+
import static com.android.server.job.JobSchedulerService.RESTRICTED_INDEX;
import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
@@ -24,6 +27,7 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
+import android.app.BackgroundStartPrivileges;
import android.app.UserSwitchObserver;
import android.app.job.JobInfo;
import android.app.job.JobParameters;
@@ -34,6 +38,7 @@ import android.content.IntentFilter;
import android.content.pm.UserInfo;
import android.os.BatteryStats;
import android.os.Handler;
+import android.os.Looper;
import android.os.PowerManager;
import android.os.RemoteException;
import android.os.ServiceManager;
@@ -55,8 +60,10 @@ import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.IBatteryStats;
import com.android.internal.app.procstats.ProcessStats;
+import com.android.internal.util.MemInfoReader;
import com.android.internal.util.StatLogger;
-import com.android.server.JobSchedulerBackgroundThread;
+import com.android.modules.expresslog.Histogram;
+import com.android.server.AppSchedulingModuleThread;
import com.android.server.LocalServices;
import com.android.server.job.controllers.JobStatus;
import com.android.server.job.controllers.StateController;
@@ -82,11 +89,33 @@ class JobConcurrencyManager {
private static final boolean DEBUG = JobSchedulerService.DEBUG;
/** The maximum number of concurrent jobs we'll aim to run at one time. */
- public static final int STANDARD_CONCURRENCY_LIMIT = 16;
+ @VisibleForTesting
+ static final int MAX_CONCURRENCY_LIMIT = 64;
/** The maximum number of objects we should retain in memory when not in use. */
- private static final int MAX_RETAINED_OBJECTS = (int) (1.5 * STANDARD_CONCURRENCY_LIMIT);
+ private static final int MAX_RETAINED_OBJECTS = (int) (1.5 * MAX_CONCURRENCY_LIMIT);
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 {
+ if (ActivityManager.isLowRamDeviceStatic()) {
+ DEFAULT_CONCURRENCY_LIMIT = 8;
+ } else {
+ final long ramBytes = new MemInfoReader().getTotalSize();
+ if (ramBytes <= GIGABYTES.toBytes(6)) {
+ DEFAULT_CONCURRENCY_LIMIT = 16;
+ } else if (ramBytes <= GIGABYTES.toBytes(8)) {
+ DEFAULT_CONCURRENCY_LIMIT = 20;
+ } else if (ramBytes <= GIGABYTES.toBytes(12)) {
+ DEFAULT_CONCURRENCY_LIMIT = 32;
+ } else {
+ DEFAULT_CONCURRENCY_LIMIT = 40;
+ }
+ }
+ }
+
private static final String KEY_SCREEN_OFF_ADJUSTMENT_DELAY_MS =
CONFIG_KEY_PREFIX_CONCURRENCY + "screen_off_adjustment_delay_ms";
private static final long DEFAULT_SCREEN_OFF_ADJUSTMENT_DELAY_MS = 30_000;
@@ -97,7 +126,23 @@ class JobConcurrencyManager {
@VisibleForTesting
static final String KEY_PKG_CONCURRENCY_LIMIT_REGULAR =
CONFIG_KEY_PREFIX_CONCURRENCY + "pkg_concurrency_limit_regular";
- private static final int DEFAULT_PKG_CONCURRENCY_LIMIT_REGULAR = STANDARD_CONCURRENCY_LIMIT / 2;
+ private static final int DEFAULT_PKG_CONCURRENCY_LIMIT_REGULAR = DEFAULT_CONCURRENCY_LIMIT / 2;
+ @VisibleForTesting
+ static final String KEY_ENABLE_MAX_WAIT_TIME_BYPASS =
+ CONFIG_KEY_PREFIX_CONCURRENCY + "enable_max_wait_time_bypass";
+ private static final boolean DEFAULT_ENABLE_MAX_WAIT_TIME_BYPASS = true;
+ @VisibleForTesting
+ static final String KEY_MAX_WAIT_UI_MS = CONFIG_KEY_PREFIX_CONCURRENCY + "max_wait_ui_ms";
+ @VisibleForTesting
+ static final long DEFAULT_MAX_WAIT_UI_MS = 5 * MINUTE_IN_MILLIS;
+ private static final String KEY_MAX_WAIT_EJ_MS =
+ CONFIG_KEY_PREFIX_CONCURRENCY + "max_wait_ej_ms";
+ @VisibleForTesting
+ static final long DEFAULT_MAX_WAIT_EJ_MS = 5 * MINUTE_IN_MILLIS;
+ private static final String KEY_MAX_WAIT_REGULAR_MS =
+ CONFIG_KEY_PREFIX_CONCURRENCY + "max_wait_regular_ms";
+ @VisibleForTesting
+ static final long DEFAULT_MAX_WAIT_REGULAR_MS = 30 * MINUTE_IN_MILLIS;
/**
* Set of possible execution types that a job can have. The actual type(s) of a job are based
@@ -119,33 +164,37 @@ class JobConcurrencyManager {
* state (excluding {@link ActivityManager#PROCESS_STATE_TOP} for a currently active user.
*/
static final int WORK_TYPE_FGS = 1 << 1;
+ /** The job is allowed to run as a user-initiated job for a currently active user. */
+ static final int WORK_TYPE_UI = 1 << 2;
/** The job is allowed to run as an expedited job for a currently active user. */
- static final int WORK_TYPE_EJ = 1 << 2;
+ static final int WORK_TYPE_EJ = 1 << 3;
/**
* The job does not satisfy any of the conditions for {@link #WORK_TYPE_TOP},
* {@link #WORK_TYPE_FGS}, or {@link #WORK_TYPE_EJ}, but is for a currently active user, so
* can run as a background job.
*/
- static final int WORK_TYPE_BG = 1 << 3;
+ static final int WORK_TYPE_BG = 1 << 4;
/**
* The job is for an app in a {@link ActivityManager#PROCESS_STATE_FOREGROUND_SERVICE} or higher
- * state, or is allowed to run as an expedited job, but is for a completely background user.
+ * state, or is allowed to run as an expedited or user-initiated job,
+ * but is for a completely background user.
*/
- static final int WORK_TYPE_BGUSER_IMPORTANT = 1 << 4;
+ static final int WORK_TYPE_BGUSER_IMPORTANT = 1 << 5;
/**
* The job does not satisfy any of the conditions for {@link #WORK_TYPE_TOP},
* {@link #WORK_TYPE_FGS}, or {@link #WORK_TYPE_EJ}, but is for a completely background user,
* so can run as a background user job.
*/
- static final int WORK_TYPE_BGUSER = 1 << 5;
+ static final int WORK_TYPE_BGUSER = 1 << 6;
@VisibleForTesting
- static final int NUM_WORK_TYPES = 6;
+ static final int NUM_WORK_TYPES = 7;
private static final int ALL_WORK_TYPES = (1 << NUM_WORK_TYPES) - 1;
@IntDef(prefix = {"WORK_TYPE_"}, flag = true, value = {
WORK_TYPE_NONE,
WORK_TYPE_TOP,
WORK_TYPE_FGS,
+ WORK_TYPE_UI,
WORK_TYPE_EJ,
WORK_TYPE_BG,
WORK_TYPE_BGUSER_IMPORTANT,
@@ -164,6 +213,8 @@ class JobConcurrencyManager {
return "TOP";
case WORK_TYPE_FGS:
return "FGS";
+ case WORK_TYPE_UI:
+ return "UI";
case WORK_TYPE_EJ:
return "EJ";
case WORK_TYPE_BG:
@@ -178,9 +229,11 @@ class JobConcurrencyManager {
}
private final Object mLock;
+ private final JobNotificationCoordinator mNotificationCoordinator;
private final JobSchedulerService mService;
private final Context mContext;
private final Handler mHandler;
+ private final Injector mInjector;
private PowerManager mPowerManager;
@@ -192,84 +245,109 @@ class JobConcurrencyManager {
private static final WorkConfigLimitsPerMemoryTrimLevel CONFIG_LIMITS_SCREEN_ON =
new WorkConfigLimitsPerMemoryTrimLevel(
- new WorkTypeConfig("screen_on_normal", 11,
+ new WorkTypeConfig("screen_on_normal", DEFAULT_CONCURRENCY_LIMIT,
+ /* defaultMaxTotal */ DEFAULT_CONCURRENCY_LIMIT * 3 / 4,
// defaultMin
- List.of(Pair.create(WORK_TYPE_TOP, 4), Pair.create(WORK_TYPE_FGS, 1),
- Pair.create(WORK_TYPE_EJ, 3), Pair.create(WORK_TYPE_BG, 2),
- Pair.create(WORK_TYPE_BGUSER_IMPORTANT, 1)),
+ List.of(Pair.create(WORK_TYPE_TOP, .4f),
+ Pair.create(WORK_TYPE_FGS, .2f),
+ Pair.create(WORK_TYPE_UI, .1f),
+ Pair.create(WORK_TYPE_EJ, .1f), Pair.create(WORK_TYPE_BG, .05f),
+ Pair.create(WORK_TYPE_BGUSER_IMPORTANT, .05f)),
// defaultMax
- List.of(Pair.create(WORK_TYPE_BG, 6),
- Pair.create(WORK_TYPE_BGUSER_IMPORTANT, 2),
- Pair.create(WORK_TYPE_BGUSER, 3))
+ List.of(Pair.create(WORK_TYPE_BG, .5f),
+ Pair.create(WORK_TYPE_BGUSER_IMPORTANT, .25f),
+ Pair.create(WORK_TYPE_BGUSER, .2f))
),
- new WorkTypeConfig("screen_on_moderate", 9,
+ new WorkTypeConfig("screen_on_moderate", DEFAULT_CONCURRENCY_LIMIT,
+ /* defaultMaxTotal */ DEFAULT_CONCURRENCY_LIMIT / 2,
// defaultMin
- List.of(Pair.create(WORK_TYPE_TOP, 4), Pair.create(WORK_TYPE_FGS, 1),
- Pair.create(WORK_TYPE_EJ, 2), Pair.create(WORK_TYPE_BG, 1),
- Pair.create(WORK_TYPE_BGUSER_IMPORTANT, 1)),
+ List.of(Pair.create(WORK_TYPE_TOP, .4f),
+ Pair.create(WORK_TYPE_FGS, .1f),
+ Pair.create(WORK_TYPE_UI, .1f),
+ Pair.create(WORK_TYPE_EJ, .1f), Pair.create(WORK_TYPE_BG, .1f),
+ Pair.create(WORK_TYPE_BGUSER_IMPORTANT, .1f)),
// defaultMax
- List.of(Pair.create(WORK_TYPE_BG, 4),
- Pair.create(WORK_TYPE_BGUSER_IMPORTANT, 1),
- Pair.create(WORK_TYPE_BGUSER, 1))
+ List.of(Pair.create(WORK_TYPE_BG, .4f),
+ Pair.create(WORK_TYPE_BGUSER_IMPORTANT, .1f),
+ Pair.create(WORK_TYPE_BGUSER, .1f))
),
- new WorkTypeConfig("screen_on_low", 6,
+ new WorkTypeConfig("screen_on_low", DEFAULT_CONCURRENCY_LIMIT,
+ /* defaultMaxTotal */ DEFAULT_CONCURRENCY_LIMIT * 4 / 10,
// defaultMin
- List.of(Pair.create(WORK_TYPE_TOP, 4), Pair.create(WORK_TYPE_FGS, 1),
- Pair.create(WORK_TYPE_EJ, 1)),
+ List.of(Pair.create(WORK_TYPE_TOP, .6f),
+ Pair.create(WORK_TYPE_FGS, .1f),
+ Pair.create(WORK_TYPE_UI, .1f),
+ Pair.create(WORK_TYPE_EJ, .1f)),
// defaultMax
- List.of(Pair.create(WORK_TYPE_BG, 2),
- Pair.create(WORK_TYPE_BGUSER_IMPORTANT, 1),
- Pair.create(WORK_TYPE_BGUSER, 1))
+ List.of(Pair.create(WORK_TYPE_BG, 1.0f / 3),
+ Pair.create(WORK_TYPE_BGUSER_IMPORTANT, 1.0f / 6),
+ Pair.create(WORK_TYPE_BGUSER, 1.0f / 6))
),
- new WorkTypeConfig("screen_on_critical", 6,
+ new WorkTypeConfig("screen_on_critical", DEFAULT_CONCURRENCY_LIMIT,
+ /* defaultMaxTotal */ DEFAULT_CONCURRENCY_LIMIT * 4 / 10,
// defaultMin
- List.of(Pair.create(WORK_TYPE_TOP, 4), Pair.create(WORK_TYPE_FGS, 1),
- Pair.create(WORK_TYPE_EJ, 1)),
+ List.of(Pair.create(WORK_TYPE_TOP, .7f),
+ Pair.create(WORK_TYPE_FGS, .1f),
+ Pair.create(WORK_TYPE_UI, .1f),
+ Pair.create(WORK_TYPE_EJ, .05f)),
// defaultMax
- List.of(Pair.create(WORK_TYPE_BG, 1),
- Pair.create(WORK_TYPE_BGUSER_IMPORTANT, 1),
- Pair.create(WORK_TYPE_BGUSER, 1))
+ List.of(Pair.create(WORK_TYPE_BG, 1.0f / 6),
+ Pair.create(WORK_TYPE_BGUSER_IMPORTANT, 1.0f / 6),
+ Pair.create(WORK_TYPE_BGUSER, 1.0f / 6))
)
);
private static final WorkConfigLimitsPerMemoryTrimLevel CONFIG_LIMITS_SCREEN_OFF =
new WorkConfigLimitsPerMemoryTrimLevel(
- new WorkTypeConfig("screen_off_normal", 16,
+ new WorkTypeConfig("screen_off_normal", DEFAULT_CONCURRENCY_LIMIT,
+ /* defaultMaxTotal */ DEFAULT_CONCURRENCY_LIMIT,
// defaultMin
- List.of(Pair.create(WORK_TYPE_TOP, 4), Pair.create(WORK_TYPE_FGS, 2),
- Pair.create(WORK_TYPE_EJ, 3), Pair.create(WORK_TYPE_BG, 2),
- Pair.create(WORK_TYPE_BGUSER_IMPORTANT, 1)),
+ List.of(Pair.create(WORK_TYPE_TOP, .3f),
+ Pair.create(WORK_TYPE_FGS, .2f),
+ Pair.create(WORK_TYPE_UI, .2f),
+ Pair.create(WORK_TYPE_EJ, .15f), Pair.create(WORK_TYPE_BG, .1f),
+ Pair.create(WORK_TYPE_BGUSER_IMPORTANT, .05f)),
// defaultMax
- List.of(Pair.create(WORK_TYPE_BG, 10),
- Pair.create(WORK_TYPE_BGUSER_IMPORTANT, 2),
- Pair.create(WORK_TYPE_BGUSER, 3))
+ List.of(Pair.create(WORK_TYPE_BG, .6f),
+ Pair.create(WORK_TYPE_BGUSER_IMPORTANT, .2f),
+ Pair.create(WORK_TYPE_BGUSER, .2f))
),
- new WorkTypeConfig("screen_off_moderate", 14,
+ new WorkTypeConfig("screen_off_moderate", DEFAULT_CONCURRENCY_LIMIT,
+ /* defaultMaxTotal */ DEFAULT_CONCURRENCY_LIMIT * 9 / 10,
// defaultMin
- List.of(Pair.create(WORK_TYPE_TOP, 4), Pair.create(WORK_TYPE_FGS, 2),
- Pair.create(WORK_TYPE_EJ, 3), Pair.create(WORK_TYPE_BG, 2),
- Pair.create(WORK_TYPE_BGUSER_IMPORTANT, 1)),
+ List.of(Pair.create(WORK_TYPE_TOP, .3f),
+ Pair.create(WORK_TYPE_FGS, .2f),
+ Pair.create(WORK_TYPE_UI, .2f),
+ Pair.create(WORK_TYPE_EJ, .15f), Pair.create(WORK_TYPE_BG, .1f),
+ Pair.create(WORK_TYPE_BGUSER_IMPORTANT, .05f)),
// defaultMax
- List.of(Pair.create(WORK_TYPE_BG, 7),
- Pair.create(WORK_TYPE_BGUSER_IMPORTANT, 1),
- Pair.create(WORK_TYPE_BGUSER, 1))
+ List.of(Pair.create(WORK_TYPE_BG, .5f),
+ Pair.create(WORK_TYPE_BGUSER_IMPORTANT, .1f),
+ Pair.create(WORK_TYPE_BGUSER, .1f))
),
- new WorkTypeConfig("screen_off_low", 9,
+ new WorkTypeConfig("screen_off_low", DEFAULT_CONCURRENCY_LIMIT,
+ /* defaultMaxTotal */ DEFAULT_CONCURRENCY_LIMIT * 6 / 10,
// defaultMin
- List.of(Pair.create(WORK_TYPE_TOP, 4), Pair.create(WORK_TYPE_FGS, 1),
- Pair.create(WORK_TYPE_EJ, 2), Pair.create(WORK_TYPE_BG, 1)),
+ List.of(Pair.create(WORK_TYPE_TOP, .3f),
+ Pair.create(WORK_TYPE_FGS, .15f),
+ Pair.create(WORK_TYPE_UI, .15f),
+ Pair.create(WORK_TYPE_EJ, .1f), Pair.create(WORK_TYPE_BG, .05f),
+ Pair.create(WORK_TYPE_BGUSER_IMPORTANT, .05f)),
// defaultMax
- List.of(Pair.create(WORK_TYPE_BG, 3),
- Pair.create(WORK_TYPE_BGUSER_IMPORTANT, 1),
- Pair.create(WORK_TYPE_BGUSER, 1))
+ List.of(Pair.create(WORK_TYPE_BG, .25f),
+ Pair.create(WORK_TYPE_BGUSER_IMPORTANT, .1f),
+ Pair.create(WORK_TYPE_BGUSER, .1f))
),
- new WorkTypeConfig("screen_off_critical", 6,
+ new WorkTypeConfig("screen_off_critical", DEFAULT_CONCURRENCY_LIMIT,
+ /* defaultMaxTotal */ DEFAULT_CONCURRENCY_LIMIT * 4 / 10,
// defaultMin
- List.of(Pair.create(WORK_TYPE_TOP, 4), Pair.create(WORK_TYPE_FGS, 1),
- Pair.create(WORK_TYPE_EJ, 1)),
+ List.of(Pair.create(WORK_TYPE_TOP, .3f),
+ Pair.create(WORK_TYPE_FGS, .1f),
+ Pair.create(WORK_TYPE_UI, .1f),
+ Pair.create(WORK_TYPE_EJ, .05f)),
// defaultMax
- List.of(Pair.create(WORK_TYPE_BG, 1),
- Pair.create(WORK_TYPE_BGUSER_IMPORTANT, 1),
- Pair.create(WORK_TYPE_BGUSER, 1))
+ List.of(Pair.create(WORK_TYPE_BG, .1f),
+ Pair.create(WORK_TYPE_BGUSER_IMPORTANT, .1f),
+ Pair.create(WORK_TYPE_BGUSER, .1f))
)
);
@@ -311,6 +389,13 @@ class JobConcurrencyManager {
private final ArraySet<ContextAssignment> mRecycledIdle = new ArraySet<>();
private final ArrayList<ContextAssignment> mRecycledPreferredUidOnly = new ArrayList<>();
private final ArrayList<ContextAssignment> mRecycledStoppable = new ArrayList<>();
+ private final AssignmentInfo mRecycledAssignmentInfo = new AssignmentInfo();
+ private final SparseIntArray mRecycledPrivilegedState = new SparseIntArray();
+
+ private static final int PRIVILEGED_STATE_UNDEFINED = 0;
+ private static final int PRIVILEGED_STATE_NONE = 1;
+ private static final int PRIVILEGED_STATE_BAL = 2;
+ private static final int PRIVILEGED_STATE_TOP = 3;
private final Pools.Pool<ContextAssignment> mContextAssignmentPool =
new Pools.SimplePool<>(MAX_RETAINED_OBJECTS);
@@ -340,6 +425,12 @@ class JobConcurrencyManager {
private long mScreenOffAdjustmentDelayMs = DEFAULT_SCREEN_OFF_ADJUSTMENT_DELAY_MS;
/**
+ * The maximum number of jobs we'll attempt to have running at one time. This may occasionally
+ * be exceeded based on other factors.
+ */
+ private int mSteadyStateConcurrencyLimit = DEFAULT_CONCURRENCY_LIMIT;
+
+ /**
* The maximum number of expedited jobs a single userId-package can have running simultaneously.
* TOP apps are not limited.
*/
@@ -351,6 +442,26 @@ class JobConcurrencyManager {
*/
private int mPkgConcurrencyLimitRegular = DEFAULT_PKG_CONCURRENCY_LIMIT_REGULAR;
+ private boolean mMaxWaitTimeBypassEnabled = DEFAULT_ENABLE_MAX_WAIT_TIME_BYPASS;
+
+ /**
+ * The maximum time a user-initiated job would have to be potentially waiting for an available
+ * slot before we would consider creating a new slot for it.
+ */
+ private long mMaxWaitUIMs = DEFAULT_MAX_WAIT_UI_MS;
+
+ /**
+ * The maximum time an expedited job would have to be potentially waiting for an available
+ * slot before we would consider creating a new slot for it.
+ */
+ private long mMaxWaitEjMs = DEFAULT_MAX_WAIT_EJ_MS;
+
+ /**
+ * The maximum time a regular job would have to be potentially waiting for an available
+ * slot before we would consider creating a new slot for it.
+ */
+ private long mMaxWaitRegularMs = DEFAULT_MAX_WAIT_REGULAR_MS;
+
/** Current memory trim level. */
private int mLastMemoryTrimLevel;
@@ -361,6 +472,13 @@ class JobConcurrencyManager {
private final Consumer<PackageStats> mPackageStatsStagingCountClearer =
PackageStats::resetStagedCount;
+ private static final Histogram sConcurrencyHistogramLogger = new Histogram(
+ "job_scheduler.value_hist_job_concurrency",
+ // Create a histogram that expects values in the range [0, 99].
+ // Include more buckets than MAX_CONCURRENCY_LIMIT to account for/identify the cases
+ // where we may create additional slots for TOP-started EJs and UIJs
+ new Histogram.UniformOptions(100, 0, 99));
+
private final StatLogger mStatLogger = new StatLogger(new String[]{
"assignJobsToContexts",
"refreshSystemState",
@@ -378,11 +496,18 @@ class JobConcurrencyManager {
}
JobConcurrencyManager(JobSchedulerService service) {
+ this(service, new Injector());
+ }
+
+ @VisibleForTesting
+ JobConcurrencyManager(JobSchedulerService service, Injector injector) {
mService = service;
- mLock = mService.mLock;
+ mLock = mService.getLock();
mContext = service.getTestableContext();
+ mInjector = injector;
+ mNotificationCoordinator = new JobNotificationCoordinator();
- mHandler = JobSchedulerBackgroundThread.getHandler();
+ mHandler = AppSchedulingModuleThread.getHandler();
mGracePeriodObserver = new GracePeriodObserver(mContext);
mShouldRestrictBgUser = mContext.getResources().getBoolean(
@@ -412,10 +537,12 @@ class JobConcurrencyManager {
void onThirdPartyAppsCanStart() {
final IBatteryStats batteryStats = IBatteryStats.Stub.asInterface(
ServiceManager.getService(BatteryStats.SERVICE_NAME));
- for (int i = 0; i < STANDARD_CONCURRENCY_LIMIT; i++) {
+ for (int i = 0; i < mSteadyStateConcurrencyLimit; ++i) {
mIdleContexts.add(
- new JobServiceContext(mService, this, batteryStats,
- mService.mJobPackageTracker, mContext.getMainLooper()));
+ mInjector.createJobServiceContext(mService, this,
+ mNotificationCoordinator, batteryStats,
+ mService.mJobPackageTracker,
+ AppSchedulingModuleThread.get().getLooper()));
}
}
@@ -452,14 +579,14 @@ class JobConcurrencyManager {
if (mPowerManager != null && mPowerManager.isDeviceIdleMode()) {
synchronized (mLock) {
stopUnexemptedJobsForDoze();
- stopLongRunningJobsLocked("deep doze");
+ stopOvertimeJobsLocked("deep doze");
}
}
break;
case PowerManager.ACTION_POWER_SAVE_MODE_CHANGED:
if (mPowerManager != null && mPowerManager.isPowerSaveMode()) {
synchronized (mLock) {
- stopLongRunningJobsLocked("battery saver");
+ stopOvertimeJobsLocked("battery saver");
}
}
break;
@@ -543,6 +670,30 @@ class JobConcurrencyManager {
}
/**
+ * Return {@code true} if the specified job has been executing for longer than the minimum
+ * execution guarantee.
+ */
+ @GuardedBy("mLock")
+ boolean isJobInOvertimeLocked(@NonNull JobStatus job) {
+ if (!mRunningJobs.contains(job)) {
+ return false;
+ }
+
+ for (int i = mActiveServices.size() - 1; i >= 0; --i) {
+ final JobServiceContext jsc = mActiveServices.get(i);
+ final JobStatus jobStatus = jsc.getRunningJobLocked();
+
+ if (jobStatus == job) {
+ return !jsc.isWithinExecutionGuaranteeTime();
+ }
+ }
+
+ Slog.wtf(TAG, "Couldn't find long running job on a context");
+ mRunningJobs.remove(job);
+ return false;
+ }
+
+ /**
* Returns true if a job that is "similar" to the provided job is currently running.
* "Similar" in this context means any job that the {@link JobStore} would consider equivalent
* and replace one with the other.
@@ -551,7 +702,7 @@ class JobConcurrencyManager {
private boolean isSimilarJobRunningLocked(JobStatus job) {
for (int i = mRunningJobs.size() - 1; i >= 0; --i) {
JobStatus js = mRunningJobs.valueAt(i);
- if (job.getUid() == js.getUid() && job.getJobId() == js.getJobId()) {
+ if (job.matches(js.getUid(), js.getNamespace(), js.getJobId())) {
return true;
}
}
@@ -633,15 +784,44 @@ class JobConcurrencyManager {
return;
}
+ prepareForAssignmentDeterminationLocked(
+ mRecycledIdle, mRecycledPreferredUidOnly, mRecycledStoppable,
+ mRecycledAssignmentInfo);
+
+ if (DEBUG) {
+ Slog.d(TAG, printAssignments("running jobs initial",
+ mRecycledStoppable, mRecycledPreferredUidOnly));
+ }
+
+ determineAssignmentsLocked(
+ mRecycledChanged, mRecycledIdle, mRecycledPreferredUidOnly, mRecycledStoppable,
+ mRecycledAssignmentInfo);
+
+ if (DEBUG) {
+ Slog.d(TAG, printAssignments("running jobs final",
+ mRecycledStoppable, mRecycledPreferredUidOnly, mRecycledChanged));
+
+ Slog.d(TAG, "work count results: " + mWorkCountTracker);
+ }
+
+ carryOutAssignmentChangesLocked(mRecycledChanged);
+
+ cleanUpAfterAssignmentChangesLocked(
+ mRecycledChanged, mRecycledIdle, mRecycledPreferredUidOnly, mRecycledStoppable,
+ mRecycledAssignmentInfo, mRecycledPrivilegedState);
+
+ noteConcurrency(true);
+ }
+
+ @VisibleForTesting
+ @GuardedBy("mLock")
+ void prepareForAssignmentDeterminationLocked(final ArraySet<ContextAssignment> idle,
+ final List<ContextAssignment> preferredUidOnly,
+ final List<ContextAssignment> stoppable,
+ final AssignmentInfo info) {
final PendingJobQueue pendingJobQueue = mService.getPendingJobQueue();
final List<JobServiceContext> activeServices = mActiveServices;
- // To avoid GC churn, we recycle the arrays.
- final ArraySet<ContextAssignment> changed = mRecycledChanged;
- final ArraySet<ContextAssignment> idle = mRecycledIdle;
- final ArrayList<ContextAssignment> preferredUidOnly = mRecycledPreferredUidOnly;
- final ArrayList<ContextAssignment> stoppable = mRecycledStoppable;
-
updateCounterConfigLocked();
// Reset everything since we'll re-evaluate the current state.
mWorkCountTracker.resetCounts();
@@ -652,6 +832,8 @@ class JobConcurrencyManager {
updateNonRunningPrioritiesLocked(pendingJobQueue, true);
final int numRunningJobs = activeServices.size();
+ final long nowElapsed = sElapsedRealtimeClock.millis();
+ long minPreferredUidOnlyWaitingTimeMs = Long.MAX_VALUE;
for (int i = 0; i < numRunningJobs; ++i) {
final JobServiceContext jsc = activeServices.get(i);
final JobStatus js = jsc.getRunningJobLocked();
@@ -666,24 +848,38 @@ class JobConcurrencyManager {
if (js != null) {
mWorkCountTracker.incrementRunningJobCount(jsc.getRunningJobWorkType());
assignment.workType = jsc.getRunningJobWorkType();
+ if (js.startedWithImmediacyPrivilege) {
+ info.numRunningImmediacyPrivileged++;
+ }
+ if (js.shouldTreatAsUserInitiatedJob()) {
+ info.numRunningUi++;
+ } else if (js.startedAsExpeditedJob) {
+ info.numRunningEj++;
+ } else {
+ info.numRunningReg++;
+ }
}
assignment.preferredUid = jsc.getPreferredUid();
if ((assignment.shouldStopJobReason = shouldStopRunningJobLocked(jsc)) != null) {
stoppable.add(assignment);
} else {
+ assignment.timeUntilStoppableMs = jsc.getRemainingGuaranteedTimeMs(nowElapsed);
+ minPreferredUidOnlyWaitingTimeMs =
+ Math.min(minPreferredUidOnlyWaitingTimeMs, assignment.timeUntilStoppableMs);
preferredUidOnly.add(assignment);
}
}
preferredUidOnly.sort(sDeterminationComparator);
stoppable.sort(sDeterminationComparator);
- for (int i = numRunningJobs; i < STANDARD_CONCURRENCY_LIMIT; ++i) {
+ for (int i = numRunningJobs; i < mSteadyStateConcurrencyLimit; ++i) {
final JobServiceContext jsc;
final int numIdleContexts = mIdleContexts.size();
if (numIdleContexts > 0) {
jsc = mIdleContexts.removeAt(numIdleContexts - 1);
} else {
- Slog.wtf(TAG, "Had fewer than " + STANDARD_CONCURRENCY_LIMIT + " in existence");
+ // This could happen if the config is changed at runtime.
+ Slog.w(TAG, "Had fewer than " + mSteadyStateConcurrencyLimit + " in existence");
jsc = createNewJobServiceContext();
}
@@ -695,15 +891,35 @@ class JobConcurrencyManager {
assignment.context = jsc;
idle.add(assignment);
}
- if (DEBUG) {
- Slog.d(TAG, printAssignments("running jobs initial", stoppable, preferredUidOnly));
- }
mWorkCountTracker.onCountDone();
+ // Set 0 if there were no preferred UID only contexts to indicate no waiting time due
+ // to such jobs.
+ info.minPreferredUidOnlyWaitingTimeMs =
+ minPreferredUidOnlyWaitingTimeMs == Long.MAX_VALUE
+ ? 0 : minPreferredUidOnlyWaitingTimeMs;
+ }
- JobStatus nextPending;
+ @VisibleForTesting
+ @GuardedBy("mLock")
+ void determineAssignmentsLocked(final ArraySet<ContextAssignment> changed,
+ final ArraySet<ContextAssignment> idle,
+ final List<ContextAssignment> preferredUidOnly,
+ final List<ContextAssignment> stoppable,
+ @NonNull AssignmentInfo info) {
+ final PendingJobQueue pendingJobQueue = mService.getPendingJobQueue();
+ final List<JobServiceContext> activeServices = mActiveServices;
pendingJobQueue.resetIterator();
- int projectedRunningCount = numRunningJobs;
+ JobStatus nextPending;
+ int projectedRunningCount = activeServices.size();
+ long minChangedWaitingTimeMs = Long.MAX_VALUE;
+ // Only allow the Context creation bypass for each type if one of that type isn't already
+ // running. That way, we don't run into issues (creating too many additional contexts)
+ // if new jobs become ready to run in rapid succession and we end up going through this
+ // loop many times before running jobs have had a decent chance to finish.
+ boolean allowMaxWaitContextBypassUi = info.numRunningUi == 0;
+ boolean allowMaxWaitContextBypassEj = info.numRunningEj == 0;
+ boolean allowMaxWaitContextBypassOthers = info.numRunningReg == 0;
while ((nextPending = pendingJobQueue.next()) != null) {
if (mRunningJobs.contains(nextPending)) {
// Should never happen.
@@ -715,13 +931,20 @@ class JobConcurrencyManager {
continue;
}
- final boolean isTopEj = nextPending.shouldTreatAsExpeditedJob()
- && nextPending.lastEvaluatedBias == JobInfo.BIAS_TOP_APP;
+ final boolean hasImmediacyPrivilege =
+ hasImmediacyPrivilegeLocked(nextPending, mRecycledPrivilegedState);
if (DEBUG && isSimilarJobRunningLocked(nextPending)) {
- Slog.w(TAG, "Already running similar " + (isTopEj ? "TOP-EJ" : "job")
- + " to: " + nextPending);
+ Slog.w(TAG, "Already running similar job to: " + nextPending);
}
+ // Factoring minChangedWaitingTimeMs into the min waiting time effectively limits
+ // the number of additional contexts that are created due to long waiting times.
+ // By factoring it in, we imply that the new slot will be available for other
+ // pending jobs that could be designated as waiting too long, and those other jobs
+ // would only have to wait for the new slots to become available.
+ final long minWaitingTimeMs =
+ Math.min(info.minPreferredUidOnlyWaitingTimeMs, minChangedWaitingTimeMs);
+
// Find an available slot for nextPending. The context should be one of the following:
// 1. Unused
// 2. Its job should have used up its minimum execution guarantee so it
@@ -730,7 +953,7 @@ class JobConcurrencyManager {
ContextAssignment selectedContext = null;
final int allWorkTypes = getJobWorkTypes(nextPending);
final boolean pkgConcurrencyOkay = !isPkgConcurrencyLimitedLocked(nextPending);
- final boolean isInOverage = projectedRunningCount > STANDARD_CONCURRENCY_LIMIT;
+ final boolean isInOverage = projectedRunningCount > mSteadyStateConcurrencyLimit;
boolean startingJob = false;
if (idle.size() > 0) {
final int idx = idle.size() - 1;
@@ -749,33 +972,39 @@ class JobConcurrencyManager {
}
}
if (selectedContext == null && stoppable.size() > 0) {
- int topEjCount = 0;
- for (int r = mRunningJobs.size() - 1; r >= 0; --r) {
- JobStatus js = mRunningJobs.valueAt(r);
- if (js.startedAsExpeditedJob && js.lastEvaluatedBias == JobInfo.BIAS_TOP_APP) {
- topEjCount++;
- }
- }
for (int s = stoppable.size() - 1; s >= 0; --s) {
final ContextAssignment assignment = stoppable.get(s);
final JobStatus runningJob = assignment.context.getRunningJobLocked();
// Maybe stop the job if it has had its day in the sun. Only allow replacing
// for one of the following conditions:
- // 1. We're putting in the current TOP app's EJ
+ // 1. We're putting in a job that has the privilege of running immediately
// 2. There aren't too many jobs running AND the current job started when the
// app was in the background
// 3. There aren't too many jobs running AND the current job started when the
// app was on TOP, but the app has since left TOP
// 4. There aren't too many jobs running AND the current job started when the
- // app was on TOP, the app is still TOP, but there are too many TOP+EJs
+ // app was on TOP, the app is still TOP, but there are too many
+ // immediacy-privileged jobs
// running (because we don't want them to starve out other apps and the
// current job has already run for the minimum guaranteed time).
- boolean canReplace = isTopEj; // Case 1
+ // 5. This new job could be waiting for too long for a slot to open up
+ boolean canReplace = hasImmediacyPrivilege; // Case 1
if (!canReplace && !isInOverage) {
final int currentJobBias = mService.evaluateJobBiasLocked(runningJob);
canReplace = runningJob.lastEvaluatedBias < JobInfo.BIAS_TOP_APP // Case 2
|| currentJobBias < JobInfo.BIAS_TOP_APP // Case 3
- || topEjCount > .5 * mWorkTypeConfig.getMaxTotal(); // Case 4
+ // Case 4
+ || info.numRunningImmediacyPrivileged
+ > (mWorkTypeConfig.getMaxTotal() / 2);
+ }
+ if (!canReplace && mMaxWaitTimeBypassEnabled) { // Case 5
+ if (nextPending.shouldTreatAsUserInitiatedJob()) {
+ canReplace = minWaitingTimeMs >= mMaxWaitUIMs;
+ } else if (nextPending.shouldTreatAsExpeditedJob()) {
+ canReplace = minWaitingTimeMs >= mMaxWaitEjMs;
+ } else {
+ canReplace = minWaitingTimeMs >= mMaxWaitRegularMs;
+ }
}
if (canReplace) {
int replaceWorkType = mWorkCountTracker.canJobStart(allWorkTypes,
@@ -795,8 +1024,9 @@ class JobConcurrencyManager {
}
}
}
- if (selectedContext == null && (!isInOverage || isTopEj)) {
+ if (selectedContext == null && (!isInOverage || hasImmediacyPrivilege)) {
int lowestBiasSeen = Integer.MAX_VALUE;
+ long newMinPreferredUidOnlyWaitingTimeMs = Long.MAX_VALUE;
for (int p = preferredUidOnly.size() - 1; p >= 0; --p) {
final ContextAssignment assignment = preferredUidOnly.get(p);
final JobStatus runningJob = assignment.context.getRunningJobLocked();
@@ -809,6 +1039,13 @@ class JobConcurrencyManager {
}
if (selectedContext == null || lowestBiasSeen > jobBias) {
+ if (selectedContext != null) {
+ // We're no longer using the previous context, so factor it into the
+ // calculation.
+ newMinPreferredUidOnlyWaitingTimeMs = Math.min(
+ newMinPreferredUidOnlyWaitingTimeMs,
+ selectedContext.timeUntilStoppableMs);
+ }
// Step down the preemption threshold - wind up replacing
// the lowest-bias running job
lowestBiasSeen = jobBias;
@@ -817,19 +1054,26 @@ class JobConcurrencyManager {
assignment.preemptReasonCode = JobParameters.STOP_REASON_PREEMPT;
// In this case, we're just going to preempt a low bias job, we're not
// actually starting a job, so don't set startingJob to true.
+ } else {
+ // We're not going to use this context, so factor it into the calculation.
+ newMinPreferredUidOnlyWaitingTimeMs = Math.min(
+ newMinPreferredUidOnlyWaitingTimeMs,
+ assignment.timeUntilStoppableMs);
}
}
if (selectedContext != null) {
selectedContext.newJob = nextPending;
preferredUidOnly.remove(selectedContext);
+ info.minPreferredUidOnlyWaitingTimeMs = newMinPreferredUidOnlyWaitingTimeMs;
}
}
- // Make sure to run EJs for the TOP app immediately.
- if (isTopEj) {
+ // Make sure to run jobs with special privilege immediately.
+ if (hasImmediacyPrivilege) {
if (selectedContext != null
&& selectedContext.context.getRunningJobLocked() != null) {
- // We're "replacing" a currently running job, but we want TOP EJs to start
- // immediately, so we'll start the EJ on a fresh available context and
+ // We're "replacing" a currently running job, but we want immediacy-privileged
+ // jobs to start immediately, so we'll start the privileged jobs on a fresh
+ // available context and
// stop this currently running job to replace in two steps.
changed.add(selectedContext);
projectedRunningCount--;
@@ -838,6 +1082,9 @@ class JobConcurrencyManager {
selectedContext = null;
}
if (selectedContext == null) {
+ if (DEBUG) {
+ Slog.d(TAG, "Allowing additional context because EJ would wait too long");
+ }
selectedContext = mContextAssignmentPool.acquire();
if (selectedContext == null) {
selectedContext = new ContextAssignment();
@@ -850,6 +1097,51 @@ class JobConcurrencyManager {
selectedContext.newWorkType =
(workType != WORK_TYPE_NONE) ? workType : WORK_TYPE_TOP;
}
+ } else if (selectedContext == null && mMaxWaitTimeBypassEnabled) {
+ final boolean wouldBeWaitingTooLong;
+ if (nextPending.shouldTreatAsUserInitiatedJob() && allowMaxWaitContextBypassUi) {
+ wouldBeWaitingTooLong = minWaitingTimeMs >= mMaxWaitUIMs;
+ // We want to create at most one additional context for each type.
+ allowMaxWaitContextBypassUi = !wouldBeWaitingTooLong;
+ } else if (nextPending.shouldTreatAsExpeditedJob() && allowMaxWaitContextBypassEj) {
+ wouldBeWaitingTooLong = minWaitingTimeMs >= mMaxWaitEjMs;
+ // We want to create at most one additional context for each type.
+ allowMaxWaitContextBypassEj = !wouldBeWaitingTooLong;
+ } else if (allowMaxWaitContextBypassOthers) {
+ // The way things are set up a UIJ or EJ could end up here and create a 2nd
+ // context as if it were a "regular" job. That's fine for now since they would
+ // still be subject to the higher waiting time threshold here.
+ wouldBeWaitingTooLong = minWaitingTimeMs >= mMaxWaitRegularMs;
+ // We want to create at most one additional context for each type.
+ allowMaxWaitContextBypassOthers = !wouldBeWaitingTooLong;
+ } else {
+ wouldBeWaitingTooLong = false;
+ }
+ if (wouldBeWaitingTooLong) {
+ if (DEBUG) {
+ Slog.d(TAG, "Allowing additional context because job would wait too long");
+ }
+ selectedContext = mContextAssignmentPool.acquire();
+ if (selectedContext == null) {
+ selectedContext = new ContextAssignment();
+ }
+ selectedContext.context = mIdleContexts.size() > 0
+ ? mIdleContexts.removeAt(mIdleContexts.size() - 1)
+ : createNewJobServiceContext();
+ selectedContext.newJob = nextPending;
+ final int workType = mWorkCountTracker.canJobStart(allWorkTypes);
+ if (workType != WORK_TYPE_NONE) {
+ selectedContext.newWorkType = workType;
+ } else {
+ // Use the strongest work type possible for this job.
+ for (int type = 1; type <= ALL_WORK_TYPES; type = type << 1) {
+ if ((type & allWorkTypes) != 0) {
+ selectedContext.newWorkType = type;
+ break;
+ }
+ }
+ }
+ }
}
final PackageStats packageStats = getPkgStatsLocked(
nextPending.getSourceUserId(), nextPending.getSourcePackageName());
@@ -859,7 +1151,10 @@ class JobConcurrencyManager {
projectedRunningCount--;
}
if (selectedContext.newJob != null) {
+ selectedContext.newJob.startedWithImmediacyPrivilege = hasImmediacyPrivilege;
projectedRunningCount++;
+ minChangedWaitingTimeMs = Math.min(minChangedWaitingTimeMs,
+ mService.getMinJobExecutionGuaranteeMs(selectedContext.newJob));
}
packageStats.adjustStagedCount(true, nextPending.shouldTreatAsExpeditedJob());
}
@@ -871,13 +1166,10 @@ class JobConcurrencyManager {
packageStats);
}
}
- if (DEBUG) {
- Slog.d(TAG, printAssignments("running jobs final",
- stoppable, preferredUidOnly, changed));
-
- Slog.d(TAG, "assignJobsToContexts: " + mWorkCountTracker.toString());
- }
+ }
+ @GuardedBy("mLock")
+ private void carryOutAssignmentChangesLocked(final ArraySet<ContextAssignment> changed) {
for (int c = changed.size() - 1; c >= 0; --c) {
final ContextAssignment assignment = changed.valueAt(c);
final JobStatus js = assignment.context.getRunningJobLocked();
@@ -901,6 +1193,15 @@ class JobConcurrencyManager {
assignment.clear();
mContextAssignmentPool.release(assignment);
}
+ }
+
+ @GuardedBy("mLock")
+ private void cleanUpAfterAssignmentChangesLocked(final ArraySet<ContextAssignment> changed,
+ final ArraySet<ContextAssignment> idle,
+ final List<ContextAssignment> preferredUidOnly,
+ final List<ContextAssignment> stoppable,
+ final AssignmentInfo assignmentInfo,
+ final SparseIntArray privilegedState) {
for (int s = stoppable.size() - 1; s >= 0; --s) {
final ContextAssignment assignment = stoppable.get(s);
assignment.clear();
@@ -921,9 +1222,63 @@ class JobConcurrencyManager {
idle.clear();
stoppable.clear();
preferredUidOnly.clear();
+ assignmentInfo.clear();
+ privilegedState.clear();
mWorkCountTracker.resetStagingCount();
mActivePkgStats.forEach(mPackageStatsStagingCountClearer);
- noteConcurrency();
+ }
+
+ @VisibleForTesting
+ @GuardedBy("mLock")
+ boolean hasImmediacyPrivilegeLocked(@NonNull JobStatus job,
+ @NonNull SparseIntArray cachedPrivilegedState) {
+ if (!job.shouldTreatAsExpeditedJob() && !job.shouldTreatAsUserInitiatedJob()) {
+ return false;
+ }
+ // EJs & user-initiated jobs for the TOP app should run immediately.
+ // However, even for user-initiated jobs, if the app has not recently been in TOP or BAL
+ // state, we don't give the immediacy privilege so that we can try and maintain
+ // reasonably concurrency behavior.
+ if (job.lastEvaluatedBias == JobInfo.BIAS_TOP_APP) {
+ return true;
+ }
+ final int uid = job.getSourceUid();
+ final int privilegedState = cachedPrivilegedState.get(uid, PRIVILEGED_STATE_UNDEFINED);
+ switch (privilegedState) {
+ case PRIVILEGED_STATE_TOP:
+ return true;
+ case PRIVILEGED_STATE_BAL:
+ return job.shouldTreatAsUserInitiatedJob();
+ case PRIVILEGED_STATE_NONE:
+ return false;
+ case PRIVILEGED_STATE_UNDEFINED:
+ default:
+ final ActivityManagerInternal activityManagerInternal =
+ LocalServices.getService(ActivityManagerInternal.class);
+ final int procState = activityManagerInternal.getUidProcessState(uid);
+ if (procState == ActivityManager.PROCESS_STATE_TOP) {
+ cachedPrivilegedState.put(uid, PRIVILEGED_STATE_TOP);
+ return true;
+ }
+ if (job.shouldTreatAsExpeditedJob()) {
+ // EJs only get the TOP privilege.
+ return false;
+ }
+
+ final BackgroundStartPrivileges bsp =
+ activityManagerInternal.getBackgroundStartPrivileges(uid);
+ if (DEBUG) {
+ Slog.d(TAG, "Job " + job.toShortString() + " bsp state: " + bsp);
+ }
+ // Intentionally use the background activity start BSP here instead of
+ // the full BAL check since the former is transient and better indicates that the
+ // user recently interacted with the app, while the latter includes
+ // permanent exceptions that don't warrant bypassing normal concurrency policy.
+ final boolean balAllowed = bsp.allowsBackgroundActivityStarts();
+ cachedPrivilegedState.put(uid,
+ balAllowed ? PRIVILEGED_STATE_BAL : PRIVILEGED_STATE_NONE);
+ return balAllowed;
+ }
}
@GuardedBy("mLock")
@@ -941,6 +1296,25 @@ class JobConcurrencyManager {
assignJobsToContextsLocked();
}
+ @Nullable
+ @GuardedBy("mLock")
+ JobServiceContext getRunningJobServiceContextLocked(JobStatus job) {
+ if (!mRunningJobs.contains(job)) {
+ return null;
+ }
+
+ for (int i = 0; i < mActiveServices.size(); i++) {
+ JobServiceContext jsc = mActiveServices.get(i);
+ final JobStatus executing = jsc.getRunningJobLocked();
+ if (executing == job) {
+ return jsc;
+ }
+ }
+ Slog.wtf(TAG, "Couldn't find running job on a context");
+ mRunningJobs.remove(job);
+ return null;
+ }
+
@GuardedBy("mLock")
boolean stopJobOnServiceContextLocked(JobStatus job,
@JobParameters.StopReason int reason, int internalReasonCode, String debugReason) {
@@ -977,7 +1351,7 @@ class JobConcurrencyManager {
}
@GuardedBy("mLock")
- private void stopLongRunningJobsLocked(@NonNull String debugReason) {
+ private void stopOvertimeJobsLocked(@NonNull String debugReason) {
for (int i = 0; i < mActiveServices.size(); ++i) {
final JobServiceContext jsc = mActiveServices.get(i);
final JobStatus jobStatus = jsc.getRunningJobLocked();
@@ -989,6 +1363,45 @@ class JobConcurrencyManager {
}
}
+ /**
+ * Stops any jobs that have run for more than their minimum execution guarantee and are
+ * restricted by the given {@link JobRestriction}.
+ */
+ @GuardedBy("mLock")
+ void maybeStopOvertimeJobsLocked(@NonNull JobRestriction restriction) {
+ for (int i = mActiveServices.size() - 1; i >= 0; --i) {
+ final JobServiceContext jsc = mActiveServices.get(i);
+ final JobStatus jobStatus = jsc.getRunningJobLocked();
+
+ if (jobStatus != null && !jsc.isWithinExecutionGuaranteeTime()
+ && restriction.isJobRestricted(jobStatus)) {
+ jsc.cancelExecutingJobLocked(restriction.getStopReason(),
+ restriction.getInternalReason(),
+ JobParameters.getInternalReasonCodeDescription(
+ restriction.getInternalReason()));
+ }
+ }
+ }
+
+ @GuardedBy("mLock")
+ void markJobsForUserStopLocked(int userId, @NonNull String packageName,
+ @Nullable String debugReason) {
+ for (int i = mActiveServices.size() - 1; i >= 0; --i) {
+ final JobServiceContext jsc = mActiveServices.get(i);
+ final JobStatus jobStatus = jsc.getRunningJobLocked();
+
+ // Normally, we handle jobs primarily using the source package and userId,
+ // however, user-visible jobs are shown as coming from the calling app, so we
+ // need to operate on the jobs from that perspective here.
+ if (jobStatus != null && userId == jobStatus.getUserId()
+ && jobStatus.getServiceComponent().getPackageName().equals(packageName)) {
+ jsc.markForProcessDeathLocked(JobParameters.STOP_REASON_USER,
+ JobParameters.INTERNAL_STOP_REASON_USER_UI_STOP,
+ debugReason);
+ }
+ }
+ }
+
@GuardedBy("mLock")
void stopNonReadyActiveJobsLocked() {
for (int i = 0; i < mActiveServices.size(); i++) {
@@ -1014,7 +1427,7 @@ class JobConcurrencyManager {
final JobRestriction restriction = mService.checkIfRestricted(running);
if (restriction != null) {
final int internalReasonCode = restriction.getInternalReason();
- serviceContext.cancelExecutingJobLocked(restriction.getReason(),
+ serviceContext.cancelExecutingJobLocked(restriction.getStopReason(),
internalReasonCode,
"restricted due to "
+ JobParameters.getInternalReasonCodeDescription(
@@ -1024,10 +1437,13 @@ class JobConcurrencyManager {
}
}
- private void noteConcurrency() {
+ private void noteConcurrency(boolean logForHistogram) {
mService.mJobPackageTracker.noteConcurrency(mRunningJobs.size(),
// TODO: log per type instead of only TOP
mWorkCountTracker.getRunningJobCount(WORK_TYPE_TOP));
+ if (logForHistogram) {
+ sConcurrencyHistogramLogger.logSample(mActiveServices.size());
+ }
}
@GuardedBy("mLock")
@@ -1130,6 +1546,7 @@ class JobConcurrencyManager {
mActivePkgStats.add(
jobStatus.getSourceUserId(), jobStatus.getSourcePackageName(),
packageStats);
+ mService.resetPendingJobReasonCache(jobStatus);
}
if (mService.getPendingJobQueue().remove(jobStatus)) {
mService.mJobPackageTracker.noteNonpending(jobStatus);
@@ -1147,7 +1564,7 @@ class JobConcurrencyManager {
mActiveServices.remove(worker);
if (mIdleContexts.size() < MAX_RETAINED_OBJECTS) {
// Don't need to save all new contexts, but keep some extra around in case we need
- // extras for another TOP+EJ overage.
+ // extras for another immediacy privileged overage.
mIdleContexts.add(worker);
} else {
mNumDroppedContexts++;
@@ -1165,13 +1582,46 @@ class JobConcurrencyManager {
}
final PendingJobQueue pendingJobQueue = mService.getPendingJobQueue();
- if (mActiveServices.size() >= STANDARD_CONCURRENCY_LIMIT || pendingJobQueue.size() == 0) {
+ if (pendingJobQueue.size() == 0) {
worker.clearPreferredUid();
- // We're over the limit (because the TOP app scheduled a lot of EJs). Don't start
- // running anything new until we get back below the limit.
- noteConcurrency();
+ // Don't log the drop in concurrency to the histogram, otherwise, we'll end up
+ // overcounting lower concurrency values as jobs end execution.
+ noteConcurrency(false);
return;
}
+ if (mActiveServices.size() >= mSteadyStateConcurrencyLimit) {
+ final boolean respectConcurrencyLimit;
+ if (!mMaxWaitTimeBypassEnabled) {
+ respectConcurrencyLimit = true;
+ } else {
+ long minWaitingTimeMs = Long.MAX_VALUE;
+ final long nowElapsed = sElapsedRealtimeClock.millis();
+ for (int i = mActiveServices.size() - 1; i >= 0; --i) {
+ minWaitingTimeMs = Math.min(minWaitingTimeMs,
+ mActiveServices.get(i).getRemainingGuaranteedTimeMs(nowElapsed));
+ }
+ final boolean wouldBeWaitingTooLong;
+ if (mWorkCountTracker.getPendingJobCount(WORK_TYPE_UI) > 0) {
+ wouldBeWaitingTooLong = minWaitingTimeMs >= mMaxWaitUIMs;
+ } else if (mWorkCountTracker.getPendingJobCount(WORK_TYPE_EJ) > 0) {
+ wouldBeWaitingTooLong = minWaitingTimeMs >= mMaxWaitEjMs;
+ } else {
+ wouldBeWaitingTooLong = minWaitingTimeMs >= mMaxWaitRegularMs;
+ }
+ respectConcurrencyLimit = !wouldBeWaitingTooLong;
+ }
+ if (respectConcurrencyLimit) {
+ worker.clearPreferredUid();
+ // We're over the limit (because there were a lot of immediacy-privileged jobs
+ // scheduled), but we should
+ // be able to stop the other jobs soon so don't start running anything new until we
+ // get back below the limit.
+ // Don't log the drop in concurrency to the histogram, otherwise, we'll end up
+ // overcounting lower concurrency values as jobs end execution.
+ noteConcurrency(false);
+ return;
+ }
+ }
if (worker.getPreferredUid() != JobServiceContext.NO_PREFERRED_UID) {
updateCounterConfigLocked();
@@ -1317,7 +1767,9 @@ class JobConcurrencyManager {
}
}
- noteConcurrency();
+ // Don't log the drop in concurrency to the histogram, otherwise, we'll end up
+ // overcounting lower concurrency values as jobs end execution.
+ noteConcurrency(false);
}
/**
@@ -1349,6 +1801,12 @@ class JobConcurrencyManager {
if (mPowerManager.isDeviceIdleMode()) {
return "deep doze";
}
+ final JobRestriction jobRestriction;
+ if ((jobRestriction = mService.checkIfRestricted(js)) != null) {
+ return "restriction:"
+ + JobParameters.getInternalReasonCodeDescription(
+ jobRestriction.getInternalReason());
+ }
// Update config in case memory usage has changed significantly.
updateCounterConfigLocked();
@@ -1383,17 +1841,17 @@ class JobConcurrencyManager {
}
} else if (mWorkCountTracker.getPendingJobCount(WORK_TYPE_EJ) > 0) {
return "blocking " + workTypeToString(WORK_TYPE_EJ) + " queue";
- } else if (js.startedAsExpeditedJob && js.lastEvaluatedBias == JobInfo.BIAS_TOP_APP) {
- // Try not to let TOP + EJ starve out other apps.
- int topEjCount = 0;
+ } else if (js.startedWithImmediacyPrivilege) {
+ // Try not to let jobs with immediacy privilege starve out other apps.
+ int immediacyPrivilegeCount = 0;
for (int r = mRunningJobs.size() - 1; r >= 0; --r) {
JobStatus j = mRunningJobs.valueAt(r);
- if (j.startedAsExpeditedJob && j.lastEvaluatedBias == JobInfo.BIAS_TOP_APP) {
- topEjCount++;
+ if (j.startedWithImmediacyPrivilege) {
+ immediacyPrivilegeCount++;
}
}
- if (topEjCount > .5 * mWorkTypeConfig.getMaxTotal()) {
- return "prevent top EJ dominance";
+ if (immediacyPrivilegeCount > mWorkTypeConfig.getMaxTotal() / 2) {
+ return "prevent immediacy privilege dominance";
}
}
// No other pending EJs. Return null so we don't let regular jobs preempt an EJ.
@@ -1427,15 +1885,17 @@ class JobConcurrencyManager {
}
@GuardedBy("mLock")
- boolean executeTimeoutCommandLocked(PrintWriter pw, String pkgName, int userId,
- boolean hasJobId, int jobId) {
+ boolean executeStopCommandLocked(PrintWriter pw, String pkgName, int userId,
+ @Nullable String namespace, boolean matchJobId, int jobId,
+ int stopReason, int internalStopReason) {
boolean foundSome = false;
for (int i = 0; i < mActiveServices.size(); i++) {
final JobServiceContext jc = mActiveServices.get(i);
final JobStatus js = jc.getRunningJobLocked();
- if (jc.timeoutIfExecutingLocked(pkgName, userId, hasJobId, jobId, "shell")) {
+ if (jc.stopIfExecutingLocked(pkgName, userId, namespace, matchJobId, jobId,
+ stopReason, internalStopReason)) {
foundSome = true;
- pw.print("Timing out: ");
+ pw.print("Stopping job: ");
js.printUniqueId(pw);
pw.print(" ");
pw.println(js.getServiceComponent().flattenToShortString());
@@ -1444,12 +1904,62 @@ class JobConcurrencyManager {
return foundSome;
}
+ /**
+ * Returns the estimated network bytes if the job is running. Returns {@code null} if the job
+ * isn't running.
+ */
+ @Nullable
+ @GuardedBy("mLock")
+ Pair<Long, Long> getEstimatedNetworkBytesLocked(String pkgName, int uid,
+ String namespace, int jobId) {
+ for (int i = 0; i < mActiveServices.size(); i++) {
+ final JobServiceContext jc = mActiveServices.get(i);
+ final JobStatus js = jc.getRunningJobLocked();
+ if (js != null && js.matches(uid, namespace, jobId)
+ && js.getSourcePackageName().equals(pkgName)) {
+ return jc.getEstimatedNetworkBytes();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the transferred network bytes if the job is running. Returns {@code null} if the job
+ * isn't running.
+ */
+ @Nullable
+ @GuardedBy("mLock")
+ Pair<Long, Long> getTransferredNetworkBytesLocked(String pkgName, int uid,
+ String namespace, int jobId) {
+ for (int i = 0; i < mActiveServices.size(); i++) {
+ final JobServiceContext jc = mActiveServices.get(i);
+ final JobStatus js = jc.getRunningJobLocked();
+ if (js != null && js.matches(uid, namespace, jobId)
+ && js.getSourcePackageName().equals(pkgName)) {
+ return jc.getTransferredNetworkBytes();
+ }
+ }
+ return null;
+ }
+
+ boolean isNotificationAssociatedWithAnyUserInitiatedJobs(int notificationId, int userId,
+ @NonNull String packageName) {
+ return mNotificationCoordinator.isNotificationAssociatedWithAnyUserInitiatedJobs(
+ notificationId, userId, packageName);
+ }
+
+ boolean isNotificationChannelAssociatedWithAnyUserInitiatedJobs(
+ @NonNull String notificationChannel, int userId, @NonNull String packageName) {
+ return mNotificationCoordinator.isNotificationChannelAssociatedWithAnyUserInitiatedJobs(
+ notificationChannel, userId, packageName);
+ }
+
@NonNull
private JobServiceContext createNewJobServiceContext() {
- return new JobServiceContext(mService, this,
+ return mInjector.createJobServiceContext(mService, this, mNotificationCoordinator,
IBatteryStats.Stub.asInterface(
ServiceManager.getService(BatteryStats.SERVICE_NAME)),
- mService.mJobPackageTracker, mContext.getMainLooper());
+ mService.mJobPackageTracker, AppSchedulingModuleThread.get().getLooper());
}
@GuardedBy("mLock")
@@ -1460,6 +1970,9 @@ class JobConcurrencyManager {
pendingJobQueue.resetIterator();
while ((js = pendingJobQueue.next()) != null) {
s.append("(")
+ .append("{")
+ .append(js.getNamespace())
+ .append("} ")
.append(js.getJob().getId())
.append(", ")
.append(js.getUid())
@@ -1484,6 +1997,9 @@ class JobConcurrencyManager {
if (job == null) {
s.append("nothing");
} else {
+ if (job.getNamespace() != null) {
+ s.append(job.getNamespace()).append(":");
+ }
s.append(job.getJobId()).append("/").append(job.getUid());
}
s.append(")");
@@ -1498,25 +2014,40 @@ class JobConcurrencyManager {
DeviceConfig.Properties properties =
DeviceConfig.getProperties(DeviceConfig.NAMESPACE_JOB_SCHEDULER);
+ // Concurrency limit should be in the range [8, MAX_CONCURRENCY_LIMIT].
+ mSteadyStateConcurrencyLimit = Math.max(8, Math.min(MAX_CONCURRENCY_LIMIT,
+ properties.getInt(KEY_CONCURRENCY_LIMIT, DEFAULT_CONCURRENCY_LIMIT)));
+
mScreenOffAdjustmentDelayMs = properties.getLong(
KEY_SCREEN_OFF_ADJUSTMENT_DELAY_MS, DEFAULT_SCREEN_OFF_ADJUSTMENT_DELAY_MS);
- CONFIG_LIMITS_SCREEN_ON.normal.update(properties);
- CONFIG_LIMITS_SCREEN_ON.moderate.update(properties);
- CONFIG_LIMITS_SCREEN_ON.low.update(properties);
- CONFIG_LIMITS_SCREEN_ON.critical.update(properties);
+ CONFIG_LIMITS_SCREEN_ON.normal.update(properties, mSteadyStateConcurrencyLimit);
+ CONFIG_LIMITS_SCREEN_ON.moderate.update(properties, mSteadyStateConcurrencyLimit);
+ CONFIG_LIMITS_SCREEN_ON.low.update(properties, mSteadyStateConcurrencyLimit);
+ CONFIG_LIMITS_SCREEN_ON.critical.update(properties, mSteadyStateConcurrencyLimit);
- CONFIG_LIMITS_SCREEN_OFF.normal.update(properties);
- CONFIG_LIMITS_SCREEN_OFF.moderate.update(properties);
- CONFIG_LIMITS_SCREEN_OFF.low.update(properties);
- CONFIG_LIMITS_SCREEN_OFF.critical.update(properties);
+ CONFIG_LIMITS_SCREEN_OFF.normal.update(properties, mSteadyStateConcurrencyLimit);
+ CONFIG_LIMITS_SCREEN_OFF.moderate.update(properties, mSteadyStateConcurrencyLimit);
+ CONFIG_LIMITS_SCREEN_OFF.low.update(properties, mSteadyStateConcurrencyLimit);
+ CONFIG_LIMITS_SCREEN_OFF.critical.update(properties, mSteadyStateConcurrencyLimit);
- // Package concurrency limits must in the range [1, STANDARD_CONCURRENCY_LIMIT].
- mPkgConcurrencyLimitEj = Math.max(1, Math.min(STANDARD_CONCURRENCY_LIMIT,
+ // Package concurrency limits must in the range [1, mSteadyStateConcurrencyLimit].
+ mPkgConcurrencyLimitEj = Math.max(1, Math.min(mSteadyStateConcurrencyLimit,
properties.getInt(KEY_PKG_CONCURRENCY_LIMIT_EJ, DEFAULT_PKG_CONCURRENCY_LIMIT_EJ)));
- mPkgConcurrencyLimitRegular = Math.max(1, Math.min(STANDARD_CONCURRENCY_LIMIT,
+ mPkgConcurrencyLimitRegular = Math.max(1, Math.min(mSteadyStateConcurrencyLimit,
properties.getInt(
KEY_PKG_CONCURRENCY_LIMIT_REGULAR, DEFAULT_PKG_CONCURRENCY_LIMIT_REGULAR)));
+
+ mMaxWaitTimeBypassEnabled = properties.getBoolean(
+ KEY_ENABLE_MAX_WAIT_TIME_BYPASS, DEFAULT_ENABLE_MAX_WAIT_TIME_BYPASS);
+ // UI max wait must be in the range [0, infinity).
+ mMaxWaitUIMs = Math.max(0, properties.getLong(KEY_MAX_WAIT_UI_MS, DEFAULT_MAX_WAIT_UI_MS));
+ // EJ max wait must be in the range [UI max wait, infinity).
+ mMaxWaitEjMs = Math.max(mMaxWaitUIMs,
+ properties.getLong(KEY_MAX_WAIT_EJ_MS, DEFAULT_MAX_WAIT_EJ_MS));
+ // Regular max wait must be in the range [EJ max wait, infinity).
+ mMaxWaitRegularMs = Math.max(mMaxWaitEjMs,
+ properties.getLong(KEY_MAX_WAIT_REGULAR_MS, DEFAULT_MAX_WAIT_REGULAR_MS));
}
@GuardedBy("mLock")
@@ -1527,9 +2058,14 @@ class JobConcurrencyManager {
try {
pw.println("Configuration:");
pw.increaseIndent();
+ pw.print(KEY_CONCURRENCY_LIMIT, mSteadyStateConcurrencyLimit).println();
pw.print(KEY_SCREEN_OFF_ADJUSTMENT_DELAY_MS, mScreenOffAdjustmentDelayMs).println();
pw.print(KEY_PKG_CONCURRENCY_LIMIT_EJ, mPkgConcurrencyLimitEj).println();
pw.print(KEY_PKG_CONCURRENCY_LIMIT_REGULAR, mPkgConcurrencyLimitRegular).println();
+ pw.print(KEY_ENABLE_MAX_WAIT_TIME_BYPASS, mMaxWaitTimeBypassEnabled).println();
+ pw.print(KEY_MAX_WAIT_UI_MS, mMaxWaitUIMs).println();
+ pw.print(KEY_MAX_WAIT_EJ_MS, mMaxWaitEjMs).println();
+ pw.print(KEY_MAX_WAIT_REGULAR_MS, mMaxWaitRegularMs).println();
pw.println();
CONFIG_LIMITS_SCREEN_ON.normal.dump(pw);
pw.println();
@@ -1712,10 +2248,12 @@ class JobConcurrencyManager {
if (js.shouldTreatAsExpeditedJob()) {
classification |= WORK_TYPE_EJ;
+ } else if (js.shouldTreatAsUserInitiatedJob()) {
+ classification |= WORK_TYPE_UI;
}
} else {
if (js.lastEvaluatedBias >= JobInfo.BIAS_FOREGROUND_SERVICE
- || js.shouldTreatAsExpeditedJob()) {
+ || js.shouldTreatAsExpeditedJob() || js.shouldTreatAsUserInitiatedJob()) {
classification |= WORK_TYPE_BGUSER_IMPORTANT;
}
// BGUSER_IMPORTANT jobs can also run as BGUSER jobs, so not an 'else' here.
@@ -1727,126 +2265,192 @@ class JobConcurrencyManager {
@VisibleForTesting
static class WorkTypeConfig {
+ private static final String KEY_PREFIX_MAX = CONFIG_KEY_PREFIX_CONCURRENCY + "max_";
+ private static final String KEY_PREFIX_MIN = CONFIG_KEY_PREFIX_CONCURRENCY + "min_";
@VisibleForTesting
static final String KEY_PREFIX_MAX_TOTAL = CONFIG_KEY_PREFIX_CONCURRENCY + "max_total_";
- private static final String KEY_PREFIX_MAX_TOP = CONFIG_KEY_PREFIX_CONCURRENCY + "max_top_";
- private static final String KEY_PREFIX_MAX_FGS = CONFIG_KEY_PREFIX_CONCURRENCY + "max_fgs_";
- private static final String KEY_PREFIX_MAX_EJ = CONFIG_KEY_PREFIX_CONCURRENCY + "max_ej_";
- private static final String KEY_PREFIX_MAX_BG = CONFIG_KEY_PREFIX_CONCURRENCY + "max_bg_";
- private static final String KEY_PREFIX_MAX_BGUSER =
- CONFIG_KEY_PREFIX_CONCURRENCY + "max_bguser_";
- private static final String KEY_PREFIX_MAX_BGUSER_IMPORTANT =
- CONFIG_KEY_PREFIX_CONCURRENCY + "max_bguser_important_";
- private static final String KEY_PREFIX_MIN_TOP = CONFIG_KEY_PREFIX_CONCURRENCY + "min_top_";
- private static final String KEY_PREFIX_MIN_FGS = CONFIG_KEY_PREFIX_CONCURRENCY + "min_fgs_";
- private static final String KEY_PREFIX_MIN_EJ = CONFIG_KEY_PREFIX_CONCURRENCY + "min_ej_";
- private static final String KEY_PREFIX_MIN_BG = CONFIG_KEY_PREFIX_CONCURRENCY + "min_bg_";
- private static final String KEY_PREFIX_MIN_BGUSER =
- CONFIG_KEY_PREFIX_CONCURRENCY + "min_bguser_";
- private static final String KEY_PREFIX_MIN_BGUSER_IMPORTANT =
- CONFIG_KEY_PREFIX_CONCURRENCY + "min_bguser_important_";
+ @VisibleForTesting
+ static final String KEY_PREFIX_MAX_RATIO = KEY_PREFIX_MAX + "ratio_";
+ private static final String KEY_PREFIX_MAX_RATIO_TOP = KEY_PREFIX_MAX_RATIO + "top_";
+ private static final String KEY_PREFIX_MAX_RATIO_FGS = KEY_PREFIX_MAX_RATIO + "fgs_";
+ private static final String KEY_PREFIX_MAX_RATIO_UI = KEY_PREFIX_MAX_RATIO + "ui_";
+ private static final String KEY_PREFIX_MAX_RATIO_EJ = KEY_PREFIX_MAX_RATIO + "ej_";
+ private static final String KEY_PREFIX_MAX_RATIO_BG = KEY_PREFIX_MAX_RATIO + "bg_";
+ private static final String KEY_PREFIX_MAX_RATIO_BGUSER = KEY_PREFIX_MAX_RATIO + "bguser_";
+ private static final String KEY_PREFIX_MAX_RATIO_BGUSER_IMPORTANT =
+ KEY_PREFIX_MAX_RATIO + "bguser_important_";
+ @VisibleForTesting
+ static final String KEY_PREFIX_MIN_RATIO = KEY_PREFIX_MIN + "ratio_";
+ private static final String KEY_PREFIX_MIN_RATIO_TOP = KEY_PREFIX_MIN_RATIO + "top_";
+ private static final String KEY_PREFIX_MIN_RATIO_FGS = KEY_PREFIX_MIN_RATIO + "fgs_";
+ private static final String KEY_PREFIX_MIN_RATIO_UI = KEY_PREFIX_MIN_RATIO + "ui_";
+ private static final String KEY_PREFIX_MIN_RATIO_EJ = KEY_PREFIX_MIN_RATIO + "ej_";
+ private static final String KEY_PREFIX_MIN_RATIO_BG = KEY_PREFIX_MIN_RATIO + "bg_";
+ private static final String KEY_PREFIX_MIN_RATIO_BGUSER = KEY_PREFIX_MIN_RATIO + "bguser_";
+ private static final String KEY_PREFIX_MIN_RATIO_BGUSER_IMPORTANT =
+ KEY_PREFIX_MIN_RATIO + "bguser_important_";
private final String mConfigIdentifier;
private int mMaxTotal;
private final SparseIntArray mMinReservedSlots = new SparseIntArray(NUM_WORK_TYPES);
private final SparseIntArray mMaxAllowedSlots = new SparseIntArray(NUM_WORK_TYPES);
private final int mDefaultMaxTotal;
- private final SparseIntArray mDefaultMinReservedSlots = new SparseIntArray(NUM_WORK_TYPES);
- private final SparseIntArray mDefaultMaxAllowedSlots = new SparseIntArray(NUM_WORK_TYPES);
-
- WorkTypeConfig(@NonNull String configIdentifier, int defaultMaxTotal,
- List<Pair<Integer, Integer>> defaultMin, List<Pair<Integer, Integer>> defaultMax) {
+ // We use SparseIntArrays to store floats because there is currently no SparseFloatArray
+ // available, and it doesn't seem worth it to add such a data structure just for this
+ // use case. We don't use SparseDoubleArrays because DeviceConfig only supports floats and
+ // converting between floats and ints is more straightforward than floats and doubles.
+ private final SparseIntArray mDefaultMinReservedSlotsRatio =
+ new SparseIntArray(NUM_WORK_TYPES);
+ private final SparseIntArray mDefaultMaxAllowedSlotsRatio =
+ new SparseIntArray(NUM_WORK_TYPES);
+
+ WorkTypeConfig(@NonNull String configIdentifier,
+ int steadyStateConcurrencyLimit, int defaultMaxTotal,
+ List<Pair<Integer, Float>> defaultMinRatio,
+ List<Pair<Integer, Float>> defaultMaxRatio) {
mConfigIdentifier = configIdentifier;
- mDefaultMaxTotal = mMaxTotal = Math.min(defaultMaxTotal, STANDARD_CONCURRENCY_LIMIT);
+ mDefaultMaxTotal = mMaxTotal = Math.min(defaultMaxTotal, steadyStateConcurrencyLimit);
int numReserved = 0;
- for (int i = defaultMin.size() - 1; i >= 0; --i) {
- mDefaultMinReservedSlots.put(defaultMin.get(i).first, defaultMin.get(i).second);
- numReserved += defaultMin.get(i).second;
+ for (int i = defaultMinRatio.size() - 1; i >= 0; --i) {
+ final float ratio = defaultMinRatio.get(i).second;
+ final int wt = defaultMinRatio.get(i).first;
+ if (ratio < 0 || 1 <= ratio) {
+ // 1 means to reserve everything. This shouldn't be allowed.
+ // We only create new configs on boot, so this should trigger during development
+ // (before the code gets checked in), so this makes sure the hard-coded defaults
+ // make sense. DeviceConfig values will be handled gracefully in update().
+ throw new IllegalArgumentException("Invalid default min ratio: wt=" + wt
+ + " minRatio=" + ratio);
+ }
+ mDefaultMinReservedSlotsRatio.put(wt, Float.floatToRawIntBits(ratio));
+ numReserved += mMaxTotal * ratio;
}
if (mDefaultMaxTotal < 0 || numReserved > mDefaultMaxTotal) {
// We only create new configs on boot, so this should trigger during development
// (before the code gets checked in), so this makes sure the hard-coded defaults
// make sense. DeviceConfig values will be handled gracefully in update().
throw new IllegalArgumentException("Invalid default config: t=" + defaultMaxTotal
- + " min=" + defaultMin + " max=" + defaultMax);
- }
- for (int i = defaultMax.size() - 1; i >= 0; --i) {
- mDefaultMaxAllowedSlots.put(defaultMax.get(i).first, defaultMax.get(i).second);
+ + " min=" + defaultMinRatio + " max=" + defaultMaxRatio);
+ }
+ for (int i = defaultMaxRatio.size() - 1; i >= 0; --i) {
+ final float ratio = defaultMaxRatio.get(i).second;
+ final int wt = defaultMaxRatio.get(i).first;
+ final float minRatio =
+ Float.intBitsToFloat(mDefaultMinReservedSlotsRatio.get(wt, 0));
+ if (ratio < minRatio || ratio <= 0) {
+ // Max ratio shouldn't be <= 0 or less than minRatio.
+ throw new IllegalArgumentException("Invalid default config:"
+ + " t=" + defaultMaxTotal
+ + " min=" + defaultMinRatio + " max=" + defaultMaxRatio);
+ }
+ mDefaultMaxAllowedSlotsRatio.put(wt, Float.floatToRawIntBits(ratio));
}
update(new DeviceConfig.Properties.Builder(
- DeviceConfig.NAMESPACE_JOB_SCHEDULER).build());
+ DeviceConfig.NAMESPACE_JOB_SCHEDULER).build(), steadyStateConcurrencyLimit);
}
- void update(@NonNull DeviceConfig.Properties properties) {
- // Ensure total in the range [1, STANDARD_CONCURRENCY_LIMIT].
- mMaxTotal = Math.max(1, Math.min(STANDARD_CONCURRENCY_LIMIT,
+ void update(@NonNull DeviceConfig.Properties properties, int steadyStateConcurrencyLimit) {
+ // Ensure total in the range [1, mSteadyStateConcurrencyLimit].
+ mMaxTotal = Math.max(1, Math.min(steadyStateConcurrencyLimit,
properties.getInt(KEY_PREFIX_MAX_TOTAL + mConfigIdentifier, mDefaultMaxTotal)));
+ final int oneIntBits = Float.floatToIntBits(1);
+
mMaxAllowedSlots.clear();
// Ensure they're in the range [1, total].
- final int maxTop = Math.max(1, Math.min(mMaxTotal,
- properties.getInt(KEY_PREFIX_MAX_TOP + mConfigIdentifier,
- mDefaultMaxAllowedSlots.get(WORK_TYPE_TOP, mMaxTotal))));
+ final int maxTop = getMaxValue(properties,
+ KEY_PREFIX_MAX_RATIO_TOP + mConfigIdentifier, WORK_TYPE_TOP, oneIntBits);
mMaxAllowedSlots.put(WORK_TYPE_TOP, maxTop);
- final int maxFgs = Math.max(1, Math.min(mMaxTotal,
- properties.getInt(KEY_PREFIX_MAX_FGS + mConfigIdentifier,
- mDefaultMaxAllowedSlots.get(WORK_TYPE_FGS, mMaxTotal))));
+ final int maxFgs = getMaxValue(properties,
+ KEY_PREFIX_MAX_RATIO_FGS + mConfigIdentifier, WORK_TYPE_FGS, oneIntBits);
mMaxAllowedSlots.put(WORK_TYPE_FGS, maxFgs);
- final int maxEj = Math.max(1, Math.min(mMaxTotal,
- properties.getInt(KEY_PREFIX_MAX_EJ + mConfigIdentifier,
- mDefaultMaxAllowedSlots.get(WORK_TYPE_EJ, mMaxTotal))));
+ final int maxUi = getMaxValue(properties,
+ KEY_PREFIX_MAX_RATIO_UI + mConfigIdentifier, WORK_TYPE_UI, oneIntBits);
+ mMaxAllowedSlots.put(WORK_TYPE_UI, maxUi);
+ final int maxEj = getMaxValue(properties,
+ KEY_PREFIX_MAX_RATIO_EJ + mConfigIdentifier, WORK_TYPE_EJ, oneIntBits);
mMaxAllowedSlots.put(WORK_TYPE_EJ, maxEj);
- final int maxBg = Math.max(1, Math.min(mMaxTotal,
- properties.getInt(KEY_PREFIX_MAX_BG + mConfigIdentifier,
- mDefaultMaxAllowedSlots.get(WORK_TYPE_BG, mMaxTotal))));
+ final int maxBg = getMaxValue(properties,
+ KEY_PREFIX_MAX_RATIO_BG + mConfigIdentifier, WORK_TYPE_BG, oneIntBits);
mMaxAllowedSlots.put(WORK_TYPE_BG, maxBg);
- final int maxBgUserImp = Math.max(1, Math.min(mMaxTotal,
- properties.getInt(KEY_PREFIX_MAX_BGUSER_IMPORTANT + mConfigIdentifier,
- mDefaultMaxAllowedSlots.get(WORK_TYPE_BGUSER_IMPORTANT, mMaxTotal))));
+ final int maxBgUserImp = getMaxValue(properties,
+ KEY_PREFIX_MAX_RATIO_BGUSER_IMPORTANT + mConfigIdentifier,
+ WORK_TYPE_BGUSER_IMPORTANT, oneIntBits);
mMaxAllowedSlots.put(WORK_TYPE_BGUSER_IMPORTANT, maxBgUserImp);
- final int maxBgUser = Math.max(1, Math.min(mMaxTotal,
- properties.getInt(KEY_PREFIX_MAX_BGUSER + mConfigIdentifier,
- mDefaultMaxAllowedSlots.get(WORK_TYPE_BGUSER, mMaxTotal))));
+ final int maxBgUser = getMaxValue(properties,
+ KEY_PREFIX_MAX_RATIO_BGUSER + mConfigIdentifier, WORK_TYPE_BGUSER, oneIntBits);
mMaxAllowedSlots.put(WORK_TYPE_BGUSER, maxBgUser);
int remaining = mMaxTotal;
mMinReservedSlots.clear();
// Ensure top is in the range [1, min(maxTop, total)]
- final int minTop = Math.max(1, Math.min(Math.min(maxTop, mMaxTotal),
- properties.getInt(KEY_PREFIX_MIN_TOP + mConfigIdentifier,
- mDefaultMinReservedSlots.get(WORK_TYPE_TOP))));
+ final int minTop = getMinValue(properties,
+ KEY_PREFIX_MIN_RATIO_TOP + mConfigIdentifier, WORK_TYPE_TOP,
+ 1, Math.min(maxTop, mMaxTotal));
mMinReservedSlots.put(WORK_TYPE_TOP, minTop);
remaining -= minTop;
// Ensure fgs is in the range [0, min(maxFgs, remaining)]
- final int minFgs = Math.max(0, Math.min(Math.min(maxFgs, remaining),
- properties.getInt(KEY_PREFIX_MIN_FGS + mConfigIdentifier,
- mDefaultMinReservedSlots.get(WORK_TYPE_FGS))));
+ final int minFgs = getMinValue(properties,
+ KEY_PREFIX_MIN_RATIO_FGS + mConfigIdentifier, WORK_TYPE_FGS,
+ 0, Math.min(maxFgs, remaining));
mMinReservedSlots.put(WORK_TYPE_FGS, minFgs);
remaining -= minFgs;
+ // Ensure ui is in the range [0, min(maxUi, remaining)]
+ final int minUi = getMinValue(properties,
+ KEY_PREFIX_MIN_RATIO_UI + mConfigIdentifier, WORK_TYPE_UI,
+ 0, Math.min(maxUi, remaining));
+ mMinReservedSlots.put(WORK_TYPE_UI, minUi);
+ remaining -= minUi;
// Ensure ej is in the range [0, min(maxEj, remaining)]
- final int minEj = Math.max(0, Math.min(Math.min(maxEj, remaining),
- properties.getInt(KEY_PREFIX_MIN_EJ + mConfigIdentifier,
- mDefaultMinReservedSlots.get(WORK_TYPE_EJ))));
+ final int minEj = getMinValue(properties,
+ KEY_PREFIX_MIN_RATIO_EJ + mConfigIdentifier, WORK_TYPE_EJ,
+ 0, Math.min(maxEj, remaining));
mMinReservedSlots.put(WORK_TYPE_EJ, minEj);
remaining -= minEj;
// Ensure bg is in the range [0, min(maxBg, remaining)]
- final int minBg = Math.max(0, Math.min(Math.min(maxBg, remaining),
- properties.getInt(KEY_PREFIX_MIN_BG + mConfigIdentifier,
- mDefaultMinReservedSlots.get(WORK_TYPE_BG))));
+ final int minBg = getMinValue(properties,
+ KEY_PREFIX_MIN_RATIO_BG + mConfigIdentifier, WORK_TYPE_BG,
+ 0, Math.min(maxBg, remaining));
mMinReservedSlots.put(WORK_TYPE_BG, minBg);
remaining -= minBg;
// Ensure bg user imp is in the range [0, min(maxBgUserImp, remaining)]
- final int minBgUserImp = Math.max(0, Math.min(Math.min(maxBgUserImp, remaining),
- properties.getInt(KEY_PREFIX_MIN_BGUSER_IMPORTANT + mConfigIdentifier,
- mDefaultMinReservedSlots.get(WORK_TYPE_BGUSER_IMPORTANT, 0))));
+ final int minBgUserImp = getMinValue(properties,
+ KEY_PREFIX_MIN_RATIO_BGUSER_IMPORTANT + mConfigIdentifier,
+ WORK_TYPE_BGUSER_IMPORTANT, 0, Math.min(maxBgUserImp, remaining));
mMinReservedSlots.put(WORK_TYPE_BGUSER_IMPORTANT, minBgUserImp);
+ remaining -= minBgUserImp;
// Ensure bg user is in the range [0, min(maxBgUser, remaining)]
- final int minBgUser = Math.max(0, Math.min(Math.min(maxBgUser, remaining),
- properties.getInt(KEY_PREFIX_MIN_BGUSER + mConfigIdentifier,
- mDefaultMinReservedSlots.get(WORK_TYPE_BGUSER, 0))));
+ final int minBgUser = getMinValue(properties,
+ KEY_PREFIX_MIN_RATIO_BGUSER + mConfigIdentifier, WORK_TYPE_BGUSER,
+ 0, Math.min(maxBgUser, remaining));
mMinReservedSlots.put(WORK_TYPE_BGUSER, minBgUser);
}
+ /**
+ * Return the calculated max value for the work type.
+ * @param defaultFloatInIntBits A {@code float} value in int bits representation (using
+ * {@link Float#floatToIntBits(float)}.
+ */
+ private int getMaxValue(@NonNull DeviceConfig.Properties properties, @NonNull String key,
+ int workType, int defaultFloatInIntBits) {
+ final float maxRatio = Math.min(1, properties.getFloat(key,
+ Float.intBitsToFloat(
+ mDefaultMaxAllowedSlotsRatio.get(workType, defaultFloatInIntBits))));
+ // Max values should be in the range [1, total].
+ return Math.max(1, (int) (mMaxTotal * maxRatio));
+ }
+
+ /**
+ * Return the calculated min value for the work type.
+ */
+ private int getMinValue(@NonNull DeviceConfig.Properties properties, @NonNull String key,
+ int workType, int lowerLimit, int upperLimit) {
+ final float minRatio = Math.min(1,
+ properties.getFloat(key,
+ Float.intBitsToFloat(mDefaultMinReservedSlotsRatio.get(workType))));
+ return Math.max(lowerLimit, Math.min(upperLimit, (int) (mMaxTotal * minRatio)));
+ }
+
int getMaxTotal() {
return mMaxTotal;
}
@@ -1861,29 +2465,43 @@ class JobConcurrencyManager {
void dump(IndentingPrintWriter pw) {
pw.print(KEY_PREFIX_MAX_TOTAL + mConfigIdentifier, mMaxTotal).println();
- pw.print(KEY_PREFIX_MIN_TOP + mConfigIdentifier, mMinReservedSlots.get(WORK_TYPE_TOP))
+ pw.print(KEY_PREFIX_MIN_RATIO_TOP + mConfigIdentifier,
+ mMinReservedSlots.get(WORK_TYPE_TOP))
.println();
- pw.print(KEY_PREFIX_MAX_TOP + mConfigIdentifier, mMaxAllowedSlots.get(WORK_TYPE_TOP))
+ pw.print(KEY_PREFIX_MAX_RATIO_TOP + mConfigIdentifier,
+ mMaxAllowedSlots.get(WORK_TYPE_TOP))
.println();
- pw.print(KEY_PREFIX_MIN_FGS + mConfigIdentifier, mMinReservedSlots.get(WORK_TYPE_FGS))
+ pw.print(KEY_PREFIX_MIN_RATIO_FGS + mConfigIdentifier,
+ mMinReservedSlots.get(WORK_TYPE_FGS))
.println();
- pw.print(KEY_PREFIX_MAX_FGS + mConfigIdentifier, mMaxAllowedSlots.get(WORK_TYPE_FGS))
+ pw.print(KEY_PREFIX_MAX_RATIO_FGS + mConfigIdentifier,
+ mMaxAllowedSlots.get(WORK_TYPE_FGS))
.println();
- pw.print(KEY_PREFIX_MIN_EJ + mConfigIdentifier, mMinReservedSlots.get(WORK_TYPE_EJ))
+ pw.print(KEY_PREFIX_MIN_RATIO_UI + mConfigIdentifier,
+ mMinReservedSlots.get(WORK_TYPE_UI))
.println();
- pw.print(KEY_PREFIX_MAX_EJ + mConfigIdentifier, mMaxAllowedSlots.get(WORK_TYPE_EJ))
+ pw.print(KEY_PREFIX_MAX_RATIO_UI + mConfigIdentifier,
+ mMaxAllowedSlots.get(WORK_TYPE_UI))
.println();
- pw.print(KEY_PREFIX_MIN_BG + mConfigIdentifier, mMinReservedSlots.get(WORK_TYPE_BG))
+ pw.print(KEY_PREFIX_MIN_RATIO_EJ + mConfigIdentifier,
+ mMinReservedSlots.get(WORK_TYPE_EJ))
.println();
- pw.print(KEY_PREFIX_MAX_BG + mConfigIdentifier, mMaxAllowedSlots.get(WORK_TYPE_BG))
+ pw.print(KEY_PREFIX_MAX_RATIO_EJ + mConfigIdentifier,
+ mMaxAllowedSlots.get(WORK_TYPE_EJ))
.println();
- pw.print(KEY_PREFIX_MIN_BGUSER + mConfigIdentifier,
+ pw.print(KEY_PREFIX_MIN_RATIO_BG + mConfigIdentifier,
+ mMinReservedSlots.get(WORK_TYPE_BG))
+ .println();
+ pw.print(KEY_PREFIX_MAX_RATIO_BG + mConfigIdentifier,
+ mMaxAllowedSlots.get(WORK_TYPE_BG))
+ .println();
+ pw.print(KEY_PREFIX_MIN_RATIO_BGUSER + mConfigIdentifier,
mMinReservedSlots.get(WORK_TYPE_BGUSER_IMPORTANT)).println();
- pw.print(KEY_PREFIX_MAX_BGUSER + mConfigIdentifier,
+ pw.print(KEY_PREFIX_MAX_RATIO_BGUSER + mConfigIdentifier,
mMaxAllowedSlots.get(WORK_TYPE_BGUSER_IMPORTANT)).println();
- pw.print(KEY_PREFIX_MIN_BGUSER + mConfigIdentifier,
+ pw.print(KEY_PREFIX_MIN_RATIO_BGUSER + mConfigIdentifier,
mMinReservedSlots.get(WORK_TYPE_BGUSER)).println();
- pw.print(KEY_PREFIX_MAX_BGUSER + mConfigIdentifier,
+ pw.print(KEY_PREFIX_MAX_RATIO_BGUSER + mConfigIdentifier,
mMaxAllowedSlots.get(WORK_TYPE_BGUSER)).println();
}
}
@@ -2279,12 +2897,14 @@ class JobConcurrencyManager {
}
}
- private static final class ContextAssignment {
+ @VisibleForTesting
+ static final class ContextAssignment {
public JobServiceContext context;
public int preferredUid = JobServiceContext.NO_PREFERRED_UID;
public int workType = WORK_TYPE_NONE;
public String preemptReason;
public int preemptReasonCode = JobParameters.STOP_REASON_UNDEFINED;
+ public long timeUntilStoppableMs;
public String shouldStopJobReason;
public JobStatus newJob;
public int newWorkType = WORK_TYPE_NONE;
@@ -2295,12 +2915,30 @@ class JobConcurrencyManager {
workType = WORK_TYPE_NONE;
preemptReason = null;
preemptReasonCode = JobParameters.STOP_REASON_UNDEFINED;
+ timeUntilStoppableMs = 0;
shouldStopJobReason = null;
newJob = null;
newWorkType = WORK_TYPE_NONE;
}
}
+ @VisibleForTesting
+ static final class AssignmentInfo {
+ public long minPreferredUidOnlyWaitingTimeMs;
+ public int numRunningImmediacyPrivileged;
+ public int numRunningUi;
+ public int numRunningEj;
+ public int numRunningReg;
+
+ void clear() {
+ minPreferredUidOnlyWaitingTimeMs = 0;
+ numRunningImmediacyPrivileged = 0;
+ numRunningUi = 0;
+ numRunningEj = 0;
+ numRunningReg = 0;
+ }
+ }
+
// TESTING HELPERS
@VisibleForTesting
@@ -2309,6 +2947,15 @@ class JobConcurrencyManager {
final PackageStats packageStats =
getPackageStatsForTesting(job.getSourceUserId(), job.getSourcePackageName());
packageStats.adjustRunningCount(true, job.shouldTreatAsExpeditedJob());
+
+ final JobServiceContext context;
+ if (mIdleContexts.size() > 0) {
+ context = mIdleContexts.removeAt(mIdleContexts.size() - 1);
+ } else {
+ context = createNewJobServiceContext();
+ }
+ context.executeRunnableJob(job, mWorkCountTracker.canJobStart(getJobWorkTypes(job)));
+ mActiveServices.add(context);
}
@VisibleForTesting
@@ -2328,4 +2975,16 @@ class JobConcurrencyManager {
mActivePkgStats.add(userId, packageName, packageStats);
return packageStats;
}
+
+ @VisibleForTesting
+ static class Injector {
+ @NonNull
+ JobServiceContext createJobServiceContext(JobSchedulerService service,
+ JobConcurrencyManager concurrencyManager,
+ JobNotificationCoordinator notificationCoordinator, IBatteryStats batteryStats,
+ JobPackageTracker tracker, Looper looper) {
+ return new JobServiceContext(service, concurrencyManager, notificationCoordinator,
+ batteryStats, tracker, looper);
+ }
+ }
}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobNotificationCoordinator.java b/apex/jobscheduler/service/java/com/android/server/job/JobNotificationCoordinator.java
new file mode 100644
index 000000000000..071707059f2d
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobNotificationCoordinator.java
@@ -0,0 +1,350 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.job;
+
+import static android.app.job.JobService.JOB_END_NOTIFICATION_POLICY_DETACH;
+import static android.app.job.JobService.JOB_END_NOTIFICATION_POLICY_REMOVE;
+
+import android.annotation.NonNull;
+import android.app.Notification;
+import android.app.job.JobParameters;
+import android.app.job.JobService;
+import android.content.pm.UserPackage;
+import android.os.UserHandle;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.IntArray;
+import android.util.Slog;
+import android.util.SparseArrayMap;
+import android.util.SparseSetArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.modules.expresslog.Counter;
+import com.android.server.LocalServices;
+import com.android.server.job.controllers.JobStatus;
+import com.android.server.notification.NotificationManagerInternal;
+
+class JobNotificationCoordinator {
+ private static final String TAG = "JobNotificationCoordinator";
+
+ /**
+ * Local lock for independent objects like mUijNotifications and mUijNotificationChannels which
+ * don't depend on other JS objects such as JobServiceContext which require the global JS lock.
+ *
+ * Note: do <b>NOT</b> acquire the global lock while this one is held.
+ */
+ private final Object mUijLock = new Object();
+
+ /**
+ * Mapping of UserPackage -> {notificationId -> List<JobServiceContext>} to track which jobs
+ * are associated with each app's notifications.
+ */
+ private final ArrayMap<UserPackage, SparseSetArray<JobServiceContext>> mCurrentAssociations =
+ new ArrayMap<>();
+ /**
+ * Set of NotificationDetails for each running job.
+ */
+ private final ArrayMap<JobServiceContext, NotificationDetails> mNotificationDetails =
+ new ArrayMap<>();
+
+ /**
+ * Mapping of userId -> {packageName, notificationIds} tracking which notifications
+ * associated with each app belong to user-initiated jobs.
+ *
+ * Note: this map can be accessed without holding the main JS lock, so that other services like
+ * NotificationManagerService can call into JS and verify associations.
+ */
+ @GuardedBy("mUijLock")
+ private final SparseArrayMap<String, IntArray> mUijNotifications = new SparseArrayMap<>();
+
+ /**
+ * Mapping of userId -> {packageName, notificationChannels} tracking which notification channels
+ * associated with each app are hosting a user-initiated job notification.
+ *
+ * Note: this map can be accessed without holding the main JS lock, so that other services like
+ * NotificationManagerService can call into JS and verify associations.
+ */
+ @GuardedBy("mUijLock")
+ private final SparseArrayMap<String, ArraySet<String>> mUijNotificationChannels =
+ new SparseArrayMap<>();
+
+ private static final class NotificationDetails {
+ @NonNull
+ public final UserPackage userPackage;
+ public final int notificationId;
+ public final String notificationChannel;
+ public final int appPid;
+ public final int appUid;
+ @JobService.JobEndNotificationPolicy
+ public final int jobEndNotificationPolicy;
+
+ NotificationDetails(@NonNull UserPackage userPackage, int appPid, int appUid,
+ int notificationId, String notificationChannel,
+ @JobService.JobEndNotificationPolicy int jobEndNotificationPolicy) {
+ this.userPackage = userPackage;
+ this.notificationId = notificationId;
+ this.notificationChannel = notificationChannel;
+ this.appPid = appPid;
+ this.appUid = appUid;
+ this.jobEndNotificationPolicy = jobEndNotificationPolicy;
+ }
+ }
+
+ private final NotificationManagerInternal mNotificationManagerInternal;
+
+ JobNotificationCoordinator() {
+ mNotificationManagerInternal = LocalServices.getService(NotificationManagerInternal.class);
+ }
+
+ void enqueueNotification(@NonNull JobServiceContext hostingContext, @NonNull String packageName,
+ int callingPid, int callingUid, int notificationId, @NonNull Notification notification,
+ @JobService.JobEndNotificationPolicy int jobEndNotificationPolicy) {
+ validateNotification(packageName, callingUid, notification, jobEndNotificationPolicy);
+ final JobStatus jobStatus = hostingContext.getRunningJobLocked();
+ if (jobStatus == null) {
+ Slog.wtfStack(TAG, "enqueueNotification called with no running job");
+ return;
+ }
+ final NotificationDetails oldDetails = mNotificationDetails.get(hostingContext);
+ if (oldDetails == null) {
+ if (jobStatus.startedAsUserInitiatedJob) {
+ Counter.logIncrementWithUid(
+ "job_scheduler.value_cntr_w_uid_initial_set_notification_call_required",
+ jobStatus.getUid());
+ } else {
+ Counter.logIncrementWithUid(
+ "job_scheduler.value_cntr_w_uid_initial_set_notification_call_optional",
+ jobStatus.getUid());
+ }
+ } else {
+ if (jobStatus.startedAsUserInitiatedJob) {
+ Counter.logIncrementWithUid(
+ "job_scheduler.value_cntr_w_uid_subsequent_set_notification_call_required",
+ jobStatus.getUid());
+ } else {
+ Counter.logIncrementWithUid(
+ "job_scheduler.value_cntr_w_uid_subsequent_set_notification_call_optional",
+ jobStatus.getUid());
+ }
+ if (oldDetails.notificationId != notificationId) {
+ // App is switching notification IDs. Remove association with the old one.
+ removeNotificationAssociation(hostingContext, JobParameters.STOP_REASON_UNDEFINED,
+ jobStatus);
+ Counter.logIncrementWithUid(
+ "job_scheduler.value_cntr_w_uid_set_notification_changed_notification_ids",
+ jobStatus.getUid());
+ }
+ }
+ final int userId = UserHandle.getUserId(callingUid);
+ if (jobStatus != null && jobStatus.startedAsUserInitiatedJob) {
+ notification.flags |= Notification.FLAG_USER_INITIATED_JOB;
+ synchronized (mUijLock) {
+ maybeCreateUijNotificationSetsLocked(userId, packageName);
+ final IntArray notificationIds = mUijNotifications.get(userId, packageName);
+ if (notificationIds.indexOf(notificationId) == -1) {
+ notificationIds.add(notificationId);
+ }
+ mUijNotificationChannels.get(userId, packageName).add(notification.getChannelId());
+ }
+ }
+ final UserPackage userPackage = UserPackage.of(userId, packageName);
+ final NotificationDetails details = new NotificationDetails(
+ userPackage, callingPid, callingUid, notificationId, notification.getChannelId(),
+ jobEndNotificationPolicy);
+ SparseSetArray<JobServiceContext> appNotifications = mCurrentAssociations.get(userPackage);
+ if (appNotifications == null) {
+ appNotifications = new SparseSetArray<>();
+ mCurrentAssociations.put(userPackage, appNotifications);
+ }
+ appNotifications.add(notificationId, hostingContext);
+ mNotificationDetails.put(hostingContext, details);
+ // Call into NotificationManager after internal data structures have been updated since
+ // NotificationManager calls into this class to check for any existing associations.
+ mNotificationManagerInternal.enqueueNotification(
+ packageName, packageName, callingUid, callingPid, /* tag */ null,
+ notificationId, notification, userId);
+ }
+
+ void removeNotificationAssociation(@NonNull JobServiceContext hostingContext,
+ @JobParameters.StopReason int stopReason, JobStatus completedJob) {
+ final NotificationDetails details = mNotificationDetails.remove(hostingContext);
+ if (details == null) {
+ return;
+ }
+ final SparseSetArray<JobServiceContext> associations =
+ mCurrentAssociations.get(details.userPackage);
+ if (associations == null || !associations.remove(details.notificationId, hostingContext)) {
+ Slog.wtf(TAG, "Association data structures not in sync");
+ return;
+ }
+ final int userId = UserHandle.getUserId(details.appUid);
+ final String packageName = details.userPackage.packageName;
+ final int notificationId = details.notificationId;
+ boolean stripUijFlag = true;
+ ArraySet<JobServiceContext> associatedContexts = associations.get(notificationId);
+ if (associatedContexts == null || associatedContexts.isEmpty()) {
+ // No more jobs using this notification. Apply the final job stop policy.
+ // If the user attempted to stop the job/app, then always remove the notification
+ // so the user doesn't get confused about the app state.
+ // Similarly, if the user background restricted the app, remove the notification so
+ // the user doesn't think the app is continuing to run in the background.
+ if (details.jobEndNotificationPolicy == JOB_END_NOTIFICATION_POLICY_REMOVE
+ || stopReason == JobParameters.STOP_REASON_BACKGROUND_RESTRICTION
+ || stopReason == JobParameters.STOP_REASON_USER) {
+ mNotificationManagerInternal.cancelNotification(
+ packageName, packageName, details.appUid, details.appPid, /* tag */ null,
+ notificationId, userId);
+ stripUijFlag = false;
+ }
+ } else {
+ // Strip the UIJ flag only if there are no other UIJs associated with the notification
+ stripUijFlag = !isNotificationUsedForAnyUij(userId, packageName, notificationId);
+ }
+ if (stripUijFlag) {
+ mNotificationManagerInternal.removeUserInitiatedJobFlagFromNotification(
+ packageName, notificationId, userId);
+ }
+
+ // Clean up UIJ related objects if the just completed job was a UIJ
+ if (completedJob != null && completedJob.startedAsUserInitiatedJob) {
+ maybeDeleteNotificationIdAssociation(userId, packageName, notificationId);
+ maybeDeleteNotificationChannelAssociation(
+ userId, packageName, details.notificationChannel);
+ }
+ }
+
+ boolean isNotificationAssociatedWithAnyUserInitiatedJobs(int notificationId,
+ int userId, @NonNull String packageName) {
+ synchronized (mUijLock) {
+ final IntArray notifications = mUijNotifications.get(userId, packageName);
+ if (notifications != null) {
+ return notifications.indexOf(notificationId) != -1;
+ }
+ return false;
+ }
+ }
+
+ boolean isNotificationChannelAssociatedWithAnyUserInitiatedJobs(
+ @NonNull String notificationChannel, int userId, @NonNull String packageName) {
+ synchronized (mUijLock) {
+ final ArraySet<String> channels = mUijNotificationChannels.get(userId, packageName);
+ if (channels != null) {
+ return channels.contains(notificationChannel);
+ }
+ return false;
+ }
+ }
+
+ private boolean isNotificationUsedForAnyUij(int userId, String packageName,
+ int notificationId) {
+ final UserPackage pkgDetails = UserPackage.of(userId, packageName);
+ final SparseSetArray<JobServiceContext> associations = mCurrentAssociations.get(pkgDetails);
+ if (associations == null) {
+ return false;
+ }
+ final ArraySet<JobServiceContext> associatedContexts = associations.get(notificationId);
+ if (associatedContexts == null) {
+ return false;
+ }
+
+ // Check if any UIJs associated with this package are using the same notification
+ for (int i = associatedContexts.size() - 1; i >= 0; i--) {
+ final JobStatus jobStatus = associatedContexts.valueAt(i).getRunningJobLocked();
+ if (jobStatus != null && jobStatus.startedAsUserInitiatedJob) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void maybeDeleteNotificationIdAssociation(int userId, String packageName,
+ int notificationId) {
+ if (isNotificationUsedForAnyUij(userId, packageName, notificationId)) {
+ return;
+ }
+
+ // Safe to delete - no UIJs for this package are using this notification id
+ synchronized (mUijLock) {
+ final IntArray notifications = mUijNotifications.get(userId, packageName);
+ if (notifications != null) {
+ notifications.remove(notifications.indexOf(notificationId));
+ if (notifications.size() == 0) {
+ mUijNotifications.delete(userId, packageName);
+ }
+ }
+ }
+ }
+
+ private void maybeDeleteNotificationChannelAssociation(int userId, String packageName,
+ String notificationChannel) {
+ for (int i = mNotificationDetails.size() - 1; i >= 0; i--) {
+ final JobServiceContext jsc = mNotificationDetails.keyAt(i);
+ final NotificationDetails details = mNotificationDetails.get(jsc);
+ // Check if the details for the given notification match and if the associated job
+ // was started as a user initiated job
+ if (details != null
+ && UserHandle.getUserId(details.appUid) == userId
+ && details.userPackage.packageName.equals(packageName)
+ && details.notificationChannel.equals(notificationChannel)) {
+ final JobStatus jobStatus = jsc.getRunningJobLocked();
+ if (jobStatus != null && jobStatus.startedAsUserInitiatedJob) {
+ return;
+ }
+ }
+ }
+
+ // Safe to delete - no UIJs for this package are using this notification channel
+ synchronized (mUijLock) {
+ ArraySet<String> channels = mUijNotificationChannels.get(userId, packageName);
+ if (channels != null) {
+ channels.remove(notificationChannel);
+ if (channels.isEmpty()) {
+ mUijNotificationChannels.delete(userId, packageName);
+ }
+ }
+ }
+ }
+
+ @GuardedBy("mUijLock")
+ private void maybeCreateUijNotificationSetsLocked(int userId, String packageName) {
+ if (mUijNotifications.get(userId, packageName) == null) {
+ mUijNotifications.add(userId, packageName, new IntArray());
+ }
+ if (mUijNotificationChannels.get(userId, packageName) == null) {
+ mUijNotificationChannels.add(userId, packageName, new ArraySet<>());
+ }
+ }
+
+ private void validateNotification(@NonNull String packageName, int callingUid,
+ @NonNull Notification notification,
+ @JobService.JobEndNotificationPolicy int jobEndNotificationPolicy) {
+ if (notification == null) {
+ throw new NullPointerException("notification");
+ }
+ if (notification.getSmallIcon() == null) {
+ throw new IllegalArgumentException("small icon required");
+ }
+ if (null == mNotificationManagerInternal.getNotificationChannel(
+ packageName, callingUid, notification.getChannelId())) {
+ throw new IllegalArgumentException("invalid notification channel");
+ }
+ if (jobEndNotificationPolicy != JOB_END_NOTIFICATION_POLICY_DETACH
+ && jobEndNotificationPolicy != JOB_END_NOTIFICATION_POLICY_REMOVE) {
+ throw new IllegalArgumentException("invalid job end notification policy");
+ }
+ }
+}
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 c6fd0bffb95e..f99bcf144b91 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -16,11 +16,15 @@
package com.android.server.job;
+import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
+import static android.Manifest.permission.MANAGE_ACTIVITY_TASKS;
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER;
import static android.text.format.DateUtils.HOUR_IN_MILLIS;
import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
+import android.Manifest;
+import android.annotation.EnforcePermission;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
@@ -29,8 +33,10 @@ import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
import android.app.AppGlobals;
import android.app.IUidObserver;
+import android.app.UidObserver;
import android.app.compat.CompatChanges;
import android.app.job.IJobScheduler;
+import android.app.job.IUserVisibleJobObserver;
import android.app.job.JobInfo;
import android.app.job.JobParameters;
import android.app.job.JobProtoEnums;
@@ -38,13 +44,18 @@ import android.app.job.JobScheduler;
import android.app.job.JobService;
import android.app.job.JobSnapshot;
import android.app.job.JobWorkItem;
+import android.app.job.UserVisibleJobSummary;
+import android.app.tare.EconomyManager;
import android.app.usage.UsageStatsManager;
import android.app.usage.UsageStatsManagerInternal;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledAfter;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.content.PermissionChecker;
import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageManager;
import android.content.pm.PackageManager;
@@ -53,17 +64,20 @@ import android.content.pm.PackageManagerInternal;
import android.content.pm.ParceledListSlice;
import android.content.pm.ProviderInfo;
import android.content.pm.ServiceInfo;
+import android.net.Network;
import android.net.Uri;
import android.os.BatteryManager;
import android.os.BatteryManagerInternal;
import android.os.BatteryStatsInternal;
import android.os.Binder;
+import android.os.Build;
import android.os.Handler;
import android.os.LimitExceededException;
import android.os.Looper;
import android.os.Message;
import android.os.ParcelFileDescriptor;
import android.os.Process;
+import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.UserHandle;
@@ -76,8 +90,10 @@ import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.IndentingPrintWriter;
import android.util.Log;
+import android.util.Pair;
import android.util.Slog;
import android.util.SparseArray;
+import android.util.SparseArrayMap;
import android.util.SparseBooleanArray;
import android.util.SparseIntArray;
import android.util.SparseSetArray;
@@ -90,10 +106,12 @@ import com.android.internal.os.SomeArgs;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.DumpUtils;
import com.android.internal.util.FrameworkStatsLog;
+import com.android.modules.expresslog.Counter;
+import com.android.modules.expresslog.Histogram;
+import com.android.server.AppSchedulingModuleThread;
import com.android.server.AppStateTracker;
import com.android.server.AppStateTrackerImpl;
import com.android.server.DeviceIdleInternal;
-import com.android.server.JobSchedulerBackgroundThread;
import com.android.server.LocalServices;
import com.android.server.job.JobSchedulerServiceDumpProto.PendingJob;
import com.android.server.job.controllers.BackgroundJobsController;
@@ -102,6 +120,7 @@ import com.android.server.job.controllers.ComponentController;
import com.android.server.job.controllers.ConnectivityController;
import com.android.server.job.controllers.ContentObserverController;
import com.android.server.job.controllers.DeviceIdleJobsController;
+import com.android.server.job.controllers.FlexibilityController;
import com.android.server.job.controllers.IdleController;
import com.android.server.job.controllers.JobStatus;
import com.android.server.job.controllers.PrefetchController;
@@ -115,6 +134,7 @@ import com.android.server.job.restrictions.JobRestriction;
import com.android.server.job.restrictions.ThermalStatusRestriction;
import com.android.server.pm.UserManagerInternal;
import com.android.server.tare.EconomyManagerInternal;
+import com.android.server.tare.JobSchedulerEconomicPolicy;
import com.android.server.usage.AppStandbyInternal;
import com.android.server.usage.AppStandbyInternal.AppIdleStateChangeListener;
import com.android.server.utils.quota.Categorizer;
@@ -136,7 +156,9 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
+import java.util.concurrent.CountDownLatch;
import java.util.function.Consumer;
import java.util.function.Predicate;
@@ -164,7 +186,23 @@ public class JobSchedulerService extends com.android.server.SystemService
/** The number of the most recently completed jobs to keep track of for debugging purposes. */
private static final int NUM_COMPLETED_JOB_HISTORY = 20;
- @VisibleForTesting
+ /**
+ * Require the hosting job to specify a network constraint if the included
+ * {@link android.app.job.JobWorkItem} indicates network usage.
+ */
+ @ChangeId
+ @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.TIRAMISU)
+ private static final long REQUIRE_NETWORK_CONSTRAINT_FOR_NETWORK_JOB_WORK_ITEMS = 241104082L;
+
+ /**
+ * Require the app to have the ACCESS_NETWORK_STATE permissions when scheduling
+ * a job with a connectivity constraint.
+ */
+ @ChangeId
+ @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.TIRAMISU)
+ static final long REQUIRE_NETWORK_PERMISSIONS_FOR_CONNECTIVITY_JOBS = 271850009L;
+
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public static Clock sSystemClock = Clock.systemUTC();
private abstract static class MySimpleClock extends Clock {
@@ -198,7 +236,7 @@ public class JobSchedulerService extends com.android.server.SystemService
}
}
- @VisibleForTesting
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public static Clock sUptimeMillisClock = new MySimpleClock(ZoneOffset.UTC) {
@Override
public long millis() {
@@ -206,7 +244,6 @@ public class JobSchedulerService extends com.android.server.SystemService
}
};
- @VisibleForTesting
public static Clock sElapsedRealtimeClock = new MySimpleClock(ZoneOffset.UTC) {
@Override
public long millis() {
@@ -218,6 +255,7 @@ public class JobSchedulerService extends com.android.server.SystemService
final Object mLock = new Object();
/** Master list of jobs. */
final JobStore mJobs;
+ private final CountDownLatch mJobStoreLoadedLatch;
/** Tracking the standby bucket state of each app */
final StandbyTracker mStandbyTracker;
/** Tracking amount of time each package runs for. */
@@ -234,6 +272,8 @@ public class JobSchedulerService extends com.android.server.SystemService
static final int MSG_UID_IDLE = 7;
static final int MSG_CHECK_CHANGED_JOB_LIST = 8;
static final int MSG_CHECK_MEDIA_EXEMPTION = 9;
+ static final int MSG_INFORM_OBSERVER_OF_ALL_USER_VISIBLE_JOBS = 10;
+ static final int MSG_INFORM_OBSERVERS_OF_USER_VISIBLE_JOB_CHANGE = 11;
/** List of controllers that will notify this service of updates to jobs. */
final List<StateController> mControllers;
@@ -244,6 +284,8 @@ public class JobSchedulerService extends com.android.server.SystemService
private final List<RestrictingController> mRestrictiveControllers;
/** Need direct access to this for testing. */
private final StorageController mStorageController;
+ /** Needed to get estimated transfer time. */
+ private final ConnectivityController mConnectivityController;
/** Need directly for sending uid state changes */
private final DeviceIdleJobsController mDeviceIdleJobsController;
/** Needed to get next estimated launch time. */
@@ -265,20 +307,40 @@ public class JobSchedulerService extends com.android.server.SystemService
@GuardedBy("mLock")
private final SparseArray<String> mCloudMediaProviderPackages = new SparseArray<>();
+ private final RemoteCallbackList<IUserVisibleJobObserver> mUserVisibleJobObservers =
+ new RemoteCallbackList<>();
+
+ /**
+ * Cache of grant status of permissions, keyed by UID->PID->permission name. A missing value
+ * means the state has not been queried.
+ */
+ @GuardedBy("mPermissionCache")
+ private final SparseArray<SparseArrayMap<String, Boolean>> mPermissionCache =
+ new SparseArray<>();
+
private final CountQuotaTracker mQuotaTracker;
private static final String QUOTA_TRACKER_SCHEDULE_PERSISTED_TAG = ".schedulePersisted()";
private static final String QUOTA_TRACKER_SCHEDULE_LOGGED =
".schedulePersisted out-of-quota logged";
+ private static final String QUOTA_TRACKER_TIMEOUT_UIJ_TAG = "timeout-uij";
+ private static final String QUOTA_TRACKER_TIMEOUT_EJ_TAG = "timeout-ej";
+ private static final String QUOTA_TRACKER_TIMEOUT_REG_TAG = "timeout-reg";
+ private static final String QUOTA_TRACKER_TIMEOUT_TOTAL_TAG = "timeout-total";
+ private static final String QUOTA_TRACKER_ANR_TAG = "anr";
private static final Category QUOTA_TRACKER_CATEGORY_SCHEDULE_PERSISTED = new Category(
".schedulePersisted()");
private static final Category QUOTA_TRACKER_CATEGORY_SCHEDULE_LOGGED = new Category(
".schedulePersisted out-of-quota logged");
- private static final Categorizer QUOTA_CATEGORIZER = (userId, packageName, tag) -> {
- if (QUOTA_TRACKER_SCHEDULE_PERSISTED_TAG.equals(tag)) {
- return QUOTA_TRACKER_CATEGORY_SCHEDULE_PERSISTED;
- }
- return QUOTA_TRACKER_CATEGORY_SCHEDULE_LOGGED;
- };
+ private static final Category QUOTA_TRACKER_CATEGORY_TIMEOUT_UIJ =
+ new Category(QUOTA_TRACKER_TIMEOUT_UIJ_TAG);
+ private static final Category QUOTA_TRACKER_CATEGORY_TIMEOUT_EJ =
+ new Category(QUOTA_TRACKER_TIMEOUT_EJ_TAG);
+ private static final Category QUOTA_TRACKER_CATEGORY_TIMEOUT_REG =
+ new Category(QUOTA_TRACKER_TIMEOUT_REG_TAG);
+ private static final Category QUOTA_TRACKER_CATEGORY_TIMEOUT_TOTAL =
+ new Category(QUOTA_TRACKER_TIMEOUT_TOTAL_TAG);
+ private static final Category QUOTA_TRACKER_CATEGORY_ANR = new Category(QUOTA_TRACKER_ANR_TAG);
+ private static final Category QUOTA_TRACKER_CATEGORY_DISABLED = new Category("disabled");
/**
* Queue of pending jobs. The JobServiceContext class will receive jobs from this list
@@ -309,14 +371,60 @@ public class JobSchedulerService extends com.android.server.SystemService
*/
boolean mReportedActive;
+ /**
+ * Track the most recently completed jobs (that had been executing and were stopped for any
+ * reason, including successful completion).
+ */
private int mLastCompletedJobIndex = 0;
private final JobStatus[] mLastCompletedJobs = new JobStatus[NUM_COMPLETED_JOB_HISTORY];
private final long[] mLastCompletedJobTimeElapsed = new long[NUM_COMPLETED_JOB_HISTORY];
/**
+ * Track the most recently cancelled jobs (that had internal reason
+ * {@link JobParameters#INTERNAL_STOP_REASON_CANCELED}.
+ */
+ private int mLastCancelledJobIndex = 0;
+ private final JobStatus[] mLastCancelledJobs =
+ new JobStatus[DEBUG ? NUM_COMPLETED_JOB_HISTORY : 0];
+ private final long[] mLastCancelledJobTimeElapsed =
+ new long[DEBUG ? NUM_COMPLETED_JOB_HISTORY : 0];
+
+ private static final Histogram sEnqueuedJwiHighWaterMarkLogger = new Histogram(
+ "job_scheduler.value_hist_w_uid_enqueued_work_items_high_water_mark",
+ new Histogram.ScaledRangeOptions(25, 0, 5, 1.4f));
+ private static final Histogram sInitialJobEstimatedNetworkDownloadKBLogger = new Histogram(
+ "job_scheduler.value_hist_initial_job_estimated_network_download_kilobytes",
+ new Histogram.ScaledRangeOptions(50, 0, 32 /* 32 KB */, 1.31f));
+ private static final Histogram sInitialJwiEstimatedNetworkDownloadKBLogger = new Histogram(
+ "job_scheduler.value_hist_initial_jwi_estimated_network_download_kilobytes",
+ new Histogram.ScaledRangeOptions(50, 0, 32 /* 32 KB */, 1.31f));
+ private static final Histogram sInitialJobEstimatedNetworkUploadKBLogger = new Histogram(
+ "job_scheduler.value_hist_initial_job_estimated_network_upload_kilobytes",
+ new Histogram.ScaledRangeOptions(50, 0, 32 /* 32 KB */, 1.31f));
+ private static final Histogram sInitialJwiEstimatedNetworkUploadKBLogger = new Histogram(
+ "job_scheduler.value_hist_initial_jwi_estimated_network_upload_kilobytes",
+ new Histogram.ScaledRangeOptions(50, 0, 32 /* 32 KB */, 1.31f));
+ private static final Histogram sJobMinimumChunkKBLogger = new Histogram(
+ "job_scheduler.value_hist_w_uid_job_minimum_chunk_kilobytes",
+ new Histogram.ScaledRangeOptions(25, 0, 5 /* 5 KB */, 1.76f));
+ private static final Histogram sJwiMinimumChunkKBLogger = new Histogram(
+ "job_scheduler.value_hist_w_uid_jwi_minimum_chunk_kilobytes",
+ new Histogram.ScaledRangeOptions(25, 0, 5 /* 5 KB */, 1.76f));
+
+ /**
* A mapping of which uids are currently in the foreground to their effective bias.
*/
final SparseIntArray mUidBiasOverride = new SparseIntArray();
+ /**
+ * A cached mapping of uids to their current capabilities.
+ */
+ @GuardedBy("mLock")
+ private final SparseIntArray mUidCapabilities = new SparseIntArray();
+ /**
+ * A cached mapping of uids to their proc states.
+ */
+ @GuardedBy("mLock")
+ private final SparseIntArray mUidProcStates = new SparseIntArray();
/**
* Which uids are currently performing backups, so we shouldn't allow their jobs to run.
@@ -336,6 +444,13 @@ public class JobSchedulerService extends com.android.server.SystemService
private final ArraySet<JobStatus> mChangedJobList = new ArraySet<>();
/**
+ * Cached pending job reasons. Mapping from UID -> namespace -> job ID -> reason.
+ */
+ @GuardedBy("mPendingJobReasonCache") // Use its own lock to avoid blocking JS processing
+ private final SparseArrayMap<String, SparseIntArray> mPendingJobReasonCache =
+ new SparseArrayMap<>();
+
+ /**
* Named indices into standby bucket arrays, for clarity in referring to
* specific buckets' bookkeeping.
*/
@@ -355,13 +470,16 @@ public class JobSchedulerService extends com.android.server.SystemService
EconomyManagerInternal.TareStateChangeListener {
public void start() {
DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_JOB_SCHEDULER,
- JobSchedulerBackgroundThread.getExecutor(), this);
+ AppSchedulingModuleThread.getExecutor(), this);
final EconomyManagerInternal economyManagerInternal =
LocalServices.getService(EconomyManagerInternal.class);
- economyManagerInternal.registerTareStateChangeListener(this);
+ economyManagerInternal
+ .registerTareStateChangeListener(this, JobSchedulerEconomicPolicy.POLICY_JOB);
// Load all the constants.
synchronized (mLock) {
- mConstants.updateTareSettingsLocked(economyManagerInternal.isEnabled());
+ mConstants.updateTareSettingsLocked(
+ economyManagerInternal.getEnabledMode(
+ JobSchedulerEconomicPolicy.POLICY_JOB));
}
onPropertiesChanged(DeviceConfig.getProperties(DeviceConfig.NAMESPACE_JOB_SCHEDULER));
}
@@ -370,6 +488,7 @@ public class JobSchedulerService extends com.android.server.SystemService
public void onPropertiesChanged(DeviceConfig.Properties properties) {
boolean apiQuotaScheduleUpdated = false;
boolean concurrencyUpdated = false;
+ boolean persistenceUpdated = false;
boolean runtimeUpdated = false;
for (int controller = 0; controller < mControllers.size(); controller++) {
final StateController sc = mControllers.get(controller);
@@ -383,10 +502,18 @@ public class JobSchedulerService extends com.android.server.SystemService
}
switch (name) {
case Constants.KEY_ENABLE_API_QUOTAS:
+ case Constants.KEY_ENABLE_EXECUTION_SAFEGUARDS_UDC:
case Constants.KEY_API_QUOTA_SCHEDULE_COUNT:
case Constants.KEY_API_QUOTA_SCHEDULE_WINDOW_MS:
case Constants.KEY_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT:
case Constants.KEY_API_QUOTA_SCHEDULE_THROW_EXCEPTION:
+ case Constants.KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT:
+ case Constants.KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT:
+ case Constants.KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT:
+ case Constants.KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_TOTAL_COUNT:
+ case Constants.KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS:
+ case Constants.KEY_EXECUTION_SAFEGUARDS_UDC_ANR_COUNT:
+ case Constants.KEY_EXECUTION_SAFEGUARDS_UDC_ANR_WINDOW_MS:
if (!apiQuotaScheduleUpdated) {
mConstants.updateApiQuotaConstantsLocked();
updateQuotaTracker();
@@ -403,6 +530,7 @@ public class JobSchedulerService extends com.android.server.SystemService
break;
case Constants.KEY_MIN_LINEAR_BACKOFF_TIME_MS:
case Constants.KEY_MIN_EXP_BACKOFF_TIME_MS:
+ case Constants.KEY_SYSTEM_STOP_TO_FAILURE_RATIO:
mConstants.updateBackoffConstantsLocked();
break;
case Constants.KEY_CONN_CONGESTION_DELAY_FRAC:
@@ -418,12 +546,25 @@ public class JobSchedulerService extends com.android.server.SystemService
case Constants.KEY_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS:
case Constants.KEY_RUNTIME_MIN_GUARANTEE_MS:
case Constants.KEY_RUNTIME_MIN_EJ_GUARANTEE_MS:
- case Constants.KEY_RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS:
+ case Constants.KEY_RUNTIME_MIN_UI_GUARANTEE_MS:
+ case Constants.KEY_RUNTIME_UI_LIMIT_MS:
+ case Constants.KEY_RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR:
+ case Constants.KEY_RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS:
+ case Constants.KEY_RUNTIME_CUMULATIVE_UI_LIMIT_MS:
+ case Constants.KEY_RUNTIME_USE_DATA_ESTIMATES_FOR_LIMITS:
if (!runtimeUpdated) {
mConstants.updateRuntimeConstantsLocked();
runtimeUpdated = true;
}
break;
+ case Constants.KEY_MAX_NUM_PERSISTED_JOB_WORK_ITEMS:
+ case Constants.KEY_PERSIST_IN_SPLIT_FILES:
+ if (!persistenceUpdated) {
+ mConstants.updatePersistingConstantsLocked();
+ mJobs.setUseSplitFiles(mConstants.PERSIST_IN_SPLIT_FILES);
+ persistenceUpdated = true;
+ }
+ break;
default:
if (name.startsWith(JobConcurrencyManager.CONFIG_KEY_PREFIX_CONCURRENCY)
&& !concurrencyUpdated) {
@@ -446,8 +587,8 @@ public class JobSchedulerService extends com.android.server.SystemService
}
@Override
- public void onTareEnabledStateChanged(boolean isTareEnabled) {
- if (mConstants.updateTareSettingsLocked(isTareEnabled)) {
+ public void onTareEnabledModeChanged(@EconomyManager.EnabledMode int enabledMode) {
+ if (mConstants.updateTareSettingsLocked(enabledMode)) {
for (int controller = 0; controller < mControllers.size(); controller++) {
final StateController sc = mControllers.get(controller);
sc.onConstantsUpdatedLocked();
@@ -459,10 +600,26 @@ public class JobSchedulerService extends com.android.server.SystemService
@VisibleForTesting
void updateQuotaTracker() {
- mQuotaTracker.setEnabled(mConstants.ENABLE_API_QUOTAS);
+ mQuotaTracker.setEnabled(
+ mConstants.ENABLE_API_QUOTAS || mConstants.ENABLE_EXECUTION_SAFEGUARDS_UDC);
mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_SCHEDULE_PERSISTED,
mConstants.API_QUOTA_SCHEDULE_COUNT,
mConstants.API_QUOTA_SCHEDULE_WINDOW_MS);
+ mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_TIMEOUT_UIJ,
+ mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT,
+ mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS);
+ mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_TIMEOUT_EJ,
+ mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT,
+ mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS);
+ mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_TIMEOUT_REG,
+ mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT,
+ mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS);
+ mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_TIMEOUT_TOTAL,
+ mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_TOTAL_COUNT,
+ mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS);
+ mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_ANR,
+ mConstants.EXECUTION_SAFEGUARDS_UDC_ANR_COUNT,
+ mConstants.EXECUTION_SAFEGUARDS_UDC_ANR_WINDOW_MS);
}
/**
@@ -480,6 +637,8 @@ public class JobSchedulerService extends com.android.server.SystemService
private static final String KEY_MIN_LINEAR_BACKOFF_TIME_MS = "min_linear_backoff_time_ms";
private static final String KEY_MIN_EXP_BACKOFF_TIME_MS = "min_exp_backoff_time_ms";
+ private static final String KEY_SYSTEM_STOP_TO_FAILURE_RATIO =
+ "system_stop_to_failure_ratio";
private static final String KEY_CONN_CONGESTION_DELAY_FRAC = "conn_congestion_delay_frac";
private static final String KEY_CONN_PREFETCH_RELAX_FRAC = "conn_prefetch_relax_frac";
private static final String KEY_CONN_USE_CELL_SIGNAL_STRENGTH =
@@ -490,6 +649,8 @@ public class JobSchedulerService extends com.android.server.SystemService
"conn_low_signal_strength_relax_frac";
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.
+ // TODO(141645789): remove this flag
private static final String KEY_ENABLE_API_QUOTAS = "enable_api_quotas";
private static final String KEY_API_QUOTA_SCHEDULE_COUNT = "aq_schedule_count";
private static final String KEY_API_QUOTA_SCHEDULE_WINDOW_MS = "aq_schedule_window_ms";
@@ -497,13 +658,42 @@ public class JobSchedulerService extends com.android.server.SystemService
"aq_schedule_throw_exception";
private static final String KEY_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT =
"aq_schedule_return_failure";
+ private static final String KEY_ENABLE_EXECUTION_SAFEGUARDS_UDC =
+ "enable_execution_safeguards_udc";
+ private static final String KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT =
+ "es_u_timeout_uij_count";
+ private static final String KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT =
+ "es_u_timeout_ej_count";
+ private static final String KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT =
+ "es_u_timeout_reg_count";
+ private static final String KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_TOTAL_COUNT =
+ "es_u_timeout_total_count";
+ private static final String KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS =
+ "es_u_timeout_window_ms";
+ private static final String KEY_EXECUTION_SAFEGUARDS_UDC_ANR_COUNT =
+ "es_u_anr_count";
+ private static final String KEY_EXECUTION_SAFEGUARDS_UDC_ANR_WINDOW_MS =
+ "es_u_anr_window_ms";
private static final String KEY_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS =
"runtime_free_quota_max_limit_ms";
private static final String KEY_RUNTIME_MIN_GUARANTEE_MS = "runtime_min_guarantee_ms";
private static final String KEY_RUNTIME_MIN_EJ_GUARANTEE_MS = "runtime_min_ej_guarantee_ms";
- private static final String KEY_RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS =
- "runtime_min_high_priority_guarantee_ms";
+ private static final String KEY_RUNTIME_MIN_UI_GUARANTEE_MS = "runtime_min_ui_guarantee_ms";
+ private static final String KEY_RUNTIME_UI_LIMIT_MS = "runtime_ui_limit_ms";
+ private static final String KEY_RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR =
+ "runtime_min_ui_data_transfer_guarantee_buffer_factor";
+ private static final String KEY_RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS =
+ "runtime_min_ui_data_transfer_guarantee_ms";
+ private static final String KEY_RUNTIME_CUMULATIVE_UI_LIMIT_MS =
+ "runtime_cumulative_ui_limit_ms";
+ private static final String KEY_RUNTIME_USE_DATA_ESTIMATES_FOR_LIMITS =
+ "runtime_use_data_estimates_for_limits";
+
+ private static final String KEY_PERSIST_IN_SPLIT_FILES = "persist_in_split_files";
+
+ 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 long DEFAULT_MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS = 31 * MINUTE_IN_MILLIS;
@@ -511,6 +701,7 @@ public class JobSchedulerService extends com.android.server.SystemService
private static final float DEFAULT_MODERATE_USE_FACTOR = .5f;
private static final long DEFAULT_MIN_LINEAR_BACKOFF_TIME_MS = JobInfo.MIN_BACKOFF_MILLIS;
private static final long DEFAULT_MIN_EXP_BACKOFF_TIME_MS = JobInfo.MIN_BACKOFF_MILLIS;
+ private static final int DEFAULT_SYSTEM_STOP_TO_FAILURE_RATIO = 3;
private static final float DEFAULT_CONN_CONGESTION_DELAY_FRAC = 0.5f;
private static final float DEFAULT_CONN_PREFETCH_RELAX_FRAC = 0.5f;
private static final boolean DEFAULT_CONN_USE_CELL_SIGNAL_STRENGTH = true;
@@ -522,15 +713,35 @@ public class JobSchedulerService extends com.android.server.SystemService
private static final long DEFAULT_API_QUOTA_SCHEDULE_WINDOW_MS = MINUTE_IN_MILLIS;
private static final boolean DEFAULT_API_QUOTA_SCHEDULE_THROW_EXCEPTION = true;
private static final boolean DEFAULT_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = false;
+ private static final boolean DEFAULT_ENABLE_EXECUTION_SAFEGUARDS_UDC = true;
+ private static final int DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT = 2;
+ // EJs have a shorter timeout, so set a higher limit for them to start with.
+ private static final int DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT = 5;
+ private static final int DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT = 3;
+ private static final int DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_TOTAL_COUNT = 10;
+ private static final long DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS =
+ 24 * HOUR_IN_MILLIS;
+ private static final int DEFAULT_EXECUTION_SAFEGUARDS_UDC_ANR_COUNT = 3;
+ private static final long DEFAULT_EXECUTION_SAFEGUARDS_UDC_ANR_WINDOW_MS =
+ 6 * HOUR_IN_MILLIS;
@VisibleForTesting
public static final long DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS = 30 * MINUTE_IN_MILLIS;
@VisibleForTesting
public static final long DEFAULT_RUNTIME_MIN_GUARANTEE_MS = 10 * MINUTE_IN_MILLIS;
@VisibleForTesting
public static final long DEFAULT_RUNTIME_MIN_EJ_GUARANTEE_MS = 3 * MINUTE_IN_MILLIS;
- @VisibleForTesting
- static final long DEFAULT_RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS = 5 * MINUTE_IN_MILLIS;
- private static final boolean DEFAULT_USE_TARE_POLICY = false;
+ public static final long DEFAULT_RUNTIME_MIN_UI_GUARANTEE_MS =
+ Math.max(6 * HOUR_IN_MILLIS, DEFAULT_RUNTIME_MIN_GUARANTEE_MS);
+ public static final long DEFAULT_RUNTIME_UI_LIMIT_MS =
+ Math.max(12 * HOUR_IN_MILLIS, DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS);
+ public static final float DEFAULT_RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR =
+ 1.35f;
+ public static final long DEFAULT_RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS =
+ Math.max(10 * MINUTE_IN_MILLIS, DEFAULT_RUNTIME_MIN_UI_GUARANTEE_MS);
+ public static final long DEFAULT_RUNTIME_CUMULATIVE_UI_LIMIT_MS = 24 * HOUR_IN_MILLIS;
+ public static final boolean DEFAULT_RUNTIME_USE_DATA_ESTIMATES_FOR_LIMITS = false;
+ static final boolean DEFAULT_PERSIST_IN_SPLIT_FILES = true;
+ 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.
@@ -560,6 +771,11 @@ public class JobSchedulerService extends com.android.server.SystemService
* The minimum backoff time to allow for exponential backoff.
*/
long MIN_EXP_BACKOFF_TIME_MS = DEFAULT_MIN_EXP_BACKOFF_TIME_MS;
+ /**
+ * The ratio to use to convert number of times a job was stopped by JobScheduler to an
+ * incremental failure in the backoff policy calculation.
+ */
+ int SYSTEM_STOP_TO_FAILURE_RATIO = DEFAULT_SYSTEM_STOP_TO_FAILURE_RATIO;
/**
* The fraction of a job's running window that must pass before we
@@ -620,6 +836,55 @@ public class JobSchedulerService extends com.android.server.SystemService
public boolean API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT =
DEFAULT_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT;
+ /**
+ * Whether to enable the execution safeguards added in UDC.
+ */
+ public boolean ENABLE_EXECUTION_SAFEGUARDS_UDC = DEFAULT_ENABLE_EXECUTION_SAFEGUARDS_UDC;
+ /**
+ * The maximum number of times an app can have a user-iniated job time out before the system
+ * begins removing some of the app's privileges.
+ */
+ public int EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT =
+ DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT;
+ /**
+ * The maximum number of times an app can have an expedited job time out before the system
+ * begins removing some of the app's privileges.
+ */
+ public int EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT =
+ DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT;
+ /**
+ * The maximum number of times an app can have a regular job time out before the system
+ * begins removing some of the app's privileges.
+ */
+ public int EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT =
+ DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT;
+ /**
+ * The maximum number of times an app can have jobs time out before the system
+ * attempts to restrict most of the app's privileges.
+ */
+ public int EXECUTION_SAFEGUARDS_UDC_TIMEOUT_TOTAL_COUNT =
+ DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_TOTAL_COUNT;
+ /**
+ * The time window that {@link #EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT},
+ * {@link #EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT},
+ * {@link #EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT}, and
+ * {@link #EXECUTION_SAFEGUARDS_UDC_TIMEOUT_TOTAL_COUNT} should be evaluated over.
+ */
+ public long EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS =
+ DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS;
+
+ /**
+ * The maximum number of times an app can ANR from JobScheduler's perspective before
+ * JobScheduler will attempt to restrict the app.
+ */
+ public int EXECUTION_SAFEGUARDS_UDC_ANR_COUNT = DEFAULT_EXECUTION_SAFEGUARDS_UDC_ANR_COUNT;
+ /**
+ * The time window that {@link #EXECUTION_SAFEGUARDS_UDC_ANR_COUNT}
+ * should be evaluated over.
+ */
+ public long EXECUTION_SAFEGUARDS_UDC_ANR_WINDOW_MS =
+ DEFAULT_EXECUTION_SAFEGUARDS_UDC_ANR_WINDOW_MS;
+
/** The maximum amount of time we will let a job run for when quota is "free". */
public long RUNTIME_FREE_QUOTA_MAX_LIMIT_MS = DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS;
@@ -634,15 +899,60 @@ public class JobSchedulerService extends com.android.server.SystemService
public long RUNTIME_MIN_EJ_GUARANTEE_MS = DEFAULT_RUNTIME_MIN_EJ_GUARANTEE_MS;
/**
- * The minimum amount of time we try to guarantee high priority jobs will run for.
+ * The minimum amount of time we try to guarantee normal user-initiated jobs will run for.
+ */
+ public long RUNTIME_MIN_UI_GUARANTEE_MS = DEFAULT_RUNTIME_MIN_UI_GUARANTEE_MS;
+
+ /**
+ * The maximum amount of time we will let a user-initiated job run for. This will only
+ * apply if there are no other limits that apply to the specific user-initiated job.
+ */
+ public long RUNTIME_UI_LIMIT_MS = DEFAULT_RUNTIME_UI_LIMIT_MS;
+
+ /**
+ * A factor to apply to estimated transfer durations for user-initiated data transfer jobs
+ * so that we give some extra time for unexpected situations. This will be at least 1 and
+ * so can just be multiplied with the original value to get the final value.
+ */
+ public float RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR =
+ DEFAULT_RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR;
+
+ /**
+ * The minimum amount of time we try to guarantee user-initiated data transfer jobs
+ * will run for. This is only considered when using data estimates to calculate
+ * execution limits.
+ */
+ public long RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS =
+ DEFAULT_RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS;
+
+ /**
+ * The maximum amount of cumulative time we will let a user-initiated job run for
+ * before downgrading it.
+ */
+ public long RUNTIME_CUMULATIVE_UI_LIMIT_MS = DEFAULT_RUNTIME_CUMULATIVE_UI_LIMIT_MS;
+
+ /**
+ * Whether to use data estimates to determine execution limits for execution limits.
*/
- public long RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS =
- DEFAULT_RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS;
+ public boolean RUNTIME_USE_DATA_ESTIMATES_FOR_LIMITS =
+ DEFAULT_RUNTIME_USE_DATA_ESTIMATES_FOR_LIMITS;
+
+ /**
+ * Whether to persist jobs in split files (by UID). If false, all persisted jobs will be
+ * saved in a single file.
+ */
+ public boolean PERSIST_IN_SPLIT_FILES = DEFAULT_PERSIST_IN_SPLIT_FILES;
+
+ /**
+ * The maximum number of {@link JobWorkItem JobWorkItems} that can be persisted per job.
+ */
+ public int MAX_NUM_PERSISTED_JOB_WORK_ITEMS = DEFAULT_MAX_NUM_PERSISTED_JOB_WORK_ITEMS;
/**
* If true, use TARE policy for job limiting. If false, use quotas.
*/
- public boolean USE_TARE_POLICY = DEFAULT_USE_TARE_POLICY;
+ public boolean USE_TARE_POLICY = EconomyManager.DEFAULT_ENABLE_POLICY_JOB_SCHEDULER
+ && EconomyManager.DEFAULT_ENABLE_TARE_MODE == EconomyManager.ENABLED_MODE_ON;
private void updateBatchingConstantsLocked() {
MIN_READY_NON_ACTIVE_JOBS_COUNT = DeviceConfig.getInt(
@@ -671,6 +981,9 @@ public class JobSchedulerService extends com.android.server.SystemService
MIN_EXP_BACKOFF_TIME_MS = DeviceConfig.getLong(DeviceConfig.NAMESPACE_JOB_SCHEDULER,
KEY_MIN_EXP_BACKOFF_TIME_MS,
DEFAULT_MIN_EXP_BACKOFF_TIME_MS);
+ SYSTEM_STOP_TO_FAILURE_RATIO = DeviceConfig.getInt(DeviceConfig.NAMESPACE_JOB_SCHEDULER,
+ KEY_SYSTEM_STOP_TO_FAILURE_RATIO,
+ DEFAULT_SYSTEM_STOP_TO_FAILURE_RATIO);
}
private void updateConnectivityConstantsLocked() {
@@ -694,6 +1007,15 @@ public class JobSchedulerService extends com.android.server.SystemService
DEFAULT_CONN_LOW_SIGNAL_STRENGTH_RELAX_FRAC);
}
+ private void updatePersistingConstantsLocked() {
+ PERSIST_IN_SPLIT_FILES = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_JOB_SCHEDULER,
+ KEY_PERSIST_IN_SPLIT_FILES, DEFAULT_PERSIST_IN_SPLIT_FILES);
+ MAX_NUM_PERSISTED_JOB_WORK_ITEMS = DeviceConfig.getInt(
+ DeviceConfig.NAMESPACE_JOB_SCHEDULER,
+ KEY_MAX_NUM_PERSISTED_JOB_WORK_ITEMS,
+ DEFAULT_MAX_NUM_PERSISTED_JOB_WORK_ITEMS);
+ }
+
private void updatePrefetchConstantsLocked() {
PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS = DeviceConfig.getLong(
DeviceConfig.NAMESPACE_JOB_SCHEDULER,
@@ -704,6 +1026,9 @@ public class JobSchedulerService extends com.android.server.SystemService
private void updateApiQuotaConstantsLocked() {
ENABLE_API_QUOTAS = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_JOB_SCHEDULER,
KEY_ENABLE_API_QUOTAS, DEFAULT_ENABLE_API_QUOTAS);
+ ENABLE_EXECUTION_SAFEGUARDS_UDC = DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_JOB_SCHEDULER,
+ KEY_ENABLE_EXECUTION_SAFEGUARDS_UDC, DEFAULT_ENABLE_EXECUTION_SAFEGUARDS_UDC);
// Set a minimum value on the quota limit so it's not so low that it interferes with
// legitimate use cases.
API_QUOTA_SCHEDULE_COUNT = Math.max(250,
@@ -720,6 +1045,40 @@ public class JobSchedulerService extends com.android.server.SystemService
DeviceConfig.NAMESPACE_JOB_SCHEDULER,
KEY_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT,
DEFAULT_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT);
+
+ // Set a minimum value on the timeout limit so it's not so low that it interferes with
+ // legitimate use cases.
+ EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT = Math.max(2,
+ DeviceConfig.getInt(DeviceConfig.NAMESPACE_JOB_SCHEDULER,
+ KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT,
+ DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT));
+ EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT = Math.max(2,
+ DeviceConfig.getInt(DeviceConfig.NAMESPACE_JOB_SCHEDULER,
+ KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT,
+ DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT));
+ EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT = Math.max(2,
+ DeviceConfig.getInt(DeviceConfig.NAMESPACE_JOB_SCHEDULER,
+ KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT,
+ DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT));
+ final int highestTimeoutCount = Math.max(EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT,
+ Math.max(EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT,
+ EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT));
+ EXECUTION_SAFEGUARDS_UDC_TIMEOUT_TOTAL_COUNT = Math.max(highestTimeoutCount,
+ DeviceConfig.getInt(DeviceConfig.NAMESPACE_JOB_SCHEDULER,
+ KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_TOTAL_COUNT,
+ DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_TOTAL_COUNT));
+ EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS = DeviceConfig.getLong(
+ DeviceConfig.NAMESPACE_JOB_SCHEDULER,
+ KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS,
+ DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS);
+ EXECUTION_SAFEGUARDS_UDC_ANR_COUNT = Math.max(1,
+ DeviceConfig.getInt(DeviceConfig.NAMESPACE_JOB_SCHEDULER,
+ KEY_EXECUTION_SAFEGUARDS_UDC_ANR_COUNT,
+ DEFAULT_EXECUTION_SAFEGUARDS_UDC_ANR_COUNT));
+ EXECUTION_SAFEGUARDS_UDC_ANR_WINDOW_MS = DeviceConfig.getLong(
+ DeviceConfig.NAMESPACE_JOB_SCHEDULER,
+ KEY_EXECUTION_SAFEGUARDS_UDC_ANR_WINDOW_MS,
+ DEFAULT_EXECUTION_SAFEGUARDS_UDC_ANR_WINDOW_MS);
}
private void updateRuntimeConstantsLocked() {
@@ -727,17 +1086,17 @@ public class JobSchedulerService extends com.android.server.SystemService
DeviceConfig.NAMESPACE_JOB_SCHEDULER,
KEY_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS,
KEY_RUNTIME_MIN_GUARANTEE_MS, KEY_RUNTIME_MIN_EJ_GUARANTEE_MS,
- KEY_RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS);
+ KEY_RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR,
+ KEY_RUNTIME_MIN_UI_GUARANTEE_MS,
+ KEY_RUNTIME_UI_LIMIT_MS,
+ KEY_RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS,
+ KEY_RUNTIME_CUMULATIVE_UI_LIMIT_MS,
+ KEY_RUNTIME_USE_DATA_ESTIMATES_FOR_LIMITS);
// Make sure min runtime for regular jobs is at least 10 minutes.
RUNTIME_MIN_GUARANTEE_MS = Math.max(10 * MINUTE_IN_MILLIS,
properties.getLong(
KEY_RUNTIME_MIN_GUARANTEE_MS, DEFAULT_RUNTIME_MIN_GUARANTEE_MS));
- // Make sure min runtime for high priority jobs is at least 4 minutes.
- RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS = Math.max(4 * MINUTE_IN_MILLIS,
- properties.getLong(
- KEY_RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS,
- DEFAULT_RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS));
// Make sure min runtime for expedited jobs is at least one minute.
RUNTIME_MIN_EJ_GUARANTEE_MS = Math.max(MINUTE_IN_MILLIS,
properties.getLong(
@@ -745,12 +1104,43 @@ public class JobSchedulerService extends com.android.server.SystemService
RUNTIME_FREE_QUOTA_MAX_LIMIT_MS = Math.max(RUNTIME_MIN_GUARANTEE_MS,
properties.getLong(KEY_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS,
DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS));
+ // Make sure min runtime is at least as long as regular jobs.
+ RUNTIME_MIN_UI_GUARANTEE_MS = Math.max(RUNTIME_MIN_GUARANTEE_MS,
+ properties.getLong(
+ KEY_RUNTIME_MIN_UI_GUARANTEE_MS, DEFAULT_RUNTIME_MIN_UI_GUARANTEE_MS));
+ // Max limit should be at least the min guarantee AND the free quota.
+ RUNTIME_UI_LIMIT_MS = Math.max(RUNTIME_FREE_QUOTA_MAX_LIMIT_MS,
+ Math.max(RUNTIME_MIN_UI_GUARANTEE_MS,
+ properties.getLong(
+ KEY_RUNTIME_UI_LIMIT_MS, DEFAULT_RUNTIME_UI_LIMIT_MS)));
+ // The buffer factor should be at least 1 (so we don't decrease the time).
+ RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR = Math.max(1,
+ properties.getFloat(
+ KEY_RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR,
+ DEFAULT_RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR
+ ));
+ // Make sure min runtime is at least as long as other user-initiated jobs.
+ RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS = Math.max(
+ RUNTIME_MIN_UI_GUARANTEE_MS,
+ properties.getLong(
+ KEY_RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS,
+ DEFAULT_RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS));
+ // The cumulative runtime limit should be at least the max execution limit.
+ RUNTIME_CUMULATIVE_UI_LIMIT_MS = Math.max(RUNTIME_UI_LIMIT_MS,
+ properties.getLong(
+ KEY_RUNTIME_CUMULATIVE_UI_LIMIT_MS,
+ DEFAULT_RUNTIME_CUMULATIVE_UI_LIMIT_MS));
+
+ RUNTIME_USE_DATA_ESTIMATES_FOR_LIMITS = properties.getBoolean(
+ KEY_RUNTIME_USE_DATA_ESTIMATES_FOR_LIMITS,
+ DEFAULT_RUNTIME_USE_DATA_ESTIMATES_FOR_LIMITS);
}
- private boolean updateTareSettingsLocked(boolean isTareEnabled) {
+ private boolean updateTareSettingsLocked(@EconomyManager.EnabledMode int enabledMode) {
boolean changed = false;
- if (USE_TARE_POLICY != isTareEnabled) {
- USE_TARE_POLICY = isTareEnabled;
+ final boolean useTare = enabledMode == EconomyManager.ENABLED_MODE_ON;
+ if (USE_TARE_POLICY != useTare) {
+ USE_TARE_POLICY = useTare;
changed = true;
}
return changed;
@@ -768,6 +1158,7 @@ public class JobSchedulerService extends com.android.server.SystemService
pw.print(KEY_MIN_LINEAR_BACKOFF_TIME_MS, MIN_LINEAR_BACKOFF_TIME_MS).println();
pw.print(KEY_MIN_EXP_BACKOFF_TIME_MS, MIN_EXP_BACKOFF_TIME_MS).println();
+ pw.print(KEY_SYSTEM_STOP_TO_FAILURE_RATIO, SYSTEM_STOP_TO_FAILURE_RATIO).println();
pw.print(KEY_CONN_CONGESTION_DELAY_FRAC, CONN_CONGESTION_DELAY_FRAC).println();
pw.print(KEY_CONN_PREFETCH_RELAX_FRAC, CONN_PREFETCH_RELAX_FRAC).println();
pw.print(KEY_CONN_USE_CELL_SIGNAL_STRENGTH, CONN_USE_CELL_SIGNAL_STRENGTH).println();
@@ -786,12 +1177,40 @@ public class JobSchedulerService extends com.android.server.SystemService
pw.print(KEY_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT,
API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT).println();
+ pw.print(KEY_ENABLE_EXECUTION_SAFEGUARDS_UDC, ENABLE_EXECUTION_SAFEGUARDS_UDC)
+ .println();
+ pw.print(KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT,
+ EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT).println();
+ pw.print(KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT,
+ EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT).println();
+ pw.print(KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT,
+ EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT).println();
+ pw.print(KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_TOTAL_COUNT,
+ EXECUTION_SAFEGUARDS_UDC_TIMEOUT_TOTAL_COUNT).println();
+ pw.print(KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS,
+ EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS).println();
+ pw.print(KEY_EXECUTION_SAFEGUARDS_UDC_ANR_COUNT,
+ EXECUTION_SAFEGUARDS_UDC_ANR_COUNT).println();
+ pw.print(KEY_EXECUTION_SAFEGUARDS_UDC_ANR_WINDOW_MS,
+ EXECUTION_SAFEGUARDS_UDC_ANR_WINDOW_MS).println();
+
pw.print(KEY_RUNTIME_MIN_GUARANTEE_MS, RUNTIME_MIN_GUARANTEE_MS).println();
pw.print(KEY_RUNTIME_MIN_EJ_GUARANTEE_MS, RUNTIME_MIN_EJ_GUARANTEE_MS).println();
- pw.print(KEY_RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS,
- RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS).println();
pw.print(KEY_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, RUNTIME_FREE_QUOTA_MAX_LIMIT_MS)
.println();
+ pw.print(KEY_RUNTIME_MIN_UI_GUARANTEE_MS, RUNTIME_MIN_UI_GUARANTEE_MS).println();
+ pw.print(KEY_RUNTIME_UI_LIMIT_MS, RUNTIME_UI_LIMIT_MS).println();
+ pw.print(KEY_RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR,
+ RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR).println();
+ pw.print(KEY_RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS,
+ RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS).println();
+ pw.print(KEY_RUNTIME_CUMULATIVE_UI_LIMIT_MS, RUNTIME_CUMULATIVE_UI_LIMIT_MS).println();
+ pw.print(KEY_RUNTIME_USE_DATA_ESTIMATES_FOR_LIMITS,
+ RUNTIME_USE_DATA_ESTIMATES_FOR_LIMITS).println();
+
+ pw.print(KEY_PERSIST_IN_SPLIT_FILES, PERSIST_IN_SPLIT_FILES).println();
+ pw.print(KEY_MAX_NUM_PERSISTED_JOB_WORK_ITEMS, MAX_NUM_PERSISTED_JOB_WORK_ITEMS)
+ .println();
pw.print(Settings.Global.ENABLE_TARE, USE_TARE_POLICY).println();
@@ -839,6 +1258,10 @@ public class JobSchedulerService extends com.android.server.SystemService
final int pkgUid = intent.getIntExtra(Intent.EXTRA_UID, -1);
if (Intent.ACTION_PACKAGE_CHANGED.equals(action)) {
+ synchronized (mPermissionCache) {
+ // Something changed. Better clear the cached permission set.
+ mPermissionCache.remove(pkgUid);
+ }
// Purge the app's jobs if the whole package was just disabled. When this is
// the case the component name will be a bare package name.
if (pkgName != null && pkgUid != -1) {
@@ -867,6 +1290,8 @@ public class JobSchedulerService extends com.android.server.SystemService
// a user-initiated action, it should be fine to just
// put USER instead of UNINSTALL or DISABLED.
cancelJobsForPackageAndUidLocked(pkgName, pkgUid,
+ /* includeSchedulingApp */ true,
+ /* includeSourceApp */ true,
JobParameters.STOP_REASON_USER,
JobParameters.INTERNAL_STOP_REASON_UNINSTALL,
"app disabled");
@@ -901,17 +1326,19 @@ public class JobSchedulerService extends com.android.server.SystemService
Slog.w(TAG, "PACKAGE_CHANGED for " + pkgName + " / uid " + pkgUid);
}
} else if (Intent.ACTION_PACKAGE_ADDED.equals(action)) {
+ synchronized (mPermissionCache) {
+ // Something changed. Better clear the cached permission set.
+ mPermissionCache.remove(pkgUid);
+ }
if (!intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
- final int uid = intent.getIntExtra(Intent.EXTRA_UID, -1);
synchronized (mLock) {
- mUidToPackageCache.remove(uid);
- }
- } else {
- synchronized (mJobSchedulerStub.mPersistCache) {
- mJobSchedulerStub.mPersistCache.remove(pkgUid);
+ mUidToPackageCache.remove(pkgUid);
}
}
} else if (Intent.ACTION_PACKAGE_FULLY_REMOVED.equals(action)) {
+ synchronized (mPermissionCache) {
+ mPermissionCache.remove(pkgUid);
+ }
if (DEBUG) {
Slog.d(TAG, "Removing jobs for " + pkgName + " (uid=" + pkgUid + ")");
}
@@ -921,6 +1348,7 @@ public class JobSchedulerService extends com.android.server.SystemService
// get here, but since this is generally a user-initiated action, it should
// be fine to just put USER instead of UNINSTALL or DISABLED.
cancelJobsForPackageAndUidLocked(pkgName, pkgUid,
+ /* includeSchedulingApp */ true, /* includeSourceApp */ true,
JobParameters.STOP_REASON_USER,
JobParameters.INTERNAL_STOP_REASON_UNINSTALL, "app uninstalled");
for (int c = 0; c < mControllers.size(); ++c) {
@@ -929,6 +1357,14 @@ public class JobSchedulerService extends com.android.server.SystemService
mDebuggableApps.remove(pkgName);
mConcurrencyManager.onAppRemovedLocked(pkgName, pkgUid);
}
+ } else if (Intent.ACTION_UID_REMOVED.equals(action)) {
+ if (!intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
+ synchronized (mLock) {
+ mUidBiasOverride.delete(pkgUid);
+ mUidCapabilities.delete(pkgUid);
+ mUidProcStates.delete(pkgUid);
+ }
+ }
} else if (Intent.ACTION_USER_ADDED.equals(action)) {
final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0);
synchronized (mLock) {
@@ -949,16 +1385,24 @@ public class JobSchedulerService extends com.android.server.SystemService
}
}
mConcurrencyManager.onUserRemoved(userId);
+ synchronized (mPermissionCache) {
+ for (int u = mPermissionCache.size() - 1; u >= 0; --u) {
+ final int uid = mPermissionCache.keyAt(u);
+ if (userId == UserHandle.getUserId(uid)) {
+ mPermissionCache.removeAt(u);
+ }
+ }
+ }
} else if (Intent.ACTION_QUERY_PACKAGE_RESTART.equals(action)) {
// Has this package scheduled any jobs, such that we will take action
// if it were to be force-stopped?
if (pkgUid != -1) {
- List<JobStatus> jobsForUid;
+ ArraySet<JobStatus> jobsForUid;
synchronized (mLock) {
jobsForUid = mJobs.getJobsByUid(pkgUid);
}
for (int i = jobsForUid.size() - 1; i >= 0; i--) {
- if (jobsForUid.get(i).getSourcePackageName().equals(pkgName)) {
+ if (jobsForUid.valueAt(i).getSourcePackageName().equals(pkgName)) {
if (DEBUG) {
Slog.d(TAG, "Restart query: package " + pkgName + " at uid "
+ pkgUid + " has jobs");
@@ -975,7 +1419,12 @@ public class JobSchedulerService extends com.android.server.SystemService
Slog.d(TAG, "Removing jobs for pkg " + pkgName + " at uid " + pkgUid);
}
synchronized (mLock) {
+ // Exclude jobs scheduled on behalf of this app for now because SyncManager
+ // and other job proxy agents may not know to reschedule the job properly
+ // after force stop.
+ // TODO(209852664): determine how to best handle syncs & other proxied jobs
cancelJobsForPackageAndUidLocked(pkgName, pkgUid,
+ /* includeSchedulingApp */ true, /* includeSourceApp */ false,
JobParameters.STOP_REASON_USER,
JobParameters.INTERNAL_STOP_REASON_CANCELED,
"app force stopped");
@@ -991,29 +1440,27 @@ public class JobSchedulerService extends com.android.server.SystemService
return pkg;
}
- final private IUidObserver mUidObserver = new IUidObserver.Stub() {
+ final private IUidObserver mUidObserver = new UidObserver() {
@Override public void onUidStateChanged(int uid, int procState, long procStateSeq,
int capability) {
- mHandler.obtainMessage(MSG_UID_STATE_CHANGED, uid, procState).sendToTarget();
+ final SomeArgs args = SomeArgs.obtain();
+ args.argi1 = uid;
+ args.argi2 = procState;
+ args.argi3 = capability;
+ mHandler.obtainMessage(MSG_UID_STATE_CHANGED, args).sendToTarget();
}
@Override public void onUidGone(int uid, boolean disabled) {
mHandler.obtainMessage(MSG_UID_GONE, uid, disabled ? 1 : 0).sendToTarget();
}
- @Override public void onUidActive(int uid) throws RemoteException {
+ @Override public void onUidActive(int uid) {
mHandler.obtainMessage(MSG_UID_ACTIVE, uid, 0).sendToTarget();
}
@Override public void onUidIdle(int uid, boolean disabled) {
mHandler.obtainMessage(MSG_UID_IDLE, uid, disabled ? 1 : 0).sendToTarget();
}
-
- @Override public void onUidCachedChanged(int uid, boolean cached) {
- }
-
- @Override public void onUidProcAdjChanged(int uid) {
- }
};
public Context getTestableContext() {
@@ -1106,7 +1553,7 @@ public class JobSchedulerService extends com.android.server.SystemService
private final Predicate<Integer> mIsUidActivePredicate = this::isUidActive;
public int scheduleAsPackage(JobInfo job, JobWorkItem work, int uId, String packageName,
- int userId, String tag) {
+ int userId, @Nullable String namespace, String tag) {
// Rate limit excessive schedule() calls.
final String servicePkg = job.getService().getPackageName();
if (job.isPersisted() && (packageName == null || packageName.equals(servicePkg))) {
@@ -1161,18 +1608,78 @@ public class JobSchedulerService extends com.android.server.SystemService
if (mActivityManagerInternal.isAppStartModeDisabled(uId, servicePkg)) {
Slog.w(TAG, "Not scheduling job " + uId + ":" + job.toString()
+ " -- package not allowed to start");
+ Counter.logIncrementWithUid(
+ "job_scheduler.value_cntr_w_uid_schedule_failure_app_start_mode_disabled",
+ uId);
return JobScheduler.RESULT_FAILURE;
}
+ if (job.getRequiredNetwork() != null) {
+ sInitialJobEstimatedNetworkDownloadKBLogger.logSample(
+ safelyScaleBytesToKBForHistogram(
+ job.getEstimatedNetworkDownloadBytes()));
+ sInitialJobEstimatedNetworkUploadKBLogger.logSample(
+ safelyScaleBytesToKBForHistogram(job.getEstimatedNetworkUploadBytes()));
+ sJobMinimumChunkKBLogger.logSampleWithUid(uId,
+ safelyScaleBytesToKBForHistogram(job.getMinimumNetworkChunkBytes()));
+ if (work != null) {
+ sInitialJwiEstimatedNetworkDownloadKBLogger.logSample(
+ safelyScaleBytesToKBForHistogram(
+ work.getEstimatedNetworkDownloadBytes()));
+ sInitialJwiEstimatedNetworkUploadKBLogger.logSample(
+ safelyScaleBytesToKBForHistogram(
+ work.getEstimatedNetworkUploadBytes()));
+ sJwiMinimumChunkKBLogger.logSampleWithUid(uId,
+ safelyScaleBytesToKBForHistogram(
+ work.getMinimumNetworkChunkBytes()));
+ }
+ }
+
+ if (work != null) {
+ Counter.logIncrementWithUid(
+ "job_scheduler.value_cntr_w_uid_job_work_items_enqueued", uId);
+ }
+
synchronized (mLock) {
- final JobStatus toCancel = mJobs.getJobByUidAndJobId(uId, job.getId());
+ final JobStatus toCancel = mJobs.getJobByUidAndJobId(uId, namespace, job.getId());
if (work != null && toCancel != null) {
// Fast path: we are adding work to an existing job, and the JobInfo is not
// changing. We can just directly enqueue this work in to the job.
if (toCancel.getJob().equals(job)) {
+ // On T and below, JobWorkItem count was unlimited but they could not be
+ // persisted. Now in U and above, we allow persisting them. In both cases,
+ // there is a danger of apps adding too many JobWorkItems and causing the
+ // system to OOM since we keep everything in memory. The persisting danger
+ // is greater because it could technically lead to a boot loop if the system
+ // keeps trying to load all the JobWorkItems that led to the initial OOM.
+ // Therefore, for now (partly for app compatibility), we tackle the latter
+ // and limit the number of JobWorkItems that can be persisted.
+ // Moving forward, we should look into two things:
+ // 1. Limiting the number of unpersisted JobWorkItems
+ // 2. Offloading some state to disk so we don't keep everything in memory
+ // TODO(273758274): improve JobScheduler's resilience and memory management
+ if (toCancel.getWorkCount() >= mConstants.MAX_NUM_PERSISTED_JOB_WORK_ITEMS
+ && toCancel.isPersisted()) {
+ Slog.w(TAG, "Too many JWIs for uid " + uId);
+ throw new IllegalStateException("Apps may not persist more than "
+ + mConstants.MAX_NUM_PERSISTED_JOB_WORK_ITEMS
+ + " JobWorkItems per job");
+ }
toCancel.enqueueWorkLocked(work);
+ if (toCancel.getJob().isUserInitiated()) {
+ // The app is in a state to successfully schedule a UI job. Presumably, the
+ // user has asked for this additional bit of work, so remove any demotion
+ // flags. Only do this for UI jobs since they have strict scheduling
+ // requirements; it's harder to assume other jobs were scheduled due to
+ // user interaction/request.
+ toCancel.removeInternalFlags(
+ JobStatus.INTERNAL_FLAG_DEMOTED_BY_USER
+ | JobStatus.INTERNAL_FLAG_DEMOTED_BY_SYSTEM_UIJ);
+ }
+ mJobs.touchJob(toCancel);
+ sEnqueuedJwiHighWaterMarkLogger.logSampleWithUid(uId, toCancel.getWorkCount());
// If any of work item is enqueued when the source is in the foreground,
// exempt the entire job.
@@ -1182,13 +1689,17 @@ public class JobSchedulerService extends com.android.server.SystemService
}
}
- JobStatus jobStatus = JobStatus.createFromJobInfo(job, uId, packageName, userId, tag);
+ JobStatus jobStatus =
+ JobStatus.createFromJobInfo(job, uId, packageName, userId, namespace, tag);
// Return failure early if expedited job quota used up.
if (jobStatus.isRequestedExpeditedJob()) {
if ((mConstants.USE_TARE_POLICY && !mTareController.canScheduleEJ(jobStatus))
|| (!mConstants.USE_TARE_POLICY
&& !mQuotaController.isWithinEJQuotaLocked(jobStatus))) {
+ Counter.logIncrementWithUid(
+ "job_scheduler.value_cntr_w_uid_schedule_failure_ej_out_of_quota",
+ uId);
return JobScheduler.RESULT_FAILURE;
}
}
@@ -1204,6 +1715,8 @@ public class JobSchedulerService extends com.android.server.SystemService
if (packageName == null) {
if (mJobs.countJobsForUid(uId) > MAX_JOBS_PER_APP) {
Slog.w(TAG, "Too many jobs for uid " + uId);
+ Counter.logIncrementWithUid(
+ "job_scheduler.value_cntr_w_uid_max_scheduling_limit_hit", uId);
throw new IllegalStateException("Apps may not schedule more than "
+ MAX_JOBS_PER_APP + " distinct jobs");
}
@@ -1213,6 +1726,26 @@ public class JobSchedulerService extends com.android.server.SystemService
jobStatus.prepareLocked();
if (toCancel != null) {
+ // On T and below, JobWorkItem count was unlimited but they could not be
+ // persisted. Now in U and above, we allow persisting them. In both cases,
+ // there is a danger of apps adding too many JobWorkItems and causing the
+ // system to OOM since we keep everything in memory. The persisting danger
+ // is greater because it could technically lead to a boot loop if the system
+ // keeps trying to load all the JobWorkItems that led to the initial OOM.
+ // Therefore, for now (partly for app compatibility), we tackle the latter
+ // and limit the number of JobWorkItems that can be persisted.
+ // Moving forward, we should look into two things:
+ // 1. Limiting the number of unpersisted JobWorkItems
+ // 2. Offloading some state to disk so we don't keep everything in memory
+ // TODO(273758274): improve JobScheduler's resilience and memory management
+ if (work != null && toCancel.isPersisted()
+ && toCancel.getWorkCount() >= mConstants.MAX_NUM_PERSISTED_JOB_WORK_ITEMS) {
+ Slog.w(TAG, "Too many JWIs for uid " + uId);
+ throw new IllegalStateException("Apps may not persist more than "
+ + mConstants.MAX_NUM_PERSISTED_JOB_WORK_ITEMS
+ + " JobWorkItems per job");
+ }
+
// Implicitly replaces the existing job record with the new instance
cancelJobImplLocked(toCancel, jobStatus, JobParameters.STOP_REASON_CANCELLED_BY_APP,
JobParameters.INTERNAL_STOP_REASON_CANCELED, "job rescheduled by app");
@@ -1223,13 +1756,14 @@ public class JobSchedulerService extends com.android.server.SystemService
if (work != null) {
// If work has been supplied, enqueue it into the new job.
jobStatus.enqueueWorkLocked(work);
+ sEnqueuedJwiHighWaterMarkLogger.logSampleWithUid(uId, jobStatus.getWorkCount());
}
FrameworkStatsLog.write_non_chained(FrameworkStatsLog.SCHEDULED_JOB_STATE_CHANGED,
uId, null, jobStatus.getBatteryName(),
FrameworkStatsLog.SCHEDULED_JOB_STATE_CHANGED__STATE__SCHEDULED,
JobProtoEnums.INTERNAL_STOP_REASON_UNKNOWN, jobStatus.getStandbyBucket(),
- jobStatus.getJobId(),
+ jobStatus.getLoggingJobId(),
jobStatus.hasChargingConstraint(),
jobStatus.hasBatteryNotLowConstraint(),
jobStatus.hasStorageNotLowConstraint(),
@@ -1244,7 +1778,26 @@ public class JobSchedulerService extends com.android.server.SystemService
jobStatus.getJob().isPrefetch(),
jobStatus.getJob().getPriority(),
jobStatus.getEffectivePriority(),
- jobStatus.getNumFailures());
+ jobStatus.getNumPreviousAttempts(),
+ jobStatus.getJob().getMaxExecutionDelayMillis(),
+ /* isDeadlineConstraintSatisfied */ false,
+ /* isChargingSatisfied */ false,
+ /* batteryNotLowSatisfied */ false,
+ /* storageNotLowSatisfied */false,
+ /* timingDelayConstraintSatisfied */ false,
+ /* isDeviceIdleSatisfied */ false,
+ /* hasConnectivityConstraintSatisfied */ false,
+ /* hasContentTriggerConstraintSatisfied */ false,
+ /* jobStartLatencyMs */ 0,
+ jobStatus.getJob().isUserInitiated(),
+ /* isRunningAsUserInitiatedJob */ false,
+ jobStatus.getJob().isPeriodic(),
+ jobStatus.getJob().getMinLatencyMillis(),
+ jobStatus.getEstimatedNetworkDownloadBytes(),
+ jobStatus.getEstimatedNetworkUploadBytes(),
+ jobStatus.getWorkCount(),
+ ActivityManager.processStateAmToProto(mUidProcStates.get(jobStatus.getUid())),
+ jobStatus.getNamespaceHash());
// 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
@@ -1261,31 +1814,186 @@ public class JobSchedulerService extends com.android.server.SystemService
mJobPackageTracker.notePending(jobStatus);
mPendingJobQueue.add(jobStatus);
maybeRunPendingJobsLocked();
- } else {
- evaluateControllerStatesLocked(jobStatus);
}
}
return JobScheduler.RESULT_SUCCESS;
}
- public List<JobInfo> getPendingJobs(int uid) {
+ private ArrayMap<String, List<JobInfo>> getPendingJobs(int uid) {
+ final ArrayMap<String, List<JobInfo>> outMap = new ArrayMap<>();
synchronized (mLock) {
- List<JobStatus> jobs = mJobs.getJobsByUid(uid);
- ArrayList<JobInfo> outList = new ArrayList<JobInfo>(jobs.size());
+ ArraySet<JobStatus> jobs = mJobs.getJobsByUid(uid);
+ // Write out for loop to avoid creating an Iterator.
for (int i = jobs.size() - 1; i >= 0; i--) {
- JobStatus job = jobs.get(i);
+ final JobStatus job = jobs.valueAt(i);
+ List<JobInfo> outList = outMap.get(job.getNamespace());
+ if (outList == null) {
+ outList = new ArrayList<>();
+ outMap.put(job.getNamespace(), outList);
+ }
+
outList.add(job.getJob());
}
+ return outMap;
+ }
+ }
+
+ private List<JobInfo> getPendingJobsInNamespace(int uid, @Nullable String namespace) {
+ synchronized (mLock) {
+ ArraySet<JobStatus> jobs = mJobs.getJobsByUid(uid);
+ ArrayList<JobInfo> outList = new ArrayList<>();
+ // Write out for loop to avoid addAll() creating an Iterator.
+ for (int i = jobs.size() - 1; i >= 0; i--) {
+ final JobStatus job = jobs.valueAt(i);
+ if (Objects.equals(namespace, job.getNamespace())) {
+ outList.add(job.getJob());
+ }
+ }
return outList;
}
}
- public JobInfo getPendingJob(int uid, int jobId) {
+ @JobScheduler.PendingJobReason
+ private int getPendingJobReason(int uid, String namespace, int jobId) {
+ int reason;
+ // Some apps may attempt to query this frequently, so cache the reason under a separate lock
+ // so that the rest of JS processing isn't negatively impacted.
+ synchronized (mPendingJobReasonCache) {
+ SparseIntArray jobIdToReason = mPendingJobReasonCache.get(uid, namespace);
+ if (jobIdToReason != null) {
+ reason = jobIdToReason.get(jobId, JobScheduler.PENDING_JOB_REASON_UNDEFINED);
+ if (reason != JobScheduler.PENDING_JOB_REASON_UNDEFINED) {
+ return reason;
+ }
+ }
+ }
synchronized (mLock) {
- List<JobStatus> jobs = mJobs.getJobsByUid(uid);
+ reason = getPendingJobReasonLocked(uid, namespace, jobId);
+ if (DEBUG) {
+ Slog.v(TAG, "getPendingJobReason("
+ + uid + "," + namespace + "," + jobId + ")=" + reason);
+ }
+ }
+ synchronized (mPendingJobReasonCache) {
+ SparseIntArray jobIdToReason = mPendingJobReasonCache.get(uid, namespace);
+ if (jobIdToReason == null) {
+ jobIdToReason = new SparseIntArray();
+ mPendingJobReasonCache.add(uid, namespace, jobIdToReason);
+ }
+ jobIdToReason.put(jobId, reason);
+ }
+ return reason;
+ }
+
+ @VisibleForTesting
+ @JobScheduler.PendingJobReason
+ int getPendingJobReason(JobStatus job) {
+ return getPendingJobReason(job.getUid(), job.getNamespace(), job.getJobId());
+ }
+
+ @JobScheduler.PendingJobReason
+ @GuardedBy("mLock")
+ private int getPendingJobReasonLocked(int uid, String namespace, int jobId) {
+ // Very similar code to isReadyToBeExecutedLocked.
+
+ JobStatus job = mJobs.getJobByUidAndJobId(uid, namespace, jobId);
+ if (job == null) {
+ // Job doesn't exist.
+ return JobScheduler.PENDING_JOB_REASON_INVALID_JOB_ID;
+ }
+
+ if (isCurrentlyRunningLocked(job)) {
+ return JobScheduler.PENDING_JOB_REASON_EXECUTING;
+ }
+
+ final boolean jobReady = job.isReady();
+
+ if (DEBUG) {
+ Slog.v(TAG, "getPendingJobReasonLocked: " + job.toShortString()
+ + " ready=" + jobReady);
+ }
+
+ if (!jobReady) {
+ return job.getPendingJobReason();
+ }
+
+ final boolean userStarted = areUsersStartedLocked(job);
+
+ if (DEBUG) {
+ Slog.v(TAG, "getPendingJobReasonLocked: " + job.toShortString()
+ + " userStarted=" + userStarted);
+ }
+ if (!userStarted) {
+ return JobScheduler.PENDING_JOB_REASON_USER;
+ }
+
+ final boolean backingUp = mBackingUpUids.get(job.getSourceUid());
+ if (DEBUG) {
+ Slog.v(TAG, "getPendingJobReasonLocked: " + job.toShortString()
+ + " backingUp=" + backingUp);
+ }
+
+ if (backingUp) {
+ // TODO: Should we make a special reason for this?
+ return JobScheduler.PENDING_JOB_REASON_APP;
+ }
+
+ JobRestriction restriction = checkIfRestricted(job);
+ if (DEBUG) {
+ Slog.v(TAG, "getPendingJobReasonLocked: " + job.toShortString()
+ + " restriction=" + restriction);
+ }
+ if (restriction != null) {
+ return restriction.getPendingReason();
+ }
+
+ // The following can be a little more expensive (especially jobActive, since we need to
+ // go through the array of all potentially active jobs), so we are doing them
+ // later... but still before checking with the package manager!
+ final boolean jobPending = mPendingJobQueue.contains(job);
+
+
+ if (DEBUG) {
+ Slog.v(TAG, "getPendingJobReasonLocked: " + job.toShortString()
+ + " pending=" + jobPending);
+ }
+
+ if (jobPending) {
+ // We haven't started the job for some reason. Presumably, there are too many jobs
+ // running.
+ return JobScheduler.PENDING_JOB_REASON_DEVICE_STATE;
+ }
+
+ final boolean jobActive = mConcurrencyManager.isJobRunningLocked(job);
+
+ if (DEBUG) {
+ Slog.v(TAG, "getPendingJobReasonLocked: " + job.toShortString()
+ + " active=" + jobActive);
+ }
+ if (jobActive) {
+ return JobScheduler.PENDING_JOB_REASON_UNDEFINED;
+ }
+
+ // Validate that the defined package+service is still present & viable.
+ final boolean componentUsable = isComponentUsable(job);
+
+ if (DEBUG) {
+ Slog.v(TAG, "getPendingJobReasonLocked: " + job.toShortString()
+ + " componentUsable=" + componentUsable);
+ }
+ if (!componentUsable) {
+ return JobScheduler.PENDING_JOB_REASON_APP;
+ }
+
+ return JobScheduler.PENDING_JOB_REASON_UNDEFINED;
+ }
+
+ private JobInfo getPendingJob(int uid, @Nullable String namespace, int jobId) {
+ synchronized (mLock) {
+ ArraySet<JobStatus> jobs = mJobs.getJobsByUid(uid);
for (int i = jobs.size() - 1; i >= 0; i--) {
- JobStatus job = jobs.get(i);
- if (job.getJobId() == jobId) {
+ JobStatus job = jobs.valueAt(i);
+ if (job.getJobId() == jobId && Objects.equals(namespace, job.getNamespace())) {
return job.getJob();
}
}
@@ -1293,61 +2001,143 @@ public class JobSchedulerService extends com.android.server.SystemService
}
}
- private void cancelJobsForUserLocked(int userHandle) {
- final List<JobStatus> jobsForUser = mJobs.getJobsByUser(userHandle);
- for (int i = 0; i < jobsForUser.size(); i++) {
- JobStatus toRemove = jobsForUser.get(i);
- // There's no guarantee that the process has been stopped by the time we get here,
- // but since this is a user-initiated action, it should be fine to just put USER
- // instead of UNINSTALL or DISABLED.
- cancelJobImplLocked(toRemove, null, JobParameters.STOP_REASON_USER,
- JobParameters.INTERNAL_STOP_REASON_UNINSTALL, "user removed");
+ @VisibleForTesting
+ void notePendingUserRequestedAppStopInternal(@NonNull String packageName, int userId,
+ @Nullable String debugReason) {
+ final int packageUid = mLocalPM.getPackageUid(packageName, 0, userId);
+ if (packageUid < 0) {
+ Slog.wtf(TAG, "Asked to stop jobs of an unknown package");
+ return;
+ }
+ synchronized (mLock) {
+ mConcurrencyManager.markJobsForUserStopLocked(userId, packageName, debugReason);
+ final ArraySet<JobStatus> jobs = mJobs.getJobsByUid(packageUid);
+ for (int i = jobs.size() - 1; i >= 0; i--) {
+ final JobStatus job = jobs.valueAt(i);
+
+ // For now, demote all jobs of the app. However, if the app was only doing work
+ // on behalf of another app and the user wanted just that work to stop, this
+ // unfairly penalizes any other jobs that may be scheduled.
+ // For example, if apps A & B ask app C to do something (thus A & B are "source"
+ // and C is "calling"), but only A's work was under way and the user wanted
+ // to stop only that work, B's jobs would be demoted as well.
+ // TODO(255768978): make it possible to demote only the relevant subset of jobs
+ job.addInternalFlags(JobStatus.INTERNAL_FLAG_DEMOTED_BY_USER);
+
+ // The app process will be killed soon. There's no point keeping its jobs in
+ // the pending queue to try and start them.
+ if (mPendingJobQueue.remove(job)) {
+ synchronized (mPendingJobReasonCache) {
+ SparseIntArray jobIdToReason = mPendingJobReasonCache.get(
+ job.getUid(), job.getNamespace());
+ if (jobIdToReason == null) {
+ jobIdToReason = new SparseIntArray();
+ mPendingJobReasonCache.add(job.getUid(), job.getNamespace(),
+ jobIdToReason);
+ }
+ jobIdToReason.put(job.getJobId(), JobScheduler.PENDING_JOB_REASON_USER);
+ }
+ }
+ }
}
}
+ private final Consumer<JobStatus> mCancelJobDueToUserRemovalConsumer = (toRemove) -> {
+ // There's no guarantee that the process has been stopped by the time we get
+ // here, but since this is a user-initiated action, it should be fine to just
+ // put USER instead of UNINSTALL or DISABLED.
+ cancelJobImplLocked(toRemove, null, JobParameters.STOP_REASON_USER,
+ JobParameters.INTERNAL_STOP_REASON_UNINSTALL, "user removed");
+ };
+
+ private void cancelJobsForUserLocked(int userHandle) {
+ mJobs.forEachJob(
+ (job) -> job.getUserId() == userHandle || job.getSourceUserId() == userHandle,
+ mCancelJobDueToUserRemovalConsumer);
+ }
+
private void cancelJobsForNonExistentUsers() {
UserManagerInternal umi = LocalServices.getService(UserManagerInternal.class);
synchronized (mLock) {
mJobs.removeJobsOfUnlistedUsers(umi.getUserIds());
}
+ synchronized (mPendingJobReasonCache) {
+ mPendingJobReasonCache.clear();
+ }
}
private void cancelJobsForPackageAndUidLocked(String pkgName, int uid,
+ boolean includeSchedulingApp, boolean includeSourceApp,
@JobParameters.StopReason int reason, int internalReasonCode, String debugReason) {
+ if (!includeSchedulingApp && !includeSourceApp) {
+ Slog.wtfStack(TAG,
+ "Didn't indicate whether to cancel jobs for scheduling and/or source app");
+ includeSourceApp = true;
+ }
if ("android".equals(pkgName)) {
Slog.wtfStack(TAG, "Can't cancel all jobs for system package");
return;
}
- final List<JobStatus> jobsForUid = mJobs.getJobsByUid(uid);
+ final ArraySet<JobStatus> jobsForUid = new ArraySet<>();
+ if (includeSchedulingApp) {
+ mJobs.getJobsByUid(uid, jobsForUid);
+ }
+ if (includeSourceApp) {
+ mJobs.getJobsBySourceUid(uid, jobsForUid);
+ }
for (int i = jobsForUid.size() - 1; i >= 0; i--) {
- final JobStatus job = jobsForUid.get(i);
- if (job.getSourcePackageName().equals(pkgName)) {
+ final JobStatus job = jobsForUid.valueAt(i);
+ final boolean shouldCancel =
+ (includeSchedulingApp
+ && job.getServiceComponent().getPackageName().equals(pkgName))
+ || (includeSourceApp && job.getSourcePackageName().equals(pkgName));
+ if (shouldCancel) {
cancelJobImplLocked(job, null, reason, internalReasonCode, debugReason);
}
}
}
/**
- * Entry point from client to cancel all jobs originating from their uid.
+ * Entry point from client to cancel all jobs scheduled for or from their uid.
* This will remove the job from the master list, and cancel the job if it was staged for
* execution or being executed.
*
- * @param uid Uid to check against for removal of a job.
+ * @param uid Uid to check against for removal of a job.
+ * @param includeSourceApp Whether to include jobs scheduled for this UID by another UID.
+ * If false, only jobs scheduled by this UID will be cancelled.
*/
- public boolean cancelJobsForUid(int uid, @JobParameters.StopReason int reason,
- int internalReasonCode, String debugReason) {
- if (uid == Process.SYSTEM_UID) {
+ public boolean cancelJobsForUid(int uid, boolean includeSourceApp,
+ @JobParameters.StopReason int reason, int internalReasonCode, String debugReason) {
+ return cancelJobsForUid(uid, includeSourceApp,
+ /* namespaceOnly */ false, /* namespace */ null,
+ reason, internalReasonCode, debugReason);
+ }
+
+ private boolean cancelJobsForUid(int uid, boolean includeSourceApp,
+ boolean namespaceOnly, @Nullable String namespace,
+ @JobParameters.StopReason int reason, int internalReasonCode, String debugReason) {
+ // Non-null system namespace means the cancelling is limited to the namespace
+ // and won't cause issues for the system at large.
+ if (uid == Process.SYSTEM_UID && (!namespaceOnly || namespace == null)) {
Slog.wtfStack(TAG, "Can't cancel all jobs for system uid");
return false;
}
boolean jobsCanceled = false;
synchronized (mLock) {
- final List<JobStatus> jobsForUid = mJobs.getJobsByUid(uid);
+ final ArraySet<JobStatus> jobsForUid = new ArraySet<>();
+ // Get jobs scheduled by the app.
+ mJobs.getJobsByUid(uid, jobsForUid);
+ if (includeSourceApp) {
+ // Get jobs scheduled for the app by someone else.
+ mJobs.getJobsBySourceUid(uid, jobsForUid);
+ }
for (int i = 0; i < jobsForUid.size(); i++) {
- JobStatus toRemove = jobsForUid.get(i);
- cancelJobImplLocked(toRemove, null, reason, internalReasonCode, debugReason);
- jobsCanceled = true;
+ JobStatus toRemove = jobsForUid.valueAt(i);
+ if (!namespaceOnly || Objects.equals(namespace, toRemove.getNamespace())) {
+ cancelJobImplLocked(toRemove, null, reason, internalReasonCode, debugReason);
+ jobsCanceled = true;
+ }
}
}
return jobsCanceled;
@@ -1361,11 +2151,11 @@ public class JobSchedulerService extends com.android.server.SystemService
* @param uid Uid of the calling client.
* @param jobId Id of the job, provided at schedule-time.
*/
- private boolean cancelJob(int uid, int jobId, int callingUid,
+ private boolean cancelJob(int uid, String namespace, int jobId, int callingUid,
@JobParameters.StopReason int reason) {
JobStatus toCancel;
synchronized (mLock) {
- toCancel = mJobs.getJobByUidAndJobId(uid, jobId);
+ toCancel = mJobs.getJobByUidAndJobId(uid, namespace, jobId);
if (toCancel != null) {
cancelJobImplLocked(toCancel, null, reason,
JobParameters.INTERNAL_STOP_REASON_CANCELED,
@@ -1382,6 +2172,7 @@ public class JobSchedulerService extends com.android.server.SystemService
* {@code incomingJob} is non-null, it replaces {@code cancelled} in the store of
* currently scheduled jobs.
*/
+ @GuardedBy("mLock")
private void cancelJobImplLocked(JobStatus cancelled, JobStatus incomingJob,
@JobParameters.StopReason int reason, int internalReasonCode, String debugReason) {
if (DEBUG) Slog.d(TAG, "CANCEL: " + cancelled.toShortString());
@@ -1393,18 +2184,72 @@ public class JobSchedulerService extends com.android.server.SystemService
}
mChangedJobList.remove(cancelled);
// Cancel if running.
- mConcurrencyManager.stopJobOnServiceContextLocked(
+ final boolean wasRunning = mConcurrencyManager.stopJobOnServiceContextLocked(
cancelled, reason, internalReasonCode, debugReason);
+ // If the job was running, the JobServiceContext should log with state FINISHED.
+ if (!wasRunning) {
+ FrameworkStatsLog.write_non_chained(FrameworkStatsLog.SCHEDULED_JOB_STATE_CHANGED,
+ cancelled.getSourceUid(), null, cancelled.getBatteryName(),
+ FrameworkStatsLog.SCHEDULED_JOB_STATE_CHANGED__STATE__CANCELLED,
+ internalReasonCode, cancelled.getStandbyBucket(),
+ cancelled.getLoggingJobId(),
+ cancelled.hasChargingConstraint(),
+ cancelled.hasBatteryNotLowConstraint(),
+ cancelled.hasStorageNotLowConstraint(),
+ cancelled.hasTimingDelayConstraint(),
+ cancelled.hasDeadlineConstraint(),
+ cancelled.hasIdleConstraint(),
+ cancelled.hasConnectivityConstraint(),
+ cancelled.hasContentTriggerConstraint(),
+ cancelled.isRequestedExpeditedJob(),
+ /* isRunningAsExpeditedJob */ false,
+ reason,
+ cancelled.getJob().isPrefetch(),
+ cancelled.getJob().getPriority(),
+ cancelled.getEffectivePriority(),
+ cancelled.getNumPreviousAttempts(),
+ cancelled.getJob().getMaxExecutionDelayMillis(),
+ cancelled.isConstraintSatisfied(JobStatus.CONSTRAINT_DEADLINE),
+ cancelled.isConstraintSatisfied(JobInfo.CONSTRAINT_FLAG_CHARGING),
+ cancelled.isConstraintSatisfied(JobInfo.CONSTRAINT_FLAG_BATTERY_NOT_LOW),
+ cancelled.isConstraintSatisfied(JobInfo.CONSTRAINT_FLAG_STORAGE_NOT_LOW),
+ cancelled.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY),
+ cancelled.isConstraintSatisfied(JobInfo.CONSTRAINT_FLAG_DEVICE_IDLE),
+ cancelled.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY),
+ cancelled.isConstraintSatisfied(JobStatus.CONSTRAINT_CONTENT_TRIGGER),
+ /* jobStartLatencyMs */ 0,
+ cancelled.getJob().isUserInitiated(),
+ /* isRunningAsUserInitiatedJob */ false,
+ cancelled.getJob().isPeriodic(),
+ cancelled.getJob().getMinLatencyMillis(),
+ cancelled.getEstimatedNetworkDownloadBytes(),
+ cancelled.getEstimatedNetworkUploadBytes(),
+ cancelled.getWorkCount(),
+ ActivityManager.processStateAmToProto(mUidProcStates.get(cancelled.getUid())),
+ cancelled.getNamespaceHash());
+ }
// If this is a replacement, bring in the new version of the job
if (incomingJob != null) {
if (DEBUG) Slog.i(TAG, "Tracking replacement job " + incomingJob.toShortString());
startTrackingJobLocked(incomingJob, cancelled);
}
reportActiveLocked();
+ if (mLastCancelledJobs.length > 0
+ && internalReasonCode == JobParameters.INTERNAL_STOP_REASON_CANCELED) {
+ mLastCancelledJobs[mLastCancelledJobIndex] = cancelled;
+ mLastCancelledJobTimeElapsed[mLastCancelledJobIndex] = sElapsedRealtimeClock.millis();
+ mLastCancelledJobIndex = (mLastCancelledJobIndex + 1) % mLastCancelledJobs.length;
+ }
}
- void updateUidState(int uid, int procState) {
+ void updateUidState(int uid, int procState, int capabilities) {
+ if (DEBUG) {
+ Slog.d(TAG, "UID " + uid + " proc state changed to "
+ + ActivityManager.procStateToString(procState)
+ + " with capabilities=" + ActivityManager.getCapabilitiesSummary(capabilities));
+ }
synchronized (mLock) {
+ mUidProcStates.put(uid, procState);
final int prevBias = mUidBiasOverride.get(uid, JobInfo.BIAS_DEFAULT);
if (procState == ActivityManager.PROCESS_STATE_TOP) {
// Only use this if we are exactly the top app. All others can live
@@ -1418,6 +2263,12 @@ public class JobSchedulerService extends com.android.server.SystemService
} else {
mUidBiasOverride.delete(uid);
}
+ if (capabilities == ActivityManager.PROCESS_CAPABILITY_NONE
+ || procState == ActivityManager.PROCESS_STATE_NONEXISTENT) {
+ mUidCapabilities.delete(uid);
+ } else {
+ mUidCapabilities.put(uid, capabilities);
+ }
final int newBias = mUidBiasOverride.get(uid, JobInfo.BIAS_DEFAULT);
if (prevBias != newBias) {
if (DEBUG) {
@@ -1438,6 +2289,23 @@ public class JobSchedulerService extends com.android.server.SystemService
}
}
+ /**
+ * Return the current {@link ActivityManager#PROCESS_CAPABILITY_ALL capabilities}
+ * of the given UID.
+ */
+ public int getUidCapabilities(int uid) {
+ synchronized (mLock) {
+ return mUidCapabilities.get(uid, ActivityManager.PROCESS_CAPABILITY_NONE);
+ }
+ }
+
+ /** Return the current proc state of the given UID. */
+ public int getUidProcState(int uid) {
+ synchronized (mLock) {
+ return mUidProcStates.get(uid, ActivityManager.PROCESS_STATE_UNKNOWN);
+ }
+ }
+
@Override
public void onDeviceIdleStateChanged(boolean deviceIdle) {
synchronized (mLock) {
@@ -1460,6 +2328,17 @@ public class JobSchedulerService extends com.android.server.SystemService
}
@Override
+ public void onNetworkChanged(JobStatus jobStatus, Network newNetwork) {
+ synchronized (mLock) {
+ final JobServiceContext jsc =
+ mConcurrencyManager.getRunningJobServiceContextLocked(jobStatus);
+ if (jsc != null) {
+ jsc.informOfNetworkChangeLocked(newNetwork);
+ }
+ }
+ }
+
+ @Override
public void onRestrictedBucketChanged(List<JobStatus> jobs) {
final int len = jobs.size();
if (len == 0) {
@@ -1528,7 +2407,7 @@ public class JobSchedulerService extends com.android.server.SystemService
mActivityManagerInternal = Objects.requireNonNull(
LocalServices.getService(ActivityManagerInternal.class));
- mHandler = new JobHandler(context.getMainLooper());
+ mHandler = new JobHandler(AppSchedulingModuleThread.get().getLooper());
mConstants = new Constants();
mConstantsObserver = new ConstantsObserver();
mJobSchedulerStub = new JobSchedulerStub();
@@ -1538,12 +2417,52 @@ public class JobSchedulerService extends com.android.server.SystemService
// Set up the app standby bucketing tracker
mStandbyTracker = new StandbyTracker();
mUsageStats = LocalServices.getService(UsageStatsManagerInternal.class);
- mQuotaTracker = new CountQuotaTracker(context, QUOTA_CATEGORIZER);
- mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_SCHEDULE_PERSISTED,
- mConstants.API_QUOTA_SCHEDULE_COUNT,
- mConstants.API_QUOTA_SCHEDULE_WINDOW_MS);
+
+ final Categorizer quotaCategorizer = (userId, packageName, tag) -> {
+ if (QUOTA_TRACKER_TIMEOUT_UIJ_TAG.equals(tag)) {
+ return mConstants.ENABLE_EXECUTION_SAFEGUARDS_UDC
+ ? QUOTA_TRACKER_CATEGORY_TIMEOUT_UIJ
+ : QUOTA_TRACKER_CATEGORY_DISABLED;
+ }
+ if (QUOTA_TRACKER_TIMEOUT_EJ_TAG.equals(tag)) {
+ return mConstants.ENABLE_EXECUTION_SAFEGUARDS_UDC
+ ? QUOTA_TRACKER_CATEGORY_TIMEOUT_EJ
+ : QUOTA_TRACKER_CATEGORY_DISABLED;
+ }
+ if (QUOTA_TRACKER_TIMEOUT_REG_TAG.equals(tag)) {
+ return mConstants.ENABLE_EXECUTION_SAFEGUARDS_UDC
+ ? QUOTA_TRACKER_CATEGORY_TIMEOUT_REG
+ : QUOTA_TRACKER_CATEGORY_DISABLED;
+ }
+ if (QUOTA_TRACKER_TIMEOUT_TOTAL_TAG.equals(tag)) {
+ return mConstants.ENABLE_EXECUTION_SAFEGUARDS_UDC
+ ? QUOTA_TRACKER_CATEGORY_TIMEOUT_TOTAL
+ : QUOTA_TRACKER_CATEGORY_DISABLED;
+ }
+ if (QUOTA_TRACKER_ANR_TAG.equals(tag)) {
+ return mConstants.ENABLE_EXECUTION_SAFEGUARDS_UDC
+ ? QUOTA_TRACKER_CATEGORY_ANR
+ : QUOTA_TRACKER_CATEGORY_DISABLED;
+ }
+ if (QUOTA_TRACKER_SCHEDULE_PERSISTED_TAG.equals(tag)) {
+ return mConstants.ENABLE_API_QUOTAS
+ ? QUOTA_TRACKER_CATEGORY_SCHEDULE_PERSISTED
+ : QUOTA_TRACKER_CATEGORY_DISABLED;
+ }
+ if (QUOTA_TRACKER_SCHEDULE_LOGGED.equals(tag)) {
+ return mConstants.ENABLE_API_QUOTAS
+ ? QUOTA_TRACKER_CATEGORY_SCHEDULE_LOGGED
+ : QUOTA_TRACKER_CATEGORY_DISABLED;
+ }
+ Slog.wtf(TAG, "Unexpected category tag: " + tag);
+ return QUOTA_TRACKER_CATEGORY_DISABLED;
+ };
+ mQuotaTracker = new CountQuotaTracker(context, quotaCategorizer);
+ updateQuotaTracker();
// Log at most once per minute.
+ // Set outside updateQuotaTracker() since this is intentionally not configurable.
mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_SCHEDULE_LOGGED, 1, 60_000);
+ mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_DISABLED, Integer.MAX_VALUE, 60_000);
mAppStandbyInternal = LocalServices.getService(AppStandbyInternal.class);
mAppStandbyInternal.addListener(mStandbyTracker);
@@ -1552,19 +2471,28 @@ public class JobSchedulerService extends com.android.server.SystemService
publishLocalService(JobSchedulerInternal.class, new LocalService());
// Initialize the job store and set up any persisted jobs
- mJobs = JobStore.initAndGet(this);
+ mJobStoreLoadedLatch = new CountDownLatch(1);
+ mJobs = JobStore.get(this);
+ mJobs.initAsync(mJobStoreLoadedLatch);
mBatteryStateTracker = new BatteryStateTracker();
mBatteryStateTracker.startTracking();
// Create the controllers.
mControllers = new ArrayList<StateController>();
- final ConnectivityController connectivityController = new ConnectivityController(this);
- mControllers.add(connectivityController);
+ mPrefetchController = new PrefetchController(this);
+ mControllers.add(mPrefetchController);
+ final FlexibilityController flexibilityController =
+ new FlexibilityController(this, mPrefetchController);
+ mControllers.add(flexibilityController);
+ mConnectivityController =
+ new ConnectivityController(this, flexibilityController);
+ mControllers.add(mConnectivityController);
mControllers.add(new TimeController(this));
- final IdleController idleController = new IdleController(this);
+ final IdleController idleController = new IdleController(this, flexibilityController);
mControllers.add(idleController);
- final BatteryController batteryController = new BatteryController(this);
+ final BatteryController batteryController =
+ new BatteryController(this, flexibilityController);
mControllers.add(batteryController);
mStorageController = new StorageController(this);
mControllers.add(mStorageController);
@@ -1574,19 +2502,17 @@ public class JobSchedulerService extends com.android.server.SystemService
mControllers.add(new ContentObserverController(this));
mDeviceIdleJobsController = new DeviceIdleJobsController(this);
mControllers.add(mDeviceIdleJobsController);
- mPrefetchController = new PrefetchController(this);
- mControllers.add(mPrefetchController);
mQuotaController =
- new QuotaController(this, backgroundJobsController, connectivityController);
+ new QuotaController(this, backgroundJobsController, mConnectivityController);
mControllers.add(mQuotaController);
mControllers.add(new ComponentController(this));
mTareController =
- new TareController(this, backgroundJobsController, connectivityController);
+ new TareController(this, backgroundJobsController, mConnectivityController);
mControllers.add(mTareController);
mRestrictiveControllers = new ArrayList<>();
mRestrictiveControllers.add(batteryController);
- mRestrictiveControllers.add(connectivityController);
+ mRestrictiveControllers.add(mConnectivityController);
mRestrictiveControllers.add(idleController);
// Create restrictions
@@ -1615,7 +2541,7 @@ public class JobSchedulerService extends com.android.server.SystemService
// And kick off the work to update the affected jobs, using a secondary
// thread instead of chugging away here on the main looper thread.
- new Thread(mJobTimeUpdater, "JobSchedulerTimeSetReceiver").start();
+ mJobs.runWorkAsync(mJobTimeUpdater);
}
}
}
@@ -1653,7 +2579,15 @@ public class JobSchedulerService extends com.android.server.SystemService
@Override
public void onBootPhase(int phase) {
- if (PHASE_SYSTEM_SERVICES_READY == phase) {
+ if (PHASE_LOCK_SETTINGS_READY == phase) {
+ // This is the last phase before PHASE_SYSTEM_SERVICES_READY. We need to ensure that
+ // persisted jobs are loaded before we can proceed to PHASE_SYSTEM_SERVICES_READY.
+ try {
+ mJobStoreLoadedLatch.await();
+ } catch (InterruptedException e) {
+ Slog.e(TAG, "Couldn't wait on job store loading latch");
+ }
+ } else if (PHASE_SYSTEM_SERVICES_READY == phase) {
mConstantsObserver.start();
for (StateController controller : mControllers) {
controller.onSystemServicesReady();
@@ -1675,6 +2609,9 @@ public class JobSchedulerService extends com.android.server.SystemService
filter.addDataScheme("package");
getContext().registerReceiverAsUser(
mBroadcastReceiver, UserHandle.ALL, filter, null, null);
+ final IntentFilter uidFilter = new IntentFilter(Intent.ACTION_UID_REMOVED);
+ getContext().registerReceiverAsUser(
+ mBroadcastReceiver, UserHandle.ALL, uidFilter, null, null);
final IntentFilter userFilter = new IntentFilter(Intent.ACTION_USER_REMOVED);
userFilter.addAction(Intent.ACTION_USER_ADDED);
getContext().registerReceiverAsUser(
@@ -1726,12 +2663,15 @@ public class JobSchedulerService extends com.android.server.SystemService
Slog.wtf(TAG, "Not yet prepared when started tracking: " + jobStatus);
}
jobStatus.enqueueTime = sElapsedRealtimeClock.millis();
- final boolean update = mJobs.add(jobStatus);
+ final boolean update = lastJob != null;
+ mJobs.add(jobStatus);
+ // Clear potentially cached INVALID_JOB_ID reason.
+ resetPendingJobReasonCache(jobStatus);
if (mReadyToRock) {
for (int i = 0; i < mControllers.size(); i++) {
StateController controller = mControllers.get(i);
if (update) {
- controller.maybeStopTrackingJobLocked(jobStatus, null, true);
+ controller.maybeStopTrackingJobLocked(jobStatus, null);
}
controller.maybeStartTrackingJobLocked(jobStatus, lastJob);
}
@@ -1748,6 +2688,14 @@ public class JobSchedulerService extends com.android.server.SystemService
// Deal with any remaining work items in the old job.
jobStatus.stopTrackingJobLocked(incomingJob);
+ synchronized (mPendingJobReasonCache) {
+ SparseIntArray reasonCache =
+ mPendingJobReasonCache.get(jobStatus.getUid(), jobStatus.getNamespace());
+ if (reasonCache != null) {
+ reasonCache.delete(jobStatus.getJobId());
+ }
+ }
+
// Remove from store as well as controllers.
final boolean removed = mJobs.remove(jobStatus, removeFromPersisted);
if (!removed) {
@@ -1764,18 +2712,35 @@ public class JobSchedulerService extends com.android.server.SystemService
if (mReadyToRock) {
for (int i = 0; i < mControllers.size(); i++) {
StateController controller = mControllers.get(i);
- controller.maybeStopTrackingJobLocked(jobStatus, incomingJob, false);
+ controller.maybeStopTrackingJobLocked(jobStatus, incomingJob);
}
}
return removed;
}
+ /** Remove the pending job reason for this job from the cache. */
+ void resetPendingJobReasonCache(@NonNull JobStatus jobStatus) {
+ synchronized (mPendingJobReasonCache) {
+ final SparseIntArray reasons =
+ mPendingJobReasonCache.get(jobStatus.getUid(), jobStatus.getNamespace());
+ if (reasons != null) {
+ reasons.delete(jobStatus.getJobId());
+ }
+ }
+ }
+
/** Return {@code true} if the specified job is currently executing. */
@GuardedBy("mLock")
public boolean isCurrentlyRunningLocked(JobStatus job) {
return mConcurrencyManager.isJobRunningLocked(job);
}
+ /** @see JobConcurrencyManager#isJobInOvertimeLocked(JobStatus) */
+ @GuardedBy("mLock")
+ public boolean isJobInOvertimeLocked(JobStatus job) {
+ return mConcurrencyManager.isJobInOvertimeLocked(job);
+ }
+
private void noteJobPending(JobStatus job) {
mJobPackageTracker.notePending(job);
}
@@ -1803,48 +2768,94 @@ public class JobSchedulerService extends com.android.server.SystemService
* Reschedules the given job based on the job's backoff policy. It doesn't make sense to
* specify an override deadline on a failed job (the failed job will run even though it's not
* ready), so we reschedule it with {@link JobStatus#NO_LATEST_RUNTIME}, but specify that any
- * ready job with {@link JobStatus#getNumFailures()} > 0 will be executed.
+ * ready job with {@link JobStatus#getNumPreviousAttempts()} > 0 will be executed.
*
* @param failureToReschedule Provided job status that we will reschedule.
* @return A newly instantiated JobStatus with the same constraints as the last job except
- * with adjusted timing constraints.
+ * with adjusted timing constraints, or {@code null} if the job shouldn't be rescheduled for
+ * some policy reason.
* @see #maybeQueueReadyJobsForExecutionLocked
*/
+ @Nullable
@VisibleForTesting
- JobStatus getRescheduleJobForFailureLocked(JobStatus failureToReschedule) {
+ JobStatus getRescheduleJobForFailureLocked(JobStatus failureToReschedule,
+ @JobParameters.StopReason int stopReason, int internalStopReason) {
+ if (internalStopReason == JobParameters.INTERNAL_STOP_REASON_USER_UI_STOP
+ && failureToReschedule.isUserVisibleJob()) {
+ // If a user stops an app via Task Manager and the job was user-visible, then assume
+ // the user wanted to stop that task and not let it run in the future. It's in the
+ // app's best interests to provide action buttons in their notification to avoid this
+ // scenario.
+ Slog.i(TAG,
+ "Dropping " + failureToReschedule.toShortString() + " because of user stop");
+ return null;
+ }
+
final long elapsedNowMillis = sElapsedRealtimeClock.millis();
final JobInfo job = failureToReschedule.getJob();
final long initialBackoffMillis = job.getInitialBackoffMillis();
- final int backoffAttempts = failureToReschedule.getNumFailures() + 1;
- long delayMillis;
-
- switch (job.getBackoffPolicy()) {
- case JobInfo.BACKOFF_POLICY_LINEAR: {
- long backoff = initialBackoffMillis;
- if (backoff < mConstants.MIN_LINEAR_BACKOFF_TIME_MS) {
- backoff = mConstants.MIN_LINEAR_BACKOFF_TIME_MS;
- }
- delayMillis = backoff * backoffAttempts;
- } break;
- default:
- if (DEBUG) {
- Slog.v(TAG, "Unrecognised back-off policy, defaulting to exponential.");
+ int numFailures = failureToReschedule.getNumFailures();
+ int numSystemStops = failureToReschedule.getNumSystemStops();
+ // We should back off slowly if JobScheduler keeps stopping the job,
+ // but back off immediately if the issue appeared to be the app's fault
+ // or the user stopped the job somehow.
+ if (internalStopReason == JobParameters.INTERNAL_STOP_REASON_SUCCESSFUL_FINISH
+ || internalStopReason == JobParameters.INTERNAL_STOP_REASON_TIMEOUT
+ || internalStopReason == JobParameters.INTERNAL_STOP_REASON_ANR
+ || stopReason == JobParameters.STOP_REASON_USER) {
+ numFailures++;
+ } else {
+ numSystemStops++;
+ }
+ final int backoffAttempts =
+ numFailures + numSystemStops / mConstants.SYSTEM_STOP_TO_FAILURE_RATIO;
+ final long earliestRuntimeMs;
+
+ if (backoffAttempts == 0) {
+ earliestRuntimeMs = JobStatus.NO_EARLIEST_RUNTIME;
+ } else {
+ long delayMillis;
+ switch (job.getBackoffPolicy()) {
+ case JobInfo.BACKOFF_POLICY_LINEAR: {
+ long backoff = initialBackoffMillis;
+ if (backoff < mConstants.MIN_LINEAR_BACKOFF_TIME_MS) {
+ backoff = mConstants.MIN_LINEAR_BACKOFF_TIME_MS;
+ }
+ delayMillis = backoff * backoffAttempts;
}
- case JobInfo.BACKOFF_POLICY_EXPONENTIAL: {
- long backoff = initialBackoffMillis;
- if (backoff < mConstants.MIN_EXP_BACKOFF_TIME_MS) {
- backoff = mConstants.MIN_EXP_BACKOFF_TIME_MS;
+ break;
+ default:
+ if (DEBUG) {
+ Slog.v(TAG, "Unrecognised back-off policy, defaulting to exponential.");
+ }
+ // Intentional fallthrough.
+ case JobInfo.BACKOFF_POLICY_EXPONENTIAL: {
+ long backoff = initialBackoffMillis;
+ if (backoff < mConstants.MIN_EXP_BACKOFF_TIME_MS) {
+ backoff = mConstants.MIN_EXP_BACKOFF_TIME_MS;
+ }
+ delayMillis = (long) Math.scalb(backoff, backoffAttempts - 1);
}
- delayMillis = (long) Math.scalb(backoff, backoffAttempts - 1);
- } break;
+ break;
+ }
+ delayMillis =
+ Math.min(delayMillis, JobInfo.MAX_BACKOFF_DELAY_MILLIS);
+ earliestRuntimeMs = elapsedNowMillis + delayMillis;
}
- delayMillis =
- Math.min(delayMillis, JobInfo.MAX_BACKOFF_DELAY_MILLIS);
JobStatus newJob = new JobStatus(failureToReschedule,
- elapsedNowMillis + delayMillis,
- JobStatus.NO_LATEST_RUNTIME, backoffAttempts,
- failureToReschedule.getLastSuccessfulRunTime(), sSystemClock.millis());
+ earliestRuntimeMs,
+ JobStatus.NO_LATEST_RUNTIME, numFailures, numSystemStops,
+ failureToReschedule.getLastSuccessfulRunTime(), sSystemClock.millis(),
+ failureToReschedule.getCumulativeExecutionTimeMs());
+ if (stopReason == JobParameters.STOP_REASON_USER) {
+ // Demote all jobs to regular for user stops so they don't keep privileges.
+ newJob.addInternalFlags(JobStatus.INTERNAL_FLAG_DEMOTED_BY_USER);
+ }
+ if (newJob.getCumulativeExecutionTimeMs() >= mConstants.RUNTIME_CUMULATIVE_UI_LIMIT_MS
+ && newJob.shouldTreatAsUserInitiatedJob()) {
+ newJob.addInternalFlags(JobStatus.INTERNAL_FLAG_DEMOTED_BY_SYSTEM_UIJ);
+ }
if (job.isPeriodic()) {
newJob.setOriginalLatestRunTimeElapsed(
failureToReschedule.getOriginalLatestRunTimeElapsed());
@@ -1934,9 +2945,10 @@ public class JobSchedulerService extends com.android.server.SystemService
+ newLatestRuntimeElapsed);
return new JobStatus(periodicToReschedule,
elapsedNow + period - flex, elapsedNow + period,
- 0 /* backoffAttempt */,
+ 0 /* numFailures */, 0 /* numSystemStops */,
sSystemClock.millis() /* lastSuccessfulRunTime */,
- periodicToReschedule.getLastFailedRunTime());
+ periodicToReschedule.getLastFailedRunTime(),
+ 0 /* Reset cumulativeExecutionTime because of successful execution */);
}
final long newEarliestRunTimeElapsed = newLatestRuntimeElapsed
@@ -1949,9 +2961,52 @@ public class JobSchedulerService extends com.android.server.SystemService
}
return new JobStatus(periodicToReschedule,
newEarliestRunTimeElapsed, newLatestRuntimeElapsed,
- 0 /* backoffAttempt */,
+ 0 /* numFailures */, 0 /* numSystemStops */,
sSystemClock.millis() /* lastSuccessfulRunTime */,
- periodicToReschedule.getLastFailedRunTime());
+ periodicToReschedule.getLastFailedRunTime(),
+ 0 /* Reset cumulativeExecutionTime because of successful execution */);
+ }
+
+ @VisibleForTesting
+ void maybeProcessBuggyJob(@NonNull JobStatus jobStatus, int debugStopReason) {
+ boolean jobTimedOut = debugStopReason == JobParameters.INTERNAL_STOP_REASON_TIMEOUT;
+ // If madeActive = 0, the job never actually started.
+ if (!jobTimedOut && jobStatus.madeActive > 0) {
+ final long executionDurationMs = sUptimeMillisClock.millis() - jobStatus.madeActive;
+ // The debug reason may be different if we stopped the job for some other reason
+ // (eg. constraints), so look at total execution time to be safe.
+ if (jobStatus.startedAsUserInitiatedJob) {
+ // TODO: factor in different min guarantees for different UI job types
+ jobTimedOut = executionDurationMs >= mConstants.RUNTIME_MIN_UI_GUARANTEE_MS;
+ } else if (jobStatus.startedAsExpeditedJob) {
+ jobTimedOut = executionDurationMs >= mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS;
+ } else {
+ jobTimedOut = executionDurationMs >= mConstants.RUNTIME_MIN_GUARANTEE_MS;
+ }
+ }
+ if (jobTimedOut) {
+ final int userId = jobStatus.getTimeoutBlameUserId();
+ final String pkg = jobStatus.getTimeoutBlamePackageName();
+ mQuotaTracker.noteEvent(userId, pkg,
+ jobStatus.startedAsUserInitiatedJob
+ ? QUOTA_TRACKER_TIMEOUT_UIJ_TAG
+ : (jobStatus.startedAsExpeditedJob
+ ? QUOTA_TRACKER_TIMEOUT_EJ_TAG
+ : QUOTA_TRACKER_TIMEOUT_REG_TAG));
+ if (!mQuotaTracker.noteEvent(userId, pkg, QUOTA_TRACKER_TIMEOUT_TOTAL_TAG)) {
+ mAppStandbyInternal.restrictApp(
+ pkg, userId, UsageStatsManager.REASON_SUB_FORCED_SYSTEM_FLAG_BUGGY);
+ }
+ }
+
+ if (debugStopReason == JobParameters.INTERNAL_STOP_REASON_ANR) {
+ final int callingUserId = jobStatus.getUserId();
+ final String callingPkg = jobStatus.getServiceComponent().getPackageName();
+ if (!mQuotaTracker.noteEvent(callingUserId, callingPkg, QUOTA_TRACKER_ANR_TAG)) {
+ mAppStandbyInternal.restrictApp(callingPkg, callingUserId,
+ UsageStatsManager.REASON_SUB_FORCED_SYSTEM_FLAG_BUGGY);
+ }
+ }
}
// JobCompletedListener implementations.
@@ -1965,8 +3020,8 @@ public class JobSchedulerService extends com.android.server.SystemService
* @param needsReschedule Whether the implementing class should reschedule this job.
*/
@Override
- public void onJobCompletedLocked(JobStatus jobStatus, int debugStopReason,
- boolean needsReschedule) {
+ public void onJobCompletedLocked(JobStatus jobStatus, @JobParameters.StopReason int stopReason,
+ int debugStopReason, boolean needsReschedule) {
if (DEBUG) {
Slog.d(TAG, "Completed " + jobStatus + ", reason=" + debugStopReason
+ ", reschedule=" + needsReschedule);
@@ -1976,6 +3031,8 @@ public class JobSchedulerService extends com.android.server.SystemService
mLastCompletedJobTimeElapsed[mLastCompletedJobIndex] = sElapsedRealtimeClock.millis();
mLastCompletedJobIndex = (mLastCompletedJobIndex + 1) % NUM_COMPLETED_JOB_HISTORY;
+ maybeProcessBuggyJob(jobStatus, debugStopReason);
+
if (debugStopReason == JobParameters.INTERNAL_STOP_REASON_UNINSTALL
|| debugStopReason == JobParameters.INTERNAL_STOP_REASON_DATA_CLEARED) {
// The job should have already been cleared from the rest of the JS tracking. No need
@@ -1993,8 +3050,9 @@ public class JobSchedulerService extends com.android.server.SystemService
// job so we can transfer any appropriate state over from the previous job when
// we stop it.
final JobStatus rescheduledJob = needsReschedule
- ? getRescheduleJobForFailureLocked(jobStatus) : null;
+ ? getRescheduleJobForFailureLocked(jobStatus, stopReason, debugStopReason) : null;
if (rescheduledJob != null
+ && !rescheduledJob.shouldTreatAsUserInitiatedJob()
&& (debugStopReason == JobParameters.INTERNAL_STOP_REASON_TIMEOUT
|| debugStopReason == JobParameters.INTERNAL_STOP_REASON_PREEMPT)) {
rescheduledJob.disallowRunInBatterySaverAndDoze();
@@ -2006,7 +3064,8 @@ public class JobSchedulerService extends com.android.server.SystemService
if (DEBUG) {
Slog.d(TAG, "Could not find job to remove. Was job removed while executing?");
}
- JobStatus newJs = mJobs.getJobByUidAndJobId(jobStatus.getUid(), jobStatus.getJobId());
+ JobStatus newJs = mJobs.getJobByUidAndJobId(
+ jobStatus.getUid(), jobStatus.getNamespace(), jobStatus.getJobId());
if (newJs != null) {
// This job was stopped because the app scheduled a new job with the same job ID.
// Check if the new job is ready to run.
@@ -2045,11 +3104,31 @@ public class JobSchedulerService extends com.android.server.SystemService
public void onControllerStateChanged(@Nullable ArraySet<JobStatus> changedJobs) {
if (changedJobs == null) {
mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget();
+ synchronized (mPendingJobReasonCache) {
+ mPendingJobReasonCache.clear();
+ }
} else if (changedJobs.size() > 0) {
synchronized (mLock) {
mChangedJobList.addAll(changedJobs);
}
mHandler.obtainMessage(MSG_CHECK_CHANGED_JOB_LIST).sendToTarget();
+ synchronized (mPendingJobReasonCache) {
+ for (int i = changedJobs.size() - 1; i >= 0; --i) {
+ final JobStatus job = changedJobs.valueAt(i);
+ resetPendingJobReasonCache(job);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onRestrictionStateChanged(@NonNull JobRestriction restriction,
+ boolean stopOvertimeJobs) {
+ mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget();
+ if (stopOvertimeJobs) {
+ synchronized (mLock) {
+ mConcurrencyManager.maybeStopOvertimeJobsLocked(restriction);
+ }
}
}
@@ -2118,17 +3197,22 @@ public class JobSchedulerService extends com.android.server.SystemService
break;
case MSG_UID_STATE_CHANGED: {
- final int uid = message.arg1;
- final int procState = message.arg2;
- updateUidState(uid, procState);
+ final SomeArgs args = (SomeArgs) message.obj;
+ final int uid = args.argi1;
+ final int procState = args.argi2;
+ final int capabilities = args.argi3;
+ updateUidState(uid, procState, capabilities);
+ args.recycle();
break;
}
case MSG_UID_GONE: {
final int uid = message.arg1;
final boolean disabled = message.arg2 != 0;
- updateUidState(uid, ActivityManager.PROCESS_STATE_CACHED_EMPTY);
+ updateUidState(uid, ActivityManager.PROCESS_STATE_CACHED_EMPTY,
+ ActivityManager.PROCESS_CAPABILITY_NONE);
if (disabled) {
cancelJobsForUid(uid,
+ /* includeSourceApp */ true,
JobParameters.STOP_REASON_BACKGROUND_RESTRICTION,
JobParameters.INTERNAL_STOP_REASON_CONSTRAINTS_NOT_SATISFIED,
"uid gone");
@@ -2150,6 +3234,7 @@ public class JobSchedulerService extends com.android.server.SystemService
final boolean disabled = message.arg2 != 0;
if (disabled) {
cancelJobsForUid(uid,
+ /* includeSourceApp */ true,
JobParameters.STOP_REASON_BACKGROUND_RESTRICTION,
JobParameters.INTERNAL_STOP_REASON_CONSTRAINTS_NOT_SATISFIED,
"app uid idle");
@@ -2169,6 +3254,52 @@ public class JobSchedulerService extends com.android.server.SystemService
args.recycle();
break;
}
+
+ case MSG_INFORM_OBSERVER_OF_ALL_USER_VISIBLE_JOBS: {
+ final IUserVisibleJobObserver observer =
+ (IUserVisibleJobObserver) message.obj;
+ synchronized (mLock) {
+ for (int i = mConcurrencyManager.mActiveServices.size() - 1; i >= 0;
+ --i) {
+ JobServiceContext context =
+ mConcurrencyManager.mActiveServices.get(i);
+ final JobStatus jobStatus = context.getRunningJobLocked();
+ if (jobStatus != null && jobStatus.isUserVisibleJob()) {
+ try {
+ observer.onUserVisibleJobStateChanged(
+ jobStatus.getUserVisibleJobSummary(),
+ /* isRunning */ true);
+ } catch (RemoteException e) {
+ // Will be unregistered automatically by
+ // RemoteCallbackList's dead-object tracking,
+ // so don't need to remove it here.
+ break;
+ }
+ }
+ }
+ }
+ break;
+ }
+
+ case MSG_INFORM_OBSERVERS_OF_USER_VISIBLE_JOB_CHANGE: {
+ final SomeArgs args = (SomeArgs) message.obj;
+ final JobServiceContext context = (JobServiceContext) args.arg1;
+ final JobStatus jobStatus = (JobStatus) args.arg2;
+ final UserVisibleJobSummary summary = jobStatus.getUserVisibleJobSummary();
+ final boolean isRunning = args.argi1 == 1;
+ for (int i = mUserVisibleJobObservers.beginBroadcast() - 1; i >= 0; --i) {
+ try {
+ mUserVisibleJobObservers.getBroadcastItem(i)
+ .onUserVisibleJobStateChanged(summary, isRunning);
+ } catch (RemoteException e) {
+ // Will be unregistered automatically by RemoteCallbackList's
+ // dead-object tracking, so nothing we need to do here.
+ }
+ }
+ mUserVisibleJobObservers.finishBroadcast();
+ args.recycle();
+ break;
+ }
}
maybeRunPendingJobsLocked();
}
@@ -2251,8 +3382,6 @@ public class JobSchedulerService extends com.android.server.SystemService
Slog.d(TAG, " queued " + job.toShortString());
}
newReadyJobs.add(job);
- } else {
- evaluateControllerStatesLocked(job);
}
}
@@ -2299,8 +3428,8 @@ public class JobSchedulerService extends com.android.server.SystemService
}
final boolean shouldForceBatchJob;
- if (job.shouldTreatAsExpeditedJob()) {
- // Never batch expedited jobs, even for RESTRICTED apps.
+ if (job.shouldTreatAsExpeditedJob() || job.shouldTreatAsUserInitiatedJob()) {
+ // Never batch expedited or user-initiated jobs, even for RESTRICTED apps.
shouldForceBatchJob = false;
} else if (job.getEffectiveStandbyBucket() == RESTRICTED_INDEX) {
// Restricted jobs must always be batched
@@ -2314,7 +3443,7 @@ public class JobSchedulerService extends com.android.server.SystemService
shouldForceBatchJob =
mPrefetchController.getNextEstimatedLaunchTimeLocked(job)
> relativelySoonCutoffTime;
- } else if (job.getNumFailures() > 0) {
+ } else if (job.getNumPreviousAttempts() > 0) {
shouldForceBatchJob = false;
} else {
final long nowElapsed = sElapsedRealtimeClock.millis();
@@ -2372,7 +3501,6 @@ public class JobSchedulerService extends com.android.server.SystemService
} else if (mPendingJobQueue.remove(job)) {
noteJobNonPending(job);
}
- evaluateControllerStatesLocked(job);
}
}
@@ -2390,6 +3518,25 @@ public class JobSchedulerService extends com.android.server.SystemService
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);
+ }
+ }
+ }
}
// Be ready for next time
@@ -2479,7 +3626,7 @@ public class JobSchedulerService extends com.android.server.SystemService
@GuardedBy("mLock")
boolean isReadyToBeExecutedLocked(JobStatus job, boolean rejectActive) {
- final boolean jobReady = job.isReady();
+ final boolean jobReady = job.isReady() || evaluateControllerStatesLocked(job);
if (DEBUG) {
Slog.v(TAG, "isReadyToBeExecutedLocked: " + job.toShortString()
@@ -2536,9 +3683,9 @@ public class JobSchedulerService extends com.android.server.SystemService
}
private boolean isComponentUsable(@NonNull JobStatus job) {
- final ServiceInfo service = job.serviceInfo;
+ final String processName = job.serviceProcessName;
- if (service == null) {
+ if (processName == null) {
if (DEBUG) {
Slog.v(TAG, "isComponentUsable: " + job.toShortString()
+ " component not present");
@@ -2547,20 +3694,24 @@ public class JobSchedulerService extends com.android.server.SystemService
}
// Everything else checked out so far, so this is the final yes/no check
- final boolean appIsBad = mActivityManagerInternal.isAppBad(
- service.processName, service.applicationInfo.uid);
+ final boolean appIsBad = mActivityManagerInternal.isAppBad(processName, job.getUid());
if (DEBUG && appIsBad) {
Slog.i(TAG, "App is bad for " + job.toShortString() + " so not runnable");
}
return !appIsBad;
}
+ /**
+ * Gets each controller to evaluate the job's state
+ * and then returns the value of {@link JobStatus#isReady()}.
+ */
@VisibleForTesting
- void evaluateControllerStatesLocked(final JobStatus job) {
+ boolean evaluateControllerStatesLocked(final JobStatus job) {
for (int c = mControllers.size() - 1; c >= 0; --c) {
final StateController sc = mControllers.get(c);
sc.evaluateStateLocked(job);
}
+ return job.isReady();
}
/**
@@ -2606,13 +3757,44 @@ public class JobSchedulerService extends com.android.server.SystemService
/** Returns the minimum amount of time we should let this job run before timing out. */
public long getMinJobExecutionGuaranteeMs(JobStatus job) {
synchronized (mLock) {
- if (job.shouldTreatAsExpeditedJob()) {
+ if (job.shouldTreatAsUserInitiatedJob()
+ && checkRunUserInitiatedJobsPermission(
+ job.getSourceUid(), job.getSourcePackageName())) {
+ // The calling package is the one doing the work, so use it in the
+ // timeout quota checks.
+ final boolean isWithinTimeoutQuota = mQuotaTracker.isWithinQuota(
+ job.getTimeoutBlameUserId(), job.getTimeoutBlamePackageName(),
+ QUOTA_TRACKER_TIMEOUT_UIJ_TAG);
+ final long upperLimitMs = isWithinTimeoutQuota
+ ? mConstants.RUNTIME_UI_LIMIT_MS
+ : mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS;
+ if (job.getJob().getRequiredNetwork() != null) {
+ // User-initiated data transfers.
+ if (mConstants.RUNTIME_USE_DATA_ESTIMATES_FOR_LIMITS) {
+ final long estimatedTransferTimeMs =
+ mConnectivityController.getEstimatedTransferTimeMs(job);
+ if (estimatedTransferTimeMs == ConnectivityController.UNKNOWN_TIME) {
+ return Math.min(upperLimitMs,
+ mConstants.RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS);
+ }
+ // Try to give the job at least as much time as we think the transfer
+ // will take, but cap it at the maximum limit.
+ final long factoredTransferTimeMs = (long) (estimatedTransferTimeMs
+ * mConstants.RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR);
+ return Math.min(upperLimitMs,
+ Math.max(factoredTransferTimeMs,
+ mConstants.RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS));
+ }
+ return Math.min(upperLimitMs,
+ Math.max(mConstants.RUNTIME_MIN_UI_GUARANTEE_MS,
+ mConstants.RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS));
+ }
+ return Math.min(upperLimitMs, mConstants.RUNTIME_MIN_UI_GUARANTEE_MS);
+ } else if (job.shouldTreatAsExpeditedJob()) {
// Don't guarantee RESTRICTED jobs more than 5 minutes.
return job.getEffectiveStandbyBucket() != RESTRICTED_INDEX
? mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS
: Math.min(mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, 5 * MINUTE_IN_MILLIS);
- } else if (job.getEffectivePriority() >= JobInfo.PRIORITY_HIGH) {
- return mConstants.RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS;
} else {
return mConstants.RUNTIME_MIN_GUARANTEE_MS;
}
@@ -2622,7 +3804,26 @@ public class JobSchedulerService extends com.android.server.SystemService
/** Returns the maximum amount of time this job could run for. */
public long getMaxJobExecutionTimeMs(JobStatus job) {
synchronized (mLock) {
- return Math.min(mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS,
+ if (job.shouldTreatAsUserInitiatedJob()
+ && checkRunUserInitiatedJobsPermission(
+ job.getSourceUid(), job.getSourcePackageName())
+ && mQuotaTracker.isWithinQuota(job.getTimeoutBlameUserId(),
+ job.getTimeoutBlamePackageName(),
+ QUOTA_TRACKER_TIMEOUT_UIJ_TAG)) {
+ return mConstants.RUNTIME_UI_LIMIT_MS;
+ }
+ if (job.shouldTreatAsUserInitiatedJob()) {
+ return mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS;
+ }
+ // Only let the app use the higher runtime if it hasn't repeatedly timed out.
+ final String timeoutTag = job.shouldTreatAsExpeditedJob()
+ ? QUOTA_TRACKER_TIMEOUT_EJ_TAG : QUOTA_TRACKER_TIMEOUT_REG_TAG;
+ final long upperLimitMs =
+ mQuotaTracker.isWithinQuota(job.getTimeoutBlameUserId(),
+ job.getTimeoutBlamePackageName(), timeoutTag)
+ ? mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS
+ : mConstants.RUNTIME_MIN_GUARANTEE_MS;
+ return Math.min(upperLimitMs,
mConstants.USE_TARE_POLICY
? mTareController.getMaxJobExecutionTimeMsLocked(job)
: mQuotaController.getMaxJobExecutionTimeMsLocked(job));
@@ -2666,6 +3867,16 @@ public class JobSchedulerService extends com.android.server.SystemService
return adjustJobBias(bias, job);
}
+ void informObserversOfUserVisibleJobChange(JobServiceContext context, JobStatus jobStatus,
+ boolean isRunning) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = context;
+ args.arg2 = jobStatus;
+ args.argi1 = isRunning ? 1 : 0;
+ mHandler.obtainMessage(MSG_INFORM_OBSERVERS_OF_USER_VISIBLE_JOB_CHANGE, args)
+ .sendToTarget();
+ }
+
private final class BatteryStateTracker extends BroadcastReceiver {
/**
* Track whether we're "charging", where charging means that we're ready to commit to
@@ -2791,27 +4002,26 @@ public class JobSchedulerService extends com.android.server.SystemService
final class LocalService implements JobSchedulerInternal {
- /**
- * Returns a list of all pending jobs. A running job is not considered pending. Periodic
- * jobs are always considered pending.
- */
@Override
- public List<JobInfo> getSystemScheduledPendingJobs() {
+ public List<JobInfo> getSystemScheduledOwnJobs(@Nullable String namespace) {
synchronized (mLock) {
- final List<JobInfo> pendingJobs = new ArrayList<JobInfo>();
+ final List<JobInfo> ownJobs = new ArrayList<>();
mJobs.forEachJob(Process.SYSTEM_UID, (job) -> {
- if (job.getJob().isPeriodic() || !mConcurrencyManager.isJobRunningLocked(job)) {
- pendingJobs.add(job.getJob());
+ if (job.getSourceUid() == Process.SYSTEM_UID
+ && Objects.equals(job.getNamespace(), namespace)
+ && "android".equals(job.getSourcePackageName())) {
+ ownJobs.add(job.getJob());
}
});
- return pendingJobs;
+ return ownJobs;
}
}
@Override
- public void cancelJobsForUid(int uid, @JobParameters.StopReason int reason,
- int internalReasonCode, String debugReason) {
- JobSchedulerService.this.cancelJobsForUid(uid, reason, internalReasonCode, debugReason);
+ public void cancelJobsForUid(int uid, boolean includeProxiedJobs,
+ @JobParameters.StopReason int reason, int internalReasonCode, String debugReason) {
+ JobSchedulerService.this.cancelJobsForUid(uid,
+ includeProxiedJobs, reason, internalReasonCode, debugReason);
}
@Override
@@ -2857,6 +4067,37 @@ public class JobSchedulerService extends com.android.server.SystemService
}
@Override
+ public boolean isAppConsideredBuggy(int callingUserId, @NonNull String callingPackageName,
+ int timeoutBlameUserId, @NonNull String timeoutBlamePackageName) {
+ return !mQuotaTracker.isWithinQuota(callingUserId, callingPackageName,
+ QUOTA_TRACKER_ANR_TAG)
+ || !mQuotaTracker.isWithinQuota(callingUserId, callingPackageName,
+ QUOTA_TRACKER_SCHEDULE_PERSISTED_TAG)
+ || !mQuotaTracker.isWithinQuota(timeoutBlameUserId, timeoutBlamePackageName,
+ QUOTA_TRACKER_TIMEOUT_TOTAL_TAG);
+ }
+
+ @Override
+ public boolean isNotificationAssociatedWithAnyUserInitiatedJobs(int notificationId,
+ int userId, @NonNull String packageName) {
+ if (packageName == null) {
+ return false;
+ }
+ return mConcurrencyManager.isNotificationAssociatedWithAnyUserInitiatedJobs(
+ notificationId, userId, packageName);
+ }
+
+ @Override
+ public boolean isNotificationChannelAssociatedWithAnyUserInitiatedJobs(
+ @NonNull String notificationChannel, int userId, @NonNull String packageName) {
+ if (packageName == null || notificationChannel == null) {
+ return false;
+ }
+ return mConcurrencyManager.isNotificationChannelAssociatedWithAnyUserInitiatedJobs(
+ notificationChannel, userId, packageName);
+ }
+
+ @Override
public JobStorePersistStats getPersistStats() {
synchronized (mLock) {
return new JobStorePersistStats(mJobs.getPersistStats());
@@ -2956,6 +4197,18 @@ public class JobSchedulerService extends com.android.server.SystemService
return bucket;
}
+ static int safelyScaleBytesToKBForHistogram(long bytes) {
+ long kilobytes = bytes / 1000;
+ // Anything over Integer.MAX_VALUE or under Integer.MIN_VALUE isn't expected and will
+ // be put into the overflow buckets.
+ if (kilobytes > Integer.MAX_VALUE) {
+ return Integer.MAX_VALUE;
+ } else if (kilobytes < Integer.MIN_VALUE) {
+ return Integer.MIN_VALUE;
+ }
+ return (int) kilobytes;
+ }
+
private class CloudProviderChangeListener implements
StorageManagerInternal.CloudProviderChangeListener {
@@ -2986,19 +4239,39 @@ public class JobSchedulerService extends com.android.server.SystemService
}
/**
+ * Returns whether the app has the permission granted.
+ * This currently only works for normal permissions and <b>DOES NOT</b> work for runtime
+ * permissions.
+ * TODO: handle runtime permissions
+ */
+ private boolean hasPermission(int uid, int pid, @NonNull String permission) {
+ synchronized (mPermissionCache) {
+ SparseArrayMap<String, Boolean> pidPermissions = mPermissionCache.get(uid);
+ if (pidPermissions == null) {
+ pidPermissions = new SparseArrayMap<>();
+ mPermissionCache.put(uid, pidPermissions);
+ }
+ final Boolean cached = pidPermissions.get(pid, permission);
+ if (cached != null) {
+ return cached;
+ }
+
+ final int result = getContext().checkPermission(permission, pid, uid);
+ final boolean permissionGranted = (result == PackageManager.PERMISSION_GRANTED);
+ pidPermissions.add(pid, permission, permissionGranted);
+ return permissionGranted;
+ }
+ }
+
+ /**
* Binder stub trampoline implementation
*/
final class JobSchedulerStub extends IJobScheduler.Stub {
- /**
- * Cache determination of whether a given app can persist jobs
- * key is uid of the calling app; value is undetermined/true/false
- */
- private final SparseArray<Boolean> mPersistCache = new SparseArray<Boolean>();
-
// Enforce that only the app itself (or shared uid participant) can schedule a
// job that runs one of the app's services, as well as verifying that the
// named service properly requires the BIND_JOB_SERVICE permission
- private void enforceValidJobRequest(int uid, JobInfo job) {
+ // TODO(141645789): merge enforceValidJobRequest() with validateJob()
+ private void enforceValidJobRequest(int uid, int pid, JobInfo job) {
final PackageManager pm = getContext()
.createContextAsUser(UserHandle.getUserHandleForUid(uid), 0)
.getPackageManager();
@@ -3022,33 +4295,38 @@ public class JobSchedulerService extends com.android.server.SystemService
throw new IllegalArgumentException(
"Tried to schedule job for non-existent component: " + service);
}
- }
-
- private boolean canPersistJobs(int pid, int uid) {
// If we get this far we're good to go; all we need to do now is check
// whether the app is allowed to persist its scheduled work.
- final boolean canPersist;
- synchronized (mPersistCache) {
- Boolean cached = mPersistCache.get(uid);
- if (cached != null) {
- canPersist = cached.booleanValue();
- } else {
- // Persisting jobs is tantamount to running at boot, so we permit
- // it when the app has declared that it uses the RECEIVE_BOOT_COMPLETED
- // permission
- int result = getContext().checkPermission(
- android.Manifest.permission.RECEIVE_BOOT_COMPLETED, pid, uid);
- canPersist = (result == PackageManager.PERMISSION_GRANTED);
- mPersistCache.put(uid, canPersist);
+ if (job.isPersisted() && !canPersistJobs(pid, uid)) {
+ throw new IllegalArgumentException("Requested job cannot be persisted without"
+ + " holding android.permission.RECEIVE_BOOT_COMPLETED permission");
+ }
+ if (job.getRequiredNetwork() != null
+ && CompatChanges.isChangeEnabled(
+ REQUIRE_NETWORK_PERMISSIONS_FOR_CONNECTIVITY_JOBS, uid)) {
+ if (!hasPermission(uid, pid, Manifest.permission.ACCESS_NETWORK_STATE)) {
+ throw new SecurityException(Manifest.permission.ACCESS_NETWORK_STATE
+ + " required for jobs with a connectivity constraint");
}
}
- return canPersist;
}
- private void validateJobFlags(JobInfo job, int callingUid) {
+ private boolean canPersistJobs(int pid, int uid) {
+ // Persisting jobs is tantamount to running at boot, so we permit
+ // it when the app has declared that it uses the RECEIVE_BOOT_COMPLETED
+ // permission
+ return hasPermission(uid, pid, Manifest.permission.RECEIVE_BOOT_COMPLETED);
+ }
+
+ private int validateJob(@NonNull JobInfo job, int callingUid, int callingPid,
+ int sourceUserId,
+ @Nullable String sourcePkgName, @Nullable JobWorkItem jobWorkItem) {
+ final boolean rejectNegativeNetworkEstimates = CompatChanges.isChangeEnabled(
+ JobInfo.REJECT_NEGATIVE_NETWORK_ESTIMATES, callingUid);
job.enforceValidity(
CompatChanges.isChangeEnabled(
- JobInfo.DISALLOW_DEADLINES_FOR_PREFETCH_JOBS, callingUid));
+ JobInfo.DISALLOW_DEADLINES_FOR_PREFETCH_JOBS, callingUid),
+ rejectNegativeNetworkEstimates);
if ((job.getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0) {
getContext().enforceCallingOrSelfPermission(
android.Manifest.permission.CONNECTIVITY_INTERNAL, TAG);
@@ -3062,11 +4340,142 @@ public class JobSchedulerService extends com.android.server.SystemService
+ " FLAG_EXEMPT_FROM_APP_STANDBY. Job=" + job);
}
}
+ if (job.isUserInitiated()) {
+ int sourceUid = -1;
+ if (sourceUserId != -1 && sourcePkgName != null) {
+ try {
+ sourceUid = AppGlobals.getPackageManager().getPackageUid(
+ sourcePkgName, 0, sourceUserId);
+ } catch (RemoteException ex) {
+ // Can't happen, PackageManager runs in the same process.
+ }
+ }
+ // We aim to check the permission of both the source and calling app so that apps
+ // don't attempt to bypass the permission by using other apps to do the work.
+ boolean isInStateToScheduleUiJobSource = false;
+ final String callingPkgName = job.getService().getPackageName();
+ if (sourceUid != -1) {
+ // Check the permission of the source app.
+ final int sourceResult =
+ validateRunUserInitiatedJobsPermission(sourceUid, sourcePkgName);
+ if (sourceResult != JobScheduler.RESULT_SUCCESS) {
+ return sourceResult;
+ }
+ final int sourcePid =
+ callingUid == sourceUid && callingPkgName.equals(sourcePkgName)
+ ? callingPid : -1;
+ isInStateToScheduleUiJobSource = isInStateToScheduleUserInitiatedJobs(
+ sourceUid, sourcePid, sourcePkgName);
+ }
+ boolean isInStateToScheduleUiJobCalling = false;
+ if (callingUid != sourceUid || !callingPkgName.equals(sourcePkgName)) {
+ // Source app is different from calling app. Make sure the calling app also has
+ // the permission.
+ final int callingResult =
+ validateRunUserInitiatedJobsPermission(callingUid, callingPkgName);
+ if (callingResult != JobScheduler.RESULT_SUCCESS) {
+ return callingResult;
+ }
+ // Avoid rechecking the state if the source app is able to schedule the job.
+ if (!isInStateToScheduleUiJobSource) {
+ isInStateToScheduleUiJobCalling = isInStateToScheduleUserInitiatedJobs(
+ callingUid, callingPid, callingPkgName);
+ }
+ }
+
+ if (!isInStateToScheduleUiJobSource && !isInStateToScheduleUiJobCalling) {
+ Slog.e(TAG, "Uid(s) " + sourceUid + "/" + callingUid
+ + " not in a state to schedule user-initiated jobs");
+ Counter.logIncrementWithUid(
+ "job_scheduler.value_cntr_w_uid_schedule_failure_uij_invalid_state",
+ callingUid);
+ return JobScheduler.RESULT_FAILURE;
+ }
+ }
+ if (jobWorkItem != null) {
+ jobWorkItem.enforceValidity(rejectNegativeNetworkEstimates);
+ if (jobWorkItem.getEstimatedNetworkDownloadBytes() != JobInfo.NETWORK_BYTES_UNKNOWN
+ || jobWorkItem.getEstimatedNetworkUploadBytes()
+ != JobInfo.NETWORK_BYTES_UNKNOWN
+ || jobWorkItem.getMinimumNetworkChunkBytes()
+ != JobInfo.NETWORK_BYTES_UNKNOWN) {
+ if (job.getRequiredNetwork() == null) {
+ final String errorMsg = "JobWorkItem implies network usage"
+ + " but job doesn't specify a network constraint";
+ if (CompatChanges.isChangeEnabled(
+ REQUIRE_NETWORK_CONSTRAINT_FOR_NETWORK_JOB_WORK_ITEMS,
+ callingUid)) {
+ throw new IllegalArgumentException(errorMsg);
+ } else {
+ Slog.e(TAG, errorMsg);
+ }
+ }
+ }
+ if (job.isPersisted()) {
+ // Intent.saveToXml() doesn't persist everything, so just reject all
+ // JobWorkItems with Intents to be safe/predictable.
+ if (jobWorkItem.getIntent() != null) {
+ throw new IllegalArgumentException(
+ "Cannot persist JobWorkItems with Intents");
+ }
+ }
+ }
+ return JobScheduler.RESULT_SUCCESS;
+ }
+
+ /** Returns a sanitized namespace if valid, or throws an exception if not. */
+ private String validateNamespace(@Nullable String namespace) {
+ namespace = JobScheduler.sanitizeNamespace(namespace);
+ if (namespace != null) {
+ if (namespace.isEmpty()) {
+ throw new IllegalArgumentException("namespace cannot be empty");
+ }
+ if (namespace.length() > 1000) {
+ throw new IllegalArgumentException(
+ "namespace cannot be more than 1000 characters");
+ }
+ namespace = namespace.intern();
+ }
+ return namespace;
+ }
+
+ private int validateRunUserInitiatedJobsPermission(int uid, String packageName) {
+ final int state = getRunUserInitiatedJobsPermissionState(uid, packageName);
+ if (state == PermissionChecker.PERMISSION_HARD_DENIED) {
+ Counter.logIncrementWithUid(
+ "job_scheduler.value_cntr_w_uid_schedule_failure_uij_no_permission", uid);
+ throw new SecurityException(android.Manifest.permission.RUN_USER_INITIATED_JOBS
+ + " required to schedule user-initiated jobs.");
+ }
+ if (state == PermissionChecker.PERMISSION_SOFT_DENIED) {
+ Counter.logIncrementWithUid(
+ "job_scheduler.value_cntr_w_uid_schedule_failure_uij_no_permission", uid);
+ return JobScheduler.RESULT_FAILURE;
+ }
+ return JobScheduler.RESULT_SUCCESS;
+ }
+
+ private boolean isInStateToScheduleUserInitiatedJobs(int uid, int pid, String pkgName) {
+ final int procState = mActivityManagerInternal.getUidProcessState(uid);
+ if (DEBUG) {
+ Slog.d(TAG, "Uid " + uid + " proc state="
+ + ActivityManager.procStateToString(procState));
+ }
+ if (procState == ActivityManager.PROCESS_STATE_TOP) {
+ return true;
+ }
+ final boolean canScheduleUiJobsInBg =
+ mActivityManagerInternal.canScheduleUserInitiatedJobs(uid, pid, pkgName);
+ if (DEBUG) {
+ Slog.d(TAG, "Uid " + uid
+ + " AM.canScheduleUserInitiatedJobs= " + canScheduleUiJobsInBg);
+ }
+ return canScheduleUiJobsInBg;
}
// IJobScheduler implementation
@Override
- public int schedule(JobInfo job) throws RemoteException {
+ public int schedule(String namespace, JobInfo job) throws RemoteException {
if (DEBUG) {
Slog.d(TAG, "Scheduling job: " + job.toString());
}
@@ -3074,20 +4483,19 @@ public class JobSchedulerService extends com.android.server.SystemService
final int uid = Binder.getCallingUid();
final int userId = UserHandle.getUserId(uid);
- enforceValidJobRequest(uid, job);
- if (job.isPersisted()) {
- if (!canPersistJobs(pid, uid)) {
- throw new IllegalArgumentException("Error: requested job be persisted without"
- + " holding RECEIVE_BOOT_COMPLETED permission.");
- }
+ enforceValidJobRequest(uid, pid, job);
+
+ final int result = validateJob(job, uid, pid, -1, null, null);
+ if (result != JobScheduler.RESULT_SUCCESS) {
+ return result;
}
- validateJobFlags(job, uid);
+ namespace = validateNamespace(namespace);
final long ident = Binder.clearCallingIdentity();
try {
return JobSchedulerService.this.scheduleAsPackage(job, null, uid, null, userId,
- null);
+ namespace, null);
} finally {
Binder.restoreCallingIdentity(ident);
}
@@ -3095,37 +4503,40 @@ public class JobSchedulerService extends com.android.server.SystemService
// IJobScheduler implementation
@Override
- public int enqueue(JobInfo job, JobWorkItem work) throws RemoteException {
+ public int enqueue(String namespace, JobInfo job, JobWorkItem work) throws RemoteException {
if (DEBUG) {
Slog.d(TAG, "Enqueueing job: " + job.toString() + " work: " + work);
}
final int uid = Binder.getCallingUid();
+ final int pid = Binder.getCallingPid();
final int userId = UserHandle.getUserId(uid);
- enforceValidJobRequest(uid, job);
- if (job.isPersisted()) {
- throw new IllegalArgumentException("Can't enqueue work for persisted jobs");
- }
+ enforceValidJobRequest(uid, pid, job);
if (work == null) {
throw new NullPointerException("work is null");
}
- work.enforceValidity();
- validateJobFlags(job, uid);
+ final int result = validateJob(job, uid, pid, -1, null, work);
+ if (result != JobScheduler.RESULT_SUCCESS) {
+ return result;
+ }
+
+ namespace = validateNamespace(namespace);
final long ident = Binder.clearCallingIdentity();
try {
return JobSchedulerService.this.scheduleAsPackage(job, work, uid, null, userId,
- null);
+ namespace, null);
} finally {
Binder.restoreCallingIdentity(ident);
}
}
@Override
- public int scheduleAsPackage(JobInfo job, String packageName, int userId, String tag)
- throws RemoteException {
+ public int scheduleAsPackage(String namespace, JobInfo job, String packageName, int userId,
+ String tag) throws RemoteException {
final int callerUid = Binder.getCallingUid();
+ final int callerPid = Binder.getCallingPid();
if (DEBUG) {
Slog.d(TAG, "Caller uid " + callerUid + " scheduling job: " + job.toString()
+ " on behalf of " + packageName + "/");
@@ -3142,36 +4553,78 @@ public class JobSchedulerService extends com.android.server.SystemService
+ " not permitted to schedule jobs for other apps");
}
- validateJobFlags(job, callerUid);
+ enforceValidJobRequest(callerUid, callerPid, job);
+
+ int result = validateJob(job, callerUid, callerPid, userId, packageName, null);
+ if (result != JobScheduler.RESULT_SUCCESS) {
+ return result;
+ }
+
+ namespace = validateNamespace(namespace);
final long ident = Binder.clearCallingIdentity();
try {
return JobSchedulerService.this.scheduleAsPackage(job, null, callerUid,
- packageName, userId, tag);
+ packageName, userId, namespace, tag);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ @Override
+ public Map<String, ParceledListSlice<JobInfo>> getAllPendingJobs() throws RemoteException {
+ final int uid = Binder.getCallingUid();
+
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ final ArrayMap<String, List<JobInfo>> jobs =
+ JobSchedulerService.this.getPendingJobs(uid);
+ final ArrayMap<String, ParceledListSlice<JobInfo>> outMap = new ArrayMap<>();
+ for (int i = 0; i < jobs.size(); ++i) {
+ outMap.put(jobs.keyAt(i), new ParceledListSlice<>(jobs.valueAt(i)));
+ }
+ return outMap;
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ @Override
+ public ParceledListSlice<JobInfo> getAllPendingJobsInNamespace(String namespace)
+ throws RemoteException {
+ final int uid = Binder.getCallingUid();
+
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ return new ParceledListSlice<>(
+ JobSchedulerService.this.getPendingJobsInNamespace(uid,
+ validateNamespace(namespace)));
} finally {
Binder.restoreCallingIdentity(ident);
}
}
@Override
- public ParceledListSlice<JobInfo> getAllPendingJobs() throws RemoteException {
+ public JobInfo getPendingJob(String namespace, int jobId) throws RemoteException {
final int uid = Binder.getCallingUid();
final long ident = Binder.clearCallingIdentity();
try {
- return new ParceledListSlice<>(JobSchedulerService.this.getPendingJobs(uid));
+ return JobSchedulerService.this.getPendingJob(
+ uid, validateNamespace(namespace), jobId);
} finally {
Binder.restoreCallingIdentity(ident);
}
}
@Override
- public JobInfo getPendingJob(int jobId) throws RemoteException {
+ public int getPendingJobReason(String namespace, int jobId) throws RemoteException {
final int uid = Binder.getCallingUid();
final long ident = Binder.clearCallingIdentity();
try {
- return JobSchedulerService.this.getPendingJob(uid, jobId);
+ return JobSchedulerService.this.getPendingJobReason(
+ uid, validateNamespace(namespace), jobId);
} finally {
Binder.restoreCallingIdentity(ident);
}
@@ -3183,6 +4636,8 @@ public class JobSchedulerService extends com.android.server.SystemService
final long ident = Binder.clearCallingIdentity();
try {
JobSchedulerService.this.cancelJobsForUid(uid,
+ // Documentation says only jobs scheduled BY the app will be cancelled
+ /* includeSourceApp */ false,
JobParameters.STOP_REASON_CANCELLED_BY_APP,
JobParameters.INTERNAL_STOP_REASON_CANCELED,
"cancelAll() called by app, callingUid=" + uid);
@@ -3192,18 +4647,60 @@ public class JobSchedulerService extends com.android.server.SystemService
}
@Override
- public void cancel(int jobId) throws RemoteException {
+ public void cancelAllInNamespace(String namespace) throws RemoteException {
+ final int uid = Binder.getCallingUid();
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ JobSchedulerService.this.cancelJobsForUid(uid,
+ // Documentation says only jobs scheduled BY the app will be cancelled
+ /* includeSourceApp */ false,
+ /* namespaceOnly */ true, validateNamespace(namespace),
+ JobParameters.STOP_REASON_CANCELLED_BY_APP,
+ JobParameters.INTERNAL_STOP_REASON_CANCELED,
+ "cancelAllInNamespace() called by app, callingUid=" + uid);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ @Override
+ public void cancel(String namespace, int jobId) throws RemoteException {
final int uid = Binder.getCallingUid();
final long ident = Binder.clearCallingIdentity();
try {
- JobSchedulerService.this.cancelJob(uid, jobId, uid,
+ JobSchedulerService.this.cancelJob(uid, validateNamespace(namespace), jobId, uid,
JobParameters.STOP_REASON_CANCELLED_BY_APP);
} finally {
Binder.restoreCallingIdentity(ident);
}
}
+ public boolean canRunUserInitiatedJobs(@NonNull String packageName) {
+ final int callingUid = Binder.getCallingUid();
+ final int userId = UserHandle.getUserId(callingUid);
+ final int packageUid = mLocalPM.getPackageUid(packageName, 0, userId);
+ if (callingUid != packageUid) {
+ throw new SecurityException("Uid " + callingUid
+ + " cannot query canRunUserInitiatedJobs for package " + packageName);
+ }
+
+ return checkRunUserInitiatedJobsPermission(packageUid, packageName);
+ }
+
+ public boolean hasRunUserInitiatedJobsPermission(@NonNull String packageName,
+ @UserIdInt int userId) {
+ final int uid = mLocalPM.getPackageUid(packageName, 0, userId);
+ final int callingUid = Binder.getCallingUid();
+ if (callingUid != uid && !UserHandle.isCore(callingUid)) {
+ throw new SecurityException("Uid " + callingUid
+ + " cannot query hasRunUserInitiatedJobsPermission for package "
+ + packageName);
+ }
+
+ return checkRunUserInitiatedJobsPermission(uid, packageName);
+ }
+
/**
* "dumpsys" infrastructure
*/
@@ -3314,11 +4811,45 @@ public class JobSchedulerService extends com.android.server.SystemService
return new ParceledListSlice<>(snapshots);
}
}
+
+ @Override
+ @EnforcePermission(allOf = {MANAGE_ACTIVITY_TASKS, INTERACT_ACROSS_USERS_FULL})
+ public void registerUserVisibleJobObserver(@NonNull IUserVisibleJobObserver observer) {
+ super.registerUserVisibleJobObserver_enforcePermission();
+ if (observer == null) {
+ throw new NullPointerException("observer");
+ }
+ mUserVisibleJobObservers.register(observer);
+ mHandler.obtainMessage(MSG_INFORM_OBSERVER_OF_ALL_USER_VISIBLE_JOBS, observer)
+ .sendToTarget();
+ }
+
+ @Override
+ @EnforcePermission(allOf = {MANAGE_ACTIVITY_TASKS, INTERACT_ACROSS_USERS_FULL})
+ public void unregisterUserVisibleJobObserver(@NonNull IUserVisibleJobObserver observer) {
+ super.unregisterUserVisibleJobObserver_enforcePermission();
+ if (observer == null) {
+ throw new NullPointerException("observer");
+ }
+ mUserVisibleJobObservers.unregister(observer);
+ }
+
+ @Override
+ @EnforcePermission(allOf = {MANAGE_ACTIVITY_TASKS, INTERACT_ACROSS_USERS_FULL})
+ public void notePendingUserRequestedAppStop(@NonNull String packageName, int userId,
+ @Nullable String debugReason) {
+ super.notePendingUserRequestedAppStop_enforcePermission();
+ if (packageName == null) {
+ throw new NullPointerException("packageName");
+ }
+ notePendingUserRequestedAppStopInternal(packageName, userId, debugReason);
+ }
}
// Shell command infrastructure: run the given job immediately
- int executeRunCommand(String pkgName, int userId, int jobId, boolean satisfied, boolean force) {
- Slog.d(TAG, "executeRunCommand(): " + pkgName + "/" + userId
+ int executeRunCommand(String pkgName, int userId, @Nullable String namespace,
+ int jobId, boolean satisfied, boolean force) {
+ Slog.d(TAG, "executeRunCommand(): " + pkgName + "/" + namespace + "/" + userId
+ " " + jobId + " s=" + satisfied + " f=" + force);
try {
@@ -3329,7 +4860,7 @@ public class JobSchedulerService extends com.android.server.SystemService
}
synchronized (mLock) {
- final JobStatus js = mJobs.getJobByUidAndJobId(uid, jobId);
+ final JobStatus js = mJobs.getJobByUidAndJobId(uid, namespace, jobId);
if (js == null) {
return JobSchedulerShellCommand.CMD_ERR_NO_JOB;
}
@@ -3358,15 +4889,18 @@ public class JobSchedulerService extends com.android.server.SystemService
}
// Shell command infrastructure: immediately timeout currently executing jobs
- int executeTimeoutCommand(PrintWriter pw, String pkgName, int userId,
- boolean hasJobId, int jobId) {
+ int executeStopCommand(PrintWriter pw, String pkgName, int userId,
+ @Nullable String namespace, boolean hasJobId, int jobId,
+ int stopReason, int internalStopReason) {
if (DEBUG) {
- Slog.v(TAG, "executeTimeoutCommand(): " + pkgName + "/" + userId + " " + jobId);
+ Slog.v(TAG, "executeStopJobCommand(): " + pkgName + "/" + userId + " " + jobId
+ + ": " + stopReason + "("
+ + JobParameters.getInternalReasonCodeDescription(internalStopReason) + ")");
}
synchronized (mLock) {
- final boolean foundSome = mConcurrencyManager.executeTimeoutCommandLocked(pw,
- pkgName, userId, hasJobId, jobId);
+ final boolean foundSome = mConcurrencyManager.executeStopCommandLocked(pw,
+ pkgName, userId, namespace, hasJobId, jobId, stopReason, internalStopReason);
if (!foundSome) {
pw.println("No matching executing jobs found.");
}
@@ -3375,7 +4909,7 @@ public class JobSchedulerService extends com.android.server.SystemService
}
// Shell command infrastructure: cancel a scheduled job
- int executeCancelCommand(PrintWriter pw, String pkgName, int userId,
+ int executeCancelCommand(PrintWriter pw, String pkgName, int userId, @Nullable String namespace,
boolean hasJobId, int jobId) {
if (DEBUG) {
Slog.v(TAG, "executeCancelCommand(): " + pkgName + "/" + userId + " " + jobId);
@@ -3394,14 +4928,17 @@ public class JobSchedulerService extends com.android.server.SystemService
if (!hasJobId) {
pw.println("Canceling all jobs for " + pkgName + " in user " + userId);
- if (!cancelJobsForUid(pkgUid, JobParameters.STOP_REASON_USER,
+ if (!cancelJobsForUid(pkgUid,
+ /* includeSourceApp */ false,
+ JobParameters.STOP_REASON_USER,
JobParameters.INTERNAL_STOP_REASON_CANCELED,
"cancel shell command for package")) {
pw.println("No matching jobs found.");
}
} else {
pw.println("Canceling job " + pkgName + "/#" + jobId + " in user " + userId);
- if (!cancelJob(pkgUid, jobId, Process.SHELL_UID, JobParameters.STOP_REASON_USER)) {
+ if (!cancelJob(pkgUid, namespace, jobId,
+ Process.SHELL_UID, JobParameters.STOP_REASON_USER)) {
pw.println("No matching job found.");
}
}
@@ -3437,36 +4974,167 @@ public class JobSchedulerService extends com.android.server.SystemService
int getStorageSeq() {
synchronized (mLock) {
- return mStorageController != null ? mStorageController.getTracker().getSeq() : -1;
+ return mStorageController.getTracker().getSeq();
}
}
boolean getStorageNotLow() {
synchronized (mLock) {
- return mStorageController != null
- ? mStorageController.getTracker().isStorageNotLow() : false;
+ return mStorageController.getTracker().isStorageNotLow();
}
}
- // Shell command infrastructure
- int getJobState(PrintWriter pw, String pkgName, int userId, int jobId) {
+ int getEstimatedNetworkBytes(PrintWriter pw, String pkgName, int userId, String namespace,
+ int jobId, int byteOption) {
try {
final int uid = AppGlobals.getPackageManager().getPackageUid(pkgName, 0,
userId != UserHandle.USER_ALL ? userId : UserHandle.USER_SYSTEM);
if (uid < 0) {
- pw.print("unknown("); pw.print(pkgName); pw.println(")");
+ pw.print("unknown(");
+ pw.print(pkgName);
+ pw.println(")");
return JobSchedulerShellCommand.CMD_ERR_NO_PACKAGE;
}
synchronized (mLock) {
- final JobStatus js = mJobs.getJobByUidAndJobId(uid, jobId);
- if (DEBUG) Slog.d(TAG, "get-job-state " + uid + "/" + jobId + ": " + js);
+ final JobStatus js = mJobs.getJobByUidAndJobId(uid, namespace, jobId);
+ if (DEBUG) {
+ Slog.d(TAG, "get-estimated-network-bytes " + uid + "/"
+ + namespace + "/" + jobId + ": " + js);
+ }
+ if (js == null) {
+ pw.print("unknown("); UserHandle.formatUid(pw, uid);
+ pw.print("/jid"); pw.print(jobId); pw.println(")");
+ return JobSchedulerShellCommand.CMD_ERR_NO_JOB;
+ }
+
+ final long downloadBytes;
+ final long uploadBytes;
+ final Pair<Long, Long> bytes = mConcurrencyManager.getEstimatedNetworkBytesLocked(
+ pkgName, uid, namespace, jobId);
+ if (bytes == null) {
+ downloadBytes = js.getEstimatedNetworkDownloadBytes();
+ uploadBytes = js.getEstimatedNetworkUploadBytes();
+ } else {
+ downloadBytes = bytes.first;
+ uploadBytes = bytes.second;
+ }
+ if (byteOption == JobSchedulerShellCommand.BYTE_OPTION_DOWNLOAD) {
+ pw.println(downloadBytes);
+ } else {
+ pw.println(uploadBytes);
+ }
+ pw.println();
+ }
+ } catch (RemoteException e) {
+ // can't happen
+ }
+ return 0;
+ }
+
+ int getTransferredNetworkBytes(PrintWriter pw, String pkgName, int userId, String namespace,
+ int jobId, int byteOption) {
+ try {
+ final int uid = AppGlobals.getPackageManager().getPackageUid(pkgName, 0,
+ userId != UserHandle.USER_ALL ? userId : UserHandle.USER_SYSTEM);
+ if (uid < 0) {
+ pw.print("unknown(");
+ pw.print(pkgName);
+ pw.println(")");
+ return JobSchedulerShellCommand.CMD_ERR_NO_PACKAGE;
+ }
+
+ synchronized (mLock) {
+ final JobStatus js = mJobs.getJobByUidAndJobId(uid, namespace, jobId);
+ if (DEBUG) {
+ Slog.d(TAG, "get-transferred-network-bytes " + uid
+ + namespace + "/" + "/" + jobId + ": " + js);
+ }
if (js == null) {
pw.print("unknown("); UserHandle.formatUid(pw, uid);
pw.print("/jid"); pw.print(jobId); pw.println(")");
return JobSchedulerShellCommand.CMD_ERR_NO_JOB;
}
+ final long downloadBytes;
+ final long uploadBytes;
+ final Pair<Long, Long> bytes = mConcurrencyManager.getTransferredNetworkBytesLocked(
+ pkgName, uid, namespace, jobId);
+ if (bytes == null) {
+ downloadBytes = 0;
+ uploadBytes = 0;
+ } else {
+ downloadBytes = bytes.first;
+ uploadBytes = bytes.second;
+ }
+ if (byteOption == JobSchedulerShellCommand.BYTE_OPTION_DOWNLOAD) {
+ pw.println(downloadBytes);
+ } else {
+ pw.println(uploadBytes);
+ }
+ pw.println();
+ }
+ } catch (RemoteException e) {
+ // can't happen
+ }
+ return 0;
+ }
+
+ /** Returns true if both the appop and permission are granted. */
+ private boolean checkRunUserInitiatedJobsPermission(int packageUid, String packageName) {
+ return getRunUserInitiatedJobsPermissionState(packageUid, packageName)
+ == PermissionChecker.PERMISSION_GRANTED;
+ }
+
+ private int getRunUserInitiatedJobsPermissionState(int packageUid, String packageName) {
+ return PermissionChecker.checkPermissionForPreflight(getTestableContext(),
+ android.Manifest.permission.RUN_USER_INITIATED_JOBS, PermissionChecker.PID_UNKNOWN,
+ packageUid, packageName);
+ }
+
+ @VisibleForTesting
+ protected ConnectivityController getConnectivityController() {
+ return mConnectivityController;
+ }
+
+ @VisibleForTesting
+ protected QuotaController getQuotaController() {
+ return mQuotaController;
+ }
+
+ @VisibleForTesting
+ protected TareController getTareController() {
+ return mTareController;
+ }
+
+ // Shell command infrastructure
+ int getJobState(PrintWriter pw, String pkgName, int userId, @Nullable String namespace,
+ int jobId) {
+ try {
+ final int uid = AppGlobals.getPackageManager().getPackageUid(pkgName, 0,
+ userId != UserHandle.USER_ALL ? userId : UserHandle.USER_SYSTEM);
+ if (uid < 0) {
+ pw.print("unknown(");
+ pw.print(pkgName);
+ pw.println(")");
+ return JobSchedulerShellCommand.CMD_ERR_NO_PACKAGE;
+ }
+
+ synchronized (mLock) {
+ final JobStatus js = mJobs.getJobByUidAndJobId(uid, namespace, jobId);
+ if (DEBUG) {
+ Slog.d(TAG,
+ "get-job-state " + namespace + "/" + uid + "/" + jobId + ": " + js);
+ }
+ if (js == null) {
+ pw.print("unknown(");
+ UserHandle.formatUid(pw, uid);
+ pw.print("/jid");
+ pw.print(jobId);
+ pw.println(")");
+ return JobSchedulerShellCommand.CMD_ERR_NO_JOB;
+ }
+
boolean printed = false;
if (mPendingJobQueue.contains(js)) {
pw.print("pending");
@@ -3639,7 +5307,9 @@ public class JobSchedulerService extends com.android.server.SystemService
}
jobPrinted = true;
- pw.print("JOB #"); job.printUniqueId(pw); pw.print(": ");
+ pw.print("JOB ");
+ job.printUniqueId(pw);
+ pw.print(": ");
pw.println(job.toShortStringExceptUniqueId());
pw.increaseIndent();
@@ -3695,6 +5365,25 @@ public class JobSchedulerService extends com.android.server.SystemService
pw.decreaseIndent();
}
+ boolean procStatePrinted = false;
+ for (int i = 0; i < mUidProcStates.size(); i++) {
+ int uid = mUidProcStates.keyAt(i);
+ if (filterAppId == -1 || filterAppId == UserHandle.getAppId(uid)) {
+ if (!procStatePrinted) {
+ procStatePrinted = true;
+ pw.println();
+ pw.println("Uid proc states:");
+ pw.increaseIndent();
+ }
+ pw.print(UserHandle.formatUid(uid));
+ pw.print(": ");
+ pw.println(ActivityManager.procStateToString(mUidProcStates.valueAt(i)));
+ }
+ }
+ if (procStatePrinted) {
+ pw.decreaseIndent();
+ }
+
boolean overridePrinted = false;
for (int i = 0; i < mUidBiasOverride.size(); i++) {
int uid = mUidBiasOverride.keyAt(i);
@@ -3713,6 +5402,25 @@ public class JobSchedulerService extends com.android.server.SystemService
pw.decreaseIndent();
}
+ boolean capabilitiesPrinted = false;
+ for (int i = 0; i < mUidCapabilities.size(); i++) {
+ int uid = mUidCapabilities.keyAt(i);
+ if (filterAppId == -1 || filterAppId == UserHandle.getAppId(uid)) {
+ if (!capabilitiesPrinted) {
+ capabilitiesPrinted = true;
+ pw.println();
+ pw.println("Uid capabilities:");
+ pw.increaseIndent();
+ }
+ pw.print(UserHandle.formatUid(uid));
+ pw.print(": ");
+ pw.println(ActivityManager.getCapabilitiesSummary(mUidCapabilities.valueAt(i)));
+ }
+ }
+ if (capabilitiesPrinted) {
+ pw.decreaseIndent();
+ }
+
boolean uidMapPrinted = false;
for (int i = 0; i < mUidToPackageCache.size(); ++i) {
final int uid = mUidToPackageCache.keyAt(i);
@@ -3829,6 +5537,38 @@ public class JobSchedulerService extends com.android.server.SystemService
pw.decreaseIndent();
pw.println();
+ boolean recentCancellationsPrinted = false;
+ for (int r = 1; r <= mLastCancelledJobs.length; ++r) {
+ // Print most recent first
+ final int idx = (mLastCancelledJobIndex + mLastCancelledJobs.length - r)
+ % mLastCancelledJobs.length;
+ job = mLastCancelledJobs[idx];
+ if (job != null) {
+ if (!predicate.test(job)) {
+ continue;
+ }
+ if (!recentCancellationsPrinted) {
+ pw.println();
+ pw.println("Recently cancelled jobs:");
+ pw.increaseIndent();
+ recentCancellationsPrinted = true;
+ }
+ TimeUtils.formatDuration(mLastCancelledJobTimeElapsed[idx], nowElapsed, pw);
+ pw.println();
+ // Double indent for readability
+ pw.increaseIndent();
+ pw.increaseIndent();
+ pw.println(job.toShortString());
+ job.dump(pw, true, nowElapsed);
+ pw.decreaseIndent();
+ pw.decreaseIndent();
+ }
+ }
+ if (!recentCancellationsPrinted) {
+ pw.decreaseIndent();
+ pw.println();
+ }
+
if (filterUid == -1) {
pw.println();
pw.print("mReadyToRock="); pw.println(mReadyToRock);
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 27268d267001..4357d4f39dce 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java
@@ -16,8 +16,10 @@
package com.android.server.job;
+import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.AppGlobals;
+import android.app.job.JobParameters;
import android.content.pm.IPackageManager;
import android.content.pm.PackageManager;
import android.os.Binder;
@@ -32,6 +34,9 @@ public final class JobSchedulerShellCommand extends BasicShellCommandHandler {
public static final int CMD_ERR_NO_JOB = -1001;
public static final int CMD_ERR_CONSTRAINTS = -1002;
+ static final int BYTE_OPTION_DOWNLOAD = 0;
+ static final int BYTE_OPTION_UPLOAD = 1;
+
JobSchedulerService mInternal;
IPackageManager mPM;
@@ -59,10 +64,18 @@ public final class JobSchedulerShellCommand extends BasicShellCommandHandler {
return getBatteryCharging(pw);
case "get-battery-not-low":
return getBatteryNotLow(pw);
+ case "get-estimated-download-bytes":
+ return getEstimatedNetworkBytes(pw, BYTE_OPTION_DOWNLOAD);
+ case "get-estimated-upload-bytes":
+ return getEstimatedNetworkBytes(pw, BYTE_OPTION_UPLOAD);
case "get-storage-seq":
return getStorageSeq(pw);
case "get-storage-not-low":
return getStorageNotLow(pw);
+ case "get-transferred-download-bytes":
+ return getTransferredNetworkBytes(pw, BYTE_OPTION_DOWNLOAD);
+ case "get-transferred-upload-bytes":
+ return getTransferredNetworkBytes(pw, BYTE_OPTION_UPLOAD);
case "get-job-state":
return getJobState(pw);
case "heartbeat":
@@ -71,6 +84,8 @@ public final class JobSchedulerShellCommand extends BasicShellCommandHandler {
return resetExecutionQuota(pw);
case "reset-schedule-quota":
return resetScheduleQuota(pw);
+ case "stop":
+ return stop(pw);
case "trigger-dock-state":
return triggerDockState(pw);
default:
@@ -96,7 +111,8 @@ public final class JobSchedulerShellCommand extends BasicShellCommandHandler {
}
}
- private boolean printError(int errCode, String pkgName, int userId, int jobId) {
+ private boolean printError(int errCode, String pkgName, int userId, @Nullable String namespace,
+ int jobId) {
PrintWriter pw;
switch (errCode) {
case CMD_ERR_NO_PACKAGE:
@@ -113,6 +129,10 @@ public final class JobSchedulerShellCommand extends BasicShellCommandHandler {
pw.print(jobId);
pw.print(" in package ");
pw.print(pkgName);
+ if (namespace != null) {
+ pw.print(" / namespace ");
+ pw.print(namespace);
+ }
pw.print(" / user ");
pw.println(userId);
return true;
@@ -123,6 +143,10 @@ public final class JobSchedulerShellCommand extends BasicShellCommandHandler {
pw.print(jobId);
pw.print(" in package ");
pw.print(pkgName);
+ if (namespace != null) {
+ pw.print(" / namespace ");
+ pw.print(namespace);
+ }
pw.print(" / user ");
pw.print(userId);
pw.println(" has functional constraints but --force not specified");
@@ -139,6 +163,7 @@ public final class JobSchedulerShellCommand extends BasicShellCommandHandler {
boolean force = false;
boolean satisfied = false;
int userId = UserHandle.USER_SYSTEM;
+ String namespace = null;
String opt;
while ((opt = getNextOption()) != null) {
@@ -158,6 +183,11 @@ public final class JobSchedulerShellCommand extends BasicShellCommandHandler {
userId = Integer.parseInt(getNextArgRequired());
break;
+ case "-n":
+ case "--namespace":
+ namespace = getNextArgRequired();
+ break;
+
default:
pw.println("Error: unknown option '" + opt + "'");
return -1;
@@ -174,8 +204,9 @@ public final class JobSchedulerShellCommand extends BasicShellCommandHandler {
final long ident = Binder.clearCallingIdentity();
try {
- int ret = mInternal.executeRunCommand(pkgName, userId, jobId, satisfied, force);
- if (printError(ret, pkgName, userId, jobId)) {
+ int ret = mInternal.executeRunCommand(pkgName, userId, namespace,
+ jobId, satisfied, force);
+ if (printError(ret, pkgName, userId, namespace, jobId)) {
return ret;
}
@@ -196,6 +227,7 @@ public final class JobSchedulerShellCommand extends BasicShellCommandHandler {
checkPermission("force timeout jobs");
int userId = UserHandle.USER_ALL;
+ String namespace = null;
String opt;
while ((opt = getNextOption()) != null) {
@@ -205,6 +237,11 @@ public final class JobSchedulerShellCommand extends BasicShellCommandHandler {
userId = UserHandle.parseUserArg(getNextArgRequired());
break;
+ case "-n":
+ case "--namespace":
+ namespace = getNextArgRequired();
+ break;
+
default:
pw.println("Error: unknown option '" + opt + "'");
return -1;
@@ -221,7 +258,9 @@ public final class JobSchedulerShellCommand extends BasicShellCommandHandler {
final long ident = Binder.clearCallingIdentity();
try {
- return mInternal.executeTimeoutCommand(pw, pkgName, userId, jobIdStr != null, jobId);
+ return mInternal.executeStopCommand(pw, pkgName, userId, namespace,
+ jobIdStr != null, jobId,
+ JobParameters.STOP_REASON_TIMEOUT, JobParameters.INTERNAL_STOP_REASON_TIMEOUT);
} finally {
Binder.restoreCallingIdentity(ident);
}
@@ -231,6 +270,7 @@ public final class JobSchedulerShellCommand extends BasicShellCommandHandler {
checkPermission("cancel jobs");
int userId = UserHandle.USER_SYSTEM;
+ String namespace = null;
String opt;
while ((opt = getNextOption()) != null) {
@@ -240,6 +280,11 @@ public final class JobSchedulerShellCommand extends BasicShellCommandHandler {
userId = UserHandle.parseUserArg(getNextArgRequired());
break;
+ case "-n":
+ case "--namespace":
+ namespace = getNextArgRequired();
+ break;
+
default:
pw.println("Error: unknown option '" + opt + "'");
return -1;
@@ -257,7 +302,8 @@ public final class JobSchedulerShellCommand extends BasicShellCommandHandler {
final long ident = Binder.clearCallingIdentity();
try {
- return mInternal.executeCancelCommand(pw, pkgName, userId, jobIdStr != null, jobId);
+ return mInternal.executeCancelCommand(pw, pkgName, userId, namespace,
+ jobIdStr != null, jobId);
} finally {
Binder.restoreCallingIdentity(ident);
}
@@ -304,6 +350,50 @@ public final class JobSchedulerShellCommand extends BasicShellCommandHandler {
return 0;
}
+ private int getEstimatedNetworkBytes(PrintWriter pw, int byteOption) throws Exception {
+ checkPermission("get estimated bytes");
+
+ int userId = UserHandle.USER_SYSTEM;
+ String namespace = null;
+
+ String opt;
+ while ((opt = getNextOption()) != null) {
+ switch (opt) {
+ case "-u":
+ case "--user":
+ userId = UserHandle.parseUserArg(getNextArgRequired());
+ break;
+
+ case "-n":
+ case "--namespace":
+ namespace = getNextArgRequired();
+ break;
+
+ default:
+ pw.println("Error: unknown option '" + opt + "'");
+ return -1;
+ }
+ }
+
+ if (userId == UserHandle.USER_CURRENT) {
+ userId = ActivityManager.getCurrentUser();
+ }
+
+ final String pkgName = getNextArgRequired();
+ final String jobIdStr = getNextArgRequired();
+ final int jobId = Integer.parseInt(jobIdStr);
+
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ int ret = mInternal.getEstimatedNetworkBytes(pw, pkgName, userId, namespace,
+ jobId, byteOption);
+ printError(ret, pkgName, userId, namespace, jobId);
+ return ret;
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
private int getStorageSeq(PrintWriter pw) {
int seq = mInternal.getStorageSeq();
pw.println(seq);
@@ -316,10 +406,55 @@ public final class JobSchedulerShellCommand extends BasicShellCommandHandler {
return 0;
}
+ private int getTransferredNetworkBytes(PrintWriter pw, int byteOption) throws Exception {
+ checkPermission("get transferred bytes");
+
+ int userId = UserHandle.USER_SYSTEM;
+ String namespace = null;
+
+ String opt;
+ while ((opt = getNextOption()) != null) {
+ switch (opt) {
+ case "-u":
+ case "--user":
+ userId = UserHandle.parseUserArg(getNextArgRequired());
+ break;
+
+ case "-n":
+ case "--namespace":
+ namespace = getNextArgRequired();
+ break;
+
+ default:
+ pw.println("Error: unknown option '" + opt + "'");
+ return -1;
+ }
+ }
+
+ if (userId == UserHandle.USER_CURRENT) {
+ userId = ActivityManager.getCurrentUser();
+ }
+
+ final String pkgName = getNextArgRequired();
+ final String jobIdStr = getNextArgRequired();
+ final int jobId = Integer.parseInt(jobIdStr);
+
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ int ret = mInternal.getTransferredNetworkBytes(pw, pkgName, userId, namespace,
+ jobId, byteOption);
+ printError(ret, pkgName, userId, namespace, jobId);
+ return ret;
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
private int getJobState(PrintWriter pw) throws Exception {
- checkPermission("force timeout jobs");
+ checkPermission("get job state");
int userId = UserHandle.USER_SYSTEM;
+ String namespace = null;
String opt;
while ((opt = getNextOption()) != null) {
@@ -329,6 +464,11 @@ public final class JobSchedulerShellCommand extends BasicShellCommandHandler {
userId = UserHandle.parseUserArg(getNextArgRequired());
break;
+ case "-n":
+ case "--namespace":
+ namespace = getNextArgRequired();
+ break;
+
default:
pw.println("Error: unknown option '" + opt + "'");
return -1;
@@ -345,8 +485,8 @@ public final class JobSchedulerShellCommand extends BasicShellCommandHandler {
final long ident = Binder.clearCallingIdentity();
try {
- int ret = mInternal.getJobState(pw, pkgName, userId, jobId);
- printError(ret, pkgName, userId, jobId);
+ int ret = mInternal.getJobState(pw, pkgName, userId, namespace, jobId);
+ printError(ret, pkgName, userId, namespace, jobId);
return ret;
} finally {
Binder.restoreCallingIdentity(ident);
@@ -406,6 +546,60 @@ public final class JobSchedulerShellCommand extends BasicShellCommandHandler {
return 0;
}
+ private int stop(PrintWriter pw) throws Exception {
+ checkPermission("stop jobs");
+
+ int userId = UserHandle.USER_ALL;
+ String namespace = null;
+ int stopReason = JobParameters.STOP_REASON_USER;
+ int internalStopReason = JobParameters.INTERNAL_STOP_REASON_UNKNOWN;
+
+ String opt;
+ while ((opt = getNextOption()) != null) {
+ switch (opt) {
+ case "-u":
+ case "--user":
+ userId = UserHandle.parseUserArg(getNextArgRequired());
+ break;
+
+ case "-n":
+ case "--namespace":
+ namespace = getNextArgRequired();
+ break;
+
+ case "-s":
+ case "--stop-reason":
+ stopReason = Integer.parseInt(getNextArgRequired());
+ break;
+
+ case "-i":
+ case "--internal-stop-reason":
+ internalStopReason = Integer.parseInt(getNextArgRequired());
+ break;
+
+ default:
+ pw.println("Error: unknown option '" + opt + "'");
+ return -1;
+ }
+ }
+
+ if (userId == UserHandle.USER_CURRENT) {
+ userId = ActivityManager.getCurrentUser();
+ }
+
+ final String pkgName = getNextArg();
+ final String jobIdStr = getNextArg();
+ final int jobId = jobIdStr != null ? Integer.parseInt(jobIdStr) : -1;
+
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ return mInternal.executeStopCommand(pw, pkgName, userId, namespace,
+ jobIdStr != null, jobId, stopReason, internalStopReason);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
private int triggerDockState(PrintWriter pw) throws Exception {
checkPermission("trigger wireless charging dock state");
@@ -436,7 +630,8 @@ public final class JobSchedulerShellCommand extends BasicShellCommandHandler {
pw.println("Job scheduler (jobscheduler) commands:");
pw.println(" help");
pw.println(" Print this help text.");
- pw.println(" run [-f | --force] [-s | --satisfied] [-u | --user USER_ID] PACKAGE JOB_ID");
+ pw.println(" run [-f | --force] [-s | --satisfied] [-u | --user USER_ID]"
+ + " [-n | --namespace NAMESPACE] PACKAGE JOB_ID");
pw.println(" Trigger immediate execution of a specific scheduled job. For historical");
pw.println(" reasons, some constraints, such as battery, are ignored when this");
pw.println(" command is called. If you don't want any constraints to be ignored,");
@@ -445,23 +640,50 @@ public final class JobSchedulerShellCommand extends BasicShellCommandHandler {
pw.println(" -f or --force: run the job even if technical constraints such as");
pw.println(" connectivity are not currently met. This is incompatible with -f ");
pw.println(" and so an error will be reported if both are given.");
+ pw.println(" -n or --namespace: specify the namespace this job sits in; the default");
+ pw.println(" is null (no namespace).");
pw.println(" -s or --satisfied: run the job only if all constraints are met.");
pw.println(" This is incompatible with -f and so an error will be reported");
pw.println(" if both are given.");
pw.println(" -u or --user: specify which user's job is to be run; the default is");
pw.println(" the primary or system user");
- pw.println(" timeout [-u | --user USER_ID] [PACKAGE] [JOB_ID]");
- pw.println(" Trigger immediate timeout of currently executing jobs, as if their.");
+ pw.println(" stop [-u | --user USER_ID] [-n | --namespace NAMESPACE]"
+ + " [-s | --stop-reason STOP_REASON] [-i | --internal-stop-reason STOP_REASON]"
+ + " [PACKAGE] [JOB_ID]");
+ pw.println(" Trigger immediate stop of currently executing jobs using the specified");
+ pw.println(" stop reasons.");
+ pw.println(" Options:");
+ pw.println(" -u or --user: specify which user's job is to be run; the default is");
+ pw.println(" all users");
+ pw.println(" -n or --namespace: specify the namespace this job sits in; the default");
+ pw.println(" is null (no namespace).");
+ pw.println(" -s or --stop-reason: specify the stop reason given to the job.");
+ pw.println(" Valid values are those that can be returned from");
+ pw.println(" JobParameters.getStopReason().");
+ pw.println(" The default value is STOP_REASON_USER.");
+ pw.println(" -i or --internal-stop-reason: specify the internal stop reason.");
+ pw.println(" JobScheduler will use for internal processing.");
+ pw.println(" Valid values are those that can be returned from");
+ pw.println(" JobParameters.getInternalStopReason().");
+ pw.println(" The default value is INTERNAL_STOP_REASON_UNDEFINED.");
+ pw.println(" timeout [-u | --user USER_ID] [-n | --namespace NAMESPACE]"
+ + " [PACKAGE] [JOB_ID]");
+ pw.println(" Trigger immediate timeout of currently executing jobs, as if their");
pw.println(" execution timeout had expired.");
+ pw.println(" This is the equivalent of calling `stop -s 3 -i 3`.");
pw.println(" Options:");
pw.println(" -u or --user: specify which user's job is to be run; the default is");
pw.println(" all users");
- pw.println(" cancel [-u | --user USER_ID] PACKAGE [JOB_ID]");
+ pw.println(" -n or --namespace: specify the namespace this job sits in; the default");
+ pw.println(" is null (no namespace).");
+ pw.println(" cancel [-u | --user USER_ID] [-n | --namespace NAMESPACE] PACKAGE [JOB_ID]");
pw.println(" Cancel a scheduled job. If a job ID is not supplied, all jobs scheduled");
pw.println(" by that package will be canceled. USE WITH CAUTION.");
pw.println(" Options:");
pw.println(" -u or --user: specify which user's job is to be run; the default is");
pw.println(" the primary or system user");
+ pw.println(" -n or --namespace: specify the namespace this job sits in; the default");
+ pw.println(" is null (no namespace).");
pw.println(" heartbeat [num]");
pw.println(" No longer used.");
pw.println(" monitor-battery [on|off]");
@@ -473,11 +695,36 @@ 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-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.");
+ pw.println(" Options:");
+ pw.println(" -u or --user: specify which user's job is to be run; the default is");
+ pw.println(" the primary or system user");
+ pw.println(" get-estimated-upload-bytes [-u | --user USER_ID]"
+ + " [-n | --namespace NAMESPACE] PACKAGE JOB_ID");
+ pw.println(" Return the most recent estimated upload bytes for the job.");
+ pw.println(" Options:");
+ pw.println(" -u or --user: specify which user's job is to be run; the default is");
+ pw.println(" the primary or system user");
pw.println(" get-storage-seq");
pw.println(" Return the last storage update sequence number that was received.");
pw.println(" get-storage-not-low");
pw.println(" Return whether storage is currently considered to not be low.");
- pw.println(" get-job-state [-u | --user USER_ID] PACKAGE JOB_ID");
+ pw.println(" get-transferred-download-bytes [-u | --user USER_ID]"
+ + " [-n | --namespace NAMESPACE] PACKAGE JOB_ID");
+ pw.println(" Return the most recent transferred download bytes for the job.");
+ pw.println(" Options:");
+ pw.println(" -u or --user: specify which user's job is to be run; the default is");
+ pw.println(" the primary or system user");
+ pw.println(" get-transferred-upload-bytes [-u | --user USER_ID]"
+ + " [-n | --namespace NAMESPACE] PACKAGE JOB_ID");
+ pw.println(" Return the most recent transferred upload bytes for the job.");
+ pw.println(" Options:");
+ pw.println(" -u or --user: specify which user's job is to be run; the default is");
+ pw.println(" the primary or system user");
+ pw.println(" get-job-state [-u | --user USER_ID] [-n | --namespace NAMESPACE]"
+ + " PACKAGE JOB_ID");
pw.println(" Return the current state of a job, may be any combination of:");
pw.println(" pending: currently on the pending list, waiting to be active");
pw.println(" active: job is actively running");
@@ -489,9 +736,10 @@ public final class JobSchedulerShellCommand extends BasicShellCommandHandler {
pw.println(" Options:");
pw.println(" -u or --user: specify which user's job is to be run; the default is");
pw.println(" the primary or system user");
+ pw.println(" -n or --namespace: specify the namespace this job sits in; the default");
+ pw.println(" is null (no namespace).");
pw.println(" trigger-dock-state [idle|active]");
pw.println(" Trigger wireless charging dock state. Active by default.");
pw.println();
}
-
}
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 58953c45a794..109686d76b2f 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
@@ -20,9 +20,16 @@ import static android.app.job.JobInfo.getPriorityString;
import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_NONE;
import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
+import static com.android.server.job.JobSchedulerService.safelyScaleBytesToKBForHistogram;
+import android.Manifest;
+import android.annotation.BytesLong;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.ActivityManagerInternal;
+import android.app.Notification;
+import android.app.compat.CompatChanges;
import android.app.job.IJobCallback;
import android.app.job.IJobService;
import android.app.job.JobInfo;
@@ -30,10 +37,14 @@ import android.app.job.JobParameters;
import android.app.job.JobProtoEnums;
import android.app.job.JobWorkItem;
import android.app.usage.UsageStatsManagerInternal;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledAfter;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.content.PermissionChecker;
import android.content.ServiceConnection;
+import android.net.Network;
import android.net.Uri;
import android.os.Binder;
import android.os.Build;
@@ -47,13 +58,17 @@ import android.os.Trace;
import android.os.UserHandle;
import android.util.EventLog;
import android.util.IndentingPrintWriter;
+import android.util.Pair;
import android.util.Slog;
import android.util.TimeUtils;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.IBatteryStats;
+import com.android.internal.os.TimeoutRecord;
import com.android.internal.util.FrameworkStatsLog;
+import com.android.modules.expresslog.Counter;
+import com.android.modules.expresslog.Histogram;
import com.android.server.EventLogTags;
import com.android.server.LocalServices;
import com.android.server.job.controllers.JobStatus;
@@ -61,6 +76,8 @@ import com.android.server.tare.EconomicPolicy;
import com.android.server.tare.EconomyManagerInternal;
import com.android.server.tare.JobSchedulerEconomicPolicy;
+import java.util.Objects;
+
/**
* Handles client binding and lifecycle of a job. Jobs execute one at a time on an instance of this
* class.
@@ -81,11 +98,37 @@ public final class JobServiceContext implements ServiceConnection {
private static final boolean DEBUG = JobSchedulerService.DEBUG;
private static final boolean DEBUG_STANDBY = JobSchedulerService.DEBUG_STANDBY;
+ /**
+ * Whether to trigger an ANR when apps are slow to respond on pre-UDC APIs and functionality.
+ */
+ @ChangeId
+ @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.TIRAMISU)
+ private static final long ANR_PRE_UDC_APIS_ON_SLOW_RESPONSES = 258236856L;
+
private static final String TAG = "JobServiceContext";
/** Amount of time the JobScheduler waits for the initial service launch+bind. */
private static final long OP_BIND_TIMEOUT_MILLIS = 18 * 1000 * Build.HW_TIMEOUT_MULTIPLIER;
/** Amount of time the JobScheduler will wait for a response from an app for a message. */
private static final long OP_TIMEOUT_MILLIS = 8 * 1000 * Build.HW_TIMEOUT_MULTIPLIER;
+ /** Amount of time the JobScheduler will wait for a job to provide a required notification. */
+ private static final long NOTIFICATION_TIMEOUT_MILLIS = 10_000L * Build.HW_TIMEOUT_MULTIPLIER;
+ private static final long EXECUTION_DURATION_STAMP_PERIOD_MILLIS = 5 * 60_000L;
+
+ private static final Histogram sEnqueuedJwiAtJobStart = new Histogram(
+ "job_scheduler.value_hist_w_uid_enqueued_work_items_at_job_start",
+ new Histogram.ScaledRangeOptions(20, 1, 3, 1.4f));
+ private static final Histogram sTransferredNetworkDownloadKBHighWaterMarkLogger = new Histogram(
+ "job_scheduler.value_hist_transferred_network_download_kilobytes_high_water_mark",
+ new Histogram.ScaledRangeOptions(50, 0, 32 /* 32 KB */, 1.31f));
+ private static final Histogram sTransferredNetworkUploadKBHighWaterMarkLogger = new Histogram(
+ "job_scheduler.value_hist_transferred_network_upload_kilobytes_high_water_mark",
+ new Histogram.ScaledRangeOptions(50, 0, 32 /* 32 KB */, 1.31f));
+ private static final Histogram sUpdatedEstimatedNetworkDownloadKBLogger = new Histogram(
+ "job_scheduler.value_hist_updated_estimated_network_download_kilobytes",
+ new Histogram.ScaledRangeOptions(50, 0, 32 /* 32 KB */, 1.31f));
+ private static final Histogram sUpdatedEstimatedNetworkUploadKBLogger = new Histogram(
+ "job_scheduler.value_hist_updated_estimated_network_upload_kilobytes",
+ new Histogram.ScaledRangeOptions(50, 0, 32 /* 32 KB */, 1.31f));
private static final String[] VERB_STRINGS = {
"VERB_BINDING", "VERB_STARTING", "VERB_EXECUTING", "VERB_STOPPING", "VERB_FINISHED"
@@ -108,10 +151,12 @@ public final class JobServiceContext implements ServiceConnection {
/** Make callbacks to {@link JobSchedulerService} to inform on job completion status. */
private final JobCompletedListener mCompletedListener;
private final JobConcurrencyManager mJobConcurrencyManager;
+ private final JobNotificationCoordinator mNotificationCoordinator;
private final JobSchedulerService mService;
/** Used for service binding, etc. */
private final Context mContext;
private final Object mLock;
+ private final ActivityManagerInternal mActivityManagerInternal;
private final IBatteryStats mBatteryStats;
private final EconomyManagerInternal mEconomyManagerInternal;
private final JobPackageTracker mJobPackageTracker;
@@ -167,6 +212,15 @@ public final class JobServiceContext implements ServiceConnection {
private long mMinExecutionGuaranteeMillis;
/** The absolute maximum amount of time the job can run */
private long mMaxExecutionTimeMillis;
+ /** Whether this job is required to provide a notification and we're still waiting for it. */
+ private boolean mAwaitingNotification;
+ /** The last time we updated the job's execution duration, in the elapsed realtime timebase. */
+ private long mLastExecutionDurationStampTimeElapsed;
+
+ private long mEstimatedDownloadBytes;
+ private long mEstimatedUploadBytes;
+ private long mTransferredDownloadBytes;
+ private long mTransferredUploadBytes;
/**
* The stop reason for a pending cancel. If there's not pending cancel, then the value should be
@@ -176,6 +230,16 @@ public final class JobServiceContext implements ServiceConnection {
private int mPendingInternalStopReason;
private String mPendingDebugStopReason;
+ private Network mPendingNetworkChange;
+
+ /**
+ * The reason this job is marked for death. If it's not marked for death,
+ * then the value should be {@link JobParameters#STOP_REASON_UNDEFINED}.
+ */
+ private int mDeathMarkStopReason = JobParameters.STOP_REASON_UNDEFINED;
+ private int mDeathMarkInternalStopReason;
+ private String mDeathMarkDebugReason;
+
// Debugging: reason this job was last stopped.
public String mStoppedReason;
@@ -187,6 +251,18 @@ public final class JobServiceContext implements ServiceConnection {
public long mStoppedTime;
@Override
+ public void acknowledgeGetTransferredDownloadBytesMessage(int jobId, int workId,
+ @BytesLong long transferredBytes) {
+ doAcknowledgeGetTransferredDownloadBytesMessage(this, jobId, workId, transferredBytes);
+ }
+
+ @Override
+ public void acknowledgeGetTransferredUploadBytesMessage(int jobId, int workId,
+ @BytesLong long transferredBytes) {
+ doAcknowledgeGetTransferredUploadBytesMessage(this, jobId, workId, transferredBytes);
+ }
+
+ @Override
public void acknowledgeStartMessage(int jobId, boolean ongoing) {
doAcknowledgeStartMessage(this, jobId, ongoing);
}
@@ -210,18 +286,39 @@ public final class JobServiceContext implements ServiceConnection {
public void jobFinished(int jobId, boolean reschedule) {
doJobFinished(this, jobId, reschedule);
}
+
+ @Override
+ public void updateEstimatedNetworkBytes(int jobId, JobWorkItem item,
+ long downloadBytes, long uploadBytes) {
+ doUpdateEstimatedNetworkBytes(this, jobId, item, downloadBytes, uploadBytes);
+ }
+
+ @Override
+ public void updateTransferredNetworkBytes(int jobId, JobWorkItem item,
+ long downloadBytes, long uploadBytes) {
+ doUpdateTransferredNetworkBytes(this, jobId, item, downloadBytes, uploadBytes);
+ }
+
+ @Override
+ public void setNotification(int jobId, int notificationId,
+ Notification notification, int jobEndNotificationPolicy) {
+ doSetNotification(this, jobId, notificationId, notification, jobEndNotificationPolicy);
+ }
}
JobServiceContext(JobSchedulerService service, JobConcurrencyManager concurrencyManager,
+ JobNotificationCoordinator notificationCoordinator,
IBatteryStats batteryStats, JobPackageTracker tracker, Looper looper) {
mContext = service.getContext();
mLock = service.getLock();
mService = service;
+ mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
mBatteryStats = batteryStats;
mEconomyManagerInternal = LocalServices.getService(EconomyManagerInternal.class);
mJobPackageTracker = tracker;
mCallbackHandler = new JobServiceHandler(looper);
mJobConcurrencyManager = concurrencyManager;
+ mNotificationCoordinator = notificationCoordinator;
mCompletedListener = service;
mPowerManager = mContext.getSystemService(PowerManager.class);
mAvailable = true;
@@ -249,6 +346,7 @@ public final class JobServiceContext implements ServiceConnection {
mRunningJob = job;
mRunningJobWorkType = workType;
mRunningCallback = new JobCallback();
+ mPendingNetworkChange = null;
final boolean isDeadlineExpired =
job.hasDeadlineConstraint() &&
(job.getLatestRunTimeElapsed() < sElapsedRealtimeClock.millis());
@@ -263,14 +361,22 @@ public final class JobServiceContext implements ServiceConnection {
job.changedAuthorities.toArray(triggeredAuthorities);
}
final JobInfo ji = job.getJob();
- mParams = new JobParameters(mRunningCallback, job.getJobId(), ji.getExtras(),
+ final Network passedNetwork = canGetNetworkInformation(job) ? job.network : null;
+ mParams = new JobParameters(mRunningCallback, job.getNamespace(), job.getJobId(),
+ ji.getExtras(),
ji.getTransientExtras(), ji.getClipData(), ji.getClipGrantFlags(),
isDeadlineExpired, job.shouldTreatAsExpeditedJob(),
- triggeredUris, triggeredAuthorities, job.network);
+ job.shouldTreatAsUserInitiatedJob(), triggeredUris, triggeredAuthorities,
+ passedNetwork);
mExecutionStartTimeElapsed = sElapsedRealtimeClock.millis();
+ mLastExecutionDurationStampTimeElapsed = mExecutionStartTimeElapsed;
mMinExecutionGuaranteeMillis = mService.getMinJobExecutionGuaranteeMs(job);
mMaxExecutionTimeMillis =
Math.max(mService.getMaxJobExecutionTimeMs(job), mMinExecutionGuaranteeMillis);
+ mEstimatedDownloadBytes = job.getEstimatedNetworkDownloadBytes();
+ mEstimatedUploadBytes = job.getEstimatedNetworkUploadBytes();
+ mTransferredDownloadBytes = mTransferredUploadBytes = 0;
+ mAwaitingNotification = job.isUserVisibleJob();
final long whenDeferred = job.getWhenStandbyDeferred();
if (whenDeferred > 0) {
@@ -303,19 +409,38 @@ public final class JobServiceContext implements ServiceConnection {
getStartActionId(job), String.valueOf(job.getJobId()));
mVerb = VERB_BINDING;
scheduleOpTimeOutLocked();
- final Intent intent = new Intent().setComponent(job.getServiceComponent());
+ // Use FLAG_FROM_BACKGROUND to avoid resetting the bad-app tracking.
+ final Intent intent = new Intent().setComponent(job.getServiceComponent())
+ .setFlags(Intent.FLAG_FROM_BACKGROUND);
boolean binding = false;
+ boolean startedWithForegroundFlag = false;
try {
- final int bindFlags;
- if (job.shouldTreatAsExpeditedJob()) {
- bindFlags = Context.BIND_AUTO_CREATE | Context.BIND_NOT_FOREGROUND
- | Context.BIND_ALMOST_PERCEPTIBLE
- | Context.BIND_BYPASS_POWER_NETWORK_RESTRICTIONS
- | Context.BIND_NOT_APP_COMPONENT_USAGE;
+ final Context.BindServiceFlags bindFlags;
+ if (job.shouldTreatAsUserInitiatedJob() && !job.isUserBgRestricted()) {
+ // If the user has bg restricted the app, don't give the job FG privileges
+ // such as bypassing data saver or getting the higher foreground proc state.
+ // If we've gotten to this point, the app is most likely in the foreground,
+ // so the job will run just fine while the user keeps the app in the foreground.
+ bindFlags = Context.BindServiceFlags.of(
+ Context.BIND_AUTO_CREATE
+ | Context.BIND_ALMOST_PERCEPTIBLE
+ | Context.BIND_BYPASS_POWER_NETWORK_RESTRICTIONS
+ | Context.BIND_BYPASS_USER_NETWORK_RESTRICTIONS
+ | Context.BIND_NOT_APP_COMPONENT_USAGE);
+ startedWithForegroundFlag = true;
+ } else if (job.shouldTreatAsExpeditedJob() || job.shouldTreatAsUserInitiatedJob()) {
+ bindFlags = Context.BindServiceFlags.of(
+ Context.BIND_AUTO_CREATE
+ | Context.BIND_NOT_FOREGROUND
+ | Context.BIND_ALMOST_PERCEPTIBLE
+ | Context.BIND_BYPASS_POWER_NETWORK_RESTRICTIONS
+ | Context.BIND_NOT_APP_COMPONENT_USAGE);
} else {
- bindFlags = Context.BIND_AUTO_CREATE | Context.BIND_NOT_FOREGROUND
- | Context.BIND_NOT_PERCEPTIBLE
- | Context.BIND_NOT_APP_COMPONENT_USAGE;
+ bindFlags = Context.BindServiceFlags.of(
+ Context.BIND_AUTO_CREATE
+ | Context.BIND_NOT_FOREGROUND
+ | Context.BIND_NOT_PERCEPTIBLE
+ | Context.BIND_NOT_APP_COMPONENT_USAGE);
}
binding = mContext.bindServiceAsUser(intent, this, bindFlags,
UserHandle.of(job.getUserId()));
@@ -346,7 +471,8 @@ public final class JobServiceContext implements ServiceConnection {
job.getSourceUid(), null, job.getBatteryName(),
FrameworkStatsLog.SCHEDULED_JOB_STATE_CHANGED__STATE__STARTED,
JobProtoEnums.INTERNAL_STOP_REASON_UNKNOWN,
- job.getStandbyBucket(), job.getJobId(),
+ job.getStandbyBucket(),
+ job.getLoggingJobId(),
job.hasChargingConstraint(),
job.hasBatteryNotLowConstraint(),
job.hasStorageNotLowConstraint(),
@@ -361,28 +487,97 @@ public final class JobServiceContext implements ServiceConnection {
job.getJob().isPrefetch(),
job.getJob().getPriority(),
job.getEffectivePriority(),
- job.getNumFailures());
- // Use the context's ID to distinguish traces since there'll only be one job running
- // per context.
- Trace.asyncTraceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, job.getTag(), getId());
+ job.getNumPreviousAttempts(),
+ job.getJob().getMaxExecutionDelayMillis(),
+ isDeadlineExpired,
+ job.isConstraintSatisfied(JobInfo.CONSTRAINT_FLAG_CHARGING),
+ job.isConstraintSatisfied(JobInfo.CONSTRAINT_FLAG_BATTERY_NOT_LOW),
+ job.isConstraintSatisfied(JobInfo.CONSTRAINT_FLAG_STORAGE_NOT_LOW),
+ job.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY),
+ job.isConstraintSatisfied(JobInfo.CONSTRAINT_FLAG_DEVICE_IDLE),
+ job.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY),
+ job.isConstraintSatisfied(JobStatus.CONSTRAINT_CONTENT_TRIGGER),
+ mExecutionStartTimeElapsed - job.enqueueTime,
+ job.getJob().isUserInitiated(),
+ job.shouldTreatAsUserInitiatedJob(),
+ job.getJob().isPeriodic(),
+ job.getJob().getMinLatencyMillis(),
+ job.getEstimatedNetworkDownloadBytes(),
+ job.getEstimatedNetworkUploadBytes(),
+ job.getWorkCount(),
+ ActivityManager.processStateAmToProto(mService.getUidProcState(job.getUid())),
+ job.getNamespaceHash());
+ sEnqueuedJwiAtJobStart.logSampleWithUid(job.getUid(), job.getWorkCount());
+ final String sourcePackage = job.getSourcePackageName();
+ if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) {
+ final String componentPackage = job.getServiceComponent().getPackageName();
+ String traceTag = "*job*<" + job.getSourceUid() + ">" + sourcePackage;
+ if (!sourcePackage.equals(componentPackage)) {
+ traceTag += ":" + componentPackage;
+ }
+ traceTag += "/" + job.getServiceComponent().getShortClassName();
+ if (!componentPackage.equals(job.serviceProcessName)) {
+ traceTag += "$" + job.serviceProcessName;
+ }
+ if (job.getNamespace() != null) {
+ traceTag += "@" + job.getNamespace();
+ }
+ traceTag += "#" + job.getJobId();
+
+ // Use the context's ID to distinguish traces since there'll only be one job
+ // running per context.
+ Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "JobScheduler",
+ traceTag, getId());
+ }
try {
mBatteryStats.noteJobStart(job.getBatteryName(), job.getSourceUid());
} catch (RemoteException e) {
// Whatever.
}
- final String jobPackage = job.getSourcePackageName();
final int jobUserId = job.getSourceUserId();
UsageStatsManagerInternal usageStats =
LocalServices.getService(UsageStatsManagerInternal.class);
- usageStats.setLastJobRunTime(jobPackage, jobUserId, mExecutionStartTimeElapsed);
+ usageStats.setLastJobRunTime(sourcePackage, jobUserId, mExecutionStartTimeElapsed);
mAvailable = false;
mStoppedReason = null;
mStoppedTime = 0;
+ // Wait until after bindService() returns a success value to set these so we don't
+ // have JobStatus objects that aren't running but have these set to true.
job.startedAsExpeditedJob = job.shouldTreatAsExpeditedJob();
+ job.startedAsUserInitiatedJob = job.shouldTreatAsUserInitiatedJob();
+ job.startedWithForegroundFlag = startedWithForegroundFlag;
return true;
}
}
+ private boolean canGetNetworkInformation(@NonNull JobStatus job) {
+ if (job.getJob().getRequiredNetwork() == null) {
+ // The job never had a network constraint, so we're not going to give it a network
+ // object. Add this check as an early return to avoid wasting cycles doing permission
+ // checks for this job.
+ return false;
+ }
+ // The calling app is doing the work, so use its UID, not the source UID.
+ final int uid = job.getUid();
+ if (CompatChanges.isChangeEnabled(
+ JobSchedulerService.REQUIRE_NETWORK_PERMISSIONS_FOR_CONNECTIVITY_JOBS, uid)) {
+ final String pkgName = job.getServiceComponent().getPackageName();
+ if (!hasPermissionForDelivery(uid, pkgName, Manifest.permission.ACCESS_NETWORK_STATE)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private boolean hasPermissionForDelivery(int uid, @NonNull String pkgName,
+ @NonNull String permission) {
+ final int result = PermissionChecker.checkPermissionForDataDelivery(mContext, permission,
+ PermissionChecker.PID_UNKNOWN, uid, pkgName, /* attributionTag */ null,
+ "network info via JS");
+ return result == PermissionChecker.PERMISSION_GRANTED;
+ }
+
@EconomicPolicy.AppAction
private static int getStartActionId(@NonNull JobStatus job) {
switch (job.getEffectivePriority()) {
@@ -429,6 +624,34 @@ public final class JobServiceContext implements ServiceConnection {
doCancelLocked(reason, internalStopReason, debugReason);
}
+ /**
+ * Called when an app's process is about to be killed and we want to update the job's stop
+ * reasons without telling the job it's going to be stopped.
+ */
+ @GuardedBy("mLock")
+ void markForProcessDeathLocked(@JobParameters.StopReason int reason,
+ int internalStopReason, @NonNull String debugReason) {
+ if (mVerb == VERB_FINISHED) {
+ if (DEBUG) {
+ Slog.d(TAG, "Too late to mark for death (verb=" + mVerb + "), ignoring.");
+ }
+ return;
+ }
+ if (DEBUG) {
+ Slog.d(TAG,
+ "Marking " + mRunningJob.toShortString() + " for death because "
+ + reason + ":" + debugReason);
+ }
+ mDeathMarkStopReason = reason;
+ mDeathMarkInternalStopReason = internalStopReason;
+ mDeathMarkDebugReason = debugReason;
+ if (mParams.getStopReason() == JobParameters.STOP_REASON_UNDEFINED) {
+ // Only set the stop reason if we're not already trying to stop the job for some
+ // other reason in case that other stop is successful before the process dies.
+ mParams.setStopReason(reason, internalStopReason, debugReason);
+ }
+ }
+
int getPreferredUid() {
return mPreferredUid;
}
@@ -449,28 +672,73 @@ public final class JobServiceContext implements ServiceConnection {
return mTimeoutElapsed;
}
+ long getRemainingGuaranteedTimeMs(long nowElapsed) {
+ return Math.max(0, mExecutionStartTimeElapsed + mMinExecutionGuaranteeMillis - nowElapsed);
+ }
+
+ void informOfNetworkChangeLocked(Network newNetwork) {
+ if (newNetwork != null && mRunningJob != null && !canGetNetworkInformation(mRunningJob)) {
+ // The app can't get network information, so there's no point informing it of network
+ // changes. This case may happen if an app had scheduled network job and then
+ // started targeting U+ without requesting the required network permissions.
+ if (DEBUG) {
+ Slog.d(TAG, "Skipping network change call because of missing permissions");
+ }
+ return;
+ }
+ if (mVerb != VERB_EXECUTING) {
+ Slog.w(TAG, "Sending onNetworkChanged for a job that isn't started. " + mRunningJob);
+ if (mVerb == VERB_BINDING || mVerb == VERB_STARTING) {
+ // The network changed before the job has fully started. Hold the change push
+ // until the job has started executing.
+ mPendingNetworkChange = newNetwork;
+ }
+ return;
+ }
+ try {
+ mParams.setNetwork(newNetwork);
+ mPendingNetworkChange = null;
+ service.onNetworkChanged(mParams);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Error sending onNetworkChanged to client.", e);
+ // The job's host app apparently crashed during the job, so we should reschedule.
+ closeAndCleanupJobLocked(/* reschedule */ true,
+ "host crashed when trying to inform of network change");
+ }
+ }
+
boolean isWithinExecutionGuaranteeTime() {
return sElapsedRealtimeClock.millis()
< mExecutionStartTimeElapsed + mMinExecutionGuaranteeMillis;
}
@GuardedBy("mLock")
- boolean timeoutIfExecutingLocked(String pkgName, int userId, boolean matchJobId, int jobId,
- String reason) {
+ boolean stopIfExecutingLocked(String pkgName, int userId, @Nullable String namespace,
+ boolean matchJobId, int jobId, int stopReason, int internalStopReason) {
final JobStatus executing = getRunningJobLocked();
if (executing != null && (userId == UserHandle.USER_ALL || userId == executing.getUserId())
&& (pkgName == null || pkgName.equals(executing.getSourcePackageName()))
+ && Objects.equals(namespace, executing.getNamespace())
&& (!matchJobId || jobId == executing.getJobId())) {
if (mVerb == VERB_EXECUTING) {
- mParams.setStopReason(JobParameters.STOP_REASON_TIMEOUT,
- JobParameters.INTERNAL_STOP_REASON_TIMEOUT, reason);
- sendStopMessageLocked("force timeout from shell");
+ mParams.setStopReason(stopReason, internalStopReason, "stop from shell");
+ sendStopMessageLocked("stop from shell");
return true;
}
}
return false;
}
+ @GuardedBy("mLock")
+ Pair<Long, Long> getEstimatedNetworkBytes() {
+ return Pair.create(mEstimatedDownloadBytes, mEstimatedUploadBytes);
+ }
+
+ @GuardedBy("mLock")
+ Pair<Long, Long> getTransferredNetworkBytes() {
+ return Pair.create(mTransferredDownloadBytes, mTransferredUploadBytes);
+ }
+
void doJobFinished(JobCallback cb, int jobId, boolean reschedule) {
final long ident = Binder.clearCallingIdentity();
try {
@@ -488,6 +756,28 @@ public final class JobServiceContext implements ServiceConnection {
}
}
+ private void doAcknowledgeGetTransferredDownloadBytesMessage(JobCallback cb, int jobId,
+ int workId, @BytesLong long transferredBytes) {
+ // TODO(255393346): Make sure apps call this appropriately and monitor for abuse
+ synchronized (mLock) {
+ if (!verifyCallerLocked(cb)) {
+ return;
+ }
+ mTransferredDownloadBytes = transferredBytes;
+ }
+ }
+
+ private void doAcknowledgeGetTransferredUploadBytesMessage(JobCallback cb, int jobId,
+ int workId, @BytesLong long transferredBytes) {
+ // TODO(255393346): Make sure apps call this appropriately and monitor for abuse
+ synchronized (mLock) {
+ if (!verifyCallerLocked(cb)) {
+ return;
+ }
+ mTransferredUploadBytes = transferredBytes;
+ }
+ }
+
void doAcknowledgeStopMessage(JobCallback cb, int jobId, boolean reschedule) {
doCallback(cb, reschedule, null);
}
@@ -516,6 +806,9 @@ public final class JobServiceContext implements ServiceConnection {
"last work dequeued");
// This will finish the job.
doCallbackLocked(false, "last work dequeued");
+ } else if (work != null) {
+ // Delivery count has been updated, so persist JobWorkItem change.
+ mService.mJobs.touchJob(mRunningJob);
}
return work;
}
@@ -533,7 +826,135 @@ public final class JobServiceContext implements ServiceConnection {
// Exception-throwing-can down the road to JobParameters.completeWork >:(
return true;
}
- return mRunningJob.completeWorkLocked(workId);
+ if (mRunningJob.completeWorkLocked(workId)) {
+ mService.mJobs.touchJob(mRunningJob);
+ return true;
+ }
+ return false;
+ }
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ private void doUpdateEstimatedNetworkBytes(JobCallback cb, int jobId,
+ @Nullable JobWorkItem item, long downloadBytes, long uploadBytes) {
+ // TODO(255393346): Make sure apps call this appropriately and monitor for abuse
+ synchronized (mLock) {
+ if (!verifyCallerLocked(cb)) {
+ return;
+ }
+ Counter.logIncrementWithUid(
+ "job_scheduler.value_cntr_w_uid_estimated_network_bytes_updated",
+ mRunningJob.getUid());
+ sUpdatedEstimatedNetworkDownloadKBLogger.logSample(
+ safelyScaleBytesToKBForHistogram(downloadBytes));
+ sUpdatedEstimatedNetworkUploadKBLogger.logSample(
+ safelyScaleBytesToKBForHistogram(uploadBytes));
+ if (mEstimatedDownloadBytes != JobInfo.NETWORK_BYTES_UNKNOWN
+ && downloadBytes != JobInfo.NETWORK_BYTES_UNKNOWN) {
+ if (mEstimatedDownloadBytes < downloadBytes) {
+ Counter.logIncrementWithUid(
+ "job_scheduler."
+ + "value_cntr_w_uid_estimated_network_download_bytes_increased",
+ mRunningJob.getUid());
+ } else if (mEstimatedDownloadBytes > downloadBytes) {
+ Counter.logIncrementWithUid(
+ "job_scheduler."
+ + "value_cntr_w_uid_estimated_network_download_bytes_decreased",
+ mRunningJob.getUid());
+ }
+ }
+ if (mEstimatedUploadBytes != JobInfo.NETWORK_BYTES_UNKNOWN
+ && uploadBytes != JobInfo.NETWORK_BYTES_UNKNOWN) {
+ if (mEstimatedUploadBytes < uploadBytes) {
+ Counter.logIncrementWithUid(
+ "job_scheduler"
+ + ".value_cntr_w_uid_estimated_network_upload_bytes_increased",
+ mRunningJob.getUid());
+ } else if (mEstimatedUploadBytes > uploadBytes) {
+ Counter.logIncrementWithUid(
+ "job_scheduler"
+ + ".value_cntr_w_uid_estimated_network_upload_bytes_decreased",
+ mRunningJob.getUid());
+ }
+ }
+ mEstimatedDownloadBytes = downloadBytes;
+ mEstimatedUploadBytes = uploadBytes;
+ }
+ }
+
+ private void doUpdateTransferredNetworkBytes(JobCallback cb, int jobId,
+ @Nullable JobWorkItem item, long downloadBytes, long uploadBytes) {
+ // TODO(255393346): Make sure apps call this appropriately and monitor for abuse
+ synchronized (mLock) {
+ if (!verifyCallerLocked(cb)) {
+ return;
+ }
+ Counter.logIncrementWithUid(
+ "job_scheduler.value_cntr_w_uid_transferred_network_bytes_updated",
+ mRunningJob.getUid());
+ sTransferredNetworkDownloadKBHighWaterMarkLogger.logSample(
+ safelyScaleBytesToKBForHistogram(downloadBytes));
+ sTransferredNetworkUploadKBHighWaterMarkLogger.logSample(
+ safelyScaleBytesToKBForHistogram(uploadBytes));
+ if (mTransferredDownloadBytes != JobInfo.NETWORK_BYTES_UNKNOWN
+ && downloadBytes != JobInfo.NETWORK_BYTES_UNKNOWN) {
+ if (mTransferredDownloadBytes < downloadBytes) {
+ Counter.logIncrementWithUid(
+ "job_scheduler."
+ + "value_cntr_w_uid_transferred_network_download_bytes_increased",
+ mRunningJob.getUid());
+ } else if (mTransferredDownloadBytes > downloadBytes) {
+ Counter.logIncrementWithUid(
+ "job_scheduler."
+ + "value_cntr_w_uid_transferred_network_download_bytes_decreased",
+ mRunningJob.getUid());
+ }
+ }
+ if (mTransferredUploadBytes != JobInfo.NETWORK_BYTES_UNKNOWN
+ && uploadBytes != JobInfo.NETWORK_BYTES_UNKNOWN) {
+ if (mTransferredUploadBytes < uploadBytes) {
+ Counter.logIncrementWithUid(
+ "job_scheduler."
+ + "value_cntr_w_uid_transferred_network_upload_bytes_increased",
+ mRunningJob.getUid());
+ } else if (mTransferredUploadBytes > uploadBytes) {
+ Counter.logIncrementWithUid(
+ "job_scheduler."
+ + "value_cntr_w_uid_transferred_network_upload_bytes_decreased",
+ mRunningJob.getUid());
+ }
+ }
+ mTransferredDownloadBytes = downloadBytes;
+ mTransferredUploadBytes = uploadBytes;
+ }
+ }
+
+ private void doSetNotification(JobCallback cb, int jodId, int notificationId,
+ Notification notification, int jobEndNotificationPolicy) {
+ final int callingPid = Binder.getCallingPid();
+ final int callingUid = Binder.getCallingUid();
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ if (!verifyCallerLocked(cb)) {
+ return;
+ }
+ if (callingUid != mRunningJob.getUid()) {
+ Slog.wtfStack(TAG, "Calling UID isn't the same as running job's UID...");
+ throw new SecurityException("Can't post notification on behalf of another app");
+ }
+ final String callingPkgName = mRunningJob.getServiceComponent().getPackageName();
+ mNotificationCoordinator.enqueueNotification(this, callingPkgName,
+ callingPid, callingUid, notificationId,
+ notification, jobEndNotificationPolicy);
+ if (mAwaitingNotification) {
+ mAwaitingNotification = false;
+ if (mVerb == VERB_EXECUTING) {
+ scheduleOpTimeOutLocked();
+ }
+ }
}
} finally {
Binder.restoreCallingIdentity(ident);
@@ -570,6 +991,17 @@ public final class JobServiceContext implements ServiceConnection {
@Override
public void onServiceDisconnected(ComponentName name) {
synchronized (mLock) {
+ if (mDeathMarkStopReason != JobParameters.STOP_REASON_UNDEFINED) {
+ // Service "unexpectedly" disconnected, but we knew the process was going to die.
+ // Use that as the stop reason for logging/debugging purposes.
+ mParams.setStopReason(
+ mDeathMarkStopReason, mDeathMarkInternalStopReason, mDeathMarkDebugReason);
+ } else if (mRunningJob != null) {
+ Counter.logIncrementWithUid(
+ "job_scheduler.value_cntr_w_uid_unexpected_service_disconnects",
+ // Use the calling UID since that's the one this context was connected to.
+ mRunningJob.getUid());
+ }
closeAndCleanupJobLocked(true /* needsReschedule */, "unexpectedly disconnected");
}
}
@@ -734,10 +1166,10 @@ public final class JobServiceContext implements ServiceConnection {
@GuardedBy("mLock")
private void doCancelLocked(@JobParameters.StopReason int stopReasonCode,
int internalStopReasonCode, @Nullable String debugReason) {
- if (mVerb == VERB_FINISHED) {
+ if (mVerb == VERB_FINISHED || mVerb == VERB_STOPPING) {
if (DEBUG) {
Slog.d(TAG,
- "Trying to process cancel for torn-down context, ignoring.");
+ "Too late to process cancel for context (verb=" + mVerb + "), ignoring.");
}
return;
}
@@ -821,6 +1253,13 @@ public final class JobServiceContext implements ServiceConnection {
return;
}
scheduleOpTimeOutLocked();
+ if (mPendingNetworkChange != null
+ && !Objects.equals(mParams.getNetwork(), mPendingNetworkChange)) {
+ informOfNetworkChangeLocked(mPendingNetworkChange);
+ }
+ if (mRunningJob.isUserVisibleJob()) {
+ mService.informObserversOfUserVisibleJobChange(this, mRunningJob, true);
+ }
break;
default:
Slog.e(TAG, "Handling started job but job wasn't starting! Was "
@@ -887,23 +1326,37 @@ public final class JobServiceContext implements ServiceConnection {
private void handleOpTimeoutLocked() {
switch (mVerb) {
case VERB_BINDING:
- Slog.w(TAG, "Time-out while trying to bind " + getRunningJobNameLocked()
- + ", dropping.");
- closeAndCleanupJobLocked(false /* needsReschedule */, "timed out while binding");
+ onSlowAppResponseLocked(/* reschedule */ false, /* updateStopReasons */ true,
+ /* texCounterMetricId */
+ "job_scheduler.value_cntr_w_uid_slow_app_response_binding",
+ /* debugReason */ "timed out while binding",
+ /* anrMessage */ "Timed out while trying to bind",
+ CompatChanges.isChangeEnabled(ANR_PRE_UDC_APIS_ON_SLOW_RESPONSES,
+ mRunningJob.getUid()));
break;
case VERB_STARTING:
// Client unresponsive - wedged or failed to respond in time. We don't really
// know what happened so let's log it and notify the JobScheduler
// FINISHED/NO-RETRY.
- Slog.w(TAG, "No response from client for onStartJob "
- + getRunningJobNameLocked());
- closeAndCleanupJobLocked(false /* needsReschedule */, "timed out while starting");
+ onSlowAppResponseLocked(/* reschedule */ false, /* updateStopReasons */ true,
+ /* texCounterMetricId */
+ "job_scheduler.value_cntr_w_uid_slow_app_response_on_start_job",
+ /* debugReason */ "timed out while starting",
+ /* anrMessage */ "No response to onStartJob",
+ CompatChanges.isChangeEnabled(ANR_PRE_UDC_APIS_ON_SLOW_RESPONSES,
+ mRunningJob.getUid()));
break;
case VERB_STOPPING:
// At least we got somewhere, so fail but ask the JobScheduler to reschedule.
- Slog.w(TAG, "No response from client for onStopJob "
- + getRunningJobNameLocked());
- closeAndCleanupJobLocked(true /* needsReschedule */, "timed out while stopping");
+ // Don't update the stop reasons since we were already stopping the job for some
+ // other reason.
+ onSlowAppResponseLocked(/* reschedule */ true, /* updateStopReasons */ false,
+ /* texCounterMetricId */
+ "job_scheduler.value_cntr_w_uid_slow_app_response_on_stop_job",
+ /* debugReason */ "timed out while stopping",
+ /* anrMessage */ "No response to onStopJob",
+ CompatChanges.isChangeEnabled(ANR_PRE_UDC_APIS_ON_SLOW_RESPONSES,
+ mRunningJob.getUid()));
break;
case VERB_EXECUTING:
if (mPendingStopReason != JobParameters.STOP_REASON_UNDEFINED) {
@@ -925,6 +1378,8 @@ public final class JobServiceContext implements ServiceConnection {
}
final long latestStopTimeElapsed =
mExecutionStartTimeElapsed + mMaxExecutionTimeMillis;
+ final long earliestStopTimeElapsed =
+ mExecutionStartTimeElapsed + mMinExecutionGuaranteeMillis;
final long nowElapsed = sElapsedRealtimeClock.millis();
if (nowElapsed >= latestStopTimeElapsed) {
// Not an error - client ran out of time.
@@ -933,7 +1388,7 @@ public final class JobServiceContext implements ServiceConnection {
mParams.setStopReason(JobParameters.STOP_REASON_TIMEOUT,
JobParameters.INTERNAL_STOP_REASON_TIMEOUT, "client timed out");
sendStopMessageLocked("timeout while executing");
- } else {
+ } else if (nowElapsed >= earliestStopTimeElapsed) {
// We've given the app the minimum execution time. See if we should stop it or
// let it continue running
final String reason = mJobConcurrencyManager.shouldStopRunningJobLocked(this);
@@ -951,6 +1406,24 @@ public final class JobServiceContext implements ServiceConnection {
+ " continue to run past min execution time");
scheduleOpTimeOutLocked();
}
+ } else if (mAwaitingNotification) {
+ onSlowAppResponseLocked(/* reschedule */ true, /* updateStopReasons */ true,
+ /* texCounterMetricId */
+ "job_scheduler.value_cntr_w_uid_slow_app_response_set_notification",
+ /* debugReason */ "timed out while stopping",
+ /* anrMessage */ "required notification not provided",
+ /* triggerAnr */ true);
+ } else {
+ final long timeSinceDurationStampTimeMs =
+ nowElapsed - mLastExecutionDurationStampTimeElapsed;
+ if (timeSinceDurationStampTimeMs < EXECUTION_DURATION_STAMP_PERIOD_MILLIS) {
+ Slog.e(TAG, "Unexpected op timeout while EXECUTING");
+ }
+ // Update the execution time even if this wasn't the pre-set time.
+ mRunningJob.incrementCumulativeExecutionTime(timeSinceDurationStampTimeMs);
+ mService.mJobs.touchJob(mRunningJob);
+ mLastExecutionDurationStampTimeElapsed = nowElapsed;
+ scheduleOpTimeOutLocked();
}
break;
default:
@@ -984,6 +1457,27 @@ public final class JobServiceContext implements ServiceConnection {
}
}
+ @GuardedBy("mLock")
+ private void onSlowAppResponseLocked(boolean reschedule, boolean updateStopReasons,
+ @NonNull String texCounterMetricId,
+ @NonNull String debugReason, @NonNull String anrMessage, boolean triggerAnr) {
+ Slog.w(TAG, anrMessage + " for " + getRunningJobNameLocked());
+ // Use the calling UID since that's the one this context was connected to.
+ Counter.logIncrementWithUid(texCounterMetricId, mRunningJob.getUid());
+ if (updateStopReasons) {
+ mParams.setStopReason(
+ JobParameters.STOP_REASON_UNDEFINED,
+ JobParameters.INTERNAL_STOP_REASON_ANR,
+ debugReason);
+ }
+ if (triggerAnr) {
+ mActivityManagerInternal.appNotResponding(
+ mRunningJob.serviceProcessName, mRunningJob.getUid(),
+ TimeoutRecord.forJobService(anrMessage));
+ }
+ closeAndCleanupJobLocked(reschedule, debugReason);
+ }
+
/**
* The provided job has finished, either by calling
* {@link android.app.job.JobService#jobFinished(android.app.job.JobParameters, boolean)}
@@ -991,28 +1485,55 @@ public final class JobServiceContext implements ServiceConnection {
* we want to clean up internally.
*/
@GuardedBy("mLock")
- private void closeAndCleanupJobLocked(boolean reschedule, @Nullable String reason) {
+ private void closeAndCleanupJobLocked(boolean reschedule, @Nullable String loggingDebugReason) {
final JobStatus completedJob;
if (mVerb == VERB_FINISHED) {
return;
}
if (DEBUG) {
Slog.d(TAG, "Cleaning up " + mRunningJob.toShortString()
- + " reschedule=" + reschedule + " reason=" + reason);
+ + " reschedule=" + reschedule + " reason=" + loggingDebugReason);
}
- applyStoppedReasonLocked(reason);
+ final long nowElapsed = sElapsedRealtimeClock.millis();
+ applyStoppedReasonLocked(loggingDebugReason);
completedJob = mRunningJob;
- final int internalStopReason = mParams.getInternalStopReasonCode();
+ completedJob.incrementCumulativeExecutionTime(
+ nowElapsed - mLastExecutionDurationStampTimeElapsed);
+ // Use the JobParameters stop reasons for logging and metric purposes,
+ // but if the job was marked for death, use that reason for rescheduling purposes.
+ // The discrepancy could happen if a job ends up stopping for some reason
+ // in the time between the job being marked and the process actually dying.
+ // Since the job stopped for another reason, we want to log the actual stop reason
+ // for the sake of accurate metrics and debugging,
+ // but we should use the death mark reasons when determining reschedule policy.
+ final int loggingStopReason = mParams.getStopReason();
+ final int loggingInternalStopReason = mParams.getInternalStopReasonCode();
+ final int reschedulingStopReason, reschedulingInternalStopReason;
+ if (mDeathMarkStopReason != JobParameters.STOP_REASON_UNDEFINED) {
+ if (DEBUG) {
+ Slog.d(TAG, "Job marked for death because of "
+ + JobParameters.getInternalReasonCodeDescription(
+ mDeathMarkInternalStopReason)
+ + ": " + mDeathMarkDebugReason);
+ }
+ reschedulingStopReason = mDeathMarkStopReason;
+ reschedulingInternalStopReason = mDeathMarkInternalStopReason;
+ } else {
+ reschedulingStopReason = loggingStopReason;
+ reschedulingInternalStopReason = loggingInternalStopReason;
+ }
mPreviousJobHadSuccessfulFinish =
- (internalStopReason == JobParameters.INTERNAL_STOP_REASON_SUCCESSFUL_FINISH);
+ (loggingInternalStopReason == JobParameters.INTERNAL_STOP_REASON_SUCCESSFUL_FINISH);
if (!mPreviousJobHadSuccessfulFinish) {
- mLastUnsuccessfulFinishElapsed = sElapsedRealtimeClock.millis();
+ mLastUnsuccessfulFinishElapsed = nowElapsed;
}
- mJobPackageTracker.noteInactive(completedJob, internalStopReason, reason);
+ mJobPackageTracker.noteInactive(completedJob,
+ loggingInternalStopReason, loggingDebugReason);
FrameworkStatsLog.write_non_chained(FrameworkStatsLog.SCHEDULED_JOB_STATE_CHANGED,
completedJob.getSourceUid(), null, completedJob.getBatteryName(),
FrameworkStatsLog.SCHEDULED_JOB_STATE_CHANGED__STATE__FINISHED,
- internalStopReason, completedJob.getStandbyBucket(), completedJob.getJobId(),
+ loggingInternalStopReason, completedJob.getStandbyBucket(),
+ completedJob.getLoggingJobId(),
completedJob.hasChargingConstraint(),
completedJob.hasBatteryNotLowConstraint(),
completedJob.hasStorageNotLowConstraint(),
@@ -1023,24 +1544,49 @@ public final class JobServiceContext implements ServiceConnection {
completedJob.hasContentTriggerConstraint(),
completedJob.isRequestedExpeditedJob(),
completedJob.startedAsExpeditedJob,
- mParams.getStopReason(),
+ loggingStopReason,
completedJob.getJob().isPrefetch(),
completedJob.getJob().getPriority(),
completedJob.getEffectivePriority(),
- completedJob.getNumFailures());
- Trace.asyncTraceEnd(Trace.TRACE_TAG_SYSTEM_SERVER, completedJob.getTag(), getId());
+ completedJob.getNumPreviousAttempts(),
+ completedJob.getJob().getMaxExecutionDelayMillis(),
+ mParams.isOverrideDeadlineExpired(),
+ completedJob.isConstraintSatisfied(JobInfo.CONSTRAINT_FLAG_CHARGING),
+ completedJob.isConstraintSatisfied(JobInfo.CONSTRAINT_FLAG_BATTERY_NOT_LOW),
+ completedJob.isConstraintSatisfied(JobInfo.CONSTRAINT_FLAG_STORAGE_NOT_LOW),
+ completedJob.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY),
+ completedJob.isConstraintSatisfied(JobInfo.CONSTRAINT_FLAG_DEVICE_IDLE),
+ completedJob.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY),
+ completedJob.isConstraintSatisfied(JobStatus.CONSTRAINT_CONTENT_TRIGGER),
+ mExecutionStartTimeElapsed - completedJob.enqueueTime,
+ completedJob.getJob().isUserInitiated(),
+ completedJob.startedAsUserInitiatedJob,
+ completedJob.getJob().isPeriodic(),
+ completedJob.getJob().getMinLatencyMillis(),
+ completedJob.getEstimatedNetworkDownloadBytes(),
+ completedJob.getEstimatedNetworkUploadBytes(),
+ completedJob.getWorkCount(),
+ ActivityManager
+ .processStateAmToProto(mService.getUidProcState(completedJob.getUid())),
+ completedJob.getNamespaceHash());
+ if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) {
+ Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_SYSTEM_SERVER, "JobScheduler",
+ getId());
+ }
try {
mBatteryStats.noteJobFinish(mRunningJob.getBatteryName(), mRunningJob.getSourceUid(),
- internalStopReason);
+ loggingInternalStopReason);
} catch (RemoteException e) {
// Whatever.
}
- if (mParams.getStopReason() == JobParameters.STOP_REASON_TIMEOUT) {
+ if (loggingStopReason == JobParameters.STOP_REASON_TIMEOUT) {
mEconomyManagerInternal.noteInstantaneousEvent(
mRunningJob.getSourceUserId(), mRunningJob.getSourcePackageName(),
JobSchedulerEconomicPolicy.ACTION_JOB_TIMEOUT,
String.valueOf(mRunningJob.getJobId()));
}
+ mNotificationCoordinator.removeNotificationAssociation(this,
+ reschedulingStopReason, completedJob);
if (mWakeLock != null) {
mWakeLock.release();
}
@@ -1055,11 +1601,20 @@ public final class JobServiceContext implements ServiceConnection {
mCancelled = false;
service = null;
mAvailable = true;
+ mDeathMarkStopReason = JobParameters.STOP_REASON_UNDEFINED;
+ mDeathMarkInternalStopReason = 0;
+ mDeathMarkDebugReason = null;
+ mLastExecutionDurationStampTimeElapsed = 0;
mPendingStopReason = JobParameters.STOP_REASON_UNDEFINED;
mPendingInternalStopReason = 0;
mPendingDebugStopReason = null;
+ mPendingNetworkChange = null;
removeOpTimeOutLocked();
- mCompletedListener.onJobCompletedLocked(completedJob, internalStopReason, reschedule);
+ if (completedJob.isUserVisibleJob()) {
+ mService.informObserversOfUserVisibleJobChange(this, completedJob, false);
+ }
+ mCompletedListener.onJobCompletedLocked(completedJob,
+ reschedulingStopReason, reschedulingInternalStopReason, reschedule);
mJobConcurrencyManager.onJobCompletedLocked(this, completedJob, workType);
}
@@ -1085,16 +1640,22 @@ public final class JobServiceContext implements ServiceConnection {
final long timeoutMillis;
switch (mVerb) {
case VERB_EXECUTING:
+ long minTimeout;
final long earliestStopTimeElapsed =
mExecutionStartTimeElapsed + mMinExecutionGuaranteeMillis;
final long latestStopTimeElapsed =
mExecutionStartTimeElapsed + mMaxExecutionTimeMillis;
final long nowElapsed = sElapsedRealtimeClock.millis();
if (nowElapsed < earliestStopTimeElapsed) {
- timeoutMillis = earliestStopTimeElapsed - nowElapsed;
+ minTimeout = earliestStopTimeElapsed - nowElapsed;
} else {
- timeoutMillis = latestStopTimeElapsed - nowElapsed;
+ minTimeout = latestStopTimeElapsed - nowElapsed;
+ }
+ if (mAwaitingNotification) {
+ minTimeout = Math.min(minTimeout, NOTIFICATION_TIMEOUT_MILLIS);
}
+ minTimeout = Math.min(minTimeout, EXECUTION_DURATION_STAMP_PERIOD_MILLIS);
+ timeoutMillis = minTimeout;
break;
case VERB_BINDING:
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 fcfb45c5f1df..0a7bffc786cc 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
@@ -22,8 +22,10 @@ import static android.net.NetworkCapabilities.TRANSPORT_TEST;
import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
import static com.android.server.job.JobSchedulerService.sSystemClock;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.job.JobInfo;
+import android.app.job.JobWorkItem;
import android.content.ComponentName;
import android.content.Context;
import android.net.NetworkRequest;
@@ -32,7 +34,6 @@ import android.os.Handler;
import android.os.PersistableBundle;
import android.os.Process;
import android.os.SystemClock;
-import android.os.UserHandle;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.ArraySet;
@@ -40,14 +41,18 @@ import android.util.AtomicFile;
import android.util.Pair;
import android.util.Slog;
import android.util.SparseArray;
+import android.util.SparseBooleanArray;
import android.util.SystemConfigFileCommitEventLogger;
-import android.util.TypedXmlSerializer;
import android.util.Xml;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.BitUtils;
+import com.android.modules.expresslog.Histogram;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+import com.android.server.AppSchedulingModuleThread;
import com.android.server.IoThread;
import com.android.server.job.JobSchedulerInternal.JobStorePersistStats;
import com.android.server.job.controllers.JobStatus;
@@ -64,8 +69,10 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
+import java.util.Objects;
import java.util.Set;
import java.util.StringJoiner;
+import java.util.concurrent.CountDownLatch;
import java.util.function.Consumer;
import java.util.function.Predicate;
@@ -89,6 +96,12 @@ public final class JobStore {
/** Threshold to adjust how often we want to write to the db. */
private static final long JOB_PERSIST_DELAY = 2000L;
+ private static final long SCHEDULED_JOB_HIGH_WATER_MARK_PERIOD_MS = 30 * 60_000L;
+ @VisibleForTesting
+ static final String JOB_FILE_SPLIT_PREFIX = "jobs_";
+ private static final int ALL_UIDS = -1;
+ @VisibleForTesting
+ static final int INVALID_UID = -2;
final Object mLock;
final Object mWriteScheduleLock; // used solely for invariants around write scheduling
@@ -105,17 +118,48 @@ public final class JobStore {
@GuardedBy("mWriteScheduleLock")
private boolean mWriteInProgress;
+ @GuardedBy("mWriteScheduleLock")
+ private boolean mSplitFileMigrationNeeded;
+
private static final Object sSingletonLock = new Object();
private final SystemConfigFileCommitEventLogger mEventLogger;
private final AtomicFile mJobsFile;
+ private final File mJobFileDirectory;
+ private final SparseBooleanArray mPendingJobWriteUids = new SparseBooleanArray();
/** Handler backed by IoThread for writing to disk. */
private final Handler mIoHandler = IoThread.getHandler();
private static JobStore sSingleton;
+ private boolean mUseSplitFiles = JobSchedulerService.Constants.DEFAULT_PERSIST_IN_SPLIT_FILES;
+
private JobStorePersistStats mPersistInfo = new JobStorePersistStats();
+ /**
+ * Separately updated value of the JobSet size to avoid recalculating it frequently for logging
+ * purposes. Continue to use {@link JobSet#size()} for the up-to-date and accurate value.
+ */
+ private int mCurrentJobSetSize = 0;
+ private int mScheduledJob30MinHighWaterMark = 0;
+ private static final Histogram sScheduledJob30MinHighWaterMarkLogger = new Histogram(
+ "job_scheduler.value_hist_scheduled_job_30_min_high_water_mark",
+ new Histogram.ScaledRangeOptions(15, 1, 99, 1.5f));
+ private final Runnable mScheduledJobHighWaterMarkLoggingRunnable = new Runnable() {
+ @Override
+ public void run() {
+ AppSchedulingModuleThread.getHandler().removeCallbacks(this);
+ synchronized (mLock) {
+ sScheduledJob30MinHighWaterMarkLogger.logSample(mScheduledJob30MinHighWaterMark);
+ mScheduledJob30MinHighWaterMark = mJobSet.size();
+ }
+ // The count doesn't need to be logged at exact times. Logging based on system uptime
+ // should be fine.
+ AppSchedulingModuleThread.getHandler()
+ .postDelayed(this, SCHEDULED_JOB_HIGH_WATER_MARK_PERIOD_MS);
+ }
+ };
+
/** Used by the {@link JobSchedulerService} to instantiate the JobStore. */
- static JobStore initAndGet(JobSchedulerService jobManagerService) {
+ static JobStore get(JobSchedulerService jobManagerService) {
synchronized (sSingletonLock) {
if (sSingleton == null) {
sSingleton = new JobStore(jobManagerService.getContext(),
@@ -131,6 +175,7 @@ public final class JobStore {
@VisibleForTesting
public static JobStore initAndGetForTesting(Context context, File dataDir) {
JobStore jobStoreUnderTest = new JobStore(context, new Object(), dataDir);
+ jobStoreUnderTest.init();
jobStoreUnderTest.clearForTesting();
return jobStoreUnderTest;
}
@@ -144,10 +189,10 @@ public final class JobStore {
mContext = context;
File systemDir = new File(dataDir, "system");
- File jobDir = new File(systemDir, "job");
- jobDir.mkdirs();
+ mJobFileDirectory = new File(systemDir, "job");
+ mJobFileDirectory.mkdirs();
mEventLogger = new SystemConfigFileCommitEventLogger("jobs");
- mJobsFile = new AtomicFile(new File(jobDir, "jobs.xml"), mEventLogger);
+ mJobsFile = createJobFile(new File(mJobFileDirectory, "jobs.xml"));
mJobSet = new JobSet();
@@ -162,12 +207,30 @@ public final class JobStore {
// an incorrect historical timestamp. That's fine; at worst we'll reboot with
// a *correct* timestamp, see a bunch of overdue jobs, and run them; then
// settle into normal operation.
- mXmlTimestamp = mJobsFile.getLastModifiedTime();
+ mXmlTimestamp = mJobsFile.exists()
+ ? mJobsFile.getLastModifiedTime() : mJobFileDirectory.lastModified();
mRtcGood = (sSystemClock.millis() > mXmlTimestamp);
+ AppSchedulingModuleThread.getHandler().postDelayed(
+ mScheduledJobHighWaterMarkLoggingRunnable, SCHEDULED_JOB_HIGH_WATER_MARK_PERIOD_MS);
+ }
+
+ private void init() {
readJobMapFromDisk(mJobSet, mRtcGood);
}
+ void initAsync(CountDownLatch completionLatch) {
+ mIoHandler.post(new ReadJobMapFromDiskRunnable(mJobSet, mRtcGood, completionLatch));
+ }
+
+ private AtomicFile createJobFile(String baseName) {
+ return createJobFile(new File(mJobFileDirectory, baseName + ".xml"));
+ }
+
+ private AtomicFile createJobFile(File file) {
+ return new AtomicFile(file, mEventLogger);
+ }
+
public boolean jobTimesInflatedValid() {
return mRtcGood;
}
@@ -177,6 +240,15 @@ public final class JobStore {
}
/**
+ * Runs any necessary work asynchronously. If this is called after
+ * {@link #initAsync(CountDownLatch)}, this ensures the given work runs after
+ * the JobStore is initialized.
+ */
+ void runWorkAsync(@NonNull Runnable r) {
+ mIoHandler.post(r);
+ }
+
+ /**
* Find all the jobs that were affected by RTC clock uncertainty at boot time. Returns
* parallel lists of the existing JobStatus objects and of new, equivalent JobStatus instances
* with now-corrected time bounds.
@@ -194,7 +266,8 @@ public final class JobStore {
convertRtcBoundsToElapsed(utcTimes, elapsedNow);
JobStatus newJob = new JobStatus(job,
elapsedRuntimes.first, elapsedRuntimes.second,
- 0, job.getLastSuccessfulRunTime(), job.getLastFailedRunTime());
+ 0, 0, job.getLastSuccessfulRunTime(), job.getLastFailedRunTime(),
+ job.getCumulativeExecutionTimeMs());
newJob.prepareLocked();
toAdd.add(newJob);
toRemove.add(job);
@@ -203,21 +276,23 @@ public final class JobStore {
}
/**
- * Add a job to the master list, persisting it if necessary. If the JobStatus already exists,
- * it will be replaced.
+ * Add a job to the master list, persisting it if necessary.
+ * Similar jobs to the new job will not be removed.
+ *
* @param jobStatus Job to add.
- * @return Whether or not an equivalent JobStatus was replaced by this operation.
*/
- public boolean add(JobStatus jobStatus) {
- boolean replaced = mJobSet.remove(jobStatus);
- mJobSet.add(jobStatus);
+ public void add(JobStatus jobStatus) {
+ if (mJobSet.add(jobStatus)) {
+ mCurrentJobSetSize++;
+ maybeUpdateHighWaterMark();
+ }
if (jobStatus.isPersisted()) {
+ mPendingJobWriteUids.put(jobStatus.getUid(), true);
maybeWriteStatusToDiskAsync();
}
if (DEBUG) {
Slog.d(TAG, "Added job status to store: " + jobStatus);
}
- return replaced;
}
/**
@@ -225,7 +300,13 @@ public final class JobStore {
*/
@VisibleForTesting
public void addForTesting(JobStatus jobStatus) {
- mJobSet.add(jobStatus);
+ if (mJobSet.add(jobStatus)) {
+ mCurrentJobSetSize++;
+ maybeUpdateHighWaterMark();
+ }
+ if (jobStatus.isPersisted()) {
+ mPendingJobWriteUids.put(jobStatus.getUid(), true);
+ }
}
boolean containsJob(JobStatus jobStatus) {
@@ -258,23 +339,50 @@ public final class JobStore {
}
return false;
}
+ mCurrentJobSetSize--;
if (removeFromPersisted && jobStatus.isPersisted()) {
+ mPendingJobWriteUids.put(jobStatus.getUid(), true);
maybeWriteStatusToDiskAsync();
}
return removed;
}
/**
+ * Like {@link #remove(JobStatus, boolean)}, but doesn't schedule a disk write.
+ */
+ @VisibleForTesting
+ public void removeForTesting(JobStatus jobStatus) {
+ if (mJobSet.remove(jobStatus)) {
+ mCurrentJobSetSize--;
+ }
+ if (jobStatus.isPersisted()) {
+ mPendingJobWriteUids.put(jobStatus.getUid(), true);
+ }
+ }
+
+ /**
* Remove the jobs of users not specified in the keepUserIds.
* @param keepUserIds Array of User IDs whose jobs should be kept and not removed.
*/
public void removeJobsOfUnlistedUsers(int[] keepUserIds) {
mJobSet.removeJobsOfUnlistedUsers(keepUserIds);
+ mCurrentJobSetSize = mJobSet.size();
+ }
+
+ /** Note a change in the specified JobStatus that necessitates writing job state to disk. */
+ void touchJob(@NonNull JobStatus jobStatus) {
+ if (!jobStatus.isPersisted()) {
+ return;
+ }
+ mPendingJobWriteUids.put(jobStatus.getUid(), true);
+ maybeWriteStatusToDiskAsync();
}
@VisibleForTesting
public void clear() {
mJobSet.clear();
+ mPendingJobWriteUids.put(ALL_UIDS, true);
+ mCurrentJobSetSize = 0;
maybeWriteStatusToDiskAsync();
}
@@ -284,31 +392,73 @@ public final class JobStore {
@VisibleForTesting
public void clearForTesting() {
mJobSet.clear();
+ mPendingJobWriteUids.put(ALL_UIDS, true);
+ mCurrentJobSetSize = 0;
+ }
+
+ void setUseSplitFiles(boolean useSplitFiles) {
+ synchronized (mLock) {
+ if (mUseSplitFiles != useSplitFiles) {
+ mUseSplitFiles = useSplitFiles;
+ migrateJobFilesAsync();
+ }
+ }
+ }
+
+ /**
+ * The same as above but does not schedule writing. This makes perf benchmarks more stable.
+ */
+ @VisibleForTesting
+ public void setUseSplitFilesForTesting(boolean useSplitFiles) {
+ final boolean changed;
+ synchronized (mLock) {
+ changed = mUseSplitFiles != useSplitFiles;
+ if (changed) {
+ mUseSplitFiles = useSplitFiles;
+ mPendingJobWriteUids.put(ALL_UIDS, true);
+ }
+ }
+ if (changed) {
+ synchronized (mWriteScheduleLock) {
+ mSplitFileMigrationNeeded = true;
+ }
+ }
}
/**
- * @param userHandle User for whom we are querying the list of jobs.
- * @return A list of all the jobs scheduled for the provided user. Never null.
+ * @param sourceUid Uid of the source app.
+ * @return A list of all the jobs scheduled for the source app. Never null.
*/
- public List<JobStatus> getJobsByUser(int userHandle) {
- return mJobSet.getJobsByUser(userHandle);
+ @NonNull
+ public ArraySet<JobStatus> getJobsBySourceUid(int sourceUid) {
+ return mJobSet.getJobsBySourceUid(sourceUid);
+ }
+
+ public void getJobsBySourceUid(int sourceUid, @NonNull Set<JobStatus> insertInto) {
+ mJobSet.getJobsBySourceUid(sourceUid, insertInto);
}
/**
* @param uid Uid of the requesting app.
* @return All JobStatus objects for a given uid from the master list. Never null.
*/
- public List<JobStatus> getJobsByUid(int uid) {
+ @NonNull
+ public ArraySet<JobStatus> getJobsByUid(int uid) {
return mJobSet.getJobsByUid(uid);
}
+ public void getJobsByUid(int uid, @NonNull Set<JobStatus> insertInto) {
+ mJobSet.getJobsByUid(uid, insertInto);
+ }
+
/**
* @param uid Uid of the requesting app.
* @param jobId Job id, specified at schedule-time.
* @return the JobStatus that matches the provided uId and jobId, or null if none found.
*/
- public JobStatus getJobByUidAndJobId(int uid, int jobId) {
- return mJobSet.get(uid, jobId);
+ @Nullable
+ public JobStatus getJobByUidAndJobId(int uid, @Nullable String namespace, int jobId) {
+ return mJobSet.get(uid, namespace, jobId);
}
/**
@@ -334,14 +484,39 @@ public final class JobStore {
mJobSet.forEachJobForSourceUid(sourceUid, functor);
}
+ private void maybeUpdateHighWaterMark() {
+ if (mScheduledJob30MinHighWaterMark < mCurrentJobSetSize) {
+ mScheduledJob30MinHighWaterMark = mCurrentJobSetSize;
+ }
+ }
+
/** Version of the db schema. */
private static final int JOBS_FILE_VERSION = 1;
+ /**
+ * For legacy reasons, this tag is used to encapsulate the entire job list.
+ */
+ private static final String XML_TAG_JOB_INFO = "job-info";
+ /**
+ * For legacy reasons, this tag represents a single {@link JobStatus} object.
+ */
+ private static final String XML_TAG_JOB = "job";
/** Tag corresponds to constraints this job needs. */
private static final String XML_TAG_PARAMS_CONSTRAINTS = "constraints";
/** Tag corresponds to execution parameters. */
private static final String XML_TAG_PERIODIC = "periodic";
private static final String XML_TAG_ONEOFF = "one-off";
private static final String XML_TAG_EXTRAS = "extras";
+ private static final String XML_TAG_JOB_WORK_ITEM = "job-work-item";
+
+ private void migrateJobFilesAsync() {
+ synchronized (mLock) {
+ mPendingJobWriteUids.put(ALL_UIDS, true);
+ }
+ synchronized (mWriteScheduleLock) {
+ mSplitFileMigrationNeeded = true;
+ maybeWriteStatusToDiskAsync();
+ }
+ }
/**
* Every time the state changes we write all the jobs in one swath, instead of trying to
@@ -435,15 +610,98 @@ public final class JobStore {
return values;
}
+ @VisibleForTesting
+ static int extractUidFromJobFileName(@NonNull File file) {
+ final String fileName = file.getName();
+ if (fileName.startsWith(JOB_FILE_SPLIT_PREFIX)) {
+ try {
+ final int subEnd = fileName.length() - 4; // -4 for ".xml"
+ final int uid = Integer.parseInt(
+ fileName.substring(JOB_FILE_SPLIT_PREFIX.length(), subEnd));
+ if (uid < 0) {
+ return INVALID_UID;
+ }
+ return uid;
+ } catch (Exception e) {
+ Slog.e(TAG, "Unexpected file name format", e);
+ }
+ }
+ return INVALID_UID;
+ }
+
/**
* Runnable that writes {@link #mJobSet} out to xml.
* NOTE: This Runnable locks on mLock
*/
private final Runnable mWriteRunnable = new Runnable() {
+ private final SparseArray<AtomicFile> mJobFiles = new SparseArray<>();
+ private final CopyConsumer mPersistedJobCopier = new CopyConsumer();
+
+ class CopyConsumer implements Consumer<JobStatus> {
+ private final SparseArray<List<JobStatus>> mJobStoreCopy = new SparseArray<>();
+ private boolean mCopyAllJobs;
+
+ private void prepare() {
+ mCopyAllJobs = !mUseSplitFiles || mPendingJobWriteUids.get(ALL_UIDS);
+ if (mUseSplitFiles) {
+ // Put the set of changed UIDs in the copy list so that we update each file,
+ // especially if we've dropped all jobs for that UID.
+ if (mPendingJobWriteUids.get(ALL_UIDS)) {
+ // ALL_UIDS is only used when we switch file splitting policy or for tests,
+ // so going through the file list here shouldn't be
+ // a large performance hit on user devices.
+
+ final File[] files;
+ try {
+ files = mJobFileDirectory.listFiles();
+ } catch (SecurityException e) {
+ Slog.wtf(TAG, "Not allowed to read job file directory", e);
+ return;
+ }
+ if (files == null) {
+ Slog.wtfStack(TAG, "Couldn't get job file list");
+ } else {
+ for (File file : files) {
+ final int uid = extractUidFromJobFileName(file);
+ if (uid != INVALID_UID) {
+ mJobStoreCopy.put(uid, new ArrayList<>());
+ }
+ }
+ }
+ } else {
+ for (int i = 0; i < mPendingJobWriteUids.size(); ++i) {
+ mJobStoreCopy.put(mPendingJobWriteUids.keyAt(i), new ArrayList<>());
+ }
+ }
+ } else {
+ // Single file mode.
+ // Put the catchall UID in the copy list so that we update the single file,
+ // especially if we've dropped all persisted jobs.
+ mJobStoreCopy.put(ALL_UIDS, new ArrayList<>());
+ }
+ }
+
+ @Override
+ public void accept(JobStatus jobStatus) {
+ final int uid = mUseSplitFiles ? jobStatus.getUid() : ALL_UIDS;
+ if (jobStatus.isPersisted() && (mCopyAllJobs || mPendingJobWriteUids.get(uid))) {
+ List<JobStatus> uidJobList = mJobStoreCopy.get(uid);
+ if (uidJobList == null) {
+ uidJobList = new ArrayList<>();
+ mJobStoreCopy.put(uid, uidJobList);
+ }
+ uidJobList.add(new JobStatus(jobStatus));
+ }
+ }
+
+ private void reset() {
+ mJobStoreCopy.clear();
+ }
+ }
+
@Override
public void run() {
final long startElapsed = sElapsedRealtimeClock.millis();
- final List<JobStatus> storeCopy = new ArrayList<JobStatus>();
// Intentionally allow new scheduling of a write operation *before* we clone
// the job set. If we reset it to false after cloning, there's a window in
// which no new write will be scheduled but mLock is not held, i.e. a new
@@ -460,48 +718,91 @@ public final class JobStore {
}
mWriteInProgress = true;
}
+ final boolean useSplitFiles;
synchronized (mLock) {
// Clone the jobs so we can release the lock before writing.
- mJobSet.forEachJob(null, (job) -> {
- if (job.isPersisted()) {
- storeCopy.add(new JobStatus(job));
+ useSplitFiles = mUseSplitFiles;
+ mPersistedJobCopier.prepare();
+ mJobSet.forEachJob(null, mPersistedJobCopier);
+ mPendingJobWriteUids.clear();
+ }
+ mPersistInfo.countAllJobsSaved = 0;
+ mPersistInfo.countSystemServerJobsSaved = 0;
+ mPersistInfo.countSystemSyncManagerJobsSaved = 0;
+ for (int i = mPersistedJobCopier.mJobStoreCopy.size() - 1; i >= 0; --i) {
+ AtomicFile file;
+ if (useSplitFiles) {
+ final int uid = mPersistedJobCopier.mJobStoreCopy.keyAt(i);
+ file = mJobFiles.get(uid);
+ if (file == null) {
+ file = createJobFile(JOB_FILE_SPLIT_PREFIX + uid);
+ mJobFiles.put(uid, file);
}
- });
+ } else {
+ file = mJobsFile;
+ }
+ if (DEBUG) {
+ Slog.d(TAG, "Writing for " + mPersistedJobCopier.mJobStoreCopy.keyAt(i)
+ + " to " + file.getBaseFile().getName() + ": "
+ + mPersistedJobCopier.mJobStoreCopy.valueAt(i).size() + " jobs");
+ }
+ writeJobsMapImpl(file, mPersistedJobCopier.mJobStoreCopy.valueAt(i));
}
- writeJobsMapImpl(storeCopy);
if (DEBUG) {
Slog.v(TAG, "Finished writing, took " + (sElapsedRealtimeClock.millis()
- startElapsed) + "ms");
}
+ mPersistedJobCopier.reset();
+ if (!useSplitFiles) {
+ mJobFiles.clear();
+ }
+ // Update the last modified time of the directory to aid in RTC time verification
+ // (see the JobStore constructor).
+ mJobFileDirectory.setLastModified(sSystemClock.millis());
synchronized (mWriteScheduleLock) {
+ if (mSplitFileMigrationNeeded) {
+ final File[] files = mJobFileDirectory.listFiles();
+ for (File file : files) {
+ if (useSplitFiles) {
+ if (!file.getName().startsWith(JOB_FILE_SPLIT_PREFIX)) {
+ // Delete the now unused file so there's no confusion in the future.
+ file.delete();
+ }
+ } else if (file.getName().startsWith(JOB_FILE_SPLIT_PREFIX)) {
+ // Delete the now unused file so there's no confusion in the future.
+ file.delete();
+ }
+ }
+ }
mWriteInProgress = false;
mWriteScheduleLock.notifyAll();
}
}
- private void writeJobsMapImpl(List<JobStatus> jobList) {
+ private void writeJobsMapImpl(@NonNull AtomicFile file, @NonNull List<JobStatus> jobList) {
int numJobs = 0;
int numSystemJobs = 0;
int numSyncJobs = 0;
mEventLogger.setStartTime(SystemClock.uptimeMillis());
- try (FileOutputStream fos = mJobsFile.startWrite()) {
+ try (FileOutputStream fos = file.startWrite()) {
TypedXmlSerializer out = Xml.resolveSerializer(fos);
out.startDocument(null, true);
out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
- out.startTag(null, "job-info");
+ out.startTag(null, XML_TAG_JOB_INFO);
out.attribute(null, "version", Integer.toString(JOBS_FILE_VERSION));
for (int i=0; i<jobList.size(); i++) {
JobStatus jobStatus = jobList.get(i);
if (DEBUG) {
Slog.d(TAG, "Saving job " + jobStatus.getJobId());
}
- out.startTag(null, "job");
+ out.startTag(null, XML_TAG_JOB);
addAttributesToJobTag(out, jobStatus);
writeConstraintsToXml(out, jobStatus);
writeExecutionCriteriaToXml(out, jobStatus);
writeBundleToXml(jobStatus.getJob().getExtras(), out);
- out.endTag(null, "job");
+ writeJobWorkItemsToXml(out, jobStatus);
+ out.endTag(null, XML_TAG_JOB);
numJobs++;
if (jobStatus.getUid() == Process.SYSTEM_UID) {
@@ -511,10 +812,10 @@ public final class JobStore {
}
}
}
- out.endTag(null, "job-info");
+ out.endTag(null, XML_TAG_JOB_INFO);
out.endDocument();
- mJobsFile.finishWrite(fos);
+ file.finishWrite(fos);
} catch (IOException e) {
if (DEBUG) {
Slog.v(TAG, "Error writing out job data.", e);
@@ -524,9 +825,9 @@ public final class JobStore {
Slog.d(TAG, "Error persisting bundle.", e);
}
} finally {
- mPersistInfo.countAllJobsSaved = numJobs;
- mPersistInfo.countSystemServerJobsSaved = numSystemJobs;
- mPersistInfo.countSystemSyncManagerJobsSaved = numSyncJobs;
+ mPersistInfo.countAllJobsSaved += numJobs;
+ mPersistInfo.countSystemServerJobsSaved += numSystemJobs;
+ mPersistInfo.countSystemSyncManagerJobsSaved += numSyncJobs;
}
}
@@ -534,7 +835,7 @@ public final class JobStore {
* Write out a tag with data comprising the required fields and bias of this job and
* its client.
*/
- private void addAttributesToJobTag(XmlSerializer out, JobStatus jobStatus)
+ private void addAttributesToJobTag(TypedXmlSerializer out, JobStatus jobStatus)
throws IOException {
out.attribute(null, "jobid", Integer.toString(jobStatus.getJobId()));
out.attribute(null, "package", jobStatus.getServiceComponent().getPackageName());
@@ -542,6 +843,9 @@ public final class JobStore {
if (jobStatus.getSourcePackageName() != null) {
out.attribute(null, "sourcePackageName", jobStatus.getSourcePackageName());
}
+ if (jobStatus.getNamespace() != null) {
+ out.attribute(null, "namespace", jobStatus.getNamespace());
+ }
if (jobStatus.getSourceTag() != null) {
out.attribute(null, "sourceTag", jobStatus.getSourceTag());
}
@@ -558,6 +862,9 @@ public final class JobStore {
String.valueOf(jobStatus.getLastSuccessfulRunTime()));
out.attribute(null, "lastFailedRunTime",
String.valueOf(jobStatus.getLastFailedRunTime()));
+
+ out.attributeLong(null, "cumulativeExecutionTime",
+ jobStatus.getCumulativeExecutionTimeMs());
}
private void writeBundleToXml(PersistableBundle extras, XmlSerializer out)
@@ -591,8 +898,10 @@ public final class JobStore {
* because currently store is not including everything (like, UIDs, bandwidth,
* signal strength etc. are lost).
*/
- private void writeConstraintsToXml(XmlSerializer out, JobStatus jobStatus) throws IOException {
+ private void writeConstraintsToXml(TypedXmlSerializer out, JobStatus jobStatus)
+ throws IOException {
out.startTag(null, XML_TAG_PARAMS_CONSTRAINTS);
+ final JobInfo job = jobStatus.getJob();
if (jobStatus.hasConnectivityConstraint()) {
final NetworkRequest network = jobStatus.getJob().getRequiredNetwork();
out.attribute(null, "net-capabilities-csv", intArrayToString(
@@ -601,19 +910,40 @@ public final class JobStore {
network.getForbiddenCapabilities()));
out.attribute(null, "net-transport-types-csv", intArrayToString(
network.getTransportTypes()));
+ if (job.getEstimatedNetworkDownloadBytes() != JobInfo.NETWORK_BYTES_UNKNOWN) {
+ out.attributeLong(null, "estimated-download-bytes",
+ job.getEstimatedNetworkDownloadBytes());
+ }
+ if (job.getEstimatedNetworkUploadBytes() != JobInfo.NETWORK_BYTES_UNKNOWN) {
+ out.attributeLong(null, "estimated-upload-bytes",
+ job.getEstimatedNetworkUploadBytes());
+ }
+ if (job.getMinimumNetworkChunkBytes() != JobInfo.NETWORK_BYTES_UNKNOWN) {
+ out.attributeLong(null, "minimum-network-chunk-bytes",
+ job.getMinimumNetworkChunkBytes());
+ }
}
- if (jobStatus.hasIdleConstraint()) {
+ if (job.isRequireDeviceIdle()) {
out.attribute(null, "idle", Boolean.toString(true));
}
- if (jobStatus.hasChargingConstraint()) {
+ if (job.isRequireCharging()) {
out.attribute(null, "charging", Boolean.toString(true));
}
- if (jobStatus.hasBatteryNotLowConstraint()) {
+ if (job.isRequireBatteryNotLow()) {
out.attribute(null, "battery-not-low", Boolean.toString(true));
}
- if (jobStatus.hasStorageNotLowConstraint()) {
+ if (job.isRequireStorageNotLow()) {
out.attribute(null, "storage-not-low", Boolean.toString(true));
}
+ if (job.isPreferBatteryNotLow()) {
+ out.attributeBoolean(null, "prefer-battery-not-low", true);
+ }
+ if (job.isPreferCharging()) {
+ out.attributeBoolean(null, "prefer-charging", true);
+ }
+ if (job.isPreferDeviceIdle()) {
+ out.attributeBoolean(null, "prefer-idle", true);
+ }
out.endTag(null, XML_TAG_PARAMS_CONSTRAINTS);
}
@@ -666,6 +996,53 @@ public final class JobStore {
out.endTag(null, XML_TAG_ONEOFF);
}
}
+
+ private void writeJobWorkItemsToXml(@NonNull TypedXmlSerializer out,
+ @NonNull JobStatus jobStatus) throws IOException, XmlPullParserException {
+ // Write executing first since they're technically at the front of the queue.
+ writeJobWorkItemListToXml(out, jobStatus.executingWork);
+ writeJobWorkItemListToXml(out, jobStatus.pendingWork);
+ }
+
+ private void writeJobWorkItemListToXml(@NonNull TypedXmlSerializer out,
+ @Nullable List<JobWorkItem> jobWorkItems)
+ throws IOException, XmlPullParserException {
+ if (jobWorkItems == null) {
+ return;
+ }
+ // Write the items in list order to maintain the enqueue order.
+ final int size = jobWorkItems.size();
+ for (int i = 0; i < size; ++i) {
+ final JobWorkItem item = jobWorkItems.get(i);
+ if (item.getGrants() != null) {
+ // We currently don't allow persisting jobs when grants are involved.
+ // TODO(256618122): allow persisting JobWorkItems with grant flags
+ continue;
+ }
+ if (item.getIntent() != null) {
+ // Intent.saveToXml() doesn't persist everything, so we shouldn't attempt to
+ // persist these JobWorkItems at all.
+ Slog.wtf(TAG, "Encountered JobWorkItem with Intent in persisting list");
+ continue;
+ }
+ out.startTag(null, XML_TAG_JOB_WORK_ITEM);
+ out.attributeInt(null, "delivery-count", item.getDeliveryCount());
+ if (item.getEstimatedNetworkDownloadBytes() != JobInfo.NETWORK_BYTES_UNKNOWN) {
+ out.attributeLong(null, "estimated-download-bytes",
+ item.getEstimatedNetworkDownloadBytes());
+ }
+ if (item.getEstimatedNetworkUploadBytes() != JobInfo.NETWORK_BYTES_UNKNOWN) {
+ out.attributeLong(null, "estimated-upload-bytes",
+ item.getEstimatedNetworkUploadBytes());
+ }
+ if (item.getMinimumNetworkChunkBytes() != JobInfo.NETWORK_BYTES_UNKNOWN) {
+ out.attributeLong(null, "minimum-network-chunk-bytes",
+ item.getMinimumNetworkChunkBytes());
+ }
+ writeBundleToXml(item.getExtras(), out);
+ out.endTag(null, XML_TAG_JOB_WORK_ITEM);
+ }
+ }
};
/**
@@ -699,54 +1076,93 @@ public final class JobStore {
private final class ReadJobMapFromDiskRunnable implements Runnable {
private final JobSet jobSet;
private final boolean rtcGood;
+ private final CountDownLatch mCompletionLatch;
/**
* @param jobSet Reference to the (empty) set of JobStatus objects that back the JobStore,
* so that after disk read we can populate it directly.
*/
ReadJobMapFromDiskRunnable(JobSet jobSet, boolean rtcIsGood) {
+ this(jobSet, rtcIsGood, null);
+ }
+
+ ReadJobMapFromDiskRunnable(JobSet jobSet, boolean rtcIsGood,
+ @Nullable CountDownLatch completionLatch) {
this.jobSet = jobSet;
this.rtcGood = rtcIsGood;
+ this.mCompletionLatch = completionLatch;
}
@Override
public void run() {
+ if (!mJobFileDirectory.isDirectory()) {
+ Slog.wtf(TAG, "jobs directory isn't a directory O.O");
+ mJobFileDirectory.mkdirs();
+ return;
+ }
+
int numJobs = 0;
int numSystemJobs = 0;
int numSyncJobs = 0;
List<JobStatus> jobs;
- try (FileInputStream fis = mJobsFile.openRead()) {
- synchronized (mLock) {
- jobs = readJobMapImpl(fis, rtcGood);
- if (jobs != null) {
- long now = sElapsedRealtimeClock.millis();
- for (int i=0; i<jobs.size(); i++) {
- JobStatus js = jobs.get(i);
- js.prepareLocked();
- js.enqueueTime = now;
- this.jobSet.add(js);
-
- numJobs++;
- if (js.getUid() == Process.SYSTEM_UID) {
- numSystemJobs++;
- if (isSyncJob(js)) {
- numSyncJobs++;
+ final File[] files;
+ try {
+ files = mJobFileDirectory.listFiles();
+ } catch (SecurityException e) {
+ Slog.wtf(TAG, "Not allowed to read job file directory", e);
+ return;
+ }
+ if (files == null) {
+ Slog.wtfStack(TAG, "Couldn't get job file list");
+ return;
+ }
+ boolean needFileMigration = false;
+ long nowElapsed = sElapsedRealtimeClock.millis();
+ synchronized (mLock) {
+ for (File file : files) {
+ final AtomicFile aFile = createJobFile(file);
+ try (FileInputStream fis = aFile.openRead()) {
+ jobs = readJobMapImpl(fis, rtcGood, nowElapsed);
+ if (jobs != null) {
+ for (int i = 0; i < jobs.size(); i++) {
+ JobStatus js = jobs.get(i);
+ js.prepareLocked();
+ js.enqueueTime = nowElapsed;
+ this.jobSet.add(js);
+
+ numJobs++;
+ if (js.getUid() == Process.SYSTEM_UID) {
+ numSystemJobs++;
+ if (isSyncJob(js)) {
+ numSyncJobs++;
+ }
}
}
}
+ } catch (FileNotFoundException e) {
+ // mJobFileDirectory.listFiles() gave us this file...why can't we find it???
+ Slog.e(TAG, "Could not find jobs file: " + file.getName());
+ } catch (XmlPullParserException | IOException e) {
+ Slog.wtf(TAG, "Error in " + file.getName(), e);
+ } catch (Exception e) {
+ // Crashing at this point would result in a boot loop, so live with a
+ // generic Exception for system stability's sake.
+ Slog.wtf(TAG, "Unexpected exception", e);
+ }
+ if (mUseSplitFiles) {
+ if (!file.getName().startsWith(JOB_FILE_SPLIT_PREFIX)) {
+ // We're supposed to be using the split file architecture,
+ // but we still have
+ // the old job file around. Fully migrate and remove the old file.
+ needFileMigration = true;
+ }
+ } else if (file.getName().startsWith(JOB_FILE_SPLIT_PREFIX)) {
+ // We're supposed to be using the legacy single file architecture,
+ // but we still have some job split files around. Fully migrate
+ // and remove the split files.
+ needFileMigration = true;
}
}
- } catch (FileNotFoundException e) {
- if (DEBUG) {
- Slog.d(TAG, "Could not find jobs file, probably there was nothing to load.");
- }
- } catch (XmlPullParserException | IOException e) {
- Slog.wtf(TAG, "Error jobstore xml.", e);
- } catch (Exception e) {
- // Crashing at this point would result in a boot loop, so live with a general
- // Exception for system stability's sake.
- Slog.wtf(TAG, "Unexpected exception", e);
- } finally {
if (mPersistInfo.countAllJobsLoaded < 0) { // Only set them once.
mPersistInfo.countAllJobsLoaded = numJobs;
mPersistInfo.countSystemServerJobsLoaded = numSystemJobs;
@@ -754,11 +1170,23 @@ public final class JobStore {
}
}
Slog.i(TAG, "Read " + numJobs + " jobs");
+ if (needFileMigration) {
+ migrateJobFilesAsync();
+ }
+
+ // Log the count immediately after loading from boot.
+ mCurrentJobSetSize = numJobs;
+ mScheduledJob30MinHighWaterMark = mCurrentJobSetSize;
+ mScheduledJobHighWaterMarkLoggingRunnable.run();
+
+ if (mCompletionLatch != null) {
+ mCompletionLatch.countDown();
+ }
}
- private List<JobStatus> readJobMapImpl(InputStream fis, boolean rtcIsGood)
+ private List<JobStatus> readJobMapImpl(InputStream fis, boolean rtcIsGood, long nowElapsed)
throws XmlPullParserException, IOException {
- XmlPullParser parser = Xml.resolvePullParser(fis);
+ TypedXmlPullParser parser = Xml.resolvePullParser(fis);
int eventType = parser.getEventType();
while (eventType != XmlPullParser.START_TAG &&
@@ -774,28 +1202,24 @@ public final class JobStore {
}
String tagName = parser.getName();
- if ("job-info".equals(tagName)) {
+ if (XML_TAG_JOB_INFO.equals(tagName)) {
final List<JobStatus> jobs = new ArrayList<JobStatus>();
- final int version;
+ final int version = parser.getAttributeInt(null, "version");
// Read in version info.
- try {
- version = Integer.parseInt(parser.getAttributeValue(null, "version"));
- if (version > JOBS_FILE_VERSION || version < 0) {
- Slog.d(TAG, "Invalid version number, aborting jobs file read.");
- return null;
- }
- } catch (NumberFormatException e) {
- Slog.e(TAG, "Invalid version number, aborting jobs file read.");
+ if (version > JOBS_FILE_VERSION || version < 0) {
+ Slog.d(TAG, "Invalid version number, aborting jobs file read.");
return null;
}
+
eventType = parser.next();
do {
// Read each <job/>
if (eventType == XmlPullParser.START_TAG) {
tagName = parser.getName();
// Start reading job.
- if ("job".equals(tagName)) {
- JobStatus persistedJob = restoreJobFromXml(rtcIsGood, parser, version);
+ if (XML_TAG_JOB.equals(tagName)) {
+ JobStatus persistedJob =
+ restoreJobFromXml(rtcIsGood, parser, version, nowElapsed);
if (persistedJob != null) {
if (DEBUG) {
Slog.d(TAG, "Read out " + persistedJob);
@@ -818,12 +1242,13 @@ public final class JobStore {
* will take the parser into the body of the job tag.
* @return Newly instantiated job holding all the information we just read out of the xml tag.
*/
- private JobStatus restoreJobFromXml(boolean rtcIsGood, XmlPullParser parser,
- int schemaVersion) throws XmlPullParserException, IOException {
+ private JobStatus restoreJobFromXml(boolean rtcIsGood, TypedXmlPullParser parser,
+ int schemaVersion, long nowElapsed) throws XmlPullParserException, IOException {
JobInfo.Builder jobBuilder;
int uid, sourceUserId;
long lastSuccessfulRunTime;
long lastFailedRunTime;
+ long cumulativeExecutionTime;
int internalFlags = 0;
// Read out job identifier attributes and bias.
@@ -864,12 +1289,16 @@ public final class JobStore {
val = parser.getAttributeValue(null, "lastFailedRunTime");
lastFailedRunTime = val == null ? 0 : Long.parseLong(val);
+
+ cumulativeExecutionTime =
+ parser.getAttributeLong(null, "cumulativeExecutionTime", 0);
} catch (NumberFormatException e) {
Slog.e(TAG, "Error parsing job's required fields, skipping");
return null;
}
String sourcePackageName = parser.getAttributeValue(null, "sourcePackageName");
+ final String namespace = parser.getAttributeValue(null, "namespace");
final String sourceTag = parser.getAttributeValue(null, "sourceTag");
int eventType;
@@ -910,18 +1339,9 @@ public final class JobStore {
}
// Tuple of (earliest runtime, latest runtime) in UTC.
- final Pair<Long, Long> rtcRuntimes;
- try {
- rtcRuntimes = buildRtcExecutionTimesFromXml(parser);
- } catch (NumberFormatException e) {
- if (DEBUG) {
- Slog.d(TAG, "Error parsing execution time parameters, skipping.");
- }
- return null;
- }
+ final Pair<Long, Long> rtcRuntimes = buildRtcExecutionTimesFromXml(parser);
- final long elapsedNow = sElapsedRealtimeClock.millis();
- Pair<Long, Long> elapsedRuntimes = convertRtcBoundsToElapsed(rtcRuntimes, elapsedNow);
+ Pair<Long, Long> elapsedRuntimes = convertRtcBoundsToElapsed(rtcRuntimes, nowElapsed);
if (XML_TAG_PERIODIC.equals(parser.getName())) {
try {
@@ -934,8 +1354,8 @@ public final class JobStore {
// from now. This is the latest the periodic could be pushed out. This could
// happen if the periodic ran early (at flex time before period), and then the
// device rebooted.
- if (elapsedRuntimes.second > elapsedNow + periodMillis + flexMillis) {
- final long clampedLateRuntimeElapsed = elapsedNow + flexMillis
+ if (elapsedRuntimes.second > nowElapsed + periodMillis + flexMillis) {
+ final long clampedLateRuntimeElapsed = nowElapsed + flexMillis
+ periodMillis;
final long clampedEarlyRuntimeElapsed = clampedLateRuntimeElapsed
- flexMillis;
@@ -960,11 +1380,11 @@ public final class JobStore {
} else if (XML_TAG_ONEOFF.equals(parser.getName())) {
try {
if (elapsedRuntimes.first != JobStatus.NO_EARLIEST_RUNTIME) {
- jobBuilder.setMinimumLatency(elapsedRuntimes.first - elapsedNow);
+ jobBuilder.setMinimumLatency(elapsedRuntimes.first - nowElapsed);
}
if (elapsedRuntimes.second != JobStatus.NO_LATEST_RUNTIME) {
jobBuilder.setOverrideDeadline(
- elapsedRuntimes.second - elapsedNow);
+ elapsedRuntimes.second - nowElapsed);
}
} catch (NumberFormatException e) {
Slog.d(TAG, "Error reading job execution criteria, skipping.");
@@ -1001,7 +1421,13 @@ public final class JobStore {
Slog.e(TAG, "Persisted extras contained invalid data", e);
return null;
}
- parser.nextTag(); // Consume </extras>
+ eventType = parser.nextTag(); // Consume </extras>
+
+ List<JobWorkItem> jobWorkItems = null;
+ if (eventType == XmlPullParser.START_TAG
+ && XML_TAG_JOB_WORK_ITEM.equals(parser.getName())) {
+ jobWorkItems = readJobWorkItemsFromXml(parser);
+ }
final JobInfo builtJob;
try {
@@ -1013,7 +1439,8 @@ public final class JobStore {
// have a deadline. If a job is rescheduled (via jobFinished(true) or onStopJob()'s
// return value), the deadline is dropped. Periodic jobs require all constraints
// to be met, so there's no issue with their deadlines.
- builtJob = jobBuilder.build(false);
+ // The same logic applies for other target SDK-based validation checks.
+ builtJob = jobBuilder.build(false, false);
} catch (Exception e) {
Slog.w(TAG, "Unable to build job from XML, ignoring: " + jobBuilder.summarize(), e);
return null;
@@ -1032,19 +1459,25 @@ public final class JobStore {
// And now we're done
final int appBucket = JobSchedulerService.standbyBucketForPackage(sourcePackageName,
- sourceUserId, elapsedNow);
+ sourceUserId, nowElapsed);
JobStatus js = new JobStatus(
builtJob, uid, sourcePackageName, sourceUserId,
- appBucket, sourceTag,
+ appBucket, namespace, sourceTag,
elapsedRuntimes.first, elapsedRuntimes.second,
- lastSuccessfulRunTime, lastFailedRunTime,
+ lastSuccessfulRunTime, lastFailedRunTime, cumulativeExecutionTime,
(rtcIsGood) ? null : rtcRuntimes, internalFlags, /* dynamicConstraints */ 0);
+ if (jobWorkItems != null) {
+ for (int i = 0; i < jobWorkItems.size(); ++i) {
+ js.enqueueWorkLocked(jobWorkItems.get(i));
+ }
+ }
return js;
}
- private JobInfo.Builder buildBuilderFromXml(XmlPullParser parser) throws NumberFormatException {
+ private JobInfo.Builder buildBuilderFromXml(TypedXmlPullParser parser)
+ throws XmlPullParserException {
// Pull out required fields from <job> attributes.
- int jobId = Integer.parseInt(parser.getAttributeValue(null, "jobid"));
+ int jobId = parser.getAttributeInt(null, "jobid");
String packageName = parser.getAttributeValue(null, "package");
String className = parser.getAttributeValue(null, "class");
ComponentName cname = new ComponentName(packageName, className);
@@ -1063,7 +1496,7 @@ public final class JobStore {
* reading, but in order to avoid issues with OEM-defined flags, the accepted capabilities
* are limited to that(maxNetCapabilityInR & maxTransportInR) defined in R.
*/
- private void buildConstraintsFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser)
+ private void buildConstraintsFromXml(JobInfo.Builder jobBuilder, TypedXmlPullParser parser)
throws XmlPullParserException, IOException {
String val;
String netCapabilitiesLong = null;
@@ -1084,6 +1517,8 @@ public final class JobStore {
}
if ((netCapabilitiesIntArray != null) && (netTransportTypesIntArray != null)) {
+ // S+ format. No capability or transport validation since the values should be in
+ // line with what's defined in the Connectivity mainline module.
final NetworkRequest.Builder builder = new NetworkRequest.Builder()
.clearCapabilities();
@@ -1098,8 +1533,19 @@ public final class JobStore {
for (int transport : stringToIntArray(netTransportTypesIntArray)) {
builder.addTransportType(transport);
}
- jobBuilder.setRequiredNetwork(builder.build());
+ jobBuilder
+ .setRequiredNetwork(builder.build())
+ .setEstimatedNetworkBytes(
+ parser.getAttributeLong(null,
+ "estimated-download-bytes", JobInfo.NETWORK_BYTES_UNKNOWN),
+ parser.getAttributeLong(null,
+ "estimated-upload-bytes", JobInfo.NETWORK_BYTES_UNKNOWN))
+ .setMinimumNetworkChunkBytes(
+ parser.getAttributeLong(null,
+ "minimum-network-chunk-bytes",
+ JobInfo.NETWORK_BYTES_UNKNOWN));
} else if (netCapabilitiesLong != null && netTransportTypesLong != null) {
+ // Format used on R- builds. Drop any unexpected capabilities and transports.
final NetworkRequest.Builder builder = new NetworkRequest.Builder()
.clearCapabilities();
final int maxNetCapabilityInR = NET_CAPABILITY_TEMPORARILY_NOT_METERED;
@@ -1125,6 +1571,8 @@ public final class JobStore {
}
}
jobBuilder.setRequiredNetwork(builder.build());
+ // Estimated bytes weren't persisted on R- builds, so no point querying for the
+ // attributes here.
} else {
// Read legacy values
val = parser.getAttributeValue(null, "connectivity");
@@ -1161,6 +1609,13 @@ public final class JobStore {
if (val != null) {
jobBuilder.setRequiresStorageNotLow(true);
}
+
+ jobBuilder.setPrefersBatteryNotLow(
+ parser.getAttributeBoolean(null, "prefer-battery-not-low", false));
+ jobBuilder.setPrefersCharging(
+ parser.getAttributeBoolean(null, "prefer-charging", false));
+ jobBuilder.setPrefersDeviceIdle(
+ parser.getAttributeBoolean(null, "prefer-idle", false));
}
/**
@@ -1186,22 +1641,73 @@ public final class JobStore {
* @return A Pair of timestamps in UTC wall-clock time. The first is the earliest
* time at which the job is to become runnable, and the second is the deadline at
* which it becomes overdue to execute.
- * @throws NumberFormatException
*/
- private Pair<Long, Long> buildRtcExecutionTimesFromXml(XmlPullParser parser)
- throws NumberFormatException {
- String val;
+ private Pair<Long, Long> buildRtcExecutionTimesFromXml(TypedXmlPullParser parser) {
// Pull out execution time data.
- val = parser.getAttributeValue(null, "delay");
- final long earliestRunTimeRtc = (val != null)
- ? Long.parseLong(val)
- : JobStatus.NO_EARLIEST_RUNTIME;
- val = parser.getAttributeValue(null, "deadline");
- final long latestRunTimeRtc = (val != null)
- ? Long.parseLong(val)
- : JobStatus.NO_LATEST_RUNTIME;
+ final long earliestRunTimeRtc =
+ parser.getAttributeLong(null, "delay", JobStatus.NO_EARLIEST_RUNTIME);
+ final long latestRunTimeRtc =
+ parser.getAttributeLong(null, "deadline", JobStatus.NO_LATEST_RUNTIME);
return Pair.create(earliestRunTimeRtc, latestRunTimeRtc);
}
+
+ @NonNull
+ private List<JobWorkItem> readJobWorkItemsFromXml(TypedXmlPullParser parser)
+ throws IOException, XmlPullParserException {
+ List<JobWorkItem> jobWorkItems = new ArrayList<>();
+
+ for (int eventType = parser.getEventType(); eventType != XmlPullParser.END_DOCUMENT;
+ eventType = parser.next()) {
+ final String tagName = parser.getName();
+ if (!XML_TAG_JOB_WORK_ITEM.equals(tagName)) {
+ // We're no longer operating with work items.
+ break;
+ }
+ try {
+ JobWorkItem jwi = readJobWorkItemFromXml(parser);
+ if (jwi != null) {
+ jobWorkItems.add(jwi);
+ }
+ } catch (Exception e) {
+ // If there's an issue with one JobWorkItem, drop only the one item and not the
+ // whole job.
+ Slog.e(TAG, "Problem with persisted JobWorkItem", e);
+ }
+ }
+
+ return jobWorkItems;
+ }
+
+ @Nullable
+ private JobWorkItem readJobWorkItemFromXml(TypedXmlPullParser parser)
+ throws IOException, XmlPullParserException {
+ JobWorkItem.Builder jwiBuilder = new JobWorkItem.Builder();
+
+ jwiBuilder
+ .setDeliveryCount(parser.getAttributeInt(null, "delivery-count"))
+ .setEstimatedNetworkBytes(
+ parser.getAttributeLong(null,
+ "estimated-download-bytes", JobInfo.NETWORK_BYTES_UNKNOWN),
+ parser.getAttributeLong(null,
+ "estimated-upload-bytes", JobInfo.NETWORK_BYTES_UNKNOWN))
+ .setMinimumNetworkChunkBytes(parser.getAttributeLong(null,
+ "minimum-network-chunk-bytes", JobInfo.NETWORK_BYTES_UNKNOWN));
+ parser.next();
+ try {
+ final PersistableBundle extras = PersistableBundle.restoreFromXml(parser);
+ jwiBuilder.setExtras(extras);
+ } catch (IllegalArgumentException e) {
+ Slog.e(TAG, "Persisted extras contained invalid data", e);
+ return null;
+ }
+
+ try {
+ return jwiBuilder.build();
+ } catch (Exception e) {
+ Slog.e(TAG, "Invalid JobWorkItem", e);
+ return null;
+ }
+ }
}
/** Set of all tracked jobs. */
@@ -1218,29 +1724,33 @@ public final class JobStore {
mJobsPerSourceUid = new SparseArray<>();
}
- public List<JobStatus> getJobsByUid(int uid) {
- ArrayList<JobStatus> matchingJobs = new ArrayList<JobStatus>();
+ public ArraySet<JobStatus> getJobsByUid(int uid) {
+ ArraySet<JobStatus> matchingJobs = new ArraySet<>();
+ getJobsByUid(uid, matchingJobs);
+ return matchingJobs;
+ }
+
+ public void getJobsByUid(int uid, Set<JobStatus> insertInto) {
ArraySet<JobStatus> jobs = mJobs.get(uid);
if (jobs != null) {
- matchingJobs.addAll(jobs);
+ insertInto.addAll(jobs);
}
- return matchingJobs;
}
- // By user, not by uid, so we need to traverse by key and check
- public List<JobStatus> getJobsByUser(int userId) {
- final ArrayList<JobStatus> result = new ArrayList<JobStatus>();
- for (int i = mJobsPerSourceUid.size() - 1; i >= 0; i--) {
- if (UserHandle.getUserId(mJobsPerSourceUid.keyAt(i)) == userId) {
- final ArraySet<JobStatus> jobs = mJobsPerSourceUid.valueAt(i);
- if (jobs != null) {
- result.addAll(jobs);
- }
- }
- }
+ @NonNull
+ public ArraySet<JobStatus> getJobsBySourceUid(int sourceUid) {
+ final ArraySet<JobStatus> result = new ArraySet<>();
+ getJobsBySourceUid(sourceUid, result);
return result;
}
+ public void getJobsBySourceUid(int sourceUid, Set<JobStatus> insertInto) {
+ final ArraySet<JobStatus> jobs = mJobsPerSourceUid.get(sourceUid);
+ if (jobs != null) {
+ insertInto.addAll(jobs);
+ }
+ }
+
public boolean add(JobStatus job) {
final int uid = job.getUid();
final int sourceUid = job.getSourceUid();
@@ -1322,12 +1832,12 @@ public final class JobStore {
return jobs != null && jobs.contains(job);
}
- public JobStatus get(int uid, int jobId) {
+ public JobStatus get(int uid, @Nullable String namespace, int jobId) {
ArraySet<JobStatus> jobs = mJobs.get(uid);
if (jobs != null) {
for (int i = jobs.size() - 1; i >= 0; i--) {
JobStatus job = jobs.valueAt(i);
- if (job.getJobId() == jobId) {
+ if (job.getJobId() == jobId && Objects.equals(namespace, job.getNamespace())) {
return job;
}
}
@@ -1382,7 +1892,7 @@ public final class JobStore {
}
public void forEachJob(@Nullable Predicate<JobStatus> filterPredicate,
- Consumer<JobStatus> functor) {
+ @NonNull Consumer<JobStatus> functor) {
for (int uidIndex = mJobs.size() - 1; uidIndex >= 0; uidIndex--) {
ArraySet<JobStatus> jobs = mJobs.valueAt(uidIndex);
if (jobs != 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 0eacfd68e385..4f4096f69ad5 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/PendingJobQueue.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/PendingJobQueue.java
@@ -28,6 +28,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
+import java.util.Objects;
import java.util.PriorityQueue;
/**
@@ -218,6 +219,8 @@ class PendingJobQueue {
ajq.clear();
mAppJobQueuePool.release(ajq);
} else if (prevTimestamp != ajq.peekNextTimestamp()) {
+ // Removing the job changed the "next timestamp" in the queue, so we need to reinsert
+ // it to fix the ordering.
mOrderedQueues.remove(ajq);
mOrderedQueues.offer(ajq);
}
@@ -272,6 +275,13 @@ class PendingJobQueue {
return Integer.compare(job2.overrideState, job1.overrideState);
}
+ final boolean job1UI = job1.getJob().isUserInitiated();
+ final boolean job2UI = job2.getJob().isUserInitiated();
+ if (job1UI != job2UI) {
+ // Attempt to run user-initiated jobs ahead of all other jobs.
+ return job1UI ? -1 : 1;
+ }
+
final boolean job1EJ = job1.isRequestedExpeditedJob();
final boolean job2EJ = job2.isRequestedExpeditedJob();
if (job1EJ != job2EJ) {
@@ -280,12 +290,14 @@ class PendingJobQueue {
return job1EJ ? -1 : 1;
}
- final int job1Priority = job1.getEffectivePriority();
- final int job2Priority = job2.getEffectivePriority();
- if (job1Priority != job2Priority) {
- // Use the priority set by an app for intra-app job ordering. Higher
- // priority should be before lower priority.
- return Integer.compare(job2Priority, job1Priority);
+ if (Objects.equals(job1.getNamespace(), job2.getNamespace())) {
+ final int job1Priority = job1.getEffectivePriority();
+ final int job2Priority = job2.getEffectivePriority();
+ if (job1Priority != job2Priority) {
+ // Use the priority set by an app for intra-app job ordering. Higher
+ // priority should be before lower priority.
+ return Integer.compare(job2Priority, job1Priority);
+ }
}
if (job1.lastEvaluatedBias != job2.lastEvaluatedBias) {
diff --git a/apex/jobscheduler/service/java/com/android/server/job/StateChangedListener.java b/apex/jobscheduler/service/java/com/android/server/job/StateChangedListener.java
index 1068bda975fb..50064bde0bbe 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/StateChangedListener.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/StateChangedListener.java
@@ -18,9 +18,11 @@ package com.android.server.job;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.net.Network;
import android.util.ArraySet;
import com.android.server.job.controllers.JobStatus;
+import com.android.server.job.restrictions.JobRestriction;
import java.util.List;
@@ -37,6 +39,16 @@ public interface StateChangedListener {
void onControllerStateChanged(@Nullable ArraySet<JobStatus> changedJobs);
/**
+ * Called by a {@link com.android.server.job.restrictions.JobRestriction} to notify the
+ * JobScheduler that it should check on the state of all jobs.
+ *
+ * @param stopOvertimeJobs Whether to stop any jobs that have run for more than their minimum
+ * execution guarantee and are restricted by the changed restriction
+ */
+ void onRestrictionStateChanged(@NonNull JobRestriction restriction,
+ boolean stopOvertimeJobs);
+
+ /**
* Called by the controller to notify the JobManager that regardless of the state of the task,
* it must be run immediately.
* @param jobStatus The state of the task which is to be run immediately. <strong>null
@@ -46,6 +58,8 @@ public interface StateChangedListener {
public void onDeviceIdleStateChanged(boolean deviceIdle);
+ void onNetworkChanged(JobStatus jobStatus, Network newNetwork);
+
/**
* Called when these jobs are added or removed from the
* {@link android.app.usage.UsageStatsManager#STANDBY_BUCKET_RESTRICTED} bucket.
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 65d712102e94..25b3421a55f0 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
@@ -19,6 +19,7 @@ package com.android.server.job.controllers;
import static com.android.server.job.JobSchedulerService.NEVER_INDEX;
import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
+import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
import android.os.SystemClock;
import android.os.UserHandle;
@@ -81,8 +82,7 @@ public final class BackgroundJobsController extends StateController {
}
@Override
- public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob,
- boolean forUpdate) {
+ public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob) {
}
@Override
@@ -206,8 +206,32 @@ public final class BackgroundJobsController extends StateController {
final int uid = jobStatus.getSourceUid();
final String packageName = jobStatus.getSourcePackageName();
- final boolean canRun = !mAppStateTracker.areJobsRestricted(uid, packageName,
- jobStatus.canRunInBatterySaver());
+ final boolean isUserBgRestricted =
+ !mActivityManagerInternal.isBgAutoRestrictedBucketFeatureFlagEnabled()
+ && !mAppStateTracker.isRunAnyInBackgroundAppOpsAllowed(uid, packageName);
+ // If a job started with the foreground flag, it'll cause the UID to stay active
+ // and thus cause areJobsRestricted() to always return false, so if
+ // areJobsRestricted() returns false and the app is BG restricted and not TOP,
+ // we need to stop any jobs that started with the foreground flag so they don't
+ // keep the app in an elevated proc state. If we were to get in this situation,
+ // then the user restricted the app after the job started, so it's best to stop
+ // the job as soon as possible, especially since the job would be visible to the
+ // user (with a notification and in Task Manager).
+ // There are several other reasons that uidActive can be true for an app even if its
+ // proc state is less important than BFGS.
+ // JobScheduler has historically (at least up through UDC) allowed the app's jobs to run
+ // when its UID was active, even if it's background restricted. This has been fine because
+ // JobScheduler stops the job as soon as the UID becomes inactive and the jobs themselves
+ // will not keep the UID active. The logic here is to ensure that special jobs
+ // (e.g. user-initiated jobs) themselves do not keep the UID active when the app is
+ // background restricted.
+ final boolean shouldStopImmediately = jobStatus.startedWithForegroundFlag
+ && isUserBgRestricted
+ && mService.getUidProcState(uid)
+ > ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
+ final boolean canRun = !shouldStopImmediately
+ && !mAppStateTracker.areJobsRestricted(
+ uid, packageName, jobStatus.canRunInBatterySaver());
final boolean isActive;
if (activeState == UNKNOWN) {
@@ -220,8 +244,7 @@ public final class BackgroundJobsController extends StateController {
}
boolean didChange =
jobStatus.setBackgroundNotRestrictedConstraintSatisfied(nowElapsed, canRun,
- !mActivityManagerInternal.isBgAutoRestrictedBucketFeatureFlagEnabled()
- && !mAppStateTracker.isRunAnyInBackgroundAppOpsAllowed(uid, packageName));
+ isUserBgRestricted);
didChange |= jobStatus.setUidActive(isActive);
return didChange;
}
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 30eacf3c7c70..5246f2bf838b 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
@@ -35,7 +35,7 @@ import android.util.proto.ProtoOutputStream;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.JobSchedulerBackgroundThread;
+import com.android.server.AppSchedulingModuleThread;
import com.android.server.LocalServices;
import com.android.server.job.JobSchedulerService;
import com.android.server.job.StateControllerProto;
@@ -62,6 +62,7 @@ public final class BatteryController extends RestrictingController {
private final PowerTracker mPowerTracker;
+ private final FlexibilityController mFlexibilityController;
/**
* Helper set to avoid too much GC churn from frequent calls to
* {@link #maybeReportNewChargingStateLocked()}.
@@ -73,10 +74,12 @@ public final class BatteryController extends RestrictingController {
@GuardedBy("mLock")
private Boolean mLastReportedStatsdStablePower = null;
- public BatteryController(JobSchedulerService service) {
+ public BatteryController(JobSchedulerService service,
+ FlexibilityController flexibilityController) {
super(service);
mPowerTracker = new PowerTracker();
mPowerTracker.startTracking();
+ mFlexibilityController = flexibilityController;
}
@Override
@@ -130,7 +133,7 @@ public final class BatteryController extends RestrictingController {
}
@Override
- public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob, boolean forUpdate) {
+ public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob) {
if (taskStatus.clearTrackingController(JobStatus.TRACKING_BATTERY)) {
mTrackedTasks.remove(taskStatus);
mTopStartedJobs.remove(taskStatus);
@@ -140,7 +143,7 @@ public final class BatteryController extends RestrictingController {
@Override
public void stopTrackingRestrictedJobLocked(JobStatus jobStatus) {
if (!jobStatus.hasPowerConstraint()) {
- maybeStopTrackingJobLocked(jobStatus, null, false);
+ maybeStopTrackingJobLocked(jobStatus, null);
}
}
@@ -148,7 +151,7 @@ public final class BatteryController extends RestrictingController {
@GuardedBy("mLock")
public void onBatteryStateChangedLocked() {
// Update job bookkeeping out of band.
- JobSchedulerBackgroundThread.getHandler().post(() -> {
+ AppSchedulingModuleThread.getHandler().post(() -> {
synchronized (mLock) {
maybeReportNewChargingStateLocked();
}
@@ -185,13 +188,19 @@ public final class BatteryController extends RestrictingController {
mLastReportedStatsdStablePower = stablePower;
}
if (mLastReportedStatsdBatteryNotLow == null
- || mLastReportedStatsdBatteryNotLow != stablePower) {
+ || mLastReportedStatsdBatteryNotLow != batteryNotLow) {
logDeviceWideConstraintStateToStatsd(JobStatus.CONSTRAINT_BATTERY_NOT_LOW,
batteryNotLow);
mLastReportedStatsdBatteryNotLow = batteryNotLow;
}
final long nowElapsed = sElapsedRealtimeClock.millis();
+
+ mFlexibilityController.setConstraintSatisfied(
+ JobStatus.CONSTRAINT_CHARGING, mService.isBatteryCharging(), nowElapsed);
+ mFlexibilityController.setConstraintSatisfied(
+ JobStatus.CONSTRAINT_BATTERY_NOT_LOW, batteryNotLow, nowElapsed);
+
for (int i = mTrackedTasks.size() - 1; i >= 0; i--) {
final JobStatus ts = mTrackedTasks.valueAt(i);
if (ts.hasChargingConstraint()) {
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/ComponentController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/ComponentController.java
index aca381ff2043..b029e0075dc2 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/ComponentController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/ComponentController.java
@@ -93,7 +93,12 @@ public class ComponentController extends StateController {
}
};
- private final SparseArrayMap<ComponentName, ServiceInfo> mServiceInfoCache =
+ /**
+ * Cache containing the processName of the ServiceInfo (see {@link ServiceInfo#processName})
+ * if the Service exists and is available.
+ * {@code null} will be stored if the service is currently unavailable.
+ */
+ private final SparseArrayMap<ComponentName, String> mServiceProcessCache =
new SparseArrayMap<>();
private final ComponentStateUpdateFunctor mComponentStateUpdateFunctor =
@@ -122,8 +127,7 @@ public class ComponentController extends StateController {
}
@Override
- public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob,
- boolean forUpdate) {
+ public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob) {
}
@Override
@@ -135,18 +139,18 @@ public class ComponentController extends StateController {
@Override
@GuardedBy("mLock")
public void onUserRemovedLocked(int userId) {
- mServiceInfoCache.delete(userId);
+ mServiceProcessCache.delete(userId);
}
@Nullable
@GuardedBy("mLock")
- private ServiceInfo getServiceInfoLocked(JobStatus jobStatus) {
+ private String getServiceProcessLocked(JobStatus jobStatus) {
final ComponentName service = jobStatus.getServiceComponent();
final int userId = jobStatus.getUserId();
- if (mServiceInfoCache.contains(userId, service)) {
+ if (mServiceProcessCache.contains(userId, service)) {
// Return whatever is in the cache, even if it's null. When something changes, we
// clear the cache.
- return mServiceInfoCache.get(userId, service);
+ return mServiceProcessCache.get(userId, service);
}
ServiceInfo si;
@@ -165,30 +169,31 @@ public class ComponentController extends StateController {
// Write null to the cache so we don't keep querying PM.
si = null;
}
- mServiceInfoCache.add(userId, service, si);
+ final String processName = si == null ? null : si.processName;
+ mServiceProcessCache.add(userId, service, processName);
- return si;
+ return processName;
}
@GuardedBy("mLock")
private boolean updateComponentEnabledStateLocked(JobStatus jobStatus) {
- final ServiceInfo service = getServiceInfoLocked(jobStatus);
+ final String processName = getServiceProcessLocked(jobStatus);
- if (DEBUG && service == null) {
+ if (DEBUG && processName == null) {
Slog.v(TAG, jobStatus.toShortString() + " component not present");
}
- final ServiceInfo ogService = jobStatus.serviceInfo;
- jobStatus.serviceInfo = service;
- return !Objects.equals(ogService, service);
+ final String ogProcess = jobStatus.serviceProcessName;
+ jobStatus.serviceProcessName = processName;
+ return !Objects.equals(ogProcess, processName);
}
@GuardedBy("mLock")
private void clearComponentsForPackageLocked(final int userId, final String pkg) {
- final int uIdx = mServiceInfoCache.indexOfKey(userId);
- for (int c = mServiceInfoCache.numElementsForKey(userId) - 1; c >= 0; --c) {
- final ComponentName cn = mServiceInfoCache.keyAt(uIdx, c);
+ final int uIdx = mServiceProcessCache.indexOfKey(userId);
+ for (int c = mServiceProcessCache.numElementsForKey(userId) - 1; c >= 0; --c) {
+ final ComponentName cn = mServiceProcessCache.keyAt(uIdx, c);
if (cn.getPackageName().equals(pkg)) {
- mServiceInfoCache.delete(userId, cn);
+ mServiceProcessCache.delete(userId, cn);
}
}
}
@@ -207,7 +212,7 @@ public class ComponentController extends StateController {
private void updateComponentStateForUser(final int userId) {
synchronized (mLock) {
- mServiceInfoCache.delete(userId);
+ mServiceProcessCache.delete(userId);
updateComponentStatesLocked(jobStatus -> {
// Using user ID instead of source user ID because the service will run under the
// user ID, not source user ID.
@@ -247,15 +252,15 @@ public class ComponentController extends StateController {
@Override
@GuardedBy("mLock")
public void dumpControllerStateLocked(IndentingPrintWriter pw, Predicate<JobStatus> predicate) {
- for (int u = 0; u < mServiceInfoCache.numMaps(); ++u) {
- final int userId = mServiceInfoCache.keyAt(u);
- for (int p = 0; p < mServiceInfoCache.numElementsForKey(userId); ++p) {
- final ComponentName componentName = mServiceInfoCache.keyAt(u, p);
+ for (int u = 0; u < mServiceProcessCache.numMaps(); ++u) {
+ final int userId = mServiceProcessCache.keyAt(u);
+ for (int p = 0; p < mServiceProcessCache.numElementsForKey(userId); ++p) {
+ final ComponentName componentName = mServiceProcessCache.keyAt(u, p);
pw.print(userId);
pw.print("-");
pw.print(componentName);
pw.print(": ");
- pw.print(mServiceInfoCache.valueAt(u, p));
+ pw.print(mServiceProcessCache.valueAt(u, p));
pw.println();
}
}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java
index 892e0c0280c0..6d938debde10 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
@@ -18,15 +18,18 @@ package com.android.server.job.controllers;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED;
import static com.android.server.job.JobSchedulerService.RESTRICTED_INDEX;
import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.app.ActivityManager;
import android.app.job.JobInfo;
import android.net.ConnectivityManager;
import android.net.ConnectivityManager.NetworkCallback;
+import android.net.INetworkPolicyListener;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkPolicyManager;
@@ -42,18 +45,18 @@ import android.telephony.TelephonyManager;
import android.text.format.DateUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
-import android.util.DataUnit;
import android.util.IndentingPrintWriter;
import android.util.Log;
import android.util.Pools;
import android.util.Slog;
import android.util.SparseArray;
+import android.util.SparseBooleanArray;
import android.util.TimeUtils;
import android.util.proto.ProtoOutputStream;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.JobSchedulerBackgroundThread;
+import com.android.server.AppSchedulingModuleThread;
import com.android.server.LocalServices;
import com.android.server.job.JobSchedulerService;
import com.android.server.job.JobSchedulerService.Constants;
@@ -82,6 +85,8 @@ public final class ConnectivityController extends RestrictingController implemen
private static final boolean DEBUG = JobSchedulerService.DEBUG
|| Log.isLoggable(TAG, Log.DEBUG);
+ public static final long UNKNOWN_TIME = -1L;
+
// The networking stack has a hard limit so we can't make this configurable.
private static final int MAX_NETWORK_CALLBACKS = 125;
/**
@@ -97,6 +102,12 @@ public final class ConnectivityController extends RestrictingController implemen
~(ConnectivityManager.BLOCKED_REASON_APP_STANDBY
| ConnectivityManager.BLOCKED_REASON_BATTERY_SAVER
| ConnectivityManager.BLOCKED_REASON_DOZE);
+ private static final int UNBYPASSABLE_UI_BLOCKED_REASONS =
+ ~(ConnectivityManager.BLOCKED_REASON_APP_STANDBY
+ | ConnectivityManager.BLOCKED_REASON_BATTERY_SAVER
+ | ConnectivityManager.BLOCKED_REASON_DOZE
+ | ConnectivityManager.BLOCKED_METERED_REASON_DATA_SAVER
+ | ConnectivityManager.BLOCKED_METERED_REASON_USER_RESTRICTED);
private static final int UNBYPASSABLE_FOREGROUND_BLOCKED_REASONS =
~(ConnectivityManager.BLOCKED_REASON_APP_STANDBY
| ConnectivityManager.BLOCKED_REASON_BATTERY_SAVER
@@ -105,7 +116,9 @@ public final class ConnectivityController extends RestrictingController implemen
| ConnectivityManager.BLOCKED_METERED_REASON_USER_RESTRICTED);
private final ConnectivityManager mConnManager;
+ private final NetworkPolicyManager mNetPolicyManager;
private final NetworkPolicyManagerInternal mNetPolicyManagerInternal;
+ private final FlexibilityController mFlexibilityController;
/** List of tracked jobs keyed by source UID. */
@GuardedBy("mLock")
@@ -147,11 +160,13 @@ public final class ConnectivityController extends RestrictingController implemen
// 2. Waiting connectivity jobs would be ready with connectivity
// 3. An existing network satisfies a waiting connectivity job's requirements
// 4. TOP proc state
- // 5. Existence of treat-as-EJ EJs (not just requested EJs)
- // 6. FGS proc state
- // 7. EJ enqueue time
- // 8. Any other important job priorities/proc states
- // 9. Enqueue time
+ // 5. Existence of treat-as-UI UIJs (not just requested UIJs)
+ // 6. Existence of treat-as-EJ EJs (not just requested EJs)
+ // 7. FGS proc state
+ // 8. UIJ enqueue time
+ // 9. EJ enqueue time
+ // 10. Any other important job priorities/proc states
+ // 11. Enqueue time
// TODO: maybe consider number of jobs
// TODO: consider IMPORTANT_WHILE_FOREGROUND bit
final int runningPriority = prioritizeExistenceOver(0,
@@ -179,8 +194,13 @@ public final class ConnectivityController extends RestrictingController implemen
if (topPriority != 0) {
return topPriority;
}
- // They're either both TOP or both not TOP. Prioritize the app that has runnable EJs
+ // They're either both TOP or both not TOP. Prioritize the app that has runnable UIJs
// pending.
+ final int uijPriority = prioritizeExistenceOver(0, us1.numUIJs, us2.numUIJs);
+ if (uijPriority != 0) {
+ return uijPriority;
+ }
+ // Still equivalent. Prioritize the app that has runnable EJs pending.
final int ejPriority = prioritizeExistenceOver(0, us1.numEJs, us2.numEJs);
if (ejPriority != 0) {
return ejPriority;
@@ -193,6 +213,12 @@ public final class ConnectivityController extends RestrictingController implemen
if (fgsPriority != 0) {
return fgsPriority;
}
+ // Order them by UIJ enqueue time to help provide low UIJ latency.
+ if (us1.earliestUIJEnqueueTime < us2.earliestUIJEnqueueTime) {
+ return -1;
+ } else if (us1.earliestUIJEnqueueTime > us2.earliestUIJEnqueueTime) {
+ return 1;
+ }
// Order them by EJ enqueue time to help provide low EJ latency.
if (us1.earliestEJEnqueueTime < us2.earliestEJEnqueueTime) {
return -1;
@@ -219,6 +245,8 @@ public final class ConnectivityController extends RestrictingController implemen
*/
private final List<UidStats> mSortedStats = new ArrayList<>();
@GuardedBy("mLock")
+ private final SparseBooleanArray mBackgroundMeteredAllowed = new SparseBooleanArray();
+ @GuardedBy("mLock")
private long mLastCallbackAdjustmentTimeElapsed;
@GuardedBy("mLock")
private final SparseArray<CellSignalStrengthCallback> mSignalStrengths = new SparseArray<>();
@@ -228,20 +256,27 @@ public final class ConnectivityController extends RestrictingController implemen
private static final int MSG_ADJUST_CALLBACKS = 0;
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 final Handler mHandler;
- public ConnectivityController(JobSchedulerService service) {
+ public ConnectivityController(JobSchedulerService service,
+ @NonNull FlexibilityController flexibilityController) {
super(service);
- mHandler = new CcHandler(mContext.getMainLooper());
+ mHandler = new CcHandler(AppSchedulingModuleThread.get().getLooper());
mConnManager = mContext.getSystemService(ConnectivityManager.class);
+ mNetPolicyManager = mContext.getSystemService(NetworkPolicyManager.class);
mNetPolicyManagerInternal = LocalServices.getService(NetworkPolicyManagerInternal.class);
+ mFlexibilityController = flexibilityController;
// We're interested in all network changes; internally we match these
// network changes against the active network for each UID with jobs.
final NetworkRequest request = new NetworkRequest.Builder().clearCapabilities().build();
mConnManager.registerNetworkCallback(request, mNetworkCallback);
+
+ mNetPolicyManager.registerListener(mNetPolicyListener);
}
@GuardedBy("mLock")
@@ -287,8 +322,7 @@ public final class ConnectivityController extends RestrictingController implemen
@GuardedBy("mLock")
@Override
- public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob,
- boolean forUpdate) {
+ public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob) {
if (jobStatus.clearTrackingController(JobStatus.TRACKING_CONNECTIVITY)) {
ArraySet<JobStatus> jobs = mTrackedJobs.get(jobStatus.getSourceUid());
if (jobs != null) {
@@ -411,7 +445,7 @@ public final class ConnectivityController extends RestrictingController implemen
final UidStats uidStats =
getUidStats(jobStatus.getSourceUid(), jobStatus.getSourcePackageName(), true);
- if (jobStatus.shouldTreatAsExpeditedJob()) {
+ if (jobStatus.shouldTreatAsExpeditedJob() || jobStatus.shouldTreatAsUserInitiatedJob()) {
if (!jobStatus.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)) {
// Don't request a direct hole through any of the firewalls. Instead, mark the
// constraint as satisfied if the network is available, and the job will get
@@ -421,7 +455,9 @@ public final class ConnectivityController extends RestrictingController implemen
}
// Don't need to update constraint here if the network goes away. We'll do that as part
// of regular processing when we're notified about the drop.
- } else if (jobStatus.isRequestedExpeditedJob()
+ } else if (((jobStatus.isRequestedExpeditedJob() && !jobStatus.shouldTreatAsExpeditedJob())
+ || (jobStatus.getJob().isUserInitiated()
+ && !jobStatus.shouldTreatAsUserInitiatedJob()))
&& jobStatus.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)) {
// Make sure we don't accidentally keep the constraint as satisfied if the job went
// from being expedited-ready to not-expeditable.
@@ -505,6 +541,7 @@ public final class ConnectivityController extends RestrictingController implemen
// All packages in the UID have been removed. It's safe to remove things based on
// UID alone.
mTrackedJobs.delete(uid);
+ mBackgroundMeteredAllowed.delete(uid);
UidStats uidStats = mUidStats.removeReturnOld(uid);
unregisterDefaultNetworkCallbackLocked(uid, sElapsedRealtimeClock.millis());
mSortedStats.remove(uidStats);
@@ -524,6 +561,12 @@ public final class ConnectivityController extends RestrictingController implemen
mUidStats.removeAt(u);
}
}
+ for (int u = mBackgroundMeteredAllowed.size() - 1; u >= 0; --u) {
+ final int uid = mBackgroundMeteredAllowed.keyAt(u);
+ if (UserHandle.getUserId(uid) == userId) {
+ mBackgroundMeteredAllowed.removeAt(u);
+ }
+ }
postAdjustCallbacks();
}
@@ -568,9 +611,8 @@ public final class ConnectivityController extends RestrictingController implemen
// If we don't know the bandwidth, all we can do is hope the job finishes the minimum
// chunk in time.
if (bandwidthDown > 0) {
- // Divide by 8 to convert bits to bytes.
- final long estimatedMillis = ((minimumChunkBytes * DateUtils.SECOND_IN_MILLIS)
- / (DataUnit.KIBIBYTES.toBytes(bandwidthDown) / 8));
+ final long estimatedMillis =
+ calculateTransferTimeMs(minimumChunkBytes, bandwidthDown);
if (estimatedMillis > maxJobExecutionTimeMs) {
// If we'd never finish the minimum chunk before the timeout, we'd be insane!
Slog.w(TAG, "Minimum chunk " + minimumChunkBytes + " bytes over "
@@ -583,9 +625,8 @@ public final class ConnectivityController extends RestrictingController implemen
final long bandwidthUp = capabilities.getLinkUpstreamBandwidthKbps();
// If we don't know the bandwidth, all we can do is hope the job finishes in time.
if (bandwidthUp > 0) {
- // Divide by 8 to convert bits to bytes.
- final long estimatedMillis = ((minimumChunkBytes * DateUtils.SECOND_IN_MILLIS)
- / (DataUnit.KIBIBYTES.toBytes(bandwidthUp) / 8));
+ final long estimatedMillis =
+ calculateTransferTimeMs(minimumChunkBytes, bandwidthUp);
if (estimatedMillis > maxJobExecutionTimeMs) {
// If we'd never finish the minimum chunk before the timeout, we'd be insane!
Slog.w(TAG, "Minimum chunk " + minimumChunkBytes + " bytes over " + bandwidthUp
@@ -613,9 +654,7 @@ public final class ConnectivityController extends RestrictingController implemen
final long bandwidth = capabilities.getLinkDownstreamBandwidthKbps();
// If we don't know the bandwidth, all we can do is hope the job finishes in time.
if (bandwidth > 0) {
- // Divide by 8 to convert bits to bytes.
- final long estimatedMillis = ((downloadBytes * DateUtils.SECOND_IN_MILLIS)
- / (DataUnit.KIBIBYTES.toBytes(bandwidth) / 8));
+ final long estimatedMillis = calculateTransferTimeMs(downloadBytes, bandwidth);
if (estimatedMillis > maxJobExecutionTimeMs) {
// If we'd never finish before the timeout, we'd be insane!
Slog.w(TAG, "Estimated " + downloadBytes + " download bytes over " + bandwidth
@@ -631,9 +670,7 @@ public final class ConnectivityController extends RestrictingController implemen
final long bandwidth = capabilities.getLinkUpstreamBandwidthKbps();
// If we don't know the bandwidth, all we can do is hope the job finishes in time.
if (bandwidth > 0) {
- // Divide by 8 to convert bits to bytes.
- final long estimatedMillis = ((uploadBytes * DateUtils.SECOND_IN_MILLIS)
- / (DataUnit.KIBIBYTES.toBytes(bandwidth) / 8));
+ final long estimatedMillis = calculateTransferTimeMs(uploadBytes, bandwidth);
if (estimatedMillis > maxJobExecutionTimeMs) {
// If we'd never finish before the timeout, we'd be insane!
Slog.w(TAG, "Estimated " + uploadBytes + " upload bytes over " + bandwidth
@@ -647,6 +684,130 @@ public final class ConnectivityController extends RestrictingController implemen
return false;
}
+ private boolean isMeteredAllowed(@NonNull JobStatus jobStatus,
+ @NonNull NetworkCapabilities networkCapabilities) {
+ // Network isn't metered. Usage is allowed. The rest of this method doesn't apply.
+ if (networkCapabilities.hasCapability(NET_CAPABILITY_NOT_METERED)
+ || networkCapabilities.hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)) {
+ return true;
+ }
+
+ final int uid = jobStatus.getSourceUid();
+ final int procState = mService.getUidProcState(uid);
+ final int capabilities = mService.getUidCapabilities(uid);
+ // Jobs don't raise the proc state to anything better than IMPORTANT_FOREGROUND.
+ // If the app is in a better state, see if it has the capability to use the metered network.
+ final boolean currentStateAllows = procState != ActivityManager.PROCESS_STATE_UNKNOWN
+ && procState < ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND
+ && NetworkPolicyManager.isProcStateAllowedWhileOnRestrictBackground(
+ procState, capabilities);
+ if (DEBUG) {
+ Slog.d(TAG, "UID " + uid
+ + " current state allows metered network=" + currentStateAllows
+ + " procState=" + ActivityManager.procStateToString(procState)
+ + " capabilities=" + ActivityManager.getCapabilitiesSummary(capabilities));
+ }
+ if (currentStateAllows) {
+ return true;
+ }
+
+ if ((jobStatus.getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0) {
+ final int expectedProcState = ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE;
+ final int mergedCapabilities = capabilities
+ | NetworkPolicyManager.getDefaultProcessNetworkCapabilities(expectedProcState);
+ final boolean wouldBeAllowed =
+ NetworkPolicyManager.isProcStateAllowedWhileOnRestrictBackground(
+ expectedProcState, mergedCapabilities);
+ if (DEBUG) {
+ Slog.d(TAG, "UID " + uid
+ + " willBeForeground flag allows metered network=" + wouldBeAllowed
+ + " capabilities="
+ + ActivityManager.getCapabilitiesSummary(mergedCapabilities));
+ }
+ if (wouldBeAllowed) {
+ return true;
+ }
+ }
+
+ if (jobStatus.shouldTreatAsUserInitiatedJob()) {
+ // Since the job is initiated by the user and will be visible to the user, it
+ // should be able to run on metered networks, similar to FGS.
+ // With user-initiated jobs, JobScheduler will request that the process
+ // run at IMPORTANT_FOREGROUND process state
+ // and get the USER_RESTRICTED_NETWORK process capability.
+ final int expectedProcState = ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND;
+ final int mergedCapabilities = capabilities
+ | ActivityManager.PROCESS_CAPABILITY_USER_RESTRICTED_NETWORK
+ | NetworkPolicyManager.getDefaultProcessNetworkCapabilities(expectedProcState);
+ final boolean wouldBeAllowed =
+ NetworkPolicyManager.isProcStateAllowedWhileOnRestrictBackground(
+ expectedProcState, mergedCapabilities);
+ if (DEBUG) {
+ Slog.d(TAG, "UID " + uid
+ + " UI job state allows metered network=" + wouldBeAllowed
+ + " capabilities=" + mergedCapabilities);
+ }
+ if (wouldBeAllowed) {
+ return true;
+ }
+ }
+
+ if (mBackgroundMeteredAllowed.indexOfKey(uid) >= 0) {
+ return mBackgroundMeteredAllowed.get(uid);
+ }
+
+ final boolean allowed =
+ mNetPolicyManager.getRestrictBackgroundStatus(uid)
+ != ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED;
+ if (DEBUG) {
+ Slog.d(TAG, "UID " + uid + " allowed in data saver=" + allowed);
+ }
+ mBackgroundMeteredAllowed.put(uid, allowed);
+ return allowed;
+ }
+
+ /**
+ * Return the estimated amount of time this job will be transferring data,
+ * based on the current network speed.
+ */
+ public long getEstimatedTransferTimeMs(JobStatus jobStatus) {
+ final long downloadBytes = jobStatus.getEstimatedNetworkDownloadBytes();
+ final long uploadBytes = jobStatus.getEstimatedNetworkUploadBytes();
+ if (downloadBytes == JobInfo.NETWORK_BYTES_UNKNOWN
+ && uploadBytes == JobInfo.NETWORK_BYTES_UNKNOWN) {
+ return UNKNOWN_TIME;
+ }
+ if (jobStatus.network == null) {
+ // This job doesn't have a network assigned.
+ return UNKNOWN_TIME;
+ }
+ NetworkCapabilities capabilities = getNetworkCapabilities(jobStatus.network);
+ if (capabilities == null) {
+ return UNKNOWN_TIME;
+ }
+ final long estimatedDownloadTimeMs = calculateTransferTimeMs(downloadBytes,
+ capabilities.getLinkDownstreamBandwidthKbps());
+ final long estimatedUploadTimeMs = calculateTransferTimeMs(uploadBytes,
+ capabilities.getLinkUpstreamBandwidthKbps());
+ if (estimatedDownloadTimeMs == UNKNOWN_TIME) {
+ return estimatedUploadTimeMs;
+ } else if (estimatedUploadTimeMs == UNKNOWN_TIME) {
+ return estimatedDownloadTimeMs;
+ }
+ return estimatedDownloadTimeMs + estimatedUploadTimeMs;
+ }
+
+ @VisibleForTesting
+ static long calculateTransferTimeMs(long transferBytes, long bandwidthKbps) {
+ if (transferBytes == JobInfo.NETWORK_BYTES_UNKNOWN || bandwidthKbps <= 0) {
+ return UNKNOWN_TIME;
+ }
+ return (transferBytes * DateUtils.SECOND_IN_MILLIS)
+ // Multiply by 1000 to convert kilobits to bits.
+ // Divide by 8 to convert bits to bytes.
+ / (bandwidthKbps * 1000 / 8);
+ }
+
private static boolean isCongestionDelayed(JobStatus jobStatus, Network network,
NetworkCapabilities capabilities, Constants constants) {
// If network is congested, and job is less than 50% through the
@@ -798,6 +959,12 @@ public final class ConnectivityController extends RestrictingController implemen
// First, are we insane?
if (isInsane(jobStatus, network, capabilities, constants)) return false;
+ // User-initiated jobs might make NetworkPolicyManager open up network access for
+ // the whole UID. If network access is opened up just because of UI jobs, we want
+ // to make sure that non-UI jobs don't run during that time,
+ // so make sure the job can make use of the metered network at this time.
+ if (!isMeteredAllowed(jobStatus, capabilities)) return false;
+
// Second, is the network congested?
if (isCongestionDelayed(jobStatus, network, capabilities, constants)) return false;
@@ -897,10 +1064,12 @@ public final class ConnectivityController extends RestrictingController implemen
if (us.lastUpdatedElapsed + MIN_STATS_UPDATE_INTERVAL_MS < nowElapsed) {
us.earliestEnqueueTime = Long.MAX_VALUE;
us.earliestEJEnqueueTime = Long.MAX_VALUE;
+ us.earliestUIJEnqueueTime = Long.MAX_VALUE;
us.numReadyWithConnectivity = 0;
us.numRequestedNetworkAvailable = 0;
us.numRegular = 0;
us.numEJs = 0;
+ us.numUIJs = 0;
for (int j = 0; j < jobs.size(); ++j) {
JobStatus job = jobs.valueAt(j);
@@ -917,10 +1086,15 @@ public final class ConnectivityController extends RestrictingController implemen
if (job.shouldTreatAsExpeditedJob() || job.startedAsExpeditedJob) {
us.earliestEJEnqueueTime =
Math.min(us.earliestEJEnqueueTime, job.enqueueTime);
+ } else if (job.shouldTreatAsUserInitiatedJob()) {
+ us.earliestUIJEnqueueTime =
+ Math.min(us.earliestUIJEnqueueTime, job.enqueueTime);
}
}
if (job.shouldTreatAsExpeditedJob() || job.startedAsExpeditedJob) {
us.numEJs++;
+ } else if (job.shouldTreatAsUserInitiatedJob()) {
+ us.numUIJs++;
} else {
us.numRegular++;
}
@@ -1021,6 +1195,11 @@ public final class ConnectivityController extends RestrictingController implemen
Slog.d(TAG, "Using FG bypass for " + jobStatus.getSourceUid());
}
unbypassableBlockedReasons = UNBYPASSABLE_FOREGROUND_BLOCKED_REASONS;
+ } else if (jobStatus.shouldTreatAsUserInitiatedJob()) {
+ if (DEBUG) {
+ Slog.d(TAG, "Using UI bypass for " + jobStatus.getSourceUid());
+ }
+ unbypassableBlockedReasons = UNBYPASSABLE_UI_BLOCKED_REASONS;
} else if (jobStatus.shouldTreatAsExpeditedJob() || jobStatus.startedAsExpeditedJob) {
if (DEBUG) {
Slog.d(TAG, "Using EJ bypass for " + jobStatus.getSourceUid());
@@ -1056,8 +1235,44 @@ public final class ConnectivityController extends RestrictingController implemen
final boolean satisfied = isSatisfied(jobStatus, network, capabilities, mConstants);
+ if (!satisfied && jobStatus.network != null
+ && mService.isCurrentlyRunningLocked(jobStatus)
+ && isSatisfied(jobStatus, jobStatus.network,
+ getNetworkCapabilities(jobStatus.network), mConstants)) {
+ // A new network became available for a currently running job
+ // (and most likely became the default network for the app),
+ // but it doesn't yet satisfy the requested constraints and the old network
+ // is still available and satisfies the constraints. Don't change the network
+ // given to the job for now and let it keep running. We will re-evaluate when
+ // the capabilities or connection state of either network change.
+ if (DEBUG) {
+ Slog.i(TAG, "Not reassigning network from " + jobStatus.network
+ + " to " + network + " for running job " + jobStatus);
+ }
+ return false;
+ }
+
final boolean changed = jobStatus.setConnectivityConstraintSatisfied(nowElapsed, satisfied);
+ jobStatus.setHasAccessToUnmetered(satisfied && capabilities != null
+ && capabilities.hasCapability(NET_CAPABILITY_NOT_METERED));
+ if (jobStatus.getPreferUnmetered()) {
+ jobStatus.setFlexibilityConstraintSatisfied(nowElapsed,
+ mFlexibilityController.isFlexibilitySatisfiedLocked(jobStatus));
+ }
+
+ // Try to handle network transitions in a reasonable manner. See the lengthy note inside
+ // UidDefaultNetworkCallback for more details.
+ if (!changed && satisfied && jobStatus.network != null
+ && mService.isCurrentlyRunningLocked(jobStatus)) {
+ // The job's connectivity constraint continues to be satisfied even though the network
+ // has changed.
+ // Inform the job of the new network so that it can attempt to switch over. This is the
+ // ideal behavior for certain transitions such as going from a metered network to an
+ // unmetered network.
+ mStateChangedListener.onNetworkChanged(jobStatus, network);
+ }
+
// Pass along the evaluated network for job to use; prevents race
// conditions as default routes change over time, and opens the door to
// using non-default routes.
@@ -1238,7 +1453,7 @@ public final class ConnectivityController extends RestrictingController implemen
TelephonyManager idTm = telephonyManager.createForSubscriptionId(subId);
CellSignalStrengthCallback callback = new CellSignalStrengthCallback();
idTm.registerTelephonyCallback(
- JobSchedulerBackgroundThread.getExecutor(), callback);
+ AppSchedulingModuleThread.getExecutor(), callback);
mSignalStrengths.put(subId, callback);
final SignalStrength signalStrength = idTm.getSignalStrength();
@@ -1281,6 +1496,26 @@ public final class ConnectivityController extends RestrictingController implemen
}
};
+ private final INetworkPolicyListener mNetPolicyListener = new NetworkPolicyManager.Listener() {
+ @Override
+ public void onRestrictBackgroundChanged(boolean restrictBackground) {
+ if (DEBUG) {
+ Slog.v(TAG, "onRestrictBackgroundChanged: " + restrictBackground);
+ }
+ mHandler.obtainMessage(MSG_DATA_SAVER_TOGGLED).sendToTarget();
+ }
+
+ @Override
+ public void onUidPoliciesChanged(int uid, int uidPolicies) {
+ if (DEBUG) {
+ Slog.v(TAG, "onUidPoliciesChanged: " + uid);
+ }
+ mHandler.obtainMessage(MSG_UID_POLICIES_CHANGED,
+ uid, mNetPolicyManager.getRestrictBackgroundStatus(uid))
+ .sendToTarget();
+ }
+ };
+
private class CcHandler extends Handler {
CcHandler(Looper looper) {
super(looper);
@@ -1302,6 +1537,27 @@ public final class ConnectivityController extends RestrictingController implemen
updateAllTrackedJobsLocked(allowThrottle);
}
break;
+
+ case MSG_DATA_SAVER_TOGGLED:
+ removeMessages(MSG_DATA_SAVER_TOGGLED);
+ synchronized (mLock) {
+ mBackgroundMeteredAllowed.clear();
+ updateTrackedJobsLocked(-1, null);
+ }
+ break;
+
+ case MSG_UID_POLICIES_CHANGED:
+ final int uid = msg.arg1;
+ final boolean newAllowed =
+ msg.arg2 != ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED;
+ synchronized (mLock) {
+ final boolean oldAllowed = mBackgroundMeteredAllowed.get(uid);
+ if (oldAllowed != newAllowed) {
+ mBackgroundMeteredAllowed.put(uid, newAllowed);
+ updateTrackedJobsLocked(uid, null);
+ }
+ }
+ break;
}
}
}
@@ -1351,8 +1607,8 @@ public final class ConnectivityController extends RestrictingController implemen
// the onBlockedStatusChanged() call, we re-evaluate the job, but keep it running
// (assuming the new network satisfies constraints). The app continues to use the old
// network (if they use the network object provided through JobParameters.getNetwork())
- // because we don't notify them of the default network change. If the old network no
- // longer satisfies requested constraints, then we have a problem. Depending on the order
+ // because we don't notify them of the default network change. If the old network later
+ // stops satisfying requested constraints, then we have a problem. Depending on the order
// of calls, if the per-UID callback gets notified of the network change before the
// general callback gets notified of the capabilities change, then the job's network
// object will point to the new network and we won't stop the job, even though we told it
@@ -1418,8 +1674,10 @@ public final class ConnectivityController extends RestrictingController implemen
public int numRequestedNetworkAvailable;
public int numEJs;
public int numRegular;
+ public int numUIJs;
public long earliestEnqueueTime;
public long earliestEJEnqueueTime;
+ public long earliestUIJEnqueueTime;
public long lastUpdatedElapsed;
private UidStats(int uid) {
@@ -1437,6 +1695,7 @@ public final class ConnectivityController extends RestrictingController implemen
pw.print("#reg", numRegular);
pw.print("earliestEnqueue", earliestEnqueueTime);
pw.print("earliestEJEnqueue", earliestEJEnqueueTime);
+ pw.print("earliestUIJEnqueue", earliestUIJEnqueueTime);
pw.print("updated=");
TimeUtils.formatDuration(lastUpdatedElapsed - nowElapsed, pw);
pw.println("}");
@@ -1515,6 +1774,12 @@ public final class ConnectivityController extends RestrictingController implemen
}
pw.println();
+ if (mBackgroundMeteredAllowed.size() > 0) {
+ pw.print("Background metered allowed: ");
+ pw.println(mBackgroundMeteredAllowed);
+ pw.println();
+ }
+
pw.println("Current default network callbacks:");
pw.increaseIndent();
for (int i = 0; i < mCurrentDefaultNetworkCallbacks.size(); i++) {
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/ContentObserverController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/ContentObserverController.java
index 83a756cf1e11..122fe695c70b 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/ContentObserverController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/ContentObserverController.java
@@ -33,6 +33,7 @@ import android.util.SparseArray;
import android.util.TimeUtils;
import android.util.proto.ProtoOutputStream;
+import com.android.server.AppSchedulingModuleThread;
import com.android.server.job.JobSchedulerService;
import com.android.server.job.StateControllerProto;
import com.android.server.job.StateControllerProto.ContentObserverController.Observer.TriggerContentData;
@@ -70,7 +71,7 @@ public final class ContentObserverController extends StateController {
public ContentObserverController(JobSchedulerService service) {
super(service);
- mHandler = new Handler(mContext.getMainLooper());
+ mHandler = new Handler(AppSchedulingModuleThread.get().getLooper());
}
@Override
@@ -159,8 +160,7 @@ public final class ContentObserverController extends StateController {
}
@Override
- public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob,
- boolean forUpdate) {
+ public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob) {
if (taskStatus.clearTrackingController(JobStatus.TRACKING_CONTENT)) {
mTrackedTasks.remove(taskStatus);
if (taskStatus.contentObserverJobInstance != null) {
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java
index abbe177c5d49..d5c9ae615486 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java
@@ -36,6 +36,7 @@ import android.util.SparseBooleanArray;
import android.util.proto.ProtoOutputStream;
import com.android.internal.util.ArrayUtils;
+import com.android.server.AppSchedulingModuleThread;
import com.android.server.DeviceIdleInternal;
import com.android.server.LocalServices;
import com.android.server.job.JobSchedulerService;
@@ -127,7 +128,7 @@ public final class DeviceIdleJobsController extends StateController {
public DeviceIdleJobsController(JobSchedulerService service) {
super(service);
- mHandler = new DeviceIdleJobsDelayHandler(mContext.getMainLooper());
+ mHandler = new DeviceIdleJobsDelayHandler(AppSchedulingModuleThread.get().getLooper());
// Register for device idle mode changes
mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
mLocalDeviceIdleController =
@@ -225,8 +226,7 @@ public final class DeviceIdleJobsController extends StateController {
}
@Override
- public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob,
- boolean forUpdate) {
+ public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob) {
if ((jobStatus.getFlags()&JobInfo.FLAG_IMPORTANT_WHILE_FOREGROUND) != 0) {
mAllowInIdleJobs.remove(jobStatus);
}
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
new file mode 100644
index 000000000000..b9e3b76b0279
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java
@@ -0,0 +1,861 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.job.controllers;
+
+import static android.text.format.DateUtils.DAY_IN_MILLIS;
+import static android.text.format.DateUtils.HOUR_IN_MILLIS;
+import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
+
+import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
+import static com.android.server.job.controllers.JobStatus.CONSTRAINT_BATTERY_NOT_LOW;
+import static com.android.server.job.controllers.JobStatus.CONSTRAINT_CHARGING;
+import static com.android.server.job.controllers.JobStatus.CONSTRAINT_CONNECTIVITY;
+import static com.android.server.job.controllers.JobStatus.CONSTRAINT_FLEXIBLE;
+import static com.android.server.job.controllers.JobStatus.CONSTRAINT_IDLE;
+
+import android.annotation.ElapsedRealtimeLong;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.job.JobInfo;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.UserHandle;
+import android.provider.DeviceConfig;
+import android.util.ArraySet;
+import android.util.IndentingPrintWriter;
+import android.util.Log;
+import android.util.Slog;
+import android.util.SparseArrayMap;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.AppSchedulingModuleThread;
+import com.android.server.job.JobSchedulerService;
+import com.android.server.utils.AlarmQueue;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.function.Predicate;
+
+/**
+ * Controller that tracks the number of flexible constraints being actively satisfied.
+ * Drops constraint for TOP apps and lowers number of required constraints with time.
+ */
+public final class FlexibilityController extends StateController {
+ private static final String TAG = "JobScheduler.Flex";
+ private static final boolean DEBUG = JobSchedulerService.DEBUG
+ || Log.isLoggable(TAG, Log.DEBUG);
+
+ /** List of all system-wide flexible constraints whose satisfaction is independent of job. */
+ static final int SYSTEM_WIDE_FLEXIBLE_CONSTRAINTS = CONSTRAINT_BATTERY_NOT_LOW
+ | CONSTRAINT_CHARGING
+ | CONSTRAINT_IDLE;
+
+ /** List of flexible constraints a job can opt into. */
+ static final int OPTIONAL_FLEXIBLE_CONSTRAINTS = CONSTRAINT_BATTERY_NOT_LOW
+ | CONSTRAINT_CHARGING
+ | CONSTRAINT_IDLE;
+
+ /** List of all job flexible constraints whose satisfaction is job specific. */
+ private static final int JOB_SPECIFIC_FLEXIBLE_CONSTRAINTS = CONSTRAINT_CONNECTIVITY;
+
+ /** List of all flexible constraints. */
+ private static final int FLEXIBLE_CONSTRAINTS =
+ JOB_SPECIFIC_FLEXIBLE_CONSTRAINTS | SYSTEM_WIDE_FLEXIBLE_CONSTRAINTS;
+
+ private static final int NUM_JOB_SPECIFIC_FLEXIBLE_CONSTRAINTS =
+ Integer.bitCount(JOB_SPECIFIC_FLEXIBLE_CONSTRAINTS);
+
+ static final int NUM_OPTIONAL_FLEXIBLE_CONSTRAINTS =
+ Integer.bitCount(OPTIONAL_FLEXIBLE_CONSTRAINTS);
+
+ static final int NUM_SYSTEM_WIDE_FLEXIBLE_CONSTRAINTS =
+ Integer.bitCount(SYSTEM_WIDE_FLEXIBLE_CONSTRAINTS);
+
+ static final int NUM_FLEXIBLE_CONSTRAINTS = Integer.bitCount(FLEXIBLE_CONSTRAINTS);
+
+ private static final long NO_LIFECYCLE_END = Long.MAX_VALUE;
+
+ /**
+ * The default deadline that all flexible constraints should be dropped by if a job lacks
+ * a deadline.
+ */
+ private long mFallbackFlexibilityDeadlineMs =
+ FcConfig.DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS;
+
+ private long mRescheduledJobDeadline = FcConfig.DEFAULT_RESCHEDULED_JOB_DEADLINE_MS;
+ private long mMaxRescheduledDeadline = FcConfig.DEFAULT_MAX_RESCHEDULED_DEADLINE_MS;
+
+ @VisibleForTesting
+ @GuardedBy("mLock")
+ boolean mFlexibilityEnabled = FcConfig.DEFAULT_FLEXIBILITY_ENABLED;
+
+ private long mMinTimeBetweenFlexibilityAlarmsMs =
+ FcConfig.DEFAULT_MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS;
+
+ /** Hard cutoff to remove flexible constraints. */
+ private long mDeadlineProximityLimitMs =
+ FcConfig.DEFAULT_DEADLINE_PROXIMITY_LIMIT_MS;
+
+ /**
+ * 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.
+ */
+ private int[] mPercentToDropConstraints;
+
+ @VisibleForTesting
+ boolean mDeviceSupportsFlexConstraints;
+
+ /**
+ * Keeps track of what flexible constraints are satisfied at the moment.
+ * Is updated by the other controllers.
+ */
+ @VisibleForTesting
+ @GuardedBy("mLock")
+ int mSatisfiedFlexibleConstraints;
+
+ @VisibleForTesting
+ @GuardedBy("mLock")
+ final FlexibilityTracker mFlexibilityTracker;
+ @VisibleForTesting
+ @GuardedBy("mLock")
+ final FlexibilityAlarmQueue mFlexibilityAlarmQueue;
+ @VisibleForTesting
+ final FcConfig mFcConfig;
+ private final FcHandler mHandler;
+ @VisibleForTesting
+ final PrefetchController mPrefetchController;
+
+ /**
+ * Stores the beginning of prefetch jobs lifecycle per app as a maximum of
+ * the last time the app was used and the last time the launch time was updated.
+ */
+ @VisibleForTesting
+ @GuardedBy("mLock")
+ final SparseArrayMap<String, Long> mPrefetchLifeCycleStart = new SparseArrayMap<>();
+
+ @VisibleForTesting
+ final PrefetchController.PrefetchChangedListener mPrefetchChangedListener =
+ new PrefetchController.PrefetchChangedListener() {
+ @Override
+ public void onPrefetchCacheUpdated(ArraySet<JobStatus> jobs, int userId,
+ String pkgName, long prevEstimatedLaunchTime,
+ long newEstimatedLaunchTime, long nowElapsed) {
+ synchronized (mLock) {
+ final long prefetchThreshold =
+ mPrefetchController.getLaunchTimeThresholdMs();
+ boolean jobWasInPrefetchWindow = prevEstimatedLaunchTime
+ - prefetchThreshold < nowElapsed;
+ boolean jobIsInPrefetchWindow = newEstimatedLaunchTime
+ - prefetchThreshold < nowElapsed;
+ if (jobIsInPrefetchWindow != jobWasInPrefetchWindow) {
+ // If the job was in the window previously then changing the start
+ // of the lifecycle to the current moment without a large change in the
+ // end would squeeze the window too tight fail to drop constraints.
+ mPrefetchLifeCycleStart.add(userId, pkgName, Math.max(nowElapsed,
+ mPrefetchLifeCycleStart.getOrDefault(userId, pkgName, 0L)));
+ }
+ for (int i = 0; i < jobs.size(); i++) {
+ JobStatus js = jobs.valueAt(i);
+ if (!js.hasFlexibilityConstraint()) {
+ continue;
+ }
+ mFlexibilityTracker.resetJobNumDroppedConstraints(js, nowElapsed);
+ mFlexibilityAlarmQueue.scheduleDropNumConstraintsAlarm(js, nowElapsed);
+ }
+ }
+ }
+ };
+
+ private static final int MSG_UPDATE_JOBS = 0;
+
+ public FlexibilityController(
+ JobSchedulerService service, PrefetchController prefetchController) {
+ super(service);
+ mHandler = new FcHandler(AppSchedulingModuleThread.get().getLooper());
+ mDeviceSupportsFlexConstraints = !mContext.getPackageManager().hasSystemFeature(
+ PackageManager.FEATURE_AUTOMOTIVE);
+ mFlexibilityEnabled &= mDeviceSupportsFlexConstraints;
+ mFlexibilityTracker = new FlexibilityTracker(NUM_FLEXIBLE_CONSTRAINTS);
+ mFcConfig = new FcConfig();
+ mFlexibilityAlarmQueue = new FlexibilityAlarmQueue(
+ mContext, AppSchedulingModuleThread.get().getLooper());
+ mPercentToDropConstraints =
+ mFcConfig.DEFAULT_PERCENT_TO_DROP_FLEXIBLE_CONSTRAINTS;
+ mPrefetchController = prefetchController;
+ if (mFlexibilityEnabled) {
+ mPrefetchController.registerPrefetchChangedListener(mPrefetchChangedListener);
+ }
+ }
+
+ /**
+ * StateController interface.
+ */
+ @Override
+ @GuardedBy("mLock")
+ public void maybeStartTrackingJobLocked(JobStatus js, JobStatus lastJob) {
+ if (js.hasFlexibilityConstraint()) {
+ final long nowElapsed = sElapsedRealtimeClock.millis();
+ if (!mDeviceSupportsFlexConstraints) {
+ js.setFlexibilityConstraintSatisfied(nowElapsed, true);
+ return;
+ }
+ js.setFlexibilityConstraintSatisfied(nowElapsed, isFlexibilitySatisfiedLocked(js));
+ mFlexibilityTracker.add(js);
+ js.setTrackingController(JobStatus.TRACKING_FLEXIBILITY);
+ mFlexibilityAlarmQueue.scheduleDropNumConstraintsAlarm(js, nowElapsed);
+ }
+ }
+
+ @Override
+ @GuardedBy("mLock")
+ public void maybeStopTrackingJobLocked(JobStatus js, JobStatus incomingJob) {
+ if (js.clearTrackingController(JobStatus.TRACKING_FLEXIBILITY)) {
+ mFlexibilityAlarmQueue.removeAlarmForKey(js);
+ mFlexibilityTracker.remove(js);
+ }
+ }
+
+ @Override
+ @GuardedBy("mLock")
+ public void onAppRemovedLocked(String packageName, int uid) {
+ final int userId = UserHandle.getUserId(uid);
+ mPrefetchLifeCycleStart.delete(userId, packageName);
+ }
+
+ @Override
+ @GuardedBy("mLock")
+ public void onUserRemovedLocked(int userId) {
+ mPrefetchLifeCycleStart.delete(userId);
+ }
+
+ /** Checks if the flexibility constraint is actively satisfied for a given job. */
+ @GuardedBy("mLock")
+ boolean isFlexibilitySatisfiedLocked(JobStatus js) {
+ return !mFlexibilityEnabled
+ || mService.getUidBias(js.getSourceUid()) == JobInfo.BIAS_TOP_APP
+ || getNumSatisfiedFlexibleConstraintsLocked(js)
+ >= js.getNumRequiredFlexibleConstraints()
+ || mService.isCurrentlyRunningLocked(js);
+ }
+
+ @VisibleForTesting
+ @GuardedBy("mLock")
+ int getNumSatisfiedFlexibleConstraintsLocked(JobStatus js) {
+ return Integer.bitCount(mSatisfiedFlexibleConstraints & js.getPreferredConstraintFlags())
+ // Connectivity is job-specific, so must be handled separately.
+ + (js.getHasAccessToUnmetered() ? 1 : 0);
+ }
+
+ /**
+ * Sets the controller's constraint to a given state.
+ * Changes flexibility constraint satisfaction for affected jobs.
+ */
+ @VisibleForTesting
+ void setConstraintSatisfied(int constraint, boolean state, long nowElapsed) {
+ synchronized (mLock) {
+ final boolean old = (mSatisfiedFlexibleConstraints & constraint) != 0;
+ if (old == state) {
+ return;
+ }
+
+ if (DEBUG) {
+ Slog.d(TAG, "setConstraintSatisfied: "
+ + " constraint: " + constraint + " state: " + state);
+ }
+
+ mSatisfiedFlexibleConstraints =
+ (mSatisfiedFlexibleConstraints & ~constraint) | (state ? constraint : 0);
+ // 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();
+ }
+ }
+
+ /** Checks if the given constraint is satisfied in the flexibility controller. */
+ @VisibleForTesting
+ boolean isConstraintSatisfied(int constraint) {
+ return (mSatisfiedFlexibleConstraints & constraint) != 0;
+ }
+
+ @VisibleForTesting
+ @GuardedBy("mLock")
+ long getLifeCycleBeginningElapsedLocked(JobStatus js) {
+ if (js.getJob().isPrefetch()) {
+ final long earliestRuntime = Math.max(js.enqueueTime, js.getEarliestRunTime());
+ final long estimatedLaunchTime =
+ mPrefetchController.getNextEstimatedLaunchTimeLocked(js);
+ long prefetchWindowStart = mPrefetchLifeCycleStart.getOrDefault(
+ js.getSourceUserId(), js.getSourcePackageName(), 0L);
+ if (estimatedLaunchTime != Long.MAX_VALUE) {
+ prefetchWindowStart = Math.max(prefetchWindowStart,
+ estimatedLaunchTime - mPrefetchController.getLaunchTimeThresholdMs());
+ }
+ return Math.max(prefetchWindowStart, earliestRuntime);
+ }
+ return js.getEarliestRunTime() == JobStatus.NO_EARLIEST_RUNTIME
+ ? js.enqueueTime : js.getEarliestRunTime();
+ }
+
+ @VisibleForTesting
+ @GuardedBy("mLock")
+ long getLifeCycleEndElapsedLocked(JobStatus js, long earliest) {
+ if (js.getJob().isPrefetch()) {
+ final long estimatedLaunchTime =
+ mPrefetchController.getNextEstimatedLaunchTimeLocked(js);
+ // Prefetch jobs aren't supposed to have deadlines after T.
+ // But some legacy apps might still schedule them with deadlines.
+ if (js.getLatestRunTimeElapsed() != JobStatus.NO_LATEST_RUNTIME) {
+ // If there is a deadline, the earliest time is the end of the lifecycle.
+ return Math.min(
+ estimatedLaunchTime - mConstants.PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS,
+ js.getLatestRunTimeElapsed());
+ }
+ if (estimatedLaunchTime != Long.MAX_VALUE) {
+ return estimatedLaunchTime - mConstants.PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS;
+ }
+ // There is no deadline and no estimated launch time.
+ return NO_LIFECYCLE_END;
+ }
+ // Increase the flex deadline for jobs rescheduled more than once.
+ if (js.getNumPreviousAttempts() > 1) {
+ return earliest + Math.min(
+ (long) Math.scalb(mRescheduledJobDeadline, js.getNumPreviousAttempts() - 2),
+ mMaxRescheduledDeadline);
+ }
+ return js.getLatestRunTimeElapsed() == JobStatus.NO_LATEST_RUNTIME
+ ? earliest + mFallbackFlexibilityDeadlineMs : js.getLatestRunTimeElapsed();
+ }
+
+ @VisibleForTesting
+ @GuardedBy("mLock")
+ int getCurPercentOfLifecycleLocked(JobStatus js, long nowElapsed) {
+ final long earliest = getLifeCycleBeginningElapsedLocked(js);
+ final long latest = getLifeCycleEndElapsedLocked(js, earliest);
+ if (latest == NO_LIFECYCLE_END || earliest >= nowElapsed) {
+ return 0;
+ }
+ if (nowElapsed > latest || latest == earliest) {
+ return 100;
+ }
+ final int percentInTime = (int) ((nowElapsed - earliest) * 100 / (latest - earliest));
+ return percentInTime;
+ }
+
+ @VisibleForTesting
+ @ElapsedRealtimeLong
+ @GuardedBy("mLock")
+ long getNextConstraintDropTimeElapsedLocked(JobStatus js) {
+ final long earliest = getLifeCycleBeginningElapsedLocked(js);
+ final long latest = getLifeCycleEndElapsedLocked(js, earliest);
+ return getNextConstraintDropTimeElapsedLocked(js, earliest, latest);
+ }
+
+ /** The elapsed time that marks when the next constraint should be dropped. */
+ @ElapsedRealtimeLong
+ @GuardedBy("mLock")
+ long getNextConstraintDropTimeElapsedLocked(JobStatus js, long earliest, long latest) {
+ if (latest == NO_LIFECYCLE_END
+ || js.getNumDroppedFlexibleConstraints() == mPercentToDropConstraints.length) {
+ return NO_LIFECYCLE_END;
+ }
+ final int percent = mPercentToDropConstraints[js.getNumDroppedFlexibleConstraints()];
+ final long percentInTime = ((latest - earliest) * percent) / 100;
+ return earliest + percentInTime;
+ }
+
+ @Override
+ @GuardedBy("mLock")
+ public void onUidBiasChangedLocked(int uid, int prevBias, int newBias) {
+ if (prevBias != JobInfo.BIAS_TOP_APP && newBias != JobInfo.BIAS_TOP_APP) {
+ return;
+ }
+ final long nowElapsed = sElapsedRealtimeClock.millis();
+ ArraySet<JobStatus> jobsByUid = mService.getJobStore().getJobsBySourceUid(uid);
+ boolean hasPrefetch = false;
+ for (int i = 0; i < jobsByUid.size(); i++) {
+ JobStatus js = jobsByUid.valueAt(i);
+ if (js.hasFlexibilityConstraint()) {
+ js.setFlexibilityConstraintSatisfied(nowElapsed, isFlexibilitySatisfiedLocked(js));
+ hasPrefetch |= js.getJob().isPrefetch();
+ }
+ }
+
+ // Prefetch jobs can't run when the app is TOP, so it should not be included in their
+ // lifecycle, and marks the beginning of a new lifecycle.
+ if (hasPrefetch && prevBias == JobInfo.BIAS_TOP_APP) {
+ final int userId = UserHandle.getUserId(uid);
+ final ArraySet<String> pkgs = mService.getPackagesForUidLocked(uid);
+ if (pkgs == null) {
+ return;
+ }
+ for (int i = 0; i < pkgs.size(); i++) {
+ String pkg = pkgs.valueAt(i);
+ mPrefetchLifeCycleStart.add(userId, pkg,
+ Math.max(mPrefetchLifeCycleStart.getOrDefault(userId, pkg, 0L),
+ nowElapsed));
+ }
+ }
+ }
+
+ @Override
+ @GuardedBy("mLock")
+ public void onConstantsUpdatedLocked() {
+ if (mFcConfig.mShouldReevaluateConstraints) {
+ AppSchedulingModuleThread.getHandler().post(() -> {
+ final ArraySet<JobStatus> changedJobs = new ArraySet<>();
+ synchronized (mLock) {
+ final long nowElapsed = sElapsedRealtimeClock.millis();
+ for (int j = 0; j < mFlexibilityTracker.size(); j++) {
+ final ArraySet<JobStatus> jobs = mFlexibilityTracker
+ .getJobsByNumRequiredConstraints(j);
+ for (int i = 0; i < jobs.size(); i++) {
+ JobStatus js = jobs.valueAt(i);
+ mFlexibilityTracker.resetJobNumDroppedConstraints(js, nowElapsed);
+ mFlexibilityAlarmQueue.scheduleDropNumConstraintsAlarm(js, nowElapsed);
+ if (js.setFlexibilityConstraintSatisfied(
+ nowElapsed, isFlexibilitySatisfiedLocked(js))) {
+ changedJobs.add(js);
+ }
+ }
+ }
+ }
+ if (changedJobs.size() > 0) {
+ mStateChangedListener.onControllerStateChanged(changedJobs);
+ }
+ });
+ }
+ }
+
+ @Override
+ @GuardedBy("mLock")
+ public void prepareForUpdatedConstantsLocked() {
+ mFcConfig.mShouldReevaluateConstraints = false;
+ }
+
+ @Override
+ @GuardedBy("mLock")
+ public void processConstantLocked(DeviceConfig.Properties properties, String key) {
+ mFcConfig.processConstantLocked(properties, key);
+ }
+
+ @VisibleForTesting
+ class FlexibilityTracker {
+ final ArrayList<ArraySet<JobStatus>> mTrackedJobs;
+
+ FlexibilityTracker(int numFlexibleConstraints) {
+ mTrackedJobs = new ArrayList<>();
+ for (int i = 0; i <= numFlexibleConstraints; i++) {
+ mTrackedJobs.add(new ArraySet<JobStatus>());
+ }
+ }
+
+ /** Gets every tracked job with a given number of required constraints. */
+ @Nullable
+ public ArraySet<JobStatus> getJobsByNumRequiredConstraints(int numRequired) {
+ if (numRequired > mTrackedJobs.size()) {
+ Slog.wtfStack(TAG, "Asked for a larger number of constraints than exists.");
+ return null;
+ }
+ return mTrackedJobs.get(numRequired);
+ }
+
+ /** adds a JobStatus object based on number of required flexible constraints. */
+ public void add(JobStatus js) {
+ if (js.getNumRequiredFlexibleConstraints() < 0) {
+ return;
+ }
+ mTrackedJobs.get(js.getNumRequiredFlexibleConstraints()).add(js);
+ }
+
+ /** Removes a JobStatus object. */
+ public void remove(JobStatus js) {
+ mTrackedJobs.get(js.getNumRequiredFlexibleConstraints()).remove(js);
+ }
+
+ public void resetJobNumDroppedConstraints(JobStatus js, long nowElapsed) {
+ final int curPercent = getCurPercentOfLifecycleLocked(js, nowElapsed);
+ int toDrop = 0;
+ final int jsMaxFlexibleConstraints = NUM_SYSTEM_WIDE_FLEXIBLE_CONSTRAINTS
+ + (js.getPreferUnmetered() ? 1 : 0);
+ for (int i = 0; i < jsMaxFlexibleConstraints; i++) {
+ if (curPercent >= mPercentToDropConstraints[i]) {
+ toDrop++;
+ }
+ }
+ adjustJobsRequiredConstraints(
+ js, js.getNumDroppedFlexibleConstraints() - toDrop, nowElapsed);
+ }
+
+ /** Returns all tracked jobs. */
+ public ArrayList<ArraySet<JobStatus>> getArrayList() {
+ return mTrackedJobs;
+ }
+
+ /**
+ * Adjusts number of required flexible constraints and sorts it into the tracker.
+ * Returns false if the job status's number of flexible constraints is now 0.
+ */
+ public boolean adjustJobsRequiredConstraints(JobStatus js, int adjustBy, long nowElapsed) {
+ if (adjustBy != 0) {
+ remove(js);
+ js.adjustNumRequiredFlexibleConstraints(adjustBy);
+ js.setFlexibilityConstraintSatisfied(nowElapsed, isFlexibilitySatisfiedLocked(js));
+ add(js);
+ }
+ return js.getNumRequiredFlexibleConstraints() > 0;
+ }
+
+ public int size() {
+ return mTrackedJobs.size();
+ }
+
+ public void dump(IndentingPrintWriter pw, Predicate<JobStatus> predicate) {
+ for (int i = 0; i < mTrackedJobs.size(); i++) {
+ ArraySet<JobStatus> jobs = mTrackedJobs.get(i);
+ for (int j = 0; j < jobs.size(); j++) {
+ final JobStatus js = jobs.valueAt(j);
+ if (!predicate.test(js)) {
+ continue;
+ }
+ pw.print("#");
+ js.printUniqueId(pw);
+ pw.print(" from ");
+ UserHandle.formatUid(pw, js.getSourceUid());
+ pw.print(" Num Required Constraints: ");
+ pw.print(js.getNumRequiredFlexibleConstraints());
+ pw.println();
+ }
+ }
+ }
+ }
+
+ @VisibleForTesting
+ class FlexibilityAlarmQueue extends AlarmQueue<JobStatus> {
+ private FlexibilityAlarmQueue(Context context, Looper looper) {
+ super(context, looper, "*job.flexibility_check*",
+ "Flexible Constraint Check", true,
+ mMinTimeBetweenFlexibilityAlarmsMs);
+ }
+
+ @Override
+ protected boolean isForUser(@NonNull JobStatus js, int userId) {
+ return js.getSourceUserId() == userId;
+ }
+
+ public void scheduleDropNumConstraintsAlarm(JobStatus js, long nowElapsed) {
+ synchronized (mLock) {
+ final long earliest = getLifeCycleBeginningElapsedLocked(js);
+ final long latest = getLifeCycleEndElapsedLocked(js, earliest);
+ final long nextTimeElapsed =
+ getNextConstraintDropTimeElapsedLocked(js, earliest, latest);
+
+ if (DEBUG) {
+ Slog.d(TAG, "scheduleDropNumConstraintsAlarm: "
+ + js.getSourcePackageName() + " " + js.getSourceUserId()
+ + " numRequired: " + js.getNumRequiredFlexibleConstraints()
+ + " numSatisfied: " + Integer.bitCount(mSatisfiedFlexibleConstraints)
+ + " curTime: " + nowElapsed
+ + " earliest: " + earliest
+ + " latest: " + latest
+ + " nextTime: " + nextTimeElapsed);
+ }
+ if (latest - nowElapsed < mDeadlineProximityLimitMs) {
+ if (DEBUG) {
+ Slog.d(TAG, "deadline proximity met: " + js);
+ }
+ mFlexibilityTracker.adjustJobsRequiredConstraints(js,
+ -js.getNumRequiredFlexibleConstraints(), nowElapsed);
+ return;
+ }
+ if (nextTimeElapsed == NO_LIFECYCLE_END) {
+ // There is no known or estimated next time to drop a constraint.
+ removeAlarmForKey(js);
+ return;
+ }
+ if (latest - nextTimeElapsed <= mDeadlineProximityLimitMs) {
+ if (DEBUG) {
+ Slog.d(TAG, "last alarm set: " + js);
+ }
+ addAlarm(js, latest - mDeadlineProximityLimitMs);
+ return;
+ }
+ addAlarm(js, nextTimeElapsed);
+ }
+ }
+
+ @Override
+ protected void processExpiredAlarms(@NonNull ArraySet<JobStatus> expired) {
+ synchronized (mLock) {
+ ArraySet<JobStatus> changedJobs = new ArraySet<>();
+ final long nowElapsed = sElapsedRealtimeClock.millis();
+ for (int i = 0; i < expired.size(); i++) {
+ JobStatus js = expired.valueAt(i);
+ boolean wasFlexibilitySatisfied = js.isConstraintSatisfied(CONSTRAINT_FLEXIBLE);
+
+ if (mFlexibilityTracker.adjustJobsRequiredConstraints(js, -1, nowElapsed)) {
+ scheduleDropNumConstraintsAlarm(js, nowElapsed);
+ }
+ if (wasFlexibilitySatisfied != js.isConstraintSatisfied(CONSTRAINT_FLEXIBLE)) {
+ changedJobs.add(js);
+ }
+ }
+ mStateChangedListener.onControllerStateChanged(changedJobs);
+ }
+ }
+ }
+
+ private class FcHandler extends Handler {
+ FcHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_UPDATE_JOBS:
+ removeMessages(MSG_UPDATE_JOBS);
+
+ synchronized (mLock) {
+ final long nowElapsed = sElapsedRealtimeClock.millis();
+ final ArraySet<JobStatus> changedJobs = new ArraySet<>();
+
+ for (int o = 0; o <= NUM_OPTIONAL_FLEXIBLE_CONSTRAINTS; ++o) {
+ final ArraySet<JobStatus> jobsByNumConstraints = mFlexibilityTracker
+ .getJobsByNumRequiredConstraints(o);
+
+ if (jobsByNumConstraints != null) {
+ for (int i = 0; i < jobsByNumConstraints.size(); i++) {
+ final JobStatus js = jobsByNumConstraints.valueAt(i);
+ if (js.setFlexibilityConstraintSatisfied(
+ nowElapsed, isFlexibilitySatisfiedLocked(js))) {
+ changedJobs.add(js);
+ }
+ }
+ }
+ }
+ if (changedJobs.size() > 0) {
+ mStateChangedListener.onControllerStateChanged(changedJobs);
+ }
+ }
+ break;
+ }
+ }
+ }
+
+ @VisibleForTesting
+ class FcConfig {
+ private boolean mShouldReevaluateConstraints = false;
+
+ /** Prefix to use with all constant keys in order to "sub-namespace" the keys. */
+ private static final String FC_CONFIG_PREFIX = "fc_";
+
+ static final String KEY_FLEXIBILITY_ENABLED = FC_CONFIG_PREFIX + "enable_flexibility";
+ static final String KEY_DEADLINE_PROXIMITY_LIMIT =
+ 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_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_MAX_RESCHEDULED_DEADLINE_MS =
+ FC_CONFIG_PREFIX + "max_rescheduled_deadline_ms";
+ static final String KEY_RESCHEDULED_JOB_DEADLINE_MS =
+ FC_CONFIG_PREFIX + "rescheduled_job_deadline_ms";
+
+ private static final boolean DEFAULT_FLEXIBILITY_ENABLED = false;
+ @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;
+ @VisibleForTesting
+ final int[] DEFAULT_PERCENT_TO_DROP_FLEXIBLE_CONSTRAINTS = {50, 60, 70, 80};
+ 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;
+
+ /**
+ * If false the controller will not track new jobs
+ * and the flexibility constraint will always be satisfied.
+ */
+ public boolean FLEXIBILITY_ENABLED = DEFAULT_FLEXIBILITY_ENABLED;
+ /** How close to a jobs' deadline all flexible constraints will be dropped. */
+ public long DEADLINE_PROXIMITY_LIMIT_MS = DEFAULT_DEADLINE_PROXIMITY_LIMIT_MS;
+ /** For jobs that lack a deadline, the time that will be used to drop all constraints by. */
+ 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;
+ /** Initial fallback flexible deadline for rescheduled jobs. */
+ public long RESCHEDULED_JOB_DEADLINE_MS = DEFAULT_RESCHEDULED_JOB_DEADLINE_MS;
+ /** The max deadline for rescheduled jobs. */
+ public long MAX_RESCHEDULED_DEADLINE_MS = DEFAULT_MAX_RESCHEDULED_DEADLINE_MS;
+
+ @GuardedBy("mLock")
+ public void processConstantLocked(@NonNull DeviceConfig.Properties properties,
+ @NonNull String key) {
+ switch (key) {
+ case KEY_FLEXIBILITY_ENABLED:
+ FLEXIBILITY_ENABLED = properties.getBoolean(key, DEFAULT_FLEXIBILITY_ENABLED)
+ && mDeviceSupportsFlexConstraints;
+ if (mFlexibilityEnabled != FLEXIBILITY_ENABLED) {
+ mFlexibilityEnabled = FLEXIBILITY_ENABLED;
+ mShouldReevaluateConstraints = true;
+ if (mFlexibilityEnabled) {
+ mPrefetchController
+ .registerPrefetchChangedListener(mPrefetchChangedListener);
+ } else {
+ mPrefetchController
+ .unRegisterPrefetchChangedListener(mPrefetchChangedListener);
+ }
+ }
+ break;
+ case KEY_RESCHEDULED_JOB_DEADLINE_MS:
+ RESCHEDULED_JOB_DEADLINE_MS =
+ properties.getLong(key, DEFAULT_RESCHEDULED_JOB_DEADLINE_MS);
+ if (mRescheduledJobDeadline != RESCHEDULED_JOB_DEADLINE_MS) {
+ mRescheduledJobDeadline = RESCHEDULED_JOB_DEADLINE_MS;
+ mShouldReevaluateConstraints = true;
+ }
+ break;
+ case KEY_MAX_RESCHEDULED_DEADLINE_MS:
+ MAX_RESCHEDULED_DEADLINE_MS =
+ properties.getLong(key, DEFAULT_MAX_RESCHEDULED_DEADLINE_MS);
+ if (mMaxRescheduledDeadline != MAX_RESCHEDULED_DEADLINE_MS) {
+ mMaxRescheduledDeadline = MAX_RESCHEDULED_DEADLINE_MS;
+ mShouldReevaluateConstraints = true;
+ }
+ break;
+ case KEY_DEADLINE_PROXIMITY_LIMIT:
+ DEADLINE_PROXIMITY_LIMIT_MS =
+ properties.getLong(key, DEFAULT_DEADLINE_PROXIMITY_LIMIT_MS);
+ if (mDeadlineProximityLimitMs != DEADLINE_PROXIMITY_LIMIT_MS) {
+ mDeadlineProximityLimitMs = DEADLINE_PROXIMITY_LIMIT_MS;
+ mShouldReevaluateConstraints = true;
+ }
+ break;
+ case KEY_FALLBACK_FLEXIBILITY_DEADLINE:
+ FALLBACK_FLEXIBILITY_DEADLINE_MS =
+ properties.getLong(key, DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS);
+ if (mFallbackFlexibilityDeadlineMs != FALLBACK_FLEXIBILITY_DEADLINE_MS) {
+ mFallbackFlexibilityDeadlineMs = FALLBACK_FLEXIBILITY_DEADLINE_MS;
+ mShouldReevaluateConstraints = true;
+ }
+ break;
+ case KEY_MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS:
+ MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS =
+ properties.getLong(key, DEFAULT_MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS);
+ if (mMinTimeBetweenFlexibilityAlarmsMs
+ != MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS) {
+ mMinTimeBetweenFlexibilityAlarmsMs = MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS;
+ mFlexibilityAlarmQueue
+ .setMinTimeBetweenAlarmsMs(MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS);
+ 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;
+ mShouldReevaluateConstraints = true;
+ }
+ break;
+ }
+ }
+
+ private int[] parsePercentToDropString(String s) {
+ String[] dropPercentString = s.split(",");
+ int[] dropPercentInt = new int[NUM_FLEXIBLE_CONSTRAINTS];
+ if (dropPercentInt.length != dropPercentString.length) {
+ return DEFAULT_PERCENT_TO_DROP_FLEXIBLE_CONSTRAINTS;
+ }
+ int prevPercent = 0;
+ for (int i = 0; i < dropPercentString.length; i++) {
+ try {
+ dropPercentInt[i] =
+ Integer.parseInt(dropPercentString[i]);
+ } catch (NumberFormatException ex) {
+ Slog.e(TAG, "Provided string was improperly formatted.", ex);
+ return DEFAULT_PERCENT_TO_DROP_FLEXIBLE_CONSTRAINTS;
+ }
+ if (dropPercentInt[i] < prevPercent) {
+ Slog.wtf(TAG, "Percents to drop constraints were not in increasing order.");
+ return DEFAULT_PERCENT_TO_DROP_FLEXIBLE_CONSTRAINTS;
+ }
+ prevPercent = dropPercentInt[i];
+ }
+
+ return dropPercentInt;
+ }
+
+ private void dump(IndentingPrintWriter pw) {
+ pw.println();
+ pw.print(FlexibilityController.class.getSimpleName());
+ pw.println(":");
+ pw.increaseIndent();
+
+ pw.print(KEY_FLEXIBILITY_ENABLED, FLEXIBILITY_ENABLED).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_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_RESCHEDULED_JOB_DEADLINE_MS, RESCHEDULED_JOB_DEADLINE_MS).println();
+ pw.print(KEY_MAX_RESCHEDULED_DEADLINE_MS, MAX_RESCHEDULED_DEADLINE_MS).println();
+
+ pw.decreaseIndent();
+ }
+ }
+
+ @VisibleForTesting
+ @NonNull
+ FcConfig getFcConfig() {
+ return mFcConfig;
+ }
+
+ @Override
+ @GuardedBy("mLock")
+ public void dumpConstants(IndentingPrintWriter pw) {
+ mFcConfig.dump(pw);
+ }
+
+ @Override
+ @GuardedBy("mLock")
+ public void dumpControllerStateLocked(IndentingPrintWriter pw, Predicate<JobStatus> predicate) {
+ pw.println("# Constraints Satisfied: " + Integer.bitCount(mSatisfiedFlexibleConstraints));
+ pw.print("Satisfied Flexible Constraints: ");
+ JobStatus.dumpConstraints(pw, mSatisfiedFlexibleConstraints);
+ pw.println();
+ pw.println();
+
+ mFlexibilityTracker.dump(pw, predicate);
+ pw.println();
+ mFlexibilityAlarmQueue.dump(pw);
+ }
+}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java
index 8311dc38f87d..a25af7110ee5 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java
@@ -48,10 +48,13 @@ public final class IdleController extends RestrictingController implements Idlen
// screen off or dreaming or wireless charging dock idle for at least this long
final ArraySet<JobStatus> mTrackedTasks = new ArraySet<>();
IdlenessTracker mIdleTracker;
+ private final FlexibilityController mFlexibilityController;
- public IdleController(JobSchedulerService service) {
+ public IdleController(JobSchedulerService service,
+ FlexibilityController flexibilityController) {
super(service);
initIdleStateTracking(mContext);
+ mFlexibilityController = flexibilityController;
}
/**
@@ -73,8 +76,7 @@ public final class IdleController extends RestrictingController implements Idlen
}
@Override
- public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob,
- boolean forUpdate) {
+ public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob) {
if (taskStatus.clearTrackingController(JobStatus.TRACKING_IDLE)) {
mTrackedTasks.remove(taskStatus);
}
@@ -83,7 +85,7 @@ public final class IdleController extends RestrictingController implements Idlen
@Override
public void stopTrackingRestrictedJobLocked(JobStatus jobStatus) {
if (!jobStatus.hasIdleConstraint()) {
- maybeStopTrackingJobLocked(jobStatus, null, false);
+ maybeStopTrackingJobLocked(jobStatus, null);
}
}
@@ -96,6 +98,8 @@ public final class IdleController extends RestrictingController implements Idlen
logDeviceWideConstraintStateToStatsd(JobStatus.CONSTRAINT_IDLE, isIdle);
final long nowElapsed = sElapsedRealtimeClock.millis();
+ mFlexibilityController.setConstraintSatisfied(
+ JobStatus.CONSTRAINT_IDLE, isIdle, nowElapsed);
for (int i = mTrackedTasks.size()-1; i >= 0; i--) {
mTrackedTasks.valueAt(i).setIdleConstraintSatisfied(nowElapsed, isIdle);
}
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 0d85dfd3b951..13903acc0439 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,6 +16,8 @@
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;
@@ -24,13 +26,16 @@ import static com.android.server.job.JobSchedulerService.WORKING_INDEX;
import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
import android.annotation.ElapsedRealtimeLong;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.app.AppGlobals;
import android.app.job.JobInfo;
import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
import android.app.job.JobWorkItem;
+import android.app.job.UserVisibleJobSummary;
import android.content.ClipData;
import android.content.ComponentName;
-import android.content.pm.ServiceInfo;
import android.net.Network;
import android.net.NetworkRequest;
import android.net.Uri;
@@ -38,6 +43,7 @@ import android.os.RemoteException;
import android.os.UserHandle;
import android.provider.MediaStore;
import android.text.format.DateFormat;
+import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.IndentingPrintWriter;
import android.util.Pair;
@@ -46,6 +52,8 @@ import android.util.Slog;
import android.util.TimeUtils;
import android.util.proto.ProtoOutputStream;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.FrameworkStatsLog;
import com.android.server.LocalServices;
@@ -59,9 +67,12 @@ import com.android.server.job.JobStatusShortInfoProto;
import dalvik.annotation.optimization.NeverCompile;
import java.io.PrintWriter;
+import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
+import java.util.Objects;
+import java.util.Random;
import java.util.function.Predicate;
/**
@@ -81,24 +92,44 @@ public final class JobStatus {
private static final String TAG = "JobScheduler.JobStatus";
static final boolean DEBUG = JobSchedulerService.DEBUG;
+ private static MessageDigest sMessageDigest;
+ /** Cache of namespace to hash to reduce how often we need to generate the namespace hash. */
+ @GuardedBy("sNamespaceHashCache")
+ private static final ArrayMap<String, String> sNamespaceHashCache = new ArrayMap<>();
+ /** Maximum size of {@link #sNamespaceHashCache}. */
+ private static final int MAX_NAMESPACE_CACHE_SIZE = 128;
+
private static final int NUM_CONSTRAINT_CHANGE_HISTORY = 10;
public static final long NO_LATEST_RUNTIME = Long.MAX_VALUE;
public static final long NO_EARLIEST_RUNTIME = 0L;
- static final int CONSTRAINT_CHARGING = JobInfo.CONSTRAINT_FLAG_CHARGING; // 1 < 0
- static final int CONSTRAINT_IDLE = JobInfo.CONSTRAINT_FLAG_DEVICE_IDLE; // 1 << 2
- static final int CONSTRAINT_BATTERY_NOT_LOW = JobInfo.CONSTRAINT_FLAG_BATTERY_NOT_LOW; // 1 << 1
- static final int CONSTRAINT_STORAGE_NOT_LOW = JobInfo.CONSTRAINT_FLAG_STORAGE_NOT_LOW; // 1 << 3
- static final int CONSTRAINT_TIMING_DELAY = 1<<31;
- static final int CONSTRAINT_DEADLINE = 1<<30;
- static final int CONSTRAINT_CONNECTIVITY = 1 << 28;
+ @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)
+ public static final int CONSTRAINT_STORAGE_NOT_LOW =
+ JobInfo.CONSTRAINT_FLAG_STORAGE_NOT_LOW; // 1 << 3
+ public static final int CONSTRAINT_TIMING_DELAY = 1 << 31;
+ public static final int CONSTRAINT_DEADLINE = 1 << 30;
+ public static final int CONSTRAINT_CONNECTIVITY = 1 << 28;
static final int CONSTRAINT_TARE_WEALTH = 1 << 27; // Implicit constraint
- static final int CONSTRAINT_CONTENT_TRIGGER = 1<<26;
+ public static final int CONSTRAINT_CONTENT_TRIGGER = 1 << 26;
static final int CONSTRAINT_DEVICE_NOT_DOZING = 1 << 25; // Implicit constraint
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;
+
+ private static final int IMPLICIT_CONSTRAINTS = 0
+ | CONSTRAINT_BACKGROUND_NOT_RESTRICTED
+ | CONSTRAINT_DEVICE_NOT_DOZING
+ | CONSTRAINT_TARE_WEALTH
+ | CONSTRAINT_WITHIN_QUOTA;
// The following set of dynamic constraints are for specific use cases (as explained in their
// relative naming and comments). Right now, they apply different constraints, which is fine,
@@ -118,6 +149,19 @@ public final class JobStatus {
| CONSTRAINT_IDLE;
/**
+ * Keeps track of how many flexible constraints must be satisfied for the job to execute.
+ */
+ private final int mNumRequiredFlexibleConstraints;
+
+ /**
+ * Number of required flexible constraints that have been dropped.
+ */
+ private int mNumDroppedFlexibleConstraints;
+
+ /** If the job is going to be passed an unmetered network. */
+ private boolean mHasAccessToUnmetered;
+
+ /**
* The additional set of dynamic constraints that must be met if this is an expedited job that
* had a long enough run while the device was Dozing or in battery saver.
*/
@@ -196,6 +240,12 @@ public final class JobStatus {
final int sourceUserId;
final int sourceUid;
final String sourceTag;
+ @Nullable
+ private final String mNamespace;
+ @Nullable
+ private final String mNamespaceHash;
+ /** An ID that can be used to uniquely identify the job when logging statsd metrics. */
+ private final long mLoggingJobId;
final String tag;
@@ -222,10 +272,22 @@ public final class JobStatus {
*/
private long mOriginalLatestRunTimeElapsedMillis;
- /** How many times this job has failed, used to compute back-off. */
+ /**
+ * How many times this job has failed to complete on its own
+ * (via {@link android.app.job.JobService#jobFinished(JobParameters, boolean)} or because of
+ * a timeout).
+ * This count doesn't include most times JobScheduler decided to stop the job
+ * (via {@link android.app.job.JobService#onStopJob(JobParameters)}.
+ */
private final int numFailures;
/**
+ * The number of times JobScheduler has forced this job to stop due to reasons mostly outside
+ * of the app's control.
+ */
+ private final int mNumSystemStops;
+
+ /**
* Which app standby bucket this job's app is in. Updated when the app is moved to a
* different bucket.
*/
@@ -247,6 +309,7 @@ public final class JobStatus {
// Constraints.
final int requiredConstraints;
+ private final int mPreferredConstraints;
private final int mRequiredConstraintsOfInterest;
int satisfiedConstraints = 0;
private int mSatisfiedConstraintsOfInterest = 0;
@@ -304,6 +367,12 @@ public final class JobStatus {
public static final int TRACKING_QUOTA = 1 << 6;
/**
+ * Flag for {@link #trackingControllers}: the flexibility controller is currently tracking this
+ * job.
+ */
+ public static final int TRACKING_FLEXIBILITY = 1 << 7;
+
+ /**
* Bit mask of controllers that are currently tracking the job.
*/
private int trackingControllers;
@@ -316,18 +385,37 @@ public final class JobStatus {
* @hide
*/
public static final int INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION = 1 << 0;
+ /**
+ * Flag for {@link #mInternalFlags}: this job was stopped by the user for some reason
+ * and is thus considered demoted from whatever privileged state it had in the past.
+ */
+ public static final int INTERNAL_FLAG_DEMOTED_BY_USER = 1 << 1;
+ /**
+ * Flag for {@link #mInternalFlags}: this job is demoted by the system
+ * from running as a user-initiated job.
+ */
+ 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.
*/
private int mInternalFlags;
+ /**
+ * The cumulative amount of time this job has run for, including previous executions.
+ * This is reset for periodic jobs upon a successful job execution.
+ */
+ private long mCumulativeExecutionTimeMs;
+
// These are filled in by controllers when preparing for execution.
public ArraySet<Uri> changedUris;
public ArraySet<String> changedAuthorities;
public Network network;
- public ServiceInfo serviceInfo;
+ public String serviceProcessName;
/** The evaluated bias of the job when it started running. */
public int lastEvaluatedBias;
@@ -337,6 +425,20 @@ public final class JobStatus {
* running. This isn't copied over when a job is rescheduled.
*/
public boolean startedAsExpeditedJob = false;
+ /**
+ * Whether or not this particular JobStatus instance was treated as a user-initiated job
+ * when it started running. This isn't copied over when a job is rescheduled.
+ */
+ public boolean startedAsUserInitiatedJob = false;
+ /**
+ * Whether this particular JobStatus instance started with the foreground flag
+ * (or more accurately, did <b>not</b> have the
+ * {@link android.content.Context#BIND_NOT_FOREGROUND} flag
+ * included in its binding flags when started).
+ */
+ public boolean startedWithForegroundFlag = false;
+
+ public boolean startedWithImmediacyPrivilege = false;
// If non-null, this is work that has been enqueued for the job.
public ArrayList<JobWorkItem> pendingWork;
@@ -406,6 +508,12 @@ public final class JobStatus {
*/
private boolean mExpeditedTareApproved;
+ /**
+ * Summary describing this job. Lazily created in {@link #getUserVisibleJobSummary()}
+ * since not every job will need it.
+ */
+ private UserVisibleJobSummary mUserVisibleJobSummary;
+
/////// Booleans that track if a job is ready to run. They should be updated whenever dependent
/////// states change.
@@ -437,6 +545,12 @@ public final class JobStatus {
/** The job's dynamic requirements have been satisfied. */
private boolean mReadyDynamicSatisfied;
+ /**
+ * The job prefers an unmetered network if it has the connectivity constraint but is
+ * okay with any meteredness.
+ */
+ private final boolean mPreferUnmetered;
+
/** The reason a job most recently went from ready to not ready. */
private int mReasonReadyToUnready = JobParameters.STOP_REASON_UNDEFINED;
@@ -451,9 +565,12 @@ public final class JobStatus {
* @param standbyBucket The standby bucket that the source package is currently assigned to,
* cached here for speed of handling during runnability evaluations (and updated when bucket
* assignments are changed)
+ * @param namespace The custom namespace the app put this job in.
* @param tag A string associated with the job for debugging/logging purposes.
* @param numFailures Count of how many times this job has requested a reschedule because
* its work was not yet finished.
+ * @param numSystemStops Count of how many times JobScheduler has forced this job to stop due to
+ * factors mostly out of the app's control.
* @param earliestRunTimeElapsedMillis Milestone: earliest point in time at which the job
* is to be considered runnable
* @param latestRunTimeElapsedMillis Milestone: point in time at which the job will be
@@ -463,13 +580,18 @@ public final class JobStatus {
* @param internalFlags Non-API property flags about this job
*/
private JobStatus(JobInfo job, int callingUid, String sourcePackageName,
- int sourceUserId, int standbyBucket, String tag, int numFailures,
+ int sourceUserId, int standbyBucket, @Nullable String namespace, String tag,
+ int numFailures, int numSystemStops,
long earliestRunTimeElapsedMillis, long latestRunTimeElapsedMillis,
- long lastSuccessfulRunTime, long lastFailedRunTime, int internalFlags,
+ long lastSuccessfulRunTime, long lastFailedRunTime, long cumulativeExecutionTimeMs,
+ int internalFlags,
int dynamicConstraints) {
this.job = job;
this.callingUid = callingUid;
this.standbyBucket = standbyBucket;
+ mNamespace = namespace;
+ mNamespaceHash = generateNamespaceHash(namespace);
+ mLoggingJobId = generateLoggingId(namespace, job.getId());
int tempSourceUid = -1;
if (sourceUserId != -1 && sourcePackageName != null) {
@@ -492,15 +614,17 @@ public final class JobStatus {
this.sourceTag = tag;
}
+ final String bnNamespace = namespace == null ? "" : "@" + namespace + "@";
this.batteryName = this.sourceTag != null
- ? this.sourceTag + ":" + job.getService().getPackageName()
- : job.getService().flattenToShortString();
+ ? bnNamespace + this.sourceTag + ":" + job.getService().getPackageName()
+ : bnNamespace + job.getService().flattenToShortString();
this.tag = "*job*/" + this.batteryName + "#" + job.getId();
this.earliestRunTimeElapsedMillis = earliestRunTimeElapsedMillis;
this.latestRunTimeElapsedMillis = latestRunTimeElapsedMillis;
this.mOriginalLatestRunTimeElapsedMillis = latestRunTimeElapsedMillis;
this.numFailures = numFailures;
+ mNumSystemStops = numSystemStops;
int requiredConstraints = job.getConstraintFlags();
if (job.getRequiredNetwork() != null) {
@@ -527,6 +651,32 @@ public final class JobStatus {
}
}
mHasExemptedMediaUrisOnly = exemptedMediaUrisOnly;
+
+ mPreferredConstraints = job.getPreferredConstraintFlags();
+
+ // Exposing a preferredNetworkRequest API requires that we make sure that the preferred
+ // NetworkRequest is a subset of the required NetworkRequest. We currently don't have the
+ // code to ensure that, so disable this part for now.
+ // TODO(236261941): look into enabling flexible network constraint requests
+ mPreferUnmetered = false;
+ // && job.getRequiredNetwork() != null
+ // && !job.getRequiredNetwork().hasCapability(NET_CAPABILITY_NOT_METERED);
+
+ 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 (mPreferredConstraints != 0 && !isRequestedExpeditedJob() && !job.isUserInitiated()
+ && satisfiesMinWindowException
+ && (numFailures + numSystemStops) != 1) {
+ mNumRequiredFlexibleConstraints = Integer.bitCount(mPreferredConstraints);
+ requiredConstraints |= CONSTRAINT_FLEXIBLE;
+ } else {
+ mNumRequiredFlexibleConstraints = 0;
+ }
+
this.requiredConstraints = requiredConstraints;
mRequiredConstraintsOfInterest = requiredConstraints & CONSTRAINTS_OF_INTEREST;
addDynamicConstraints(dynamicConstraints);
@@ -537,6 +687,8 @@ public final class JobStatus {
mReadyDynamicSatisfied = false;
}
+ mCumulativeExecutionTimeMs = cumulativeExecutionTimeMs;
+
mLastSuccessfulRunTime = lastSuccessfulRunTime;
mLastFailedRunTime = lastFailedRunTime;
@@ -554,9 +706,9 @@ public final class JobStatus {
requestBuilder.setUids(
Collections.singleton(new Range<Integer>(this.sourceUid, this.sourceUid)));
builder.setRequiredNetwork(requestBuilder.build());
- // Don't perform prefetch-deadline check at this point. We've already passed the
+ // Don't perform validation checks at this point since we've already passed the
// initial validation check.
- job = builder.build(false);
+ job = builder.build(false, false);
}
updateMediaBackupExemptionStatus();
@@ -567,10 +719,11 @@ public final class JobStatus {
public JobStatus(JobStatus jobStatus) {
this(jobStatus.getJob(), jobStatus.getUid(),
jobStatus.getSourcePackageName(), jobStatus.getSourceUserId(),
- jobStatus.getStandbyBucket(),
- jobStatus.getSourceTag(), jobStatus.getNumFailures(),
+ jobStatus.getStandbyBucket(), jobStatus.getNamespace(),
+ jobStatus.getSourceTag(), jobStatus.getNumFailures(), jobStatus.getNumSystemStops(),
jobStatus.getEarliestRunTime(), jobStatus.getLatestRunTimeElapsed(),
jobStatus.getLastSuccessfulRunTime(), jobStatus.getLastFailedRunTime(),
+ jobStatus.getCumulativeExecutionTimeMs(),
jobStatus.getInternalFlags(), jobStatus.mDynamicConstraints);
mPersistedUtcTimes = jobStatus.mPersistedUtcTimes;
if (jobStatus.mPersistedUtcTimes != null) {
@@ -578,6 +731,12 @@ public final class JobStatus {
Slog.i(TAG, "Cloning job with persisted run times", new RuntimeException("here"));
}
}
+ if (jobStatus.executingWork != null && jobStatus.executingWork.size() > 0) {
+ executingWork = new ArrayList<>(jobStatus.executingWork);
+ }
+ if (jobStatus.pendingWork != null && jobStatus.pendingWork.size() > 0) {
+ pendingWork = new ArrayList<>(jobStatus.pendingWork);
+ }
}
/**
@@ -589,16 +748,18 @@ public final class JobStatus {
* standby bucket is whatever the OS thinks it should be at this moment.
*/
public JobStatus(JobInfo job, int callingUid, String sourcePkgName, int sourceUserId,
- int standbyBucket, String sourceTag,
+ int standbyBucket, @Nullable String namespace, String sourceTag,
long earliestRunTimeElapsedMillis, long latestRunTimeElapsedMillis,
long lastSuccessfulRunTime, long lastFailedRunTime,
+ long cumulativeExecutionTimeMs,
Pair<Long, Long> persistedExecutionTimesUTC,
int innerFlags, int dynamicConstraints) {
this(job, callingUid, sourcePkgName, sourceUserId,
- standbyBucket,
- sourceTag, 0,
+ standbyBucket, namespace,
+ sourceTag, /* numFailures */ 0, /* numSystemStops */ 0,
earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis,
- lastSuccessfulRunTime, lastFailedRunTime, innerFlags, dynamicConstraints);
+ lastSuccessfulRunTime, lastFailedRunTime, cumulativeExecutionTimeMs,
+ innerFlags, dynamicConstraints);
// Only during initial inflation do we record the UTC-timebase execution bounds
// read from the persistent store. If we ever have to recreate the JobStatus on
@@ -615,14 +776,17 @@ public final class JobStatus {
/** Create a new job to be rescheduled with the provided parameters. */
public JobStatus(JobStatus rescheduling,
long newEarliestRuntimeElapsedMillis,
- long newLatestRuntimeElapsedMillis, int backoffAttempt,
- long lastSuccessfulRunTime, long lastFailedRunTime) {
+ long newLatestRuntimeElapsedMillis, int numFailures, int numSystemStops,
+ long lastSuccessfulRunTime, long lastFailedRunTime,
+ long cumulativeExecutionTimeMs) {
this(rescheduling.job, rescheduling.getUid(),
rescheduling.getSourcePackageName(), rescheduling.getSourceUserId(),
- rescheduling.getStandbyBucket(),
- rescheduling.getSourceTag(), backoffAttempt, newEarliestRuntimeElapsedMillis,
+ rescheduling.getStandbyBucket(), rescheduling.getNamespace(),
+ rescheduling.getSourceTag(), numFailures, numSystemStops,
+ newEarliestRuntimeElapsedMillis,
newLatestRuntimeElapsedMillis,
- lastSuccessfulRunTime, lastFailedRunTime, rescheduling.getInternalFlags(),
+ lastSuccessfulRunTime, lastFailedRunTime, cumulativeExecutionTimeMs,
+ rescheduling.getInternalFlags(),
rescheduling.mDynamicConstraints);
}
@@ -635,7 +799,7 @@ public final class JobStatus {
* caller.
*/
public static JobStatus createFromJobInfo(JobInfo job, int callingUid, String sourcePkg,
- int sourceUserId, String tag) {
+ int sourceUserId, @Nullable String namespace, String tag) {
final long elapsedNow = sElapsedRealtimeClock.millis();
final long earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis;
if (job.isPeriodic()) {
@@ -657,12 +821,70 @@ public final class JobStatus {
int standbyBucket = JobSchedulerService.standbyBucketForPackage(jobPackage,
sourceUserId, elapsedNow);
return new JobStatus(job, callingUid, sourcePkg, sourceUserId,
- standbyBucket, tag, 0,
+ standbyBucket, namespace, tag, /* numFailures */ 0, /* numSystemStops */ 0,
earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis,
0 /* lastSuccessfulRunTime */, 0 /* lastFailedRunTime */,
+ /* cumulativeExecutionTime */ 0,
/*innerFlags=*/ 0, /* dynamicConstraints */ 0);
}
+ private long generateLoggingId(@Nullable String namespace, int jobId) {
+ if (namespace == null) {
+ return jobId;
+ }
+ return ((long) namespace.hashCode()) << 31 | jobId;
+ }
+
+ @Nullable
+ private static String generateNamespaceHash(@Nullable String namespace) {
+ if (namespace == null) {
+ return null;
+ }
+ if (namespace.trim().isEmpty()) {
+ // Input is composed of all spaces (or nothing at all).
+ return namespace;
+ }
+ synchronized (sNamespaceHashCache) {
+ final int idx = sNamespaceHashCache.indexOfKey(namespace);
+ if (idx >= 0) {
+ return sNamespaceHashCache.valueAt(idx);
+ }
+ }
+ String hash = null;
+ try {
+ // .hashCode() can result in conflicts that would make distinguishing between
+ // namespaces hard and reduce the accuracy of certain metrics. Use SHA-256
+ // to generate the hash since the probability of collision is extremely low.
+ if (sMessageDigest == null) {
+ sMessageDigest = MessageDigest.getInstance("SHA-256");
+ }
+ final byte[] digest = sMessageDigest.digest(namespace.getBytes());
+ // Convert to hexadecimal representation
+ StringBuilder hexBuilder = new StringBuilder(digest.length);
+ for (byte byteChar : digest) {
+ hexBuilder.append(String.format("%02X", byteChar));
+ }
+ hash = hexBuilder.toString();
+ } catch (Exception e) {
+ Slog.wtf(TAG, "Couldn't hash input", e);
+ }
+ if (hash == null) {
+ // If we get to this point, something went wrong with the MessageDigest above.
+ // Don't return the raw input value (which would defeat the purpose of hashing).
+ return "failed_namespace_hash";
+ }
+ hash = hash.intern();
+ synchronized (sNamespaceHashCache) {
+ if (sNamespaceHashCache.size() >= MAX_NAMESPACE_CACHE_SIZE) {
+ // Drop a random mapping instead of dropping at a predefined index to avoid
+ // potentially always dropping the same mapping.
+ sNamespaceHashCache.removeAt((new Random()).nextInt(MAX_NAMESPACE_CACHE_SIZE));
+ }
+ sNamespaceHashCache.put(namespace, hash);
+ }
+ return hash;
+ }
+
public void enqueueWorkLocked(JobWorkItem work) {
if (pendingWork == null) {
pendingWork = new ArrayList<>();
@@ -688,12 +910,18 @@ public final class JobStatus {
executingWork.add(work);
work.bumpDeliveryCount();
}
- updateNetworkBytesLocked();
return work;
}
return null;
}
+ /** Returns the number of {@link JobWorkItem JobWorkItems} attached to this job. */
+ public int getWorkCount() {
+ final int pendingCount = pendingWork == null ? 0 : pendingWork.size();
+ final int executingCount = executingWork == null ? 0 : executingWork.size();
+ return pendingCount + executingCount;
+ }
+
public boolean hasWorkLocked() {
return (pendingWork != null && pendingWork.size() > 0) || hasExecutingWorkLocked();
}
@@ -708,6 +936,10 @@ public final class JobStatus {
}
}
+ /**
+ * Returns {@code true} if the JobWorkItem queue was updated,
+ * and {@code false} if nothing changed.
+ */
public boolean completeWorkLocked(int workId) {
if (executingWork != null) {
final int N = executingWork.size();
@@ -716,6 +948,7 @@ public final class JobStatus {
if (work.getWorkId() == workId) {
executingWork.remove(i);
ungrantWorkItem(work);
+ updateNetworkBytesLocked();
return true;
}
}
@@ -804,16 +1037,44 @@ public final class JobStatus {
return job.getId();
}
+ /** Returns an ID that can be used to uniquely identify the job when logging statsd metrics. */
+ public long getLoggingJobId() {
+ return mLoggingJobId;
+ }
+
public void printUniqueId(PrintWriter pw) {
+ if (mNamespace != null) {
+ pw.print(mNamespace);
+ pw.print(":");
+ } else {
+ pw.print("#");
+ }
UserHandle.formatUid(pw, callingUid);
pw.print("/");
pw.print(job.getId());
}
+ /**
+ * Returns the number of times the job stopped previously for reasons that appeared to be within
+ * the app's control.
+ */
public int getNumFailures() {
return numFailures;
}
+ /**
+ * Returns the number of times the system stopped a previous execution of this job for reasons
+ * that were likely outside the app's control.
+ */
+ public int getNumSystemStops() {
+ return mNumSystemStops;
+ }
+
+ /** Returns the total number of times we've attempted to run this job in the past. */
+ public int getNumPreviousAttempts() {
+ return numFailures + mNumSystemStops;
+ }
+
public ComponentName getServiceComponent() {
return job.getService();
}
@@ -834,13 +1095,77 @@ public final class JobStatus {
return UserHandle.getUserId(callingUid);
}
+ private boolean shouldBlameSourceForTimeout() {
+ // If the system scheduled the job on behalf of an app, assume the app is the one
+ // doing the work and blame the app directly. This is the case with things like
+ // syncs via SyncManager.
+ // If the system didn't schedule the job on behalf of an app, then
+ // blame the app doing the actual work. Proxied jobs are a little tricky.
+ // Proxied jobs scheduled by built-in system apps like DownloadManager may be fine
+ // and we could consider exempting those jobs. For example, in DownloadManager's
+ // case, all it does is download files and the code is vetted. A timeout likely
+ // means it's downloading a large file, which isn't an error. For now, DownloadManager
+ // is an exempted app, so this shouldn't be an issue.
+ // However, proxied jobs coming from other system apps (such as those that can
+ // be updated separately from an OTA) may not be fine and we would want to apply
+ // this policy to those jobs/apps.
+ // TODO(284512488): consider exempting DownloadManager or other system apps
+ return UserHandle.isCore(callingUid);
+ }
+
+ /**
+ * Returns the package name that should most likely be blamed for the job timing out.
+ */
+ public String getTimeoutBlamePackageName() {
+ if (shouldBlameSourceForTimeout()) {
+ return sourcePackageName;
+ }
+ return getServiceComponent().getPackageName();
+ }
+
+ /**
+ * Returns the UID that should most likely be blamed for the job timing out.
+ */
+ public int getTimeoutBlameUid() {
+ if (shouldBlameSourceForTimeout()) {
+ return sourceUid;
+ }
+ return callingUid;
+ }
+
+ /**
+ * Returns the userId that should most likely be blamed for the job timing out.
+ */
+ public int getTimeoutBlameUserId() {
+ if (shouldBlameSourceForTimeout()) {
+ return sourceUserId;
+ }
+ return UserHandle.getUserId(callingUid);
+ }
+
/**
* Returns an appropriate standby bucket for the job, taking into account any standby
* exemptions.
*/
public int getEffectiveStandbyBucket() {
+ final JobSchedulerInternal jsi = LocalServices.getService(JobSchedulerInternal.class);
+ final boolean isBuggy = jsi.isAppConsideredBuggy(
+ getUserId(), getServiceComponent().getPackageName(),
+ getTimeoutBlameUserId(), getTimeoutBlamePackageName());
+
final int actualBucket = getStandbyBucket();
if (actualBucket == EXEMPTED_INDEX) {
+ // EXEMPTED apps always have their jobs exempted, even if they're buggy, because the
+ // user has explicitly told the system to avoid restricting the app for power reasons.
+ if (isBuggy) {
+ final String pkg;
+ if (getServiceComponent().getPackageName().equals(sourcePackageName)) {
+ pkg = sourcePackageName;
+ } else {
+ pkg = getServiceComponent().getPackageName() + "/" + sourcePackageName;
+ }
+ Slog.w(TAG, "Exempted app " + pkg + " considered buggy");
+ }
return actualBucket;
}
if (uidActive || getJob().isExemptedFromAppStandby()) {
@@ -848,13 +1173,18 @@ public final class JobStatus {
// like other ACTIVE apps.
return ACTIVE_INDEX;
}
+ // 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.
+ final int highestBucket = isBuggy ? WORKING_INDEX : ACTIVE_INDEX;
if (actualBucket != RESTRICTED_INDEX && actualBucket != NEVER_INDEX
&& mHasMediaBackupExemption) {
- // Cap it at WORKING_INDEX as media back up jobs are important to the user, and the
+ // Treat it as if it's at least WORKING_INDEX since media backup jobs are important
+ // to the user, and the
// source package may not have been used directly in a while.
- return Math.min(WORKING_INDEX, actualBucket);
+ return Math.max(highestBucket, Math.min(WORKING_INDEX, actualBucket));
}
- return actualBucket;
+ return Math.max(highestBucket, actualBucket);
}
/** Returns the real standby bucket of the job. */
@@ -927,6 +1257,16 @@ public final class JobStatus {
return true;
}
+ @Nullable
+ public String getNamespace() {
+ return mNamespace;
+ }
+
+ @Nullable
+ public String getNamespaceHash() {
+ return mNamespaceHash;
+ }
+
public String getSourceTag() {
return sourceTag;
}
@@ -953,10 +1293,25 @@ public final class JobStatus {
*/
@JobInfo.Priority
public int getEffectivePriority() {
- final int rawPriority = job.getPriority();
+ final boolean isDemoted =
+ (getInternalFlags() & INTERNAL_FLAG_DEMOTED_BY_USER) != 0
+ || (job.isUserInitiated()
+ && (getInternalFlags() & INTERNAL_FLAG_DEMOTED_BY_SYSTEM_UIJ) != 0);
+ final int maxPriority;
+ if (isDemoted) {
+ // If the job was demoted for some reason, limit its priority to HIGH.
+ maxPriority = JobInfo.PRIORITY_HIGH;
+ } else {
+ maxPriority = JobInfo.PRIORITY_MAX;
+ }
+ final int rawPriority = Math.min(maxPriority, job.getPriority());
if (numFailures < 2) {
return rawPriority;
}
+ if (shouldTreatAsUserInitiatedJob()) {
+ // Don't drop priority of UI jobs.
+ return rawPriority;
+ }
// Slowly decay priority of jobs to prevent starvation of other jobs.
if (isRequestedExpeditedJob()) {
// EJs can't fall below HIGH priority.
@@ -983,6 +1338,14 @@ public final class JobStatus {
mInternalFlags |= flags;
}
+ public void removeInternalFlags(int flags) {
+ mInternalFlags = mInternalFlags & ~flags;
+ }
+
+ int getPreferredConstraintFlags() {
+ return mPreferredConstraints;
+ }
+
public int getSatisfiedConstraintFlags() {
return satisfiedConstraints;
}
@@ -1003,27 +1366,41 @@ public final class JobStatus {
private void updateNetworkBytesLocked() {
mTotalNetworkDownloadBytes = job.getEstimatedNetworkDownloadBytes();
+ if (mTotalNetworkDownloadBytes < 0) {
+ // Legacy apps may have provided invalid negative values. Ignore invalid values.
+ mTotalNetworkDownloadBytes = JobInfo.NETWORK_BYTES_UNKNOWN;
+ }
mTotalNetworkUploadBytes = job.getEstimatedNetworkUploadBytes();
+ if (mTotalNetworkUploadBytes < 0) {
+ // Legacy apps may have provided invalid negative values. Ignore invalid values.
+ mTotalNetworkUploadBytes = JobInfo.NETWORK_BYTES_UNKNOWN;
+ }
+ // Minimum network chunk bytes has had data validation since its introduction, so no
+ // need to do validation again.
mMinimumNetworkChunkBytes = job.getMinimumNetworkChunkBytes();
if (pendingWork != null) {
for (int i = 0; i < pendingWork.size(); i++) {
- if (mTotalNetworkDownloadBytes != JobInfo.NETWORK_BYTES_UNKNOWN) {
- // If any component of the job has unknown usage, we don't have a
- // complete picture of what data will be used, and we have to treat the
- // entire up/download as unknown.
- long downloadBytes = pendingWork.get(i).getEstimatedNetworkDownloadBytes();
- if (downloadBytes != JobInfo.NETWORK_BYTES_UNKNOWN) {
+ long downloadBytes = pendingWork.get(i).getEstimatedNetworkDownloadBytes();
+ if (downloadBytes != JobInfo.NETWORK_BYTES_UNKNOWN && downloadBytes > 0) {
+ // If any component of the job has unknown usage, we won't have a
+ // complete picture of what data will be used. However, we use what we are given
+ // to get us as close to the complete picture as possible.
+ if (mTotalNetworkDownloadBytes != JobInfo.NETWORK_BYTES_UNKNOWN) {
mTotalNetworkDownloadBytes += downloadBytes;
+ } else {
+ mTotalNetworkDownloadBytes = downloadBytes;
}
}
- if (mTotalNetworkUploadBytes != JobInfo.NETWORK_BYTES_UNKNOWN) {
- // If any component of the job has unknown usage, we don't have a
- // complete picture of what data will be used, and we have to treat the
- // entire up/download as unknown.
- long uploadBytes = pendingWork.get(i).getEstimatedNetworkUploadBytes();
- if (uploadBytes != JobInfo.NETWORK_BYTES_UNKNOWN) {
+ long uploadBytes = pendingWork.get(i).getEstimatedNetworkUploadBytes();
+ if (uploadBytes != JobInfo.NETWORK_BYTES_UNKNOWN && uploadBytes > 0) {
+ // If any component of the job has unknown usage, we won't have a
+ // complete picture of what data will be used. However, we use what we are given
+ // to get us as close to the complete picture as possible.
+ if (mTotalNetworkUploadBytes != JobInfo.NETWORK_BYTES_UNKNOWN) {
mTotalNetworkUploadBytes += uploadBytes;
+ } else {
+ mTotalNetworkUploadBytes = uploadBytes;
}
}
final long chunkBytes = pendingWork.get(i).getMinimumNetworkChunkBytes();
@@ -1090,6 +1467,24 @@ public final class JobStatus {
return (requiredConstraints&CONSTRAINT_CONTENT_TRIGGER) != 0;
}
+ /** Returns true if the job has flexible job constraints enabled */
+ public boolean hasFlexibilityConstraint() {
+ return (requiredConstraints & CONSTRAINT_FLEXIBLE) != 0;
+ }
+
+ /** Returns the number of flexible job constraints required to be satisfied to execute */
+ public int getNumRequiredFlexibleConstraints() {
+ return mNumRequiredFlexibleConstraints - mNumDroppedFlexibleConstraints;
+ }
+
+ /**
+ * 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.
+ */
+ public int getNumDroppedFlexibleConstraints() {
+ return mNumDroppedFlexibleConstraints;
+ }
+
/**
* Checks both {@link #requiredConstraints} and {@link #mDynamicConstraints} to see if this job
* requires the specified constraint.
@@ -1118,6 +1513,14 @@ public final class JobStatus {
return job.isPersisted();
}
+ public long getCumulativeExecutionTimeMs() {
+ return mCumulativeExecutionTimeMs;
+ }
+
+ public void incrementCumulativeExecutionTime(long incrementMs) {
+ mCumulativeExecutionTimeMs += incrementMs;
+ }
+
public long getEarliestRunTime() {
return earliestRunTimeElapsedMillis;
}
@@ -1134,6 +1537,19 @@ public final class JobStatus {
mOriginalLatestRunTimeElapsedMillis = latestRunTimeElapsed;
}
+ /** Sets the jobs access to an unmetered network. */
+ void setHasAccessToUnmetered(boolean access) {
+ mHasAccessToUnmetered = access;
+ }
+
+ boolean getHasAccessToUnmetered() {
+ return mHasAccessToUnmetered;
+ }
+
+ boolean getPreferUnmetered() {
+ return mPreferUnmetered;
+ }
+
@JobParameters.StopReason
public int getStopReason() {
return mReasonReadyToUnready;
@@ -1193,20 +1609,64 @@ public final class JobStatus {
}
/**
+ * @return true if the job was scheduled as a user-initiated job and it hasn't been downgraded
+ * for any reason.
+ */
+ public boolean shouldTreatAsUserInitiatedJob() {
+ // isUserBgRestricted is intentionally excluded from this method. It should be fine to
+ // treat the job as a UI job while the app is TOP, but just not in the background.
+ // Instead of adding a proc state check here, the parts of JS that can make the distinction
+ // and care about the distinction can do the check.
+ return getJob().isUserInitiated()
+ && (getInternalFlags() & INTERNAL_FLAG_DEMOTED_BY_USER) == 0
+ && (getInternalFlags() & INTERNAL_FLAG_DEMOTED_BY_SYSTEM_UIJ) == 0;
+ }
+
+ /**
+ * Return a summary that uniquely identifies the underlying job.
+ */
+ @NonNull
+ public UserVisibleJobSummary getUserVisibleJobSummary() {
+ if (mUserVisibleJobSummary == null) {
+ mUserVisibleJobSummary = new UserVisibleJobSummary(
+ callingUid, getServiceComponent().getPackageName(),
+ getSourceUserId(), getSourcePackageName(),
+ getNamespace(), getJobId());
+ }
+ return mUserVisibleJobSummary;
+ }
+
+ /**
+ * @return true if this is a job whose execution should be made visible to the user.
+ */
+ public boolean isUserVisibleJob() {
+ return shouldTreatAsUserInitiatedJob() || startedAsUserInitiatedJob;
+ }
+
+ /**
* @return true if the job is exempted from Doze restrictions and therefore allowed to run
* in Doze.
*/
public boolean canRunInDoze() {
return appHasDozeExemption
|| (getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0
+ || shouldTreatAsUserInitiatedJob()
+ // EJs can't run in Doze if we explicitly require that the device is not Dozing.
|| ((shouldTreatAsExpeditedJob() || startedAsExpeditedJob)
- && (mDynamicConstraints & CONSTRAINT_DEVICE_NOT_DOZING) == 0);
+ && (mDynamicConstraints & CONSTRAINT_DEVICE_NOT_DOZING) == 0);
}
boolean canRunInBatterySaver() {
return (getInternalFlags() & INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION) != 0
+ || shouldTreatAsUserInitiatedJob()
+ // EJs can't run in Battery Saver if we explicitly require that Battery Saver is off
|| ((shouldTreatAsExpeditedJob() || startedAsExpeditedJob)
- && (mDynamicConstraints & CONSTRAINT_BACKGROUND_NOT_RESTRICTED) == 0);
+ && (mDynamicConstraints & CONSTRAINT_BACKGROUND_NOT_RESTRICTED) == 0);
+ }
+
+ /** Returns whether or not the app is background restricted by the user (FAS). */
+ public boolean isUserBgRestricted() {
+ return mIsUserBgRestricted;
}
/** @return true if the constraint was changed, false otherwise. */
@@ -1303,6 +1763,11 @@ public final class JobStatus {
return false;
}
+ /** @return true if the constraint was changed, false otherwise. */
+ boolean setFlexibilityConstraintSatisfied(final long nowElapsed, boolean state) {
+ return setConstraintSatisfied(CONSTRAINT_FLEXIBLE, nowElapsed, state);
+ }
+
/**
* Sets whether or not this job is approved to be treated as an expedited job based on quota
* policy.
@@ -1472,7 +1937,103 @@ public final class JobStatus {
}
}
- boolean isConstraintSatisfied(int constraint) {
+ /**
+ * If {@link #isReady()} returns false, this will return a single reason why the job isn't
+ * ready. If {@link #isReady()} returns true, this will return
+ * {@link JobScheduler#PENDING_JOB_REASON_UNDEFINED}.
+ */
+ @JobScheduler.PendingJobReason
+ public int getPendingJobReason() {
+ final int unsatisfiedConstraints = ~satisfiedConstraints
+ & (requiredConstraints | mDynamicConstraints | IMPLICIT_CONSTRAINTS);
+ if ((CONSTRAINT_BACKGROUND_NOT_RESTRICTED & unsatisfiedConstraints) != 0) {
+ // The BACKGROUND_NOT_RESTRICTED constraint could be unsatisfied either because
+ // the app is background restricted, or because we're restricting background work
+ // in battery saver. Assume that background restriction is the reason apps that
+ // jobs are not ready, and battery saver otherwise.
+ // This has the benefit of being consistent for background restricted apps
+ // (they'll always get BACKGROUND_RESTRICTION) as the reason, regardless of
+ // battery saver state.
+ if (mIsUserBgRestricted) {
+ return JobScheduler.PENDING_JOB_REASON_BACKGROUND_RESTRICTION;
+ }
+ return JobScheduler.PENDING_JOB_REASON_DEVICE_STATE;
+ }
+ if ((CONSTRAINT_BATTERY_NOT_LOW & unsatisfiedConstraints) != 0) {
+ if ((CONSTRAINT_BATTERY_NOT_LOW & requiredConstraints) != 0) {
+ // The developer requested this constraint, so it makes sense to return the
+ // explicit constraint reason.
+ return JobScheduler.PENDING_JOB_REASON_CONSTRAINT_BATTERY_NOT_LOW;
+ }
+ // Hard-coding right now since the current dynamic constraint sets don't overlap
+ // TODO: return based on active dynamic constraint sets when they start overlapping
+ return JobScheduler.PENDING_JOB_REASON_APP_STANDBY;
+ }
+ if ((CONSTRAINT_CHARGING & unsatisfiedConstraints) != 0) {
+ if ((CONSTRAINT_CHARGING & requiredConstraints) != 0) {
+ // The developer requested this constraint, so it makes sense to return the
+ // explicit constraint reason.
+ return JobScheduler.PENDING_JOB_REASON_CONSTRAINT_CHARGING;
+ }
+ // Hard-coding right now since the current dynamic constraint sets don't overlap
+ // TODO: return based on active dynamic constraint sets when they start overlapping
+ return JobScheduler.PENDING_JOB_REASON_APP_STANDBY;
+ }
+ if ((CONSTRAINT_CONNECTIVITY & unsatisfiedConstraints) != 0) {
+ return JobScheduler.PENDING_JOB_REASON_CONSTRAINT_CONNECTIVITY;
+ }
+ if ((CONSTRAINT_CONTENT_TRIGGER & unsatisfiedConstraints) != 0) {
+ return JobScheduler.PENDING_JOB_REASON_CONSTRAINT_CONTENT_TRIGGER;
+ }
+ if ((CONSTRAINT_DEVICE_NOT_DOZING & unsatisfiedConstraints) != 0) {
+ return JobScheduler.PENDING_JOB_REASON_DEVICE_STATE;
+ }
+ if ((CONSTRAINT_FLEXIBLE & unsatisfiedConstraints) != 0) {
+ return JobScheduler.PENDING_JOB_REASON_JOB_SCHEDULER_OPTIMIZATION;
+ }
+ if ((CONSTRAINT_IDLE & unsatisfiedConstraints) != 0) {
+ if ((CONSTRAINT_IDLE & requiredConstraints) != 0) {
+ // The developer requested this constraint, so it makes sense to return the
+ // explicit constraint reason.
+ return JobScheduler.PENDING_JOB_REASON_CONSTRAINT_DEVICE_IDLE;
+ }
+ // Hard-coding right now since the current dynamic constraint sets don't overlap
+ // TODO: return based on active dynamic constraint sets when they start overlapping
+ return JobScheduler.PENDING_JOB_REASON_APP_STANDBY;
+ }
+ if ((CONSTRAINT_PREFETCH & unsatisfiedConstraints) != 0) {
+ return JobScheduler.PENDING_JOB_REASON_CONSTRAINT_PREFETCH;
+ }
+ if ((CONSTRAINT_STORAGE_NOT_LOW & unsatisfiedConstraints) != 0) {
+ return JobScheduler.PENDING_JOB_REASON_CONSTRAINT_STORAGE_NOT_LOW;
+ }
+ if ((CONSTRAINT_TARE_WEALTH & unsatisfiedConstraints) != 0) {
+ return JobScheduler.PENDING_JOB_REASON_QUOTA;
+ }
+ if ((CONSTRAINT_TIMING_DELAY & unsatisfiedConstraints) != 0) {
+ return JobScheduler.PENDING_JOB_REASON_CONSTRAINT_MINIMUM_LATENCY;
+ }
+ if ((CONSTRAINT_WITHIN_QUOTA & unsatisfiedConstraints) != 0) {
+ return JobScheduler.PENDING_JOB_REASON_QUOTA;
+ }
+
+ if (getEffectiveStandbyBucket() == NEVER_INDEX) {
+ Slog.wtf(TAG, "App in NEVER bucket querying pending job reason");
+ // The user hasn't officially launched this app.
+ return JobScheduler.PENDING_JOB_REASON_USER;
+ }
+ if (serviceProcessName != null) {
+ return JobScheduler.PENDING_JOB_REASON_APP;
+ }
+
+ if (!isReady()) {
+ Slog.wtf(TAG, "Unknown reason job isn't ready");
+ }
+ return JobScheduler.PENDING_JOB_REASON_UNDEFINED;
+ }
+
+ /** @return whether or not the @param constraint is satisfied */
+ public boolean isConstraintSatisfied(int constraint) {
return (satisfiedConstraints&constraint) != 0;
}
@@ -1492,6 +2053,12 @@ public final class JobStatus {
trackingControllers |= which;
}
+ /** Adjusts the number of required flexible constraints by the given number */
+ public void adjustNumRequiredFlexibleConstraints(int adjustment) {
+ mNumDroppedFlexibleConstraints = Math.max(0, Math.min(mNumRequiredFlexibleConstraints,
+ mNumDroppedFlexibleConstraints - adjustment));
+ }
+
/**
* Add additional constraints to prevent this job from running when doze or battery saver are
* active.
@@ -1505,7 +2072,8 @@ public final class JobStatus {
* separately from the job's explicitly requested constraints and MUST be satisfied before
* the job can run if the app doesn't have quota.
*/
- private void addDynamicConstraints(int constraints) {
+ @VisibleForTesting
+ public void addDynamicConstraints(int constraints) {
if ((constraints & CONSTRAINT_WITHIN_QUOTA) != 0) {
// Quota should never be used as a dynamic constraint.
Slog.wtf(TAG, "Tried to set quota as a dynamic constraint");
@@ -1567,7 +2135,8 @@ public final class JobStatus {
return readinessStatusWithConstraint(constraint, true);
}
- private boolean readinessStatusWithConstraint(int constraint, boolean value) {
+ @VisibleForTesting
+ boolean readinessStatusWithConstraint(int constraint, boolean value) {
boolean oldValue = false;
int satisfied = mSatisfiedConstraintsOfInterest;
switch (constraint) {
@@ -1603,6 +2172,15 @@ public final class JobStatus {
break;
}
+ // The flexibility constraint relies on other constraints to be satisfied.
+ // This function lacks the information to determine if flexibility will be satisfied.
+ // But for the purposes of this function it is still useful to know the jobs' readiness
+ // not including the flexibility constraint. If flexibility is the constraint in question
+ // we can proceed as normal.
+ if (constraint != CONSTRAINT_FLEXIBLE) {
+ satisfied |= CONSTRAINT_FLEXIBLE;
+ }
+
boolean toReturn = isReady(satisfied);
switch (constraint) {
@@ -1645,19 +2223,21 @@ public final class JobStatus {
// run if its constraints are satisfied).
// DeviceNotDozing implicit constraint must be satisfied
// NotRestrictedInBackground implicit constraint must be satisfied
- return mReadyNotDozing && mReadyNotRestrictedInBg && (serviceInfo != null)
+ return mReadyNotDozing && mReadyNotRestrictedInBg && (serviceProcessName != null)
&& (mReadyDeadlineSatisfied || isConstraintsSatisfied(satisfiedConstraints));
}
/** All constraints besides implicit and deadline. */
static final int CONSTRAINTS_OF_INTEREST = CONSTRAINT_CHARGING | CONSTRAINT_BATTERY_NOT_LOW
| CONSTRAINT_STORAGE_NOT_LOW | CONSTRAINT_TIMING_DELAY | CONSTRAINT_CONNECTIVITY
- | CONSTRAINT_IDLE | CONSTRAINT_CONTENT_TRIGGER | CONSTRAINT_PREFETCH;
+ | CONSTRAINT_IDLE | CONSTRAINT_CONTENT_TRIGGER | CONSTRAINT_PREFETCH
+ | CONSTRAINT_FLEXIBLE;
// Soft override covers all non-"functional" constraints
static final int SOFT_OVERRIDE_CONSTRAINTS =
CONSTRAINT_CHARGING | CONSTRAINT_BATTERY_NOT_LOW | CONSTRAINT_STORAGE_NOT_LOW
- | CONSTRAINT_TIMING_DELAY | CONSTRAINT_IDLE | CONSTRAINT_PREFETCH;
+ | CONSTRAINT_TIMING_DELAY | CONSTRAINT_IDLE | CONSTRAINT_PREFETCH
+ | CONSTRAINT_FLEXIBLE;
/** Returns true whenever all dynamically set constraints are satisfied. */
public boolean areDynamicConstraintsSatisfied() {
@@ -1686,8 +2266,12 @@ public final class JobStatus {
return (sat & mRequiredConstraintsOfInterest) == mRequiredConstraintsOfInterest;
}
- public boolean matches(int uid, int jobId) {
- return this.job.getId() == jobId && this.callingUid == uid;
+ /**
+ * Returns true if the given parameters match this job's unique identifier.
+ */
+ public boolean matches(int uid, @Nullable String namespace, int jobId) {
+ return this.job.getId() == jobId && this.callingUid == uid
+ && Objects.equals(mNamespace, namespace);
}
@Override
@@ -1695,7 +2279,13 @@ public final class JobStatus {
StringBuilder sb = new StringBuilder(128);
sb.append("JobStatus{");
sb.append(Integer.toHexString(System.identityHashCode(this)));
- sb.append(" #");
+ if (mNamespace != null) {
+ sb.append(" ");
+ sb.append(mNamespace);
+ sb.append(":");
+ } else {
+ sb.append(" #");
+ }
UserHandle.formatUid(sb, callingUid);
sb.append("/");
sb.append(job.getId());
@@ -1745,13 +2335,17 @@ public final class JobStatus {
sb.append(" failures=");
sb.append(numFailures);
}
+ if (mNumSystemStops != 0) {
+ sb.append(" system stops=");
+ sb.append(mNumSystemStops);
+ }
if (isReady()) {
sb.append(" READY");
} else {
sb.append(" satisfied:0x").append(Integer.toHexString(satisfiedConstraints));
+ final int requiredConstraints = mRequiredConstraintsOfInterest | IMPLICIT_CONSTRAINTS;
sb.append(" unsatisfied:0x").append(Integer.toHexString(
- (satisfiedConstraints & mRequiredConstraintsOfInterest)
- ^ mRequiredConstraintsOfInterest));
+ (satisfiedConstraints & requiredConstraints) ^ requiredConstraints));
}
sb.append("}");
return sb.toString();
@@ -1780,6 +2374,9 @@ public final class JobStatus {
public String toShortString() {
StringBuilder sb = new StringBuilder();
sb.append(Integer.toHexString(System.identityHashCode(this)));
+ if (mNamespace != null) {
+ sb.append(" {").append(mNamespace).append("}");
+ }
sb.append(" #");
UserHandle.formatUid(sb, callingUid);
sb.append("/");
@@ -1815,35 +2412,38 @@ public final class JobStatus {
proto.end(token);
}
- void dumpConstraints(PrintWriter pw, int constraints) {
- if ((constraints&CONSTRAINT_CHARGING) != 0) {
+ static void dumpConstraints(PrintWriter pw, int constraints) {
+ if ((constraints & CONSTRAINT_CHARGING) != 0) {
pw.print(" CHARGING");
}
- if ((constraints& CONSTRAINT_BATTERY_NOT_LOW) != 0) {
+ if ((constraints & CONSTRAINT_BATTERY_NOT_LOW) != 0) {
pw.print(" BATTERY_NOT_LOW");
}
- if ((constraints& CONSTRAINT_STORAGE_NOT_LOW) != 0) {
+ if ((constraints & CONSTRAINT_STORAGE_NOT_LOW) != 0) {
pw.print(" STORAGE_NOT_LOW");
}
- if ((constraints&CONSTRAINT_TIMING_DELAY) != 0) {
+ if ((constraints & CONSTRAINT_TIMING_DELAY) != 0) {
pw.print(" TIMING_DELAY");
}
- if ((constraints&CONSTRAINT_DEADLINE) != 0) {
+ if ((constraints & CONSTRAINT_DEADLINE) != 0) {
pw.print(" DEADLINE");
}
- if ((constraints&CONSTRAINT_IDLE) != 0) {
+ if ((constraints & CONSTRAINT_IDLE) != 0) {
pw.print(" IDLE");
}
- if ((constraints&CONSTRAINT_CONNECTIVITY) != 0) {
+ if ((constraints & CONSTRAINT_CONNECTIVITY) != 0) {
pw.print(" CONNECTIVITY");
}
- if ((constraints&CONSTRAINT_CONTENT_TRIGGER) != 0) {
+ if ((constraints & CONSTRAINT_FLEXIBLE) != 0) {
+ pw.print(" FLEXIBILITY");
+ }
+ if ((constraints & CONSTRAINT_CONTENT_TRIGGER) != 0) {
pw.print(" CONTENT_TRIGGER");
}
- if ((constraints&CONSTRAINT_DEVICE_NOT_DOZING) != 0) {
+ if ((constraints & CONSTRAINT_DEVICE_NOT_DOZING) != 0) {
pw.print(" DEVICE_NOT_DOZING");
}
- if ((constraints&CONSTRAINT_BACKGROUND_NOT_RESTRICTED) != 0) {
+ if ((constraints & CONSTRAINT_BACKGROUND_NOT_RESTRICTED) != 0) {
pw.print(" BACKGROUND_NOT_RESTRICTED");
}
if ((constraints & CONSTRAINT_PREFETCH) != 0) {
@@ -1879,6 +2479,8 @@ public final class JobStatus {
return JobServerProtoEnums.CONSTRAINT_DEADLINE;
case CONSTRAINT_DEVICE_NOT_DOZING:
return JobServerProtoEnums.CONSTRAINT_DEVICE_NOT_DOZING;
+ case CONSTRAINT_FLEXIBLE:
+ return JobServerProtoEnums.CONSTRAINT_FLEXIBILITY;
case CONSTRAINT_IDLE:
return JobServerProtoEnums.CONSTRAINT_IDLE;
case CONSTRAINT_PREFETCH:
@@ -2121,6 +2723,9 @@ public final class JobStatus {
pw.print("Required constraints:");
dumpConstraints(pw, requiredConstraints);
pw.println();
+ pw.print("Preferred constraints:");
+ dumpConstraints(pw, mPreferredConstraints);
+ pw.println();
pw.print("Dynamic constraints:");
dumpConstraints(pw, mDynamicConstraints);
pw.println();
@@ -2133,6 +2738,14 @@ public final class JobStatus {
((requiredConstraints | CONSTRAINT_WITHIN_QUOTA | CONSTRAINT_TARE_WEALTH)
& ~satisfiedConstraints));
pw.println();
+ if (hasFlexibilityConstraint()) {
+ pw.print("Num Required Flexible constraints: ");
+ pw.print(getNumRequiredFlexibleConstraints());
+ pw.println();
+ pw.print("Num Dropped Flexible constraints: ");
+ pw.print(getNumDroppedFlexibleConstraints());
+ pw.println();
+ }
pw.println("Constraint history:");
pw.increaseIndent();
@@ -2186,7 +2799,7 @@ public final class JobStatus {
pw.println(mReadyDynamicSatisfied);
}
pw.print("readyComponentEnabled: ");
- pw.println(serviceInfo != null);
+ pw.println(serviceProcessName != null);
if ((getFlags() & JobInfo.FLAG_EXPEDITED) != 0) {
pw.print("expeditedQuotaApproved: ");
pw.print(mExpeditedQuotaApproved);
@@ -2196,8 +2809,21 @@ public final class JobStatus {
pw.print(startedAsExpeditedJob);
pw.println(")");
}
+ if ((getFlags() & JobInfo.FLAG_USER_INITIATED) != 0) {
+ pw.print("userInitiatedApproved: ");
+ pw.print(shouldTreatAsUserInitiatedJob());
+ pw.print(" (started as UIJ: ");
+ pw.print(startedAsUserInitiatedJob);
+ pw.println(")");
+ }
pw.decreaseIndent();
+ pw.print("Started with foreground flag: ");
+ pw.println(startedWithForegroundFlag);
+ if (mIsUserBgRestricted) {
+ pw.println("User BG restricted");
+ }
+
if (changedAuthorities != null) {
pw.println("Changed authorities:");
pw.increaseIndent();
@@ -2254,9 +2880,17 @@ public final class JobStatus {
pw.print(", original latest=");
formatRunTime(pw, mOriginalLatestRunTimeElapsedMillis, NO_LATEST_RUNTIME, nowElapsed);
pw.println();
+ if (mCumulativeExecutionTimeMs != 0) {
+ pw.print("Cumulative execution time=");
+ TimeUtils.formatDuration(mCumulativeExecutionTimeMs, pw);
+ pw.println();
+ }
if (numFailures != 0) {
pw.print("Num failures: "); pw.println(numFailures);
}
+ if (mNumSystemStops != 0) {
+ pw.print("Num system stops: "); pw.println(mNumSystemStops);
+ }
if (mLastSuccessfulRunTime != 0) {
pw.print("Last successful run: ");
pw.println(formatTime(mLastSuccessfulRunTime));
@@ -2454,7 +3088,7 @@ public final class JobStatus {
proto.write(JobStatusDumpProto.ORIGINAL_LATEST_RUNTIME_ELAPSED,
mOriginalLatestRunTimeElapsedMillis);
- proto.write(JobStatusDumpProto.NUM_FAILURES, numFailures);
+ proto.write(JobStatusDumpProto.NUM_FAILURES, numFailures + mNumSystemStops);
proto.write(JobStatusDumpProto.LAST_SUCCESSFUL_RUN_TIME, mLastSuccessfulRunTime);
proto.write(JobStatusDumpProto.LAST_FAILED_RUN_TIME, mLastFailedRunTime);
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/Package.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/Package.java
deleted file mode 100644
index 78a77fe46f3c..000000000000
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/Package.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.job.controllers;
-
-import java.util.Objects;
-
-/** Wrapper class to represent a userId-pkgName combo. */
-final class Package {
- public final String packageName;
- public final int userId;
-
- Package(int userId, String packageName) {
- this.userId = userId;
- this.packageName = packageName;
- }
-
- @Override
- public String toString() {
- return packageToString(userId, packageName);
- }
-
- @Override
- public boolean equals(Object obj) {
- if (this == obj) {
- return true;
- }
- if (!(obj instanceof Package)) {
- return false;
- }
- Package other = (Package) obj;
- return userId == other.userId && Objects.equals(packageName, other.packageName);
- }
-
- @Override
- public int hashCode() {
- return packageName.hashCode() + userId;
- }
-
- /**
- * Standardize the output of userId-packageName combo.
- */
- static String packageToString(int userId, String packageName) {
- return "<" + userId + ">" + packageName;
- }
-}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/PrefetchController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/PrefetchController.java
index 0f385efae5cc..2b7438c862bd 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/PrefetchController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/PrefetchController.java
@@ -21,7 +21,6 @@ import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
import static com.android.server.job.JobSchedulerService.sSystemClock;
-import static com.android.server.job.controllers.Package.packageToString;
import android.annotation.CurrentTimeMillisLong;
import android.annotation.ElapsedRealtimeLong;
@@ -31,6 +30,7 @@ import android.app.usage.UsageStatsManagerInternal;
import android.app.usage.UsageStatsManagerInternal.EstimatedLaunchTimeChangedListener;
import android.appwidget.AppWidgetManager;
import android.content.Context;
+import android.content.pm.UserPackage;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
@@ -46,7 +46,7 @@ import android.util.TimeUtils;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.SomeArgs;
-import com.android.server.JobSchedulerBackgroundThread;
+import com.android.server.AppSchedulingModuleThread;
import com.android.server.LocalServices;
import com.android.server.job.JobSchedulerService;
import com.android.server.utils.AlarmQueue;
@@ -81,6 +81,8 @@ public class PrefetchController extends StateController {
*/
@GuardedBy("mLock")
private final SparseArrayMap<String, Long> mEstimatedLaunchTimes = new SparseArrayMap<>();
+ @GuardedBy("mLock")
+ private final ArraySet<PrefetchChangedListener> mPrefetchChangedListeners = new ArraySet<>();
private final ThresholdAlarmListener mThresholdAlarmListener;
/**
@@ -99,6 +101,13 @@ public class PrefetchController extends StateController {
@GuardedBy("mLock")
private long mLaunchTimeAllowanceMs = PcConstants.DEFAULT_LAUNCH_TIME_ALLOWANCE_MS;
+ /** Called by Prefetch Controller after local cache has been updated */
+ public interface PrefetchChangedListener {
+ /** Callback to inform listeners when estimated launch times change. */
+ void onPrefetchCacheUpdated(ArraySet<JobStatus> jobs, int userId, String pkgName,
+ long prevEstimatedLaunchTime, long newEstimatedLaunchTime, long nowElapsed);
+ }
+
@SuppressWarnings("FieldCanBeLocal")
private final EstimatedLaunchTimeChangedListener mEstimatedLaunchTimeChangedListener =
new EstimatedLaunchTimeChangedListener() {
@@ -121,9 +130,9 @@ public class PrefetchController extends StateController {
public PrefetchController(JobSchedulerService service) {
super(service);
mPcConstants = new PcConstants();
- mHandler = new PcHandler(mContext.getMainLooper());
+ mHandler = new PcHandler(AppSchedulingModuleThread.get().getLooper());
mThresholdAlarmListener = new ThresholdAlarmListener(
- mContext, JobSchedulerBackgroundThread.get().getLooper());
+ mContext, AppSchedulingModuleThread.get().getLooper());
mUsageStatsManagerInternal = LocalServices.getService(UsageStatsManagerInternal.class);
mUsageStatsManagerInternal
@@ -158,13 +167,12 @@ public class PrefetchController extends StateController {
@Override
@GuardedBy("mLock")
- public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob,
- boolean forUpdate) {
+ public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob) {
final int userId = jobStatus.getSourceUserId();
final String pkgName = jobStatus.getSourcePackageName();
final ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, pkgName);
if (jobs != null && jobs.remove(jobStatus) && jobs.size() == 0) {
- mThresholdAlarmListener.removeAlarmForKey(new Package(userId, pkgName));
+ mThresholdAlarmListener.removeAlarmForKey(UserPackage.of(userId, pkgName));
}
}
@@ -178,7 +186,7 @@ public class PrefetchController extends StateController {
final int userId = UserHandle.getUserId(uid);
mTrackedJobs.delete(userId, packageName);
mEstimatedLaunchTimes.delete(userId, packageName);
- mThresholdAlarmListener.removeAlarmForKey(new Package(userId, packageName));
+ mThresholdAlarmListener.removeAlarmForKey(UserPackage.of(userId, packageName));
}
@Override
@@ -291,12 +299,18 @@ public class PrefetchController extends StateController {
// Don't bother caching the value unless the app has scheduled prefetch jobs
// before. This is based on the assumption that if an app has scheduled a
// prefetch job before, then it will probably schedule another one again.
+ final long prevEstimatedLaunchTime = mEstimatedLaunchTimes.get(userId, pkgName);
mEstimatedLaunchTimes.add(userId, pkgName, newEstimatedLaunchTime);
if (!jobs.isEmpty()) {
final long now = sSystemClock.millis();
final long nowElapsed = sElapsedRealtimeClock.millis();
updateThresholdAlarmLocked(userId, pkgName, now, nowElapsed);
+ for (int i = 0; i < mPrefetchChangedListeners.size(); i++) {
+ mPrefetchChangedListeners.valueAt(i).onPrefetchCacheUpdated(
+ jobs, userId, pkgName, prevEstimatedLaunchTime,
+ newEstimatedLaunchTime, nowElapsed);
+ }
if (maybeUpdateConstraintForPkgLocked(now, nowElapsed, userId, pkgName)) {
mStateChangedListener.onControllerStateChanged(jobs);
}
@@ -340,7 +354,7 @@ public class PrefetchController extends StateController {
@CurrentTimeMillisLong long now, @ElapsedRealtimeLong long nowElapsed) {
final ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, pkgName);
if (jobs == null || jobs.size() == 0) {
- mThresholdAlarmListener.removeAlarmForKey(new Package(userId, pkgName));
+ mThresholdAlarmListener.removeAlarmForKey(UserPackage.of(userId, pkgName));
return;
}
@@ -351,10 +365,10 @@ public class PrefetchController extends StateController {
// Set alarm to be notified when this crosses the threshold.
final long timeToCrossThresholdMs =
nextEstimatedLaunchTime - (now + mLaunchTimeThresholdMs);
- mThresholdAlarmListener.addAlarm(new Package(userId, pkgName),
+ mThresholdAlarmListener.addAlarm(UserPackage.of(userId, pkgName),
nowElapsed + timeToCrossThresholdMs);
} else {
- mThresholdAlarmListener.removeAlarmForKey(new Package(userId, pkgName));
+ mThresholdAlarmListener.removeAlarmForKey(UserPackage.of(userId, pkgName));
}
}
@@ -386,7 +400,7 @@ public class PrefetchController extends StateController {
public void onConstantsUpdatedLocked() {
if (mPcConstants.mShouldReevaluateConstraints) {
// Update job bookkeeping out of band.
- JobSchedulerBackgroundThread.getHandler().post(() -> {
+ AppSchedulingModuleThread.getHandler().post(() -> {
final ArraySet<JobStatus> changedJobs = new ArraySet<>();
synchronized (mLock) {
final long nowElapsed = sElapsedRealtimeClock.millis();
@@ -413,25 +427,25 @@ public class PrefetchController extends StateController {
}
/** Track when apps will cross the "will run soon" threshold. */
- private class ThresholdAlarmListener extends AlarmQueue<Package> {
+ private class ThresholdAlarmListener extends AlarmQueue<UserPackage> {
private ThresholdAlarmListener(Context context, Looper looper) {
super(context, looper, "*job.prefetch*", "Prefetch threshold", false,
PcConstants.DEFAULT_LAUNCH_TIME_THRESHOLD_MS / 10);
}
@Override
- protected boolean isForUser(@NonNull Package key, int userId) {
+ protected boolean isForUser(@NonNull UserPackage key, int userId) {
return key.userId == userId;
}
@Override
- protected void processExpiredAlarms(@NonNull ArraySet<Package> expired) {
+ protected void processExpiredAlarms(@NonNull ArraySet<UserPackage> expired) {
final ArraySet<JobStatus> changedJobs = new ArraySet<>();
synchronized (mLock) {
final long now = sSystemClock.millis();
final long nowElapsed = sElapsedRealtimeClock.millis();
for (int i = 0; i < expired.size(); ++i) {
- Package p = expired.valueAt(i);
+ UserPackage p = expired.valueAt(i);
if (!willBeLaunchedSoonLocked(p.userId, p.packageName, now)) {
Slog.e(TAG, "Alarm expired for "
+ packageToString(p.userId, p.packageName) + " at the wrong time");
@@ -448,6 +462,18 @@ public class PrefetchController extends StateController {
}
}
+ void registerPrefetchChangedListener(PrefetchChangedListener listener) {
+ synchronized (mLock) {
+ mPrefetchChangedListeners.add(listener);
+ }
+ }
+
+ void unRegisterPrefetchChangedListener(PrefetchChangedListener listener) {
+ synchronized (mLock) {
+ mPrefetchChangedListeners.remove(listener);
+ }
+ }
+
private class PcHandler extends Handler {
PcHandler(Looper looper) {
super(looper);
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
index bb8d175c2375..1c29982dbd48 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
@@ -28,7 +28,6 @@ import static com.android.server.job.JobSchedulerService.RARE_INDEX;
import static com.android.server.job.JobSchedulerService.RESTRICTED_INDEX;
import static com.android.server.job.JobSchedulerService.WORKING_INDEX;
import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
-import static com.android.server.job.controllers.Package.packageToString;
import android.Manifest;
import android.annotation.NonNull;
@@ -36,7 +35,7 @@ import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.ActivityManager;
import android.app.AlarmManager;
-import android.app.IUidObserver;
+import android.app.UidObserver;
import android.app.usage.UsageEvents;
import android.app.usage.UsageStatsManagerInternal;
import android.app.usage.UsageStatsManagerInternal.UsageEventListener;
@@ -44,6 +43,7 @@ import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
+import android.content.pm.UserPackage;
import android.os.BatteryManager;
import android.os.Handler;
import android.os.Looper;
@@ -65,7 +65,7 @@ import android.util.proto.ProtoOutputStream;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
-import com.android.server.JobSchedulerBackgroundThread;
+import com.android.server.AppSchedulingModuleThread;
import com.android.server.LocalServices;
import com.android.server.PowerAllowlistInternal;
import com.android.server.job.ConstantsProto;
@@ -382,31 +382,11 @@ public final class QuotaController extends StateController {
}
};
- private class QcUidObserver extends IUidObserver.Stub {
+ private class QcUidObserver extends UidObserver {
@Override
public void onUidStateChanged(int uid, int procState, long procStateSeq, int capability) {
mHandler.obtainMessage(MSG_UID_PROCESS_STATE_CHANGED, uid, procState).sendToTarget();
}
-
- @Override
- public void onUidGone(int uid, boolean disabled) {
- }
-
- @Override
- public void onUidActive(int uid) {
- }
-
- @Override
- public void onUidIdle(int uid, boolean disabled) {
- }
-
- @Override
- public void onUidCachedChanged(int uid, boolean cached) {
- }
-
- @Override
- public void onUidProcAdjChanged(int uid) {
- }
}
/**
@@ -532,7 +512,7 @@ public final class QuotaController extends StateController {
*/
private final SparseSetArray<String> mSystemInstallers = new SparseSetArray<>();
- /** An app has reached its quota. The message should contain a {@link Package} object. */
+ /** An app has reached its quota. The message should contain a {@link UserPackage} object. */
@VisibleForTesting
static final int MSG_REACHED_QUOTA = 0;
/** Drop any old timing sessions. */
@@ -542,7 +522,7 @@ public final class QuotaController extends StateController {
/** Process state for a UID has changed. */
private static final int MSG_UID_PROCESS_STATE_CHANGED = 3;
/**
- * An app has reached its expedited job quota. The message should contain a {@link Package}
+ * An app has reached its expedited job quota. The message should contain a {@link UserPackage}
* object.
*/
@VisibleForTesting
@@ -560,13 +540,14 @@ public final class QuotaController extends StateController {
@NonNull BackgroundJobsController backgroundJobsController,
@NonNull ConnectivityController connectivityController) {
super(service);
- mHandler = new QcHandler(mContext.getMainLooper());
+ mHandler = new QcHandler(AppSchedulingModuleThread.get().getLooper());
mAlarmManager = mContext.getSystemService(AlarmManager.class);
mQcConstants = new QcConstants();
mBackgroundJobsController = backgroundJobsController;
mConnectivityController = connectivityController;
mIsEnabled = !mConstants.USE_TARE_POLICY;
- mInQuotaAlarmQueue = new InQuotaAlarmQueue(mContext, mContext.getMainLooper());
+ mInQuotaAlarmQueue =
+ new InQuotaAlarmQueue(mContext, AppSchedulingModuleThread.get().getLooper());
// Set up the app standby bucketing tracker
AppStandbyInternal appStandby = LocalServices.getService(AppStandbyInternal.class);
@@ -641,6 +622,9 @@ public final class QuotaController extends StateController {
mTopStartedJobs.add(jobStatus);
// Top jobs won't count towards quota so there's no need to involve the Timer.
return;
+ } else if (jobStatus.shouldTreatAsUserInitiatedJob()) {
+ // User-initiated jobs won't count towards quota.
+ return;
}
final int userId = jobStatus.getSourceUserId();
@@ -673,15 +657,14 @@ public final class QuotaController extends StateController {
@Override
@GuardedBy("mLock")
- public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob,
- boolean forUpdate) {
+ public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob) {
if (jobStatus.clearTrackingController(JobStatus.TRACKING_QUOTA)) {
unprepareFromExecutionLocked(jobStatus);
final int userId = jobStatus.getSourceUserId();
final String pkgName = jobStatus.getSourcePackageName();
ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, pkgName);
if (jobs != null && jobs.remove(jobStatus) && jobs.size() == 0) {
- mInQuotaAlarmQueue.removeAlarmForKey(new Package(userId, pkgName));
+ mInQuotaAlarmQueue.removeAlarmForKey(UserPackage.of(userId, pkgName));
}
}
}
@@ -747,7 +730,7 @@ public final class QuotaController extends StateController {
}
mTimingEvents.delete(userId, packageName);
mEJTimingSessions.delete(userId, packageName);
- mInQuotaAlarmQueue.removeAlarmForKey(new Package(userId, packageName));
+ mInQuotaAlarmQueue.removeAlarmForKey(UserPackage.of(userId, packageName));
mExecutionStatsCache.delete(userId, packageName);
mEJStats.delete(userId, packageName);
mTopAppTrackers.delete(userId, packageName);
@@ -790,6 +773,14 @@ public final class QuotaController extends StateController {
// If quota is currently "free", then the job can run for the full amount of time,
// regardless of bucket (hence using charging instead of isQuotaFreeLocked()).
if (mService.isBatteryCharging()
+ // The top and foreground cases here were added because apps in those states
+ // aren't really restricted and the work could be something the user is
+ // waiting for. Now that user-initiated jobs are a defined concept, we may
+ // not need these exemptions as much. However, UIJs are currently limited
+ // (as of UDC) to data transfer work. There may be other work that could
+ // rely on this exception. Once we add more UIJ types, we can re-evaluate
+ // the need for these exceptions.
+ // TODO: re-evaluate the need for these exceptions
|| mTopAppCache.get(jobStatus.getSourceUid())
|| isTopStartedJobLocked(jobStatus)
|| isUidInForeground(jobStatus.getSourceUid())) {
@@ -893,7 +884,8 @@ public final class QuotaController extends StateController {
// 1. it was started while the app was in the TOP state
// 2. the app is currently in the foreground
// 3. the app overall is within its quota
- return isTopStartedJobLocked(jobStatus)
+ return jobStatus.shouldTreatAsUserInitiatedJob()
+ || isTopStartedJobLocked(jobStatus)
|| isUidInForeground(jobStatus.getSourceUid())
|| isWithinQuotaLocked(
jobStatus.getSourceUserId(), jobStatus.getSourcePackageName(), standbyBucket);
@@ -1646,7 +1638,7 @@ public final class QuotaController extends StateController {
mEJPkgTimers.forEach(mTimerChargingUpdateFunctor);
mPkgTimers.forEach(mTimerChargingUpdateFunctor);
// Now update jobs out of band so broadcast processing can proceed.
- JobSchedulerBackgroundThread.getHandler().post(() -> {
+ AppSchedulingModuleThread.getHandler().post(() -> {
synchronized (mLock) {
maybeUpdateAllConstraintsLocked();
}
@@ -1726,7 +1718,7 @@ public final class QuotaController extends StateController {
// exempted.
maybeScheduleStartAlarmLocked(userId, packageName, realStandbyBucket);
} else {
- mInQuotaAlarmQueue.removeAlarmForKey(new Package(userId, packageName));
+ mInQuotaAlarmQueue.removeAlarmForKey(UserPackage.of(userId, packageName));
}
return changedJobs;
}
@@ -1765,7 +1757,7 @@ public final class QuotaController extends StateController {
&& isWithinQuotaLocked(userId, packageName, realStandbyBucket)) {
// TODO(141645789): we probably shouldn't cancel the alarm until we've verified
// that all jobs for the userId-package are within quota.
- mInQuotaAlarmQueue.removeAlarmForKey(new Package(userId, packageName));
+ mInQuotaAlarmQueue.removeAlarmForKey(UserPackage.of(userId, packageName));
} else {
mToScheduleStartAlarms.add(userId, packageName, realStandbyBucket);
}
@@ -1815,7 +1807,7 @@ public final class QuotaController extends StateController {
if (jobs == null || jobs.size() == 0) {
Slog.e(TAG, "maybeScheduleStartAlarmLocked called for "
+ packageToString(userId, packageName) + " that has no jobs");
- mInQuotaAlarmQueue.removeAlarmForKey(new Package(userId, packageName));
+ mInQuotaAlarmQueue.removeAlarmForKey(UserPackage.of(userId, packageName));
return;
}
@@ -1839,7 +1831,7 @@ public final class QuotaController extends StateController {
+ getRemainingExecutionTimeLocked(userId, packageName, standbyBucket)
+ "ms in its quota.");
}
- mInQuotaAlarmQueue.removeAlarmForKey(new Package(userId, packageName));
+ mInQuotaAlarmQueue.removeAlarmForKey(UserPackage.of(userId, packageName));
mHandler.obtainMessage(MSG_CHECK_PACKAGE, userId, 0, packageName).sendToTarget();
return;
}
@@ -1904,7 +1896,7 @@ public final class QuotaController extends StateController {
+ nowElapsed + ", inQuotaTime=" + inQuotaTimeElapsed + ": " + stats);
inQuotaTimeElapsed = nowElapsed + 5 * MINUTE_IN_MILLIS;
}
- mInQuotaAlarmQueue.addAlarm(new Package(userId, packageName), inQuotaTimeElapsed);
+ mInQuotaAlarmQueue.addAlarm(UserPackage.of(userId, packageName), inQuotaTimeElapsed);
}
private boolean setConstraintSatisfied(@NonNull JobStatus jobStatus, long nowElapsed,
@@ -2099,7 +2091,7 @@ public final class QuotaController extends StateController {
}
private final class Timer {
- private final Package mPkg;
+ private final UserPackage mPkg;
private final int mUid;
private final boolean mRegularJobTimer;
@@ -2111,12 +2103,19 @@ public final class QuotaController extends StateController {
private long mDebitAdjustment;
Timer(int uid, int userId, String packageName, boolean regularJobTimer) {
- mPkg = new Package(userId, packageName);
+ mPkg = UserPackage.of(userId, packageName);
mUid = uid;
mRegularJobTimer = regularJobTimer;
}
void startTrackingJobLocked(@NonNull JobStatus jobStatus) {
+ if (jobStatus.shouldTreatAsUserInitiatedJob()) {
+ if (DEBUG) {
+ Slog.v(TAG, "Timer ignoring " + jobStatus.toShortString()
+ + " because it's user-initiated");
+ }
+ return;
+ }
if (isTopStartedJobLocked(jobStatus)) {
// We intentionally don't pay attention to fg state changes after a TOP job has
// started.
@@ -2366,7 +2365,7 @@ public final class QuotaController extends StateController {
}
private final class TopAppTimer {
- private final Package mPkg;
+ private final UserPackage mPkg;
// List of jobs currently running for this app that started when the app wasn't in the
// foreground.
@@ -2374,7 +2373,7 @@ public final class QuotaController extends StateController {
private long mStartTimeElapsed;
TopAppTimer(int userId, String packageName) {
- mPkg = new Package(userId, packageName);
+ mPkg = UserPackage.of(userId, packageName);
}
private int calculateTimeChunks(final long nowElapsed) {
@@ -2476,7 +2475,7 @@ public final class QuotaController extends StateController {
public void onAppIdleStateChanged(final String packageName, final @UserIdInt int userId,
boolean idle, int bucket, int reason) {
// Update job bookkeeping out of band.
- JobSchedulerBackgroundThread.getHandler().post(() -> {
+ AppSchedulingModuleThread.getHandler().post(() -> {
final int bucketIndex = JobSchedulerService.standbyBucketToBucketIndex(bucket);
updateStandbyBucket(userId, packageName, bucketIndex);
});
@@ -2657,7 +2656,7 @@ public final class QuotaController extends StateController {
synchronized (mLock) {
switch (msg.what) {
case MSG_REACHED_QUOTA: {
- Package pkg = (Package) msg.obj;
+ UserPackage pkg = (UserPackage) msg.obj;
if (DEBUG) {
Slog.d(TAG, "Checking if " + pkg + " has reached its quota.");
}
@@ -2686,7 +2685,7 @@ public final class QuotaController extends StateController {
break;
}
case MSG_REACHED_EJ_QUOTA: {
- Package pkg = (Package) msg.obj;
+ UserPackage pkg = (UserPackage) msg.obj;
if (DEBUG) {
Slog.d(TAG, "Checking if " + pkg + " has reached its EJ quota.");
}
@@ -2888,21 +2887,21 @@ public final class QuotaController extends StateController {
}
/** Track when UPTCs are expected to come back into quota. */
- private class InQuotaAlarmQueue extends AlarmQueue<Package> {
+ private class InQuotaAlarmQueue extends AlarmQueue<UserPackage> {
private InQuotaAlarmQueue(Context context, Looper looper) {
super(context, looper, ALARM_TAG_QUOTA_CHECK, "In quota", false,
QcConstants.DEFAULT_MIN_QUOTA_CHECK_DELAY_MS);
}
@Override
- protected boolean isForUser(@NonNull Package key, int userId) {
+ protected boolean isForUser(@NonNull UserPackage key, int userId) {
return key.userId == userId;
}
@Override
- protected void processExpiredAlarms(@NonNull ArraySet<Package> expired) {
+ protected void processExpiredAlarms(@NonNull ArraySet<UserPackage> expired) {
for (int i = 0; i < expired.size(); ++i) {
- Package p = expired.valueAt(i);
+ UserPackage p = expired.valueAt(i);
mHandler.obtainMessage(MSG_CHECK_PACKAGE, p.userId, 0, p.packageName)
.sendToTarget();
}
@@ -2928,7 +2927,7 @@ public final class QuotaController extends StateController {
if (mQcConstants.mShouldReevaluateConstraints || mIsEnabled == mConstants.USE_TARE_POLICY) {
mIsEnabled = !mConstants.USE_TARE_POLICY;
// Update job bookkeeping out of band.
- JobSchedulerBackgroundThread.getHandler().post(() -> {
+ AppSchedulingModuleThread.getHandler().post(() -> {
synchronized (mLock) {
invalidateAllExecutionStatsLocked();
maybeUpdateAllConstraintsLocked();
@@ -3162,7 +3161,8 @@ public final class QuotaController extends StateController {
private static final int DEFAULT_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = 20;
private static final long DEFAULT_TIMING_SESSION_COALESCING_DURATION_MS = 5000; // 5 seconds
private static final long DEFAULT_MIN_QUOTA_CHECK_DELAY_MS = MINUTE_IN_MILLIS;
- private static final long DEFAULT_EJ_LIMIT_EXEMPTED_MS = 45 * MINUTE_IN_MILLIS;
+ // TODO(267949143): set a different limit for headless system apps
+ private static final long DEFAULT_EJ_LIMIT_EXEMPTED_MS = 60 * MINUTE_IN_MILLIS;
private static final long DEFAULT_EJ_LIMIT_ACTIVE_MS = 30 * MINUTE_IN_MILLIS;
private static final long DEFAULT_EJ_LIMIT_WORKING_MS = DEFAULT_EJ_LIMIT_ACTIVE_MS;
private static final long DEFAULT_EJ_LIMIT_FREQUENT_MS = 10 * MINUTE_IN_MILLIS;
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java
index 8453e53782ca..44ac798c2912 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java
@@ -85,8 +85,7 @@ public abstract class StateController {
/**
* Remove task - this will happen if the task is cancelled, completed, etc.
*/
- public abstract void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob,
- boolean forUpdate);
+ public abstract void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob);
/**
* Called when a new job is being created to reschedule an old failed job.
@@ -187,4 +186,11 @@ public abstract class StateController {
/** Dump any internal constants the Controller may have. */
public void dumpConstants(ProtoOutputStream proto) {
}
+
+ /**
+ * Standardize the output of userId-packageName combo.
+ */
+ static String packageToString(int userId, String packageName) {
+ return "<" + userId + ">" + packageName;
+ }
}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/StorageController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/StorageController.java
index 1ce0a7f6b4c7..11e2ff7bd77f 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/StorageController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/StorageController.java
@@ -70,8 +70,7 @@ public final class StorageController extends StateController {
}
@Override
- public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob,
- boolean forUpdate) {
+ public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob) {
if (taskStatus.clearTrackingController(JobStatus.TRACKING_STORAGE)) {
mTrackedTasks.remove(taskStatus);
}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/TareController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/TareController.java
index a1a541f92b38..7408088b8efc 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/TareController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/TareController.java
@@ -30,7 +30,7 @@ import android.util.Slog;
import android.util.SparseArrayMap;
import com.android.internal.annotations.GuardedBy;
-import com.android.server.JobSchedulerBackgroundThread;
+import com.android.server.AppSchedulingModuleThread;
import com.android.server.LocalServices;
import com.android.server.job.JobSchedulerService;
import com.android.server.tare.EconomicPolicy;
@@ -313,6 +313,11 @@ public class TareController extends StateController {
@GuardedBy("mLock")
public void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob) {
final long nowElapsed = sElapsedRealtimeClock.millis();
+ if (jobStatus.shouldTreatAsUserInitiatedJob()) {
+ // User-initiated jobs should always be allowed to run.
+ jobStatus.setTareWealthConstraintSatisfied(nowElapsed, true);
+ return;
+ }
jobStatus.setTareWealthConstraintSatisfied(nowElapsed, hasEnoughWealthLocked(jobStatus));
setExpeditedTareApproved(jobStatus, nowElapsed,
jobStatus.isRequestedExpeditedJob() && canAffordExpeditedBillLocked(jobStatus));
@@ -326,6 +331,11 @@ public class TareController extends StateController {
@Override
@GuardedBy("mLock")
public void prepareForExecutionLocked(JobStatus jobStatus) {
+ if (jobStatus.shouldTreatAsUserInitiatedJob()) {
+ // TODO(202954395): consider noting execution with the EconomyManager even though it
+ // won't affect this job
+ return;
+ }
final int userId = jobStatus.getSourceUserId();
final String pkgName = jobStatus.getSourcePackageName();
ArrayMap<ActionBill, ArraySet<JobStatus>> billToJobMap =
@@ -337,7 +347,6 @@ public class TareController extends StateController {
removeJobFromBillList(jobStatus, billToJobMap.keyAt(i));
}
}
- addJobToBillList(jobStatus, getRunningBill(jobStatus));
final int uid = jobStatus.getSourceUid();
if (mService.getUidBias(uid) == JobInfo.BIAS_TOP_APP) {
@@ -347,6 +356,7 @@ public class TareController extends StateController {
mTopStartedJobs.add(jobStatus);
// Top jobs won't count towards quota so there's no need to involve the EconomyManager.
} else {
+ addJobToBillList(jobStatus, getRunningBill(jobStatus));
mEconomyManagerInternal.noteOngoingEventStarted(userId, pkgName,
getRunningActionId(jobStatus), String.valueOf(jobStatus.getJobId()));
}
@@ -355,11 +365,19 @@ public class TareController extends StateController {
@Override
@GuardedBy("mLock")
public void unprepareFromExecutionLocked(JobStatus jobStatus) {
+ if (jobStatus.shouldTreatAsUserInitiatedJob()) {
+ return;
+ }
final int userId = jobStatus.getSourceUserId();
final String pkgName = jobStatus.getSourcePackageName();
- mEconomyManagerInternal.noteOngoingEventStopped(userId, pkgName,
- getRunningActionId(jobStatus), String.valueOf(jobStatus.getJobId()));
- mTopStartedJobs.remove(jobStatus);
+ // If this method is called, then jobStatus.madeActive was never updated, so don't use it
+ // to determine if the EconomyManager was notified.
+ if (!mTopStartedJobs.remove(jobStatus)) {
+ // If the job was started while the app was top, then the EconomyManager wasn't notified
+ // of the job start.
+ mEconomyManagerInternal.noteOngoingEventStopped(userId, pkgName,
+ getRunningActionId(jobStatus), String.valueOf(jobStatus.getJobId()));
+ }
final ArraySet<ActionBill> bills = getPossibleStartBills(jobStatus);
ArrayMap<ActionBill, ArraySet<JobStatus>> billToJobMap =
@@ -378,13 +396,19 @@ public class TareController extends StateController {
@Override
@GuardedBy("mLock")
- public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob,
- boolean forUpdate) {
+ public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob) {
+ if (jobStatus.shouldTreatAsUserInitiatedJob()) {
+ return;
+ }
final int userId = jobStatus.getSourceUserId();
final String pkgName = jobStatus.getSourcePackageName();
- mEconomyManagerInternal.noteOngoingEventStopped(userId, pkgName,
- getRunningActionId(jobStatus), String.valueOf(jobStatus.getJobId()));
- mTopStartedJobs.remove(jobStatus);
+ if (!mTopStartedJobs.remove(jobStatus) && jobStatus.madeActive > 0) {
+ // Only note the job stop if we previously told the EconomyManager that the job started.
+ // If the job was started while the app was top, then the EconomyManager wasn't notified
+ // of the job start.
+ mEconomyManagerInternal.noteOngoingEventStopped(userId, pkgName,
+ getRunningActionId(jobStatus), String.valueOf(jobStatus.getJobId()));
+ }
ArrayMap<ActionBill, ArraySet<JobStatus>> billToJobMap =
mRegisteredBillsAndJobs.get(userId, pkgName);
if (billToJobMap != null) {
@@ -400,7 +424,7 @@ public class TareController extends StateController {
if (mIsEnabled != mConstants.USE_TARE_POLICY) {
mIsEnabled = mConstants.USE_TARE_POLICY;
// Update job bookkeeping out of band.
- JobSchedulerBackgroundThread.getHandler().post(() -> {
+ AppSchedulingModuleThread.getHandler().post(() -> {
synchronized (mLock) {
final long nowElapsed = sElapsedRealtimeClock.millis();
mService.getJobStore().forEachJob((jobStatus) -> {
@@ -629,6 +653,10 @@ public class TareController extends StateController {
if (!mIsEnabled) {
return true;
}
+ if (jobStatus.shouldTreatAsUserInitiatedJob()) {
+ // Always allow user-initiated jobs.
+ return true;
+ }
if (mService.getUidBias(jobStatus.getSourceUid()) == JobInfo.BIAS_TOP_APP
|| isTopStartedJobLocked(jobStatus)) {
// Jobs for the top app should always be allowed to run, and any jobs started while
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/TimeController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/TimeController.java
index 1cd1a4611a94..c272af00f040 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/TimeController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/TimeController.java
@@ -80,7 +80,7 @@ public final class TimeController extends StateController {
@Override
public void maybeStartTrackingJobLocked(JobStatus job, JobStatus lastJob) {
if (job.hasTimingDelayConstraint() || job.hasDeadlineConstraint()) {
- maybeStopTrackingJobLocked(job, null, false);
+ maybeStopTrackingJobLocked(job, null);
// First: check the constraints now, because if they are already satisfied
// then there is no need to track it. This gives us a fast path for a common
@@ -90,6 +90,7 @@ public final class TimeController extends StateController {
final long nowElapsedMillis = sElapsedRealtimeClock.millis();
if (job.hasDeadlineConstraint() && evaluateDeadlineConstraint(job, nowElapsedMillis)) {
// We're intentionally excluding jobs whose deadlines have passed
+ // from the job_scheduler.value_job_scheduler_job_deadline_expired_counter count
// (mostly like deadlines of 0) when the job was scheduled.
return;
} else if (job.hasTimingDelayConstraint() && evaluateTimingDelayConstraint(job,
@@ -137,8 +138,7 @@ public final class TimeController extends StateController {
* tracking was the one our alarms were based off of.
*/
@Override
- public void maybeStopTrackingJobLocked(JobStatus job, JobStatus incomingJob,
- boolean forUpdate) {
+ public void maybeStopTrackingJobLocked(JobStatus job, JobStatus incomingJob) {
if (job.clearTrackingController(JobStatus.TRACKING_TIME)) {
if (mTrackedJobs.remove(job)) {
checkExpiredDelaysAndResetAlarm();
@@ -162,6 +162,7 @@ public final class TimeController extends StateController {
// Scheduler.
mStateChangedListener.onRunJobNow(job);
}
+ mTrackedJobs.remove(job);
Counter.logIncrement(
"job_scheduler.value_job_scheduler_job_deadline_expired_counter");
} else if (wouldBeReadyWithConstraintLocked(job, JobStatus.CONSTRAINT_DEADLINE)) {
@@ -175,8 +176,11 @@ public final class TimeController extends StateController {
&& job.getEarliestRunTime() <= mNextDelayExpiredElapsedMillis) {
// Since this is just the delay, we don't need to rush the Scheduler to run the job
// immediately if the constraint is satisfied here.
- if (!evaluateTimingDelayConstraint(job, nowElapsedMillis)
- && wouldBeReadyWithConstraintLocked(job, JobStatus.CONSTRAINT_TIMING_DELAY)) {
+ if (evaluateTimingDelayConstraint(job, nowElapsedMillis)) {
+ if (canStopTrackingJobLocked(job)) {
+ mTrackedJobs.remove(job);
+ }
+ } else if (wouldBeReadyWithConstraintLocked(job, JobStatus.CONSTRAINT_TIMING_DELAY)) {
// This job's delay is earlier than the current set alarm. Update the alarm.
setDelayExpiredAlarmLocked(job.getEarliestRunTime(),
mService.deriveWorkSource(job.getSourceUid(), job.getSourcePackageName()));
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/CarIdlenessTracker.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/CarIdlenessTracker.java
index 9ada8dc3ef32..c458caec2873 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/CarIdlenessTracker.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/CarIdlenessTracker.java
@@ -24,6 +24,7 @@ import android.util.Log;
import android.util.Slog;
import android.util.proto.ProtoOutputStream;
+import com.android.server.AppSchedulingModuleThread;
import com.android.server.am.ActivityManagerService;
import com.android.server.job.JobSchedulerService;
import com.android.server.job.StateControllerProto;
@@ -50,7 +51,7 @@ public final class CarIdlenessTracker extends BroadcastReceiver implements Idlen
public static final String ACTION_UNFORCE_IDLE = "com.android.server.jobscheduler.UNFORCE_IDLE";
// After construction, mutations of idle/screen-on state will only happen
- // on the main looper thread, either in onReceive() or in an alarm callback.
+ // on the JobScheduler thread, either in onReceive() or in an alarm callback.
private boolean mIdle;
private boolean mGarageModeOn;
private boolean mForced;
@@ -90,7 +91,7 @@ public final class CarIdlenessTracker extends BroadcastReceiver implements Idlen
filter.addAction(ACTION_UNFORCE_IDLE);
filter.addAction(ActivityManagerService.ACTION_TRIGGER_IDLE);
- context.registerReceiver(this, filter);
+ context.registerReceiver(this, filter, null, AppSchedulingModuleThread.getHandler());
}
@Override
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/DeviceIdlenessTracker.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/DeviceIdlenessTracker.java
index 140cca679e14..c943e73eb12c 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/DeviceIdlenessTracker.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/DeviceIdlenessTracker.java
@@ -31,6 +31,7 @@ import android.util.Log;
import android.util.Slog;
import android.util.proto.ProtoOutputStream;
+import com.android.server.AppSchedulingModuleThread;
import com.android.server.am.ActivityManagerService;
import com.android.server.job.JobSchedulerService;
import com.android.server.job.StateControllerProto;
@@ -47,8 +48,8 @@ public final class DeviceIdlenessTracker extends BroadcastReceiver implements Id
private AlarmManager mAlarm;
private PowerManager mPowerManager;
- // After construction, mutations of idle/screen-on state will only happen
- // on the main looper thread, either in onReceive() or in an alarm callback.
+ // After construction, mutations of idle/screen-on/projection states will only happen
+ // on the JobScheduler thread, either in onReceive(), in an alarm callback, or in on.*Changed.
private long mInactivityIdleThreshold;
private long mIdleWindowSlop;
private boolean mIdle;
@@ -101,12 +102,10 @@ public final class DeviceIdlenessTracker extends BroadcastReceiver implements Id
filter.addAction(Intent.ACTION_DOCK_IDLE);
filter.addAction(Intent.ACTION_DOCK_ACTIVE);
- context.registerReceiver(this, filter);
+ context.registerReceiver(this, filter, null, AppSchedulingModuleThread.getHandler());
- // TODO(b/172579710): Move the callbacks off the main executor and on to
- // JobSchedulerBackgroundThread.getExecutor() once synchronization is fixed in this class.
context.getSystemService(UiModeManager.class).addOnProjectionStateChangedListener(
- UiModeManager.PROJECTION_TYPE_ALL, context.getMainExecutor(),
+ UiModeManager.PROJECTION_TYPE_ALL, AppSchedulingModuleThread.getExecutor(),
mOnProjectionStateChangedListener);
}
@@ -226,7 +225,8 @@ public final class DeviceIdlenessTracker extends BroadcastReceiver implements Id
Slog.v(TAG, "Scheduling idle : " + reason + " now:" + nowElapsed + " when=" + when);
}
mAlarm.setWindow(AlarmManager.ELAPSED_REALTIME_WAKEUP,
- when, mIdleWindowSlop, "JS idleness", mIdleAlarmListener, null);
+ when, mIdleWindowSlop, "JS idleness",
+ AppSchedulingModuleThread.getExecutor(), mIdleAlarmListener);
}
}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/restrictions/JobRestriction.java b/apex/jobscheduler/service/java/com/android/server/job/restrictions/JobRestriction.java
index 4067541b4646..7aab67a00b1d 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/restrictions/JobRestriction.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/restrictions/JobRestriction.java
@@ -18,6 +18,7 @@ package com.android.server.job.restrictions;
import android.app.job.JobInfo;
import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
import android.util.IndentingPrintWriter;
import android.util.proto.ProtoOutputStream;
@@ -28,20 +29,23 @@ import com.android.server.job.controllers.JobStatus;
* Used by {@link JobSchedulerService} to impose additional restrictions regarding whether jobs
* should be scheduled or not based on the state of the system/device.
* Every restriction is associated with exactly one stop reason, which could be retrieved using
- * {@link #getReason()} (and the internal reason via {@link #getInternalReason()}).
+ * {@link #getStopReason()}, one pending reason (retrievable via {@link #getPendingReason()},
+ * (and the internal reason via {@link #getInternalReason()}).
* Note, that this is not taken into account for the jobs that have
* {@link JobInfo#BIAS_FOREGROUND_SERVICE} bias or higher.
*/
public abstract class JobRestriction {
final JobSchedulerService mService;
- private final int mReason;
+ private final int mStopReason;
+ private final int mPendingReason;
private final int mInternalReason;
- JobRestriction(JobSchedulerService service, @JobParameters.StopReason int reason,
- int internalReason) {
+ protected JobRestriction(JobSchedulerService service, @JobParameters.StopReason int stopReason,
+ @JobScheduler.PendingJobReason int pendingReason, int internalReason) {
mService = service;
- mReason = reason;
+ mPendingReason = pendingReason;
+ mStopReason = stopReason;
mInternalReason = internalReason;
}
@@ -70,10 +74,15 @@ public abstract class JobRestriction {
public void dumpConstants(ProtoOutputStream proto) {
}
- /** @return reason code for the Restriction. */
+ @JobScheduler.PendingJobReason
+ public final int getPendingReason() {
+ return mPendingReason;
+ }
+
+ /** @return stop reason code for the Restriction. */
@JobParameters.StopReason
- public final int getReason() {
- return mReason;
+ public final int getStopReason() {
+ return mStopReason;
}
public final int getInternalReason() {
diff --git a/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java b/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java
index 40244e8cc386..ef634b565b65 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java
@@ -18,6 +18,7 @@ package com.android.server.job.restrictions;
import android.app.job.JobInfo;
import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
import android.os.PowerManager;
import android.os.PowerManager.OnThermalStatusChangedListener;
import android.util.IndentingPrintWriter;
@@ -42,6 +43,7 @@ public class ThermalStatusRestriction extends JobRestriction {
public ThermalStatusRestriction(JobSchedulerService service) {
super(service, JobParameters.STOP_REASON_DEVICE_STATE,
+ JobScheduler.PENDING_JOB_REASON_DEVICE_STATE,
JobParameters.INTERNAL_STOP_REASON_DEVICE_THERMAL);
}
@@ -73,9 +75,10 @@ public class ThermalStatusRestriction extends JobRestriction {
// bucket (thus resulting in us beginning to enforce the tightest
// restrictions).
|| (mThermalStatus < UPPER_THRESHOLD && status > UPPER_THRESHOLD);
+ final boolean increased = mThermalStatus < status;
mThermalStatus = status;
if (significantChange) {
- mService.onControllerStateChanged(null);
+ mService.onRestrictionStateChanged(ThermalStatusRestriction.this, increased);
}
}
});
@@ -88,17 +91,35 @@ public class ThermalStatusRestriction extends JobRestriction {
}
final int priority = job.getEffectivePriority();
if (mThermalStatus >= HIGHER_PRIORITY_THRESHOLD) {
- // For moderate throttling, only let expedited jobs and high priority regular jobs that
- // are already running run.
- return !job.shouldTreatAsExpeditedJob()
- && !(priority == JobInfo.PRIORITY_HIGH
- && mService.isCurrentlyRunningLocked(job));
+ // For moderate throttling:
+ // Let all user-initiated jobs run.
+ // Only let expedited jobs run if:
+ // 1. They haven't previously run
+ // 2. They're already running and aren't yet in overtime
+ // Only let high priority jobs run if:
+ // They are already running and aren't yet in overtime
+ // Don't let any other job run.
+ if (job.shouldTreatAsUserInitiatedJob()) {
+ return false;
+ }
+ if (job.shouldTreatAsExpeditedJob()) {
+ return job.getNumPreviousAttempts() > 0
+ || (mService.isCurrentlyRunningLocked(job)
+ && mService.isJobInOvertimeLocked(job));
+ }
+ if (priority == JobInfo.PRIORITY_HIGH) {
+ return !mService.isCurrentlyRunningLocked(job)
+ || mService.isJobInOvertimeLocked(job);
+ }
+ return true;
}
if (mThermalStatus >= LOW_PRIORITY_THRESHOLD) {
// For light throttling, throttle all min priority jobs and all low priority jobs that
- // aren't already running.
- return (priority == JobInfo.PRIORITY_LOW && !mService.isCurrentlyRunningLocked(job))
- || priority == JobInfo.PRIORITY_MIN;
+ // aren't already running or have been running for long enough.
+ return priority == JobInfo.PRIORITY_MIN
+ || (priority == JobInfo.PRIORITY_LOW
+ && (!mService.isCurrentlyRunningLocked(job)
+ || mService.isJobInOvertimeLocked(job)));
}
return false;
}
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/Agent.java b/apex/jobscheduler/service/java/com/android/server/tare/Agent.java
index 8b8a57d248b9..dcc324deaac6 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/Agent.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/Agent.java
@@ -16,9 +16,12 @@
package com.android.server.tare;
+import static android.app.tare.EconomyManager.ENABLED_MODE_OFF;
import static android.text.format.DateUtils.DAY_IN_MILLIS;
import static com.android.server.tare.EconomicPolicy.REGULATION_BASIC_INCOME;
+import static com.android.server.tare.EconomicPolicy.REGULATION_BG_RESTRICTED;
+import static com.android.server.tare.EconomicPolicy.REGULATION_BG_UNRESTRICTED;
import static com.android.server.tare.EconomicPolicy.REGULATION_BIRTHRIGHT;
import static com.android.server.tare.EconomicPolicy.REGULATION_DEMOTION;
import static com.android.server.tare.EconomicPolicy.REGULATION_PROMOTION;
@@ -34,8 +37,7 @@ import static com.android.server.tare.TareUtils.getCurrentTimeMillis;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageInfo;
+import android.content.pm.UserPackage;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
@@ -46,6 +48,7 @@ import android.util.IndentingPrintWriter;
import android.util.Log;
import android.util.Slog;
import android.util.SparseArrayMap;
+import android.util.SparseSetArray;
import android.util.TimeUtils;
import com.android.internal.annotations.GuardedBy;
@@ -162,8 +165,9 @@ class Agent {
}
@GuardedBy("mLock")
- private boolean isAffordableLocked(long balance, long price, long ctp) {
- return balance >= price && mScribe.getRemainingConsumableCakesLocked() >= ctp;
+ private boolean isAffordableLocked(long balance, long price, long stockLimitHonoringCtp) {
+ return balance >= price
+ && mScribe.getRemainingConsumableCakesLocked() >= stockLimitHonoringCtp;
}
@GuardedBy("mLock")
@@ -284,6 +288,7 @@ class Agent {
for (int i = 0; i < pkgNames.size(); ++i) {
final String pkgName = pkgNames.valueAt(i);
+ final boolean isVip = mIrs.isVip(userId, pkgName, nowElapsed);
SparseArrayMap<String, OngoingEvent> ongoingEvents =
mCurrentOngoingEvents.get(userId, pkgName);
if (ongoingEvents != null) {
@@ -298,9 +303,10 @@ class Agent {
for (int n = 0; n < size; ++n) {
final ActionAffordabilityNote note = actionAffordabilityNotes.valueAt(n);
note.recalculateCosts(economicPolicy, userId, pkgName);
- final boolean isAffordable =
- isAffordableLocked(newBalance,
- note.getCachedModifiedPrice(), note.getCtp());
+ final boolean isAffordable = isVip
+ || isAffordableLocked(newBalance,
+ note.getCachedModifiedPrice(),
+ note.getStockLimitHonoringCtp());
if (note.isCurrentlyAffordable() != isAffordable) {
note.setNewAffordability(isAffordable);
mIrs.postAffordabilityChanged(userId, pkgName, note);
@@ -313,6 +319,51 @@ class Agent {
}
@GuardedBy("mLock")
+ void onVipStatusChangedLocked(final int userId, @NonNull String pkgName) {
+ final long now = getCurrentTimeMillis();
+ final long nowElapsed = SystemClock.elapsedRealtime();
+ final CompleteEconomicPolicy economicPolicy = mIrs.getCompleteEconomicPolicyLocked();
+
+ final boolean isVip = mIrs.isVip(userId, pkgName, nowElapsed);
+ SparseArrayMap<String, OngoingEvent> ongoingEvents =
+ mCurrentOngoingEvents.get(userId, pkgName);
+ if (ongoingEvents != null) {
+ mOngoingEventUpdater.reset(userId, pkgName, now, nowElapsed);
+ ongoingEvents.forEach(mOngoingEventUpdater);
+ }
+ final ArraySet<ActionAffordabilityNote> actionAffordabilityNotes =
+ mActionAffordabilityNotes.get(userId, pkgName);
+ if (actionAffordabilityNotes != null) {
+ final int size = actionAffordabilityNotes.size();
+ final long newBalance =
+ mScribe.getLedgerLocked(userId, pkgName).getCurrentBalance();
+ for (int n = 0; n < size; ++n) {
+ final ActionAffordabilityNote note = actionAffordabilityNotes.valueAt(n);
+ note.recalculateCosts(economicPolicy, userId, pkgName);
+ final boolean isAffordable = isVip
+ || isAffordableLocked(newBalance,
+ note.getCachedModifiedPrice(), note.getStockLimitHonoringCtp());
+ if (note.isCurrentlyAffordable() != isAffordable) {
+ note.setNewAffordability(isAffordable);
+ mIrs.postAffordabilityChanged(userId, pkgName, note);
+ }
+ }
+ }
+ scheduleBalanceCheckLocked(userId, pkgName);
+ }
+
+ @GuardedBy("mLock")
+ void onVipStatusChangedLocked(@NonNull SparseSetArray<String> pkgs) {
+ for (int u = pkgs.size() - 1; u >= 0; --u) {
+ final int userId = pkgs.keyAt(u);
+
+ for (int p = pkgs.sizeAt(u) - 1; p >= 0; --p) {
+ onVipStatusChangedLocked(userId, pkgs.valueAt(u, p));
+ }
+ }
+ }
+
+ @GuardedBy("mLock")
private void onAnythingChangedLocked(final boolean updateOngoingEvents) {
final long now = getCurrentTimeMillis();
final long nowElapsed = SystemClock.elapsedRealtime();
@@ -349,12 +400,14 @@ class Agent {
if (actionAffordabilityNotes != null) {
final int size = actionAffordabilityNotes.size();
final long newBalance = getBalanceLocked(userId, pkgName);
+ final boolean isVip = mIrs.isVip(userId, pkgName, nowElapsed);
for (int n = 0; n < size; ++n) {
final ActionAffordabilityNote note = actionAffordabilityNotes.valueAt(n);
note.recalculateCosts(economicPolicy, userId, pkgName);
- final boolean isAffordable =
- isAffordableLocked(newBalance,
- note.getCachedModifiedPrice(), note.getCtp());
+ final boolean isAffordable = isVip
+ || isAffordableLocked(newBalance,
+ note.getCachedModifiedPrice(),
+ note.getStockLimitHonoringCtp());
if (note.isCurrentlyAffordable() != isAffordable) {
note.setNewAffordability(isAffordable);
mIrs.postAffordabilityChanged(userId, pkgName, note);
@@ -454,14 +507,23 @@ class Agent {
"Tried to adjust system balance for " + appToString(userId, pkgName));
return;
}
+ final boolean isVip = mIrs.isVip(userId, pkgName);
+ if (isVip) {
+ // This could happen if the app was made a VIP after it started performing actions.
+ // Continue recording the transaction for debugging purposes, but don't let it change
+ // any numbers.
+ transaction = new Ledger.Transaction(
+ transaction.startTimeMs, transaction.endTimeMs,
+ transaction.eventId, transaction.tag, 0 /* delta */, transaction.ctp);
+ }
final CompleteEconomicPolicy economicPolicy = mIrs.getCompleteEconomicPolicyLocked();
final long originalBalance = ledger.getCurrentBalance();
+ final long maxBalance = economicPolicy.getMaxSatiatedBalance(userId, pkgName);
if (transaction.delta > 0
- && originalBalance + transaction.delta > economicPolicy.getMaxSatiatedBalance()) {
+ && originalBalance + transaction.delta > maxBalance) {
// Set lower bound at 0 so we don't accidentally take away credits when we were trying
// to _give_ the app credits.
- final long newDelta =
- Math.max(0, economicPolicy.getMaxSatiatedBalance() - originalBalance);
+ final long newDelta = Math.max(0, maxBalance - originalBalance);
Slog.i(TAG, "Would result in becoming too rich. Decreasing transaction "
+ eventToString(transaction.eventId)
+ (transaction.tag == null ? "" : ":" + transaction.tag)
@@ -481,9 +543,9 @@ class Agent {
final long newBalance = ledger.getCurrentBalance();
for (int i = 0; i < actionAffordabilityNotes.size(); ++i) {
final ActionAffordabilityNote note = actionAffordabilityNotes.valueAt(i);
- final boolean isAffordable =
- isAffordableLocked(newBalance,
- note.getCachedModifiedPrice(), note.getCtp());
+ final boolean isAffordable = isVip
+ || isAffordableLocked(newBalance,
+ note.getCachedModifiedPrice(), note.getStockLimitHonoringCtp());
if (note.isCurrentlyAffordable() != isAffordable) {
note.setNewAffordability(isAffordable);
mIrs.postAffordabilityChanged(userId, pkgName, note);
@@ -497,6 +559,25 @@ class Agent {
}
}
+ @GuardedBy("mLock")
+ void reclaimAllAssetsLocked(final int userId, @NonNull final String pkgName, int regulationId) {
+ final Ledger ledger = mScribe.getLedgerLocked(userId, pkgName);
+ final long curBalance = ledger.getCurrentBalance();
+ if (curBalance <= 0) {
+ return;
+ }
+ if (DEBUG) {
+ Slog.i(TAG, "Reclaiming " + cakeToString(curBalance)
+ + " from " + appToString(userId, pkgName)
+ + " because of " + eventToString(regulationId));
+ }
+
+ final long now = getCurrentTimeMillis();
+ recordTransactionLocked(userId, pkgName, ledger,
+ new Ledger.Transaction(now, now, regulationId, null, -curBalance, 0),
+ true);
+ }
+
/**
* Reclaim a percentage of unused ARCs from every app that hasn't been used recently. The
* reclamation will not reduce an app's balance below its minimum balance as dictated by
@@ -507,7 +588,7 @@ class Agent {
* @param minUnusedTimeMs The minimum amount of time (in milliseconds) that must have
* transpired since the last user usage event before we will consider
* reclaiming ARCs from the app.
- * @param scaleMinBalance Whether or not to used the scaled minimum app balance. If false,
+ * @param scaleMinBalance Whether or not to use the scaled minimum app balance. If false,
* this will use the constant min balance floor given by
* {@link EconomicPolicy#getMinSatiatedBalance(int, String)}. If true,
* this will use the scaled balance given by
@@ -546,7 +627,8 @@ class Agent {
}
if (toReclaim > 0) {
if (DEBUG) {
- Slog.i(TAG, "Reclaiming unused wealth! Taking " + toReclaim
+ Slog.i(TAG, "Reclaiming unused wealth! Taking "
+ + cakeToString(toReclaim)
+ " from " + appToString(userId, pkgName));
}
@@ -605,16 +687,43 @@ class Agent {
}
}
+ /**
+ * Reclaim all ARCs from an app that was just restricted.
+ */
+ @GuardedBy("mLock")
+ void onAppRestrictedLocked(final int userId, @NonNull final String pkgName) {
+ reclaimAllAssetsLocked(userId, pkgName, REGULATION_BG_RESTRICTED);
+ }
+
+ /**
+ * Give an app that was just unrestricted some ARCs.
+ */
+ @GuardedBy("mLock")
+ void onAppUnrestrictedLocked(final int userId, @NonNull final String pkgName) {
+ final Ledger ledger = mScribe.getLedgerLocked(userId, pkgName);
+ if (ledger.getCurrentBalance() > 0) {
+ Slog.wtf(TAG, "App " + pkgName + " had credits while it was restricted");
+ // App already got credits somehow. Move along.
+ return;
+ }
+
+ final long now = getCurrentTimeMillis();
+
+ recordTransactionLocked(userId, pkgName, ledger,
+ new Ledger.Transaction(now, now, REGULATION_BG_UNRESTRICTED, null,
+ mIrs.getMinBalanceLocked(userId, pkgName), 0), true);
+ }
+
/** Returns true if an app should be given credits in the general distributions. */
- private boolean shouldGiveCredits(@NonNull PackageInfo packageInfo) {
- final ApplicationInfo applicationInfo = packageInfo.applicationInfo;
+ private boolean shouldGiveCredits(@NonNull InstalledPackageInfo packageInfo) {
// Skip apps that wouldn't be doing any work. Giving them ARCs would be wasteful.
- if (applicationInfo == null || !applicationInfo.hasCode()) {
+ if (!packageInfo.hasCode) {
return false;
}
- final int userId = UserHandle.getUserId(packageInfo.applicationInfo.uid);
+ final int userId = UserHandle.getUserId(packageInfo.uid);
// No point allocating ARCs to the system. It can do whatever it wants.
- return !mIrs.isSystem(userId, packageInfo.packageName);
+ return !mIrs.isSystem(userId, packageInfo.packageName)
+ && !mIrs.isPackageRestricted(userId, packageInfo.packageName);
}
void onCreditSupplyChanged() {
@@ -623,25 +732,28 @@ class Agent {
@GuardedBy("mLock")
void distributeBasicIncomeLocked(int batteryLevel) {
- List<PackageInfo> pkgs = mIrs.getInstalledPackages();
+ final SparseArrayMap<String, InstalledPackageInfo> pkgs = mIrs.getInstalledPackages();
final long now = getCurrentTimeMillis();
- for (int i = 0; i < pkgs.size(); ++i) {
- final PackageInfo pkgInfo = pkgs.get(i);
- if (!shouldGiveCredits(pkgInfo)) {
- continue;
- }
- final int userId = UserHandle.getUserId(pkgInfo.applicationInfo.uid);
- final String pkgName = pkgInfo.packageName;
- final Ledger ledger = mScribe.getLedgerLocked(userId, pkgName);
- final long minBalance = mIrs.getMinBalanceLocked(userId, pkgName);
- final double perc = batteryLevel / 100d;
- // TODO: maybe don't give credits to bankrupt apps until battery level >= 50%
- final long shortfall = minBalance - ledger.getCurrentBalance();
- if (shortfall > 0) {
- recordTransactionLocked(userId, pkgName, ledger,
- new Ledger.Transaction(now, now, REGULATION_BASIC_INCOME,
- null, (long) (perc * shortfall), 0), true);
+ for (int uIdx = pkgs.numMaps() - 1; uIdx >= 0; --uIdx) {
+ final int userId = pkgs.keyAt(uIdx);
+
+ for (int pIdx = pkgs.numElementsForKeyAt(uIdx) - 1; pIdx >= 0; --pIdx) {
+ final InstalledPackageInfo pkgInfo = pkgs.valueAt(uIdx, pIdx);
+ if (!shouldGiveCredits(pkgInfo)) {
+ continue;
+ }
+ final String pkgName = pkgInfo.packageName;
+ final Ledger ledger = mScribe.getLedgerLocked(userId, pkgName);
+ final long minBalance = mIrs.getMinBalanceLocked(userId, pkgName);
+ final double perc = batteryLevel / 100d;
+ // TODO: maybe don't give credits to bankrupt apps until battery level >= 50%
+ final long shortfall = minBalance - ledger.getCurrentBalance();
+ if (shortfall > 0) {
+ recordTransactionLocked(userId, pkgName, ledger,
+ new Ledger.Transaction(now, now, REGULATION_BASIC_INCOME,
+ null, (long) (perc * shortfall), 0), true);
+ }
}
}
}
@@ -659,11 +771,11 @@ class Agent {
@GuardedBy("mLock")
void grantBirthrightsLocked(final int userId) {
- final List<PackageInfo> pkgs = mIrs.getInstalledPackages(userId);
+ final List<InstalledPackageInfo> pkgs = mIrs.getInstalledPackages(userId);
final long now = getCurrentTimeMillis();
for (int i = 0; i < pkgs.size(); ++i) {
- final PackageInfo packageInfo = pkgs.get(i);
+ final InstalledPackageInfo packageInfo = pkgs.get(i);
if (!shouldGiveCredits(packageInfo)) {
continue;
}
@@ -715,37 +827,17 @@ class Agent {
@GuardedBy("mLock")
void onPackageRemovedLocked(final int userId, @NonNull final String pkgName) {
- reclaimAssetsLocked(userId, pkgName);
- mBalanceThresholdAlarmQueue.removeAlarmForKey(new Package(userId, pkgName));
- }
-
- /**
- * Reclaims any ARCs granted to the app, making them available to other apps. Also deletes the
- * app's ledger and stops any ongoing event tracking.
- */
- @GuardedBy("mLock")
- private void reclaimAssetsLocked(final int userId, @NonNull final String pkgName) {
- final Ledger ledger = mScribe.getLedgerLocked(userId, pkgName);
- if (ledger.getCurrentBalance() != 0) {
- mScribe.adjustRemainingConsumableCakesLocked(-ledger.getCurrentBalance());
- }
mScribe.discardLedgerLocked(userId, pkgName);
mCurrentOngoingEvents.delete(userId, pkgName);
+ mBalanceThresholdAlarmQueue.removeAlarmForKey(UserPackage.of(userId, pkgName));
}
@GuardedBy("mLock")
- void onUserRemovedLocked(final int userId, @NonNull final List<String> pkgNames) {
- reclaimAssetsLocked(userId, pkgNames);
+ void onUserRemovedLocked(final int userId) {
+ mCurrentOngoingEvents.delete(userId);
mBalanceThresholdAlarmQueue.removeAlarmsForUserId(userId);
}
- @GuardedBy("mLock")
- private void reclaimAssetsLocked(final int userId, @NonNull final List<String> pkgNames) {
- for (int i = 0; i < pkgNames.size(); ++i) {
- reclaimAssetsLocked(userId, pkgNames.get(i));
- }
- }
-
@VisibleForTesting
static class TrendCalculator implements Consumer<OngoingEvent> {
static final long WILL_NOT_CROSS_THRESHOLD = -1;
@@ -794,7 +886,7 @@ class Agent {
mUpperThreshold = (mUpperThreshold == Long.MIN_VALUE)
? price : Math.min(mUpperThreshold, price);
}
- final long ctp = note.getCtp();
+ final long ctp = note.getStockLimitHonoringCtp();
if (ctp <= mRemainingConsumableCredits) {
mCtpThreshold = Math.max(mCtpThreshold, ctp);
}
@@ -869,9 +961,9 @@ class Agent {
private void scheduleBalanceCheckLocked(final int userId, @NonNull final String pkgName) {
SparseArrayMap<String, OngoingEvent> ongoingEvents =
mCurrentOngoingEvents.get(userId, pkgName);
- if (ongoingEvents == null) {
+ if (ongoingEvents == null || mIrs.isVip(userId, pkgName)) {
// No ongoing transactions. No reason to schedule
- mBalanceThresholdAlarmQueue.removeAlarmForKey(new Package(userId, pkgName));
+ mBalanceThresholdAlarmQueue.removeAlarmForKey(UserPackage.of(userId, pkgName));
return;
}
mTrendCalculator.reset(getBalanceLocked(userId, pkgName),
@@ -884,7 +976,7 @@ class Agent {
if (lowerTimeMs == TrendCalculator.WILL_NOT_CROSS_THRESHOLD) {
if (upperTimeMs == TrendCalculator.WILL_NOT_CROSS_THRESHOLD) {
// Will never cross a threshold based on current events.
- mBalanceThresholdAlarmQueue.removeAlarmForKey(new Package(userId, pkgName));
+ mBalanceThresholdAlarmQueue.removeAlarmForKey(UserPackage.of(userId, pkgName));
return;
}
timeToThresholdMs = upperTimeMs;
@@ -892,7 +984,7 @@ class Agent {
timeToThresholdMs = (upperTimeMs == TrendCalculator.WILL_NOT_CROSS_THRESHOLD)
? lowerTimeMs : Math.min(lowerTimeMs, upperTimeMs);
}
- mBalanceThresholdAlarmQueue.addAlarm(new Package(userId, pkgName),
+ mBalanceThresholdAlarmQueue.addAlarm(UserPackage.of(userId, pkgName),
SystemClock.elapsedRealtime() + timeToThresholdMs);
}
@@ -983,57 +1075,22 @@ class Agent {
private final OngoingEventUpdater mOngoingEventUpdater = new OngoingEventUpdater();
- private static final class Package {
- public final String packageName;
- public final int userId;
-
- Package(int userId, String packageName) {
- this.userId = userId;
- this.packageName = packageName;
- }
-
- @Override
- public String toString() {
- return appToString(userId, packageName);
- }
-
- @Override
- public boolean equals(Object obj) {
- if (obj == null) {
- return false;
- }
- if (this == obj) {
- return true;
- }
- if (obj instanceof Package) {
- Package other = (Package) obj;
- return userId == other.userId && Objects.equals(packageName, other.packageName);
- }
- return false;
- }
-
- @Override
- public int hashCode() {
- return packageName.hashCode() + userId;
- }
- }
-
/** Track when apps will cross the closest affordability threshold (in both directions). */
- private class BalanceThresholdAlarmQueue extends AlarmQueue<Package> {
+ private class BalanceThresholdAlarmQueue extends AlarmQueue<UserPackage> {
private BalanceThresholdAlarmQueue(Context context, Looper looper) {
super(context, looper, ALARM_TAG_AFFORDABILITY_CHECK, "Affordability check", true,
15_000L);
}
@Override
- protected boolean isForUser(@NonNull Package key, int userId) {
+ protected boolean isForUser(@NonNull UserPackage key, int userId) {
return key.userId == userId;
}
@Override
- protected void processExpiredAlarms(@NonNull ArraySet<Package> expired) {
+ protected void processExpiredAlarms(@NonNull ArraySet<UserPackage> expired) {
for (int i = 0; i < expired.size(); ++i) {
- Package p = expired.valueAt(i);
+ UserPackage p = expired.valueAt(i);
mHandler.obtainMessage(
MSG_CHECK_INDIVIDUAL_AFFORDABILITY, p.userId, 0, p.packageName)
.sendToTarget();
@@ -1055,17 +1112,18 @@ class Agent {
final ActionAffordabilityNote note =
new ActionAffordabilityNote(bill, listener, economicPolicy);
if (actionAffordabilityNotes.add(note)) {
- if (!mIrs.isEnabled()) {
+ if (mIrs.getEnabledMode() == ENABLED_MODE_OFF) {
// When TARE isn't enabled, we always say something is affordable. We also don't
// want to silently drop affordability change listeners in case TARE becomes enabled
// because then clients will be in an ambiguous state.
note.setNewAffordability(true);
return;
}
+ final boolean isVip = mIrs.isVip(userId, pkgName);
note.recalculateCosts(economicPolicy, userId, pkgName);
- note.setNewAffordability(
- isAffordableLocked(getBalanceLocked(userId, pkgName),
- note.getCachedModifiedPrice(), note.getCtp()));
+ note.setNewAffordability(isVip
+ || isAffordableLocked(getBalanceLocked(userId, pkgName),
+ note.getCachedModifiedPrice(), note.getStockLimitHonoringCtp()));
mIrs.postAffordabilityChanged(userId, pkgName, note);
// Update ongoing alarm
scheduleBalanceCheckLocked(userId, pkgName);
@@ -1092,7 +1150,7 @@ class Agent {
static final class ActionAffordabilityNote {
private final EconomyManagerInternal.ActionBill mActionBill;
private final EconomyManagerInternal.AffordabilityChangeListener mListener;
- private long mCtp;
+ private long mStockLimitHonoringCtp;
private long mModifiedPrice;
private boolean mIsAffordable;
@@ -1107,7 +1165,11 @@ class Agent {
final EconomyManagerInternal.AnticipatedAction aa = anticipatedActions.get(i);
final EconomicPolicy.Action action = economicPolicy.getAction(aa.actionId);
if (action == null) {
- throw new IllegalArgumentException("Invalid action id: " + aa.actionId);
+ if ((aa.actionId & EconomicPolicy.ALL_POLICIES) == 0) {
+ throw new IllegalArgumentException("Invalid action id: " + aa.actionId);
+ } else {
+ Slog.w(TAG, "Tracking disabled policy's action? " + aa.actionId);
+ }
}
}
mListener = listener;
@@ -1127,29 +1189,34 @@ class Agent {
return mModifiedPrice;
}
- private long getCtp() {
- return mCtp;
+ /** Returns the cumulative CTP of actions in this note that respect the stock limit. */
+ private long getStockLimitHonoringCtp() {
+ return mStockLimitHonoringCtp;
}
@VisibleForTesting
void recalculateCosts(@NonNull EconomicPolicy economicPolicy,
int userId, @NonNull String pkgName) {
long modifiedPrice = 0;
- long ctp = 0;
+ long stockLimitHonoringCtp = 0;
final List<EconomyManagerInternal.AnticipatedAction> anticipatedActions =
mActionBill.getAnticipatedActions();
for (int i = 0; i < anticipatedActions.size(); ++i) {
final EconomyManagerInternal.AnticipatedAction aa = anticipatedActions.get(i);
+ final EconomicPolicy.Action action = economicPolicy.getAction(aa.actionId);
final EconomicPolicy.Cost actionCost =
economicPolicy.getCostOfAction(aa.actionId, userId, pkgName);
modifiedPrice += actionCost.price * aa.numInstantaneousCalls
+ actionCost.price * (aa.ongoingDurationMs / 1000);
- ctp += actionCost.costToProduce * aa.numInstantaneousCalls
- + actionCost.costToProduce * (aa.ongoingDurationMs / 1000);
+ if (action.respectsStockLimit) {
+ stockLimitHonoringCtp +=
+ actionCost.costToProduce * aa.numInstantaneousCalls
+ + actionCost.costToProduce * (aa.ongoingDurationMs / 1000);
+ }
}
mModifiedPrice = modifiedPrice;
- mCtp = ctp;
+ mStockLimitHonoringCtp = stockLimitHonoringCtp;
}
boolean isCurrentlyAffordable() {
@@ -1203,12 +1270,14 @@ class Agent {
if (actionAffordabilityNotes != null
&& actionAffordabilityNotes.size() > 0) {
final long newBalance = getBalanceLocked(userId, pkgName);
+ final boolean isVip = mIrs.isVip(userId, pkgName);
for (int i = 0; i < actionAffordabilityNotes.size(); ++i) {
final ActionAffordabilityNote note =
actionAffordabilityNotes.valueAt(i);
- final boolean isAffordable = isAffordableLocked(
- newBalance, note.getCachedModifiedPrice(), note.getCtp());
+ final boolean isAffordable = isVip || isAffordableLocked(
+ newBalance, note.getCachedModifiedPrice(),
+ note.getStockLimitHonoringCtp());
if (note.isCurrentlyAffordable() != isAffordable) {
note.setNewAffordability(isAffordable);
mIrs.postAffordabilityChanged(userId, pkgName, note);
@@ -1225,7 +1294,6 @@ class Agent {
@GuardedBy("mLock")
void dumpLocked(IndentingPrintWriter pw) {
- pw.println();
mBalanceThresholdAlarmQueue.dump(pw);
pw.println();
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/AlarmManagerEconomicPolicy.java b/apex/jobscheduler/service/java/com/android/server/tare/AlarmManagerEconomicPolicy.java
index d0f719b13b89..d2150b80761f 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/AlarmManagerEconomicPolicy.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/AlarmManagerEconomicPolicy.java
@@ -33,10 +33,12 @@ import static android.app.tare.EconomyManager.DEFAULT_AM_ACTION_ALARM_INEXACT_NO
import static android.app.tare.EconomyManager.DEFAULT_AM_ACTION_ALARM_INEXACT_NONWAKEUP_CTP_CAKES;
import static android.app.tare.EconomyManager.DEFAULT_AM_ACTION_ALARM_INEXACT_WAKEUP_BASE_PRICE_CAKES;
import static android.app.tare.EconomyManager.DEFAULT_AM_ACTION_ALARM_INEXACT_WAKEUP_CTP_CAKES;
-import static android.app.tare.EconomyManager.DEFAULT_AM_HARD_CONSUMPTION_LIMIT_CAKES;
import static android.app.tare.EconomyManager.DEFAULT_AM_INITIAL_CONSUMPTION_LIMIT_CAKES;
+import static android.app.tare.EconomyManager.DEFAULT_AM_MAX_CONSUMPTION_LIMIT_CAKES;
import static android.app.tare.EconomyManager.DEFAULT_AM_MAX_SATIATED_BALANCE_CAKES;
+import static android.app.tare.EconomyManager.DEFAULT_AM_MIN_CONSUMPTION_LIMIT_CAKES;
import static android.app.tare.EconomyManager.DEFAULT_AM_MIN_SATIATED_BALANCE_EXEMPTED_CAKES;
+import static android.app.tare.EconomyManager.DEFAULT_AM_MIN_SATIATED_BALANCE_HEADLESS_SYSTEM_APP_CAKES;
import static android.app.tare.EconomyManager.DEFAULT_AM_MIN_SATIATED_BALANCE_OTHER_APP_CAKES;
import static android.app.tare.EconomyManager.DEFAULT_AM_REWARD_NOTIFICATION_INTERACTION_INSTANT_CAKES;
import static android.app.tare.EconomyManager.DEFAULT_AM_REWARD_NOTIFICATION_INTERACTION_MAX_CAKES;
@@ -71,10 +73,12 @@ import static android.app.tare.EconomyManager.KEY_AM_ACTION_ALARM_INEXACT_NONWAK
import static android.app.tare.EconomyManager.KEY_AM_ACTION_ALARM_INEXACT_NONWAKEUP_CTP;
import static android.app.tare.EconomyManager.KEY_AM_ACTION_ALARM_INEXACT_WAKEUP_BASE_PRICE;
import static android.app.tare.EconomyManager.KEY_AM_ACTION_ALARM_INEXACT_WAKEUP_CTP;
-import static android.app.tare.EconomyManager.KEY_AM_HARD_CONSUMPTION_LIMIT;
import static android.app.tare.EconomyManager.KEY_AM_INITIAL_CONSUMPTION_LIMIT;
+import static android.app.tare.EconomyManager.KEY_AM_MAX_CONSUMPTION_LIMIT;
import static android.app.tare.EconomyManager.KEY_AM_MAX_SATIATED_BALANCE;
+import static android.app.tare.EconomyManager.KEY_AM_MIN_CONSUMPTION_LIMIT;
import static android.app.tare.EconomyManager.KEY_AM_MIN_SATIATED_BALANCE_EXEMPTED;
+import static android.app.tare.EconomyManager.KEY_AM_MIN_SATIATED_BALANCE_HEADLESS_SYSTEM_APP;
import static android.app.tare.EconomyManager.KEY_AM_MIN_SATIATED_BALANCE_OTHER_APP;
import static android.app.tare.EconomyManager.KEY_AM_REWARD_NOTIFICATION_INTERACTION_INSTANT;
import static android.app.tare.EconomyManager.KEY_AM_REWARD_NOTIFICATION_INTERACTION_MAX;
@@ -91,6 +95,7 @@ import static android.app.tare.EconomyManager.KEY_AM_REWARD_TOP_ACTIVITY_ONGOING
import static android.app.tare.EconomyManager.KEY_AM_REWARD_WIDGET_INTERACTION_INSTANT;
import static android.app.tare.EconomyManager.KEY_AM_REWARD_WIDGET_INTERACTION_MAX;
import static android.app.tare.EconomyManager.KEY_AM_REWARD_WIDGET_INTERACTION_ONGOING;
+import static android.app.tare.EconomyManager.arcToCake;
import static android.provider.Settings.Global.TARE_ALARM_MANAGER_CONSTANTS;
import static com.android.server.tare.Modifier.COST_MODIFIER_CHARGING;
@@ -103,7 +108,6 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ContentResolver;
import android.provider.DeviceConfig;
-import android.provider.Settings;
import android.util.IndentingPrintWriter;
import android.util.KeyValueListParser;
import android.util.Slog;
@@ -117,23 +121,23 @@ public class AlarmManagerEconomicPolicy extends EconomicPolicy {
private static final String TAG = "TARE- " + AlarmManagerEconomicPolicy.class.getSimpleName();
public static final int ACTION_ALARM_WAKEUP_EXACT_ALLOW_WHILE_IDLE =
- TYPE_ACTION | POLICY_AM | 0;
+ TYPE_ACTION | POLICY_ALARM | 0;
public static final int ACTION_ALARM_WAKEUP_EXACT =
- TYPE_ACTION | POLICY_AM | 1;
+ TYPE_ACTION | POLICY_ALARM | 1;
public static final int ACTION_ALARM_WAKEUP_INEXACT_ALLOW_WHILE_IDLE =
- TYPE_ACTION | POLICY_AM | 2;
+ TYPE_ACTION | POLICY_ALARM | 2;
public static final int ACTION_ALARM_WAKEUP_INEXACT =
- TYPE_ACTION | POLICY_AM | 3;
+ TYPE_ACTION | POLICY_ALARM | 3;
public static final int ACTION_ALARM_NONWAKEUP_EXACT_ALLOW_WHILE_IDLE =
- TYPE_ACTION | POLICY_AM | 4;
+ TYPE_ACTION | POLICY_ALARM | 4;
public static final int ACTION_ALARM_NONWAKEUP_EXACT =
- TYPE_ACTION | POLICY_AM | 5;
+ TYPE_ACTION | POLICY_ALARM | 5;
public static final int ACTION_ALARM_NONWAKEUP_INEXACT_ALLOW_WHILE_IDLE =
- TYPE_ACTION | POLICY_AM | 6;
+ TYPE_ACTION | POLICY_ALARM | 6;
public static final int ACTION_ALARM_NONWAKEUP_INEXACT =
- TYPE_ACTION | POLICY_AM | 7;
+ TYPE_ACTION | POLICY_ALARM | 7;
public static final int ACTION_ALARM_CLOCK =
- TYPE_ACTION | POLICY_AM | 8;
+ TYPE_ACTION | POLICY_ALARM | 8;
private static final int[] COST_MODIFIERS = new int[]{
COST_MODIFIER_CHARGING,
@@ -143,42 +147,53 @@ public class AlarmManagerEconomicPolicy extends EconomicPolicy {
};
private long mMinSatiatedBalanceExempted;
+ private long mMinSatiatedBalanceHeadlessSystemApp;
private long mMinSatiatedBalanceOther;
private long mMaxSatiatedBalance;
private long mInitialSatiatedConsumptionLimit;
- private long mHardSatiatedConsumptionLimit;
+ private long mMinSatiatedConsumptionLimit;
+ private long mMaxSatiatedConsumptionLimit;
private final KeyValueListParser mParser = new KeyValueListParser(',');
- private final InternalResourceService mInternalResourceService;
+ private final Injector mInjector;
private final SparseArray<Action> mActions = new SparseArray<>();
private final SparseArray<Reward> mRewards = new SparseArray<>();
- AlarmManagerEconomicPolicy(InternalResourceService irs) {
+ AlarmManagerEconomicPolicy(InternalResourceService irs, Injector injector) {
super(irs);
- mInternalResourceService = irs;
+ mInjector = injector;
loadConstants("", null);
}
@Override
void setup(@NonNull DeviceConfig.Properties properties) {
super.setup(properties);
- ContentResolver resolver = mInternalResourceService.getContext().getContentResolver();
- loadConstants(Settings.Global.getString(resolver, TARE_ALARM_MANAGER_CONSTANTS),
+ ContentResolver resolver = mIrs.getContext().getContentResolver();
+ loadConstants(mInjector.getSettingsGlobalString(resolver, TARE_ALARM_MANAGER_CONSTANTS),
properties);
}
@Override
long getMinSatiatedBalance(final int userId, @NonNull final String pkgName) {
- if (mInternalResourceService.isPackageExempted(userId, pkgName)) {
+ if (mIrs.isPackageRestricted(userId, pkgName)) {
+ return 0;
+ }
+ if (mIrs.isPackageExempted(userId, pkgName)) {
return mMinSatiatedBalanceExempted;
}
+ if (mIrs.isHeadlessSystemApp(userId, pkgName)) {
+ return mMinSatiatedBalanceHeadlessSystemApp;
+ }
// TODO: take other exemptions into account
return mMinSatiatedBalanceOther;
}
@Override
- long getMaxSatiatedBalance() {
+ long getMaxSatiatedBalance(int userId, @NonNull String pkgName) {
+ if (mIrs.isPackageRestricted(userId, pkgName)) {
+ return 0;
+ }
// TODO(230501287): adjust balance based on whether the app has the SCHEDULE_EXACT_ALARM
// permission granted. Apps without the permission granted shouldn't need a high balance
// since they won't be able to use exact alarms. Apps with the permission granted could
@@ -193,8 +208,13 @@ public class AlarmManagerEconomicPolicy extends EconomicPolicy {
}
@Override
- long getHardSatiatedConsumptionLimit() {
- return mHardSatiatedConsumptionLimit;
+ long getMinSatiatedConsumptionLimit() {
+ return mMinSatiatedConsumptionLimit;
+ }
+
+ @Override
+ long getMaxSatiatedConsumptionLimit() {
+ return mMaxSatiatedConsumptionLimit;
}
@NonNull
@@ -226,31 +246,44 @@ public class AlarmManagerEconomicPolicy extends EconomicPolicy {
Slog.e(TAG, "Global setting key incorrect: ", e);
}
+ mMinSatiatedBalanceOther = getConstantAsCake(mParser, properties,
+ KEY_AM_MIN_SATIATED_BALANCE_OTHER_APP, DEFAULT_AM_MIN_SATIATED_BALANCE_OTHER_APP_CAKES);
+ mMinSatiatedBalanceHeadlessSystemApp = getConstantAsCake(mParser, properties,
+ KEY_AM_MIN_SATIATED_BALANCE_HEADLESS_SYSTEM_APP,
+ DEFAULT_AM_MIN_SATIATED_BALANCE_HEADLESS_SYSTEM_APP_CAKES,
+ mMinSatiatedBalanceOther);
mMinSatiatedBalanceExempted = getConstantAsCake(mParser, properties,
KEY_AM_MIN_SATIATED_BALANCE_EXEMPTED,
- DEFAULT_AM_MIN_SATIATED_BALANCE_EXEMPTED_CAKES);
- mMinSatiatedBalanceOther = getConstantAsCake(mParser, properties,
- KEY_AM_MIN_SATIATED_BALANCE_OTHER_APP,
- DEFAULT_AM_MIN_SATIATED_BALANCE_OTHER_APP_CAKES);
+ DEFAULT_AM_MIN_SATIATED_BALANCE_EXEMPTED_CAKES,
+ mMinSatiatedBalanceHeadlessSystemApp);
mMaxSatiatedBalance = getConstantAsCake(mParser, properties,
- KEY_AM_MAX_SATIATED_BALANCE,
- DEFAULT_AM_MAX_SATIATED_BALANCE_CAKES);
+ KEY_AM_MAX_SATIATED_BALANCE, DEFAULT_AM_MAX_SATIATED_BALANCE_CAKES,
+ Math.max(arcToCake(1), mMinSatiatedBalanceExempted));
+ mMinSatiatedConsumptionLimit = getConstantAsCake(mParser, properties,
+ KEY_AM_MIN_CONSUMPTION_LIMIT, DEFAULT_AM_MIN_CONSUMPTION_LIMIT_CAKES,
+ arcToCake(1));
mInitialSatiatedConsumptionLimit = getConstantAsCake(mParser, properties,
- KEY_AM_INITIAL_CONSUMPTION_LIMIT, DEFAULT_AM_INITIAL_CONSUMPTION_LIMIT_CAKES);
- mHardSatiatedConsumptionLimit = Math.max(mInitialSatiatedConsumptionLimit,
- getConstantAsCake(mParser, properties,
- KEY_AM_HARD_CONSUMPTION_LIMIT, DEFAULT_AM_HARD_CONSUMPTION_LIMIT_CAKES));
+ KEY_AM_INITIAL_CONSUMPTION_LIMIT, DEFAULT_AM_INITIAL_CONSUMPTION_LIMIT_CAKES,
+ mMinSatiatedConsumptionLimit);
+ mMaxSatiatedConsumptionLimit = getConstantAsCake(mParser, properties,
+ KEY_AM_MAX_CONSUMPTION_LIMIT, DEFAULT_AM_MAX_CONSUMPTION_LIMIT_CAKES,
+ mInitialSatiatedConsumptionLimit);
final long exactAllowWhileIdleWakeupBasePrice = getConstantAsCake(mParser, properties,
KEY_AM_ACTION_ALARM_ALLOW_WHILE_IDLE_EXACT_WAKEUP_BASE_PRICE,
DEFAULT_AM_ACTION_ALARM_ALLOW_WHILE_IDLE_EXACT_WAKEUP_BASE_PRICE_CAKES);
+ // Apps must hold the SCHEDULE_EXACT_ALARM or USE_EXACT_ALARMS permission in order to use
+ // exact alarms. Since the user has the option of granting/revoking the permission, we can
+ // be a little lenient on the action cost checks and only stop the action if the app has
+ // run out of credits, and not when the system has run out of stock.
mActions.put(ACTION_ALARM_WAKEUP_EXACT_ALLOW_WHILE_IDLE,
new Action(ACTION_ALARM_WAKEUP_EXACT_ALLOW_WHILE_IDLE,
getConstantAsCake(mParser, properties,
KEY_AM_ACTION_ALARM_ALLOW_WHILE_IDLE_EXACT_WAKEUP_CTP,
DEFAULT_AM_ACTION_ALARM_ALLOW_WHILE_IDLE_EXACT_WAKEUP_CTP_CAKES),
- exactAllowWhileIdleWakeupBasePrice));
+ exactAllowWhileIdleWakeupBasePrice,
+ /* respectsStockLimit */ false));
mActions.put(ACTION_ALARM_WAKEUP_EXACT,
new Action(ACTION_ALARM_WAKEUP_EXACT,
getConstantAsCake(mParser, properties,
@@ -258,7 +291,8 @@ public class AlarmManagerEconomicPolicy extends EconomicPolicy {
DEFAULT_AM_ACTION_ALARM_EXACT_WAKEUP_CTP_CAKES),
getConstantAsCake(mParser, properties,
KEY_AM_ACTION_ALARM_EXACT_WAKEUP_BASE_PRICE,
- DEFAULT_AM_ACTION_ALARM_EXACT_WAKEUP_BASE_PRICE_CAKES)));
+ DEFAULT_AM_ACTION_ALARM_EXACT_WAKEUP_BASE_PRICE_CAKES),
+ /* respectsStockLimit */ false));
final long inexactAllowWhileIdleWakeupBasePrice =
getConstantAsCake(mParser, properties,
@@ -270,7 +304,8 @@ public class AlarmManagerEconomicPolicy extends EconomicPolicy {
getConstantAsCake(mParser, properties,
KEY_AM_ACTION_ALARM_ALLOW_WHILE_IDLE_INEXACT_WAKEUP_CTP,
DEFAULT_AM_ACTION_ALARM_ALLOW_WHILE_IDLE_INEXACT_WAKEUP_CTP_CAKES),
- inexactAllowWhileIdleWakeupBasePrice));
+ inexactAllowWhileIdleWakeupBasePrice,
+ /* respectsStockLimit */ false));
mActions.put(ACTION_ALARM_WAKEUP_INEXACT,
new Action(ACTION_ALARM_WAKEUP_INEXACT,
getConstantAsCake(mParser, properties,
@@ -278,7 +313,8 @@ public class AlarmManagerEconomicPolicy extends EconomicPolicy {
DEFAULT_AM_ACTION_ALARM_INEXACT_WAKEUP_CTP_CAKES),
getConstantAsCake(mParser, properties,
KEY_AM_ACTION_ALARM_INEXACT_WAKEUP_BASE_PRICE,
- DEFAULT_AM_ACTION_ALARM_INEXACT_WAKEUP_BASE_PRICE_CAKES)));
+ DEFAULT_AM_ACTION_ALARM_INEXACT_WAKEUP_BASE_PRICE_CAKES),
+ /* respectsStockLimit */ false));
final long exactAllowWhileIdleNonWakeupBasePrice = getConstantAsCake(mParser, properties,
KEY_AM_ACTION_ALARM_ALLOW_WHILE_IDLE_EXACT_NONWAKEUP_BASE_PRICE,
@@ -288,7 +324,8 @@ public class AlarmManagerEconomicPolicy extends EconomicPolicy {
getConstantAsCake(mParser, properties,
KEY_AM_ACTION_ALARM_ALLOW_WHILE_IDLE_EXACT_NONWAKEUP_CTP,
DEFAULT_AM_ACTION_ALARM_ALLOW_WHILE_IDLE_EXACT_NONWAKEUP_CTP_CAKES),
- exactAllowWhileIdleNonWakeupBasePrice));
+ exactAllowWhileIdleNonWakeupBasePrice,
+ /* respectsStockLimit */ false));
mActions.put(ACTION_ALARM_NONWAKEUP_EXACT,
new Action(ACTION_ALARM_NONWAKEUP_EXACT,
@@ -297,7 +334,8 @@ public class AlarmManagerEconomicPolicy extends EconomicPolicy {
DEFAULT_AM_ACTION_ALARM_EXACT_NONWAKEUP_CTP_CAKES),
getConstantAsCake(mParser, properties,
KEY_AM_ACTION_ALARM_EXACT_NONWAKEUP_BASE_PRICE,
- DEFAULT_AM_ACTION_ALARM_EXACT_NONWAKEUP_BASE_PRICE_CAKES)));
+ DEFAULT_AM_ACTION_ALARM_EXACT_NONWAKEUP_BASE_PRICE_CAKES),
+ /* respectsStockLimit */ false));
final long inexactAllowWhileIdleNonWakeupBasePrice = getConstantAsCake(mParser, properties,
KEY_AM_ACTION_ALARM_ALLOW_WHILE_IDLE_INEXACT_NONWAKEUP_BASE_PRICE,
@@ -325,7 +363,8 @@ public class AlarmManagerEconomicPolicy extends EconomicPolicy {
DEFAULT_AM_ACTION_ALARM_ALARMCLOCK_CTP_CAKES),
getConstantAsCake(mParser, properties,
KEY_AM_ACTION_ALARM_ALARMCLOCK_BASE_PRICE,
- DEFAULT_AM_ACTION_ALARM_ALARMCLOCK_BASE_PRICE_CAKES)));
+ DEFAULT_AM_ACTION_ALARM_ALARMCLOCK_BASE_PRICE_CAKES),
+ /* respectsStockLimit */ false));
mRewards.put(REWARD_TOP_ACTIVITY, new Reward(REWARD_TOP_ACTIVITY,
getConstantAsCake(mParser, properties,
@@ -390,9 +429,11 @@ public class AlarmManagerEconomicPolicy extends EconomicPolicy {
pw.decreaseIndent();
pw.print("Max satiated balance", cakeToString(mMaxSatiatedBalance)).println();
pw.print("Consumption limits: [");
+ pw.print(cakeToString(mMinSatiatedConsumptionLimit));
+ pw.print(", ");
pw.print(cakeToString(mInitialSatiatedConsumptionLimit));
pw.print(", ");
- pw.print(cakeToString(mHardSatiatedConsumptionLimit));
+ pw.print(cakeToString(mMaxSatiatedConsumptionLimit));
pw.println("]");
pw.println();
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/Analyst.java b/apex/jobscheduler/service/java/com/android/server/tare/Analyst.java
index bc6fe7e5a535..06333f16dbf2 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/Analyst.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/Analyst.java
@@ -16,6 +16,8 @@
package com.android.server.tare;
+import static android.text.format.DateUtils.HOUR_IN_MILLIS;
+
import static com.android.server.tare.EconomicPolicy.TYPE_ACTION;
import static com.android.server.tare.EconomicPolicy.TYPE_REGULATION;
import static com.android.server.tare.EconomicPolicy.TYPE_REWARD;
@@ -23,9 +25,16 @@ import static com.android.server.tare.EconomicPolicy.getEventType;
import static com.android.server.tare.TareUtils.cakeToString;
import android.annotation.NonNull;
+import android.os.BatteryManagerInternal;
+import android.os.RemoteException;
import android.util.IndentingPrintWriter;
import android.util.Log;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.app.IBatteryStats;
+import com.android.server.LocalServices;
+import com.android.server.am.BatteryStatsService;
+
import java.util.ArrayList;
import java.util.List;
@@ -38,6 +47,8 @@ public class Analyst {
|| Log.isLoggable(TAG, Log.DEBUG);
private static final int NUM_PERIODS_TO_RETAIN = 8;
+ @VisibleForTesting
+ static final long MIN_REPORT_DURATION_FOR_RESET = 24 * HOUR_IN_MILLIS;
static final class Report {
/** How much the battery was discharged over the tracked period. */
@@ -73,6 +84,22 @@ public class Analyst {
public long cumulativeNegativeRegulations = 0;
public int numNegativeRegulations = 0;
+ /**
+ * The approximate amount of time the screen has been off while on battery while this
+ * report has been active.
+ */
+ public long screenOffDurationMs = 0;
+ /**
+ * The approximate amount of battery discharge while this report has been active.
+ */
+ public long screenOffDischargeMah = 0;
+ /** The offset used to get the delta when polling the screen off time from BatteryStats. */
+ private long bsScreenOffRealtimeBase = 0;
+ /**
+ * The offset used to get the delta when polling the screen off discharge from BatteryStats.
+ */
+ private long bsScreenOffDischargeMahBase = 0;
+
private void clear() {
cumulativeBatteryDischarge = 0;
currentBatteryLevel = 0;
@@ -86,13 +113,27 @@ public class Analyst {
numPositiveRegulations = 0;
cumulativeNegativeRegulations = 0;
numNegativeRegulations = 0;
+ screenOffDurationMs = 0;
+ screenOffDischargeMah = 0;
+ bsScreenOffRealtimeBase = 0;
+ bsScreenOffDischargeMahBase = 0;
}
}
+ private final IBatteryStats mIBatteryStats;
+
private int mPeriodIndex = 0;
/** How much the battery was discharged over the tracked period. */
private final Report[] mReports = new Report[NUM_PERIODS_TO_RETAIN];
+ Analyst() {
+ this(BatteryStatsService.getService());
+ }
+
+ @VisibleForTesting Analyst(IBatteryStats iBatteryStats) {
+ mIBatteryStats = iBatteryStats;
+ }
+
/** Returns the list of most recent reports, with the oldest report first. */
@NonNull
List<Report> getReports() {
@@ -107,13 +148,35 @@ public class Analyst {
return list;
}
+ long getBatteryScreenOffDischargeMah() {
+ long discharge = 0;
+ for (Report report : mReports) {
+ if (report == null) {
+ continue;
+ }
+ discharge += report.screenOffDischargeMah;
+ }
+ return discharge;
+ }
+
+ long getBatteryScreenOffDurationMs() {
+ long duration = 0;
+ for (Report report : mReports) {
+ if (report == null) {
+ continue;
+ }
+ duration += report.screenOffDurationMs;
+ }
+ return duration;
+ }
+
/**
* Tracks the given reports instead of whatever is currently saved. Reports should be ordered
* oldest to most recent.
*/
void loadReports(@NonNull List<Report> reports) {
final int numReports = reports.size();
- mPeriodIndex = Math.max(0, numReports - 1);
+ mPeriodIndex = Math.max(0, Math.min(NUM_PERIODS_TO_RETAIN, numReports) - 1);
for (int i = 0; i < NUM_PERIODS_TO_RETAIN; ++i) {
if (i < numReports) {
mReports[i] = reports.get(i);
@@ -121,22 +184,38 @@ public class Analyst {
mReports[i] = null;
}
}
+ final Report latest = mReports[mPeriodIndex];
+ if (latest != null) {
+ latest.bsScreenOffRealtimeBase = getLatestBatteryScreenOffRealtimeMs();
+ latest.bsScreenOffDischargeMahBase = getLatestScreenOffDischargeMah();
+ }
}
void noteBatteryLevelChange(int newBatteryLevel) {
- if (newBatteryLevel == 100 && mReports[mPeriodIndex] != null
- && mReports[mPeriodIndex].currentBatteryLevel < newBatteryLevel) {
+ final boolean deviceDischargedEnough = mReports[mPeriodIndex] != null
+ && newBatteryLevel >= 90
+ // Battery level is increasing, so device is charging.
+ && mReports[mPeriodIndex].currentBatteryLevel < newBatteryLevel
+ && mReports[mPeriodIndex].cumulativeBatteryDischarge >= 25;
+ final boolean reportLongEnough = mReports[mPeriodIndex] != null
+ // Battery level is increasing, so device is charging.
+ && mReports[mPeriodIndex].currentBatteryLevel < newBatteryLevel
+ && mReports[mPeriodIndex].screenOffDurationMs >= MIN_REPORT_DURATION_FOR_RESET;
+ final boolean shouldStartNewReport = deviceDischargedEnough || reportLongEnough;
+ if (shouldStartNewReport) {
mPeriodIndex = (mPeriodIndex + 1) % NUM_PERIODS_TO_RETAIN;
if (mReports[mPeriodIndex] != null) {
final Report report = mReports[mPeriodIndex];
report.clear();
report.currentBatteryLevel = newBatteryLevel;
+ report.bsScreenOffRealtimeBase = getLatestBatteryScreenOffRealtimeMs();
+ report.bsScreenOffDischargeMahBase = getLatestScreenOffDischargeMah();
return;
}
}
if (mReports[mPeriodIndex] == null) {
- Report report = new Report();
+ Report report = initializeReport();
mReports[mPeriodIndex] = report;
report.currentBatteryLevel = newBatteryLevel;
return;
@@ -145,13 +224,27 @@ public class Analyst {
final Report report = mReports[mPeriodIndex];
if (newBatteryLevel < report.currentBatteryLevel) {
report.cumulativeBatteryDischarge += (report.currentBatteryLevel - newBatteryLevel);
+
+ final long latestScreenOffRealtime = getLatestBatteryScreenOffRealtimeMs();
+ final long latestScreenOffDischargeMah = getLatestScreenOffDischargeMah();
+ if (report.bsScreenOffRealtimeBase > latestScreenOffRealtime) {
+ // BatteryStats reset
+ report.bsScreenOffRealtimeBase = 0;
+ report.bsScreenOffDischargeMahBase = 0;
+ }
+ report.screenOffDurationMs +=
+ (latestScreenOffRealtime - report.bsScreenOffRealtimeBase);
+ report.screenOffDischargeMah +=
+ (latestScreenOffDischargeMah - report.bsScreenOffDischargeMahBase);
+ report.bsScreenOffRealtimeBase = latestScreenOffRealtime;
+ report.bsScreenOffDischargeMahBase = latestScreenOffDischargeMah;
}
report.currentBatteryLevel = newBatteryLevel;
}
void noteTransaction(@NonNull Ledger.Transaction transaction) {
if (mReports[mPeriodIndex] == null) {
- mReports[mPeriodIndex] = new Report();
+ mReports[mPeriodIndex] = initializeReport();
}
final Report report = mReports[mPeriodIndex];
switch (getEventType(transaction.eventId)) {
@@ -191,6 +284,32 @@ public class Analyst {
mPeriodIndex = 0;
}
+ private long getLatestBatteryScreenOffRealtimeMs() {
+ try {
+ return mIBatteryStats.computeBatteryScreenOffRealtimeMs();
+ } catch (RemoteException e) {
+ // Shouldn't happen
+ return 0;
+ }
+ }
+
+ private long getLatestScreenOffDischargeMah() {
+ try {
+ return mIBatteryStats.getScreenOffDischargeMah();
+ } catch (RemoteException e) {
+ // Shouldn't happen
+ return 0;
+ }
+ }
+
+ @NonNull
+ private Report initializeReport() {
+ final Report report = new Report();
+ report.bsScreenOffRealtimeBase = getLatestBatteryScreenOffRealtimeMs();
+ report.bsScreenOffDischargeMahBase = getLatestScreenOffDischargeMah();
+ return report;
+ }
+
@NonNull
private String padStringWithSpaces(@NonNull String text, int targetLength) {
// Make sure to have at least one space on either side.
@@ -199,6 +318,8 @@ public class Analyst {
}
void dump(IndentingPrintWriter pw) {
+ final BatteryManagerInternal bmi = LocalServices.getService(BatteryManagerInternal.class);
+ final long batteryCapacityMah = bmi.getBatteryFullCharge() / 1000;
pw.println("Reports:");
pw.increaseIndent();
pw.print(" Total Discharge");
@@ -208,6 +329,7 @@ public class Analyst {
pw.print(padStringWithSpaces("Rewards (avg/reward : avg/discharge)", statColsLength));
pw.print(padStringWithSpaces("+Regs (avg/reg : avg/discharge)", statColsLength));
pw.print(padStringWithSpaces("-Regs (avg/reg : avg/discharge)", statColsLength));
+ pw.print(padStringWithSpaces("Bg drain estimate", statColsLength));
pw.println();
for (int r = 0; r < NUM_PERIODS_TO_RETAIN; ++r) {
final int idx = (mPeriodIndex - r + NUM_PERIODS_TO_RETAIN) % NUM_PERIODS_TO_RETAIN;
@@ -283,6 +405,15 @@ public class Analyst {
} else {
pw.print(padStringWithSpaces("N/A", statColsLength));
}
+ if (report.screenOffDurationMs > 0) {
+ pw.print(padStringWithSpaces(String.format("%d mAh (%.2f%%/hr)",
+ report.screenOffDischargeMah,
+ 100.0 * report.screenOffDischargeMah * HOUR_IN_MILLIS
+ / (batteryCapacityMah * report.screenOffDurationMs)),
+ statColsLength));
+ } else {
+ pw.print(padStringWithSpaces("N/A", statColsLength));
+ }
pw.println();
}
pw.decreaseIndent();
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/CompleteEconomicPolicy.java b/apex/jobscheduler/service/java/com/android/server/tare/CompleteEconomicPolicy.java
index c3eb5bf51161..7a9607657972 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/CompleteEconomicPolicy.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/CompleteEconomicPolicy.java
@@ -18,66 +18,100 @@ package com.android.server.tare;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.app.tare.EconomyManager;
import android.provider.DeviceConfig;
import android.util.ArraySet;
import android.util.IndentingPrintWriter;
+import android.util.Slog;
import android.util.SparseArray;
-import libcore.util.EmptyArray;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ArrayUtils;
+import libcore.util.EmptyArray;
/** Combines all enabled policies into one. */
public class CompleteEconomicPolicy extends EconomicPolicy {
+ private static final String TAG = "TARE-" + CompleteEconomicPolicy.class.getSimpleName();
+
+ private final CompleteInjector mInjector;
private final ArraySet<EconomicPolicy> mEnabledEconomicPolicies = new ArraySet<>();
/** Lazily populated set of actions covered by this policy. */
private final SparseArray<Action> mActions = new SparseArray<>();
/** Lazily populated set of rewards covered by this policy. */
private final SparseArray<Reward> mRewards = new SparseArray<>();
- private final int[] mCostModifiers;
- private long mMaxSatiatedBalance;
- private long mConsumptionLimit;
+ private int mEnabledEconomicPolicyIds = 0;
+ private int[] mCostModifiers = EmptyArray.INT;
+ private long mInitialConsumptionLimit;
+ private long mMinConsumptionLimit;
+ private long mMaxConsumptionLimit;
CompleteEconomicPolicy(@NonNull InternalResourceService irs) {
+ this(irs, new CompleteInjector());
+ }
+
+ @VisibleForTesting
+ CompleteEconomicPolicy(@NonNull InternalResourceService irs,
+ @NonNull CompleteInjector injector) {
super(irs);
- mEnabledEconomicPolicies.add(new AlarmManagerEconomicPolicy(irs));
- mEnabledEconomicPolicies.add(new JobSchedulerEconomicPolicy(irs));
+ mInjector = injector;
- ArraySet<Integer> costModifiers = new ArraySet<>();
- for (int i = 0; i < mEnabledEconomicPolicies.size(); ++i) {
- final int[] sm = mEnabledEconomicPolicies.valueAt(i).getCostModifiers();
- for (int s : sm) {
- costModifiers.add(s);
- }
+ if (mInjector.isPolicyEnabled(POLICY_ALARM, null)) {
+ mEnabledEconomicPolicyIds |= POLICY_ALARM;
+ mEnabledEconomicPolicies.add(new AlarmManagerEconomicPolicy(mIrs, mInjector));
}
- mCostModifiers = new int[costModifiers.size()];
- for (int i = 0; i < costModifiers.size(); ++i) {
- mCostModifiers[i] = costModifiers.valueAt(i);
+ if (mInjector.isPolicyEnabled(POLICY_JOB, null)) {
+ mEnabledEconomicPolicyIds |= POLICY_JOB;
+ mEnabledEconomicPolicies.add(new JobSchedulerEconomicPolicy(mIrs, mInjector));
}
-
- updateMaxBalances();
}
@Override
void setup(@NonNull DeviceConfig.Properties properties) {
super.setup(properties);
+
+ mActions.clear();
+ mRewards.clear();
+
+ mEnabledEconomicPolicies.clear();
+ mEnabledEconomicPolicyIds = 0;
+ if (mInjector.isPolicyEnabled(POLICY_ALARM, properties)) {
+ mEnabledEconomicPolicyIds |= POLICY_ALARM;
+ mEnabledEconomicPolicies.add(new AlarmManagerEconomicPolicy(mIrs, mInjector));
+ }
+ if (mInjector.isPolicyEnabled(POLICY_JOB, properties)) {
+ mEnabledEconomicPolicyIds |= POLICY_JOB;
+ mEnabledEconomicPolicies.add(new JobSchedulerEconomicPolicy(mIrs, mInjector));
+ }
+
+ ArraySet<Integer> costModifiers = new ArraySet<>();
for (int i = 0; i < mEnabledEconomicPolicies.size(); ++i) {
- mEnabledEconomicPolicies.valueAt(i).setup(properties);
+ final int[] sm = mEnabledEconomicPolicies.valueAt(i).getCostModifiers();
+ for (int s : sm) {
+ costModifiers.add(s);
+ }
}
- updateMaxBalances();
- }
+ mCostModifiers = ArrayUtils.convertToIntArray(costModifiers);
- private void updateMaxBalances() {
- long max = 0;
for (int i = 0; i < mEnabledEconomicPolicies.size(); ++i) {
- max += mEnabledEconomicPolicies.valueAt(i).getMaxSatiatedBalance();
+ mEnabledEconomicPolicies.valueAt(i).setup(properties);
}
- mMaxSatiatedBalance = max;
+ updateLimits();
+ }
- max = 0;
+ private void updateLimits() {
+ long initialConsumptionLimit = 0;
+ long minConsumptionLimit = 0;
+ long maxConsumptionLimit = 0;
for (int i = 0; i < mEnabledEconomicPolicies.size(); ++i) {
- max += mEnabledEconomicPolicies.valueAt(i).getInitialSatiatedConsumptionLimit();
+ final EconomicPolicy economicPolicy = mEnabledEconomicPolicies.valueAt(i);
+ initialConsumptionLimit += economicPolicy.getInitialSatiatedConsumptionLimit();
+ minConsumptionLimit += economicPolicy.getMinSatiatedConsumptionLimit();
+ maxConsumptionLimit += economicPolicy.getMaxSatiatedConsumptionLimit();
}
- mConsumptionLimit = max;
+ mInitialConsumptionLimit = initialConsumptionLimit;
+ mMinConsumptionLimit = minConsumptionLimit;
+ mMaxConsumptionLimit = maxConsumptionLimit;
}
@Override
@@ -90,18 +124,27 @@ public class CompleteEconomicPolicy extends EconomicPolicy {
}
@Override
- long getMaxSatiatedBalance() {
- return mMaxSatiatedBalance;
+ long getMaxSatiatedBalance(int userId, @NonNull String pkgName) {
+ long max = 0;
+ for (int i = 0; i < mEnabledEconomicPolicies.size(); ++i) {
+ max += mEnabledEconomicPolicies.valueAt(i).getMaxSatiatedBalance(userId, pkgName);
+ }
+ return max;
}
@Override
long getInitialSatiatedConsumptionLimit() {
- return mConsumptionLimit;
+ return mInitialConsumptionLimit;
+ }
+
+ @Override
+ long getMinSatiatedConsumptionLimit() {
+ return mMinConsumptionLimit;
}
@Override
- long getHardSatiatedConsumptionLimit() {
- return mConsumptionLimit;
+ long getMaxSatiatedConsumptionLimit() {
+ return mMaxConsumptionLimit;
}
@NonNull
@@ -156,6 +199,40 @@ public class CompleteEconomicPolicy extends EconomicPolicy {
return reward;
}
+ boolean isPolicyEnabled(@Policy int policyId) {
+ return (mEnabledEconomicPolicyIds & policyId) == policyId;
+ }
+
+ int getEnabledPolicyIds() {
+ return mEnabledEconomicPolicyIds;
+ }
+
+ @VisibleForTesting
+ static class CompleteInjector extends Injector {
+
+ boolean isPolicyEnabled(int policy, @Nullable DeviceConfig.Properties properties) {
+ final String key;
+ final boolean defaultEnable;
+ switch (policy) {
+ case POLICY_ALARM:
+ key = EconomyManager.KEY_ENABLE_POLICY_ALARM;
+ defaultEnable = EconomyManager.DEFAULT_ENABLE_POLICY_ALARM;
+ break;
+ case POLICY_JOB:
+ key = EconomyManager.KEY_ENABLE_POLICY_JOB_SCHEDULER;
+ defaultEnable = EconomyManager.DEFAULT_ENABLE_POLICY_JOB_SCHEDULER;
+ break;
+ default:
+ Slog.wtf(TAG, "Unknown policy: " + policy);
+ return false;
+ }
+ if (properties == null) {
+ return defaultEnable;
+ }
+ return properties.getBoolean(key, defaultEnable);
+ }
+ }
+
@Override
void dump(IndentingPrintWriter pw) {
dumpActiveModifiers(pw);
@@ -167,7 +244,10 @@ public class CompleteEconomicPolicy extends EconomicPolicy {
pw.println("Cached actions:");
pw.increaseIndent();
for (int i = 0; i < mActions.size(); ++i) {
- dumpAction(pw, mActions.valueAt(i));
+ final Action action = mActions.valueAt(i);
+ if (action != null) {
+ dumpAction(pw, action);
+ }
}
pw.decreaseIndent();
@@ -175,7 +255,10 @@ public class CompleteEconomicPolicy extends EconomicPolicy {
pw.println("Cached rewards:");
pw.increaseIndent();
for (int i = 0; i < mRewards.size(); ++i) {
- dumpReward(pw, mRewards.valueAt(i));
+ final Reward reward = mRewards.valueAt(i);
+ if (reward != null) {
+ dumpReward(pw, reward);
+ }
}
pw.decreaseIndent();
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/EconomicPolicy.java b/apex/jobscheduler/service/java/com/android/server/tare/EconomicPolicy.java
index d401373c0066..a4043dd8ba78 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/EconomicPolicy.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/EconomicPolicy.java
@@ -29,10 +29,14 @@ import android.annotation.CallSuper;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.content.ContentResolver;
import android.provider.DeviceConfig;
+import android.provider.Settings;
import android.util.IndentingPrintWriter;
import android.util.KeyValueListParser;
+import com.android.internal.annotations.VisibleForTesting;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -51,18 +55,24 @@ public abstract class EconomicPolicy {
static final int TYPE_ACTION = 1 << SHIFT_TYPE;
static final int TYPE_REWARD = 2 << SHIFT_TYPE;
- private static final int SHIFT_POLICY = 29;
- static final int MASK_POLICY = 0b1 << SHIFT_POLICY;
- static final int POLICY_AM = 0 << SHIFT_POLICY;
- static final int POLICY_JS = 1 << SHIFT_POLICY;
+ private static final int SHIFT_POLICY = 28;
+ static final int MASK_POLICY = 0b11 << SHIFT_POLICY;
+ static final int ALL_POLICIES = MASK_POLICY;
+ // Reserve 0 for the base/common policy.
+ public static final int POLICY_ALARM = 1 << SHIFT_POLICY;
+ public static final int POLICY_JOB = 2 << SHIFT_POLICY;
- static final int MASK_EVENT = ~0 - (0b111 << SHIFT_POLICY);
+ static final int MASK_EVENT = -1 ^ (MASK_TYPE | MASK_POLICY);
static final int REGULATION_BASIC_INCOME = TYPE_REGULATION | 0;
static final int REGULATION_BIRTHRIGHT = TYPE_REGULATION | 1;
static final int REGULATION_WEALTH_RECLAMATION = TYPE_REGULATION | 2;
static final int REGULATION_PROMOTION = TYPE_REGULATION | 3;
static final int REGULATION_DEMOTION = TYPE_REGULATION | 4;
+ /** App is fully restricted from running in the background. */
+ static final int REGULATION_BG_RESTRICTED = TYPE_REGULATION | 5;
+ static final int REGULATION_BG_UNRESTRICTED = TYPE_REGULATION | 6;
+ static final int REGULATION_FORCE_STOP = TYPE_REGULATION | 8;
static final int REWARD_NOTIFICATION_SEEN = TYPE_REWARD | 0;
static final int REWARD_NOTIFICATION_INTERACTION = TYPE_REWARD | 1;
@@ -106,11 +116,21 @@ public abstract class EconomicPolicy {
}
@IntDef({
+ ALL_POLICIES,
+ POLICY_ALARM,
+ POLICY_JOB,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Policy {
+ }
+
+ @IntDef({
REWARD_TOP_ACTIVITY,
REWARD_NOTIFICATION_SEEN,
REWARD_NOTIFICATION_INTERACTION,
REWARD_WIDGET_INTERACTION,
REWARD_OTHER_USER_INTERACTION,
+ JobSchedulerEconomicPolicy.REWARD_APP_INSTALL,
})
@Retention(RetentionPolicy.SOURCE)
public @interface UtilityReward {
@@ -129,11 +149,22 @@ public abstract class EconomicPolicy {
* the action unless a modifier lowers the cost to produce.
*/
public final long basePrice;
+ /**
+ * Whether the remaining stock limit affects an app's ability to perform this action.
+ * If {@code false}, then the action can be performed, even if the cost is higher
+ * than the remaining stock. This does not affect checking against an app's balance.
+ */
+ public final boolean respectsStockLimit;
Action(int id, long costToProduce, long basePrice) {
+ this(id, costToProduce, basePrice, true);
+ }
+
+ Action(int id, long costToProduce, long basePrice, boolean respectsStockLimit) {
this.id = id;
this.costToProduce = costToProduce;
this.basePrice = basePrice;
+ this.respectsStockLimit = respectsStockLimit;
}
}
@@ -165,9 +196,11 @@ public abstract class EconomicPolicy {
}
}
+ protected final InternalResourceService mIrs;
private static final Modifier[] COST_MODIFIER_BY_INDEX = new Modifier[NUM_COST_MODIFIERS];
EconomicPolicy(@NonNull InternalResourceService irs) {
+ mIrs = irs;
for (int mId : getCostModifiers()) {
initModifier(mId, irs);
}
@@ -204,21 +237,27 @@ public abstract class EconomicPolicy {
* exists to ensure that no single app accumulate all available resources and increases fairness
* for all apps.
*/
- abstract long getMaxSatiatedBalance();
+ abstract long getMaxSatiatedBalance(int userId, @NonNull String pkgName);
/**
* Returns the maximum number of cakes that should be consumed during a full 100% discharge
* cycle. This is the initial limit. The system may choose to increase the limit over time,
* but the increased limit should never exceed the value returned from
- * {@link #getHardSatiatedConsumptionLimit()}.
+ * {@link #getMaxSatiatedConsumptionLimit()}.
*/
abstract long getInitialSatiatedConsumptionLimit();
/**
- * Returns the maximum number of cakes that should be consumed during a full 100% discharge
- * cycle. This is the hard limit that should never be exceeded.
+ * Returns the minimum number of cakes that should be available for consumption during a full
+ * 100% discharge cycle.
*/
- abstract long getHardSatiatedConsumptionLimit();
+ abstract long getMinSatiatedConsumptionLimit();
+
+ /**
+ * Returns the maximum number of cakes that should be available for consumption during a full
+ * 100% discharge cycle.
+ */
+ abstract long getMaxSatiatedConsumptionLimit();
/** Return the set of modifiers that should apply to this policy's costs. */
@NonNull
@@ -236,7 +275,7 @@ public abstract class EconomicPolicy {
@NonNull
final Cost getCostOfAction(int actionId, int userId, @NonNull String pkgName) {
final Action action = getAction(actionId);
- if (action == null) {
+ if (action == null || mIrs.isVip(userId, pkgName)) {
return new Cost(0, 0);
}
long ctp = action.costToProduce;
@@ -306,6 +345,10 @@ public abstract class EconomicPolicy {
return eventId & MASK_TYPE;
}
+ static boolean isReward(int eventId) {
+ return getEventType(eventId) == TYPE_REWARD;
+ }
+
@NonNull
static String eventToString(int eventId) {
switch (eventId & MASK_TYPE) {
@@ -326,7 +369,7 @@ public abstract class EconomicPolicy {
@NonNull
static String actionToString(int eventId) {
switch (eventId & MASK_POLICY) {
- case POLICY_AM:
+ case POLICY_ALARM:
switch (eventId) {
case AlarmManagerEconomicPolicy.ACTION_ALARM_WAKEUP_EXACT_ALLOW_WHILE_IDLE:
return "ALARM_WAKEUP_EXACT_ALLOW_WHILE_IDLE";
@@ -349,7 +392,7 @@ public abstract class EconomicPolicy {
}
break;
- case POLICY_JS:
+ case POLICY_JOB:
switch (eventId) {
case JobSchedulerEconomicPolicy.ACTION_JOB_MAX_START:
return "JOB_MAX_START";
@@ -392,6 +435,12 @@ public abstract class EconomicPolicy {
return "PROMOTION";
case REGULATION_DEMOTION:
return "DEMOTION";
+ case REGULATION_BG_RESTRICTED:
+ return "BG_RESTRICTED";
+ case REGULATION_BG_UNRESTRICTED:
+ return "BG_UNRESTRICTED";
+ case REGULATION_FORCE_STOP:
+ return "FORCE_STOP";
}
return "UNKNOWN_REGULATION:" + Integer.toHexString(eventId);
}
@@ -409,24 +458,42 @@ public abstract class EconomicPolicy {
return "REWARD_WIDGET_INTERACTION";
case REWARD_OTHER_USER_INTERACTION:
return "REWARD_OTHER_USER_INTERACTION";
+ case JobSchedulerEconomicPolicy.REWARD_APP_INSTALL:
+ return "REWARD_JOB_APP_INSTALL";
}
return "UNKNOWN_REWARD:" + Integer.toHexString(eventId);
}
protected long getConstantAsCake(@NonNull KeyValueListParser parser,
@Nullable DeviceConfig.Properties properties, String key, long defaultValCake) {
+ return getConstantAsCake(parser, properties, key, defaultValCake, 0);
+ }
+
+ protected long getConstantAsCake(@NonNull KeyValueListParser parser,
+ @Nullable DeviceConfig.Properties properties, String key, long defaultValCake,
+ long minValCake) {
// Don't cross the streams! Mixing Settings/local user config changes with DeviceConfig
// config can cause issues since the scales may be different, so use one or the other.
if (parser.size() > 0) {
// User settings take precedence. Just stick with the Settings constants, even if there
// are invalid values. It's not worth the time to evaluate all the key/value pairs to
// make sure there are valid ones before deciding.
- return parseCreditValue(parser.getString(key, null), defaultValCake);
+ return Math.max(minValCake,
+ parseCreditValue(parser.getString(key, null), defaultValCake));
}
if (properties != null) {
- return parseCreditValue(properties.getString(key, null), defaultValCake);
+ return Math.max(minValCake,
+ parseCreditValue(properties.getString(key, null), defaultValCake));
+ }
+ return Math.max(minValCake, defaultValCake);
+ }
+
+ @VisibleForTesting
+ static class Injector {
+ @Nullable
+ String getSettingsGlobalString(@NonNull ContentResolver resolver, @NonNull String name) {
+ return Settings.Global.getString(resolver, name);
}
- return defaultValCake;
}
protected static void dumpActiveModifiers(IndentingPrintWriter pw) {
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/EconomyManagerInternal.java b/apex/jobscheduler/service/java/com/android/server/tare/EconomyManagerInternal.java
index 0fa0c47e1b49..5b305ad91118 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/EconomyManagerInternal.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/EconomyManagerInternal.java
@@ -18,6 +18,7 @@ package com.android.server.tare;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.app.tare.EconomyManager;
import java.util.ArrayList;
import java.util.Collections;
@@ -121,7 +122,7 @@ public interface EconomyManagerInternal {
/** Listener for various TARE state changes. */
interface TareStateChangeListener {
- void onTareEnabledStateChanged(boolean isTareEnabled);
+ void onTareEnabledModeChanged(@EconomyManager.EnabledMode int tareEnabledMode);
}
/**
@@ -135,8 +136,13 @@ public interface EconomyManagerInternal {
*/
long getMaxDurationMs(int userId, @NonNull String pkgName, @NonNull ActionBill bill);
- /** Returns true if TARE is enabled. */
- boolean isEnabled();
+ /** Returns the current TARE enabled mode. */
+ @EconomyManager.EnabledMode
+ int getEnabledMode();
+
+ /** Returns the current TARE enabled mode for the specified policy. */
+ @EconomyManager.EnabledMode
+ int getEnabledMode(@EconomicPolicy.Policy int policyId);
/**
* Register an {@link AffordabilityChangeListener} to track when an app's ability to afford the
@@ -155,7 +161,8 @@ public interface EconomyManagerInternal {
/**
* Register a {@link TareStateChangeListener} to track when TARE's state changes.
*/
- void registerTareStateChangeListener(@NonNull TareStateChangeListener listener);
+ void registerTareStateChangeListener(@NonNull TareStateChangeListener listener,
+ @EconomicPolicy.Policy int policyId);
/**
* Unregister a {@link TareStateChangeListener} from being notified when TARE's state changes.
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/InstalledPackageInfo.java b/apex/jobscheduler/service/java/com/android/server/tare/InstalledPackageInfo.java
new file mode 100644
index 000000000000..49c6d1b928d7
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/tare/InstalledPackageInfo.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.tare;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.app.AppGlobals;
+import android.content.Context;
+import android.content.Intent;
+import android.content.PermissionChecker;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.InstallSourceInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.os.RemoteException;
+
+import com.android.internal.util.ArrayUtils;
+
+/** POJO to cache only the information about installed packages that TARE cares about. */
+class InstalledPackageInfo {
+ static final int NO_UID = -1;
+
+ /**
+ * Flags to use when querying for front door activities. Disabled components are included
+ * are included for completeness since the app can enable them at any time.
+ */
+ private static final int HEADLESS_APP_QUERY_FLAGS = PackageManager.MATCH_DIRECT_BOOT_AWARE
+ | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
+ | PackageManager.MATCH_DISABLED_COMPONENTS;
+
+ public final int uid;
+ public final String packageName;
+ public final boolean hasCode;
+ /**
+ * Whether the app is a system app that is "headless." Headless in this context means that
+ * the app doesn't have any "front door" activities --- activities that would show in the
+ * launcher.
+ */
+ public final boolean isHeadlessSystemApp;
+ public final boolean isSystemInstaller;
+ @Nullable
+ public final String installerPackageName;
+
+ InstalledPackageInfo(@NonNull Context context, @UserIdInt int userId,
+ @NonNull PackageInfo packageInfo) {
+ final ApplicationInfo applicationInfo = packageInfo.applicationInfo;
+ uid = applicationInfo == null ? NO_UID : applicationInfo.uid;
+ packageName = packageInfo.packageName;
+ hasCode = applicationInfo != null && applicationInfo.hasCode();
+
+ final PackageManager packageManager = context.getPackageManager();
+ final Intent frontDoorActivityIntent = new Intent(Intent.ACTION_MAIN)
+ .addCategory(Intent.CATEGORY_LAUNCHER)
+ .setPackage(packageName);
+ isHeadlessSystemApp = applicationInfo != null
+ && (applicationInfo.isSystemApp() || applicationInfo.isUpdatedSystemApp())
+ && ArrayUtils.isEmpty(
+ packageManager.queryIntentActivitiesAsUser(
+ frontDoorActivityIntent, HEADLESS_APP_QUERY_FLAGS, userId));
+
+ isSystemInstaller = applicationInfo != null
+ && ArrayUtils.indexOf(
+ packageInfo.requestedPermissions, Manifest.permission.INSTALL_PACKAGES) >= 0
+ && PackageManager.PERMISSION_GRANTED
+ == PermissionChecker.checkPermissionForPreflight(context,
+ Manifest.permission.INSTALL_PACKAGES, PermissionChecker.PID_UNKNOWN,
+ applicationInfo.uid, packageName);
+ InstallSourceInfo installSourceInfo = null;
+ try {
+ installSourceInfo = AppGlobals.getPackageManager().getInstallSourceInfo(packageName,
+ userId);
+ } catch (RemoteException e) {
+ // Shouldn't happen.
+ }
+ installerPackageName =
+ installSourceInfo == null ? null : installSourceInfo.getInstallingPackageName();
+ }
+
+ @Override
+ public String toString() {
+ return "IPO{"
+ + "uid=" + uid
+ + ", pkgName=" + packageName
+ + (hasCode ? " HAS_CODE" : "")
+ + (isHeadlessSystemApp ? " HEADLESS_SYSTEM" : "")
+ + (isSystemInstaller ? " SYSTEM_INSTALLER" : "")
+ + ", installer=" + installerPackageName
+ + '}';
+ }
+}
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 50a77f371b6e..2550a27baa81 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java
@@ -16,6 +16,10 @@
package com.android.server.tare;
+import static android.app.tare.EconomyManager.ENABLED_MODE_OFF;
+import static android.app.tare.EconomyManager.ENABLED_MODE_ON;
+import static android.app.tare.EconomyManager.ENABLED_MODE_SHADOW;
+import static android.app.tare.EconomyManager.enabledModeToString;
import static android.provider.Settings.Global.TARE_ALARM_MANAGER_CONSTANTS;
import static android.provider.Settings.Global.TARE_JOB_SCHEDULER_CONSTANTS;
import static android.text.format.DateUtils.DAY_IN_MILLIS;
@@ -29,6 +33,8 @@ import static com.android.server.tare.TareUtils.getCurrentTimeMillis;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.AlarmManager;
+import android.app.AppOpsManager;
+import android.app.tare.EconomyManager;
import android.app.tare.IEconomyManager;
import android.app.usage.UsageEvents;
import android.app.usage.UsageStatsManagerInternal;
@@ -42,12 +48,14 @@ import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.database.ContentObserver;
import android.net.Uri;
+import android.os.BatteryManager;
import android.os.BatteryManagerInternal;
import android.os.Binder;
import android.os.Handler;
import android.os.IDeviceIdleController;
import android.os.Looper;
import android.os.Message;
+import android.os.ParcelFileDescriptor;
import android.os.PowerManager;
import android.os.RemoteException;
import android.os.ServiceManager;
@@ -60,9 +68,12 @@ import android.util.IndentingPrintWriter;
import android.util.Log;
import android.util.Slog;
import android.util.SparseArrayMap;
+import android.util.SparseLongArray;
import android.util.SparseSetArray;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.app.IAppOpsCallback;
+import com.android.internal.app.IAppOpsService;
import com.android.internal.os.SomeArgs;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.DumpUtils;
@@ -76,7 +87,7 @@ import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
-import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.Objects;
/**
* Responsible for handling app's ARC count based on events, ensuring ARCs are credited when
@@ -102,13 +113,36 @@ public class InternalResourceService extends SystemService {
/** The amount of time to delay reclamation by after boot. */
private static final long RECLAMATION_STARTUP_DELAY_MS = 30_000L;
/**
+ * The amount of time after TARE has first been set up that a system installer will be allowed
+ * expanded credit privileges.
+ */
+ static final long INSTALLER_FIRST_SETUP_GRACE_PERIOD_MS = 7 * DAY_IN_MILLIS;
+ /**
+ * The amount of time to wait after TARE has first been set up before considering adjusting the
+ * stock/consumption limit.
+ */
+ private static final long STOCK_ADJUSTMENT_FIRST_SETUP_GRACE_PERIOD_MS = 5 * DAY_IN_MILLIS;
+ /**
* The battery level above which we may consider quantitative easing (increasing the consumption
* limit).
*/
private static final int QUANTITATIVE_EASING_BATTERY_THRESHOLD = 50;
+ /**
+ * The battery level above which we may consider adjusting the desired stock level.
+ */
+ private static final int STOCK_RECALCULATION_BATTERY_THRESHOLD = 80;
+ /**
+ * The amount of time to wait before considering recalculating the desired stock level.
+ */
+ private static final long STOCK_RECALCULATION_DELAY_MS = 16 * HOUR_IN_MILLIS;
+ /**
+ * The minimum amount of time we must have background drain for before considering
+ * recalculating the desired stock level.
+ */
+ private static final long STOCK_RECALCULATION_MIN_DATA_DURATION_MS = 8 * HOUR_IN_MILLIS;
private static final int PACKAGE_QUERY_FLAGS =
PackageManager.MATCH_DIRECT_BOOT_AWARE | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
- | PackageManager.MATCH_APEX;
+ | PackageManager.MATCH_APEX | PackageManager.GET_PERMISSIONS;
/** Global lock for all resource economy state. */
private final Object mLock = new Object();
@@ -118,6 +152,7 @@ public class InternalResourceService extends SystemService {
private final PackageManager mPackageManager;
private final PackageManagerInternal mPackageManagerInternal;
+ private IAppOpsService mAppOpsService;
private IDeviceIdleController mDeviceIdleController;
private final Agent mAgent;
@@ -131,7 +166,7 @@ public class InternalResourceService extends SystemService {
@NonNull
@GuardedBy("mLock")
- private final List<PackageInfo> mPkgCache = new ArrayList<>();
+ private final SparseArrayMap<String, InstalledPackageInfo> mPkgCache = new SparseArrayMap<>();
/** Cached mapping of UIDs (for all users) to a list of packages in the UID. */
@GuardedBy("mLock")
@@ -141,21 +176,78 @@ public class InternalResourceService extends SystemService {
@GuardedBy("mPackageToUidCache")
private final SparseArrayMap<String, Integer> mPackageToUidCache = new SparseArrayMap<>();
- private final CopyOnWriteArraySet<TareStateChangeListener> mStateChangeListeners =
- new CopyOnWriteArraySet<>();
+ @GuardedBy("mStateChangeListeners")
+ private final SparseSetArray<TareStateChangeListener> mStateChangeListeners =
+ new SparseSetArray<>();
+
+ /**
+ * List of packages that are fully restricted and shouldn't be allowed to run in the background.
+ */
+ @GuardedBy("mLock")
+ private final SparseSetArray<String> mRestrictedApps = new SparseSetArray<>();
/** List of packages that are "exempted" from battery restrictions. */
// TODO(144864180): include userID
@GuardedBy("mLock")
private ArraySet<String> mExemptedApps = new ArraySet<>();
- private volatile boolean mIsEnabled;
+ @GuardedBy("mLock")
+ private final SparseArrayMap<String, Boolean> mVipOverrides = new SparseArrayMap<>();
+
+ /**
+ * Set of temporary Very Important Packages and when their VIP status ends, in the elapsed
+ * realtime ({@link android.annotation.ElapsedRealtimeLong}) timebase.
+ */
+ @GuardedBy("mLock")
+ private final SparseArrayMap<String, Long> mTemporaryVips = new SparseArrayMap<>();
+
+ /** Set of apps each installer is responsible for installing. */
+ @GuardedBy("mLock")
+ private final SparseArrayMap<String, ArraySet<String>> mInstallers = new SparseArrayMap<>();
+
+ /** The package name of the wellbeing app. */
+ @GuardedBy("mLock")
+ @Nullable
+ private String mWellbeingPackage;
+
+ private volatile boolean mHasBattery = true;
+ @EconomyManager.EnabledMode
+ private volatile int mEnabledMode;
private volatile int mBootPhase;
private volatile boolean mExemptListLoaded;
// In the range [0,100] to represent 0% to 100% battery.
@GuardedBy("mLock")
private int mCurrentBatteryLevel;
+ // TODO(250007395): make configurable per device (via config.xml)
+ private final int mDefaultTargetBackgroundBatteryLifeHours;
+ @GuardedBy("mLock")
+ private int mTargetBackgroundBatteryLifeHours;
+
+ private final IAppOpsCallback mApbListener = new IAppOpsCallback.Stub() {
+ @Override
+ public void opChanged(int op, int uid, String packageName) {
+ boolean restricted = false;
+ try {
+ restricted = mAppOpsService.checkOperation(
+ AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, uid, packageName)
+ != AppOpsManager.MODE_ALLOWED;
+ } catch (RemoteException e) {
+ // Shouldn't happen
+ }
+ final int userId = UserHandle.getUserId(uid);
+ synchronized (mLock) {
+ if (restricted) {
+ if (mRestrictedApps.add(userId, packageName)) {
+ mAgent.onAppRestrictedLocked(userId, packageName);
+ }
+ } else if (mRestrictedApps.remove(UserHandle.getUserId(uid), packageName)) {
+ mAgent.onAppUnrestrictedLocked(userId, packageName);
+ }
+ }
+ }
+ };
+
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Nullable
private String getPackageName(Intent intent) {
@@ -166,6 +258,15 @@ public class InternalResourceService extends SystemService {
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
+ case Intent.ACTION_BATTERY_CHANGED: {
+ final boolean hasBattery =
+ intent.getBooleanExtra(BatteryManager.EXTRA_PRESENT, mHasBattery);
+ if (mHasBattery != hasBattery) {
+ mHasBattery = hasBattery;
+ mConfigObserver.updateEnabledStatus();
+ }
+ }
+ break;
case Intent.ACTION_BATTERY_LEVEL_CHANGED:
onBatteryLevelChanged();
break;
@@ -236,6 +337,8 @@ public class InternalResourceService extends SystemService {
private static final int MSG_SCHEDULE_UNUSED_WEALTH_RECLAMATION_EVENT = 1;
private static final int MSG_PROCESS_USAGE_EVENT = 2;
private static final int MSG_NOTIFY_STATE_CHANGE_LISTENERS = 3;
+ private static final int MSG_NOTIFY_STATE_CHANGE_LISTENER = 4;
+ private static final int MSG_CLEAN_UP_TEMP_VIP_LIST = 5;
private static final String ALARM_TAG_WEALTH_RECLAMATION = "*tare.reclamation*";
/**
@@ -262,6 +365,12 @@ public class InternalResourceService extends SystemService {
mConfigObserver = new ConfigObserver(mHandler, context);
+ mDefaultTargetBackgroundBatteryLifeHours =
+ mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)
+ ? 100 // ~ 1.0%/hr
+ : 40; // ~ 2.5%/hr
+ mTargetBackgroundBatteryLifeHours = mDefaultTargetBackgroundBatteryLifeHours;
+
publishLocalService(EconomyManagerInternal.class, new LocalService());
}
@@ -274,24 +383,21 @@ public class InternalResourceService extends SystemService {
public void onBootPhase(int phase) {
mBootPhase = phase;
- if (PHASE_SYSTEM_SERVICES_READY == phase) {
- mConfigObserver.start();
- mDeviceIdleController = IDeviceIdleController.Stub.asInterface(
- ServiceManager.getService(Context.DEVICE_IDLE_CONTROLLER));
- setupEverything();
- } else if (PHASE_BOOT_COMPLETED == phase) {
- if (!mExemptListLoaded) {
- synchronized (mLock) {
- try {
- mExemptedApps =
- new ArraySet<>(mDeviceIdleController.getFullPowerWhitelist());
- } catch (RemoteException e) {
- // Shouldn't happen.
- Slog.wtf(TAG, e);
- }
- mExemptListLoaded = true;
- }
- }
+ switch (phase) {
+ case PHASE_SYSTEM_SERVICES_READY:
+ mAppOpsService = IAppOpsService.Stub.asInterface(
+ ServiceManager.getService(Context.APP_OPS_SERVICE));
+ mDeviceIdleController = IDeviceIdleController.Stub.asInterface(
+ ServiceManager.getService(Context.DEVICE_IDLE_CONTROLLER));
+ mConfigObserver.start();
+ onBootPhaseSystemServicesReady();
+ break;
+ case PHASE_THIRD_PARTY_APPS_CAN_START:
+ onBootPhaseThirdPartyAppsCanStart();
+ break;
+ case PHASE_BOOT_COMPLETED:
+ onBootPhaseBootCompleted();
+ break;
}
}
@@ -307,8 +413,16 @@ public class InternalResourceService extends SystemService {
return mCompleteEconomicPolicy;
}
+ /** Returns the number of apps that this app is expected to update at some point. */
+ int getAppUpdateResponsibilityCount(final int userId, @NonNull final String pkgName) {
+ synchronized (mLock) {
+ // TODO(248274798): return 0 if the app has lost the install permission
+ return ArrayUtils.size(mInstallers.get(userId, pkgName));
+ }
+ }
+
@NonNull
- List<PackageInfo> getInstalledPackages() {
+ SparseArrayMap<String, InstalledPackageInfo> getInstalledPackages() {
synchronized (mLock) {
return mPkgCache;
}
@@ -316,20 +430,28 @@ public class InternalResourceService extends SystemService {
/** Returns the installed packages for the specified user. */
@NonNull
- List<PackageInfo> getInstalledPackages(final int userId) {
- final List<PackageInfo> userPkgs = new ArrayList<>();
+ List<InstalledPackageInfo> getInstalledPackages(final int userId) {
+ final List<InstalledPackageInfo> userPkgs = new ArrayList<>();
synchronized (mLock) {
- for (int i = 0; i < mPkgCache.size(); ++i) {
- final PackageInfo packageInfo = mPkgCache.get(i);
- if (packageInfo.applicationInfo != null
- && UserHandle.getUserId(packageInfo.applicationInfo.uid) == userId) {
- userPkgs.add(packageInfo);
- }
+ final int uIdx = mPkgCache.indexOfKey(userId);
+ if (uIdx < 0) {
+ return userPkgs;
+ }
+ for (int p = mPkgCache.numElementsForKeyAt(uIdx) - 1; p >= 0; --p) {
+ final InstalledPackageInfo packageInfo = mPkgCache.valueAt(uIdx, p);
+ userPkgs.add(packageInfo);
}
}
return userPkgs;
}
+ @Nullable
+ InstalledPackageInfo getInstalledPackageInfo(final int userId, @NonNull final String pkgName) {
+ synchronized (mLock) {
+ return mPkgCache.get(userId, pkgName);
+ }
+ }
+
@GuardedBy("mLock")
long getConsumptionLimitLocked() {
return mCurrentBatteryLevel * mScribe.getSatiatedConsumptionLimitLocked() / 100;
@@ -346,6 +468,11 @@ public class InternalResourceService extends SystemService {
return mCompleteEconomicPolicy.getInitialSatiatedConsumptionLimit();
}
+
+ long getRealtimeSinceFirstSetupMs() {
+ return mScribe.getRealtimeSinceFirstSetupMs(SystemClock.elapsedRealtime());
+ }
+
int getUid(final int userId, @NonNull final String pkgName) {
synchronized (mPackageToUidCache) {
Integer uid = mPackageToUidCache.get(userId, pkgName);
@@ -357,8 +484,39 @@ public class InternalResourceService extends SystemService {
}
}
- boolean isEnabled() {
- return mIsEnabled;
+ @EconomyManager.EnabledMode
+ int getEnabledMode() {
+ return mEnabledMode;
+ }
+
+ @EconomyManager.EnabledMode
+ int getEnabledMode(int policyId) {
+ synchronized (mLock) {
+ // For now, treat enabled policies as using the same enabled mode as full TARE.
+ // TODO: have enabled mode by policy
+ if (mCompleteEconomicPolicy.isPolicyEnabled(policyId)) {
+ return mEnabledMode;
+ }
+ return ENABLED_MODE_OFF;
+ }
+ }
+
+ boolean isHeadlessSystemApp(final int userId, @NonNull String pkgName) {
+ if (pkgName == null) {
+ Slog.wtfStack(TAG, "isHeadlessSystemApp called with null package");
+ return false;
+ }
+ synchronized (mLock) {
+ final InstalledPackageInfo ipo = getInstalledPackageInfo(userId, pkgName);
+ if (ipo != null && ipo.isHeadlessSystemApp) {
+ return true;
+ }
+ // The wellbeing app is pre-set on the device, not expected to be interacted with
+ // much by the user, but can be expected to do work in the background on behalf of
+ // the user. As such, it's a pseudo-headless system app, so treat it as a headless
+ // system app.
+ return pkgName.equals(mWellbeingPackage);
+ }
}
boolean isPackageExempted(final int userId, @NonNull String pkgName) {
@@ -367,6 +525,12 @@ public class InternalResourceService extends SystemService {
}
}
+ boolean isPackageRestricted(final int userId, @NonNull String pkgName) {
+ synchronized (mLock) {
+ return mRestrictedApps.contains(userId, pkgName);
+ }
+ }
+
boolean isSystem(final int userId, @NonNull String pkgName) {
if ("android".equals(pkgName)) {
return true;
@@ -374,12 +538,40 @@ public class InternalResourceService extends SystemService {
return UserHandle.isCore(getUid(userId, pkgName));
}
+ boolean isVip(final int userId, @NonNull String pkgName) {
+ return isVip(userId, pkgName, SystemClock.elapsedRealtime());
+ }
+
+ boolean isVip(final int userId, @NonNull String pkgName, final long nowElapsed) {
+ synchronized (mLock) {
+ final Boolean override = mVipOverrides.get(userId, pkgName);
+ if (override != null) {
+ return override;
+ }
+ }
+ if (isSystem(userId, pkgName)) {
+ // The government, I mean the system, can create ARCs as it needs to in order to
+ // operate.
+ return true;
+ }
+ synchronized (mLock) {
+ final Long expirationTimeElapsed = mTemporaryVips.get(userId, pkgName);
+ if (expirationTimeElapsed != null) {
+ return nowElapsed <= expirationTimeElapsed;
+ }
+ }
+ return false;
+ }
+
void onBatteryLevelChanged() {
synchronized (mLock) {
final int newBatteryLevel = getCurrentBatteryLevel();
mAnalyst.noteBatteryLevelChange(newBatteryLevel);
final boolean increased = newBatteryLevel > mCurrentBatteryLevel;
if (increased) {
+ if (newBatteryLevel >= STOCK_RECALCULATION_BATTERY_THRESHOLD) {
+ maybeAdjustDesiredStockLevelLocked();
+ }
mAgent.distributeBasicIncomeLocked(newBatteryLevel);
} else if (newBatteryLevel == mCurrentBatteryLevel) {
// The broadcast is also sent when the plug type changes...
@@ -403,10 +595,9 @@ public class InternalResourceService extends SystemService {
final ArraySet<String> added = new ArraySet<>();
try {
mExemptedApps = new ArraySet<>(mDeviceIdleController.getFullPowerWhitelist());
+ mExemptListLoaded = true;
} catch (RemoteException e) {
// Shouldn't happen.
- Slog.wtf(TAG, e);
- return;
}
for (int i = mExemptedApps.size() - 1; i >= 0; --i) {
@@ -457,16 +648,25 @@ public class InternalResourceService extends SystemService {
mPackageToUidCache.add(userId, pkgName, uid);
}
synchronized (mLock) {
- mPkgCache.add(packageInfo);
+ final InstalledPackageInfo ipo = new InstalledPackageInfo(getContext(), userId,
+ packageInfo);
+ final InstalledPackageInfo oldIpo = mPkgCache.add(userId, pkgName, ipo);
+ maybeUpdateInstallerStatusLocked(oldIpo, ipo);
mUidToPackageCache.add(uid, pkgName);
// TODO: only do this when the user first launches the app (app leaves stopped state)
mAgent.grantBirthrightLocked(userId, pkgName);
+ if (ipo.installerPackageName != null) {
+ mAgent.noteInstantaneousEventLocked(userId, ipo.installerPackageName,
+ JobSchedulerEconomicPolicy.REWARD_APP_INSTALL, null);
+ }
}
}
void onPackageForceStopped(final int userId, @NonNull final String pkgName) {
synchronized (mLock) {
- // TODO: reduce ARC count by some amount
+ // Remove all credits if the user force stops the app. It will slowly regain them
+ // in response to different events.
+ mAgent.reclaimAllAssetsLocked(userId, pkgName, EconomicPolicy.REGULATION_FORCE_STOP);
}
}
@@ -477,12 +677,13 @@ public class InternalResourceService extends SystemService {
}
synchronized (mLock) {
mUidToPackageCache.remove(uid, pkgName);
- for (int i = 0; i < mPkgCache.size(); ++i) {
- PackageInfo pkgInfo = mPkgCache.get(i);
- if (UserHandle.getUserId(pkgInfo.applicationInfo.uid) == userId
- && pkgName.equals(pkgInfo.packageName)) {
- mPkgCache.remove(i);
- break;
+ mVipOverrides.delete(userId, pkgName);
+ final InstalledPackageInfo ipo = mPkgCache.delete(userId, pkgName);
+ mInstallers.delete(userId, pkgName);
+ if (ipo != null && ipo.installerPackageName != null) {
+ final ArraySet<String> list = mInstallers.get(userId, ipo.installerPackageName);
+ if (list != null) {
+ list.remove(pkgName);
}
}
mAgent.onPackageRemovedLocked(userId, pkgName);
@@ -502,24 +703,36 @@ public class InternalResourceService extends SystemService {
void onUserAdded(final int userId) {
synchronized (mLock) {
- mPkgCache.addAll(
- mPackageManager.getInstalledPackagesAsUser(PACKAGE_QUERY_FLAGS, userId));
+ final List<PackageInfo> pkgs =
+ mPackageManager.getInstalledPackagesAsUser(PACKAGE_QUERY_FLAGS, userId);
+ for (int i = pkgs.size() - 1; i >= 0; --i) {
+ final InstalledPackageInfo ipo =
+ new InstalledPackageInfo(getContext(), userId, pkgs.get(i));
+ final InstalledPackageInfo oldIpo = mPkgCache.add(userId, ipo.packageName, ipo);
+ maybeUpdateInstallerStatusLocked(oldIpo, ipo);
+ }
mAgent.grantBirthrightsLocked(userId);
+ final long nowElapsed = SystemClock.elapsedRealtime();
+ mScribe.setUserAddedTimeLocked(userId, nowElapsed);
+ grantInstallersTemporaryVipStatusLocked(userId,
+ nowElapsed, INSTALLER_FIRST_SETUP_GRACE_PERIOD_MS);
}
}
void onUserRemoved(final int userId) {
synchronized (mLock) {
- ArrayList<String> removedPkgs = new ArrayList<>();
- for (int i = mPkgCache.size() - 1; i >= 0; --i) {
- PackageInfo pkgInfo = mPkgCache.get(i);
- if (UserHandle.getUserId(pkgInfo.applicationInfo.uid) == userId) {
- removedPkgs.add(pkgInfo.packageName);
- mUidToPackageCache.remove(pkgInfo.applicationInfo.uid);
- mPkgCache.remove(i);
+ mVipOverrides.delete(userId);
+ final int uIdx = mPkgCache.indexOfKey(userId);
+ if (uIdx >= 0) {
+ for (int p = mPkgCache.numElementsForKeyAt(uIdx) - 1; p >= 0; --p) {
+ final InstalledPackageInfo pkgInfo = mPkgCache.valueAt(uIdx, p);
+ mUidToPackageCache.remove(pkgInfo.uid);
}
}
- mAgent.onUserRemovedLocked(userId, removedPkgs);
+ mInstallers.delete(userId);
+ mPkgCache.delete(userId);
+ mAgent.onUserRemovedLocked(userId);
+ mScribe.onUserRemovedLocked(userId);
}
}
@@ -528,6 +741,14 @@ public class InternalResourceService extends SystemService {
*/
@GuardedBy("mLock")
void maybePerformQuantitativeEasingLocked() {
+ if (mConfigObserver.ENABLE_TIP3) {
+ maybeAdjustDesiredStockLevelLocked();
+ return;
+ }
+ if (getRealtimeSinceFirstSetupMs() < STOCK_ADJUSTMENT_FIRST_SETUP_GRACE_PERIOD_MS) {
+ // Things can be very tumultuous soon after first setup.
+ return;
+ }
// We don't need to increase the limit if the device runs out of consumable credits
// when the battery is low.
final long remainingConsumableCakes = mScribe.getRemainingConsumableCakesLocked();
@@ -539,7 +760,7 @@ public class InternalResourceService extends SystemService {
final long shortfall = (mCurrentBatteryLevel - QUANTITATIVE_EASING_BATTERY_THRESHOLD)
* currentConsumptionLimit / 100;
final long newConsumptionLimit = Math.min(currentConsumptionLimit + shortfall,
- mCompleteEconomicPolicy.getHardSatiatedConsumptionLimit());
+ mCompleteEconomicPolicy.getMaxSatiatedConsumptionLimit());
if (newConsumptionLimit != currentConsumptionLimit) {
Slog.i(TAG, "Increasing consumption limit from " + cakeToString(currentConsumptionLimit)
+ " to " + cakeToString(newConsumptionLimit));
@@ -548,6 +769,76 @@ public class InternalResourceService extends SystemService {
}
}
+ /**
+ * Adjust the consumption limit based on historical data and the target battery drain.
+ */
+ @GuardedBy("mLock")
+ void maybeAdjustDesiredStockLevelLocked() {
+ if (!mConfigObserver.ENABLE_TIP3) {
+ return;
+ }
+ if (getRealtimeSinceFirstSetupMs() < STOCK_ADJUSTMENT_FIRST_SETUP_GRACE_PERIOD_MS) {
+ // Things can be very tumultuous soon after first setup.
+ return;
+ }
+ // Don't adjust the limit too often or while the battery is low.
+ final long now = getCurrentTimeMillis();
+ if ((now - mScribe.getLastStockRecalculationTimeLocked()) < STOCK_RECALCULATION_DELAY_MS
+ || mCurrentBatteryLevel <= STOCK_RECALCULATION_BATTERY_THRESHOLD) {
+ return;
+ }
+
+ // For now, use screen off battery drain as a proxy for background battery drain.
+ // TODO: get more accurate background battery drain numbers
+ final long totalScreenOffDurationMs = mAnalyst.getBatteryScreenOffDurationMs();
+ if (totalScreenOffDurationMs < STOCK_RECALCULATION_MIN_DATA_DURATION_MS) {
+ return;
+ }
+ final long totalDischargeMah = mAnalyst.getBatteryScreenOffDischargeMah();
+ if (totalDischargeMah == 0) {
+ Slog.i(TAG, "Total discharge was 0");
+ return;
+ }
+ final long batteryCapacityMah = mBatteryManagerInternal.getBatteryFullCharge() / 1000;
+ final long estimatedLifeHours = batteryCapacityMah * totalScreenOffDurationMs
+ / totalDischargeMah / HOUR_IN_MILLIS;
+ final long percentageOfTarget =
+ 100 * estimatedLifeHours / mTargetBackgroundBatteryLifeHours;
+ if (DEBUG) {
+ Slog.d(TAG, "maybeAdjustDesiredStockLevelLocked:"
+ + " screenOffMs=" + totalScreenOffDurationMs
+ + " dischargeMah=" + totalDischargeMah
+ + " capacityMah=" + batteryCapacityMah
+ + " estimatedLifeHours=" + estimatedLifeHours
+ + " %ofTarget=" + percentageOfTarget);
+ }
+ final long currentConsumptionLimit = mScribe.getSatiatedConsumptionLimitLocked();
+ final long newConsumptionLimit;
+ if (percentageOfTarget > 105) {
+ // The stock is too low. We're doing pretty well. We can increase the stock slightly
+ // to let apps do more work in the background.
+ newConsumptionLimit = Math.min((long) (currentConsumptionLimit * 1.01),
+ mCompleteEconomicPolicy.getMaxSatiatedConsumptionLimit());
+ } else if (percentageOfTarget < 100) {
+ // The stock is too high IMO. We're below the target. Decrease the stock to reduce
+ // background work.
+ newConsumptionLimit = Math.max((long) (currentConsumptionLimit * .98),
+ mCompleteEconomicPolicy.getMinSatiatedConsumptionLimit());
+ } else {
+ // The stock is just right.
+ return;
+ }
+ // TODO(250007191): calculate and log implied service level
+ if (newConsumptionLimit != currentConsumptionLimit) {
+ Slog.i(TAG, "Adjusting consumption limit from " + cakeToString(currentConsumptionLimit)
+ + " to " + cakeToString(newConsumptionLimit)
+ + " because drain was " + percentageOfTarget + "% of target");
+ mScribe.setConsumptionLimitLocked(newConsumptionLimit);
+ adjustCreditSupplyLocked(/* allowIncrease */ true);
+ mScribe.setLastStockRecalculationTimeLocked(now);
+ }
+ }
+
void postAffordabilityChanged(final int userId, @NonNull final String pkgName,
@NonNull Agent.ActionAffordabilityNote affordabilityNote) {
if (DEBUG) {
@@ -579,8 +870,30 @@ public class InternalResourceService extends SystemService {
}
@GuardedBy("mLock")
+ private void grantInstallersTemporaryVipStatusLocked(int userId, long nowElapsed,
+ long grantDurationMs) {
+ final long grantEndTimeElapsed = nowElapsed + grantDurationMs;
+ final int uIdx = mPkgCache.indexOfKey(userId);
+ if (uIdx < 0) {
+ return;
+ }
+ for (int pIdx = mPkgCache.numElementsForKey(uIdx) - 1; pIdx >= 0; --pIdx) {
+ final InstalledPackageInfo ipo = mPkgCache.valueAt(uIdx, pIdx);
+
+ if (ipo.isSystemInstaller) {
+ final Long currentGrantEndTimeElapsed = mTemporaryVips.get(userId, ipo.packageName);
+ if (currentGrantEndTimeElapsed == null
+ || currentGrantEndTimeElapsed < grantEndTimeElapsed) {
+ mTemporaryVips.add(userId, ipo.packageName, grantEndTimeElapsed);
+ }
+ }
+ }
+ mHandler.sendEmptyMessageDelayed(MSG_CLEAN_UP_TEMP_VIP_LIST, grantDurationMs);
+ }
+
+ @GuardedBy("mLock")
private void processUsageEventLocked(final int userId, @NonNull UsageEvents.Event event) {
- if (!mIsEnabled) {
+ if (mEnabledMode == ENABLED_MODE_OFF) {
return;
}
final String pkgName = event.getPackageName();
@@ -657,6 +970,12 @@ public class InternalResourceService extends SystemService {
return packages;
}
+ private boolean isTareSupported() {
+ // TARE is presently designed for devices with batteries. Don't enable it on
+ // battery-less devices for now.
+ return mHasBattery;
+ }
+
@GuardedBy("mLock")
private void loadInstalledPackageListLocked() {
mPkgCache.clear();
@@ -664,13 +983,57 @@ public class InternalResourceService extends SystemService {
LocalServices.getService(UserManagerInternal.class);
final int[] userIds = userManagerInternal.getUserIds();
for (int userId : userIds) {
- mPkgCache.addAll(
- mPackageManager.getInstalledPackagesAsUser(PACKAGE_QUERY_FLAGS, userId));
+ final List<PackageInfo> pkgs =
+ mPackageManager.getInstalledPackagesAsUser(PACKAGE_QUERY_FLAGS, userId);
+ for (int i = pkgs.size() - 1; i >= 0; --i) {
+ final InstalledPackageInfo ipo =
+ new InstalledPackageInfo(getContext(), userId, pkgs.get(i));
+ final InstalledPackageInfo oldIpo = mPkgCache.add(userId, ipo.packageName, ipo);
+ maybeUpdateInstallerStatusLocked(oldIpo, ipo);
+ }
+ }
+ }
+
+ /**
+ * Used to update the set of installed apps for each installer. This only has an effect if the
+ * installer package name is different between {@code oldIpo} and {@code newIpo}.
+ */
+ @GuardedBy("mLock")
+ private void maybeUpdateInstallerStatusLocked(@Nullable InstalledPackageInfo oldIpo,
+ @NonNull InstalledPackageInfo newIpo) {
+ final boolean changed;
+ if (oldIpo == null) {
+ changed = newIpo.installerPackageName != null;
+ } else {
+ changed = !Objects.equals(oldIpo.installerPackageName, newIpo.installerPackageName);
+ }
+ if (!changed) {
+ return;
+ }
+ // InstallSourceInfo doesn't track userId, so for now, assume the installer on the package's
+ // user profile did the installation.
+ // TODO(246640162): use the actual installer's user ID
+ final int userId = UserHandle.getUserId(newIpo.uid);
+ final String pkgName = newIpo.packageName;
+ if (oldIpo != null) {
+ final ArraySet<String> oldList = mInstallers.get(userId, oldIpo.installerPackageName);
+ if (oldList != null) {
+ oldList.remove(pkgName);
+ }
+ }
+ if (newIpo.installerPackageName != null) {
+ ArraySet<String> newList = mInstallers.get(userId, newIpo.installerPackageName);
+ if (newList == null) {
+ newList = new ArraySet<>();
+ mInstallers.add(userId, newIpo.installerPackageName, newList);
+ }
+ newList.add(pkgName);
}
}
private void registerListeners() {
final IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_BATTERY_CHANGED);
filter.addAction(Intent.ACTION_BATTERY_LEVEL_CHANGED);
filter.addAction(PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED);
getContext().registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, filter, null, null);
@@ -690,55 +1053,132 @@ public class InternalResourceService extends SystemService {
UsageStatsManagerInternal usmi = LocalServices.getService(UsageStatsManagerInternal.class);
usmi.registerListener(mSurveillanceAgent);
+
+ try {
+ mAppOpsService
+ .startWatchingMode(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, null, mApbListener);
+ } catch (RemoteException e) {
+ // shouldn't happen.
+ }
}
/** Perform long-running and/or heavy setup work. This should be called off the main thread. */
private void setupHeavyWork() {
+ if (mBootPhase < PHASE_THIRD_PARTY_APPS_CAN_START || mEnabledMode == ENABLED_MODE_OFF) {
+ return;
+ }
synchronized (mLock) {
+ mCompleteEconomicPolicy.setup(mConfigObserver.getAllDeviceConfigProperties());
loadInstalledPackageListLocked();
- if (mBootPhase >= PHASE_BOOT_COMPLETED && !mExemptListLoaded) {
- try {
- mExemptedApps = new ArraySet<>(mDeviceIdleController.getFullPowerWhitelist());
- } catch (RemoteException e) {
- // Shouldn't happen.
- Slog.wtf(TAG, e);
- }
- mExemptListLoaded = true;
- }
+ final SparseLongArray timeSinceUsersAdded;
final boolean isFirstSetup = !mScribe.recordExists();
+ final long nowElapsed = SystemClock.elapsedRealtime();
if (isFirstSetup) {
mAgent.grantBirthrightsLocked();
mScribe.setConsumptionLimitLocked(
mCompleteEconomicPolicy.getInitialSatiatedConsumptionLimit());
+ // Set the last reclamation time to now so we don't start reclaiming assets
+ // too early.
+ mScribe.setLastReclamationTimeLocked(getCurrentTimeMillis());
+ timeSinceUsersAdded = new SparseLongArray();
} else {
mScribe.loadFromDiskLocked();
if (mScribe.getSatiatedConsumptionLimitLocked()
- < mCompleteEconomicPolicy.getInitialSatiatedConsumptionLimit()
+ < mCompleteEconomicPolicy.getMinSatiatedConsumptionLimit()
|| mScribe.getSatiatedConsumptionLimitLocked()
- > mCompleteEconomicPolicy.getHardSatiatedConsumptionLimit()) {
+ > mCompleteEconomicPolicy.getMaxSatiatedConsumptionLimit()) {
// Reset the consumption limit since several factors may have changed.
mScribe.setConsumptionLimitLocked(
mCompleteEconomicPolicy.getInitialSatiatedConsumptionLimit());
+ } else {
+ // Adjust the supply in case battery level changed while the device was off.
+ adjustCreditSupplyLocked(true);
+ }
+ timeSinceUsersAdded = mScribe.getRealtimeSinceUsersAddedLocked(nowElapsed);
+ }
+
+ final int[] userIds = LocalServices.getService(UserManagerInternal.class).getUserIds();
+ for (int userId : userIds) {
+ final long timeSinceUserAddedMs = timeSinceUsersAdded.get(userId, 0);
+ // Temporarily mark installers as VIPs so they aren't subject to credit
+ // limits and policies on first boot.
+ if (timeSinceUserAddedMs < INSTALLER_FIRST_SETUP_GRACE_PERIOD_MS) {
+ final long remainingGraceDurationMs =
+ INSTALLER_FIRST_SETUP_GRACE_PERIOD_MS - timeSinceUserAddedMs;
+
+ grantInstallersTemporaryVipStatusLocked(userId, nowElapsed,
+ remainingGraceDurationMs);
}
}
scheduleUnusedWealthReclamationLocked();
}
}
- private void setupEverything() {
- if (mBootPhase < PHASE_SYSTEM_SERVICES_READY || !mIsEnabled) {
+ private void onBootPhaseSystemServicesReady() {
+ if (mBootPhase < PHASE_SYSTEM_SERVICES_READY || mEnabledMode == ENABLED_MODE_OFF) {
return;
}
synchronized (mLock) {
registerListeners();
+ // As of Android UDC, users can't change the wellbeing package, so load it once
+ // as soon as possible and don't bother trying to update it afterwards.
+ mWellbeingPackage = mPackageManager.getWellbeingPackageName();
mCurrentBatteryLevel = getCurrentBatteryLevel();
- mHandler.post(this::setupHeavyWork);
- mCompleteEconomicPolicy.setup(mConfigObserver.getAllDeviceConfigProperties());
+ // Get the current battery presence, if available. This would succeed if TARE is
+ // toggled long after boot.
+ final Intent batteryStatus = getContext().registerReceiver(null,
+ new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
+ if (batteryStatus != null) {
+ final boolean hasBattery =
+ batteryStatus.getBooleanExtra(BatteryManager.EXTRA_PRESENT, true);
+ if (mHasBattery != hasBattery) {
+ mHasBattery = hasBattery;
+ mConfigObserver.updateEnabledStatus();
+ }
+ }
+ }
+ }
+
+ private void onBootPhaseThirdPartyAppsCanStart() {
+ if (mBootPhase < PHASE_THIRD_PARTY_APPS_CAN_START || mEnabledMode == ENABLED_MODE_OFF) {
+ return;
+ }
+ mHandler.post(this::setupHeavyWork);
+ }
+
+ private void onBootPhaseBootCompleted() {
+ if (mBootPhase < PHASE_BOOT_COMPLETED || mEnabledMode == ENABLED_MODE_OFF) {
+ return;
+ }
+ synchronized (mLock) {
+ if (!mExemptListLoaded) {
+ try {
+ mExemptedApps = new ArraySet<>(mDeviceIdleController.getFullPowerWhitelist());
+ mExemptListLoaded = true;
+ } catch (RemoteException e) {
+ // Shouldn't happen.
+ }
+ }
+ }
+ }
+
+ private void setupEverything() {
+ if (mEnabledMode == ENABLED_MODE_OFF) {
+ return;
+ }
+ if (mBootPhase >= PHASE_SYSTEM_SERVICES_READY) {
+ onBootPhaseSystemServicesReady();
+ }
+ if (mBootPhase >= PHASE_THIRD_PARTY_APPS_CAN_START) {
+ onBootPhaseThirdPartyAppsCanStart();
+ }
+ if (mBootPhase >= PHASE_BOOT_COMPLETED) {
+ onBootPhaseBootCompleted();
}
}
private void tearDownEverything() {
- if (mIsEnabled) {
+ if (mEnabledMode != ENABLED_MODE_OFF) {
return;
}
synchronized (mLock) {
@@ -761,6 +1201,11 @@ public class InternalResourceService extends SystemService {
UsageStatsManagerInternal usmi =
LocalServices.getService(UsageStatsManagerInternal.class);
usmi.unregisterListener(mSurveillanceAgent);
+ try {
+ mAppOpsService.stopWatchingMode(mApbListener);
+ } catch (RemoteException e) {
+ // shouldn't happen.
+ }
}
synchronized (mPackageToUidCache) {
mPackageToUidCache.clear();
@@ -775,6 +1220,36 @@ public class InternalResourceService extends SystemService {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
+ case MSG_CLEAN_UP_TEMP_VIP_LIST: {
+ removeMessages(MSG_CLEAN_UP_TEMP_VIP_LIST);
+
+ synchronized (mLock) {
+ final long nowElapsed = SystemClock.elapsedRealtime();
+
+ long earliestExpiration = Long.MAX_VALUE;
+ for (int u = 0; u < mTemporaryVips.numMaps(); ++u) {
+ final int userId = mTemporaryVips.keyAt(u);
+
+ for (int p = mTemporaryVips.numElementsForKeyAt(u) - 1; p >= 0; --p) {
+ final String pkgName = mTemporaryVips.keyAt(u, p);
+ final Long expiration = mTemporaryVips.valueAt(u, p);
+
+ if (expiration == null || expiration < nowElapsed) {
+ mTemporaryVips.delete(userId, pkgName);
+ } else {
+ earliestExpiration = Math.min(earliestExpiration, expiration);
+ }
+ }
+ }
+
+ if (earliestExpiration < Long.MAX_VALUE) {
+ sendEmptyMessageDelayed(MSG_CLEAN_UP_TEMP_VIP_LIST,
+ earliestExpiration - nowElapsed);
+ }
+ }
+ }
+ break;
+
case MSG_NOTIFY_AFFORDABILITY_CHANGE_LISTENER: {
final SomeArgs args = (SomeArgs) msg.obj;
final int userId = args.argi1;
@@ -792,9 +1267,30 @@ public class InternalResourceService extends SystemService {
}
break;
+ case MSG_NOTIFY_STATE_CHANGE_LISTENER: {
+ final int policy = msg.arg1;
+ final TareStateChangeListener listener = (TareStateChangeListener) msg.obj;
+ listener.onTareEnabledModeChanged(getEnabledMode(policy));
+ }
+ break;
+
case MSG_NOTIFY_STATE_CHANGE_LISTENERS: {
- for (TareStateChangeListener listener : mStateChangeListeners) {
- listener.onTareEnabledStateChanged(mIsEnabled);
+ final int changedPolicies = msg.arg1;
+ synchronized (mStateChangeListeners) {
+ final int size = mStateChangeListeners.size();
+ for (int l = 0; l < size; ++l) {
+ final int policy = mStateChangeListeners.keyAt(l);
+ if ((policy & changedPolicies) == 0) {
+ continue;
+ }
+ final ArraySet<TareStateChangeListener> listeners =
+ mStateChangeListeners.get(policy);
+ final int enabledMode = getEnabledMode(policy);
+ for (int p = listeners.size() - 1; p >= 0; --p) {
+ final TareStateChangeListener listener = listeners.valueAt(p);
+ listener.onTareEnabledModeChanged(enabledMode);
+ }
+ }
}
}
break;
@@ -853,6 +1349,21 @@ public class InternalResourceService extends SystemService {
Binder.restoreCallingIdentity(identityToken);
}
}
+
+ @Override
+ @EconomyManager.EnabledMode
+ public int getEnabledMode() {
+ return InternalResourceService.this.getEnabledMode();
+ }
+
+ @Override
+ public int handleShellCommand(@NonNull ParcelFileDescriptor in,
+ @NonNull ParcelFileDescriptor out, @NonNull ParcelFileDescriptor err,
+ @NonNull String[] args) {
+ return (new TareShellCommand(InternalResourceService.this)).exec(
+ this, in.getFileDescriptor(), out.getFileDescriptor(), err.getFileDescriptor(),
+ args);
+ }
}
private final class LocalService implements EconomyManagerInternal {
@@ -868,7 +1379,7 @@ public class InternalResourceService extends SystemService {
@Override
public void registerAffordabilityChangeListener(int userId, @NonNull String pkgName,
@NonNull AffordabilityChangeListener listener, @NonNull ActionBill bill) {
- if (isSystem(userId, pkgName)) {
+ if (!isTareSupported() || isSystem(userId, pkgName)) {
// The system's affordability never changes.
return;
}
@@ -890,23 +1401,38 @@ public class InternalResourceService extends SystemService {
}
@Override
- public void registerTareStateChangeListener(@NonNull TareStateChangeListener listener) {
- mStateChangeListeners.add(listener);
+ public void registerTareStateChangeListener(@NonNull TareStateChangeListener listener,
+ int policyId) {
+ if (!isTareSupported()) {
+ return;
+ }
+ synchronized (mStateChangeListeners) {
+ if (mStateChangeListeners.add(policyId, listener)) {
+ mHandler.obtainMessage(MSG_NOTIFY_STATE_CHANGE_LISTENER, policyId, 0, listener)
+ .sendToTarget();
+ }
+ }
}
@Override
public void unregisterTareStateChangeListener(@NonNull TareStateChangeListener listener) {
- mStateChangeListeners.remove(listener);
+ synchronized (mStateChangeListeners) {
+ for (int i = mStateChangeListeners.size() - 1; i >= 0; --i) {
+ final ArraySet<TareStateChangeListener> listeners =
+ mStateChangeListeners.get(mStateChangeListeners.keyAt(i));
+ listeners.remove(listener);
+ }
+ }
}
@Override
public boolean canPayFor(int userId, @NonNull String pkgName, @NonNull ActionBill bill) {
- if (!mIsEnabled) {
+ if (mEnabledMode == ENABLED_MODE_OFF) {
return true;
}
- if (isSystem(userId, pkgName)) {
+ if (isVip(userId, pkgName)) {
// The government, I mean the system, can create ARCs as it needs to in order to
- // operate.
+ // allow VIPs to operate.
return true;
}
// TODO: take temp-allowlist into consideration
@@ -929,10 +1455,10 @@ public class InternalResourceService extends SystemService {
@Override
public long getMaxDurationMs(int userId, @NonNull String pkgName,
@NonNull ActionBill bill) {
- if (!mIsEnabled) {
+ if (mEnabledMode == ENABLED_MODE_OFF) {
return FOREVER_MS;
}
- if (isSystem(userId, pkgName)) {
+ if (isVip(userId, pkgName)) {
return FOREVER_MS;
}
long totalCostPerSecond = 0;
@@ -956,14 +1482,19 @@ public class InternalResourceService extends SystemService {
}
@Override
- public boolean isEnabled() {
- return mIsEnabled;
+ public int getEnabledMode() {
+ return mEnabledMode;
+ }
+
+ @Override
+ public int getEnabledMode(int policyId) {
+ return InternalResourceService.this.getEnabledMode(policyId);
}
@Override
public void noteInstantaneousEvent(int userId, @NonNull String pkgName, int eventId,
@Nullable String tag) {
- if (!mIsEnabled) {
+ if (mEnabledMode == ENABLED_MODE_OFF) {
return;
}
synchronized (mLock) {
@@ -974,7 +1505,7 @@ public class InternalResourceService extends SystemService {
@Override
public void noteOngoingEventStarted(int userId, @NonNull String pkgName, int eventId,
@Nullable String tag) {
- if (!mIsEnabled) {
+ if (mEnabledMode == ENABLED_MODE_OFF) {
return;
}
synchronized (mLock) {
@@ -986,7 +1517,7 @@ public class InternalResourceService extends SystemService {
@Override
public void noteOngoingEventStopped(int userId, @NonNull String pkgName, int eventId,
@Nullable String tag) {
- if (!mIsEnabled) {
+ if (mEnabledMode == ENABLED_MODE_OFF) {
return;
}
final long nowElapsed = SystemClock.elapsedRealtime();
@@ -999,7 +1530,14 @@ public class InternalResourceService extends SystemService {
private class ConfigObserver extends ContentObserver
implements DeviceConfig.OnPropertiesChangedListener {
- private static final String KEY_DC_ENABLE_TARE = "enable_tare";
+ private static final String KEY_ENABLE_TIP3 = "enable_tip3";
+ private static final String KEY_TARGET_BACKGROUND_BATTERY_LIFE_HOURS =
+ "target_bg_battery_life_hrs";
+
+ private static final boolean DEFAULT_ENABLE_TIP3 = true;
+
+ /** Use a target background battery drain rate to determine consumption limits. */
+ public boolean ENABLE_TIP3 = DEFAULT_ENABLE_TIP3;
private final ContentResolver mContentResolver;
@@ -1047,12 +1585,23 @@ public class InternalResourceService extends SystemService {
continue;
}
switch (name) {
- case KEY_DC_ENABLE_TARE:
+ case EconomyManager.KEY_ENABLE_TARE_MODE:
updateEnabledStatus();
break;
+ case KEY_ENABLE_TIP3:
+ ENABLE_TIP3 = properties.getBoolean(name, DEFAULT_ENABLE_TIP3);
+ break;
+ case KEY_TARGET_BACKGROUND_BATTERY_LIFE_HOURS:
+ synchronized (mLock) {
+ mTargetBackgroundBatteryLifeHours = properties.getInt(name,
+ mDefaultTargetBackgroundBatteryLifeHours);
+ maybeAdjustDesiredStockLevelLocked();
+ }
+ break;
default:
if (!economicPolicyUpdated
- && (name.startsWith("am") || name.startsWith("js"))) {
+ && (name.startsWith("am") || name.startsWith("js")
+ || name.startsWith("enable_policy"))) {
updateEconomicPolicy();
economicPolicyUpdated = true;
}
@@ -1063,46 +1612,111 @@ public class InternalResourceService extends SystemService {
private void updateEnabledStatus() {
// User setting should override DeviceConfig setting.
- // NOTE: There's currently no way for a user to reset the value (via UI), so if a user
- // manually toggles TARE via UI, we'll always defer to the user's current setting
- // TODO: add a "reset" value if the user toggle is an issue
- final boolean isTareEnabledDC = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_TARE,
- KEY_DC_ENABLE_TARE, Settings.Global.DEFAULT_ENABLE_TARE == 1);
- final boolean isTareEnabled = Settings.Global.getInt(mContentResolver,
- Settings.Global.ENABLE_TARE, isTareEnabledDC ? 1 : 0) == 1;
- if (mIsEnabled != isTareEnabled) {
- mIsEnabled = isTareEnabled;
- if (mIsEnabled) {
- setupEverything();
- } else {
- tearDownEverything();
+ final int tareEnabledModeDC = DeviceConfig.getInt(DeviceConfig.NAMESPACE_TARE,
+ EconomyManager.KEY_ENABLE_TARE_MODE, EconomyManager.DEFAULT_ENABLE_TARE_MODE);
+ final int tareEnabledModeConfig = isTareSupported()
+ ? Settings.Global.getInt(mContentResolver,
+ Settings.Global.ENABLE_TARE, tareEnabledModeDC)
+ : ENABLED_MODE_OFF;
+ final int enabledMode;
+ if (tareEnabledModeConfig == ENABLED_MODE_OFF
+ || tareEnabledModeConfig == ENABLED_MODE_ON
+ || tareEnabledModeConfig == ENABLED_MODE_SHADOW) {
+ // Config has a valid enabled mode.
+ enabledMode = tareEnabledModeConfig;
+ } else {
+ enabledMode = EconomyManager.DEFAULT_ENABLE_TARE_MODE;
+ }
+ if (mEnabledMode != enabledMode) {
+ // A full change where we've gone from OFF to {SHADOW or ON}, or vie versa.
+ // With this transition, we'll have to set up or tear down.
+ final boolean fullEnableChange =
+ mEnabledMode == ENABLED_MODE_OFF || enabledMode == ENABLED_MODE_OFF;
+ mEnabledMode = enabledMode;
+ if (fullEnableChange) {
+ if (mEnabledMode != ENABLED_MODE_OFF) {
+ setupEverything();
+ } else {
+ tearDownEverything();
+ }
}
- mHandler.sendEmptyMessage(MSG_NOTIFY_STATE_CHANGE_LISTENERS);
+ mHandler.obtainMessage(
+ MSG_NOTIFY_STATE_CHANGE_LISTENERS, EconomicPolicy.ALL_POLICIES, 0)
+ .sendToTarget();
}
}
private void updateEconomicPolicy() {
synchronized (mLock) {
- final long initialLimit =
- mCompleteEconomicPolicy.getInitialSatiatedConsumptionLimit();
- final long hardLimit = mCompleteEconomicPolicy.getHardSatiatedConsumptionLimit();
+ final long minLimit = mCompleteEconomicPolicy.getMinSatiatedConsumptionLimit();
+ final long maxLimit = mCompleteEconomicPolicy.getMaxSatiatedConsumptionLimit();
+ final int oldEnabledPolicies = mCompleteEconomicPolicy.getEnabledPolicyIds();
mCompleteEconomicPolicy.tearDown();
mCompleteEconomicPolicy = new CompleteEconomicPolicy(InternalResourceService.this);
- if (mIsEnabled && mBootPhase >= PHASE_SYSTEM_SERVICES_READY) {
+ if (mEnabledMode != ENABLED_MODE_OFF
+ && mBootPhase >= PHASE_THIRD_PARTY_APPS_CAN_START) {
mCompleteEconomicPolicy.setup(getAllDeviceConfigProperties());
- if (initialLimit != mCompleteEconomicPolicy.getInitialSatiatedConsumptionLimit()
- || hardLimit
- != mCompleteEconomicPolicy.getHardSatiatedConsumptionLimit()) {
+ if (minLimit != mCompleteEconomicPolicy.getMinSatiatedConsumptionLimit()
+ || maxLimit
+ != mCompleteEconomicPolicy.getMaxSatiatedConsumptionLimit()) {
// Reset the consumption limit since several factors may have changed.
mScribe.setConsumptionLimitLocked(
mCompleteEconomicPolicy.getInitialSatiatedConsumptionLimit());
}
mAgent.onPricingChangedLocked();
+ final int newEnabledPolicies = mCompleteEconomicPolicy.getEnabledPolicyIds();
+ if (oldEnabledPolicies != newEnabledPolicies) {
+ final int changedPolicies = oldEnabledPolicies ^ newEnabledPolicies;
+ mHandler.obtainMessage(
+ MSG_NOTIFY_STATE_CHANGE_LISTENERS, changedPolicies, 0)
+ .sendToTarget();
+ }
+ }
+ }
+ }
+ }
+
+ // Shell command infrastructure
+ int executeClearVip(@NonNull PrintWriter pw) {
+ synchronized (mLock) {
+ final SparseSetArray<String> changedPkgs = new SparseSetArray<>();
+ for (int u = mVipOverrides.numMaps() - 1; u >= 0; --u) {
+ final int userId = mVipOverrides.keyAt(u);
+
+ for (int p = mVipOverrides.numElementsForKeyAt(u) - 1; p >= 0; --p) {
+ changedPkgs.add(userId, mVipOverrides.keyAt(u, p));
}
}
+ mVipOverrides.clear();
+ if (mEnabledMode != ENABLED_MODE_OFF) {
+ mAgent.onVipStatusChangedLocked(changedPkgs);
+ }
+ }
+ pw.println("Cleared all VIP statuses");
+ return TareShellCommand.COMMAND_SUCCESS;
+ }
+
+ int executeSetVip(@NonNull PrintWriter pw,
+ int userId, @NonNull String pkgName, @Nullable Boolean newVipState) {
+ final boolean changed;
+ synchronized (mLock) {
+ final boolean wasVip = isVip(userId, pkgName);
+ if (newVipState == null) {
+ mVipOverrides.delete(userId, pkgName);
+ } else {
+ mVipOverrides.add(userId, pkgName, newVipState);
+ }
+ changed = isVip(userId, pkgName) != wasVip;
+ if (mEnabledMode != ENABLED_MODE_OFF && changed) {
+ mAgent.onVipStatusChangedLocked(userId, pkgName);
+ }
}
+ pw.println(appToString(userId, pkgName) + " VIP status set to " + newVipState + "."
+ + " Final VIP state changed? " + changed);
+ return TareShellCommand.COMMAND_SUCCESS;
}
+ // Dump infrastructure
private static void dumpHelp(PrintWriter pw) {
pw.println("Resource Economy (economy) dump options:");
pw.println(" [-h|--help] [package] ...");
@@ -1111,9 +1725,13 @@ public class InternalResourceService extends SystemService {
}
private void dumpInternal(final IndentingPrintWriter pw, final boolean dumpAll) {
+ if (!isTareSupported()) {
+ pw.print("Unsupported by device");
+ return;
+ }
synchronized (mLock) {
- pw.print("Is enabled: ");
- pw.println(mIsEnabled);
+ pw.print("Enabled mode: ");
+ pw.println(enabledModeToString(mEnabledMode));
pw.print("Current battery level: ");
pw.println(mCurrentBatteryLevel);
@@ -1126,6 +1744,12 @@ public class InternalResourceService extends SystemService {
pw.print("/");
pw.println(cakeToString(mScribe.getSatiatedConsumptionLimitLocked()));
+ pw.print("Target bg battery life (hours): ");
+ pw.print(mTargetBackgroundBatteryLifeHours);
+ pw.print(" (");
+ pw.print(String.format("%.2f", 100f / mTargetBackgroundBatteryLifeHours));
+ pw.println("%/hr)");
+
final long remainingConsumable = mScribe.getRemainingConsumableCakesLocked();
pw.print("Goods remaining: ");
pw.print(cakeToString(remainingConsumable));
@@ -1141,6 +1765,77 @@ public class InternalResourceService extends SystemService {
pw.println();
pw.println();
+ pw.print("Wellbeing app=");
+ pw.println(mWellbeingPackage == null ? "None" : mWellbeingPackage);
+
+ boolean printedVips = false;
+ pw.println();
+ pw.print("VIPs:");
+ pw.increaseIndent();
+ for (int u = 0; u < mVipOverrides.numMaps(); ++u) {
+ final int userId = mVipOverrides.keyAt(u);
+
+ for (int p = 0; p < mVipOverrides.numElementsForKeyAt(u); ++p) {
+ final String pkgName = mVipOverrides.keyAt(u, p);
+
+ printedVips = true;
+ pw.println();
+ pw.print(appToString(userId, pkgName));
+ pw.print("=");
+ pw.print(mVipOverrides.valueAt(u, p));
+ }
+ }
+ if (printedVips) {
+ pw.println();
+ } else {
+ pw.print(" None");
+ }
+ pw.decreaseIndent();
+ pw.println();
+
+ boolean printedTempVips = false;
+ pw.println();
+ pw.print("Temp VIPs:");
+ pw.increaseIndent();
+ for (int u = 0; u < mTemporaryVips.numMaps(); ++u) {
+ final int userId = mTemporaryVips.keyAt(u);
+
+ for (int p = 0; p < mTemporaryVips.numElementsForKeyAt(u); ++p) {
+ final String pkgName = mTemporaryVips.keyAt(u, p);
+
+ printedTempVips = true;
+ pw.println();
+ pw.print(appToString(userId, pkgName));
+ pw.print("=");
+ pw.print(mTemporaryVips.valueAt(u, p));
+ }
+ }
+ if (printedTempVips) {
+ pw.println();
+ } else {
+ pw.print(" None");
+ }
+ pw.decreaseIndent();
+ pw.println();
+
+ pw.println();
+ pw.println("Installers:");
+ pw.increaseIndent();
+ for (int u = 0; u < mInstallers.numMaps(); ++u) {
+ final int userId = mInstallers.keyAt(u);
+
+ for (int p = 0; p < mInstallers.numElementsForKeyAt(u); ++p) {
+ final String pkgName = mInstallers.keyAt(u, p);
+
+ pw.print(appToString(userId, pkgName));
+ pw.print(": ");
+ pw.print(mInstallers.valueAt(u, p).size());
+ pw.println(" apps");
+ }
+ }
+ pw.decreaseIndent();
+
+ pw.println();
mCompleteEconomicPolicy.dump(pw);
pw.println();
@@ -1151,6 +1846,37 @@ public class InternalResourceService extends SystemService {
pw.println();
mAnalyst.dump(pw);
+
+ // Put this at the end since this may be a lot and we want to have the earlier
+ // information easily accessible.
+ boolean printedInterestingIpos = false;
+ pw.println();
+ pw.print("Interesting apps:");
+ pw.increaseIndent();
+ for (int u = 0; u < mPkgCache.numMaps(); ++u) {
+ for (int p = 0; p < mPkgCache.numElementsForKeyAt(u); ++p) {
+ final InstalledPackageInfo ipo = mPkgCache.valueAt(u, p);
+
+ // Printing out every single app will be too much. Only print apps that
+ // have some interesting characteristic.
+ final boolean isInteresting = ipo.hasCode
+ && ipo.isHeadlessSystemApp
+ && !UserHandle.isCore(ipo.uid);
+ if (!isInteresting) {
+ continue;
+ }
+
+ printedInterestingIpos = true;
+ pw.println();
+ pw.print(ipo);
+ }
+ }
+ if (printedInterestingIpos) {
+ pw.println();
+ } else {
+ pw.print(" None");
+ }
+ pw.decreaseIndent();
}
}
}
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/JobSchedulerEconomicPolicy.java b/apex/jobscheduler/service/java/com/android/server/tare/JobSchedulerEconomicPolicy.java
index 948f0a71510c..91a291fe20db 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/JobSchedulerEconomicPolicy.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/JobSchedulerEconomicPolicy.java
@@ -38,11 +38,17 @@ import static android.app.tare.EconomyManager.DEFAULT_JS_ACTION_JOB_MIN_START_BA
import static android.app.tare.EconomyManager.DEFAULT_JS_ACTION_JOB_MIN_START_CTP_CAKES;
import static android.app.tare.EconomyManager.DEFAULT_JS_ACTION_JOB_TIMEOUT_PENALTY_BASE_PRICE_CAKES;
import static android.app.tare.EconomyManager.DEFAULT_JS_ACTION_JOB_TIMEOUT_PENALTY_CTP_CAKES;
-import static android.app.tare.EconomyManager.DEFAULT_JS_HARD_CONSUMPTION_LIMIT_CAKES;
import static android.app.tare.EconomyManager.DEFAULT_JS_INITIAL_CONSUMPTION_LIMIT_CAKES;
+import static android.app.tare.EconomyManager.DEFAULT_JS_MAX_CONSUMPTION_LIMIT_CAKES;
import static android.app.tare.EconomyManager.DEFAULT_JS_MAX_SATIATED_BALANCE_CAKES;
+import static android.app.tare.EconomyManager.DEFAULT_JS_MIN_CONSUMPTION_LIMIT_CAKES;
import static android.app.tare.EconomyManager.DEFAULT_JS_MIN_SATIATED_BALANCE_EXEMPTED_CAKES;
+import static android.app.tare.EconomyManager.DEFAULT_JS_MIN_SATIATED_BALANCE_HEADLESS_SYSTEM_APP_CAKES;
+import static android.app.tare.EconomyManager.DEFAULT_JS_MIN_SATIATED_BALANCE_INCREMENT_APP_UPDATER_CAKES;
import static android.app.tare.EconomyManager.DEFAULT_JS_MIN_SATIATED_BALANCE_OTHER_APP_CAKES;
+import static android.app.tare.EconomyManager.DEFAULT_JS_REWARD_APP_INSTALL_INSTANT_CAKES;
+import static android.app.tare.EconomyManager.DEFAULT_JS_REWARD_APP_INSTALL_MAX_CAKES;
+import static android.app.tare.EconomyManager.DEFAULT_JS_REWARD_APP_INSTALL_ONGOING_CAKES;
import static android.app.tare.EconomyManager.DEFAULT_JS_REWARD_NOTIFICATION_INTERACTION_INSTANT_CAKES;
import static android.app.tare.EconomyManager.DEFAULT_JS_REWARD_NOTIFICATION_INTERACTION_MAX_CAKES;
import static android.app.tare.EconomyManager.DEFAULT_JS_REWARD_NOTIFICATION_INTERACTION_ONGOING_CAKES;
@@ -80,11 +86,17 @@ import static android.app.tare.EconomyManager.KEY_JS_ACTION_JOB_MIN_START_BASE_P
import static android.app.tare.EconomyManager.KEY_JS_ACTION_JOB_MIN_START_CTP;
import static android.app.tare.EconomyManager.KEY_JS_ACTION_JOB_TIMEOUT_PENALTY_BASE_PRICE;
import static android.app.tare.EconomyManager.KEY_JS_ACTION_JOB_TIMEOUT_PENALTY_CTP;
-import static android.app.tare.EconomyManager.KEY_JS_HARD_CONSUMPTION_LIMIT;
import static android.app.tare.EconomyManager.KEY_JS_INITIAL_CONSUMPTION_LIMIT;
+import static android.app.tare.EconomyManager.KEY_JS_MAX_CONSUMPTION_LIMIT;
import static android.app.tare.EconomyManager.KEY_JS_MAX_SATIATED_BALANCE;
+import static android.app.tare.EconomyManager.KEY_JS_MIN_CONSUMPTION_LIMIT;
import static android.app.tare.EconomyManager.KEY_JS_MIN_SATIATED_BALANCE_EXEMPTED;
+import static android.app.tare.EconomyManager.KEY_JS_MIN_SATIATED_BALANCE_HEADLESS_SYSTEM_APP;
+import static android.app.tare.EconomyManager.KEY_JS_MIN_SATIATED_BALANCE_INCREMENT_APP_UPDATER;
import static android.app.tare.EconomyManager.KEY_JS_MIN_SATIATED_BALANCE_OTHER_APP;
+import static android.app.tare.EconomyManager.KEY_JS_REWARD_APP_INSTALL_INSTANT;
+import static android.app.tare.EconomyManager.KEY_JS_REWARD_APP_INSTALL_MAX;
+import static android.app.tare.EconomyManager.KEY_JS_REWARD_APP_INSTALL_ONGOING;
import static android.app.tare.EconomyManager.KEY_JS_REWARD_NOTIFICATION_INTERACTION_INSTANT;
import static android.app.tare.EconomyManager.KEY_JS_REWARD_NOTIFICATION_INTERACTION_MAX;
import static android.app.tare.EconomyManager.KEY_JS_REWARD_NOTIFICATION_INTERACTION_ONGOING;
@@ -100,19 +112,20 @@ import static android.app.tare.EconomyManager.KEY_JS_REWARD_TOP_ACTIVITY_ONGOING
import static android.app.tare.EconomyManager.KEY_JS_REWARD_WIDGET_INTERACTION_INSTANT;
import static android.app.tare.EconomyManager.KEY_JS_REWARD_WIDGET_INTERACTION_MAX;
import static android.app.tare.EconomyManager.KEY_JS_REWARD_WIDGET_INTERACTION_ONGOING;
+import static android.app.tare.EconomyManager.arcToCake;
import static android.provider.Settings.Global.TARE_JOB_SCHEDULER_CONSTANTS;
import static com.android.server.tare.Modifier.COST_MODIFIER_CHARGING;
import static com.android.server.tare.Modifier.COST_MODIFIER_DEVICE_IDLE;
import static com.android.server.tare.Modifier.COST_MODIFIER_POWER_SAVE_MODE;
import static com.android.server.tare.Modifier.COST_MODIFIER_PROCESS_STATE;
+import static com.android.server.tare.TareUtils.appToString;
import static com.android.server.tare.TareUtils.cakeToString;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ContentResolver;
import android.provider.DeviceConfig;
-import android.provider.Settings;
import android.util.IndentingPrintWriter;
import android.util.KeyValueListParser;
import android.util.Slog;
@@ -125,17 +138,19 @@ import android.util.SparseArray;
public class JobSchedulerEconomicPolicy extends EconomicPolicy {
private static final String TAG = "TARE- " + JobSchedulerEconomicPolicy.class.getSimpleName();
- public static final int ACTION_JOB_MAX_START = TYPE_ACTION | POLICY_JS | 0;
- public static final int ACTION_JOB_MAX_RUNNING = TYPE_ACTION | POLICY_JS | 1;
- public static final int ACTION_JOB_HIGH_START = TYPE_ACTION | POLICY_JS | 2;
- public static final int ACTION_JOB_HIGH_RUNNING = TYPE_ACTION | POLICY_JS | 3;
- public static final int ACTION_JOB_DEFAULT_START = TYPE_ACTION | POLICY_JS | 4;
- public static final int ACTION_JOB_DEFAULT_RUNNING = TYPE_ACTION | POLICY_JS | 5;
- public static final int ACTION_JOB_LOW_START = TYPE_ACTION | POLICY_JS | 6;
- public static final int ACTION_JOB_LOW_RUNNING = TYPE_ACTION | POLICY_JS | 7;
- public static final int ACTION_JOB_MIN_START = TYPE_ACTION | POLICY_JS | 8;
- public static final int ACTION_JOB_MIN_RUNNING = TYPE_ACTION | POLICY_JS | 9;
- public static final int ACTION_JOB_TIMEOUT = TYPE_ACTION | POLICY_JS | 10;
+ public static final int ACTION_JOB_MAX_START = TYPE_ACTION | POLICY_JOB | 0;
+ public static final int ACTION_JOB_MAX_RUNNING = TYPE_ACTION | POLICY_JOB | 1;
+ public static final int ACTION_JOB_HIGH_START = TYPE_ACTION | POLICY_JOB | 2;
+ public static final int ACTION_JOB_HIGH_RUNNING = TYPE_ACTION | POLICY_JOB | 3;
+ public static final int ACTION_JOB_DEFAULT_START = TYPE_ACTION | POLICY_JOB | 4;
+ public static final int ACTION_JOB_DEFAULT_RUNNING = TYPE_ACTION | POLICY_JOB | 5;
+ public static final int ACTION_JOB_LOW_START = TYPE_ACTION | POLICY_JOB | 6;
+ public static final int ACTION_JOB_LOW_RUNNING = TYPE_ACTION | POLICY_JOB | 7;
+ public static final int ACTION_JOB_MIN_START = TYPE_ACTION | POLICY_JOB | 8;
+ public static final int ACTION_JOB_MIN_RUNNING = TYPE_ACTION | POLICY_JOB | 9;
+ public static final int ACTION_JOB_TIMEOUT = TYPE_ACTION | POLICY_JOB | 10;
+
+ public static final int REWARD_APP_INSTALL = TYPE_REWARD | POLICY_JOB | 0;
private static final int[] COST_MODIFIERS = new int[]{
COST_MODIFIER_CHARGING,
@@ -145,42 +160,78 @@ public class JobSchedulerEconomicPolicy extends EconomicPolicy {
};
private long mMinSatiatedBalanceExempted;
+ private long mMinSatiatedBalanceHeadlessSystemApp;
private long mMinSatiatedBalanceOther;
+ private long mMinSatiatedBalanceIncrementalAppUpdater;
private long mMaxSatiatedBalance;
private long mInitialSatiatedConsumptionLimit;
- private long mHardSatiatedConsumptionLimit;
+ private long mMinSatiatedConsumptionLimit;
+ private long mMaxSatiatedConsumptionLimit;
private final KeyValueListParser mParser = new KeyValueListParser(',');
- private final InternalResourceService mInternalResourceService;
+ private final Injector mInjector;
private final SparseArray<Action> mActions = new SparseArray<>();
private final SparseArray<Reward> mRewards = new SparseArray<>();
- JobSchedulerEconomicPolicy(InternalResourceService irs) {
+ JobSchedulerEconomicPolicy(InternalResourceService irs, Injector injector) {
super(irs);
- mInternalResourceService = irs;
+ mInjector = injector;
loadConstants("", null);
}
@Override
void setup(@NonNull DeviceConfig.Properties properties) {
super.setup(properties);
- ContentResolver resolver = mInternalResourceService.getContext().getContentResolver();
- loadConstants(Settings.Global.getString(resolver, TARE_JOB_SCHEDULER_CONSTANTS),
+ final ContentResolver resolver = mIrs.getContext().getContentResolver();
+ loadConstants(mInjector.getSettingsGlobalString(resolver, TARE_JOB_SCHEDULER_CONSTANTS),
properties);
}
@Override
long getMinSatiatedBalance(final int userId, @NonNull final String pkgName) {
- if (mInternalResourceService.isPackageExempted(userId, pkgName)) {
- return mMinSatiatedBalanceExempted;
+ if (mIrs.isPackageRestricted(userId, pkgName)) {
+ return 0;
+ }
+
+ final long baseBalance;
+ if (mIrs.isPackageExempted(userId, pkgName)) {
+ baseBalance = mMinSatiatedBalanceExempted;
+ } else if (mIrs.isHeadlessSystemApp(userId, pkgName)) {
+ baseBalance = mMinSatiatedBalanceHeadlessSystemApp;
+ } else {
+ baseBalance = mMinSatiatedBalanceOther;
}
- // TODO: take other exemptions into account
- return mMinSatiatedBalanceOther;
+
+ long minBalance = baseBalance;
+
+ final int updateResponsibilityCount = mIrs.getAppUpdateResponsibilityCount(userId, pkgName);
+ minBalance += updateResponsibilityCount * mMinSatiatedBalanceIncrementalAppUpdater;
+
+ return Math.min(minBalance, mMaxSatiatedBalance);
}
@Override
- long getMaxSatiatedBalance() {
+ long getMaxSatiatedBalance(int userId, @NonNull String pkgName) {
+ if (mIrs.isPackageRestricted(userId, pkgName)) {
+ return 0;
+ }
+ final InstalledPackageInfo ipo = mIrs.getInstalledPackageInfo(userId, pkgName);
+ if (ipo == null) {
+ Slog.wtfStack(TAG,
+ "Tried to get max balance of invalid app: " + appToString(userId, pkgName));
+ } else {
+ // A system installer's max balance is elevated for some time after first boot so
+ // they can use jobs to download and install apps.
+ if (ipo.isSystemInstaller) {
+ final long timeSinceFirstSetupMs = mIrs.getRealtimeSinceFirstSetupMs();
+ final boolean stillExempted = timeSinceFirstSetupMs
+ < InternalResourceService.INSTALLER_FIRST_SETUP_GRACE_PERIOD_MS;
+ if (stillExempted) {
+ return mMaxSatiatedConsumptionLimit;
+ }
+ }
+ }
return mMaxSatiatedBalance;
}
@@ -190,8 +241,13 @@ public class JobSchedulerEconomicPolicy extends EconomicPolicy {
}
@Override
- long getHardSatiatedConsumptionLimit() {
- return mHardSatiatedConsumptionLimit;
+ long getMinSatiatedConsumptionLimit() {
+ return mMinSatiatedConsumptionLimit;
+ }
+
+ @Override
+ long getMaxSatiatedConsumptionLimit() {
+ return mMaxSatiatedConsumptionLimit;
}
@NonNull
@@ -223,22 +279,31 @@ public class JobSchedulerEconomicPolicy extends EconomicPolicy {
Slog.e(TAG, "Global setting key incorrect: ", e);
}
+ mMinSatiatedBalanceOther = getConstantAsCake(mParser, properties,
+ KEY_JS_MIN_SATIATED_BALANCE_OTHER_APP, DEFAULT_JS_MIN_SATIATED_BALANCE_OTHER_APP_CAKES);
+ mMinSatiatedBalanceHeadlessSystemApp = getConstantAsCake(mParser, properties,
+ KEY_JS_MIN_SATIATED_BALANCE_HEADLESS_SYSTEM_APP,
+ DEFAULT_JS_MIN_SATIATED_BALANCE_HEADLESS_SYSTEM_APP_CAKES,
+ mMinSatiatedBalanceOther);
mMinSatiatedBalanceExempted = getConstantAsCake(mParser, properties,
KEY_JS_MIN_SATIATED_BALANCE_EXEMPTED,
- DEFAULT_JS_MIN_SATIATED_BALANCE_EXEMPTED_CAKES);
- mMinSatiatedBalanceOther = getConstantAsCake(mParser, properties,
- KEY_JS_MIN_SATIATED_BALANCE_OTHER_APP,
- DEFAULT_JS_MIN_SATIATED_BALANCE_OTHER_APP_CAKES);
+ DEFAULT_JS_MIN_SATIATED_BALANCE_EXEMPTED_CAKES,
+ mMinSatiatedBalanceHeadlessSystemApp);
+ mMinSatiatedBalanceIncrementalAppUpdater = getConstantAsCake(mParser, properties,
+ KEY_JS_MIN_SATIATED_BALANCE_INCREMENT_APP_UPDATER,
+ DEFAULT_JS_MIN_SATIATED_BALANCE_INCREMENT_APP_UPDATER_CAKES);
mMaxSatiatedBalance = getConstantAsCake(mParser, properties,
- KEY_JS_MAX_SATIATED_BALANCE,
- DEFAULT_JS_MAX_SATIATED_BALANCE_CAKES);
+ KEY_JS_MAX_SATIATED_BALANCE, DEFAULT_JS_MAX_SATIATED_BALANCE_CAKES,
+ Math.max(arcToCake(1), mMinSatiatedBalanceExempted));
+ mMinSatiatedConsumptionLimit = getConstantAsCake(mParser, properties,
+ KEY_JS_MIN_CONSUMPTION_LIMIT, DEFAULT_JS_MIN_CONSUMPTION_LIMIT_CAKES,
+ arcToCake(1));
mInitialSatiatedConsumptionLimit = getConstantAsCake(mParser, properties,
- KEY_JS_INITIAL_CONSUMPTION_LIMIT,
- DEFAULT_JS_INITIAL_CONSUMPTION_LIMIT_CAKES);
- mHardSatiatedConsumptionLimit = Math.max(mInitialSatiatedConsumptionLimit,
- getConstantAsCake(mParser, properties,
- KEY_JS_HARD_CONSUMPTION_LIMIT,
- DEFAULT_JS_HARD_CONSUMPTION_LIMIT_CAKES));
+ KEY_JS_INITIAL_CONSUMPTION_LIMIT, DEFAULT_JS_INITIAL_CONSUMPTION_LIMIT_CAKES,
+ mMinSatiatedConsumptionLimit);
+ mMaxSatiatedConsumptionLimit = getConstantAsCake(mParser, properties,
+ KEY_JS_MAX_CONSUMPTION_LIMIT, DEFAULT_JS_MAX_CONSUMPTION_LIMIT_CAKES,
+ mInitialSatiatedConsumptionLimit);
mActions.put(ACTION_JOB_MAX_START, new Action(ACTION_JOB_MAX_START,
getConstantAsCake(mParser, properties,
@@ -370,20 +435,34 @@ public class JobSchedulerEconomicPolicy extends EconomicPolicy {
getConstantAsCake(mParser, properties,
KEY_JS_REWARD_OTHER_USER_INTERACTION_MAX,
DEFAULT_JS_REWARD_OTHER_USER_INTERACTION_MAX_CAKES)));
+ mRewards.put(REWARD_APP_INSTALL,
+ new Reward(REWARD_APP_INSTALL,
+ getConstantAsCake(mParser, properties,
+ KEY_JS_REWARD_APP_INSTALL_INSTANT,
+ DEFAULT_JS_REWARD_APP_INSTALL_INSTANT_CAKES),
+ getConstantAsCake(mParser, properties,
+ KEY_JS_REWARD_APP_INSTALL_ONGOING,
+ DEFAULT_JS_REWARD_APP_INSTALL_ONGOING_CAKES),
+ getConstantAsCake(mParser, properties,
+ KEY_JS_REWARD_APP_INSTALL_MAX,
+ DEFAULT_JS_REWARD_APP_INSTALL_MAX_CAKES)));
}
@Override
void dump(IndentingPrintWriter pw) {
- pw.println("Min satiated balances:");
+ pw.println("Min satiated balance:");
pw.increaseIndent();
pw.print("Exempted", cakeToString(mMinSatiatedBalanceExempted)).println();
pw.print("Other", cakeToString(mMinSatiatedBalanceOther)).println();
+ pw.print("+App Updater", cakeToString(mMinSatiatedBalanceIncrementalAppUpdater)).println();
pw.decreaseIndent();
pw.print("Max satiated balance", cakeToString(mMaxSatiatedBalance)).println();
pw.print("Consumption limits: [");
+ pw.print(cakeToString(mMinSatiatedConsumptionLimit));
+ pw.print(", ");
pw.print(cakeToString(mInitialSatiatedConsumptionLimit));
pw.print(", ");
- pw.print(cakeToString(mHardSatiatedConsumptionLimit));
+ pw.print(cakeToString(mMaxSatiatedConsumptionLimit));
pw.println("]");
pw.println();
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/Ledger.java b/apex/jobscheduler/service/java/com/android/server/tare/Ledger.java
index 2e2a9b56d3df..a68170c9bac7 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/Ledger.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/Ledger.java
@@ -17,15 +17,19 @@
package com.android.server.tare;
import static android.text.format.DateUtils.HOUR_IN_MILLIS;
+import static android.util.TimeUtils.dumpTime;
import static com.android.server.tare.TareUtils.cakeToString;
-import static com.android.server.tare.TareUtils.dumpTime;
import static com.android.server.tare.TareUtils.getCurrentTimeMillis;
+import android.annotation.CurrentTimeMillisLong;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.util.IndentingPrintWriter;
import android.util.SparseLongArray;
+import android.util.TimeUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
import java.util.ArrayList;
import java.util.List;
@@ -34,6 +38,21 @@ import java.util.List;
* Ledger to track the last recorded balance and recent activities of an app.
*/
class Ledger {
+ /** The window size within which rewards will be counted and used towards reward limiting. */
+ private static final long TOTAL_REWARD_WINDOW_MS = 24 * HOUR_IN_MILLIS;
+ /** The number of buckets to split {@link #TOTAL_REWARD_WINDOW_MS} into. */
+ @VisibleForTesting
+ static final int NUM_REWARD_BUCKET_WINDOWS = 4;
+ /**
+ * The duration size of each bucket resulting from splitting {@link #TOTAL_REWARD_WINDOW_MS}
+ * into smaller buckets.
+ */
+ private static final long REWARD_BUCKET_WINDOW_SIZE_MS =
+ TOTAL_REWARD_WINDOW_MS / NUM_REWARD_BUCKET_WINDOWS;
+ /** The maximum number of transactions to retain in memory at any one time. */
+ @VisibleForTesting
+ static final int MAX_TRANSACTION_COUNT = 50;
+
static class Transaction {
public final long startTimeMs;
public final long endTimeMs;
@@ -54,18 +73,47 @@ class Ledger {
}
}
+ static class RewardBucket {
+ @CurrentTimeMillisLong
+ public long startTimeMs;
+ public final SparseLongArray cumulativeDelta = new SparseLongArray();
+
+ private void reset() {
+ startTimeMs = 0;
+ cumulativeDelta.clear();
+ }
+ }
+
/** Last saved balance. This doesn't take currently ongoing events into account. */
private long mCurrentBalance = 0;
- private final List<Transaction> mTransactions = new ArrayList<>();
- private final SparseLongArray mCumulativeDeltaPerReason = new SparseLongArray();
- private long mEarliestSumTime;
+ private final Transaction[] mTransactions = new Transaction[MAX_TRANSACTION_COUNT];
+ /** Index within {@link #mTransactions} where the next transaction should be placed. */
+ private int mTransactionIndex = 0;
+ private final RewardBucket[] mRewardBuckets = new RewardBucket[NUM_REWARD_BUCKET_WINDOWS];
+ /** Index within {@link #mRewardBuckets} of the current active bucket. */
+ private int mRewardBucketIndex = 0;
Ledger() {
}
- Ledger(long currentBalance, @NonNull List<Transaction> transactions) {
+ Ledger(long currentBalance, @NonNull List<Transaction> transactions,
+ @NonNull List<RewardBucket> rewardBuckets) {
mCurrentBalance = currentBalance;
- mTransactions.addAll(transactions);
+
+ final int numTxs = transactions.size();
+ for (int i = Math.max(0, numTxs - MAX_TRANSACTION_COUNT); i < numTxs; ++i) {
+ mTransactions[mTransactionIndex++] = transactions.get(i);
+ }
+ mTransactionIndex %= MAX_TRANSACTION_COUNT;
+
+ final int numBuckets = rewardBuckets.size();
+ if (numBuckets > 0) {
+ // Set the index to -1 so that we put the first bucket in index 0.
+ mRewardBucketIndex = -1;
+ for (int i = Math.max(0, numBuckets - NUM_REWARD_BUCKET_WINDOWS); i < numBuckets; ++i) {
+ mRewardBuckets[++mRewardBucketIndex] = rewardBuckets.get(i);
+ }
+ }
}
long getCurrentBalance() {
@@ -74,66 +122,142 @@ class Ledger {
@Nullable
Transaction getEarliestTransaction() {
- if (mTransactions.size() > 0) {
- return mTransactions.get(0);
+ for (int t = 0; t < mTransactions.length; ++t) {
+ final Transaction transaction =
+ mTransactions[(mTransactionIndex + t) % mTransactions.length];
+ if (transaction != null) {
+ return transaction;
+ }
}
return null;
}
@NonNull
+ List<RewardBucket> getRewardBuckets() {
+ final long cutoffMs = getCurrentTimeMillis() - TOTAL_REWARD_WINDOW_MS;
+ final List<RewardBucket> list = new ArrayList<>(NUM_REWARD_BUCKET_WINDOWS);
+ for (int i = 1; i <= NUM_REWARD_BUCKET_WINDOWS; ++i) {
+ final int idx = (mRewardBucketIndex + i) % NUM_REWARD_BUCKET_WINDOWS;
+ final RewardBucket rewardBucket = mRewardBuckets[idx];
+ if (rewardBucket != null) {
+ if (cutoffMs <= rewardBucket.startTimeMs) {
+ list.add(rewardBucket);
+ } else {
+ rewardBucket.reset();
+ }
+ }
+ }
+ return list;
+ }
+
+ @NonNull
List<Transaction> getTransactions() {
- return mTransactions;
+ final List<Transaction> list = new ArrayList<>(MAX_TRANSACTION_COUNT);
+ for (int i = 0; i < MAX_TRANSACTION_COUNT; ++i) {
+ final int idx = (mTransactionIndex + i) % MAX_TRANSACTION_COUNT;
+ final Transaction transaction = mTransactions[idx];
+ if (transaction != null) {
+ list.add(transaction);
+ }
+ }
+ return list;
}
void recordTransaction(@NonNull Transaction transaction) {
- mTransactions.add(transaction);
+ mTransactions[mTransactionIndex] = transaction;
mCurrentBalance += transaction.delta;
+ mTransactionIndex = (mTransactionIndex + 1) % MAX_TRANSACTION_COUNT;
- final long sum = mCumulativeDeltaPerReason.get(transaction.eventId);
- mCumulativeDeltaPerReason.put(transaction.eventId, sum + transaction.delta);
- mEarliestSumTime = Math.min(mEarliestSumTime, transaction.startTimeMs);
+ if (EconomicPolicy.isReward(transaction.eventId)) {
+ final RewardBucket bucket = getCurrentRewardBucket();
+ bucket.cumulativeDelta.put(transaction.eventId,
+ bucket.cumulativeDelta.get(transaction.eventId, 0) + transaction.delta);
+ }
+ }
+
+ @NonNull
+ private RewardBucket getCurrentRewardBucket() {
+ RewardBucket bucket = mRewardBuckets[mRewardBucketIndex];
+ final long now = getCurrentTimeMillis();
+ if (bucket == null) {
+ bucket = new RewardBucket();
+ bucket.startTimeMs = now;
+ mRewardBuckets[mRewardBucketIndex] = bucket;
+ return bucket;
+ }
+
+ if (now - bucket.startTimeMs < REWARD_BUCKET_WINDOW_SIZE_MS) {
+ return bucket;
+ }
+
+ mRewardBucketIndex = (mRewardBucketIndex + 1) % NUM_REWARD_BUCKET_WINDOWS;
+ bucket = mRewardBuckets[mRewardBucketIndex];
+ if (bucket == null) {
+ bucket = new RewardBucket();
+ mRewardBuckets[mRewardBucketIndex] = bucket;
+ }
+ bucket.reset();
+ // Using now as the start time means there will be some gaps between sequential buckets,
+ // but makes processing of large gaps between events easier.
+ bucket.startTimeMs = now;
+ return bucket;
}
long get24HourSum(int eventId, final long now) {
final long windowStartTime = now - 24 * HOUR_IN_MILLIS;
- if (mEarliestSumTime < windowStartTime) {
- // Need to redo sums
- mCumulativeDeltaPerReason.clear();
- for (int i = mTransactions.size() - 1; i >= 0; --i) {
- final Transaction transaction = mTransactions.get(i);
- if (transaction.endTimeMs <= windowStartTime) {
- break;
- }
- long sum = mCumulativeDeltaPerReason.get(transaction.eventId);
- if (transaction.startTimeMs >= windowStartTime) {
- sum += transaction.delta;
- } else {
- // Pro-rate durationed deltas. Intentionally floor the result.
- sum += (long) (1.0 * (transaction.endTimeMs - windowStartTime)
- * transaction.delta)
- / (transaction.endTimeMs - transaction.startTimeMs);
- }
- mCumulativeDeltaPerReason.put(transaction.eventId, sum);
+ long sum = 0;
+ for (int i = 0; i < mRewardBuckets.length; ++i) {
+ final RewardBucket bucket = mRewardBuckets[i];
+ if (bucket != null
+ && bucket.startTimeMs >= windowStartTime && bucket.startTimeMs < now) {
+ sum += bucket.cumulativeDelta.get(eventId, 0);
}
- mEarliestSumTime = windowStartTime;
}
- return mCumulativeDeltaPerReason.get(eventId);
+ return sum;
}
- /** Deletes transactions that are older than {@code minAgeMs}. */
- void removeOldTransactions(long minAgeMs) {
+ /**
+ * Deletes transactions that are older than {@code minAgeMs}.
+ * @return The earliest transaction in the ledger, or {@code null} if there are no more
+ * transactions.
+ */
+ @Nullable
+ Transaction removeOldTransactions(long minAgeMs) {
final long cutoff = getCurrentTimeMillis() - minAgeMs;
- while (mTransactions.size() > 0 && mTransactions.get(0).endTimeMs <= cutoff) {
- mTransactions.remove(0);
+ for (int t = 0; t < mTransactions.length; ++t) {
+ final int idx = (mTransactionIndex + t) % mTransactions.length;
+ final Transaction transaction = mTransactions[idx];
+ if (transaction == null) {
+ continue;
+ }
+ if (transaction.endTimeMs <= cutoff) {
+ mTransactions[idx] = null;
+ } else {
+ // Everything we look at after this transaction will also be within the window,
+ // so no need to go further.
+ return transaction;
+ }
}
+ return null;
}
void dump(IndentingPrintWriter pw, int numRecentTransactions) {
pw.print("Current balance", cakeToString(getCurrentBalance())).println();
+ pw.println();
- final int size = mTransactions.size();
- for (int i = Math.max(0, size - numRecentTransactions); i < size; ++i) {
- final Transaction transaction = mTransactions.get(i);
+ boolean printedTransactionTitle = false;
+ for (int t = 0; t < Math.min(MAX_TRANSACTION_COUNT, numRecentTransactions); ++t) {
+ final int idx = (mTransactionIndex + t) % MAX_TRANSACTION_COUNT;
+ final Transaction transaction = mTransactions[idx];
+ if (transaction == null) {
+ continue;
+ }
+
+ if (!printedTransactionTitle) {
+ pw.println("Transactions:");
+ pw.increaseIndent();
+ printedTransactionTitle = true;
+ }
dumpTime(pw, transaction.startTimeMs);
pw.print("--");
@@ -151,5 +275,42 @@ class Ledger {
pw.print(cakeToString(transaction.ctp));
pw.println(")");
}
+ if (printedTransactionTitle) {
+ pw.decreaseIndent();
+ pw.println();
+ }
+
+ final long now = getCurrentTimeMillis();
+ boolean printedBucketTitle = false;
+ for (int b = 0; b < NUM_REWARD_BUCKET_WINDOWS; ++b) {
+ final int idx = (mRewardBucketIndex - b + NUM_REWARD_BUCKET_WINDOWS)
+ % NUM_REWARD_BUCKET_WINDOWS;
+ final RewardBucket rewardBucket = mRewardBuckets[idx];
+ if (rewardBucket == null || rewardBucket.startTimeMs == 0) {
+ continue;
+ }
+
+ if (!printedBucketTitle) {
+ pw.println("Reward buckets:");
+ pw.increaseIndent();
+ printedBucketTitle = true;
+ }
+
+ dumpTime(pw, rewardBucket.startTimeMs);
+ pw.print(" (");
+ TimeUtils.formatDuration(now - rewardBucket.startTimeMs, pw);
+ pw.println(" ago):");
+ pw.increaseIndent();
+ for (int r = 0; r < rewardBucket.cumulativeDelta.size(); ++r) {
+ pw.print(EconomicPolicy.eventToString(rewardBucket.cumulativeDelta.keyAt(r)));
+ pw.print(": ");
+ pw.println(cakeToString(rewardBucket.cumulativeDelta.valueAt(r)));
+ }
+ pw.decreaseIndent();
+ }
+ if (printedBucketTitle) {
+ pw.decreaseIndent();
+ pw.println();
+ }
}
}
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/ProcessStateModifier.java b/apex/jobscheduler/service/java/com/android/server/tare/ProcessStateModifier.java
index 3578c8acbd0e..585366755191 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/ProcessStateModifier.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/ProcessStateModifier.java
@@ -20,6 +20,7 @@ import android.annotation.IntDef;
import android.annotation.NonNull;
import android.app.ActivityManager;
import android.app.IUidObserver;
+import android.app.UidObserver;
import android.os.RemoteException;
import android.util.IndentingPrintWriter;
import android.util.Slog;
@@ -61,7 +62,7 @@ class ProcessStateModifier extends Modifier {
@GuardedBy("mLock")
private final SparseIntArray mUidProcStateBucketCache = new SparseIntArray();
- private final IUidObserver mUidObserver = new IUidObserver.Stub() {
+ private final IUidObserver mUidObserver = new UidObserver() {
@Override
public void onUidStateChanged(int uid, int procState, long procStateSeq, int capability) {
final int newBucket = getProcStateBucket(procState);
@@ -85,22 +86,6 @@ class ProcessStateModifier extends Modifier {
notifyStateChangedLocked(uid);
}
}
-
- @Override
- public void onUidActive(int uid) {
- }
-
- @Override
- public void onUidIdle(int uid, boolean disabled) {
- }
-
- @Override
- public void onUidCachedChanged(int uid, boolean cached) {
- }
-
- @Override
- public void onUidProcAdjChanged(int uid) {
- }
};
ProcessStateModifier(@NonNull InternalResourceService irs) {
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/README.md b/apex/jobscheduler/service/java/com/android/server/tare/README.md
index e338ed1c6987..8d25ecce8431 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/README.md
+++ b/apex/jobscheduler/service/java/com/android/server/tare/README.md
@@ -80,9 +80,9 @@ consumption limit, then the available resources are decreased to match the scale
Regulations are unique events invoked by the ~~government~~ system in order to get the whole economy
moving smoothly.
-# Previous Implementations
+# Significant Changes
-## V0
+## Tare Improvement Proposal #1 (TIP1)
The initial implementation/proposal combined the supply of resources with the allocation in a single
mechanism. It defined the maximum number of resources (ARCs) available at a time, and then divided
@@ -98,10 +98,25 @@ allocated as part of the rewards. There were several problems with that mechanis
These problems effectively meant that misallocation was a big problem, demand wasn't well reflected,
and some apps may not have been able to perform work even though they otherwise should have been.
-Tare Improvement Proposal #1 (TIP1) separated allocation (to apps) from supply (by the system) and
+TIP1 separated allocation (to apps) from supply (by the system) and
allowed apps to accrue credits as appropriate while still limiting the total number of credits
consumed.
+## Tare Improvement Proposal #3 (TIP3)
+
+TIP1 introduced Consumption Limits, which control the total number of ARCs that can be used to
+perform actions, based on the production costs of each action. The Consumption Limits were initially
+determined manually, but could increase in the system if apps used the full consumption limit before
+the device had drained to 50% battery. As with any system that relies on manually deciding
+parameters, the only mechanism to identify an optimal value is through experimentation, which can
+take many iterations and requires extended periods of time to observe results. The limits are also
+chosen and adjusted without consideration of the resulting battery drain of each possible value. In
+addition, having the system potentially increase the limit without considering a decrease introduced
+potential for battery life to get worse as time goes on and the user installed more background-work
+demanding apps.
+
+TIP3 uses a target background battery drain rate to dynamically adjust the Consumption Limit.
+
# Potential Future Changes
These are some ideas for further changes. There's no guarantee that they'll be implemented.
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java b/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java
index 941cc39f2d97..08439f383f04 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java
@@ -16,14 +16,16 @@
package com.android.server.tare;
+import static android.app.tare.EconomyManager.ENABLED_MODE_OFF;
import static android.text.format.DateUtils.HOUR_IN_MILLIS;
import static com.android.server.tare.TareUtils.appToString;
+import static com.android.server.tare.TareUtils.cakeToString;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.content.pm.PackageInfo;
import android.os.Environment;
+import android.os.SystemClock;
import android.os.UserHandle;
import android.util.ArraySet;
import android.util.AtomicFile;
@@ -33,12 +35,13 @@ import android.util.Pair;
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseArrayMap;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
+import android.util.SparseLongArray;
import android.util.Xml;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -62,15 +65,14 @@ public class Scribe {
private static final int MAX_NUM_TRANSACTION_DUMP = 25;
/**
* The maximum amount of time we'll keep a transaction around for.
- * For now, only keep transactions we actually have a use for. We can increase it if we want
- * to use older transactions or provide older transactions to apps.
*/
- private static final long MAX_TRANSACTION_AGE_MS = 24 * HOUR_IN_MILLIS;
+ private static final long MAX_TRANSACTION_AGE_MS = 8 * 24 * HOUR_IN_MILLIS;
private static final String XML_TAG_HIGH_LEVEL_STATE = "irs-state";
private static final String XML_TAG_LEDGER = "ledger";
private static final String XML_TAG_TARE = "tare";
private static final String XML_TAG_TRANSACTION = "transaction";
+ private static final String XML_TAG_REWARD_BUCKET = "rewardBucket";
private static final String XML_TAG_USER = "user";
private static final String XML_TAG_PERIOD_REPORT = "report";
@@ -85,8 +87,11 @@ public class Scribe {
private static final String XML_ATTR_USER_ID = "userId";
private static final String XML_ATTR_VERSION = "version";
private static final String XML_ATTR_LAST_RECLAMATION_TIME = "lastReclamationTime";
+ private static final String XML_ATTR_LAST_STOCK_RECALCULATION_TIME =
+ "lastStockRecalculationTime";
private static final String XML_ATTR_REMAINING_CONSUMABLE_CAKES = "remainingConsumableCakes";
private static final String XML_ATTR_CONSUMPTION_LIMIT = "consumptionLimit";
+ private static final String XML_ATTR_TIME_SINCE_FIRST_SETUP_MS = "timeSinceFirstSetup";
private static final String XML_ATTR_PR_DISCHARGE = "discharge";
private static final String XML_ATTR_PR_BATTERY_LEVEL = "batteryLevel";
private static final String XML_ATTR_PR_PROFIT = "profit";
@@ -99,6 +104,8 @@ public class Scribe {
private static final String XML_ATTR_PR_NUM_POS_REGULATIONS = "numPosRegulations";
private static final String XML_ATTR_PR_NEG_REGULATIONS = "negRegulations";
private static final String XML_ATTR_PR_NUM_NEG_REGULATIONS = "numNegRegulations";
+ private static final String XML_ATTR_PR_SCREEN_OFF_DURATION_MS = "screenOffDurationMs";
+ private static final String XML_ATTR_PR_SCREEN_OFF_DISCHARGE_MAH = "screenOffDischargeMah";
/** Version of the file schema. */
private static final int STATE_FILE_VERSION = 0;
@@ -109,14 +116,24 @@ public class Scribe {
private final InternalResourceService mIrs;
private final Analyst mAnalyst;
+ /**
+ * The value of elapsed realtime since TARE was first setup that was read from disk.
+ * This will only be changed when the persisted file is read.
+ */
+ private long mLoadedTimeSinceFirstSetup;
@GuardedBy("mIrs.getLock()")
private long mLastReclamationTime;
@GuardedBy("mIrs.getLock()")
+ private long mLastStockRecalculationTime;
+ @GuardedBy("mIrs.getLock()")
private long mSatiatedConsumptionLimit;
@GuardedBy("mIrs.getLock()")
private long mRemainingConsumableCakes;
@GuardedBy("mIrs.getLock()")
private final SparseArrayMap<String, Ledger> mLedgers = new SparseArrayMap<>();
+ /** Offsets used to calculate the total realtime since each user was added. */
+ @GuardedBy("mIrs.getLock()")
+ private final SparseLongArray mRealtimeSinceUsersAddedOffsets = new SparseLongArray();
private final Runnable mCleanRunnable = this::cleanupLedgers;
private final Runnable mWriteRunnable = this::writeState;
@@ -138,9 +155,15 @@ public class Scribe {
@GuardedBy("mIrs.getLock()")
void adjustRemainingConsumableCakesLocked(long delta) {
- if (delta != 0) {
- // No point doing any work if the change is 0.
- mRemainingConsumableCakes += delta;
+ final long staleCakes = mRemainingConsumableCakes;
+ mRemainingConsumableCakes += delta;
+ if (mRemainingConsumableCakes < 0) {
+ Slog.w(TAG, "Overdrew consumable cakes by " + cakeToString(-mRemainingConsumableCakes));
+ // A negative value would interfere with allowing free actions, so set the minimum as 0.
+ mRemainingConsumableCakes = 0;
+ }
+ if (mRemainingConsumableCakes != staleCakes) {
+ // No point doing any work if there was no functional change.
postWrite();
}
}
@@ -152,6 +175,13 @@ public class Scribe {
}
@GuardedBy("mIrs.getLock()")
+ void onUserRemovedLocked(final int userId) {
+ mLedgers.delete(userId);
+ mRealtimeSinceUsersAddedOffsets.delete(userId);
+ postWrite();
+ }
+
+ @GuardedBy("mIrs.getLock()")
long getSatiatedConsumptionLimitLocked() {
return mSatiatedConsumptionLimit;
}
@@ -162,6 +192,11 @@ public class Scribe {
}
@GuardedBy("mIrs.getLock()")
+ long getLastStockRecalculationTimeLocked() {
+ return mLastStockRecalculationTime;
+ }
+
+ @GuardedBy("mIrs.getLock()")
@NonNull
Ledger getLedgerLocked(final int userId, @NonNull final String pkgName) {
Ledger ledger = mLedgers.get(userId, pkgName);
@@ -193,6 +228,11 @@ public class Scribe {
return sum;
}
+ /** Returns the cumulative elapsed realtime since TARE was first setup. */
+ long getRealtimeSinceFirstSetupMs(long nowElapsed) {
+ return mLoadedTimeSinceFirstSetup + nowElapsed;
+ }
+
/** Returns the total amount of cakes that remain to be consumed. */
@GuardedBy("mIrs.getLock()")
long getRemainingConsumableCakesLocked() {
@@ -200,6 +240,16 @@ public class Scribe {
}
@GuardedBy("mIrs.getLock()")
+ SparseLongArray getRealtimeSinceUsersAddedLocked(long nowElapsed) {
+ final SparseLongArray realtimes = new SparseLongArray();
+ for (int i = mRealtimeSinceUsersAddedOffsets.size() - 1; i >= 0; --i) {
+ realtimes.put(mRealtimeSinceUsersAddedOffsets.keyAt(i),
+ mRealtimeSinceUsersAddedOffsets.valueAt(i) + nowElapsed);
+ }
+ return realtimes;
+ }
+
+ @GuardedBy("mIrs.getLock()")
void loadFromDiskLocked() {
mLedgers.clear();
if (!recordExists()) {
@@ -211,17 +261,21 @@ public class Scribe {
mRemainingConsumableCakes = 0;
final SparseArray<ArraySet<String>> installedPackagesPerUser = new SparseArray<>();
- final List<PackageInfo> installedPackages = mIrs.getInstalledPackages();
- for (int i = 0; i < installedPackages.size(); ++i) {
- final PackageInfo packageInfo = installedPackages.get(i);
- if (packageInfo.applicationInfo != null) {
- final int userId = UserHandle.getUserId(packageInfo.applicationInfo.uid);
- ArraySet<String> pkgsForUser = installedPackagesPerUser.get(userId);
- if (pkgsForUser == null) {
- pkgsForUser = new ArraySet<>();
- installedPackagesPerUser.put(userId, pkgsForUser);
+ final SparseArrayMap<String, InstalledPackageInfo> installedPackages =
+ mIrs.getInstalledPackages();
+ for (int uIdx = installedPackages.numMaps() - 1; uIdx >= 0; --uIdx) {
+ final int userId = installedPackages.keyAt(uIdx);
+
+ for (int pIdx = installedPackages.numElementsForKeyAt(uIdx) - 1; pIdx >= 0; --pIdx) {
+ final InstalledPackageInfo packageInfo = installedPackages.valueAt(uIdx, pIdx);
+ if (packageInfo.uid != InstalledPackageInfo.NO_UID) {
+ ArraySet<String> pkgsForUser = installedPackagesPerUser.get(userId);
+ if (pkgsForUser == null) {
+ pkgsForUser = new ArraySet<>();
+ installedPackagesPerUser.put(userId, pkgsForUser);
+ }
+ pkgsForUser.add(packageInfo.packageName);
}
- pkgsForUser.add(packageInfo.packageName);
}
}
@@ -250,7 +304,8 @@ public class Scribe {
}
}
- final long endTimeCutoff = System.currentTimeMillis() - MAX_TRANSACTION_AGE_MS;
+ final long now = System.currentTimeMillis();
+ final long endTimeCutoff = now - MAX_TRANSACTION_AGE_MS;
long earliestEndTime = Long.MAX_VALUE;
for (eventType = parser.next(); eventType != XmlPullParser.END_DOCUMENT;
eventType = parser.next()) {
@@ -266,6 +321,14 @@ public class Scribe {
case XML_TAG_HIGH_LEVEL_STATE:
mLastReclamationTime =
parser.getAttributeLong(null, XML_ATTR_LAST_RECLAMATION_TIME);
+ mLastStockRecalculationTime = parser.getAttributeLong(null,
+ XML_ATTR_LAST_STOCK_RECALCULATION_TIME, 0);
+ mLoadedTimeSinceFirstSetup =
+ parser.getAttributeLong(null, XML_ATTR_TIME_SINCE_FIRST_SETUP_MS,
+ // If there's no recorded time since first setup, then
+ // offset the current elapsed time so it doesn't shift the
+ // timing too much.
+ -SystemClock.elapsedRealtime());
mSatiatedConsumptionLimit =
parser.getAttributeLong(null, XML_ATTR_CONSUMPTION_LIMIT,
mIrs.getInitialSatiatedConsumptionLimitLocked());
@@ -322,6 +385,19 @@ public class Scribe {
}
@GuardedBy("mIrs.getLock()")
+ void setLastStockRecalculationTimeLocked(long time) {
+ mLastStockRecalculationTime = time;
+ postWrite();
+ }
+
+ @GuardedBy("mIrs.getLock()")
+ void setUserAddedTimeLocked(int userId, long timeElapsed) {
+ // Use the current time as an offset so that when we persist the time, it correctly persists
+ // as "time since now".
+ mRealtimeSinceUsersAddedOffsets.put(userId, -timeElapsed);
+ }
+
+ @GuardedBy("mIrs.getLock()")
void tearDownLocked() {
TareHandlerThread.getHandler().removeCallbacks(mCleanRunnable);
TareHandlerThread.getHandler().removeCallbacks(mWriteRunnable);
@@ -346,8 +422,8 @@ public class Scribe {
for (int pIdx = mLedgers.numElementsForKey(userId) - 1; pIdx >= 0; --pIdx) {
final String pkgName = mLedgers.keyAt(uIdx, pIdx);
final Ledger ledger = mLedgers.get(userId, pkgName);
- ledger.removeOldTransactions(MAX_TRANSACTION_AGE_MS);
- Ledger.Transaction transaction = ledger.getEarliestTransaction();
+ final Ledger.Transaction transaction =
+ ledger.removeOldTransactions(MAX_TRANSACTION_AGE_MS);
if (transaction != null) {
earliestEndTime = Math.min(earliestEndTime, transaction.endTimeMs);
}
@@ -370,6 +446,7 @@ public class Scribe {
final String pkgName;
final long curBalance;
final List<Ledger.Transaction> transactions = new ArrayList<>();
+ final List<Ledger.RewardBucket> rewardBuckets = new ArrayList<>();
pkgName = parser.getAttributeValue(null, XML_ATTR_PACKAGE_NAME);
curBalance = parser.getAttributeLong(null, XML_ATTR_CURRENT_BALANCE);
@@ -391,8 +468,7 @@ public class Scribe {
}
continue;
}
- if (eventType != XmlPullParser.START_TAG || !XML_TAG_TRANSACTION.equals(tagName)) {
- // Expecting only "transaction" tags.
+ if (eventType != XmlPullParser.START_TAG || tagName == null) {
Slog.e(TAG, "Unexpected event: (" + eventType + ") " + tagName);
return null;
}
@@ -402,25 +478,37 @@ public class Scribe {
if (DEBUG) {
Slog.d(TAG, "Starting ledger tag: " + tagName);
}
- final String tag = parser.getAttributeValue(null, XML_ATTR_TAG);
- final long startTime = parser.getAttributeLong(null, XML_ATTR_START_TIME);
- final long endTime = parser.getAttributeLong(null, XML_ATTR_END_TIME);
- final int eventId = parser.getAttributeInt(null, XML_ATTR_EVENT_ID);
- final long delta = parser.getAttributeLong(null, XML_ATTR_DELTA);
- final long ctp = parser.getAttributeLong(null, XML_ATTR_CTP);
- if (endTime <= endTimeCutoff) {
- if (DEBUG) {
- Slog.d(TAG, "Skipping event because it's too old.");
- }
- continue;
+ switch (tagName) {
+ case XML_TAG_TRANSACTION:
+ final long endTime = parser.getAttributeLong(null, XML_ATTR_END_TIME);
+ if (endTime <= endTimeCutoff) {
+ if (DEBUG) {
+ Slog.d(TAG, "Skipping event because it's too old.");
+ }
+ continue;
+ }
+ final String tag = parser.getAttributeValue(null, XML_ATTR_TAG);
+ final long startTime = parser.getAttributeLong(null, XML_ATTR_START_TIME);
+ final int eventId = parser.getAttributeInt(null, XML_ATTR_EVENT_ID);
+ final long delta = parser.getAttributeLong(null, XML_ATTR_DELTA);
+ final long ctp = parser.getAttributeLong(null, XML_ATTR_CTP);
+ transactions.add(
+ new Ledger.Transaction(startTime, endTime, eventId, tag, delta, ctp));
+ break;
+ case XML_TAG_REWARD_BUCKET:
+ rewardBuckets.add(readRewardBucketFromXml(parser));
+ break;
+ default:
+ // Expecting only "transaction" and "rewardBucket" tags.
+ Slog.e(TAG, "Unexpected event: (" + eventType + ") " + tagName);
+ return null;
}
- transactions.add(new Ledger.Transaction(startTime, endTime, eventId, tag, delta, ctp));
}
if (!isInstalled) {
return null;
}
- return Pair.create(pkgName, new Ledger(curBalance, transactions));
+ return Pair.create(pkgName, new Ledger(curBalance, transactions, rewardBuckets));
}
/**
@@ -440,6 +528,14 @@ public class Scribe {
// Don't return early since we need to go through all the ledger tags and get to the end
// of the user tag.
}
+ if (curUser != UserHandle.USER_NULL) {
+ mRealtimeSinceUsersAddedOffsets.put(curUser,
+ parser.getAttributeLong(null, XML_ATTR_TIME_SINCE_FIRST_SETUP_MS,
+ // If there's no recorded time since first setup, then
+ // offset the current elapsed time so it doesn't shift the
+ // timing too much.
+ -SystemClock.elapsedRealtime()));
+ }
long earliestEndTime = Long.MAX_VALUE;
for (int eventType = parser.next(); eventType != XmlPullParser.END_DOCUMENT;
@@ -477,7 +573,6 @@ public class Scribe {
return earliestEndTime;
}
-
/**
* @param parser Xml parser at the beginning of a {@link #XML_TAG_PERIOD_REPORT} tag. The next
* "parser.next()" call will take the parser into the body of the report tag.
@@ -504,10 +599,52 @@ public class Scribe {
parser.getAttributeLong(null, XML_ATTR_PR_NEG_REGULATIONS);
report.numNegativeRegulations =
parser.getAttributeInt(null, XML_ATTR_PR_NUM_NEG_REGULATIONS);
+ report.screenOffDurationMs =
+ parser.getAttributeLong(null, XML_ATTR_PR_SCREEN_OFF_DURATION_MS, 0);
+ report.screenOffDischargeMah =
+ parser.getAttributeLong(null, XML_ATTR_PR_SCREEN_OFF_DISCHARGE_MAH, 0);
return report;
}
+ /**
+ * @param parser Xml parser at the beginning of a {@value #XML_TAG_REWARD_BUCKET} tag. The next
+ * "parser.next()" call will take the parser into the body of the tag.
+ * @return Newly instantiated {@link Ledger.RewardBucket} holding all the information we just
+ * read out of the xml tag.
+ */
+ @Nullable
+ private static Ledger.RewardBucket readRewardBucketFromXml(TypedXmlPullParser parser)
+ throws XmlPullParserException, IOException {
+
+ final Ledger.RewardBucket rewardBucket = new Ledger.RewardBucket();
+
+ rewardBucket.startTimeMs = parser.getAttributeLong(null, XML_ATTR_START_TIME);
+
+ for (int eventType = parser.next(); eventType != XmlPullParser.END_DOCUMENT;
+ eventType = parser.next()) {
+ final String tagName = parser.getName();
+ if (eventType == XmlPullParser.END_TAG) {
+ if (XML_TAG_REWARD_BUCKET.equals(tagName)) {
+ // We've reached the end of the rewardBucket tag.
+ break;
+ }
+ continue;
+ }
+ if (eventType != XmlPullParser.START_TAG || !XML_ATTR_DELTA.equals(tagName)) {
+ // Expecting only delta tags.
+ Slog.e(TAG, "Unexpected event: (" + eventType + ") " + tagName);
+ return null;
+ }
+
+ final int eventId = parser.getAttributeInt(null, XML_ATTR_EVENT_ID);
+ final long delta = parser.getAttributeLong(null, XML_ATTR_DELTA);
+ rewardBucket.cumulativeDelta.put(eventId, delta);
+ }
+
+ return rewardBucket;
+ }
+
private void scheduleCleanup(long earliestEndTime) {
if (earliestEndTime == Long.MAX_VALUE) {
return;
@@ -526,7 +663,7 @@ public class Scribe {
// Remove mCleanRunnable callbacks since we're going to clean up the ledgers before
// writing anyway.
TareHandlerThread.getHandler().removeCallbacks(mCleanRunnable);
- if (!mIrs.isEnabled()) {
+ if (mIrs.getEnabledMode() == ENABLED_MODE_OFF) {
// If it's no longer enabled, we would have cleared all the data in memory and would
// accidentally write an empty file, thus deleting all the history.
return;
@@ -541,6 +678,10 @@ public class Scribe {
out.startTag(null, XML_TAG_HIGH_LEVEL_STATE);
out.attributeLong(null, XML_ATTR_LAST_RECLAMATION_TIME, mLastReclamationTime);
+ out.attributeLong(null,
+ XML_ATTR_LAST_STOCK_RECALCULATION_TIME, mLastStockRecalculationTime);
+ out.attributeLong(null, XML_ATTR_TIME_SINCE_FIRST_SETUP_MS,
+ mLoadedTimeSinceFirstSetup + SystemClock.elapsedRealtime());
out.attributeLong(null, XML_ATTR_CONSUMPTION_LIMIT, mSatiatedConsumptionLimit);
out.attributeLong(null, XML_ATTR_REMAINING_CONSUMABLE_CAKES,
mRemainingConsumableCakes);
@@ -576,6 +717,9 @@ public class Scribe {
out.startTag(null, XML_TAG_USER);
out.attributeInt(null, XML_ATTR_USER_ID, userId);
+ out.attributeLong(null, XML_ATTR_TIME_SINCE_FIRST_SETUP_MS,
+ mRealtimeSinceUsersAddedOffsets.get(userId, mLoadedTimeSinceFirstSetup)
+ + SystemClock.elapsedRealtime());
for (int pIdx = mLedgers.numElementsForKey(userId) - 1; pIdx >= 0; --pIdx) {
final String pkgName = mLedgers.keyAt(uIdx, pIdx);
final Ledger ledger = mLedgers.get(userId, pkgName);
@@ -595,6 +739,11 @@ public class Scribe {
}
writeTransaction(out, transaction);
}
+
+ final List<Ledger.RewardBucket> rewardBuckets = ledger.getRewardBuckets();
+ for (int r = 0; r < rewardBuckets.size(); ++r) {
+ writeRewardBucket(out, rewardBuckets.get(r));
+ }
out.endTag(null, XML_TAG_LEDGER);
}
out.endTag(null, XML_TAG_USER);
@@ -616,6 +765,23 @@ public class Scribe {
out.endTag(null, XML_TAG_TRANSACTION);
}
+ private static void writeRewardBucket(@NonNull TypedXmlSerializer out,
+ @NonNull Ledger.RewardBucket rewardBucket) throws IOException {
+ final int numEvents = rewardBucket.cumulativeDelta.size();
+ if (numEvents == 0) {
+ return;
+ }
+ out.startTag(null, XML_TAG_REWARD_BUCKET);
+ out.attributeLong(null, XML_ATTR_START_TIME, rewardBucket.startTimeMs);
+ for (int i = 0; i < numEvents; ++i) {
+ out.startTag(null, XML_ATTR_DELTA);
+ out.attributeInt(null, XML_ATTR_EVENT_ID, rewardBucket.cumulativeDelta.keyAt(i));
+ out.attributeLong(null, XML_ATTR_DELTA, rewardBucket.cumulativeDelta.valueAt(i));
+ out.endTag(null, XML_ATTR_DELTA);
+ }
+ out.endTag(null, XML_TAG_REWARD_BUCKET);
+ }
+
private static void writeReport(@NonNull TypedXmlSerializer out,
@NonNull Analyst.Report report) throws IOException {
out.startTag(null, XML_TAG_PERIOD_REPORT);
@@ -631,6 +797,8 @@ public class Scribe {
out.attributeInt(null, XML_ATTR_PR_NUM_POS_REGULATIONS, report.numPositiveRegulations);
out.attributeLong(null, XML_ATTR_PR_NEG_REGULATIONS, report.cumulativeNegativeRegulations);
out.attributeInt(null, XML_ATTR_PR_NUM_NEG_REGULATIONS, report.numNegativeRegulations);
+ out.attributeLong(null, XML_ATTR_PR_SCREEN_OFF_DURATION_MS, report.screenOffDurationMs);
+ out.attributeLong(null, XML_ATTR_PR_SCREEN_OFF_DISCHARGE_MAH, report.screenOffDischargeMah);
out.endTag(null, XML_TAG_PERIOD_REPORT);
}
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/TareShellCommand.java b/apex/jobscheduler/service/java/com/android/server/tare/TareShellCommand.java
new file mode 100644
index 000000000000..5e380b408d01
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/tare/TareShellCommand.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.tare;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.content.pm.PackageManager;
+import android.os.Binder;
+
+import com.android.modules.utils.BasicShellCommandHandler;
+
+import java.io.PrintWriter;
+
+/**
+ * Shell command handler for TARE.
+ */
+public class TareShellCommand extends BasicShellCommandHandler {
+ static final int COMMAND_ERROR = -1;
+ static final int COMMAND_SUCCESS = 0;
+
+ private final InternalResourceService mIrs;
+
+ public TareShellCommand(@NonNull InternalResourceService irs) {
+ mIrs = irs;
+ }
+
+ @Override
+ public int onCommand(String cmd) {
+ final PrintWriter pw = getOutPrintWriter();
+ try {
+ switch (cmd != null ? cmd : "") {
+ case "clear-vip":
+ return runClearVip(pw);
+ case "set-vip":
+ return runSetVip(pw);
+ default:
+ return handleDefaultCommands(cmd);
+ }
+ } catch (Exception e) {
+ pw.println("Exception: " + e);
+ }
+ return COMMAND_ERROR;
+ }
+
+ @Override
+ public void onHelp() {
+ final PrintWriter pw = getOutPrintWriter();
+
+ pw.println("TARE commands:");
+ pw.println(" help");
+ pw.println(" Print this help text.");
+ pw.println(" clear-vip");
+ pw.println(" Clears all VIP settings resulting from previous calls using `set-vip` and");
+ pw.println(" resets them all to default.");
+ pw.println(" set-vip <USER_ID> <PACKAGE> <true|false|default>");
+ pw.println(" Designate the app as a Very Important Package or not. A VIP is allowed to");
+ pw.println(" do as much work as it wants, regardless of TARE state.");
+ pw.println(" The user ID must be an explicit user ID. USER_ALL, CURRENT, etc. are not");
+ pw.println(" supported.");
+ pw.println();
+ }
+
+ private void checkPermission(@NonNull String operation) throws Exception {
+ final int perm = mIrs.getContext()
+ .checkCallingOrSelfPermission(Manifest.permission.CHANGE_APP_IDLE_STATE);
+ if (perm != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("Uid " + Binder.getCallingUid()
+ + " not permitted to " + operation);
+ }
+ }
+
+ private int runClearVip(@NonNull PrintWriter pw) throws Exception {
+ checkPermission("clear vip");
+
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ return mIrs.executeClearVip(pw);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ private int runSetVip(@NonNull PrintWriter pw) throws Exception {
+ checkPermission("modify vip");
+
+ final int userId = Integer.parseInt(getNextArgRequired());
+ final String pkgName = getNextArgRequired();
+ final String vipState = getNextArgRequired();
+ final Boolean isVip = "default".equals(vipState) ? null : Boolean.valueOf(vipState);
+
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ return mIrs.executeSetVip(pw, userId, pkgName, isVip);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+}
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/TareUtils.java b/apex/jobscheduler/service/java/com/android/server/tare/TareUtils.java
index 6b6984f6ac17..aa4c75a0be80 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/TareUtils.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/TareUtils.java
@@ -19,26 +19,15 @@ package com.android.server.tare;
import static android.app.tare.EconomyManager.CAKE_IN_ARC;
import android.annotation.NonNull;
-import android.annotation.SuppressLint;
-import android.util.IndentingPrintWriter;
import com.android.internal.annotations.VisibleForTesting;
-import java.text.SimpleDateFormat;
import java.time.Clock;
class TareUtils {
- @SuppressLint("SimpleDateFormat")
- private static final SimpleDateFormat sDumpDateFormat =
- new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
-
@VisibleForTesting
static Clock sSystemClock = Clock.systemUTC();
- static void dumpTime(IndentingPrintWriter pw, long time) {
- pw.print(sDumpDateFormat.format(time));
- }
-
static long getCurrentTimeMillis() {
return sSystemClock.millis();
}
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 c46b24e8dfe8..7d3837786be9 100644
--- a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
+++ b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
@@ -63,6 +63,7 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.ActivityManager;
+import android.app.AppOpsManager;
import android.app.usage.AppStandbyInfo;
import android.app.usage.UsageEvents;
import android.app.usage.UsageStatsManager.ForcedReasons;
@@ -108,6 +109,7 @@ import android.util.IndentingPrintWriter;
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
+import android.util.SparseIntArray;
import android.util.SparseLongArray;
import android.util.SparseSetArray;
import android.util.TimeUtils;
@@ -117,13 +119,15 @@ import android.widget.Toast;
import com.android.internal.R;
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.app.IBatteryStats;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.ConcurrentUtils;
import com.android.server.AlarmManagerInternal;
-import com.android.server.JobSchedulerBackgroundThread;
+import com.android.server.AppSchedulingModuleThread;
import com.android.server.LocalServices;
-import com.android.server.pm.parsing.pkg.AndroidPackage;
+import com.android.server.pm.pkg.AndroidPackage;
import com.android.server.usage.AppIdleHistory.AppUsageHistory;
import libcore.util.EmptyArray;
@@ -284,6 +288,13 @@ public class AppStandbyController
@GuardedBy("mPendingIdleStateChecks")
private final SparseLongArray mPendingIdleStateChecks = new SparseLongArray();
+ /**
+ * Map of uids to their current app-op mode for
+ * {@link AppOpsManager#OPSTR_SYSTEM_EXEMPT_FROM_POWER_RESTRICTIONS}.
+ */
+ @GuardedBy("mSystemExemptionAppOpMode")
+ private final SparseIntArray mSystemExemptionAppOpMode = new SparseIntArray();
+
// Cache the active network scorer queried from the network scorer service
private volatile String mCachedNetworkScorer = null;
// The last time the network scorer service was queried
@@ -478,14 +489,6 @@ public class AppStandbyController
| PackageManager.MATCH_DIRECT_BOOT_AWARE
| PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
- /**
- * Whether we should allow apps into the
- * {@link android.app.usage.UsageStatsManager#STANDBY_BUCKET_RESTRICTED} bucket or not.
- * If false, any attempts to put an app into the bucket will put the app into the
- * {@link android.app.usage.UsageStatsManager#STANDBY_BUCKET_RARE} bucket instead.
- */
- private boolean mAllowRestrictedBucket;
-
private volatile boolean mAppIdleEnabled;
private volatile boolean mIsCharging;
private boolean mSystemServicesReady = false;
@@ -499,6 +502,7 @@ public class AppStandbyController
private AppWidgetManager mAppWidgetManager;
private PackageManager mPackageManager;
+ private AppOpsManager mAppOpsManager;
Injector mInjector;
private static class Pool<T> {
@@ -584,7 +588,7 @@ public class AppStandbyController
}
public AppStandbyController(Context context) {
- this(new Injector(context, JobSchedulerBackgroundThread.get().getLooper()));
+ this(new Injector(context, AppSchedulingModuleThread.get().getLooper()));
}
AppStandbyController(Injector injector) {
@@ -658,6 +662,28 @@ public class AppStandbyController
settingsObserver.start();
mAppWidgetManager = mContext.getSystemService(AppWidgetManager.class);
+ mAppOpsManager = mContext.getSystemService(AppOpsManager.class);
+ IAppOpsService iAppOpsService = mInjector.getAppOpsService();
+ try {
+ iAppOpsService.startWatchingMode(
+ AppOpsManager.OP_SYSTEM_EXEMPT_FROM_POWER_RESTRICTIONS,
+ /*packageName=*/ null,
+ new IAppOpsCallback.Stub() {
+ @Override
+ public void opChanged(int op, int uid, String packageName) {
+ final int userId = UserHandle.getUserId(uid);
+ synchronized (mSystemExemptionAppOpMode) {
+ mSystemExemptionAppOpMode.delete(uid);
+ }
+ mHandler.obtainMessage(
+ MSG_CHECK_PACKAGE_IDLE_STATE, userId, uid, packageName)
+ .sendToTarget();
+ }
+ });
+ } catch (RemoteException e) {
+ // Should not happen.
+ Slog.wtf(TAG, "Failed start watching for app op", e);
+ }
mInjector.registerDisplayListener(mDisplayListener, mHandler);
synchronized (mAppIdleLock) {
@@ -935,17 +961,21 @@ public class AppStandbyController
Slog.d(TAG, " Checking idle state for " + packageName
+ " minBucket=" + standbyBucketToString(minBucket));
}
+ final boolean previouslyIdle, stillIdle;
if (minBucket <= STANDBY_BUCKET_ACTIVE) {
// No extra processing needed for ACTIVE or higher since apps can't drop into lower
// buckets.
synchronized (mAppIdleLock) {
+ previouslyIdle = mAppIdleHistory.isIdle(packageName, userId, elapsedRealtime);
mAppIdleHistory.setAppStandbyBucket(packageName, userId, elapsedRealtime,
minBucket, REASON_MAIN_DEFAULT);
+ stillIdle = mAppIdleHistory.isIdle(packageName, userId, elapsedRealtime);
}
maybeInformListeners(packageName, userId, elapsedRealtime,
minBucket, REASON_MAIN_DEFAULT, false);
} else {
synchronized (mAppIdleLock) {
+ previouslyIdle = mAppIdleHistory.isIdle(packageName, userId, elapsedRealtime);
final AppIdleHistory.AppUsageHistory app =
mAppIdleHistory.getAppUsageHistory(packageName,
userId, elapsedRealtime);
@@ -1020,13 +1050,6 @@ public class AppStandbyController
Slog.d(TAG, "Bringing down to RESTRICTED due to timeout");
}
}
- if (newBucket == STANDBY_BUCKET_RESTRICTED && !mAllowRestrictedBucket) {
- newBucket = STANDBY_BUCKET_RARE;
- // Leave the reason alone.
- if (DEBUG) {
- Slog.d(TAG, "Bringing up from RESTRICTED to RARE due to off switch");
- }
- }
if (newBucket > minBucket) {
newBucket = minBucket;
// Leave the reason alone.
@@ -1043,11 +1066,17 @@ public class AppStandbyController
if (oldBucket != newBucket || predictionLate) {
mAppIdleHistory.setAppStandbyBucket(packageName, userId,
elapsedRealtime, newBucket, reason);
+ stillIdle = mAppIdleHistory.isIdle(packageName, userId, elapsedRealtime);
maybeInformListeners(packageName, userId, elapsedRealtime,
newBucket, reason, false);
+ } else {
+ stillIdle = previouslyIdle;
}
}
}
+ if (previouslyIdle != stillIdle) {
+ notifyBatteryStats(packageName, userId, stillIdle);
+ }
}
/** Returns true if there hasn't been a prediction for the app in a while. */
@@ -1204,8 +1233,9 @@ public class AppStandbyController
appHistory.currentBucket, reason, userStartedInteracting);
}
- if (previouslyIdle) {
- notifyBatteryStats(pkg, userId, false);
+ final boolean stillIdle = appHistory.currentBucket >= AppIdleHistory.IDLE_BUCKET_CUTOFF;
+ if (previouslyIdle != stillIdle) {
+ notifyBatteryStats(pkg, userId, stillIdle);
}
}
@@ -1439,6 +1469,24 @@ public class AppStandbyController
return STANDBY_BUCKET_EXEMPTED;
}
+ final int uid = UserHandle.getUid(userId, appId);
+ synchronized (mSystemExemptionAppOpMode) {
+ if (mSystemExemptionAppOpMode.indexOfKey(uid) >= 0) {
+ if (mSystemExemptionAppOpMode.get(uid)
+ == AppOpsManager.MODE_ALLOWED) {
+ return STANDBY_BUCKET_EXEMPTED;
+ }
+ } else {
+ int mode = mAppOpsManager.checkOpNoThrow(
+ AppOpsManager.OP_SYSTEM_EXEMPT_FROM_POWER_RESTRICTIONS, uid,
+ packageName);
+ mSystemExemptionAppOpMode.put(uid, mode);
+ if (mode == AppOpsManager.MODE_ALLOWED) {
+ return STANDBY_BUCKET_EXEMPTED;
+ }
+ }
+ }
+
if (mAppWidgetManager != null
&& mInjector.isBoundWidgetPackage(mAppWidgetManager, packageName, userId)) {
return STANDBY_BUCKET_ACTIVE;
@@ -1452,7 +1500,8 @@ public class AppStandbyController
return STANDBY_BUCKET_WORKING_SET;
}
- if (mInjector.hasExactAlarmPermission(packageName, UserHandle.getUid(userId, appId))) {
+ if (mInjector.shouldGetExactAlarmBucketElevation(packageName,
+ UserHandle.getUid(userId, appId))) {
return STANDBY_BUCKET_WORKING_SET;
}
}
@@ -1625,7 +1674,7 @@ public class AppStandbyController
final int reason = (REASON_MAIN_MASK & mainReason) | (REASON_SUB_MASK & restrictReason);
final long nowElapsed = mInjector.elapsedRealtime();
- final int bucket = mAllowRestrictedBucket ? STANDBY_BUCKET_RESTRICTED : STANDBY_BUCKET_RARE;
+ final int bucket = STANDBY_BUCKET_RESTRICTED;
setAppStandbyBucket(packageName, userId, bucket, reason, nowElapsed, false);
}
@@ -1729,9 +1778,6 @@ public class AppStandbyController
Slog.e(TAG, "Tried to set bucket of uninstalled app: " + packageName);
return;
}
- if (newBucket == STANDBY_BUCKET_RESTRICTED && !mAllowRestrictedBucket) {
- newBucket = STANDBY_BUCKET_RARE;
- }
AppIdleHistory.AppUsageHistory app = mAppIdleHistory.getAppUsageHistory(packageName,
userId, elapsedRealtime);
boolean predicted = (reason & REASON_MAIN_MASK) == REASON_MAIN_PREDICTED;
@@ -1767,8 +1813,14 @@ public class AppStandbyController
reason = REASON_MAIN_FORCED_BY_SYSTEM
| (app.bucketingReason & REASON_SUB_MASK)
| (reason & REASON_SUB_MASK);
+ final boolean previouslyIdle =
+ app.currentBucket >= AppIdleHistory.IDLE_BUCKET_CUTOFF;
mAppIdleHistory.setAppStandbyBucket(packageName, userId, elapsedRealtime,
newBucket, reason, resetTimeout);
+ final boolean stillIdle = newBucket >= AppIdleHistory.IDLE_BUCKET_CUTOFF;
+ if (previouslyIdle != stillIdle) {
+ notifyBatteryStats(packageName, userId, stillIdle);
+ }
return;
}
@@ -1851,7 +1903,6 @@ public class AppStandbyController
+ " due to min timeout");
}
} else if (newBucket == STANDBY_BUCKET_RARE
- && mAllowRestrictedBucket
&& getBucketForLocked(packageName, userId, elapsedRealtime)
== STANDBY_BUCKET_RESTRICTED) {
// Prediction doesn't think the app will be used anytime soon and
@@ -1869,8 +1920,13 @@ public class AppStandbyController
// Make sure we don't put the app in a lower bucket than it's supposed to be in.
newBucket = Math.min(newBucket, getAppMinBucket(packageName, userId));
+ final boolean previouslyIdle = app.currentBucket >= AppIdleHistory.IDLE_BUCKET_CUTOFF;
mAppIdleHistory.setAppStandbyBucket(packageName, userId, elapsedRealtime, newBucket,
reason, resetTimeout);
+ final boolean stillIdle = newBucket >= AppIdleHistory.IDLE_BUCKET_CUTOFF;
+ if (previouslyIdle != stillIdle) {
+ notifyBatteryStats(packageName, userId, stillIdle);
+ }
}
maybeInformListeners(packageName, userId, elapsedRealtime, newBucket, reason, false);
}
@@ -2200,6 +2256,12 @@ public class AppStandbyController
}
}
}
+ synchronized (mSystemExemptionAppOpMode) {
+ if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) {
+ mSystemExemptionAppOpMode.delete(UserHandle.getUid(userId, getAppId(pkgName)));
+ }
+ }
+
}
}
@@ -2438,18 +2500,16 @@ public class AppStandbyController
pw.print(mNoteResponseEventForAllBroadcastSessions);
pw.println();
- pw.print(" mBroadcastResponseExemptedRoles");
+ pw.print(" mBroadcastResponseExemptedRoles=");
pw.print(mBroadcastResponseExemptedRoles);
pw.println();
- pw.print(" mBroadcastResponseExemptedPermissions");
+ pw.print(" mBroadcastResponseExemptedPermissions=");
pw.print(mBroadcastResponseExemptedPermissions);
pw.println();
pw.println();
pw.print("mAppIdleEnabled="); pw.print(mAppIdleEnabled);
- pw.print(" mAllowRestrictedBucket=");
- pw.print(mAllowRestrictedBucket);
pw.print(" mIsCharging=");
pw.print(mIsCharging);
pw.println();
@@ -2595,6 +2655,11 @@ public class AppStandbyController
}
}
+ IAppOpsService getAppOpsService() {
+ return IAppOpsService.Stub.asInterface(
+ ServiceManager.getService(Context.APP_OPS_SERVICE));
+ }
+
/**
* Returns {@code true} if the supplied package is the wellbeing app. Otherwise,
* returns {@code false}.
@@ -2603,8 +2668,8 @@ public class AppStandbyController
return packageName.equals(mWellbeingApp);
}
- boolean hasExactAlarmPermission(String packageName, int uid) {
- return mAlarmManagerInternal.hasExactAlarmPermission(packageName, uid);
+ boolean shouldGetExactAlarmBucketElevation(String packageName, int uid) {
+ return mAlarmManagerInternal.shouldGetBucketElevation(packageName, uid);
}
void updatePowerWhitelistCache() {
@@ -2625,12 +2690,6 @@ public class AppStandbyController
}
}
- boolean isRestrictedBucketEnabled() {
- return Global.getInt(mContext.getContentResolver(),
- Global.ENABLE_RESTRICTED_BUCKET,
- Global.DEFAULT_ENABLE_RESTRICTED_BUCKET) == 1;
- }
-
File getDataSystemDirectory() {
return Environment.getDataSystemDirectory();
}
@@ -2645,7 +2704,9 @@ public class AppStandbyController
}
void noteEvent(int event, String packageName, int uid) throws RemoteException {
- mBatteryStats.noteEvent(event, packageName, uid);
+ if (mBatteryStats != null) {
+ mBatteryStats.noteEvent(event, packageName, uid);
+ }
}
PackageManagerInternal getPackageManagerInternal() {
@@ -2713,7 +2774,7 @@ public class AppStandbyController
void registerDeviceConfigPropertiesChangedListener(
@NonNull DeviceConfig.OnPropertiesChangedListener listener) {
DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_APP_STANDBY,
- JobSchedulerBackgroundThread.getExecutor(), listener);
+ AppSchedulingModuleThread.getExecutor(), listener);
}
void dump(PrintWriter pw) {
@@ -2964,7 +3025,7 @@ public class AppStandbyController
public static final long DEFAULT_INITIAL_FOREGROUND_SERVICE_START_TIMEOUT =
COMPRESS_TIME ? ONE_MINUTE : 30 * ONE_MINUTE;
public static final long DEFAULT_AUTO_RESTRICTED_BUCKET_DELAY_MS =
- COMPRESS_TIME ? ONE_MINUTE : ONE_DAY;
+ COMPRESS_TIME ? ONE_MINUTE : ONE_HOUR;
public static final boolean DEFAULT_CROSS_PROFILE_APPS_SHARE_STANDBY_BUCKETS = true;
public static final long DEFAULT_BROADCAST_RESPONSE_WINDOW_DURATION_MS =
2 * ONE_MINUTE;
@@ -2991,11 +3052,6 @@ public class AppStandbyController
// APP_STANDBY_ENABLED is a SystemApi that some apps may be watching, so best to
// leave it in Settings.
cr.registerContentObserver(Global.getUriFor(Global.APP_STANDBY_ENABLED), false, this);
- // Leave ENABLE_RESTRICTED_BUCKET as a user-controlled setting which will stay in
- // Settings.
- // TODO: make setting user-specific
- cr.registerContentObserver(Global.getUriFor(Global.ENABLE_RESTRICTED_BUCKET),
- false, this);
// ADAPTIVE_BATTERY_MANAGEMENT_ENABLED is a user setting, so it has to stay in Settings.
cr.registerContentObserver(Global.getUriFor(Global.ADAPTIVE_BATTERY_MANAGEMENT_ENABLED),
false, this);
@@ -3196,10 +3252,6 @@ public class AppStandbyController
Global.ADAPTIVE_BATTERY_MANAGEMENT_ENABLED));
}
- synchronized (mAppIdleLock) {
- mAllowRestrictedBucket = mInjector.isRestrictedBucketEnabled();
- }
-
setAppIdleEnabled(mInjector.isAppIdleEnabled());
}
diff --git a/apex/jobscheduler/service/jni/com_android_server_alarm_AlarmManagerService.cpp b/apex/jobscheduler/service/jni/com_android_server_alarm_AlarmManagerService.cpp
index 8f9e187a7a93..b2ed4d47adf4 100644
--- a/apex/jobscheduler/service/jni/com_android_server_alarm_AlarmManagerService.cpp
+++ b/apex/jobscheduler/service/jni/com_android_server_alarm_AlarmManagerService.cpp
@@ -76,19 +76,17 @@ typedef std::array<int, N_ANDROID_TIMERFDS> TimerFds;
class AlarmImpl
{
public:
- AlarmImpl(const TimerFds &fds, int epollfd, const std::string &rtc_dev)
- : fds{fds}, epollfd{epollfd}, rtc_dev{rtc_dev} {}
+ AlarmImpl(const TimerFds &fds, int epollfd)
+ : fds{fds}, epollfd{epollfd} {}
~AlarmImpl();
int set(int type, struct timespec *ts);
- int setTime(struct timeval *tv);
int waitForAlarm();
int getTime(int type, struct itimerspec *spec);
private:
const TimerFds fds;
const int epollfd;
- std::string rtc_dev;
};
AlarmImpl::~AlarmImpl()
@@ -131,43 +129,6 @@ int AlarmImpl::getTime(int type, struct itimerspec *spec)
return timerfd_gettime(fds[type], spec);
}
-int AlarmImpl::setTime(struct timeval *tv)
-{
- if (settimeofday(tv, NULL) == -1) {
- ALOGV("settimeofday() failed: %s", strerror(errno));
- return -1;
- }
-
- android::base::unique_fd fd{open(rtc_dev.c_str(), O_RDWR)};
- if (!fd.ok()) {
- ALOGE("Unable to open %s: %s", rtc_dev.c_str(), strerror(errno));
- return -1;
- }
-
- struct tm tm;
- if (!gmtime_r(&tv->tv_sec, &tm)) {
- ALOGV("gmtime_r() failed: %s", strerror(errno));
- return -1;
- }
-
- struct rtc_time rtc = {};
- rtc.tm_sec = tm.tm_sec;
- rtc.tm_min = tm.tm_min;
- rtc.tm_hour = tm.tm_hour;
- rtc.tm_mday = tm.tm_mday;
- rtc.tm_mon = tm.tm_mon;
- rtc.tm_year = tm.tm_year;
- rtc.tm_wday = tm.tm_wday;
- rtc.tm_yday = tm.tm_yday;
- rtc.tm_isdst = tm.tm_isdst;
- if (ioctl(fd, RTC_SET_TIME, &rtc) == -1) {
- ALOGV("RTC_SET_TIME ioctl failed: %s", strerror(errno));
- return -1;
- }
-
- return 0;
-}
-
int AlarmImpl::waitForAlarm()
{
epoll_event events[N_ANDROID_TIMERFDS];
@@ -198,28 +159,6 @@ int AlarmImpl::waitForAlarm()
return result;
}
-static jint android_server_alarm_AlarmManagerService_setKernelTime(JNIEnv*, jobject, jlong nativeData, jlong millis)
-{
- AlarmImpl *impl = reinterpret_cast<AlarmImpl *>(nativeData);
-
- if (millis <= 0 || millis / 1000LL >= std::numeric_limits<time_t>::max()) {
- return -1;
- }
-
- struct timeval tv;
- tv.tv_sec = (millis / 1000LL);
- tv.tv_usec = ((millis % 1000LL) * 1000LL);
-
- ALOGD("Setting time of day to sec=%ld", tv.tv_sec);
-
- int ret = impl->setTime(&tv);
- if (ret < 0) {
- ALOGW("Unable to set rtc to %ld: %s", tv.tv_sec, strerror(errno));
- ret = -1;
- }
- return ret;
-}
-
static jint android_server_alarm_AlarmManagerService_setKernelTimezone(JNIEnv*, jobject, jlong, jint minswest)
{
struct timezone tz;
@@ -287,19 +226,7 @@ static jlong android_server_alarm_AlarmManagerService_init(JNIEnv*, jobject)
}
}
- // Find the wall clock RTC. We expect this always to be /dev/rtc0, but
- // check the /dev/rtc symlink first so that legacy devices that don't use
- // rtc0 can add a symlink rather than need to carry a local patch to this
- // code.
- //
- // TODO: if you're reading this in a world where all devices are using the
- // GKI, you can remove the readlink and just assume /dev/rtc0.
- std::string dev_rtc;
- if (!android::base::Readlink("/dev/rtc", &dev_rtc)) {
- dev_rtc = "/dev/rtc0";
- }
-
- std::unique_ptr<AlarmImpl> alarm{new AlarmImpl(fds, epollfd, dev_rtc)};
+ std::unique_ptr<AlarmImpl> alarm{new AlarmImpl(fds, epollfd)};
for (size_t i = 0; i < fds.size(); i++) {
epoll_event event;
@@ -392,7 +319,6 @@ static const JNINativeMethod sMethods[] = {
{"close", "(J)V", (void*)android_server_alarm_AlarmManagerService_close},
{"set", "(JIJJ)I", (void*)android_server_alarm_AlarmManagerService_set},
{"waitForAlarm", "(J)I", (void*)android_server_alarm_AlarmManagerService_waitForAlarm},
- {"setKernelTime", "(JJ)I", (void*)android_server_alarm_AlarmManagerService_setKernelTime},
{"setKernelTimezone", "(JI)I", (void*)android_server_alarm_AlarmManagerService_setKernelTimezone},
{"getNextAlarm", "(JI)J", (void*)android_server_alarm_AlarmManagerService_getNextAlarm},
};