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.content.ComponentName;
20 import android.app.job.JobInfo;
21 import android.content.Context;
22 import android.os.Environment;
23 import android.os.Handler;
24 import android.os.PersistableBundle;
25 import android.os.SystemClock;
26 import android.os.UserHandle;
27 import android.text.format.DateUtils;
28 import android.util.AtomicFile;
29 import android.util.ArraySet;
30 import android.util.Pair;
31 import android.util.Slog;
32 import android.util.SparseArray;
33 import android.util.Xml;
34 
35 import com.android.internal.annotations.VisibleForTesting;
36 import com.android.internal.util.FastXmlSerializer;
37 import com.android.server.IoThread;
38 import com.android.server.job.controllers.JobStatus;
39 
40 import java.io.ByteArrayOutputStream;
41 import java.io.File;
42 import java.io.FileInputStream;
43 import java.io.FileNotFoundException;
44 import java.io.FileOutputStream;
45 import java.io.IOException;
46 import java.nio.charset.StandardCharsets;
47 import java.util.ArrayList;
48 import java.util.List;
49 import java.util.Set;
50 
51 import org.xmlpull.v1.XmlPullParser;
52 import org.xmlpull.v1.XmlPullParserException;
53 import org.xmlpull.v1.XmlSerializer;
54 
55 /**
56  * Maintains the master list of jobs that the job scheduler is tracking. These jobs are compared by
57  * reference, so none of the functions in this class should make a copy.
58  * Also handles read/write of persisted jobs.
59  *
60  * Note on locking:
61  *      All callers to this class must <strong>lock on the class object they are calling</strong>.
62  *      This is important b/c {@link com.android.server.job.JobStore.WriteJobsMapToDiskRunnable}
63  *      and {@link com.android.server.job.JobStore.ReadJobMapFromDiskRunnable} lock on that
64  *      object.
65  */
66 public class JobStore {
67     private static final String TAG = "JobStore";
68     private static final boolean DEBUG = JobSchedulerService.DEBUG;
69 
70     /** Threshold to adjust how often we want to write to the db. */
71     private static final int MAX_OPS_BEFORE_WRITE = 1;
72     final Object mLock;
73     final JobSet mJobSet; // per-caller-uid tracking
74     final Context mContext;
75 
76     private int mDirtyOperations;
77 
78     private static final Object sSingletonLock = new Object();
79     private final AtomicFile mJobsFile;
80     /** Handler backed by IoThread for writing to disk. */
81     private final Handler mIoHandler = IoThread.getHandler();
82     private static JobStore sSingleton;
83 
84     /** Used by the {@link JobSchedulerService} to instantiate the JobStore. */
initAndGet(JobSchedulerService jobManagerService)85     static JobStore initAndGet(JobSchedulerService jobManagerService) {
86         synchronized (sSingletonLock) {
87             if (sSingleton == null) {
88                 sSingleton = new JobStore(jobManagerService.getContext(),
89                         jobManagerService.getLock(), Environment.getDataDirectory());
90             }
91             return sSingleton;
92         }
93     }
94 
95     /**
96      * @return A freshly initialized job store object, with no loaded jobs.
97      */
98     @VisibleForTesting
initAndGetForTesting(Context context, File dataDir)99     public static JobStore initAndGetForTesting(Context context, File dataDir) {
100         JobStore jobStoreUnderTest = new JobStore(context, new Object(), dataDir);
101         jobStoreUnderTest.clear();
102         return jobStoreUnderTest;
103     }
104 
105     /**
106      * Construct the instance of the job store. This results in a blocking read from disk.
107      */
JobStore(Context context, Object lock, File dataDir)108     private JobStore(Context context, Object lock, File dataDir) {
109         mLock = lock;
110         mContext = context;
111         mDirtyOperations = 0;
112 
113         File systemDir = new File(dataDir, "system");
114         File jobDir = new File(systemDir, "job");
115         jobDir.mkdirs();
116         mJobsFile = new AtomicFile(new File(jobDir, "jobs.xml"));
117 
118         mJobSet = new JobSet();
119 
120         readJobMapFromDisk(mJobSet);
121     }
122 
123     /**
124      * Add a job to the master list, persisting it if necessary. If the JobStatus already exists,
125      * it will be replaced.
126      * @param jobStatus Job to add.
127      * @return Whether or not an equivalent JobStatus was replaced by this operation.
128      */
add(JobStatus jobStatus)129     public boolean add(JobStatus jobStatus) {
130         boolean replaced = mJobSet.remove(jobStatus);
131         mJobSet.add(jobStatus);
132         if (jobStatus.isPersisted()) {
133             maybeWriteStatusToDiskAsync();
134         }
135         if (DEBUG) {
136             Slog.d(TAG, "Added job status to store: " + jobStatus);
137         }
138         return replaced;
139     }
140 
containsJob(JobStatus jobStatus)141     boolean containsJob(JobStatus jobStatus) {
142         return mJobSet.contains(jobStatus);
143     }
144 
size()145     public int size() {
146         return mJobSet.size();
147     }
148 
countJobsForUid(int uid)149     public int countJobsForUid(int uid) {
150         return mJobSet.countJobsForUid(uid);
151     }
152 
153     /**
154      * Remove the provided job. Will also delete the job if it was persisted.
155      * @param writeBack If true, the job will be deleted (if it was persisted) immediately.
156      * @return Whether or not the job existed to be removed.
157      */
remove(JobStatus jobStatus, boolean writeBack)158     public boolean remove(JobStatus jobStatus, boolean writeBack) {
159         boolean removed = mJobSet.remove(jobStatus);
160         if (!removed) {
161             if (DEBUG) {
162                 Slog.d(TAG, "Couldn't remove job: didn't exist: " + jobStatus);
163             }
164             return false;
165         }
166         if (writeBack && jobStatus.isPersisted()) {
167             maybeWriteStatusToDiskAsync();
168         }
169         return removed;
170     }
171 
172     @VisibleForTesting
clear()173     public void clear() {
174         mJobSet.clear();
175         maybeWriteStatusToDiskAsync();
176     }
177 
178     /**
179      * @param userHandle User for whom we are querying the list of jobs.
180      * @return A list of all the jobs scheduled by the provided user. Never null.
181      */
getJobsByUser(int userHandle)182     public List<JobStatus> getJobsByUser(int userHandle) {
183         return mJobSet.getJobsByUser(userHandle);
184     }
185 
186     /**
187      * @param uid Uid of the requesting app.
188      * @return All JobStatus objects for a given uid from the master list. Never null.
189      */
getJobsByUid(int uid)190     public List<JobStatus> getJobsByUid(int uid) {
191         return mJobSet.getJobsByUid(uid);
192     }
193 
194     /**
195      * @param uid Uid of the requesting app.
196      * @param jobId Job id, specified at schedule-time.
197      * @return the JobStatus that matches the provided uId and jobId, or null if none found.
198      */
getJobByUidAndJobId(int uid, int jobId)199     public JobStatus getJobByUidAndJobId(int uid, int jobId) {
200         return mJobSet.get(uid, jobId);
201     }
202 
203     /**
204      * Iterate over the set of all jobs, invoking the supplied functor on each.  This is for
205      * customers who need to examine each job; we'd much rather not have to generate
206      * transient unified collections for them to iterate over and then discard, or creating
207      * iterators every time a client needs to perform a sweep.
208      */
forEachJob(JobStatusFunctor functor)209     public void forEachJob(JobStatusFunctor functor) {
210         mJobSet.forEachJob(functor);
211     }
212 
forEachJob(int uid, JobStatusFunctor functor)213     public void forEachJob(int uid, JobStatusFunctor functor) {
214         mJobSet.forEachJob(uid, functor);
215     }
216 
217     public interface JobStatusFunctor {
process(JobStatus jobStatus)218         public void process(JobStatus jobStatus);
219     }
220 
221     /** Version of the db schema. */
222     private static final int JOBS_FILE_VERSION = 0;
223     /** Tag corresponds to constraints this job needs. */
224     private static final String XML_TAG_PARAMS_CONSTRAINTS = "constraints";
225     /** Tag corresponds to execution parameters. */
226     private static final String XML_TAG_PERIODIC = "periodic";
227     private static final String XML_TAG_ONEOFF = "one-off";
228     private static final String XML_TAG_EXTRAS = "extras";
229 
230     /**
231      * Every time the state changes we write all the jobs in one swath, instead of trying to
232      * track incremental changes.
233      * @return Whether the operation was successful. This will only fail for e.g. if the system is
234      * low on storage. If this happens, we continue as normal
235      */
maybeWriteStatusToDiskAsync()236     private void maybeWriteStatusToDiskAsync() {
237         mDirtyOperations++;
238         if (mDirtyOperations >= MAX_OPS_BEFORE_WRITE) {
239             if (DEBUG) {
240                 Slog.v(TAG, "Writing jobs to disk.");
241             }
242             mIoHandler.post(new WriteJobsMapToDiskRunnable());
243         }
244     }
245 
246     @VisibleForTesting
readJobMapFromDisk(JobSet jobSet)247     public void readJobMapFromDisk(JobSet jobSet) {
248         new ReadJobMapFromDiskRunnable(jobSet).run();
249     }
250 
251     /**
252      * Runnable that writes {@link #mJobSet} out to xml.
253      * NOTE: This Runnable locks on mLock
254      */
255     private class WriteJobsMapToDiskRunnable implements Runnable {
256         @Override
run()257         public void run() {
258             final long startElapsed = SystemClock.elapsedRealtime();
259             final List<JobStatus> storeCopy = new ArrayList<JobStatus>();
260             synchronized (mLock) {
261                 // Clone the jobs so we can release the lock before writing.
262                 mJobSet.forEachJob(new JobStatusFunctor() {
263                     @Override
264                     public void process(JobStatus job) {
265                         if (job.isPersisted()) {
266                             storeCopy.add(new JobStatus(job));
267                         }
268                     }
269                 });
270             }
271             writeJobsMapImpl(storeCopy);
272             if (JobSchedulerService.DEBUG) {
273                 Slog.v(TAG, "Finished writing, took " + (SystemClock.elapsedRealtime()
274                         - startElapsed) + "ms");
275             }
276         }
277 
writeJobsMapImpl(List<JobStatus> jobList)278         private void writeJobsMapImpl(List<JobStatus> jobList) {
279             try {
280                 ByteArrayOutputStream baos = new ByteArrayOutputStream();
281                 XmlSerializer out = new FastXmlSerializer();
282                 out.setOutput(baos, StandardCharsets.UTF_8.name());
283                 out.startDocument(null, true);
284                 out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
285 
286                 out.startTag(null, "job-info");
287                 out.attribute(null, "version", Integer.toString(JOBS_FILE_VERSION));
288                 for (int i=0; i<jobList.size(); i++) {
289                     JobStatus jobStatus = jobList.get(i);
290                     if (DEBUG) {
291                         Slog.d(TAG, "Saving job " + jobStatus.getJobId());
292                     }
293                     out.startTag(null, "job");
294                     addAttributesToJobTag(out, jobStatus);
295                     writeConstraintsToXml(out, jobStatus);
296                     writeExecutionCriteriaToXml(out, jobStatus);
297                     writeBundleToXml(jobStatus.getExtras(), out);
298                     out.endTag(null, "job");
299                 }
300                 out.endTag(null, "job-info");
301                 out.endDocument();
302 
303                 // Write out to disk in one fell sweep.
304                 FileOutputStream fos = mJobsFile.startWrite();
305                 fos.write(baos.toByteArray());
306                 mJobsFile.finishWrite(fos);
307                 mDirtyOperations = 0;
308             } catch (IOException e) {
309                 if (DEBUG) {
310                     Slog.v(TAG, "Error writing out job data.", e);
311                 }
312             } catch (XmlPullParserException e) {
313                 if (DEBUG) {
314                     Slog.d(TAG, "Error persisting bundle.", e);
315                 }
316             }
317         }
318 
319         /** Write out a tag with data comprising the required fields and priority of this job and
320          * its client.
321          */
addAttributesToJobTag(XmlSerializer out, JobStatus jobStatus)322         private void addAttributesToJobTag(XmlSerializer out, JobStatus jobStatus)
323                 throws IOException {
324             out.attribute(null, "jobid", Integer.toString(jobStatus.getJobId()));
325             out.attribute(null, "package", jobStatus.getServiceComponent().getPackageName());
326             out.attribute(null, "class", jobStatus.getServiceComponent().getClassName());
327             if (jobStatus.getSourcePackageName() != null) {
328                 out.attribute(null, "sourcePackageName", jobStatus.getSourcePackageName());
329             }
330             if (jobStatus.getSourceTag() != null) {
331                 out.attribute(null, "sourceTag", jobStatus.getSourceTag());
332             }
333             out.attribute(null, "sourceUserId", String.valueOf(jobStatus.getSourceUserId()));
334             out.attribute(null, "uid", Integer.toString(jobStatus.getUid()));
335             out.attribute(null, "priority", String.valueOf(jobStatus.getPriority()));
336             out.attribute(null, "flags", String.valueOf(jobStatus.getFlags()));
337         }
338 
writeBundleToXml(PersistableBundle extras, XmlSerializer out)339         private void writeBundleToXml(PersistableBundle extras, XmlSerializer out)
340                 throws IOException, XmlPullParserException {
341             out.startTag(null, XML_TAG_EXTRAS);
342             PersistableBundle extrasCopy = deepCopyBundle(extras, 10);
343             extrasCopy.saveToXml(out);
344             out.endTag(null, XML_TAG_EXTRAS);
345         }
346 
deepCopyBundle(PersistableBundle bundle, int maxDepth)347         private PersistableBundle deepCopyBundle(PersistableBundle bundle, int maxDepth) {
348             if (maxDepth <= 0) {
349                 return null;
350             }
351             PersistableBundle copy = (PersistableBundle) bundle.clone();
352             Set<String> keySet = bundle.keySet();
353             for (String key: keySet) {
354                 Object o = copy.get(key);
355                 if (o instanceof PersistableBundle) {
356                     PersistableBundle bCopy = deepCopyBundle((PersistableBundle) o, maxDepth-1);
357                     copy.putPersistableBundle(key, bCopy);
358                 }
359             }
360             return copy;
361         }
362 
363         /**
364          * Write out a tag with data identifying this job's constraints. If the constraint isn't here
365          * it doesn't apply.
366          */
writeConstraintsToXml(XmlSerializer out, JobStatus jobStatus)367         private void writeConstraintsToXml(XmlSerializer out, JobStatus jobStatus) throws IOException {
368             out.startTag(null, XML_TAG_PARAMS_CONSTRAINTS);
369             if (jobStatus.hasConnectivityConstraint()) {
370                 out.attribute(null, "connectivity", Boolean.toString(true));
371             }
372             if (jobStatus.hasUnmeteredConstraint()) {
373                 out.attribute(null, "unmetered", Boolean.toString(true));
374             }
375             if (jobStatus.hasNotRoamingConstraint()) {
376                 out.attribute(null, "not-roaming", Boolean.toString(true));
377             }
378             if (jobStatus.hasIdleConstraint()) {
379                 out.attribute(null, "idle", Boolean.toString(true));
380             }
381             if (jobStatus.hasChargingConstraint()) {
382                 out.attribute(null, "charging", Boolean.toString(true));
383             }
384             out.endTag(null, XML_TAG_PARAMS_CONSTRAINTS);
385         }
386 
writeExecutionCriteriaToXml(XmlSerializer out, JobStatus jobStatus)387         private void writeExecutionCriteriaToXml(XmlSerializer out, JobStatus jobStatus)
388                 throws IOException {
389             final JobInfo job = jobStatus.getJob();
390             if (jobStatus.getJob().isPeriodic()) {
391                 out.startTag(null, XML_TAG_PERIODIC);
392                 out.attribute(null, "period", Long.toString(job.getIntervalMillis()));
393                 out.attribute(null, "flex", Long.toString(job.getFlexMillis()));
394             } else {
395                 out.startTag(null, XML_TAG_ONEOFF);
396             }
397 
398             if (jobStatus.hasDeadlineConstraint()) {
399                 // Wall clock deadline.
400                 final long deadlineWallclock =  System.currentTimeMillis() +
401                         (jobStatus.getLatestRunTimeElapsed() - SystemClock.elapsedRealtime());
402                 out.attribute(null, "deadline", Long.toString(deadlineWallclock));
403             }
404             if (jobStatus.hasTimingDelayConstraint()) {
405                 final long delayWallclock = System.currentTimeMillis() +
406                         (jobStatus.getEarliestRunTime() - SystemClock.elapsedRealtime());
407                 out.attribute(null, "delay", Long.toString(delayWallclock));
408             }
409 
410             // Only write out back-off policy if it differs from the default.
411             // This also helps the case where the job is idle -> these aren't allowed to specify
412             // back-off.
413             if (jobStatus.getJob().getInitialBackoffMillis() != JobInfo.DEFAULT_INITIAL_BACKOFF_MILLIS
414                     || jobStatus.getJob().getBackoffPolicy() != JobInfo.DEFAULT_BACKOFF_POLICY) {
415                 out.attribute(null, "backoff-policy", Integer.toString(job.getBackoffPolicy()));
416                 out.attribute(null, "initial-backoff", Long.toString(job.getInitialBackoffMillis()));
417             }
418             if (job.isPeriodic()) {
419                 out.endTag(null, XML_TAG_PERIODIC);
420             } else {
421                 out.endTag(null, XML_TAG_ONEOFF);
422             }
423         }
424     }
425 
426     /**
427      * Runnable that reads list of persisted job from xml. This is run once at start up, so doesn't
428      * need to go through {@link JobStore#add(com.android.server.job.controllers.JobStatus)}.
429      */
430     private class ReadJobMapFromDiskRunnable implements Runnable {
431         private final JobSet jobSet;
432 
433         /**
434          * @param jobSet Reference to the (empty) set of JobStatus objects that back the JobStore,
435          *               so that after disk read we can populate it directly.
436          */
ReadJobMapFromDiskRunnable(JobSet jobSet)437         ReadJobMapFromDiskRunnable(JobSet jobSet) {
438             this.jobSet = jobSet;
439         }
440 
441         @Override
run()442         public void run() {
443             try {
444                 List<JobStatus> jobs;
445                 FileInputStream fis = mJobsFile.openRead();
446                 synchronized (mLock) {
447                     jobs = readJobMapImpl(fis);
448                     if (jobs != null) {
449                         for (int i=0; i<jobs.size(); i++) {
450                             this.jobSet.add(jobs.get(i));
451                         }
452                     }
453                 }
454                 fis.close();
455             } catch (FileNotFoundException e) {
456                 if (JobSchedulerService.DEBUG) {
457                     Slog.d(TAG, "Could not find jobs file, probably there was nothing to load.");
458                 }
459             } catch (XmlPullParserException e) {
460                 if (JobSchedulerService.DEBUG) {
461                     Slog.d(TAG, "Error parsing xml.", e);
462                 }
463             } catch (IOException e) {
464                 if (JobSchedulerService.DEBUG) {
465                     Slog.d(TAG, "Error parsing xml.", e);
466                 }
467             }
468         }
469 
readJobMapImpl(FileInputStream fis)470         private List<JobStatus> readJobMapImpl(FileInputStream fis)
471                 throws XmlPullParserException, IOException {
472             XmlPullParser parser = Xml.newPullParser();
473             parser.setInput(fis, StandardCharsets.UTF_8.name());
474 
475             int eventType = parser.getEventType();
476             while (eventType != XmlPullParser.START_TAG &&
477                     eventType != XmlPullParser.END_DOCUMENT) {
478                 eventType = parser.next();
479                 Slog.d(TAG, "Start tag: " + parser.getName());
480             }
481             if (eventType == XmlPullParser.END_DOCUMENT) {
482                 if (DEBUG) {
483                     Slog.d(TAG, "No persisted jobs.");
484                 }
485                 return null;
486             }
487 
488             String tagName = parser.getName();
489             if ("job-info".equals(tagName)) {
490                 final List<JobStatus> jobs = new ArrayList<JobStatus>();
491                 // Read in version info.
492                 try {
493                     int version = Integer.parseInt(parser.getAttributeValue(null, "version"));
494                     if (version != JOBS_FILE_VERSION) {
495                         Slog.d(TAG, "Invalid version number, aborting jobs file read.");
496                         return null;
497                     }
498                 } catch (NumberFormatException e) {
499                     Slog.e(TAG, "Invalid version number, aborting jobs file read.");
500                     return null;
501                 }
502                 eventType = parser.next();
503                 do {
504                     // Read each <job/>
505                     if (eventType == XmlPullParser.START_TAG) {
506                         tagName = parser.getName();
507                         // Start reading job.
508                         if ("job".equals(tagName)) {
509                             JobStatus persistedJob = restoreJobFromXml(parser);
510                             if (persistedJob != null) {
511                                 if (DEBUG) {
512                                     Slog.d(TAG, "Read out " + persistedJob);
513                                 }
514                                 jobs.add(persistedJob);
515                             } else {
516                                 Slog.d(TAG, "Error reading job from file.");
517                             }
518                         }
519                     }
520                     eventType = parser.next();
521                 } while (eventType != XmlPullParser.END_DOCUMENT);
522                 return jobs;
523             }
524             return null;
525         }
526 
527         /**
528          * @param parser Xml parser at the beginning of a "<job/>" tag. The next "parser.next()" call
529          *               will take the parser into the body of the job tag.
530          * @return Newly instantiated job holding all the information we just read out of the xml tag.
531          */
restoreJobFromXml(XmlPullParser parser)532         private JobStatus restoreJobFromXml(XmlPullParser parser) throws XmlPullParserException,
533                 IOException {
534             JobInfo.Builder jobBuilder;
535             int uid, sourceUserId;
536 
537             // Read out job identifier attributes and priority.
538             try {
539                 jobBuilder = buildBuilderFromXml(parser);
540                 jobBuilder.setPersisted(true);
541                 uid = Integer.parseInt(parser.getAttributeValue(null, "uid"));
542 
543                 String val = parser.getAttributeValue(null, "priority");
544                 if (val != null) {
545                     jobBuilder.setPriority(Integer.parseInt(val));
546                 }
547                 val = parser.getAttributeValue(null, "flags");
548                 if (val != null) {
549                     jobBuilder.setFlags(Integer.parseInt(val));
550                 }
551                 val = parser.getAttributeValue(null, "sourceUserId");
552                 sourceUserId = val == null ? -1 : Integer.parseInt(val);
553             } catch (NumberFormatException e) {
554                 Slog.e(TAG, "Error parsing job's required fields, skipping");
555                 return null;
556             }
557 
558             String sourcePackageName = parser.getAttributeValue(null, "sourcePackageName");
559 
560             final String sourceTag = parser.getAttributeValue(null, "sourceTag");
561 
562             int eventType;
563             // Read out constraints tag.
564             do {
565                 eventType = parser.next();
566             } while (eventType == XmlPullParser.TEXT);  // Push through to next START_TAG.
567 
568             if (!(eventType == XmlPullParser.START_TAG &&
569                     XML_TAG_PARAMS_CONSTRAINTS.equals(parser.getName()))) {
570                 // Expecting a <constraints> start tag.
571                 return null;
572             }
573             try {
574                 buildConstraintsFromXml(jobBuilder, parser);
575             } catch (NumberFormatException e) {
576                 Slog.d(TAG, "Error reading constraints, skipping.");
577                 return null;
578             }
579             parser.next(); // Consume </constraints>
580 
581             // Read out execution parameters tag.
582             do {
583                 eventType = parser.next();
584             } while (eventType == XmlPullParser.TEXT);
585             if (eventType != XmlPullParser.START_TAG) {
586                 return null;
587             }
588 
589             // Tuple of (earliest runtime, latest runtime) in elapsed realtime after disk load.
590             Pair<Long, Long> elapsedRuntimes;
591             try {
592                 elapsedRuntimes = buildExecutionTimesFromXml(parser);
593             } catch (NumberFormatException e) {
594                 if (DEBUG) {
595                     Slog.d(TAG, "Error parsing execution time parameters, skipping.");
596                 }
597                 return null;
598             }
599 
600             final long elapsedNow = SystemClock.elapsedRealtime();
601             if (XML_TAG_PERIODIC.equals(parser.getName())) {
602                 try {
603                     String val = parser.getAttributeValue(null, "period");
604                     final long periodMillis = Long.valueOf(val);
605                     val = parser.getAttributeValue(null, "flex");
606                     final long flexMillis = (val != null) ? Long.valueOf(val) : periodMillis;
607                     jobBuilder.setPeriodic(periodMillis, flexMillis);
608                     // As a sanity check, cap the recreated run time to be no later than flex+period
609                     // from now. This is the latest the periodic could be pushed out. This could
610                     // happen if the periodic ran early (at flex time before period), and then the
611                     // device rebooted.
612                     if (elapsedRuntimes.second > elapsedNow + periodMillis + flexMillis) {
613                         final long clampedLateRuntimeElapsed = elapsedNow + flexMillis
614                                 + periodMillis;
615                         final long clampedEarlyRuntimeElapsed = clampedLateRuntimeElapsed
616                                 - flexMillis;
617                         Slog.w(TAG,
618                                 String.format("Periodic job for uid='%d' persisted run-time is" +
619                                                 " too big [%s, %s]. Clamping to [%s,%s]",
620                                         uid,
621                                         DateUtils.formatElapsedTime(elapsedRuntimes.first / 1000),
622                                         DateUtils.formatElapsedTime(elapsedRuntimes.second / 1000),
623                                         DateUtils.formatElapsedTime(
624                                                 clampedEarlyRuntimeElapsed / 1000),
625                                         DateUtils.formatElapsedTime(
626                                                 clampedLateRuntimeElapsed / 1000))
627                         );
628                         elapsedRuntimes =
629                                 Pair.create(clampedEarlyRuntimeElapsed, clampedLateRuntimeElapsed);
630                     }
631                 } catch (NumberFormatException e) {
632                     Slog.d(TAG, "Error reading periodic execution criteria, skipping.");
633                     return null;
634                 }
635             } else if (XML_TAG_ONEOFF.equals(parser.getName())) {
636                 try {
637                     if (elapsedRuntimes.first != JobStatus.NO_EARLIEST_RUNTIME) {
638                         jobBuilder.setMinimumLatency(elapsedRuntimes.first - elapsedNow);
639                     }
640                     if (elapsedRuntimes.second != JobStatus.NO_LATEST_RUNTIME) {
641                         jobBuilder.setOverrideDeadline(
642                                 elapsedRuntimes.second - elapsedNow);
643                     }
644                 } catch (NumberFormatException e) {
645                     Slog.d(TAG, "Error reading job execution criteria, skipping.");
646                     return null;
647                 }
648             } else {
649                 if (DEBUG) {
650                     Slog.d(TAG, "Invalid parameter tag, skipping - " + parser.getName());
651                 }
652                 // Expecting a parameters start tag.
653                 return null;
654             }
655             maybeBuildBackoffPolicyFromXml(jobBuilder, parser);
656 
657             parser.nextTag(); // Consume parameters end tag.
658 
659             // Read out extras Bundle.
660             do {
661                 eventType = parser.next();
662             } while (eventType == XmlPullParser.TEXT);
663             if (!(eventType == XmlPullParser.START_TAG
664                     && XML_TAG_EXTRAS.equals(parser.getName()))) {
665                 if (DEBUG) {
666                     Slog.d(TAG, "Error reading extras, skipping.");
667                 }
668                 return null;
669             }
670 
671             PersistableBundle extras = PersistableBundle.restoreFromXml(parser);
672             jobBuilder.setExtras(extras);
673             parser.nextTag(); // Consume </extras>
674 
675             // Migrate sync jobs forward from earlier, incomplete representation
676             if ("android".equals(sourcePackageName)
677                     && extras != null
678                     && extras.getBoolean("SyncManagerJob", false)) {
679                 sourcePackageName = extras.getString("owningPackage", sourcePackageName);
680                 if (DEBUG) {
681                     Slog.i(TAG, "Fixing up sync job source package name from 'android' to '"
682                             + sourcePackageName + "'");
683                 }
684             }
685 
686             // And now we're done
687             JobStatus js = new JobStatus(
688                     jobBuilder.build(), uid, sourcePackageName, sourceUserId, sourceTag,
689                     elapsedRuntimes.first, elapsedRuntimes.second);
690             return js;
691         }
692 
buildBuilderFromXml(XmlPullParser parser)693         private JobInfo.Builder buildBuilderFromXml(XmlPullParser parser) throws NumberFormatException {
694             // Pull out required fields from <job> attributes.
695             int jobId = Integer.parseInt(parser.getAttributeValue(null, "jobid"));
696             String packageName = parser.getAttributeValue(null, "package");
697             String className = parser.getAttributeValue(null, "class");
698             ComponentName cname = new ComponentName(packageName, className);
699 
700             return new JobInfo.Builder(jobId, cname);
701         }
702 
buildConstraintsFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser)703         private void buildConstraintsFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser) {
704             String val = parser.getAttributeValue(null, "connectivity");
705             if (val != null) {
706                 jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
707             }
708             val = parser.getAttributeValue(null, "unmetered");
709             if (val != null) {
710                 jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED);
711             }
712             val = parser.getAttributeValue(null, "not-roaming");
713             if (val != null) {
714                 jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_NOT_ROAMING);
715             }
716             val = parser.getAttributeValue(null, "idle");
717             if (val != null) {
718                 jobBuilder.setRequiresDeviceIdle(true);
719             }
720             val = parser.getAttributeValue(null, "charging");
721             if (val != null) {
722                 jobBuilder.setRequiresCharging(true);
723             }
724         }
725 
726         /**
727          * Builds the back-off policy out of the params tag. These attributes may not exist, depending
728          * on whether the back-off was set when the job was first scheduled.
729          */
maybeBuildBackoffPolicyFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser)730         private void maybeBuildBackoffPolicyFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser) {
731             String val = parser.getAttributeValue(null, "initial-backoff");
732             if (val != null) {
733                 long initialBackoff = Long.valueOf(val);
734                 val = parser.getAttributeValue(null, "backoff-policy");
735                 int backoffPolicy = Integer.parseInt(val);  // Will throw NFE which we catch higher up.
736                 jobBuilder.setBackoffCriteria(initialBackoff, backoffPolicy);
737             }
738         }
739 
740         /**
741          * Convenience function to read out and convert deadline and delay from xml into elapsed real
742          * time.
743          * @return A {@link android.util.Pair}, where the first value is the earliest elapsed runtime
744          * and the second is the latest elapsed runtime.
745          */
buildExecutionTimesFromXml(XmlPullParser parser)746         private Pair<Long, Long> buildExecutionTimesFromXml(XmlPullParser parser)
747                 throws NumberFormatException {
748             // Pull out execution time data.
749             final long nowWallclock = System.currentTimeMillis();
750             final long nowElapsed = SystemClock.elapsedRealtime();
751 
752             long earliestRunTimeElapsed = JobStatus.NO_EARLIEST_RUNTIME;
753             long latestRunTimeElapsed = JobStatus.NO_LATEST_RUNTIME;
754             String val = parser.getAttributeValue(null, "deadline");
755             if (val != null) {
756                 long latestRuntimeWallclock = Long.valueOf(val);
757                 long maxDelayElapsed =
758                         Math.max(latestRuntimeWallclock - nowWallclock, 0);
759                 latestRunTimeElapsed = nowElapsed + maxDelayElapsed;
760             }
761             val = parser.getAttributeValue(null, "delay");
762             if (val != null) {
763                 long earliestRuntimeWallclock = Long.valueOf(val);
764                 long minDelayElapsed =
765                         Math.max(earliestRuntimeWallclock - nowWallclock, 0);
766                 earliestRunTimeElapsed = nowElapsed + minDelayElapsed;
767 
768             }
769             return Pair.create(earliestRunTimeElapsed, latestRunTimeElapsed);
770         }
771     }
772 
773     static class JobSet {
774         // Key is the getUid() originator of the jobs in each sheaf
775         private SparseArray<ArraySet<JobStatus>> mJobs;
776 
JobSet()777         public JobSet() {
778             mJobs = new SparseArray<ArraySet<JobStatus>>();
779         }
780 
getJobsByUid(int uid)781         public List<JobStatus> getJobsByUid(int uid) {
782             ArrayList<JobStatus> matchingJobs = new ArrayList<JobStatus>();
783             ArraySet<JobStatus> jobs = mJobs.get(uid);
784             if (jobs != null) {
785                 matchingJobs.addAll(jobs);
786             }
787             return matchingJobs;
788         }
789 
790         // By user, not by uid, so we need to traverse by key and check
getJobsByUser(int userId)791         public List<JobStatus> getJobsByUser(int userId) {
792             ArrayList<JobStatus> result = new ArrayList<JobStatus>();
793             for (int i = mJobs.size() - 1; i >= 0; i--) {
794                 if (UserHandle.getUserId(mJobs.keyAt(i)) == userId) {
795                     ArraySet<JobStatus> jobs = mJobs.get(i);
796                     if (jobs != null) {
797                         result.addAll(jobs);
798                     }
799                 }
800             }
801             return result;
802         }
803 
add(JobStatus job)804         public boolean add(JobStatus job) {
805             final int uid = job.getUid();
806             ArraySet<JobStatus> jobs = mJobs.get(uid);
807             if (jobs == null) {
808                 jobs = new ArraySet<JobStatus>();
809                 mJobs.put(uid, jobs);
810             }
811             return jobs.add(job);
812         }
813 
remove(JobStatus job)814         public boolean remove(JobStatus job) {
815             final int uid = job.getUid();
816             ArraySet<JobStatus> jobs = mJobs.get(uid);
817             boolean didRemove = (jobs != null) ? jobs.remove(job) : false;
818             if (didRemove && jobs.size() == 0) {
819                 // no more jobs for this uid; let the now-empty set object be GC'd.
820                 mJobs.remove(uid);
821             }
822             return didRemove;
823         }
824 
contains(JobStatus job)825         public boolean contains(JobStatus job) {
826             final int uid = job.getUid();
827             ArraySet<JobStatus> jobs = mJobs.get(uid);
828             return jobs != null && jobs.contains(job);
829         }
830 
get(int uid, int jobId)831         public JobStatus get(int uid, int jobId) {
832             ArraySet<JobStatus> jobs = mJobs.get(uid);
833             if (jobs != null) {
834                 for (int i = jobs.size() - 1; i >= 0; i--) {
835                     JobStatus job = jobs.valueAt(i);
836                     if (job.getJobId() == jobId) {
837                         return job;
838                     }
839                 }
840             }
841             return null;
842         }
843 
844         // Inefficient; use only for testing
getAllJobs()845         public List<JobStatus> getAllJobs() {
846             ArrayList<JobStatus> allJobs = new ArrayList<JobStatus>(size());
847             for (int i = mJobs.size() - 1; i >= 0; i--) {
848                 ArraySet<JobStatus> jobs = mJobs.valueAt(i);
849                 if (jobs != null) {
850                     // Use a for loop over the ArraySet, so we don't need to make its
851                     // optional collection class iterator implementation or have to go
852                     // through a temporary array from toArray().
853                     for (int j = jobs.size() - 1; j >= 0; j--) {
854                         allJobs.add(jobs.valueAt(j));
855                     }
856                 }
857             }
858             return allJobs;
859         }
860 
clear()861         public void clear() {
862             mJobs.clear();
863         }
864 
size()865         public int size() {
866             int total = 0;
867             for (int i = mJobs.size() - 1; i >= 0; i--) {
868                 total += mJobs.valueAt(i).size();
869             }
870             return total;
871         }
872 
873         // We only want to count the jobs that this uid has scheduled on its own
874         // behalf, not those that the app has scheduled on someone else's behalf.
countJobsForUid(int uid)875         public int countJobsForUid(int uid) {
876             int total = 0;
877             ArraySet<JobStatus> jobs = mJobs.get(uid);
878             if (jobs != null) {
879                 for (int i = jobs.size() - 1; i >= 0; i--) {
880                     JobStatus job = jobs.valueAt(i);
881                     if (job.getUid() == job.getSourceUid()) {
882                         total++;
883                     }
884                 }
885             }
886             return total;
887         }
888 
forEachJob(JobStatusFunctor functor)889         public void forEachJob(JobStatusFunctor functor) {
890             for (int uidIndex = mJobs.size() - 1; uidIndex >= 0; uidIndex--) {
891                 ArraySet<JobStatus> jobs = mJobs.valueAt(uidIndex);
892                 for (int i = jobs.size() - 1; i >= 0; i--) {
893                     functor.process(jobs.valueAt(i));
894                 }
895             }
896         }
897 
forEachJob(int uid, JobStatusFunctor functor)898         public void forEachJob(int uid, JobStatusFunctor functor) {
899             ArraySet<JobStatus> jobs = mJobs.get(uid);
900             if (jobs != null) {
901                 for (int i = jobs.size() - 1; i >= 0; i--) {
902                     functor.process(jobs.valueAt(i));
903                 }
904             }
905         }
906     }
907 }
908