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.util.AtomicFile;
28import android.util.ArraySet;
29import android.util.Pair;
30import android.util.Slog;
31import android.util.Xml;
32
33import com.android.internal.annotations.VisibleForTesting;
34import com.android.internal.util.FastXmlSerializer;
35import com.android.server.IoThread;
36import com.android.server.job.controllers.JobStatus;
37
38import java.io.ByteArrayOutputStream;
39import java.io.File;
40import java.io.FileInputStream;
41import java.io.FileNotFoundException;
42import java.io.FileOutputStream;
43import java.io.IOException;
44import java.util.ArrayList;
45import java.util.Iterator;
46import java.util.List;
47
48import org.xmlpull.v1.XmlPullParser;
49import org.xmlpull.v1.XmlPullParserException;
50import org.xmlpull.v1.XmlSerializer;
51
52/**
53 * Maintains the master list of jobs that the job scheduler is tracking. These jobs are compared by
54 * reference, so none of the functions in this class should make a copy.
55 * Also handles read/write of persisted jobs.
56 *
57 * Note on locking:
58 *      All callers to this class must <strong>lock on the class object they are calling</strong>.
59 *      This is important b/c {@link com.android.server.job.JobStore.WriteJobsMapToDiskRunnable}
60 *      and {@link com.android.server.job.JobStore.ReadJobMapFromDiskRunnable} lock on that
61 *      object.
62 */
63public class JobStore {
64    private static final String TAG = "JobStore";
65    private static final boolean DEBUG = JobSchedulerService.DEBUG;
66
67    /** Threshold to adjust how often we want to write to the db. */
68    private static final int MAX_OPS_BEFORE_WRITE = 1;
69    final ArraySet<JobStatus> mJobSet;
70    final Context mContext;
71
72    private int mDirtyOperations;
73
74    private static final Object sSingletonLock = new Object();
75    private final AtomicFile mJobsFile;
76    /** Handler backed by IoThread for writing to disk. */
77    private final Handler mIoHandler = IoThread.getHandler();
78    private static JobStore sSingleton;
79
80    /** Used by the {@link JobSchedulerService} to instantiate the JobStore. */
81    static JobStore initAndGet(JobSchedulerService jobManagerService) {
82        synchronized (sSingletonLock) {
83            if (sSingleton == null) {
84                sSingleton = new JobStore(jobManagerService.getContext(),
85                        Environment.getDataDirectory());
86            }
87            return sSingleton;
88        }
89    }
90
91    /**
92     * @return A freshly initialized job store object, with no loaded jobs.
93     */
94    @VisibleForTesting
95    public static JobStore initAndGetForTesting(Context context, File dataDir) {
96        JobStore jobStoreUnderTest = new JobStore(context, dataDir);
97        jobStoreUnderTest.clear();
98        return jobStoreUnderTest;
99    }
100
101    /**
102     * Construct the instance of the job store. This results in a blocking read from disk.
103     */
104    private JobStore(Context context, File dataDir) {
105        mContext = context;
106        mDirtyOperations = 0;
107
108        File systemDir = new File(dataDir, "system");
109        File jobDir = new File(systemDir, "job");
110        jobDir.mkdirs();
111        mJobsFile = new AtomicFile(new File(jobDir, "jobs.xml"));
112
113        mJobSet = new ArraySet<JobStatus>();
114
115        readJobMapFromDisk(mJobSet);
116    }
117
118    /**
119     * Add a job to the master list, persisting it if necessary. If the JobStatus already exists,
120     * it will be replaced.
121     * @param jobStatus Job to add.
122     * @return Whether or not an equivalent JobStatus was replaced by this operation.
123     */
124    public boolean add(JobStatus jobStatus) {
125        boolean replaced = mJobSet.remove(jobStatus);
126        mJobSet.add(jobStatus);
127        if (jobStatus.isPersisted()) {
128            maybeWriteStatusToDiskAsync();
129        }
130        if (DEBUG) {
131            Slog.d(TAG, "Added job status to store: " + jobStatus);
132        }
133        return replaced;
134    }
135
136    /**
137     * Whether this jobStatus object already exists in the JobStore.
138     */
139    public boolean containsJobIdForUid(int jobId, int uId) {
140        for (int i=mJobSet.size()-1; i>=0; i--) {
141            JobStatus ts = mJobSet.valueAt(i);
142            if (ts.getUid() == uId && ts.getJobId() == jobId) {
143                return true;
144            }
145        }
146        return false;
147    }
148
149    boolean containsJob(JobStatus jobStatus) {
150        return mJobSet.contains(jobStatus);
151    }
152
153    public int size() {
154        return mJobSet.size();
155    }
156
157    /**
158     * Remove the provided job. Will also delete the job if it was persisted.
159     * @return Whether or not the job existed to be removed.
160     */
161    public boolean remove(JobStatus jobStatus) {
162        boolean removed = mJobSet.remove(jobStatus);
163        if (!removed) {
164            if (DEBUG) {
165                Slog.d(TAG, "Couldn't remove job: didn't exist: " + jobStatus);
166            }
167            return false;
168        }
169        if (jobStatus.isPersisted()) {
170            maybeWriteStatusToDiskAsync();
171        }
172        return removed;
173    }
174
175    @VisibleForTesting
176    public void clear() {
177        mJobSet.clear();
178        maybeWriteStatusToDiskAsync();
179    }
180
181    /**
182     * @param userHandle User for whom we are querying the list of jobs.
183     * @return A list of all the jobs scheduled by the provided user. Never null.
184     */
185    public List<JobStatus> getJobsByUser(int userHandle) {
186        List<JobStatus> matchingJobs = new ArrayList<JobStatus>();
187        Iterator<JobStatus> it = mJobSet.iterator();
188        while (it.hasNext()) {
189            JobStatus ts = it.next();
190            if (UserHandle.getUserId(ts.getUid()) == userHandle) {
191                matchingJobs.add(ts);
192            }
193        }
194        return matchingJobs;
195    }
196
197    /**
198     * @param uid Uid of the requesting app.
199     * @return All JobStatus objects for a given uid from the master list. Never null.
200     */
201    public List<JobStatus> getJobsByUid(int uid) {
202        List<JobStatus> matchingJobs = new ArrayList<JobStatus>();
203        Iterator<JobStatus> it = mJobSet.iterator();
204        while (it.hasNext()) {
205            JobStatus ts = it.next();
206            if (ts.getUid() == uid) {
207                matchingJobs.add(ts);
208            }
209        }
210        return matchingJobs;
211    }
212
213    /**
214     * @param uid Uid of the requesting app.
215     * @param jobId Job id, specified at schedule-time.
216     * @return the JobStatus that matches the provided uId and jobId, or null if none found.
217     */
218    public JobStatus getJobByUidAndJobId(int uid, int jobId) {
219        Iterator<JobStatus> it = mJobSet.iterator();
220        while (it.hasNext()) {
221            JobStatus ts = it.next();
222            if (ts.getUid() == uid && ts.getJobId() == jobId) {
223                return ts;
224            }
225        }
226        return null;
227    }
228
229    /**
230     * @return The live array of JobStatus objects.
231     */
232    public ArraySet<JobStatus> getJobs() {
233        return mJobSet;
234    }
235
236    /** Version of the db schema. */
237    private static final int JOBS_FILE_VERSION = 0;
238    /** Tag corresponds to constraints this job needs. */
239    private static final String XML_TAG_PARAMS_CONSTRAINTS = "constraints";
240    /** Tag corresponds to execution parameters. */
241    private static final String XML_TAG_PERIODIC = "periodic";
242    private static final String XML_TAG_ONEOFF = "one-off";
243    private static final String XML_TAG_EXTRAS = "extras";
244
245    /**
246     * Every time the state changes we write all the jobs in one swath, instead of trying to
247     * track incremental changes.
248     * @return Whether the operation was successful. This will only fail for e.g. if the system is
249     * low on storage. If this happens, we continue as normal
250     */
251    private void maybeWriteStatusToDiskAsync() {
252        mDirtyOperations++;
253        if (mDirtyOperations >= MAX_OPS_BEFORE_WRITE) {
254            if (DEBUG) {
255                Slog.v(TAG, "Writing jobs to disk.");
256            }
257            mIoHandler.post(new WriteJobsMapToDiskRunnable());
258        }
259    }
260
261    @VisibleForTesting
262    public void readJobMapFromDisk(ArraySet<JobStatus> jobSet) {
263        new ReadJobMapFromDiskRunnable(jobSet).run();
264    }
265
266    /**
267     * Runnable that writes {@link #mJobSet} out to xml.
268     * NOTE: This Runnable locks on JobStore.this
269     */
270    private class WriteJobsMapToDiskRunnable implements Runnable {
271        @Override
272        public void run() {
273            final long startElapsed = SystemClock.elapsedRealtime();
274            List<JobStatus> mStoreCopy = new ArrayList<JobStatus>();
275            synchronized (JobStore.this) {
276                // Copy over the jobs so we can release the lock before writing.
277                for (int i=0; i<mJobSet.size(); i++) {
278                    JobStatus jobStatus = mJobSet.valueAt(i);
279                    JobStatus copy = new JobStatus(jobStatus.getJob(), jobStatus.getUid(),
280                            jobStatus.getEarliestRunTime(), jobStatus.getLatestRunTimeElapsed());
281                    mStoreCopy.add(copy);
282                }
283            }
284            writeJobsMapImpl(mStoreCopy);
285            if (JobSchedulerService.DEBUG) {
286                Slog.v(TAG, "Finished writing, took " + (SystemClock.elapsedRealtime()
287                        - startElapsed) + "ms");
288            }
289        }
290
291        private void writeJobsMapImpl(List<JobStatus> jobList) {
292            try {
293                ByteArrayOutputStream baos = new ByteArrayOutputStream();
294                XmlSerializer out = new FastXmlSerializer();
295                out.setOutput(baos, "utf-8");
296                out.startDocument(null, true);
297                out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
298
299                out.startTag(null, "job-info");
300                out.attribute(null, "version", Integer.toString(JOBS_FILE_VERSION));
301                for (int i=0; i<jobList.size(); i++) {
302                    JobStatus jobStatus = jobList.get(i);
303                    if (DEBUG) {
304                        Slog.d(TAG, "Saving job " + jobStatus.getJobId());
305                    }
306                    out.startTag(null, "job");
307                    addIdentifierAttributesToJobTag(out, jobStatus);
308                    writeConstraintsToXml(out, jobStatus);
309                    writeExecutionCriteriaToXml(out, jobStatus);
310                    writeBundleToXml(jobStatus.getExtras(), out);
311                    out.endTag(null, "job");
312                }
313                out.endTag(null, "job-info");
314                out.endDocument();
315
316                // Write out to disk in one fell sweep.
317                FileOutputStream fos = mJobsFile.startWrite();
318                fos.write(baos.toByteArray());
319                mJobsFile.finishWrite(fos);
320                mDirtyOperations = 0;
321            } catch (IOException e) {
322                if (DEBUG) {
323                    Slog.v(TAG, "Error writing out job data.", e);
324                }
325            } catch (XmlPullParserException e) {
326                if (DEBUG) {
327                    Slog.d(TAG, "Error persisting bundle.", e);
328                }
329            }
330        }
331
332        /** Write out a tag with data comprising the required fields of this job and its client. */
333        private void addIdentifierAttributesToJobTag(XmlSerializer out, JobStatus jobStatus)
334                throws IOException {
335            out.attribute(null, "jobid", Integer.toString(jobStatus.getJobId()));
336            out.attribute(null, "package", jobStatus.getServiceComponent().getPackageName());
337            out.attribute(null, "class", jobStatus.getServiceComponent().getClassName());
338            out.attribute(null, "uid", Integer.toString(jobStatus.getUid()));
339        }
340
341        private void writeBundleToXml(PersistableBundle extras, XmlSerializer out)
342                throws IOException, XmlPullParserException {
343            out.startTag(null, XML_TAG_EXTRAS);
344            extras.saveToXml(out);
345            out.endTag(null, XML_TAG_EXTRAS);
346        }
347        /**
348         * Write out a tag with data identifying this job's constraints. If the constraint isn't here
349         * it doesn't apply.
350         */
351        private void writeConstraintsToXml(XmlSerializer out, JobStatus jobStatus) throws IOException {
352            out.startTag(null, XML_TAG_PARAMS_CONSTRAINTS);
353            if (jobStatus.hasUnmeteredConstraint()) {
354                out.attribute(null, "unmetered", Boolean.toString(true));
355            }
356            if (jobStatus.hasConnectivityConstraint()) {
357                out.attribute(null, "connectivity", Boolean.toString(true));
358            }
359            if (jobStatus.hasIdleConstraint()) {
360                out.attribute(null, "idle", Boolean.toString(true));
361            }
362            if (jobStatus.hasChargingConstraint()) {
363                out.attribute(null, "charging", Boolean.toString(true));
364            }
365            out.endTag(null, XML_TAG_PARAMS_CONSTRAINTS);
366        }
367
368        private void writeExecutionCriteriaToXml(XmlSerializer out, JobStatus jobStatus)
369                throws IOException {
370            final JobInfo job = jobStatus.getJob();
371            if (jobStatus.getJob().isPeriodic()) {
372                out.startTag(null, XML_TAG_PERIODIC);
373                out.attribute(null, "period", Long.toString(job.getIntervalMillis()));
374            } else {
375                out.startTag(null, XML_TAG_ONEOFF);
376            }
377
378            if (jobStatus.hasDeadlineConstraint()) {
379                // Wall clock deadline.
380                final long deadlineWallclock =  System.currentTimeMillis() +
381                        (jobStatus.getLatestRunTimeElapsed() - SystemClock.elapsedRealtime());
382                out.attribute(null, "deadline", Long.toString(deadlineWallclock));
383            }
384            if (jobStatus.hasTimingDelayConstraint()) {
385                final long delayWallclock = System.currentTimeMillis() +
386                        (jobStatus.getEarliestRunTime() - SystemClock.elapsedRealtime());
387                out.attribute(null, "delay", Long.toString(delayWallclock));
388            }
389
390            // Only write out back-off policy if it differs from the default.
391            // This also helps the case where the job is idle -> these aren't allowed to specify
392            // back-off.
393            if (jobStatus.getJob().getInitialBackoffMillis() != JobInfo.DEFAULT_INITIAL_BACKOFF_MILLIS
394                    || jobStatus.getJob().getBackoffPolicy() != JobInfo.DEFAULT_BACKOFF_POLICY) {
395                out.attribute(null, "backoff-policy", Integer.toString(job.getBackoffPolicy()));
396                out.attribute(null, "initial-backoff", Long.toString(job.getInitialBackoffMillis()));
397            }
398            if (job.isPeriodic()) {
399                out.endTag(null, XML_TAG_PERIODIC);
400            } else {
401                out.endTag(null, XML_TAG_ONEOFF);
402            }
403        }
404    }
405
406    /**
407     * Runnable that reads list of persisted job from xml. This is run once at start up, so doesn't
408     * need to go through {@link JobStore#add(com.android.server.job.controllers.JobStatus)}.
409     */
410    private class ReadJobMapFromDiskRunnable implements Runnable {
411        private final ArraySet<JobStatus> jobSet;
412
413        /**
414         * @param jobSet Reference to the (empty) set of JobStatus objects that back the JobStore,
415         *               so that after disk read we can populate it directly.
416         */
417        ReadJobMapFromDiskRunnable(ArraySet<JobStatus> jobSet) {
418            this.jobSet = jobSet;
419        }
420
421        @Override
422        public void run() {
423            try {
424                List<JobStatus> jobs;
425                FileInputStream fis = mJobsFile.openRead();
426                synchronized (JobStore.this) {
427                    jobs = readJobMapImpl(fis);
428                    if (jobs != null) {
429                        for (int i=0; i<jobs.size(); i++) {
430                            this.jobSet.add(jobs.get(i));
431                        }
432                    }
433                }
434                fis.close();
435            } catch (FileNotFoundException e) {
436                if (JobSchedulerService.DEBUG) {
437                    Slog.d(TAG, "Could not find jobs file, probably there was nothing to load.");
438                }
439            } catch (XmlPullParserException e) {
440                if (JobSchedulerService.DEBUG) {
441                    Slog.d(TAG, "Error parsing xml.", e);
442                }
443            } catch (IOException e) {
444                if (JobSchedulerService.DEBUG) {
445                    Slog.d(TAG, "Error parsing xml.", e);
446                }
447            }
448        }
449
450        private List<JobStatus> readJobMapImpl(FileInputStream fis)
451                throws XmlPullParserException, IOException {
452            XmlPullParser parser = Xml.newPullParser();
453            parser.setInput(fis, null);
454
455            int eventType = parser.getEventType();
456            while (eventType != XmlPullParser.START_TAG &&
457                    eventType != XmlPullParser.END_DOCUMENT) {
458                eventType = parser.next();
459                Slog.d(TAG, parser.getName());
460            }
461            if (eventType == XmlPullParser.END_DOCUMENT) {
462                if (DEBUG) {
463                    Slog.d(TAG, "No persisted jobs.");
464                }
465                return null;
466            }
467
468            String tagName = parser.getName();
469            if ("job-info".equals(tagName)) {
470                final List<JobStatus> jobs = new ArrayList<JobStatus>();
471                // Read in version info.
472                try {
473                    int version = Integer.valueOf(parser.getAttributeValue(null, "version"));
474                    if (version != JOBS_FILE_VERSION) {
475                        Slog.d(TAG, "Invalid version number, aborting jobs file read.");
476                        return null;
477                    }
478                } catch (NumberFormatException e) {
479                    Slog.e(TAG, "Invalid version number, aborting jobs file read.");
480                    return null;
481                }
482                eventType = parser.next();
483                do {
484                    // Read each <job/>
485                    if (eventType == XmlPullParser.START_TAG) {
486                        tagName = parser.getName();
487                        // Start reading job.
488                        if ("job".equals(tagName)) {
489                            JobStatus persistedJob = restoreJobFromXml(parser);
490                            if (persistedJob != null) {
491                                if (DEBUG) {
492                                    Slog.d(TAG, "Read out " + persistedJob);
493                                }
494                                jobs.add(persistedJob);
495                            } else {
496                                Slog.d(TAG, "Error reading job from file.");
497                            }
498                        }
499                    }
500                    eventType = parser.next();
501                } while (eventType != XmlPullParser.END_DOCUMENT);
502                return jobs;
503            }
504            return null;
505        }
506
507        /**
508         * @param parser Xml parser at the beginning of a "<job/>" tag. The next "parser.next()" call
509         *               will take the parser into the body of the job tag.
510         * @return Newly instantiated job holding all the information we just read out of the xml tag.
511         */
512        private JobStatus restoreJobFromXml(XmlPullParser parser) throws XmlPullParserException,
513                IOException {
514            JobInfo.Builder jobBuilder;
515            int uid;
516
517            // Read out job identifier attributes.
518            try {
519                jobBuilder = buildBuilderFromXml(parser);
520                jobBuilder.setPersisted(true);
521                uid = Integer.valueOf(parser.getAttributeValue(null, "uid"));
522            } catch (NumberFormatException e) {
523                Slog.e(TAG, "Error parsing job's required fields, skipping");
524                return null;
525            }
526
527            int eventType;
528            // Read out constraints tag.
529            do {
530                eventType = parser.next();
531            } while (eventType == XmlPullParser.TEXT);  // Push through to next START_TAG.
532
533            if (!(eventType == XmlPullParser.START_TAG &&
534                    XML_TAG_PARAMS_CONSTRAINTS.equals(parser.getName()))) {
535                // Expecting a <constraints> start tag.
536                return null;
537            }
538            try {
539                buildConstraintsFromXml(jobBuilder, parser);
540            } catch (NumberFormatException e) {
541                Slog.d(TAG, "Error reading constraints, skipping.");
542                return null;
543            }
544            parser.next(); // Consume </constraints>
545
546            // Read out execution parameters tag.
547            do {
548                eventType = parser.next();
549            } while (eventType == XmlPullParser.TEXT);
550            if (eventType != XmlPullParser.START_TAG) {
551                return null;
552            }
553
554            Pair<Long, Long> runtimes;
555            try {
556                runtimes = buildExecutionTimesFromXml(parser);
557            } catch (NumberFormatException e) {
558                if (DEBUG) {
559                    Slog.d(TAG, "Error parsing execution time parameters, skipping.");
560                }
561                return null;
562            }
563
564            if (XML_TAG_PERIODIC.equals(parser.getName())) {
565                try {
566                    String val = parser.getAttributeValue(null, "period");
567                    jobBuilder.setPeriodic(Long.valueOf(val));
568                } catch (NumberFormatException e) {
569                    Slog.d(TAG, "Error reading periodic execution criteria, skipping.");
570                    return null;
571                }
572            } else if (XML_TAG_ONEOFF.equals(parser.getName())) {
573                try {
574                    if (runtimes.first != JobStatus.NO_EARLIEST_RUNTIME) {
575                        jobBuilder.setMinimumLatency(runtimes.first - SystemClock.elapsedRealtime());
576                    }
577                    if (runtimes.second != JobStatus.NO_LATEST_RUNTIME) {
578                        jobBuilder.setOverrideDeadline(
579                                runtimes.second - SystemClock.elapsedRealtime());
580                    }
581                } catch (NumberFormatException e) {
582                    Slog.d(TAG, "Error reading job execution criteria, skipping.");
583                    return null;
584                }
585            } else {
586                if (DEBUG) {
587                    Slog.d(TAG, "Invalid parameter tag, skipping - " + parser.getName());
588                }
589                // Expecting a parameters start tag.
590                return null;
591            }
592            maybeBuildBackoffPolicyFromXml(jobBuilder, parser);
593
594            parser.nextTag(); // Consume parameters end tag.
595
596            // Read out extras Bundle.
597            do {
598                eventType = parser.next();
599            } while (eventType == XmlPullParser.TEXT);
600            if (!(eventType == XmlPullParser.START_TAG && XML_TAG_EXTRAS.equals(parser.getName()))) {
601                if (DEBUG) {
602                    Slog.d(TAG, "Error reading extras, skipping.");
603                }
604                return null;
605            }
606
607            PersistableBundle extras = PersistableBundle.restoreFromXml(parser);
608            jobBuilder.setExtras(extras);
609            parser.nextTag(); // Consume </extras>
610
611            return new JobStatus(jobBuilder.build(), uid, runtimes.first, runtimes.second);
612        }
613
614        private JobInfo.Builder buildBuilderFromXml(XmlPullParser parser) throws NumberFormatException {
615            // Pull out required fields from <job> attributes.
616            int jobId = Integer.valueOf(parser.getAttributeValue(null, "jobid"));
617            String packageName = parser.getAttributeValue(null, "package");
618            String className = parser.getAttributeValue(null, "class");
619            ComponentName cname = new ComponentName(packageName, className);
620
621            return new JobInfo.Builder(jobId, cname);
622        }
623
624        private void buildConstraintsFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser) {
625            String val = parser.getAttributeValue(null, "unmetered");
626            if (val != null) {
627                jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED);
628            }
629            val = parser.getAttributeValue(null, "connectivity");
630            if (val != null) {
631                jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
632            }
633            val = parser.getAttributeValue(null, "idle");
634            if (val != null) {
635                jobBuilder.setRequiresDeviceIdle(true);
636            }
637            val = parser.getAttributeValue(null, "charging");
638            if (val != null) {
639                jobBuilder.setRequiresCharging(true);
640            }
641        }
642
643        /**
644         * Builds the back-off policy out of the params tag. These attributes may not exist, depending
645         * on whether the back-off was set when the job was first scheduled.
646         */
647        private void maybeBuildBackoffPolicyFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser) {
648            String val = parser.getAttributeValue(null, "initial-backoff");
649            if (val != null) {
650                long initialBackoff = Long.valueOf(val);
651                val = parser.getAttributeValue(null, "backoff-policy");
652                int backoffPolicy = Integer.valueOf(val);  // Will throw NFE which we catch higher up.
653                jobBuilder.setBackoffCriteria(initialBackoff, backoffPolicy);
654            }
655        }
656
657        /**
658         * Convenience function to read out and convert deadline and delay from xml into elapsed real
659         * time.
660         * @return A {@link android.util.Pair}, where the first value is the earliest elapsed runtime
661         * and the second is the latest elapsed runtime.
662         */
663        private Pair<Long, Long> buildExecutionTimesFromXml(XmlPullParser parser)
664                throws NumberFormatException {
665            // Pull out execution time data.
666            final long nowWallclock = System.currentTimeMillis();
667            final long nowElapsed = SystemClock.elapsedRealtime();
668
669            long earliestRunTimeElapsed = JobStatus.NO_EARLIEST_RUNTIME;
670            long latestRunTimeElapsed = JobStatus.NO_LATEST_RUNTIME;
671            String val = parser.getAttributeValue(null, "deadline");
672            if (val != null) {
673                long latestRuntimeWallclock = Long.valueOf(val);
674                long maxDelayElapsed =
675                        Math.max(latestRuntimeWallclock - nowWallclock, 0);
676                latestRunTimeElapsed = nowElapsed + maxDelayElapsed;
677            }
678            val = parser.getAttributeValue(null, "delay");
679            if (val != null) {
680                long earliestRuntimeWallclock = Long.valueOf(val);
681                long minDelayElapsed =
682                        Math.max(earliestRuntimeWallclock - nowWallclock, 0);
683                earliestRunTimeElapsed = nowElapsed + minDelayElapsed;
684
685            }
686            return Pair.create(earliestRunTimeElapsed, latestRunTimeElapsed);
687        }
688    }
689}
690