diff options
Diffstat (limited to 'apex')
27 files changed, 1202 insertions, 608 deletions
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 3af36ebb08ca..e200434d9ff2 100644 --- a/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java +++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java @@ -1404,7 +1404,11 @@ public class BlobStoreManagerService extends SystemService { if (LOGV) { Slog.v(TAG, "Received " + intent); } - switch (intent.getAction()) { + final String action = intent.getAction(); + if (action == null) { + return; + } + switch (action) { case Intent.ACTION_PACKAGE_FULLY_REMOVED: case Intent.ACTION_PACKAGE_DATA_CLEARED: final String packageName = intent.getData().getSchemeSpecificPart(); @@ -1431,7 +1435,11 @@ public class BlobStoreManagerService extends SystemService { if (LOGV) { Slog.v(TAG, "Received: " + intent); } - switch (intent.getAction()) { + final String action = intent.getAction(); + if (action == null) { + return; + } + switch (action) { case Intent.ACTION_USER_REMOVED: final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, USER_NULL); diff --git a/apex/jobscheduler/framework/aconfig/job.aconfig b/apex/jobscheduler/framework/aconfig/job.aconfig index 5f5507587f72..63624d8cad4a 100644 --- a/apex/jobscheduler/framework/aconfig/job.aconfig +++ b/apex/jobscheduler/framework/aconfig/job.aconfig @@ -25,8 +25,33 @@ flag { } flag { - name: "cleanup_empty_jobs" + name: "handle_abandoned_jobs" namespace: "backstage_power" - description: "Enables automatic cancellation of jobs due to leaked JobParameters, reducing unnecessary battery drain and improving system efficiency. This includes logging and traces for better issue diagnosis." - bug: "349688611" + description: "Detect, report and take action on jobs that maybe abandoned by the app without calling jobFinished." + bug: "372529068" + is_exported: true +} + +flag { + name: "ignore_important_while_foreground" + namespace: "backstage_power" + description: "Ignore the important_while_foreground flag and change the related APIs to be not effective" + bug: "374175032" + is_exported: true +} + +flag { + name: "get_pending_job_reasons_api" + is_exported: true + namespace: "backstage_power" + description: "Introduce a new getPendingJobReasons() API which returns reasons why a job may not have executed. Also deprecate the existing getPendingJobReason() API." + bug: "372031023" +} + +flag { + name: "get_pending_job_reasons_history_api" + is_exported: true + namespace: "backstage_power" + description: "Introduce a new getPendingJobReasonsHistory() API which returns a limited historical view of getPendingJobReasons()." + bug: "372031023" } diff --git a/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java b/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java index 3cfddc6d8e2b..e9b11f46ddde 100644 --- a/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java +++ b/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java @@ -25,11 +25,13 @@ import android.app.job.JobInfo; import android.app.job.JobScheduler; import android.app.job.JobSnapshot; import android.app.job.JobWorkItem; +import android.app.job.PendingJobReasonsInfo; import android.content.Context; import android.content.pm.ParceledListSlice; import android.os.RemoteException; import android.util.ArrayMap; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; @@ -173,6 +175,26 @@ public class JobSchedulerImpl extends JobScheduler { } @Override + @NonNull + public int[] getPendingJobReasons(int jobId) { + try { + return mBinder.getPendingJobReasons(mNamespace, jobId); + } catch (RemoteException e) { + return new int[] { PENDING_JOB_REASON_UNDEFINED }; + } + } + + @Override + @NonNull + public List<PendingJobReasonsInfo> getPendingJobReasonsHistory(int jobId) { + try { + return mBinder.getPendingJobReasonsHistory(mNamespace, jobId); + } catch (RemoteException e) { + return Collections.EMPTY_LIST; + } + } + + @Override public boolean canRunUserInitiatedJobs() { try { return mBinder.canRunUserInitiatedJobs(mContext.getOpPackageName()); diff --git a/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl b/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl index 11d17ca749b7..887f812aebca 100644 --- a/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl +++ b/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl @@ -87,11 +87,12 @@ interface IJobCallback { void jobFinished(int jobId, boolean reschedule); /* - * Inform JobScheduler to force finish this job because the client has lost - * the job handle. jobFinished can no longer be called from the client. + * Inform JobScheduler that this job may have been abandoned because the client process + * has lost strong references to the JobParameters object without calling jobFinished. + * * @param jobId Unique integer used to identify this job */ - void forceJobFinished(int jobId); + void handleAbandonedJob(int jobId); /* * Inform JobScheduler of a change in the estimated transfer payload. diff --git a/apex/jobscheduler/framework/java/android/app/job/IJobScheduler.aidl b/apex/jobscheduler/framework/java/android/app/job/IJobScheduler.aidl index 416a2d8c0002..dc7f3d143e4c 100644 --- a/apex/jobscheduler/framework/java/android/app/job/IJobScheduler.aidl +++ b/apex/jobscheduler/framework/java/android/app/job/IJobScheduler.aidl @@ -20,6 +20,7 @@ import android.app.job.IUserVisibleJobObserver; import android.app.job.JobInfo; import android.app.job.JobSnapshot; import android.app.job.JobWorkItem; +import android.app.job.PendingJobReasonsInfo; import android.content.pm.ParceledListSlice; import java.util.Map; @@ -39,6 +40,8 @@ interface IJobScheduler { ParceledListSlice<JobInfo> getAllPendingJobsInNamespace(String namespace); JobInfo getPendingJob(String namespace, int jobId); int getPendingJobReason(String namespace, int jobId); + int[] getPendingJobReasons(String namespace, int jobId); + List<PendingJobReasonsInfo> getPendingJobReasonsHistory(String namespace, int jobId); boolean canRunUserInitiatedJobs(String packageName); boolean hasRunUserInitiatedJobsPermission(String packageName, int userId); List<JobInfo> getStartedJobs(); diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java index 5f57c3973ab0..cc2d104813e4 100644 --- a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java +++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java @@ -809,9 +809,21 @@ public class JobInfo implements Parcelable { } /** + * <p class="caution"><strong>Note:</strong> Beginning with + * {@link android.os.Build.VERSION_CODES#B}, this flag will be ignored and no longer + * function effectively, regardless of the calling app's target SDK version. + * Calling this method will always return {@code false}. + * * @see JobInfo.Builder#setImportantWhileForeground(boolean) + * + * @deprecated Use {@link #isExpedited()} instead. */ + @FlaggedApi(Flags.FLAG_IGNORE_IMPORTANT_WHILE_FOREGROUND) + @Deprecated public boolean isImportantWhileForeground() { + if (Flags.ignoreImportantWhileForeground()) { + return false; + } return (flags & FLAG_IMPORTANT_WHILE_FOREGROUND) != 0; } @@ -2124,6 +2136,13 @@ public class JobInfo implements Parcelable { * <p> * Jobs marked as important-while-foreground are given {@link #PRIORITY_HIGH} by default. * + * <p class="caution"><strong>Note:</strong> Beginning with + * {@link android.os.Build.VERSION_CODES#B}, this flag will be ignored and no longer + * function effectively, regardless of the calling app's target SDK version. + * {link #isImportantWhileForeground()} will always return {@code false}. + * Apps should use {link #setExpedited(boolean)} with {@code true} to indicate + * that this job is important and needs to run as soon as possible. + * * @param importantWhileForeground whether to relax doze restrictions for this job when the * app is in the foreground. False by default. * @see JobInfo#isImportantWhileForeground() @@ -2131,6 +2150,12 @@ public class JobInfo implements Parcelable { */ @Deprecated public Builder setImportantWhileForeground(boolean importantWhileForeground) { + if (Flags.ignoreImportantWhileForeground()) { + Log.w(TAG, "Requested important-while-foreground flag for job" + mJobId + + " is ignored and takes no effect"); + return this; + } + if (importantWhileForeground) { mFlags |= FLAG_IMPORTANT_WHILE_FOREGROUND; if (mPriority == PRIORITY_DEFAULT) { diff --git a/apex/jobscheduler/framework/java/android/app/job/JobParameters.java b/apex/jobscheduler/framework/java/android/app/job/JobParameters.java index 52a761f8d486..7fef4e502c97 100644 --- a/apex/jobscheduler/framework/java/android/app/job/JobParameters.java +++ b/apex/jobscheduler/framework/java/android/app/job/JobParameters.java @@ -16,12 +16,17 @@ package android.app.job; +import android.annotation.FlaggedApi; 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.Compatibility; +import android.compat.annotation.ChangeId; +import android.compat.annotation.Disabled; +import android.compat.annotation.Overridable; import android.compat.annotation.UnsupportedAppUsage; import android.content.ClipData; import android.content.pm.PackageManager; @@ -33,6 +38,7 @@ import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; import android.os.PersistableBundle; +import android.os.Process; import android.os.RemoteException; import android.system.SystemCleaner; import android.util.Log; @@ -120,6 +126,15 @@ public class JobParameters implements Parcelable { JobProtoEnums.INTERNAL_STOP_REASON_ANR; // 12. /** + * The job ran for at least its minimum execution limit and the app lost the strong reference + * to the {@link JobParameters}. This could indicate that the job is empty because the app + * can no longer call {@link JobService#jobFinished(JobParameters, boolean)}. + * @hide + */ + public static final int INTERNAL_STOP_REASON_TIMEOUT_ABANDONED = + JobProtoEnums.INTERNAL_STOP_REASON_TIMEOUT_ABANDONED; // 13. + + /** * All the stop reason codes. This should be regarded as an immutable array at runtime. * * Note the order of these values will affect "dumpsys batterystats", and we do not want to @@ -143,6 +158,7 @@ public class JobParameters implements Parcelable { INTERNAL_STOP_REASON_SUCCESSFUL_FINISH, INTERNAL_STOP_REASON_USER_UI_STOP, INTERNAL_STOP_REASON_ANR, + INTERNAL_STOP_REASON_TIMEOUT_ABANDONED, }; /** @@ -165,6 +181,7 @@ public class JobParameters implements Parcelable { 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"; + case INTERNAL_STOP_REASON_TIMEOUT_ABANDONED: return "timeout_abandoned"; default: return "unknown:" + reasonCode; } } @@ -268,6 +285,25 @@ public class JobParameters implements Parcelable { */ public static final int STOP_REASON_ESTIMATED_APP_LAUNCH_TIME_CHANGED = 15; + /** + * The job used up its maximum execution time and timed out. The system also detected that the + * app can no longer call {@link JobService#jobFinished(JobParameters, boolean)} for this job, + * likely because the strong reference to the job handle ({@link JobParameters}) passed to it + * via {@link JobService#onStartJob(JobParameters)} was lost. This can occur even if the app + * called {@link JobScheduler#cancel(int)}, {@link JobScheduler#cancelAll()}, or + * {@link JobScheduler#cancelInAllNamespaces()} to stop an active job while losing strong + * references to the job handle. In this case, the job is not necessarily abandoned. However, + * the system cannot distinguish such cases from truly abandoned jobs. + * <p> + * It is recommended that you use {@link JobService#jobFinished(JobParameters, boolean)} or + * return false from {@link JobService#onStartJob(JobParameters)} to stop an active job. This + * will prevent the occurrence of this stop reason and the need to handle it. The primary use + * case for this stop reason is to report a probable case of a job being abandoned. + * <p> + */ + @FlaggedApi(Flags.FLAG_HANDLE_ABANDONED_JOBS) + public static final int STOP_REASON_TIMEOUT_ABANDONED = 16; + /** @hide */ @IntDef(prefix = {"STOP_REASON_"}, value = { STOP_REASON_UNDEFINED, @@ -286,6 +322,7 @@ public class JobParameters implements Parcelable { STOP_REASON_USER, STOP_REASON_SYSTEM_PROCESSING, STOP_REASON_ESTIMATED_APP_LAUNCH_TIME_CHANGED, + STOP_REASON_TIMEOUT_ABANDONED, }) @Retention(RetentionPolicy.SOURCE) public @interface StopReason { @@ -316,6 +353,16 @@ public class JobParameters implements Parcelable { private JobCleanupCallback mJobCleanupCallback; @Nullable private Cleaner.Cleanable mCleanable; + /** + * Override handling of abandoned jobs in the system. Overriding this change + * will prevent the system to handle abandoned jobs and report it as a new + * stop reason STOP_REASON_TIMEOUT_ABANDONED. + * @hide + */ + @ChangeId + @Disabled + @Overridable + public static final long OVERRIDE_HANDLE_ABANDONED_JOBS = 372529068L; /** @hide */ public JobParameters(IBinder callback, String namespace, int jobId, PersistableBundle extras, @@ -360,6 +407,12 @@ public class JobParameters implements Parcelable { } /** + * Returns the reason {@link JobService#onStopJob(JobParameters)} was called on this job. + * <p> + * Apps should not rely on the stop reason for critical decision-making, as additional stop + * reasons may be added in subsequent Android releases. The primary intended use of this method + * is for logging and diagnostic purposes to gain insights into the causes of job termination. + * <p> * @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. @@ -638,6 +691,16 @@ public class JobParameters implements Parcelable { * @hide */ public void enableCleaner() { + if (!Flags.handleAbandonedJobs() + || Compatibility.isChangeEnabled(OVERRIDE_HANDLE_ABANDONED_JOBS)) { + return; + } + // JobParameters objects are passed by reference in local Binder + // transactions for clients running as SYSTEM. The life cycle of the + // JobParameters objects are no longer controlled by the client. + if (Process.myUid() == Process.SYSTEM_UID) { + return; + } if (mJobCleanupCallback == null) { initCleaner(new JobCleanupCallback(IJobCallback.Stub.asInterface(callback), jobId)); } @@ -650,6 +713,10 @@ public class JobParameters implements Parcelable { * @hide */ public void disableCleaner() { + if (!Flags.handleAbandonedJobs() + || Compatibility.isChangeEnabled(OVERRIDE_HANDLE_ABANDONED_JOBS)) { + return; + } if (mJobCleanupCallback != null) { mJobCleanupCallback.disableCleaner(); if (mCleanable != null) { @@ -763,7 +830,7 @@ public class JobParameters implements Parcelable { return; } try { - mCallback.forceJobFinished(mJobId); + mCallback.handleAbandonedJob(mJobId); } catch (Exception e) { Log.wtf(TAG, "Could not destroy running job", e); } diff --git a/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java index ad54cd397413..4fbd55a5d528 100644 --- a/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java +++ b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java @@ -16,6 +16,7 @@ package android.app.job; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -238,6 +239,13 @@ public abstract class JobScheduler { * to defer this job. */ public static final int PENDING_JOB_REASON_USER = 15; + /** + * The override deadline has not transpired. + * + * @see JobInfo.Builder#setOverrideDeadline(long) + */ + @FlaggedApi(Flags.FLAG_GET_PENDING_JOB_REASONS_API) + public static final int PENDING_JOB_REASON_CONSTRAINT_DEADLINE = 16; /** @hide */ @IntDef(prefix = {"PENDING_JOB_REASON_"}, value = { @@ -259,6 +267,7 @@ public abstract class JobScheduler { PENDING_JOB_REASON_JOB_SCHEDULER_OPTIMIZATION, PENDING_JOB_REASON_QUOTA, PENDING_JOB_REASON_USER, + PENDING_JOB_REASON_CONSTRAINT_DEADLINE, }) @Retention(RetentionPolicy.SOURCE) public @interface PendingJobReason { @@ -458,6 +467,10 @@ public abstract class JobScheduler { /** * 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. + * + * @apiNote + * To know all the potential reasons why the job may be pending, + * use {@link #getPendingJobReasons(int)} instead. */ @PendingJobReason public int getPendingJobReason(int jobId) { @@ -465,6 +478,49 @@ public abstract class JobScheduler { } /** + * Returns potential reasons why the job with the given {@code jobId} may be pending + * and not currently executing. + * + * The returned array will include {@link PendingJobReason reasons} composed of both + * explicitly set constraints on the job and implicit constraints imposed by the system. + * The results can be used to debug why a given job may not be currently executing. + */ + @FlaggedApi(Flags.FLAG_GET_PENDING_JOB_REASONS_API) + @NonNull + @PendingJobReason + public int[] getPendingJobReasons(int jobId) { + return new int[] { PENDING_JOB_REASON_UNDEFINED }; + } + + /** + * For the given {@code jobId}, returns a limited historical view of why the job may have + * been pending execution. The returned list is composed of {@link PendingJobReasonsInfo} + * objects, each of which include a timestamp since epoch along with an array of + * unsatisfied constraints represented by {@link PendingJobReason PendingJobReason constants}. + * <p> + * These constants could either be explicitly set constraints on the job or implicit + * constraints imposed by the system due to various reasons. + * The results can be used to debug why a given job may have been pending execution. + * <p> + * If the only {@link PendingJobReason} for the timestamp is + * {@link PendingJobReason#PENDING_JOB_REASON_UNDEFINED}, it could mean that + * the job was ready to be executed at that point in time. + * <p> + * Note: there is no set interval for the timestamps in the returned list since + * constraint changes occur based on device status and various other factors. + * <p> + * Note: the pending job reasons history is not persisted across device reboots. + * <p> + * @throws IllegalArgumentException if the {@code jobId} is invalid. + * @see #getPendingJobReasons(int) + */ + @FlaggedApi(Flags.FLAG_GET_PENDING_JOB_REASONS_HISTORY_API) + @NonNull + public List<PendingJobReasonsInfo> getPendingJobReasonsHistory(int jobId) { + throw new UnsupportedOperationException("Not implemented by " + getClass()); + } + + /** * 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. diff --git a/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java b/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java index 5f80c52388b4..d460dcc65473 100644 --- a/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java +++ b/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java @@ -165,11 +165,9 @@ public abstract class JobServiceEngine { case MSG_EXECUTE_JOB: { final JobParameters params = (JobParameters) msg.obj; try { - if (Flags.cleanupEmptyJobs()) { - params.enableCleaner(); - } + params.enableCleaner(); boolean workOngoing = JobServiceEngine.this.onStartJob(params); - if (Flags.cleanupEmptyJobs() && !workOngoing) { + if (!workOngoing) { params.disableCleaner(); } ackStartMessage(params, workOngoing); @@ -196,9 +194,7 @@ public abstract class JobServiceEngine { IJobCallback callback = params.getCallback(); if (callback != null) { try { - if (Flags.cleanupEmptyJobs()) { - params.disableCleaner(); - } + params.disableCleaner(); callback.jobFinished(params.getJobId(), needsReschedule); } catch (RemoteException e) { Log.e(TAG, "Error reporting job finish to system: binder has gone" + diff --git a/apex/jobscheduler/framework/java/android/app/job/PendingJobReasonsInfo.aidl b/apex/jobscheduler/framework/java/android/app/job/PendingJobReasonsInfo.aidl new file mode 100644 index 000000000000..1a027020e25f --- /dev/null +++ b/apex/jobscheduler/framework/java/android/app/job/PendingJobReasonsInfo.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2024 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 android.app.job; + + parcelable PendingJobReasonsInfo; diff --git a/apex/jobscheduler/framework/java/android/app/job/PendingJobReasonsInfo.java b/apex/jobscheduler/framework/java/android/app/job/PendingJobReasonsInfo.java new file mode 100644 index 000000000000..3c96bab80794 --- /dev/null +++ b/apex/jobscheduler/framework/java/android/app/job/PendingJobReasonsInfo.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2024 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 android.app.job; + +import android.annotation.CurrentTimeMillisLong; +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * A simple wrapper which includes a timestamp (in millis since epoch) + * and an array of {@link JobScheduler.PendingJobReason reasons} at that timestamp + * for why a particular job may be pending. + */ +@FlaggedApi(Flags.FLAG_GET_PENDING_JOB_REASONS_HISTORY_API) +public final class PendingJobReasonsInfo implements Parcelable { + + @CurrentTimeMillisLong + private final long mTimestampMillis; + + @NonNull + @JobScheduler.PendingJobReason + private final int[] mPendingJobReasons; + + public PendingJobReasonsInfo(long timestampMillis, + @NonNull @JobScheduler.PendingJobReason int[] reasons) { + mTimestampMillis = timestampMillis; + mPendingJobReasons = reasons; + } + + /** + * @return the time (in millis since epoch) associated with the set of pending job reasons. + */ + @CurrentTimeMillisLong + public long getTimestampMillis() { + return mTimestampMillis; + } + + /** + * Returns a set of {@link android.app.job.JobScheduler.PendingJobReason reasons} representing + * why the job may not have executed at the associated timestamp. + * <p> + * These reasons could either be explicitly set constraints on the job or implicit + * constraints imposed by the system due to various reasons. + * <p> + * Note: if the only {@link android.app.job.JobScheduler.PendingJobReason} present is + * {@link JobScheduler.PendingJobReason#PENDING_JOB_REASON_UNDEFINED}, it could mean + * that the job was ready to be executed at that time. + */ + @NonNull + @JobScheduler.PendingJobReason + public int[] getPendingJobReasons() { + return mPendingJobReasons; + } + + private PendingJobReasonsInfo(Parcel in) { + mTimestampMillis = in.readLong(); + mPendingJobReasons = in.createIntArray(); + } + + @NonNull + public static final Creator<PendingJobReasonsInfo> CREATOR = + new Creator<>() { + @Override + public PendingJobReasonsInfo createFromParcel(Parcel in) { + return new PendingJobReasonsInfo(in); + } + + @Override + public PendingJobReasonsInfo[] newArray(int size) { + return new PendingJobReasonsInfo[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeLong(mTimestampMillis); + dest.writeIntArray(mPendingJobReasons); + } +} diff --git a/apex/jobscheduler/service/aconfig/alarm.aconfig b/apex/jobscheduler/service/aconfig/alarm.aconfig index d3068d7d37e8..a6e980726a9a 100644 --- a/apex/jobscheduler/service/aconfig/alarm.aconfig +++ b/apex/jobscheduler/service/aconfig/alarm.aconfig @@ -2,16 +2,6 @@ package: "com.android.server.alarm" container: "system" flag { - name: "use_frozen_state_to_drop_listener_alarms" - namespace: "backstage_power" - description: "Use frozen state callback to drop listener alarms for cached apps" - bug: "324470945" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "start_user_before_scheduled_alarms" namespace: "multiuser" description: "Persist list of users with alarms scheduled and wakeup stopped users before alarms are due" diff --git a/apex/jobscheduler/service/aconfig/app_idle.aconfig b/apex/jobscheduler/service/aconfig/app_idle.aconfig index c8976ca8361e..74d2a590086f 100644 --- a/apex/jobscheduler/service/aconfig/app_idle.aconfig +++ b/apex/jobscheduler/service/aconfig/app_idle.aconfig @@ -12,3 +12,19 @@ flag { } } +flag { + name: "screen_time_bypass" + namespace: "backstage_power" + description: "Bypass the screen time check for bucket evaluation" + bug: "374114769" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "adjust_default_bucket_elevation_params" + namespace: "backstage_power" + description: "Adjust the default bucket evaluation parameters" + bug: "379909479" +} diff --git a/apex/jobscheduler/service/aconfig/device_idle.aconfig b/apex/jobscheduler/service/aconfig/device_idle.aconfig index c4d0d1850a18..426031fbeb9c 100644 --- a/apex/jobscheduler/service/aconfig/device_idle.aconfig +++ b/apex/jobscheduler/service/aconfig/device_idle.aconfig @@ -17,3 +17,13 @@ flag { description: "Disable wakelocks for background apps while Light Device Idle is active" bug: "326607666" } + +flag { + name: "use_cpu_time_for_temp_allowlist" + namespace: "backstage_power" + description: "Use CPU time for temporary allowlists" + bug: "376561328" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/apex/jobscheduler/service/aconfig/job.aconfig b/apex/jobscheduler/service/aconfig/job.aconfig index e5389b4f96fb..86ed06bf4e3d 100644 --- a/apex/jobscheduler/service/aconfig/job.aconfig +++ b/apex/jobscheduler/service/aconfig/job.aconfig @@ -63,7 +63,7 @@ flag { name: "remove_user_during_user_switch" namespace: "backstage_power" description: "Remove started user if user will be stopped due to user switch" - bug: "321598070" + bug: "337077643" } flag { @@ -75,3 +75,34 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "enforce_quota_policy_to_fgs_jobs" + namespace: "backstage_power" + description: "Applies the normal quota policy to FGS jobs" + bug: "341201311" +} + +flag { + name: "adjust_quota_default_constants" + namespace: "backstage_power" + description: "Adjust quota default parameters" + bug: "347058927" +} + +flag { + name: "enforce_quota_policy_to_top_started_jobs" + namespace: "backstage_power" + description: "Apply the quota policy to jobs started when the app was in TOP state" + bug: "374323858" +} + +flag { + name: "enforce_schedule_limit_to_proxy_jobs" + namespace: "backstage_power" + description: "Limit the schedule calls towards the persisted proxy jobs" + bug: "377912323" + metadata { + purpose: PURPOSE_BUGFIX + } +}
\ No newline at end of file diff --git a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java index 3e650da2e66f..41fd4a29cfd1 100644 --- a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java +++ b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java @@ -620,8 +620,8 @@ public class DeviceIdleController extends SystemService * the network and acquire wakelocks. Times are in milliseconds. */ @GuardedBy("this") - private final SparseArray<Pair<MutableLong, String>> mTempWhitelistAppIdEndTimes - = new SparseArray<>(); + @VisibleForTesting + final SparseArray<Pair<MutableLong, String>> mTempWhitelistAppIdEndTimes = new SparseArray<>(); private NetworkPolicyManagerInternal mNetworkPolicyManagerInternal; @@ -1941,7 +1941,8 @@ public class DeviceIdleController extends SystemService private static final int MSG_REPORT_IDLE_ON_LIGHT = 3; private static final int MSG_REPORT_IDLE_OFF = 4; private static final int MSG_REPORT_ACTIVE = 5; - private static final int MSG_TEMP_APP_WHITELIST_TIMEOUT = 6; + @VisibleForTesting + static final int MSG_TEMP_APP_WHITELIST_TIMEOUT = 6; @VisibleForTesting static final int MSG_REPORT_STATIONARY_STATUS = 7; private static final int MSG_FINISH_IDLE_OP = 8; @@ -2511,6 +2512,11 @@ public class DeviceIdleController extends SystemService return SystemClock.elapsedRealtime(); } + /** Returns the current elapsed realtime in milliseconds. */ + long getUptimeMillis() { + return SystemClock.uptimeMillis(); + } + LocationManager getLocationManager() { if (mLocationManager == null) { mLocationManager = mContext.getSystemService(LocationManager.class); @@ -3264,7 +3270,8 @@ public class DeviceIdleController extends SystemService void addPowerSaveTempWhitelistAppDirectInternal(int callingUid, int uid, long duration, @TempAllowListType int tempAllowListType, boolean sync, @ReasonCode int reasonCode, @Nullable String reason) { - final long timeNow = SystemClock.elapsedRealtime(); + final long timeNow = Flags.useCpuTimeForTempAllowlist() ? mInjector.getUptimeMillis() + : mInjector.getElapsedRealtime(); boolean informWhitelistChanged = false; int appId = UserHandle.getAppId(uid); synchronized (this) { @@ -3350,7 +3357,8 @@ public class DeviceIdleController extends SystemService } void checkTempAppWhitelistTimeout(int uid) { - final long timeNow = SystemClock.elapsedRealtime(); + final long timeNow = Flags.useCpuTimeForTempAllowlist() ? mInjector.getUptimeMillis() + : mInjector.getElapsedRealtime(); final int appId = UserHandle.getAppId(uid); if (DEBUG) { Slog.d(TAG, "checkTempAppWhitelistTimeout: uid=" + uid + ", timeNow=" + timeNow); @@ -5219,6 +5227,17 @@ public class DeviceIdleController extends SystemService } } + pw.println(" Flags:"); + pw.print(" "); + pw.print(Flags.FLAG_USE_CPU_TIME_FOR_TEMP_ALLOWLIST); + pw.print("="); + pw.println(Flags.useCpuTimeForTempAllowlist()); + pw.print(" "); + pw.print(Flags.FLAG_REMOVE_IDLE_LOCATION); + pw.print("="); + pw.println(Flags.removeIdleLocation()); + pw.println(); + synchronized (this) { mConstants.dump(pw); @@ -5449,7 +5468,8 @@ public class DeviceIdleController extends SystemService pw.println(" Temp whitelist schedule:"); prefix = " "; } - final long timeNow = SystemClock.elapsedRealtime(); + final long timeNow = Flags.useCpuTimeForTempAllowlist() ? mInjector.getUptimeMillis() + : mInjector.getElapsedRealtime(); for (int i = 0; i < size; i++) { pw.print(prefix); pw.print("UID="); 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 033da2df9bf6..60ba3b896a28 100644 --- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java +++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java @@ -282,7 +282,6 @@ public class AlarmManagerService extends SystemService { private final Injector mInjector; int mBroadcastRefCount = 0; - boolean mUseFrozenStateToDropListenerAlarms; MetricsHelper mMetricsHelper; PowerManager.WakeLock mWakeLock; SparseIntArray mAlarmsPerUid = new SparseIntArray(); @@ -1784,40 +1783,37 @@ public class AlarmManagerService extends SystemService { mMetricsHelper = new MetricsHelper(getContext(), mLock); mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class); - mUseFrozenStateToDropListenerAlarms = Flags.useFrozenStateToDropListenerAlarms(); mStartUserBeforeScheduledAlarms = Flags.startUserBeforeScheduledAlarms() && UserManager.supportsMultipleUsers(); if (mStartUserBeforeScheduledAlarms) { mUserWakeupStore = new UserWakeupStore(); mUserWakeupStore.init(); } - if (mUseFrozenStateToDropListenerAlarms) { - final ActivityManager.UidFrozenStateChangedCallback callback = (uids, frozenStates) -> { - final int size = frozenStates.length; - if (uids.length != size) { - Slog.wtf(TAG, "Got different length arrays in frozen state callback!" - + " uids.length: " + uids.length + " frozenStates.length: " + size); - // Cannot process received data in any meaningful way. - return; - } - final IntArray affectedUids = new IntArray(); - for (int i = 0; i < size; i++) { - if (frozenStates[i] != UID_FROZEN_STATE_FROZEN) { - continue; - } - if (!CompatChanges.isChangeEnabled(EXACT_LISTENER_ALARMS_DROPPED_ON_CACHED, - uids[i])) { - continue; - } - affectedUids.add(uids[i]); + final ActivityManager.UidFrozenStateChangedCallback callback = (uids, frozenStates) -> { + final int size = frozenStates.length; + if (uids.length != size) { + Slog.wtf(TAG, "Got different length arrays in frozen state callback!" + + " uids.length: " + uids.length + " frozenStates.length: " + size); + // Cannot process received data in any meaningful way. + return; + } + final IntArray affectedUids = new IntArray(); + for (int i = 0; i < size; i++) { + if (frozenStates[i] != UID_FROZEN_STATE_FROZEN) { + continue; } - if (affectedUids.size() > 0) { - removeExactListenerAlarms(affectedUids.toArray()); + if (!CompatChanges.isChangeEnabled(EXACT_LISTENER_ALARMS_DROPPED_ON_CACHED, + uids[i])) { + continue; } - }; - final ActivityManager am = getContext().getSystemService(ActivityManager.class); - am.registerUidFrozenStateChangedCallback(new HandlerExecutor(mHandler), callback); - } + affectedUids.add(uids[i]); + } + if (affectedUids.size() > 0) { + removeExactListenerAlarms(affectedUids.toArray()); + } + }; + final ActivityManager am = getContext().getSystemService(ActivityManager.class); + am.registerUidFrozenStateChangedCallback(new HandlerExecutor(mHandler), callback); mListenerDeathRecipient = new IBinder.DeathRecipient() { @Override @@ -2994,13 +2990,10 @@ public class AlarmManagerService extends SystemService { pw.println("Feature Flags:"); pw.increaseIndent(); - pw.print(Flags.FLAG_USE_FROZEN_STATE_TO_DROP_LISTENER_ALARMS, - mUseFrozenStateToDropListenerAlarms); - pw.println(); pw.print(Flags.FLAG_START_USER_BEFORE_SCHEDULED_ALARMS, Flags.startUserBeforeScheduledAlarms()); - pw.decreaseIndent(); pw.println(); + pw.decreaseIndent(); pw.println(); pw.println("App Standby Parole: " + mAppStandbyParole); @@ -5146,38 +5139,6 @@ public class AlarmManagerService extends SystemService { removeForStoppedLocked(uid); } } - - @Override - public void handleUidCachedChanged(int uid, boolean cached) { - if (mUseFrozenStateToDropListenerAlarms) { - // Use ActivityManager#UidFrozenStateChangedCallback instead. - return; - } - if (!CompatChanges.isChangeEnabled(EXACT_LISTENER_ALARMS_DROPPED_ON_CACHED, uid)) { - return; - } - // 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) { 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 5255e7227709..5dfb3754e8fb 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java @@ -1550,7 +1550,7 @@ class JobConcurrencyManager { mActivePkgStats.add( jobStatus.getSourceUserId(), jobStatus.getSourcePackageName(), packageStats); - mService.resetPendingJobReasonCache(jobStatus); + mService.resetPendingJobReasonsCache(jobStatus); } if (mService.getPendingJobQueue().remove(jobStatus)) { mService.mJobPackageTracker.noteNonpending(jobStatus); 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 97c6e25eeb75..4335cae65a3c 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -16,6 +16,7 @@ package com.android.server.job; +import static android.app.job.JobParameters.OVERRIDE_HANDLE_ABANDONED_JOBS; 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; @@ -44,6 +45,7 @@ import android.app.job.JobScheduler; import android.app.job.JobService; import android.app.job.JobSnapshot; import android.app.job.JobWorkItem; +import android.app.job.PendingJobReasonsInfo; import android.app.job.UserVisibleJobSummary; import android.app.usage.UsageStatsManager; import android.app.usage.UsageStatsManagerInternal; @@ -460,10 +462,10 @@ 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. + * Cached pending job reasons. Mapping from UID -> namespace -> job ID -> reasons. */ - @GuardedBy("mPendingJobReasonCache") // Use its own lock to avoid blocking JS processing - private final SparseArrayMap<String, SparseIntArray> mPendingJobReasonCache = + @GuardedBy("mPendingJobReasonsCache") // Use its own lock to avoid blocking JS processing + private final SparseArrayMap<String, SparseArray<int[]>> mPendingJobReasonsCache = new SparseArrayMap<>(); /** @@ -572,6 +574,7 @@ public class JobSchedulerService extends com.android.server.SystemService case Constants.KEY_MIN_LINEAR_BACKOFF_TIME_MS: case Constants.KEY_MIN_EXP_BACKOFF_TIME_MS: case Constants.KEY_SYSTEM_STOP_TO_FAILURE_RATIO: + case Constants.KEY_ABANDONED_JOB_TIMEOUTS_BEFORE_AGGRESSIVE_BACKOFF: mConstants.updateBackoffConstantsLocked(); break; case Constants.KEY_CONN_CONGESTION_DELAY_FRAC: @@ -678,6 +681,8 @@ public class JobSchedulerService extends com.android.server.SystemService private static final String KEY_MIN_EXP_BACKOFF_TIME_MS = "min_exp_backoff_time_ms"; private static final String KEY_SYSTEM_STOP_TO_FAILURE_RATIO = "system_stop_to_failure_ratio"; + private static final String KEY_ABANDONED_JOB_TIMEOUTS_BEFORE_AGGRESSIVE_BACKOFF = + "abandoned_job_timeouts_before_aggressive_backoff"; 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 = @@ -749,6 +754,7 @@ public class JobSchedulerService extends com.android.server.SystemService 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 int DEFAULT_ABANDONED_JOB_TIMEOUTS_BEFORE_AGGRESSIVE_BACKOFF = 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; @@ -844,7 +850,12 @@ public class JobSchedulerService extends com.android.server.SystemService * incremental failure in the backoff policy calculation. */ int SYSTEM_STOP_TO_FAILURE_RATIO = DEFAULT_SYSTEM_STOP_TO_FAILURE_RATIO; - + /** + * Number of consecutive timeouts by abandoned jobs before we change to aggressive backoff + * policy. + */ + int ABANDONED_JOB_TIMEOUTS_BEFORE_AGGRESSIVE_BACKOFF = + DEFAULT_ABANDONED_JOB_TIMEOUTS_BEFORE_AGGRESSIVE_BACKOFF; /** * The fraction of a job's running window that must pass before we * consider running it when the network is congested. @@ -1077,6 +1088,10 @@ public class JobSchedulerService extends com.android.server.SystemService SYSTEM_STOP_TO_FAILURE_RATIO = DeviceConfig.getInt(DeviceConfig.NAMESPACE_JOB_SCHEDULER, KEY_SYSTEM_STOP_TO_FAILURE_RATIO, DEFAULT_SYSTEM_STOP_TO_FAILURE_RATIO); + ABANDONED_JOB_TIMEOUTS_BEFORE_AGGRESSIVE_BACKOFF = DeviceConfig.getInt( + DeviceConfig.NAMESPACE_JOB_SCHEDULER, + KEY_ABANDONED_JOB_TIMEOUTS_BEFORE_AGGRESSIVE_BACKOFF, + DEFAULT_ABANDONED_JOB_TIMEOUTS_BEFORE_AGGRESSIVE_BACKOFF); } // TODO(141645789): move into ConnectivityController.CcConfig @@ -1286,6 +1301,8 @@ 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_ABANDONED_JOB_TIMEOUTS_BEFORE_AGGRESSIVE_BACKOFF, + ABANDONED_JOB_TIMEOUTS_BEFORE_AGGRESSIVE_BACKOFF).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(); @@ -1702,8 +1719,9 @@ public class JobSchedulerService extends com.android.server.SystemService 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))) { - // Only limit schedule calls for persisted jobs scheduled by the app itself. + if (job.isPersisted() && (Flags.enforceScheduleLimitToProxyJobs() + || (packageName == null || packageName.equals(servicePkg)))) { + // limit excessive schedule calls for persisted jobs. final String pkg = packageName == null ? servicePkg : packageName; if (!mQuotaTracker.isWithinQuota(userId, pkg, QUOTA_TRACKER_SCHEDULE_PERSISTED_TAG)) { if (mQuotaTracker.isWithinQuota(userId, pkg, QUOTA_TRACKER_SCHEDULE_LOGGED)) { @@ -1965,7 +1983,12 @@ public class JobSchedulerService extends com.android.server.SystemService jobStatus.getNumAppliedFlexibleConstraints(), jobStatus.getNumDroppedFlexibleConstraints(), jobStatus.getFilteredTraceTag(), - jobStatus.getFilteredDebugTags()); + jobStatus.getFilteredDebugTags(), + jobStatus.getNumAbandonedFailures(), + /* 0 is reserved for UNKNOWN_POLICY */ + jobStatus.getJob().getBackoffPolicy() + 1, + shouldUseAggressiveBackoff( + jobStatus.getNumAbandonedFailures(), jobStatus.getSourceUid())); // 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 @@ -2021,139 +2044,129 @@ public class JobSchedulerService extends com.android.server.SystemService } } - @JobScheduler.PendingJobReason - private int getPendingJobReason(int uid, String namespace, int jobId) { - int reason; + @NonNull + private int[] getPendingJobReasons(int uid, String namespace, int jobId) { + int[] reasons; // 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 (mPendingJobReasonsCache) { + SparseArray<int[]> jobIdToReasons = mPendingJobReasonsCache.get(uid, namespace); + if (jobIdToReasons != null) { + reasons = jobIdToReasons.get(jobId); + if (reasons != null) { + return reasons; } } } synchronized (mLock) { - reason = getPendingJobReasonLocked(uid, namespace, jobId); + reasons = getPendingJobReasonsLocked(uid, namespace, jobId); if (DEBUG) { - Slog.v(TAG, "getPendingJobReason(" - + uid + "," + namespace + "," + jobId + ")=" + reason); + Slog.v(TAG, "getPendingJobReasons(" + + uid + "," + namespace + "," + jobId + ")=" + Arrays.toString(reasons)); } } - synchronized (mPendingJobReasonCache) { - SparseIntArray jobIdToReason = mPendingJobReasonCache.get(uid, namespace); - if (jobIdToReason == null) { - jobIdToReason = new SparseIntArray(); - mPendingJobReasonCache.add(uid, namespace, jobIdToReason); + synchronized (mPendingJobReasonsCache) { + SparseArray<int[]> jobIdToReasons = mPendingJobReasonsCache.get(uid, namespace); + if (jobIdToReasons == null) { + jobIdToReasons = new SparseArray<>(); + mPendingJobReasonsCache.add(uid, namespace, jobIdToReasons); } - jobIdToReason.put(jobId, reason); + jobIdToReasons.put(jobId, reasons); } - return reason; + return reasons; } @VisibleForTesting @JobScheduler.PendingJobReason int getPendingJobReason(JobStatus job) { - return getPendingJobReason(job.getUid(), job.getNamespace(), job.getJobId()); + // keep original method to enable unit testing with flags + return getPendingJobReasons(job.getUid(), job.getNamespace(), job.getJobId())[0]; + } + + @VisibleForTesting + @NonNull + int[] getPendingJobReasons(JobStatus job) { + return getPendingJobReasons(job.getUid(), job.getNamespace(), job.getJobId()); } - @JobScheduler.PendingJobReason @GuardedBy("mLock") - private int getPendingJobReasonLocked(int uid, String namespace, int jobId) { + @NonNull + private int[] getPendingJobReasonsLocked(int uid, String namespace, int jobId) { // Very similar code to isReadyToBeExecutedLocked. - - JobStatus job = mJobs.getJobByUidAndJobId(uid, namespace, jobId); + final JobStatus job = mJobs.getJobByUidAndJobId(uid, namespace, jobId); if (job == null) { // Job doesn't exist. - return JobScheduler.PENDING_JOB_REASON_INVALID_JOB_ID; + return new int[] { JobScheduler.PENDING_JOB_REASON_INVALID_JOB_ID }; } - if (isCurrentlyRunningLocked(job)) { - return JobScheduler.PENDING_JOB_REASON_EXECUTING; + return new int[] { JobScheduler.PENDING_JOB_REASON_EXECUTING }; } + final String debugPrefix = "getPendingJobReasonsLocked: " + job.toShortString(); final boolean jobReady = job.isReady(); - if (DEBUG) { - Slog.v(TAG, "getPendingJobReasonLocked: " + job.toShortString() - + " ready=" + jobReady); + Slog.v(TAG, debugPrefix + " ready=" + jobReady); } - - if (!jobReady) { - return job.getPendingJobReason(); + final JobRestriction restriction = checkIfRestricted(job); + if (DEBUG) { + Slog.v(TAG, debugPrefix + " restriction=" + restriction); + } + if (!jobReady || restriction != null) { + return job.getPendingJobReasons(restriction); } final boolean userStarted = areUsersStartedLocked(job); - if (DEBUG) { - Slog.v(TAG, "getPendingJobReasonLocked: " + job.toShortString() - + " userStarted=" + userStarted); + Slog.v(TAG, debugPrefix + " userStarted=" + userStarted); } if (!userStarted) { - return JobScheduler.PENDING_JOB_REASON_USER; + return new int[] { JobScheduler.PENDING_JOB_REASON_USER }; } final boolean backingUp = mBackingUpUids.get(job.getSourceUid()); if (DEBUG) { - Slog.v(TAG, "getPendingJobReasonLocked: " + job.toShortString() - + " backingUp=" + backingUp); + Slog.v(TAG, debugPrefix + " 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(); + return new int[] { JobScheduler.PENDING_JOB_REASON_APP }; } - // 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! + // The following can be a little more expensive, so we are doing it 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); + Slog.v(TAG, debugPrefix + " 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; + // We haven't started the job - presumably, there are too many jobs running. + return new int[] { JobScheduler.PENDING_JOB_REASON_DEVICE_STATE }; } // 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); + Slog.v(TAG, debugPrefix + " componentUsable=" + componentUsable); } if (!componentUsable) { - return JobScheduler.PENDING_JOB_REASON_APP; + return new int[] { JobScheduler.PENDING_JOB_REASON_APP }; } - return JobScheduler.PENDING_JOB_REASON_UNDEFINED; + return new int[] { JobScheduler.PENDING_JOB_REASON_UNDEFINED }; + } + + @NonNull + private List<PendingJobReasonsInfo> getPendingJobReasonsHistory( + int uid, String namespace, int jobId) { + synchronized (mLock) { + final JobStatus job = mJobs.getJobByUidAndJobId(uid, namespace, jobId); + if (job == null) { + // Job doesn't exist. + throw new IllegalArgumentException("Invalid job id"); + } + + return job.getPendingJobReasonsHistory(); + } } private JobInfo getPendingJob(int uid, @Nullable String namespace, int jobId) { @@ -2195,15 +2208,16 @@ public class JobSchedulerService extends com.android.server.SystemService // 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( + synchronized (mPendingJobReasonsCache) { + SparseArray<int[]> jobIdToReason = mPendingJobReasonsCache.get( job.getUid(), job.getNamespace()); if (jobIdToReason == null) { - jobIdToReason = new SparseIntArray(); - mPendingJobReasonCache.add(job.getUid(), job.getNamespace(), + jobIdToReason = new SparseArray<>(); + mPendingJobReasonsCache.add(job.getUid(), job.getNamespace(), jobIdToReason); } - jobIdToReason.put(job.getJobId(), JobScheduler.PENDING_JOB_REASON_USER); + jobIdToReason.put(job.getJobId(), + new int[] { JobScheduler.PENDING_JOB_REASON_USER }); } } } @@ -2229,8 +2243,8 @@ public class JobSchedulerService extends com.android.server.SystemService synchronized (mLock) { mJobs.removeJobsOfUnlistedUsers(umi.getUserIds()); } - synchronized (mPendingJobReasonCache) { - mPendingJobReasonCache.clear(); + synchronized (mPendingJobReasonsCache) { + mPendingJobReasonsCache.clear(); } } @@ -2415,7 +2429,12 @@ public class JobSchedulerService extends com.android.server.SystemService cancelled.getNumAppliedFlexibleConstraints(), cancelled.getNumDroppedFlexibleConstraints(), cancelled.getFilteredTraceTag(), - cancelled.getFilteredDebugTags()); + cancelled.getFilteredDebugTags(), + cancelled.getNumAbandonedFailures(), + /* 0 is reserved for UNKNOWN_POLICY */ + cancelled.getJob().getBackoffPolicy() + 1, + shouldUseAggressiveBackoff( + cancelled.getNumAbandonedFailures(), cancelled.getSourceUid())); } // If this is a replacement, bring in the new version of the job if (incomingJob != null) { @@ -2875,7 +2894,7 @@ public class JobSchedulerService extends com.android.server.SystemService final boolean update = lastJob != null; mJobs.add(jobStatus); // Clear potentially cached INVALID_JOB_ID reason. - resetPendingJobReasonCache(jobStatus); + resetPendingJobReasonsCache(jobStatus); if (mReadyToRock) { for (int i = 0; i < mControllers.size(); i++) { StateController controller = mControllers.get(i); @@ -2897,9 +2916,9 @@ 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()); + synchronized (mPendingJobReasonsCache) { + SparseArray<int[]> reasonCache = + mPendingJobReasonsCache.get(jobStatus.getUid(), jobStatus.getNamespace()); if (reasonCache != null) { reasonCache.delete(jobStatus.getJobId()); } @@ -2927,11 +2946,11 @@ public class JobSchedulerService extends com.android.server.SystemService 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()); + /** Remove the pending job reasons for this job from the cache. */ + void resetPendingJobReasonsCache(@NonNull JobStatus jobStatus) { + synchronized (mPendingJobReasonsCache) { + final SparseArray<int[]> reasons = + mPendingJobReasonsCache.get(jobStatus.getUid(), jobStatus.getNamespace()); if (reasons != null) { reasons.delete(jobStatus.getJobId()); } @@ -3005,7 +3024,9 @@ public class JobSchedulerService extends com.android.server.SystemService final long initialBackoffMillis = job.getInitialBackoffMillis(); int numFailures = failureToReschedule.getNumFailures(); + int numAbandonedFailures = failureToReschedule.getNumAbandonedFailures(); int numSystemStops = failureToReschedule.getNumSystemStops(); + final int uid = failureToReschedule.getSourceUid(); // 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. @@ -3014,9 +3035,20 @@ public class JobSchedulerService extends com.android.server.SystemService || internalStopReason == JobParameters.INTERNAL_STOP_REASON_ANR || stopReason == JobParameters.STOP_REASON_USER) { numFailures++; + } else if (android.app.job.Flags.handleAbandonedJobs() + && !CompatChanges.isChangeEnabled(OVERRIDE_HANDLE_ABANDONED_JOBS, uid) + && internalStopReason == JobParameters.INTERNAL_STOP_REASON_TIMEOUT_ABANDONED) { + numAbandonedFailures++; + numFailures++; } else { numSystemStops++; } + + int backoffPolicy = job.getBackoffPolicy(); + if (shouldUseAggressiveBackoff(numAbandonedFailures, uid)) { + backoffPolicy = JobInfo.BACKOFF_POLICY_EXPONENTIAL; + } + final int backoffAttempts = numFailures + numSystemStops / mConstants.SYSTEM_STOP_TO_FAILURE_RATIO; final long earliestRuntimeMs; @@ -3025,7 +3057,7 @@ public class JobSchedulerService extends com.android.server.SystemService earliestRuntimeMs = JobStatus.NO_EARLIEST_RUNTIME; } else { long delayMillis; - switch (job.getBackoffPolicy()) { + switch (backoffPolicy) { case JobInfo.BACKOFF_POLICY_LINEAR: { long backoff = initialBackoffMillis; if (backoff < mConstants.MIN_LINEAR_BACKOFF_TIME_MS) { @@ -3054,7 +3086,7 @@ public class JobSchedulerService extends com.android.server.SystemService } JobStatus newJob = new JobStatus(failureToReschedule, earliestRuntimeMs, - JobStatus.NO_LATEST_RUNTIME, numFailures, numSystemStops, + JobStatus.NO_LATEST_RUNTIME, numFailures, numAbandonedFailures, numSystemStops, failureToReschedule.getLastSuccessfulRunTime(), sSystemClock.millis(), failureToReschedule.getCumulativeExecutionTimeMs()); if (stopReason == JobParameters.STOP_REASON_USER) { @@ -3077,6 +3109,21 @@ public class JobSchedulerService extends com.android.server.SystemService } /** + * Returns {@code true} if the given number of abandoned failures indicates that JobScheduler + * should use an aggressive backoff policy. + * + * @param numAbandonedFailures The number of abandoned failures. + * @return {@code true} if the given number of abandoned failures indicates that JobScheduler + * should use an aggressive backoff policy. + */ + public boolean shouldUseAggressiveBackoff(int numAbandonedFailures, int uid) { + return android.app.job.Flags.handleAbandonedJobs() + && !CompatChanges.isChangeEnabled(OVERRIDE_HANDLE_ABANDONED_JOBS, uid) + && numAbandonedFailures + > mConstants.ABANDONED_JOB_TIMEOUTS_BEFORE_AGGRESSIVE_BACKOFF; + } + + /** * Maximum time buffer in which JobScheduler will try to optimize periodic job scheduling. This * does not cause a job's period to be larger than requested (eg: if the requested period is * shorter than this buffer). This is used to put a limit on when JobScheduler will intervene @@ -3155,6 +3202,7 @@ public class JobSchedulerService extends com.android.server.SystemService return new JobStatus(periodicToReschedule, elapsedNow + period - flex, elapsedNow + period, 0 /* numFailures */, 0 /* numSystemStops */, + 0 /* numAbandonedFailures */, sSystemClock.millis() /* lastSuccessfulRunTime */, periodicToReschedule.getLastFailedRunTime(), 0 /* Reset cumulativeExecutionTime because of successful execution */); @@ -3171,6 +3219,7 @@ public class JobSchedulerService extends com.android.server.SystemService return new JobStatus(periodicToReschedule, newEarliestRunTimeElapsed, newLatestRuntimeElapsed, 0 /* numFailures */, 0 /* numSystemStops */, + 0 /* numAbandonedFailures */, sSystemClock.millis() /* lastSuccessfulRunTime */, periodicToReschedule.getLastFailedRunTime(), 0 /* Reset cumulativeExecutionTime because of successful execution */); @@ -3179,6 +3228,12 @@ public class JobSchedulerService extends com.android.server.SystemService @VisibleForTesting void maybeProcessBuggyJob(@NonNull JobStatus jobStatus, int debugStopReason) { boolean jobTimedOut = debugStopReason == JobParameters.INTERNAL_STOP_REASON_TIMEOUT; + if (android.app.job.Flags.handleAbandonedJobs() + && !CompatChanges.isChangeEnabled( + OVERRIDE_HANDLE_ABANDONED_JOBS, jobStatus.getSourceUid())) { + jobTimedOut |= (debugStopReason + == JobParameters.INTERNAL_STOP_REASON_TIMEOUT_ABANDONED); + } // If madeActive = 0, the job never actually started. if (!jobTimedOut && jobStatus.madeActive > 0) { final long executionDurationMs = sUptimeMillisClock.millis() - jobStatus.madeActive; @@ -3260,9 +3315,14 @@ public class JobSchedulerService extends com.android.server.SystemService // we stop it. final JobStatus rescheduledJob = needsReschedule ? getRescheduleJobForFailureLocked(jobStatus, stopReason, debugStopReason) : null; + final boolean isStopReasonAbandoned = android.app.job.Flags.handleAbandonedJobs() + && !CompatChanges.isChangeEnabled( + OVERRIDE_HANDLE_ABANDONED_JOBS, jobStatus.getSourceUid()) + && (debugStopReason == JobParameters.INTERNAL_STOP_REASON_TIMEOUT_ABANDONED); if (rescheduledJob != null && !rescheduledJob.shouldTreatAsUserInitiatedJob() && (debugStopReason == JobParameters.INTERNAL_STOP_REASON_TIMEOUT + || isStopReasonAbandoned || debugStopReason == JobParameters.INTERNAL_STOP_REASON_PREEMPT)) { rescheduledJob.disallowRunInBatterySaverAndDoze(); } @@ -3313,18 +3373,18 @@ 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(); + synchronized (mPendingJobReasonsCache) { + mPendingJobReasonsCache.clear(); } } else if (changedJobs.size() > 0) { synchronized (mLock) { mChangedJobList.addAll(changedJobs); } mHandler.obtainMessage(MSG_CHECK_CHANGED_JOB_LIST).sendToTarget(); - synchronized (mPendingJobReasonCache) { + synchronized (mPendingJobReasonsCache) { for (int i = changedJobs.size() - 1; i >= 0; --i) { final JobStatus job = changedJobs.valueAt(i); - resetPendingJobReasonCache(job); + resetPendingJobReasonsCache(job); } } } @@ -3893,23 +3953,21 @@ public class JobSchedulerService extends com.android.server.SystemService // Update the pending reason for any jobs that aren't going to be run. final int numRunnableJobs = runnableJobs.size(); if (numRunnableJobs > 0 && numRunnableJobs != jobsToRun.size()) { - synchronized (mPendingJobReasonCache) { + synchronized (mPendingJobReasonsCache) { for (int i = 0; i < numRunnableJobs; ++i) { final JobStatus job = runnableJobs.get(i); if (jobsToRun.contains(job)) { - // We're running this job. Skip updating the pending reason. - continue; + continue; // we're running this job - skip updating the pending reason. } - SparseIntArray reasons = - mPendingJobReasonCache.get(job.getUid(), job.getNamespace()); + SparseArray<int[]> reasons = + mPendingJobReasonsCache.get(job.getUid(), job.getNamespace()); if (reasons == null) { - reasons = new SparseIntArray(); - mPendingJobReasonCache.add(job.getUid(), job.getNamespace(), reasons); + reasons = new SparseArray<>(); + mPendingJobReasonsCache.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); + // we're force batching these jobs - note it as optimization. + reasons.put(job.getJobId(), new int[] + { JobScheduler.PENDING_JOB_REASON_JOB_SCHEDULER_OPTIMIZATION }); } } } @@ -5123,11 +5181,28 @@ public class JobSchedulerService extends com.android.server.SystemService @Override public int getPendingJobReason(String namespace, int jobId) throws RemoteException { + return getPendingJobReasons(validateNamespace(namespace), jobId)[0]; + } + + @Override + public int[] getPendingJobReasons(String namespace, int jobId) throws RemoteException { final int uid = Binder.getCallingUid(); + final long ident = Binder.clearCallingIdentity(); + try { + return JobSchedulerService.this.getPendingJobReasons( + uid, validateNamespace(namespace), jobId); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + @Override + public List<PendingJobReasonsInfo> getPendingJobReasonsHistory(String namespace, int jobId) + throws RemoteException { + final int uid = Binder.getCallingUid(); final long ident = Binder.clearCallingIdentity(); try { - return JobSchedulerService.this.getPendingJobReason( + return JobSchedulerService.this.getPendingJobReasonsHistory( uid, validateNamespace(namespace), jobId); } finally { Binder.restoreCallingIdentity(ident); @@ -5861,8 +5936,14 @@ public class JobSchedulerService extends com.android.server.SystemService pw.print(Flags.FLAG_DO_NOT_FORCE_RUSH_EXECUTION_AT_BOOT, Flags.doNotForceRushExecutionAtBoot()); pw.println(); - pw.print(android.app.job.Flags.FLAG_BACKUP_JOBS_EXEMPTION, - android.app.job.Flags.backupJobsExemption()); + pw.print(android.app.job.Flags.FLAG_IGNORE_IMPORTANT_WHILE_FOREGROUND, + android.app.job.Flags.ignoreImportantWhileForeground()); + pw.println(); + pw.print(android.app.job.Flags.FLAG_GET_PENDING_JOB_REASONS_API, + android.app.job.Flags.getPendingJobReasonsApi()); + pw.println(); + pw.print(android.app.job.Flags.FLAG_GET_PENDING_JOB_REASONS_HISTORY_API, + android.app.job.Flags.getPendingJobReasonsHistoryApi()); pw.println(); pw.decreaseIndent(); pw.println(); 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 ac240ccff017..42c8250a6185 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java @@ -433,8 +433,14 @@ public final class JobSchedulerShellCommand extends BasicShellCommandHandler { case com.android.server.job.Flags.FLAG_DO_NOT_FORCE_RUSH_EXECUTION_AT_BOOT: pw.println(com.android.server.job.Flags.doNotForceRushExecutionAtBoot()); break; - case android.app.job.Flags.FLAG_BACKUP_JOBS_EXEMPTION: - pw.println(android.app.job.Flags.backupJobsExemption()); + case android.app.job.Flags.FLAG_IGNORE_IMPORTANT_WHILE_FOREGROUND: + pw.println(android.app.job.Flags.ignoreImportantWhileForeground()); + break; + case android.app.job.Flags.FLAG_GET_PENDING_JOB_REASONS_API: + pw.println(android.app.job.Flags.getPendingJobReasonsApi()); + break; + case android.app.job.Flags.FLAG_GET_PENDING_JOB_REASONS_HISTORY_API: + pw.println(android.app.job.Flags.getPendingJobReasonsHistoryApi()); break; default: pw.println("Unknown flag: " + flagName); 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 ee246d84997f..ebfda527001d 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java @@ -16,6 +16,8 @@ package com.android.server.job; +import static android.app.job.JobParameters.OVERRIDE_HANDLE_ABANDONED_JOBS; + 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; @@ -129,8 +131,8 @@ public final class JobServiceContext implements ServiceConnection { private static final String[] VERB_STRINGS = { "VERB_BINDING", "VERB_STARTING", "VERB_EXECUTING", "VERB_STOPPING", "VERB_FINISHED" }; - private static final String TRACE_JOB_FORCE_FINISHED_PREFIX = "forceJobFinished:"; - private static final String TRACE_JOB_FORCE_FINISHED_DELIMITER = "#"; + private static final String TRACE_ABANDONED_JOB = "abandonedJob:"; + private static final String TRACE_ABANDONED_JOB_DELIMITER = "#"; // States that a job occupies while interacting with the client. static final int VERB_BINDING = 0; @@ -294,8 +296,8 @@ public final class JobServiceContext implements ServiceConnection { } @Override - public void forceJobFinished(int jobId) { - doForceJobFinished(this, jobId); + public void handleAbandonedJob(int jobId) { + doHandleAbandonedJob(this, jobId); } @Override @@ -546,7 +548,12 @@ public final class JobServiceContext implements ServiceConnection { job.getNumAppliedFlexibleConstraints(), job.getNumDroppedFlexibleConstraints(), job.getFilteredTraceTag(), - job.getFilteredDebugTags()); + job.getFilteredDebugTags(), + job.getNumAbandonedFailures(), + /* 0 is reserved for UNKNOWN_POLICY */ + job.getJob().getBackoffPolicy() + 1, + mService.shouldUseAggressiveBackoff( + job.getNumAbandonedFailures(), job.getSourceUid())); sEnqueuedJwiAtJobStart.logSampleWithUid(job.getUid(), job.getWorkCount()); final String sourcePackage = job.getSourcePackageName(); if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) { @@ -618,6 +625,26 @@ public final class JobServiceContext implements ServiceConnection { return mRunningJob; } + @VisibleForTesting + void setRunningJobLockedForTest(JobStatus job) { + mRunningJob = job; + } + + @VisibleForTesting + void setJobParamsLockedForTest(JobParameters params) { + mParams = params; + } + + @VisibleForTesting + void setRunningCallbackLockedForTest(JobCallback callback) { + mRunningCallback = callback; + } + + @VisibleForTesting + void setPendingStopReasonLockedForTest(int stopReason) { + mPendingStopReason = stopReason; + } + @JobConcurrencyManager.WorkType int getRunningJobWorkType() { return mRunningJobWorkType; @@ -773,7 +800,7 @@ public final class JobServiceContext implements ServiceConnection { * This method just adds traces to evaluate jobs that leak jobparameters at the client. * It does not stop the job. */ - void doForceJobFinished(JobCallback cb, int jobId) { + void doHandleAbandonedJob(JobCallback cb, int jobId) { final long ident = Binder.clearCallingIdentity(); try { final JobStatus executing; @@ -786,10 +813,11 @@ public final class JobServiceContext implements ServiceConnection { executing = getRunningJobLocked(); } if (executing != null && jobId == executing.getJobId()) { + executing.setAbandoned(true); final StringBuilder stateSuffix = new StringBuilder(); - stateSuffix.append(TRACE_JOB_FORCE_FINISHED_PREFIX); + stateSuffix.append(TRACE_ABANDONED_JOB); stateSuffix.append(executing.getBatteryName()); - stateSuffix.append(TRACE_JOB_FORCE_FINISHED_DELIMITER); + stateSuffix.append(TRACE_ABANDONED_JOB_DELIMITER); stateSuffix.append(executing.getJobId()); Trace.instant(Trace.TRACE_TAG_POWER, stateSuffix.toString()); } @@ -1364,8 +1392,9 @@ public final class JobServiceContext implements ServiceConnection { } /** Process MSG_TIMEOUT here. */ + @VisibleForTesting @GuardedBy("mLock") - private void handleOpTimeoutLocked() { + void handleOpTimeoutLocked() { switch (mVerb) { case VERB_BINDING: // The system may have been too busy. Don't drop the job or trigger an ANR. @@ -1427,9 +1456,28 @@ public final class JobServiceContext implements ServiceConnection { // Not an error - client ran out of time. Slog.i(TAG, "Client timed out while executing (no jobFinished received)." + " Sending onStop: " + getRunningJobNameLocked()); - mParams.setStopReason(JobParameters.STOP_REASON_TIMEOUT, - JobParameters.INTERNAL_STOP_REASON_TIMEOUT, "client timed out"); - sendStopMessageLocked("timeout while executing"); + + final JobStatus executing = getRunningJobLocked(); + int stopReason = JobParameters.STOP_REASON_TIMEOUT; + int internalStopReason = JobParameters.INTERNAL_STOP_REASON_TIMEOUT; + final StringBuilder stopMessage = new StringBuilder("timeout while executing"); + final StringBuilder debugStopReason = new StringBuilder("client timed out"); + + if (android.app.job.Flags.handleAbandonedJobs() + && executing != null + && !CompatChanges.isChangeEnabled( + OVERRIDE_HANDLE_ABANDONED_JOBS, executing.getSourceUid()) + && executing.isAbandoned()) { + final String abandonedMessage = " and maybe abandoned"; + stopReason = JobParameters.STOP_REASON_TIMEOUT_ABANDONED; + internalStopReason = JobParameters.INTERNAL_STOP_REASON_TIMEOUT_ABANDONED; + stopMessage.append(abandonedMessage); + debugStopReason.append(abandonedMessage); + } + + mParams.setStopReason(stopReason, + internalStopReason, debugStopReason.toString()); + sendStopMessageLocked(stopMessage.toString()); } else if (nowElapsed >= earliestStopTimeElapsed) { // We've given the app the minimum execution time. See if we should stop it or // let it continue running @@ -1479,8 +1527,9 @@ public final class JobServiceContext implements ServiceConnection { * Already running, need to stop. Will switch {@link #mVerb} from VERB_EXECUTING -> * VERB_STOPPING. */ + @VisibleForTesting @GuardedBy("mLock") - private void sendStopMessageLocked(@Nullable String reason) { + void sendStopMessageLocked(@Nullable String reason) { removeOpTimeOutLocked(); if (mVerb != VERB_EXECUTING) { Slog.e(TAG, "Sending onStopJob for a job that isn't started. " + mRunningJob); @@ -1642,7 +1691,12 @@ public final class JobServiceContext implements ServiceConnection { completedJob.getNumAppliedFlexibleConstraints(), completedJob.getNumDroppedFlexibleConstraints(), completedJob.getFilteredTraceTag(), - completedJob.getFilteredDebugTags()); + completedJob.getFilteredDebugTags(), + completedJob.getNumAbandonedFailures(), + /* 0 is reserved for UNKNOWN_POLICY */ + completedJob.getJob().getBackoffPolicy() + 1, + mService.shouldUseAggressiveBackoff( + completedJob.getNumAbandonedFailures(), completedJob.getSourceUid())); if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) { Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_SYSTEM_SERVER, JobSchedulerService.TRACE_TRACK_NAME, getId()); diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java index d8934d8f83b8..dfb36818c818 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java @@ -269,7 +269,9 @@ public final class JobStore { convertRtcBoundsToElapsed(utcTimes, elapsedNow); JobStatus newJob = new JobStatus(job, elapsedRuntimes.first, elapsedRuntimes.second, - 0, 0, job.getLastSuccessfulRunTime(), job.getLastFailedRunTime(), + 0 /* numFailures */, 0 /* numAbandonedFailures */, + 0 /* numSystemStops */, job.getLastSuccessfulRunTime(), + job.getLastFailedRunTime(), job.getCumulativeExecutionTimeMs()); newJob.prepareLocked(); toAdd.add(newJob); 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 d5c9ae615486..abec170f3b7d 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 @@ -210,7 +210,9 @@ public final class DeviceIdleJobsController extends StateController { } private boolean updateTaskStateLocked(JobStatus task, final long nowElapsed) { - final boolean allowInIdle = ((task.getFlags()&JobInfo.FLAG_IMPORTANT_WHILE_FOREGROUND) != 0) + final boolean allowInIdle = + (!android.app.job.Flags.ignoreImportantWhileForeground() + && ((task.getFlags() & JobInfo.FLAG_IMPORTANT_WHILE_FOREGROUND) != 0)) && (mForegroundUids.get(task.getSourceUid()) || isTempWhitelistedLocked(task)); final boolean whitelisted = isWhitelistedLocked(task); final boolean enableTask = !mDeviceIdleMode || whitelisted || allowInIdle; @@ -219,7 +221,8 @@ public final class DeviceIdleJobsController extends StateController { @Override public void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob) { - if ((jobStatus.getFlags()&JobInfo.FLAG_IMPORTANT_WHILE_FOREGROUND) != 0) { + if (!android.app.job.Flags.ignoreImportantWhileForeground() + && (jobStatus.getFlags() & JobInfo.FLAG_IMPORTANT_WHILE_FOREGROUND) != 0) { mAllowInIdleJobs.add(jobStatus); } updateTaskStateLocked(jobStatus, sElapsedRealtimeClock.millis()); @@ -227,7 +230,8 @@ public final class DeviceIdleJobsController extends StateController { @Override public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob) { - if ((jobStatus.getFlags()&JobInfo.FLAG_IMPORTANT_WHILE_FOREGROUND) != 0) { + if (!android.app.job.Flags.ignoreImportantWhileForeground() + && (jobStatus.getFlags() & JobInfo.FLAG_IMPORTANT_WHILE_FOREGROUND) != 0) { mAllowInIdleJobs.remove(jobStatus); } } 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 e3af1d894762..5a33aa0ab7eb 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 @@ -32,6 +32,7 @@ import android.app.job.JobInfo; import android.app.job.JobParameters; import android.app.job.JobScheduler; import android.app.job.JobWorkItem; +import android.app.job.PendingJobReasonsInfo; import android.app.job.UserVisibleJobSummary; import android.content.ClipData; import android.content.ComponentName; @@ -39,6 +40,7 @@ import android.net.Network; import android.net.NetworkRequest; import android.net.Uri; import android.os.RemoteException; +import android.os.SystemClock; import android.os.UserHandle; import android.provider.MediaStore; import android.text.format.DateFormat; @@ -64,6 +66,7 @@ import com.android.server.job.JobSchedulerService; import com.android.server.job.JobServerProtoEnums; import com.android.server.job.JobStatusDumpProto; import com.android.server.job.JobStatusShortInfoProto; +import com.android.server.job.restrictions.JobRestriction; import dalvik.annotation.optimization.NeverCompile; @@ -72,6 +75,7 @@ import java.security.MessageDigest; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.List; import java.util.Objects; import java.util.Random; import java.util.function.Predicate; @@ -312,6 +316,12 @@ public final class JobStatus { private final int numFailures; /** + * How many times this job has stopped due to {@link + * JobParameters#STOP_REASON_TIMEOUT_ABANDONED}. + */ + private final int mNumAbandonedFailures; + + /** * The number of times JobScheduler has forced this job to stop due to reasons mostly outside * of the app's control. */ @@ -515,6 +525,10 @@ public final class JobStatus { private final long[] mConstraintUpdatedTimesElapsed = new long[NUM_CONSTRAINT_CHANGE_HISTORY]; private final int[] mConstraintStatusHistory = new int[NUM_CONSTRAINT_CHANGE_HISTORY]; + private final List<PendingJobReasonsInfo> mPendingJobReasonsHistory = new ArrayList<>(); + private static final int PENDING_JOB_HISTORY_RETURN_LIMIT = 10; + private static final int PENDING_JOB_HISTORY_TRIM_THRESHOLD = 25; + /** * For use only by ContentObserverController: state it is maintaining about content URIs * being observed. @@ -576,6 +590,13 @@ public final class JobStatus { private String mSystemTraceTag; /** + * Job maybe abandoned by not calling + * {@link android.app.job.JobService#jobFinished(JobParameters, boolean)} while + * the strong reference to {@link android.app.job.JobParameters} is lost + */ + private boolean mIsAbandoned; + + /** * Core constructor for JobStatus instances. All other ctors funnel down to this one. * * @param job The actual requested parameters for the job @@ -590,6 +611,8 @@ public final class JobStatus { * @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 mNumAbandonedFailures Count of how many times this job has requested a reschedule + * because it was abandoned. * @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 @@ -602,7 +625,7 @@ public final class JobStatus { */ private JobStatus(JobInfo job, int callingUid, String sourcePackageName, int sourceUserId, int standbyBucket, @Nullable String namespace, String tag, - int numFailures, int numSystemStops, + int numFailures, int mNumAbandonedFailures, int numSystemStops, long earliestRunTimeElapsedMillis, long latestRunTimeElapsedMillis, long lastSuccessfulRunTime, long lastFailedRunTime, long cumulativeExecutionTimeMs, int internalFlags, @@ -662,6 +685,7 @@ public final class JobStatus { this.latestRunTimeElapsedMillis = latestRunTimeElapsedMillis; this.mOriginalLatestRunTimeElapsedMillis = latestRunTimeElapsedMillis; this.numFailures = numFailures; + this.mNumAbandonedFailures = mNumAbandonedFailures; mNumSystemStops = numSystemStops; int requiredConstraints = job.getConstraintFlags(); @@ -725,6 +749,8 @@ public final class JobStatus { updateNetworkBytesLocked(); updateMediaBackupExemptionStatus(); + + mIsAbandoned = false; } /** Copy constructor: used specifically when cloning JobStatus objects for persistence, @@ -733,7 +759,8 @@ public final class JobStatus { this(jobStatus.getJob(), jobStatus.getUid(), jobStatus.getSourcePackageName(), jobStatus.getSourceUserId(), jobStatus.getStandbyBucket(), jobStatus.getNamespace(), - jobStatus.getSourceTag(), jobStatus.getNumFailures(), jobStatus.getNumSystemStops(), + jobStatus.getSourceTag(), jobStatus.getNumFailures(), + jobStatus.getNumAbandonedFailures(), jobStatus.getNumSystemStops(), jobStatus.getEarliestRunTime(), jobStatus.getLatestRunTimeElapsed(), jobStatus.getLastSuccessfulRunTime(), jobStatus.getLastFailedRunTime(), jobStatus.getCumulativeExecutionTimeMs(), @@ -770,6 +797,7 @@ public final class JobStatus { this(job, callingUid, sourcePkgName, sourceUserId, standbyBucket, namespace, sourceTag, /* numFailures */ 0, /* numSystemStops */ 0, + /* mNumAbandonedFailures */ 0, earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis, lastSuccessfulRunTime, lastFailedRunTime, cumulativeExecutionTimeMs, innerFlags, dynamicConstraints); @@ -789,13 +817,15 @@ public final class JobStatus { /** Create a new job to be rescheduled with the provided parameters. */ public JobStatus(JobStatus rescheduling, long newEarliestRuntimeElapsedMillis, - long newLatestRuntimeElapsedMillis, int numFailures, int numSystemStops, + long newLatestRuntimeElapsedMillis, int numFailures, + int mNumAbandonedFailures, int numSystemStops, long lastSuccessfulRunTime, long lastFailedRunTime, long cumulativeExecutionTimeMs) { this(rescheduling.job, rescheduling.getUid(), rescheduling.getSourcePackageName(), rescheduling.getSourceUserId(), rescheduling.getStandbyBucket(), rescheduling.getNamespace(), - rescheduling.getSourceTag(), numFailures, numSystemStops, + rescheduling.getSourceTag(), numFailures, + mNumAbandonedFailures, numSystemStops, newEarliestRuntimeElapsedMillis, newLatestRuntimeElapsedMillis, lastSuccessfulRunTime, lastFailedRunTime, cumulativeExecutionTimeMs, @@ -834,7 +864,8 @@ public final class JobStatus { int standbyBucket = JobSchedulerService.standbyBucketForPackage(jobPackage, sourceUserId, elapsedNow); return new JobStatus(job, callingUid, sourcePkg, sourceUserId, - standbyBucket, namespace, tag, /* numFailures */ 0, /* numSystemStops */ 0, + standbyBucket, namespace, tag, /* numFailures */ 0, + /* mNumAbandonedFailures */ 0, /* numSystemStops */ 0, earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis, 0 /* lastSuccessfulRunTime */, 0 /* lastFailedRunTime */, /* cumulativeExecutionTime */ 0, @@ -1061,6 +1092,16 @@ public final class JobStatus { return job.getTraceTag(); } + /** Returns if the job maybe abandoned */ + public boolean isAbandoned() { + return mIsAbandoned; + } + + /** Set the job maybe abandoned state*/ + public void setAbandoned(boolean abandoned) { + mIsAbandoned = abandoned; + } + /** Returns a trace tag using debug information provided by job scheduler service. */ @NonNull public String computeSystemTraceTag() { @@ -1119,6 +1160,13 @@ public final class JobStatus { } /** + * Returns the number of times the job stopped previously for STOP_REASON_TIMEOUT_ABANDONED. + */ + public int getNumAbandonedFailures() { + return mNumAbandonedFailures; + } + + /** * Returns the number of times the system stopped a previous execution of this job for reasons * that were likely outside the app's control. */ @@ -1973,6 +2021,16 @@ public final class JobStatus { mReasonReadyToUnready = JobParameters.STOP_REASON_UNDEFINED; } + final int unsatisfiedConstraints = ~satisfiedConstraints + & (requiredConstraints | mDynamicConstraints | IMPLICIT_CONSTRAINTS); + populatePendingJobReasonsHistoryMap(isReady, nowElapsed, unsatisfiedConstraints); + final int historySize = mPendingJobReasonsHistory.size(); + if (historySize >= PENDING_JOB_HISTORY_TRIM_THRESHOLD) { + // Ensure trimming doesn't occur too often - max history we currently return is 10 + mPendingJobReasonsHistory.subList(0, historySize - PENDING_JOB_HISTORY_RETURN_LIMIT) + .clear(); + } + return true; } @@ -2047,15 +2105,10 @@ public final class JobStatus { } } - /** - * 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); + @NonNull + public ArrayList<Integer> constraintsToPendingJobReasons(int unsatisfiedConstraints) { + final ArrayList<Integer> reasons = new ArrayList<>(); + 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 @@ -2065,78 +2118,169 @@ public final class JobStatus { // (they'll always get BACKGROUND_RESTRICTION) as the reason, regardless of // battery saver state. if (mIsUserBgRestricted) { - return JobScheduler.PENDING_JOB_REASON_BACKGROUND_RESTRICTION; + reasons.addLast(JobScheduler.PENDING_JOB_REASON_BACKGROUND_RESTRICTION); + } else { + reasons.addLast(JobScheduler.PENDING_JOB_REASON_DEVICE_STATE); } - return JobScheduler.PENDING_JOB_REASON_DEVICE_STATE; } + if ((CONSTRAINT_DEVICE_NOT_DOZING & unsatisfiedConstraints) != 0) { + if (!reasons.contains(JobScheduler.PENDING_JOB_REASON_DEVICE_STATE)) { + reasons.addLast(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; + reasons.addLast(JobScheduler.PENDING_JOB_REASON_CONSTRAINT_BATTERY_NOT_LOW); + } else { + // 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 + reasons.addLast(JobScheduler.PENDING_JOB_REASON_APP_STANDBY); } - // 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; + reasons.addLast(JobScheduler.PENDING_JOB_REASON_CONSTRAINT_CHARGING); + } else { + // 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 + if (!reasons.contains(JobScheduler.PENDING_JOB_REASON_APP_STANDBY)) { + reasons.addLast(JobScheduler.PENDING_JOB_REASON_APP_STANDBY); + } } - // 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; + reasons.addLast(JobScheduler.PENDING_JOB_REASON_CONSTRAINT_DEVICE_IDLE); + } else { + // 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 + if (!reasons.contains(JobScheduler.PENDING_JOB_REASON_APP_STANDBY)) { + reasons.addLast(JobScheduler.PENDING_JOB_REASON_APP_STANDBY); + } } - // 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) { + reasons.addLast(JobScheduler.PENDING_JOB_REASON_CONSTRAINT_CONNECTIVITY); + } + if ((CONSTRAINT_CONTENT_TRIGGER & unsatisfiedConstraints) != 0) { + reasons.addLast(JobScheduler.PENDING_JOB_REASON_CONSTRAINT_CONTENT_TRIGGER); + } + if ((CONSTRAINT_FLEXIBLE & unsatisfiedConstraints) != 0) { + reasons.addLast(JobScheduler.PENDING_JOB_REASON_JOB_SCHEDULER_OPTIMIZATION); } if ((CONSTRAINT_PREFETCH & unsatisfiedConstraints) != 0) { - return JobScheduler.PENDING_JOB_REASON_CONSTRAINT_PREFETCH; + reasons.addLast(JobScheduler.PENDING_JOB_REASON_CONSTRAINT_PREFETCH); } if ((CONSTRAINT_STORAGE_NOT_LOW & unsatisfiedConstraints) != 0) { - return JobScheduler.PENDING_JOB_REASON_CONSTRAINT_STORAGE_NOT_LOW; + reasons.addLast(JobScheduler.PENDING_JOB_REASON_CONSTRAINT_STORAGE_NOT_LOW); } if ((CONSTRAINT_TIMING_DELAY & unsatisfiedConstraints) != 0) { - return JobScheduler.PENDING_JOB_REASON_CONSTRAINT_MINIMUM_LATENCY; + reasons.addLast(JobScheduler.PENDING_JOB_REASON_CONSTRAINT_MINIMUM_LATENCY); } if ((CONSTRAINT_WITHIN_QUOTA & unsatisfiedConstraints) != 0) { - return JobScheduler.PENDING_JOB_REASON_QUOTA; + reasons.addLast(JobScheduler.PENDING_JOB_REASON_QUOTA); + } + if (android.app.job.Flags.getPendingJobReasonsApi()) { + if ((CONSTRAINT_DEADLINE & unsatisfiedConstraints) != 0) { + reasons.addLast(JobScheduler.PENDING_JOB_REASON_CONSTRAINT_DEADLINE); + } + } + + return reasons; + } + + /** + * This will return all potential reasons why the job is pending. + */ + @NonNull + public int[] getPendingJobReasons(@Nullable JobRestriction restriction) { + final int unsatisfiedConstraints = ~satisfiedConstraints + & (requiredConstraints | mDynamicConstraints | IMPLICIT_CONSTRAINTS); + final ArrayList<Integer> reasons = constraintsToPendingJobReasons(unsatisfiedConstraints); + + if (restriction != null) { + // Currently only ThermalStatusRestriction extends the JobRestriction class and + // returns PENDING_JOB_REASON_DEVICE_STATE if the job is restricted because of thermal. + @JobScheduler.PendingJobReason final int reason = restriction.getPendingReason(); + if (!reasons.contains(reason)) { + reasons.addLast(reason); + } } - 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 (reasons.isEmpty()) { + if (getEffectiveStandbyBucket() == NEVER_INDEX) { + Slog.wtf(TAG, "App in NEVER bucket querying pending job reason"); + // The user hasn't officially launched this app. + reasons.add(JobScheduler.PENDING_JOB_REASON_USER); + } else if (serviceProcessName != null) { + reasons.add(JobScheduler.PENDING_JOB_REASON_APP); + } else { + reasons.add(JobScheduler.PENDING_JOB_REASON_UNDEFINED); + } } - if (serviceProcessName != null) { - return JobScheduler.PENDING_JOB_REASON_APP; + + final int[] reasonsArr = new int[reasons.size()]; + for (int i = 0; i < reasonsArr.length; i++) { + reasonsArr[i] = reasons.get(i); } + return reasonsArr; + } + + private void populatePendingJobReasonsHistoryMap(boolean isReady, + long constraintTimestamp, int unsatisfiedConstraints) { + final long constraintTimestampEpoch = // system_boot_time + constraint_satisfied_time + (System.currentTimeMillis() - SystemClock.elapsedRealtime()) + constraintTimestamp; - if (!isReady()) { - Slog.wtf(TAG, "Unknown reason job isn't ready"); + if (isReady) { + // Job is ready to execute. At this point, if the job doesn't execute, it might be + // because of the app itself; if not, note it as undefined (documented in javadoc). + mPendingJobReasonsHistory.addLast( + new PendingJobReasonsInfo( + constraintTimestampEpoch, + new int[] { serviceProcessName != null + ? JobScheduler.PENDING_JOB_REASON_APP + : JobScheduler.PENDING_JOB_REASON_UNDEFINED })); + return; + } + + final ArrayList<Integer> reasons = constraintsToPendingJobReasons(unsatisfiedConstraints); + if (reasons.isEmpty()) { + // If the job is not waiting on any constraints to be met, note it as undefined. + reasons.add(JobScheduler.PENDING_JOB_REASON_UNDEFINED); + } + + final int[] reasonsArr = new int[reasons.size()]; + for (int i = 0; i < reasonsArr.length; i++) { + reasonsArr[i] = reasons.get(i); } - return JobScheduler.PENDING_JOB_REASON_UNDEFINED; + mPendingJobReasonsHistory.addLast( + new PendingJobReasonsInfo(constraintTimestampEpoch, reasonsArr)); + } + + /** + * Returns the last {@link #PENDING_JOB_HISTORY_RETURN_LIMIT} constraint changes. + */ + @NonNull + public List<PendingJobReasonsInfo> getPendingJobReasonsHistory() { + final List<PendingJobReasonsInfo> returnList = + new ArrayList<>(PENDING_JOB_HISTORY_RETURN_LIMIT); + final int historySize = mPendingJobReasonsHistory.size(); + if (historySize != 0) { + returnList.addAll( + mPendingJobReasonsHistory.subList( + Math.max(0, historySize - PENDING_JOB_HISTORY_RETURN_LIMIT), + historySize)); + } + + return returnList; } /** @return whether or not the @param constraint is satisfied */ 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 a1c72fb4c06c..637c726a9bd1 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 @@ -36,10 +36,14 @@ import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.AlarmManager; import android.app.UidObserver; +import android.app.compat.CompatChanges; import android.app.job.JobInfo; import android.app.usage.UsageEvents; import android.app.usage.UsageStatsManagerInternal; import android.app.usage.UsageStatsManagerInternal.UsageEventListener; +import android.compat.annotation.ChangeId; +import android.compat.annotation.Disabled; +import android.compat.annotation.Overridable; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; @@ -99,10 +103,10 @@ import java.util.function.Predicate; * the number of jobs or sessions that can run within the window. Regardless of bucket, apps will * not be allowed to run more than 20 jobs within the past 10 minutes. * - * Jobs are throttled while an app is not in a foreground state. All jobs are allowed to run - * freely when an app enters the foreground state and are restricted when the app leaves the - * foreground state. However, jobs that are started while the app is in the TOP state do not count - * towards any quota and are not restricted regardless of the app's state change. + * Jobs are throttled while an app is not in a TOP or BOUND_TOP state. All jobs are allowed to run + * freely when an app enters the TOP or BOUND_TOP state and are restricted when the app leaves those + * states. However, jobs that are started while the app is in the TOP state do not count towards any + * quota and are not restricted regardless of the app's state change. * * Jobs will not be throttled when the device is charging. The device is considered to be charging * once the {@link BatteryManager#ACTION_CHARGING} intent has been broadcast. @@ -121,6 +125,9 @@ public final class QuotaController extends StateController { private static final String ALARM_TAG_CLEANUP = "*job.cleanup*"; private static final String ALARM_TAG_QUOTA_CHECK = "*job.quota_check*"; + private static final String TRACE_QUOTA_STATE_CHANGED_TAG = "QuotaStateChanged:"; + private static final String TRACE_QUOTA_STATE_CHANGED_DELIMITER = "#"; + private static final int SYSTEM_APP_CHECK_FLAGS = PackageManager.MATCH_DIRECT_BOOT_AWARE | PackageManager.MATCH_DIRECT_BOOT_UNAWARE | PackageManager.GET_PERMISSIONS | PackageManager.MATCH_KNOWN_PACKAGES; @@ -129,6 +136,27 @@ public final class QuotaController extends StateController { return (int) (val ^ (val >>> 32)); } + /** + * When enabled this change id overrides the default quota policy enforcement to the jobs + * running in the foreground process state. + */ + // TODO: b/379681266 - Might need some refactoring for a better app-compat strategy. + @VisibleForTesting + @ChangeId + @Disabled // Disabled by default + @Overridable // The change can be overridden in user build + static final long OVERRIDE_QUOTA_ENFORCEMENT_TO_FGS_JOBS = 341201311L; + + /** + * When enabled this change id overrides the default quota policy enforcement policy + * the jobs started when app was in the TOP state. + */ + @VisibleForTesting + @ChangeId + @Disabled // Disabled by default + @Overridable // The change can be overridden in user build. + static final long OVERRIDE_QUOTA_ENFORCEMENT_TO_TOP_STARTED_JOBS = 374323858L; + @VisibleForTesting static class ExecutionStats { /** @@ -394,13 +422,13 @@ public final class QuotaController extends StateController { * minutes to run its jobs. */ private final long[] mBucketPeriodsMs = new long[]{ - QcConstants.DEFAULT_WINDOW_SIZE_ACTIVE_MS, - QcConstants.DEFAULT_WINDOW_SIZE_WORKING_MS, - QcConstants.DEFAULT_WINDOW_SIZE_FREQUENT_MS, + QcConstants.DEFAULT_LEGACY_WINDOW_SIZE_ACTIVE_MS, + QcConstants.DEFAULT_LEGACY_WINDOW_SIZE_WORKING_MS, + QcConstants.DEFAULT_LEGACY_WINDOW_SIZE_FREQUENT_MS, QcConstants.DEFAULT_WINDOW_SIZE_RARE_MS, 0, // NEVER QcConstants.DEFAULT_WINDOW_SIZE_RESTRICTED_MS, - QcConstants.DEFAULT_WINDOW_SIZE_EXEMPTED_MS + QcConstants.DEFAULT_LEGACY_WINDOW_SIZE_EXEMPTED_MS }; /** The maximum period any bucket can have. */ @@ -454,7 +482,7 @@ public final class QuotaController extends StateController { */ private final long[] mEJLimitsMs = new long[]{ QcConstants.DEFAULT_EJ_LIMIT_ACTIVE_MS, - QcConstants.DEFAULT_EJ_LIMIT_WORKING_MS, + QcConstants.DEFAULT_LEGACY_EJ_LIMIT_WORKING_MS, QcConstants.DEFAULT_EJ_LIMIT_FREQUENT_MS, QcConstants.DEFAULT_EJ_LIMIT_RARE_MS, 0, // NEVER @@ -476,7 +504,8 @@ public final class QuotaController extends StateController { /** * Length of time used to split an app's top time into chunks. */ - private long mEJTopAppTimeChunkSizeMs = QcConstants.DEFAULT_EJ_TOP_APP_TIME_CHUNK_SIZE_MS; + private long mEJTopAppTimeChunkSizeMs = + QcConstants.DEFAULT_LEGACY_EJ_TOP_APP_TIME_CHUNK_SIZE_MS; /** * How much EJ quota to give back to an app based on the number of top app time chunks it had. @@ -486,7 +515,7 @@ public final class QuotaController extends StateController { /** * How much EJ quota to give back to an app based on each non-top user interaction. */ - private long mEJRewardInteractionMs = QcConstants.DEFAULT_EJ_REWARD_INTERACTION_MS; + private long mEJRewardInteractionMs = QcConstants.DEFAULT_LEGACY_EJ_REWARD_INTERACTION_MS; /** * How much EJ quota to give back to an app based on each notification seen event. @@ -498,14 +527,6 @@ public final class QuotaController extends StateController { private long mEJGracePeriodTopAppMs = QcConstants.DEFAULT_EJ_GRACE_PERIOD_TOP_APP_MS; - private long mQuotaBumpAdditionalDurationMs = - QcConstants.DEFAULT_QUOTA_BUMP_ADDITIONAL_DURATION_MS; - private int mQuotaBumpAdditionalJobCount = QcConstants.DEFAULT_QUOTA_BUMP_ADDITIONAL_JOB_COUNT; - private int mQuotaBumpAdditionalSessionCount = - QcConstants.DEFAULT_QUOTA_BUMP_ADDITIONAL_SESSION_COUNT; - private long mQuotaBumpWindowSizeMs = QcConstants.DEFAULT_QUOTA_BUMP_WINDOW_SIZE_MS; - private int mQuotaBumpLimit = QcConstants.DEFAULT_QUOTA_BUMP_LIMIT; - /** * List of system apps with the {@link android.Manifest.permission#INSTALL_PACKAGES} permission * granted for each user. @@ -567,12 +588,19 @@ public final class QuotaController extends StateController { ActivityManager.getService().registerUidObserver(new QcUidObserver(), ActivityManager.UID_OBSERVER_PROCSTATE, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, null); + if (Flags.enforceQuotaPolicyToFgsJobs()) { + ActivityManager.getService().registerUidObserver(new QcUidObserver(), + ActivityManager.UID_OBSERVER_PROCSTATE, + ActivityManager.PROCESS_STATE_BOUND_TOP, null); + } ActivityManager.getService().registerUidObserver(new QcUidObserver(), ActivityManager.UID_OBSERVER_PROCSTATE, ActivityManager.PROCESS_STATE_TOP, null); } catch (RemoteException e) { // ignored; both services live in system_server } + + processQuotaConstantsAdjustment(); } @Override @@ -619,7 +647,9 @@ public final class QuotaController extends StateController { } final int uid = jobStatus.getSourceUid(); - if (mTopAppCache.get(uid)) { + if ((!Flags.enforceQuotaPolicyToTopStartedJobs() + || CompatChanges.isChangeEnabled(OVERRIDE_QUOTA_ENFORCEMENT_TO_TOP_STARTED_JOBS, + uid)) && mTopAppCache.get(uid)) { if (DEBUG) { Slog.d(TAG, jobStatus.toShortString() + " is top started job"); } @@ -656,7 +686,11 @@ public final class QuotaController extends StateController { timer.stopTrackingJob(jobStatus); } } - mTopStartedJobs.remove(jobStatus); + if (!Flags.enforceQuotaPolicyToTopStartedJobs() + || CompatChanges.isChangeEnabled(OVERRIDE_QUOTA_ENFORCEMENT_TO_TOP_STARTED_JOBS, + jobStatus.getSourceUid())) { + mTopStartedJobs.remove(jobStatus); + } } @Override @@ -767,7 +801,13 @@ public final class QuotaController extends StateController { /** @return true if the job was started while the app was in the TOP state. */ private boolean isTopStartedJobLocked(@NonNull final JobStatus jobStatus) { - return mTopStartedJobs.contains(jobStatus); + if (!Flags.enforceQuotaPolicyToTopStartedJobs() + || CompatChanges.isChangeEnabled(OVERRIDE_QUOTA_ENFORCEMENT_TO_TOP_STARTED_JOBS, + jobStatus.getSourceUid())) { + return mTopStartedJobs.contains(jobStatus); + } + + return false; } /** Returns the maximum amount of time this job could run for. */ @@ -791,7 +831,9 @@ public final class QuotaController extends StateController { || isTopStartedJobLocked(jobStatus) || isUidInForeground(jobStatus.getSourceUid()); final boolean isJobImportant = jobStatus.getEffectivePriority() >= JobInfo.PRIORITY_HIGH - || (jobStatus.getFlags() & JobInfo.FLAG_IMPORTANT_WHILE_FOREGROUND) != 0; + || (!android.app.job.Flags.ignoreImportantWhileForeground() + && (jobStatus.getFlags() + & JobInfo.FLAG_IMPORTANT_WHILE_FOREGROUND) != 0); if (isInPrivilegedState && isJobImportant) { return mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS; } @@ -1090,7 +1132,7 @@ public final class QuotaController extends StateController { // essentially run until they reach the maximum limit. if (stats.windowSizeMs == mAllowedTimePerPeriodMs[standbyBucket]) { return calculateTimeUntilQuotaConsumedLocked( - events, startMaxElapsed, maxExecutionTimeRemainingMs, false); + events, startMaxElapsed, maxExecutionTimeRemainingMs); } // Need to check both max time and period time in case one is less than the other. @@ -1099,9 +1141,9 @@ public final class QuotaController extends StateController { // bucket value. return Math.min( calculateTimeUntilQuotaConsumedLocked( - events, startMaxElapsed, maxExecutionTimeRemainingMs, false), + events, startMaxElapsed, maxExecutionTimeRemainingMs), calculateTimeUntilQuotaConsumedLocked( - events, startWindowElapsed, allowedTimeRemainingMs, true)); + events, startWindowElapsed, allowedTimeRemainingMs)); } /** @@ -1111,36 +1153,12 @@ public final class QuotaController extends StateController { * @param deadSpaceMs How much time can be allowed to count towards the quota */ private long calculateTimeUntilQuotaConsumedLocked(@NonNull List<TimedEvent> sessions, - final long windowStartElapsed, long deadSpaceMs, boolean allowQuotaBumps) { + final long windowStartElapsed, long deadSpaceMs) { long timeUntilQuotaConsumedMs = 0; long start = windowStartElapsed; - int numQuotaBumps = 0; - final long quotaBumpWindowStartElapsed = - sElapsedRealtimeClock.millis() - mQuotaBumpWindowSizeMs; final int numSessions = sessions.size(); - if (allowQuotaBumps) { - for (int i = numSessions - 1; i >= 0; --i) { - TimedEvent event = sessions.get(i); - - if (event instanceof QuotaBump) { - if (event.getEndTimeElapsed() >= quotaBumpWindowStartElapsed - && numQuotaBumps++ < mQuotaBumpLimit) { - deadSpaceMs += mQuotaBumpAdditionalDurationMs; - } else { - break; - } - } - } - } for (int i = 0; i < numSessions; ++i) { - TimedEvent event = sessions.get(i); - - if (event instanceof QuotaBump) { - continue; - } - - TimingSession session = (TimingSession) event; - + TimingSession session = (TimingSession) sessions.get(i); if (session.endTimeElapsed < windowStartElapsed) { // Outside of window. Ignore. continue; @@ -1325,41 +1343,15 @@ public final class QuotaController extends StateController { final long startWindowElapsed = nowElapsed - stats.windowSizeMs; final long startMaxElapsed = nowElapsed - MAX_PERIOD_MS; int sessionCountInWindow = 0; - int numQuotaBumps = 0; - final long quotaBumpWindowStartElapsed = nowElapsed - mQuotaBumpWindowSizeMs; // The minimum time between the start time and the beginning of the events that were // looked at --> how much time the stats will be valid for. long emptyTimeMs = Long.MAX_VALUE; // Sessions are non-overlapping and in order of occurrence, so iterating backwards will get // the most recent ones. final int loopStart = events.size() - 1; - // Process QuotaBumps first to ensure the limits are properly adjusted. - for (int i = loopStart; i >= 0; --i) { - TimedEvent event = events.get(i); - - if (event.getEndTimeElapsed() < quotaBumpWindowStartElapsed - || numQuotaBumps >= mQuotaBumpLimit) { - break; - } - - if (event instanceof QuotaBump) { - stats.allowedTimePerPeriodMs += mQuotaBumpAdditionalDurationMs; - stats.jobCountLimit += mQuotaBumpAdditionalJobCount; - stats.sessionCountLimit += mQuotaBumpAdditionalSessionCount; - emptyTimeMs = Math.min(emptyTimeMs, - event.getEndTimeElapsed() - quotaBumpWindowStartElapsed); - numQuotaBumps++; - } - } TimingSession lastSeenTimingSession = null; for (int i = loopStart; i >= 0; --i) { - TimedEvent event = events.get(i); - - if (event instanceof QuotaBump) { - continue; - } - - TimingSession session = (TimingSession) event; + TimingSession session = (TimingSession) events.get(i); // Window management. if (startWindowElapsed < session.endTimeElapsed) { @@ -1464,6 +1456,13 @@ public final class QuotaController extends StateController { } } + void processQuotaConstantsAdjustment() { + if (Flags.adjustQuotaDefaultConstants()) { + mQcConstants.adjustDefaultBucketWindowSizes(); + mQcConstants.adjustDefaultEjLimits(); + } + } + @VisibleForTesting void incrementJobCountLocked(final int userId, @NonNull final String packageName, int count) { final long now = sElapsedRealtimeClock.millis(); @@ -2053,28 +2052,6 @@ public final class QuotaController extends StateController { } @VisibleForTesting - static final class QuotaBump implements TimedEvent { - // Event timestamp in elapsed realtime timebase. - public final long eventTimeElapsed; - - QuotaBump(long eventElapsed) { - this.eventTimeElapsed = eventElapsed; - } - - @Override - public long getEndTimeElapsed() { - return eventTimeElapsed; - } - - @Override - public void dump(IndentingPrintWriter pw) { - pw.print("Quota bump @ "); - pw.print(eventTimeElapsed); - pw.println(); - } - } - - @VisibleForTesting static final class ShrinkableDebits { /** The amount of quota remaining. Can be negative if limit changes. */ private long mDebitTally; @@ -2523,21 +2500,6 @@ public final class QuotaController extends StateController { updateStandbyBucket(userId, packageName, bucketIndex); }); } - - @Override - public void triggerTemporaryQuotaBump(String packageName, @UserIdInt int userId) { - synchronized (mLock) { - List<TimedEvent> events = mTimingEvents.get(userId, packageName); - if (events == null || events.size() == 0) { - // If the app hasn't run any jobs, there's no point giving it a quota bump. - return; - } - events.add(new QuotaBump(sElapsedRealtimeClock.millis())); - invalidateAllExecutionStatsLocked(userId, packageName); - } - // Update jobs out of band. - mHandler.obtainMessage(MSG_CHECK_PACKAGE, userId, 0, packageName).sendToTarget(); - } } @VisibleForTesting @@ -2706,6 +2668,16 @@ public final class QuotaController extends StateController { } } + @VisibleForTesting + int getProcessStateQuotaFreeThreshold(int uid) { + if (Flags.enforceQuotaPolicyToFgsJobs() + && !CompatChanges.isChangeEnabled(OVERRIDE_QUOTA_ENFORCEMENT_TO_FGS_JOBS, uid)) { + return ActivityManager.PROCESS_STATE_BOUND_TOP; + } + + return ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE; + } + private class QcHandler extends Handler { QcHandler(Looper looper) { @@ -2727,11 +2699,12 @@ public final class QuotaController extends StateController { if (timeRemainingMs <= 50) { // Less than 50 milliseconds left. Start process of shutting down jobs. if (DEBUG) Slog.d(TAG, pkg + " has reached its quota."); - if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) { - Trace.instantForTrack(Trace.TRACE_TAG_SYSTEM_SERVER, - JobSchedulerService.TRACE_TRACK_NAME, - pkg + "#" + MSG_REACHED_TIME_QUOTA); - } + final StringBuilder traceMsg = new StringBuilder(); + traceMsg.append(TRACE_QUOTA_STATE_CHANGED_TAG) + .append(pkg) + .append(TRACE_QUOTA_STATE_CHANGED_DELIMITER) + .append(MSG_REACHED_TIME_QUOTA); + Trace.instant(Trace.TRACE_TAG_POWER, traceMsg.toString()); mStateChangedListener.onControllerStateChanged( maybeUpdateConstraintForPkgLocked( sElapsedRealtimeClock.millis(), @@ -2760,11 +2733,12 @@ public final class QuotaController extends StateController { pkg.userId, pkg.packageName); if (timeRemainingMs <= 0) { if (DEBUG) Slog.d(TAG, pkg + " has reached its EJ quota."); - if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) { - Trace.instantForTrack(Trace.TRACE_TAG_SYSTEM_SERVER, - JobSchedulerService.TRACE_TRACK_NAME, - pkg + "#" + MSG_REACHED_EJ_TIME_QUOTA); - } + final StringBuilder traceMsg = new StringBuilder(); + traceMsg.append(TRACE_QUOTA_STATE_CHANGED_TAG) + .append(pkg) + .append(TRACE_QUOTA_STATE_CHANGED_DELIMITER) + .append(MSG_REACHED_EJ_TIME_QUOTA); + Trace.instant(Trace.TRACE_TAG_POWER, traceMsg.toString()); mStateChangedListener.onControllerStateChanged( maybeUpdateConstraintForPkgLocked( sElapsedRealtimeClock.millis(), @@ -2789,11 +2763,12 @@ public final class QuotaController extends StateController { Slog.d(TAG, pkg + " has reached its count quota."); } - if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) { - Trace.instantForTrack(Trace.TRACE_TAG_SYSTEM_SERVER, - JobSchedulerService.TRACE_TRACK_NAME, - pkg + "#" + MSG_REACHED_COUNT_QUOTA); - } + final StringBuilder traceMsg = new StringBuilder(); + traceMsg.append(TRACE_QUOTA_STATE_CHANGED_TAG) + .append(pkg) + .append(TRACE_QUOTA_STATE_CHANGED_DELIMITER) + .append(MSG_REACHED_COUNT_QUOTA); + Trace.instant(Trace.TRACE_TAG_POWER, traceMsg.toString()); mStateChangedListener.onControllerStateChanged( maybeUpdateConstraintForPkgLocked( @@ -2832,15 +2807,15 @@ public final class QuotaController extends StateController { mTopAppCache.put(uid, true); mTopAppGraceCache.delete(uid); if (mForegroundUids.get(uid)) { - // Went from FGS to TOP. We don't need to reprocess timers or - // jobs. + // Went from a process state with quota free to TOP. We don't + // need to reprocess timers or jobs. break; } mForegroundUids.put(uid, true); isQuotaFree = true; } else { final boolean reprocess; - if (procState <= ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) { + if (procState <= getProcessStateQuotaFreeThreshold(uid)) { reprocess = !mForegroundUids.get(uid); mForegroundUids.put(uid, true); isQuotaFree = true; @@ -3008,7 +2983,6 @@ public final class QuotaController extends StateController { mQcConstants.mRateLimitingConstantsUpdated = false; mQcConstants.mExecutionPeriodConstantsUpdated = false; mQcConstants.mEJLimitConstantsUpdated = false; - mQcConstants.mQuotaBumpConstantsUpdated = false; } @Override @@ -3035,7 +3009,6 @@ public final class QuotaController extends StateController { private boolean mRateLimitingConstantsUpdated = false; private boolean mExecutionPeriodConstantsUpdated = false; private boolean mEJLimitConstantsUpdated = false; - private boolean mQuotaBumpConstantsUpdated = false; /** Prefix to use with all constant keys in order to "sub-namespace" the keys. */ private static final String QC_CONSTANT_PREFIX = "qc_"; @@ -3183,21 +3156,6 @@ public final class QuotaController extends StateController { @VisibleForTesting static final String KEY_EJ_GRACE_PERIOD_TOP_APP_MS = QC_CONSTANT_PREFIX + "ej_grace_period_top_app_ms"; - @VisibleForTesting - static final String KEY_QUOTA_BUMP_ADDITIONAL_DURATION_MS = - QC_CONSTANT_PREFIX + "quota_bump_additional_duration_ms"; - @VisibleForTesting - static final String KEY_QUOTA_BUMP_ADDITIONAL_JOB_COUNT = - QC_CONSTANT_PREFIX + "quota_bump_additional_job_count"; - @VisibleForTesting - static final String KEY_QUOTA_BUMP_ADDITIONAL_SESSION_COUNT = - QC_CONSTANT_PREFIX + "quota_bump_additional_session_count"; - @VisibleForTesting - static final String KEY_QUOTA_BUMP_WINDOW_SIZE_MS = - QC_CONSTANT_PREFIX + "quota_bump_window_size_ms"; - @VisibleForTesting - static final String KEY_QUOTA_BUMP_LIMIT = - QC_CONSTANT_PREFIX + "quota_bump_limit"; private static final long DEFAULT_ALLOWED_TIME_PER_PERIOD_EXEMPTED_MS = 10 * 60 * 1000L; // 10 minutes @@ -3213,14 +3171,28 @@ public final class QuotaController extends StateController { 10 * 60 * 1000L; // 10 minutes private static final long DEFAULT_IN_QUOTA_BUFFER_MS = 30 * 1000L; // 30 seconds - private static final long DEFAULT_WINDOW_SIZE_EXEMPTED_MS = + // Legacy default window size for EXEMPTED bucket + private static final long DEFAULT_LEGACY_WINDOW_SIZE_EXEMPTED_MS = DEFAULT_ALLOWED_TIME_PER_PERIOD_EXEMPTED_MS; // EXEMPT apps can run jobs at any time - private static final long DEFAULT_WINDOW_SIZE_ACTIVE_MS = + // Legacy default window size for ACTIVE bucket + private static final long DEFAULT_LEGACY_WINDOW_SIZE_ACTIVE_MS = DEFAULT_ALLOWED_TIME_PER_PERIOD_ACTIVE_MS; // ACTIVE apps can run jobs at any time - private static final long DEFAULT_WINDOW_SIZE_WORKING_MS = + // Legacy default window size for WORKING bucket + private static final long DEFAULT_LEGACY_WINDOW_SIZE_WORKING_MS = 2 * 60 * 60 * 1000L; // 2 hours - private static final long DEFAULT_WINDOW_SIZE_FREQUENT_MS = + // Legacy default window size for FREQUENT bucket + private static final long DEFAULT_LEGACY_WINDOW_SIZE_FREQUENT_MS = 8 * 60 * 60 * 1000L; // 8 hours + + private static final long DEFAULT_CURRENT_WINDOW_SIZE_EXEMPTED_MS = + 20 * 60 * 1000L; // 20 minutes. + private static final long DEFAULT_CURRENT_WINDOW_SIZE_ACTIVE_MS = + 30 * 60 * 1000L; // 30 minutes. + private static final long DEFAULT_CURRENT_WINDOW_SIZE_WORKING_MS = + 4 * 60 * 60 * 1000L; // 4 hours + private static final long DEFAULT_CURRENT_WINDOW_SIZE_FREQUENT_MS = + 12 * 60 * 60 * 1000L; // 12 hours + private static final long DEFAULT_WINDOW_SIZE_RARE_MS = 24 * 60 * 60 * 1000L; // 24 hours private static final long DEFAULT_WINDOW_SIZE_RESTRICTED_MS = @@ -3234,9 +3206,9 @@ public final class QuotaController extends StateController { 75; // 75/window = 450/hr = 1/session private static final int DEFAULT_MAX_JOB_COUNT_ACTIVE = DEFAULT_MAX_JOB_COUNT_EXEMPTED; private static final int DEFAULT_MAX_JOB_COUNT_WORKING = // 120/window = 60/hr = 12/session - (int) (60.0 * DEFAULT_WINDOW_SIZE_WORKING_MS / HOUR_IN_MILLIS); + (int) (60.0 * DEFAULT_LEGACY_WINDOW_SIZE_WORKING_MS / HOUR_IN_MILLIS); private static final int DEFAULT_MAX_JOB_COUNT_FREQUENT = // 200/window = 25/hr = 25/session - (int) (25.0 * DEFAULT_WINDOW_SIZE_FREQUENT_MS / HOUR_IN_MILLIS); + (int) (25.0 * DEFAULT_LEGACY_WINDOW_SIZE_FREQUENT_MS / HOUR_IN_MILLIS); private static final int DEFAULT_MAX_JOB_COUNT_RARE = // 48/window = 2/hr = 16/session (int) (2.0 * DEFAULT_WINDOW_SIZE_RARE_MS / HOUR_IN_MILLIS); private static final int DEFAULT_MAX_JOB_COUNT_RESTRICTED = 10; @@ -3257,24 +3229,24 @@ public final class QuotaController extends StateController { // 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_LEGACY_EJ_LIMIT_WORKING_MS = DEFAULT_EJ_LIMIT_ACTIVE_MS; + private static final long DEFAULT_CURRENT_EJ_LIMIT_WORKING_MS = 15 * MINUTE_IN_MILLIS; private static final long DEFAULT_EJ_LIMIT_FREQUENT_MS = 10 * MINUTE_IN_MILLIS; private static final long DEFAULT_EJ_LIMIT_RARE_MS = DEFAULT_EJ_LIMIT_FREQUENT_MS; private static final long DEFAULT_EJ_LIMIT_RESTRICTED_MS = 5 * MINUTE_IN_MILLIS; private static final long DEFAULT_EJ_LIMIT_ADDITION_SPECIAL_MS = 15 * MINUTE_IN_MILLIS; private static final long DEFAULT_EJ_LIMIT_ADDITION_INSTALLER_MS = 30 * MINUTE_IN_MILLIS; private static final long DEFAULT_EJ_WINDOW_SIZE_MS = 24 * HOUR_IN_MILLIS; - private static final long DEFAULT_EJ_TOP_APP_TIME_CHUNK_SIZE_MS = 30 * SECOND_IN_MILLIS; + private static final long DEFAULT_LEGACY_EJ_TOP_APP_TIME_CHUNK_SIZE_MS = + 30 * SECOND_IN_MILLIS; + private static final long DEFAULT_CURRENT_EJ_TOP_APP_TIME_CHUNK_SIZE_MS = + 5 * MINUTE_IN_MILLIS; private static final long DEFAULT_EJ_REWARD_TOP_APP_MS = 10 * SECOND_IN_MILLIS; - private static final long DEFAULT_EJ_REWARD_INTERACTION_MS = 15 * SECOND_IN_MILLIS; + private static final long DEFAULT_LEGACY_EJ_REWARD_INTERACTION_MS = 15 * SECOND_IN_MILLIS; + private static final long DEFAULT_CURRENT_EJ_REWARD_INTERACTION_MS = 5 * SECOND_IN_MILLIS; private static final long DEFAULT_EJ_REWARD_NOTIFICATION_SEEN_MS = 0; private static final long DEFAULT_EJ_GRACE_PERIOD_TEMP_ALLOWLIST_MS = 3 * MINUTE_IN_MILLIS; private static final long DEFAULT_EJ_GRACE_PERIOD_TOP_APP_MS = 1 * MINUTE_IN_MILLIS; - private static final long DEFAULT_QUOTA_BUMP_ADDITIONAL_DURATION_MS = 1 * MINUTE_IN_MILLIS; - private static final int DEFAULT_QUOTA_BUMP_ADDITIONAL_JOB_COUNT = 2; - private static final int DEFAULT_QUOTA_BUMP_ADDITIONAL_SESSION_COUNT = 1; - private static final long DEFAULT_QUOTA_BUMP_WINDOW_SIZE_MS = 8 * HOUR_IN_MILLIS; - private static final int DEFAULT_QUOTA_BUMP_LIMIT = 8; /** * How much time each app in the exempted bucket will have to run jobs within their standby @@ -3321,28 +3293,28 @@ public final class QuotaController extends StateController { * expected to run only {@link #ALLOWED_TIME_PER_PERIOD_EXEMPTED_MS} within the past * WINDOW_SIZE_MS. */ - public long WINDOW_SIZE_EXEMPTED_MS = DEFAULT_WINDOW_SIZE_EXEMPTED_MS; + public long WINDOW_SIZE_EXEMPTED_MS = DEFAULT_LEGACY_WINDOW_SIZE_EXEMPTED_MS; /** * The quota window size of the particular standby bucket. Apps in this standby bucket are * expected to run only {@link #ALLOWED_TIME_PER_PERIOD_ACTIVE_MS} within the past * WINDOW_SIZE_MS. */ - public long WINDOW_SIZE_ACTIVE_MS = DEFAULT_WINDOW_SIZE_ACTIVE_MS; + public long WINDOW_SIZE_ACTIVE_MS = DEFAULT_LEGACY_WINDOW_SIZE_ACTIVE_MS; /** * The quota window size of the particular standby bucket. Apps in this standby bucket are * expected to run only {@link #ALLOWED_TIME_PER_PERIOD_WORKING_MS} within the past * WINDOW_SIZE_MS. */ - public long WINDOW_SIZE_WORKING_MS = DEFAULT_WINDOW_SIZE_WORKING_MS; + public long WINDOW_SIZE_WORKING_MS = DEFAULT_LEGACY_WINDOW_SIZE_WORKING_MS; /** * The quota window size of the particular standby bucket. Apps in this standby bucket are * expected to run only {@link #ALLOWED_TIME_PER_PERIOD_FREQUENT_MS} within the past * WINDOW_SIZE_MS. */ - public long WINDOW_SIZE_FREQUENT_MS = DEFAULT_WINDOW_SIZE_FREQUENT_MS; + public long WINDOW_SIZE_FREQUENT_MS = DEFAULT_LEGACY_WINDOW_SIZE_FREQUENT_MS; /** * The quota window size of the particular standby bucket. Apps in this standby bucket are @@ -3503,7 +3475,7 @@ public final class QuotaController extends StateController { * standby bucket can only have expedited job sessions totalling EJ_LIMIT (without factoring * in any rewards or free EJs). */ - public long EJ_LIMIT_WORKING_MS = DEFAULT_EJ_LIMIT_WORKING_MS; + public long EJ_LIMIT_WORKING_MS = DEFAULT_LEGACY_EJ_LIMIT_WORKING_MS; /** * The total expedited job session limit of the particular standby bucket. Apps in this @@ -3547,7 +3519,7 @@ public final class QuotaController extends StateController { /** * Length of time used to split an app's top time into chunks. */ - public long EJ_TOP_APP_TIME_CHUNK_SIZE_MS = DEFAULT_EJ_TOP_APP_TIME_CHUNK_SIZE_MS; + public long EJ_TOP_APP_TIME_CHUNK_SIZE_MS = DEFAULT_LEGACY_EJ_TOP_APP_TIME_CHUNK_SIZE_MS; /** * How much EJ quota to give back to an app based on the number of top app time chunks it @@ -3558,7 +3530,7 @@ public final class QuotaController extends StateController { /** * How much EJ quota to give back to an app based on each non-top user interaction. */ - public long EJ_REWARD_INTERACTION_MS = DEFAULT_EJ_REWARD_INTERACTION_MS; + public long EJ_REWARD_INTERACTION_MS = DEFAULT_LEGACY_EJ_REWARD_INTERACTION_MS; /** * How much EJ quota to give back to an app based on each notification seen event. @@ -3576,32 +3548,51 @@ public final class QuotaController extends StateController { */ public long EJ_GRACE_PERIOD_TOP_APP_MS = DEFAULT_EJ_GRACE_PERIOD_TOP_APP_MS; - /** - * How much additional session duration to give an app for each accepted quota bump. - */ - public long QUOTA_BUMP_ADDITIONAL_DURATION_MS = DEFAULT_QUOTA_BUMP_ADDITIONAL_DURATION_MS; + void adjustDefaultBucketWindowSizes() { + WINDOW_SIZE_EXEMPTED_MS = DEFAULT_CURRENT_WINDOW_SIZE_EXEMPTED_MS; + WINDOW_SIZE_ACTIVE_MS = DEFAULT_CURRENT_WINDOW_SIZE_ACTIVE_MS; + WINDOW_SIZE_WORKING_MS = DEFAULT_CURRENT_WINDOW_SIZE_WORKING_MS; + WINDOW_SIZE_FREQUENT_MS = DEFAULT_CURRENT_WINDOW_SIZE_FREQUENT_MS; - /** - * How many additional regular jobs to give an app for each accepted quota bump. - */ - public int QUOTA_BUMP_ADDITIONAL_JOB_COUNT = DEFAULT_QUOTA_BUMP_ADDITIONAL_JOB_COUNT; + mBucketPeriodsMs[EXEMPTED_INDEX] = Math.max( + mAllowedTimePerPeriodMs[EXEMPTED_INDEX], + Math.min(MAX_PERIOD_MS, WINDOW_SIZE_EXEMPTED_MS)); + mBucketPeriodsMs[ACTIVE_INDEX] = Math.max( + mAllowedTimePerPeriodMs[ACTIVE_INDEX], + Math.min(MAX_PERIOD_MS, WINDOW_SIZE_ACTIVE_MS)); + mBucketPeriodsMs[WORKING_INDEX] = Math.max( + mAllowedTimePerPeriodMs[WORKING_INDEX], + Math.min(MAX_PERIOD_MS, WINDOW_SIZE_WORKING_MS)); + mBucketPeriodsMs[FREQUENT_INDEX] = Math.max( + mAllowedTimePerPeriodMs[FREQUENT_INDEX], + Math.min(MAX_PERIOD_MS, WINDOW_SIZE_FREQUENT_MS)); + } - /** - * How many additional sessions to give an app for each accepted quota bump. - */ - public int QUOTA_BUMP_ADDITIONAL_SESSION_COUNT = - DEFAULT_QUOTA_BUMP_ADDITIONAL_SESSION_COUNT; + void adjustDefaultEjLimits() { + EJ_LIMIT_WORKING_MS = DEFAULT_CURRENT_EJ_LIMIT_WORKING_MS; + EJ_TOP_APP_TIME_CHUNK_SIZE_MS = DEFAULT_CURRENT_EJ_TOP_APP_TIME_CHUNK_SIZE_MS; + EJ_REWARD_INTERACTION_MS = DEFAULT_CURRENT_EJ_REWARD_INTERACTION_MS; - /** - * The rolling window size within which to accept and apply quota bump events. - */ - public long QUOTA_BUMP_WINDOW_SIZE_MS = DEFAULT_QUOTA_BUMP_WINDOW_SIZE_MS; + // The limit must be in the range [15 minutes, active limit]. + mEJLimitsMs[WORKING_INDEX] = Math.max(15 * MINUTE_IN_MILLIS, + Math.min(mEJLimitsMs[ACTIVE_INDEX], EJ_LIMIT_WORKING_MS)); - /** - * The maximum number of quota bumps to accept and apply within the - * {@link #QUOTA_BUMP_WINDOW_SIZE_MS window}. - */ - public int QUOTA_BUMP_LIMIT = DEFAULT_QUOTA_BUMP_LIMIT; + // Limit interaction reward to be in the range [5 seconds, 15 minutes] per event. + mEJRewardInteractionMs = Math.min(15 * MINUTE_IN_MILLIS, + Math.max(5 * SECOND_IN_MILLIS, EJ_REWARD_INTERACTION_MS)); + + // Limit chunking to be in the range [1 millisecond, 15 minutes] per event. + long newChunkSizeMs = Math.min(15 * MINUTE_IN_MILLIS, + Math.max(1, EJ_TOP_APP_TIME_CHUNK_SIZE_MS)); + mEJTopAppTimeChunkSizeMs = newChunkSizeMs; + if (mEJTopAppTimeChunkSizeMs < mEJRewardTopAppMs) { + // Not making chunk sizes and top rewards to be the upper/lower + // limits of the other to allow trying different policies. Just log + // the discrepancy. + Slog.w(TAG, "EJ top app time chunk less than reward: " + + mEJTopAppTimeChunkSizeMs + " vs " + mEJRewardTopAppMs); + } + } public void processConstantLocked(@NonNull DeviceConfig.Properties properties, @NonNull String key) { @@ -3639,14 +3630,6 @@ public final class QuotaController extends StateController { updateEJLimitConstantsLocked(); break; - case KEY_QUOTA_BUMP_ADDITIONAL_DURATION_MS: - case KEY_QUOTA_BUMP_ADDITIONAL_JOB_COUNT: - case KEY_QUOTA_BUMP_ADDITIONAL_SESSION_COUNT: - case KEY_QUOTA_BUMP_WINDOW_SIZE_MS: - case KEY_QUOTA_BUMP_LIMIT: - updateQuotaBumpConstantsLocked(); - break; - case KEY_MAX_JOB_COUNT_EXEMPTED: MAX_JOB_COUNT_EXEMPTED = properties.getInt(key, DEFAULT_MAX_JOB_COUNT_EXEMPTED); int newExemptedMaxJobCount = @@ -3779,7 +3762,9 @@ public final class QuotaController extends StateController { case KEY_EJ_TOP_APP_TIME_CHUNK_SIZE_MS: // We don't need to re-evaluate execution stats or constraint status for this. EJ_TOP_APP_TIME_CHUNK_SIZE_MS = - properties.getLong(key, DEFAULT_EJ_TOP_APP_TIME_CHUNK_SIZE_MS); + properties.getLong(key, Flags.adjustQuotaDefaultConstants() + ? DEFAULT_CURRENT_EJ_TOP_APP_TIME_CHUNK_SIZE_MS : + DEFAULT_LEGACY_EJ_TOP_APP_TIME_CHUNK_SIZE_MS); // Limit chunking to be in the range [1 millisecond, 15 minutes] per event. long newChunkSizeMs = Math.min(15 * MINUTE_IN_MILLIS, Math.max(1, EJ_TOP_APP_TIME_CHUNK_SIZE_MS)); @@ -3815,7 +3800,9 @@ public final class QuotaController extends StateController { case KEY_EJ_REWARD_INTERACTION_MS: // We don't need to re-evaluate execution stats or constraint status for this. EJ_REWARD_INTERACTION_MS = - properties.getLong(key, DEFAULT_EJ_REWARD_INTERACTION_MS); + properties.getLong(key, Flags.adjustQuotaDefaultConstants() + ? DEFAULT_CURRENT_EJ_REWARD_INTERACTION_MS : + DEFAULT_LEGACY_EJ_REWARD_INTERACTION_MS); // Limit interaction reward to be in the range [5 seconds, 15 minutes] per // event. mEJRewardInteractionMs = Math.min(15 * MINUTE_IN_MILLIS, @@ -3889,14 +3876,23 @@ public final class QuotaController extends StateController { MAX_EXECUTION_TIME_MS = properties.getLong(KEY_MAX_EXECUTION_TIME_MS, DEFAULT_MAX_EXECUTION_TIME_MS); WINDOW_SIZE_EXEMPTED_MS = properties.getLong(KEY_WINDOW_SIZE_EXEMPTED_MS, - DEFAULT_WINDOW_SIZE_EXEMPTED_MS); + Flags.adjustQuotaDefaultConstants() + ? DEFAULT_CURRENT_WINDOW_SIZE_EXEMPTED_MS : + DEFAULT_LEGACY_WINDOW_SIZE_EXEMPTED_MS); WINDOW_SIZE_ACTIVE_MS = properties.getLong(KEY_WINDOW_SIZE_ACTIVE_MS, - DEFAULT_WINDOW_SIZE_ACTIVE_MS); + Flags.adjustQuotaDefaultConstants() + ? DEFAULT_CURRENT_WINDOW_SIZE_ACTIVE_MS : + DEFAULT_LEGACY_WINDOW_SIZE_ACTIVE_MS); WINDOW_SIZE_WORKING_MS = - properties.getLong(KEY_WINDOW_SIZE_WORKING_MS, DEFAULT_WINDOW_SIZE_WORKING_MS); + properties.getLong(KEY_WINDOW_SIZE_WORKING_MS, + Flags.adjustQuotaDefaultConstants() + ? DEFAULT_CURRENT_WINDOW_SIZE_WORKING_MS : + DEFAULT_LEGACY_WINDOW_SIZE_WORKING_MS); WINDOW_SIZE_FREQUENT_MS = properties.getLong(KEY_WINDOW_SIZE_FREQUENT_MS, - DEFAULT_WINDOW_SIZE_FREQUENT_MS); + Flags.adjustQuotaDefaultConstants() + ? DEFAULT_CURRENT_WINDOW_SIZE_FREQUENT_MS : + DEFAULT_LEGACY_WINDOW_SIZE_FREQUENT_MS); WINDOW_SIZE_RARE_MS = properties.getLong(KEY_WINDOW_SIZE_RARE_MS, DEFAULT_WINDOW_SIZE_RARE_MS); WINDOW_SIZE_RESTRICTED_MS = @@ -4067,7 +4063,9 @@ public final class QuotaController extends StateController { EJ_LIMIT_ACTIVE_MS = properties.getLong( KEY_EJ_LIMIT_ACTIVE_MS, DEFAULT_EJ_LIMIT_ACTIVE_MS); EJ_LIMIT_WORKING_MS = properties.getLong( - KEY_EJ_LIMIT_WORKING_MS, DEFAULT_EJ_LIMIT_WORKING_MS); + KEY_EJ_LIMIT_WORKING_MS, Flags.adjustQuotaDefaultConstants() + ? DEFAULT_CURRENT_EJ_LIMIT_WORKING_MS : + DEFAULT_LEGACY_EJ_LIMIT_WORKING_MS); EJ_LIMIT_FREQUENT_MS = properties.getLong( KEY_EJ_LIMIT_FREQUENT_MS, DEFAULT_EJ_LIMIT_FREQUENT_MS); EJ_LIMIT_RARE_MS = properties.getLong( @@ -4145,65 +4143,6 @@ public final class QuotaController extends StateController { } } - private void updateQuotaBumpConstantsLocked() { - if (mQuotaBumpConstantsUpdated) { - return; - } - mQuotaBumpConstantsUpdated = true; - - // Query the values as an atomic set. - final DeviceConfig.Properties properties = DeviceConfig.getProperties( - DeviceConfig.NAMESPACE_JOB_SCHEDULER, - KEY_QUOTA_BUMP_ADDITIONAL_DURATION_MS, - KEY_QUOTA_BUMP_ADDITIONAL_JOB_COUNT, KEY_QUOTA_BUMP_ADDITIONAL_SESSION_COUNT, - KEY_QUOTA_BUMP_WINDOW_SIZE_MS, KEY_QUOTA_BUMP_LIMIT); - QUOTA_BUMP_ADDITIONAL_DURATION_MS = properties.getLong( - KEY_QUOTA_BUMP_ADDITIONAL_DURATION_MS, - DEFAULT_QUOTA_BUMP_ADDITIONAL_DURATION_MS); - QUOTA_BUMP_ADDITIONAL_JOB_COUNT = properties.getInt( - KEY_QUOTA_BUMP_ADDITIONAL_JOB_COUNT, DEFAULT_QUOTA_BUMP_ADDITIONAL_JOB_COUNT); - QUOTA_BUMP_ADDITIONAL_SESSION_COUNT = properties.getInt( - KEY_QUOTA_BUMP_ADDITIONAL_SESSION_COUNT, - DEFAULT_QUOTA_BUMP_ADDITIONAL_SESSION_COUNT); - QUOTA_BUMP_WINDOW_SIZE_MS = properties.getLong( - KEY_QUOTA_BUMP_WINDOW_SIZE_MS, DEFAULT_QUOTA_BUMP_WINDOW_SIZE_MS); - QUOTA_BUMP_LIMIT = properties.getInt( - KEY_QUOTA_BUMP_LIMIT, DEFAULT_QUOTA_BUMP_LIMIT); - - // The window must be in the range [1 hour, 24 hours]. - long newWindowSizeMs = Math.max(HOUR_IN_MILLIS, - Math.min(MAX_PERIOD_MS, QUOTA_BUMP_WINDOW_SIZE_MS)); - if (mQuotaBumpWindowSizeMs != newWindowSizeMs) { - mQuotaBumpWindowSizeMs = newWindowSizeMs; - mShouldReevaluateConstraints = true; - } - // The limit must be nonnegative. - int newLimit = Math.max(0, QUOTA_BUMP_LIMIT); - if (mQuotaBumpLimit != newLimit) { - mQuotaBumpLimit = newLimit; - mShouldReevaluateConstraints = true; - } - // The job count must be nonnegative. - int newJobAddition = Math.max(0, QUOTA_BUMP_ADDITIONAL_JOB_COUNT); - if (mQuotaBumpAdditionalJobCount != newJobAddition) { - mQuotaBumpAdditionalJobCount = newJobAddition; - mShouldReevaluateConstraints = true; - } - // The session count must be nonnegative. - int newSessionAddition = Math.max(0, QUOTA_BUMP_ADDITIONAL_SESSION_COUNT); - if (mQuotaBumpAdditionalSessionCount != newSessionAddition) { - mQuotaBumpAdditionalSessionCount = newSessionAddition; - mShouldReevaluateConstraints = true; - } - // The additional duration must be in the range [0, 10 minutes]. - long newAdditionalDuration = Math.max(0, - Math.min(10 * MINUTE_IN_MILLIS, QUOTA_BUMP_ADDITIONAL_DURATION_MS)); - if (mQuotaBumpAdditionalDurationMs != newAdditionalDuration) { - mQuotaBumpAdditionalDurationMs = newAdditionalDuration; - mShouldReevaluateConstraints = true; - } - } - private void dump(IndentingPrintWriter pw) { pw.println(); pw.println("QuotaController:"); @@ -4266,15 +4205,6 @@ public final class QuotaController extends StateController { EJ_GRACE_PERIOD_TEMP_ALLOWLIST_MS).println(); pw.print(KEY_EJ_GRACE_PERIOD_TOP_APP_MS, EJ_GRACE_PERIOD_TOP_APP_MS).println(); - pw.print(KEY_QUOTA_BUMP_ADDITIONAL_DURATION_MS, - QUOTA_BUMP_ADDITIONAL_DURATION_MS).println(); - pw.print(KEY_QUOTA_BUMP_ADDITIONAL_JOB_COUNT, - QUOTA_BUMP_ADDITIONAL_JOB_COUNT).println(); - pw.print(KEY_QUOTA_BUMP_ADDITIONAL_SESSION_COUNT, - QUOTA_BUMP_ADDITIONAL_SESSION_COUNT).println(); - pw.print(KEY_QUOTA_BUMP_WINDOW_SIZE_MS, QUOTA_BUMP_WINDOW_SIZE_MS).println(); - pw.print(KEY_QUOTA_BUMP_LIMIT, QUOTA_BUMP_LIMIT).println(); - pw.decreaseIndent(); } @@ -4492,37 +4422,21 @@ public final class QuotaController extends StateController { return mQcConstants; } - @VisibleForTesting - long getQuotaBumpAdditionDurationMs() { - return mQuotaBumpAdditionalDurationMs; - } - - @VisibleForTesting - int getQuotaBumpAdditionJobCount() { - return mQuotaBumpAdditionalJobCount; - } - - @VisibleForTesting - int getQuotaBumpAdditionSessionCount() { - return mQuotaBumpAdditionalSessionCount; - } - - @VisibleForTesting - int getQuotaBumpLimit() { - return mQuotaBumpLimit; - } - - @VisibleForTesting - long getQuotaBumpWindowSizeMs() { - return mQuotaBumpWindowSizeMs; - } - //////////////////////////// DATA DUMP ////////////////////////////// @NeverCompile // Avoid size overhead of debugging code. @Override public void dumpControllerStateLocked(final IndentingPrintWriter pw, final Predicate<JobStatus> predicate) { + pw.println("Aconfig Flags:"); + pw.println(" " + Flags.FLAG_ADJUST_QUOTA_DEFAULT_CONSTANTS + + ": " + Flags.adjustQuotaDefaultConstants()); + pw.println(" " + Flags.FLAG_ENFORCE_QUOTA_POLICY_TO_FGS_JOBS + + ": " + Flags.enforceQuotaPolicyToFgsJobs()); + pw.println(" " + Flags.FLAG_ENFORCE_QUOTA_POLICY_TO_TOP_STARTED_JOBS + + ": " + Flags.enforceQuotaPolicyToTopStartedJobs()); + pw.println(); + pw.println("Current elapsed time: " + sElapsedRealtimeClock.millis()); pw.println(); diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java b/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java index 6265d9bb815f..a8641ae43509 100644 --- a/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java +++ b/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java @@ -629,14 +629,15 @@ public class AppIdleHistory { } /** - * Returns the index in the arrays of screenTimeThresholds and elapsedTimeThresholds - * that corresponds to how long since the app was used. + * Returns the index in the array of elapsedTimeThresholds that corresponds to + * how long since the app was used. * @param packageName * @param userId * @param elapsedRealtime current time - * @param screenTimeThresholds Array of screen times, in ascending order, first one is 0 + * @param screenTimeThresholds Array of screen times, in ascending order, + * first one is 0 (this will not be used any more) * @param elapsedTimeThresholds Array of elapsed time, in ascending order, first one is 0 - * @return The index whose values the app's used time exceeds (in both arrays) or {@code -1} to + * @return The index whose values the app's used time exceeds or {@code -1} to * indicate that the app has never been used. */ int getThresholdIndex(String packageName, int userId, long elapsedRealtime, @@ -646,7 +647,7 @@ public class AppIdleHistory { elapsedRealtime, false); // If we don't have any state for the app, assume never used if (appUsageHistory == null || appUsageHistory.lastUsedElapsedTime < 0 - || appUsageHistory.lastUsedScreenTime < 0) { + || (!Flags.screenTimeBypass() && appUsageHistory.lastUsedScreenTime < 0)) { return -1; } @@ -659,7 +660,7 @@ public class AppIdleHistory { if (DEBUG) Slog.d(TAG, packageName + " screenOn=" + screenOnDelta + ", elapsed=" + elapsedDelta); for (int i = screenTimeThresholds.length - 1; i >= 0; i--) { - if (screenOnDelta >= screenTimeThresholds[i] + if ((Flags.screenTimeBypass() || screenOnDelta >= screenTimeThresholds[i]) && elapsedDelta >= elapsedTimeThresholds[i]) { return i; } 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 c9d340757c6b..9871d713178e 100644 --- a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java +++ b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java @@ -337,11 +337,11 @@ public class AppStandbyController */ long[] mAppStandbyElapsedThresholds = DEFAULT_ELAPSED_TIME_THRESHOLDS; /** Minimum time a strong usage event should keep the bucket elevated. */ - long mStrongUsageTimeoutMillis = ConstantsObserver.DEFAULT_STRONG_USAGE_TIMEOUT; + long mStrongUsageTimeoutMillis = ConstantsObserver.DEFAULT_LEGACY_STRONG_USAGE_TIMEOUT; /** Minimum time a notification seen event should keep the bucket elevated. */ long mNotificationSeenTimeoutMillis = ConstantsObserver.DEFAULT_NOTIFICATION_TIMEOUT; /** Minimum time a slice pinned event should keep the bucket elevated. */ - long mSlicePinnedTimeoutMillis = ConstantsObserver.DEFAULT_SLICE_PINNED_TIMEOUT; + long mSlicePinnedTimeoutMillis = ConstantsObserver.DEFAULT_LEGACY_SLICE_PINNED_TIMEOUT; /** The standby bucket that an app will be promoted on a notification-seen event */ int mNotificationSeenPromotedBucket = ConstantsObserver.DEFAULT_NOTIFICATION_SEEN_PROMOTED_BUCKET; @@ -362,7 +362,9 @@ public class AppStandbyController /** Maximum time to wait for a prediction before using simple timeouts to downgrade buckets. */ long mPredictionTimeoutMillis = DEFAULT_PREDICTION_TIMEOUT; /** Maximum time a sync adapter associated with a CP should keep the buckets elevated. */ - long mSyncAdapterTimeoutMillis = ConstantsObserver.DEFAULT_SYNC_ADAPTER_TIMEOUT; + long mSyncAdapterTimeoutMillis = ConstantsObserver.DEFAULT_LEGACY_SYNC_ADAPTER_TIMEOUT; + /** The bucket that an app will be promoted on a sync adapter associated with a CP */ + int mSyncAdapaterPromotedBucket = STANDBY_BUCKET_ACTIVE; /** * Maximum time an exempted sync should keep the buckets elevated, when sync is scheduled in * non-doze @@ -751,7 +753,7 @@ public class AppStandbyController userId); synchronized (mAppIdleLock) { reportNoninteractiveUsageCrossUserLocked(packageName, userId, - STANDBY_BUCKET_ACTIVE, REASON_SUB_USAGE_SYNC_ADAPTER, + mSyncAdapaterPromotedBucket, REASON_SUB_USAGE_SYNC_ADAPTER, elapsedRealtime, mSyncAdapterTimeoutMillis, linkedProfiles); } } @@ -2446,6 +2448,8 @@ public class AppStandbyController pw.println("Flags: "); pw.println(" " + Flags.FLAG_AVOID_IDLE_CHECK + ": " + Flags.avoidIdleCheck()); + pw.println(" " + Flags.FLAG_ADJUST_DEFAULT_BUCKET_ELEVATION_PARAMS + + ": " + Flags.adjustDefaultBucketElevationParams()); pw.println(); synchronized (mCarrierPrivilegedLock) { @@ -2481,6 +2485,9 @@ public class AppStandbyController pw.print(" mSyncAdapterTimeoutMillis="); TimeUtils.formatDuration(mSyncAdapterTimeoutMillis, pw); pw.println(); + pw.print(" mSyncAdapaterPromotedBucket="); + pw.print(standbyBucketToString(mSyncAdapaterPromotedBucket)); + pw.println(); pw.print(" mSystemInteractionTimeoutMillis="); TimeUtils.formatDuration(mSystemInteractionTimeoutMillis, pw); pw.println(); @@ -3059,12 +3066,18 @@ public class AppStandbyController public static final long DEFAULT_CHECK_IDLE_INTERVAL_MS = COMPRESS_TIME ? ONE_MINUTE : 4 * ONE_HOUR; - public static final long DEFAULT_STRONG_USAGE_TIMEOUT = + public static final long DEFAULT_LEGACY_STRONG_USAGE_TIMEOUT = COMPRESS_TIME ? ONE_MINUTE : 1 * ONE_HOUR; + + public static final long DEFAULT_CURRENT_STRONG_USAGE_TIMEOUT = + COMPRESS_TIME ? ONE_MINUTE : 5 * ONE_MINUTE; public static final long DEFAULT_NOTIFICATION_TIMEOUT = COMPRESS_TIME ? 12 * ONE_MINUTE : 12 * ONE_HOUR; - public static final long DEFAULT_SLICE_PINNED_TIMEOUT = + public static final long DEFAULT_LEGACY_SLICE_PINNED_TIMEOUT = COMPRESS_TIME ? 12 * ONE_MINUTE : 12 * ONE_HOUR; + + public static final long DEFAULT_CURRENT_SLICE_PINNED_TIMEOUT = + COMPRESS_TIME ? 12 * ONE_MINUTE : 2 * ONE_HOUR; public static final int DEFAULT_NOTIFICATION_SEEN_PROMOTED_BUCKET = STANDBY_BUCKET_WORKING_SET; public static final boolean DEFAULT_RETAIN_NOTIFICATION_SEEN_IMPACT_FOR_PRE_T_APPS = false; @@ -3073,8 +3086,11 @@ public class AppStandbyController COMPRESS_TIME ? 2 * ONE_MINUTE : 2 * ONE_HOUR; public static final long DEFAULT_SYSTEM_INTERACTION_TIMEOUT = COMPRESS_TIME ? ONE_MINUTE : 10 * ONE_MINUTE; - public static final long DEFAULT_SYNC_ADAPTER_TIMEOUT = + public static final long DEFAULT_LEGACY_SYNC_ADAPTER_TIMEOUT = COMPRESS_TIME ? ONE_MINUTE : 10 * ONE_MINUTE; + + public static final long DEFAULT_CURRENT_SYNC_ADAPTER_TIMEOUT = + COMPRESS_TIME ? ONE_MINUTE : 2 * ONE_HOUR; public static final long DEFAULT_EXEMPTED_SYNC_SCHEDULED_NON_DOZE_TIMEOUT = COMPRESS_TIME ? (ONE_MINUTE / 2) : 10 * ONE_MINUTE; public static final long DEFAULT_EXEMPTED_SYNC_SCHEDULED_DOZE_TIMEOUT = @@ -3117,6 +3133,9 @@ public class AppStandbyController cr.registerContentObserver(Global.getUriFor(Global.ADAPTIVE_BATTERY_MANAGEMENT_ENABLED), false, this); mInjector.registerDeviceConfigPropertiesChangedListener(this); + + processDefaultConstants(); + // Load all the constants. // postOneTimeCheckIdleStates() doesn't need to be called on boot. processProperties(mInjector.getDeviceConfigProperties()); @@ -3135,6 +3154,17 @@ public class AppStandbyController postOneTimeCheckIdleStates(); } + private void processDefaultConstants() { + if (!Flags.adjustDefaultBucketElevationParams()) { + return; + } + + mSlicePinnedTimeoutMillis = DEFAULT_CURRENT_SLICE_PINNED_TIMEOUT; + mSyncAdapterTimeoutMillis = DEFAULT_CURRENT_SYNC_ADAPTER_TIMEOUT; + mSyncAdapaterPromotedBucket = STANDBY_BUCKET_WORKING_SET; + mStrongUsageTimeoutMillis = DEFAULT_CURRENT_STRONG_USAGE_TIMEOUT; + } + private void processProperties(DeviceConfig.Properties properties) { boolean timeThresholdsUpdated = false; synchronized (mAppIdleLock) { @@ -3182,11 +3212,16 @@ public class AppStandbyController case KEY_SLICE_PINNED_HOLD_DURATION: mSlicePinnedTimeoutMillis = properties.getLong( KEY_SLICE_PINNED_HOLD_DURATION, - DEFAULT_SLICE_PINNED_TIMEOUT); + Flags.adjustDefaultBucketElevationParams() + ? DEFAULT_CURRENT_SLICE_PINNED_TIMEOUT + : DEFAULT_LEGACY_SLICE_PINNED_TIMEOUT); break; case KEY_STRONG_USAGE_HOLD_DURATION: mStrongUsageTimeoutMillis = properties.getLong( - KEY_STRONG_USAGE_HOLD_DURATION, DEFAULT_STRONG_USAGE_TIMEOUT); + KEY_STRONG_USAGE_HOLD_DURATION, + Flags.adjustDefaultBucketElevationParams() + ? DEFAULT_CURRENT_STRONG_USAGE_TIMEOUT + : DEFAULT_LEGACY_STRONG_USAGE_TIMEOUT); break; case KEY_PREDICTION_TIMEOUT: mPredictionTimeoutMillis = properties.getLong( @@ -3203,7 +3238,10 @@ public class AppStandbyController break; case KEY_SYNC_ADAPTER_HOLD_DURATION: mSyncAdapterTimeoutMillis = properties.getLong( - KEY_SYNC_ADAPTER_HOLD_DURATION, DEFAULT_SYNC_ADAPTER_TIMEOUT); + KEY_SYNC_ADAPTER_HOLD_DURATION, + Flags.adjustDefaultBucketElevationParams() + ? DEFAULT_CURRENT_SYNC_ADAPTER_TIMEOUT + : DEFAULT_LEGACY_SYNC_ADAPTER_TIMEOUT); break; case KEY_EXEMPTED_SYNC_SCHEDULED_DOZE_HOLD_DURATION: mExemptedSyncScheduledDozeTimeoutMillis = properties.getLong( |