diff --git a/AndroidAnnotations/androidannotations-api/src/main/java/org/androidannotations/annotations/UiThread.java b/AndroidAnnotations/androidannotations-api/src/main/java/org/androidannotations/annotations/UiThread.java
index 8486fcd267..4025ac47b0 100644
--- a/AndroidAnnotations/androidannotations-api/src/main/java/org/androidannotations/annotations/UiThread.java
+++ b/AndroidAnnotations/androidannotations-api/src/main/java/org/androidannotations/annotations/UiThread.java
@@ -87,8 +87,39 @@
*
*
*
+ *
Cancellation
+ *
+ * You can cancel UiThread tasks if you provide an id (which cannot be an empty
+ * string) with the {@link #id()} parameter. Please note tasks which use
+ * REUSE {@link #propagation()} cannot have an id hence cannot be
+ * cancelled. To cancel all {@link UiThread} tasks with a given id, call
+ * {@link org.androidannotations.api.UiThreadExecutor#cancelAll(String)
+ * UiThreadExecutor#cancelAll(String)}.
+ *
+ *
+ * Example :
+ *
+ *
+ * @EBean
+ * public class MyBean {
+ *
+ * @UiThread(id = "myId")
+ * void uiThreadTask() {
+ * // do sg
+ * }
+ * }
+ *
+ * ...
+ *
+ * UiThreadExecutor.cancelAll("myId");
+ *
+ *
+ *
+ *
+ *
* @see Background
* @see android.os.Handler
+ * @see org.androidannotations.api.UiThreadExecutor#cancelAll(String)
*/
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
@@ -127,4 +158,17 @@ public enum Propagation {
*/
REUSE
}
+
+ /**
+ * Identifier for cancellation.
+ *
+ * To cancel all tasks having a specified id:
+ *
+ *
+ * UiThreadExecutor.cancelAll("my_background_id");
+ *
+ *
+ * @return the id for cancellation
+ */
+ String id() default "";
}
diff --git a/AndroidAnnotations/androidannotations-api/src/main/java/org/androidannotations/api/UiThreadExecutor.java b/AndroidAnnotations/androidannotations-api/src/main/java/org/androidannotations/api/UiThreadExecutor.java
new file mode 100644
index 0000000000..1d71139904
--- /dev/null
+++ b/AndroidAnnotations/androidannotations-api/src/main/java/org/androidannotations/api/UiThreadExecutor.java
@@ -0,0 +1,123 @@
+/**
+ * Copyright (C) 2010-2015 eBusiness Information, Excilys Group
+ *
+ * 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 org.androidannotations.api;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.SystemClock;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * This class provide operations for
+ * {@link org.androidannotations.annotations.UiThread UiThread} tasks.
+ */
+public class UiThreadExecutor {
+
+ private static final Handler HANDLER = new Handler(Looper.getMainLooper()) {
+ @Override
+ public void handleMessage(Message msg) {
+ Runnable callback = msg.getCallback();
+ if (callback != null) {
+ callback.run();
+ decrementToken((Token)msg.obj);
+ } else {
+ super.handleMessage(msg);
+ }
+ }
+ };
+
+ private static final Map TOKENS = new HashMap();
+
+ private UiThreadExecutor() {
+ // should not be instantiated
+ }
+
+ /**
+ * Store a new task in the map for providing cancellation. This method is
+ * used by AndroidAnnotations and not intended to be called by clients.
+ *
+ * @param id
+ * the identifier of the task
+ * @param task
+ * the task itself
+ * @param delay
+ * the delay or zero to run immediately
+ */
+ public static void runTask(String id, Runnable task, long delay) {
+ if ("".equals(id)) {
+ HANDLER.postDelayed(task, delay);
+ return;
+ }
+ long time = SystemClock.uptimeMillis() + delay;
+ HANDLER.postAtTime(task, nextToken(id), time);
+ }
+
+ private static Token nextToken(String id) {
+ synchronized (TOKENS) {
+ Token token = TOKENS.get(id);
+ if (token == null) {
+ token = new Token(id);
+ TOKENS.put(id, token);
+ }
+ token.runnablesCount++;
+ return token;
+ }
+ }
+
+ private static void decrementToken(Token token) {
+ synchronized (TOKENS) {
+ if (--token.runnablesCount == 0) {
+ String id = token.id;
+ Token old = TOKENS.remove(id);
+ if (old != token) {
+ //a runnable finished after cancelling, we just removed a wrong token, lets put it back
+ TOKENS.put(id, old);
+ }
+ }
+ }
+ }
+
+ /**
+ * Cancel all tasks having the specified id.
+ *
+ * @param id
+ * the cancellation identifier
+ */
+ public static void cancelAll(String id) {
+ Token token;
+ synchronized (TOKENS) {
+ token = TOKENS.remove(id);
+ }
+ if (token == null) {
+ //nothing to cancel
+ return;
+ }
+ HANDLER.removeCallbacksAndMessages(token);
+ }
+
+ private static class Token {
+ int runnablesCount = 0;
+ final String id;
+
+ private Token(String id) {
+ this.id = id;
+ }
+ }
+
+}
diff --git a/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/handler/UiThreadHandler.java b/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/handler/UiThreadHandler.java
index b9954e3120..ea3a994817 100644
--- a/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/handler/UiThreadHandler.java
+++ b/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/handler/UiThreadHandler.java
@@ -23,8 +23,11 @@
import javax.lang.model.element.ExecutableElement;
import org.androidannotations.annotations.UiThread;
+import org.androidannotations.api.UiThreadExecutor;
import org.androidannotations.helper.APTCodeModelHelper;
import org.androidannotations.holder.EComponentHolder;
+import org.androidannotations.model.AnnotationElements;
+import org.androidannotations.process.IsValid;
import com.sun.codemodel.JBlock;
import com.sun.codemodel.JClass;
@@ -40,6 +43,7 @@ public class UiThreadHandler extends AbstractRunnableHandler {
private static final String METHOD_CUR_THREAD = "currentThread";
private static final String METHOD_MAIN_LOOPER = "getMainLooper";
private static final String METHOD_GET_THREAD = "getThread";
+ private static final String METHOD_RUN_TASK = "runTask";
private final APTCodeModelHelper codeModelHelper = new APTCodeModelHelper();
@@ -47,6 +51,13 @@ public UiThreadHandler(ProcessingEnvironment processingEnvironment) {
super(UiThread.class, processingEnvironment);
}
+ @Override
+ public void validate(Element element, AnnotationElements validatedElements, IsValid valid) {
+ super.validate(element, validatedElements, valid);
+
+ validatorHelper.usesEnqueueIfHasId(element, valid);
+ }
+
@Override
public void process(Element element, EComponentHolder holder) throws Exception {
ExecutableElement executableElement = (ExecutableElement) element;
@@ -58,16 +69,14 @@ public void process(Element element, EComponentHolder holder) throws Exception {
long delay = annotation.delay();
UiThread.Propagation propagation = annotation.propagation();
- if (delay == 0) {
- if (propagation == UiThread.Propagation.REUSE) {
- // Put in the check for the UI thread.
- addUIThreadCheck(delegatingMethod, previousBody, holder);
- }
-
- delegatingMethod.body().invoke(holder.getHandler(), "post").arg(_new(anonymousRunnableClass));
- } else {
- delegatingMethod.body().invoke(holder.getHandler(), "postDelayed").arg(_new(anonymousRunnableClass)).arg(lit(delay));
+ if (delay == 0 && propagation == UiThread.Propagation.REUSE) {
+ // Put in the check for the UI thread.
+ addUIThreadCheck(delegatingMethod, previousBody, holder);
}
+ delegatingMethod.body().add(refClass(UiThreadExecutor.class).staticInvoke(METHOD_RUN_TASK)
+ .arg(annotation.id())
+ .arg(_new(anonymousRunnableClass))
+ .arg(lit(delay)));
}
/**
@@ -91,4 +100,5 @@ private void addUIThreadCheck(JMethod delegatingMethod, JBlock previousBody, ECo
JBlock thenBlock = con._then().add(previousBody);
thenBlock._return();
}
+
}
diff --git a/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/helper/ValidatorHelper.java b/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/helper/ValidatorHelper.java
index b71cd89c48..6dadeaaeba 100644
--- a/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/helper/ValidatorHelper.java
+++ b/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/helper/ValidatorHelper.java
@@ -66,6 +66,8 @@
import org.androidannotations.annotations.OnActivityResult;
import org.androidannotations.annotations.Receiver;
import org.androidannotations.annotations.Trace;
+import org.androidannotations.annotations.UiThread;
+import org.androidannotations.annotations.UiThread.Propagation;
import org.androidannotations.annotations.ViewById;
import org.androidannotations.annotations.WakeLock;
import org.androidannotations.annotations.WakeLock.Level;
@@ -1498,4 +1500,12 @@ public void hasSupportV4JarIfLocal(Element element, IsValid valid) {
}
}
+ public void usesEnqueueIfHasId(Element element, IsValid valid) {
+ UiThread annotation = element.getAnnotation(UiThread.class);
+
+ if (!"".equals(annotation.id()) && annotation.propagation() == Propagation.REUSE) {
+ annotationHelper.printAnnotationError(element, "An id only can be used with Propagation.ENQUEUE");
+ }
+ }
+
}
diff --git a/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/holder/EComponentHolder.java b/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/holder/EComponentHolder.java
index 3cba7521ee..b014c4456d 100644
--- a/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/holder/EComponentHolder.java
+++ b/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/holder/EComponentHolder.java
@@ -30,25 +30,19 @@
import com.sun.codemodel.JBlock;
import com.sun.codemodel.JClass;
-import com.sun.codemodel.JExpr;
import com.sun.codemodel.JExpression;
import com.sun.codemodel.JFieldRef;
import com.sun.codemodel.JFieldVar;
-import com.sun.codemodel.JInvocation;
import com.sun.codemodel.JMethod;
-import com.sun.codemodel.JMod;
import com.sun.codemodel.JVar;
public abstract class EComponentHolder extends BaseGeneratedClassHolder {
- private static final String METHOD_MAIN_LOOPER = "getMainLooper";
-
protected JExpression contextRef;
protected JMethod init;
private JVar resourcesRef;
private JFieldVar powerManagerRef;
private Map databaseHelperRefs = new HashMap();
- private JVar handler;
public EComponentHolder(ProcessHolder processHolder, TypeElement annotatedElement) throws Exception {
super(processHolder, annotatedElement);
@@ -124,17 +118,4 @@ protected JFieldVar setDatabaseHelperRef(TypeMirror databaseHelperTypeMirror) {
return databaseHelperRef;
}
- public JVar getHandler() {
- if (handler == null) {
- setHandler();
- }
- return handler;
- }
-
- private void setHandler() {
- JClass handlerClass = classes().HANDLER;
- JClass looperClass = classes().LOOPER;
- JInvocation arg = JExpr._new(handlerClass).arg(looperClass.staticInvoke(METHOD_MAIN_LOOPER));
- handler = generatedClass.field(JMod.PRIVATE, handlerClass, "handler_", arg);
- }
}
diff --git a/AndroidAnnotations/functional-test-1-5/src/main/java/org/androidannotations/test15/efragment/MyListFragment.java b/AndroidAnnotations/functional-test-1-5/src/main/java/org/androidannotations/test15/efragment/MyListFragment.java
index 3ebd5fe41c..2790b913e1 100644
--- a/AndroidAnnotations/functional-test-1-5/src/main/java/org/androidannotations/test15/efragment/MyListFragment.java
+++ b/AndroidAnnotations/functional-test-1-5/src/main/java/org/androidannotations/test15/efragment/MyListFragment.java
@@ -38,6 +38,8 @@ public class MyListFragment extends ListFragment {
boolean didExecute;
+ boolean uiThreadWithIdDidExecute;
+
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
@@ -61,6 +63,11 @@ void uiThreadIgnored() {
didExecute = true;
}
+ @UiThread(id = "id")
+ void uiThreadWithId() {
+ uiThreadWithIdDidExecute = true;
+ }
+
@Background
void backgroundThread() {
didExecute = true;
diff --git a/AndroidAnnotations/functional-test-1-5/src/test/java/org/androidannotations/test15/ThreadActivityTest.java b/AndroidAnnotations/functional-test-1-5/src/test/java/org/androidannotations/test15/ThreadActivityTest.java
index cbbc2f117e..ac8c0cd05e 100644
--- a/AndroidAnnotations/functional-test-1-5/src/test/java/org/androidannotations/test15/ThreadActivityTest.java
+++ b/AndroidAnnotations/functional-test-1-5/src/test/java/org/androidannotations/test15/ThreadActivityTest.java
@@ -341,36 +341,6 @@ public void execute(Runnable command) {
}
}
- @Test
- public void assertHandlerWithMainThread() throws Exception {
- /*
- * For this test we need to recreate the activity in a separate thread,
- * in order to check the handler is well associated to the main thread.
- */
- final ThreadActivity_[] threadActivityHolder = new ThreadActivity_[1];
-
- new Thread(new Runnable() {
- @Override
- public void run() {
- synchronized (threadActivityHolder) {
- threadActivityHolder[0] = Robolectric.buildActivity(ThreadActivity_.class).create().get();
- threadActivityHolder.notify();
- }
- }
- }).start();
- synchronized (threadActivityHolder) {
- do {
- threadActivityHolder.wait();
- } while (threadActivityHolder[0] == null);
- }
-
- Field handlerField = ThreadActivity_.class.getDeclaredField("handler_");
- handlerField.setAccessible(true);
-
- Handler handler = (Handler) handlerField.get(threadActivityHolder[0]);
- Assert.assertTrue("Handler field not associated to the main thread", handler.getLooper() == Looper.getMainLooper());
- }
-
@Test
public void propagateExceptionToGlobalExceptionHandler() {
/* set an executor with 4 threads */
diff --git a/AndroidAnnotations/functional-test-1-5/src/test/java/org/androidannotations/test15/UiThreadExecutorTest.java b/AndroidAnnotations/functional-test-1-5/src/test/java/org/androidannotations/test15/UiThreadExecutorTest.java
new file mode 100644
index 0000000000..361b16fda2
--- /dev/null
+++ b/AndroidAnnotations/functional-test-1-5/src/test/java/org/androidannotations/test15/UiThreadExecutorTest.java
@@ -0,0 +1,76 @@
+package org.androidannotations.test15;
+
+import org.androidannotations.api.UiThreadExecutor;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static org.junit.Assert.*;
+
+@RunWith(RobolectricTestRunner.class)
+public class UiThreadExecutorTest {
+
+ @Test
+ public void oneTaskTest() throws Exception {
+ final AtomicBoolean done = new AtomicBoolean(false);
+ UiThreadExecutor.runTask("test", new Runnable() {
+ @Override
+ public void run() {
+ done.set(true);
+ }
+ }, 10);
+ Robolectric.runUiThreadTasksIncludingDelayedTasks();
+ assertTrue("Task is still under execution", done.get());
+ }
+
+ @Test
+ public void oneTaskCancelTest() throws Exception {
+ final AtomicBoolean done = new AtomicBoolean(false);
+ UiThreadExecutor.runTask("test", new Runnable() {
+ @Override
+ public void run() {
+ done.set(true);
+ }
+ }, 10);
+ UiThreadExecutor.cancelAll("test");
+ Robolectric.runUiThreadTasksIncludingDelayedTasks();
+ assertFalse("Task is not cancelled", done.get());
+ }
+
+ @Test
+ public void oneTaskInThreadTest() throws Exception {
+ final CountDownLatch taskStartedLatch = new CountDownLatch(1);
+ final CountDownLatch taskFinishedLatch = new CountDownLatch(1);
+ new Thread() {
+ @Override
+ public void run() {
+ UiThreadExecutor.runTask("test", new Runnable() {
+ @Override
+ public void run() {
+ await(taskStartedLatch);
+ taskFinishedLatch.countDown();
+ }
+ }, 10);
+ taskStartedLatch.countDown();
+ Robolectric.runUiThreadTasksIncludingDelayedTasks();
+ }
+ }.start();
+ await(taskFinishedLatch);
+ }
+
+ private void await(CountDownLatch latch) {
+ try {
+ if (!latch.await(5, TimeUnit.SECONDS)) {
+ throw new IllegalStateException("Execution hanged up");
+ }
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+}
diff --git a/AndroidAnnotations/functional-test-1-5/src/test/java/org/androidannotations/test15/efragment/MyListFragmentTest.java b/AndroidAnnotations/functional-test-1-5/src/test/java/org/androidannotations/test15/efragment/MyListFragmentTest.java
index 195e196868..cf804ee0ac 100644
--- a/AndroidAnnotations/functional-test-1-5/src/test/java/org/androidannotations/test15/efragment/MyListFragmentTest.java
+++ b/AndroidAnnotations/functional-test-1-5/src/test/java/org/androidannotations/test15/efragment/MyListFragmentTest.java
@@ -22,12 +22,14 @@
import java.util.concurrent.Executor;
import org.androidannotations.api.BackgroundExecutor;
+import org.androidannotations.api.UiThreadExecutor;
import org.androidannotations.test15.R;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
+import org.robolectric.shadows.ShadowLooper;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
@@ -75,6 +77,15 @@ public void uithreadMethodIsCalled() {
assertTrue(myListFragment.didExecute);
}
+ @Test
+ public void uithreadMethodIsCanceled() {
+ ShadowLooper.pauseMainLooper();
+ myListFragment.uiThreadWithId();
+ UiThreadExecutor.cancelAll("id");
+ ShadowLooper.unPauseMainLooper();
+ assertFalse(myListFragment.uiThreadWithIdDidExecute);
+ }
+
@Test
public void backgroundMethodIsCalled() {
assertFalse(myListFragment.didExecute);
diff --git a/AndroidAnnotations/functional-test-1-5/src/test/java/org/androidannotations/test15/roboguice/TestSampleRoboApplication_.java b/AndroidAnnotations/functional-test-1-5/src/test/java/org/androidannotations/test15/roboguice/TestSampleRoboApplication_.java
index ef081655ff..14fa48a787 100644
--- a/AndroidAnnotations/functional-test-1-5/src/test/java/org/androidannotations/test15/roboguice/TestSampleRoboApplication_.java
+++ b/AndroidAnnotations/functional-test-1-5/src/test/java/org/androidannotations/test15/roboguice/TestSampleRoboApplication_.java
@@ -1,20 +1,54 @@
package org.androidannotations.test15.roboguice;
+import java.lang.reflect.Field;
import java.lang.reflect.Method;
+import android.os.Handler;
+import android.os.Message;
+import org.androidannotations.api.UiThreadExecutor;
import org.robolectric.Robolectric;
import org.robolectric.TestLifecycleApplication;
+import org.robolectric.shadows.ShadowHandler;
import roboguice.RoboGuice;
import android.app.Application;
// CHECKSTYLE:OFF
public class TestSampleRoboApplication_ extends Application implements TestLifecycleApplication {
+
@Override
public void onCreate() {
super.onCreate();
RoboGuice.overrideApplicationInjector(this, RoboGuice.newDefaultRoboModule(this), new RobolectricSampleModule());
+ hackHandler();
+
+
+ }
+
+ //TODO remove this after upgrading robolectric to 3+
+ private void hackHandler() {
+ final Handler handler;
+ try {
+ Field handlerField = UiThreadExecutor.class.getDeclaredField("HANDLER");
+ handlerField.setAccessible(true);
+ handler = (Handler) handlerField.get(null);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ } catch (NoSuchFieldException e) {
+ throw new RuntimeException(e);
+ }
+ ShadowHandler shadowHandler = Robolectric.shadowOf_(handler);
+ shadowHandler.__constructor__(new Handler.Callback() {
+ @Override
+ public boolean handleMessage(Message msg) {
+ //in the robolectric 2.4 there is a strange code - they call Handler's handleMessage (it do nothing)
+ //instead of dispatch, that actually do the job. This is just a dirty work-around. It should be removed
+ //with newer version of robolectric.
+ handler.dispatchMessage(msg);
+ return true;
+ }
+ });
}
@Override