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