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
17package com.android.server.job;
18
19import android.app.ActivityManager;
20import android.app.IActivityManager;
21import android.content.ComponentName;
22import android.app.job.JobInfo;
23import android.content.Context;
24import android.os.Environment;
25import android.os.Handler;
26import android.os.PersistableBundle;
27import android.os.SystemClock;
28import android.os.UserHandle;
29import android.text.format.DateUtils;
30import android.util.AtomicFile;
31import android.util.ArraySet;
32import android.util.Pair;
33import android.util.Slog;
34import android.util.SparseArray;
35import android.util.Xml;
36
37import com.android.internal.annotations.VisibleForTesting;
38import com.android.internal.util.ArrayUtils;
39import com.android.internal.util.FastXmlSerializer;
40import com.android.server.IoThread;
41import com.android.server.job.controllers.JobStatus;
42
43import java.io.ByteArrayOutputStream;
44import java.io.File;
45import java.io.FileInputStream;
46import java.io.FileNotFoundException;
47import java.io.FileOutputStream;
48import java.io.IOException;
49import java.nio.charset.StandardCharsets;
50import java.util.ArrayList;
51import java.util.List;
52import java.util.Set;
53
54import org.xmlpull.v1.XmlPullParser;
55import org.xmlpull.v1.XmlPullParserException;
56import 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 */
69public 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. */
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
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     */
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     */
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
144    boolean containsJob(JobStatus jobStatus) {
145        return mJobSet.contains(jobStatus);
146    }
147
148    public int size() {
149        return mJobSet.size();
150    }
151
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     */
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     */
179    public void removeJobsOfNonUsers(int[] whitelist) {
180        mJobSet.removeJobsOfNonUsers(whitelist);
181    }
182
183    @VisibleForTesting
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     */
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     */
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     */
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     */
220    public void forEachJob(JobStatusFunctor functor) {
221        mJobSet.forEachJob(functor);
222    }
223
224    public void forEachJob(int uid, JobStatusFunctor functor) {
225        mJobSet.forEachJob(uid, functor);
226    }
227
228    public interface JobStatusFunctor {
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     */
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
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
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
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         */
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
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
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         */
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
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         */
454        ReadJobMapFromDiskRunnable(JobSet jobSet) {
455            this.jobSet = jobSet;
456        }
457
458        @Override
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
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         */
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
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
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         */
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         */
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
803        public JobSet() {
804            mJobs = new SparseArray<ArraySet<JobStatus>>();
805        }
806
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
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
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
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
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
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
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
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
898        public void clear() {
899            mJobs.clear();
900        }
901
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.
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
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
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