blob: 4268dab01a92d32216d79bf2adfe8596835bc4be [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
213 public interface JobStatusFunctor {
214 public void process(JobStatus jobStatus);
Christopher Tate7060b042014-06-09 19:50:00 -0700215 }
216
217 /** Version of the db schema. */
218 private static final int JOBS_FILE_VERSION = 0;
219 /** Tag corresponds to constraints this job needs. */
220 private static final String XML_TAG_PARAMS_CONSTRAINTS = "constraints";
221 /** Tag corresponds to execution parameters. */
222 private static final String XML_TAG_PERIODIC = "periodic";
223 private static final String XML_TAG_ONEOFF = "one-off";
224 private static final String XML_TAG_EXTRAS = "extras";
225
226 /**
227 * Every time the state changes we write all the jobs in one swath, instead of trying to
228 * track incremental changes.
229 * @return Whether the operation was successful. This will only fail for e.g. if the system is
230 * low on storage. If this happens, we continue as normal
231 */
232 private void maybeWriteStatusToDiskAsync() {
233 mDirtyOperations++;
234 if (mDirtyOperations >= MAX_OPS_BEFORE_WRITE) {
235 if (DEBUG) {
236 Slog.v(TAG, "Writing jobs to disk.");
237 }
238 mIoHandler.post(new WriteJobsMapToDiskRunnable());
239 }
240 }
241
Matthew Williams01ac45b2014-07-22 20:44:12 -0700242 @VisibleForTesting
Christopher Tate2f36fd62016-02-18 18:36:08 -0800243 public void readJobMapFromDisk(JobSet jobSet) {
Matthew Williams01ac45b2014-07-22 20:44:12 -0700244 new ReadJobMapFromDiskRunnable(jobSet).run();
Christopher Tate7060b042014-06-09 19:50:00 -0700245 }
246
247 /**
248 * Runnable that writes {@link #mJobSet} out to xml.
Dianne Hackborn33d31c52016-02-16 10:30:33 -0800249 * NOTE: This Runnable locks on mLock
Christopher Tate7060b042014-06-09 19:50:00 -0700250 */
251 private class WriteJobsMapToDiskRunnable implements Runnable {
252 @Override
253 public void run() {
254 final long startElapsed = SystemClock.elapsedRealtime();
Christopher Tate2f36fd62016-02-18 18:36:08 -0800255 final List<JobStatus> storeCopy = new ArrayList<JobStatus>();
Dianne Hackborn33d31c52016-02-16 10:30:33 -0800256 synchronized (mLock) {
Christopher Tate2f36fd62016-02-18 18:36:08 -0800257 // Clone the jobs so we can release the lock before writing.
258 mJobSet.forEachJob(new JobStatusFunctor() {
259 @Override
260 public void process(JobStatus job) {
261 if (job.isPersisted()) {
262 storeCopy.add(new JobStatus(job));
263 }
Shreyas Basarge7ef490f2015-12-03 16:45:22 +0000264 }
Christopher Tate2f36fd62016-02-18 18:36:08 -0800265 });
Christopher Tate7060b042014-06-09 19:50:00 -0700266 }
Christopher Tate2f36fd62016-02-18 18:36:08 -0800267 writeJobsMapImpl(storeCopy);
Christopher Tate7060b042014-06-09 19:50:00 -0700268 if (JobSchedulerService.DEBUG) {
269 Slog.v(TAG, "Finished writing, took " + (SystemClock.elapsedRealtime()
270 - startElapsed) + "ms");
271 }
272 }
273
Matthew Williams49a85b62014-06-12 11:02:34 -0700274 private void writeJobsMapImpl(List<JobStatus> jobList) {
Christopher Tate7060b042014-06-09 19:50:00 -0700275 try {
276 ByteArrayOutputStream baos = new ByteArrayOutputStream();
277 XmlSerializer out = new FastXmlSerializer();
Wojciech Staszkiewicz4f117542015-05-08 14:58:46 +0100278 out.setOutput(baos, StandardCharsets.UTF_8.name());
Christopher Tate7060b042014-06-09 19:50:00 -0700279 out.startDocument(null, true);
280 out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
281
282 out.startTag(null, "job-info");
283 out.attribute(null, "version", Integer.toString(JOBS_FILE_VERSION));
Dianne Hackbornfdb19562014-07-11 16:03:36 -0700284 for (int i=0; i<jobList.size(); i++) {
285 JobStatus jobStatus = jobList.get(i);
Christopher Tate7060b042014-06-09 19:50:00 -0700286 if (DEBUG) {
287 Slog.d(TAG, "Saving job " + jobStatus.getJobId());
288 }
289 out.startTag(null, "job");
Shreyas Basarge5db09082016-01-07 13:38:29 +0000290 addAttributesToJobTag(out, jobStatus);
Christopher Tate7060b042014-06-09 19:50:00 -0700291 writeConstraintsToXml(out, jobStatus);
292 writeExecutionCriteriaToXml(out, jobStatus);
293 writeBundleToXml(jobStatus.getExtras(), out);
294 out.endTag(null, "job");
295 }
296 out.endTag(null, "job-info");
297 out.endDocument();
298
299 // Write out to disk in one fell sweep.
300 FileOutputStream fos = mJobsFile.startWrite();
301 fos.write(baos.toByteArray());
302 mJobsFile.finishWrite(fos);
303 mDirtyOperations = 0;
304 } catch (IOException e) {
305 if (DEBUG) {
306 Slog.v(TAG, "Error writing out job data.", e);
307 }
308 } catch (XmlPullParserException e) {
309 if (DEBUG) {
310 Slog.d(TAG, "Error persisting bundle.", e);
311 }
312 }
313 }
314
Shreyas Basarge5db09082016-01-07 13:38:29 +0000315 /** Write out a tag with data comprising the required fields and priority of this job and
316 * its client.
317 */
318 private void addAttributesToJobTag(XmlSerializer out, JobStatus jobStatus)
Christopher Tate7060b042014-06-09 19:50:00 -0700319 throws IOException {
320 out.attribute(null, "jobid", Integer.toString(jobStatus.getJobId()));
321 out.attribute(null, "package", jobStatus.getServiceComponent().getPackageName());
322 out.attribute(null, "class", jobStatus.getServiceComponent().getClassName());
Shreyas Basarge968ac752016-01-11 23:09:26 +0000323 if (jobStatus.getSourcePackageName() != null) {
324 out.attribute(null, "sourcePackageName", jobStatus.getSourcePackageName());
325 }
Dianne Hackborn1085ff62016-02-23 17:04:58 -0800326 if (jobStatus.getSourceTag() != null) {
327 out.attribute(null, "sourceTag", jobStatus.getSourceTag());
328 }
Shreyas Basarge968ac752016-01-11 23:09:26 +0000329 out.attribute(null, "sourceUserId", String.valueOf(jobStatus.getSourceUserId()));
Christopher Tate7060b042014-06-09 19:50:00 -0700330 out.attribute(null, "uid", Integer.toString(jobStatus.getUid()));
Shreyas Basarge5db09082016-01-07 13:38:29 +0000331 out.attribute(null, "priority", String.valueOf(jobStatus.getPriority()));
Christopher Tate7060b042014-06-09 19:50:00 -0700332 }
333
334 private void writeBundleToXml(PersistableBundle extras, XmlSerializer out)
335 throws IOException, XmlPullParserException {
336 out.startTag(null, XML_TAG_EXTRAS);
Shreyas Basarged09973b2015-12-16 18:10:05 +0000337 PersistableBundle extrasCopy = deepCopyBundle(extras, 10);
338 extrasCopy.saveToXml(out);
Christopher Tate7060b042014-06-09 19:50:00 -0700339 out.endTag(null, XML_TAG_EXTRAS);
340 }
Shreyas Basarged09973b2015-12-16 18:10:05 +0000341
342 private PersistableBundle deepCopyBundle(PersistableBundle bundle, int maxDepth) {
343 if (maxDepth <= 0) {
344 return null;
345 }
346 PersistableBundle copy = (PersistableBundle) bundle.clone();
347 Set<String> keySet = bundle.keySet();
348 for (String key: keySet) {
Shreyas Basarge5db09082016-01-07 13:38:29 +0000349 Object o = copy.get(key);
350 if (o instanceof PersistableBundle) {
351 PersistableBundle bCopy = deepCopyBundle((PersistableBundle) o, maxDepth-1);
Shreyas Basarged09973b2015-12-16 18:10:05 +0000352 copy.putPersistableBundle(key, bCopy);
353 }
354 }
355 return copy;
356 }
357
Christopher Tate7060b042014-06-09 19:50:00 -0700358 /**
359 * Write out a tag with data identifying this job's constraints. If the constraint isn't here
360 * it doesn't apply.
361 */
362 private void writeConstraintsToXml(XmlSerializer out, JobStatus jobStatus) throws IOException {
363 out.startTag(null, XML_TAG_PARAMS_CONSTRAINTS);
364 if (jobStatus.hasUnmeteredConstraint()) {
365 out.attribute(null, "unmetered", Boolean.toString(true));
366 }
367 if (jobStatus.hasConnectivityConstraint()) {
368 out.attribute(null, "connectivity", Boolean.toString(true));
369 }
370 if (jobStatus.hasIdleConstraint()) {
371 out.attribute(null, "idle", Boolean.toString(true));
372 }
373 if (jobStatus.hasChargingConstraint()) {
374 out.attribute(null, "charging", Boolean.toString(true));
375 }
376 out.endTag(null, XML_TAG_PARAMS_CONSTRAINTS);
377 }
378
379 private void writeExecutionCriteriaToXml(XmlSerializer out, JobStatus jobStatus)
380 throws IOException {
381 final JobInfo job = jobStatus.getJob();
382 if (jobStatus.getJob().isPeriodic()) {
383 out.startTag(null, XML_TAG_PERIODIC);
384 out.attribute(null, "period", Long.toString(job.getIntervalMillis()));
Shreyas Basarge89ee6182015-12-17 15:16:36 +0000385 out.attribute(null, "flex", Long.toString(job.getFlexMillis()));
Christopher Tate7060b042014-06-09 19:50:00 -0700386 } else {
387 out.startTag(null, XML_TAG_ONEOFF);
388 }
389
390 if (jobStatus.hasDeadlineConstraint()) {
391 // Wall clock deadline.
392 final long deadlineWallclock = System.currentTimeMillis() +
393 (jobStatus.getLatestRunTimeElapsed() - SystemClock.elapsedRealtime());
394 out.attribute(null, "deadline", Long.toString(deadlineWallclock));
395 }
396 if (jobStatus.hasTimingDelayConstraint()) {
397 final long delayWallclock = System.currentTimeMillis() +
398 (jobStatus.getEarliestRunTime() - SystemClock.elapsedRealtime());
399 out.attribute(null, "delay", Long.toString(delayWallclock));
400 }
401
402 // Only write out back-off policy if it differs from the default.
403 // This also helps the case where the job is idle -> these aren't allowed to specify
404 // back-off.
405 if (jobStatus.getJob().getInitialBackoffMillis() != JobInfo.DEFAULT_INITIAL_BACKOFF_MILLIS
406 || jobStatus.getJob().getBackoffPolicy() != JobInfo.DEFAULT_BACKOFF_POLICY) {
407 out.attribute(null, "backoff-policy", Integer.toString(job.getBackoffPolicy()));
408 out.attribute(null, "initial-backoff", Long.toString(job.getInitialBackoffMillis()));
409 }
410 if (job.isPeriodic()) {
411 out.endTag(null, XML_TAG_PERIODIC);
412 } else {
413 out.endTag(null, XML_TAG_ONEOFF);
414 }
415 }
416 }
417
418 /**
Matthew Williams01ac45b2014-07-22 20:44:12 -0700419 * Runnable that reads list of persisted job from xml. This is run once at start up, so doesn't
420 * need to go through {@link JobStore#add(com.android.server.job.controllers.JobStatus)}.
Christopher Tate7060b042014-06-09 19:50:00 -0700421 */
422 private class ReadJobMapFromDiskRunnable implements Runnable {
Christopher Tate2f36fd62016-02-18 18:36:08 -0800423 private final JobSet jobSet;
Matthew Williams01ac45b2014-07-22 20:44:12 -0700424
425 /**
426 * @param jobSet Reference to the (empty) set of JobStatus objects that back the JobStore,
427 * so that after disk read we can populate it directly.
428 */
Christopher Tate2f36fd62016-02-18 18:36:08 -0800429 ReadJobMapFromDiskRunnable(JobSet jobSet) {
Matthew Williams01ac45b2014-07-22 20:44:12 -0700430 this.jobSet = jobSet;
Christopher Tate7060b042014-06-09 19:50:00 -0700431 }
432
433 @Override
434 public void run() {
435 try {
436 List<JobStatus> jobs;
437 FileInputStream fis = mJobsFile.openRead();
Dianne Hackborn33d31c52016-02-16 10:30:33 -0800438 synchronized (mLock) {
Christopher Tate7060b042014-06-09 19:50:00 -0700439 jobs = readJobMapImpl(fis);
Matthew Williams01ac45b2014-07-22 20:44:12 -0700440 if (jobs != null) {
441 for (int i=0; i<jobs.size(); i++) {
442 this.jobSet.add(jobs.get(i));
443 }
444 }
Christopher Tate7060b042014-06-09 19:50:00 -0700445 }
446 fis.close();
Christopher Tate7060b042014-06-09 19:50:00 -0700447 } catch (FileNotFoundException e) {
448 if (JobSchedulerService.DEBUG) {
449 Slog.d(TAG, "Could not find jobs file, probably there was nothing to load.");
450 }
451 } catch (XmlPullParserException e) {
452 if (JobSchedulerService.DEBUG) {
453 Slog.d(TAG, "Error parsing xml.", e);
454 }
455 } catch (IOException e) {
456 if (JobSchedulerService.DEBUG) {
457 Slog.d(TAG, "Error parsing xml.", e);
458 }
459 }
460 }
461
Matthew Williams900c67f2014-07-09 12:46:53 -0700462 private List<JobStatus> readJobMapImpl(FileInputStream fis)
463 throws XmlPullParserException, IOException {
Christopher Tate7060b042014-06-09 19:50:00 -0700464 XmlPullParser parser = Xml.newPullParser();
Wojciech Staszkiewicz4f117542015-05-08 14:58:46 +0100465 parser.setInput(fis, StandardCharsets.UTF_8.name());
Christopher Tate7060b042014-06-09 19:50:00 -0700466
467 int eventType = parser.getEventType();
468 while (eventType != XmlPullParser.START_TAG &&
469 eventType != XmlPullParser.END_DOCUMENT) {
470 eventType = parser.next();
riddle_hsu98bfb342015-07-30 21:52:58 +0800471 Slog.d(TAG, "Start tag: " + parser.getName());
Christopher Tate7060b042014-06-09 19:50:00 -0700472 }
473 if (eventType == XmlPullParser.END_DOCUMENT) {
474 if (DEBUG) {
475 Slog.d(TAG, "No persisted jobs.");
476 }
477 return null;
478 }
479
480 String tagName = parser.getName();
481 if ("job-info".equals(tagName)) {
482 final List<JobStatus> jobs = new ArrayList<JobStatus>();
483 // Read in version info.
484 try {
485 int version = Integer.valueOf(parser.getAttributeValue(null, "version"));
486 if (version != JOBS_FILE_VERSION) {
487 Slog.d(TAG, "Invalid version number, aborting jobs file read.");
488 return null;
489 }
490 } catch (NumberFormatException e) {
491 Slog.e(TAG, "Invalid version number, aborting jobs file read.");
492 return null;
493 }
494 eventType = parser.next();
495 do {
496 // Read each <job/>
497 if (eventType == XmlPullParser.START_TAG) {
498 tagName = parser.getName();
499 // Start reading job.
500 if ("job".equals(tagName)) {
501 JobStatus persistedJob = restoreJobFromXml(parser);
502 if (persistedJob != null) {
503 if (DEBUG) {
504 Slog.d(TAG, "Read out " + persistedJob);
505 }
506 jobs.add(persistedJob);
507 } else {
508 Slog.d(TAG, "Error reading job from file.");
509 }
510 }
511 }
512 eventType = parser.next();
513 } while (eventType != XmlPullParser.END_DOCUMENT);
514 return jobs;
515 }
516 return null;
517 }
518
519 /**
520 * @param parser Xml parser at the beginning of a "<job/>" tag. The next "parser.next()" call
521 * will take the parser into the body of the job tag.
522 * @return Newly instantiated job holding all the information we just read out of the xml tag.
523 */
524 private JobStatus restoreJobFromXml(XmlPullParser parser) throws XmlPullParserException,
525 IOException {
526 JobInfo.Builder jobBuilder;
Shreyas Basarge8e64e2e2016-02-12 15:49:31 +0000527 int uid, sourceUserId;
Christopher Tate7060b042014-06-09 19:50:00 -0700528
Shreyas Basarge5db09082016-01-07 13:38:29 +0000529 // Read out job identifier attributes and priority.
Christopher Tate7060b042014-06-09 19:50:00 -0700530 try {
531 jobBuilder = buildBuilderFromXml(parser);
Matthew Williamsd1c06752014-08-22 14:15:28 -0700532 jobBuilder.setPersisted(true);
Christopher Tate7060b042014-06-09 19:50:00 -0700533 uid = Integer.valueOf(parser.getAttributeValue(null, "uid"));
Shreyas Basarge5db09082016-01-07 13:38:29 +0000534
Shreyas Basarge968ac752016-01-11 23:09:26 +0000535 String val = parser.getAttributeValue(null, "priority");
536 if (val != null) {
537 jobBuilder.setPriority(Integer.valueOf(val));
Shreyas Basarge5db09082016-01-07 13:38:29 +0000538 }
Shreyas Basarge968ac752016-01-11 23:09:26 +0000539 val = parser.getAttributeValue(null, "sourceUserId");
Shreyas Basarge8e64e2e2016-02-12 15:49:31 +0000540 sourceUserId = val == null ? -1 : Integer.valueOf(val);
Christopher Tate7060b042014-06-09 19:50:00 -0700541 } catch (NumberFormatException e) {
542 Slog.e(TAG, "Error parsing job's required fields, skipping");
543 return null;
544 }
545
Shreyas Basarge968ac752016-01-11 23:09:26 +0000546 final String sourcePackageName = parser.getAttributeValue(null, "sourcePackageName");
547
Dianne Hackborn1085ff62016-02-23 17:04:58 -0800548 final String sourceTag = parser.getAttributeValue(null, "sourceTag");
549
Christopher Tate7060b042014-06-09 19:50:00 -0700550 int eventType;
551 // Read out constraints tag.
552 do {
553 eventType = parser.next();
554 } while (eventType == XmlPullParser.TEXT); // Push through to next START_TAG.
555
556 if (!(eventType == XmlPullParser.START_TAG &&
557 XML_TAG_PARAMS_CONSTRAINTS.equals(parser.getName()))) {
558 // Expecting a <constraints> start tag.
559 return null;
560 }
561 try {
562 buildConstraintsFromXml(jobBuilder, parser);
563 } catch (NumberFormatException e) {
564 Slog.d(TAG, "Error reading constraints, skipping.");
565 return null;
566 }
567 parser.next(); // Consume </constraints>
568
569 // Read out execution parameters tag.
570 do {
571 eventType = parser.next();
572 } while (eventType == XmlPullParser.TEXT);
573 if (eventType != XmlPullParser.START_TAG) {
574 return null;
575 }
576
Matthew Williamsfa8e5082015-10-15 15:59:12 -0700577 // Tuple of (earliest runtime, latest runtime) in elapsed realtime after disk load.
578 Pair<Long, Long> elapsedRuntimes;
Christopher Tate7060b042014-06-09 19:50:00 -0700579 try {
Matthew Williamsfa8e5082015-10-15 15:59:12 -0700580 elapsedRuntimes = buildExecutionTimesFromXml(parser);
Christopher Tate7060b042014-06-09 19:50:00 -0700581 } catch (NumberFormatException e) {
582 if (DEBUG) {
583 Slog.d(TAG, "Error parsing execution time parameters, skipping.");
584 }
585 return null;
586 }
587
Matthew Williamsfa8e5082015-10-15 15:59:12 -0700588 final long elapsedNow = SystemClock.elapsedRealtime();
Christopher Tate7060b042014-06-09 19:50:00 -0700589 if (XML_TAG_PERIODIC.equals(parser.getName())) {
590 try {
591 String val = parser.getAttributeValue(null, "period");
Matthew Williamsfa8e5082015-10-15 15:59:12 -0700592 final long periodMillis = Long.valueOf(val);
Shreyas Basarge89ee6182015-12-17 15:16:36 +0000593 val = parser.getAttributeValue(null, "flex");
594 final long flexMillis = (val != null) ? Long.valueOf(val) : periodMillis;
Shreyas Basarge8e64e2e2016-02-12 15:49:31 +0000595 jobBuilder.setPeriodic(periodMillis, flexMillis);
Shreyas Basarge89ee6182015-12-17 15:16:36 +0000596 // As a sanity check, cap the recreated run time to be no later than flex+period
Matthew Williamsfa8e5082015-10-15 15:59:12 -0700597 // from now. This is the latest the periodic could be pushed out. This could
Shreyas Basarge89ee6182015-12-17 15:16:36 +0000598 // happen if the periodic ran early (at flex time before period), and then the
Matthew Williamsfa8e5082015-10-15 15:59:12 -0700599 // device rebooted.
Shreyas Basarge89ee6182015-12-17 15:16:36 +0000600 if (elapsedRuntimes.second > elapsedNow + periodMillis + flexMillis) {
601 final long clampedLateRuntimeElapsed = elapsedNow + flexMillis
602 + periodMillis;
603 final long clampedEarlyRuntimeElapsed = clampedLateRuntimeElapsed
604 - flexMillis;
Matthew Williamsfa8e5082015-10-15 15:59:12 -0700605 Slog.w(TAG,
606 String.format("Periodic job for uid='%d' persisted run-time is" +
607 " too big [%s, %s]. Clamping to [%s,%s]",
608 uid,
609 DateUtils.formatElapsedTime(elapsedRuntimes.first / 1000),
610 DateUtils.formatElapsedTime(elapsedRuntimes.second / 1000),
611 DateUtils.formatElapsedTime(
612 clampedEarlyRuntimeElapsed / 1000),
613 DateUtils.formatElapsedTime(
614 clampedLateRuntimeElapsed / 1000))
615 );
616 elapsedRuntimes =
617 Pair.create(clampedEarlyRuntimeElapsed, clampedLateRuntimeElapsed);
618 }
Christopher Tate7060b042014-06-09 19:50:00 -0700619 } catch (NumberFormatException e) {
620 Slog.d(TAG, "Error reading periodic execution criteria, skipping.");
621 return null;
622 }
623 } else if (XML_TAG_ONEOFF.equals(parser.getName())) {
624 try {
Matthew Williamsfa8e5082015-10-15 15:59:12 -0700625 if (elapsedRuntimes.first != JobStatus.NO_EARLIEST_RUNTIME) {
626 jobBuilder.setMinimumLatency(elapsedRuntimes.first - elapsedNow);
Christopher Tate7060b042014-06-09 19:50:00 -0700627 }
Matthew Williamsfa8e5082015-10-15 15:59:12 -0700628 if (elapsedRuntimes.second != JobStatus.NO_LATEST_RUNTIME) {
Christopher Tate7060b042014-06-09 19:50:00 -0700629 jobBuilder.setOverrideDeadline(
Matthew Williamsfa8e5082015-10-15 15:59:12 -0700630 elapsedRuntimes.second - elapsedNow);
Christopher Tate7060b042014-06-09 19:50:00 -0700631 }
632 } catch (NumberFormatException e) {
633 Slog.d(TAG, "Error reading job execution criteria, skipping.");
634 return null;
635 }
636 } else {
637 if (DEBUG) {
638 Slog.d(TAG, "Invalid parameter tag, skipping - " + parser.getName());
639 }
640 // Expecting a parameters start tag.
641 return null;
642 }
643 maybeBuildBackoffPolicyFromXml(jobBuilder, parser);
644
645 parser.nextTag(); // Consume parameters end tag.
646
647 // Read out extras Bundle.
648 do {
649 eventType = parser.next();
650 } while (eventType == XmlPullParser.TEXT);
Matthew Williamsfa8e5082015-10-15 15:59:12 -0700651 if (!(eventType == XmlPullParser.START_TAG
652 && XML_TAG_EXTRAS.equals(parser.getName()))) {
Christopher Tate7060b042014-06-09 19:50:00 -0700653 if (DEBUG) {
654 Slog.d(TAG, "Error reading extras, skipping.");
655 }
656 return null;
657 }
658
659 PersistableBundle extras = PersistableBundle.restoreFromXml(parser);
660 jobBuilder.setExtras(extras);
661 parser.nextTag(); // Consume </extras>
662
Shreyas Basarge968ac752016-01-11 23:09:26 +0000663 JobStatus js = new JobStatus(
Dianne Hackborn1085ff62016-02-23 17:04:58 -0800664 jobBuilder.build(), uid, sourcePackageName, sourceUserId, sourceTag,
Dianne Hackborn33d31c52016-02-16 10:30:33 -0800665 elapsedRuntimes.first, elapsedRuntimes.second);
Shreyas Basarge968ac752016-01-11 23:09:26 +0000666 return js;
Christopher Tate7060b042014-06-09 19:50:00 -0700667 }
668
669 private JobInfo.Builder buildBuilderFromXml(XmlPullParser parser) throws NumberFormatException {
670 // Pull out required fields from <job> attributes.
671 int jobId = Integer.valueOf(parser.getAttributeValue(null, "jobid"));
672 String packageName = parser.getAttributeValue(null, "package");
673 String className = parser.getAttributeValue(null, "class");
674 ComponentName cname = new ComponentName(packageName, className);
675
676 return new JobInfo.Builder(jobId, cname);
677 }
678
679 private void buildConstraintsFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser) {
680 String val = parser.getAttributeValue(null, "unmetered");
681 if (val != null) {
Matthew Williamsd1c06752014-08-22 14:15:28 -0700682 jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED);
Christopher Tate7060b042014-06-09 19:50:00 -0700683 }
684 val = parser.getAttributeValue(null, "connectivity");
685 if (val != null) {
Matthew Williamsd1c06752014-08-22 14:15:28 -0700686 jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
Christopher Tate7060b042014-06-09 19:50:00 -0700687 }
688 val = parser.getAttributeValue(null, "idle");
689 if (val != null) {
690 jobBuilder.setRequiresDeviceIdle(true);
691 }
692 val = parser.getAttributeValue(null, "charging");
693 if (val != null) {
694 jobBuilder.setRequiresCharging(true);
695 }
696 }
697
698 /**
699 * Builds the back-off policy out of the params tag. These attributes may not exist, depending
700 * on whether the back-off was set when the job was first scheduled.
701 */
702 private void maybeBuildBackoffPolicyFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser) {
703 String val = parser.getAttributeValue(null, "initial-backoff");
704 if (val != null) {
705 long initialBackoff = Long.valueOf(val);
706 val = parser.getAttributeValue(null, "backoff-policy");
707 int backoffPolicy = Integer.valueOf(val); // Will throw NFE which we catch higher up.
708 jobBuilder.setBackoffCriteria(initialBackoff, backoffPolicy);
709 }
710 }
711
712 /**
713 * Convenience function to read out and convert deadline and delay from xml into elapsed real
714 * time.
715 * @return A {@link android.util.Pair}, where the first value is the earliest elapsed runtime
716 * and the second is the latest elapsed runtime.
717 */
718 private Pair<Long, Long> buildExecutionTimesFromXml(XmlPullParser parser)
719 throws NumberFormatException {
720 // Pull out execution time data.
721 final long nowWallclock = System.currentTimeMillis();
722 final long nowElapsed = SystemClock.elapsedRealtime();
723
724 long earliestRunTimeElapsed = JobStatus.NO_EARLIEST_RUNTIME;
725 long latestRunTimeElapsed = JobStatus.NO_LATEST_RUNTIME;
726 String val = parser.getAttributeValue(null, "deadline");
727 if (val != null) {
728 long latestRuntimeWallclock = Long.valueOf(val);
729 long maxDelayElapsed =
730 Math.max(latestRuntimeWallclock - nowWallclock, 0);
731 latestRunTimeElapsed = nowElapsed + maxDelayElapsed;
732 }
733 val = parser.getAttributeValue(null, "delay");
734 if (val != null) {
735 long earliestRuntimeWallclock = Long.valueOf(val);
736 long minDelayElapsed =
737 Math.max(earliestRuntimeWallclock - nowWallclock, 0);
738 earliestRunTimeElapsed = nowElapsed + minDelayElapsed;
739
740 }
741 return Pair.create(earliestRunTimeElapsed, latestRunTimeElapsed);
742 }
743 }
Christopher Tate2f36fd62016-02-18 18:36:08 -0800744
745 static class JobSet {
746 // Key is the getUid() originator of the jobs in each sheaf
747 private SparseArray<ArraySet<JobStatus>> mJobs;
748
749 public JobSet() {
750 mJobs = new SparseArray<ArraySet<JobStatus>>();
751 }
752
753 public List<JobStatus> getJobsByUid(int uid) {
754 ArrayList<JobStatus> matchingJobs = new ArrayList<JobStatus>();
755 ArraySet<JobStatus> jobs = mJobs.get(uid);
756 if (jobs != null) {
757 matchingJobs.addAll(jobs);
758 }
759 return matchingJobs;
760 }
761
762 // By user, not by uid, so we need to traverse by key and check
763 public List<JobStatus> getJobsByUser(int userId) {
764 ArrayList<JobStatus> result = new ArrayList<JobStatus>();
765 for (int i = mJobs.size() - 1; i >= 0; i--) {
766 if (UserHandle.getUserId(mJobs.keyAt(i)) == userId) {
767 ArraySet<JobStatus> jobs = mJobs.get(i);
768 if (jobs != null) {
769 result.addAll(jobs);
770 }
771 }
772 }
773 return result;
774 }
775
776 public boolean add(JobStatus job) {
777 final int uid = job.getUid();
778 ArraySet<JobStatus> jobs = mJobs.get(uid);
779 if (jobs == null) {
780 jobs = new ArraySet<JobStatus>();
781 mJobs.put(uid, jobs);
782 }
783 return jobs.add(job);
784 }
785
786 public boolean remove(JobStatus job) {
787 final int uid = job.getUid();
788 ArraySet<JobStatus> jobs = mJobs.get(uid);
789 boolean didRemove = (jobs != null) ? jobs.remove(job) : false;
790 if (didRemove && jobs.size() == 0) {
791 // no more jobs for this uid; let the now-empty set object be GC'd.
792 mJobs.remove(uid);
793 }
794 return didRemove;
795 }
796
797 public boolean contains(JobStatus job) {
798 final int uid = job.getUid();
799 ArraySet<JobStatus> jobs = mJobs.get(uid);
800 return jobs != null && jobs.contains(job);
801 }
802
803 public JobStatus get(int uid, int jobId) {
804 ArraySet<JobStatus> jobs = mJobs.get(uid);
805 if (jobs != null) {
806 for (int i = jobs.size() - 1; i >= 0; i--) {
807 JobStatus job = jobs.valueAt(i);
808 if (job.getJobId() == jobId) {
809 return job;
810 }
811 }
812 }
813 return null;
814 }
815
816 // Inefficient; use only for testing
817 public List<JobStatus> getAllJobs() {
818 ArrayList<JobStatus> allJobs = new ArrayList<JobStatus>(size());
819 for (int i = mJobs.size(); i >= 0; i--) {
820 allJobs.addAll(mJobs.valueAt(i));
821 }
822 return allJobs;
823 }
824
825 public void clear() {
826 mJobs.clear();
827 }
828
829 public int size() {
830 int total = 0;
831 for (int i = mJobs.size() - 1; i >= 0; i--) {
832 total += mJobs.valueAt(i).size();
833 }
834 return total;
835 }
836
837 // We only want to count the jobs that this uid has scheduled on its own
838 // behalf, not those that the app has scheduled on someone else's behalf.
839 public int countJobsForUid(int uid) {
840 int total = 0;
841 ArraySet<JobStatus> jobs = mJobs.get(uid);
842 if (jobs != null) {
843 for (int i = jobs.size() - 1; i >= 0; i--) {
844 JobStatus job = jobs.valueAt(i);
845 if (job.getUid() == job.getSourceUid()) {
846 total++;
847 }
848 }
849 }
850 return total;
851 }
852
853 public void forEachJob(JobStatusFunctor functor) {
854 for (int uidIndex = mJobs.size() - 1; uidIndex >= 0; uidIndex--) {
855 ArraySet<JobStatus> jobs = mJobs.valueAt(uidIndex);
856 for (int i = jobs.size() - 1; i >= 0; i--) {
857 functor.process(jobs.valueAt(i));
858 }
859 }
860 }
861 }
Matthew Williams01ac45b2014-07-22 20:44:12 -0700862}