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