blob: 602b9c755e884c71ef56c1e797e2a7b43fbc073d [file] [log] [blame]
Christopher Tate7060b042014-06-09 19:50:00 -07001/*
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
17package com.android.server.job;
18
19import android.content.ComponentName;
20import android.app.job.JobInfo;
21import android.content.Context;
22import android.os.Environment;
23import android.os.Handler;
24import android.os.PersistableBundle;
25import android.os.SystemClock;
26import android.os.UserHandle;
Matthew Williamsfa8e5082015-10-15 15:59:12 -070027import android.text.format.DateUtils;
Christopher Tate7060b042014-06-09 19:50:00 -070028import android.util.AtomicFile;
29import android.util.ArraySet;
30import android.util.Pair;
31import android.util.Slog;
Christopher Tate2f36fd62016-02-18 18:36:08 -080032import android.util.SparseArray;
Christopher Tate7060b042014-06-09 19:50:00 -070033import android.util.Xml;
34
35import com.android.internal.annotations.VisibleForTesting;
36import com.android.internal.util.FastXmlSerializer;
37import com.android.server.IoThread;
38import com.android.server.job.controllers.JobStatus;
39
40import java.io.ByteArrayOutputStream;
41import java.io.File;
42import java.io.FileInputStream;
43import java.io.FileNotFoundException;
44import java.io.FileOutputStream;
45import java.io.IOException;
Wojciech Staszkiewicz4f117542015-05-08 14:58:46 +010046import java.nio.charset.StandardCharsets;
Christopher Tate7060b042014-06-09 19:50:00 -070047import java.util.ArrayList;
Christopher Tate7060b042014-06-09 19:50:00 -070048import java.util.List;
Shreyas Basarged09973b2015-12-16 18:10:05 +000049import java.util.Set;
Christopher Tate7060b042014-06-09 19:50:00 -070050
51import org.xmlpull.v1.XmlPullParser;
52import org.xmlpull.v1.XmlPullParserException;
53import org.xmlpull.v1.XmlSerializer;
54
55/**
Matthew Williams48a30db2014-09-23 13:39:36 -070056 * Maintains the master list of jobs that the job scheduler is tracking. These jobs are compared by
57 * reference, so none of the functions in this class should make a copy.
58 * Also handles read/write of persisted jobs.
Christopher Tate7060b042014-06-09 19:50:00 -070059 *
60 * Note on locking:
61 * All callers to this class must <strong>lock on the class object they are calling</strong>.
62 * This is important b/c {@link com.android.server.job.JobStore.WriteJobsMapToDiskRunnable}
63 * and {@link com.android.server.job.JobStore.ReadJobMapFromDiskRunnable} lock on that
64 * object.
65 */
66public class JobStore {
67 private static final String TAG = "JobStore";
68 private static final boolean DEBUG = JobSchedulerService.DEBUG;
69
70 /** Threshold to adjust how often we want to write to the db. */
71 private static final int MAX_OPS_BEFORE_WRITE = 1;
Dianne Hackborn33d31c52016-02-16 10:30:33 -080072 final Object mLock;
Christopher Tate2f36fd62016-02-18 18:36:08 -080073 final JobSet mJobSet; // per-caller-uid tracking
Christopher Tate7060b042014-06-09 19:50:00 -070074 final Context mContext;
75
76 private int mDirtyOperations;
77
78 private static final Object sSingletonLock = new Object();
79 private final AtomicFile mJobsFile;
80 /** Handler backed by IoThread for writing to disk. */
81 private final Handler mIoHandler = IoThread.getHandler();
82 private static JobStore sSingleton;
83
84 /** Used by the {@link JobSchedulerService} to instantiate the JobStore. */
85 static JobStore initAndGet(JobSchedulerService jobManagerService) {
86 synchronized (sSingletonLock) {
87 if (sSingleton == null) {
88 sSingleton = new JobStore(jobManagerService.getContext(),
Dianne Hackborn33d31c52016-02-16 10:30:33 -080089 jobManagerService.getLock(), Environment.getDataDirectory());
Christopher Tate7060b042014-06-09 19:50:00 -070090 }
91 return sSingleton;
92 }
93 }
94
Matthew Williams01ac45b2014-07-22 20:44:12 -070095 /**
96 * @return A freshly initialized job store object, with no loaded jobs.
97 */
Christopher Tate7060b042014-06-09 19:50:00 -070098 @VisibleForTesting
Matthew Williams01ac45b2014-07-22 20:44:12 -070099 public static JobStore initAndGetForTesting(Context context, File dataDir) {
Dianne Hackborn33d31c52016-02-16 10:30:33 -0800100 JobStore jobStoreUnderTest = new JobStore(context, new Object(), dataDir);
Matthew Williams01ac45b2014-07-22 20:44:12 -0700101 jobStoreUnderTest.clear();
102 return jobStoreUnderTest;
Christopher Tate7060b042014-06-09 19:50:00 -0700103 }
104
Matthew Williams01ac45b2014-07-22 20:44:12 -0700105 /**
106 * Construct the instance of the job store. This results in a blocking read from disk.
107 */
Dianne Hackborn33d31c52016-02-16 10:30:33 -0800108 private JobStore(Context context, Object lock, File dataDir) {
109 mLock = lock;
Christopher Tate7060b042014-06-09 19:50:00 -0700110 mContext = context;
111 mDirtyOperations = 0;
112
113 File systemDir = new File(dataDir, "system");
114 File jobDir = new File(systemDir, "job");
115 jobDir.mkdirs();
116 mJobsFile = new AtomicFile(new File(jobDir, "jobs.xml"));
117
Christopher Tate2f36fd62016-02-18 18:36:08 -0800118 mJobSet = new JobSet();
Christopher Tate7060b042014-06-09 19:50:00 -0700119
Matthew Williams01ac45b2014-07-22 20:44:12 -0700120 readJobMapFromDisk(mJobSet);
Christopher Tate7060b042014-06-09 19:50:00 -0700121 }
122
123 /**
124 * Add a job to the master list, persisting it if necessary. If the JobStatus already exists,
125 * it will be replaced.
126 * @param jobStatus Job to add.
127 * @return Whether or not an equivalent JobStatus was replaced by this operation.
128 */
129 public boolean add(JobStatus jobStatus) {
130 boolean replaced = mJobSet.remove(jobStatus);
131 mJobSet.add(jobStatus);
132 if (jobStatus.isPersisted()) {
133 maybeWriteStatusToDiskAsync();
134 }
135 if (DEBUG) {
136 Slog.d(TAG, "Added job status to store: " + jobStatus);
137 }
138 return replaced;
139 }
140
Matthew Williams48a30db2014-09-23 13:39:36 -0700141 boolean containsJob(JobStatus jobStatus) {
142 return mJobSet.contains(jobStatus);
143 }
144
Christopher Tate7060b042014-06-09 19:50:00 -0700145 public int size() {
146 return mJobSet.size();
147 }
148
Christopher Tate2f36fd62016-02-18 18:36:08 -0800149 public int countJobsForUid(int uid) {
150 return mJobSet.countJobsForUid(uid);
151 }
152
Christopher Tate7060b042014-06-09 19:50:00 -0700153 /**
154 * Remove the provided job. Will also delete the job if it was persisted.
Shreyas Basarge73f10252016-02-11 17:06:13 +0000155 * @param writeBack If true, the job will be deleted (if it was persisted) immediately.
Christopher Tate7060b042014-06-09 19:50:00 -0700156 * @return Whether or not the job existed to be removed.
157 */
Shreyas Basarge73f10252016-02-11 17:06:13 +0000158 public boolean remove(JobStatus jobStatus, boolean writeBack) {
Christopher Tate7060b042014-06-09 19:50:00 -0700159 boolean removed = mJobSet.remove(jobStatus);
160 if (!removed) {
161 if (DEBUG) {
162 Slog.d(TAG, "Couldn't remove job: didn't exist: " + jobStatus);
163 }
164 return false;
165 }
Shreyas Basarge73f10252016-02-11 17:06:13 +0000166 if (writeBack && jobStatus.isPersisted()) {
Matthew Williams900c67f2014-07-09 12:46:53 -0700167 maybeWriteStatusToDiskAsync();
168 }
Christopher Tate7060b042014-06-09 19:50:00 -0700169 return removed;
170 }
171
172 @VisibleForTesting
173 public void clear() {
174 mJobSet.clear();
175 maybeWriteStatusToDiskAsync();
176 }
177
Matthew Williams48a30db2014-09-23 13:39:36 -0700178 /**
179 * @param userHandle User for whom we are querying the list of jobs.
180 * @return A list of all the jobs scheduled by the provided user. Never null.
181 */
Christopher Tate7060b042014-06-09 19:50:00 -0700182 public List<JobStatus> getJobsByUser(int userHandle) {
Christopher Tate2f36fd62016-02-18 18:36:08 -0800183 return mJobSet.getJobsByUser(userHandle);
Christopher Tate7060b042014-06-09 19:50:00 -0700184 }
185
186 /**
187 * @param uid Uid of the requesting app.
Matthew Williams48a30db2014-09-23 13:39:36 -0700188 * @return All JobStatus objects for a given uid from the master list. Never null.
Christopher Tate7060b042014-06-09 19:50:00 -0700189 */
190 public List<JobStatus> getJobsByUid(int uid) {
Christopher Tate2f36fd62016-02-18 18:36:08 -0800191 return mJobSet.getJobsByUid(uid);
Christopher Tate7060b042014-06-09 19:50:00 -0700192 }
193
194 /**
195 * @param uid Uid of the requesting app.
196 * @param jobId Job id, specified at schedule-time.
197 * @return the JobStatus that matches the provided uId and jobId, or null if none found.
198 */
199 public JobStatus getJobByUidAndJobId(int uid, int jobId) {
Christopher Tate2f36fd62016-02-18 18:36:08 -0800200 return mJobSet.get(uid, jobId);
Christopher Tate7060b042014-06-09 19:50:00 -0700201 }
202
203 /**
Christopher Tate2f36fd62016-02-18 18:36:08 -0800204 * Iterate over the set of all jobs, invoking the supplied functor on each. This is for
205 * customers who need to examine each job; we'd much rather not have to generate
206 * transient unified collections for them to iterate over and then discard, or creating
207 * iterators every time a client needs to perform a sweep.
Christopher Tate7060b042014-06-09 19:50:00 -0700208 */
Christopher Tate2f36fd62016-02-18 18:36:08 -0800209 public void forEachJob(JobStatusFunctor functor) {
210 mJobSet.forEachJob(functor);
211 }
212
Shreyas Basargecbf5ae92016-03-08 16:13:06 +0000213 public void forEachJob(int uid, JobStatusFunctor functor) {
214 mJobSet.forEachJob(uid, functor);
215 }
216
Christopher Tate2f36fd62016-02-18 18:36:08 -0800217 public interface JobStatusFunctor {
218 public void process(JobStatus jobStatus);
Christopher Tate7060b042014-06-09 19:50:00 -0700219 }
220
221 /** Version of the db schema. */
222 private static final int JOBS_FILE_VERSION = 0;
223 /** Tag corresponds to constraints this job needs. */
224 private static final String XML_TAG_PARAMS_CONSTRAINTS = "constraints";
225 /** Tag corresponds to execution parameters. */
226 private static final String XML_TAG_PERIODIC = "periodic";
227 private static final String XML_TAG_ONEOFF = "one-off";
228 private static final String XML_TAG_EXTRAS = "extras";
229
230 /**
231 * Every time the state changes we write all the jobs in one swath, instead of trying to
232 * track incremental changes.
233 * @return Whether the operation was successful. This will only fail for e.g. if the system is
234 * low on storage. If this happens, we continue as normal
235 */
236 private void maybeWriteStatusToDiskAsync() {
237 mDirtyOperations++;
238 if (mDirtyOperations >= MAX_OPS_BEFORE_WRITE) {
239 if (DEBUG) {
240 Slog.v(TAG, "Writing jobs to disk.");
241 }
242 mIoHandler.post(new WriteJobsMapToDiskRunnable());
243 }
244 }
245
Matthew Williams01ac45b2014-07-22 20:44:12 -0700246 @VisibleForTesting
Christopher Tate2f36fd62016-02-18 18:36:08 -0800247 public void readJobMapFromDisk(JobSet jobSet) {
Matthew Williams01ac45b2014-07-22 20:44:12 -0700248 new ReadJobMapFromDiskRunnable(jobSet).run();
Christopher Tate7060b042014-06-09 19:50:00 -0700249 }
250
251 /**
252 * Runnable that writes {@link #mJobSet} out to xml.
Dianne Hackborn33d31c52016-02-16 10:30:33 -0800253 * NOTE: This Runnable locks on mLock
Christopher Tate7060b042014-06-09 19:50:00 -0700254 */
255 private class WriteJobsMapToDiskRunnable implements Runnable {
256 @Override
257 public void run() {
258 final long startElapsed = SystemClock.elapsedRealtime();
Christopher Tate2f36fd62016-02-18 18:36:08 -0800259 final List<JobStatus> storeCopy = new ArrayList<JobStatus>();
Dianne Hackborn33d31c52016-02-16 10:30:33 -0800260 synchronized (mLock) {
Christopher Tate2f36fd62016-02-18 18:36:08 -0800261 // Clone the jobs so we can release the lock before writing.
262 mJobSet.forEachJob(new JobStatusFunctor() {
263 @Override
264 public void process(JobStatus job) {
265 if (job.isPersisted()) {
266 storeCopy.add(new JobStatus(job));
267 }
Shreyas Basarge7ef490f2015-12-03 16:45:22 +0000268 }
Christopher Tate2f36fd62016-02-18 18:36:08 -0800269 });
Christopher Tate7060b042014-06-09 19:50:00 -0700270 }
Christopher Tate2f36fd62016-02-18 18:36:08 -0800271 writeJobsMapImpl(storeCopy);
Christopher Tate7060b042014-06-09 19:50:00 -0700272 if (JobSchedulerService.DEBUG) {
273 Slog.v(TAG, "Finished writing, took " + (SystemClock.elapsedRealtime()
274 - startElapsed) + "ms");
275 }
276 }
277
Matthew Williams49a85b62014-06-12 11:02:34 -0700278 private void writeJobsMapImpl(List<JobStatus> jobList) {
Christopher Tate7060b042014-06-09 19:50:00 -0700279 try {
280 ByteArrayOutputStream baos = new ByteArrayOutputStream();
281 XmlSerializer out = new FastXmlSerializer();
Wojciech Staszkiewicz4f117542015-05-08 14:58:46 +0100282 out.setOutput(baos, StandardCharsets.UTF_8.name());
Christopher Tate7060b042014-06-09 19:50:00 -0700283 out.startDocument(null, true);
284 out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
285
286 out.startTag(null, "job-info");
287 out.attribute(null, "version", Integer.toString(JOBS_FILE_VERSION));
Dianne Hackbornfdb19562014-07-11 16:03:36 -0700288 for (int i=0; i<jobList.size(); i++) {
289 JobStatus jobStatus = jobList.get(i);
Christopher Tate7060b042014-06-09 19:50:00 -0700290 if (DEBUG) {
291 Slog.d(TAG, "Saving job " + jobStatus.getJobId());
292 }
293 out.startTag(null, "job");
Shreyas Basarge5db09082016-01-07 13:38:29 +0000294 addAttributesToJobTag(out, jobStatus);
Christopher Tate7060b042014-06-09 19:50:00 -0700295 writeConstraintsToXml(out, jobStatus);
296 writeExecutionCriteriaToXml(out, jobStatus);
297 writeBundleToXml(jobStatus.getExtras(), out);
298 out.endTag(null, "job");
299 }
300 out.endTag(null, "job-info");
301 out.endDocument();
302
303 // Write out to disk in one fell sweep.
304 FileOutputStream fos = mJobsFile.startWrite();
305 fos.write(baos.toByteArray());
306 mJobsFile.finishWrite(fos);
307 mDirtyOperations = 0;
308 } catch (IOException e) {
309 if (DEBUG) {
310 Slog.v(TAG, "Error writing out job data.", e);
311 }
312 } catch (XmlPullParserException e) {
313 if (DEBUG) {
314 Slog.d(TAG, "Error persisting bundle.", e);
315 }
316 }
317 }
318
Shreyas Basarge5db09082016-01-07 13:38:29 +0000319 /** Write out a tag with data comprising the required fields and priority of this job and
320 * its client.
321 */
322 private void addAttributesToJobTag(XmlSerializer out, JobStatus jobStatus)
Christopher Tate7060b042014-06-09 19:50:00 -0700323 throws IOException {
324 out.attribute(null, "jobid", Integer.toString(jobStatus.getJobId()));
325 out.attribute(null, "package", jobStatus.getServiceComponent().getPackageName());
326 out.attribute(null, "class", jobStatus.getServiceComponent().getClassName());
Shreyas Basarge968ac752016-01-11 23:09:26 +0000327 if (jobStatus.getSourcePackageName() != null) {
328 out.attribute(null, "sourcePackageName", jobStatus.getSourcePackageName());
329 }
Dianne Hackborn1085ff62016-02-23 17:04:58 -0800330 if (jobStatus.getSourceTag() != null) {
331 out.attribute(null, "sourceTag", jobStatus.getSourceTag());
332 }
Shreyas Basarge968ac752016-01-11 23:09:26 +0000333 out.attribute(null, "sourceUserId", String.valueOf(jobStatus.getSourceUserId()));
Christopher Tate7060b042014-06-09 19:50:00 -0700334 out.attribute(null, "uid", Integer.toString(jobStatus.getUid()));
Shreyas Basarge5db09082016-01-07 13:38:29 +0000335 out.attribute(null, "priority", String.valueOf(jobStatus.getPriority()));
Jeff Sharkey1b6519b2016-04-28 15:33:18 -0600336 out.attribute(null, "flags", String.valueOf(jobStatus.getFlags()));
Christopher Tate7060b042014-06-09 19:50:00 -0700337 }
338
339 private void writeBundleToXml(PersistableBundle extras, XmlSerializer out)
340 throws IOException, XmlPullParserException {
341 out.startTag(null, XML_TAG_EXTRAS);
Shreyas Basarged09973b2015-12-16 18:10:05 +0000342 PersistableBundle extrasCopy = deepCopyBundle(extras, 10);
343 extrasCopy.saveToXml(out);
Christopher Tate7060b042014-06-09 19:50:00 -0700344 out.endTag(null, XML_TAG_EXTRAS);
345 }
Shreyas Basarged09973b2015-12-16 18:10:05 +0000346
347 private PersistableBundle deepCopyBundle(PersistableBundle bundle, int maxDepth) {
348 if (maxDepth <= 0) {
349 return null;
350 }
351 PersistableBundle copy = (PersistableBundle) bundle.clone();
352 Set<String> keySet = bundle.keySet();
353 for (String key: keySet) {
Shreyas Basarge5db09082016-01-07 13:38:29 +0000354 Object o = copy.get(key);
355 if (o instanceof PersistableBundle) {
356 PersistableBundle bCopy = deepCopyBundle((PersistableBundle) o, maxDepth-1);
Shreyas Basarged09973b2015-12-16 18:10:05 +0000357 copy.putPersistableBundle(key, bCopy);
358 }
359 }
360 return copy;
361 }
362
Christopher Tate7060b042014-06-09 19:50:00 -0700363 /**
364 * Write out a tag with data identifying this job's constraints. If the constraint isn't here
365 * it doesn't apply.
366 */
367 private void writeConstraintsToXml(XmlSerializer out, JobStatus jobStatus) throws IOException {
368 out.startTag(null, XML_TAG_PARAMS_CONSTRAINTS);
Jeff Sharkeyf07c7b92016-04-22 09:50:16 -0600369 if (jobStatus.hasConnectivityConstraint()) {
370 out.attribute(null, "connectivity", Boolean.toString(true));
371 }
Christopher Tate7060b042014-06-09 19:50:00 -0700372 if (jobStatus.hasUnmeteredConstraint()) {
373 out.attribute(null, "unmetered", Boolean.toString(true));
374 }
Jeff Sharkeyf07c7b92016-04-22 09:50:16 -0600375 if (jobStatus.hasNotRoamingConstraint()) {
376 out.attribute(null, "not-roaming", Boolean.toString(true));
Christopher Tate7060b042014-06-09 19:50:00 -0700377 }
378 if (jobStatus.hasIdleConstraint()) {
379 out.attribute(null, "idle", Boolean.toString(true));
380 }
381 if (jobStatus.hasChargingConstraint()) {
382 out.attribute(null, "charging", Boolean.toString(true));
383 }
384 out.endTag(null, XML_TAG_PARAMS_CONSTRAINTS);
385 }
386
387 private void writeExecutionCriteriaToXml(XmlSerializer out, JobStatus jobStatus)
388 throws IOException {
389 final JobInfo job = jobStatus.getJob();
390 if (jobStatus.getJob().isPeriodic()) {
391 out.startTag(null, XML_TAG_PERIODIC);
392 out.attribute(null, "period", Long.toString(job.getIntervalMillis()));
Shreyas Basarge89ee6182015-12-17 15:16:36 +0000393 out.attribute(null, "flex", Long.toString(job.getFlexMillis()));
Christopher Tate7060b042014-06-09 19:50:00 -0700394 } else {
395 out.startTag(null, XML_TAG_ONEOFF);
396 }
397
398 if (jobStatus.hasDeadlineConstraint()) {
399 // Wall clock deadline.
400 final long deadlineWallclock = System.currentTimeMillis() +
401 (jobStatus.getLatestRunTimeElapsed() - SystemClock.elapsedRealtime());
402 out.attribute(null, "deadline", Long.toString(deadlineWallclock));
403 }
404 if (jobStatus.hasTimingDelayConstraint()) {
405 final long delayWallclock = System.currentTimeMillis() +
406 (jobStatus.getEarliestRunTime() - SystemClock.elapsedRealtime());
407 out.attribute(null, "delay", Long.toString(delayWallclock));
408 }
409
410 // Only write out back-off policy if it differs from the default.
411 // This also helps the case where the job is idle -> these aren't allowed to specify
412 // back-off.
413 if (jobStatus.getJob().getInitialBackoffMillis() != JobInfo.DEFAULT_INITIAL_BACKOFF_MILLIS
414 || jobStatus.getJob().getBackoffPolicy() != JobInfo.DEFAULT_BACKOFF_POLICY) {
415 out.attribute(null, "backoff-policy", Integer.toString(job.getBackoffPolicy()));
416 out.attribute(null, "initial-backoff", Long.toString(job.getInitialBackoffMillis()));
417 }
418 if (job.isPeriodic()) {
419 out.endTag(null, XML_TAG_PERIODIC);
420 } else {
421 out.endTag(null, XML_TAG_ONEOFF);
422 }
423 }
424 }
425
426 /**
Matthew Williams01ac45b2014-07-22 20:44:12 -0700427 * Runnable that reads list of persisted job from xml. This is run once at start up, so doesn't
428 * need to go through {@link JobStore#add(com.android.server.job.controllers.JobStatus)}.
Christopher Tate7060b042014-06-09 19:50:00 -0700429 */
430 private class ReadJobMapFromDiskRunnable implements Runnable {
Christopher Tate2f36fd62016-02-18 18:36:08 -0800431 private final JobSet jobSet;
Matthew Williams01ac45b2014-07-22 20:44:12 -0700432
433 /**
434 * @param jobSet Reference to the (empty) set of JobStatus objects that back the JobStore,
435 * so that after disk read we can populate it directly.
436 */
Christopher Tate2f36fd62016-02-18 18:36:08 -0800437 ReadJobMapFromDiskRunnable(JobSet jobSet) {
Matthew Williams01ac45b2014-07-22 20:44:12 -0700438 this.jobSet = jobSet;
Christopher Tate7060b042014-06-09 19:50:00 -0700439 }
440
441 @Override
442 public void run() {
443 try {
444 List<JobStatus> jobs;
445 FileInputStream fis = mJobsFile.openRead();
Dianne Hackborn33d31c52016-02-16 10:30:33 -0800446 synchronized (mLock) {
Christopher Tate7060b042014-06-09 19:50:00 -0700447 jobs = readJobMapImpl(fis);
Matthew Williams01ac45b2014-07-22 20:44:12 -0700448 if (jobs != null) {
449 for (int i=0; i<jobs.size(); i++) {
450 this.jobSet.add(jobs.get(i));
451 }
452 }
Christopher Tate7060b042014-06-09 19:50:00 -0700453 }
454 fis.close();
Christopher Tate7060b042014-06-09 19:50:00 -0700455 } catch (FileNotFoundException e) {
456 if (JobSchedulerService.DEBUG) {
457 Slog.d(TAG, "Could not find jobs file, probably there was nothing to load.");
458 }
459 } catch (XmlPullParserException e) {
460 if (JobSchedulerService.DEBUG) {
461 Slog.d(TAG, "Error parsing xml.", e);
462 }
463 } catch (IOException e) {
464 if (JobSchedulerService.DEBUG) {
465 Slog.d(TAG, "Error parsing xml.", e);
466 }
467 }
468 }
469
Matthew Williams900c67f2014-07-09 12:46:53 -0700470 private List<JobStatus> readJobMapImpl(FileInputStream fis)
471 throws XmlPullParserException, IOException {
Christopher Tate7060b042014-06-09 19:50:00 -0700472 XmlPullParser parser = Xml.newPullParser();
Wojciech Staszkiewicz4f117542015-05-08 14:58:46 +0100473 parser.setInput(fis, StandardCharsets.UTF_8.name());
Christopher Tate7060b042014-06-09 19:50:00 -0700474
475 int eventType = parser.getEventType();
476 while (eventType != XmlPullParser.START_TAG &&
477 eventType != XmlPullParser.END_DOCUMENT) {
478 eventType = parser.next();
riddle_hsu98bfb342015-07-30 21:52:58 +0800479 Slog.d(TAG, "Start tag: " + parser.getName());
Christopher Tate7060b042014-06-09 19:50:00 -0700480 }
481 if (eventType == XmlPullParser.END_DOCUMENT) {
482 if (DEBUG) {
483 Slog.d(TAG, "No persisted jobs.");
484 }
485 return null;
486 }
487
488 String tagName = parser.getName();
489 if ("job-info".equals(tagName)) {
490 final List<JobStatus> jobs = new ArrayList<JobStatus>();
491 // Read in version info.
492 try {
Narayan Kamatha09b4d22016-04-15 18:32:45 +0100493 int version = Integer.parseInt(parser.getAttributeValue(null, "version"));
Christopher Tate7060b042014-06-09 19:50:00 -0700494 if (version != JOBS_FILE_VERSION) {
495 Slog.d(TAG, "Invalid version number, aborting jobs file read.");
496 return null;
497 }
498 } catch (NumberFormatException e) {
499 Slog.e(TAG, "Invalid version number, aborting jobs file read.");
500 return null;
501 }
502 eventType = parser.next();
503 do {
504 // Read each <job/>
505 if (eventType == XmlPullParser.START_TAG) {
506 tagName = parser.getName();
507 // Start reading job.
508 if ("job".equals(tagName)) {
509 JobStatus persistedJob = restoreJobFromXml(parser);
510 if (persistedJob != null) {
511 if (DEBUG) {
512 Slog.d(TAG, "Read out " + persistedJob);
513 }
514 jobs.add(persistedJob);
515 } else {
516 Slog.d(TAG, "Error reading job from file.");
517 }
518 }
519 }
520 eventType = parser.next();
521 } while (eventType != XmlPullParser.END_DOCUMENT);
522 return jobs;
523 }
524 return null;
525 }
526
527 /**
528 * @param parser Xml parser at the beginning of a "<job/>" tag. The next "parser.next()" call
529 * will take the parser into the body of the job tag.
530 * @return Newly instantiated job holding all the information we just read out of the xml tag.
531 */
532 private JobStatus restoreJobFromXml(XmlPullParser parser) throws XmlPullParserException,
533 IOException {
534 JobInfo.Builder jobBuilder;
Shreyas Basarge8e64e2e2016-02-12 15:49:31 +0000535 int uid, sourceUserId;
Christopher Tate7060b042014-06-09 19:50:00 -0700536
Shreyas Basarge5db09082016-01-07 13:38:29 +0000537 // Read out job identifier attributes and priority.
Christopher Tate7060b042014-06-09 19:50:00 -0700538 try {
539 jobBuilder = buildBuilderFromXml(parser);
Matthew Williamsd1c06752014-08-22 14:15:28 -0700540 jobBuilder.setPersisted(true);
Narayan Kamatha09b4d22016-04-15 18:32:45 +0100541 uid = Integer.parseInt(parser.getAttributeValue(null, "uid"));
Shreyas Basarge5db09082016-01-07 13:38:29 +0000542
Shreyas Basarge968ac752016-01-11 23:09:26 +0000543 String val = parser.getAttributeValue(null, "priority");
544 if (val != null) {
Narayan Kamatha09b4d22016-04-15 18:32:45 +0100545 jobBuilder.setPriority(Integer.parseInt(val));
Shreyas Basarge5db09082016-01-07 13:38:29 +0000546 }
Jeff Sharkey1b6519b2016-04-28 15:33:18 -0600547 val = parser.getAttributeValue(null, "flags");
548 if (val != null) {
549 jobBuilder.setFlags(Integer.parseInt(val));
550 }
Shreyas Basarge968ac752016-01-11 23:09:26 +0000551 val = parser.getAttributeValue(null, "sourceUserId");
Narayan Kamatha09b4d22016-04-15 18:32:45 +0100552 sourceUserId = val == null ? -1 : Integer.parseInt(val);
Christopher Tate7060b042014-06-09 19:50:00 -0700553 } catch (NumberFormatException e) {
554 Slog.e(TAG, "Error parsing job's required fields, skipping");
555 return null;
556 }
557
Christopher Tate0213ace02016-02-24 14:18:35 -0800558 String sourcePackageName = parser.getAttributeValue(null, "sourcePackageName");
Shreyas Basarge968ac752016-01-11 23:09:26 +0000559
Dianne Hackborn1085ff62016-02-23 17:04:58 -0800560 final String sourceTag = parser.getAttributeValue(null, "sourceTag");
561
Christopher Tate7060b042014-06-09 19:50:00 -0700562 int eventType;
563 // Read out constraints tag.
564 do {
565 eventType = parser.next();
566 } while (eventType == XmlPullParser.TEXT); // Push through to next START_TAG.
567
568 if (!(eventType == XmlPullParser.START_TAG &&
569 XML_TAG_PARAMS_CONSTRAINTS.equals(parser.getName()))) {
570 // Expecting a <constraints> start tag.
571 return null;
572 }
573 try {
574 buildConstraintsFromXml(jobBuilder, parser);
575 } catch (NumberFormatException e) {
576 Slog.d(TAG, "Error reading constraints, skipping.");
577 return null;
578 }
579 parser.next(); // Consume </constraints>
580
581 // Read out execution parameters tag.
582 do {
583 eventType = parser.next();
584 } while (eventType == XmlPullParser.TEXT);
585 if (eventType != XmlPullParser.START_TAG) {
586 return null;
587 }
588
Matthew Williamsfa8e5082015-10-15 15:59:12 -0700589 // Tuple of (earliest runtime, latest runtime) in elapsed realtime after disk load.
590 Pair<Long, Long> elapsedRuntimes;
Christopher Tate7060b042014-06-09 19:50:00 -0700591 try {
Matthew Williamsfa8e5082015-10-15 15:59:12 -0700592 elapsedRuntimes = buildExecutionTimesFromXml(parser);
Christopher Tate7060b042014-06-09 19:50:00 -0700593 } catch (NumberFormatException e) {
594 if (DEBUG) {
595 Slog.d(TAG, "Error parsing execution time parameters, skipping.");
596 }
597 return null;
598 }
599
Matthew Williamsfa8e5082015-10-15 15:59:12 -0700600 final long elapsedNow = SystemClock.elapsedRealtime();
Christopher Tate7060b042014-06-09 19:50:00 -0700601 if (XML_TAG_PERIODIC.equals(parser.getName())) {
602 try {
603 String val = parser.getAttributeValue(null, "period");
Matthew Williamsfa8e5082015-10-15 15:59:12 -0700604 final long periodMillis = Long.valueOf(val);
Shreyas Basarge89ee6182015-12-17 15:16:36 +0000605 val = parser.getAttributeValue(null, "flex");
606 final long flexMillis = (val != null) ? Long.valueOf(val) : periodMillis;
Shreyas Basarge8e64e2e2016-02-12 15:49:31 +0000607 jobBuilder.setPeriodic(periodMillis, flexMillis);
Shreyas Basarge89ee6182015-12-17 15:16:36 +0000608 // As a sanity check, cap the recreated run time to be no later than flex+period
Matthew Williamsfa8e5082015-10-15 15:59:12 -0700609 // from now. This is the latest the periodic could be pushed out. This could
Shreyas Basarge89ee6182015-12-17 15:16:36 +0000610 // happen if the periodic ran early (at flex time before period), and then the
Matthew Williamsfa8e5082015-10-15 15:59:12 -0700611 // device rebooted.
Shreyas Basarge89ee6182015-12-17 15:16:36 +0000612 if (elapsedRuntimes.second > elapsedNow + periodMillis + flexMillis) {
613 final long clampedLateRuntimeElapsed = elapsedNow + flexMillis
614 + periodMillis;
615 final long clampedEarlyRuntimeElapsed = clampedLateRuntimeElapsed
616 - flexMillis;
Matthew Williamsfa8e5082015-10-15 15:59:12 -0700617 Slog.w(TAG,
618 String.format("Periodic job for uid='%d' persisted run-time is" +
619 " too big [%s, %s]. Clamping to [%s,%s]",
620 uid,
621 DateUtils.formatElapsedTime(elapsedRuntimes.first / 1000),
622 DateUtils.formatElapsedTime(elapsedRuntimes.second / 1000),
623 DateUtils.formatElapsedTime(
624 clampedEarlyRuntimeElapsed / 1000),
625 DateUtils.formatElapsedTime(
626 clampedLateRuntimeElapsed / 1000))
627 );
628 elapsedRuntimes =
629 Pair.create(clampedEarlyRuntimeElapsed, clampedLateRuntimeElapsed);
630 }
Christopher Tate7060b042014-06-09 19:50:00 -0700631 } catch (NumberFormatException e) {
632 Slog.d(TAG, "Error reading periodic execution criteria, skipping.");
633 return null;
634 }
635 } else if (XML_TAG_ONEOFF.equals(parser.getName())) {
636 try {
Matthew Williamsfa8e5082015-10-15 15:59:12 -0700637 if (elapsedRuntimes.first != JobStatus.NO_EARLIEST_RUNTIME) {
638 jobBuilder.setMinimumLatency(elapsedRuntimes.first - elapsedNow);
Christopher Tate7060b042014-06-09 19:50:00 -0700639 }
Matthew Williamsfa8e5082015-10-15 15:59:12 -0700640 if (elapsedRuntimes.second != JobStatus.NO_LATEST_RUNTIME) {
Christopher Tate7060b042014-06-09 19:50:00 -0700641 jobBuilder.setOverrideDeadline(
Matthew Williamsfa8e5082015-10-15 15:59:12 -0700642 elapsedRuntimes.second - elapsedNow);
Christopher Tate7060b042014-06-09 19:50:00 -0700643 }
644 } catch (NumberFormatException e) {
645 Slog.d(TAG, "Error reading job execution criteria, skipping.");
646 return null;
647 }
648 } else {
649 if (DEBUG) {
650 Slog.d(TAG, "Invalid parameter tag, skipping - " + parser.getName());
651 }
652 // Expecting a parameters start tag.
653 return null;
654 }
655 maybeBuildBackoffPolicyFromXml(jobBuilder, parser);
656
657 parser.nextTag(); // Consume parameters end tag.
658
659 // Read out extras Bundle.
660 do {
661 eventType = parser.next();
662 } while (eventType == XmlPullParser.TEXT);
Matthew Williamsfa8e5082015-10-15 15:59:12 -0700663 if (!(eventType == XmlPullParser.START_TAG
664 && XML_TAG_EXTRAS.equals(parser.getName()))) {
Christopher Tate7060b042014-06-09 19:50:00 -0700665 if (DEBUG) {
666 Slog.d(TAG, "Error reading extras, skipping.");
667 }
668 return null;
669 }
670
671 PersistableBundle extras = PersistableBundle.restoreFromXml(parser);
672 jobBuilder.setExtras(extras);
673 parser.nextTag(); // Consume </extras>
674
Christopher Tate0213ace02016-02-24 14:18:35 -0800675 // Migrate sync jobs forward from earlier, incomplete representation
676 if ("android".equals(sourcePackageName)
677 && extras != null
678 && extras.getBoolean("SyncManagerJob", false)) {
679 sourcePackageName = extras.getString("owningPackage", sourcePackageName);
680 if (DEBUG) {
681 Slog.i(TAG, "Fixing up sync job source package name from 'android' to '"
682 + sourcePackageName + "'");
683 }
684 }
685
686 // And now we're done
Shreyas Basarge968ac752016-01-11 23:09:26 +0000687 JobStatus js = new JobStatus(
Dianne Hackborn1085ff62016-02-23 17:04:58 -0800688 jobBuilder.build(), uid, sourcePackageName, sourceUserId, sourceTag,
Dianne Hackborn33d31c52016-02-16 10:30:33 -0800689 elapsedRuntimes.first, elapsedRuntimes.second);
Shreyas Basarge968ac752016-01-11 23:09:26 +0000690 return js;
Christopher Tate7060b042014-06-09 19:50:00 -0700691 }
692
693 private JobInfo.Builder buildBuilderFromXml(XmlPullParser parser) throws NumberFormatException {
694 // Pull out required fields from <job> attributes.
Narayan Kamatha09b4d22016-04-15 18:32:45 +0100695 int jobId = Integer.parseInt(parser.getAttributeValue(null, "jobid"));
Christopher Tate7060b042014-06-09 19:50:00 -0700696 String packageName = parser.getAttributeValue(null, "package");
697 String className = parser.getAttributeValue(null, "class");
698 ComponentName cname = new ComponentName(packageName, className);
699
700 return new JobInfo.Builder(jobId, cname);
701 }
702
703 private void buildConstraintsFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser) {
Jeff Sharkeyf07c7b92016-04-22 09:50:16 -0600704 String val = parser.getAttributeValue(null, "connectivity");
705 if (val != null) {
706 jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
707 }
708 val = parser.getAttributeValue(null, "unmetered");
Christopher Tate7060b042014-06-09 19:50:00 -0700709 if (val != null) {
Matthew Williamsd1c06752014-08-22 14:15:28 -0700710 jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED);
Christopher Tate7060b042014-06-09 19:50:00 -0700711 }
Jeff Sharkeyf07c7b92016-04-22 09:50:16 -0600712 val = parser.getAttributeValue(null, "not-roaming");
Christopher Tate7060b042014-06-09 19:50:00 -0700713 if (val != null) {
Jeff Sharkeyf07c7b92016-04-22 09:50:16 -0600714 jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_NOT_ROAMING);
Christopher Tate7060b042014-06-09 19:50:00 -0700715 }
716 val = parser.getAttributeValue(null, "idle");
717 if (val != null) {
718 jobBuilder.setRequiresDeviceIdle(true);
719 }
720 val = parser.getAttributeValue(null, "charging");
721 if (val != null) {
722 jobBuilder.setRequiresCharging(true);
723 }
724 }
725
726 /**
727 * Builds the back-off policy out of the params tag. These attributes may not exist, depending
728 * on whether the back-off was set when the job was first scheduled.
729 */
730 private void maybeBuildBackoffPolicyFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser) {
731 String val = parser.getAttributeValue(null, "initial-backoff");
732 if (val != null) {
733 long initialBackoff = Long.valueOf(val);
734 val = parser.getAttributeValue(null, "backoff-policy");
Narayan Kamatha09b4d22016-04-15 18:32:45 +0100735 int backoffPolicy = Integer.parseInt(val); // Will throw NFE which we catch higher up.
Christopher Tate7060b042014-06-09 19:50:00 -0700736 jobBuilder.setBackoffCriteria(initialBackoff, backoffPolicy);
737 }
738 }
739
740 /**
741 * Convenience function to read out and convert deadline and delay from xml into elapsed real
742 * time.
743 * @return A {@link android.util.Pair}, where the first value is the earliest elapsed runtime
744 * and the second is the latest elapsed runtime.
745 */
746 private Pair<Long, Long> buildExecutionTimesFromXml(XmlPullParser parser)
747 throws NumberFormatException {
748 // Pull out execution time data.
749 final long nowWallclock = System.currentTimeMillis();
750 final long nowElapsed = SystemClock.elapsedRealtime();
751
752 long earliestRunTimeElapsed = JobStatus.NO_EARLIEST_RUNTIME;
753 long latestRunTimeElapsed = JobStatus.NO_LATEST_RUNTIME;
754 String val = parser.getAttributeValue(null, "deadline");
755 if (val != null) {
756 long latestRuntimeWallclock = Long.valueOf(val);
757 long maxDelayElapsed =
758 Math.max(latestRuntimeWallclock - nowWallclock, 0);
759 latestRunTimeElapsed = nowElapsed + maxDelayElapsed;
760 }
761 val = parser.getAttributeValue(null, "delay");
762 if (val != null) {
763 long earliestRuntimeWallclock = Long.valueOf(val);
764 long minDelayElapsed =
765 Math.max(earliestRuntimeWallclock - nowWallclock, 0);
766 earliestRunTimeElapsed = nowElapsed + minDelayElapsed;
767
768 }
769 return Pair.create(earliestRunTimeElapsed, latestRunTimeElapsed);
770 }
771 }
Christopher Tate2f36fd62016-02-18 18:36:08 -0800772
773 static class JobSet {
774 // Key is the getUid() originator of the jobs in each sheaf
775 private SparseArray<ArraySet<JobStatus>> mJobs;
776
777 public JobSet() {
778 mJobs = new SparseArray<ArraySet<JobStatus>>();
779 }
780
781 public List<JobStatus> getJobsByUid(int uid) {
782 ArrayList<JobStatus> matchingJobs = new ArrayList<JobStatus>();
783 ArraySet<JobStatus> jobs = mJobs.get(uid);
784 if (jobs != null) {
785 matchingJobs.addAll(jobs);
786 }
787 return matchingJobs;
788 }
789
790 // By user, not by uid, so we need to traverse by key and check
791 public List<JobStatus> getJobsByUser(int userId) {
792 ArrayList<JobStatus> result = new ArrayList<JobStatus>();
793 for (int i = mJobs.size() - 1; i >= 0; i--) {
794 if (UserHandle.getUserId(mJobs.keyAt(i)) == userId) {
795 ArraySet<JobStatus> jobs = mJobs.get(i);
796 if (jobs != null) {
797 result.addAll(jobs);
798 }
799 }
800 }
801 return result;
802 }
803
804 public boolean add(JobStatus job) {
805 final int uid = job.getUid();
806 ArraySet<JobStatus> jobs = mJobs.get(uid);
807 if (jobs == null) {
808 jobs = new ArraySet<JobStatus>();
809 mJobs.put(uid, jobs);
810 }
811 return jobs.add(job);
812 }
813
814 public boolean remove(JobStatus job) {
815 final int uid = job.getUid();
816 ArraySet<JobStatus> jobs = mJobs.get(uid);
817 boolean didRemove = (jobs != null) ? jobs.remove(job) : false;
818 if (didRemove && jobs.size() == 0) {
819 // no more jobs for this uid; let the now-empty set object be GC'd.
820 mJobs.remove(uid);
821 }
822 return didRemove;
823 }
824
825 public boolean contains(JobStatus job) {
826 final int uid = job.getUid();
827 ArraySet<JobStatus> jobs = mJobs.get(uid);
828 return jobs != null && jobs.contains(job);
829 }
830
831 public JobStatus get(int uid, int jobId) {
832 ArraySet<JobStatus> jobs = mJobs.get(uid);
833 if (jobs != null) {
834 for (int i = jobs.size() - 1; i >= 0; i--) {
835 JobStatus job = jobs.valueAt(i);
836 if (job.getJobId() == jobId) {
837 return job;
838 }
839 }
840 }
841 return null;
842 }
843
844 // Inefficient; use only for testing
845 public List<JobStatus> getAllJobs() {
846 ArrayList<JobStatus> allJobs = new ArrayList<JobStatus>(size());
Dianne Hackborne9a988c2016-05-27 17:59:40 -0700847 for (int i = mJobs.size() - 1; i >= 0; i--) {
848 ArraySet<JobStatus> jobs = mJobs.valueAt(i);
849 if (jobs != null) {
850 // Use a for loop over the ArraySet, so we don't need to make its
851 // optional collection class iterator implementation or have to go
852 // through a temporary array from toArray().
853 for (int j = jobs.size() - 1; j >= 0; j--) {
854 allJobs.add(jobs.valueAt(j));
855 }
856 }
Christopher Tate2f36fd62016-02-18 18:36:08 -0800857 }
858 return allJobs;
859 }
860
861 public void clear() {
862 mJobs.clear();
863 }
864
865 public int size() {
866 int total = 0;
867 for (int i = mJobs.size() - 1; i >= 0; i--) {
868 total += mJobs.valueAt(i).size();
869 }
870 return total;
871 }
872
873 // We only want to count the jobs that this uid has scheduled on its own
874 // behalf, not those that the app has scheduled on someone else's behalf.
875 public int countJobsForUid(int uid) {
876 int total = 0;
877 ArraySet<JobStatus> jobs = mJobs.get(uid);
878 if (jobs != null) {
879 for (int i = jobs.size() - 1; i >= 0; i--) {
880 JobStatus job = jobs.valueAt(i);
881 if (job.getUid() == job.getSourceUid()) {
882 total++;
883 }
884 }
885 }
886 return total;
887 }
888
889 public void forEachJob(JobStatusFunctor functor) {
890 for (int uidIndex = mJobs.size() - 1; uidIndex >= 0; uidIndex--) {
891 ArraySet<JobStatus> jobs = mJobs.valueAt(uidIndex);
892 for (int i = jobs.size() - 1; i >= 0; i--) {
893 functor.process(jobs.valueAt(i));
894 }
895 }
896 }
Shreyas Basargecbf5ae92016-03-08 16:13:06 +0000897
898 public void forEachJob(int uid, JobStatusFunctor functor) {
899 ArraySet<JobStatus> jobs = mJobs.get(uid);
900 if (jobs != null) {
901 for (int i = jobs.size() - 1; i >= 0; i--) {
902 functor.process(jobs.valueAt(i));
903 }
904 }
905 }
Christopher Tate2f36fd62016-02-18 18:36:08 -0800906 }
Matthew Williams01ac45b2014-07-22 20:44:12 -0700907}