diff --git a/AndroidAnnotations/androidannotations-api/src/main/java/org/androidannotations/annotations/Background.java b/AndroidAnnotations/androidannotations-api/src/main/java/org/androidannotations/annotations/Background.java index 323b489223..1302ab7023 100644 --- a/AndroidAnnotations/androidannotations-api/src/main/java/org/androidannotations/annotations/Background.java +++ b/AndroidAnnotations/androidannotations-api/src/main/java/org/androidannotations/annotations/Background.java @@ -28,5 +28,26 @@ @Retention(RetentionPolicy.CLASS) @Target(ElementType.METHOD) public @interface Background { - long delay() default 0; + /** + * Identifier for task cancellation. + * + * To cancel all tasks having a specified background id: + * + *
+ * boolean mayInterruptIfRunning = true;
+ * BackgroundExecutor.cancelAll("my_background_id", mayInterruptIfRunning);
+ *
+ **/
+ String id() default "";
+
+ /** Minimum delay, in milliseconds, before the background task is executed. */
+ int delay() default 0;
+
+ /**
+ * Serial execution group.
+ *
+ * All background tasks having the same serial will be executed
+ * sequentially.
+ **/
+ String serial() default "";
}
diff --git a/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/api/BackgroundExecutor.java b/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/api/BackgroundExecutor.java
index 35b90a96aa..f757ffc656 100644
--- a/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/api/BackgroundExecutor.java
+++ b/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/api/BackgroundExecutor.java
@@ -15,29 +15,291 @@
*/
package org.androidannotations.api;
+import java.util.ArrayList;
+import java.util.List;
import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
+import android.util.Log;
+
public class BackgroundExecutor {
- private static Executor executor = Executors.newCachedThreadPool();
- private static ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(2 * Runtime.getRuntime().availableProcessors());
+ private static final String TAG = "BackgroundExecutor";
+
+ private static Executor executor = Executors.newScheduledThreadPool(2 * Runtime.getRuntime().availableProcessors());
+
+ private static final Listdelay is strictly positive and the current
+ * executor does not support scheduling (if
+ * {@link #setExecutor(Executor)} has been called with such an
+ * executor)
+ * @return Future associated to the running task
+ */
+ private static Future> directExecute(Runnable runnable, int delay) {
+ Future> future = null;
+ if (delay > 0) {
+ /* no serial, but a delay: schedule the task */
+ if (!(executor instanceof ScheduledExecutorService)) {
+ throw new IllegalArgumentException("The executor set does not support scheduling");
+ }
+ ScheduledExecutorService scheduledExecutorService = (ScheduledExecutorService) executor;
+ future = scheduledExecutorService.schedule(runnable, delay, TimeUnit.MILLISECONDS);
+ } else {
+ if (executor instanceof ExecutorService) {
+ ExecutorService executorService = (ExecutorService) executor;
+ future = executorService.submit(runnable);
+ } else {
+ /* non-cancellable task */
+ executor.execute(runnable);
+ }
+ }
+ return future;
+ }
+
+ /**
+ * Execute a task after (at least) its delay and after all
+ * tasks added with the same non-null serial (if any) have
+ * completed execution.
+ *
+ * @param task
+ * the task to execute
+ * @throws IllegalArgumentException
+ * if task.delay is strictly positive and the
+ * current executor does not support scheduling (if
+ * {@link #setExecutor(Executor)} has been called with such an
+ * executor)
+ */
+ public static synchronized void execute(Task task) {
+ Future> future = null;
+ if (task.serial == null || !hasSerialRunning(task.serial)) {
+ task.executionAsked = true;
+ future = directExecute(task, task.remainingDelay);
+ }
+ if (task.id != null || task.serial != null) {
+ /* keep task */
+ task.future = future;
+ tasks.add(task);
+ }
+ }
+ /**
+ * Execute a task.
+ *
+ * @param runnable
+ * the task to execute
+ * @param id
+ * identifier used for task cancellation
+ * @param delay
+ * the time from now to delay execution, in milliseconds
+ * @param serial
+ * the serial queue (null or "" for no
+ * serial execution)
+ * @throws IllegalArgumentException
+ * if delay is strictly positive and the current
+ * executor does not support scheduling (if
+ * {@link #setExecutor(Executor)} has been called with such an
+ * executor)
+ */
+ public static void execute(final Runnable runnable, String id, int delay, String serial) {
+ execute(new Task(id, delay, serial) {
+ @Override
+ public void execute() {
+ runnable.run();
+ }
+ });
+ }
+
+ /**
+ * Execute a task after the given delay.
+ *
+ * @param runnable
+ * the task to execute
+ * @param delay
+ * the time from now to delay execution, in milliseconds
+ * @throws IllegalArgumentException
+ * if delay is strictly positive and the current
+ * executor does not support scheduling (if
+ * {@link #setExecutor(Executor)} has been called with such an
+ * executor)
+ */
+ public static void execute(Runnable runnable, int delay) {
+ directExecute(runnable, delay);
+ }
+
+ /**
+ * Execute a task.
+ *
+ * @param runnable
+ * the task to execute
+ */
public static void execute(Runnable runnable) {
- executor.execute(runnable);
+ directExecute(runnable, 0);
+ }
+
+ /**
+ * Execute a task after all tasks added with the same non-null
+ * serial (if any) have completed execution.
+ *
+ * Equivalent to {@link #execute(Runnable, String, int, String)
+ * execute(runnable, id, 0, serial)}.
+ *
+ * @param runnable
+ * the task to execute
+ * @param id
+ * identifier used for task cancellation
+ * @param serial
+ * the serial queue to use (null or ""
+ * for no serial execution)
+ */
+ public static void execute(Runnable runnable, String id, String serial) {
+ execute(runnable, id, 0, serial);
}
+ /**
+ * Change the executor.
+ *
+ * Note that if the given executor is not a {@link ScheduledExecutorService}
+ * then executing a task after a delay will not be supported anymore. If it
+ * is not even a {@link ExecutorService} then tasks will not be cancellable
+ * anymore.
+ *
+ * @param executor
+ * the new executor
+ */
public static void setExecutor(Executor executor) {
BackgroundExecutor.executor = executor;
}
- public static void executeDelayed(Runnable runnable, long delay) {
- scheduledExecutor.schedule(runnable, delay, TimeUnit.MILLISECONDS);
+ /**
+ * Cancel all tasks having the specified id.
+ *
+ * @param id
+ * the cancellation identifier
+ * @param mayInterruptIfRunning
+ * true if the thread executing this task should be
+ * interrupted; otherwise, in-progress tasks are allowed to
+ * complete
+ */
+ public static synchronized void cancelAll(String id, boolean mayInterruptIfRunning) {
+ for (int i = tasks.size() - 1; i >= 0; i--) {
+ Task task = tasks.get(i);
+ if (id.equals(task.id)) {
+ tasks.remove(i);
+ if (task.future != null) {
+ task.future.cancel(mayInterruptIfRunning);
+ } else if (task.executionAsked) {
+ Log.w(TAG, "A task with id " + task.id + " cannot be cancelled (the executor set does not support it)");
+ }
+ }
+ }
}
- public static void setScheduledExecutor(ScheduledExecutorService scheduledExecutor) {
- BackgroundExecutor.scheduledExecutor = scheduledExecutor;
+ /**
+ * Indicates whether a task with the specified serial has been
+ * submitted to the executor.
+ *
+ * @param serial
+ * the serial queue
+ * @return true if such a task has been submitted,
+ * false otherwise
+ */
+ private static boolean hasSerialRunning(String serial) {
+ for (Task task : tasks) {
+ if (task.executionAsked && serial.equals(task.serial)) {
+ return true;
+ }
+ }
+ return false;
}
+
+ /**
+ * Retrieve and remove the first task having the specified
+ * serial (if any).
+ *
+ * @param serial
+ * the serial queue
+ * @return task if found, null otherwise
+ */
+ private static Task take(String serial) {
+ int len = tasks.size();
+ for (int i = 0; i < len; i++) {
+ if (serial.equals(tasks.get(i).serial)) {
+ return tasks.remove(i);
+ }
+ }
+ return null;
+ }
+
+ public static abstract class Task implements Runnable {
+
+ private String id;
+ private int remainingDelay;
+ private long targetTimeMillis; /* since epoch */
+ private String serial;
+ private boolean executionAsked;
+ private Future> future;
+
+ public Task(String id, int delay, String serial) {
+ if (!"".equals(id)) {
+ this.id = id;
+ }
+ if (delay > 0) {
+ remainingDelay = delay;
+ targetTimeMillis = System.currentTimeMillis() + delay;
+ }
+ if (!"".equals(serial)) {
+ this.serial = serial;
+ }
+ }
+
+ @Override
+ public void run() {
+ try {
+ execute();
+ } finally {
+ /* handle next tasks */
+ postExecute();
+ }
+ }
+
+ public abstract void execute();
+
+ private void postExecute() {
+ if (id == null && serial == null) {
+ /* nothing to do */
+ return;
+ }
+ synchronized (BackgroundExecutor.class) {
+ /* execution complete */
+ tasks.remove(this);
+
+ if (serial != null) {
+ Task next = take(serial);
+ if (next != null) {
+ if (next.remainingDelay != 0) {
+ /* the delay may not have elapsed yet */
+ next.remainingDelay = Math.max(0, (int) (targetTimeMillis - System.currentTimeMillis()));
+ }
+ /* a task having the same serial was queued, execute it */
+ BackgroundExecutor.execute(next);
+ }
+ }
+ }
+ }
+
+ }
+
}
diff --git a/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/helper/APTCodeModelHelper.java b/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/helper/APTCodeModelHelper.java
index 1648c76447..4153fec49a 100644
--- a/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/helper/APTCodeModelHelper.java
+++ b/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/helper/APTCodeModelHelper.java
@@ -228,6 +228,20 @@ public String getIdStringFromIdFieldRef(JFieldRef idRef) {
throw new IllegalStateException("Unable to extract target name from JFieldRef");
}
+ public JTryBlock surroundWithTryCatch(EBeanHolder holder, JBlock block, JBlock content, String exceptionMessage) {
+ Classes classes = holder.classes();
+ JTryBlock tryBlock = block._try();
+ tryBlock.body().add(content);
+ JCatchBlock catchBlock = tryBlock._catch(classes.RUNTIME_EXCEPTION);
+ JVar exceptionParam = catchBlock.param("e");
+ JInvocation errorInvoke = classes.LOG.staticInvoke("e");
+ errorInvoke.arg(holder.generatedClass.name());
+ errorInvoke.arg(exceptionMessage);
+ errorInvoke.arg(exceptionParam);
+ catchBlock.body().add(errorInvoke);
+ return tryBlock;
+ }
+
public JDefinedClass createDelegatingAnonymousRunnableClass(EBeanHolder holder, JMethod delegatedMethod) {
JCodeModel codeModel = holder.codeModel();
@@ -242,20 +256,9 @@ public JDefinedClass createDelegatingAnonymousRunnableClass(EBeanHolder holder,
runMethod.annotate(Override.class);
JBlock runMethodBody = runMethod.body();
- JTryBlock runTry = runMethodBody._try();
- runTry.body().add(previousMethodBody);
-
- JCatchBlock runCatch = runTry._catch(classes.RUNTIME_EXCEPTION);
- JVar exceptionParam = runCatch.param("e");
-
- JInvocation errorInvoke = classes.LOG.staticInvoke("e");
-
- errorInvoke.arg(holder.generatedClass.name());
- errorInvoke.arg("A runtime exception was thrown while executing code in a runnable");
- errorInvoke.arg(exceptionParam);
+ surroundWithTryCatch(holder, runMethodBody, previousMethodBody, "A runtime exception was thrown while executing code in a runnable");
- runCatch.body().add(errorInvoke);
return anonymousRunnableClass;
}
diff --git a/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/processing/BackgroundProcessor.java b/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/processing/BackgroundProcessor.java
index 9db4750e47..896839e428 100644
--- a/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/processing/BackgroundProcessor.java
+++ b/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/processing/BackgroundProcessor.java
@@ -23,14 +23,17 @@
import org.androidannotations.annotations.Background;
import org.androidannotations.api.BackgroundExecutor;
+import org.androidannotations.api.BackgroundExecutor.Task;
import org.androidannotations.helper.APTCodeModelHelper;
+import com.sun.codemodel.JBlock;
import com.sun.codemodel.JClass;
import com.sun.codemodel.JClassAlreadyExistsException;
import com.sun.codemodel.JCodeModel;
import com.sun.codemodel.JDefinedClass;
import com.sun.codemodel.JInvocation;
import com.sun.codemodel.JMethod;
+import com.sun.codemodel.JMod;
public class BackgroundProcessor implements DecoratingElementProcessor {
@@ -50,25 +53,25 @@ public void process(Element element, JCodeModel codeModel, EBeanHolder holder) t
JMethod delegatingMethod = helper.overrideAnnotatedMethod(executableElement, holder);
- JDefinedClass anonymousRunnableClass = helper.createDelegatingAnonymousRunnableClass(holder, delegatingMethod);
+ JBlock previousMethodBody = helper.removeBody(delegatingMethod);
- {
- // Execute Runnable
- Background annotation = element.getAnnotation(Background.class);
- long delay = annotation.delay();
+ JDefinedClass anonymousTaskClass = codeModel.anonymousClass(Task.class);
- JClass backgroundExecutorClass = holder.refClass(BackgroundExecutor.class);
- JInvocation executeCall;
+ JMethod executeMethod = anonymousTaskClass.method(JMod.PUBLIC, codeModel.VOID, "execute");
+ executeMethod.annotate(Override.class);
- if (delay == 0) {
- executeCall = backgroundExecutorClass.staticInvoke("execute").arg(_new(anonymousRunnableClass));
- } else {
- executeCall = backgroundExecutorClass.staticInvoke("executeDelayed").arg(_new(anonymousRunnableClass)).arg(lit(delay));
- }
+ JBlock runMethodBody = executeMethod.body();
+ helper.surroundWithTryCatch(holder, runMethodBody, previousMethodBody, "A runtime exception was thrown while executing code in a background task");
- delegatingMethod.body().add(executeCall);
+ Background annotation = element.getAnnotation(Background.class);
+ String id = annotation.id();
+ int delay = annotation.delay();
+ String serial = annotation.serial();
- }
+ JClass backgroundExecutorClass = holder.refClass(BackgroundExecutor.class);
+ JInvocation newTask = _new(anonymousTaskClass).arg(lit(id)).arg(lit(delay)).arg(lit(serial));
+ JInvocation executeCall = backgroundExecutorClass.staticInvoke("execute").arg(newTask);
+ delegatingMethod.body().add(executeCall);
}
}
diff --git a/AndroidAnnotations/functional-test-1-5-tests/src/test/java/org/androidannotations/test15/ThreadActivityTest.java b/AndroidAnnotations/functional-test-1-5-tests/src/test/java/org/androidannotations/test15/ThreadActivityTest.java
index 23c53fe4ce..00fda24974 100644
--- a/AndroidAnnotations/functional-test-1-5-tests/src/test/java/org/androidannotations/test15/ThreadActivityTest.java
+++ b/AndroidAnnotations/functional-test-1-5-tests/src/test/java/org/androidannotations/test15/ThreadActivityTest.java
@@ -19,18 +19,27 @@
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Random;
import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+import org.androidannotations.api.BackgroundExecutor;
+import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
-import org.androidannotations.api.BackgroundExecutor;
-
@RunWith(AndroidAnnotationsTestRunner.class)
public class ThreadActivityTest {
+ private static final int MAX_WAITING_TIME = 3000; /* milliseconds */
+
private ThreadActivity_ activity;
@Before
@@ -51,4 +60,207 @@ public void backgroundDelegatesToExecutor() {
verify(executor).execute(Mockito.