diff options
author | 2024-11-08 06:26:39 +0000 | |
---|---|---|
committer | 2024-11-11 20:01:01 +0000 | |
commit | b2b7b9a4385e35756b2a83fa1cfd080363236dfd (patch) | |
tree | 87c0c84c9ec5506390d3aca3f8f064ed36ccd3d7 /apex | |
parent | f874d583413dbaa93e69073e8746627d722032a5 (diff) |
Add a new API to fetch pending job reasons history.
JobScheduler.getPendingJobReasonsHistory() will allow an app to query
its constraint history for a particular job. This will only be a
limited historical view of the constraints (currently last 10
constraint changes).
Bug: 372031023
Test: atest JobSchedulingTest
Flag: android.app.job.get_pending_job_reasons_history_api
Change-Id: I9f858925e990e6dfd6db1b79b9f6cff36d1761e9
Diffstat (limited to 'apex')
8 files changed, 275 insertions, 6 deletions
diff --git a/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java b/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java index fb5ef8771c26..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; @@ -183,6 +185,16 @@ public class JobSchedulerImpl extends JobScheduler { } @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/IJobScheduler.aidl b/apex/jobscheduler/framework/java/android/app/job/IJobScheduler.aidl index 21051b520d84..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; @@ -40,6 +41,7 @@ interface IJobScheduler { 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/JobScheduler.java b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java index bfdd15e9b0cd..4fbd55a5d528 100644 --- a/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java +++ b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java @@ -493,6 +493,34 @@ public abstract class JobScheduler { } /** + * 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/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/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java index f569388ef3c1..1c6e40e25a92 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -44,6 +44,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; @@ -2140,6 +2141,20 @@ public class JobSchedulerService extends com.android.server.SystemService 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) { synchronized (mLock) { ArraySet<JobStatus> jobs = mJobs.getJobsByUid(uid); @@ -5122,6 +5137,19 @@ public class JobSchedulerService extends com.android.server.SystemService } @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.getPendingJobReasonsHistory( + uid, validateNamespace(namespace), jobId); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + @Override public void cancelAll() throws RemoteException { final int uid = Binder.getCallingUid(); final long ident = Binder.clearCallingIdentity(); @@ -5857,6 +5885,9 @@ public class JobSchedulerService extends com.android.server.SystemService 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 a4a302450849..f3bc9c747f17 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java @@ -442,6 +442,9 @@ public final class JobSchedulerShellCommand extends BasicShellCommandHandler { 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); break; 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 58579eb0db47..b0784f1c69fd 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; @@ -72,6 +74,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; @@ -515,6 +518,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. @@ -1992,6 +1999,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; } @@ -2066,14 +2083,10 @@ public final class JobStatus { } } - /** - * This will return all potential reasons why the job is pending. - */ @NonNull - public int[] getPendingJobReasons() { + public ArrayList<Integer> constraintsToPendingJobReasons(int unsatisfiedConstraints) { final ArrayList<Integer> reasons = new ArrayList<>(); - final int unsatisfiedConstraints = ~satisfiedConstraints - & (requiredConstraints | mDynamicConstraints | IMPLICIT_CONSTRAINTS); + if ((CONSTRAINT_BACKGROUND_NOT_RESTRICTED & unsatisfiedConstraints) != 0) { // The BACKGROUND_NOT_RESTRICTED constraint could be unsatisfied either because // the app is background restricted, or because we're restricting background work @@ -2159,6 +2172,18 @@ public final class JobStatus { } } + return reasons; + } + + /** + * This will return all potential reasons why the job is pending. + */ + @NonNull + public int[] getPendingJobReasons() { + final int unsatisfiedConstraints = ~satisfiedConstraints + & (requiredConstraints | mDynamicConstraints | IMPLICIT_CONSTRAINTS); + final ArrayList<Integer> reasons = constraintsToPendingJobReasons(unsatisfiedConstraints); + if (reasons.isEmpty()) { if (getEffectiveStandbyBucket() == NEVER_INDEX) { Slog.wtf(TAG, "App in NEVER bucket querying pending job reason"); @@ -2178,6 +2203,55 @@ public final class JobStatus { 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) { + // 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); + } + 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 */ public boolean isConstraintSatisfied(int constraint) { return (satisfiedConstraints&constraint) != 0; |