diff options
8 files changed, 350 insertions, 0 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index b5f398b28b9e..ce3e985e22d5 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -36,6 +36,7 @@ aconfig_srcjars = [ ":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 @@ cc_aconfig_library { 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 000000000000..f5e33a80211b --- /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 9961c4fdf3f7..742ed5f2eeb7 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.net.NetworkCapabilities.TRANSPORT_CELLULAR; 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.Bundle; 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 class JobInfo implements Parcelable { */ 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 @@ public class JobInfo implements Parcelable { 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 @@ public class JobInfo implements Parcelable { } /** + * @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 @@ public class JobInfo implements Parcelable { 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 @@ public class JobInfo implements Parcelable { 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 @@ public class JobInfo implements Parcelable { 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 @@ public class JobInfo implements Parcelable { mBias = b.mBias; mPriority = b.mPriority; flags = b.mFlags; + mDebugTags = b.mDebugTags; + mTraceTag = b.mTraceTag; } @Override @@ -1024,6 +1093,14 @@ public class JobInfo implements Parcelable { 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 @@ public class JobInfo implements Parcelable { 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 @@ public class JobInfo implements Parcelable { 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 @@ public class JobInfo implements Parcelable { } /** + * 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 @@ public class JobInfo implements Parcelable { "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 721a8bdce57a..6449edcd3103 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 @@ public final class JobServiceContext implements ServiceConnection { 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 @@ public final class JobServiceContext implements ServiceConnection { 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 d466f0d8912c..afcbddad611e 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 @@ public final class JobStore { 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 @@ public final class JobStore { 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 @@ public final class JobStore { } } + 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 @@ public final class JobStore { 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 @@ public final class JobStore { 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 d6ada4cd7fdc..b828f39a120c 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 @@ public final class JobStatus { 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 @@ public final class JobStatus { 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 b9719e10ea02..f09036bc6162 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -8859,6 +8859,7 @@ package android.app.job { 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 @@ package android.app.job { 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 @@ package android.app.job { 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 @@ package android.app.job { 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 2db46e60aea0..46ead854bded 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 @@ public class JobStoreTest { } @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 @@ public class JobStoreTest { } @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) |