JobServiceContext.java revision 03a4da6e8e92b19c1345016c06694cb3aabbfc27
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(this, job.getJobId(), job.getExtras(), 157 !job.isConstraintsSatisfied()); 158 mExecutionStartTimeElapsed = SystemClock.elapsedRealtime(); 159 160 mVerb = VERB_BINDING; 161 scheduleOpTimeOut(); 162 final Intent intent = new Intent().setComponent(job.getServiceComponent()); 163 boolean binding = mContext.bindServiceAsUser(intent, this, 164 Context.BIND_AUTO_CREATE | Context.BIND_NOT_FOREGROUND, 165 new UserHandle(job.getUserId())); 166 if (!binding) { 167 if (DEBUG) { 168 Slog.d(TAG, job.getServiceComponent().getShortClassName() + " unavailable."); 169 } 170 mRunningJob = null; 171 mParams = null; 172 mExecutionStartTimeElapsed = 0L; 173 return false; 174 } 175 try { 176 mBatteryStats.noteJobStart(job.getName(), job.getUid()); 177 } catch (RemoteException e) { 178 // Whatever. 179 } 180 mAvailable = false; 181 return true; 182 } 183 } 184 185 /** 186 * Used externally to query the running job. Will return null if there is no job running. 187 * Be careful when using this function, at any moment it's possible that the job returned may 188 * stop executing. 189 */ 190 JobStatus getRunningJob() { 191 synchronized (mLock) { 192 return mRunningJob; 193 } 194 } 195 196 /** Called externally when a job that was scheduled for execution should be cancelled. */ 197 void cancelExecutingJob() { 198 mCallbackHandler.obtainMessage(MSG_CANCEL).sendToTarget(); 199 } 200 201 /** 202 * @return Whether this context is available to handle incoming work. 203 */ 204 boolean isAvailable() { 205 synchronized (mLock) { 206 return mAvailable; 207 } 208 } 209 210 long getExecutionStartTimeElapsed() { 211 return mExecutionStartTimeElapsed; 212 } 213 214 long getTimeoutElapsed() { 215 return mTimeoutElapsed; 216 } 217 218 @Override 219 public void jobFinished(int jobId, boolean reschedule) { 220 if (!verifyCallingUid()) { 221 return; 222 } 223 mCallbackHandler.obtainMessage(MSG_CALLBACK, jobId, reschedule ? 1 : 0) 224 .sendToTarget(); 225 } 226 227 @Override 228 public void acknowledgeStopMessage(int jobId, boolean reschedule) { 229 if (!verifyCallingUid()) { 230 return; 231 } 232 mCallbackHandler.obtainMessage(MSG_CALLBACK, jobId, reschedule ? 1 : 0) 233 .sendToTarget(); 234 } 235 236 @Override 237 public void acknowledgeStartMessage(int jobId, boolean ongoing) { 238 if (!verifyCallingUid()) { 239 return; 240 } 241 mCallbackHandler.obtainMessage(MSG_CALLBACK, jobId, ongoing ? 1 : 0).sendToTarget(); 242 } 243 244 /** 245 * We acquire/release a wakelock on onServiceConnected/unbindService. This mirrors the work 246 * we intend to send to the client - we stop sending work when the service is unbound so until 247 * then we keep the wakelock. 248 * @param name The concrete component name of the service that has been connected. 249 * @param service The IBinder of the Service's communication channel, 250 */ 251 @Override 252 public void onServiceConnected(ComponentName name, IBinder service) { 253 if (!name.equals(mRunningJob.getServiceComponent())) { 254 mCallbackHandler.obtainMessage(MSG_SHUTDOWN_EXECUTION).sendToTarget(); 255 return; 256 } 257 this.service = IJobService.Stub.asInterface(service); 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 removeMessages(MSG_TIMEOUT); 303 handleServiceBoundH(); 304 break; 305 case MSG_CALLBACK: 306 if (DEBUG) { 307 Slog.d(TAG, "MSG_CALLBACK of : " + mRunningJob + " v:" + 308 (mVerb >= 0 ? VERB_STRINGS[mVerb] : "[invalid]")); 309 } 310 removeMessages(MSG_TIMEOUT); 311 312 if (mVerb == VERB_STARTING) { 313 final boolean workOngoing = message.arg2 == 1; 314 handleStartedH(workOngoing); 315 } else if (mVerb == VERB_EXECUTING || 316 mVerb == VERB_STOPPING) { 317 final boolean reschedule = message.arg2 == 1; 318 handleFinishedH(reschedule); 319 } else { 320 if (DEBUG) { 321 Slog.d(TAG, "Unrecognised callback: " + mRunningJob); 322 } 323 } 324 break; 325 case MSG_CANCEL: 326 handleCancelH(); 327 break; 328 case MSG_TIMEOUT: 329 handleOpTimeoutH(); 330 break; 331 case MSG_SHUTDOWN_EXECUTION: 332 closeAndCleanupJobH(true /* needsReschedule */); 333 break; 334 default: 335 Slog.e(TAG, "Unrecognised message: " + message); 336 } 337 } 338 339 /** Start the job on the service. */ 340 private void handleServiceBoundH() { 341 if (DEBUG) { 342 Slog.d(TAG, "MSG_SERVICE_BOUND for " + mRunningJob.toShortString()); 343 } 344 if (mVerb != VERB_BINDING) { 345 Slog.e(TAG, "Sending onStartJob for a job that isn't pending. " 346 + VERB_STRINGS[mVerb]); 347 closeAndCleanupJobH(false /* reschedule */); 348 return; 349 } 350 if (mCancelled.get()) { 351 if (DEBUG) { 352 Slog.d(TAG, "Job cancelled while waiting for bind to complete. " 353 + mRunningJob); 354 } 355 closeAndCleanupJobH(true /* reschedule */); 356 return; 357 } 358 try { 359 mVerb = VERB_STARTING; 360 scheduleOpTimeOut(); 361 service.startJob(mParams); 362 } catch (RemoteException e) { 363 Slog.e(TAG, "Error sending onStart message to '" + 364 mRunningJob.getServiceComponent().getShortClassName() + "' ", e); 365 } 366 } 367 368 /** 369 * State behaviours. 370 * VERB_STARTING -> Successful start, change job to VERB_EXECUTING and post timeout. 371 * _PENDING -> Error 372 * _EXECUTING -> Error 373 * _STOPPING -> Error 374 */ 375 private void handleStartedH(boolean workOngoing) { 376 switch (mVerb) { 377 case VERB_STARTING: 378 mVerb = VERB_EXECUTING; 379 if (!workOngoing) { 380 // Job is finished already so fast-forward to handleFinished. 381 handleFinishedH(false); 382 return; 383 } 384 if (mCancelled.get()) { 385 if (DEBUG) { 386 Slog.d(TAG, "Job cancelled while waiting for onStartJob to complete."); 387 } 388 // Cancelled *while* waiting for acknowledgeStartMessage from client. 389 handleCancelH(); 390 return; 391 } 392 scheduleOpTimeOut(); 393 break; 394 default: 395 Slog.e(TAG, "Handling started job but job wasn't starting! Was " 396 + VERB_STRINGS[mVerb] + "."); 397 return; 398 } 399 } 400 401 /** 402 * VERB_EXECUTING -> Client called jobFinished(), clean up and notify done. 403 * _STOPPING -> Successful finish, clean up and notify done. 404 * _STARTING -> Error 405 * _PENDING -> Error 406 */ 407 private void handleFinishedH(boolean reschedule) { 408 switch (mVerb) { 409 case VERB_EXECUTING: 410 case VERB_STOPPING: 411 closeAndCleanupJobH(reschedule); 412 break; 413 default: 414 Slog.e(TAG, "Got an execution complete message for a job that wasn't being" + 415 "executed. Was " + VERB_STRINGS[mVerb] + "."); 416 } 417 } 418 419 /** 420 * A job can be in various states when a cancel request comes in: 421 * VERB_BINDING -> Cancelled before bind completed. Mark as cancelled and wait for 422 * {@link #onServiceConnected(android.content.ComponentName, android.os.IBinder)} 423 * _STARTING -> Mark as cancelled and wait for 424 * {@link JobServiceContext#acknowledgeStartMessage(int, boolean)} 425 * _EXECUTING -> call {@link #sendStopMessageH}}, but only if there are no callbacks 426 * in the message queue. 427 * _ENDING -> No point in doing anything here, so we ignore. 428 */ 429 private void handleCancelH() { 430 if (mRunningJob == null) { 431 if (DEBUG) { 432 Slog.d(TAG, "Trying to process cancel for torn-down context, ignoring."); 433 } 434 return; 435 } 436 if (JobSchedulerService.DEBUG) { 437 Slog.d(TAG, "Handling cancel for: " + mRunningJob.getJobId() + " " 438 + VERB_STRINGS[mVerb]); 439 } 440 switch (mVerb) { 441 case VERB_BINDING: 442 case VERB_STARTING: 443 mCancelled.set(true); 444 break; 445 case VERB_EXECUTING: 446 if (hasMessages(MSG_CALLBACK)) { 447 // If the client has called jobFinished, ignore this cancel. 448 return; 449 } 450 sendStopMessageH(); 451 break; 452 case VERB_STOPPING: 453 // Nada. 454 break; 455 default: 456 Slog.e(TAG, "Cancelling a job without a valid verb: " + mVerb); 457 break; 458 } 459 } 460 461 /** Process MSG_TIMEOUT here. */ 462 private void handleOpTimeoutH() { 463 switch (mVerb) { 464 case VERB_BINDING: 465 Slog.e(TAG, "Time-out while trying to bind " + mRunningJob.toShortString() + 466 ", dropping."); 467 closeAndCleanupJobH(false /* needsReschedule */); 468 break; 469 case VERB_STARTING: 470 // Client unresponsive - wedged or failed to respond in time. We don't really 471 // know what happened so let's log it and notify the JobScheduler 472 // FINISHED/NO-RETRY. 473 Slog.e(TAG, "No response from client for onStartJob '" + 474 mRunningJob.toShortString()); 475 closeAndCleanupJobH(false /* needsReschedule */); 476 break; 477 case VERB_STOPPING: 478 // At least we got somewhere, so fail but ask the JobScheduler to reschedule. 479 Slog.e(TAG, "No response from client for onStopJob, '" + 480 mRunningJob.toShortString()); 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. " + mRunningJob.toShortString()); 487 sendStopMessageH(); 488 break; 489 default: 490 Slog.e(TAG, "Handling timeout for an invalid job state: " + 491 mRunningJob.toShortString() + ", dropping."); 492 closeAndCleanupJobH(false /* needsReschedule */); 493 } 494 } 495 496 /** 497 * Already running, need to stop. Will switch {@link #mVerb} from VERB_EXECUTING -> 498 * VERB_STOPPING. 499 */ 500 private void sendStopMessageH() { 501 mCallbackHandler.removeMessages(MSG_TIMEOUT); 502 if (mVerb != VERB_EXECUTING) { 503 Slog.e(TAG, "Sending onStopJob for a job that isn't started. " + mRunningJob); 504 closeAndCleanupJobH(false /* reschedule */); 505 return; 506 } 507 try { 508 mVerb = VERB_STOPPING; 509 scheduleOpTimeOut(); 510 service.stopJob(mParams); 511 } catch (RemoteException e) { 512 Slog.e(TAG, "Error sending onStopJob to client.", e); 513 closeAndCleanupJobH(false /* reschedule */); 514 } 515 } 516 517 /** 518 * The provided job has finished, either by calling 519 * {@link android.app.job.JobService#jobFinished(android.app.job.JobParameters, boolean)} 520 * or from acknowledging the stop message we sent. Either way, we're done tracking it and 521 * we want to clean up internally. 522 */ 523 private void closeAndCleanupJobH(boolean reschedule) { 524 final JobStatus completedJob = mRunningJob; 525 synchronized (mLock) { 526 try { 527 mBatteryStats.noteJobFinish(mRunningJob.getName(), mRunningJob.getUid()); 528 } catch (RemoteException e) { 529 // Whatever. 530 } 531 if (mWakeLock != null) { 532 mWakeLock.release(); 533 } 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 /** 553 * Called when sending a message to the client, over whose execution we have no control. If 554 * we haven't received a response in a certain amount of time, we want to give up and carry 555 * on with life. 556 */ 557 private void scheduleOpTimeOut() { 558 mCallbackHandler.removeMessages(MSG_TIMEOUT); 559 560 final long timeoutMillis = (mVerb == VERB_EXECUTING) ? 561 EXECUTING_TIMESLICE_MILLIS : OP_TIMEOUT_MILLIS; 562 if (DEBUG) { 563 Slog.d(TAG, "Scheduling time out for '" + 564 mRunningJob.getServiceComponent().getShortClassName() + "' jId: " + 565 mParams.getJobId() + ", in " + (timeoutMillis / 1000) + " s"); 566 } 567 Message m = mCallbackHandler.obtainMessage(MSG_TIMEOUT); 568 mCallbackHandler.sendMessageDelayed(m, timeoutMillis); 569 mTimeoutElapsed = SystemClock.elapsedRealtime() + timeoutMillis; 570 } 571} 572