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. */
initAndGet(JobSchedulerService jobManagerService)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
initAndGetForTesting(Context context, File dataDir)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      */
JobStore(Context context, Object lock, File dataDir)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 
jobTimesInflatedValid()167     public boolean jobTimesInflatedValid() {
168         return mRtcGood;
169     }
170 
clockNowValidToInflate(long now)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      */
getRtcCorrectedJobsLocked(final ArrayList<JobStatus> toAdd, final ArrayList<JobStatus> toRemove)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      */
add(JobStatus jobStatus)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 
containsJob(JobStatus jobStatus)220     boolean containsJob(JobStatus jobStatus) {
221         return mJobSet.contains(jobStatus);
222     }
223 
size()224     public int size() {
225         return mJobSet.size();
226     }
227 
getPersistStats()228     public JobStorePersistStats getPersistStats() {
229         return mPersistInfo;
230     }
231 
countJobsForUid(int uid)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      */
remove(JobStatus jobStatus, boolean writeBack)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      */
removeJobsOfNonUsers(int[] whitelist)259     public void removeJobsOfNonUsers(int[] whitelist) {
260         mJobSet.removeJobsOfNonUsers(whitelist);
261     }
262 
263     @VisibleForTesting
clear()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      */
getJobsByUser(int userHandle)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      */
getJobsByUid(int uid)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      */
getJobByUidAndJobId(int uid, int jobId)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      */
forEachJob(Consumer<JobStatus> functor)300     public void forEachJob(Consumer<JobStatus> functor) {
301         mJobSet.forEachJob(null, functor);
302     }
303 
forEachJob(@ullable Predicate<JobStatus> filterPredicate, Consumer<JobStatus> functor)304     public void forEachJob(@Nullable Predicate<JobStatus> filterPredicate,
305             Consumer<JobStatus> functor) {
306         mJobSet.forEachJob(filterPredicate, functor);
307     }
308 
forEachJob(int uid, Consumer<JobStatus> functor)309     public void forEachJob(int uid, Consumer<JobStatus> functor) {
310         mJobSet.forEachJob(uid, functor);
311     }
312 
forEachJobForSourceUid(int sourceUid, Consumer<JobStatus> functor)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      */
maybeWriteStatusToDiskAsync()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
readJobMapFromDisk(JobSet jobSet, boolean rtcGood)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
waitForWriteToCompleteForTesting(long maxWaitMillis)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      */
convertRtcBoundsToElapsed(Pair<Long, Long> rtcTimes, long nowElapsed)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 
isSyncJob(JobStatus status)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          */
ReadJobMapFromDiskRunnable(JobSet jobSet, boolean rtcIsGood)639         ReadJobMapFromDiskRunnable(JobSet jobSet, boolean rtcIsGood) {
640             this.jobSet = jobSet;
641             this.rtcGood = rtcIsGood;
642         }
643 
644         @Override
run()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 
readJobMapImpl(FileInputStream fis, boolean rtcIsGood)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          */
restoreJobFromXml(boolean rtcIsGood, XmlPullParser parser)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 
buildBuilderFromXml(XmlPullParser parser)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 
buildConstraintsFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser)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          */
maybeBuildBackoffPolicyFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser)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          */
buildRtcExecutionTimesFromXml(XmlPullParser parser)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 
JobSet()1059         public JobSet() {
1060             mJobs = new SparseArray<ArraySet<JobStatus>>();
1061             mJobsPerSourceUid = new SparseArray<>();
1062         }
1063 
getJobsByUid(int uid)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
getJobsByUser(int userId)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 
add(JobStatus job)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 
remove(JobStatus job)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          */
removeJobsOfNonUsers(final int[] whitelist)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 
removeAll(Predicate<JobStatus> predicate)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 
contains(JobStatus job)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 
get(int uid, int jobId)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
getAllJobs()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 
clear()1207         public void clear() {
1208             mJobs.clear();
1209             mJobsPerSourceUid.clear();
1210         }
1211 
size()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.
countJobsForUid(int uid)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 
forEachJob(@ullable Predicate<JobStatus> filterPredicate, Consumer<JobStatus> functor)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 
forEachJob(int callingUid, Consumer<JobStatus> functor)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 
forEachJobForSourceUid(int sourceUid, Consumer<JobStatus> functor)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