package com.github.ignition.core.tasks;
import java.util.concurrent.Callable;
import android.app.Activity;
import android.content.Context;
import android.os.AsyncTask;
/**
* An extension to AsyncTask that removes some of the boilerplate that's typically involved. Some of
* the things handled for you by this class are:
* <ul>
* <li>handles a {@link Context} reference for you, which is passed to all status callbacks</li>
* <li>allows worker method to throw exceptions, which will be passed to
* {@link #onTaskFailed(Context, Exception)}</li>
* <li>allows re-using a task as a skeleton that delegates to different worker implementations via
* {@link IgnitedAsyncTaskCallable}</li>
* <li>allows a {@link Context} to register itself as a callback handler via
* {@link IgnitedAsyncTaskContextHandler}</li>
* <li>allows any arbitrary object to register itself as a callback handler via
* {@link IgnitedAsyncTaskHandler}</li>
* </ul>
* <p>
* Since this class keeps a reference to a Context (either directly or indirectly through a callback
* handler), you MUST ensure that this reference is cleared when the Context gets destroyed. You can
* handle Context and handler connection and disconnection using the {@link #connect} and
* {@link #disconnect} methods. For Activities, a good place to call them is onCreate and onDestroy
* respectively.
* </p>
* <p>
* Please note that the callbacks added by this class will ONLY be called if the context reference
* is valid. You can still receive AsyncTask's callbacks by overriding the callbacks defined in that
* class.
* </p>
*
* @author Matthias Kaeppler
*
* @param <ContextT>
* @param <ParameterT>
* @param <ProgressT>
* @param <ReturnT>
*/
public abstract class IgnitedAsyncTask<ContextT extends Context, ParameterT, ProgressT, ReturnT>
extends AsyncTask<ParameterT, ProgressT, ReturnT> implements
IgnitedAsyncTaskHandler<ContextT, ProgressT, ReturnT>,
IgnitedAsyncTaskContextHandler<ProgressT, ReturnT> {
public interface IgnitedAsyncTaskCallable<ContextT extends Context, ParameterT, ProgressT, ReturnT> {
public ReturnT call(IgnitedAsyncTask<ContextT, ParameterT, ProgressT, ReturnT> task)
throws Exception;
}
private volatile ContextT context;
private IgnitedAsyncTaskContextHandler<ProgressT, ReturnT> contextHandler;
private IgnitedAsyncTaskHandler<ContextT, ProgressT, ReturnT> delegateHandler;
private IgnitedAsyncTaskCallable<ContextT, ParameterT, ProgressT, ReturnT> callable;
private Exception error;
protected boolean cancelOnActivityBack = true;
public IgnitedAsyncTask() {
}
public void setCancelOnActivityBack(boolean cancelOnActivityBack) {
this.cancelOnActivityBack = cancelOnActivityBack;
}
public boolean isCancelOnActivityBack() {
return cancelOnActivityBack;
}
/**
* Connects a context object to this task. Use this method immediately after first creating the
* task, and again in {@link Activity#onCreate} to re-connect tasks that you retained via
* {@link Activity#onRetainNonConfigurationInstance()}. Make sure to always
* {@link #disconnect()} a context in {@link Activity#onDestroy}.
*
* <p>
* As long as this context is not null, it will be passed to any handler callbacks associated
* with this task, as well as to the task's own callbacks. If the context you pass here
* implements {@link IgnitedAsyncTaskContextHandler}, it will be registered to receive callbacks
* itself. If instead it implements the more generic {@link IgnitedAsyncTaskHandler}, <b>and if
* no other handler of this type has been connected before (!)</b>, then the context will also
* receive these callbacks. If however an ordinary POJO had already been connected to this task
* as a {@link IgnitedAsyncTaskHandler}, then the context will not receive callbacks via this
* interface (this is because a context must be disconnected from the task in onDestroy, while
* ordinary handlers that are not Contexts will be retained by the task instance).
* </p>
*/
@SuppressWarnings("unchecked")
public void connect(ContextT context) {
this.context = context;
if (context instanceof IgnitedAsyncTaskContextHandler) {
this.contextHandler = (IgnitedAsyncTaskContextHandler<ProgressT, ReturnT>) context;
} else if (delegateHandler == null && context instanceof IgnitedAsyncTaskHandler) {
this.delegateHandler = (IgnitedAsyncTaskHandler<ContextT, ProgressT, ReturnT>) context;
}
if (delegateHandler != null) {
delegateHandler.setContext(context);
}
}
/**
* Connects an {@link IgnitedAsyncTaskHandler} to this task to receive callbacks. The context
* instance wrapped by this handler will be passed to {@link #connect(Context)}.
*
* @param handler
* the handler object to receive task callbacks
*/
public void connect(IgnitedAsyncTaskHandler<ContextT, ProgressT, ReturnT> handler) {
this.delegateHandler = handler;
connect(handler.getContext());
}
/**
* Disconnects all context references, also those that are both a Context and handler at the
* same time. Call this method in {@link Context#onDestroy}. <b>Note that this will NOT
* disconnect handlers which are not Contexts but plain POJOs, so make sure you do not leak an
* implicit reference to the same context from any handlers you have connected.</b>
*/
public void disconnect() {
this.contextHandler = null;
this.context = null;
if (delegateHandler != null) {
if (delegateHandler instanceof Context) {
delegateHandler = null;
} else {
delegateHandler.setContext(null);
}
}
}
/**
* Returns the task's current context reference. Can be null. Handling the context reference is
* done in {@link #connect(Context)} and {@link #disconnect()}.
*/
@Override
public ContextT getContext() {
return context;
}
/**
* DON'T use this to handle the task's context reference. Use {@link #connect(Context)} and
* {@link #disconnect()} instead.
*/
@Override
public void setContext(ContextT context) {
this.context = context;
}
/**
* If you have connected a Context which implements the {@link IgnitedAsyncTaskContextHandler}
* interface, this instance will be returned. Null otherwise.
*/
public IgnitedAsyncTaskContextHandler<ProgressT, ReturnT> getContextHandler() {
return contextHandler;
}
/**
* If you have connected a Context or POJO which implements the {@link IgnitedAsyncTaskHandler}
* interface, this instance will be returned. Null otherwise.
*/
public IgnitedAsyncTaskHandler<ContextT, ProgressT, ReturnT> getDelegateHandler() {
return delegateHandler;
}
/**
* If you rely on a valid context reference, override {@link #onTaskStarted(Context)} instead.
*/
@Override
protected void onPreExecute() {
boolean eventHandled = false;
if (context != null) {
if (contextHandler != null) {
eventHandled = contextHandler.onTaskStarted();
}
if (delegateHandler != null) {
eventHandled = delegateHandler.onTaskStarted(context);
}
if (!eventHandled) {
eventHandled = onTaskStarted(context);
}
}
if (!eventHandled) {
onTaskStarted();
}
}
/**
* Override this method to prepare task execution. The default implementation simply returns
* false. This variant of the method is only called if context is not null.
*
* @see {@link AsyncTask#onPreExecute}
* @param context
* The most recent instance of the Context that executed this IgnitedAsyncTask
*/
@Override
public boolean onTaskStarted(ContextT context) {
return false;
}
/**
* Override this method to prepare task execution. The default implementation simply returns
* false. This variant of the method is called even when no context is connected.
*
* @see {@link AsyncTask#onPreExecute}
*/
@Override
public boolean onTaskStarted() {
return false;
}
/**
* If you rely on a valid context reference, override {@link #onProgress} instead.
*/
@Override
protected void onProgressUpdate(ProgressT... values) {
boolean eventHandled = false;
if (context != null) {
if (contextHandler != null) {
eventHandled = contextHandler.onTaskProgress(values);
}
if (delegateHandler != null) {
eventHandled = delegateHandler.onTaskProgress(context, values);
}
if (!eventHandled) {
eventHandled = onTaskProgress(context, values);
}
}
if (!eventHandled) {
onTaskProgress(values);
}
}
/**
* Override this method to update progress elements on the UI thread. The default implementation
* simply returns false. This variant of the method is only called if context is not null.
*
* @see {@link AsyncTask#publishProgress}
* @see {@link AsyncTask#onProgressUpdate}
* @param context
* The most recent instance of the Context that executed this IgnitedAsyncTask
* @param progress
* the progress values
*/
@Override
public boolean onTaskProgress(ContextT context, ProgressT... progress) {
return false;
}
/**
* Override this method to update progress elements on the UI thread. The default implementation
* simply returns false. This variant of the method is called even when no context is connected.
*
* @see {@link AsyncTask#publishProgress}
* @see {@link AsyncTask#onProgressUpdate}
* @param progress
* the progress values
*/
@Override
public boolean onTaskProgress(ProgressT... progress) {
return false;
}
/**
* No, you want to override {@link #run(Object...)} instead.
*/
@Override
protected final ReturnT doInBackground(ParameterT... params) {
ReturnT result = null;
try {
if (callable != null) {
result = callable.call(this);
} else {
result = run(params);
}
} catch (Exception e) {
this.error = e;
}
return result;
}
/**
* Implement this method to define your task execution. If your task logic is pluggable, but
* shares progress reporting or pre/post execute hooks, you can also set a {@link Callable} via
* {@link #setCallable(IgnitedAsyncTaskCallable)}
* <p>
* Typically you do not call this method directly; instead it's called by {@link #execute()} and
* run on a worker thread. If however you want to execute the task synchronously, you can invoke
* this method directly.
* </p>
*
* @see {@link AsyncTask#doInBackground}
* @param params
* the parameters for your task
* @return the result of your task
* @throws Exception
*/
public ReturnT run(ParameterT... params) throws Exception {
return null;
}
/**
* Don't override this method; use {@link #onTaskCompleted}, {@link #onTaskSuccess}, and
* {@link #onTaskFailed} instead.
*/
@Override
protected void onPostExecute(ReturnT result) {
handleTaskCompleted(result);
if (failed()) {
handleTaskFailed(error);
} else {
handleTaskSuccess(result);
}
}
private void handleTaskCompleted(ReturnT result) {
boolean eventHandled = false;
if (context != null) {
if (contextHandler != null) {
eventHandled = contextHandler.onTaskCompleted(result);
}
if (delegateHandler != null) {
eventHandled = delegateHandler.onTaskCompleted(context, result);
}
if (!eventHandled) {
eventHandled = onTaskCompleted(context, result);
}
}
if (!eventHandled) {
onTaskCompleted(result);
}
}
private void handleTaskSuccess(ReturnT result) {
boolean eventHandled = false;
if (context != null) {
if (contextHandler != null) {
eventHandled = contextHandler.onTaskSuccess(result);
}
if (delegateHandler != null) {
eventHandled = delegateHandler.onTaskSuccess(context, result);
}
if (!eventHandled) {
eventHandled = onTaskSuccess(context, result);
}
}
if (!eventHandled) {
onTaskSuccess(result);
}
}
private void handleTaskFailed(Exception error) {
boolean eventHandled = false;
if (context != null) {
if (contextHandler != null) {
eventHandled = contextHandler.onTaskFailed(error);
}
if (delegateHandler != null) {
eventHandled = delegateHandler.onTaskFailed(context, error);
}
if (!eventHandled) {
eventHandled = onTaskFailed(context, error);
}
}
if (!eventHandled) {
onTaskFailed(error);
}
}
/**
* Implement this method to handle a completed task execution, regardless of outcome. The
* default implementation simply returns false. This variant of the method is only called if
* context is not null.
*
* @see {@link AsyncTask#onPostExecute}
* @param context
* The most recent instance of the Context that executed this IgnitedAsyncTask the
* result of the task execution
*/
@Override
public boolean onTaskCompleted(ContextT context, ReturnT result) {
return false;
}
/**
* Implement this method to handle a completed task execution, regardless of outcome. The
* default implementation simply returns false. This variant of the method is called even when
* no context is connected.
*
* @see {@link AsyncTask#onPostExecute}
* @param result
* the result of the task execution
*/
@Override
public boolean onTaskCompleted(ReturnT result) {
return false;
}
/**
* Implement this method to handle a successful task execution. The default implementation
* simply returns false. This variant of the method is only called if context is not null.
*
* @param context
* The most recent instance of the Context that executed this IgnitedAsyncTask
* @param result
* the result of the task execution
*/
@Override
public boolean onTaskSuccess(ContextT context, ReturnT result) {
return false;
}
/**
* Implement this method to handle a successful task execution. The default implementation
* simply returns false. This variant of the method is called even when no context is connected.
*
* @param result
* the result of the task execution
*/
@Override
public boolean onTaskSuccess(ReturnT result) {
return false;
}
/**
* Override this method to handle an error that occurred during task execution in a graceful
* manner. The default implementation returns false. This variant of the method is only called
* if context is not null.
*
* @param context
* The most recent instance of the Context that executed this IgnitedAsyncTask
* @param error
* The exception that was thrown during task execution
*/
@Override
public boolean onTaskFailed(ContextT context, Exception error) {
return false;
}
/**
* Override this method to handle an error that occurred during task execution in a graceful
* manner. The default implementation returns false. This variant of the method is called even
* when no context is connected.
*
* @param error
* The exception that was thrown during task execution
*/
@Override
public boolean onTaskFailed(Exception error) {
return false;
}
/**
* @return true if an Exception was thrown in {@link #run}
*/
public boolean failed() {
return error != null;
}
/**
* @see AsyncTask.Status.PENDING
*/
public boolean isPending() {
return getStatus().equals(AsyncTask.Status.PENDING);
}
/**
* @see AsyncTask.Status.RUNNING
*/
public boolean isRunning() {
return getStatus().equals(AsyncTask.Status.RUNNING);
}
/**
* @see AsyncTask.Status.FINISHED
*/
public boolean isFinished() {
return getStatus().equals(AsyncTask.Status.FINISHED);
}
/**
* Use an {@link IgnitedAsyncTaskCallable} instead of overriding {@link #run}. This can be
* useful when creating task skeletons that need to implement the worker method differently.
*
* @param callable
*/
public void setCallable(
IgnitedAsyncTaskCallable<ContextT, ParameterT, ProgressT, ReturnT> callable) {
this.callable = callable;
}
}