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