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