summaryrefslogtreecommitdiff
path: root/apex
diff options
context:
space:
mode:
author Treehugger Robot <android-test-infra-autosubmit@system.gserviceaccount.com> 2024-11-11 22:29:06 +0000
committer Android (Google) Code Review <android-gerrit@google.com> 2024-11-11 22:29:06 +0000
commit2cd2b44fc83e652599b187179f0fb320233ba719 (patch)
treeed1ff60c7666678bb283ec86eecf1d31838c47d5 /apex
parentadb79c981344aa1afcde0002df100839ddce3544 (diff)
parentb2b7b9a4385e35756b2a83fa1cfd080363236dfd (diff)
Merge "Add a new API to fetch pending job reasons history." into main
Diffstat (limited to 'apex')
-rw-r--r--apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java12
-rw-r--r--apex/jobscheduler/framework/java/android/app/job/IJobScheduler.aidl2
-rw-r--r--apex/jobscheduler/framework/java/android/app/job/JobScheduler.java28
-rw-r--r--apex/jobscheduler/framework/java/android/app/job/PendingJobReasonsInfo.aidl19
-rw-r--r--apex/jobscheduler/framework/java/android/app/job/PendingJobReasonsInfo.java100
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java31
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java3
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java86
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;