1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License
15 */
16
17package com.android.server.job;
18
19import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
20import static com.android.server.job.JobSchedulerService.sSystemClock;
21
22import android.annotation.Nullable;
23import android.app.ActivityManager;
24import android.app.IActivityManager;
25import android.app.job.JobInfo;
26import android.content.ComponentName;
27import android.content.Context;
28import android.net.NetworkRequest;
29import android.os.Environment;
30import android.os.Handler;
31import android.os.PersistableBundle;
32import android.os.Process;
33import android.os.SystemClock;
34import android.os.UserHandle;
35import android.text.format.DateUtils;
36import android.util.ArraySet;
37import android.util.AtomicFile;
38import android.util.Pair;
39import android.util.Slog;
40import android.util.SparseArray;
41import android.util.Xml;
42
43import com.android.internal.annotations.VisibleForTesting;
44import com.android.internal.util.ArrayUtils;
45import com.android.internal.util.BitUtils;
46import com.android.internal.util.FastXmlSerializer;
47import com.android.server.IoThread;
48import com.android.server.LocalServices;
49import com.android.server.job.JobSchedulerInternal.JobStorePersistStats;
50import com.android.server.job.controllers.JobStatus;
51
52import org.xmlpull.v1.XmlPullParser;
53import org.xmlpull.v1.XmlPullParserException;
54import org.xmlpull.v1.XmlSerializer;
55
56import java.io.ByteArrayOutputStream;
57import java.io.File;
58import java.io.FileInputStream;
59import java.io.FileNotFoundException;
60import java.io.FileOutputStream;
61import java.io.IOException;
62import java.nio.charset.StandardCharsets;
63import java.util.ArrayList;
64import java.util.List;
65import java.util.Set;
66import java.util.function.Consumer;
67import java.util.function.Predicate;
68
69/**
70 * Maintains the master list of jobs that the job scheduler is tracking. These jobs are compared by
71 * reference, so none of the functions in this class should make a copy.
72 * Also handles read/write of persisted jobs.
73 *
74 * Note on locking:
75 *      All callers to this class must <strong>lock on the class object they are calling</strong>.
76 *      This is important b/c {@link com.android.server.job.JobStore.WriteJobsMapToDiskRunnable}
77 *      and {@link com.android.server.job.JobStore.ReadJobMapFromDiskRunnable} lock on that
78 *      object.
79 *
80 * Test:
81 * atest $ANDROID_BUILD_TOP/frameworks/base/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java
82 */
83public final class JobStore {
84    private static final String TAG = "JobStore";
85    private static final boolean DEBUG = JobSchedulerService.DEBUG;
86
87    /** Threshold to adjust how often we want to write to the db. */
88    private static final int MAX_OPS_BEFORE_WRITE = 1;
89
90    final Object mLock;
91    final JobSet mJobSet; // per-caller-uid and per-source-uid tracking
92    final Context mContext;
93
94    // Bookkeeping around incorrect boot-time system clock
95    private final long mXmlTimestamp;
96    private boolean mRtcGood;
97
98    private int mDirtyOperations;
99
100    private static final Object sSingletonLock = new Object();
101    private final AtomicFile mJobsFile;
102    /** Handler backed by IoThread for writing to disk. */
103    private final Handler mIoHandler = IoThread.getHandler();
104    private static JobStore sSingleton;
105
106    private JobStorePersistStats mPersistInfo = new JobStorePersistStats();
107
108    /** Used by the {@link JobSchedulerService} to instantiate the JobStore. */
109    static JobStore initAndGet(JobSchedulerService jobManagerService) {
110        synchronized (sSingletonLock) {
111            if (sSingleton == null) {
112                sSingleton = new JobStore(jobManagerService.getContext(),
113                        jobManagerService.getLock(), Environment.getDataDirectory());
114            }
115            return sSingleton;
116        }
117    }
118
119    /**
120     * @return A freshly initialized job store object, with no loaded jobs.
121     */
122    @VisibleForTesting
123    public static JobStore initAndGetForTesting(Context context, File dataDir) {
124        JobStore jobStoreUnderTest = new JobStore(context, new Object(), dataDir);
125        jobStoreUnderTest.clear();
126        return jobStoreUnderTest;
127    }
128
129    /**
130     * Construct the instance of the job store. This results in a blocking read from disk.
131     */
132    private JobStore(Context context, Object lock, File dataDir) {
133        mLock = lock;
134        mContext = context;
135        mDirtyOperations = 0;
136
137        File systemDir = new File(dataDir, "system");
138        File jobDir = new File(systemDir, "job");
139        jobDir.mkdirs();
140        mJobsFile = new AtomicFile(new File(jobDir, "jobs.xml"), "jobs");
141
142        mJobSet = new JobSet();
143
144        // If the current RTC is earlier than the timestamp on our persisted jobs file,
145        // we suspect that the RTC is uninitialized and so we cannot draw conclusions
146        // about persisted job scheduling.
147        //
148        // Note that if the persisted jobs file does not exist, we proceed with the
149        // assumption that the RTC is good.  This is less work and is safe: if the
150        // clock updates to sanity then we'll be saving the persisted jobs file in that
151        // correct state, which is normal; or we'll wind up writing the jobs file with
152        // an incorrect historical timestamp.  That's fine; at worst we'll reboot with
153        // a *correct* timestamp, see a bunch of overdue jobs, and run them; then
154        // settle into normal operation.
155        mXmlTimestamp = mJobsFile.getLastModifiedTime();
156        mRtcGood = (sSystemClock.millis() > mXmlTimestamp);
157
158        readJobMapFromDisk(mJobSet, mRtcGood);
159    }
160
161    public boolean jobTimesInflatedValid() {
162        return mRtcGood;
163    }
164
165    public boolean clockNowValidToInflate(long now) {
166        return now >= mXmlTimestamp;
167    }
168
169    /**
170     * Find all the jobs that were affected by RTC clock uncertainty at boot time.  Returns
171     * parallel lists of the existing JobStatus objects and of new, equivalent JobStatus instances
172     * with now-corrected time bounds.
173     */
174    public void getRtcCorrectedJobsLocked(final ArrayList<JobStatus> toAdd,
175            final ArrayList<JobStatus> toRemove) {
176        final long elapsedNow = sElapsedRealtimeClock.millis();
177
178        // Find the jobs that need to be fixed up, collecting them for post-iteration
179        // replacement with their new versions
180        forEachJob(job -> {
181            final Pair<Long, Long> utcTimes = job.getPersistedUtcTimes();
182            if (utcTimes != null) {
183                Pair<Long, Long> elapsedRuntimes =
184                        convertRtcBoundsToElapsed(utcTimes, elapsedNow);
185                toAdd.add(new JobStatus(job, job.getBaseHeartbeat(),
186                        elapsedRuntimes.first, elapsedRuntimes.second,
187                        0, job.getLastSuccessfulRunTime(), job.getLastFailedRunTime()));
188                toRemove.add(job);
189            }
190        });
191    }
192
193    /**
194     * Add a job to the master list, persisting it if necessary. If the JobStatus already exists,
195     * it will be replaced.
196     * @param jobStatus Job to add.
197     * @return Whether or not an equivalent JobStatus was replaced by this operation.
198     */
199    public boolean add(JobStatus jobStatus) {
200        boolean replaced = mJobSet.remove(jobStatus);
201        mJobSet.add(jobStatus);
202        if (jobStatus.isPersisted()) {
203            maybeWriteStatusToDiskAsync();
204        }
205        if (DEBUG) {
206            Slog.d(TAG, "Added job status to store: " + jobStatus);
207        }
208        return replaced;
209    }
210
211    boolean containsJob(JobStatus jobStatus) {
212        return mJobSet.contains(jobStatus);
213    }
214
215    public int size() {
216        return mJobSet.size();
217    }
218
219    public JobStorePersistStats getPersistStats() {
220        return mPersistInfo;
221    }
222
223    public int countJobsForUid(int uid) {
224        return mJobSet.countJobsForUid(uid);
225    }
226
227    /**
228     * Remove the provided job. Will also delete the job if it was persisted.
229     * @param writeBack If true, the job will be deleted (if it was persisted) immediately.
230     * @return Whether or not the job existed to be removed.
231     */
232    public boolean remove(JobStatus jobStatus, boolean writeBack) {
233        boolean removed = mJobSet.remove(jobStatus);
234        if (!removed) {
235            if (DEBUG) {
236                Slog.d(TAG, "Couldn't remove job: didn't exist: " + jobStatus);
237            }
238            return false;
239        }
240        if (writeBack && jobStatus.isPersisted()) {
241            maybeWriteStatusToDiskAsync();
242        }
243        return removed;
244    }
245
246    /**
247     * Remove the jobs of users not specified in the whitelist.
248     * @param whitelist Array of User IDs whose jobs are not to be removed.
249     */
250    public void removeJobsOfNonUsers(int[] whitelist) {
251        mJobSet.removeJobsOfNonUsers(whitelist);
252    }
253
254    @VisibleForTesting
255    public void clear() {
256        mJobSet.clear();
257        maybeWriteStatusToDiskAsync();
258    }
259
260    /**
261     * @param userHandle User for whom we are querying the list of jobs.
262     * @return A list of all the jobs scheduled for the provided user. Never null.
263     */
264    public List<JobStatus> getJobsByUser(int userHandle) {
265        return mJobSet.getJobsByUser(userHandle);
266    }
267
268    /**
269     * @param uid Uid of the requesting app.
270     * @return All JobStatus objects for a given uid from the master list. Never null.
271     */
272    public List<JobStatus> getJobsByUid(int uid) {
273        return mJobSet.getJobsByUid(uid);
274    }
275
276    /**
277     * @param uid Uid of the requesting app.
278     * @param jobId Job id, specified at schedule-time.
279     * @return the JobStatus that matches the provided uId and jobId, or null if none found.
280     */
281    public JobStatus getJobByUidAndJobId(int uid, int jobId) {
282        return mJobSet.get(uid, jobId);
283    }
284
285    /**
286     * Iterate over the set of all jobs, invoking the supplied functor on each.  This is for
287     * customers who need to examine each job; we'd much rather not have to generate
288     * transient unified collections for them to iterate over and then discard, or creating
289     * iterators every time a client needs to perform a sweep.
290     */
291    public void forEachJob(Consumer<JobStatus> functor) {
292        mJobSet.forEachJob(null, functor);
293    }
294
295    public void forEachJob(@Nullable Predicate<JobStatus> filterPredicate,
296            Consumer<JobStatus> functor) {
297        mJobSet.forEachJob(filterPredicate, functor);
298    }
299
300    public void forEachJob(int uid, Consumer<JobStatus> functor) {
301        mJobSet.forEachJob(uid, functor);
302    }
303
304    public void forEachJobForSourceUid(int sourceUid, Consumer<JobStatus> functor) {
305        mJobSet.forEachJobForSourceUid(sourceUid, functor);
306    }
307
308    /** Version of the db schema. */
309    private static final int JOBS_FILE_VERSION = 0;
310    /** Tag corresponds to constraints this job needs. */
311    private static final String XML_TAG_PARAMS_CONSTRAINTS = "constraints";
312    /** Tag corresponds to execution parameters. */
313    private static final String XML_TAG_PERIODIC = "periodic";
314    private static final String XML_TAG_ONEOFF = "one-off";
315    private static final String XML_TAG_EXTRAS = "extras";
316
317    /**
318     * Every time the state changes we write all the jobs in one swath, instead of trying to
319     * track incremental changes.
320     */
321    private void maybeWriteStatusToDiskAsync() {
322        mDirtyOperations++;
323        if (mDirtyOperations >= MAX_OPS_BEFORE_WRITE) {
324            if (DEBUG) {
325                Slog.v(TAG, "Writing jobs to disk.");
326            }
327            mIoHandler.removeCallbacks(mWriteRunnable);
328            mIoHandler.post(mWriteRunnable);
329        }
330    }
331
332    @VisibleForTesting
333    public void readJobMapFromDisk(JobSet jobSet, boolean rtcGood) {
334        new ReadJobMapFromDiskRunnable(jobSet, rtcGood).run();
335    }
336
337    /**
338     * Runnable that writes {@link #mJobSet} out to xml.
339     * NOTE: This Runnable locks on mLock
340     */
341    private final Runnable mWriteRunnable = new Runnable() {
342        @Override
343        public void run() {
344            final long startElapsed = sElapsedRealtimeClock.millis();
345            final List<JobStatus> storeCopy = new ArrayList<JobStatus>();
346            synchronized (mLock) {
347                // Clone the jobs so we can release the lock before writing.
348                mJobSet.forEachJob(null, (job) -> {
349                    if (job.isPersisted()) {
350                        storeCopy.add(new JobStatus(job));
351                    }
352                });
353            }
354            writeJobsMapImpl(storeCopy);
355            if (DEBUG) {
356                Slog.v(TAG, "Finished writing, took " + (sElapsedRealtimeClock.millis()
357                        - startElapsed) + "ms");
358            }
359        }
360
361        private void writeJobsMapImpl(List<JobStatus> jobList) {
362            int numJobs = 0;
363            int numSystemJobs = 0;
364            int numSyncJobs = 0;
365            try {
366                final long startTime = SystemClock.uptimeMillis();
367                ByteArrayOutputStream baos = new ByteArrayOutputStream();
368                XmlSerializer out = new FastXmlSerializer();
369                out.setOutput(baos, StandardCharsets.UTF_8.name());
370                out.startDocument(null, true);
371                out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
372
373                out.startTag(null, "job-info");
374                out.attribute(null, "version", Integer.toString(JOBS_FILE_VERSION));
375                for (int i=0; i<jobList.size(); i++) {
376                    JobStatus jobStatus = jobList.get(i);
377                    if (DEBUG) {
378                        Slog.d(TAG, "Saving job " + jobStatus.getJobId());
379                    }
380                    out.startTag(null, "job");
381                    addAttributesToJobTag(out, jobStatus);
382                    writeConstraintsToXml(out, jobStatus);
383                    writeExecutionCriteriaToXml(out, jobStatus);
384                    writeBundleToXml(jobStatus.getJob().getExtras(), out);
385                    out.endTag(null, "job");
386
387                    numJobs++;
388                    if (jobStatus.getUid() == Process.SYSTEM_UID) {
389                        numSystemJobs++;
390                        if (isSyncJob(jobStatus)) {
391                            numSyncJobs++;
392                        }
393                    }
394                }
395                out.endTag(null, "job-info");
396                out.endDocument();
397
398                // Write out to disk in one fell swoop.
399                FileOutputStream fos = mJobsFile.startWrite(startTime);
400                fos.write(baos.toByteArray());
401                mJobsFile.finishWrite(fos);
402                mDirtyOperations = 0;
403            } catch (IOException e) {
404                if (DEBUG) {
405                    Slog.v(TAG, "Error writing out job data.", e);
406                }
407            } catch (XmlPullParserException e) {
408                if (DEBUG) {
409                    Slog.d(TAG, "Error persisting bundle.", e);
410                }
411            } finally {
412                mPersistInfo.countAllJobsSaved = numJobs;
413                mPersistInfo.countSystemServerJobsSaved = numSystemJobs;
414                mPersistInfo.countSystemSyncManagerJobsSaved = numSyncJobs;
415            }
416        }
417
418        /** Write out a tag with data comprising the required fields and priority of this job and
419         * its client.
420         */
421        private void addAttributesToJobTag(XmlSerializer out, JobStatus jobStatus)
422                throws IOException {
423            out.attribute(null, "jobid", Integer.toString(jobStatus.getJobId()));
424            out.attribute(null, "package", jobStatus.getServiceComponent().getPackageName());
425            out.attribute(null, "class", jobStatus.getServiceComponent().getClassName());
426            if (jobStatus.getSourcePackageName() != null) {
427                out.attribute(null, "sourcePackageName", jobStatus.getSourcePackageName());
428            }
429            if (jobStatus.getSourceTag() != null) {
430                out.attribute(null, "sourceTag", jobStatus.getSourceTag());
431            }
432            out.attribute(null, "sourceUserId", String.valueOf(jobStatus.getSourceUserId()));
433            out.attribute(null, "uid", Integer.toString(jobStatus.getUid()));
434            out.attribute(null, "priority", String.valueOf(jobStatus.getPriority()));
435            out.attribute(null, "flags", String.valueOf(jobStatus.getFlags()));
436            if (jobStatus.getInternalFlags() != 0) {
437                out.attribute(null, "internalFlags", String.valueOf(jobStatus.getInternalFlags()));
438            }
439
440            out.attribute(null, "lastSuccessfulRunTime",
441                    String.valueOf(jobStatus.getLastSuccessfulRunTime()));
442            out.attribute(null, "lastFailedRunTime",
443                    String.valueOf(jobStatus.getLastFailedRunTime()));
444        }
445
446        private void writeBundleToXml(PersistableBundle extras, XmlSerializer out)
447                throws IOException, XmlPullParserException {
448            out.startTag(null, XML_TAG_EXTRAS);
449            PersistableBundle extrasCopy = deepCopyBundle(extras, 10);
450            extrasCopy.saveToXml(out);
451            out.endTag(null, XML_TAG_EXTRAS);
452        }
453
454        private PersistableBundle deepCopyBundle(PersistableBundle bundle, int maxDepth) {
455            if (maxDepth <= 0) {
456                return null;
457            }
458            PersistableBundle copy = (PersistableBundle) bundle.clone();
459            Set<String> keySet = bundle.keySet();
460            for (String key: keySet) {
461                Object o = copy.get(key);
462                if (o instanceof PersistableBundle) {
463                    PersistableBundle bCopy = deepCopyBundle((PersistableBundle) o, maxDepth-1);
464                    copy.putPersistableBundle(key, bCopy);
465                }
466            }
467            return copy;
468        }
469
470        /**
471         * Write out a tag with data identifying this job's constraints. If the constraint isn't here
472         * it doesn't apply.
473         */
474        private void writeConstraintsToXml(XmlSerializer out, JobStatus jobStatus) throws IOException {
475            out.startTag(null, XML_TAG_PARAMS_CONSTRAINTS);
476            if (jobStatus.hasConnectivityConstraint()) {
477                final NetworkRequest network = jobStatus.getJob().getRequiredNetwork();
478                out.attribute(null, "net-capabilities", Long.toString(
479                        BitUtils.packBits(network.networkCapabilities.getCapabilities())));
480                out.attribute(null, "net-unwanted-capabilities", Long.toString(
481                        BitUtils.packBits(network.networkCapabilities.getUnwantedCapabilities())));
482
483                out.attribute(null, "net-transport-types", Long.toString(
484                        BitUtils.packBits(network.networkCapabilities.getTransportTypes())));
485            }
486            if (jobStatus.hasIdleConstraint()) {
487                out.attribute(null, "idle", Boolean.toString(true));
488            }
489            if (jobStatus.hasChargingConstraint()) {
490                out.attribute(null, "charging", Boolean.toString(true));
491            }
492            if (jobStatus.hasBatteryNotLowConstraint()) {
493                out.attribute(null, "battery-not-low", Boolean.toString(true));
494            }
495            out.endTag(null, XML_TAG_PARAMS_CONSTRAINTS);
496        }
497
498        private void writeExecutionCriteriaToXml(XmlSerializer out, JobStatus jobStatus)
499                throws IOException {
500            final JobInfo job = jobStatus.getJob();
501            if (jobStatus.getJob().isPeriodic()) {
502                out.startTag(null, XML_TAG_PERIODIC);
503                out.attribute(null, "period", Long.toString(job.getIntervalMillis()));
504                out.attribute(null, "flex", Long.toString(job.getFlexMillis()));
505            } else {
506                out.startTag(null, XML_TAG_ONEOFF);
507            }
508
509            // If we still have the persisted times, we need to record those directly because
510            // we haven't yet been able to calculate the usual elapsed-timebase bounds
511            // correctly due to wall-clock uncertainty.
512            Pair <Long, Long> utcJobTimes = jobStatus.getPersistedUtcTimes();
513            if (DEBUG && utcJobTimes != null) {
514                Slog.i(TAG, "storing original UTC timestamps for " + jobStatus);
515            }
516
517            final long nowRTC = sSystemClock.millis();
518            final long nowElapsed = sElapsedRealtimeClock.millis();
519            if (jobStatus.hasDeadlineConstraint()) {
520                // Wall clock deadline.
521                final long deadlineWallclock = (utcJobTimes == null)
522                        ? nowRTC + (jobStatus.getLatestRunTimeElapsed() - nowElapsed)
523                        : utcJobTimes.second;
524                out.attribute(null, "deadline", Long.toString(deadlineWallclock));
525            }
526            if (jobStatus.hasTimingDelayConstraint()) {
527                final long delayWallclock = (utcJobTimes == null)
528                        ? nowRTC + (jobStatus.getEarliestRunTime() - nowElapsed)
529                        : utcJobTimes.first;
530                out.attribute(null, "delay", Long.toString(delayWallclock));
531            }
532
533            // Only write out back-off policy if it differs from the default.
534            // This also helps the case where the job is idle -> these aren't allowed to specify
535            // back-off.
536            if (jobStatus.getJob().getInitialBackoffMillis() != JobInfo.DEFAULT_INITIAL_BACKOFF_MILLIS
537                    || jobStatus.getJob().getBackoffPolicy() != JobInfo.DEFAULT_BACKOFF_POLICY) {
538                out.attribute(null, "backoff-policy", Integer.toString(job.getBackoffPolicy()));
539                out.attribute(null, "initial-backoff", Long.toString(job.getInitialBackoffMillis()));
540            }
541            if (job.isPeriodic()) {
542                out.endTag(null, XML_TAG_PERIODIC);
543            } else {
544                out.endTag(null, XML_TAG_ONEOFF);
545            }
546        }
547    };
548
549    /**
550     * Translate the supplied RTC times to the elapsed timebase, with clamping appropriate
551     * to interpreting them as a job's delay + deadline times for alarm-setting purposes.
552     * @param rtcTimes a Pair<Long, Long> in which {@code first} is the "delay" earliest
553     *     allowable runtime for the job, and {@code second} is the "deadline" time at which
554     *     the job becomes overdue.
555     */
556    private static Pair<Long, Long> convertRtcBoundsToElapsed(Pair<Long, Long> rtcTimes,
557            long nowElapsed) {
558        final long nowWallclock = sSystemClock.millis();
559        final long earliest = (rtcTimes.first > JobStatus.NO_EARLIEST_RUNTIME)
560                ? nowElapsed + Math.max(rtcTimes.first - nowWallclock, 0)
561                : JobStatus.NO_EARLIEST_RUNTIME;
562        final long latest = (rtcTimes.second < JobStatus.NO_LATEST_RUNTIME)
563                ? nowElapsed + Math.max(rtcTimes.second - nowWallclock, 0)
564                : JobStatus.NO_LATEST_RUNTIME;
565        return Pair.create(earliest, latest);
566    }
567
568    private static boolean isSyncJob(JobStatus status) {
569        return com.android.server.content.SyncJobService.class.getName()
570                .equals(status.getServiceComponent().getClassName());
571    }
572
573    /**
574     * Runnable that reads list of persisted job from xml. This is run once at start up, so doesn't
575     * need to go through {@link JobStore#add(com.android.server.job.controllers.JobStatus)}.
576     */
577    private final class ReadJobMapFromDiskRunnable implements Runnable {
578        private final JobSet jobSet;
579        private final boolean rtcGood;
580
581        /**
582         * @param jobSet Reference to the (empty) set of JobStatus objects that back the JobStore,
583         *               so that after disk read we can populate it directly.
584         */
585        ReadJobMapFromDiskRunnable(JobSet jobSet, boolean rtcIsGood) {
586            this.jobSet = jobSet;
587            this.rtcGood = rtcIsGood;
588        }
589
590        @Override
591        public void run() {
592            int numJobs = 0;
593            int numSystemJobs = 0;
594            int numSyncJobs = 0;
595            try {
596                List<JobStatus> jobs;
597                FileInputStream fis = mJobsFile.openRead();
598                synchronized (mLock) {
599                    jobs = readJobMapImpl(fis, rtcGood);
600                    if (jobs != null) {
601                        long now = sElapsedRealtimeClock.millis();
602                        IActivityManager am = ActivityManager.getService();
603                        for (int i=0; i<jobs.size(); i++) {
604                            JobStatus js = jobs.get(i);
605                            js.prepareLocked(am);
606                            js.enqueueTime = now;
607                            this.jobSet.add(js);
608
609                            numJobs++;
610                            if (js.getUid() == Process.SYSTEM_UID) {
611                                numSystemJobs++;
612                                if (isSyncJob(js)) {
613                                    numSyncJobs++;
614                                }
615                            }
616                        }
617                    }
618                }
619                fis.close();
620            } catch (FileNotFoundException e) {
621                if (DEBUG) {
622                    Slog.d(TAG, "Could not find jobs file, probably there was nothing to load.");
623                }
624            } catch (XmlPullParserException | IOException e) {
625                Slog.wtf(TAG, "Error jobstore xml.", e);
626            } finally {
627                if (mPersistInfo.countAllJobsLoaded < 0) { // Only set them once.
628                    mPersistInfo.countAllJobsLoaded = numJobs;
629                    mPersistInfo.countSystemServerJobsLoaded = numSystemJobs;
630                    mPersistInfo.countSystemSyncManagerJobsLoaded = numSyncJobs;
631                }
632            }
633            Slog.i(TAG, "Read " + numJobs + " jobs");
634        }
635
636        private List<JobStatus> readJobMapImpl(FileInputStream fis, boolean rtcIsGood)
637                throws XmlPullParserException, IOException {
638            XmlPullParser parser = Xml.newPullParser();
639            parser.setInput(fis, StandardCharsets.UTF_8.name());
640
641            int eventType = parser.getEventType();
642            while (eventType != XmlPullParser.START_TAG &&
643                    eventType != XmlPullParser.END_DOCUMENT) {
644                eventType = parser.next();
645                Slog.d(TAG, "Start tag: " + parser.getName());
646            }
647            if (eventType == XmlPullParser.END_DOCUMENT) {
648                if (DEBUG) {
649                    Slog.d(TAG, "No persisted jobs.");
650                }
651                return null;
652            }
653
654            String tagName = parser.getName();
655            if ("job-info".equals(tagName)) {
656                final List<JobStatus> jobs = new ArrayList<JobStatus>();
657                // Read in version info.
658                try {
659                    int version = Integer.parseInt(parser.getAttributeValue(null, "version"));
660                    if (version != JOBS_FILE_VERSION) {
661                        Slog.d(TAG, "Invalid version number, aborting jobs file read.");
662                        return null;
663                    }
664                } catch (NumberFormatException e) {
665                    Slog.e(TAG, "Invalid version number, aborting jobs file read.");
666                    return null;
667                }
668                eventType = parser.next();
669                do {
670                    // Read each <job/>
671                    if (eventType == XmlPullParser.START_TAG) {
672                        tagName = parser.getName();
673                        // Start reading job.
674                        if ("job".equals(tagName)) {
675                            JobStatus persistedJob = restoreJobFromXml(rtcIsGood, parser);
676                            if (persistedJob != null) {
677                                if (DEBUG) {
678                                    Slog.d(TAG, "Read out " + persistedJob);
679                                }
680                                jobs.add(persistedJob);
681                            } else {
682                                Slog.d(TAG, "Error reading job from file.");
683                            }
684                        }
685                    }
686                    eventType = parser.next();
687                } while (eventType != XmlPullParser.END_DOCUMENT);
688                return jobs;
689            }
690            return null;
691        }
692
693        /**
694         * @param parser Xml parser at the beginning of a "<job/>" tag. The next "parser.next()" call
695         *               will take the parser into the body of the job tag.
696         * @return Newly instantiated job holding all the information we just read out of the xml tag.
697         */
698        private JobStatus restoreJobFromXml(boolean rtcIsGood, XmlPullParser parser)
699                throws XmlPullParserException, IOException {
700            JobInfo.Builder jobBuilder;
701            int uid, sourceUserId;
702            long lastSuccessfulRunTime;
703            long lastFailedRunTime;
704            int internalFlags = 0;
705
706            // Read out job identifier attributes and priority.
707            try {
708                jobBuilder = buildBuilderFromXml(parser);
709                jobBuilder.setPersisted(true);
710                uid = Integer.parseInt(parser.getAttributeValue(null, "uid"));
711
712                String val = parser.getAttributeValue(null, "priority");
713                if (val != null) {
714                    jobBuilder.setPriority(Integer.parseInt(val));
715                }
716                val = parser.getAttributeValue(null, "flags");
717                if (val != null) {
718                    jobBuilder.setFlags(Integer.parseInt(val));
719                }
720                val = parser.getAttributeValue(null, "internalFlags");
721                if (val != null) {
722                    internalFlags = Integer.parseInt(val);
723                }
724                val = parser.getAttributeValue(null, "sourceUserId");
725                sourceUserId = val == null ? -1 : Integer.parseInt(val);
726
727                val = parser.getAttributeValue(null, "lastSuccessfulRunTime");
728                lastSuccessfulRunTime = val == null ? 0 : Long.parseLong(val);
729
730                val = parser.getAttributeValue(null, "lastFailedRunTime");
731                lastFailedRunTime = val == null ? 0 : Long.parseLong(val);
732            } catch (NumberFormatException e) {
733                Slog.e(TAG, "Error parsing job's required fields, skipping");
734                return null;
735            }
736
737            String sourcePackageName = parser.getAttributeValue(null, "sourcePackageName");
738            final String sourceTag = parser.getAttributeValue(null, "sourceTag");
739
740            int eventType;
741            // Read out constraints tag.
742            do {
743                eventType = parser.next();
744            } while (eventType == XmlPullParser.TEXT);  // Push through to next START_TAG.
745
746            if (!(eventType == XmlPullParser.START_TAG &&
747                    XML_TAG_PARAMS_CONSTRAINTS.equals(parser.getName()))) {
748                // Expecting a <constraints> start tag.
749                return null;
750            }
751            try {
752                buildConstraintsFromXml(jobBuilder, parser);
753            } catch (NumberFormatException e) {
754                Slog.d(TAG, "Error reading constraints, skipping.");
755                return null;
756            }
757            parser.next(); // Consume </constraints>
758
759            // Read out execution parameters tag.
760            do {
761                eventType = parser.next();
762            } while (eventType == XmlPullParser.TEXT);
763            if (eventType != XmlPullParser.START_TAG) {
764                return null;
765            }
766
767            // Tuple of (earliest runtime, latest runtime) in UTC.
768            final Pair<Long, Long> rtcRuntimes;
769            try {
770                rtcRuntimes = buildRtcExecutionTimesFromXml(parser);
771            } catch (NumberFormatException e) {
772                if (DEBUG) {
773                    Slog.d(TAG, "Error parsing execution time parameters, skipping.");
774                }
775                return null;
776            }
777
778            final long elapsedNow = sElapsedRealtimeClock.millis();
779            Pair<Long, Long> elapsedRuntimes = convertRtcBoundsToElapsed(rtcRuntimes, elapsedNow);
780
781            if (XML_TAG_PERIODIC.equals(parser.getName())) {
782                try {
783                    String val = parser.getAttributeValue(null, "period");
784                    final long periodMillis = Long.parseLong(val);
785                    val = parser.getAttributeValue(null, "flex");
786                    final long flexMillis = (val != null) ? Long.valueOf(val) : periodMillis;
787                    jobBuilder.setPeriodic(periodMillis, flexMillis);
788                    // As a sanity check, cap the recreated run time to be no later than flex+period
789                    // from now. This is the latest the periodic could be pushed out. This could
790                    // happen if the periodic ran early (at flex time before period), and then the
791                    // device rebooted.
792                    if (elapsedRuntimes.second > elapsedNow + periodMillis + flexMillis) {
793                        final long clampedLateRuntimeElapsed = elapsedNow + flexMillis
794                                + periodMillis;
795                        final long clampedEarlyRuntimeElapsed = clampedLateRuntimeElapsed
796                                - flexMillis;
797                        Slog.w(TAG,
798                                String.format("Periodic job for uid='%d' persisted run-time is" +
799                                                " too big [%s, %s]. Clamping to [%s,%s]",
800                                        uid,
801                                        DateUtils.formatElapsedTime(elapsedRuntimes.first / 1000),
802                                        DateUtils.formatElapsedTime(elapsedRuntimes.second / 1000),
803                                        DateUtils.formatElapsedTime(
804                                                clampedEarlyRuntimeElapsed / 1000),
805                                        DateUtils.formatElapsedTime(
806                                                clampedLateRuntimeElapsed / 1000))
807                        );
808                        elapsedRuntimes =
809                                Pair.create(clampedEarlyRuntimeElapsed, clampedLateRuntimeElapsed);
810                    }
811                } catch (NumberFormatException e) {
812                    Slog.d(TAG, "Error reading periodic execution criteria, skipping.");
813                    return null;
814                }
815            } else if (XML_TAG_ONEOFF.equals(parser.getName())) {
816                try {
817                    if (elapsedRuntimes.first != JobStatus.NO_EARLIEST_RUNTIME) {
818                        jobBuilder.setMinimumLatency(elapsedRuntimes.first - elapsedNow);
819                    }
820                    if (elapsedRuntimes.second != JobStatus.NO_LATEST_RUNTIME) {
821                        jobBuilder.setOverrideDeadline(
822                                elapsedRuntimes.second - elapsedNow);
823                    }
824                } catch (NumberFormatException e) {
825                    Slog.d(TAG, "Error reading job execution criteria, skipping.");
826                    return null;
827                }
828            } else {
829                if (DEBUG) {
830                    Slog.d(TAG, "Invalid parameter tag, skipping - " + parser.getName());
831                }
832                // Expecting a parameters start tag.
833                return null;
834            }
835            maybeBuildBackoffPolicyFromXml(jobBuilder, parser);
836
837            parser.nextTag(); // Consume parameters end tag.
838
839            // Read out extras Bundle.
840            do {
841                eventType = parser.next();
842            } while (eventType == XmlPullParser.TEXT);
843            if (!(eventType == XmlPullParser.START_TAG
844                    && XML_TAG_EXTRAS.equals(parser.getName()))) {
845                if (DEBUG) {
846                    Slog.d(TAG, "Error reading extras, skipping.");
847                }
848                return null;
849            }
850
851            PersistableBundle extras = PersistableBundle.restoreFromXml(parser);
852            jobBuilder.setExtras(extras);
853            parser.nextTag(); // Consume </extras>
854
855            // Migrate sync jobs forward from earlier, incomplete representation
856            if ("android".equals(sourcePackageName)
857                    && extras != null
858                    && extras.getBoolean("SyncManagerJob", false)) {
859                sourcePackageName = extras.getString("owningPackage", sourcePackageName);
860                if (DEBUG) {
861                    Slog.i(TAG, "Fixing up sync job source package name from 'android' to '"
862                            + sourcePackageName + "'");
863                }
864            }
865
866            // And now we're done
867            JobSchedulerInternal service = LocalServices.getService(JobSchedulerInternal.class);
868            final int appBucket = JobSchedulerService.standbyBucketForPackage(sourcePackageName,
869                    sourceUserId, elapsedNow);
870            long currentHeartbeat = service != null ? service.currentHeartbeat() : 0;
871            JobStatus js = new JobStatus(
872                    jobBuilder.build(), uid, sourcePackageName, sourceUserId,
873                    appBucket, currentHeartbeat, sourceTag,
874                    elapsedRuntimes.first, elapsedRuntimes.second,
875                    lastSuccessfulRunTime, lastFailedRunTime,
876                    (rtcIsGood) ? null : rtcRuntimes, internalFlags);
877            return js;
878        }
879
880        private JobInfo.Builder buildBuilderFromXml(XmlPullParser parser) throws NumberFormatException {
881            // Pull out required fields from <job> attributes.
882            int jobId = Integer.parseInt(parser.getAttributeValue(null, "jobid"));
883            String packageName = parser.getAttributeValue(null, "package");
884            String className = parser.getAttributeValue(null, "class");
885            ComponentName cname = new ComponentName(packageName, className);
886
887            return new JobInfo.Builder(jobId, cname);
888        }
889
890        private void buildConstraintsFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser) {
891            String val;
892
893            final String netCapabilities = parser.getAttributeValue(null, "net-capabilities");
894            final String netUnwantedCapabilities = parser.getAttributeValue(
895                    null, "net-unwanted-capabilities");
896            final String netTransportTypes = parser.getAttributeValue(null, "net-transport-types");
897            if (netCapabilities != null && netTransportTypes != null) {
898                final NetworkRequest request = new NetworkRequest.Builder().build();
899                final long unwantedCapabilities = netUnwantedCapabilities != null
900                        ? Long.parseLong(netUnwantedCapabilities)
901                        : BitUtils.packBits(request.networkCapabilities.getUnwantedCapabilities());
902
903                // We're okay throwing NFE here; caught by caller
904                request.networkCapabilities.setCapabilities(
905                        BitUtils.unpackBits(Long.parseLong(netCapabilities)),
906                        BitUtils.unpackBits(unwantedCapabilities));
907                request.networkCapabilities.setTransportTypes(
908                        BitUtils.unpackBits(Long.parseLong(netTransportTypes)));
909                jobBuilder.setRequiredNetwork(request);
910            } else {
911                // Read legacy values
912                val = parser.getAttributeValue(null, "connectivity");
913                if (val != null) {
914                    jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
915                }
916                val = parser.getAttributeValue(null, "metered");
917                if (val != null) {
918                    jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_METERED);
919                }
920                val = parser.getAttributeValue(null, "unmetered");
921                if (val != null) {
922                    jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED);
923                }
924                val = parser.getAttributeValue(null, "not-roaming");
925                if (val != null) {
926                    jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_NOT_ROAMING);
927                }
928            }
929
930            val = parser.getAttributeValue(null, "idle");
931            if (val != null) {
932                jobBuilder.setRequiresDeviceIdle(true);
933            }
934            val = parser.getAttributeValue(null, "charging");
935            if (val != null) {
936                jobBuilder.setRequiresCharging(true);
937            }
938        }
939
940        /**
941         * Builds the back-off policy out of the params tag. These attributes may not exist, depending
942         * on whether the back-off was set when the job was first scheduled.
943         */
944        private void maybeBuildBackoffPolicyFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser) {
945            String val = parser.getAttributeValue(null, "initial-backoff");
946            if (val != null) {
947                long initialBackoff = Long.parseLong(val);
948                val = parser.getAttributeValue(null, "backoff-policy");
949                int backoffPolicy = Integer.parseInt(val);  // Will throw NFE which we catch higher up.
950                jobBuilder.setBackoffCriteria(initialBackoff, backoffPolicy);
951            }
952        }
953
954        /**
955         * Extract a job's earliest/latest run time data from XML.  These are returned in
956         * unadjusted UTC wall clock time, because we do not yet know whether the system
957         * clock is reliable for purposes of calculating deltas from 'now'.
958         *
959         * @param parser
960         * @return A Pair of timestamps in UTC wall-clock time.  The first is the earliest
961         *     time at which the job is to become runnable, and the second is the deadline at
962         *     which it becomes overdue to execute.
963         * @throws NumberFormatException
964         */
965        private Pair<Long, Long> buildRtcExecutionTimesFromXml(XmlPullParser parser)
966                throws NumberFormatException {
967            String val;
968            // Pull out execution time data.
969            val = parser.getAttributeValue(null, "delay");
970            final long earliestRunTimeRtc = (val != null)
971                    ? Long.parseLong(val)
972                    : JobStatus.NO_EARLIEST_RUNTIME;
973            val = parser.getAttributeValue(null, "deadline");
974            final long latestRunTimeRtc = (val != null)
975                    ? Long.parseLong(val)
976                    : JobStatus.NO_LATEST_RUNTIME;
977            return Pair.create(earliestRunTimeRtc, latestRunTimeRtc);
978        }
979
980        /**
981         * Convenience function to read out and convert deadline and delay from xml into elapsed real
982         * time.
983         * @return A {@link android.util.Pair}, where the first value is the earliest elapsed runtime
984         * and the second is the latest elapsed runtime.
985         */
986        private Pair<Long, Long> buildExecutionTimesFromXml(XmlPullParser parser)
987                throws NumberFormatException {
988            // Pull out execution time data.
989            final long nowWallclock = sSystemClock.millis();
990            final long nowElapsed = sElapsedRealtimeClock.millis();
991
992            long earliestRunTimeElapsed = JobStatus.NO_EARLIEST_RUNTIME;
993            long latestRunTimeElapsed = JobStatus.NO_LATEST_RUNTIME;
994            String val = parser.getAttributeValue(null, "deadline");
995            if (val != null) {
996                long latestRuntimeWallclock = Long.parseLong(val);
997                long maxDelayElapsed =
998                        Math.max(latestRuntimeWallclock - nowWallclock, 0);
999                latestRunTimeElapsed = nowElapsed + maxDelayElapsed;
1000            }
1001            val = parser.getAttributeValue(null, "delay");
1002            if (val != null) {
1003                long earliestRuntimeWallclock = Long.parseLong(val);
1004                long minDelayElapsed =
1005                        Math.max(earliestRuntimeWallclock - nowWallclock, 0);
1006                earliestRunTimeElapsed = nowElapsed + minDelayElapsed;
1007
1008            }
1009            return Pair.create(earliestRunTimeElapsed, latestRunTimeElapsed);
1010        }
1011    }
1012
1013    static final class JobSet {
1014        @VisibleForTesting // Key is the getUid() originator of the jobs in each sheaf
1015        final SparseArray<ArraySet<JobStatus>> mJobs;
1016
1017        @VisibleForTesting // Same data but with the key as getSourceUid() of the jobs in each sheaf
1018        final SparseArray<ArraySet<JobStatus>> mJobsPerSourceUid;
1019
1020        public JobSet() {
1021            mJobs = new SparseArray<ArraySet<JobStatus>>();
1022            mJobsPerSourceUid = new SparseArray<>();
1023        }
1024
1025        public List<JobStatus> getJobsByUid(int uid) {
1026            ArrayList<JobStatus> matchingJobs = new ArrayList<JobStatus>();
1027            ArraySet<JobStatus> jobs = mJobs.get(uid);
1028            if (jobs != null) {
1029                matchingJobs.addAll(jobs);
1030            }
1031            return matchingJobs;
1032        }
1033
1034        // By user, not by uid, so we need to traverse by key and check
1035        public List<JobStatus> getJobsByUser(int userId) {
1036            final ArrayList<JobStatus> result = new ArrayList<JobStatus>();
1037            for (int i = mJobsPerSourceUid.size() - 1; i >= 0; i--) {
1038                if (UserHandle.getUserId(mJobsPerSourceUid.keyAt(i)) == userId) {
1039                    final ArraySet<JobStatus> jobs = mJobsPerSourceUid.valueAt(i);
1040                    if (jobs != null) {
1041                        result.addAll(jobs);
1042                    }
1043                }
1044            }
1045            return result;
1046        }
1047
1048        public boolean add(JobStatus job) {
1049            final int uid = job.getUid();
1050            final int sourceUid = job.getSourceUid();
1051            ArraySet<JobStatus> jobs = mJobs.get(uid);
1052            if (jobs == null) {
1053                jobs = new ArraySet<JobStatus>();
1054                mJobs.put(uid, jobs);
1055            }
1056            ArraySet<JobStatus> jobsForSourceUid = mJobsPerSourceUid.get(sourceUid);
1057            if (jobsForSourceUid == null) {
1058                jobsForSourceUid = new ArraySet<>();
1059                mJobsPerSourceUid.put(sourceUid, jobsForSourceUid);
1060            }
1061            final boolean added = jobs.add(job);
1062            final boolean addedInSource = jobsForSourceUid.add(job);
1063            if (added != addedInSource) {
1064                Slog.wtf(TAG, "mJobs and mJobsPerSourceUid mismatch; caller= " + added
1065                        + " source= " + addedInSource);
1066            }
1067            return added || addedInSource;
1068        }
1069
1070        public boolean remove(JobStatus job) {
1071            final int uid = job.getUid();
1072            final ArraySet<JobStatus> jobs = mJobs.get(uid);
1073            final int sourceUid = job.getSourceUid();
1074            final ArraySet<JobStatus> jobsForSourceUid = mJobsPerSourceUid.get(sourceUid);
1075            final boolean didRemove = jobs != null && jobs.remove(job);
1076            final boolean sourceRemove = jobsForSourceUid != null && jobsForSourceUid.remove(job);
1077            if (didRemove != sourceRemove) {
1078                Slog.wtf(TAG, "Job presence mismatch; caller=" + didRemove
1079                        + " source=" + sourceRemove);
1080            }
1081            if (didRemove || sourceRemove) {
1082                // no more jobs for this uid?  let the now-empty set objects be GC'd.
1083                if (jobs != null && jobs.size() == 0) {
1084                    mJobs.remove(uid);
1085                }
1086                if (jobsForSourceUid != null && jobsForSourceUid.size() == 0) {
1087                    mJobsPerSourceUid.remove(sourceUid);
1088                }
1089                return true;
1090            }
1091            return false;
1092        }
1093
1094        /**
1095         * Removes the jobs of all users not specified by the whitelist of user ids.
1096         * This will remove jobs scheduled *by* non-existent users as well as jobs scheduled *for*
1097         * non-existent users
1098         */
1099        public void removeJobsOfNonUsers(final int[] whitelist) {
1100            final Predicate<JobStatus> noSourceUser =
1101                    job -> !ArrayUtils.contains(whitelist, job.getSourceUserId());
1102            final Predicate<JobStatus> noCallingUser =
1103                    job -> !ArrayUtils.contains(whitelist, job.getUserId());
1104            removeAll(noSourceUser.or(noCallingUser));
1105        }
1106
1107        private void removeAll(Predicate<JobStatus> predicate) {
1108            for (int jobSetIndex = mJobs.size() - 1; jobSetIndex >= 0; jobSetIndex--) {
1109                final ArraySet<JobStatus> jobs = mJobs.valueAt(jobSetIndex);
1110                for (int jobIndex = jobs.size() - 1; jobIndex >= 0; jobIndex--) {
1111                    if (predicate.test(jobs.valueAt(jobIndex))) {
1112                        jobs.removeAt(jobIndex);
1113                    }
1114                }
1115                if (jobs.size() == 0) {
1116                    mJobs.removeAt(jobSetIndex);
1117                }
1118            }
1119            for (int jobSetIndex = mJobsPerSourceUid.size() - 1; jobSetIndex >= 0; jobSetIndex--) {
1120                final ArraySet<JobStatus> jobs = mJobsPerSourceUid.valueAt(jobSetIndex);
1121                for (int jobIndex = jobs.size() - 1; jobIndex >= 0; jobIndex--) {
1122                    if (predicate.test(jobs.valueAt(jobIndex))) {
1123                        jobs.removeAt(jobIndex);
1124                    }
1125                }
1126                if (jobs.size() == 0) {
1127                    mJobsPerSourceUid.removeAt(jobSetIndex);
1128                }
1129            }
1130        }
1131
1132        public boolean contains(JobStatus job) {
1133            final int uid = job.getUid();
1134            ArraySet<JobStatus> jobs = mJobs.get(uid);
1135            return jobs != null && jobs.contains(job);
1136        }
1137
1138        public JobStatus get(int uid, int jobId) {
1139            ArraySet<JobStatus> jobs = mJobs.get(uid);
1140            if (jobs != null) {
1141                for (int i = jobs.size() - 1; i >= 0; i--) {
1142                    JobStatus job = jobs.valueAt(i);
1143                    if (job.getJobId() == jobId) {
1144                        return job;
1145                    }
1146                }
1147            }
1148            return null;
1149        }
1150
1151        // Inefficient; use only for testing
1152        public List<JobStatus> getAllJobs() {
1153            ArrayList<JobStatus> allJobs = new ArrayList<JobStatus>(size());
1154            for (int i = mJobs.size() - 1; i >= 0; i--) {
1155                ArraySet<JobStatus> jobs = mJobs.valueAt(i);
1156                if (jobs != null) {
1157                    // Use a for loop over the ArraySet, so we don't need to make its
1158                    // optional collection class iterator implementation or have to go
1159                    // through a temporary array from toArray().
1160                    for (int j = jobs.size() - 1; j >= 0; j--) {
1161                        allJobs.add(jobs.valueAt(j));
1162                    }
1163                }
1164            }
1165            return allJobs;
1166        }
1167
1168        public void clear() {
1169            mJobs.clear();
1170            mJobsPerSourceUid.clear();
1171        }
1172
1173        public int size() {
1174            int total = 0;
1175            for (int i = mJobs.size() - 1; i >= 0; i--) {
1176                total += mJobs.valueAt(i).size();
1177            }
1178            return total;
1179        }
1180
1181        // We only want to count the jobs that this uid has scheduled on its own
1182        // behalf, not those that the app has scheduled on someone else's behalf.
1183        public int countJobsForUid(int uid) {
1184            int total = 0;
1185            ArraySet<JobStatus> jobs = mJobs.get(uid);
1186            if (jobs != null) {
1187                for (int i = jobs.size() - 1; i >= 0; i--) {
1188                    JobStatus job = jobs.valueAt(i);
1189                    if (job.getUid() == job.getSourceUid()) {
1190                        total++;
1191                    }
1192                }
1193            }
1194            return total;
1195        }
1196
1197        public void forEachJob(@Nullable Predicate<JobStatus> filterPredicate,
1198                Consumer<JobStatus> functor) {
1199            for (int uidIndex = mJobs.size() - 1; uidIndex >= 0; uidIndex--) {
1200                ArraySet<JobStatus> jobs = mJobs.valueAt(uidIndex);
1201                if (jobs != null) {
1202                    for (int i = jobs.size() - 1; i >= 0; i--) {
1203                        final JobStatus jobStatus = jobs.valueAt(i);
1204                        if ((filterPredicate == null) || filterPredicate.test(jobStatus)) {
1205                            functor.accept(jobStatus);
1206                        }
1207                    }
1208                }
1209            }
1210        }
1211
1212        public void forEachJob(int callingUid, Consumer<JobStatus> functor) {
1213            ArraySet<JobStatus> jobs = mJobs.get(callingUid);
1214            if (jobs != null) {
1215                for (int i = jobs.size() - 1; i >= 0; i--) {
1216                    functor.accept(jobs.valueAt(i));
1217                }
1218            }
1219        }
1220
1221        public void forEachJobForSourceUid(int sourceUid, Consumer<JobStatus> functor) {
1222            final ArraySet<JobStatus> jobs = mJobsPerSourceUid.get(sourceUid);
1223            if (jobs != null) {
1224                for (int i = jobs.size() - 1; i >= 0; i--) {
1225                    functor.accept(jobs.valueAt(i));
1226                }
1227            }
1228        }
1229    }
1230}
1231