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