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.text.format.DateUtils;
     28 import android.util.AtomicFile;
     29 import android.util.ArraySet;
     30 import android.util.Pair;
     31 import android.util.Slog;
     32 import android.util.SparseArray;
     33 import android.util.Xml;
     34 
     35 import com.android.internal.annotations.VisibleForTesting;
     36 import com.android.internal.util.FastXmlSerializer;
     37 import com.android.server.IoThread;
     38 import com.android.server.job.controllers.JobStatus;
     39 
     40 import java.io.ByteArrayOutputStream;
     41 import java.io.File;
     42 import java.io.FileInputStream;
     43 import java.io.FileNotFoundException;
     44 import java.io.FileOutputStream;
     45 import java.io.IOException;
     46 import java.nio.charset.StandardCharsets;
     47 import java.util.ArrayList;
     48 import java.util.List;
     49 import java.util.Set;
     50 
     51 import org.xmlpull.v1.XmlPullParser;
     52 import org.xmlpull.v1.XmlPullParserException;
     53 import org.xmlpull.v1.XmlSerializer;
     54 
     55 /**
     56  * Maintains the master list of jobs that the job scheduler is tracking. These jobs are compared by
     57  * reference, so none of the functions in this class should make a copy.
     58  * Also handles read/write of persisted jobs.
     59  *
     60  * Note on locking:
     61  *      All callers to this class must <strong>lock on the class object they are calling</strong>.
     62  *      This is important b/c {@link com.android.server.job.JobStore.WriteJobsMapToDiskRunnable}
     63  *      and {@link com.android.server.job.JobStore.ReadJobMapFromDiskRunnable} lock on that
     64  *      object.
     65  */
     66 public class JobStore {
     67     private static final String TAG = "JobStore";
     68     private static final boolean DEBUG = JobSchedulerService.DEBUG;
     69 
     70     /** Threshold to adjust how often we want to write to the db. */
     71     private static final int MAX_OPS_BEFORE_WRITE = 1;
     72     final Object mLock;
     73     final JobSet mJobSet; // per-caller-uid tracking
     74     final Context mContext;
     75 
     76     private int mDirtyOperations;
     77 
     78     private static final Object sSingletonLock = new Object();
     79     private final AtomicFile mJobsFile;
     80     /** Handler backed by IoThread for writing to disk. */
     81     private final Handler mIoHandler = IoThread.getHandler();
     82     private static JobStore sSingleton;
     83 
     84     /** Used by the {@link JobSchedulerService} to instantiate the JobStore. */
     85     static JobStore initAndGet(JobSchedulerService jobManagerService) {
     86         synchronized (sSingletonLock) {
     87             if (sSingleton == null) {
     88                 sSingleton = new JobStore(jobManagerService.getContext(),
     89                         jobManagerService.getLock(), Environment.getDataDirectory());
     90             }
     91             return sSingleton;
     92         }
     93     }
     94 
     95     /**
     96      * @return A freshly initialized job store object, with no loaded jobs.
     97      */
     98     @VisibleForTesting
     99     public static JobStore initAndGetForTesting(Context context, File dataDir) {
    100         JobStore jobStoreUnderTest = new JobStore(context, new Object(), dataDir);
    101         jobStoreUnderTest.clear();
    102         return jobStoreUnderTest;
    103     }
    104 
    105     /**
    106      * Construct the instance of the job store. This results in a blocking read from disk.
    107      */
    108     private JobStore(Context context, Object lock, File dataDir) {
    109         mLock = lock;
    110         mContext = context;
    111         mDirtyOperations = 0;
    112 
    113         File systemDir = new File(dataDir, "system");
    114         File jobDir = new File(systemDir, "job");
    115         jobDir.mkdirs();
    116         mJobsFile = new AtomicFile(new File(jobDir, "jobs.xml"));
    117 
    118         mJobSet = new JobSet();
    119 
    120         readJobMapFromDisk(mJobSet);
    121     }
    122 
    123     /**
    124      * Add a job to the master list, persisting it if necessary. If the JobStatus already exists,
    125      * it will be replaced.
    126      * @param jobStatus Job to add.
    127      * @return Whether or not an equivalent JobStatus was replaced by this operation.
    128      */
    129     public boolean add(JobStatus jobStatus) {
    130         boolean replaced = mJobSet.remove(jobStatus);
    131         mJobSet.add(jobStatus);
    132         if (jobStatus.isPersisted()) {
    133             maybeWriteStatusToDiskAsync();
    134         }
    135         if (DEBUG) {
    136             Slog.d(TAG, "Added job status to store: " + jobStatus);
    137         }
    138         return replaced;
    139     }
    140 
    141     boolean containsJob(JobStatus jobStatus) {
    142         return mJobSet.contains(jobStatus);
    143     }
    144 
    145     public int size() {
    146         return mJobSet.size();
    147     }
    148 
    149     public int countJobsForUid(int uid) {
    150         return mJobSet.countJobsForUid(uid);
    151     }
    152 
    153     /**
    154      * Remove the provided job. Will also delete the job if it was persisted.
    155      * @param writeBack If true, the job will be deleted (if it was persisted) immediately.
    156      * @return Whether or not the job existed to be removed.
    157      */
    158     public boolean remove(JobStatus jobStatus, boolean writeBack) {
    159         boolean removed = mJobSet.remove(jobStatus);
    160         if (!removed) {
    161             if (DEBUG) {
    162                 Slog.d(TAG, "Couldn't remove job: didn't exist: " + jobStatus);
    163             }
    164             return false;
    165         }
    166         if (writeBack && jobStatus.isPersisted()) {
    167             maybeWriteStatusToDiskAsync();
    168         }
    169         return removed;
    170     }
    171 
    172     @VisibleForTesting
    173     public void clear() {
    174         mJobSet.clear();
    175         maybeWriteStatusToDiskAsync();
    176     }
    177 
    178     /**
    179      * @param userHandle User for whom we are querying the list of jobs.
    180      * @return A list of all the jobs scheduled by the provided user. Never null.
    181      */
    182     public List<JobStatus> getJobsByUser(int userHandle) {
    183         return mJobSet.getJobsByUser(userHandle);
    184     }
    185 
    186     /**
    187      * @param uid Uid of the requesting app.
    188      * @return All JobStatus objects for a given uid from the master list. Never null.
    189      */
    190     public List<JobStatus> getJobsByUid(int uid) {
    191         return mJobSet.getJobsByUid(uid);
    192     }
    193 
    194     /**
    195      * @param uid Uid of the requesting app.
    196      * @param jobId Job id, specified at schedule-time.
    197      * @return the JobStatus that matches the provided uId and jobId, or null if none found.
    198      */
    199     public JobStatus getJobByUidAndJobId(int uid, int jobId) {
    200         return mJobSet.get(uid, jobId);
    201     }
    202 
    203     /**
    204      * Iterate over the set of all jobs, invoking the supplied functor on each.  This is for
    205      * customers who need to examine each job; we'd much rather not have to generate
    206      * transient unified collections for them to iterate over and then discard, or creating
    207      * iterators every time a client needs to perform a sweep.
    208      */
    209     public void forEachJob(JobStatusFunctor functor) {
    210         mJobSet.forEachJob(functor);
    211     }
    212 
    213     public void forEachJob(int uid, JobStatusFunctor functor) {
    214         mJobSet.forEachJob(uid, functor);
    215     }
    216 
    217     public interface JobStatusFunctor {
    218         public void process(JobStatus jobStatus);
    219     }
    220 
    221     /** Version of the db schema. */
    222     private static final int JOBS_FILE_VERSION = 0;
    223     /** Tag corresponds to constraints this job needs. */
    224     private static final String XML_TAG_PARAMS_CONSTRAINTS = "constraints";
    225     /** Tag corresponds to execution parameters. */
    226     private static final String XML_TAG_PERIODIC = "periodic";
    227     private static final String XML_TAG_ONEOFF = "one-off";
    228     private static final String XML_TAG_EXTRAS = "extras";
    229 
    230     /**
    231      * Every time the state changes we write all the jobs in one swath, instead of trying to
    232      * track incremental changes.
    233      * @return Whether the operation was successful. This will only fail for e.g. if the system is
    234      * low on storage. If this happens, we continue as normal
    235      */
    236     private void maybeWriteStatusToDiskAsync() {
    237         mDirtyOperations++;
    238         if (mDirtyOperations >= MAX_OPS_BEFORE_WRITE) {
    239             if (DEBUG) {
    240                 Slog.v(TAG, "Writing jobs to disk.");
    241             }
    242             mIoHandler.post(new WriteJobsMapToDiskRunnable());
    243         }
    244     }
    245 
    246     @VisibleForTesting
    247     public void readJobMapFromDisk(JobSet jobSet) {
    248         new ReadJobMapFromDiskRunnable(jobSet).run();
    249     }
    250 
    251     /**
    252      * Runnable that writes {@link #mJobSet} out to xml.
    253      * NOTE: This Runnable locks on mLock
    254      */
    255     private class WriteJobsMapToDiskRunnable implements Runnable {
    256         @Override
    257         public void run() {
    258             final long startElapsed = SystemClock.elapsedRealtime();
    259             final List<JobStatus> storeCopy = new ArrayList<JobStatus>();
    260             synchronized (mLock) {
    261                 // Clone the jobs so we can release the lock before writing.
    262                 mJobSet.forEachJob(new JobStatusFunctor() {
    263                     @Override
    264                     public void process(JobStatus job) {
    265                         if (job.isPersisted()) {
    266                             storeCopy.add(new JobStatus(job));
    267                         }
    268                     }
    269                 });
    270             }
    271             writeJobsMapImpl(storeCopy);
    272             if (JobSchedulerService.DEBUG) {
    273                 Slog.v(TAG, "Finished writing, took " + (SystemClock.elapsedRealtime()
    274                         - startElapsed) + "ms");
    275             }
    276         }
    277 
    278         private void writeJobsMapImpl(List<JobStatus> jobList) {
    279             try {
    280                 ByteArrayOutputStream baos = new ByteArrayOutputStream();
    281                 XmlSerializer out = new FastXmlSerializer();
    282                 out.setOutput(baos, StandardCharsets.UTF_8.name());
    283                 out.startDocument(null, true);
    284                 out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
    285 
    286                 out.startTag(null, "job-info");
    287                 out.attribute(null, "version", Integer.toString(JOBS_FILE_VERSION));
    288                 for (int i=0; i<jobList.size(); i++) {
    289                     JobStatus jobStatus = jobList.get(i);
    290                     if (DEBUG) {
    291                         Slog.d(TAG, "Saving job " + jobStatus.getJobId());
    292                     }
    293                     out.startTag(null, "job");
    294                     addAttributesToJobTag(out, jobStatus);
    295                     writeConstraintsToXml(out, jobStatus);
    296                     writeExecutionCriteriaToXml(out, jobStatus);
    297                     writeBundleToXml(jobStatus.getExtras(), out);
    298                     out.endTag(null, "job");
    299                 }
    300                 out.endTag(null, "job-info");
    301                 out.endDocument();
    302 
    303                 // Write out to disk in one fell sweep.
    304                 FileOutputStream fos = mJobsFile.startWrite();
    305                 fos.write(baos.toByteArray());
    306                 mJobsFile.finishWrite(fos);
    307                 mDirtyOperations = 0;
    308             } catch (IOException e) {
    309                 if (DEBUG) {
    310                     Slog.v(TAG, "Error writing out job data.", e);
    311                 }
    312             } catch (XmlPullParserException e) {
    313                 if (DEBUG) {
    314                     Slog.d(TAG, "Error persisting bundle.", e);
    315                 }
    316             }
    317         }
    318 
    319         /** Write out a tag with data comprising the required fields and priority of this job and
    320          * its client.
    321          */
    322         private void addAttributesToJobTag(XmlSerializer out, JobStatus jobStatus)
    323                 throws IOException {
    324             out.attribute(null, "jobid", Integer.toString(jobStatus.getJobId()));
    325             out.attribute(null, "package", jobStatus.getServiceComponent().getPackageName());
    326             out.attribute(null, "class", jobStatus.getServiceComponent().getClassName());
    327             if (jobStatus.getSourcePackageName() != null) {
    328                 out.attribute(null, "sourcePackageName", jobStatus.getSourcePackageName());
    329             }
    330             if (jobStatus.getSourceTag() != null) {
    331                 out.attribute(null, "sourceTag", jobStatus.getSourceTag());
    332             }
    333             out.attribute(null, "sourceUserId", String.valueOf(jobStatus.getSourceUserId()));
    334             out.attribute(null, "uid", Integer.toString(jobStatus.getUid()));
    335             out.attribute(null, "priority", String.valueOf(jobStatus.getPriority()));
    336             out.attribute(null, "flags", String.valueOf(jobStatus.getFlags()));
    337         }
    338 
    339         private void writeBundleToXml(PersistableBundle extras, XmlSerializer out)
    340                 throws IOException, XmlPullParserException {
    341             out.startTag(null, XML_TAG_EXTRAS);
    342             PersistableBundle extrasCopy = deepCopyBundle(extras, 10);
    343             extrasCopy.saveToXml(out);
    344             out.endTag(null, XML_TAG_EXTRAS);
    345         }
    346 
    347         private PersistableBundle deepCopyBundle(PersistableBundle bundle, int maxDepth) {
    348             if (maxDepth <= 0) {
    349                 return null;
    350             }
    351             PersistableBundle copy = (PersistableBundle) bundle.clone();
    352             Set<String> keySet = bundle.keySet();
    353             for (String key: keySet) {
    354                 Object o = copy.get(key);
    355                 if (o instanceof PersistableBundle) {
    356                     PersistableBundle bCopy = deepCopyBundle((PersistableBundle) o, maxDepth-1);
    357                     copy.putPersistableBundle(key, bCopy);
    358                 }
    359             }
    360             return copy;
    361         }
    362 
    363         /**
    364          * Write out a tag with data identifying this job's constraints. If the constraint isn't here
    365          * it doesn't apply.
    366          */
    367         private void writeConstraintsToXml(XmlSerializer out, JobStatus jobStatus) throws IOException {
    368             out.startTag(null, XML_TAG_PARAMS_CONSTRAINTS);
    369             if (jobStatus.hasConnectivityConstraint()) {
    370                 out.attribute(null, "connectivity", Boolean.toString(true));
    371             }
    372             if (jobStatus.hasUnmeteredConstraint()) {
    373                 out.attribute(null, "unmetered", Boolean.toString(true));
    374             }
    375             if (jobStatus.hasNotRoamingConstraint()) {
    376                 out.attribute(null, "not-roaming", Boolean.toString(true));
    377             }
    378             if (jobStatus.hasIdleConstraint()) {
    379                 out.attribute(null, "idle", Boolean.toString(true));
    380             }
    381             if (jobStatus.hasChargingConstraint()) {
    382                 out.attribute(null, "charging", Boolean.toString(true));
    383             }
    384             out.endTag(null, XML_TAG_PARAMS_CONSTRAINTS);
    385         }
    386 
    387         private void writeExecutionCriteriaToXml(XmlSerializer out, JobStatus jobStatus)
    388                 throws IOException {
    389             final JobInfo job = jobStatus.getJob();
    390             if (jobStatus.getJob().isPeriodic()) {
    391                 out.startTag(null, XML_TAG_PERIODIC);
    392                 out.attribute(null, "period", Long.toString(job.getIntervalMillis()));
    393                 out.attribute(null, "flex", Long.toString(job.getFlexMillis()));
    394             } else {
    395                 out.startTag(null, XML_TAG_ONEOFF);
    396             }
    397 
    398             if (jobStatus.hasDeadlineConstraint()) {
    399                 // Wall clock deadline.
    400                 final long deadlineWallclock =  System.currentTimeMillis() +
    401                         (jobStatus.getLatestRunTimeElapsed() - SystemClock.elapsedRealtime());
    402                 out.attribute(null, "deadline", Long.toString(deadlineWallclock));
    403             }
    404             if (jobStatus.hasTimingDelayConstraint()) {
    405                 final long delayWallclock = System.currentTimeMillis() +
    406                         (jobStatus.getEarliestRunTime() - SystemClock.elapsedRealtime());
    407                 out.attribute(null, "delay", Long.toString(delayWallclock));
    408             }
    409 
    410             // Only write out back-off policy if it differs from the default.
    411             // This also helps the case where the job is idle -> these aren't allowed to specify
    412             // back-off.
    413             if (jobStatus.getJob().getInitialBackoffMillis() != JobInfo.DEFAULT_INITIAL_BACKOFF_MILLIS
    414                     || jobStatus.getJob().getBackoffPolicy() != JobInfo.DEFAULT_BACKOFF_POLICY) {
    415                 out.attribute(null, "backoff-policy", Integer.toString(job.getBackoffPolicy()));
    416                 out.attribute(null, "initial-backoff", Long.toString(job.getInitialBackoffMillis()));
    417             }
    418             if (job.isPeriodic()) {
    419                 out.endTag(null, XML_TAG_PERIODIC);
    420             } else {
    421                 out.endTag(null, XML_TAG_ONEOFF);
    422             }
    423         }
    424     }
    425 
    426     /**
    427      * Runnable that reads list of persisted job from xml. This is run once at start up, so doesn't
    428      * need to go through {@link JobStore#add(com.android.server.job.controllers.JobStatus)}.
    429      */
    430     private class ReadJobMapFromDiskRunnable implements Runnable {
    431         private final JobSet jobSet;
    432 
    433         /**
    434          * @param jobSet Reference to the (empty) set of JobStatus objects that back the JobStore,
    435          *               so that after disk read we can populate it directly.
    436          */
    437         ReadJobMapFromDiskRunnable(JobSet jobSet) {
    438             this.jobSet = jobSet;
    439         }
    440 
    441         @Override
    442         public void run() {
    443             try {
    444                 List<JobStatus> jobs;
    445                 FileInputStream fis = mJobsFile.openRead();
    446                 synchronized (mLock) {
    447                     jobs = readJobMapImpl(fis);
    448                     if (jobs != null) {
    449                         for (int i=0; i<jobs.size(); i++) {
    450                             this.jobSet.add(jobs.get(i));
    451                         }
    452                     }
    453                 }
    454                 fis.close();
    455             } catch (FileNotFoundException e) {
    456                 if (JobSchedulerService.DEBUG) {
    457                     Slog.d(TAG, "Could not find jobs file, probably there was nothing to load.");
    458                 }
    459             } catch (XmlPullParserException e) {
    460                 if (JobSchedulerService.DEBUG) {
    461                     Slog.d(TAG, "Error parsing xml.", e);
    462                 }
    463             } catch (IOException e) {
    464                 if (JobSchedulerService.DEBUG) {
    465                     Slog.d(TAG, "Error parsing xml.", e);
    466                 }
    467             }
    468         }
    469 
    470         private List<JobStatus> readJobMapImpl(FileInputStream fis)
    471                 throws XmlPullParserException, IOException {
    472             XmlPullParser parser = Xml.newPullParser();
    473             parser.setInput(fis, StandardCharsets.UTF_8.name());
    474 
    475             int eventType = parser.getEventType();
    476             while (eventType != XmlPullParser.START_TAG &&
    477                     eventType != XmlPullParser.END_DOCUMENT) {
    478                 eventType = parser.next();
    479                 Slog.d(TAG, "Start tag: " + parser.getName());
    480             }
    481             if (eventType == XmlPullParser.END_DOCUMENT) {
    482                 if (DEBUG) {
    483                     Slog.d(TAG, "No persisted jobs.");
    484                 }
    485                 return null;
    486             }
    487 
    488             String tagName = parser.getName();
    489             if ("job-info".equals(tagName)) {
    490                 final List<JobStatus> jobs = new ArrayList<JobStatus>();
    491                 // Read in version info.
    492                 try {
    493                     int version = Integer.parseInt(parser.getAttributeValue(null, "version"));
    494                     if (version != JOBS_FILE_VERSION) {
    495                         Slog.d(TAG, "Invalid version number, aborting jobs file read.");
    496                         return null;
    497                     }
    498                 } catch (NumberFormatException e) {
    499                     Slog.e(TAG, "Invalid version number, aborting jobs file read.");
    500                     return null;
    501                 }
    502                 eventType = parser.next();
    503                 do {
    504                     // Read each <job/>
    505                     if (eventType == XmlPullParser.START_TAG) {
    506                         tagName = parser.getName();
    507                         // Start reading job.
    508                         if ("job".equals(tagName)) {
    509                             JobStatus persistedJob = restoreJobFromXml(parser);
    510                             if (persistedJob != null) {
    511                                 if (DEBUG) {
    512                                     Slog.d(TAG, "Read out " + persistedJob);
    513                                 }
    514                                 jobs.add(persistedJob);
    515                             } else {
    516                                 Slog.d(TAG, "Error reading job from file.");
    517                             }
    518                         }
    519                     }
    520                     eventType = parser.next();
    521                 } while (eventType != XmlPullParser.END_DOCUMENT);
    522                 return jobs;
    523             }
    524             return null;
    525         }
    526 
    527         /**
    528          * @param parser Xml parser at the beginning of a "<job/>" tag. The next "parser.next()" call
    529          *               will take the parser into the body of the job tag.
    530          * @return Newly instantiated job holding all the information we just read out of the xml tag.
    531          */
    532         private JobStatus restoreJobFromXml(XmlPullParser parser) throws XmlPullParserException,
    533                 IOException {
    534             JobInfo.Builder jobBuilder;
    535             int uid, sourceUserId;
    536 
    537             // Read out job identifier attributes and priority.
    538             try {
    539                 jobBuilder = buildBuilderFromXml(parser);
    540                 jobBuilder.setPersisted(true);
    541                 uid = Integer.parseInt(parser.getAttributeValue(null, "uid"));
    542 
    543                 String val = parser.getAttributeValue(null, "priority");
    544                 if (val != null) {
    545                     jobBuilder.setPriority(Integer.parseInt(val));
    546                 }
    547                 val = parser.getAttributeValue(null, "flags");
    548                 if (val != null) {
    549                     jobBuilder.setFlags(Integer.parseInt(val));
    550                 }
    551                 val = parser.getAttributeValue(null, "sourceUserId");
    552                 sourceUserId = val == null ? -1 : Integer.parseInt(val);
    553             } catch (NumberFormatException e) {
    554                 Slog.e(TAG, "Error parsing job's required fields, skipping");
    555                 return null;
    556             }
    557 
    558             String sourcePackageName = parser.getAttributeValue(null, "sourcePackageName");
    559 
    560             final String sourceTag = parser.getAttributeValue(null, "sourceTag");
    561 
    562             int eventType;
    563             // Read out constraints tag.
    564             do {
    565                 eventType = parser.next();
    566             } while (eventType == XmlPullParser.TEXT);  // Push through to next START_TAG.
    567 
    568             if (!(eventType == XmlPullParser.START_TAG &&
    569                     XML_TAG_PARAMS_CONSTRAINTS.equals(parser.getName()))) {
    570                 // Expecting a <constraints> start tag.
    571                 return null;
    572             }
    573             try {
    574                 buildConstraintsFromXml(jobBuilder, parser);
    575             } catch (NumberFormatException e) {
    576                 Slog.d(TAG, "Error reading constraints, skipping.");
    577                 return null;
    578             }
    579             parser.next(); // Consume </constraints>
    580 
    581             // Read out execution parameters tag.
    582             do {
    583                 eventType = parser.next();
    584             } while (eventType == XmlPullParser.TEXT);
    585             if (eventType != XmlPullParser.START_TAG) {
    586                 return null;
    587             }
    588 
    589             // Tuple of (earliest runtime, latest runtime) in elapsed realtime after disk load.
    590             Pair<Long, Long> elapsedRuntimes;
    591             try {
    592                 elapsedRuntimes = buildExecutionTimesFromXml(parser);
    593             } catch (NumberFormatException e) {
    594                 if (DEBUG) {
    595                     Slog.d(TAG, "Error parsing execution time parameters, skipping.");
    596                 }
    597                 return null;
    598             }
    599 
    600             final long elapsedNow = SystemClock.elapsedRealtime();
    601             if (XML_TAG_PERIODIC.equals(parser.getName())) {
    602                 try {
    603                     String val = parser.getAttributeValue(null, "period");
    604                     final long periodMillis = Long.valueOf(val);
    605                     val = parser.getAttributeValue(null, "flex");
    606                     final long flexMillis = (val != null) ? Long.valueOf(val) : periodMillis;
    607                     jobBuilder.setPeriodic(periodMillis, flexMillis);
    608                     // As a sanity check, cap the recreated run time to be no later than flex+period
    609                     // from now. This is the latest the periodic could be pushed out. This could
    610                     // happen if the periodic ran early (at flex time before period), and then the
    611                     // device rebooted.
    612                     if (elapsedRuntimes.second > elapsedNow + periodMillis + flexMillis) {
    613                         final long clampedLateRuntimeElapsed = elapsedNow + flexMillis
    614                                 + periodMillis;
    615                         final long clampedEarlyRuntimeElapsed = clampedLateRuntimeElapsed
    616                                 - flexMillis;
    617                         Slog.w(TAG,
    618                                 String.format("Periodic job for uid='%d' persisted run-time is" +
    619                                                 " too big [%s, %s]. Clamping to [%s,%s]",
    620                                         uid,
    621                                         DateUtils.formatElapsedTime(elapsedRuntimes.first / 1000),
    622                                         DateUtils.formatElapsedTime(elapsedRuntimes.second / 1000),
    623                                         DateUtils.formatElapsedTime(
    624                                                 clampedEarlyRuntimeElapsed / 1000),
    625                                         DateUtils.formatElapsedTime(
    626                                                 clampedLateRuntimeElapsed / 1000))
    627                         );
    628                         elapsedRuntimes =
    629                                 Pair.create(clampedEarlyRuntimeElapsed, clampedLateRuntimeElapsed);
    630                     }
    631                 } catch (NumberFormatException e) {
    632                     Slog.d(TAG, "Error reading periodic execution criteria, skipping.");
    633                     return null;
    634                 }
    635             } else if (XML_TAG_ONEOFF.equals(parser.getName())) {
    636                 try {
    637                     if (elapsedRuntimes.first != JobStatus.NO_EARLIEST_RUNTIME) {
    638                         jobBuilder.setMinimumLatency(elapsedRuntimes.first - elapsedNow);
    639                     }
    640                     if (elapsedRuntimes.second != JobStatus.NO_LATEST_RUNTIME) {
    641                         jobBuilder.setOverrideDeadline(
    642                                 elapsedRuntimes.second - elapsedNow);
    643                     }
    644                 } catch (NumberFormatException e) {
    645                     Slog.d(TAG, "Error reading job execution criteria, skipping.");
    646                     return null;
    647                 }
    648             } else {
    649                 if (DEBUG) {
    650                     Slog.d(TAG, "Invalid parameter tag, skipping - " + parser.getName());
    651                 }
    652                 // Expecting a parameters start tag.
    653                 return null;
    654             }
    655             maybeBuildBackoffPolicyFromXml(jobBuilder, parser);
    656 
    657             parser.nextTag(); // Consume parameters end tag.
    658 
    659             // Read out extras Bundle.
    660             do {
    661                 eventType = parser.next();
    662             } while (eventType == XmlPullParser.TEXT);
    663             if (!(eventType == XmlPullParser.START_TAG
    664                     && XML_TAG_EXTRAS.equals(parser.getName()))) {
    665                 if (DEBUG) {
    666                     Slog.d(TAG, "Error reading extras, skipping.");
    667                 }
    668                 return null;
    669             }
    670 
    671             PersistableBundle extras = PersistableBundle.restoreFromXml(parser);
    672             jobBuilder.setExtras(extras);
    673             parser.nextTag(); // Consume </extras>
    674 
    675             // Migrate sync jobs forward from earlier, incomplete representation
    676             if ("android".equals(sourcePackageName)
    677                     && extras != null
    678                     && extras.getBoolean("SyncManagerJob", false)) {
    679                 sourcePackageName = extras.getString("owningPackage", sourcePackageName);
    680                 if (DEBUG) {
    681                     Slog.i(TAG, "Fixing up sync job source package name from 'android' to '"
    682                             + sourcePackageName + "'");
    683                 }
    684             }
    685 
    686             // And now we're done
    687             JobStatus js = new JobStatus(
    688                     jobBuilder.build(), uid, sourcePackageName, sourceUserId, sourceTag,
    689                     elapsedRuntimes.first, elapsedRuntimes.second);
    690             return js;
    691         }
    692 
    693         private JobInfo.Builder buildBuilderFromXml(XmlPullParser parser) throws NumberFormatException {
    694             // Pull out required fields from <job> attributes.
    695             int jobId = Integer.parseInt(parser.getAttributeValue(null, "jobid"));
    696             String packageName = parser.getAttributeValue(null, "package");
    697             String className = parser.getAttributeValue(null, "class");
    698             ComponentName cname = new ComponentName(packageName, className);
    699 
    700             return new JobInfo.Builder(jobId, cname);
    701         }
    702 
    703         private void buildConstraintsFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser) {
    704             String val = parser.getAttributeValue(null, "connectivity");
    705             if (val != null) {
    706                 jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
    707             }
    708             val = parser.getAttributeValue(null, "unmetered");
    709             if (val != null) {
    710                 jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED);
    711             }
    712             val = parser.getAttributeValue(null, "not-roaming");
    713             if (val != null) {
    714                 jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_NOT_ROAMING);
    715             }
    716             val = parser.getAttributeValue(null, "idle");
    717             if (val != null) {
    718                 jobBuilder.setRequiresDeviceIdle(true);
    719             }
    720             val = parser.getAttributeValue(null, "charging");
    721             if (val != null) {
    722                 jobBuilder.setRequiresCharging(true);
    723             }
    724         }
    725 
    726         /**
    727          * Builds the back-off policy out of the params tag. These attributes may not exist, depending
    728          * on whether the back-off was set when the job was first scheduled.
    729          */
    730         private void maybeBuildBackoffPolicyFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser) {
    731             String val = parser.getAttributeValue(null, "initial-backoff");
    732             if (val != null) {
    733                 long initialBackoff = Long.valueOf(val);
    734                 val = parser.getAttributeValue(null, "backoff-policy");
    735                 int backoffPolicy = Integer.parseInt(val);  // Will throw NFE which we catch higher up.
    736                 jobBuilder.setBackoffCriteria(initialBackoff, backoffPolicy);
    737             }
    738         }
    739 
    740         /**
    741          * Convenience function to read out and convert deadline and delay from xml into elapsed real
    742          * time.
    743          * @return A {@link android.util.Pair}, where the first value is the earliest elapsed runtime
    744          * and the second is the latest elapsed runtime.
    745          */
    746         private Pair<Long, Long> buildExecutionTimesFromXml(XmlPullParser parser)
    747                 throws NumberFormatException {
    748             // Pull out execution time data.
    749             final long nowWallclock = System.currentTimeMillis();
    750             final long nowElapsed = SystemClock.elapsedRealtime();
    751 
    752             long earliestRunTimeElapsed = JobStatus.NO_EARLIEST_RUNTIME;
    753             long latestRunTimeElapsed = JobStatus.NO_LATEST_RUNTIME;
    754             String val = parser.getAttributeValue(null, "deadline");
    755             if (val != null) {
    756                 long latestRuntimeWallclock = Long.valueOf(val);
    757                 long maxDelayElapsed =
    758                         Math.max(latestRuntimeWallclock - nowWallclock, 0);
    759                 latestRunTimeElapsed = nowElapsed + maxDelayElapsed;
    760             }
    761             val = parser.getAttributeValue(null, "delay");
    762             if (val != null) {
    763                 long earliestRuntimeWallclock = Long.valueOf(val);
    764                 long minDelayElapsed =
    765                         Math.max(earliestRuntimeWallclock - nowWallclock, 0);
    766                 earliestRunTimeElapsed = nowElapsed + minDelayElapsed;
    767 
    768             }
    769             return Pair.create(earliestRunTimeElapsed, latestRunTimeElapsed);
    770         }
    771     }
    772 
    773     static class JobSet {
    774         // Key is the getUid() originator of the jobs in each sheaf
    775         private SparseArray<ArraySet<JobStatus>> mJobs;
    776 
    777         public JobSet() {
    778             mJobs = new SparseArray<ArraySet<JobStatus>>();
    779         }
    780 
    781         public List<JobStatus> getJobsByUid(int uid) {
    782             ArrayList<JobStatus> matchingJobs = new ArrayList<JobStatus>();
    783             ArraySet<JobStatus> jobs = mJobs.get(uid);
    784             if (jobs != null) {
    785                 matchingJobs.addAll(jobs);
    786             }
    787             return matchingJobs;
    788         }
    789 
    790         // By user, not by uid, so we need to traverse by key and check
    791         public List<JobStatus> getJobsByUser(int userId) {
    792             ArrayList<JobStatus> result = new ArrayList<JobStatus>();
    793             for (int i = mJobs.size() - 1; i >= 0; i--) {
    794                 if (UserHandle.getUserId(mJobs.keyAt(i)) == userId) {
    795                     ArraySet<JobStatus> jobs = mJobs.get(i);
    796                     if (jobs != null) {
    797                         result.addAll(jobs);
    798                     }
    799                 }
    800             }
    801             return result;
    802         }
    803 
    804         public boolean add(JobStatus job) {
    805             final int uid = job.getUid();
    806             ArraySet<JobStatus> jobs = mJobs.get(uid);
    807             if (jobs == null) {
    808                 jobs = new ArraySet<JobStatus>();
    809                 mJobs.put(uid, jobs);
    810             }
    811             return jobs.add(job);
    812         }
    813 
    814         public boolean remove(JobStatus job) {
    815             final int uid = job.getUid();
    816             ArraySet<JobStatus> jobs = mJobs.get(uid);
    817             boolean didRemove = (jobs != null) ? jobs.remove(job) : false;
    818             if (didRemove && jobs.size() == 0) {
    819                 // no more jobs for this uid; let the now-empty set object be GC'd.
    820                 mJobs.remove(uid);
    821             }
    822             return didRemove;
    823         }
    824 
    825         public boolean contains(JobStatus job) {
    826             final int uid = job.getUid();
    827             ArraySet<JobStatus> jobs = mJobs.get(uid);
    828             return jobs != null && jobs.contains(job);
    829         }
    830 
    831         public JobStatus get(int uid, int jobId) {
    832             ArraySet<JobStatus> jobs = mJobs.get(uid);
    833             if (jobs != null) {
    834                 for (int i = jobs.size() - 1; i >= 0; i--) {
    835                     JobStatus job = jobs.valueAt(i);
    836                     if (job.getJobId() == jobId) {
    837                         return job;
    838                     }
    839                 }
    840             }
    841             return null;
    842         }
    843 
    844         // Inefficient; use only for testing
    845         public List<JobStatus> getAllJobs() {
    846             ArrayList<JobStatus> allJobs = new ArrayList<JobStatus>(size());
    847             for (int i = mJobs.size() - 1; i >= 0; i--) {
    848                 ArraySet<JobStatus> jobs = mJobs.valueAt(i);
    849                 if (jobs != null) {
    850                     // Use a for loop over the ArraySet, so we don't need to make its
    851                     // optional collection class iterator implementation or have to go
    852                     // through a temporary array from toArray().
    853                     for (int j = jobs.size() - 1; j >= 0; j--) {
    854                         allJobs.add(jobs.valueAt(j));
    855                     }
    856                 }
    857             }
    858             return allJobs;
    859         }
    860 
    861         public void clear() {
    862             mJobs.clear();
    863         }
    864 
    865         public int size() {
    866             int total = 0;
    867             for (int i = mJobs.size() - 1; i >= 0; i--) {
    868                 total += mJobs.valueAt(i).size();
    869             }
    870             return total;
    871         }
    872 
    873         // We only want to count the jobs that this uid has scheduled on its own
    874         // behalf, not those that the app has scheduled on someone else's behalf.
    875         public int countJobsForUid(int uid) {
    876             int total = 0;
    877             ArraySet<JobStatus> jobs = mJobs.get(uid);
    878             if (jobs != null) {
    879                 for (int i = jobs.size() - 1; i >= 0; i--) {
    880                     JobStatus job = jobs.valueAt(i);
    881                     if (job.getUid() == job.getSourceUid()) {
    882                         total++;
    883                     }
    884                 }
    885             }
    886             return total;
    887         }
    888 
    889         public void forEachJob(JobStatusFunctor functor) {
    890             for (int uidIndex = mJobs.size() - 1; uidIndex >= 0; uidIndex--) {
    891                 ArraySet<JobStatus> jobs = mJobs.valueAt(uidIndex);
    892                 for (int i = jobs.size() - 1; i >= 0; i--) {
    893                     functor.process(jobs.valueAt(i));
    894                 }
    895             }
    896         }
    897 
    898         public void forEachJob(int uid, JobStatusFunctor functor) {
    899             ArraySet<JobStatus> jobs = mJobs.get(uid);
    900             if (jobs != null) {
    901                 for (int i = jobs.size() - 1; i >= 0; i--) {
    902                     functor.process(jobs.valueAt(i));
    903                 }
    904             }
    905         }
    906     }
    907 }
    908