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