JobStore.java revision f07c7b9fd0a640bff4bf7690373613da217fe69b
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 android.content.ComponentName;
20import android.app.job.JobInfo;
21import android.content.Context;
22import android.os.Environment;
23import android.os.Handler;
24import android.os.PersistableBundle;
25import android.os.SystemClock;
26import android.os.UserHandle;
27import android.text.format.DateUtils;
28import android.util.AtomicFile;
29import android.util.ArraySet;
30import android.util.Pair;
31import android.util.Slog;
32import android.util.SparseArray;
33import android.util.Xml;
34
35import com.android.internal.annotations.VisibleForTesting;
36import com.android.internal.util.FastXmlSerializer;
37import com.android.server.IoThread;
38import com.android.server.job.controllers.JobStatus;
39
40import java.io.ByteArrayOutputStream;
41import java.io.File;
42import java.io.FileInputStream;
43import java.io.FileNotFoundException;
44import java.io.FileOutputStream;
45import java.io.IOException;
46import java.nio.charset.StandardCharsets;
47import java.util.ArrayList;
48import java.util.List;
49import java.util.Set;
50
51import org.xmlpull.v1.XmlPullParser;
52import org.xmlpull.v1.XmlPullParserException;
53import org.xmlpull.v1.XmlSerializer;
54
55/**
56 * Maintains the master list of jobs that the job scheduler is tracking. These jobs are compared by
57 * reference, so none of the functions in this class should make a copy.
58 * Also handles read/write of persisted jobs.
59 *
60 * Note on locking:
61 *      All callers to this class must <strong>lock on the class object they are calling</strong>.
62 *      This is important b/c {@link com.android.server.job.JobStore.WriteJobsMapToDiskRunnable}
63 *      and {@link com.android.server.job.JobStore.ReadJobMapFromDiskRunnable} lock on that
64 *      object.
65 */
66public class JobStore {
67    private static final String TAG = "JobStore";
68    private static final boolean DEBUG = JobSchedulerService.DEBUG;
69
70    /** Threshold to adjust how often we want to write to the db. */
71    private static final int MAX_OPS_BEFORE_WRITE = 1;
72    final Object mLock;
73    final JobSet mJobSet; // per-caller-uid tracking
74    final Context mContext;
75
76    private int mDirtyOperations;
77
78    private static final Object sSingletonLock = new Object();
79    private final AtomicFile mJobsFile;
80    /** Handler backed by IoThread for writing to disk. */
81    private final Handler mIoHandler = IoThread.getHandler();
82    private static JobStore sSingleton;
83
84    /** Used by the {@link JobSchedulerService} to instantiate the JobStore. */
85    static JobStore initAndGet(JobSchedulerService jobManagerService) {
86        synchronized (sSingletonLock) {
87            if (sSingleton == null) {
88                sSingleton = new JobStore(jobManagerService.getContext(),
89                        jobManagerService.getLock(), Environment.getDataDirectory());
90            }
91            return sSingleton;
92        }
93    }
94
95    /**
96     * @return A freshly initialized job store object, with no loaded jobs.
97     */
98    @VisibleForTesting
99    public static JobStore initAndGetForTesting(Context context, File dataDir) {
100        JobStore jobStoreUnderTest = new JobStore(context, new Object(), dataDir);
101        jobStoreUnderTest.clear();
102        return jobStoreUnderTest;
103    }
104
105    /**
106     * Construct the instance of the job store. This results in a blocking read from disk.
107     */
108    private JobStore(Context context, Object lock, File dataDir) {
109        mLock = lock;
110        mContext = context;
111        mDirtyOperations = 0;
112
113        File systemDir = new File(dataDir, "system");
114        File jobDir = new File(systemDir, "job");
115        jobDir.mkdirs();
116        mJobsFile = new AtomicFile(new File(jobDir, "jobs.xml"));
117
118        mJobSet = new JobSet();
119
120        readJobMapFromDisk(mJobSet);
121    }
122
123    /**
124     * Add a job to the master list, persisting it if necessary. If the JobStatus already exists,
125     * it will be replaced.
126     * @param jobStatus Job to add.
127     * @return Whether or not an equivalent JobStatus was replaced by this operation.
128     */
129    public boolean add(JobStatus jobStatus) {
130        boolean replaced = mJobSet.remove(jobStatus);
131        mJobSet.add(jobStatus);
132        if (jobStatus.isPersisted()) {
133            maybeWriteStatusToDiskAsync();
134        }
135        if (DEBUG) {
136            Slog.d(TAG, "Added job status to store: " + jobStatus);
137        }
138        return replaced;
139    }
140
141    boolean containsJob(JobStatus jobStatus) {
142        return mJobSet.contains(jobStatus);
143    }
144
145    public int size() {
146        return mJobSet.size();
147    }
148
149    public int countJobsForUid(int uid) {
150        return mJobSet.countJobsForUid(uid);
151    }
152
153    /**
154     * Remove the provided job. Will also delete the job if it was persisted.
155     * @param writeBack If true, the job will be deleted (if it was persisted) immediately.
156     * @return Whether or not the job existed to be removed.
157     */
158    public boolean remove(JobStatus jobStatus, boolean writeBack) {
159        boolean removed = mJobSet.remove(jobStatus);
160        if (!removed) {
161            if (DEBUG) {
162                Slog.d(TAG, "Couldn't remove job: didn't exist: " + jobStatus);
163            }
164            return false;
165        }
166        if (writeBack && jobStatus.isPersisted()) {
167            maybeWriteStatusToDiskAsync();
168        }
169        return removed;
170    }
171
172    @VisibleForTesting
173    public void clear() {
174        mJobSet.clear();
175        maybeWriteStatusToDiskAsync();
176    }
177
178    /**
179     * @param userHandle User for whom we are querying the list of jobs.
180     * @return A list of all the jobs scheduled by the provided user. Never null.
181     */
182    public List<JobStatus> getJobsByUser(int userHandle) {
183        return mJobSet.getJobsByUser(userHandle);
184    }
185
186    /**
187     * @param uid Uid of the requesting app.
188     * @return All JobStatus objects for a given uid from the master list. Never null.
189     */
190    public List<JobStatus> getJobsByUid(int uid) {
191        return mJobSet.getJobsByUid(uid);
192    }
193
194    /**
195     * @param uid Uid of the requesting app.
196     * @param jobId Job id, specified at schedule-time.
197     * @return the JobStatus that matches the provided uId and jobId, or null if none found.
198     */
199    public JobStatus getJobByUidAndJobId(int uid, int jobId) {
200        return mJobSet.get(uid, jobId);
201    }
202
203    /**
204     * Iterate over the set of all jobs, invoking the supplied functor on each.  This is for
205     * customers who need to examine each job; we'd much rather not have to generate
206     * transient unified collections for them to iterate over and then discard, or creating
207     * iterators every time a client needs to perform a sweep.
208     */
209    public void forEachJob(JobStatusFunctor functor) {
210        mJobSet.forEachJob(functor);
211    }
212
213    public void forEachJob(int uid, JobStatusFunctor functor) {
214        mJobSet.forEachJob(uid, functor);
215    }
216
217    public interface JobStatusFunctor {
218        public void process(JobStatus jobStatus);
219    }
220
221    /** Version of the db schema. */
222    private static final int JOBS_FILE_VERSION = 0;
223    /** Tag corresponds to constraints this job needs. */
224    private static final String XML_TAG_PARAMS_CONSTRAINTS = "constraints";
225    /** Tag corresponds to execution parameters. */
226    private static final String XML_TAG_PERIODIC = "periodic";
227    private static final String XML_TAG_ONEOFF = "one-off";
228    private static final String XML_TAG_EXTRAS = "extras";
229
230    /**
231     * Every time the state changes we write all the jobs in one swath, instead of trying to
232     * track incremental changes.
233     * @return Whether the operation was successful. This will only fail for e.g. if the system is
234     * low on storage. If this happens, we continue as normal
235     */
236    private void maybeWriteStatusToDiskAsync() {
237        mDirtyOperations++;
238        if (mDirtyOperations >= MAX_OPS_BEFORE_WRITE) {
239            if (DEBUG) {
240                Slog.v(TAG, "Writing jobs to disk.");
241            }
242            mIoHandler.post(new WriteJobsMapToDiskRunnable());
243        }
244    }
245
246    @VisibleForTesting
247    public void readJobMapFromDisk(JobSet jobSet) {
248        new ReadJobMapFromDiskRunnable(jobSet).run();
249    }
250
251    /**
252     * Runnable that writes {@link #mJobSet} out to xml.
253     * NOTE: This Runnable locks on mLock
254     */
255    private class WriteJobsMapToDiskRunnable implements Runnable {
256        @Override
257        public void run() {
258            final long startElapsed = SystemClock.elapsedRealtime();
259            final List<JobStatus> storeCopy = new ArrayList<JobStatus>();
260            synchronized (mLock) {
261                // Clone the jobs so we can release the lock before writing.
262                mJobSet.forEachJob(new JobStatusFunctor() {
263                    @Override
264                    public void process(JobStatus job) {
265                        if (job.isPersisted()) {
266                            storeCopy.add(new JobStatus(job));
267                        }
268                    }
269                });
270            }
271            writeJobsMapImpl(storeCopy);
272            if (JobSchedulerService.DEBUG) {
273                Slog.v(TAG, "Finished writing, took " + (SystemClock.elapsedRealtime()
274                        - startElapsed) + "ms");
275            }
276        }
277
278        private void writeJobsMapImpl(List<JobStatus> jobList) {
279            try {
280                ByteArrayOutputStream baos = new ByteArrayOutputStream();
281                XmlSerializer out = new FastXmlSerializer();
282                out.setOutput(baos, StandardCharsets.UTF_8.name());
283                out.startDocument(null, true);
284                out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
285
286                out.startTag(null, "job-info");
287                out.attribute(null, "version", Integer.toString(JOBS_FILE_VERSION));
288                for (int i=0; i<jobList.size(); i++) {
289                    JobStatus jobStatus = jobList.get(i);
290                    if (DEBUG) {
291                        Slog.d(TAG, "Saving job " + jobStatus.getJobId());
292                    }
293                    out.startTag(null, "job");
294                    addAttributesToJobTag(out, jobStatus);
295                    writeConstraintsToXml(out, jobStatus);
296                    writeExecutionCriteriaToXml(out, jobStatus);
297                    writeBundleToXml(jobStatus.getExtras(), out);
298                    out.endTag(null, "job");
299                }
300                out.endTag(null, "job-info");
301                out.endDocument();
302
303                // Write out to disk in one fell sweep.
304                FileOutputStream fos = mJobsFile.startWrite();
305                fos.write(baos.toByteArray());
306                mJobsFile.finishWrite(fos);
307                mDirtyOperations = 0;
308            } catch (IOException e) {
309                if (DEBUG) {
310                    Slog.v(TAG, "Error writing out job data.", e);
311                }
312            } catch (XmlPullParserException e) {
313                if (DEBUG) {
314                    Slog.d(TAG, "Error persisting bundle.", e);
315                }
316            }
317        }
318
319        /** Write out a tag with data comprising the required fields and priority of this job and
320         * its client.
321         */
322        private void addAttributesToJobTag(XmlSerializer out, JobStatus jobStatus)
323                throws IOException {
324            out.attribute(null, "jobid", Integer.toString(jobStatus.getJobId()));
325            out.attribute(null, "package", jobStatus.getServiceComponent().getPackageName());
326            out.attribute(null, "class", jobStatus.getServiceComponent().getClassName());
327            if (jobStatus.getSourcePackageName() != null) {
328                out.attribute(null, "sourcePackageName", jobStatus.getSourcePackageName());
329            }
330            if (jobStatus.getSourceTag() != null) {
331                out.attribute(null, "sourceTag", jobStatus.getSourceTag());
332            }
333            out.attribute(null, "sourceUserId", String.valueOf(jobStatus.getSourceUserId()));
334            out.attribute(null, "uid", Integer.toString(jobStatus.getUid()));
335            out.attribute(null, "priority", String.valueOf(jobStatus.getPriority()));
336        }
337
338        private void writeBundleToXml(PersistableBundle extras, XmlSerializer out)
339                throws IOException, XmlPullParserException {
340            out.startTag(null, XML_TAG_EXTRAS);
341            PersistableBundle extrasCopy = deepCopyBundle(extras, 10);
342            extrasCopy.saveToXml(out);
343            out.endTag(null, XML_TAG_EXTRAS);
344        }
345
346        private PersistableBundle deepCopyBundle(PersistableBundle bundle, int maxDepth) {
347            if (maxDepth <= 0) {
348                return null;
349            }
350            PersistableBundle copy = (PersistableBundle) bundle.clone();
351            Set<String> keySet = bundle.keySet();
352            for (String key: keySet) {
353                Object o = copy.get(key);
354                if (o instanceof PersistableBundle) {
355                    PersistableBundle bCopy = deepCopyBundle((PersistableBundle) o, maxDepth-1);
356                    copy.putPersistableBundle(key, bCopy);
357                }
358            }
359            return copy;
360        }
361
362        /**
363         * Write out a tag with data identifying this job's constraints. If the constraint isn't here
364         * it doesn't apply.
365         */
366        private void writeConstraintsToXml(XmlSerializer out, JobStatus jobStatus) throws IOException {
367            out.startTag(null, XML_TAG_PARAMS_CONSTRAINTS);
368            if (jobStatus.hasConnectivityConstraint()) {
369                out.attribute(null, "connectivity", Boolean.toString(true));
370            }
371            if (jobStatus.hasUnmeteredConstraint()) {
372                out.attribute(null, "unmetered", Boolean.toString(true));
373            }
374            if (jobStatus.hasNotRoamingConstraint()) {
375                out.attribute(null, "not-roaming", Boolean.toString(true));
376            }
377            if (jobStatus.hasIdleConstraint()) {
378                out.attribute(null, "idle", Boolean.toString(true));
379            }
380            if (jobStatus.hasChargingConstraint()) {
381                out.attribute(null, "charging", Boolean.toString(true));
382            }
383            out.endTag(null, XML_TAG_PARAMS_CONSTRAINTS);
384        }
385
386        private void writeExecutionCriteriaToXml(XmlSerializer out, JobStatus jobStatus)
387                throws IOException {
388            final JobInfo job = jobStatus.getJob();
389            if (jobStatus.getJob().isPeriodic()) {
390                out.startTag(null, XML_TAG_PERIODIC);
391                out.attribute(null, "period", Long.toString(job.getIntervalMillis()));
392                out.attribute(null, "flex", Long.toString(job.getFlexMillis()));
393            } else {
394                out.startTag(null, XML_TAG_ONEOFF);
395            }
396
397            if (jobStatus.hasDeadlineConstraint()) {
398                // Wall clock deadline.
399                final long deadlineWallclock =  System.currentTimeMillis() +
400                        (jobStatus.getLatestRunTimeElapsed() - SystemClock.elapsedRealtime());
401                out.attribute(null, "deadline", Long.toString(deadlineWallclock));
402            }
403            if (jobStatus.hasTimingDelayConstraint()) {
404                final long delayWallclock = System.currentTimeMillis() +
405                        (jobStatus.getEarliestRunTime() - SystemClock.elapsedRealtime());
406                out.attribute(null, "delay", Long.toString(delayWallclock));
407            }
408
409            // Only write out back-off policy if it differs from the default.
410            // This also helps the case where the job is idle -> these aren't allowed to specify
411            // back-off.
412            if (jobStatus.getJob().getInitialBackoffMillis() != JobInfo.DEFAULT_INITIAL_BACKOFF_MILLIS
413                    || jobStatus.getJob().getBackoffPolicy() != JobInfo.DEFAULT_BACKOFF_POLICY) {
414                out.attribute(null, "backoff-policy", Integer.toString(job.getBackoffPolicy()));
415                out.attribute(null, "initial-backoff", Long.toString(job.getInitialBackoffMillis()));
416            }
417            if (job.isPeriodic()) {
418                out.endTag(null, XML_TAG_PERIODIC);
419            } else {
420                out.endTag(null, XML_TAG_ONEOFF);
421            }
422        }
423    }
424
425    /**
426     * Runnable that reads list of persisted job from xml. This is run once at start up, so doesn't
427     * need to go through {@link JobStore#add(com.android.server.job.controllers.JobStatus)}.
428     */
429    private class ReadJobMapFromDiskRunnable implements Runnable {
430        private final JobSet jobSet;
431
432        /**
433         * @param jobSet Reference to the (empty) set of JobStatus objects that back the JobStore,
434         *               so that after disk read we can populate it directly.
435         */
436        ReadJobMapFromDiskRunnable(JobSet jobSet) {
437            this.jobSet = jobSet;
438        }
439
440        @Override
441        public void run() {
442            try {
443                List<JobStatus> jobs;
444                FileInputStream fis = mJobsFile.openRead();
445                synchronized (mLock) {
446                    jobs = readJobMapImpl(fis);
447                    if (jobs != null) {
448                        for (int i=0; i<jobs.size(); i++) {
449                            this.jobSet.add(jobs.get(i));
450                        }
451                    }
452                }
453                fis.close();
454            } catch (FileNotFoundException e) {
455                if (JobSchedulerService.DEBUG) {
456                    Slog.d(TAG, "Could not find jobs file, probably there was nothing to load.");
457                }
458            } catch (XmlPullParserException e) {
459                if (JobSchedulerService.DEBUG) {
460                    Slog.d(TAG, "Error parsing xml.", e);
461                }
462            } catch (IOException e) {
463                if (JobSchedulerService.DEBUG) {
464                    Slog.d(TAG, "Error parsing xml.", e);
465                }
466            }
467        }
468
469        private List<JobStatus> readJobMapImpl(FileInputStream fis)
470                throws XmlPullParserException, IOException {
471            XmlPullParser parser = Xml.newPullParser();
472            parser.setInput(fis, StandardCharsets.UTF_8.name());
473
474            int eventType = parser.getEventType();
475            while (eventType != XmlPullParser.START_TAG &&
476                    eventType != XmlPullParser.END_DOCUMENT) {
477                eventType = parser.next();
478                Slog.d(TAG, "Start tag: " + parser.getName());
479            }
480            if (eventType == XmlPullParser.END_DOCUMENT) {
481                if (DEBUG) {
482                    Slog.d(TAG, "No persisted jobs.");
483                }
484                return null;
485            }
486
487            String tagName = parser.getName();
488            if ("job-info".equals(tagName)) {
489                final List<JobStatus> jobs = new ArrayList<JobStatus>();
490                // Read in version info.
491                try {
492                    int version = Integer.parseInt(parser.getAttributeValue(null, "version"));
493                    if (version != JOBS_FILE_VERSION) {
494                        Slog.d(TAG, "Invalid version number, aborting jobs file read.");
495                        return null;
496                    }
497                } catch (NumberFormatException e) {
498                    Slog.e(TAG, "Invalid version number, aborting jobs file read.");
499                    return null;
500                }
501                eventType = parser.next();
502                do {
503                    // Read each <job/>
504                    if (eventType == XmlPullParser.START_TAG) {
505                        tagName = parser.getName();
506                        // Start reading job.
507                        if ("job".equals(tagName)) {
508                            JobStatus persistedJob = restoreJobFromXml(parser);
509                            if (persistedJob != null) {
510                                if (DEBUG) {
511                                    Slog.d(TAG, "Read out " + persistedJob);
512                                }
513                                jobs.add(persistedJob);
514                            } else {
515                                Slog.d(TAG, "Error reading job from file.");
516                            }
517                        }
518                    }
519                    eventType = parser.next();
520                } while (eventType != XmlPullParser.END_DOCUMENT);
521                return jobs;
522            }
523            return null;
524        }
525
526        /**
527         * @param parser Xml parser at the beginning of a "<job/>" tag. The next "parser.next()" call
528         *               will take the parser into the body of the job tag.
529         * @return Newly instantiated job holding all the information we just read out of the xml tag.
530         */
531        private JobStatus restoreJobFromXml(XmlPullParser parser) throws XmlPullParserException,
532                IOException {
533            JobInfo.Builder jobBuilder;
534            int uid, sourceUserId;
535
536            // Read out job identifier attributes and priority.
537            try {
538                jobBuilder = buildBuilderFromXml(parser);
539                jobBuilder.setPersisted(true);
540                uid = Integer.parseInt(parser.getAttributeValue(null, "uid"));
541
542                String val = parser.getAttributeValue(null, "priority");
543                if (val != null) {
544                    jobBuilder.setPriority(Integer.parseInt(val));
545                }
546                val = parser.getAttributeValue(null, "sourceUserId");
547                sourceUserId = val == null ? -1 : Integer.parseInt(val);
548            } catch (NumberFormatException e) {
549                Slog.e(TAG, "Error parsing job's required fields, skipping");
550                return null;
551            }
552
553            String sourcePackageName = parser.getAttributeValue(null, "sourcePackageName");
554
555            final String sourceTag = parser.getAttributeValue(null, "sourceTag");
556
557            int eventType;
558            // Read out constraints tag.
559            do {
560                eventType = parser.next();
561            } while (eventType == XmlPullParser.TEXT);  // Push through to next START_TAG.
562
563            if (!(eventType == XmlPullParser.START_TAG &&
564                    XML_TAG_PARAMS_CONSTRAINTS.equals(parser.getName()))) {
565                // Expecting a <constraints> start tag.
566                return null;
567            }
568            try {
569                buildConstraintsFromXml(jobBuilder, parser);
570            } catch (NumberFormatException e) {
571                Slog.d(TAG, "Error reading constraints, skipping.");
572                return null;
573            }
574            parser.next(); // Consume </constraints>
575
576            // Read out execution parameters tag.
577            do {
578                eventType = parser.next();
579            } while (eventType == XmlPullParser.TEXT);
580            if (eventType != XmlPullParser.START_TAG) {
581                return null;
582            }
583
584            // Tuple of (earliest runtime, latest runtime) in elapsed realtime after disk load.
585            Pair<Long, Long> elapsedRuntimes;
586            try {
587                elapsedRuntimes = buildExecutionTimesFromXml(parser);
588            } catch (NumberFormatException e) {
589                if (DEBUG) {
590                    Slog.d(TAG, "Error parsing execution time parameters, skipping.");
591                }
592                return null;
593            }
594
595            final long elapsedNow = SystemClock.elapsedRealtime();
596            if (XML_TAG_PERIODIC.equals(parser.getName())) {
597                try {
598                    String val = parser.getAttributeValue(null, "period");
599                    final long periodMillis = Long.valueOf(val);
600                    val = parser.getAttributeValue(null, "flex");
601                    final long flexMillis = (val != null) ? Long.valueOf(val) : periodMillis;
602                    jobBuilder.setPeriodic(periodMillis, flexMillis);
603                    // As a sanity check, cap the recreated run time to be no later than flex+period
604                    // from now. This is the latest the periodic could be pushed out. This could
605                    // happen if the periodic ran early (at flex time before period), and then the
606                    // device rebooted.
607                    if (elapsedRuntimes.second > elapsedNow + periodMillis + flexMillis) {
608                        final long clampedLateRuntimeElapsed = elapsedNow + flexMillis
609                                + periodMillis;
610                        final long clampedEarlyRuntimeElapsed = clampedLateRuntimeElapsed
611                                - flexMillis;
612                        Slog.w(TAG,
613                                String.format("Periodic job for uid='%d' persisted run-time is" +
614                                                " too big [%s, %s]. Clamping to [%s,%s]",
615                                        uid,
616                                        DateUtils.formatElapsedTime(elapsedRuntimes.first / 1000),
617                                        DateUtils.formatElapsedTime(elapsedRuntimes.second / 1000),
618                                        DateUtils.formatElapsedTime(
619                                                clampedEarlyRuntimeElapsed / 1000),
620                                        DateUtils.formatElapsedTime(
621                                                clampedLateRuntimeElapsed / 1000))
622                        );
623                        elapsedRuntimes =
624                                Pair.create(clampedEarlyRuntimeElapsed, clampedLateRuntimeElapsed);
625                    }
626                } catch (NumberFormatException e) {
627                    Slog.d(TAG, "Error reading periodic execution criteria, skipping.");
628                    return null;
629                }
630            } else if (XML_TAG_ONEOFF.equals(parser.getName())) {
631                try {
632                    if (elapsedRuntimes.first != JobStatus.NO_EARLIEST_RUNTIME) {
633                        jobBuilder.setMinimumLatency(elapsedRuntimes.first - elapsedNow);
634                    }
635                    if (elapsedRuntimes.second != JobStatus.NO_LATEST_RUNTIME) {
636                        jobBuilder.setOverrideDeadline(
637                                elapsedRuntimes.second - elapsedNow);
638                    }
639                } catch (NumberFormatException e) {
640                    Slog.d(TAG, "Error reading job execution criteria, skipping.");
641                    return null;
642                }
643            } else {
644                if (DEBUG) {
645                    Slog.d(TAG, "Invalid parameter tag, skipping - " + parser.getName());
646                }
647                // Expecting a parameters start tag.
648                return null;
649            }
650            maybeBuildBackoffPolicyFromXml(jobBuilder, parser);
651
652            parser.nextTag(); // Consume parameters end tag.
653
654            // Read out extras Bundle.
655            do {
656                eventType = parser.next();
657            } while (eventType == XmlPullParser.TEXT);
658            if (!(eventType == XmlPullParser.START_TAG
659                    && XML_TAG_EXTRAS.equals(parser.getName()))) {
660                if (DEBUG) {
661                    Slog.d(TAG, "Error reading extras, skipping.");
662                }
663                return null;
664            }
665
666            PersistableBundle extras = PersistableBundle.restoreFromXml(parser);
667            jobBuilder.setExtras(extras);
668            parser.nextTag(); // Consume </extras>
669
670            // Migrate sync jobs forward from earlier, incomplete representation
671            if ("android".equals(sourcePackageName)
672                    && extras != null
673                    && extras.getBoolean("SyncManagerJob", false)) {
674                sourcePackageName = extras.getString("owningPackage", sourcePackageName);
675                if (DEBUG) {
676                    Slog.i(TAG, "Fixing up sync job source package name from 'android' to '"
677                            + sourcePackageName + "'");
678                }
679            }
680
681            // And now we're done
682            JobStatus js = new JobStatus(
683                    jobBuilder.build(), uid, sourcePackageName, sourceUserId, sourceTag,
684                    elapsedRuntimes.first, elapsedRuntimes.second);
685            return js;
686        }
687
688        private JobInfo.Builder buildBuilderFromXml(XmlPullParser parser) throws NumberFormatException {
689            // Pull out required fields from <job> attributes.
690            int jobId = Integer.parseInt(parser.getAttributeValue(null, "jobid"));
691            String packageName = parser.getAttributeValue(null, "package");
692            String className = parser.getAttributeValue(null, "class");
693            ComponentName cname = new ComponentName(packageName, className);
694
695            return new JobInfo.Builder(jobId, cname);
696        }
697
698        private void buildConstraintsFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser) {
699            String val = parser.getAttributeValue(null, "connectivity");
700            if (val != null) {
701                jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
702            }
703            val = parser.getAttributeValue(null, "unmetered");
704            if (val != null) {
705                jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED);
706            }
707            val = parser.getAttributeValue(null, "not-roaming");
708            if (val != null) {
709                jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_NOT_ROAMING);
710            }
711            val = parser.getAttributeValue(null, "idle");
712            if (val != null) {
713                jobBuilder.setRequiresDeviceIdle(true);
714            }
715            val = parser.getAttributeValue(null, "charging");
716            if (val != null) {
717                jobBuilder.setRequiresCharging(true);
718            }
719        }
720
721        /**
722         * Builds the back-off policy out of the params tag. These attributes may not exist, depending
723         * on whether the back-off was set when the job was first scheduled.
724         */
725        private void maybeBuildBackoffPolicyFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser) {
726            String val = parser.getAttributeValue(null, "initial-backoff");
727            if (val != null) {
728                long initialBackoff = Long.valueOf(val);
729                val = parser.getAttributeValue(null, "backoff-policy");
730                int backoffPolicy = Integer.parseInt(val);  // Will throw NFE which we catch higher up.
731                jobBuilder.setBackoffCriteria(initialBackoff, backoffPolicy);
732            }
733        }
734
735        /**
736         * Convenience function to read out and convert deadline and delay from xml into elapsed real
737         * time.
738         * @return A {@link android.util.Pair}, where the first value is the earliest elapsed runtime
739         * and the second is the latest elapsed runtime.
740         */
741        private Pair<Long, Long> buildExecutionTimesFromXml(XmlPullParser parser)
742                throws NumberFormatException {
743            // Pull out execution time data.
744            final long nowWallclock = System.currentTimeMillis();
745            final long nowElapsed = SystemClock.elapsedRealtime();
746
747            long earliestRunTimeElapsed = JobStatus.NO_EARLIEST_RUNTIME;
748            long latestRunTimeElapsed = JobStatus.NO_LATEST_RUNTIME;
749            String val = parser.getAttributeValue(null, "deadline");
750            if (val != null) {
751                long latestRuntimeWallclock = Long.valueOf(val);
752                long maxDelayElapsed =
753                        Math.max(latestRuntimeWallclock - nowWallclock, 0);
754                latestRunTimeElapsed = nowElapsed + maxDelayElapsed;
755            }
756            val = parser.getAttributeValue(null, "delay");
757            if (val != null) {
758                long earliestRuntimeWallclock = Long.valueOf(val);
759                long minDelayElapsed =
760                        Math.max(earliestRuntimeWallclock - nowWallclock, 0);
761                earliestRunTimeElapsed = nowElapsed + minDelayElapsed;
762
763            }
764            return Pair.create(earliestRunTimeElapsed, latestRunTimeElapsed);
765        }
766    }
767
768    static class JobSet {
769        // Key is the getUid() originator of the jobs in each sheaf
770        private SparseArray<ArraySet<JobStatus>> mJobs;
771
772        public JobSet() {
773            mJobs = new SparseArray<ArraySet<JobStatus>>();
774        }
775
776        public List<JobStatus> getJobsByUid(int uid) {
777            ArrayList<JobStatus> matchingJobs = new ArrayList<JobStatus>();
778            ArraySet<JobStatus> jobs = mJobs.get(uid);
779            if (jobs != null) {
780                matchingJobs.addAll(jobs);
781            }
782            return matchingJobs;
783        }
784
785        // By user, not by uid, so we need to traverse by key and check
786        public List<JobStatus> getJobsByUser(int userId) {
787            ArrayList<JobStatus> result = new ArrayList<JobStatus>();
788            for (int i = mJobs.size() - 1; i >= 0; i--) {
789                if (UserHandle.getUserId(mJobs.keyAt(i)) == userId) {
790                    ArraySet<JobStatus> jobs = mJobs.get(i);
791                    if (jobs != null) {
792                        result.addAll(jobs);
793                    }
794                }
795            }
796            return result;
797        }
798
799        public boolean add(JobStatus job) {
800            final int uid = job.getUid();
801            ArraySet<JobStatus> jobs = mJobs.get(uid);
802            if (jobs == null) {
803                jobs = new ArraySet<JobStatus>();
804                mJobs.put(uid, jobs);
805            }
806            return jobs.add(job);
807        }
808
809        public boolean remove(JobStatus job) {
810            final int uid = job.getUid();
811            ArraySet<JobStatus> jobs = mJobs.get(uid);
812            boolean didRemove = (jobs != null) ? jobs.remove(job) : false;
813            if (didRemove && jobs.size() == 0) {
814                // no more jobs for this uid; let the now-empty set object be GC'd.
815                mJobs.remove(uid);
816            }
817            return didRemove;
818        }
819
820        public boolean contains(JobStatus job) {
821            final int uid = job.getUid();
822            ArraySet<JobStatus> jobs = mJobs.get(uid);
823            return jobs != null && jobs.contains(job);
824        }
825
826        public JobStatus get(int uid, int jobId) {
827            ArraySet<JobStatus> jobs = mJobs.get(uid);
828            if (jobs != null) {
829                for (int i = jobs.size() - 1; i >= 0; i--) {
830                    JobStatus job = jobs.valueAt(i);
831                    if (job.getJobId() == jobId) {
832                        return job;
833                    }
834                }
835            }
836            return null;
837        }
838
839        // Inefficient; use only for testing
840        public List<JobStatus> getAllJobs() {
841            ArrayList<JobStatus> allJobs = new ArrayList<JobStatus>(size());
842            for (int i = mJobs.size(); i >= 0; i--) {
843                allJobs.addAll(mJobs.valueAt(i));
844            }
845            return allJobs;
846        }
847
848        public void clear() {
849            mJobs.clear();
850        }
851
852        public int size() {
853            int total = 0;
854            for (int i = mJobs.size() - 1; i >= 0; i--) {
855                total += mJobs.valueAt(i).size();
856            }
857            return total;
858        }
859
860        // We only want to count the jobs that this uid has scheduled on its own
861        // behalf, not those that the app has scheduled on someone else's behalf.
862        public int countJobsForUid(int uid) {
863            int total = 0;
864            ArraySet<JobStatus> jobs = mJobs.get(uid);
865            if (jobs != null) {
866                for (int i = jobs.size() - 1; i >= 0; i--) {
867                    JobStatus job = jobs.valueAt(i);
868                    if (job.getUid() == job.getSourceUid()) {
869                        total++;
870                    }
871                }
872            }
873            return total;
874        }
875
876        public void forEachJob(JobStatusFunctor functor) {
877            for (int uidIndex = mJobs.size() - 1; uidIndex >= 0; uidIndex--) {
878                ArraySet<JobStatus> jobs = mJobs.valueAt(uidIndex);
879                for (int i = jobs.size() - 1; i >= 0; i--) {
880                    functor.process(jobs.valueAt(i));
881                }
882            }
883        }
884
885        public void forEachJob(int uid, JobStatusFunctor functor) {
886            ArraySet<JobStatus> jobs = mJobs.get(uid);
887            if (jobs != null) {
888                for (int i = jobs.size() - 1; i >= 0; i--) {
889                    functor.process(jobs.valueAt(i));
890                }
891            }
892        }
893    }
894}
895