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