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