blob: 3e1532d74c891dffb2a84dedde6c016434cdb1fc [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.core.app;
import android.app.Service;
import android.app.job.JobInfo;
import android.app.job.JobParameters;
import android.app.job.JobScheduler;
import android.app.job.JobServiceEngine;
import android.app.job.JobWorkItem;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Build;
import android.os.IBinder;
import android.os.PowerManager;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import java.util.ArrayList;
import java.util.HashMap;
/**
* Helper for processing work that has been enqueued for a job/service. When running on
* {@link android.os.Build.VERSION_CODES#O Android O} or later, the work will be dispatched
* as a job via {@link android.app.job.JobScheduler#enqueue JobScheduler.enqueue}. When running
* on older versions of the platform, it will use
* {@link android.content.Context#startService Context.startService}.
*
* <p>You must publish your subclass in your manifest for the system to interact with. This
* should be published as a {@link android.app.job.JobService}, as described for that class,
* since on O and later platforms it will be executed that way.</p>
*
* <p>Use {@link #enqueueWork(Context, Class, int, Intent)} to enqueue new work to be
* dispatched to and handled by your service. It will be executed in
* {@link #onHandleWork(Intent)}.</p>
*
* <p>You do not need to use {@link androidx.legacy.content.WakefulBroadcastReceiver}
* when using this class. When running on {@link android.os.Build.VERSION_CODES#O Android O},
* the JobScheduler will take care of wake locks for you (holding a wake lock from the time
* you enqueue work until the job has been dispatched and while it is running). When running
* on previous versions of the platform, this wake lock handling is emulated in the class here
* by directly calling the PowerManager; this means the application must request the
* {@link android.Manifest.permission#WAKE_LOCK} permission.</p>
*
* <p>There are a few important differences in behavior when running on
* {@link android.os.Build.VERSION_CODES#O Android O} or later as a Job vs. pre-O:</p>
*
* <ul>
* <li><p>When running as a pre-O service, the act of enqueueing work will generally start
* the service immediately, regardless of whether the device is dozing or in other
* conditions. When running as a Job, it will be subject to standard JobScheduler
* policies for a Job with a {@link android.app.job.JobInfo.Builder#setOverrideDeadline(long)}
* of 0: the job will not run while the device is dozing, it may get delayed more than
* a service if the device is under strong memory pressure with lots of demand to run
* jobs.</p></li>
* <li><p>When running as a pre-O service, the normal service execution semantics apply:
* the service can run indefinitely, though the longer it runs the more likely the system
* will be to outright kill its process, and under memory pressure one should expect
* the process to be killed even of recently started services. When running as a Job,
* the typical {@link android.app.job.JobService} execution time limit will apply, after
* which the job will be stopped (cleanly, not by killing the process) and rescheduled
* to continue its execution later. Job are generally not killed when the system is
* under memory pressure, since the number of concurrent jobs is adjusted based on the
* memory state of the device.</p></li>
* </ul>
*
* <p>Here is an example implementation of this class:</p>
*
* {@sample frameworks/support/samples/Support4Demos/src/main/java/com/example/android/supportv4/app/SimpleJobIntentService.java
* complete}
*/
public abstract class JobIntentService extends Service {
static final String TAG = "JobIntentService";
static final boolean DEBUG = false;
CompatJobEngine mJobImpl;
WorkEnqueuer mCompatWorkEnqueuer;
CommandProcessor mCurProcessor;
boolean mInterruptIfStopped = false;
boolean mStopped = false;
boolean mDestroyed = false;
final ArrayList<CompatWorkItem> mCompatQueue;
static final Object sLock = new Object();
static final HashMap<ComponentName, WorkEnqueuer> sClassWorkEnqueuer = new HashMap<>();
/**
* Base class for the target service we can deliver work to and the implementation of
* how to deliver that work.
*/
abstract static class WorkEnqueuer {
final ComponentName mComponentName;
boolean mHasJobId;
int mJobId;
WorkEnqueuer(Context context, ComponentName cn) {
mComponentName = cn;
}
void ensureJobId(int jobId) {
if (!mHasJobId) {
mHasJobId = true;
mJobId = jobId;
} else if (mJobId != jobId) {
throw new IllegalArgumentException("Given job ID " + jobId
+ " is different than previous " + mJobId);
}
}
abstract void enqueueWork(Intent work);
public void serviceStartReceived() {
}
public void serviceProcessingStarted() {
}
public void serviceProcessingFinished() {
}
}
/**
* Get rid of lint warnings about API levels.
*/
interface CompatJobEngine {
IBinder compatGetBinder();
GenericWorkItem dequeueWork();
}
/**
* An implementation of WorkEnqueuer that works for pre-O (raw Service-based).
*/
static final class CompatWorkEnqueuer extends WorkEnqueuer {
private final Context mContext;
private final PowerManager.WakeLock mLaunchWakeLock;
private final PowerManager.WakeLock mRunWakeLock;
boolean mLaunchingService;
boolean mServiceProcessing;
CompatWorkEnqueuer(Context context, ComponentName cn) {
super(context, cn);
mContext = context.getApplicationContext();
// Make wake locks. We need two, because the launch wake lock wants to have
// a timeout, and the system does not do the right thing if you mix timeout and
// non timeout (or even changing the timeout duration) in one wake lock.
PowerManager pm = ((PowerManager) context.getSystemService(Context.POWER_SERVICE));
mLaunchWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
cn.getClassName() + ":launch");
mLaunchWakeLock.setReferenceCounted(false);
mRunWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
cn.getClassName() + ":run");
mRunWakeLock.setReferenceCounted(false);
}
@Override
void enqueueWork(Intent work) {
Intent intent = new Intent(work);
intent.setComponent(mComponentName);
if (DEBUG) Log.d(TAG, "Starting service for work: " + work);
if (mContext.startService(intent) != null) {
synchronized (this) {
if (!mLaunchingService) {
mLaunchingService = true;
if (!mServiceProcessing) {
// If the service is not already holding the wake lock for
// itself, acquire it now to keep the system running until
// we get this work dispatched. We use a timeout here to
// protect against whatever problem may cause it to not get
// the work.
mLaunchWakeLock.acquire(60 * 1000);
}
}
}
}
}
@Override
public void serviceStartReceived() {
synchronized (this) {
// Once we have started processing work, we can count whatever last
// enqueueWork() that happened as handled.
mLaunchingService = false;
}
}
@Override
public void serviceProcessingStarted() {
synchronized (this) {
// We hold the wake lock as long as the service is processing commands.
if (!mServiceProcessing) {
mServiceProcessing = true;
// Keep the device awake, but only for at most 10 minutes at a time
// (Similar to JobScheduler.)
mRunWakeLock.acquire(10 * 60 * 1000L);
mLaunchWakeLock.release();
}
}
}
@Override
public void serviceProcessingFinished() {
synchronized (this) {
if (mServiceProcessing) {
// If we are transitioning back to a wakelock with a timeout, do the same
// as if we had enqueued work without the service running.
if (mLaunchingService) {
mLaunchWakeLock.acquire(60 * 1000);
}
mServiceProcessing = false;
mRunWakeLock.release();
}
}
}
}
/**
* Implementation of a JobServiceEngine for interaction with JobIntentService.
*/
@RequiresApi(26)
static final class JobServiceEngineImpl extends JobServiceEngine
implements JobIntentService.CompatJobEngine {
static final String TAG = "JobServiceEngineImpl";
static final boolean DEBUG = false;
final JobIntentService mService;
final Object mLock = new Object();
JobParameters mParams;
final class WrapperWorkItem implements JobIntentService.GenericWorkItem {
final JobWorkItem mJobWork;
WrapperWorkItem(JobWorkItem jobWork) {
mJobWork = jobWork;
}
@Override
public Intent getIntent() {
return mJobWork.getIntent();
}
@Override
public void complete() {
synchronized (mLock) {
if (mParams != null) {
mParams.completeWork(mJobWork);
}
}
}
}
JobServiceEngineImpl(JobIntentService service) {
super(service);
mService = service;
}
@Override
public IBinder compatGetBinder() {
return getBinder();
}
@Override
public boolean onStartJob(JobParameters params) {
if (DEBUG) Log.d(TAG, "onStartJob: " + params);
mParams = params;
// We can now start dequeuing work!
mService.ensureProcessorRunningLocked(false);
return true;
}
@Override
public boolean onStopJob(JobParameters params) {
if (DEBUG) Log.d(TAG, "onStartJob: " + params);
boolean result = mService.doStopCurrentWork();
synchronized (mLock) {
// Once we return, the job is stopped, so its JobParameters are no
// longer valid and we should not be doing anything with them.
mParams = null;
}
return result;
}
/**
* Dequeue some work.
*/
@Override
public JobIntentService.GenericWorkItem dequeueWork() {
JobWorkItem work;
synchronized (mLock) {
if (mParams == null) {
return null;
}
work = mParams.dequeueWork();
}
if (work != null) {
work.getIntent().setExtrasClassLoader(mService.getClassLoader());
return new WrapperWorkItem(work);
} else {
return null;
}
}
}
@RequiresApi(26)
static final class JobWorkEnqueuer extends JobIntentService.WorkEnqueuer {
private final JobInfo mJobInfo;
private final JobScheduler mJobScheduler;
JobWorkEnqueuer(Context context, ComponentName cn, int jobId) {
super(context, cn);
ensureJobId(jobId);
JobInfo.Builder b = new JobInfo.Builder(jobId, mComponentName);
mJobInfo = b.setOverrideDeadline(0).build();
mJobScheduler = (JobScheduler) context.getApplicationContext().getSystemService(
Context.JOB_SCHEDULER_SERVICE);
}
@Override
void enqueueWork(Intent work) {
if (DEBUG) Log.d(TAG, "Enqueueing work: " + work);
mJobScheduler.enqueue(mJobInfo, new JobWorkItem(work));
}
}
/**
* Abstract definition of an item of work that is being dispatched.
*/
interface GenericWorkItem {
Intent getIntent();
void complete();
}
/**
* An implementation of GenericWorkItem that dispatches work for pre-O platforms: intents
* received through a raw service's onStartCommand.
*/
final class CompatWorkItem implements GenericWorkItem {
final Intent mIntent;
final int mStartId;
CompatWorkItem(Intent intent, int startId) {
mIntent = intent;
mStartId = startId;
}
@Override
public Intent getIntent() {
return mIntent;
}
@Override
public void complete() {
if (DEBUG) Log.d(TAG, "Stopping self: #" + mStartId);
stopSelf(mStartId);
}
}
/**
* This is a task to dequeue and process work in the background.
*/
final class CommandProcessor extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... params) {
GenericWorkItem work;
if (DEBUG) Log.d(TAG, "Starting to dequeue work...");
while ((work = dequeueWork()) != null) {
if (DEBUG) Log.d(TAG, "Processing next work: " + work);
onHandleWork(work.getIntent());
if (DEBUG) Log.d(TAG, "Completing work: " + work);
work.complete();
}
if (DEBUG) Log.d(TAG, "Done processing work!");
return null;
}
@Override
protected void onCancelled(Void aVoid) {
processorFinished();
}
@Override
protected void onPostExecute(Void aVoid) {
processorFinished();
}
}
/**
* Default empty constructor.
*/
public JobIntentService() {
if (Build.VERSION.SDK_INT >= 26) {
mCompatQueue = null;
} else {
mCompatQueue = new ArrayList<>();
}
}
@Override
public void onCreate() {
super.onCreate();
if (DEBUG) Log.d(TAG, "CREATING: " + this);
if (Build.VERSION.SDK_INT >= 26) {
mJobImpl = new JobServiceEngineImpl(this);
mCompatWorkEnqueuer = null;
} else {
mJobImpl = null;
ComponentName cn = new ComponentName(this, this.getClass());
mCompatWorkEnqueuer = getWorkEnqueuer(this, cn, false, 0);
}
}
/**
* Processes start commands when running as a pre-O service, enqueueing them to be
* later dispatched in {@link #onHandleWork(Intent)}.
*/
@Override
public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
if (mCompatQueue != null) {
mCompatWorkEnqueuer.serviceStartReceived();
if (DEBUG) Log.d(TAG, "Received compat start command #" + startId + ": " + intent);
synchronized (mCompatQueue) {
mCompatQueue.add(new CompatWorkItem(intent != null ? intent : new Intent(),
startId));
ensureProcessorRunningLocked(true);
}
return START_REDELIVER_INTENT;
} else {
if (DEBUG) Log.d(TAG, "Ignoring start command: " + intent);
return START_NOT_STICKY;
}
}
/**
* Returns the IBinder for the {@link android.app.job.JobServiceEngine} when
* running as a JobService on O and later platforms.
*/
@Override
public IBinder onBind(@NonNull Intent intent) {
if (mJobImpl != null) {
IBinder engine = mJobImpl.compatGetBinder();
if (DEBUG) Log.d(TAG, "Returning engine: " + engine);
return engine;
} else {
return null;
}
}
@Override
public void onDestroy() {
super.onDestroy();
if (mCompatQueue != null) {
synchronized (mCompatQueue) {
mDestroyed = true;
mCompatWorkEnqueuer.serviceProcessingFinished();
}
}
}
/**
* Call this to enqueue work for your subclass of {@link JobIntentService}. This will
* either directly start the service (when running on pre-O platforms) or enqueue work
* for it as a job (when running on O and later). In either case, a wake lock will be
* held for you to ensure you continue running. The work you enqueue will ultimately
* appear at {@link #onHandleWork(Intent)}.
*
* @param context Context this is being called from.
* @param cls The concrete class the work should be dispatched to (this is the class that
* is published in your manifest).
* @param jobId A unique job ID for scheduling; must be the same value for all work
* enqueued for the same class.
* @param work The Intent of work to enqueue.
*/
public static void enqueueWork(@NonNull Context context, @NonNull Class cls, int jobId,
@NonNull Intent work) {
enqueueWork(context, new ComponentName(context, cls), jobId, work);
}
/**
* Like {@link #enqueueWork(Context, Class, int, Intent)}, but supplies a ComponentName
* for the service to interact with instead of its class.
*
* @param context Context this is being called from.
* @param component The published ComponentName of the class this work should be
* dispatched to.
* @param jobId A unique job ID for scheduling; must be the same value for all work
* enqueued for the same class.
* @param work The Intent of work to enqueue.
*/
public static void enqueueWork(@NonNull Context context, @NonNull ComponentName component,
int jobId, @NonNull Intent work) {
if (work == null) {
throw new IllegalArgumentException("work must not be null");
}
synchronized (sLock) {
WorkEnqueuer we = getWorkEnqueuer(context, component, true, jobId);
we.ensureJobId(jobId);
we.enqueueWork(work);
}
}
static WorkEnqueuer getWorkEnqueuer(Context context, ComponentName cn, boolean hasJobId,
int jobId) {
WorkEnqueuer we = sClassWorkEnqueuer.get(cn);
if (we == null) {
if (Build.VERSION.SDK_INT >= 26) {
if (!hasJobId) {
throw new IllegalArgumentException("Can't be here without a job id");
}
we = new JobWorkEnqueuer(context, cn, jobId);
} else {
we = new CompatWorkEnqueuer(context, cn);
}
sClassWorkEnqueuer.put(cn, we);
}
return we;
}
/**
* Called serially for each work dispatched to and processed by the service. This
* method is called on a background thread, so you can do long blocking operations
* here. Upon returning, that work will be considered complete and either the next
* pending work dispatched here or the overall service destroyed now that it has
* nothing else to do.
*
* <p>Be aware that when running as a job, you are limited by the maximum job execution
* time and any single or total sequential items of work that exceeds that limit will
* cause the service to be stopped while in progress and later restarted with the
* last unfinished work. (There is currently no limit on execution duration when
* running as a pre-O plain Service.)</p>
*
* @param intent The intent describing the work to now be processed.
*/
protected abstract void onHandleWork(@NonNull Intent intent);
/**
* Control whether code executing in {@link #onHandleWork(Intent)} will be interrupted
* if the job is stopped. By default this is false. If called and set to true, any
* time {@link #onStopCurrentWork()} is called, the class will first call
* {@link AsyncTask#cancel(boolean) AsyncTask.cancel(true)} to interrupt the running
* task.
*
* @param interruptIfStopped Set to true to allow the system to interrupt actively
* running work.
*/
public void setInterruptIfStopped(boolean interruptIfStopped) {
mInterruptIfStopped = interruptIfStopped;
}
/**
* Returns true if {@link #onStopCurrentWork()} has been called. You can use this,
* while executing your work, to see if it should be stopped.
*/
public boolean isStopped() {
return mStopped;
}
/**
* This will be called if the JobScheduler has decided to stop this job. The job for
* this service does not have any constraints specified, so this will only generally happen
* if the service exceeds the job's maximum execution time.
*
* @return True to indicate to the JobManager whether you'd like to reschedule this work,
* false to drop this and all following work. Regardless of the value returned, your service
* must stop executing or the system will ultimately kill it. The default implementation
* returns true, and that is most likely what you want to return as well (so no work gets
* lost).
*/
public boolean onStopCurrentWork() {
return true;
}
boolean doStopCurrentWork() {
if (mCurProcessor != null) {
mCurProcessor.cancel(mInterruptIfStopped);
}
mStopped = true;
return onStopCurrentWork();
}
void ensureProcessorRunningLocked(boolean reportStarted) {
if (mCurProcessor == null) {
mCurProcessor = new CommandProcessor();
if (mCompatWorkEnqueuer != null && reportStarted) {
mCompatWorkEnqueuer.serviceProcessingStarted();
}
if (DEBUG) Log.d(TAG, "Starting processor: " + mCurProcessor);
mCurProcessor.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}
void processorFinished() {
if (mCompatQueue != null) {
synchronized (mCompatQueue) {
mCurProcessor = null;
// The async task has finished, but we may have gotten more work scheduled in the
// meantime. If so, we need to restart the new processor to execute it. If there
// is no more work at this point, either the service is in the process of being
// destroyed (because we called stopSelf on the last intent started for it), or
// someone has already called startService with a new Intent that will be
// arriving shortly. In either case, we want to just leave the service
// waiting -- either to get destroyed, or get a new onStartCommand() callback
// which will then kick off a new processor.
if (mCompatQueue != null && mCompatQueue.size() > 0) {
ensureProcessorRunningLocked(false);
} else if (!mDestroyed) {
mCompatWorkEnqueuer.serviceProcessingFinished();
}
}
}
}
GenericWorkItem dequeueWork() {
if (mJobImpl != null) {
return mJobImpl.dequeueWork();
} else {
synchronized (mCompatQueue) {
if (mCompatQueue.size() > 0) {
return mCompatQueue.remove(0);
} else {
return null;
}
}
}
}
}