OSDN Git Service

SyncManager: detect suspicious periodic sync removal.
[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             int numJobs = 0;
548             try {
549                 List<JobStatus> jobs;
550                 FileInputStream fis = mJobsFile.openRead();
551                 synchronized (mLock) {
552                     jobs = readJobMapImpl(fis, rtcGood);
553                     if (jobs != null) {
554                         long now = SystemClock.elapsedRealtime();
555                         IActivityManager am = ActivityManager.getService();
556                         for (int i=0; i<jobs.size(); i++) {
557                             JobStatus js = jobs.get(i);
558                             js.prepareLocked(am);
559                             js.enqueueTime = now;
560                             this.jobSet.add(js);
561                             numJobs++;
562                         }
563                     }
564                 }
565                 fis.close();
566             } catch (FileNotFoundException e) {
567                 if (DEBUG) {
568                     Slog.d(TAG, "Could not find jobs file, probably there was nothing to load.");
569                 }
570             } catch (XmlPullParserException | IOException e) {
571                 Slog.wtf(TAG, "Error jobstore xml.", e);
572             }
573             Slog.i(TAG, "Read " + numJobs + " jobs");
574         }
575
576         private List<JobStatus> readJobMapImpl(FileInputStream fis, boolean rtcIsGood)
577                 throws XmlPullParserException, IOException {
578             XmlPullParser parser = Xml.newPullParser();
579             parser.setInput(fis, StandardCharsets.UTF_8.name());
580
581             int eventType = parser.getEventType();
582             while (eventType != XmlPullParser.START_TAG &&
583                     eventType != XmlPullParser.END_DOCUMENT) {
584                 eventType = parser.next();
585                 Slog.d(TAG, "Start tag: " + parser.getName());
586             }
587             if (eventType == XmlPullParser.END_DOCUMENT) {
588                 if (DEBUG) {
589                     Slog.d(TAG, "No persisted jobs.");
590                 }
591                 return null;
592             }
593
594             String tagName = parser.getName();
595             if ("job-info".equals(tagName)) {
596                 final List<JobStatus> jobs = new ArrayList<JobStatus>();
597                 // Read in version info.
598                 try {
599                     int version = Integer.parseInt(parser.getAttributeValue(null, "version"));
600                     if (version != JOBS_FILE_VERSION) {
601                         Slog.d(TAG, "Invalid version number, aborting jobs file read.");
602                         return null;
603                     }
604                 } catch (NumberFormatException e) {
605                     Slog.e(TAG, "Invalid version number, aborting jobs file read.");
606                     return null;
607                 }
608                 eventType = parser.next();
609                 do {
610                     // Read each <job/>
611                     if (eventType == XmlPullParser.START_TAG) {
612                         tagName = parser.getName();
613                         // Start reading job.
614                         if ("job".equals(tagName)) {
615                             JobStatus persistedJob = restoreJobFromXml(rtcIsGood, parser);
616                             if (persistedJob != null) {
617                                 if (DEBUG) {
618                                     Slog.d(TAG, "Read out " + persistedJob);
619                                 }
620                                 jobs.add(persistedJob);
621                             } else {
622                                 Slog.d(TAG, "Error reading job from file.");
623                             }
624                         }
625                     }
626                     eventType = parser.next();
627                 } while (eventType != XmlPullParser.END_DOCUMENT);
628                 return jobs;
629             }
630             return null;
631         }
632
633         /**
634          * @param parser Xml parser at the beginning of a "<job/>" tag. The next "parser.next()" call
635          *               will take the parser into the body of the job tag.
636          * @return Newly instantiated job holding all the information we just read out of the xml tag.
637          */
638         private JobStatus restoreJobFromXml(boolean rtcIsGood, XmlPullParser parser)
639                 throws XmlPullParserException, IOException {
640             JobInfo.Builder jobBuilder;
641             int uid, sourceUserId;
642             long lastSuccessfulRunTime;
643             long lastFailedRunTime;
644
645             // Read out job identifier attributes and priority.
646             try {
647                 jobBuilder = buildBuilderFromXml(parser);
648                 jobBuilder.setPersisted(true);
649                 uid = Integer.parseInt(parser.getAttributeValue(null, "uid"));
650
651                 String val = parser.getAttributeValue(null, "priority");
652                 if (val != null) {
653                     jobBuilder.setPriority(Integer.parseInt(val));
654                 }
655                 val = parser.getAttributeValue(null, "flags");
656                 if (val != null) {
657                     jobBuilder.setFlags(Integer.parseInt(val));
658                 }
659                 val = parser.getAttributeValue(null, "sourceUserId");
660                 sourceUserId = val == null ? -1 : Integer.parseInt(val);
661
662                 val = parser.getAttributeValue(null, "lastSuccessfulRunTime");
663                 lastSuccessfulRunTime = val == null ? 0 : Long.parseLong(val);
664
665                 val = parser.getAttributeValue(null, "lastFailedRunTime");
666                 lastFailedRunTime = val == null ? 0 : Long.parseLong(val);
667             } catch (NumberFormatException e) {
668                 Slog.e(TAG, "Error parsing job's required fields, skipping");
669                 return null;
670             }
671
672             String sourcePackageName = parser.getAttributeValue(null, "sourcePackageName");
673
674             final String sourceTag = parser.getAttributeValue(null, "sourceTag");
675
676             int eventType;
677             // Read out constraints tag.
678             do {
679                 eventType = parser.next();
680             } while (eventType == XmlPullParser.TEXT);  // Push through to next START_TAG.
681
682             if (!(eventType == XmlPullParser.START_TAG &&
683                     XML_TAG_PARAMS_CONSTRAINTS.equals(parser.getName()))) {
684                 // Expecting a <constraints> start tag.
685                 return null;
686             }
687             try {
688                 buildConstraintsFromXml(jobBuilder, parser);
689             } catch (NumberFormatException e) {
690                 Slog.d(TAG, "Error reading constraints, skipping.");
691                 return null;
692             }
693             parser.next(); // Consume </constraints>
694
695             // Read out execution parameters tag.
696             do {
697                 eventType = parser.next();
698             } while (eventType == XmlPullParser.TEXT);
699             if (eventType != XmlPullParser.START_TAG) {
700                 return null;
701             }
702
703             // Tuple of (earliest runtime, latest runtime) in UTC.
704             final Pair<Long, Long> rtcRuntimes;
705             try {
706                 rtcRuntimes = buildRtcExecutionTimesFromXml(parser);
707             } catch (NumberFormatException e) {
708                 if (DEBUG) {
709                     Slog.d(TAG, "Error parsing execution time parameters, skipping.");
710                 }
711                 return null;
712             }
713
714             final long elapsedNow = SystemClock.elapsedRealtime();
715             Pair<Long, Long> elapsedRuntimes = convertRtcBoundsToElapsed(rtcRuntimes, elapsedNow);
716
717             if (XML_TAG_PERIODIC.equals(parser.getName())) {
718                 try {
719                     String val = parser.getAttributeValue(null, "period");
720                     final long periodMillis = Long.parseLong(val);
721                     val = parser.getAttributeValue(null, "flex");
722                     final long flexMillis = (val != null) ? Long.valueOf(val) : periodMillis;
723                     jobBuilder.setPeriodic(periodMillis, flexMillis);
724                     // As a sanity check, cap the recreated run time to be no later than flex+period
725                     // from now. This is the latest the periodic could be pushed out. This could
726                     // happen if the periodic ran early (at flex time before period), and then the
727                     // device rebooted.
728                     if (elapsedRuntimes.second > elapsedNow + periodMillis + flexMillis) {
729                         final long clampedLateRuntimeElapsed = elapsedNow + flexMillis
730                                 + periodMillis;
731                         final long clampedEarlyRuntimeElapsed = clampedLateRuntimeElapsed
732                                 - flexMillis;
733                         Slog.w(TAG,
734                                 String.format("Periodic job for uid='%d' persisted run-time is" +
735                                                 " too big [%s, %s]. Clamping to [%s,%s]",
736                                         uid,
737                                         DateUtils.formatElapsedTime(elapsedRuntimes.first / 1000),
738                                         DateUtils.formatElapsedTime(elapsedRuntimes.second / 1000),
739                                         DateUtils.formatElapsedTime(
740                                                 clampedEarlyRuntimeElapsed / 1000),
741                                         DateUtils.formatElapsedTime(
742                                                 clampedLateRuntimeElapsed / 1000))
743                         );
744                         elapsedRuntimes =
745                                 Pair.create(clampedEarlyRuntimeElapsed, clampedLateRuntimeElapsed);
746                     }
747                 } catch (NumberFormatException e) {
748                     Slog.d(TAG, "Error reading periodic execution criteria, skipping.");
749                     return null;
750                 }
751             } else if (XML_TAG_ONEOFF.equals(parser.getName())) {
752                 try {
753                     if (elapsedRuntimes.first != JobStatus.NO_EARLIEST_RUNTIME) {
754                         jobBuilder.setMinimumLatency(elapsedRuntimes.first - elapsedNow);
755                     }
756                     if (elapsedRuntimes.second != JobStatus.NO_LATEST_RUNTIME) {
757                         jobBuilder.setOverrideDeadline(
758                                 elapsedRuntimes.second - elapsedNow);
759                     }
760                 } catch (NumberFormatException e) {
761                     Slog.d(TAG, "Error reading job execution criteria, skipping.");
762                     return null;
763                 }
764             } else {
765                 if (DEBUG) {
766                     Slog.d(TAG, "Invalid parameter tag, skipping - " + parser.getName());
767                 }
768                 // Expecting a parameters start tag.
769                 return null;
770             }
771             maybeBuildBackoffPolicyFromXml(jobBuilder, parser);
772
773             parser.nextTag(); // Consume parameters end tag.
774
775             // Read out extras Bundle.
776             do {
777                 eventType = parser.next();
778             } while (eventType == XmlPullParser.TEXT);
779             if (!(eventType == XmlPullParser.START_TAG
780                     && XML_TAG_EXTRAS.equals(parser.getName()))) {
781                 if (DEBUG) {
782                     Slog.d(TAG, "Error reading extras, skipping.");
783                 }
784                 return null;
785             }
786
787             PersistableBundle extras = PersistableBundle.restoreFromXml(parser);
788             jobBuilder.setExtras(extras);
789             parser.nextTag(); // Consume </extras>
790
791             // Migrate sync jobs forward from earlier, incomplete representation
792             if ("android".equals(sourcePackageName)
793                     && extras != null
794                     && extras.getBoolean("SyncManagerJob", false)) {
795                 sourcePackageName = extras.getString("owningPackage", sourcePackageName);
796                 if (DEBUG) {
797                     Slog.i(TAG, "Fixing up sync job source package name from 'android' to '"
798                             + sourcePackageName + "'");
799                 }
800             }
801
802             // And now we're done
803             JobStatus js = new JobStatus(
804                     jobBuilder.build(), uid, sourcePackageName, sourceUserId, sourceTag,
805                     elapsedRuntimes.first, elapsedRuntimes.second,
806                     lastSuccessfulRunTime, lastFailedRunTime,
807                     (rtcIsGood) ? null : rtcRuntimes);
808             return js;
809         }
810
811         private JobInfo.Builder buildBuilderFromXml(XmlPullParser parser) throws NumberFormatException {
812             // Pull out required fields from <job> attributes.
813             int jobId = Integer.parseInt(parser.getAttributeValue(null, "jobid"));
814             String packageName = parser.getAttributeValue(null, "package");
815             String className = parser.getAttributeValue(null, "class");
816             ComponentName cname = new ComponentName(packageName, className);
817
818             return new JobInfo.Builder(jobId, cname);
819         }
820
821         private void buildConstraintsFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser) {
822             String val = parser.getAttributeValue(null, "connectivity");
823             if (val != null) {
824                 jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
825             }
826             val = parser.getAttributeValue(null, "metered");
827             if (val != null) {
828                 jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_METERED);
829             }
830             val = parser.getAttributeValue(null, "unmetered");
831             if (val != null) {
832                 jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED);
833             }
834             val = parser.getAttributeValue(null, "not-roaming");
835             if (val != null) {
836                 jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_NOT_ROAMING);
837             }
838             val = parser.getAttributeValue(null, "idle");
839             if (val != null) {
840                 jobBuilder.setRequiresDeviceIdle(true);
841             }
842             val = parser.getAttributeValue(null, "charging");
843             if (val != null) {
844                 jobBuilder.setRequiresCharging(true);
845             }
846         }
847
848         /**
849          * Builds the back-off policy out of the params tag. These attributes may not exist, depending
850          * on whether the back-off was set when the job was first scheduled.
851          */
852         private void maybeBuildBackoffPolicyFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser) {
853             String val = parser.getAttributeValue(null, "initial-backoff");
854             if (val != null) {
855                 long initialBackoff = Long.parseLong(val);
856                 val = parser.getAttributeValue(null, "backoff-policy");
857                 int backoffPolicy = Integer.parseInt(val);  // Will throw NFE which we catch higher up.
858                 jobBuilder.setBackoffCriteria(initialBackoff, backoffPolicy);
859             }
860         }
861
862         /**
863          * Extract a job's earliest/latest run time data from XML.  These are returned in
864          * unadjusted UTC wall clock time, because we do not yet know whether the system
865          * clock is reliable for purposes of calculating deltas from 'now'.
866          *
867          * @param parser
868          * @return A Pair of timestamps in UTC wall-clock time.  The first is the earliest
869          *     time at which the job is to become runnable, and the second is the deadline at
870          *     which it becomes overdue to execute.
871          * @throws NumberFormatException
872          */
873         private Pair<Long, Long> buildRtcExecutionTimesFromXml(XmlPullParser parser)
874                 throws NumberFormatException {
875             String val;
876             // Pull out execution time data.
877             val = parser.getAttributeValue(null, "delay");
878             final long earliestRunTimeRtc = (val != null)
879                     ? Long.parseLong(val)
880                     : JobStatus.NO_EARLIEST_RUNTIME;
881             val = parser.getAttributeValue(null, "deadline");
882             final long latestRunTimeRtc = (val != null)
883                     ? Long.parseLong(val)
884                     : JobStatus.NO_LATEST_RUNTIME;
885             return Pair.create(earliestRunTimeRtc, latestRunTimeRtc);
886         }
887
888         /**
889          * Convenience function to read out and convert deadline and delay from xml into elapsed real
890          * time.
891          * @return A {@link android.util.Pair}, where the first value is the earliest elapsed runtime
892          * and the second is the latest elapsed runtime.
893          */
894         private Pair<Long, Long> buildExecutionTimesFromXml(XmlPullParser parser)
895                 throws NumberFormatException {
896             // Pull out execution time data.
897             final long nowWallclock = System.currentTimeMillis();
898             final long nowElapsed = SystemClock.elapsedRealtime();
899
900             long earliestRunTimeElapsed = JobStatus.NO_EARLIEST_RUNTIME;
901             long latestRunTimeElapsed = JobStatus.NO_LATEST_RUNTIME;
902             String val = parser.getAttributeValue(null, "deadline");
903             if (val != null) {
904                 long latestRuntimeWallclock = Long.parseLong(val);
905                 long maxDelayElapsed =
906                         Math.max(latestRuntimeWallclock - nowWallclock, 0);
907                 latestRunTimeElapsed = nowElapsed + maxDelayElapsed;
908             }
909             val = parser.getAttributeValue(null, "delay");
910             if (val != null) {
911                 long earliestRuntimeWallclock = Long.parseLong(val);
912                 long minDelayElapsed =
913                         Math.max(earliestRuntimeWallclock - nowWallclock, 0);
914                 earliestRunTimeElapsed = nowElapsed + minDelayElapsed;
915
916             }
917             return Pair.create(earliestRunTimeElapsed, latestRunTimeElapsed);
918         }
919     }
920
921     static final class JobSet {
922         // Key is the getUid() originator of the jobs in each sheaf
923         private SparseArray<ArraySet<JobStatus>> mJobs;
924
925         public JobSet() {
926             mJobs = new SparseArray<ArraySet<JobStatus>>();
927         }
928
929         public List<JobStatus> getJobsByUid(int uid) {
930             ArrayList<JobStatus> matchingJobs = new ArrayList<JobStatus>();
931             ArraySet<JobStatus> jobs = mJobs.get(uid);
932             if (jobs != null) {
933                 matchingJobs.addAll(jobs);
934             }
935             return matchingJobs;
936         }
937
938         // By user, not by uid, so we need to traverse by key and check
939         public List<JobStatus> getJobsByUser(int userId) {
940             ArrayList<JobStatus> result = new ArrayList<JobStatus>();
941             for (int i = mJobs.size() - 1; i >= 0; i--) {
942                 if (UserHandle.getUserId(mJobs.keyAt(i)) == userId) {
943                     ArraySet<JobStatus> jobs = mJobs.valueAt(i);
944                     if (jobs != null) {
945                         result.addAll(jobs);
946                     }
947                 }
948             }
949             return result;
950         }
951
952         public boolean add(JobStatus job) {
953             final int uid = job.getUid();
954             ArraySet<JobStatus> jobs = mJobs.get(uid);
955             if (jobs == null) {
956                 jobs = new ArraySet<JobStatus>();
957                 mJobs.put(uid, jobs);
958             }
959             return jobs.add(job);
960         }
961
962         public boolean remove(JobStatus job) {
963             final int uid = job.getUid();
964             ArraySet<JobStatus> jobs = mJobs.get(uid);
965             boolean didRemove = (jobs != null) ? jobs.remove(job) : false;
966             if (didRemove && jobs.size() == 0) {
967                 // no more jobs for this uid; let the now-empty set object be GC'd.
968                 mJobs.remove(uid);
969             }
970             return didRemove;
971         }
972
973         // Remove the jobs all users not specified by the whitelist of user ids
974         public void removeJobsOfNonUsers(int[] whitelist) {
975             for (int jobIndex = mJobs.size() - 1; jobIndex >= 0; jobIndex--) {
976                 int jobUserId = UserHandle.getUserId(mJobs.keyAt(jobIndex));
977                 // check if job's user id is not in the whitelist
978                 if (!ArrayUtils.contains(whitelist, jobUserId)) {
979                     mJobs.removeAt(jobIndex);
980                 }
981             }
982         }
983
984         public boolean contains(JobStatus job) {
985             final int uid = job.getUid();
986             ArraySet<JobStatus> jobs = mJobs.get(uid);
987             return jobs != null && jobs.contains(job);
988         }
989
990         public JobStatus get(int uid, int jobId) {
991             ArraySet<JobStatus> jobs = mJobs.get(uid);
992             if (jobs != null) {
993                 for (int i = jobs.size() - 1; i >= 0; i--) {
994                     JobStatus job = jobs.valueAt(i);
995                     if (job.getJobId() == jobId) {
996                         return job;
997                     }
998                 }
999             }
1000             return null;
1001         }
1002
1003         // Inefficient; use only for testing
1004         public List<JobStatus> getAllJobs() {
1005             ArrayList<JobStatus> allJobs = new ArrayList<JobStatus>(size());
1006             for (int i = mJobs.size() - 1; i >= 0; i--) {
1007                 ArraySet<JobStatus> jobs = mJobs.valueAt(i);
1008                 if (jobs != null) {
1009                     // Use a for loop over the ArraySet, so we don't need to make its
1010                     // optional collection class iterator implementation or have to go
1011                     // through a temporary array from toArray().
1012                     for (int j = jobs.size() - 1; j >= 0; j--) {
1013                         allJobs.add(jobs.valueAt(j));
1014                     }
1015                 }
1016             }
1017             return allJobs;
1018         }
1019
1020         public void clear() {
1021             mJobs.clear();
1022         }
1023
1024         public int size() {
1025             int total = 0;
1026             for (int i = mJobs.size() - 1; i >= 0; i--) {
1027                 total += mJobs.valueAt(i).size();
1028             }
1029             return total;
1030         }
1031
1032         // We only want to count the jobs that this uid has scheduled on its own
1033         // behalf, not those that the app has scheduled on someone else's behalf.
1034         public int countJobsForUid(int uid) {
1035             int total = 0;
1036             ArraySet<JobStatus> jobs = mJobs.get(uid);
1037             if (jobs != null) {
1038                 for (int i = jobs.size() - 1; i >= 0; i--) {
1039                     JobStatus job = jobs.valueAt(i);
1040                     if (job.getUid() == job.getSourceUid()) {
1041                         total++;
1042                     }
1043                 }
1044             }
1045             return total;
1046         }
1047
1048         public void forEachJob(JobStatusFunctor functor) {
1049             for (int uidIndex = mJobs.size() - 1; uidIndex >= 0; uidIndex--) {
1050                 ArraySet<JobStatus> jobs = mJobs.valueAt(uidIndex);
1051                 for (int i = jobs.size() - 1; i >= 0; i--) {
1052                     functor.process(jobs.valueAt(i));
1053                 }
1054             }
1055         }
1056
1057         public void forEachJob(int uid, JobStatusFunctor functor) {
1058             ArraySet<JobStatus> jobs = mJobs.get(uid);
1059             if (jobs != null) {
1060                 for (int i = jobs.size() - 1; i >= 0; i--) {
1061                     functor.process(jobs.valueAt(i));
1062                 }
1063             }
1064         }
1065     }
1066 }