JobSchedulerService.java revision 900c67fc51fc2672458dd1c9641250f2ecc01a31
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 java.io.FileDescriptor;
20import java.io.PrintWriter;
21import java.util.ArrayList;
22import java.util.Iterator;
23import java.util.List;
24
25import android.app.AppGlobals;
26import android.app.job.JobInfo;
27import android.app.job.JobScheduler;
28import android.app.job.JobService;
29import android.app.job.IJobScheduler;
30import android.content.BroadcastReceiver;
31import android.content.ComponentName;
32import android.content.Context;
33import android.content.Intent;
34import android.content.IntentFilter;
35import android.content.pm.IPackageManager;
36import android.content.pm.PackageManager;
37import android.content.pm.ServiceInfo;
38import android.os.Binder;
39import android.os.Handler;
40import android.os.Looper;
41import android.os.Message;
42import android.os.RemoteException;
43import android.os.SystemClock;
44import android.os.UserHandle;
45import android.util.Slog;
46import android.util.SparseArray;
47
48import com.android.server.job.controllers.BatteryController;
49import com.android.server.job.controllers.ConnectivityController;
50import com.android.server.job.controllers.IdleController;
51import com.android.server.job.controllers.JobStatus;
52import com.android.server.job.controllers.StateController;
53import com.android.server.job.controllers.TimeController;
54
55import java.util.LinkedList;
56
57/**
58 * Responsible for taking jobs representing work to be performed by a client app, and determining
59 * based on the criteria specified when that job should be run against the client application's
60 * endpoint.
61 * Implements logic for scheduling, and rescheduling jobs. The JobSchedulerService knows nothing
62 * about constraints, or the state of active jobs. It receives callbacks from the various
63 * controllers and completed jobs and operates accordingly.
64 *
65 * Note on locking: Any operations that manipulate {@link #mJobs} need to lock on that object.
66 * Any function with the suffix 'Locked' also needs to lock on {@link #mJobs}.
67 * @hide
68 */
69public class JobSchedulerService extends com.android.server.SystemService
70        implements StateChangedListener, JobCompletedListener, JobMapReadFinishedListener {
71    // TODO: Switch this off for final version.
72    static final boolean DEBUG = true;
73    /** The number of concurrent jobs we run at one time. */
74    private static final int MAX_JOB_CONTEXTS_COUNT = 3;
75    static final String TAG = "JobManagerService";
76    /** Master list of jobs. */
77    private final JobStore mJobs;
78
79    static final int MSG_JOB_EXPIRED = 0;
80    static final int MSG_CHECK_JOB = 1;
81
82    // Policy constants
83    /**
84     * Minimum # of idle jobs that must be ready in order to force the JMS to schedule things
85     * early.
86     */
87    private static final int MIN_IDLE_COUNT = 1;
88    /**
89     * Minimum # of connectivity jobs that must be ready in order to force the JMS to schedule
90     * things early.
91     */
92    private static final int MIN_CONNECTIVITY_COUNT = 2;
93    /**
94     * Minimum # of jobs (with no particular constraints) for which the JMS will be happy running
95     * some work early.
96     */
97    private static final int MIN_READY_JOBS_COUNT = 4;
98
99    /**
100     * Track Services that have currently active or pending jobs. The index is provided by
101     * {@link JobStatus#getServiceToken()}
102     */
103    private final List<JobServiceContext> mActiveServices = new LinkedList<JobServiceContext>();
104    /** List of controllers that will notify this service of updates to jobs. */
105    private List<StateController> mControllers;
106    /**
107     * Queue of pending jobs. The JobServiceContext class will receive jobs from this list
108     * when ready to execute them.
109     */
110    private final LinkedList<JobStatus> mPendingJobs = new LinkedList<JobStatus>();
111
112    private final JobHandler mHandler;
113    private final JobSchedulerStub mJobSchedulerStub;
114    /**
115     * Cleans up outstanding jobs when a package is removed. Even if it's being replaced later we
116     * still clean up. On reinstall the package will have a new uid.
117     */
118    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
119        @Override
120        public void onReceive(Context context, Intent intent) {
121            Slog.d(TAG, "Receieved: " + intent.getAction());
122            if (Intent.ACTION_PACKAGE_REMOVED.equals(intent.getAction())) {
123                int uidRemoved = intent.getIntExtra(Intent.EXTRA_UID, -1);
124                if (DEBUG) {
125                    Slog.d(TAG, "Removing jobs for uid: " + uidRemoved);
126                }
127                cancelJobsForUid(uidRemoved);
128            } else if (Intent.ACTION_USER_REMOVED.equals(intent.getAction())) {
129                final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0);
130                if (DEBUG) {
131                    Slog.d(TAG, "Removing jobs for user: " + userId);
132                }
133                cancelJobsForUser(userId);
134            }
135        }
136    };
137
138    /**
139     * Entry point from client to schedule the provided job.
140     * This cancels the job if it's already been scheduled, and replaces it with the one provided.
141     * @param job JobInfo object containing execution parameters
142     * @param uId The package identifier of the application this job is for.
143     * @return Result of this operation. See <code>JobScheduler#RESULT_*</code> return codes.
144     */
145    public int schedule(JobInfo job, int uId) {
146        JobStatus jobStatus = new JobStatus(job, uId);
147        cancelJob(uId, job.getId());
148        startTrackingJob(jobStatus);
149        return JobScheduler.RESULT_SUCCESS;
150    }
151
152    public List<JobInfo> getPendingJobs(int uid) {
153        ArrayList<JobInfo> outList = new ArrayList<JobInfo>();
154        synchronized (mJobs) {
155            for (JobStatus job : mJobs.getJobs()) {
156                if (job.getUid() == uid) {
157                    outList.add(job.getJob());
158                }
159            }
160        }
161        return outList;
162    }
163
164    private void cancelJobsForUser(int userHandle) {
165        synchronized (mJobs) {
166            List<JobStatus> jobsForUser = mJobs.getJobsByUser(userHandle);
167            for (JobStatus toRemove : jobsForUser) {
168                if (DEBUG) {
169                    Slog.d(TAG, "Cancelling: " + toRemove);
170                }
171                cancelJobLocked(toRemove);
172            }
173        }
174    }
175
176    /**
177     * Entry point from client to cancel all jobs originating from their uid.
178     * This will remove the job from the master list, and cancel the job if it was staged for
179     * execution or being executed.
180     * @param uid To check against for removal of a job.
181     */
182    public void cancelJobsForUid(int uid) {
183        // Remove from master list.
184        synchronized (mJobs) {
185            List<JobStatus> jobsForUid = mJobs.getJobsByUid(uid);
186            for (JobStatus toRemove : jobsForUid) {
187                if (DEBUG) {
188                    Slog.d(TAG, "Cancelling: " + toRemove);
189                }
190                cancelJobLocked(toRemove);
191            }
192        }
193    }
194
195    /**
196     * Entry point from client to cancel the job corresponding to the jobId provided.
197     * This will remove the job from the master list, and cancel the job if it was staged for
198     * execution or being executed.
199     * @param uid Uid of the calling client.
200     * @param jobId Id of the job, provided at schedule-time.
201     */
202    public void cancelJob(int uid, int jobId) {
203        JobStatus toCancel;
204        synchronized (mJobs) {
205            toCancel = mJobs.getJobByUidAndJobId(uid, jobId);
206            if (toCancel != null) {
207                cancelJobLocked(toCancel);
208            }
209        }
210    }
211
212    private void cancelJobLocked(JobStatus cancelled) {
213        // Remove from store.
214        stopTrackingJob(cancelled);
215        // Remove from pending queue.
216        mPendingJobs.remove(cancelled);
217        // Cancel if running.
218        stopJobOnServiceContextLocked(cancelled);
219    }
220
221    /**
222     * Initializes the system service.
223     * <p>
224     * Subclasses must define a single argument constructor that accepts the context
225     * and passes it to super.
226     * </p>
227     *
228     * @param context The system server context.
229     */
230    public JobSchedulerService(Context context) {
231        super(context);
232        // Create the controllers.
233        mControllers = new LinkedList<StateController>();
234        mControllers.add(ConnectivityController.get(this));
235        mControllers.add(TimeController.get(this));
236        mControllers.add(IdleController.get(this));
237        mControllers.add(BatteryController.get(this));
238
239        mHandler = new JobHandler(context.getMainLooper());
240        mJobSchedulerStub = new JobSchedulerStub();
241        // Create the "runners".
242        for (int i = 0; i < MAX_JOB_CONTEXTS_COUNT; i++) {
243            mActiveServices.add(
244                    new JobServiceContext(this, context.getMainLooper()));
245        }
246        mJobs = JobStore.initAndGet(this);
247    }
248
249    @Override
250    public void onStart() {
251        publishBinderService(Context.JOB_SCHEDULER_SERVICE, mJobSchedulerStub);
252    }
253
254    @Override
255    public void onBootPhase(int phase) {
256        if (PHASE_SYSTEM_SERVICES_READY == phase) {
257            // Register br for package removals and user removals.
258            final IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_REMOVED);
259            filter.addDataScheme("package");
260            getContext().registerReceiverAsUser(
261                    mBroadcastReceiver, UserHandle.ALL, filter, null, null);
262            final IntentFilter userFilter = new IntentFilter(Intent.ACTION_USER_REMOVED);
263            getContext().registerReceiverAsUser(
264                    mBroadcastReceiver, UserHandle.ALL, userFilter, null, null);
265        }
266    }
267
268    /**
269     * Called when we have a job status object that we need to insert in our
270     * {@link com.android.server.job.JobStore}, and make sure all the relevant controllers know
271     * about.
272     */
273    private void startTrackingJob(JobStatus jobStatus) {
274        boolean update;
275        synchronized (mJobs) {
276            update = mJobs.add(jobStatus);
277        }
278        for (StateController controller : mControllers) {
279            if (update) {
280                controller.maybeStopTrackingJob(jobStatus);
281            }
282            controller.maybeStartTrackingJob(jobStatus);
283        }
284    }
285
286    /**
287     * Called when we want to remove a JobStatus object that we've finished executing. Returns the
288     * object removed.
289     */
290    private boolean stopTrackingJob(JobStatus jobStatus) {
291        boolean removed;
292        synchronized (mJobs) {
293            // Remove from store as well as controllers.
294            removed = mJobs.remove(jobStatus);
295        }
296        if (removed) {
297            for (StateController controller : mControllers) {
298                controller.maybeStopTrackingJob(jobStatus);
299            }
300        }
301        return removed;
302    }
303
304    private boolean stopJobOnServiceContextLocked(JobStatus job) {
305        for (JobServiceContext jsc : mActiveServices) {
306            final JobStatus executing = jsc.getRunningJob();
307            if (executing != null && executing.matches(job.getUid(), job.getJobId())) {
308                jsc.cancelExecutingJob();
309                return true;
310            }
311        }
312        return false;
313    }
314
315    /**
316     * @param job JobStatus we are querying against.
317     * @return Whether or not the job represented by the status object is currently being run or
318     * is pending.
319     */
320    private boolean isCurrentlyActiveLocked(JobStatus job) {
321        for (JobServiceContext serviceContext : mActiveServices) {
322            final JobStatus running = serviceContext.getRunningJob();
323            if (running != null && running.matches(job.getUid(), job.getJobId())) {
324                return true;
325            }
326        }
327        return false;
328    }
329
330    /**
331     * A job is rescheduled with exponential back-off if the client requests this from their
332     * execution logic.
333     * A caveat is for idle-mode jobs, for which the idle-mode constraint will usurp the
334     * timeliness of the reschedule. For an idle-mode job, no deadline is given.
335     * @param failureToReschedule Provided job status that we will reschedule.
336     * @return A newly instantiated JobStatus with the same constraints as the last job except
337     * with adjusted timing constraints.
338     */
339    private JobStatus getRescheduleJobForFailure(JobStatus failureToReschedule) {
340        final long elapsedNowMillis = SystemClock.elapsedRealtime();
341        final JobInfo job = failureToReschedule.getJob();
342
343        final long initialBackoffMillis = job.getInitialBackoffMillis();
344        final int backoffAttempt = failureToReschedule.getNumFailures() + 1;
345        long newEarliestRuntimeElapsed = elapsedNowMillis;
346
347        switch (job.getBackoffPolicy()) {
348            case JobInfo.BackoffPolicy.LINEAR:
349                newEarliestRuntimeElapsed += initialBackoffMillis * backoffAttempt;
350                break;
351            default:
352                if (DEBUG) {
353                    Slog.v(TAG, "Unrecognised back-off policy, defaulting to exponential.");
354                }
355            case JobInfo.BackoffPolicy.EXPONENTIAL:
356                newEarliestRuntimeElapsed +=
357                        Math.pow(initialBackoffMillis * 0.001, backoffAttempt) * 1000;
358                break;
359        }
360        newEarliestRuntimeElapsed =
361                Math.min(newEarliestRuntimeElapsed, JobInfo.MAX_BACKOFF_DELAY_MILLIS);
362        return new JobStatus(failureToReschedule, newEarliestRuntimeElapsed,
363                JobStatus.NO_LATEST_RUNTIME, backoffAttempt);
364    }
365
366    /**
367     * Called after a periodic has executed so we can to re-add it. We take the last execution time
368     * of the job to be the time of completion (i.e. the time at which this function is called).
369     * This could be inaccurate b/c the job can run for as long as
370     * {@link com.android.server.job.JobServiceContext#EXECUTING_TIMESLICE_MILLIS}, but will lead
371     * to underscheduling at least, rather than if we had taken the last execution time to be the
372     * start of the execution.
373     * @return A new job representing the execution criteria for this instantiation of the
374     * recurring job.
375     */
376    private JobStatus getRescheduleJobForPeriodic(JobStatus periodicToReschedule) {
377        final long elapsedNow = SystemClock.elapsedRealtime();
378        // Compute how much of the period is remaining.
379        long runEarly = Math.max(periodicToReschedule.getLatestRunTimeElapsed() - elapsedNow, 0);
380        long newEarliestRunTimeElapsed = elapsedNow + runEarly;
381        long period = periodicToReschedule.getJob().getIntervalMillis();
382        long newLatestRuntimeElapsed = newEarliestRunTimeElapsed + period;
383
384        if (DEBUG) {
385            Slog.v(TAG, "Rescheduling executed periodic. New execution window [" +
386                    newEarliestRunTimeElapsed/1000 + ", " + newLatestRuntimeElapsed/1000 + "]s");
387        }
388        return new JobStatus(periodicToReschedule, newEarliestRunTimeElapsed,
389                newLatestRuntimeElapsed, 0 /* backoffAttempt */);
390    }
391
392    // JobCompletedListener implementations.
393
394    /**
395     * A job just finished executing. We fetch the
396     * {@link com.android.server.job.controllers.JobStatus} from the store and depending on
397     * whether we want to reschedule we readd it to the controllers.
398     * @param jobStatus Completed job.
399     * @param needsReschedule Whether the implementing class should reschedule this job.
400     */
401    @Override
402    public void onJobCompleted(JobStatus jobStatus, boolean needsReschedule) {
403        if (DEBUG) {
404            Slog.d(TAG, "Completed " + jobStatus + ", reschedule=" + needsReschedule);
405        }
406        if (!stopTrackingJob(jobStatus)) {
407            if (DEBUG) {
408                Slog.e(TAG, "Error removing job: could not find job to remove. Was job " +
409                        "removed while executing?");
410            }
411            return;
412        }
413        if (needsReschedule) {
414            JobStatus rescheduled = getRescheduleJobForFailure(jobStatus);
415            startTrackingJob(rescheduled);
416        } else if (jobStatus.getJob().isPeriodic()) {
417            JobStatus rescheduledPeriodic = getRescheduleJobForPeriodic(jobStatus);
418            startTrackingJob(rescheduledPeriodic);
419        }
420        mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget();
421    }
422
423    // StateChangedListener implementations.
424
425    /**
426     * Off-board work to our handler thread as quickly as possible, b/c this call is probably being
427     * made on the main thread.
428     * For now this takes the job and if it's ready to run it will run it. In future we might not
429     * provide the job, so that the StateChangedListener has to run through its list of jobs to
430     * see which are ready. This will further decouple the controllers from the execution logic.
431     */
432    @Override
433    public void onControllerStateChanged() {
434        // Post a message to to run through the list of jobs and start/stop any that are eligible.
435        mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget();
436    }
437
438    @Override
439    public void onRunJobNow(JobStatus jobStatus) {
440        mHandler.obtainMessage(MSG_JOB_EXPIRED, jobStatus).sendToTarget();
441    }
442
443    /**
444     * Disk I/O is finished, take the list of jobs we read from disk and add them to our
445     * {@link JobStore}.
446     * This is run on the {@link com.android.server.IoThread} instance, which is a separate thread,
447     * and is called once at boot.
448     */
449    @Override
450    public void onJobMapReadFinished(List<JobStatus> jobs) {
451        synchronized (mJobs) {
452            for (JobStatus js : jobs) {
453                if (mJobs.containsJobIdForUid(js.getJobId(), js.getUid())) {
454                    // An app with BOOT_COMPLETED *might* have decided to reschedule their job, in
455                    // the same amount of time it took us to read it from disk. If this is the case
456                    // we leave it be.
457                    continue;
458                }
459                startTrackingJob(js);
460            }
461        }
462    }
463
464    private class JobHandler extends Handler {
465
466        public JobHandler(Looper looper) {
467            super(looper);
468        }
469
470        @Override
471        public void handleMessage(Message message) {
472            switch (message.what) {
473                case MSG_JOB_EXPIRED:
474                    synchronized (mJobs) {
475                        JobStatus runNow = (JobStatus) message.obj;
476                        if (!mPendingJobs.contains(runNow)) {
477                            mPendingJobs.add(runNow);
478                        }
479                    }
480                    queueReadyJobsForExecutionH();
481                    break;
482                case MSG_CHECK_JOB:
483                    // Check the list of jobs and run some of them if we feel inclined.
484                    maybeQueueReadyJobsForExecutionH();
485                    break;
486            }
487            maybeRunPendingJobsH();
488            // Don't remove JOB_EXPIRED in case one came along while processing the queue.
489            removeMessages(MSG_CHECK_JOB);
490        }
491
492        /**
493         * Run through list of jobs and execute all possible - at least one is expired so we do
494         * as many as we can.
495         */
496        private void queueReadyJobsForExecutionH() {
497            synchronized (mJobs) {
498                for (JobStatus job : mJobs.getJobs()) {
499                    if (isReadyToBeExecutedLocked(job)) {
500                        mPendingJobs.add(job);
501                    } else if (isReadyToBeCancelledLocked(job)) {
502                        stopJobOnServiceContextLocked(job);
503                    }
504                }
505            }
506        }
507
508        /**
509         * The state of at least one job has changed. Here is where we could enforce various
510         * policies on when we want to execute jobs.
511         * Right now the policy is such:
512         * If >1 of the ready jobs is idle mode we send all of them off
513         * if more than 2 network connectivity jobs are ready we send them all off.
514         * If more than 4 jobs total are ready we send them all off.
515         * TODO: It would be nice to consolidate these sort of high-level policies somewhere.
516         */
517        private void maybeQueueReadyJobsForExecutionH() {
518            synchronized (mJobs) {
519                int idleCount = 0;
520                int backoffCount = 0;
521                int connectivityCount = 0;
522                List<JobStatus> runnableJobs = new ArrayList<JobStatus>();
523                for (JobStatus job : mJobs.getJobs()) {
524                    if (isReadyToBeExecutedLocked(job)) {
525                        if (job.getNumFailures() > 0) {
526                            backoffCount++;
527                        }
528                        if (job.hasIdleConstraint()) {
529                            idleCount++;
530                        }
531                        if (job.hasConnectivityConstraint() || job.hasUnmeteredConstraint()) {
532                            connectivityCount++;
533                        }
534                        runnableJobs.add(job);
535                    } else if (isReadyToBeCancelledLocked(job)) {
536                        stopJobOnServiceContextLocked(job);
537                    }
538                }
539                if (backoffCount > 0 || idleCount >= MIN_IDLE_COUNT ||
540                        connectivityCount >= MIN_CONNECTIVITY_COUNT ||
541                        runnableJobs.size() >= MIN_READY_JOBS_COUNT) {
542                    for (JobStatus job : runnableJobs) {
543                        mPendingJobs.add(job);
544                    }
545                }
546            }
547        }
548
549        /**
550         * Criteria for moving a job into the pending queue:
551         *      - It's ready.
552         *      - It's not pending.
553         *      - It's not already running on a JSC.
554         */
555        private boolean isReadyToBeExecutedLocked(JobStatus job) {
556              return job.isReady() && !mPendingJobs.contains(job) && !isCurrentlyActiveLocked(job);
557        }
558
559        /**
560         * Criteria for cancelling an active job:
561         *      - It's not ready
562         *      - It's running on a JSC.
563         */
564        private boolean isReadyToBeCancelledLocked(JobStatus job) {
565            return !job.isReady() && isCurrentlyActiveLocked(job);
566        }
567
568        /**
569         * Reconcile jobs in the pending queue against available execution contexts.
570         * A controller can force a job into the pending queue even if it's already running, but
571         * here is where we decide whether to actually execute it.
572         */
573        private void maybeRunPendingJobsH() {
574            synchronized (mJobs) {
575                Iterator<JobStatus> it = mPendingJobs.iterator();
576                while (it.hasNext()) {
577                    JobStatus nextPending = it.next();
578                    JobServiceContext availableContext = null;
579                    for (JobServiceContext jsc : mActiveServices) {
580                        final JobStatus running = jsc.getRunningJob();
581                        if (running != null && running.matches(nextPending.getUid(),
582                                nextPending.getJobId())) {
583                            // Already running this tId for this uId, skip.
584                            availableContext = null;
585                            break;
586                        }
587                        if (jsc.isAvailable()) {
588                            availableContext = jsc;
589                        }
590                    }
591                    if (availableContext != null) {
592                        if (!availableContext.executeRunnableJob(nextPending)) {
593                            if (DEBUG) {
594                                Slog.d(TAG, "Error executing " + nextPending);
595                            }
596                            mJobs.remove(nextPending);
597                        }
598                        it.remove();
599                    }
600                }
601            }
602        }
603    }
604
605    /**
606     * Binder stub trampoline implementation
607     */
608    final class JobSchedulerStub extends IJobScheduler.Stub {
609        /** Cache determination of whether a given app can persist jobs
610         * key is uid of the calling app; value is undetermined/true/false
611         */
612        private final SparseArray<Boolean> mPersistCache = new SparseArray<Boolean>();
613
614        // Enforce that only the app itself (or shared uid participant) can schedule a
615        // job that runs one of the app's services, as well as verifying that the
616        // named service properly requires the BIND_JOB_SERVICE permission
617        private void enforceValidJobRequest(int uid, JobInfo job) {
618            final IPackageManager pm = AppGlobals.getPackageManager();
619            final ComponentName service = job.getService();
620            try {
621                ServiceInfo si = pm.getServiceInfo(service, 0, UserHandle.getUserId(uid));
622                if (si == null) {
623                    throw new IllegalArgumentException("No such service " + service);
624                }
625                if (si.applicationInfo.uid != uid) {
626                    throw new IllegalArgumentException("uid " + uid +
627                            " cannot schedule job in " + service.getPackageName());
628                }
629                if (!JobService.PERMISSION_BIND.equals(si.permission)) {
630                    throw new IllegalArgumentException("Scheduled service " + service
631                            + " does not require android.permission.BIND_JOB_SERVICE permission");
632                }
633            } catch (RemoteException e) {
634                // Can't happen; the Package Manager is in this same process
635            }
636        }
637
638        private boolean canPersistJobs(int pid, int uid) {
639            // If we get this far we're good to go; all we need to do now is check
640            // whether the app is allowed to persist its scheduled work.
641            final boolean canPersist;
642            synchronized (mPersistCache) {
643                Boolean cached = mPersistCache.get(uid);
644                if (cached != null) {
645                    canPersist = cached.booleanValue();
646                } else {
647                    // Persisting jobs is tantamount to running at boot, so we permit
648                    // it when the app has declared that it uses the RECEIVE_BOOT_COMPLETED
649                    // permission
650                    int result = getContext().checkPermission(
651                            android.Manifest.permission.RECEIVE_BOOT_COMPLETED, pid, uid);
652                    canPersist = (result == PackageManager.PERMISSION_GRANTED);
653                    mPersistCache.put(uid, canPersist);
654                }
655            }
656            return canPersist;
657        }
658
659        // IJobScheduler implementation
660        @Override
661        public int schedule(JobInfo job) throws RemoteException {
662            if (DEBUG) {
663                Slog.d(TAG, "Scheduling job: " + job);
664            }
665            final int pid = Binder.getCallingPid();
666            final int uid = Binder.getCallingUid();
667
668            enforceValidJobRequest(uid, job);
669            if (job.isPersisted()) {
670                if (!canPersistJobs(pid, uid)) {
671                    throw new IllegalArgumentException("Error: requested job be persisted without"
672                            + " holding RECEIVE_BOOT_COMPLETED permission.");
673                }
674            }
675
676            long ident = Binder.clearCallingIdentity();
677            try {
678                return JobSchedulerService.this.schedule(job, uid);
679            } finally {
680                Binder.restoreCallingIdentity(ident);
681            }
682        }
683
684        @Override
685        public List<JobInfo> getAllPendingJobs() throws RemoteException {
686            final int uid = Binder.getCallingUid();
687
688            long ident = Binder.clearCallingIdentity();
689            try {
690                return JobSchedulerService.this.getPendingJobs(uid);
691            } finally {
692                Binder.restoreCallingIdentity(ident);
693            }
694        }
695
696        @Override
697        public void cancelAll() throws RemoteException {
698            final int uid = Binder.getCallingUid();
699
700            long ident = Binder.clearCallingIdentity();
701            try {
702                JobSchedulerService.this.cancelJobsForUid(uid);
703            } finally {
704                Binder.restoreCallingIdentity(ident);
705            }
706        }
707
708        @Override
709        public void cancel(int jobId) throws RemoteException {
710            final int uid = Binder.getCallingUid();
711
712            long ident = Binder.clearCallingIdentity();
713            try {
714                JobSchedulerService.this.cancelJob(uid, jobId);
715            } finally {
716                Binder.restoreCallingIdentity(ident);
717            }
718        }
719
720        /**
721         * "dumpsys" infrastructure
722         */
723        @Override
724        public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
725            getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, TAG);
726
727            long identityToken = Binder.clearCallingIdentity();
728            try {
729                JobSchedulerService.this.dumpInternal(pw);
730            } finally {
731                Binder.restoreCallingIdentity(identityToken);
732            }
733        }
734    };
735
736    void dumpInternal(PrintWriter pw) {
737        synchronized (mJobs) {
738            pw.println("Registered jobs:");
739            if (mJobs.size() > 0) {
740                for (JobStatus job : mJobs.getJobs()) {
741                    job.dump(pw, "  ");
742                }
743            } else {
744                pw.println();
745                pw.println("No jobs scheduled.");
746            }
747            for (StateController controller : mControllers) {
748                pw.println();
749                controller.dumpControllerState(pw);
750            }
751            pw.println();
752            pw.println("Pending");
753            for (JobStatus jobStatus : mPendingJobs) {
754                pw.println(jobStatus.hashCode());
755            }
756            pw.println();
757            pw.println("Active jobs:");
758            for (JobServiceContext jsc : mActiveServices) {
759                if (jsc.isAvailable()) {
760                    continue;
761                } else {
762                    pw.println(jsc.getRunningJob().hashCode() + " for: " +
763                            (SystemClock.elapsedRealtime()
764                                    - jsc.getExecutionStartTimeElapsed())/1000 + "s " +
765                            "timeout: " + jsc.getTimeoutElapsed());
766                }
767            }
768        }
769        pw.println();
770    }
771}
772