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)