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 17 package android.support.v4.app; 18 19 import android.app.Service; 20 import android.app.job.JobInfo; 21 import android.app.job.JobParameters; 22 import android.app.job.JobScheduler; 23 import android.app.job.JobServiceEngine; 24 import android.app.job.JobWorkItem; 25 import android.content.ComponentName; 26 import android.content.Context; 27 import android.content.Intent; 28 import android.os.AsyncTask; 29 import android.os.Build; 30 import android.os.IBinder; 31 import android.os.PowerManager; 32 import android.support.annotation.NonNull; 33 import android.support.annotation.Nullable; 34 import android.support.annotation.RequiresApi; 35 import android.support.v4.os.BuildCompat; 36 import android.util.Log; 37 38 import java.util.ArrayList; 39 import 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 android.support.v4.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/com/example/android/supportv4/app/SimpleJobIntentService.java 89 * complete} 90 */ 91 public 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 100 final ArrayList<CompatWorkItem> mCompatQueue; 101 102 static final Object sLock = new Object(); 103 static final HashMap<Class, WorkEnqueuer> sClassWorkEnqueuer = new HashMap<>(); 104 105 /** 106 * Base class for the target service we can deliver work to and the implementation of 107 * how to deliver that work. 108 */ 109 abstract static class WorkEnqueuer { 110 final ComponentName mComponentName; 111 112 boolean mHasJobId; 113 int mJobId; 114 115 WorkEnqueuer(Context context, Class cls) { 116 mComponentName = new ComponentName(context, cls); 117 } 118 119 void ensureJobId(int jobId) { 120 if (!mHasJobId) { 121 mHasJobId = true; 122 mJobId = jobId; 123 } else if (mJobId != jobId) { 124 throw new IllegalArgumentException("Given job ID " + jobId 125 + " is different than previous " + mJobId); 126 } 127 } 128 129 abstract void enqueueWork(Intent work); 130 131 public void serviceCreated() { 132 } 133 134 public void serviceStartReceived() { 135 } 136 137 public void serviceDestroyed() { 138 } 139 } 140 141 /** 142 * Get rid of lint warnings about API levels. 143 */ 144 interface CompatJobEngine { 145 IBinder compatGetBinder(); 146 GenericWorkItem dequeueWork(); 147 } 148 149 /** 150 * An implementation of WorkEnqueuer that works for pre-O (raw Service-based). 151 */ 152 static final class CompatWorkEnqueuer extends WorkEnqueuer { 153 private final Context mContext; 154 private final PowerManager.WakeLock mLaunchWakeLock; 155 private final PowerManager.WakeLock mRunWakeLock; 156 boolean mLaunchingService; 157 boolean mServiceRunning; 158 159 CompatWorkEnqueuer(Context context, Class cls) { 160 super(context, cls); 161 mContext = context.getApplicationContext(); 162 // Make wake locks. We need two, because the launch wake lock wants to have 163 // a timeout, and the system does not do the right thing if you mix timeout and 164 // non timeout (or even changing the timeout duration) in one wake lock. 165 PowerManager pm = ((PowerManager) context.getSystemService(Context.POWER_SERVICE)); 166 mLaunchWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, cls.getName()); 167 mLaunchWakeLock.setReferenceCounted(false); 168 mRunWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, cls.getName()); 169 mRunWakeLock.setReferenceCounted(false); 170 } 171 172 @Override 173 void enqueueWork(Intent work) { 174 Intent intent = new Intent(work); 175 intent.setComponent(mComponentName); 176 if (DEBUG) Log.d(TAG, "Starting service for work: " + work); 177 if (mContext.startService(intent) != null) { 178 synchronized (this) { 179 if (!mLaunchingService) { 180 mLaunchingService = true; 181 if (!mServiceRunning) { 182 // If the service is not already holding the wake lock for 183 // itself, acquire it now to keep the system running until 184 // we get this work dispatched. We use a timeout here to 185 // protect against whatever problem may cause is to not get 186 // the work. 187 mLaunchWakeLock.acquire(60 * 1000); 188 } 189 } 190 } 191 } 192 } 193 194 @Override 195 public void serviceCreated() { 196 synchronized (this) { 197 // We hold the wake lock as long as the service is running. 198 if (!mServiceRunning) { 199 mServiceRunning = true; 200 mRunWakeLock.acquire(); 201 mLaunchWakeLock.release(); 202 } 203 } 204 } 205 206 @Override 207 public void serviceStartReceived() { 208 synchronized (this) { 209 // Once we have started processing work, we can count whatever last 210 // enqueueWork() that happened as handled. 211 mLaunchingService = false; 212 } 213 } 214 215 @Override 216 public void serviceDestroyed() { 217 synchronized (this) { 218 // If we are transitioning back to a wakelock with a timeout, do the same 219 // as if we had enqueued work without the service running. 220 if (mLaunchingService) { 221 mLaunchWakeLock.acquire(60 * 1000); 222 } 223 mServiceRunning = false; 224 mRunWakeLock.release(); 225 } 226 } 227 } 228 229 /** 230 * Implementation of a JobServiceEngine for interaction with JobIntentService. 231 */ 232 @RequiresApi(26) 233 static final class JobServiceEngineImpl extends JobServiceEngine 234 implements JobIntentService.CompatJobEngine { 235 static final String TAG = "JobServiceEngineImpl"; 236 237 static final boolean DEBUG = false; 238 239 final JobIntentService mService; 240 JobParameters mParams; 241 242 final class WrapperWorkItem implements JobIntentService.GenericWorkItem { 243 final JobWorkItem mJobWork; 244 245 WrapperWorkItem(JobWorkItem jobWork) { 246 mJobWork = jobWork; 247 } 248 249 @Override 250 public Intent getIntent() { 251 return mJobWork.getIntent(); 252 } 253 254 @Override 255 public void complete() { 256 mParams.completeWork(mJobWork); 257 } 258 } 259 260 JobServiceEngineImpl(JobIntentService service) { 261 super(service); 262 mService = service; 263 } 264 265 @Override 266 public IBinder compatGetBinder() { 267 return getBinder(); 268 } 269 270 @Override 271 public boolean onStartJob(JobParameters params) { 272 if (DEBUG) Log.d(TAG, "onStartJob: " + params); 273 mParams = params; 274 // We can now start dequeuing work! 275 mService.ensureProcessorRunningLocked(); 276 return true; 277 } 278 279 @Override 280 public boolean onStopJob(JobParameters params) { 281 if (DEBUG) Log.d(TAG, "onStartJob: " + params); 282 return mService.onStopCurrentWork(); 283 } 284 285 /** 286 * Dequeue some work. 287 */ 288 @Override 289 public JobIntentService.GenericWorkItem dequeueWork() { 290 JobWorkItem work = mParams.dequeueWork(); 291 if (work != null) { 292 return new WrapperWorkItem(work); 293 } else { 294 return null; 295 } 296 } 297 } 298 299 @RequiresApi(26) 300 static final class JobWorkEnqueuer extends JobIntentService.WorkEnqueuer { 301 private final JobInfo mJobInfo; 302 private final JobScheduler mJobScheduler; 303 304 JobWorkEnqueuer(Context context, Class cls, int jobId) { 305 super(context, cls); 306 ensureJobId(jobId); 307 JobInfo.Builder b = new JobInfo.Builder(jobId, mComponentName); 308 mJobInfo = b.setOverrideDeadline(0).build(); 309 mJobScheduler = (JobScheduler) context.getApplicationContext().getSystemService( 310 Context.JOB_SCHEDULER_SERVICE); 311 } 312 313 @Override 314 void enqueueWork(Intent work) { 315 if (DEBUG) Log.d(TAG, "Enqueueing work: " + work); 316 mJobScheduler.enqueue(mJobInfo, new JobWorkItem(work)); 317 } 318 } 319 320 /** 321 * Abstract definition of an item of work that is being dispatched. 322 */ 323 interface GenericWorkItem { 324 Intent getIntent(); 325 void complete(); 326 } 327 328 /** 329 * An implementation of GenericWorkItem that dispatches work for pre-O platforms: intents 330 * received through a raw service's onStartCommand. 331 */ 332 final class CompatWorkItem implements GenericWorkItem { 333 final Intent mIntent; 334 final int mStartId; 335 336 CompatWorkItem(Intent intent, int startId) { 337 mIntent = intent; 338 mStartId = startId; 339 } 340 341 @Override 342 public Intent getIntent() { 343 return mIntent; 344 } 345 346 @Override 347 public void complete() { 348 if (DEBUG) Log.d(TAG, "Stopping self: #" + mStartId); 349 stopSelf(mStartId); 350 } 351 } 352 353 /** 354 * This is a task to dequeue and process work in the background. 355 */ 356 final class CommandProcessor extends AsyncTask<Void, Void, Void> { 357 @Override 358 protected Void doInBackground(Void... params) { 359 GenericWorkItem work; 360 361 if (DEBUG) Log.d(TAG, "Starting to dequeue work..."); 362 363 while ((work = dequeueWork()) != null) { 364 if (DEBUG) Log.d(TAG, "Processing next work: " + work); 365 onHandleWork(work.getIntent()); 366 if (DEBUG) Log.d(TAG, "Completing work: " + work); 367 work.complete(); 368 } 369 370 if (DEBUG) Log.d(TAG, "Done processing work!"); 371 372 return null; 373 } 374 375 @Override 376 protected void onPostExecute(Void aVoid) { 377 if (mCompatQueue != null) { 378 synchronized (mCompatQueue) { 379 mCurProcessor = null; 380 checkForMoreCompatWorkLocked(); 381 } 382 } 383 } 384 } 385 386 /** 387 * Default empty constructor. 388 */ 389 public JobIntentService() { 390 if (Build.VERSION.SDK_INT >= 26) { 391 mCompatQueue = null; 392 } else { 393 mCompatQueue = new ArrayList<>(); 394 } 395 } 396 397 @Override 398 public void onCreate() { 399 super.onCreate(); 400 if (DEBUG) Log.d(TAG, "CREATING: " + this); 401 if (Build.VERSION.SDK_INT >= 26) { 402 mJobImpl = new JobServiceEngineImpl(this); 403 mCompatWorkEnqueuer = null; 404 } else { 405 mJobImpl = null; 406 mCompatWorkEnqueuer = getWorkEnqueuer(this, this.getClass(), false, 0); 407 mCompatWorkEnqueuer.serviceCreated(); 408 } 409 } 410 411 /** 412 * Processes start commands when running as a pre-O service, enqueueing them to be 413 * later dispatched in {@link #onHandleWork(Intent)}. 414 */ 415 @Override 416 public int onStartCommand(@Nullable Intent intent, int flags, int startId) { 417 if (mCompatQueue != null) { 418 mCompatWorkEnqueuer.serviceStartReceived(); 419 if (DEBUG) Log.d(TAG, "Received compat start command #" + startId + ": " + intent); 420 synchronized (mCompatQueue) { 421 mCompatQueue.add(new CompatWorkItem(intent != null ? intent : new Intent(), 422 startId)); 423 ensureProcessorRunningLocked(); 424 } 425 return START_REDELIVER_INTENT; 426 } else { 427 if (DEBUG) Log.d(TAG, "Ignoring start command: " + intent); 428 return START_NOT_STICKY; 429 } 430 } 431 432 /** 433 * Returns the IBinder for the {@link android.app.job.JobServiceEngine} when 434 * running as a JobService on O and later platforms. 435 */ 436 @Override 437 public IBinder onBind(@NonNull Intent intent) { 438 if (mJobImpl != null) { 439 IBinder engine = mJobImpl.compatGetBinder(); 440 if (DEBUG) Log.d(TAG, "Returning engine: " + engine); 441 return engine; 442 } else { 443 return null; 444 } 445 } 446 447 @Override 448 public void onDestroy() { 449 super.onDestroy(); 450 if (mCompatWorkEnqueuer != null) { 451 mCompatWorkEnqueuer.serviceDestroyed(); 452 } 453 } 454 455 /** 456 * Call this to enqueue work for your subclass of {@link JobIntentService}. This will 457 * either directly start the service (when running on pre-O platforms) or enqueue work 458 * for it as a job (when running on O and later). In either case, a wake lock will be 459 * held for you to ensure you continue running. The work you enqueue will ultimately 460 * appear at {@link #onHandleWork(Intent)}. 461 * 462 * @param context Context this is being called from. 463 * @param cls The concrete class the work should be dispatched to (this is the class that 464 * is published in your manifest). 465 * @param jobId A unique job ID for scheduling; must be the same value for all work 466 * enqueued for the same class. 467 * @param work The Intent of work to enqueue. 468 */ 469 public static void enqueueWork(@NonNull Context context, @NonNull Class cls, int jobId, 470 @NonNull Intent work) { 471 if (work == null) { 472 throw new IllegalArgumentException("work must not be null"); 473 } 474 synchronized (sLock) { 475 WorkEnqueuer we = getWorkEnqueuer(context, cls, true, jobId); 476 we.ensureJobId(jobId); 477 we.enqueueWork(work); 478 } 479 } 480 481 static WorkEnqueuer getWorkEnqueuer(Context context, Class cls, boolean hasJobId, int jobId) { 482 WorkEnqueuer we = sClassWorkEnqueuer.get(cls); 483 if (we == null) { 484 if (BuildCompat.isAtLeastO()) { 485 if (!hasJobId) { 486 throw new IllegalArgumentException("Can't be here without a job id"); 487 } 488 we = new JobWorkEnqueuer(context, cls, jobId); 489 } else { 490 we = new CompatWorkEnqueuer(context, cls); 491 } 492 sClassWorkEnqueuer.put(cls, we); 493 } 494 return we; 495 } 496 497 /** 498 * Called serially for each work dispatched to and processed by the service. This 499 * method is called on a background thread, so you can do long blocking operations 500 * here. Upon returning, that work will be considered complete and either the next 501 * pending work dispatched here or the overall service destroyed now that it has 502 * nothing else to do. 503 * 504 * <p>Be aware that when running as a job, you are limited by the maximum job execution 505 * time and any single or total sequential items of work that exceeds that limit will 506 * cause the service to be stopped while in progress and later restarted with the 507 * last unfinished work. (There is currently no limit on execution duration when 508 * running as a pre-O plain Service.)</p> 509 * 510 * @param intent The intent describing the work to now be processed. 511 */ 512 protected abstract void onHandleWork(@NonNull Intent intent); 513 514 /** 515 * This will be called if the JobScheduler has decided to stop this job. The job for 516 * this service does not have any constraints specified, so this will only generally happen 517 * if the service exceeds the job's maximum execution time. 518 * 519 * @return True to indicate to the JobManager whether you'd like to reschedule this work, 520 * false to drop this and all following work. Regardless of the value returned, your service 521 * must stop executing or the system will ultimately kill it. The default implementation 522 * returns true, and that is most likely what you want to return as well (so no work gets 523 * lost). 524 */ 525 public boolean onStopCurrentWork() { 526 return true; 527 } 528 529 void ensureProcessorRunningLocked() { 530 if (mCurProcessor == null) { 531 mCurProcessor = new CommandProcessor(); 532 if (DEBUG) Log.d(TAG, "Starting processor: " + mCurProcessor); 533 mCurProcessor.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 534 } 535 } 536 537 void checkForMoreCompatWorkLocked() { 538 // The async task has finished, but we may have gotten more work scheduled in the 539 // meantime. If so, 540 if (mCompatQueue != null && mCompatQueue.size() > 0) { 541 ensureProcessorRunningLocked(); 542 } 543 } 544 545 GenericWorkItem dequeueWork() { 546 if (mJobImpl != null) { 547 return mJobImpl.dequeueWork(); 548 } else { 549 synchronized (mCompatQueue) { 550 if (mCompatQueue.size() > 0) { 551 return mCompatQueue.remove(0); 552 } else { 553 return null; 554 } 555 } 556 } 557 } 558 } 559