// Copyright 2016 Google, Inc.
//
// 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 com.firebase.jobdispatcher;
import android.app.Service;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Binder;
import android.os.IBinder;
import android.os.Message;
import android.support.annotation.IntDef;
import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.support.v4.util.SimpleArrayMap;
import android.util.Log;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Locale;
/**
* JobService is the fundamental unit of work used in the JobDispatcher.
*
* Users will need to override {@link #onStartJob(JobParameters)}, which is where any asynchronous
* execution should start. This method, like most lifecycle methods, runs on the main thread; you
* <b>must</b> offload execution to another thread (or {@link android.os.AsyncTask}, or
* {@link android.os.Handler}, or your favorite flavor of concurrency).
*
* Once any asynchronous work is complete {@link #jobFinished(JobParameters, boolean)} should be
* called to inform the backing driver of the result.
*
* Implementations should also override {@link #onStopJob(JobParameters)}, which will be called if
* the scheduling engine wishes to interrupt your work (most likely because the runtime constraints
* that are associated with the job in question are no longer met).
*/
public abstract class JobService extends Service {
/**
* Returned to indicate the job was executed successfully. If the job is not recurring (i.e. a
* one-off) it will be dequeued and forgotten. If it is recurring the trigger will be reset and
* the job will be requeued.
*/
public static final int RESULT_SUCCESS = 0;
/**
* Returned to indicate the job encountered an error during execution and should be retried after
* a backoff period.
*/
public static final int RESULT_FAIL_RETRY = 1;
/**
* Returned to indicate the job encountered an error during execution but should not be retried.
* If the job is not recurring (i.e. a one-off) it will be dequeued and forgotten. If it is
* recurring the trigger will be reset and the job will be requeued.
*/
public static final int RESULT_FAIL_NORETRY = 2;
static final String TAG = "FJD.JobService";
@VisibleForTesting
static final String ACTION_EXECUTE = "com.firebase.jobdispatcher.ACTION_EXECUTE";
/**
* Correlates job tags (unique strings) with Messages, which are used to signal the completion
* of a job.
*/
private final SimpleArrayMap<String, JobCallback> runningJobs = new SimpleArrayMap<>(1);
private LocalBinder binder = new LocalBinder();
/**
* The entry point to your Job. Implementations should offload work to another thread of
* execution as soon as possible because this runs on the main thread. If work was offloaded,
* call {@link JobService#jobFinished(JobParameters, boolean)} to notify the scheduling service
* that the work is completed.
*
* In order to reschedule use {@link JobService#jobFinished(JobParameters, boolean)}.
*
* @return {@code true} if there is more work remaining in the worker thread, {@code false} if
* the job was completed.
*/
@MainThread
public abstract boolean onStartJob(JobParameters job);
/**
* Called when the scheduling engine has decided to interrupt the execution of a running job,
* most likely because the runtime constraints associated with the job are no longer satisfied.
* The job must stop execution.
*
* @return true if the job should be retried
* @see com.firebase.jobdispatcher.JobInvocation.Builder#setRetryStrategy(RetryStrategy)
* @see RetryStrategy
*/
@MainThread
public abstract boolean onStopJob(JobParameters job);
@MainThread
void start(JobParameters job, Message msg) {
synchronized (runningJobs) {
if (runningJobs.containsKey(job.getTag())) {
Log.w(TAG, String
.format(Locale.US, "Job with tag = %s was already running.", job.getTag()));
return;
}
runningJobs.put(job.getTag(), new JobCallback(msg));
boolean moreWork = onStartJob(job);
if (!moreWork) {
JobCallback callback = runningJobs.remove(job.getTag());
if (callback != null) {
callback.sendResult(RESULT_SUCCESS);
}
}
}
}
@MainThread
void stop(JobInvocation job) {
synchronized (runningJobs) {
JobCallback jobCallback = runningJobs.remove(job.getTag());
if (jobCallback == null) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Provided job has already been executed.");
}
return;
}
boolean shouldRetry = onStopJob(job);
jobCallback.sendResult(shouldRetry ? RESULT_FAIL_RETRY : RESULT_SUCCESS);
}
}
/**
* Callback to inform the scheduling driver that you've finished executing. Can be called from
* any thread. When the system receives this message, it will release the wakelock being held.
*
* @param job
* @param needsReschedule whether the job should be rescheduled
*
* @see com.firebase.jobdispatcher.JobInvocation.Builder#setRetryStrategy(RetryStrategy)
*/
public final void jobFinished(@NonNull JobParameters job, boolean needsReschedule) {
if (job == null) {
Log.e(TAG, "jobFinished called with a null JobParameters");
return;
}
synchronized (runningJobs) {
JobCallback jobCallback = runningJobs.remove(job.getTag());
if (jobCallback != null) {
jobCallback.sendResult(needsReschedule ? RESULT_FAIL_RETRY : RESULT_SUCCESS);
}
}
}
@Override
public final int onStartCommand(Intent intent, int flags, int startId) {
stopSelf(startId);
return START_NOT_STICKY;
}
@Nullable
@Override
public final IBinder onBind(Intent intent) {
return binder;
}
@Override
public final boolean onUnbind(Intent intent) {
synchronized (runningJobs) {
for (int i = runningJobs.size() - 1; i >= 0; i--) {
JobCallback callback = runningJobs.get(runningJobs.keyAt(i));
if (callback != null) {
callback.sendResult(onStopJob((JobParameters) callback.message.obj)
// returned true, would like to be rescheduled
? RESULT_FAIL_RETRY
// returned false, but was interrupted so consider it a fail
: RESULT_FAIL_NORETRY);
}
}
}
return super.onUnbind(intent);
}
@Override
public final void onRebind(Intent intent) {
super.onRebind(intent);
}
@Override
public final void onStart(Intent intent, int startId) {
}
@Override
protected final void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
super.dump(fd, writer, args);
}
@Override
public final void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
}
@Override
public final void onTaskRemoved(Intent rootIntent) {
super.onTaskRemoved(rootIntent);
}
/**
* The result returned from a job execution.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({RESULT_SUCCESS, RESULT_FAIL_RETRY, RESULT_FAIL_NORETRY})
public @interface JobResult {
}
private final static class JobCallback {
public final Message message;
private JobCallback(Message message) {
this.message = message;
}
void sendResult(@JobResult int result) {
message.arg1 = result;
message.sendToTarget();
}
}
class LocalBinder extends Binder {
JobService getService() {
return JobService.this;
}
}
}