JobServiceContext.java revision ee410da42b6b8352213f03f7725fd041f703b035
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.job.JobParameters; 21import android.app.job.IJobCallback; 22import android.app.job.IJobService; 23import android.content.ComponentName; 24import android.content.Context; 25import android.content.Intent; 26import android.content.ServiceConnection; 27import android.os.Binder; 28import android.os.Handler; 29import android.os.IBinder; 30import android.os.Looper; 31import android.os.Message; 32import android.os.PowerManager; 33import android.os.RemoteException; 34import android.os.SystemClock; 35import android.os.UserHandle; 36import android.os.WorkSource; 37import android.util.Slog; 38 39import com.android.internal.annotations.GuardedBy; 40import com.android.internal.annotations.VisibleForTesting; 41import com.android.internal.app.IBatteryStats; 42import com.android.server.job.controllers.JobStatus; 43 44import java.util.concurrent.atomic.AtomicBoolean; 45 46/** 47 * Handles client binding and lifecycle of a job. Jobs execute one at a time on an instance of this 48 * class. 49 * 50 * There are two important interactions into this class from the 51 * {@link com.android.server.job.JobSchedulerService}. To execute a job and to cancel a job. 52 * - Execution of a new job is handled by the {@link #mAvailable}. This bit is flipped once when a 53 * job lands, and again when it is complete. 54 * - Cancelling is trickier, because there are also interactions from the client. It's possible 55 * the {@link com.android.server.job.JobServiceContext.JobServiceHandler} tries to process a 56 * {@link #MSG_CANCEL} after the client has already finished. This is handled by having 57 * {@link com.android.server.job.JobServiceContext.JobServiceHandler#handleCancelH} check whether 58 * the context is still valid. 59 * To mitigate this, tearing down the context removes all messages from the handler, including any 60 * tardy {@link #MSG_CANCEL}s. Additionally, we avoid sending duplicate onStopJob() 61 * calls to the client after they've specified jobFinished(). 62 * 63 */ 64public class JobServiceContext extends IJobCallback.Stub implements ServiceConnection { 65 private static final boolean DEBUG = true; 66 private static final String TAG = "JobServiceContext"; 67 /** Define the maximum # of jobs allowed to run on a service at once. */ 68 private static final int defaultMaxActiveJobsPerService = 69 ActivityManager.isLowRamDeviceStatic() ? 1 : 3; 70 /** Amount of time a job is allowed to execute for before being considered timed-out. */ 71 private static final long EXECUTING_TIMESLICE_MILLIS = 60 * 1000; 72 /** Amount of time the JobScheduler will wait for a response from an app for a message. */ 73 private static final long OP_TIMEOUT_MILLIS = 8 * 1000; 74 75 private static final String[] VERB_STRINGS = { 76 "VERB_BINDING", "VERB_STARTING", "VERB_EXECUTING", "VERB_STOPPING" 77 }; 78 79 // States that a job occupies while interacting with the client. 80 static final int VERB_BINDING = 0; 81 static final int VERB_STARTING = 1; 82 static final int VERB_EXECUTING = 2; 83 static final int VERB_STOPPING = 3; 84 85 // Messages that result from interactions with the client service. 86 /** System timed out waiting for a response. */ 87 private static final int MSG_TIMEOUT = 0; 88 /** Received a callback from client. */ 89 private static final int MSG_CALLBACK = 1; 90 /** Run through list and start any ready jobs.*/ 91 private static final int MSG_SERVICE_BOUND = 2; 92 /** Cancel a job. */ 93 private static final int MSG_CANCEL = 3; 94 /** Shutdown the job. Used when the client crashes and we can't die gracefully.*/ 95 private static final int MSG_SHUTDOWN_EXECUTION = 4; 96 97 private final Handler mCallbackHandler; 98 /** Make callbacks to {@link JobSchedulerService} to inform on job completion status. */ 99 private final JobCompletedListener mCompletedListener; 100 /** Used for service binding, etc. */ 101 private final Context mContext; 102 private final IBatteryStats mBatteryStats; 103 private PowerManager.WakeLock mWakeLock; 104 105 // Execution state. 106 private JobParameters mParams; 107 @VisibleForTesting 108 int mVerb; 109 private AtomicBoolean mCancelled = new AtomicBoolean(); 110 111 /** All the information maintained about the job currently being executed. */ 112 private JobStatus mRunningJob; 113 /** Binder to the client service. */ 114 IJobService service; 115 116 private final Object mLock = new Object(); 117 /** 118 * Whether this context is free. This is set to false at the start of execution, and reset to 119 * true when execution is complete. 120 */ 121 @GuardedBy("mLock") 122 private boolean mAvailable; 123 /** Track start time. */ 124 private long mExecutionStartTimeElapsed; 125 /** Track when job will timeout. */ 126 private long mTimeoutElapsed; 127 128 JobServiceContext(JobSchedulerService service, IBatteryStats batteryStats, Looper looper) { 129 this(service.getContext(), batteryStats, service, looper); 130 } 131 132 @VisibleForTesting 133 JobServiceContext(Context context, IBatteryStats batteryStats, 134 JobCompletedListener completedListener, Looper looper) { 135 mContext = context; 136 mBatteryStats = batteryStats; 137 mCallbackHandler = new JobServiceHandler(looper); 138 mCompletedListener = completedListener; 139 mAvailable = true; 140 } 141 142 /** 143 * Give a job to this context for execution. Callers must first check {@link #isAvailable()} 144 * to make sure this is a valid context. 145 * @param job The status of the job that we are going to run. 146 * @return True if the job is valid and is running. False if the job cannot be executed. 147 */ 148 boolean executeRunnableJob(JobStatus job) { 149 synchronized (mLock) { 150 if (!mAvailable) { 151 Slog.e(TAG, "Starting new runnable but context is unavailable > Error."); 152 return false; 153 } 154 155 mRunningJob = job; 156 mParams = new JobParameters(job.getJobId(), job.getExtras(), this); 157 mExecutionStartTimeElapsed = SystemClock.elapsedRealtime(); 158 159 mVerb = VERB_BINDING; 160 final Intent intent = new Intent().setComponent(job.getServiceComponent()); 161 boolean binding = mContext.bindServiceAsUser(intent, this, 162 Context.BIND_AUTO_CREATE | Context.BIND_NOT_FOREGROUND, 163 new UserHandle(job.getUserId())); 164 if (!binding) { 165 if (DEBUG) { 166 Slog.d(TAG, job.getServiceComponent().getShortClassName() + " unavailable."); 167 } 168 mRunningJob = null; 169 mParams = null; 170 mExecutionStartTimeElapsed = 0L; 171 return false; 172 } 173 try { 174 mBatteryStats.noteJobStart(job.getName(), job.getUid()); 175 } catch (RemoteException e) { 176 // Whatever. 177 } 178 mAvailable = false; 179 return true; 180 } 181 } 182 183 /** 184 * Used externally to query the running job. Will return null if there is no job running. 185 * Be careful when using this function, at any moment it's possible that the job returned may 186 * stop executing. 187 */ 188 JobStatus getRunningJob() { 189 synchronized (mLock) { 190 return mRunningJob; 191 } 192 } 193 194 /** Called externally when a job that was scheduled for execution should be cancelled. */ 195 void cancelExecutingJob() { 196 mCallbackHandler.obtainMessage(MSG_CANCEL).sendToTarget(); 197 } 198 199 /** 200 * @return Whether this context is available to handle incoming work. 201 */ 202 boolean isAvailable() { 203 synchronized (mLock) { 204 return mAvailable; 205 } 206 } 207 208 long getExecutionStartTimeElapsed() { 209 return mExecutionStartTimeElapsed; 210 } 211 212 long getTimeoutElapsed() { 213 return mTimeoutElapsed; 214 } 215 216 @Override 217 public void jobFinished(int jobId, boolean reschedule) { 218 if (!verifyCallingUid()) { 219 return; 220 } 221 mCallbackHandler.obtainMessage(MSG_CALLBACK, jobId, reschedule ? 1 : 0) 222 .sendToTarget(); 223 } 224 225 @Override 226 public void acknowledgeStopMessage(int jobId, boolean reschedule) { 227 if (!verifyCallingUid()) { 228 return; 229 } 230 mCallbackHandler.obtainMessage(MSG_CALLBACK, jobId, reschedule ? 1 : 0) 231 .sendToTarget(); 232 } 233 234 @Override 235 public void acknowledgeStartMessage(int jobId, boolean ongoing) { 236 if (!verifyCallingUid()) { 237 return; 238 } 239 mCallbackHandler.obtainMessage(MSG_CALLBACK, jobId, ongoing ? 1 : 0).sendToTarget(); 240 } 241 242 /** 243 * We acquire/release a wakelock on onServiceConnected/unbindService. This mirrors the work 244 * we intend to send to the client - we stop sending work when the service is unbound so until 245 * then we keep the wakelock. 246 * @param name The concrete component name of the service that has been connected. 247 * @param service The IBinder of the Service's communication channel, 248 */ 249 @Override 250 public void onServiceConnected(ComponentName name, IBinder service) { 251 if (!name.equals(mRunningJob.getServiceComponent())) { 252 mCallbackHandler.obtainMessage(MSG_SHUTDOWN_EXECUTION).sendToTarget(); 253 return; 254 } 255 this.service = IJobService.Stub.asInterface(service); 256 // Remove all timeouts. 257 mCallbackHandler.removeMessages(MSG_TIMEOUT); 258 final PowerManager pm = 259 (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); 260 mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, mRunningJob.getTag()); 261 mWakeLock.setWorkSource(new WorkSource(mRunningJob.getUid())); 262 mWakeLock.setReferenceCounted(false); 263 mWakeLock.acquire(); 264 mCallbackHandler.obtainMessage(MSG_SERVICE_BOUND).sendToTarget(); 265 } 266 267 /** If the client service crashes we reschedule this job and clean up. */ 268 @Override 269 public void onServiceDisconnected(ComponentName name) { 270 mCallbackHandler.obtainMessage(MSG_SHUTDOWN_EXECUTION).sendToTarget(); 271 } 272 273 /** 274 * This class is reused across different clients, and passes itself in as a callback. Check 275 * whether the client exercising the callback is the client we expect. 276 * @return True if the binder calling is coming from the client we expect. 277 */ 278 private boolean verifyCallingUid() { 279 if (mRunningJob == null || Binder.getCallingUid() != mRunningJob.getUid()) { 280 if (DEBUG) { 281 Slog.d(TAG, "Stale callback received, ignoring."); 282 } 283 return false; 284 } 285 return true; 286 } 287 288 /** 289 * Handles the lifecycle of the JobService binding/callbacks, etc. The convention within this 290 * class is to append 'H' to each function name that can only be called on this handler. This 291 * isn't strictly necessary because all of these functions are private, but helps clarity. 292 */ 293 private class JobServiceHandler extends Handler { 294 JobServiceHandler(Looper looper) { 295 super(looper); 296 } 297 298 @Override 299 public void handleMessage(Message message) { 300 switch (message.what) { 301 case MSG_SERVICE_BOUND: 302 handleServiceBoundH(); 303 break; 304 case MSG_CALLBACK: 305 if (DEBUG) { 306 Slog.d(TAG, "MSG_CALLBACK of : " + mRunningJob + " v:" + 307 (mVerb >= 0 ? VERB_STRINGS[mVerb] : "[invalid]")); 308 } 309 removeMessages(MSG_TIMEOUT); 310 311 if (mVerb == VERB_STARTING) { 312 final boolean workOngoing = message.arg2 == 1; 313 handleStartedH(workOngoing); 314 } else if (mVerb == VERB_EXECUTING || 315 mVerb == VERB_STOPPING) { 316 final boolean reschedule = message.arg2 == 1; 317 handleFinishedH(reschedule); 318 } else { 319 if (DEBUG) { 320 Slog.d(TAG, "Unrecognised callback: " + mRunningJob); 321 } 322 } 323 break; 324 case MSG_CANCEL: 325 handleCancelH(); 326 break; 327 case MSG_TIMEOUT: 328 handleOpTimeoutH(); 329 break; 330 case MSG_SHUTDOWN_EXECUTION: 331 closeAndCleanupJobH(true /* needsReschedule */); 332 break; 333 default: 334 Slog.e(TAG, "Unrecognised message: " + message); 335 } 336 } 337 338 /** Start the job on the service. */ 339 private void handleServiceBoundH() { 340 if (mVerb != VERB_BINDING) { 341 Slog.e(TAG, "Sending onStartJob for a job that isn't pending. " 342 + VERB_STRINGS[mVerb]); 343 closeAndCleanupJobH(false /* reschedule */); 344 return; 345 } 346 if (mCancelled.get()) { 347 if (DEBUG) { 348 Slog.d(TAG, "Job cancelled while waiting for bind to complete. " 349 + mRunningJob); 350 } 351 closeAndCleanupJobH(true /* reschedule */); 352 return; 353 } 354 try { 355 mVerb = VERB_STARTING; 356 scheduleOpTimeOut(); 357 service.startJob(mParams); 358 } catch (RemoteException e) { 359 Slog.e(TAG, "Error sending onStart message to '" + 360 mRunningJob.getServiceComponent().getShortClassName() + "' ", e); 361 } 362 } 363 364 /** 365 * State behaviours. 366 * VERB_STARTING -> Successful start, change job to VERB_EXECUTING and post timeout. 367 * _PENDING -> Error 368 * _EXECUTING -> Error 369 * _STOPPING -> Error 370 */ 371 private void handleStartedH(boolean workOngoing) { 372 switch (mVerb) { 373 case VERB_STARTING: 374 mVerb = VERB_EXECUTING; 375 if (!workOngoing) { 376 // Job is finished already so fast-forward to handleFinished. 377 handleFinishedH(false); 378 return; 379 } 380 if (mCancelled.get()) { 381 if (DEBUG) { 382 Slog.d(TAG, "Job cancelled while waiting for onStartJob to complete."); 383 } 384 // Cancelled *while* waiting for acknowledgeStartMessage from client. 385 handleCancelH(); 386 return; 387 } 388 scheduleOpTimeOut(); 389 break; 390 default: 391 Slog.e(TAG, "Handling started job but job wasn't starting! Was " 392 + VERB_STRINGS[mVerb] + "."); 393 return; 394 } 395 } 396 397 /** 398 * VERB_EXECUTING -> Client called jobFinished(), clean up and notify done. 399 * _STOPPING -> Successful finish, clean up and notify done. 400 * _STARTING -> Error 401 * _PENDING -> Error 402 */ 403 private void handleFinishedH(boolean reschedule) { 404 switch (mVerb) { 405 case VERB_EXECUTING: 406 case VERB_STOPPING: 407 closeAndCleanupJobH(reschedule); 408 break; 409 default: 410 Slog.e(TAG, "Got an execution complete message for a job that wasn't being" + 411 "executed. Was " + VERB_STRINGS[mVerb] + "."); 412 } 413 } 414 415 /** 416 * A job can be in various states when a cancel request comes in: 417 * VERB_BINDING -> Cancelled before bind completed. Mark as cancelled and wait for 418 * {@link #onServiceConnected(android.content.ComponentName, android.os.IBinder)} 419 * _STARTING -> Mark as cancelled and wait for 420 * {@link JobServiceContext#acknowledgeStartMessage(int, boolean)} 421 * _EXECUTING -> call {@link #sendStopMessageH}}, but only if there are no callbacks 422 * in the message queue. 423 * _ENDING -> No point in doing anything here, so we ignore. 424 */ 425 private void handleCancelH() { 426 if (mRunningJob == null) { 427 if (DEBUG) { 428 Slog.d(TAG, "Trying to process cancel for torn-down context, ignoring."); 429 } 430 return; 431 } 432 if (JobSchedulerService.DEBUG) { 433 Slog.d(TAG, "Handling cancel for: " + mRunningJob.getJobId() + " " 434 + VERB_STRINGS[mVerb]); 435 } 436 switch (mVerb) { 437 case VERB_BINDING: 438 case VERB_STARTING: 439 mCancelled.set(true); 440 break; 441 case VERB_EXECUTING: 442 if (hasMessages(MSG_CALLBACK)) { 443 // If the client has called jobFinished, ignore this cancel. 444 return; 445 } 446 sendStopMessageH(); 447 break; 448 case VERB_STOPPING: 449 // Nada. 450 break; 451 default: 452 Slog.e(TAG, "Cancelling a job without a valid verb: " + mVerb); 453 break; 454 } 455 } 456 457 /** Process MSG_TIMEOUT here. */ 458 private void handleOpTimeoutH() { 459 if (JobSchedulerService.DEBUG) { 460 Slog.d(TAG, "MSG_TIMEOUT of " + 461 mRunningJob.getServiceComponent().getShortClassName() + " : " 462 + mParams.getJobId()); 463 } 464 465 final int jobId = mParams.getJobId(); 466 switch (mVerb) { 467 case VERB_STARTING: 468 // Client unresponsive - wedged or failed to respond in time. We don't really 469 // know what happened so let's log it and notify the JobScheduler 470 // FINISHED/NO-RETRY. 471 Slog.e(TAG, "No response from client for onStartJob '" + 472 mRunningJob.getServiceComponent().getShortClassName() + "' jId: " 473 + jobId); 474 closeAndCleanupJobH(false /* needsReschedule */); 475 break; 476 case VERB_STOPPING: 477 // At least we got somewhere, so fail but ask the JobScheduler to reschedule. 478 Slog.e(TAG, "No response from client for onStopJob, '" + 479 mRunningJob.getServiceComponent().getShortClassName() + "' jId: " 480 + jobId); 481 closeAndCleanupJobH(true /* needsReschedule */); 482 break; 483 case VERB_EXECUTING: 484 // Not an error - client ran out of time. 485 Slog.i(TAG, "Client timed out while executing (no jobFinished received)." + 486 " sending onStop. " + 487 mRunningJob.getServiceComponent().getShortClassName() + "' jId: " 488 + jobId); 489 sendStopMessageH(); 490 break; 491 default: 492 Slog.e(TAG, "Handling timeout for an unknown active job state: " 493 + mRunningJob); 494 return; 495 } 496 } 497 498 /** 499 * Already running, need to stop. Will switch {@link #mVerb} from VERB_EXECUTING -> 500 * VERB_STOPPING. 501 */ 502 private void sendStopMessageH() { 503 mCallbackHandler.removeMessages(MSG_TIMEOUT); 504 if (mVerb != VERB_EXECUTING) { 505 Slog.e(TAG, "Sending onStopJob for a job that isn't started. " + mRunningJob); 506 closeAndCleanupJobH(false /* reschedule */); 507 return; 508 } 509 try { 510 mVerb = VERB_STOPPING; 511 scheduleOpTimeOut(); 512 service.stopJob(mParams); 513 } catch (RemoteException e) { 514 Slog.e(TAG, "Error sending onStopJob to client.", e); 515 closeAndCleanupJobH(false /* reschedule */); 516 } 517 } 518 519 /** 520 * The provided job has finished, either by calling 521 * {@link android.app.job.JobService#jobFinished(android.app.job.JobParameters, boolean)} 522 * or from acknowledging the stop message we sent. Either way, we're done tracking it and 523 * we want to clean up internally. 524 */ 525 private void closeAndCleanupJobH(boolean reschedule) { 526 final JobStatus completedJob = mRunningJob; 527 synchronized (mLock) { 528 try { 529 mBatteryStats.noteJobFinish(mRunningJob.getName(), mRunningJob.getUid()); 530 } catch (RemoteException e) { 531 // Whatever. 532 } 533 mWakeLock.release(); 534 mContext.unbindService(JobServiceContext.this); 535 mWakeLock = null; 536 mRunningJob = null; 537 mParams = null; 538 mVerb = -1; 539 mCancelled.set(false); 540 service = null; 541 mAvailable = true; 542 } 543 removeMessages(MSG_TIMEOUT); 544 removeMessages(MSG_CALLBACK); 545 removeMessages(MSG_SERVICE_BOUND); 546 removeMessages(MSG_CANCEL); 547 removeMessages(MSG_SHUTDOWN_EXECUTION); 548 mCompletedListener.onJobCompleted(completedJob, reschedule); 549 } 550 551 /** 552 * Called when sending a message to the client, over whose execution we have no control. If 553 * we haven't received a response in a certain amount of time, we want to give up and carry 554 * on with life. 555 */ 556 private void scheduleOpTimeOut() { 557 mCallbackHandler.removeMessages(MSG_TIMEOUT); 558 559 final long timeoutMillis = (mVerb == VERB_EXECUTING) ? 560 EXECUTING_TIMESLICE_MILLIS : OP_TIMEOUT_MILLIS; 561 if (DEBUG) { 562 Slog.d(TAG, "Scheduling time out for '" + 563 mRunningJob.getServiceComponent().getShortClassName() + "' jId: " + 564 mParams.getJobId() + ", in " + (timeoutMillis / 1000) + " s"); 565 } 566 Message m = mCallbackHandler.obtainMessage(MSG_TIMEOUT); 567 mCallbackHandler.sendMessageDelayed(m, timeoutMillis); 568 mTimeoutElapsed = SystemClock.elapsedRealtime() + timeoutMillis; 569 } 570 } 571} 572