Home | History | Annotate | Download | only in app
      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