OSDN Git Service

62b06d6242ea0396009c5390d2448692954ae015
[android-x86/frameworks-base.git] / services / core / java / com / android / server / job / JobStore.java
1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License
15  */
16
17 package com.android.server.job;
18
19 import 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;
36
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;
42
43 import java.io.ByteArrayOutputStream;
44 import java.io.File;
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;
52 import java.util.Set;
53
54 import org.xmlpull.v1.XmlPullParser;
55 import org.xmlpull.v1.XmlPullParserException;
56 import org.xmlpull.v1.XmlSerializer;
57
58 /**
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.
62  *
63  * Note on locking:
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
67  *      object.
68  */
69 public final class JobStore {
70     private static final String TAG = "JobStore";
71     private static final boolean DEBUG = JobSchedulerService.DEBUG;
72
73     /** Threshold to adjust how often we want to write to the db. */
74     private static final int MAX_OPS_BEFORE_WRITE = 1;
75
76     final Object mLock;
77     final JobSet mJobSet; // per-caller-uid tracking
78     final Context mContext;
79
80     // Bookkeeping around incorrect boot-time system clock
81     private final long mXmlTimestamp;
82     private boolean mRtcGood;
83
84     private int mDirtyOperations;
85
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;
91
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());
98             }
99             return sSingleton;
100         }
101     }
102
103     /**
104      * @return A freshly initialized job store object, with no loaded jobs.
105      */
106     @VisibleForTesting
107     public static JobStore initAndGetForTesting(Context context, File dataDir) {
108         JobStore jobStoreUnderTest = new JobStore(context, new Object(), dataDir);
109         jobStoreUnderTest.clear();
110         return jobStoreUnderTest;
111     }
112
113     /**
114      * Construct the instance of the job store. This results in a blocking read from disk.
115      */
116     private JobStore(Context context, Object lock, File dataDir) {
117         mLock = lock;
118         mContext = context;
119         mDirtyOperations = 0;
120
121         File systemDir = new File(dataDir, "system");
122         File jobDir = new File(systemDir, "job");
123         jobDir.mkdirs();
124         mJobsFile = new AtomicFile(new File(jobDir, "jobs.xml"));
125
126         mJobSet = new JobSet();
127
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.
131         //
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);
141
142         readJobMapFromDisk(mJobSet, mRtcGood);
143     }
144
145     public boolean jobTimesInflatedValid() {
146         return mRtcGood;
147     }
148
149     public boolean clockNowValidToInflate(long now) {
150         return now >= mXmlTimestamp;
151     }
152
153     /**
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.
157      */
158     public void getRtcCorrectedJobsLocked(final ArrayList<JobStatus> toAdd,
159             final ArrayList<JobStatus> toRemove) {
160         final long elapsedNow = SystemClock.elapsedRealtime();
161
162         // Find the jobs that need to be fixed up, collecting them for post-iteration
163         // replacement with their new versions
164         forEachJob(job -> {
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()));
171                 toRemove.add(job);
172             }
173         });
174     }
175
176     /**
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.
181      */
182     public boolean add(JobStatus jobStatus) {
183         boolean replaced = mJobSet.remove(jobStatus);
184         mJobSet.add(jobStatus);
185         if (jobStatus.isPersisted()) {
186             maybeWriteStatusToDiskAsync();
187         }
188         if (DEBUG) {
189             Slog.d(TAG, "Added job status to store: " + jobStatus);
190         }
191         return replaced;
192     }
193
194     boolean containsJob(JobStatus jobStatus) {
195         return mJobSet.contains(jobStatus);
196     }
197
198     public int size() {
199         return mJobSet.size();
200     }
201
202     public int countJobsForUid(int uid) {
203         return mJobSet.countJobsForUid(uid);
204     }
205
206     /**
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.
210      */
211     public boolean remove(JobStatus jobStatus, boolean writeBack) {
212         boolean removed = mJobSet.remove(jobStatus);
213         if (!removed) {
214             if (DEBUG) {
215                 Slog.d(TAG, "Couldn't remove job: didn't exist: " + jobStatus);
216             }
217             return false;
218         }
219         if (writeBack && jobStatus.isPersisted()) {
220             maybeWriteStatusToDiskAsync();
221         }
222         return removed;
223     }
224
225     /**
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.
228      */
229     public void removeJobsOfNonUsers(int[] whitelist) {
230         mJobSet.removeJobsOfNonUsers(whitelist);
231     }
232
233     @VisibleForTesting
234     public void clear() {
235         mJobSet.clear();
236         maybeWriteStatusToDiskAsync();
237     }
238
239     /**
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.
242      */
243     public List<JobStatus> getJobsByUser(int userHandle) {
244         return mJobSet.getJobsByUser(userHandle);
245     }
246
247     /**
248      * @param uid Uid of the requesting app.
249      * @return All JobStatus objects for a given uid from the master list. Never null.
250      */
251     public List<JobStatus> getJobsByUid(int uid) {
252         return mJobSet.getJobsByUid(uid);
253     }
254
255     /**
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.
259      */
260     public JobStatus getJobByUidAndJobId(int uid, int jobId) {
261         return mJobSet.get(uid, jobId);
262     }
263
264     /**
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.
269      */
270     public void forEachJob(JobStatusFunctor functor) {
271         mJobSet.forEachJob(functor);
272     }
273
274     public void forEachJob(int uid, JobStatusFunctor functor) {
275         mJobSet.forEachJob(uid, functor);
276     }
277
278     public interface JobStatusFunctor {
279         public void process(JobStatus jobStatus);
280     }
281
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";
290
291     /**
292      * Every time the state changes we write all the jobs in one swath, instead of trying to
293      * track incremental changes.
294      */
295     private void maybeWriteStatusToDiskAsync() {
296         mDirtyOperations++;
297         if (mDirtyOperations >= MAX_OPS_BEFORE_WRITE) {
298             if (DEBUG) {
299                 Slog.v(TAG, "Writing jobs to disk.");
300             }
301             mIoHandler.removeCallbacks(mWriteRunnable);
302             mIoHandler.post(mWriteRunnable);
303         }
304     }
305
306     @VisibleForTesting
307     public void readJobMapFromDisk(JobSet jobSet, boolean rtcGood) {
308         new ReadJobMapFromDiskRunnable(jobSet, rtcGood).run();
309     }
310
311     /**
312      * Runnable that writes {@link #mJobSet} out to xml.
313      * NOTE: This Runnable locks on mLock
314      */
315     private final Runnable mWriteRunnable = new Runnable() {
316         @Override
317         public void run() {
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() {
323                     @Override
324                     public void process(JobStatus job) {
325                         if (job.isPersisted()) {
326                             storeCopy.add(new JobStatus(job));
327                         }
328                     }
329                 });
330             }
331             writeJobsMapImpl(storeCopy);
332             if (DEBUG) {
333                 Slog.v(TAG, "Finished writing, took " + (SystemClock.elapsedRealtime()
334                         - startElapsed) + "ms");
335             }
336         }
337
338         private void writeJobsMapImpl(List<JobStatus> jobList) {
339             try {
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);
345
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);
350                     if (DEBUG) {
351                         Slog.d(TAG, "Saving job " + jobStatus.getJobId());
352                     }
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");
359                 }
360                 out.endTag(null, "job-info");
361                 out.endDocument();
362
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) {
369                 if (DEBUG) {
370                     Slog.v(TAG, "Error writing out job data.", e);
371                 }
372             } catch (XmlPullParserException e) {
373                 if (DEBUG) {
374                     Slog.d(TAG, "Error persisting bundle.", e);
375                 }
376             }
377         }
378
379         /** Write out a tag with data comprising the required fields and priority of this job and
380          * its client.
381          */
382         private void addAttributesToJobTag(XmlSerializer out, JobStatus jobStatus)
383                 throws IOException {
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());
389             }
390             if (jobStatus.getSourceTag() != null) {
391                 out.attribute(null, "sourceTag", jobStatus.getSourceTag());
392             }
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()));
397
398             out.attribute(null, "lastSuccessfulRunTime",
399                     String.valueOf(jobStatus.getLastSuccessfulRunTime()));
400             out.attribute(null, "lastFailedRunTime",
401                     String.valueOf(jobStatus.getLastFailedRunTime()));
402         }
403
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);
410         }
411
412         private PersistableBundle deepCopyBundle(PersistableBundle bundle, int maxDepth) {
413             if (maxDepth <= 0) {
414                 return null;
415             }
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);
423                 }
424             }
425             return copy;
426         }
427
428         /**
429          * Write out a tag with data identifying this job's constraints. If the constraint isn't here
430          * it doesn't apply.
431          */
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));
436             }
437             if (jobStatus.needsMeteredConnectivity()) {
438                 out.attribute(null, "metered", Boolean.toString(true));
439             }
440             if (jobStatus.needsUnmeteredConnectivity()) {
441                 out.attribute(null, "unmetered", Boolean.toString(true));
442             }
443             if (jobStatus.needsNonRoamingConnectivity()) {
444                 out.attribute(null, "not-roaming", Boolean.toString(true));
445             }
446             if (jobStatus.hasIdleConstraint()) {
447                 out.attribute(null, "idle", Boolean.toString(true));
448             }
449             if (jobStatus.hasChargingConstraint()) {
450                 out.attribute(null, "charging", Boolean.toString(true));
451             }
452             if (jobStatus.hasBatteryNotLowConstraint()) {
453                 out.attribute(null, "battery-not-low", Boolean.toString(true));
454             }
455             out.endTag(null, XML_TAG_PARAMS_CONSTRAINTS);
456         }
457
458         private void writeExecutionCriteriaToXml(XmlSerializer out, JobStatus jobStatus)
459                 throws IOException {
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()));
465             } else {
466                 out.startTag(null, XML_TAG_ONEOFF);
467             }
468
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);
475             }
476
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));
485             }
486             if (jobStatus.hasTimingDelayConstraint()) {
487                 final long delayWallclock = (utcJobTimes == null)
488                         ? nowRTC + (jobStatus.getEarliestRunTime() - nowElapsed)
489                         : utcJobTimes.first;
490                 out.attribute(null, "delay", Long.toString(delayWallclock));
491             }
492
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
495             // back-off.
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()));
500             }
501             if (job.isPeriodic()) {
502                 out.endTag(null, XML_TAG_PERIODIC);
503             } else {
504                 out.endTag(null, XML_TAG_ONEOFF);
505             }
506         }
507     };
508
509     /**
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.
515      */
516     private static Pair<Long, Long> convertRtcBoundsToElapsed(Pair<Long, Long> rtcTimes,
517             long nowElapsed) {
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);
526     }
527
528     /**
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)}.
531      */
532     private final class ReadJobMapFromDiskRunnable implements Runnable {
533         private final JobSet jobSet;
534         private final boolean rtcGood;
535
536         /**
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.
539          */
540         ReadJobMapFromDiskRunnable(JobSet jobSet, boolean rtcIsGood) {
541             this.jobSet = jobSet;
542             this.rtcGood = rtcIsGood;
543         }
544
545         @Override
546         public void run() {
547             try {
548                 List<JobStatus> jobs;
549                 FileInputStream fis = mJobsFile.openRead();
550                 synchronized (mLock) {
551                     jobs = readJobMapImpl(fis, rtcGood);
552                     if (jobs != null) {
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;
559                             this.jobSet.add(js);
560                         }
561                     }
562                 }
563                 fis.close();
564             } catch (FileNotFoundException e) {
565                 if (DEBUG) {
566                     Slog.d(TAG, "Could not find jobs file, probably there was nothing to load.");
567                 }
568             } catch (XmlPullParserException e) {
569                 if (DEBUG) {
570                     Slog.d(TAG, "Error parsing xml.", e);
571                 }
572             } catch (IOException e) {
573                 if (DEBUG) {
574                     Slog.d(TAG, "Error parsing xml.", e);
575                 }
576             }
577         }
578
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());
583
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());
589             }
590             if (eventType == XmlPullParser.END_DOCUMENT) {
591                 if (DEBUG) {
592                     Slog.d(TAG, "No persisted jobs.");
593                 }
594                 return null;
595             }
596
597             String tagName = parser.getName();
598             if ("job-info".equals(tagName)) {
599                 final List<JobStatus> jobs = new ArrayList<JobStatus>();
600                 // Read in version info.
601                 try {
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.");
605                         return null;
606                     }
607                 } catch (NumberFormatException e) {
608                     Slog.e(TAG, "Invalid version number, aborting jobs file read.");
609                     return null;
610                 }
611                 eventType = parser.next();
612                 do {
613                     // Read each <job/>
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) {
620                                 if (DEBUG) {
621                                     Slog.d(TAG, "Read out " + persistedJob);
622                                 }
623                                 jobs.add(persistedJob);
624                             } else {
625                                 Slog.d(TAG, "Error reading job from file.");
626                             }
627                         }
628                     }
629                     eventType = parser.next();
630                 } while (eventType != XmlPullParser.END_DOCUMENT);
631                 return jobs;
632             }
633             return null;
634         }
635
636         /**
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.
640          */
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;
647
648             // Read out job identifier attributes and priority.
649             try {
650                 jobBuilder = buildBuilderFromXml(parser);
651                 jobBuilder.setPersisted(true);
652                 uid = Integer.parseInt(parser.getAttributeValue(null, "uid"));
653
654                 String val = parser.getAttributeValue(null, "priority");
655                 if (val != null) {
656                     jobBuilder.setPriority(Integer.parseInt(val));
657                 }
658                 val = parser.getAttributeValue(null, "flags");
659                 if (val != null) {
660                     jobBuilder.setFlags(Integer.parseInt(val));
661                 }
662                 val = parser.getAttributeValue(null, "sourceUserId");
663                 sourceUserId = val == null ? -1 : Integer.parseInt(val);
664
665                 val = parser.getAttributeValue(null, "lastSuccessfulRunTime");
666                 lastSuccessfulRunTime = val == null ? 0 : Long.parseLong(val);
667
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");
672                 return null;
673             }
674
675             String sourcePackageName = parser.getAttributeValue(null, "sourcePackageName");
676
677             final String sourceTag = parser.getAttributeValue(null, "sourceTag");
678
679             int eventType;
680             // Read out constraints tag.
681             do {
682                 eventType = parser.next();
683             } while (eventType == XmlPullParser.TEXT);  // Push through to next START_TAG.
684
685             if (!(eventType == XmlPullParser.START_TAG &&
686                     XML_TAG_PARAMS_CONSTRAINTS.equals(parser.getName()))) {
687                 // Expecting a <constraints> start tag.
688                 return null;
689             }
690             try {
691                 buildConstraintsFromXml(jobBuilder, parser);
692             } catch (NumberFormatException e) {
693                 Slog.d(TAG, "Error reading constraints, skipping.");
694                 return null;
695             }
696             parser.next(); // Consume </constraints>
697
698             // Read out execution parameters tag.
699             do {
700                 eventType = parser.next();
701             } while (eventType == XmlPullParser.TEXT);
702             if (eventType != XmlPullParser.START_TAG) {
703                 return null;
704             }
705
706             // Tuple of (earliest runtime, latest runtime) in UTC.
707             final Pair<Long, Long> rtcRuntimes;
708             try {
709                 rtcRuntimes = buildRtcExecutionTimesFromXml(parser);
710             } catch (NumberFormatException e) {
711                 if (DEBUG) {
712                     Slog.d(TAG, "Error parsing execution time parameters, skipping.");
713                 }
714                 return null;
715             }
716
717             final long elapsedNow = SystemClock.elapsedRealtime();
718             Pair<Long, Long> elapsedRuntimes = convertRtcBoundsToElapsed(rtcRuntimes, elapsedNow);
719
720             if (XML_TAG_PERIODIC.equals(parser.getName())) {
721                 try {
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
730                     // device rebooted.
731                     if (elapsedRuntimes.second > elapsedNow + periodMillis + flexMillis) {
732                         final long clampedLateRuntimeElapsed = elapsedNow + flexMillis
733                                 + periodMillis;
734                         final long clampedEarlyRuntimeElapsed = clampedLateRuntimeElapsed
735                                 - flexMillis;
736                         Slog.w(TAG,
737                                 String.format("Periodic job for uid='%d' persisted run-time is" +
738                                                 " too big [%s, %s]. Clamping to [%s,%s]",
739                                         uid,
740                                         DateUtils.formatElapsedTime(elapsedRuntimes.first / 1000),
741                                         DateUtils.formatElapsedTime(elapsedRuntimes.second / 1000),
742                                         DateUtils.formatElapsedTime(
743                                                 clampedEarlyRuntimeElapsed / 1000),
744                                         DateUtils.formatElapsedTime(
745                                                 clampedLateRuntimeElapsed / 1000))
746                         );
747                         elapsedRuntimes =
748                                 Pair.create(clampedEarlyRuntimeElapsed, clampedLateRuntimeElapsed);
749                     }
750                 } catch (NumberFormatException e) {
751                     Slog.d(TAG, "Error reading periodic execution criteria, skipping.");
752                     return null;
753                 }
754             } else if (XML_TAG_ONEOFF.equals(parser.getName())) {
755                 try {
756                     if (elapsedRuntimes.first != JobStatus.NO_EARLIEST_RUNTIME) {
757                         jobBuilder.setMinimumLatency(elapsedRuntimes.first - elapsedNow);
758                     }
759                     if (elapsedRuntimes.second != JobStatus.NO_LATEST_RUNTIME) {
760                         jobBuilder.setOverrideDeadline(
761                                 elapsedRuntimes.second - elapsedNow);
762                     }
763                 } catch (NumberFormatException e) {
764                     Slog.d(TAG, "Error reading job execution criteria, skipping.");
765                     return null;
766                 }
767             } else {
768                 if (DEBUG) {
769                     Slog.d(TAG, "Invalid parameter tag, skipping - " + parser.getName());
770                 }
771                 // Expecting a parameters start tag.
772                 return null;
773             }
774             maybeBuildBackoffPolicyFromXml(jobBuilder, parser);
775
776             parser.nextTag(); // Consume parameters end tag.
777
778             // Read out extras Bundle.
779             do {
780                 eventType = parser.next();
781             } while (eventType == XmlPullParser.TEXT);
782             if (!(eventType == XmlPullParser.START_TAG
783                     && XML_TAG_EXTRAS.equals(parser.getName()))) {
784                 if (DEBUG) {
785                     Slog.d(TAG, "Error reading extras, skipping.");
786                 }
787                 return null;
788             }
789
790             PersistableBundle extras = PersistableBundle.restoreFromXml(parser);
791             jobBuilder.setExtras(extras);
792             parser.nextTag(); // Consume </extras>
793
794             // Migrate sync jobs forward from earlier, incomplete representation
795             if ("android".equals(sourcePackageName)
796                     && extras != null
797                     && extras.getBoolean("SyncManagerJob", false)) {
798                 sourcePackageName = extras.getString("owningPackage", sourcePackageName);
799                 if (DEBUG) {
800                     Slog.i(TAG, "Fixing up sync job source package name from 'android' to '"
801                             + sourcePackageName + "'");
802                 }
803             }
804
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);
811             return js;
812         }
813
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);
820
821             return new JobInfo.Builder(jobId, cname);
822         }
823
824         private void buildConstraintsFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser) {
825             String val = parser.getAttributeValue(null, "connectivity");
826             if (val != null) {
827                 jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
828             }
829             val = parser.getAttributeValue(null, "metered");
830             if (val != null) {
831                 jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_METERED);
832             }
833             val = parser.getAttributeValue(null, "unmetered");
834             if (val != null) {
835                 jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED);
836             }
837             val = parser.getAttributeValue(null, "not-roaming");
838             if (val != null) {
839                 jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_NOT_ROAMING);
840             }
841             val = parser.getAttributeValue(null, "idle");
842             if (val != null) {
843                 jobBuilder.setRequiresDeviceIdle(true);
844             }
845             val = parser.getAttributeValue(null, "charging");
846             if (val != null) {
847                 jobBuilder.setRequiresCharging(true);
848             }
849         }
850
851         /**
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.
854          */
855         private void maybeBuildBackoffPolicyFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser) {
856             String val = parser.getAttributeValue(null, "initial-backoff");
857             if (val != null) {
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);
862             }
863         }
864
865         /**
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'.
869          *
870          * @param parser
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
875          */
876         private Pair<Long, Long> buildRtcExecutionTimesFromXml(XmlPullParser parser)
877                 throws NumberFormatException {
878             String val;
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);
889         }
890
891         /**
892          * Convenience function to read out and convert deadline and delay from xml into elapsed real
893          * time.
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.
896          */
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();
902
903             long earliestRunTimeElapsed = JobStatus.NO_EARLIEST_RUNTIME;
904             long latestRunTimeElapsed = JobStatus.NO_LATEST_RUNTIME;
905             String val = parser.getAttributeValue(null, "deadline");
906             if (val != null) {
907                 long latestRuntimeWallclock = Long.parseLong(val);
908                 long maxDelayElapsed =
909                         Math.max(latestRuntimeWallclock - nowWallclock, 0);
910                 latestRunTimeElapsed = nowElapsed + maxDelayElapsed;
911             }
912             val = parser.getAttributeValue(null, "delay");
913             if (val != null) {
914                 long earliestRuntimeWallclock = Long.parseLong(val);
915                 long minDelayElapsed =
916                         Math.max(earliestRuntimeWallclock - nowWallclock, 0);
917                 earliestRunTimeElapsed = nowElapsed + minDelayElapsed;
918
919             }
920             return Pair.create(earliestRunTimeElapsed, latestRunTimeElapsed);
921         }
922     }
923
924     static final class JobSet {
925         // Key is the getUid() originator of the jobs in each sheaf
926         private SparseArray<ArraySet<JobStatus>> mJobs;
927
928         public JobSet() {
929             mJobs = new SparseArray<ArraySet<JobStatus>>();
930         }
931
932         public List<JobStatus> getJobsByUid(int uid) {
933             ArrayList<JobStatus> matchingJobs = new ArrayList<JobStatus>();
934             ArraySet<JobStatus> jobs = mJobs.get(uid);
935             if (jobs != null) {
936                 matchingJobs.addAll(jobs);
937             }
938             return matchingJobs;
939         }
940
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);
947                     if (jobs != null) {
948                         result.addAll(jobs);
949                     }
950                 }
951             }
952             return result;
953         }
954
955         public boolean add(JobStatus job) {
956             final int uid = job.getUid();
957             ArraySet<JobStatus> jobs = mJobs.get(uid);
958             if (jobs == null) {
959                 jobs = new ArraySet<JobStatus>();
960                 mJobs.put(uid, jobs);
961             }
962             return jobs.add(job);
963         }
964
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.
971                 mJobs.remove(uid);
972             }
973             return didRemove;
974         }
975
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);
983                 }
984             }
985         }
986
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);
991         }
992
993         public JobStatus get(int uid, int jobId) {
994             ArraySet<JobStatus> jobs = mJobs.get(uid);
995             if (jobs != null) {
996                 for (int i = jobs.size() - 1; i >= 0; i--) {
997                     JobStatus job = jobs.valueAt(i);
998                     if (job.getJobId() == jobId) {
999                         return job;
1000                     }
1001                 }
1002             }
1003             return null;
1004         }
1005
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);
1011                 if (jobs != null) {
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));
1017                     }
1018                 }
1019             }
1020             return allJobs;
1021         }
1022
1023         public void clear() {
1024             mJobs.clear();
1025         }
1026
1027         public int size() {
1028             int total = 0;
1029             for (int i = mJobs.size() - 1; i >= 0; i--) {
1030                 total += mJobs.valueAt(i).size();
1031             }
1032             return total;
1033         }
1034
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) {
1038             int total = 0;
1039             ArraySet<JobStatus> jobs = mJobs.get(uid);
1040             if (jobs != null) {
1041                 for (int i = jobs.size() - 1; i >= 0; i--) {
1042                     JobStatus job = jobs.valueAt(i);
1043                     if (job.getUid() == job.getSourceUid()) {
1044                         total++;
1045                     }
1046                 }
1047             }
1048             return total;
1049         }
1050
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));
1056                 }
1057             }
1058         }
1059
1060         public void forEachJob(int uid, JobStatusFunctor functor) {
1061             ArraySet<JobStatus> jobs = mJobs.get(uid);
1062             if (jobs != null) {
1063                 for (int i = jobs.size() - 1; i >= 0; i--) {
1064                     functor.process(jobs.valueAt(i));
1065                 }
1066             }
1067         }
1068     }
1069 }