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