Home | History | Annotate | Download | only in job
      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. */
     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
     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      */
    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      */
    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      */
    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 
    150     boolean containsJob(JobStatus jobStatus) {
    151         return mJobSet.contains(jobStatus);
    152     }
    153 
    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      */
    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
    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      */
    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      */
    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      */
    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      */
    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      */
    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
    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
    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 
    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. */
    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 
    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          */
    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 
    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          */
    418         ReadJobMapFromDiskRunnable(ArraySet<JobStatus> jobSet) {
    419             this.jobSet = jobSet;
    420         }
    421 
    422         @Override
    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 
    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          */
    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 
    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 
    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          */
    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          */
    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