Java tutorial
/* * 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 android.support.v4.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.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; import android.util.Log; 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 android.support.v4.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; } } } } }