Merge "Add APIs to let apps attach debug info to jobs." into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index b5f398b..ce3e985 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -36,6 +36,7 @@
":com.android.hardware.input-aconfig-java{.generated_srcjars}",
":com.android.input.flags-aconfig-java{.generated_srcjars}",
":com.android.text.flags-aconfig-java{.generated_srcjars}",
+ ":framework-jobscheduler-job.flags-aconfig-java{.generated_srcjars}",
":telecom_flags_core_java_lib{.generated_srcjars}",
":telephony_flags_core_java_lib{.generated_srcjars}",
":android.companion.virtual.flags-aconfig-java{.generated_srcjars}",
@@ -664,6 +665,19 @@
aconfig_declarations: "device_policy_aconfig_flags",
}
+// JobScheduler
+aconfig_declarations {
+ name: "framework-jobscheduler-job.flags-aconfig",
+ package: "android.app.job",
+ srcs: ["apex/jobscheduler/framework/aconfig/job.aconfig"],
+}
+
+java_aconfig_library {
+ name: "framework-jobscheduler-job.flags-aconfig-java",
+ aconfig_declarations: "framework-jobscheduler-job.flags-aconfig",
+ defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
+
// Notifications
aconfig_declarations {
name: "android.service.notification.flags-aconfig",
diff --git a/apex/jobscheduler/framework/aconfig/job.aconfig b/apex/jobscheduler/framework/aconfig/job.aconfig
new file mode 100644
index 0000000..f5e33a80
--- /dev/null
+++ b/apex/jobscheduler/framework/aconfig/job.aconfig
@@ -0,0 +1,8 @@
+package: "android.app.job"
+
+flag {
+ name: "job_debug_info_apis"
+ namespace: "backstage_power"
+ description: "Add APIs to let apps attach debug information to jobs"
+ bug: "293491637"
+}
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
index 9961c4f..742ed5f 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
@@ -26,6 +26,7 @@
import static android.util.TimeUtils.formatDuration;
import android.annotation.BytesLong;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -47,13 +48,17 @@
import android.os.Parcel;
import android.os.Parcelable;
import android.os.PersistableBundle;
+import android.os.Trace;
+import android.util.ArraySet;
import android.util.Log;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.Objects;
+import java.util.Set;
/**
* Container of data passed to the {@link android.app.job.JobScheduler} fully encapsulating the
@@ -423,6 +428,15 @@
*/
public static final int CONSTRAINT_FLAG_STORAGE_NOT_LOW = 1 << 3;
+ /** @hide */
+ public static final int MAX_NUM_DEBUG_TAGS = 32;
+
+ /** @hide */
+ public static final int MAX_DEBUG_TAG_LENGTH = 127;
+
+ /** @hide */
+ public static final int MAX_TRACE_TAG_LENGTH = Trace.MAX_SECTION_NAME_LEN;
+
@UnsupportedAppUsage
private final int jobId;
private final PersistableBundle extras;
@@ -454,6 +468,9 @@
private final int mPriority;
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
private final int flags;
+ private final ArraySet<String> mDebugTags;
+ @Nullable
+ private final String mTraceTag;
/**
* Unique job id associated with this application (uid). This is the same job ID
@@ -724,6 +741,33 @@
}
/**
+ * @see JobInfo.Builder#addDebugTag(String)
+ */
+ @FlaggedApi(Flags.FLAG_JOB_DEBUG_INFO_APIS)
+ @NonNull
+ public Set<String> getDebugTags() {
+ return Collections.unmodifiableSet(mDebugTags);
+ }
+
+ /**
+ * @see JobInfo.Builder#addDebugTag(String)
+ * @hide
+ */
+ @NonNull
+ public ArraySet<String> getDebugTagsArraySet() {
+ return mDebugTags;
+ }
+
+ /**
+ * @see JobInfo.Builder#setTraceTag(String)
+ */
+ @FlaggedApi(Flags.FLAG_JOB_DEBUG_INFO_APIS)
+ @Nullable
+ public String getTraceTag() {
+ return mTraceTag;
+ }
+
+ /**
* @see JobInfo.Builder#setExpedited(boolean)
*/
public boolean isExpedited() {
@@ -860,6 +904,12 @@
if (flags != j.flags) {
return false;
}
+ if (!mDebugTags.equals(j.mDebugTags)) {
+ return false;
+ }
+ if (!Objects.equals(mTraceTag, j.mTraceTag)) {
+ return false;
+ }
return true;
}
@@ -904,6 +954,12 @@
hashCode = 31 * hashCode + mBias;
hashCode = 31 * hashCode + mPriority;
hashCode = 31 * hashCode + flags;
+ if (mDebugTags.size() > 0) {
+ hashCode = 31 * hashCode + mDebugTags.hashCode();
+ }
+ if (mTraceTag != null) {
+ hashCode = 31 * hashCode + mTraceTag.hashCode();
+ }
return hashCode;
}
@@ -946,6 +1002,17 @@
mBias = in.readInt();
mPriority = in.readInt();
flags = in.readInt();
+ final int numDebugTags = in.readInt();
+ mDebugTags = new ArraySet<>();
+ for (int i = 0; i < numDebugTags; ++i) {
+ final String tag = in.readString();
+ if (tag == null) {
+ throw new IllegalStateException("malformed parcel");
+ }
+ mDebugTags.add(tag.intern());
+ }
+ final String traceTag = in.readString();
+ mTraceTag = traceTag == null ? null : traceTag.intern();
}
private JobInfo(JobInfo.Builder b) {
@@ -978,6 +1045,8 @@
mBias = b.mBias;
mPriority = b.mPriority;
flags = b.mFlags;
+ mDebugTags = b.mDebugTags;
+ mTraceTag = b.mTraceTag;
}
@Override
@@ -1024,6 +1093,14 @@
out.writeInt(mBias);
out.writeInt(mPriority);
out.writeInt(this.flags);
+ // Explicitly write out values here to avoid double looping to intern the strings
+ // when unparcelling.
+ final int numDebugTags = mDebugTags.size();
+ out.writeInt(numDebugTags);
+ for (int i = 0; i < numDebugTags; ++i) {
+ out.writeString(mDebugTags.valueAt(i));
+ }
+ out.writeString(mTraceTag);
}
public static final @android.annotation.NonNull Creator<JobInfo> CREATOR = new Creator<JobInfo>() {
@@ -1168,6 +1245,8 @@
private int mBackoffPolicy = DEFAULT_BACKOFF_POLICY;
/** Easy way to track whether the client has tried to set a back-off policy. */
private boolean mBackoffPolicySet = false;
+ private final ArraySet<String> mDebugTags = new ArraySet<>();
+ private String mTraceTag;
/**
* Initialize a new Builder to construct a {@link JobInfo}.
@@ -1222,6 +1301,51 @@
mPriority = job.getPriority();
}
+ /**
+ * Add a debug tag to help track what this job is for. The tags may show in debug dumps
+ * or app metrics. Do not put personally identifiable information (PII) in the tag.
+ * <p>
+ * Tags have the following requirements:
+ * <ul>
+ * <li>Tags cannot be more than 127 characters.</li>
+ * <li>
+ * Since leading and trailing whitespace can lead to hard-to-debug issues,
+ * tags should not include leading or trailing whitespace.
+ * All tags will be {@link String#trim() trimmed}.
+ * </li>
+ * <li>An empty String (after trimming) is not allowed.</li>
+ * <li>Should not have personally identifiable information (PII).</li>
+ * <li>A job cannot have more than 32 tags.</li>
+ * </ul>
+ *
+ * @param tag A debug tag that helps describe what the job is for.
+ * @return This object for method chaining
+ */
+ @FlaggedApi(Flags.FLAG_JOB_DEBUG_INFO_APIS)
+ @NonNull
+ public Builder addDebugTag(@NonNull String tag) {
+ mDebugTags.add(validateDebugTag(tag));
+ return this;
+ }
+
+ /** @hide */
+ @NonNull
+ public void addDebugTags(@NonNull Set<String> tags) {
+ mDebugTags.addAll(tags);
+ }
+
+ /**
+ * Remove a tag set via {@link #addDebugTag(String)}.
+ * @param tag The tag to remove
+ * @return This object for method chaining
+ */
+ @FlaggedApi(Flags.FLAG_JOB_DEBUG_INFO_APIS)
+ @NonNull
+ public Builder removeDebugTag(@NonNull String tag) {
+ mDebugTags.remove(tag);
+ return this;
+ }
+
/** @hide */
@NonNull
@RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS)
@@ -1997,6 +2121,24 @@
}
/**
+ * Set a tag that will be used in {@link android.os.Trace traces}.
+ * Since this is a trace tag, it must follow the rules set in
+ * {@link android.os.Trace#beginSection(String)}, such as it cannot be more
+ * than 127 Unicode code units.
+ * Additionally, since leading and trailing whitespace can lead to hard-to-debug issues,
+ * they will be {@link String#trim() trimmed}.
+ * An empty String (after trimming) is not allowed.
+ * @param traceTag The tag to use in traces.
+ * @return This object for method chaining
+ */
+ @FlaggedApi(Flags.FLAG_JOB_DEBUG_INFO_APIS)
+ @NonNull
+ public Builder setTraceTag(@Nullable String traceTag) {
+ mTraceTag = validateTraceTag(traceTag);
+ return this;
+ }
+
+ /**
* @return The job object to hand to the JobScheduler. This object is immutable.
*/
public JobInfo build() {
@@ -2209,6 +2351,62 @@
"A user-initiated data transfer job must specify a valid network type");
}
}
+
+ if (mDebugTags.size() > MAX_NUM_DEBUG_TAGS) {
+ throw new IllegalArgumentException(
+ "Can't have more than " + MAX_NUM_DEBUG_TAGS + " tags");
+ }
+ final ArraySet<String> validatedDebugTags = new ArraySet<>();
+ for (int i = 0; i < mDebugTags.size(); ++i) {
+ validatedDebugTags.add(validateDebugTag(mDebugTags.valueAt(i)));
+ }
+ mDebugTags.clear();
+ mDebugTags.addAll(validatedDebugTags);
+
+ validateTraceTag(mTraceTag);
+ }
+
+ /**
+ * Returns a sanitized debug tag if valid, or throws an exception if not.
+ * @hide
+ */
+ @NonNull
+ public static String validateDebugTag(@Nullable String debugTag) {
+ if (debugTag == null) {
+ throw new NullPointerException("debug tag cannot be null");
+ }
+ debugTag = debugTag.trim();
+ if (debugTag.isEmpty()) {
+ throw new IllegalArgumentException("debug tag cannot be empty");
+ }
+ if (debugTag.length() > MAX_DEBUG_TAG_LENGTH) {
+ throw new IllegalArgumentException(
+ "debug tag cannot be more than " + MAX_DEBUG_TAG_LENGTH + " characters");
+ }
+ return debugTag.intern();
+ }
+
+ /**
+ * Returns a sanitized trace tag if valid, or throws an exception if not.
+ * @hide
+ */
+ @Nullable
+ public static String validateTraceTag(@Nullable String traceTag) {
+ if (traceTag == null) {
+ return null;
+ }
+ traceTag = traceTag.trim();
+ if (traceTag.isEmpty()) {
+ throw new IllegalArgumentException("trace tag cannot be empty");
+ }
+ if (traceTag.length() > MAX_TRACE_TAG_LENGTH) {
+ throw new IllegalArgumentException(
+ "traceTag tag cannot be more than " + MAX_TRACE_TAG_LENGTH + " characters");
+ }
+ if (traceTag.contains("|") || traceTag.contains("\n") || traceTag.contains("\0")) {
+ throw new IllegalArgumentException("Trace tag cannot contain |, \\n, or \\0");
+ }
+ return traceTag.intern();
}
/**
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 721a8bd..6449edc 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
@@ -557,6 +557,11 @@
Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "JobScheduler",
traceTag, getId());
}
+ if (job.getAppTraceTag() != null) {
+ // Use the job's ID to distinguish traces since the ID will be unique per app.
+ Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_APP, "JobScheduler",
+ job.getAppTraceTag(), job.getJobId());
+ }
try {
mBatteryStats.noteJobStart(job.getBatteryName(), job.getSourceUid());
} catch (RemoteException e) {
@@ -1616,6 +1621,10 @@
Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_SYSTEM_SERVER, "JobScheduler",
getId());
}
+ if (completedJob.getAppTraceTag() != null) {
+ Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_APP, "JobScheduler",
+ completedJob.getJobId());
+ }
try {
mBatteryStats.noteJobFinish(mRunningJob.getBatteryName(), mRunningJob.getSourceUid(),
loggingInternalStopReason);
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 d466f0d..afcbdda 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
@@ -510,6 +510,8 @@
private static final String XML_TAG_ONEOFF = "one-off";
private static final String XML_TAG_EXTRAS = "extras";
private static final String XML_TAG_JOB_WORK_ITEM = "job-work-item";
+ private static final String XML_TAG_DEBUG_INFO = "debug-info";
+ private static final String XML_TAG_DEBUG_TAG = "debug-tag";
private void migrateJobFilesAsync() {
synchronized (mLock) {
@@ -805,6 +807,7 @@
writeExecutionCriteriaToXml(out, jobStatus);
writeBundleToXml(jobStatus.getJob().getExtras(), out);
writeJobWorkItemsToXml(out, jobStatus);
+ writeDebugInfoToXml(out, jobStatus);
out.endTag(null, XML_TAG_JOB);
numJobs++;
@@ -991,6 +994,26 @@
}
}
+ private void writeDebugInfoToXml(@NonNull TypedXmlSerializer out,
+ @NonNull JobStatus jobStatus) throws IOException, XmlPullParserException {
+ final ArraySet<String> debugTags = jobStatus.getJob().getDebugTagsArraySet();
+ final int numTags = debugTags.size();
+ final String traceTag = jobStatus.getJob().getTraceTag();
+ if (traceTag == null && numTags == 0) {
+ return;
+ }
+ out.startTag(null, XML_TAG_DEBUG_INFO);
+ if (traceTag != null) {
+ out.attribute(null, "trace-tag", traceTag);
+ }
+ for (int i = 0; i < numTags; ++i) {
+ out.startTag(null, XML_TAG_DEBUG_TAG);
+ out.attribute(null, "tag", debugTags.valueAt(i));
+ out.endTag(null, XML_TAG_DEBUG_TAG);
+ }
+ out.endTag(null, XML_TAG_DEBUG_INFO);
+ }
+
private void writeJobWorkItemsToXml(@NonNull TypedXmlSerializer out,
@NonNull JobStatus jobStatus) throws IOException, XmlPullParserException {
// Write executing first since they're technically at the front of the queue.
@@ -1449,6 +1472,18 @@
jobWorkItems = readJobWorkItemsFromXml(parser);
}
+ if (eventType == XmlPullParser.START_TAG
+ && XML_TAG_DEBUG_INFO.equals(parser.getName())) {
+ try {
+ jobBuilder.setTraceTag(parser.getAttributeValue(null, "trace-tag"));
+ } catch (Exception e) {
+ Slog.wtf(TAG, "Invalid trace tag persisted to disk", e);
+ }
+ parser.next();
+ jobBuilder.addDebugTags(readDebugTagsFromXml(parser));
+ eventType = parser.nextTag(); // Consume </debug-info>
+ }
+
final JobInfo builtJob;
try {
// Don't perform prefetch-deadline check here. Apps targeting S- shouldn't have
@@ -1721,6 +1756,33 @@
return null;
}
}
+
+ @NonNull
+ private Set<String> readDebugTagsFromXml(TypedXmlPullParser parser)
+ throws IOException, XmlPullParserException {
+ Set<String> debugTags = new ArraySet<>();
+
+ for (int eventType = parser.getEventType(); eventType != XmlPullParser.END_DOCUMENT;
+ eventType = parser.next()) {
+ final String tagName = parser.getName();
+ if (!XML_TAG_DEBUG_TAG.equals(tagName)) {
+ // We're no longer operating with debug tags.
+ break;
+ }
+ if (debugTags.size() < JobInfo.MAX_NUM_DEBUG_TAGS) {
+ final String debugTag;
+ try {
+ debugTag = JobInfo.validateDebugTag(parser.getAttributeValue(null, "tag"));
+ } catch (Exception e) {
+ Slog.wtf(TAG, "Invalid debug tag persisted to disk", e);
+ continue;
+ }
+ debugTags.add(debugTag);
+ }
+ }
+
+ return debugTags;
+ }
}
/** Set of all tracked jobs. */
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 d6ada4c..b828f39 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
@@ -1054,6 +1054,12 @@
return mLoggingJobId;
}
+ /** Returns a trace tag using debug information provided by the app. */
+ @Nullable
+ public String getAppTraceTag() {
+ return job.getTraceTag();
+ }
+
/** Returns whether this job was scheduled by one app on behalf of another. */
public boolean isProxyJob() {
return mIsProxyJob;
@@ -2763,6 +2769,15 @@
pw.println("Has late constraint");
}
+ if (job.getTraceTag() != null) {
+ pw.print("Trace tag: ");
+ pw.println(job.getTraceTag());
+ }
+ if (job.getDebugTags().size() > 0) {
+ pw.print("Debug tags: ");
+ pw.println(job.getDebugTags());
+ }
+
pw.decreaseIndent();
}
diff --git a/core/api/current.txt b/core/api/current.txt
index b9719e1..f09036b 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -8859,6 +8859,7 @@
method public int getBackoffPolicy();
method @Nullable public android.content.ClipData getClipData();
method public int getClipGrantFlags();
+ method @FlaggedApi("android.app.job.job_debug_info_apis") @NonNull public java.util.Set<java.lang.String> getDebugTags();
method public long getEstimatedNetworkDownloadBytes();
method public long getEstimatedNetworkUploadBytes();
method @NonNull public android.os.PersistableBundle getExtras();
@@ -8875,6 +8876,7 @@
method public int getPriority();
method @Nullable public android.net.NetworkRequest getRequiredNetwork();
method @NonNull public android.content.ComponentName getService();
+ method @FlaggedApi("android.app.job.job_debug_info_apis") @Nullable public String getTraceTag();
method @NonNull public android.os.Bundle getTransientExtras();
method public long getTriggerContentMaxDelay();
method public long getTriggerContentUpdateDelay();
@@ -8911,8 +8913,10 @@
public static final class JobInfo.Builder {
ctor public JobInfo.Builder(int, @NonNull android.content.ComponentName);
+ method @FlaggedApi("android.app.job.job_debug_info_apis") @NonNull public android.app.job.JobInfo.Builder addDebugTag(@NonNull String);
method public android.app.job.JobInfo.Builder addTriggerContentUri(@NonNull android.app.job.JobInfo.TriggerContentUri);
method public android.app.job.JobInfo build();
+ method @FlaggedApi("android.app.job.job_debug_info_apis") @NonNull public android.app.job.JobInfo.Builder removeDebugTag(@NonNull String);
method public android.app.job.JobInfo.Builder setBackoffCriteria(long, int);
method public android.app.job.JobInfo.Builder setClipData(@Nullable android.content.ClipData, int);
method public android.app.job.JobInfo.Builder setEstimatedNetworkBytes(long, long);
@@ -8933,6 +8937,7 @@
method public android.app.job.JobInfo.Builder setRequiresCharging(boolean);
method public android.app.job.JobInfo.Builder setRequiresDeviceIdle(boolean);
method public android.app.job.JobInfo.Builder setRequiresStorageNotLow(boolean);
+ method @FlaggedApi("android.app.job.job_debug_info_apis") @NonNull public android.app.job.JobInfo.Builder setTraceTag(@Nullable String);
method public android.app.job.JobInfo.Builder setTransientExtras(@NonNull android.os.Bundle);
method public android.app.job.JobInfo.Builder setTriggerContentMaxDelay(long);
method public android.app.job.JobInfo.Builder setTriggerContentUpdateDelay(long);
diff --git a/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java b/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java
index 2db46e6..46ead85 100644
--- a/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java
+++ b/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java
@@ -571,6 +571,29 @@
}
@Test
+ public void testDebugTagsPersisted() throws Exception {
+ JobInfo ji = new Builder(53, mComponent)
+ .setPersisted(true)
+ .addDebugTag("a")
+ .addDebugTag("b")
+ .addDebugTag("c")
+ .addDebugTag("d")
+ .removeDebugTag("d")
+ .build();
+ final JobStatus js = JobStatus.createFromJobInfo(ji, SOME_UID, null, -1, null, null);
+ mTaskStoreUnderTest.add(js);
+ waitForPendingIo();
+
+ Set<String> expectedTags = Set.of("a", "b", "c");
+
+ final JobSet jobStatusSet = new JobSet();
+ mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true);
+ JobStatus loaded = jobStatusSet.getAllJobs().iterator().next();
+ assertEquals("Debug tags not correctly persisted",
+ expectedTags, loaded.getJob().getDebugTags());
+ }
+
+ @Test
public void testNamespacePersisted() throws Exception {
final String namespace = "my.test.namespace";
JobInfo.Builder b = new Builder(93, mComponent)
@@ -675,6 +698,22 @@
}
@Test
+ public void testTraceTagPersisted() throws Exception {
+ JobInfo ji = new Builder(53, mComponent)
+ .setPersisted(true)
+ .setTraceTag("tag")
+ .build();
+ final JobStatus js = JobStatus.createFromJobInfo(ji, SOME_UID, null, -1, null, null);
+ mTaskStoreUnderTest.add(js);
+ waitForPendingIo();
+
+ final JobSet jobStatusSet = new JobSet();
+ mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true);
+ JobStatus loaded = jobStatusSet.getAllJobs().iterator().next();
+ assertEquals("Trace tag not correctly persisted", "tag", loaded.getJob().getTraceTag());
+ }
+
+ @Test
public void testEstimatedNetworkBytes() throws Exception {
assertPersistedEquals(new JobInfo.Builder(0, mComponent)
.setPersisted(true)