/**
* 2011 Foxykeep (http://datadroid.foxykeep.com)
* <p>
* Licensed under the Beerware License : <br />
* As long as you retain this notice you can do whatever you want with this stuff. If we meet some
* day, and you think this stuff is worth it, you can buy me a beer in return
*/
package com.foxykeep.datadroid.service;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import java.util.ArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
/**
* MultiThreadIntentService is a base class for {@link Service}s that handle asynchronous requests
* (expressed as {@link Intent}s) on demand. Clients send requests through
* {@link android.content.Context#startService(Intent)} calls; the service is started as needed,
* handles each Intent in turn using a worker thread, and stops itself when it runs out of work.
* <p>
* This "work queue processor" pattern is commonly used to offload tasks from an application's main
* thread. The MultiThreadedIntentService class exists to simplify this pattern and take care of the
* mechanics. To use it, extend MultiThreadedIntentService and implement
* {@link #onHandleIntent(Intent)}. MultiThreadedIntentService will receive the Intents, launch a
* worker thread, and stop the service as appropriate.
* <p>
* All requests are handled on multiple worker threads -- they may take as long as necessary (and
* will not block the application's main loop). By default only one concurrent worker thread is
* used. You can modify the number of current worker threads by overriding
* {@link #getMaximumNumberOfThreads()}.
* <p>
* For obvious efficiency reasons, MultiThreadedIntentService won't stop itself as soon as all tasks
* has been processed. It will only stop itself after a certain delay (about 30s). This optimization
* prevents the system from creating new instances over and over again when tasks are sent.
*
* @author Foxykeep
*/
public abstract class MultiThreadedIntentService extends Service {
private static final long STOP_SELF_DELAY = TimeUnit.SECONDS.toMillis(30L);
private ExecutorService mThreadPool;
private boolean mRedelivery;
private ArrayList<Future<?>> mFutureList;
private Handler mHandler;
private final Runnable mStopSelfRunnable = new Runnable() {
@Override
public void run() {
stopSelf();
}
};
private final Runnable mWorkDoneRunnable = new Runnable() {
@Override
public void run() {
if (Looper.getMainLooper().getThread() != Thread.currentThread()) {
throw new IllegalStateException(
"This runnable can only be called in the Main thread!");
}
final ArrayList<Future<?>> futureList = mFutureList;
for (int i = 0; i < futureList.size(); i++) {
if (futureList.get(i).isDone()) {
futureList.remove(i);
i--;
}
}
if (futureList.isEmpty()) {
mHandler.postDelayed(mStopSelfRunnable, STOP_SELF_DELAY);
}
}
};
/**
* Sets intent redelivery preferences. Usually called from the constructor with your preferred
* semantics.
* <p>
* If enabled is true, {@link #onStartCommand(Intent, int, int)} will return
* {@link Service#START_REDELIVER_INTENT}, so if this process dies before
* {@link #onHandleIntent(Intent)} returns, the process will be restarted and the intent
* redelivered. If multiple Intents have been sent, only the most recent one is guaranteed to be
* redelivered.
* <p>
* If enabled is false (the default), {@link #onStartCommand(Intent, int, int)} will return
* {@link Service#START_NOT_STICKY}, and if the process dies, the Intent dies along with it.
*/
public void setIntentRedelivery(boolean enabled) {
mRedelivery = enabled;
}
@Override
public void onCreate() {
super.onCreate();
int maximumNumberOfThreads = getMaximumNumberOfThreads();
if (maximumNumberOfThreads <= 0) {
throw new IllegalArgumentException("Maximum number of threads must be " +
"strictly positive");
}
mThreadPool = Executors.newFixedThreadPool(maximumNumberOfThreads);
mHandler = new Handler();
mFutureList = new ArrayList<Future<?>>();
}
@Override
public void onStart(Intent intent, int startId) {
mHandler.removeCallbacks(mStopSelfRunnable);
mFutureList.add(mThreadPool.submit(new IntentRunnable(intent)));
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
onStart(intent, startId);
return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY;
}
@Override
public void onDestroy() {
super.onDestroy();
mThreadPool.shutdown();
}
/**
* Unless you provide binding for your service, you don't need to implement this method, because
* the default implementation returns null.
*
* @see android.app.Service#onBind
*/
@Override
public IBinder onBind(Intent intent) {
return null;
}
/**
* Define the maximum number of concurrent worker threads used to execute the incoming Intents.
* <p>
* By default only one concurrent worker thread is used at the same time. Overrides this method
* in subclasses to change this number.
* <p>
* This method is called once in the {@link #onCreate()}. Modifying the value returned after the
* {@link #onCreate()} is called will have no effect.
*
* @return The maximum number of concurrent worker threads
*/
protected int getMaximumNumberOfThreads() {
return 1;
}
private class IntentRunnable implements Runnable {
private final Intent mIntent;
public IntentRunnable(Intent intent) {
mIntent = intent;
}
public void run() {
onHandleIntent(mIntent);
mHandler.removeCallbacks(mWorkDoneRunnable);
mHandler.post(mWorkDoneRunnable);
}
}
/**
* This method is invoked on the worker thread with a request to process. The processing happens
* on a worker thread that runs independently from other application logic. When all requests
* have been handled, the IntentService stops itself, so you should not call {@link #stopSelf}.
*
* @param intent The value passed to {@link Context#startService(Intent)}.
*/
abstract protected void onHandleIntent(Intent intent);
}