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            out.attribute(null, "flags", String.valueOf(jobStatus.getFlags()));
337        }
338
339        private void writeBundleToXml(PersistableBundle extras, XmlSerializer out)
340                throws IOException, XmlPullParserException {
341            out.startTag(null, XML_TAG_EXTRAS);
342            PersistableBundle extrasCopy = deepCopyBundle(extras, 10);
343            extrasCopy.saveToXml(out);
344            out.endTag(null, XML_TAG_EXTRAS);
345        }
346
347        private PersistableBundle deepCopyBundle(PersistableBundle bundle, int maxDepth) {
348            if (maxDepth <= 0) {
349                return null;
350            }
351            PersistableBundle copy = (PersistableBundle) bundle.clone();
352            Set<String> keySet = bundle.keySet();
353            for (String key: keySet) {
354                Object o = copy.get(key);
355                if (o instanceof PersistableBundle) {
356                    PersistableBundle bCopy = deepCopyBundle((PersistableBundle) o, maxDepth-1);
357                    copy.putPersistableBundle(key, bCopy);
358                }
359            }
360            return copy;
361        }
362
363        /**
364         * Write out a tag with data identifying this job's constraints. If the constraint isn't here
365         * it doesn't apply.
366         */
367        private void writeConstraintsToXml(XmlSerializer out, JobStatus jobStatus) throws IOException {
368            out.startTag(null, XML_TAG_PARAMS_CONSTRAINTS);
369            if (jobStatus.hasConnectivityConstraint()) {
370                out.attribute(null, "connectivity", Boolean.toString(true));
371            }
372            if (jobStatus.hasUnmeteredConstraint()) {
373                out.attribute(null, "unmetered", Boolean.toString(true));
374            }
375            if (jobStatus.hasNotRoamingConstraint()) {
376                out.attribute(null, "not-roaming", Boolean.toString(true));
377            }
378            if (jobStatus.hasIdleConstraint()) {
379                out.attribute(null, "idle", Boolean.toString(true));
380            }
381            if (jobStatus.hasChargingConstraint()) {
382                out.attribute(null, "charging", Boolean.toString(true));
383            }
384            out.endTag(null, XML_TAG_PARAMS_CONSTRAINTS);
385        }
386
387        private void writeExecutionCriteriaToXml(XmlSerializer out, JobStatus jobStatus)
388                throws IOException {
389            final JobInfo job = jobStatus.getJob();
390            if (jobStatus.getJob().isPeriodic()) {
391                out.startTag(null, XML_TAG_PERIODIC);
392                out.attribute(null, "period", Long.toString(job.getIntervalMillis()));
393                out.attribute(null, "flex", Long.toString(job.getFlexMillis()));
394            } else {
395                out.startTag(null, XML_TAG_ONEOFF);
396            }
397
398            if (jobStatus.hasDeadlineConstraint()) {
399                // Wall clock deadline.
400                final long deadlineWallclock =  System.currentTimeMillis() +
401                        (jobStatus.getLatestRunTimeElapsed() - SystemClock.elapsedRealtime());
402                out.attribute(null, "deadline", Long.toString(deadlineWallclock));
403            }
404            if (jobStatus.hasTimingDelayConstraint()) {
405                final long delayWallclock = System.currentTimeMillis() +
406                        (jobStatus.getEarliestRunTime() - SystemClock.elapsedRealtime());
407                out.attribute(null, "delay", Long.toString(delayWallclock));
408            }
409
410            // Only write out back-off policy if it differs from the default.
411            // This also helps the case where the job is idle -> these aren't allowed to specify
412            // back-off.
413            if (jobStatus.getJob().getInitialBackoffMillis() != JobInfo.DEFAULT_INITIAL_BACKOFF_MILLIS
414                    || jobStatus.getJob().getBackoffPolicy() != JobInfo.DEFAULT_BACKOFF_POLICY) {
415                out.attribute(null, "backoff-policy", Integer.toString(job.getBackoffPolicy()));
416                out.attribute(null, "initial-backoff", Long.toString(job.getInitialBackoffMillis()));
417            }
418            if (job.isPeriodic()) {
419                out.endTag(null, XML_TAG_PERIODIC);
420            } else {
421                out.endTag(null, XML_TAG_ONEOFF);
422            }
423        }
424    }
425
426    /**
427     * Runnable that reads list of persisted job from xml. This is run once at start up, so doesn't
428     * need to go through {@link JobStore#add(com.android.server.job.controllers.JobStatus)}.
429     */
430    private class ReadJobMapFromDiskRunnable implements Runnable {
431        private final JobSet jobSet;
432
433        /**
434         * @param jobSet Reference to the (empty) set of JobStatus objects that back the JobStore,
435         *               so that after disk read we can populate it directly.
436         */
437        ReadJobMapFromDiskRunnable(JobSet jobSet) {
438            this.jobSet = jobSet;
439        }
440
441        @Override
442        public void run() {
443            try {
444                List<JobStatus> jobs;
445                FileInputStream fis = mJobsFile.openRead();
446                synchronized (mLock) {
447                    jobs = readJobMapImpl(fis);
448                    if (jobs != null) {
449                        for (int i=0; i<jobs.size(); i++) {
450                            this.jobSet.add(jobs.get(i));
451                        }
452                    }
453                }
454                fis.close();
455            } catch (FileNotFoundException e) {
456                if (JobSchedulerService.DEBUG) {
457                    Slog.d(TAG, "Could not find jobs file, probably there was nothing to load.");
458                }
459            } catch (XmlPullParserException e) {
460                if (JobSchedulerService.DEBUG) {
461                    Slog.d(TAG, "Error parsing xml.", e);
462                }
463            } catch (IOException e) {
464                if (JobSchedulerService.DEBUG) {
465                    Slog.d(TAG, "Error parsing xml.", e);
466                }
467            }
468        }
469
470        private List<JobStatus> readJobMapImpl(FileInputStream fis)
471                throws XmlPullParserException, IOException {
472            XmlPullParser parser = Xml.newPullParser();
473            parser.setInput(fis, StandardCharsets.UTF_8.name());
474
475            int eventType = parser.getEventType();
476            while (eventType != XmlPullParser.START_TAG &&
477                    eventType != XmlPullParser.END_DOCUMENT) {
478                eventType = parser.next();
479                Slog.d(TAG, "Start tag: " + parser.getName());
480            }
481            if (eventType == XmlPullParser.END_DOCUMENT) {
482                if (DEBUG) {
483                    Slog.d(TAG, "No persisted jobs.");
484                }
485                return null;
486            }
487
488            String tagName = parser.getName();
489            if ("job-info".equals(tagName)) {
490                final List<JobStatus> jobs = new ArrayList<JobStatus>();
491                // Read in version info.
492                try {
493                    int version = Integer.parseInt(parser.getAttributeValue(null, "version"));
494                    if (version != JOBS_FILE_VERSION) {
495                        Slog.d(TAG, "Invalid version number, aborting jobs file read.");
496                        return null;
497                    }
498                } catch (NumberFormatException e) {
499                    Slog.e(TAG, "Invalid version number, aborting jobs file read.");
500                    return null;
501                }
502                eventType = parser.next();
503                do {
504                    // Read each <job/>
505                    if (eventType == XmlPullParser.START_TAG) {
506                        tagName = parser.getName();
507                        // Start reading job.
508                        if ("job".equals(tagName)) {
509                            JobStatus persistedJob = restoreJobFromXml(parser);
510                            if (persistedJob != null) {
511                                if (DEBUG) {
512                                    Slog.d(TAG, "Read out " + persistedJob);
513                                }
514                                jobs.add(persistedJob);
515                            } else {
516                                Slog.d(TAG, "Error reading job from file.");
517                            }
518                        }
519                    }
520                    eventType = parser.next();
521                } while (eventType != XmlPullParser.END_DOCUMENT);
522                return jobs;
523            }
524            return null;
525        }
526
527        /**
528         * @param parser Xml parser at the beginning of a "<job/>" tag. The next "parser.next()" call
529         *               will take the parser into the body of the job tag.
530         * @return Newly instantiated job holding all the information we just read out of the xml tag.
531         */
532        private JobStatus restoreJobFromXml(XmlPullParser parser) throws XmlPullParserException,
533                IOException {
534            JobInfo.Builder jobBuilder;
535            int uid, sourceUserId;
536
537            // Read out job identifier attributes and priority.
538            try {
539                jobBuilder = buildBuilderFromXml(parser);
540                jobBuilder.setPersisted(true);
541                uid = Integer.parseInt(parser.getAttributeValue(null, "uid"));
542
543                String val = parser.getAttributeValue(null, "priority");
544                if (val != null) {
545                    jobBuilder.setPriority(Integer.parseInt(val));
546                }
547                val = parser.getAttributeValue(null, "flags");
548                if (val != null) {
549                    jobBuilder.setFlags(Integer.parseInt(val));
550                }
551                val = parser.getAttributeValue(null, "sourceUserId");
552                sourceUserId = val == null ? -1 : Integer.parseInt(val);
553            } catch (NumberFormatException e) {
554                Slog.e(TAG, "Error parsing job's required fields, skipping");
555                return null;
556            }
557
558            String sourcePackageName = parser.getAttributeValue(null, "sourcePackageName");
559
560            final String sourceTag = parser.getAttributeValue(null, "sourceTag");
561
562            int eventType;
563            // Read out constraints tag.
564            do {
565                eventType = parser.next();
566            } while (eventType == XmlPullParser.TEXT);  // Push through to next START_TAG.
567
568            if (!(eventType == XmlPullParser.START_TAG &&
569                    XML_TAG_PARAMS_CONSTRAINTS.equals(parser.getName()))) {
570                // Expecting a <constraints> start tag.
571                return null;
572            }
573            try {
574                buildConstraintsFromXml(jobBuilder, parser);
575            } catch (NumberFormatException e) {
576                Slog.d(TAG, "Error reading constraints, skipping.");
577                return null;
578            }
579            parser.next(); // Consume </constraints>
580
581            // Read out execution parameters tag.
582            do {
583                eventType = parser.next();
584            } while (eventType == XmlPullParser.TEXT);
585            if (eventType != XmlPullParser.START_TAG) {
586                return null;
587            }
588
589            // Tuple of (earliest runtime, latest runtime) in elapsed realtime after disk load.
590            Pair<Long, Long> elapsedRuntimes;
591            try {
592                elapsedRuntimes = buildExecutionTimesFromXml(parser);
593            } catch (NumberFormatException e) {
594                if (DEBUG) {
595                    Slog.d(TAG, "Error parsing execution time parameters, skipping.");
596                }
597                return null;
598            }
599
600            final long elapsedNow = SystemClock.elapsedRealtime();
601            if (XML_TAG_PERIODIC.equals(parser.getName())) {
602                try {
603                    String val = parser.getAttributeValue(null, "period");
604                    final long periodMillis = Long.valueOf(val);
605                    val = parser.getAttributeValue(null, "flex");
606                    final long flexMillis = (val != null) ? Long.valueOf(val) : periodMillis;
607                    jobBuilder.setPeriodic(periodMillis, flexMillis);
608                    // As a sanity check, cap the recreated run time to be no later than flex+period
609                    // from now. This is the latest the periodic could be pushed out. This could
610                    // happen if the periodic ran early (at flex time before period), and then the
611                    // device rebooted.
612                    if (elapsedRuntimes.second > elapsedNow + periodMillis + flexMillis) {
613                        final long clampedLateRuntimeElapsed = elapsedNow + flexMillis
614                                + periodMillis;
615                        final long clampedEarlyRuntimeElapsed = clampedLateRuntimeElapsed
616                                - flexMillis;
617                        Slog.w(TAG,
618                                String.format("Periodic job for uid='%d' persisted run-time is" +
619                                                " too big [%s, %s]. Clamping to [%s,%s]",
620                                        uid,
621                                        DateUtils.formatElapsedTime(elapsedRuntimes.first / 1000),
622                                        DateUtils.formatElapsedTime(elapsedRuntimes.second / 1000),
623                                        DateUtils.formatElapsedTime(
624                                                clampedEarlyRuntimeElapsed / 1000),
625                                        DateUtils.formatElapsedTime(
626                                                clampedLateRuntimeElapsed / 1000))
627                        );
628                        elapsedRuntimes =
629                                Pair.create(clampedEarlyRuntimeElapsed, clampedLateRuntimeElapsed);
630                    }
631                } catch (NumberFormatException e) {
632                    Slog.d(TAG, "Error reading periodic execution criteria, skipping.");
633                    return null;
634                }
635            } else if (XML_TAG_ONEOFF.equals(parser.getName())) {
636                try {
637                    if (elapsedRuntimes.first != JobStatus.NO_EARLIEST_RUNTIME) {
638                        jobBuilder.setMinimumLatency(elapsedRuntimes.first - elapsedNow);
639                    }
640                    if (elapsedRuntimes.second != JobStatus.NO_LATEST_RUNTIME) {
641                        jobBuilder.setOverrideDeadline(
642                                elapsedRuntimes.second - elapsedNow);
643                    }
644                } catch (NumberFormatException e) {
645                    Slog.d(TAG, "Error reading job execution criteria, skipping.");
646                    return null;
647                }
648            } else {
649                if (DEBUG) {
650                    Slog.d(TAG, "Invalid parameter tag, skipping - " + parser.getName());
651                }
652                // Expecting a parameters start tag.
653                return null;
654            }
655            maybeBuildBackoffPolicyFromXml(jobBuilder, parser);
656
657            parser.nextTag(); // Consume parameters end tag.
658
659            // Read out extras Bundle.
660            do {
661                eventType = parser.next();
662            } while (eventType == XmlPullParser.TEXT);
663            if (!(eventType == XmlPullParser.START_TAG
664                    && XML_TAG_EXTRAS.equals(parser.getName()))) {
665                if (DEBUG) {
666                    Slog.d(TAG, "Error reading extras, skipping.");
667                }
668                return null;
669            }
670
671            PersistableBundle extras = PersistableBundle.restoreFromXml(parser);
672            jobBuilder.setExtras(extras);
673            parser.nextTag(); // Consume </extras>
674
675            // Migrate sync jobs forward from earlier, incomplete representation
676            if ("android".equals(sourcePackageName)
677                    && extras != null
678                    && extras.getBoolean("SyncManagerJob", false)) {
679                sourcePackageName = extras.getString("owningPackage", sourcePackageName);
680                if (DEBUG) {
681                    Slog.i(TAG, "Fixing up sync job source package name from 'android' to '"
682                            + sourcePackageName + "'");
683                }
684            }
685
686            // And now we're done
687            JobStatus js = new JobStatus(
688                    jobBuilder.build(), uid, sourcePackageName, sourceUserId, sourceTag,
689                    elapsedRuntimes.first, elapsedRuntimes.second);
690            return js;
691        }
692
693        private JobInfo.Builder buildBuilderFromXml(XmlPullParser parser) throws NumberFormatException {
694            // Pull out required fields from <job> attributes.
695            int jobId = Integer.parseInt(parser.getAttributeValue(null, "jobid"));
696            String packageName = parser.getAttributeValue(null, "package");
697            String className = parser.getAttributeValue(null, "class");
698            ComponentName cname = new ComponentName(packageName, className);
699
700            return new JobInfo.Builder(jobId, cname);
701        }
702
703        private void buildConstraintsFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser) {
704            String val = parser.getAttributeValue(null, "connectivity");
705            if (val != null) {
706                jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
707            }
708            val = parser.getAttributeValue(null, "unmetered");
709            if (val != null) {
710                jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED);
711            }
712            val = parser.getAttributeValue(null, "not-roaming");
713            if (val != null) {
714                jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_NOT_ROAMING);
715            }
716            val = parser.getAttributeValue(null, "idle");
717            if (val != null) {
718                jobBuilder.setRequiresDeviceIdle(true);
719            }
720            val = parser.getAttributeValue(null, "charging");
721            if (val != null) {
722                jobBuilder.setRequiresCharging(true);
723            }
724        }
725
726        /**
727         * Builds the back-off policy out of the params tag. These attributes may not exist, depending
728         * on whether the back-off was set when the job was first scheduled.
729         */
730        private void maybeBuildBackoffPolicyFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser) {
731            String val = parser.getAttributeValue(null, "initial-backoff");
732            if (val != null) {
733                long initialBackoff = Long.valueOf(val);
734                val = parser.getAttributeValue(null, "backoff-policy");
735                int backoffPolicy = Integer.parseInt(val);  // Will throw NFE which we catch higher up.
736                jobBuilder.setBackoffCriteria(initialBackoff, backoffPolicy);
737            }
738        }
739
740        /**
741         * Convenience function to read out and convert deadline and delay from xml into elapsed real
742         * time.
743         * @return A {@link android.util.Pair}, where the first value is the earliest elapsed runtime
744         * and the second is the latest elapsed runtime.
745         */
746        private Pair<Long, Long> buildExecutionTimesFromXml(XmlPullParser parser)
747                throws NumberFormatException {
748            // Pull out execution time data.
749            final long nowWallclock = System.currentTimeMillis();
750            final long nowElapsed = SystemClock.elapsedRealtime();
751
752            long earliestRunTimeElapsed = JobStatus.NO_EARLIEST_RUNTIME;
753            long latestRunTimeElapsed = JobStatus.NO_LATEST_RUNTIME;
754            String val = parser.getAttributeValue(null, "deadline");
755            if (val != null) {
756                long latestRuntimeWallclock = Long.valueOf(val);
757                long maxDelayElapsed =
758                        Math.max(latestRuntimeWallclock - nowWallclock, 0);
759                latestRunTimeElapsed = nowElapsed + maxDelayElapsed;
760            }
761            val = parser.getAttributeValue(null, "delay");
762            if (val != null) {
763                long earliestRuntimeWallclock = Long.valueOf(val);
764                long minDelayElapsed =
765                        Math.max(earliestRuntimeWallclock - nowWallclock, 0);
766                earliestRunTimeElapsed = nowElapsed + minDelayElapsed;
767
768            }
769            return Pair.create(earliestRunTimeElapsed, latestRunTimeElapsed);
770        }
771    }
772
773    static class JobSet {
774        // Key is the getUid() originator of the jobs in each sheaf
775        private SparseArray<ArraySet<JobStatus>> mJobs;
776
777        public JobSet() {
778            mJobs = new SparseArray<ArraySet<JobStatus>>();
779        }
780
781        public List<JobStatus> getJobsByUid(int uid) {
782            ArrayList<JobStatus> matchingJobs = new ArrayList<JobStatus>();
783            ArraySet<JobStatus> jobs = mJobs.get(uid);
784            if (jobs != null) {
785                matchingJobs.addAll(jobs);
786            }
787            return matchingJobs;
788        }
789
790        // By user, not by uid, so we need to traverse by key and check
791        public List<JobStatus> getJobsByUser(int userId) {
792            ArrayList<JobStatus> result = new ArrayList<JobStatus>();
793            for (int i = mJobs.size() - 1; i >= 0; i--) {
794                if (UserHandle.getUserId(mJobs.keyAt(i)) == userId) {
795                    ArraySet<JobStatus> jobs = mJobs.get(i);
796                    if (jobs != null) {
797                        result.addAll(jobs);
798                    }
799                }
800            }
801            return result;
802        }
803
804        public boolean add(JobStatus job) {
805            final int uid = job.getUid();
806            ArraySet<JobStatus> jobs = mJobs.get(uid);
807            if (jobs == null) {
808                jobs = new ArraySet<JobStatus>();
809                mJobs.put(uid, jobs);
810            }
811            return jobs.add(job);
812        }
813
814        public boolean remove(JobStatus job) {
815            final int uid = job.getUid();
816            ArraySet<JobStatus> jobs = mJobs.get(uid);
817            boolean didRemove = (jobs != null) ? jobs.remove(job) : false;
818            if (didRemove && jobs.size() == 0) {
819                // no more jobs for this uid; let the now-empty set object be GC'd.
820                mJobs.remove(uid);
821            }
822            return didRemove;
823        }
824
825        public boolean contains(JobStatus job) {
826            final int uid = job.getUid();
827            ArraySet<JobStatus> jobs = mJobs.get(uid);
828            return jobs != null && jobs.contains(job);
829        }
830
831        public JobStatus get(int uid, int jobId) {
832            ArraySet<JobStatus> jobs = mJobs.get(uid);
833            if (jobs != null) {
834                for (int i = jobs.size() - 1; i >= 0; i--) {
835                    JobStatus job = jobs.valueAt(i);
836                    if (job.getJobId() == jobId) {
837                        return job;
838                    }
839                }
840            }
841            return null;
842        }
843
844        // Inefficient; use only for testing
845        public List<JobStatus> getAllJobs() {
846            ArrayList<JobStatus> allJobs = new ArrayList<JobStatus>(size());
847            for (int i = mJobs.size() - 1; i >= 0; i--) {
848                ArraySet<JobStatus> jobs = mJobs.valueAt(i);
849                if (jobs != null) {
850                    // Use a for loop over the ArraySet, so we don't need to make its
851                    // optional collection class iterator implementation or have to go
852                    // through a temporary array from toArray().
853                    for (int j = jobs.size() - 1; j >= 0; j--) {
854                        allJobs.add(jobs.valueAt(j));
855                    }
856                }
857            }
858            return allJobs;
859        }
860
861        public void clear() {
862            mJobs.clear();
863        }
864
865        public int size() {
866            int total = 0;
867            for (int i = mJobs.size() - 1; i >= 0; i--) {
868                total += mJobs.valueAt(i).size();
869            }
870            return total;
871        }
872
873        // We only want to count the jobs that this uid has scheduled on its own
874        // behalf, not those that the app has scheduled on someone else's behalf.
875        public int countJobsForUid(int uid) {
876            int total = 0;
877            ArraySet<JobStatus> jobs = mJobs.get(uid);
878            if (jobs != null) {
879                for (int i = jobs.size() - 1; i >= 0; i--) {
880                    JobStatus job = jobs.valueAt(i);
881                    if (job.getUid() == job.getSourceUid()) {
882                        total++;
883                    }
884                }
885            }
886            return total;
887        }
888
889        public void forEachJob(JobStatusFunctor functor) {
890            for (int uidIndex = mJobs.size() - 1; uidIndex >= 0; uidIndex--) {
891                ArraySet<JobStatus> jobs = mJobs.valueAt(uidIndex);
892                for (int i = jobs.size() - 1; i >= 0; i--) {
893                    functor.process(jobs.valueAt(i));
894                }
895            }
896        }
897
898        public void forEachJob(int uid, JobStatusFunctor functor) {
899            ArraySet<JobStatus> jobs = mJobs.get(uid);
900            if (jobs != null) {
901                for (int i = jobs.size() - 1; i >= 0; i--) {
902                    functor.process(jobs.valueAt(i));
903                }
904            }
905        }
906    }
907}
908