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;
548 List<JobStatus> jobs;
549 FileInputStream fis = mJobsFile.openRead();
550 synchronized (mLock) {
551 jobs = readJobMapImpl(fis, rtcGood);
553 long now = SystemClock.elapsedRealtime();
554 IActivityManager am = ActivityManager.getService();
555 for (int i=0; i<jobs.size(); i++) {
556 JobStatus js = jobs.get(i);
557 js.prepareLocked(am);
558 js.enqueueTime = now;
564 } catch (FileNotFoundException e) {
566 Slog.d(TAG, "Could not find jobs file, probably there was nothing to load.");
568 } catch (XmlPullParserException e) {
570 Slog.d(TAG, "Error parsing xml.", e);
572 } catch (IOException e) {
574 Slog.d(TAG, "Error parsing xml.", e);
579 private List<JobStatus> readJobMapImpl(FileInputStream fis, boolean rtcIsGood)
580 throws XmlPullParserException, IOException {
581 XmlPullParser parser = Xml.newPullParser();
582 parser.setInput(fis, StandardCharsets.UTF_8.name());
584 int eventType = parser.getEventType();
585 while (eventType != XmlPullParser.START_TAG &&
586 eventType != XmlPullParser.END_DOCUMENT) {
587 eventType = parser.next();
588 Slog.d(TAG, "Start tag: " + parser.getName());
590 if (eventType == XmlPullParser.END_DOCUMENT) {
592 Slog.d(TAG, "No persisted jobs.");
597 String tagName = parser.getName();
598 if ("job-info".equals(tagName)) {
599 final List<JobStatus> jobs = new ArrayList<JobStatus>();
600 // Read in version info.
602 int version = Integer.parseInt(parser.getAttributeValue(null, "version"));
603 if (version != JOBS_FILE_VERSION) {
604 Slog.d(TAG, "Invalid version number, aborting jobs file read.");
607 } catch (NumberFormatException e) {
608 Slog.e(TAG, "Invalid version number, aborting jobs file read.");
611 eventType = parser.next();
614 if (eventType == XmlPullParser.START_TAG) {
615 tagName = parser.getName();
616 // Start reading job.
617 if ("job".equals(tagName)) {
618 JobStatus persistedJob = restoreJobFromXml(rtcIsGood, parser);
619 if (persistedJob != null) {
621 Slog.d(TAG, "Read out " + persistedJob);
623 jobs.add(persistedJob);
625 Slog.d(TAG, "Error reading job from file.");
629 eventType = parser.next();
630 } while (eventType != XmlPullParser.END_DOCUMENT);
637 * @param parser Xml parser at the beginning of a "<job/>" tag. The next "parser.next()" call
638 * will take the parser into the body of the job tag.
639 * @return Newly instantiated job holding all the information we just read out of the xml tag.
641 private JobStatus restoreJobFromXml(boolean rtcIsGood, XmlPullParser parser)
642 throws XmlPullParserException, IOException {
643 JobInfo.Builder jobBuilder;
644 int uid, sourceUserId;
645 long lastSuccessfulRunTime;
646 long lastFailedRunTime;
648 // Read out job identifier attributes and priority.
650 jobBuilder = buildBuilderFromXml(parser);
651 jobBuilder.setPersisted(true);
652 uid = Integer.parseInt(parser.getAttributeValue(null, "uid"));
654 String val = parser.getAttributeValue(null, "priority");
656 jobBuilder.setPriority(Integer.parseInt(val));
658 val = parser.getAttributeValue(null, "flags");
660 jobBuilder.setFlags(Integer.parseInt(val));
662 val = parser.getAttributeValue(null, "sourceUserId");
663 sourceUserId = val == null ? -1 : Integer.parseInt(val);
665 val = parser.getAttributeValue(null, "lastSuccessfulRunTime");
666 lastSuccessfulRunTime = val == null ? 0 : Long.parseLong(val);
668 val = parser.getAttributeValue(null, "lastFailedRunTime");
669 lastFailedRunTime = val == null ? 0 : Long.parseLong(val);
670 } catch (NumberFormatException e) {
671 Slog.e(TAG, "Error parsing job's required fields, skipping");
675 String sourcePackageName = parser.getAttributeValue(null, "sourcePackageName");
677 final String sourceTag = parser.getAttributeValue(null, "sourceTag");
680 // Read out constraints tag.
682 eventType = parser.next();
683 } while (eventType == XmlPullParser.TEXT); // Push through to next START_TAG.
685 if (!(eventType == XmlPullParser.START_TAG &&
686 XML_TAG_PARAMS_CONSTRAINTS.equals(parser.getName()))) {
687 // Expecting a <constraints> start tag.
691 buildConstraintsFromXml(jobBuilder, parser);
692 } catch (NumberFormatException e) {
693 Slog.d(TAG, "Error reading constraints, skipping.");
696 parser.next(); // Consume </constraints>
698 // Read out execution parameters tag.
700 eventType = parser.next();
701 } while (eventType == XmlPullParser.TEXT);
702 if (eventType != XmlPullParser.START_TAG) {
706 // Tuple of (earliest runtime, latest runtime) in UTC.
707 final Pair<Long, Long> rtcRuntimes;
709 rtcRuntimes = buildRtcExecutionTimesFromXml(parser);
710 } catch (NumberFormatException e) {
712 Slog.d(TAG, "Error parsing execution time parameters, skipping.");
717 final long elapsedNow = SystemClock.elapsedRealtime();
718 Pair<Long, Long> elapsedRuntimes = convertRtcBoundsToElapsed(rtcRuntimes, elapsedNow);
720 if (XML_TAG_PERIODIC.equals(parser.getName())) {
722 String val = parser.getAttributeValue(null, "period");
723 final long periodMillis = Long.parseLong(val);
724 val = parser.getAttributeValue(null, "flex");
725 final long flexMillis = (val != null) ? Long.valueOf(val) : periodMillis;
726 jobBuilder.setPeriodic(periodMillis, flexMillis);
727 // As a sanity check, cap the recreated run time to be no later than flex+period
728 // from now. This is the latest the periodic could be pushed out. This could
729 // happen if the periodic ran early (at flex time before period), and then the
731 if (elapsedRuntimes.second > elapsedNow + periodMillis + flexMillis) {
732 final long clampedLateRuntimeElapsed = elapsedNow + flexMillis
734 final long clampedEarlyRuntimeElapsed = clampedLateRuntimeElapsed
737 String.format("Periodic job for uid='%d' persisted run-time is" +
738 " too big [%s, %s]. Clamping to [%s,%s]",
740 DateUtils.formatElapsedTime(elapsedRuntimes.first / 1000),
741 DateUtils.formatElapsedTime(elapsedRuntimes.second / 1000),
742 DateUtils.formatElapsedTime(
743 clampedEarlyRuntimeElapsed / 1000),
744 DateUtils.formatElapsedTime(
745 clampedLateRuntimeElapsed / 1000))
748 Pair.create(clampedEarlyRuntimeElapsed, clampedLateRuntimeElapsed);
750 } catch (NumberFormatException e) {
751 Slog.d(TAG, "Error reading periodic execution criteria, skipping.");
754 } else if (XML_TAG_ONEOFF.equals(parser.getName())) {
756 if (elapsedRuntimes.first != JobStatus.NO_EARLIEST_RUNTIME) {
757 jobBuilder.setMinimumLatency(elapsedRuntimes.first - elapsedNow);
759 if (elapsedRuntimes.second != JobStatus.NO_LATEST_RUNTIME) {
760 jobBuilder.setOverrideDeadline(
761 elapsedRuntimes.second - elapsedNow);
763 } catch (NumberFormatException e) {
764 Slog.d(TAG, "Error reading job execution criteria, skipping.");
769 Slog.d(TAG, "Invalid parameter tag, skipping - " + parser.getName());
771 // Expecting a parameters start tag.
774 maybeBuildBackoffPolicyFromXml(jobBuilder, parser);
776 parser.nextTag(); // Consume parameters end tag.
778 // Read out extras Bundle.
780 eventType = parser.next();
781 } while (eventType == XmlPullParser.TEXT);
782 if (!(eventType == XmlPullParser.START_TAG
783 && XML_TAG_EXTRAS.equals(parser.getName()))) {
785 Slog.d(TAG, "Error reading extras, skipping.");
790 PersistableBundle extras = PersistableBundle.restoreFromXml(parser);
791 jobBuilder.setExtras(extras);
792 parser.nextTag(); // Consume </extras>
794 // Migrate sync jobs forward from earlier, incomplete representation
795 if ("android".equals(sourcePackageName)
797 && extras.getBoolean("SyncManagerJob", false)) {
798 sourcePackageName = extras.getString("owningPackage", sourcePackageName);
800 Slog.i(TAG, "Fixing up sync job source package name from 'android' to '"
801 + sourcePackageName + "'");
805 // And now we're done
806 JobStatus js = new JobStatus(
807 jobBuilder.build(), uid, sourcePackageName, sourceUserId, sourceTag,
808 elapsedRuntimes.first, elapsedRuntimes.second,
809 lastSuccessfulRunTime, lastFailedRunTime,
810 (rtcIsGood) ? null : rtcRuntimes);
814 private JobInfo.Builder buildBuilderFromXml(XmlPullParser parser) throws NumberFormatException {
815 // Pull out required fields from <job> attributes.
816 int jobId = Integer.parseInt(parser.getAttributeValue(null, "jobid"));
817 String packageName = parser.getAttributeValue(null, "package");
818 String className = parser.getAttributeValue(null, "class");
819 ComponentName cname = new ComponentName(packageName, className);
821 return new JobInfo.Builder(jobId, cname);
824 private void buildConstraintsFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser) {
825 String val = parser.getAttributeValue(null, "connectivity");
827 jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
829 val = parser.getAttributeValue(null, "metered");
831 jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_METERED);
833 val = parser.getAttributeValue(null, "unmetered");
835 jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED);
837 val = parser.getAttributeValue(null, "not-roaming");
839 jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_NOT_ROAMING);
841 val = parser.getAttributeValue(null, "idle");
843 jobBuilder.setRequiresDeviceIdle(true);
845 val = parser.getAttributeValue(null, "charging");
847 jobBuilder.setRequiresCharging(true);
852 * Builds the back-off policy out of the params tag. These attributes may not exist, depending
853 * on whether the back-off was set when the job was first scheduled.
855 private void maybeBuildBackoffPolicyFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser) {
856 String val = parser.getAttributeValue(null, "initial-backoff");
858 long initialBackoff = Long.parseLong(val);
859 val = parser.getAttributeValue(null, "backoff-policy");
860 int backoffPolicy = Integer.parseInt(val); // Will throw NFE which we catch higher up.
861 jobBuilder.setBackoffCriteria(initialBackoff, backoffPolicy);
866 * Extract a job's earliest/latest run time data from XML. These are returned in
867 * unadjusted UTC wall clock time, because we do not yet know whether the system
868 * clock is reliable for purposes of calculating deltas from 'now'.
871 * @return A Pair of timestamps in UTC wall-clock time. The first is the earliest
872 * time at which the job is to become runnable, and the second is the deadline at
873 * which it becomes overdue to execute.
874 * @throws NumberFormatException
876 private Pair<Long, Long> buildRtcExecutionTimesFromXml(XmlPullParser parser)
877 throws NumberFormatException {
879 // Pull out execution time data.
880 val = parser.getAttributeValue(null, "delay");
881 final long earliestRunTimeRtc = (val != null)
882 ? Long.parseLong(val)
883 : JobStatus.NO_EARLIEST_RUNTIME;
884 val = parser.getAttributeValue(null, "deadline");
885 final long latestRunTimeRtc = (val != null)
886 ? Long.parseLong(val)
887 : JobStatus.NO_LATEST_RUNTIME;
888 return Pair.create(earliestRunTimeRtc, latestRunTimeRtc);
892 * Convenience function to read out and convert deadline and delay from xml into elapsed real
894 * @return A {@link android.util.Pair}, where the first value is the earliest elapsed runtime
895 * and the second is the latest elapsed runtime.
897 private Pair<Long, Long> buildExecutionTimesFromXml(XmlPullParser parser)
898 throws NumberFormatException {
899 // Pull out execution time data.
900 final long nowWallclock = System.currentTimeMillis();
901 final long nowElapsed = SystemClock.elapsedRealtime();
903 long earliestRunTimeElapsed = JobStatus.NO_EARLIEST_RUNTIME;
904 long latestRunTimeElapsed = JobStatus.NO_LATEST_RUNTIME;
905 String val = parser.getAttributeValue(null, "deadline");
907 long latestRuntimeWallclock = Long.parseLong(val);
908 long maxDelayElapsed =
909 Math.max(latestRuntimeWallclock - nowWallclock, 0);
910 latestRunTimeElapsed = nowElapsed + maxDelayElapsed;
912 val = parser.getAttributeValue(null, "delay");
914 long earliestRuntimeWallclock = Long.parseLong(val);
915 long minDelayElapsed =
916 Math.max(earliestRuntimeWallclock - nowWallclock, 0);
917 earliestRunTimeElapsed = nowElapsed + minDelayElapsed;
920 return Pair.create(earliestRunTimeElapsed, latestRunTimeElapsed);
924 static final class JobSet {
925 // Key is the getUid() originator of the jobs in each sheaf
926 private SparseArray<ArraySet<JobStatus>> mJobs;
929 mJobs = new SparseArray<ArraySet<JobStatus>>();
932 public List<JobStatus> getJobsByUid(int uid) {
933 ArrayList<JobStatus> matchingJobs = new ArrayList<JobStatus>();
934 ArraySet<JobStatus> jobs = mJobs.get(uid);
936 matchingJobs.addAll(jobs);
941 // By user, not by uid, so we need to traverse by key and check
942 public List<JobStatus> getJobsByUser(int userId) {
943 ArrayList<JobStatus> result = new ArrayList<JobStatus>();
944 for (int i = mJobs.size() - 1; i >= 0; i--) {
945 if (UserHandle.getUserId(mJobs.keyAt(i)) == userId) {
946 ArraySet<JobStatus> jobs = mJobs.valueAt(i);
955 public boolean add(JobStatus job) {
956 final int uid = job.getUid();
957 ArraySet<JobStatus> jobs = mJobs.get(uid);
959 jobs = new ArraySet<JobStatus>();
960 mJobs.put(uid, jobs);
962 return jobs.add(job);
965 public boolean remove(JobStatus job) {
966 final int uid = job.getUid();
967 ArraySet<JobStatus> jobs = mJobs.get(uid);
968 boolean didRemove = (jobs != null) ? jobs.remove(job) : false;
969 if (didRemove && jobs.size() == 0) {
970 // no more jobs for this uid; let the now-empty set object be GC'd.
976 // Remove the jobs all users not specified by the whitelist of user ids
977 public void removeJobsOfNonUsers(int[] whitelist) {
978 for (int jobIndex = mJobs.size() - 1; jobIndex >= 0; jobIndex--) {
979 int jobUserId = UserHandle.getUserId(mJobs.keyAt(jobIndex));
980 // check if job's user id is not in the whitelist
981 if (!ArrayUtils.contains(whitelist, jobUserId)) {
982 mJobs.removeAt(jobIndex);
987 public boolean contains(JobStatus job) {
988 final int uid = job.getUid();
989 ArraySet<JobStatus> jobs = mJobs.get(uid);
990 return jobs != null && jobs.contains(job);
993 public JobStatus get(int uid, int jobId) {
994 ArraySet<JobStatus> jobs = mJobs.get(uid);
996 for (int i = jobs.size() - 1; i >= 0; i--) {
997 JobStatus job = jobs.valueAt(i);
998 if (job.getJobId() == jobId) {
1006 // Inefficient; use only for testing
1007 public List<JobStatus> getAllJobs() {
1008 ArrayList<JobStatus> allJobs = new ArrayList<JobStatus>(size());
1009 for (int i = mJobs.size() - 1; i >= 0; i--) {
1010 ArraySet<JobStatus> jobs = mJobs.valueAt(i);
1012 // Use a for loop over the ArraySet, so we don't need to make its
1013 // optional collection class iterator implementation or have to go
1014 // through a temporary array from toArray().
1015 for (int j = jobs.size() - 1; j >= 0; j--) {
1016 allJobs.add(jobs.valueAt(j));
1023 public void clear() {
1029 for (int i = mJobs.size() - 1; i >= 0; i--) {
1030 total += mJobs.valueAt(i).size();
1035 // We only want to count the jobs that this uid has scheduled on its own
1036 // behalf, not those that the app has scheduled on someone else's behalf.
1037 public int countJobsForUid(int uid) {
1039 ArraySet<JobStatus> jobs = mJobs.get(uid);
1041 for (int i = jobs.size() - 1; i >= 0; i--) {
1042 JobStatus job = jobs.valueAt(i);
1043 if (job.getUid() == job.getSourceUid()) {
1051 public void forEachJob(JobStatusFunctor functor) {
1052 for (int uidIndex = mJobs.size() - 1; uidIndex >= 0; uidIndex--) {
1053 ArraySet<JobStatus> jobs = mJobs.valueAt(uidIndex);
1054 for (int i = jobs.size() - 1; i >= 0; i--) {
1055 functor.process(jobs.valueAt(i));
1060 public void forEachJob(int uid, JobStatusFunctor functor) {
1061 ArraySet<JobStatus> jobs = mJobs.get(uid);
1063 for (int i = jobs.size() - 1; i >= 0; i--) {
1064 functor.process(jobs.valueAt(i));