/*
* @copyright 2011 Philip Warner
* @license GNU General Public License V3
*
* This file is part of Book Catalogue.
*
* Book Catalogue is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Book Catalogue is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Book Catalogue. If not, see <http://www.gnu.org/licenses/>.
*/
package com.eleybourn.bookcatalogue.utils;
import java.util.ArrayList;
import java.util.Stack;
import java.util.concurrent.LinkedBlockingQueue;
import android.os.Handler;
import com.eleybourn.bookcatalogue.BookCatalogueApp;
import com.eleybourn.bookcatalogue.CatalogueDBAdapter;
import com.eleybourn.bookcatalogue.database.CoversDbHelper;
/**
* Class to perform time consuming but light-weight tasks in a worker thread. Users of this
* class should implement their tasks as self-contained objects that implement SimpleTask.
*
* The tasks run from (currently) a LIFO queue in a single thread; the run() method is called
* in the alternate thread and the finished() method is called in the UI thread.
*
* The execution queue is (currently) a stack so that the most recent queued is loaded. This is
* good for loading (eg) gallery images to make sure that the most recently viewed is loaded.
*
* The results queue is executed in FIFO order.
*
* In the future, both queues could be done independently and this object could be broken into
* 3 classes: SimpleTaskQueueBase, SimpleTaskQueueFIFO and SimpleTaskQueueLIFO. For now, this is
* not needed.
*
* TODO: Consider adding an 'AbortListener' interface so tasks can be told when queue is aborted
* TODO: Consider adding an 'aborted' flag to onFinish() and always calling onFinish() when queue is killed
*
* NOTE: Tasks can call context.isTerminating() if necessary
*
* @author Philip Warner
*/
public class SimpleTaskQueue {
// Execution queue
private BlockingStack<SimpleTaskWrapper> mQueue = new BlockingStack<SimpleTaskWrapper>();
// Results queue
private LinkedBlockingQueue<SimpleTaskWrapper> mResultQueue = new LinkedBlockingQueue<SimpleTaskWrapper>();
// Flag indicating this object should terminate.
private boolean mTerminate = false;
// Handler for sending tasks to the UI thread.
private Handler mHandler = new Handler();
// Name for this queue
private final String mName;
// Threads associate with this queue
private ArrayList<SimpleTaskQueueThread> mThreads = new ArrayList<SimpleTaskQueueThread>();
/** Max number of threads to create */
private int mMaxTasks;
/** Number of currently queued, executing (or starting/finishing) tasks */
private int mManagedTaskCount = 0;
private OnTaskStartListener mTaskStartListener = null;
private OnTaskFinishListener mTaskFinishListener = null;
/**
* SimpleTask interface.
*
* run() is called in worker thread
* finished() is called in UI thread.
*
* @author Philip Warner
*
*/
public interface SimpleTask {
/**
* Method called in queue thread to perform the background task.
*/
void run(SimpleTaskContext taskContext) throws Exception;
/**
* Method called in UI thread after the background task has finished.
* @param e TODO
*/
void onFinish(Exception e);
}
/**
* Interface for an object to listen for when tasks start.
*
* @author Philip Warner
*/
public interface OnTaskStartListener {
void onTaskStart(SimpleTask task);
}
/**
* Interface for an object to listen for when tasks finish.
*
* @author Philip Warner
*/
public interface OnTaskFinishListener {
void onTaskFinish(SimpleTask task, Exception e);
}
/**
* Accessor.
*
* @param listener
*/
public void setTaskStartListener(OnTaskStartListener listener) {
mTaskStartListener = listener;
}
/**
* Accessor.
*
* @param listener
*/
public OnTaskStartListener getTaskStartListener() {
return mTaskStartListener;
}
/**
* Accessor.
*
* @param listener
*/
public void setTaskFinishListener(OnTaskFinishListener listener) {
mTaskFinishListener = listener;
}
/**
* Accessor.
*
* @param listener
*/
public OnTaskFinishListener getTaskFinishListener() {
return mTaskFinishListener;
}
/**
* Accessor
*
* @return Flag indicating queue is terminating (finish() was called)
*/
public boolean isTerminating() {
return mTerminate;
}
/**
* Class to wrap a simpleTask with more info needed by the queue.
*
* @author Philip Warner
*/
private static class SimpleTaskWrapper implements SimpleTaskContext {
private static Long mCounter = 0L;
private final SimpleTaskQueue mOwner;
public SimpleTask task;
public Exception exception;
public boolean finishRequested = true;
public long id;
public SimpleTaskQueueThread activeThread = null;
SimpleTaskWrapper(SimpleTaskQueue owner, SimpleTask task) {
mOwner = owner;
this.task = task;
synchronized(mCounter) {
this.id = ++mCounter;
}
}
/**
* Accessor when behaving as a context
*/
@Override
public CatalogueDBAdapter getDb() {
if (activeThread == null)
throw new RuntimeException("SimpleTaskWrapper can only be used a context during the run() stage");
return activeThread.getDb();
}
@Override
public CoversDbHelper getCoversDb() {
if (activeThread == null)
throw new RuntimeException("SimpleTaskWrapper can only be used a context during the run() stage");
return activeThread.getCoversDb();
}
@Override
public Utils getUtils() {
if (activeThread == null)
throw new RuntimeException("SimpleTaskWrapper can only be used a context during the run() stage");
return activeThread.getUtils();
}
@Override
public void setRequiresFinish(boolean requiresFinish) {
this.finishRequested = requiresFinish;
}
@Override
public boolean getRequiresFinish() {
return finishRequested;
}
@Override
public boolean isTerminating() {
return mOwner.isTerminating();
}
}
/**
* Constructor. Nothing to see here, move along. Just start the thread.
*
* @author Philip Warner
*
*/
public SimpleTaskQueue(String name) {
mName = name;
mMaxTasks = 5;
}
/**
* Constructor. Nothing to see here, move along. Just start the thread.
*
* @author Philip Warner
*
*/
public SimpleTaskQueue(String name, int maxTasks) {
mName = name;
mMaxTasks = maxTasks;
if (maxTasks < 1 || maxTasks > 10)
throw new RuntimeException("Illegal value for maxTasks");
}
/**
* Terminate processing.
*/
public void finish() {
synchronized(this) {
mTerminate = true;
for(Thread t : mThreads) {
try { t.interrupt(); } catch (Exception e) {};
}
}
}
/**
* Check to see if any tasks are active -- either queued, or with ending results.
*
* @return
*/
public boolean hasActiveTasks() {
synchronized(this) {
return ( mManagedTaskCount > 0 );
}
}
/**
* Queue a request to run in the worker thread.
*
* @param task Task to run.
*/
public long enqueue(SimpleTask task) {
SimpleTaskWrapper wrapper = new SimpleTaskWrapper(this, task);
try {
synchronized(this) {
mQueue.push(wrapper);
mManagedTaskCount++;
}
} catch (InterruptedException e) {
// Ignore. This happens if the queue object is being terminated.
}
//System.out.println("SimpleTaskQueue(added): " + mQueue.size());
synchronized(this) {
int qSize = mQueue.size();
int nThreads = mThreads.size();
if (nThreads < qSize && nThreads < mMaxTasks) {
SimpleTaskQueueThread t = new SimpleTaskQueueThread();
mThreads.add(t);
t.start();
}
}
return wrapper.id;
}
/**
* Remove a previously requested task based on ID, if present
*/
public boolean remove(long id) {
Stack<SimpleTaskWrapper> currTasks = mQueue.getElements();
for (SimpleTaskWrapper w : currTasks) {
if (w.id == id) {
synchronized(this) {
if (mQueue.remove(w))
mManagedTaskCount--;
}
//System.out.println("SimpleTaskQueue(removeok): " + mQueue.size());
return true;
}
}
//System.out.println("SimpleTaskQueue(removefail): " + mQueue.size());
return false;
}
/**
* Remove a previously requested task, if present
*/
public boolean remove(SimpleTask t) {
Stack<SimpleTaskWrapper> currTasks = mQueue.getElements();
for (SimpleTaskWrapper w : currTasks) {
if (w.task.equals(t)) {
synchronized(this) {
if (mQueue.remove(w))
mManagedTaskCount--;
}
//System.out.println("SimpleTaskQueue(removeok): " + mQueue.size());
return true;
}
}
//System.out.println("SimpleTaskQueue(removefail): " + mQueue.size());
return false;
}
/**
* Flag indicating runnable is queued but not run; avoids multiple unnecessary runnables
*/
private boolean mDoProcessResultsIsQueued = false;
/**
* Method to ensure results queue is processed.
*/
private Runnable mDoProcessResults = new Runnable() {
@Override
public void run() {
synchronized(mDoProcessResults) {
mDoProcessResultsIsQueued = false;
}
processResults();
}
};
/**
* Run the task then queue the results.
*
* @param task
*/
private void handleRequest(final SimpleTaskQueueThread thread, final SimpleTaskWrapper taskWrapper) {
final SimpleTask task = taskWrapper.task;
if (mTaskStartListener != null) {
try {
mTaskStartListener.onTaskStart(task);
} catch (Exception e) {
// Ignore
}
}
// Use the thread object to get some context stuff (mainly DBs)
taskWrapper.activeThread = thread;
try {
task.run(taskWrapper);
} catch (Exception e) {
taskWrapper.exception = e;
Logger.logError(e, "Error running task");
} finally {
// Dereference
taskWrapper.activeThread = null;
}
// Feature removed because onFinish() now gets any exception caught from run()
// Now the run() method can be used to change if onFinish() is called via the TaskContext
//
// 90% of implementations had to implement onFinish() and always returned true.
//
// See if we need to call finished(). Default to true.
//try {
// taskWrapper.finishRequested = task.requiresOnFinish();
//} catch (Exception e) {
// taskWrapper.finishRequested = true;
//}
synchronized(this) {
// Queue the call to finished() if necessary.
if (taskWrapper.finishRequested || mTaskFinishListener != null) {
try {
mResultQueue.put(taskWrapper);
} catch (InterruptedException e) {
}
// Queue Runnable in the UI thread.
synchronized(mDoProcessResults) {
if (!mDoProcessResultsIsQueued) {
mDoProcessResultsIsQueued = true;
mHandler.post(mDoProcessResults);
}
}
} else {
// If no other methods are going to be called, then decrement
// managed task count. We do not care about this task any more.
mManagedTaskCount--;
}
}
}
/**
* Run in the UI thread, process the results queue.
*/
private void processResults() {
try {
while (!mTerminate) {
// Get next; if none, exit.
SimpleTaskWrapper req = mResultQueue.poll();
if (req == null)
break;
final SimpleTask task = req.task;
// Decrement the managed task count BEFORE we call any methods.
// This allows them to call hasActiveTasks() and get a useful result
// when they are the last task.
synchronized(this) {
mManagedTaskCount--;
}
// Call the task handler; log and ignore errors.
if (req.finishRequested) {
try {
task.onFinish(req.exception);
} catch (Exception e) {
Logger.logError(e, "Error processing request result");
}
}
// Call the task listener; log and ignore errors.
if (mTaskFinishListener != null)
try {
mTaskFinishListener.onTaskFinish(task, req.exception);
} catch (Exception e) {
Logger.logError(e, "Error from listener while processing request result");
}
}
} catch (Exception e) {
Logger.logError(e, "Exception in processResults in UI thread");
}
}
public static interface SimpleTaskContext {
public CatalogueDBAdapter getDb();
/** 'Covers' database helper */
public CoversDbHelper getCoversDb();
/** Utils object */
public Utils getUtils();
/** Accessor */
public void setRequiresFinish(boolean requiresFinish);
/** Accessor */
public boolean getRequiresFinish();
/** Accessor */
public boolean isTerminating();
}
/**
* Class to actually run the tasks. Can start more than one. They wait until there is nothing left in
* the queue before terminating.
*
* @author Philip Warner
*/
private class SimpleTaskQueueThread extends Thread {
/** DB Connection, if task requests one. Survives while thread is alive */
CatalogueDBAdapter mDb = null;
/** Covers DB Connection, if task requests one. Survives while thread is alive */
CoversDbHelper mCoversDb = null;
/** Utils object, if needed. Survives while thread is alive */
Utils mUtils = null;
/**
* Main worker thread logic
*/
public void run() {
try {
this.setName(mName);
while (!mTerminate) {
SimpleTaskWrapper req = mQueue.pop(15000);
// If timeout occurred, get a lock on the queue and see if anything was queued
// in the intervening milliseconds. If not, delete this tread and exit.
if (req == null) {
synchronized(SimpleTaskQueue.this) {
req = mQueue.poll();
if (req == null) {
mThreads.remove(this);
return;
}
}
}
//System.out.println("SimpleTaskQueue(run): " + mQueue.size());
handleRequest(this, req);
}
} catch (InterruptedException e) {
// Ignore; these will happen when object is destroyed
} catch (Exception e) {
Logger.logError(e);
} finally {
try {
if (mDb != null)
mDb.close();
} catch (Exception e) {}
try {
if (mCoversDb != null)
mCoversDb.close();
} catch (Exception e) {}
try {
if (mUtils != null)
mUtils.close();
} catch (Exception e) {}
}
}
public CatalogueDBAdapter getDb() {
if (mDb == null) {
mDb = new CatalogueDBAdapter(BookCatalogueApp.context);
mDb.open();
}
return mDb;
}
public Utils getUtils() {
if (mUtils == null)
mUtils = new Utils();
return mUtils;
}
public CoversDbHelper getCoversDb() {
if (mCoversDb == null)
mCoversDb = new CoversDbHelper();
return mCoversDb;
}
}
}