Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c7cd8d19 authored by Kweku Adams's avatar Kweku Adams
Browse files

Add APIs to let apps attach debug info to jobs.

1. Let apps specify a specific trace tag for when their jobs are
   running.
2. Allow apps to attach some tags to a job for debugging purposes.

Bug: 293491637
Bug: 297106461
Test: atest CtsJobSchedulerTestCases:JobInfoTest
Test: atest FrameworksServicesTests:JobStoreTest
Change-Id: I7398210fc4730043f618579bad0d2e5140b912a7
parent 57ea0232
Loading
Loading
Loading
Loading
+14 −0
Original line number Diff line number Diff line
@@ -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",
+8 −0
Original line number Diff line number Diff line
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"
}
+198 −0
Original line number Diff line number Diff line
@@ -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
@@ -723,6 +740,33 @@ public class JobInfo implements Parcelable {
        return backoffPolicy;
    }

    /**
     * @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)
     */
@@ -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)
@@ -1996,6 +2120,24 @@ public class JobInfo implements Parcelable {
            return this;
        }

        /**
         * 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.
         */
@@ -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();
    }

    /**
+9 −0
Original line number Diff line number Diff line
@@ -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);
+62 −0
Original line number Diff line number Diff line
@@ -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. */
Loading