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