diff options
Diffstat (limited to 'apex')
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}, }; |