blob: c3bc306974a55ac32c34051439f805c0230f4550 [file] [log] [blame]
Christopher Tate7060b042014-06-09 19:50:00 -07001/*
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
Christopher Tate5568f542014-06-18 13:53:31 -070025import android.app.AppGlobals;
Christopher Tate7060b042014-06-09 19:50:00 -070026import 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;
Christopher Tate5568f542014-06-18 13:53:31 -070035import android.content.pm.IPackageManager;
Christopher Tate7060b042014-06-09 19:50:00 -070036import android.content.pm.PackageManager;
Christopher Tate7060b042014-06-09 19:50:00 -070037import android.content.pm.ServiceInfo;
Dianne Hackbornfdb19562014-07-11 16:03:36 -070038import android.os.BatteryStats;
Christopher Tate7060b042014-06-09 19:50:00 -070039import android.os.Binder;
40import android.os.Handler;
41import android.os.Looper;
42import android.os.Message;
43import android.os.RemoteException;
Dianne Hackbornfdb19562014-07-11 16:03:36 -070044import android.os.ServiceManager;
Christopher Tate7060b042014-06-09 19:50:00 -070045import android.os.SystemClock;
46import android.os.UserHandle;
Dianne Hackbornfdb19562014-07-11 16:03:36 -070047import android.util.ArraySet;
Christopher Tate7060b042014-06-09 19:50:00 -070048import android.util.Slog;
49import android.util.SparseArray;
50
Dianne Hackbornfdb19562014-07-11 16:03:36 -070051import com.android.internal.app.IBatteryStats;
Christopher Tate7060b042014-06-09 19:50:00 -070052import com.android.server.job.controllers.BatteryController;
53import com.android.server.job.controllers.ConnectivityController;
54import com.android.server.job.controllers.IdleController;
55import com.android.server.job.controllers.JobStatus;
56import com.android.server.job.controllers.StateController;
57import com.android.server.job.controllers.TimeController;
58
Christopher Tate7060b042014-06-09 19:50:00 -070059/**
60 * Responsible for taking jobs representing work to be performed by a client app, and determining
61 * based on the criteria specified when that job should be run against the client application's
62 * endpoint.
63 * Implements logic for scheduling, and rescheduling jobs. The JobSchedulerService knows nothing
64 * about constraints, or the state of active jobs. It receives callbacks from the various
65 * controllers and completed jobs and operates accordingly.
66 *
67 * Note on locking: Any operations that manipulate {@link #mJobs} need to lock on that object.
68 * Any function with the suffix 'Locked' also needs to lock on {@link #mJobs}.
69 * @hide
70 */
71public class JobSchedulerService extends com.android.server.SystemService
Matthew Williams01ac45b2014-07-22 20:44:12 -070072 implements StateChangedListener, JobCompletedListener {
Christopher Tate7060b042014-06-09 19:50:00 -070073 // TODO: Switch this off for final version.
74 static final boolean DEBUG = true;
75 /** The number of concurrent jobs we run at one time. */
76 private static final int MAX_JOB_CONTEXTS_COUNT = 3;
Matthew Williamsbe0c4172014-08-06 18:14:16 -070077 static final String TAG = "JobSchedulerService";
Christopher Tate7060b042014-06-09 19:50:00 -070078 /** Master list of jobs. */
Dianne Hackbornfdb19562014-07-11 16:03:36 -070079 final JobStore mJobs;
Christopher Tate7060b042014-06-09 19:50:00 -070080
81 static final int MSG_JOB_EXPIRED = 0;
82 static final int MSG_CHECK_JOB = 1;
83
84 // Policy constants
85 /**
86 * Minimum # of idle jobs that must be ready in order to force the JMS to schedule things
87 * early.
88 */
Dianne Hackbornfdb19562014-07-11 16:03:36 -070089 static final int MIN_IDLE_COUNT = 1;
Christopher Tate7060b042014-06-09 19:50:00 -070090 /**
Matthew Williamsbe0c4172014-08-06 18:14:16 -070091 * Minimum # of charging jobs that must be ready in order to force the JMS to schedule things
92 * early.
93 */
94 static final int MIN_CHARGING_COUNT = 1;
95 /**
Christopher Tate7060b042014-06-09 19:50:00 -070096 * Minimum # of connectivity jobs that must be ready in order to force the JMS to schedule
97 * things early.
98 */
Dianne Hackbornfdb19562014-07-11 16:03:36 -070099 static final int MIN_CONNECTIVITY_COUNT = 2;
Christopher Tate7060b042014-06-09 19:50:00 -0700100 /**
101 * Minimum # of jobs (with no particular constraints) for which the JMS will be happy running
102 * some work early.
Matthew Williamsbe0c4172014-08-06 18:14:16 -0700103 * This is correlated with the amount of batching we'll be able to do.
Christopher Tate7060b042014-06-09 19:50:00 -0700104 */
Matthew Williamsbe0c4172014-08-06 18:14:16 -0700105 static final int MIN_READY_JOBS_COUNT = 2;
Christopher Tate7060b042014-06-09 19:50:00 -0700106
107 /**
108 * Track Services that have currently active or pending jobs. The index is provided by
109 * {@link JobStatus#getServiceToken()}
110 */
Dianne Hackbornfdb19562014-07-11 16:03:36 -0700111 final List<JobServiceContext> mActiveServices = new ArrayList<JobServiceContext>();
Christopher Tate7060b042014-06-09 19:50:00 -0700112 /** List of controllers that will notify this service of updates to jobs. */
Dianne Hackbornfdb19562014-07-11 16:03:36 -0700113 List<StateController> mControllers;
Christopher Tate7060b042014-06-09 19:50:00 -0700114 /**
115 * Queue of pending jobs. The JobServiceContext class will receive jobs from this list
116 * when ready to execute them.
117 */
Dianne Hackbornfdb19562014-07-11 16:03:36 -0700118 final ArrayList<JobStatus> mPendingJobs = new ArrayList<JobStatus>();
Christopher Tate7060b042014-06-09 19:50:00 -0700119
Matthew Williams9ae3dbe2014-08-21 13:47:47 -0700120 final ArrayList<Integer> mStartedUsers = new ArrayList();
121
Dianne Hackbornfdb19562014-07-11 16:03:36 -0700122 final JobHandler mHandler;
123 final JobSchedulerStub mJobSchedulerStub;
124
125 IBatteryStats mBatteryStats;
126
127 /**
128 * Set to true once we are allowed to run third party apps.
129 */
130 boolean mReadyToRock;
131
Christopher Tate7060b042014-06-09 19:50:00 -0700132 /**
133 * Cleans up outstanding jobs when a package is removed. Even if it's being replaced later we
134 * still clean up. On reinstall the package will have a new uid.
135 */
136 private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
137 @Override
138 public void onReceive(Context context, Intent intent) {
139 Slog.d(TAG, "Receieved: " + intent.getAction());
140 if (Intent.ACTION_PACKAGE_REMOVED.equals(intent.getAction())) {
141 int uidRemoved = intent.getIntExtra(Intent.EXTRA_UID, -1);
142 if (DEBUG) {
143 Slog.d(TAG, "Removing jobs for uid: " + uidRemoved);
144 }
145 cancelJobsForUid(uidRemoved);
146 } else if (Intent.ACTION_USER_REMOVED.equals(intent.getAction())) {
147 final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0);
148 if (DEBUG) {
149 Slog.d(TAG, "Removing jobs for user: " + userId);
150 }
151 cancelJobsForUser(userId);
152 }
153 }
154 };
155
Matthew Williams9ae3dbe2014-08-21 13:47:47 -0700156 @Override
157 public void onStartUser(int userHandle) {
158 mStartedUsers.add(userHandle);
159 // Let's kick any outstanding jobs for this user.
160 mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget();
161 }
162
163 @Override
164 public void onStopUser(int userHandle) {
165 mStartedUsers.remove(Integer.valueOf(userHandle));
166 }
167
Christopher Tate7060b042014-06-09 19:50:00 -0700168 /**
169 * Entry point from client to schedule the provided job.
170 * This cancels the job if it's already been scheduled, and replaces it with the one provided.
171 * @param job JobInfo object containing execution parameters
172 * @param uId The package identifier of the application this job is for.
Christopher Tate7060b042014-06-09 19:50:00 -0700173 * @return Result of this operation. See <code>JobScheduler#RESULT_*</code> return codes.
174 */
Matthew Williams900c67f2014-07-09 12:46:53 -0700175 public int schedule(JobInfo job, int uId) {
176 JobStatus jobStatus = new JobStatus(job, uId);
Christopher Tate7060b042014-06-09 19:50:00 -0700177 cancelJob(uId, job.getId());
178 startTrackingJob(jobStatus);
Matthew Williamsbafeeb92014-08-08 11:51:06 -0700179 mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget();
Christopher Tate7060b042014-06-09 19:50:00 -0700180 return JobScheduler.RESULT_SUCCESS;
181 }
182
183 public List<JobInfo> getPendingJobs(int uid) {
184 ArrayList<JobInfo> outList = new ArrayList<JobInfo>();
185 synchronized (mJobs) {
Dianne Hackbornfdb19562014-07-11 16:03:36 -0700186 ArraySet<JobStatus> jobs = mJobs.getJobs();
187 for (int i=0; i<jobs.size(); i++) {
188 JobStatus job = jobs.valueAt(i);
Christopher Tate7060b042014-06-09 19:50:00 -0700189 if (job.getUid() == uid) {
190 outList.add(job.getJob());
191 }
192 }
193 }
194 return outList;
195 }
196
197 private void cancelJobsForUser(int userHandle) {
198 synchronized (mJobs) {
199 List<JobStatus> jobsForUser = mJobs.getJobsByUser(userHandle);
Dianne Hackbornfdb19562014-07-11 16:03:36 -0700200 for (int i=0; i<jobsForUser.size(); i++) {
201 JobStatus toRemove = jobsForUser.get(i);
Christopher Tate7060b042014-06-09 19:50:00 -0700202 if (DEBUG) {
203 Slog.d(TAG, "Cancelling: " + toRemove);
204 }
205 cancelJobLocked(toRemove);
206 }
207 }
208 }
209
210 /**
211 * Entry point from client to cancel all jobs originating from their uid.
212 * This will remove the job from the master list, and cancel the job if it was staged for
213 * execution or being executed.
214 * @param uid To check against for removal of a job.
215 */
216 public void cancelJobsForUid(int uid) {
217 // Remove from master list.
218 synchronized (mJobs) {
219 List<JobStatus> jobsForUid = mJobs.getJobsByUid(uid);
Dianne Hackbornfdb19562014-07-11 16:03:36 -0700220 for (int i=0; i<jobsForUid.size(); i++) {
221 JobStatus toRemove = jobsForUid.get(i);
Christopher Tate7060b042014-06-09 19:50:00 -0700222 if (DEBUG) {
223 Slog.d(TAG, "Cancelling: " + toRemove);
224 }
225 cancelJobLocked(toRemove);
226 }
227 }
228 }
229
230 /**
231 * Entry point from client to cancel the job corresponding to the jobId provided.
232 * This will remove the job from the master list, and cancel the job if it was staged for
233 * execution or being executed.
234 * @param uid Uid of the calling client.
235 * @param jobId Id of the job, provided at schedule-time.
236 */
237 public void cancelJob(int uid, int jobId) {
238 JobStatus toCancel;
239 synchronized (mJobs) {
240 toCancel = mJobs.getJobByUidAndJobId(uid, jobId);
241 if (toCancel != null) {
242 cancelJobLocked(toCancel);
243 }
244 }
245 }
246
247 private void cancelJobLocked(JobStatus cancelled) {
Matthew Williamsee410da2014-07-25 11:30:40 -0700248 if (DEBUG) {
249 Slog.d(TAG, "Cancelling: " + cancelled);
250 }
Christopher Tate7060b042014-06-09 19:50:00 -0700251 // Remove from store.
252 stopTrackingJob(cancelled);
253 // Remove from pending queue.
254 mPendingJobs.remove(cancelled);
255 // Cancel if running.
256 stopJobOnServiceContextLocked(cancelled);
257 }
258
259 /**
260 * Initializes the system service.
261 * <p>
262 * Subclasses must define a single argument constructor that accepts the context
263 * and passes it to super.
264 * </p>
265 *
266 * @param context The system server context.
267 */
268 public JobSchedulerService(Context context) {
269 super(context);
270 // Create the controllers.
Dianne Hackbornfdb19562014-07-11 16:03:36 -0700271 mControllers = new ArrayList<StateController>();
Christopher Tate7060b042014-06-09 19:50:00 -0700272 mControllers.add(ConnectivityController.get(this));
273 mControllers.add(TimeController.get(this));
274 mControllers.add(IdleController.get(this));
275 mControllers.add(BatteryController.get(this));
276
277 mHandler = new JobHandler(context.getMainLooper());
278 mJobSchedulerStub = new JobSchedulerStub();
Christopher Tate7060b042014-06-09 19:50:00 -0700279 mJobs = JobStore.initAndGet(this);
280 }
281
282 @Override
283 public void onStart() {
284 publishBinderService(Context.JOB_SCHEDULER_SERVICE, mJobSchedulerStub);
285 }
286
287 @Override
288 public void onBootPhase(int phase) {
289 if (PHASE_SYSTEM_SERVICES_READY == phase) {
290 // Register br for package removals and user removals.
291 final IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_REMOVED);
292 filter.addDataScheme("package");
293 getContext().registerReceiverAsUser(
294 mBroadcastReceiver, UserHandle.ALL, filter, null, null);
295 final IntentFilter userFilter = new IntentFilter(Intent.ACTION_USER_REMOVED);
296 getContext().registerReceiverAsUser(
297 mBroadcastReceiver, UserHandle.ALL, userFilter, null, null);
Dianne Hackbornfdb19562014-07-11 16:03:36 -0700298 } else if (phase == PHASE_THIRD_PARTY_APPS_CAN_START) {
299 synchronized (mJobs) {
300 // Let's go!
301 mReadyToRock = true;
302 mBatteryStats = IBatteryStats.Stub.asInterface(ServiceManager.getService(
303 BatteryStats.SERVICE_NAME));
304 // Create the "runners".
305 for (int i = 0; i < MAX_JOB_CONTEXTS_COUNT; i++) {
306 mActiveServices.add(
307 new JobServiceContext(this, mBatteryStats,
308 getContext().getMainLooper()));
309 }
310 // Attach jobs to their controllers.
311 ArraySet<JobStatus> jobs = mJobs.getJobs();
312 for (int i=0; i<jobs.size(); i++) {
313 JobStatus job = jobs.valueAt(i);
Christopher Tate4a79dae2014-07-18 17:01:40 -0700314 for (int controller=0; controller<mControllers.size(); controller++) {
315 mControllers.get(controller).maybeStartTrackingJob(job);
Dianne Hackbornfdb19562014-07-11 16:03:36 -0700316 }
317 }
318 // GO GO GO!
319 mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget();
320 }
Christopher Tate7060b042014-06-09 19:50:00 -0700321 }
322 }
323
324 /**
325 * Called when we have a job status object that we need to insert in our
326 * {@link com.android.server.job.JobStore}, and make sure all the relevant controllers know
327 * about.
328 */
329 private void startTrackingJob(JobStatus jobStatus) {
330 boolean update;
Dianne Hackbornfdb19562014-07-11 16:03:36 -0700331 boolean rocking;
Christopher Tate7060b042014-06-09 19:50:00 -0700332 synchronized (mJobs) {
333 update = mJobs.add(jobStatus);
Dianne Hackbornfdb19562014-07-11 16:03:36 -0700334 rocking = mReadyToRock;
Christopher Tate7060b042014-06-09 19:50:00 -0700335 }
Dianne Hackbornfdb19562014-07-11 16:03:36 -0700336 if (rocking) {
337 for (int i=0; i<mControllers.size(); i++) {
338 StateController controller = mControllers.get(i);
339 if (update) {
340 controller.maybeStopTrackingJob(jobStatus);
341 }
342 controller.maybeStartTrackingJob(jobStatus);
Christopher Tate7060b042014-06-09 19:50:00 -0700343 }
Christopher Tate7060b042014-06-09 19:50:00 -0700344 }
345 }
346
347 /**
348 * Called when we want to remove a JobStatus object that we've finished executing. Returns the
349 * object removed.
350 */
351 private boolean stopTrackingJob(JobStatus jobStatus) {
352 boolean removed;
Dianne Hackbornfdb19562014-07-11 16:03:36 -0700353 boolean rocking;
Christopher Tate7060b042014-06-09 19:50:00 -0700354 synchronized (mJobs) {
355 // Remove from store as well as controllers.
356 removed = mJobs.remove(jobStatus);
Dianne Hackbornfdb19562014-07-11 16:03:36 -0700357 rocking = mReadyToRock;
Christopher Tate7060b042014-06-09 19:50:00 -0700358 }
Dianne Hackbornfdb19562014-07-11 16:03:36 -0700359 if (removed && rocking) {
360 for (int i=0; i<mControllers.size(); i++) {
361 StateController controller = mControllers.get(i);
Christopher Tate7060b042014-06-09 19:50:00 -0700362 controller.maybeStopTrackingJob(jobStatus);
363 }
364 }
365 return removed;
366 }
367
368 private boolean stopJobOnServiceContextLocked(JobStatus job) {
Dianne Hackbornfdb19562014-07-11 16:03:36 -0700369 for (int i=0; i<mActiveServices.size(); i++) {
370 JobServiceContext jsc = mActiveServices.get(i);
Christopher Tate7060b042014-06-09 19:50:00 -0700371 final JobStatus executing = jsc.getRunningJob();
372 if (executing != null && executing.matches(job.getUid(), job.getJobId())) {
373 jsc.cancelExecutingJob();
374 return true;
375 }
376 }
377 return false;
378 }
379
380 /**
381 * @param job JobStatus we are querying against.
382 * @return Whether or not the job represented by the status object is currently being run or
383 * is pending.
384 */
385 private boolean isCurrentlyActiveLocked(JobStatus job) {
Dianne Hackbornfdb19562014-07-11 16:03:36 -0700386 for (int i=0; i<mActiveServices.size(); i++) {
387 JobServiceContext serviceContext = mActiveServices.get(i);
Christopher Tate7060b042014-06-09 19:50:00 -0700388 final JobStatus running = serviceContext.getRunningJob();
389 if (running != null && running.matches(job.getUid(), job.getJobId())) {
390 return true;
391 }
392 }
393 return false;
394 }
395
396 /**
397 * A job is rescheduled with exponential back-off if the client requests this from their
398 * execution logic.
399 * A caveat is for idle-mode jobs, for which the idle-mode constraint will usurp the
400 * timeliness of the reschedule. For an idle-mode job, no deadline is given.
401 * @param failureToReschedule Provided job status that we will reschedule.
402 * @return A newly instantiated JobStatus with the same constraints as the last job except
403 * with adjusted timing constraints.
404 */
405 private JobStatus getRescheduleJobForFailure(JobStatus failureToReschedule) {
406 final long elapsedNowMillis = SystemClock.elapsedRealtime();
407 final JobInfo job = failureToReschedule.getJob();
408
409 final long initialBackoffMillis = job.getInitialBackoffMillis();
Matthew Williamsd1c06752014-08-22 14:15:28 -0700410 final int backoffAttempts = failureToReschedule.getNumFailures() + 1;
411 long delayMillis;
Christopher Tate7060b042014-06-09 19:50:00 -0700412
413 switch (job.getBackoffPolicy()) {
Matthew Williamsd1c06752014-08-22 14:15:28 -0700414 case JobInfo.BACKOFF_POLICY_LINEAR:
415 delayMillis = initialBackoffMillis * backoffAttempts;
Christopher Tate7060b042014-06-09 19:50:00 -0700416 break;
417 default:
418 if (DEBUG) {
419 Slog.v(TAG, "Unrecognised back-off policy, defaulting to exponential.");
420 }
Matthew Williamsd1c06752014-08-22 14:15:28 -0700421 case JobInfo.BACKOFF_POLICY_EXPONENTIAL:
422 delayMillis =
423 (long) Math.scalb(initialBackoffMillis, backoffAttempts - 1);
Christopher Tate7060b042014-06-09 19:50:00 -0700424 break;
425 }
Matthew Williamsd1c06752014-08-22 14:15:28 -0700426 delayMillis =
427 Math.min(delayMillis, JobInfo.MAX_BACKOFF_DELAY_MILLIS);
428 return new JobStatus(failureToReschedule, elapsedNowMillis + delayMillis,
429 JobStatus.NO_LATEST_RUNTIME, backoffAttempts);
Christopher Tate7060b042014-06-09 19:50:00 -0700430 }
431
432 /**
433 * Called after a periodic has executed so we can to re-add it. We take the last execution time
434 * of the job to be the time of completion (i.e. the time at which this function is called).
435 * This could be inaccurate b/c the job can run for as long as
436 * {@link com.android.server.job.JobServiceContext#EXECUTING_TIMESLICE_MILLIS}, but will lead
437 * to underscheduling at least, rather than if we had taken the last execution time to be the
438 * start of the execution.
439 * @return A new job representing the execution criteria for this instantiation of the
440 * recurring job.
441 */
442 private JobStatus getRescheduleJobForPeriodic(JobStatus periodicToReschedule) {
443 final long elapsedNow = SystemClock.elapsedRealtime();
444 // Compute how much of the period is remaining.
445 long runEarly = Math.max(periodicToReschedule.getLatestRunTimeElapsed() - elapsedNow, 0);
446 long newEarliestRunTimeElapsed = elapsedNow + runEarly;
447 long period = periodicToReschedule.getJob().getIntervalMillis();
448 long newLatestRuntimeElapsed = newEarliestRunTimeElapsed + period;
449
450 if (DEBUG) {
451 Slog.v(TAG, "Rescheduling executed periodic. New execution window [" +
452 newEarliestRunTimeElapsed/1000 + ", " + newLatestRuntimeElapsed/1000 + "]s");
453 }
454 return new JobStatus(periodicToReschedule, newEarliestRunTimeElapsed,
455 newLatestRuntimeElapsed, 0 /* backoffAttempt */);
456 }
457
458 // JobCompletedListener implementations.
459
460 /**
461 * A job just finished executing. We fetch the
462 * {@link com.android.server.job.controllers.JobStatus} from the store and depending on
463 * whether we want to reschedule we readd it to the controllers.
464 * @param jobStatus Completed job.
465 * @param needsReschedule Whether the implementing class should reschedule this job.
466 */
467 @Override
468 public void onJobCompleted(JobStatus jobStatus, boolean needsReschedule) {
469 if (DEBUG) {
470 Slog.d(TAG, "Completed " + jobStatus + ", reschedule=" + needsReschedule);
471 }
472 if (!stopTrackingJob(jobStatus)) {
473 if (DEBUG) {
Matthew Williamsee410da2014-07-25 11:30:40 -0700474 Slog.d(TAG, "Could not find job to remove. Was job removed while executing?");
Christopher Tate7060b042014-06-09 19:50:00 -0700475 }
476 return;
477 }
478 if (needsReschedule) {
479 JobStatus rescheduled = getRescheduleJobForFailure(jobStatus);
480 startTrackingJob(rescheduled);
481 } else if (jobStatus.getJob().isPeriodic()) {
482 JobStatus rescheduledPeriodic = getRescheduleJobForPeriodic(jobStatus);
483 startTrackingJob(rescheduledPeriodic);
484 }
485 mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget();
486 }
487
488 // StateChangedListener implementations.
489
490 /**
491 * Off-board work to our handler thread as quickly as possible, b/c this call is probably being
492 * made on the main thread.
493 * For now this takes the job and if it's ready to run it will run it. In future we might not
494 * provide the job, so that the StateChangedListener has to run through its list of jobs to
495 * see which are ready. This will further decouple the controllers from the execution logic.
496 */
497 @Override
498 public void onControllerStateChanged() {
Dianne Hackbornfdb19562014-07-11 16:03:36 -0700499 synchronized (mJobs) {
500 if (mReadyToRock) {
501 // Post a message to to run through the list of jobs and start/stop any that
502 // are eligible.
503 mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget();
504 }
505 }
Christopher Tate7060b042014-06-09 19:50:00 -0700506 }
507
508 @Override
509 public void onRunJobNow(JobStatus jobStatus) {
510 mHandler.obtainMessage(MSG_JOB_EXPIRED, jobStatus).sendToTarget();
511 }
512
Christopher Tate7060b042014-06-09 19:50:00 -0700513 private class JobHandler extends Handler {
514
515 public JobHandler(Looper looper) {
516 super(looper);
517 }
518
519 @Override
520 public void handleMessage(Message message) {
521 switch (message.what) {
522 case MSG_JOB_EXPIRED:
523 synchronized (mJobs) {
524 JobStatus runNow = (JobStatus) message.obj;
Matthew Williamsbafeeb92014-08-08 11:51:06 -0700525 // runNow can be null, which is a controller's way of indicating that its
526 // state is such that all ready jobs should be run immediately.
527 if (runNow != null && !mPendingJobs.contains(runNow)) {
Christopher Tate7060b042014-06-09 19:50:00 -0700528 mPendingJobs.add(runNow);
529 }
530 }
531 queueReadyJobsForExecutionH();
532 break;
533 case MSG_CHECK_JOB:
534 // Check the list of jobs and run some of them if we feel inclined.
535 maybeQueueReadyJobsForExecutionH();
536 break;
537 }
538 maybeRunPendingJobsH();
539 // Don't remove JOB_EXPIRED in case one came along while processing the queue.
540 removeMessages(MSG_CHECK_JOB);
541 }
542
543 /**
544 * Run through list of jobs and execute all possible - at least one is expired so we do
545 * as many as we can.
546 */
547 private void queueReadyJobsForExecutionH() {
548 synchronized (mJobs) {
Dianne Hackbornfdb19562014-07-11 16:03:36 -0700549 ArraySet<JobStatus> jobs = mJobs.getJobs();
550 for (int i=0; i<jobs.size(); i++) {
551 JobStatus job = jobs.valueAt(i);
Christopher Tate7060b042014-06-09 19:50:00 -0700552 if (isReadyToBeExecutedLocked(job)) {
553 mPendingJobs.add(job);
554 } else if (isReadyToBeCancelledLocked(job)) {
555 stopJobOnServiceContextLocked(job);
556 }
557 }
558 }
559 }
560
561 /**
562 * The state of at least one job has changed. Here is where we could enforce various
563 * policies on when we want to execute jobs.
564 * Right now the policy is such:
565 * If >1 of the ready jobs is idle mode we send all of them off
566 * if more than 2 network connectivity jobs are ready we send them all off.
567 * If more than 4 jobs total are ready we send them all off.
568 * TODO: It would be nice to consolidate these sort of high-level policies somewhere.
569 */
570 private void maybeQueueReadyJobsForExecutionH() {
571 synchronized (mJobs) {
Matthew Williamsbe0c4172014-08-06 18:14:16 -0700572 int chargingCount = 0;
573 int idleCount = 0;
Christopher Tate7060b042014-06-09 19:50:00 -0700574 int backoffCount = 0;
575 int connectivityCount = 0;
576 List<JobStatus> runnableJobs = new ArrayList<JobStatus>();
Dianne Hackbornfdb19562014-07-11 16:03:36 -0700577 ArraySet<JobStatus> jobs = mJobs.getJobs();
578 for (int i=0; i<jobs.size(); i++) {
579 JobStatus job = jobs.valueAt(i);
Christopher Tate7060b042014-06-09 19:50:00 -0700580 if (isReadyToBeExecutedLocked(job)) {
581 if (job.getNumFailures() > 0) {
582 backoffCount++;
583 }
584 if (job.hasIdleConstraint()) {
585 idleCount++;
586 }
587 if (job.hasConnectivityConstraint() || job.hasUnmeteredConstraint()) {
588 connectivityCount++;
589 }
Matthew Williamsbe0c4172014-08-06 18:14:16 -0700590 if (job.hasChargingConstraint()) {
591 chargingCount++;
592 }
Christopher Tate7060b042014-06-09 19:50:00 -0700593 runnableJobs.add(job);
594 } else if (isReadyToBeCancelledLocked(job)) {
595 stopJobOnServiceContextLocked(job);
596 }
597 }
Matthew Williamsbe0c4172014-08-06 18:14:16 -0700598 if (backoffCount > 0 ||
599 idleCount >= MIN_IDLE_COUNT ||
Christopher Tate7060b042014-06-09 19:50:00 -0700600 connectivityCount >= MIN_CONNECTIVITY_COUNT ||
Matthew Williamsbe0c4172014-08-06 18:14:16 -0700601 chargingCount >= MIN_CHARGING_COUNT ||
Christopher Tate7060b042014-06-09 19:50:00 -0700602 runnableJobs.size() >= MIN_READY_JOBS_COUNT) {
Matthew Williamsbe0c4172014-08-06 18:14:16 -0700603 if (DEBUG) {
604 Slog.d(TAG, "maybeQueueReadyJobsForExecutionH: Running jobs.");
605 }
Dianne Hackbornfdb19562014-07-11 16:03:36 -0700606 for (int i=0; i<runnableJobs.size(); i++) {
607 mPendingJobs.add(runnableJobs.get(i));
Christopher Tate7060b042014-06-09 19:50:00 -0700608 }
Matthew Williamsbe0c4172014-08-06 18:14:16 -0700609 } else {
610 if (DEBUG) {
611 Slog.d(TAG, "maybeQueueReadyJobsForExecutionH: Not running anything.");
612 }
613 }
614 if (DEBUG) {
615 Slog.d(TAG, "idle=" + idleCount + " connectivity=" +
616 connectivityCount + " charging=" + chargingCount + " tot=" +
617 runnableJobs.size());
Christopher Tate7060b042014-06-09 19:50:00 -0700618 }
619 }
620 }
621
622 /**
623 * Criteria for moving a job into the pending queue:
624 * - It's ready.
625 * - It's not pending.
626 * - It's not already running on a JSC.
Matthew Williams9ae3dbe2014-08-21 13:47:47 -0700627 * - The user that requested the job is running.
Christopher Tate7060b042014-06-09 19:50:00 -0700628 */
629 private boolean isReadyToBeExecutedLocked(JobStatus job) {
Matthew Williams9ae3dbe2014-08-21 13:47:47 -0700630 final boolean jobReady = job.isReady();
631 final boolean jobPending = mPendingJobs.contains(job);
632 final boolean jobActive = isCurrentlyActiveLocked(job);
633 final boolean userRunning = mStartedUsers.contains(job.getUserId());
634
635 if (DEBUG) {
636 Slog.v(TAG, "isReadyToBeExecutedLocked: " + job.toShortString()
637 + " ready=" + jobReady + " pending=" + jobPending
638 + " active=" + jobActive + " userRunning=" + userRunning);
639 }
640 return userRunning && jobReady && !jobPending && !jobActive;
Christopher Tate7060b042014-06-09 19:50:00 -0700641 }
642
643 /**
644 * Criteria for cancelling an active job:
645 * - It's not ready
646 * - It's running on a JSC.
647 */
648 private boolean isReadyToBeCancelledLocked(JobStatus job) {
649 return !job.isReady() && isCurrentlyActiveLocked(job);
650 }
651
652 /**
653 * Reconcile jobs in the pending queue against available execution contexts.
654 * A controller can force a job into the pending queue even if it's already running, but
655 * here is where we decide whether to actually execute it.
656 */
657 private void maybeRunPendingJobsH() {
658 synchronized (mJobs) {
659 Iterator<JobStatus> it = mPendingJobs.iterator();
660 while (it.hasNext()) {
661 JobStatus nextPending = it.next();
662 JobServiceContext availableContext = null;
Dianne Hackbornfdb19562014-07-11 16:03:36 -0700663 for (int i=0; i<mActiveServices.size(); i++) {
664 JobServiceContext jsc = mActiveServices.get(i);
Christopher Tate7060b042014-06-09 19:50:00 -0700665 final JobStatus running = jsc.getRunningJob();
666 if (running != null && running.matches(nextPending.getUid(),
667 nextPending.getJobId())) {
Matthew Williamsee410da2014-07-25 11:30:40 -0700668 // Already running this job for this uId, skip.
Christopher Tate7060b042014-06-09 19:50:00 -0700669 availableContext = null;
670 break;
671 }
672 if (jsc.isAvailable()) {
673 availableContext = jsc;
674 }
675 }
676 if (availableContext != null) {
677 if (!availableContext.executeRunnableJob(nextPending)) {
678 if (DEBUG) {
679 Slog.d(TAG, "Error executing " + nextPending);
680 }
681 mJobs.remove(nextPending);
682 }
683 it.remove();
684 }
685 }
686 }
687 }
688 }
689
690 /**
691 * Binder stub trampoline implementation
692 */
693 final class JobSchedulerStub extends IJobScheduler.Stub {
694 /** Cache determination of whether a given app can persist jobs
695 * key is uid of the calling app; value is undetermined/true/false
696 */
697 private final SparseArray<Boolean> mPersistCache = new SparseArray<Boolean>();
698
699 // Enforce that only the app itself (or shared uid participant) can schedule a
700 // job that runs one of the app's services, as well as verifying that the
701 // named service properly requires the BIND_JOB_SERVICE permission
702 private void enforceValidJobRequest(int uid, JobInfo job) {
Christopher Tate5568f542014-06-18 13:53:31 -0700703 final IPackageManager pm = AppGlobals.getPackageManager();
Christopher Tate7060b042014-06-09 19:50:00 -0700704 final ComponentName service = job.getService();
705 try {
Christopher Tate5568f542014-06-18 13:53:31 -0700706 ServiceInfo si = pm.getServiceInfo(service, 0, UserHandle.getUserId(uid));
707 if (si == null) {
708 throw new IllegalArgumentException("No such service " + service);
709 }
Christopher Tate7060b042014-06-09 19:50:00 -0700710 if (si.applicationInfo.uid != uid) {
711 throw new IllegalArgumentException("uid " + uid +
712 " cannot schedule job in " + service.getPackageName());
713 }
714 if (!JobService.PERMISSION_BIND.equals(si.permission)) {
715 throw new IllegalArgumentException("Scheduled service " + service
716 + " does not require android.permission.BIND_JOB_SERVICE permission");
717 }
Christopher Tate5568f542014-06-18 13:53:31 -0700718 } catch (RemoteException e) {
719 // Can't happen; the Package Manager is in this same process
Christopher Tate7060b042014-06-09 19:50:00 -0700720 }
721 }
722
723 private boolean canPersistJobs(int pid, int uid) {
724 // If we get this far we're good to go; all we need to do now is check
725 // whether the app is allowed to persist its scheduled work.
726 final boolean canPersist;
727 synchronized (mPersistCache) {
728 Boolean cached = mPersistCache.get(uid);
729 if (cached != null) {
730 canPersist = cached.booleanValue();
731 } else {
732 // Persisting jobs is tantamount to running at boot, so we permit
733 // it when the app has declared that it uses the RECEIVE_BOOT_COMPLETED
734 // permission
735 int result = getContext().checkPermission(
736 android.Manifest.permission.RECEIVE_BOOT_COMPLETED, pid, uid);
737 canPersist = (result == PackageManager.PERMISSION_GRANTED);
738 mPersistCache.put(uid, canPersist);
739 }
740 }
741 return canPersist;
742 }
743
744 // IJobScheduler implementation
745 @Override
746 public int schedule(JobInfo job) throws RemoteException {
747 if (DEBUG) {
Matthew Williamsee410da2014-07-25 11:30:40 -0700748 Slog.d(TAG, "Scheduling job: " + job.toString());
Christopher Tate7060b042014-06-09 19:50:00 -0700749 }
750 final int pid = Binder.getCallingPid();
751 final int uid = Binder.getCallingUid();
752
753 enforceValidJobRequest(uid, job);
Matthew Williams900c67f2014-07-09 12:46:53 -0700754 if (job.isPersisted()) {
755 if (!canPersistJobs(pid, uid)) {
756 throw new IllegalArgumentException("Error: requested job be persisted without"
757 + " holding RECEIVE_BOOT_COMPLETED permission.");
758 }
759 }
Christopher Tate7060b042014-06-09 19:50:00 -0700760
761 long ident = Binder.clearCallingIdentity();
762 try {
Matthew Williams900c67f2014-07-09 12:46:53 -0700763 return JobSchedulerService.this.schedule(job, uid);
Christopher Tate7060b042014-06-09 19:50:00 -0700764 } finally {
765 Binder.restoreCallingIdentity(ident);
766 }
767 }
768
769 @Override
770 public List<JobInfo> getAllPendingJobs() throws RemoteException {
771 final int uid = Binder.getCallingUid();
772
773 long ident = Binder.clearCallingIdentity();
774 try {
775 return JobSchedulerService.this.getPendingJobs(uid);
776 } finally {
777 Binder.restoreCallingIdentity(ident);
778 }
779 }
780
781 @Override
782 public void cancelAll() throws RemoteException {
783 final int uid = Binder.getCallingUid();
784
785 long ident = Binder.clearCallingIdentity();
786 try {
787 JobSchedulerService.this.cancelJobsForUid(uid);
788 } finally {
789 Binder.restoreCallingIdentity(ident);
790 }
791 }
792
793 @Override
794 public void cancel(int jobId) throws RemoteException {
795 final int uid = Binder.getCallingUid();
796
797 long ident = Binder.clearCallingIdentity();
798 try {
799 JobSchedulerService.this.cancelJob(uid, jobId);
800 } finally {
801 Binder.restoreCallingIdentity(ident);
802 }
803 }
804
805 /**
806 * "dumpsys" infrastructure
807 */
808 @Override
809 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
810 getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, TAG);
811
812 long identityToken = Binder.clearCallingIdentity();
813 try {
814 JobSchedulerService.this.dumpInternal(pw);
815 } finally {
816 Binder.restoreCallingIdentity(identityToken);
817 }
818 }
819 };
820
821 void dumpInternal(PrintWriter pw) {
Christopher Tatef973a7b2014-08-29 12:54:08 -0700822 final long now = SystemClock.elapsedRealtime();
Christopher Tate7060b042014-06-09 19:50:00 -0700823 synchronized (mJobs) {
Matthew Williams9ae3dbe2014-08-21 13:47:47 -0700824 pw.print("Started users: ");
825 for (int i=0; i<mStartedUsers.size(); i++) {
826 pw.print("u" + mStartedUsers.get(i) + " ");
827 }
828 pw.println();
Christopher Tate7060b042014-06-09 19:50:00 -0700829 pw.println("Registered jobs:");
830 if (mJobs.size() > 0) {
Dianne Hackbornfdb19562014-07-11 16:03:36 -0700831 ArraySet<JobStatus> jobs = mJobs.getJobs();
832 for (int i=0; i<jobs.size(); i++) {
833 JobStatus job = jobs.valueAt(i);
Christopher Tate7060b042014-06-09 19:50:00 -0700834 job.dump(pw, " ");
835 }
836 } else {
Christopher Tatef973a7b2014-08-29 12:54:08 -0700837 pw.println(" None.");
Christopher Tate7060b042014-06-09 19:50:00 -0700838 }
Dianne Hackbornfdb19562014-07-11 16:03:36 -0700839 for (int i=0; i<mControllers.size(); i++) {
Christopher Tate7060b042014-06-09 19:50:00 -0700840 pw.println();
Dianne Hackbornfdb19562014-07-11 16:03:36 -0700841 mControllers.get(i).dumpControllerState(pw);
Christopher Tate7060b042014-06-09 19:50:00 -0700842 }
843 pw.println();
Christopher Tatef973a7b2014-08-29 12:54:08 -0700844 pw.println("Pending:");
Dianne Hackbornfdb19562014-07-11 16:03:36 -0700845 for (int i=0; i<mPendingJobs.size(); i++) {
846 pw.println(mPendingJobs.get(i).hashCode());
Christopher Tate7060b042014-06-09 19:50:00 -0700847 }
848 pw.println();
849 pw.println("Active jobs:");
Dianne Hackbornfdb19562014-07-11 16:03:36 -0700850 for (int i=0; i<mActiveServices.size(); i++) {
851 JobServiceContext jsc = mActiveServices.get(i);
Christopher Tate7060b042014-06-09 19:50:00 -0700852 if (jsc.isAvailable()) {
853 continue;
854 } else {
Christopher Tatef973a7b2014-08-29 12:54:08 -0700855 final long timeout = jsc.getTimeoutElapsed();
856 pw.print("Running for: ");
857 pw.print((now - jsc.getExecutionStartTimeElapsed())/1000);
858 pw.print("s timeout=");
859 pw.print(timeout);
860 pw.print(" fromnow=");
861 pw.println(timeout-now);
862 jsc.getRunningJob().dump(pw, " ");
Christopher Tate7060b042014-06-09 19:50:00 -0700863 }
864 }
Dianne Hackbornfdb19562014-07-11 16:03:36 -0700865 pw.println();
866 pw.print("mReadyToRock="); pw.println(mReadyToRock);
Christopher Tate7060b042014-06-09 19:50:00 -0700867 }
868 pw.println();
869 }
870}