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