2 * Copyright (C) 2014 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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
17 package com.android.server.job;
19 import android.app.ActivityManager;
20 import android.app.IActivityManager;
21 import android.content.ComponentName;
22 import android.app.job.JobInfo;
23 import android.content.Context;
24 import android.os.Environment;
25 import android.os.Handler;
26 import android.os.PersistableBundle;
27 import android.os.SystemClock;
28 import android.os.UserHandle;
29 import android.text.format.DateUtils;
30 import android.util.AtomicFile;
31 import android.util.ArraySet;
32 import android.util.Pair;
33 import android.util.Slog;
34 import android.util.SparseArray;
35 import android.util.Xml;
37 import com.android.internal.annotations.VisibleForTesting;
38 import com.android.internal.util.ArrayUtils;
39 import com.android.internal.util.FastXmlSerializer;
40 import com.android.server.IoThread;
41 import com.android.server.job.controllers.JobStatus;
43 import java.io.ByteArrayOutputStream;
45 import java.io.FileInputStream;
46 import java.io.FileNotFoundException;
47 import java.io.FileOutputStream;
48 import java.io.IOException;
49 import java.nio.charset.StandardCharsets;
50 import java.util.ArrayList;
51 import java.util.List;
54 import org.xmlpull.v1.XmlPullParser;
55 import org.xmlpull.v1.XmlPullParserException;
56 import org.xmlpull.v1.XmlSerializer;
59 * Maintains the master list of jobs that the job scheduler is tracking. These jobs are compared by
60 * reference, so none of the functions in this class should make a copy.
61 * Also handles read/write of persisted jobs.
64 * All callers to this class must <strong>lock on the class object they are calling</strong>.
65 * This is important b/c {@link com.android.server.job.JobStore.WriteJobsMapToDiskRunnable}
66 * and {@link com.android.server.job.JobStore.ReadJobMapFromDiskRunnable} lock on that
69 public final class JobStore {
70 private static final String TAG = "JobStore";
71 private static final boolean DEBUG = JobSchedulerService.DEBUG;
73 /** Threshold to adjust how often we want to write to the db. */
74 private static final int MAX_OPS_BEFORE_WRITE = 1;
77 final JobSet mJobSet; // per-caller-uid tracking
78 final Context mContext;
80 // Bookkeeping around incorrect boot-time system clock
81 private final long mXmlTimestamp;
82 private boolean mRtcGood;
84 private int mDirtyOperations;
86 private static final Object sSingletonLock = new Object();
87 private final AtomicFile mJobsFile;
88 /** Handler backed by IoThread for writing to disk. */
89 private final Handler mIoHandler = IoThread.getHandler();
90 private static JobStore sSingleton;
92 /** Used by the {@link JobSchedulerService} to instantiate the JobStore. */
93 static JobStore initAndGet(JobSchedulerService jobManagerService) {
94 synchronized (sSingletonLock) {
95 if (sSingleton == null) {
96 sSingleton = new JobStore(jobManagerService.getContext(),
97 jobManagerService.getLock(), Environment.getDataDirectory());
104 * @return A freshly initialized job store object, with no loaded jobs.
107 public static JobStore initAndGetForTesting(Context context, File dataDir) {
108 JobStore jobStoreUnderTest = new JobStore(context, new Object(), dataDir);
109 jobStoreUnderTest.clear();
110 return jobStoreUnderTest;
114 * Construct the instance of the job store. This results in a blocking read from disk.
116 private JobStore(Context context, Object lock, File dataDir) {
119 mDirtyOperations = 0;
121 File systemDir = new File(dataDir, "system");
122 File jobDir = new File(systemDir, "job");
124 mJobsFile = new AtomicFile(new File(jobDir, "jobs.xml"));
126 mJobSet = new JobSet();
128 // If the current RTC is earlier than the timestamp on our persisted jobs file,
129 // we suspect that the RTC is uninitialized and so we cannot draw conclusions
130 // about persisted job scheduling.
132 // Note that if the persisted jobs file does not exist, we proceed with the
133 // assumption that the RTC is good. This is less work and is safe: if the
134 // clock updates to sanity then we'll be saving the persisted jobs file in that
135 // correct state, which is normal; or we'll wind up writing the jobs file with
136 // an incorrect historical timestamp. That's fine; at worst we'll reboot with
137 // a *correct* timestamp, see a bunch of overdue jobs, and run them; then
138 // settle into normal operation.
139 mXmlTimestamp = mJobsFile.getLastModifiedTime();
140 mRtcGood = (System.currentTimeMillis() > mXmlTimestamp);
142 readJobMapFromDisk(mJobSet, mRtcGood);
145 public boolean jobTimesInflatedValid() {
149 public boolean clockNowValidToInflate(long now) {
150 return now >= mXmlTimestamp;
154 * Find all the jobs that were affected by RTC clock uncertainty at boot time. Returns
155 * parallel lists of the existing JobStatus objects and of new, equivalent JobStatus instances
156 * with now-corrected time bounds.
158 public void getRtcCorrectedJobsLocked(final ArrayList<JobStatus> toAdd,
159 final ArrayList<JobStatus> toRemove) {
160 final long elapsedNow = SystemClock.elapsedRealtime();
162 // Find the jobs that need to be fixed up, collecting them for post-iteration
163 // replacement with their new versions
165 final Pair<Long, Long> utcTimes = job.getPersistedUtcTimes();
166 if (utcTimes != null) {
167 Pair<Long, Long> elapsedRuntimes =
168 convertRtcBoundsToElapsed(utcTimes, elapsedNow);
169 toAdd.add(new JobStatus(job, elapsedRuntimes.first, elapsedRuntimes.second,
170 0, job.getLastSuccessfulRunTime(), job.getLastFailedRunTime()));
177 * Add a job to the master list, persisting it if necessary. If the JobStatus already exists,
178 * it will be replaced.
179 * @param jobStatus Job to add.
180 * @return Whether or not an equivalent JobStatus was replaced by this operation.
182 public boolean add(JobStatus jobStatus) {
183 boolean replaced = mJobSet.remove(jobStatus);
184 mJobSet.add(jobStatus);
185 if (jobStatus.isPersisted()) {
186 maybeWriteStatusToDiskAsync();
189 Slog.d(TAG, "Added job status to store: " + jobStatus);
194 boolean containsJob(JobStatus jobStatus) {
195 return mJobSet.contains(jobStatus);
199 return mJobSet.size();
202 public int countJobsForUid(int uid) {
203 return mJobSet.countJobsForUid(uid);
207 * Remove the provided job. Will also delete the job if it was persisted.
208 * @param writeBack If true, the job will be deleted (if it was persisted) immediately.
209 * @return Whether or not the job existed to be removed.
211 public boolean remove(JobStatus jobStatus, boolean writeBack) {
212 boolean removed = mJobSet.remove(jobStatus);
215 Slog.d(TAG, "Couldn't remove job: didn't exist: " + jobStatus);
219 if (writeBack && jobStatus.isPersisted()) {
220 maybeWriteStatusToDiskAsync();
226 * Remove the jobs of users not specified in the whitelist.
227 * @param whitelist Array of User IDs whose jobs are not to be removed.
229 public void removeJobsOfNonUsers(int[] whitelist) {
230 mJobSet.removeJobsOfNonUsers(whitelist);
234 public void clear() {
236 maybeWriteStatusToDiskAsync();
240 * @param userHandle User for whom we are querying the list of jobs.
241 * @return A list of all the jobs scheduled by the provided user. Never null.
243 public List<JobStatus> getJobsByUser(int userHandle) {
244 return mJobSet.getJobsByUser(userHandle);
248 * @param uid Uid of the requesting app.
249 * @return All JobStatus objects for a given uid from the master list. Never null.
251 public List<JobStatus> getJobsByUid(int uid) {
252 return mJobSet.getJobsByUid(uid);
256 * @param uid Uid of the requesting app.
257 * @param jobId Job id, specified at schedule-time.
258 * @return the JobStatus that matches the provided uId and jobId, or null if none found.
260 public JobStatus getJobByUidAndJobId(int uid, int jobId) {
261 return mJobSet.get(uid, jobId);
265 * Iterate over the set of all jobs, invoking the supplied functor on each. This is for
266 * customers who need to examine each job; we'd much rather not have to generate
267 * transient unified collections for them to iterate over and then discard, or creating
268 * iterators every time a client needs to perform a sweep.
270 public void forEachJob(JobStatusFunctor functor) {
271 mJobSet.forEachJob(functor);
274 public void forEachJob(int uid, JobStatusFunctor functor) {
275 mJobSet.forEachJob(uid, functor);
278 public interface JobStatusFunctor {
279 public void process(JobStatus jobStatus);
282 /** Version of the db schema. */
283 private static final int JOBS_FILE_VERSION = 0;
284 /** Tag corresponds to constraints this job needs. */
285 private static final String XML_TAG_PARAMS_CONSTRAINTS = "constraints";
286 /** Tag corresponds to execution parameters. */
287 private static final String XML_TAG_PERIODIC = "periodic";
288 private static final String XML_TAG_ONEOFF = "one-off";
289 private static final String XML_TAG_EXTRAS = "extras";
292 * Every time the state changes we write all the jobs in one swath, instead of trying to
293 * track incremental changes.
295 private void maybeWriteStatusToDiskAsync() {
297 if (mDirtyOperations >= MAX_OPS_BEFORE_WRITE) {
299 Slog.v(TAG, "Writing jobs to disk.");
301 mIoHandler.removeCallbacks(mWriteRunnable);
302 mIoHandler.post(mWriteRunnable);
307 public void readJobMapFromDisk(JobSet jobSet, boolean rtcGood) {
308 new ReadJobMapFromDiskRunnable(jobSet, rtcGood).run();
312 * Runnable that writes {@link #mJobSet} out to xml.
313 * NOTE: This Runnable locks on mLock
315 private final Runnable mWriteRunnable = new Runnable() {
318 final long startElapsed = SystemClock.elapsedRealtime();
319 final List<JobStatus> storeCopy = new ArrayList<JobStatus>();
320 synchronized (mLock) {
321 // Clone the jobs so we can release the lock before writing.
322 mJobSet.forEachJob(new JobStatusFunctor() {
324 public void process(JobStatus job) {
325 if (job.isPersisted()) {
326 storeCopy.add(new JobStatus(job));
331 writeJobsMapImpl(storeCopy);
333 Slog.v(TAG, "Finished writing, took " + (SystemClock.elapsedRealtime()
334 - startElapsed) + "ms");
338 private void writeJobsMapImpl(List<JobStatus> jobList) {
340 ByteArrayOutputStream baos = new ByteArrayOutputStream();
341 XmlSerializer out = new FastXmlSerializer();
342 out.setOutput(baos, StandardCharsets.UTF_8.name());
343 out.startDocument(null, true);
344 out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
346 out.startTag(null, "job-info");
347 out.attribute(null, "version", Integer.toString(JOBS_FILE_VERSION));
348 for (int i=0; i<jobList.size(); i++) {
349 JobStatus jobStatus = jobList.get(i);
351 Slog.d(TAG, "Saving job " + jobStatus.getJobId());
353 out.startTag(null, "job");
354 addAttributesToJobTag(out, jobStatus);
355 writeConstraintsToXml(out, jobStatus);
356 writeExecutionCriteriaToXml(out, jobStatus);
357 writeBundleToXml(jobStatus.getJob().getExtras(), out);
358 out.endTag(null, "job");
360 out.endTag(null, "job-info");
363 // Write out to disk in one fell swoop.
364 FileOutputStream fos = mJobsFile.startWrite();
365 fos.write(baos.toByteArray());
366 mJobsFile.finishWrite(fos);
367 mDirtyOperations = 0;
368 } catch (IOException e) {
370 Slog.v(TAG, "Error writing out job data.", e);
372 } catch (XmlPullParserException e) {
374 Slog.d(TAG, "Error persisting bundle.", e);
379 /** Write out a tag with data comprising the required fields and priority of this job and
382 private void addAttributesToJobTag(XmlSerializer out, JobStatus jobStatus)
384 out.attribute(null, "jobid", Integer.toString(jobStatus.getJobId()));
385 out.attribute(null, "package", jobStatus.getServiceComponent().getPackageName());
386 out.attribute(null, "class", jobStatus.getServiceComponent().getClassName());
387 if (jobStatus.getSourcePackageName() != null) {
388 out.attribute(null, "sourcePackageName", jobStatus.getSourcePackageName());
390 if (jobStatus.getSourceTag() != null) {
391 out.attribute(null, "sourceTag", jobStatus.getSourceTag());
393 out.attribute(null, "sourceUserId", String.valueOf(jobStatus.getSourceUserId()));
394 out.attribute(null, "uid", Integer.toString(jobStatus.getUid()));
395 out.attribute(null, "priority", String.valueOf(jobStatus.getPriority()));
396 out.attribute(null, "flags", String.valueOf(jobStatus.getFlags()));
398 out.attribute(null, "lastSuccessfulRunTime",
399 String.valueOf(jobStatus.getLastSuccessfulRunTime()));
400 out.attribute(null, "lastFailedRunTime",
401 String.valueOf(jobStatus.getLastFailedRunTime()));
404 private void writeBundleToXml(PersistableBundle extras, XmlSerializer out)
405 throws IOException, XmlPullParserException {
406 out.startTag(null, XML_TAG_EXTRAS);
407 PersistableBundle extrasCopy = deepCopyBundle(extras, 10);
408 extrasCopy.saveToXml(out);
409 out.endTag(null, XML_TAG_EXTRAS);
412 private PersistableBundle deepCopyBundle(PersistableBundle bundle, int maxDepth) {
416 PersistableBundle copy = (PersistableBundle) bundle.clone();
417 Set<String> keySet = bundle.keySet();
418 for (String key: keySet) {
419 Object o = copy.get(key);
420 if (o instanceof PersistableBundle) {
421 PersistableBundle bCopy = deepCopyBundle((PersistableBundle) o, maxDepth-1);
422 copy.putPersistableBundle(key, bCopy);
429 * Write out a tag with data identifying this job's constraints. If the constraint isn't here
432 private void writeConstraintsToXml(XmlSerializer out, JobStatus jobStatus) throws IOException {
433 out.startTag(null, XML_TAG_PARAMS_CONSTRAINTS);
434 if (jobStatus.needsAnyConnectivity()) {
435 out.attribute(null, "connectivity", Boolean.toString(true));
437 if (jobStatus.needsMeteredConnectivity()) {
438 out.attribute(null, "metered", Boolean.toString(true));
440 if (jobStatus.needsUnmeteredConnectivity()) {
441 out.attribute(null, "unmetered", Boolean.toString(true));
443 if (jobStatus.needsNonRoamingConnectivity()) {
444 out.attribute(null, "not-roaming", Boolean.toString(true));
446 if (jobStatus.hasIdleConstraint()) {
447 out.attribute(null, "idle", Boolean.toString(true));
449 if (jobStatus.hasChargingConstraint()) {
450 out.attribute(null, "charging", Boolean.toString(true));
452 if (jobStatus.hasBatteryNotLowConstraint()) {
453 out.attribute(null, "battery-not-low", Boolean.toString(true));
455 out.endTag(null, XML_TAG_PARAMS_CONSTRAINTS);
458 private void writeExecutionCriteriaToXml(XmlSerializer out, JobStatus jobStatus)
460 final JobInfo job = jobStatus.getJob();
461 if (jobStatus.getJob().isPeriodic()) {
462 out.startTag(null, XML_TAG_PERIODIC);
463 out.attribute(null, "period", Long.toString(job.getIntervalMillis()));
464 out.attribute(null, "flex", Long.toString(job.getFlexMillis()));
466 out.startTag(null, XML_TAG_ONEOFF);
469 // If we still have the persisted times, we need to record those directly because
470 // we haven't yet been able to calculate the usual elapsed-timebase bounds
471 // correctly due to wall-clock uncertainty.
472 Pair <Long, Long> utcJobTimes = jobStatus.getPersistedUtcTimes();
473 if (DEBUG && utcJobTimes != null) {
474 Slog.i(TAG, "storing original UTC timestamps for " + jobStatus);
477 final long nowRTC = System.currentTimeMillis();
478 final long nowElapsed = SystemClock.elapsedRealtime();
479 if (jobStatus.hasDeadlineConstraint()) {
480 // Wall clock deadline.
481 final long deadlineWallclock = (utcJobTimes == null)
482 ? nowRTC + (jobStatus.getLatestRunTimeElapsed() - nowElapsed)
483 : utcJobTimes.second;
484 out.attribute(null, "deadline", Long.toString(deadlineWallclock));
486 if (jobStatus.hasTimingDelayConstraint()) {
487 final long delayWallclock = (utcJobTimes == null)
488 ? nowRTC + (jobStatus.getEarliestRunTime() - nowElapsed)
490 out.attribute(null, "delay", Long.toString(delayWallclock));
493 // Only write out back-off policy if it differs from the default.
494 // This also helps the case where the job is idle -> these aren't allowed to specify
496 if (jobStatus.getJob().getInitialBackoffMillis() != JobInfo.DEFAULT_INITIAL_BACKOFF_MILLIS
497 || jobStatus.getJob().getBackoffPolicy() != JobInfo.DEFAULT_BACKOFF_POLICY) {
498 out.attribute(null, "backoff-policy", Integer.toString(job.getBackoffPolicy()));
499 out.attribute(null, "initial-backoff", Long.toString(job.getInitialBackoffMillis()));
501 if (job.isPeriodic()) {
502 out.endTag(null, XML_TAG_PERIODIC);
504 out.endTag(null, XML_TAG_ONEOFF);
510 * Translate the supplied RTC times to the elapsed timebase, with clamping appropriate
511 * to interpreting them as a job's delay + deadline times for alarm-setting purposes.
512 * @param rtcTimes a Pair<Long, Long> in which {@code first} is the "delay" earliest
513 * allowable runtime for the job, and {@code second} is the "deadline" time at which
514 * the job becomes overdue.
516 private static Pair<Long, Long> convertRtcBoundsToElapsed(Pair<Long, Long> rtcTimes,
518 final long nowWallclock = System.currentTimeMillis();
519 final long earliest = (rtcTimes.first > JobStatus.NO_EARLIEST_RUNTIME)
520 ? nowElapsed + Math.max(rtcTimes.first - nowWallclock, 0)
521 : JobStatus.NO_EARLIEST_RUNTIME;
522 final long latest = (rtcTimes.second < JobStatus.NO_LATEST_RUNTIME)
523 ? nowElapsed + Math.max(rtcTimes.second - nowWallclock, 0)
524 : JobStatus.NO_LATEST_RUNTIME;
525 return Pair.create(earliest, latest);
529 * Runnable that reads list of persisted job from xml. This is run once at start up, so doesn't
530 * need to go through {@link JobStore#add(com.android.server.job.controllers.JobStatus)}.
532 private final class ReadJobMapFromDiskRunnable implements Runnable {
533 private final JobSet jobSet;
534 private final boolean rtcGood;
537 * @param jobSet Reference to the (empty) set of JobStatus objects that back the JobStore,
538 * so that after disk read we can populate it directly.
540 ReadJobMapFromDiskRunnable(JobSet jobSet, boolean rtcIsGood) {
541 this.jobSet = jobSet;
542 this.rtcGood = rtcIsGood;
549 List<JobStatus> jobs;
550 FileInputStream fis = mJobsFile.openRead();
551 synchronized (mLock) {
552 jobs = readJobMapImpl(fis, rtcGood);
554 long now = SystemClock.elapsedRealtime();
555 IActivityManager am = ActivityManager.getService();
556 for (int i=0; i<jobs.size(); i++) {
557 JobStatus js = jobs.get(i);
558 js.prepareLocked(am);
559 js.enqueueTime = now;
566 } catch (FileNotFoundException e) {
568 Slog.d(TAG, "Could not find jobs file, probably there was nothing to load.");
570 } catch (XmlPullParserException | IOException e) {
571 Slog.wtf(TAG, "Error jobstore xml.", e);
573 Slog.i(TAG, "Read " + numJobs + " jobs");
576 private List<JobStatus> readJobMapImpl(FileInputStream fis, boolean rtcIsGood)
577 throws XmlPullParserException, IOException {
578 XmlPullParser parser = Xml.newPullParser();
579 parser.setInput(fis, StandardCharsets.UTF_8.name());
581 int eventType = parser.getEventType();
582 while (eventType != XmlPullParser.START_TAG &&
583 eventType != XmlPullParser.END_DOCUMENT) {
584 eventType = parser.next();
585 Slog.d(TAG, "Start tag: " + parser.getName());
587 if (eventType == XmlPullParser.END_DOCUMENT) {
589 Slog.d(TAG, "No persisted jobs.");
594 String tagName = parser.getName();
595 if ("job-info".equals(tagName)) {
596 final List<JobStatus> jobs = new ArrayList<JobStatus>();
597 // Read in version info.
599 int version = Integer.parseInt(parser.getAttributeValue(null, "version"));
600 if (version != JOBS_FILE_VERSION) {
601 Slog.d(TAG, "Invalid version number, aborting jobs file read.");
604 } catch (NumberFormatException e) {
605 Slog.e(TAG, "Invalid version number, aborting jobs file read.");
608 eventType = parser.next();
611 if (eventType == XmlPullParser.START_TAG) {
612 tagName = parser.getName();
613 // Start reading job.
614 if ("job".equals(tagName)) {
615 JobStatus persistedJob = restoreJobFromXml(rtcIsGood, parser);
616 if (persistedJob != null) {
618 Slog.d(TAG, "Read out " + persistedJob);
620 jobs.add(persistedJob);
622 Slog.d(TAG, "Error reading job from file.");
626 eventType = parser.next();
627 } while (eventType != XmlPullParser.END_DOCUMENT);
634 * @param parser Xml parser at the beginning of a "<job/>" tag. The next "parser.next()" call
635 * will take the parser into the body of the job tag.
636 * @return Newly instantiated job holding all the information we just read out of the xml tag.
638 private JobStatus restoreJobFromXml(boolean rtcIsGood, XmlPullParser parser)
639 throws XmlPullParserException, IOException {
640 JobInfo.Builder jobBuilder;
641 int uid, sourceUserId;
642 long lastSuccessfulRunTime;
643 long lastFailedRunTime;
645 // Read out job identifier attributes and priority.
647 jobBuilder = buildBuilderFromXml(parser);
648 jobBuilder.setPersisted(true);
649 uid = Integer.parseInt(parser.getAttributeValue(null, "uid"));
651 String val = parser.getAttributeValue(null, "priority");
653 jobBuilder.setPriority(Integer.parseInt(val));
655 val = parser.getAttributeValue(null, "flags");
657 jobBuilder.setFlags(Integer.parseInt(val));
659 val = parser.getAttributeValue(null, "sourceUserId");
660 sourceUserId = val == null ? -1 : Integer.parseInt(val);
662 val = parser.getAttributeValue(null, "lastSuccessfulRunTime");
663 lastSuccessfulRunTime = val == null ? 0 : Long.parseLong(val);
665 val = parser.getAttributeValue(null, "lastFailedRunTime");
666 lastFailedRunTime = val == null ? 0 : Long.parseLong(val);
667 } catch (NumberFormatException e) {
668 Slog.e(TAG, "Error parsing job's required fields, skipping");
672 String sourcePackageName = parser.getAttributeValue(null, "sourcePackageName");
674 final String sourceTag = parser.getAttributeValue(null, "sourceTag");
677 // Read out constraints tag.
679 eventType = parser.next();
680 } while (eventType == XmlPullParser.TEXT); // Push through to next START_TAG.
682 if (!(eventType == XmlPullParser.START_TAG &&
683 XML_TAG_PARAMS_CONSTRAINTS.equals(parser.getName()))) {
684 // Expecting a <constraints> start tag.
688 buildConstraintsFromXml(jobBuilder, parser);
689 } catch (NumberFormatException e) {
690 Slog.d(TAG, "Error reading constraints, skipping.");
693 parser.next(); // Consume </constraints>
695 // Read out execution parameters tag.
697 eventType = parser.next();
698 } while (eventType == XmlPullParser.TEXT);
699 if (eventType != XmlPullParser.START_TAG) {
703 // Tuple of (earliest runtime, latest runtime) in UTC.
704 final Pair<Long, Long> rtcRuntimes;
706 rtcRuntimes = buildRtcExecutionTimesFromXml(parser);
707 } catch (NumberFormatException e) {
709 Slog.d(TAG, "Error parsing execution time parameters, skipping.");
714 final long elapsedNow = SystemClock.elapsedRealtime();
715 Pair<Long, Long> elapsedRuntimes = convertRtcBoundsToElapsed(rtcRuntimes, elapsedNow);
717 if (XML_TAG_PERIODIC.equals(parser.getName())) {
719 String val = parser.getAttributeValue(null, "period");
720 final long periodMillis = Long.parseLong(val);
721 val = parser.getAttributeValue(null, "flex");
722 final long flexMillis = (val != null) ? Long.valueOf(val) : periodMillis;
723 jobBuilder.setPeriodic(periodMillis, flexMillis);
724 // As a sanity check, cap the recreated run time to be no later than flex+period
725 // from now. This is the latest the periodic could be pushed out. This could
726 // happen if the periodic ran early (at flex time before period), and then the
728 if (elapsedRuntimes.second > elapsedNow + periodMillis + flexMillis) {
729 final long clampedLateRuntimeElapsed = elapsedNow + flexMillis
731 final long clampedEarlyRuntimeElapsed = clampedLateRuntimeElapsed
734 String.format("Periodic job for uid='%d' persisted run-time is" +
735 " too big [%s, %s]. Clamping to [%s,%s]",
737 DateUtils.formatElapsedTime(elapsedRuntimes.first / 1000),
738 DateUtils.formatElapsedTime(elapsedRuntimes.second / 1000),
739 DateUtils.formatElapsedTime(
740 clampedEarlyRuntimeElapsed / 1000),
741 DateUtils.formatElapsedTime(
742 clampedLateRuntimeElapsed / 1000))
745 Pair.create(clampedEarlyRuntimeElapsed, clampedLateRuntimeElapsed);
747 } catch (NumberFormatException e) {
748 Slog.d(TAG, "Error reading periodic execution criteria, skipping.");
751 } else if (XML_TAG_ONEOFF.equals(parser.getName())) {
753 if (elapsedRuntimes.first != JobStatus.NO_EARLIEST_RUNTIME) {
754 jobBuilder.setMinimumLatency(elapsedRuntimes.first - elapsedNow);
756 if (elapsedRuntimes.second != JobStatus.NO_LATEST_RUNTIME) {
757 jobBuilder.setOverrideDeadline(
758 elapsedRuntimes.second - elapsedNow);
760 } catch (NumberFormatException e) {
761 Slog.d(TAG, "Error reading job execution criteria, skipping.");
766 Slog.d(TAG, "Invalid parameter tag, skipping - " + parser.getName());
768 // Expecting a parameters start tag.
771 maybeBuildBackoffPolicyFromXml(jobBuilder, parser);
773 parser.nextTag(); // Consume parameters end tag.
775 // Read out extras Bundle.
777 eventType = parser.next();
778 } while (eventType == XmlPullParser.TEXT);
779 if (!(eventType == XmlPullParser.START_TAG
780 && XML_TAG_EXTRAS.equals(parser.getName()))) {
782 Slog.d(TAG, "Error reading extras, skipping.");
787 PersistableBundle extras = PersistableBundle.restoreFromXml(parser);
788 jobBuilder.setExtras(extras);
789 parser.nextTag(); // Consume </extras>
791 // Migrate sync jobs forward from earlier, incomplete representation
792 if ("android".equals(sourcePackageName)
794 && extras.getBoolean("SyncManagerJob", false)) {
795 sourcePackageName = extras.getString("owningPackage", sourcePackageName);
797 Slog.i(TAG, "Fixing up sync job source package name from 'android' to '"
798 + sourcePackageName + "'");
802 // And now we're done
803 JobStatus js = new JobStatus(
804 jobBuilder.build(), uid, sourcePackageName, sourceUserId, sourceTag,
805 elapsedRuntimes.first, elapsedRuntimes.second,
806 lastSuccessfulRunTime, lastFailedRunTime,
807 (rtcIsGood) ? null : rtcRuntimes);
811 private JobInfo.Builder buildBuilderFromXml(XmlPullParser parser) throws NumberFormatException {
812 // Pull out required fields from <job> attributes.
813 int jobId = Integer.parseInt(parser.getAttributeValue(null, "jobid"));
814 String packageName = parser.getAttributeValue(null, "package");
815 String className = parser.getAttributeValue(null, "class");
816 ComponentName cname = new ComponentName(packageName, className);
818 return new JobInfo.Builder(jobId, cname);
821 private void buildConstraintsFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser) {
822 String val = parser.getAttributeValue(null, "connectivity");
824 jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
826 val = parser.getAttributeValue(null, "metered");
828 jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_METERED);
830 val = parser.getAttributeValue(null, "unmetered");
832 jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED);
834 val = parser.getAttributeValue(null, "not-roaming");
836 jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_NOT_ROAMING);
838 val = parser.getAttributeValue(null, "idle");
840 jobBuilder.setRequiresDeviceIdle(true);
842 val = parser.getAttributeValue(null, "charging");
844 jobBuilder.setRequiresCharging(true);
849 * Builds the back-off policy out of the params tag. These attributes may not exist, depending
850 * on whether the back-off was set when the job was first scheduled.
852 private void maybeBuildBackoffPolicyFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser) {
853 String val = parser.getAttributeValue(null, "initial-backoff");
855 long initialBackoff = Long.parseLong(val);
856 val = parser.getAttributeValue(null, "backoff-policy");
857 int backoffPolicy = Integer.parseInt(val); // Will throw NFE which we catch higher up.
858 jobBuilder.setBackoffCriteria(initialBackoff, backoffPolicy);
863 * Extract a job's earliest/latest run time data from XML. These are returned in
864 * unadjusted UTC wall clock time, because we do not yet know whether the system
865 * clock is reliable for purposes of calculating deltas from 'now'.
868 * @return A Pair of timestamps in UTC wall-clock time. The first is the earliest
869 * time at which the job is to become runnable, and the second is the deadline at
870 * which it becomes overdue to execute.
871 * @throws NumberFormatException
873 private Pair<Long, Long> buildRtcExecutionTimesFromXml(XmlPullParser parser)
874 throws NumberFormatException {
876 // Pull out execution time data.
877 val = parser.getAttributeValue(null, "delay");
878 final long earliestRunTimeRtc = (val != null)
879 ? Long.parseLong(val)
880 : JobStatus.NO_EARLIEST_RUNTIME;
881 val = parser.getAttributeValue(null, "deadline");
882 final long latestRunTimeRtc = (val != null)
883 ? Long.parseLong(val)
884 : JobStatus.NO_LATEST_RUNTIME;
885 return Pair.create(earliestRunTimeRtc, latestRunTimeRtc);
889 * Convenience function to read out and convert deadline and delay from xml into elapsed real
891 * @return A {@link android.util.Pair}, where the first value is the earliest elapsed runtime
892 * and the second is the latest elapsed runtime.
894 private Pair<Long, Long> buildExecutionTimesFromXml(XmlPullParser parser)
895 throws NumberFormatException {
896 // Pull out execution time data.
897 final long nowWallclock = System.currentTimeMillis();
898 final long nowElapsed = SystemClock.elapsedRealtime();
900 long earliestRunTimeElapsed = JobStatus.NO_EARLIEST_RUNTIME;
901 long latestRunTimeElapsed = JobStatus.NO_LATEST_RUNTIME;
902 String val = parser.getAttributeValue(null, "deadline");
904 long latestRuntimeWallclock = Long.parseLong(val);
905 long maxDelayElapsed =
906 Math.max(latestRuntimeWallclock - nowWallclock, 0);
907 latestRunTimeElapsed = nowElapsed + maxDelayElapsed;
909 val = parser.getAttributeValue(null, "delay");
911 long earliestRuntimeWallclock = Long.parseLong(val);
912 long minDelayElapsed =
913 Math.max(earliestRuntimeWallclock - nowWallclock, 0);
914 earliestRunTimeElapsed = nowElapsed + minDelayElapsed;
917 return Pair.create(earliestRunTimeElapsed, latestRunTimeElapsed);
921 static final class JobSet {
922 // Key is the getUid() originator of the jobs in each sheaf
923 private SparseArray<ArraySet<JobStatus>> mJobs;
926 mJobs = new SparseArray<ArraySet<JobStatus>>();
929 public List<JobStatus> getJobsByUid(int uid) {
930 ArrayList<JobStatus> matchingJobs = new ArrayList<JobStatus>();
931 ArraySet<JobStatus> jobs = mJobs.get(uid);
933 matchingJobs.addAll(jobs);
938 // By user, not by uid, so we need to traverse by key and check
939 public List<JobStatus> getJobsByUser(int userId) {
940 ArrayList<JobStatus> result = new ArrayList<JobStatus>();
941 for (int i = mJobs.size() - 1; i >= 0; i--) {
942 if (UserHandle.getUserId(mJobs.keyAt(i)) == userId) {
943 ArraySet<JobStatus> jobs = mJobs.valueAt(i);
952 public boolean add(JobStatus job) {
953 final int uid = job.getUid();
954 ArraySet<JobStatus> jobs = mJobs.get(uid);
956 jobs = new ArraySet<JobStatus>();
957 mJobs.put(uid, jobs);
959 return jobs.add(job);
962 public boolean remove(JobStatus job) {
963 final int uid = job.getUid();
964 ArraySet<JobStatus> jobs = mJobs.get(uid);
965 boolean didRemove = (jobs != null) ? jobs.remove(job) : false;
966 if (didRemove && jobs.size() == 0) {
967 // no more jobs for this uid; let the now-empty set object be GC'd.
973 // Remove the jobs all users not specified by the whitelist of user ids
974 public void removeJobsOfNonUsers(int[] whitelist) {
975 for (int jobIndex = mJobs.size() - 1; jobIndex >= 0; jobIndex--) {
976 int jobUserId = UserHandle.getUserId(mJobs.keyAt(jobIndex));
977 // check if job's user id is not in the whitelist
978 if (!ArrayUtils.contains(whitelist, jobUserId)) {
979 mJobs.removeAt(jobIndex);
984 public boolean contains(JobStatus job) {
985 final int uid = job.getUid();
986 ArraySet<JobStatus> jobs = mJobs.get(uid);
987 return jobs != null && jobs.contains(job);
990 public JobStatus get(int uid, int jobId) {
991 ArraySet<JobStatus> jobs = mJobs.get(uid);
993 for (int i = jobs.size() - 1; i >= 0; i--) {
994 JobStatus job = jobs.valueAt(i);
995 if (job.getJobId() == jobId) {
1003 // Inefficient; use only for testing
1004 public List<JobStatus> getAllJobs() {
1005 ArrayList<JobStatus> allJobs = new ArrayList<JobStatus>(size());
1006 for (int i = mJobs.size() - 1; i >= 0; i--) {
1007 ArraySet<JobStatus> jobs = mJobs.valueAt(i);
1009 // Use a for loop over the ArraySet, so we don't need to make its
1010 // optional collection class iterator implementation or have to go
1011 // through a temporary array from toArray().
1012 for (int j = jobs.size() - 1; j >= 0; j--) {
1013 allJobs.add(jobs.valueAt(j));
1020 public void clear() {
1026 for (int i = mJobs.size() - 1; i >= 0; i--) {
1027 total += mJobs.valueAt(i).size();
1032 // We only want to count the jobs that this uid has scheduled on its own
1033 // behalf, not those that the app has scheduled on someone else's behalf.
1034 public int countJobsForUid(int uid) {
1036 ArraySet<JobStatus> jobs = mJobs.get(uid);
1038 for (int i = jobs.size() - 1; i >= 0; i--) {
1039 JobStatus job = jobs.valueAt(i);
1040 if (job.getUid() == job.getSourceUid()) {
1048 public void forEachJob(JobStatusFunctor functor) {
1049 for (int uidIndex = mJobs.size() - 1; uidIndex >= 0; uidIndex--) {
1050 ArraySet<JobStatus> jobs = mJobs.valueAt(uidIndex);
1051 for (int i = jobs.size() - 1; i >= 0; i--) {
1052 functor.process(jobs.valueAt(i));
1057 public void forEachJob(int uid, JobStatusFunctor functor) {
1058 ArraySet<JobStatus> jobs = mJobs.get(uid);
1060 for (int i = jobs.size() - 1; i >= 0; i--) {
1061 functor.process(jobs.valueAt(i));