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