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