1/* 2 * Copyright (C) 2017 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 androidx.core.app; 18 19import android.app.Service; 20import android.app.job.JobInfo; 21import android.app.job.JobParameters; 22import android.app.job.JobScheduler; 23import android.app.job.JobServiceEngine; 24import android.app.job.JobWorkItem; 25import android.content.ComponentName; 26import android.content.Context; 27import android.content.Intent; 28import android.os.AsyncTask; 29import android.os.Build; 30import android.os.IBinder; 31import android.os.PowerManager; 32import android.util.Log; 33 34import androidx.annotation.NonNull; 35import androidx.annotation.Nullable; 36import androidx.annotation.RequiresApi; 37 38import java.util.ArrayList; 39import java.util.HashMap; 40 41/** 42 * Helper for processing work that has been enqueued for a job/service. When running on 43 * {@link android.os.Build.VERSION_CODES#O Android O} or later, the work will be dispatched 44 * as a job via {@link android.app.job.JobScheduler#enqueue JobScheduler.enqueue}. When running 45 * on older versions of the platform, it will use 46 * {@link android.content.Context#startService Context.startService}. 47 * 48 * <p>You must publish your subclass in your manifest for the system to interact with. This 49 * should be published as a {@link android.app.job.JobService}, as described for that class, 50 * since on O and later platforms it will be executed that way.</p> 51 * 52 * <p>Use {@link #enqueueWork(Context, Class, int, Intent)} to enqueue new work to be 53 * dispatched to and handled by your service. It will be executed in 54 * {@link #onHandleWork(Intent)}.</p> 55 * 56 * <p>You do not need to use {@link androidx.legacy.content.WakefulBroadcastReceiver} 57 * when using this class. When running on {@link android.os.Build.VERSION_CODES#O Android O}, 58 * the JobScheduler will take care of wake locks for you (holding a wake lock from the time 59 * you enqueue work until the job has been dispatched and while it is running). When running 60 * on previous versions of the platform, this wake lock handling is emulated in the class here 61 * by directly calling the PowerManager; this means the application must request the 62 * {@link android.Manifest.permission#WAKE_LOCK} permission.</p> 63 * 64 * <p>There are a few important differences in behavior when running on 65 * {@link android.os.Build.VERSION_CODES#O Android O} or later as a Job vs. pre-O:</p> 66 * 67 * <ul> 68 * <li><p>When running as a pre-O service, the act of enqueueing work will generally start 69 * the service immediately, regardless of whether the device is dozing or in other 70 * conditions. When running as a Job, it will be subject to standard JobScheduler 71 * policies for a Job with a {@link android.app.job.JobInfo.Builder#setOverrideDeadline(long)} 72 * of 0: the job will not run while the device is dozing, it may get delayed more than 73 * a service if the device is under strong memory pressure with lots of demand to run 74 * jobs.</p></li> 75 * <li><p>When running as a pre-O service, the normal service execution semantics apply: 76 * the service can run indefinitely, though the longer it runs the more likely the system 77 * will be to outright kill its process, and under memory pressure one should expect 78 * the process to be killed even of recently started services. When running as a Job, 79 * the typical {@link android.app.job.JobService} execution time limit will apply, after 80 * which the job will be stopped (cleanly, not by killing the process) and rescheduled 81 * to continue its execution later. Job are generally not killed when the system is 82 * under memory pressure, since the number of concurrent jobs is adjusted based on the 83 * memory state of the device.</p></li> 84 * </ul> 85 * 86 * <p>Here is an example implementation of this class:</p> 87 * 88 * {@sample frameworks/support/samples/Support4Demos/src/main/java/com/example/android/supportv4/app/SimpleJobIntentService.java 89 * complete} 90 */ 91public abstract class JobIntentService extends Service { 92 static final String TAG = "JobIntentService"; 93 94 static final boolean DEBUG = false; 95 96 CompatJobEngine mJobImpl; 97 WorkEnqueuer mCompatWorkEnqueuer; 98 CommandProcessor mCurProcessor; 99 boolean mInterruptIfStopped = false; 100 boolean mStopped = false; 101 boolean mDestroyed = false; 102 103 final ArrayList<CompatWorkItem> mCompatQueue; 104 105 static final Object sLock = new Object(); 106 static final HashMap<ComponentName, WorkEnqueuer> sClassWorkEnqueuer = new HashMap<>(); 107 108 /** 109 * Base class for the target service we can deliver work to and the implementation of 110 * how to deliver that work. 111 */ 112 abstract static class WorkEnqueuer { 113 final ComponentName mComponentName; 114 115 boolean mHasJobId; 116 int mJobId; 117 118 WorkEnqueuer(Context context, ComponentName cn) { 119 mComponentName = cn; 120 } 121 122 void ensureJobId(int jobId) { 123 if (!mHasJobId) { 124 mHasJobId = true; 125 mJobId = jobId; 126 } else if (mJobId != jobId) { 127 throw new IllegalArgumentException("Given job ID " + jobId 128 + " is different than previous " + mJobId); 129 } 130 } 131 132 abstract void enqueueWork(Intent work); 133 134 public void serviceStartReceived() { 135 } 136 137 public void serviceProcessingStarted() { 138 } 139 140 public void serviceProcessingFinished() { 141 } 142 } 143 144 /** 145 * Get rid of lint warnings about API levels. 146 */ 147 interface CompatJobEngine { 148 IBinder compatGetBinder(); 149 GenericWorkItem dequeueWork(); 150 } 151 152 /** 153 * An implementation of WorkEnqueuer that works for pre-O (raw Service-based). 154 */ 155 static final class CompatWorkEnqueuer extends WorkEnqueuer { 156 private final Context mContext; 157 private final PowerManager.WakeLock mLaunchWakeLock; 158 private final PowerManager.WakeLock mRunWakeLock; 159 boolean mLaunchingService; 160 boolean mServiceProcessing; 161 162 CompatWorkEnqueuer(Context context, ComponentName cn) { 163 super(context, cn); 164 mContext = context.getApplicationContext(); 165 // Make wake locks. We need two, because the launch wake lock wants to have 166 // a timeout, and the system does not do the right thing if you mix timeout and 167 // non timeout (or even changing the timeout duration) in one wake lock. 168 PowerManager pm = ((PowerManager) context.getSystemService(Context.POWER_SERVICE)); 169 mLaunchWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, 170 cn.getClassName() + ":launch"); 171 mLaunchWakeLock.setReferenceCounted(false); 172 mRunWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, 173 cn.getClassName() + ":run"); 174 mRunWakeLock.setReferenceCounted(false); 175 } 176 177 @Override 178 void enqueueWork(Intent work) { 179 Intent intent = new Intent(work); 180 intent.setComponent(mComponentName); 181 if (DEBUG) Log.d(TAG, "Starting service for work: " + work); 182 if (mContext.startService(intent) != null) { 183 synchronized (this) { 184 if (!mLaunchingService) { 185 mLaunchingService = true; 186 if (!mServiceProcessing) { 187 // If the service is not already holding the wake lock for 188 // itself, acquire it now to keep the system running until 189 // we get this work dispatched. We use a timeout here to 190 // protect against whatever problem may cause it to not get 191 // the work. 192 mLaunchWakeLock.acquire(60 * 1000); 193 } 194 } 195 } 196 } 197 } 198 199 @Override 200 public void serviceStartReceived() { 201 synchronized (this) { 202 // Once we have started processing work, we can count whatever last 203 // enqueueWork() that happened as handled. 204 mLaunchingService = false; 205 } 206 } 207 208 @Override 209 public void serviceProcessingStarted() { 210 synchronized (this) { 211 // We hold the wake lock as long as the service is processing commands. 212 if (!mServiceProcessing) { 213 mServiceProcessing = true; 214 // Keep the device awake, but only for at most 10 minutes at a time 215 // (Similar to JobScheduler.) 216 mRunWakeLock.acquire(10 * 60 * 1000L); 217 mLaunchWakeLock.release(); 218 } 219 } 220 } 221 222 @Override 223 public void serviceProcessingFinished() { 224 synchronized (this) { 225 if (mServiceProcessing) { 226 // If we are transitioning back to a wakelock with a timeout, do the same 227 // as if we had enqueued work without the service running. 228 if (mLaunchingService) { 229 mLaunchWakeLock.acquire(60 * 1000); 230 } 231 mServiceProcessing = false; 232 mRunWakeLock.release(); 233 } 234 } 235 } 236 } 237 238 /** 239 * Implementation of a JobServiceEngine for interaction with JobIntentService. 240 */ 241 @RequiresApi(26) 242 static final class JobServiceEngineImpl extends JobServiceEngine 243 implements JobIntentService.CompatJobEngine { 244 static final String TAG = "JobServiceEngineImpl"; 245 246 static final boolean DEBUG = false; 247 248 final JobIntentService mService; 249 final Object mLock = new Object(); 250 JobParameters mParams; 251 252 final class WrapperWorkItem implements JobIntentService.GenericWorkItem { 253 final JobWorkItem mJobWork; 254 255 WrapperWorkItem(JobWorkItem jobWork) { 256 mJobWork = jobWork; 257 } 258 259 @Override 260 public Intent getIntent() { 261 return mJobWork.getIntent(); 262 } 263 264 @Override 265 public void complete() { 266 synchronized (mLock) { 267 if (mParams != null) { 268 mParams.completeWork(mJobWork); 269 } 270 } 271 } 272 } 273 274 JobServiceEngineImpl(JobIntentService service) { 275 super(service); 276 mService = service; 277 } 278 279 @Override 280 public IBinder compatGetBinder() { 281 return getBinder(); 282 } 283 284 @Override 285 public boolean onStartJob(JobParameters params) { 286 if (DEBUG) Log.d(TAG, "onStartJob: " + params); 287 mParams = params; 288 // We can now start dequeuing work! 289 mService.ensureProcessorRunningLocked(false); 290 return true; 291 } 292 293 @Override 294 public boolean onStopJob(JobParameters params) { 295 if (DEBUG) Log.d(TAG, "onStartJob: " + params); 296 boolean result = mService.doStopCurrentWork(); 297 synchronized (mLock) { 298 // Once we return, the job is stopped, so its JobParameters are no 299 // longer valid and we should not be doing anything with them. 300 mParams = null; 301 } 302 return result; 303 } 304 305 /** 306 * Dequeue some work. 307 */ 308 @Override 309 public JobIntentService.GenericWorkItem dequeueWork() { 310 JobWorkItem work; 311 synchronized (mLock) { 312 if (mParams == null) { 313 return null; 314 } 315 work = mParams.dequeueWork(); 316 } 317 if (work != null) { 318 work.getIntent().setExtrasClassLoader(mService.getClassLoader()); 319 return new WrapperWorkItem(work); 320 } else { 321 return null; 322 } 323 } 324 } 325 326 @RequiresApi(26) 327 static final class JobWorkEnqueuer extends JobIntentService.WorkEnqueuer { 328 private final JobInfo mJobInfo; 329 private final JobScheduler mJobScheduler; 330 331 JobWorkEnqueuer(Context context, ComponentName cn, int jobId) { 332 super(context, cn); 333 ensureJobId(jobId); 334 JobInfo.Builder b = new JobInfo.Builder(jobId, mComponentName); 335 mJobInfo = b.setOverrideDeadline(0).build(); 336 mJobScheduler = (JobScheduler) context.getApplicationContext().getSystemService( 337 Context.JOB_SCHEDULER_SERVICE); 338 } 339 340 @Override 341 void enqueueWork(Intent work) { 342 if (DEBUG) Log.d(TAG, "Enqueueing work: " + work); 343 mJobScheduler.enqueue(mJobInfo, new JobWorkItem(work)); 344 } 345 } 346 347 /** 348 * Abstract definition of an item of work that is being dispatched. 349 */ 350 interface GenericWorkItem { 351 Intent getIntent(); 352 void complete(); 353 } 354 355 /** 356 * An implementation of GenericWorkItem that dispatches work for pre-O platforms: intents 357 * received through a raw service's onStartCommand. 358 */ 359 final class CompatWorkItem implements GenericWorkItem { 360 final Intent mIntent; 361 final int mStartId; 362 363 CompatWorkItem(Intent intent, int startId) { 364 mIntent = intent; 365 mStartId = startId; 366 } 367 368 @Override 369 public Intent getIntent() { 370 return mIntent; 371 } 372 373 @Override 374 public void complete() { 375 if (DEBUG) Log.d(TAG, "Stopping self: #" + mStartId); 376 stopSelf(mStartId); 377 } 378 } 379 380 /** 381 * This is a task to dequeue and process work in the background. 382 */ 383 final class CommandProcessor extends AsyncTask<Void, Void, Void> { 384 @Override 385 protected Void doInBackground(Void... params) { 386 GenericWorkItem work; 387 388 if (DEBUG) Log.d(TAG, "Starting to dequeue work..."); 389 390 while ((work = dequeueWork()) != null) { 391 if (DEBUG) Log.d(TAG, "Processing next work: " + work); 392 onHandleWork(work.getIntent()); 393 if (DEBUG) Log.d(TAG, "Completing work: " + work); 394 work.complete(); 395 } 396 397 if (DEBUG) Log.d(TAG, "Done processing work!"); 398 399 return null; 400 } 401 402 @Override 403 protected void onCancelled(Void aVoid) { 404 processorFinished(); 405 } 406 407 @Override 408 protected void onPostExecute(Void aVoid) { 409 processorFinished(); 410 } 411 } 412 413 /** 414 * Default empty constructor. 415 */ 416 public JobIntentService() { 417 if (Build.VERSION.SDK_INT >= 26) { 418 mCompatQueue = null; 419 } else { 420 mCompatQueue = new ArrayList<>(); 421 } 422 } 423 424 @Override 425 public void onCreate() { 426 super.onCreate(); 427 if (DEBUG) Log.d(TAG, "CREATING: " + this); 428 if (Build.VERSION.SDK_INT >= 26) { 429 mJobImpl = new JobServiceEngineImpl(this); 430 mCompatWorkEnqueuer = null; 431 } else { 432 mJobImpl = null; 433 ComponentName cn = new ComponentName(this, this.getClass()); 434 mCompatWorkEnqueuer = getWorkEnqueuer(this, cn, false, 0); 435 } 436 } 437 438 /** 439 * Processes start commands when running as a pre-O service, enqueueing them to be 440 * later dispatched in {@link #onHandleWork(Intent)}. 441 */ 442 @Override 443 public int onStartCommand(@Nullable Intent intent, int flags, int startId) { 444 if (mCompatQueue != null) { 445 mCompatWorkEnqueuer.serviceStartReceived(); 446 if (DEBUG) Log.d(TAG, "Received compat start command #" + startId + ": " + intent); 447 synchronized (mCompatQueue) { 448 mCompatQueue.add(new CompatWorkItem(intent != null ? intent : new Intent(), 449 startId)); 450 ensureProcessorRunningLocked(true); 451 } 452 return START_REDELIVER_INTENT; 453 } else { 454 if (DEBUG) Log.d(TAG, "Ignoring start command: " + intent); 455 return START_NOT_STICKY; 456 } 457 } 458 459 /** 460 * Returns the IBinder for the {@link android.app.job.JobServiceEngine} when 461 * running as a JobService on O and later platforms. 462 */ 463 @Override 464 public IBinder onBind(@NonNull Intent intent) { 465 if (mJobImpl != null) { 466 IBinder engine = mJobImpl.compatGetBinder(); 467 if (DEBUG) Log.d(TAG, "Returning engine: " + engine); 468 return engine; 469 } else { 470 return null; 471 } 472 } 473 474 @Override 475 public void onDestroy() { 476 super.onDestroy(); 477 if (mCompatQueue != null) { 478 synchronized (mCompatQueue) { 479 mDestroyed = true; 480 mCompatWorkEnqueuer.serviceProcessingFinished(); 481 } 482 } 483 } 484 485 /** 486 * Call this to enqueue work for your subclass of {@link JobIntentService}. This will 487 * either directly start the service (when running on pre-O platforms) or enqueue work 488 * for it as a job (when running on O and later). In either case, a wake lock will be 489 * held for you to ensure you continue running. The work you enqueue will ultimately 490 * appear at {@link #onHandleWork(Intent)}. 491 * 492 * @param context Context this is being called from. 493 * @param cls The concrete class the work should be dispatched to (this is the class that 494 * is published in your manifest). 495 * @param jobId A unique job ID for scheduling; must be the same value for all work 496 * enqueued for the same class. 497 * @param work The Intent of work to enqueue. 498 */ 499 public static void enqueueWork(@NonNull Context context, @NonNull Class cls, int jobId, 500 @NonNull Intent work) { 501 enqueueWork(context, new ComponentName(context, cls), jobId, work); 502 } 503 504 /** 505 * Like {@link #enqueueWork(Context, Class, int, Intent)}, but supplies a ComponentName 506 * for the service to interact with instead of its class. 507 * 508 * @param context Context this is being called from. 509 * @param component The published ComponentName of the class this work should be 510 * dispatched to. 511 * @param jobId A unique job ID for scheduling; must be the same value for all work 512 * enqueued for the same class. 513 * @param work The Intent of work to enqueue. 514 */ 515 public static void enqueueWork(@NonNull Context context, @NonNull ComponentName component, 516 int jobId, @NonNull Intent work) { 517 if (work == null) { 518 throw new IllegalArgumentException("work must not be null"); 519 } 520 synchronized (sLock) { 521 WorkEnqueuer we = getWorkEnqueuer(context, component, true, jobId); 522 we.ensureJobId(jobId); 523 we.enqueueWork(work); 524 } 525 } 526 527 static WorkEnqueuer getWorkEnqueuer(Context context, ComponentName cn, boolean hasJobId, 528 int jobId) { 529 WorkEnqueuer we = sClassWorkEnqueuer.get(cn); 530 if (we == null) { 531 if (Build.VERSION.SDK_INT >= 26) { 532 if (!hasJobId) { 533 throw new IllegalArgumentException("Can't be here without a job id"); 534 } 535 we = new JobWorkEnqueuer(context, cn, jobId); 536 } else { 537 we = new CompatWorkEnqueuer(context, cn); 538 } 539 sClassWorkEnqueuer.put(cn, we); 540 } 541 return we; 542 } 543 544 /** 545 * Called serially for each work dispatched to and processed by the service. This 546 * method is called on a background thread, so you can do long blocking operations 547 * here. Upon returning, that work will be considered complete and either the next 548 * pending work dispatched here or the overall service destroyed now that it has 549 * nothing else to do. 550 * 551 * <p>Be aware that when running as a job, you are limited by the maximum job execution 552 * time and any single or total sequential items of work that exceeds that limit will 553 * cause the service to be stopped while in progress and later restarted with the 554 * last unfinished work. (There is currently no limit on execution duration when 555 * running as a pre-O plain Service.)</p> 556 * 557 * @param intent The intent describing the work to now be processed. 558 */ 559 protected abstract void onHandleWork(@NonNull Intent intent); 560 561 /** 562 * Control whether code executing in {@link #onHandleWork(Intent)} will be interrupted 563 * if the job is stopped. By default this is false. If called and set to true, any 564 * time {@link #onStopCurrentWork()} is called, the class will first call 565 * {@link AsyncTask#cancel(boolean) AsyncTask.cancel(true)} to interrupt the running 566 * task. 567 * 568 * @param interruptIfStopped Set to true to allow the system to interrupt actively 569 * running work. 570 */ 571 public void setInterruptIfStopped(boolean interruptIfStopped) { 572 mInterruptIfStopped = interruptIfStopped; 573 } 574 575 /** 576 * Returns true if {@link #onStopCurrentWork()} has been called. You can use this, 577 * while executing your work, to see if it should be stopped. 578 */ 579 public boolean isStopped() { 580 return mStopped; 581 } 582 583 /** 584 * This will be called if the JobScheduler has decided to stop this job. The job for 585 * this service does not have any constraints specified, so this will only generally happen 586 * if the service exceeds the job's maximum execution time. 587 * 588 * @return True to indicate to the JobManager whether you'd like to reschedule this work, 589 * false to drop this and all following work. Regardless of the value returned, your service 590 * must stop executing or the system will ultimately kill it. The default implementation 591 * returns true, and that is most likely what you want to return as well (so no work gets 592 * lost). 593 */ 594 public boolean onStopCurrentWork() { 595 return true; 596 } 597 598 boolean doStopCurrentWork() { 599 if (mCurProcessor != null) { 600 mCurProcessor.cancel(mInterruptIfStopped); 601 } 602 mStopped = true; 603 return onStopCurrentWork(); 604 } 605 606 void ensureProcessorRunningLocked(boolean reportStarted) { 607 if (mCurProcessor == null) { 608 mCurProcessor = new CommandProcessor(); 609 if (mCompatWorkEnqueuer != null && reportStarted) { 610 mCompatWorkEnqueuer.serviceProcessingStarted(); 611 } 612 if (DEBUG) Log.d(TAG, "Starting processor: " + mCurProcessor); 613 mCurProcessor.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 614 } 615 } 616 617 void processorFinished() { 618 if (mCompatQueue != null) { 619 synchronized (mCompatQueue) { 620 mCurProcessor = null; 621 // The async task has finished, but we may have gotten more work scheduled in the 622 // meantime. If so, we need to restart the new processor to execute it. If there 623 // is no more work at this point, either the service is in the process of being 624 // destroyed (because we called stopSelf on the last intent started for it), or 625 // someone has already called startService with a new Intent that will be 626 // arriving shortly. In either case, we want to just leave the service 627 // waiting -- either to get destroyed, or get a new onStartCommand() callback 628 // which will then kick off a new processor. 629 if (mCompatQueue != null && mCompatQueue.size() > 0) { 630 ensureProcessorRunningLocked(false); 631 } else if (!mDestroyed) { 632 mCompatWorkEnqueuer.serviceProcessingFinished(); 633 } 634 } 635 } 636 } 637 638 GenericWorkItem dequeueWork() { 639 if (mJobImpl != null) { 640 return mJobImpl.dequeueWork(); 641 } else { 642 synchronized (mCompatQueue) { 643 if (mCompatQueue.size() > 0) { 644 return mCompatQueue.remove(0); 645 } else { 646 return null; 647 } 648 } 649 } 650 } 651} 652