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