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