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