JobServiceContext.java revision 75fc5258b73b4b9b079a9383420a1d6b88575d72
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 scheduleOpTimeOut(); 161 final Intent intent = new Intent().setComponent(job.getServiceComponent()); 162 boolean binding = mContext.bindServiceAsUser(intent, this, 163 Context.BIND_AUTO_CREATE | Context.BIND_NOT_FOREGROUND, 164 new UserHandle(job.getUserId())); 165 if (!binding) { 166 if (DEBUG) { 167 Slog.d(TAG, job.getServiceComponent().getShortClassName() + " unavailable."); 168 } 169 mRunningJob = null; 170 mParams = null; 171 mExecutionStartTimeElapsed = 0L; 172 return false; 173 } 174 try { 175 mBatteryStats.noteJobStart(job.getName(), job.getUid()); 176 } catch (RemoteException e) { 177 // Whatever. 178 } 179 mAvailable = false; 180 return true; 181 } 182 } 183 184 /** 185 * Used externally to query the running job. Will return null if there is no job running. 186 * Be careful when using this function, at any moment it's possible that the job returned may 187 * stop executing. 188 */ 189 JobStatus getRunningJob() { 190 synchronized (mLock) { 191 return mRunningJob; 192 } 193 } 194 195 /** Called externally when a job that was scheduled for execution should be cancelled. */ 196 void cancelExecutingJob() { 197 mCallbackHandler.obtainMessage(MSG_CANCEL).sendToTarget(); 198 } 199 200 /** 201 * @return Whether this context is available to handle incoming work. 202 */ 203 boolean isAvailable() { 204 synchronized (mLock) { 205 return mAvailable; 206 } 207 } 208 209 long getExecutionStartTimeElapsed() { 210 return mExecutionStartTimeElapsed; 211 } 212 213 long getTimeoutElapsed() { 214 return mTimeoutElapsed; 215 } 216 217 @Override 218 public void jobFinished(int jobId, boolean reschedule) { 219 if (!verifyCallingUid()) { 220 return; 221 } 222 mCallbackHandler.obtainMessage(MSG_CALLBACK, jobId, reschedule ? 1 : 0) 223 .sendToTarget(); 224 } 225 226 @Override 227 public void acknowledgeStopMessage(int jobId, boolean reschedule) { 228 if (!verifyCallingUid()) { 229 return; 230 } 231 mCallbackHandler.obtainMessage(MSG_CALLBACK, jobId, reschedule ? 1 : 0) 232 .sendToTarget(); 233 } 234 235 @Override 236 public void acknowledgeStartMessage(int jobId, boolean ongoing) { 237 if (!verifyCallingUid()) { 238 return; 239 } 240 mCallbackHandler.obtainMessage(MSG_CALLBACK, jobId, ongoing ? 1 : 0).sendToTarget(); 241 } 242 243 /** 244 * We acquire/release a wakelock on onServiceConnected/unbindService. This mirrors the work 245 * we intend to send to the client - we stop sending work when the service is unbound so until 246 * then we keep the wakelock. 247 * @param name The concrete component name of the service that has been connected. 248 * @param service The IBinder of the Service's communication channel, 249 */ 250 @Override 251 public void onServiceConnected(ComponentName name, IBinder service) { 252 if (!name.equals(mRunningJob.getServiceComponent())) { 253 mCallbackHandler.obtainMessage(MSG_SHUTDOWN_EXECUTION).sendToTarget(); 254 return; 255 } 256 this.service = IJobService.Stub.asInterface(service); 257 final PowerManager pm = 258 (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); 259 mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, mRunningJob.getTag()); 260 mWakeLock.setWorkSource(new WorkSource(mRunningJob.getUid())); 261 mWakeLock.setReferenceCounted(false); 262 mWakeLock.acquire(); 263 mCallbackHandler.obtainMessage(MSG_SERVICE_BOUND).sendToTarget(); 264 } 265 266 /** If the client service crashes we reschedule this job and clean up. */ 267 @Override 268 public void onServiceDisconnected(ComponentName name) { 269 mCallbackHandler.obtainMessage(MSG_SHUTDOWN_EXECUTION).sendToTarget(); 270 } 271 272 /** 273 * This class is reused across different clients, and passes itself in as a callback. Check 274 * whether the client exercising the callback is the client we expect. 275 * @return True if the binder calling is coming from the client we expect. 276 */ 277 private boolean verifyCallingUid() { 278 if (mRunningJob == null || Binder.getCallingUid() != mRunningJob.getUid()) { 279 if (DEBUG) { 280 Slog.d(TAG, "Stale callback received, ignoring."); 281 } 282 return false; 283 } 284 return true; 285 } 286 287 /** 288 * Handles the lifecycle of the JobService binding/callbacks, etc. The convention within this 289 * class is to append 'H' to each function name that can only be called on this handler. This 290 * isn't strictly necessary because all of these functions are private, but helps clarity. 291 */ 292 private class JobServiceHandler extends Handler { 293 JobServiceHandler(Looper looper) { 294 super(looper); 295 } 296 297 @Override 298 public void handleMessage(Message message) { 299 switch (message.what) { 300 case MSG_SERVICE_BOUND: 301 removeMessages(MSG_TIMEOUT); 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 (DEBUG) { 341 Slog.d(TAG, "MSG_SERVICE_BOUND for " + mRunningJob.toShortString()); 342 } 343 if (mVerb != VERB_BINDING) { 344 Slog.e(TAG, "Sending onStartJob for a job that isn't pending. " 345 + VERB_STRINGS[mVerb]); 346 closeAndCleanupJobH(false /* reschedule */); 347 return; 348 } 349 if (mCancelled.get()) { 350 if (DEBUG) { 351 Slog.d(TAG, "Job cancelled while waiting for bind to complete. " 352 + mRunningJob); 353 } 354 closeAndCleanupJobH(true /* reschedule */); 355 return; 356 } 357 try { 358 mVerb = VERB_STARTING; 359 scheduleOpTimeOut(); 360 service.startJob(mParams); 361 } catch (RemoteException e) { 362 Slog.e(TAG, "Error sending onStart message to '" + 363 mRunningJob.getServiceComponent().getShortClassName() + "' ", e); 364 } 365 } 366 367 /** 368 * State behaviours. 369 * VERB_STARTING -> Successful start, change job to VERB_EXECUTING and post timeout. 370 * _PENDING -> Error 371 * _EXECUTING -> Error 372 * _STOPPING -> Error 373 */ 374 private void handleStartedH(boolean workOngoing) { 375 switch (mVerb) { 376 case VERB_STARTING: 377 mVerb = VERB_EXECUTING; 378 if (!workOngoing) { 379 // Job is finished already so fast-forward to handleFinished. 380 handleFinishedH(false); 381 return; 382 } 383 if (mCancelled.get()) { 384 if (DEBUG) { 385 Slog.d(TAG, "Job cancelled while waiting for onStartJob to complete."); 386 } 387 // Cancelled *while* waiting for acknowledgeStartMessage from client. 388 handleCancelH(); 389 return; 390 } 391 scheduleOpTimeOut(); 392 break; 393 default: 394 Slog.e(TAG, "Handling started job but job wasn't starting! Was " 395 + VERB_STRINGS[mVerb] + "."); 396 return; 397 } 398 } 399 400 /** 401 * VERB_EXECUTING -> Client called jobFinished(), clean up and notify done. 402 * _STOPPING -> Successful finish, clean up and notify done. 403 * _STARTING -> Error 404 * _PENDING -> Error 405 */ 406 private void handleFinishedH(boolean reschedule) { 407 switch (mVerb) { 408 case VERB_EXECUTING: 409 case VERB_STOPPING: 410 closeAndCleanupJobH(reschedule); 411 break; 412 default: 413 Slog.e(TAG, "Got an execution complete message for a job that wasn't being" + 414 "executed. Was " + VERB_STRINGS[mVerb] + "."); 415 } 416 } 417 418 /** 419 * A job can be in various states when a cancel request comes in: 420 * VERB_BINDING -> Cancelled before bind completed. Mark as cancelled and wait for 421 * {@link #onServiceConnected(android.content.ComponentName, android.os.IBinder)} 422 * _STARTING -> Mark as cancelled and wait for 423 * {@link JobServiceContext#acknowledgeStartMessage(int, boolean)} 424 * _EXECUTING -> call {@link #sendStopMessageH}}, but only if there are no callbacks 425 * in the message queue. 426 * _ENDING -> No point in doing anything here, so we ignore. 427 */ 428 private void handleCancelH() { 429 if (mRunningJob == null) { 430 if (DEBUG) { 431 Slog.d(TAG, "Trying to process cancel for torn-down context, ignoring."); 432 } 433 return; 434 } 435 if (JobSchedulerService.DEBUG) { 436 Slog.d(TAG, "Handling cancel for: " + mRunningJob.getJobId() + " " 437 + VERB_STRINGS[mVerb]); 438 } 439 switch (mVerb) { 440 case VERB_BINDING: 441 case VERB_STARTING: 442 mCancelled.set(true); 443 break; 444 case VERB_EXECUTING: 445 if (hasMessages(MSG_CALLBACK)) { 446 // If the client has called jobFinished, ignore this cancel. 447 return; 448 } 449 sendStopMessageH(); 450 break; 451 case VERB_STOPPING: 452 // Nada. 453 break; 454 default: 455 Slog.e(TAG, "Cancelling a job without a valid verb: " + mVerb); 456 break; 457 } 458 } 459 460 /** Process MSG_TIMEOUT here. */ 461 private void handleOpTimeoutH() { 462 switch (mVerb) { 463 case VERB_BINDING: 464 Slog.e(TAG, "Time-out while trying to bind " + mRunningJob.toShortString() + 465 ", dropping."); 466 closeAndCleanupJobH(false /* needsReschedule */); 467 break; 468 case VERB_STARTING: 469 // Client unresponsive - wedged or failed to respond in time. We don't really 470 // know what happened so let's log it and notify the JobScheduler 471 // FINISHED/NO-RETRY. 472 Slog.e(TAG, "No response from client for onStartJob '" + 473 mRunningJob.toShortString()); 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.toShortString()); 480 closeAndCleanupJobH(true /* needsReschedule */); 481 break; 482 case VERB_EXECUTING: 483 // Not an error - client ran out of time. 484 Slog.i(TAG, "Client timed out while executing (no jobFinished received)." + 485 " sending onStop. " + mRunningJob.toShortString()); 486 sendStopMessageH(); 487 break; 488 default: 489 Slog.e(TAG, "Handling timeout for an invalid job state: " + 490 mRunningJob.toShortString() + ", dropping."); 491 closeAndCleanupJobH(false /* needsReschedule */); 492 } 493 } 494 495 /** 496 * Already running, need to stop. Will switch {@link #mVerb} from VERB_EXECUTING -> 497 * VERB_STOPPING. 498 */ 499 private void sendStopMessageH() { 500 mCallbackHandler.removeMessages(MSG_TIMEOUT); 501 if (mVerb != VERB_EXECUTING) { 502 Slog.e(TAG, "Sending onStopJob for a job that isn't started. " + mRunningJob); 503 closeAndCleanupJobH(false /* reschedule */); 504 return; 505 } 506 try { 507 mVerb = VERB_STOPPING; 508 scheduleOpTimeOut(); 509 service.stopJob(mParams); 510 } catch (RemoteException e) { 511 Slog.e(TAG, "Error sending onStopJob to client.", e); 512 closeAndCleanupJobH(false /* reschedule */); 513 } 514 } 515 516 /** 517 * The provided job has finished, either by calling 518 * {@link android.app.job.JobService#jobFinished(android.app.job.JobParameters, boolean)} 519 * or from acknowledging the stop message we sent. Either way, we're done tracking it and 520 * we want to clean up internally. 521 */ 522 private void closeAndCleanupJobH(boolean reschedule) { 523 final JobStatus completedJob = mRunningJob; 524 synchronized (mLock) { 525 try { 526 mBatteryStats.noteJobFinish(mRunningJob.getName(), mRunningJob.getUid()); 527 } catch (RemoteException e) { 528 // Whatever. 529 } 530 if (mWakeLock != null) { 531 mWakeLock.release(); 532 } 533 mContext.unbindService(JobServiceContext.this); 534 mWakeLock = null; 535 mRunningJob = null; 536 mParams = null; 537 mVerb = -1; 538 mCancelled.set(false); 539 service = null; 540 mAvailable = true; 541 } 542 removeMessages(MSG_TIMEOUT); 543 removeMessages(MSG_CALLBACK); 544 removeMessages(MSG_SERVICE_BOUND); 545 removeMessages(MSG_CANCEL); 546 removeMessages(MSG_SHUTDOWN_EXECUTION); 547 mCompletedListener.onJobCompleted(completedJob, reschedule); 548 } 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