From 1b500aad5ff78b49056ddf9877327e6fb52c0409 Mon Sep 17 00:00:00 2001 From: WonderCsabo Date: Sat, 28 Mar 2015 11:58:44 +0100 Subject: [PATCH 01/10] Add id param to @UiThread --- .../annotations/UiThread.java | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) 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 ""; } From a880692d34149300eab911fb560570ff955ac77f Mon Sep 17 00:00:00 2001 From: WonderCsabo Date: Sat, 28 Mar 2015 11:59:10 +0100 Subject: [PATCH 02/10] Add UiThreadExecutor which handles cancellation --- .../api/UiThreadExecutor.java | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 AndroidAnnotations/androidannotations-api/src/main/java/org/androidannotations/api/UiThreadExecutor.java 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..5d9b37e0a9 --- /dev/null +++ b/AndroidAnnotations/androidannotations-api/src/main/java/org/androidannotations/api/UiThreadExecutor.java @@ -0,0 +1,84 @@ +/** + * 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 java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.WeakHashMap; + +import android.os.Handler; + +/** + * This class provide operations for + * {@link org.androidannotations.annotations.UiThread UiThread} tasks. + */ +public class UiThreadExecutor { + + private static final Map TASKS = new WeakHashMap(); + + 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 handler + * the {@link Handler} which runs the task + */ + public static synchronized void addTask(String id, Runnable task, Handler handler) { + TASKS.put(task, new TaskInfo(id, handler)); + } + + /** + * Cancel all tasks having the specified id. + * + * @param id + * the cancellation identifier + */ + public static synchronized void cancelAll(String id) { + Set> entrySet = TASKS.entrySet(); + Iterator> iterator = entrySet.iterator(); + + while (iterator.hasNext()) { + Entry next = iterator.next(); + Runnable task = next.getKey(); + TaskInfo info = next.getValue(); + + if (info.id.equals(id)) { + info.handler.removeCallbacks(task); + } + } + } + + private static class TaskInfo { + String id; + + Handler handler; + + TaskInfo(String id, Handler handler) { + this.id = id; + this.handler = handler; + } + } +} From f7ec0c54e9c3d72ada764dac3d92241762855c1b Mon Sep 17 00:00:00 2001 From: WonderCsabo Date: Sat, 28 Mar 2015 11:59:33 +0100 Subject: [PATCH 03/10] Handle @UiThread#id --- .../handler/UiThreadHandler.java | 29 +++++++++++++++++-- .../helper/ValidatorHelper.java | 10 +++++++ 2 files changed, 37 insertions(+), 2 deletions(-) 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..c0ea8236f5 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; @@ -34,12 +37,14 @@ import com.sun.codemodel.JExpression; import com.sun.codemodel.JMethod; import com.sun.codemodel.JOp; +import com.sun.codemodel.JVar; 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_ADD_TASK = "addTask"; private final APTCodeModelHelper codeModelHelper = new APTCodeModelHelper(); @@ -47,6 +52,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,15 +70,17 @@ public void process(Element element, EComponentHolder holder) throws Exception { long delay = annotation.delay(); UiThread.Propagation propagation = annotation.propagation(); + JExpression runnable = createAndStoreRunnable(delegatingMethod.body(), annotation.id(), anonymousRunnableClass, holder); + 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)); + delegatingMethod.body().invoke(holder.getHandler(), "post").arg(runnable); } else { - delegatingMethod.body().invoke(holder.getHandler(), "postDelayed").arg(_new(anonymousRunnableClass)).arg(lit(delay)); + delegatingMethod.body().invoke(holder.getHandler(), "postDelayed").arg(runnable).arg(lit(delay)); } } @@ -91,4 +105,15 @@ private void addUIThreadCheck(JMethod delegatingMethod, JBlock previousBody, ECo JBlock thenBlock = con._then().add(previousBody); thenBlock._return(); } + + private JExpression createAndStoreRunnable(JBlock body, String id, JDefinedClass anonymousRunnableClass, EComponentHolder holder) { + if ("".equals(id)) { + return _new(anonymousRunnableClass); + } + + JVar runnableVar = body.decl(refClass(Runnable.class), "runnable", _new(anonymousRunnableClass)); + body.add(refClass(UiThreadExecutor.class).staticInvoke(METHOD_ADD_TASK).arg(id).arg(runnableVar).arg(holder.getHandler())); + + return runnableVar; + } } 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"); + } + } + } From a02f0ec1d3b6ec196a372a2e09fc9c8a161b77e3 Mon Sep 17 00:00:00 2001 From: WonderCsabo Date: Sat, 28 Mar 2015 11:59:57 +0100 Subject: [PATCH 04/10] Test UiThread task cancellation --- .../test15/efragment/MyListFragment.java | 7 +++++++ .../test15/efragment/MyListFragmentTest.java | 11 +++++++++++ 2 files changed, 18 insertions(+) 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/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); From faaadbec506a2e08d93e1b67695dc4c9faf5aad0 Mon Sep 17 00:00:00 2001 From: Drozdov Artyom Date: Sat, 4 Apr 2015 13:40:45 +0300 Subject: [PATCH 05/10] refactoring: reimplemented ui task cancellation in the more straight way --- .../api/UiThreadExecutor.java | 46 +++++++++---------- .../handler/UiThreadHandler.java | 18 ++++++-- 2 files changed, 36 insertions(+), 28 deletions(-) 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 index 5d9b37e0a9..d0f5f8e157 100644 --- a/AndroidAnnotations/androidannotations-api/src/main/java/org/androidannotations/api/UiThreadExecutor.java +++ b/AndroidAnnotations/androidannotations-api/src/main/java/org/androidannotations/api/UiThreadExecutor.java @@ -15,11 +15,8 @@ */ package org.androidannotations.api; -import java.util.Iterator; -import java.util.Map; +import java.util.*; import java.util.Map.Entry; -import java.util.Set; -import java.util.WeakHashMap; import android.os.Handler; @@ -29,7 +26,8 @@ */ public class UiThreadExecutor { - private static final Map TASKS = new WeakHashMap(); + private static final Map> TASKS = new HashMap>(); + private static final Map HANDLERS = new HashMap(); private UiThreadExecutor() { // should not be instantiated @@ -47,7 +45,13 @@ private UiThreadExecutor() { * the {@link Handler} which runs the task */ public static synchronized void addTask(String id, Runnable task, Handler handler) { - TASKS.put(task, new TaskInfo(id, handler)); + List runnables = TASKS.get(id); + if (runnables == null) { + runnables = new ArrayList(); + TASKS.put(id, runnables); + } + runnables.add(task); + HANDLERS.put(task, handler); } /** @@ -57,28 +61,22 @@ public static synchronized void addTask(String id, Runnable task, Handler handle * the cancellation identifier */ public static synchronized void cancelAll(String id) { - Set> entrySet = TASKS.entrySet(); - Iterator> iterator = entrySet.iterator(); - - while (iterator.hasNext()) { - Entry next = iterator.next(); - Runnable task = next.getKey(); - TaskInfo info = next.getValue(); - - if (info.id.equals(id)) { - info.handler.removeCallbacks(task); + List runnables = TASKS.remove(id); + if (runnables != null) { + for (Runnable runnable : runnables) { + HANDLERS.remove(runnable).removeCallbacks(runnable); } } } - private static class TaskInfo { - String id; - - Handler handler; - - TaskInfo(String id, Handler handler) { - this.id = id; - this.handler = handler; + public static synchronized void done(String id, Runnable runnable) { + Handler handler = HANDLERS.remove(runnable); + if (handler != null) { + List runnables = TASKS.get(id); + runnables.remove(runnable); + //potentially empty array stays in map for reducing garbage collecting - + //it is highly possible, that array will be reused at the next addTask call } } + } 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 c0ea8236f5..9c048e38e5 100644 --- a/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/handler/UiThreadHandler.java +++ b/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/handler/UiThreadHandler.java @@ -16,6 +16,7 @@ package org.androidannotations.handler; import static com.sun.codemodel.JExpr._new; +import static com.sun.codemodel.JExpr._this; import static com.sun.codemodel.JExpr.lit; import javax.annotation.processing.ProcessingEnvironment; @@ -45,6 +46,7 @@ public class UiThreadHandler extends AbstractRunnableHandler { private static final String METHOD_MAIN_LOOPER = "getMainLooper"; private static final String METHOD_GET_THREAD = "getThread"; private static final String METHOD_ADD_TASK = "addTask"; + private static final String METHOD_DONE = "done"; private final APTCodeModelHelper codeModelHelper = new APTCodeModelHelper(); @@ -70,7 +72,14 @@ public void process(Element element, EComponentHolder holder) throws Exception { long delay = annotation.delay(); UiThread.Propagation propagation = annotation.propagation(); - JExpression runnable = createAndStoreRunnable(delegatingMethod.body(), annotation.id(), anonymousRunnableClass, holder); + JExpression runnable; + String id = annotation.id(); + if ("".equals(id)) { + runnable = _new(anonymousRunnableClass); + } else { + runnable = createAndStoreRunnable(delegatingMethod.body(), id, anonymousRunnableClass, holder); + callCancelOnFinish(previousBody, id); + } if (delay == 0) { if (propagation == UiThread.Propagation.REUSE) { @@ -107,13 +116,14 @@ private void addUIThreadCheck(JMethod delegatingMethod, JBlock previousBody, ECo } private JExpression createAndStoreRunnable(JBlock body, String id, JDefinedClass anonymousRunnableClass, EComponentHolder holder) { - if ("".equals(id)) { - return _new(anonymousRunnableClass); - } JVar runnableVar = body.decl(refClass(Runnable.class), "runnable", _new(anonymousRunnableClass)); body.add(refClass(UiThreadExecutor.class).staticInvoke(METHOD_ADD_TASK).arg(id).arg(runnableVar).arg(holder.getHandler())); return runnableVar; } + + private void callCancelOnFinish(JBlock body, String id) { + body.add(refClass(UiThreadExecutor.class).staticInvoke(METHOD_DONE).arg(id).arg(_this())); + } } From 25fa9af78818459ca289145503ae702f78726380 Mon Sep 17 00:00:00 2001 From: Drozdov Artyom Date: Mon, 13 Apr 2015 10:13:03 +0300 Subject: [PATCH 06/10] add: lock-free UIThreadHandler implementation --- .../api/UiThreadExecutor.java | 113 ++++++++++++++---- .../handler/UiThreadHandler.java | 33 ++--- .../holder/EComponentHolder.java | 19 --- .../api/UiThreadExecutorTest.java | 63 ++++++++++ .../test15/ThreadActivityTest.java | 30 ----- 5 files changed, 163 insertions(+), 95 deletions(-) create mode 100644 AndroidAnnotations/functional-test-1-5/src/test/java/org/androidannotations/api/UiThreadExecutorTest.java 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 index d0f5f8e157..72804d757d 100644 --- a/AndroidAnnotations/androidannotations-api/src/main/java/org/androidannotations/api/UiThreadExecutor.java +++ b/AndroidAnnotations/androidannotations-api/src/main/java/org/androidannotations/api/UiThreadExecutor.java @@ -15,10 +15,14 @@ */ package org.androidannotations.api; -import java.util.*; -import java.util.Map.Entry; - import android.os.Handler; +import android.os.Looper; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicInteger; /** * This class provide operations for @@ -26,8 +30,15 @@ */ public class UiThreadExecutor { - private static final Map> TASKS = new HashMap>(); - private static final Map HANDLERS = new HashMap(); + private static final Handler HANDLER = new Handler(Looper.getMainLooper()); + + static final ConcurrentHashMap> TASKS = new ConcurrentHashMap>(); + + private static final Set JUST_POSTED_TASKS = Collections.newSetFromMap(new ConcurrentHashMap()); + + private static final ConcurrentLinkedQueue> ALLOCATION_STASH = new ConcurrentLinkedQueue>(); + private static final AtomicInteger ALLOCATION_STASH_SIZE = new AtomicInteger(0); + private static final int ALLOCATION_STASH_MAX_SIZE = 16; private UiThreadExecutor() { // should not be instantiated @@ -41,17 +52,64 @@ private UiThreadExecutor() { * the identifier of the task * @param task * the task itself - * @param handler - * the {@link Handler} which runs the task + * @param delay + * the delay or zero to run immediately */ - public static synchronized void addTask(String id, Runnable task, Handler handler) { - List runnables = TASKS.get(id); + public static void runTask(String id, Runnable task, long delay) { + if ("".equals(id)) { + HANDLER.postDelayed(task, delay); + return; + } + JUST_POSTED_TASKS.add(task); + HANDLER.postDelayed(task, delay); + + Set runnables = TASKS.get(id); if (runnables == null) { - runnables = new ArrayList(); - TASKS.put(id, runnables); + runnables = allocateRunnables(); + runnables.add(task); + + while (true) { + Set oldRunnables = TASKS.putIfAbsent(id, runnables); + if (oldRunnables != null) { + if (oldRunnables.isEmpty()) { + //it is about to be removed - lets try to replace it + if (!TASKS.replace(id, oldRunnables, runnables)) { + //it is already has bean replaced by different thread - lets try again + continue; + } + } else { + recycle(runnables); + oldRunnables.add(task); + } + } + break; + } + } else { + runnables.add(task); + } + + //if the task has already been completed - make sure to clean up + if (!JUST_POSTED_TASKS.remove(task)) { + done(id, task); + } + } + + private static Set allocateRunnables() { + Set runnables = ALLOCATION_STASH.poll(); + if (runnables != null) { + ALLOCATION_STASH_SIZE.getAndDecrement(); + return runnables; + } + return Collections.newSetFromMap(new ConcurrentHashMap()); + } + + private static void recycle(Set runnables) { + if (ALLOCATION_STASH_SIZE.getAndIncrement() < ALLOCATION_STASH_MAX_SIZE) { + runnables.clear(); + ALLOCATION_STASH.offer(runnables); + } else { + ALLOCATION_STASH_SIZE.getAndDecrement(); } - runnables.add(task); - HANDLERS.put(task, handler); } /** @@ -60,22 +118,31 @@ public static synchronized void addTask(String id, Runnable task, Handler handle * @param id * the cancellation identifier */ - public static synchronized void cancelAll(String id) { - List runnables = TASKS.remove(id); + public static void cancelAll(String id) { + Set runnables = TASKS.remove(id); if (runnables != null) { for (Runnable runnable : runnables) { - HANDLERS.remove(runnable).removeCallbacks(runnable); + HANDLER.removeCallbacks(runnable); } + recycle(runnables); } } - public static synchronized void done(String id, Runnable runnable) { - Handler handler = HANDLERS.remove(runnable); - if (handler != null) { - List runnables = TASKS.get(id); - runnables.remove(runnable); - //potentially empty array stays in map for reducing garbage collecting - - //it is highly possible, that array will be reused at the next addTask call + /** + * Should be called after the task has been executed. It is ok to call it more then once for the case of cleaning up. + * @param id the task id + * @param runnable the task itself + */ + public static void done(String id, Runnable runnable) { + JUST_POSTED_TASKS.remove(runnable); + Set runnables = TASKS.get(id); + if (runnables != null) { + if (runnables.remove(runnable)) { + if (runnables.isEmpty()) { //if it is empty + TASKS.remove(id, runnables); + recycle(runnables); + } + } } } 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 9c048e38e5..aab563098f 100644 --- a/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/handler/UiThreadHandler.java +++ b/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/handler/UiThreadHandler.java @@ -45,7 +45,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_ADD_TASK = "addTask"; + private static final String METHOD_RUN_TASK = "runTask"; private static final String METHOD_DONE = "done"; private final APTCodeModelHelper codeModelHelper = new APTCodeModelHelper(); @@ -77,20 +77,18 @@ public void process(Element element, EComponentHolder holder) throws Exception { if ("".equals(id)) { runnable = _new(anonymousRunnableClass); } else { - runnable = createAndStoreRunnable(delegatingMethod.body(), id, anonymousRunnableClass, holder); - callCancelOnFinish(previousBody, id); + runnable = delegatingMethod.body().decl(refClass(Runnable.class), "runnable", _new(anonymousRunnableClass)); + previousBody.add(refClass(UiThreadExecutor.class).staticInvoke(METHOD_DONE).arg(id).arg(_this())); } - 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(runnable); - } else { - delegatingMethod.body().invoke(holder.getHandler(), "postDelayed").arg(runnable).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(id) + .arg(runnable) + .arg(lit(delay))); } /** @@ -115,15 +113,4 @@ private void addUIThreadCheck(JMethod delegatingMethod, JBlock previousBody, ECo thenBlock._return(); } - private JExpression createAndStoreRunnable(JBlock body, String id, JDefinedClass anonymousRunnableClass, EComponentHolder holder) { - - JVar runnableVar = body.decl(refClass(Runnable.class), "runnable", _new(anonymousRunnableClass)); - body.add(refClass(UiThreadExecutor.class).staticInvoke(METHOD_ADD_TASK).arg(id).arg(runnableVar).arg(holder.getHandler())); - - return runnableVar; - } - - private void callCancelOnFinish(JBlock body, String id) { - body.add(refClass(UiThreadExecutor.class).staticInvoke(METHOD_DONE).arg(id).arg(_this())); - } } 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/test/java/org/androidannotations/api/UiThreadExecutorTest.java b/AndroidAnnotations/functional-test-1-5/src/test/java/org/androidannotations/api/UiThreadExecutorTest.java new file mode 100644 index 0000000000..d574350452 --- /dev/null +++ b/AndroidAnnotations/functional-test-1-5/src/test/java/org/androidannotations/api/UiThreadExecutorTest.java @@ -0,0 +1,63 @@ +package org.androidannotations.api; + +import org.androidannotations.api.UiThreadExecutor; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.*; + +@RunWith(RobolectricTestRunner.class) +public class UiThreadExecutorTest { + + @Test + public void oneTaskTest() throws Exception { + final CountDownLatch latch = new CountDownLatch(1); + UiThreadExecutor.runTask("test", new Runnable() { + @Override + public void run() { + UiThreadExecutor.done("test", this); + latch.countDown(); + } + }, 0); + await(latch); + assertTrue("The task is leaked", UiThreadExecutor.TASKS.isEmpty()); + } + + @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); + assertFalse("There is no tasks in map", UiThreadExecutor.TASKS.isEmpty()); + UiThreadExecutor.done("test", this); + taskFinishedLatch.countDown(); + } + }, 0); + taskStartedLatch.countDown(); + await(taskFinishedLatch); + assertTrue("The task is leaked", UiThreadExecutor.TASKS.isEmpty()); + } + }.start(); + } + + 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/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 */ From f0478d7bd67c86aa7b65db3c2d729b07601db416 Mon Sep 17 00:00:00 2001 From: Drozdov Artyom Date: Sun, 19 Apr 2015 22:08:55 +0300 Subject: [PATCH 07/10] simple and effective implementation of cancelling ui tasks --- .../api/UiThreadExecutor.java | 103 ++++-------------- .../handler/UiThreadHandler.java | 16 +-- .../api/UiThreadExecutorTest.java | 39 +++++-- 3 files changed, 51 insertions(+), 107 deletions(-) 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 index 72804d757d..bd3b18f21f 100644 --- a/AndroidAnnotations/androidannotations-api/src/main/java/org/androidannotations/api/UiThreadExecutor.java +++ b/AndroidAnnotations/androidannotations-api/src/main/java/org/androidannotations/api/UiThreadExecutor.java @@ -17,12 +17,9 @@ import android.os.Handler; import android.os.Looper; +import android.os.SystemClock; -import java.util.Collections; -import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.atomic.AtomicInteger; /** * This class provide operations for @@ -30,15 +27,9 @@ */ public class UiThreadExecutor { - private static final Handler HANDLER = new Handler(Looper.getMainLooper()); + static final Handler HANDLER = new Handler(Looper.getMainLooper()); - static final ConcurrentHashMap> TASKS = new ConcurrentHashMap>(); - - private static final Set JUST_POSTED_TASKS = Collections.newSetFromMap(new ConcurrentHashMap()); - - private static final ConcurrentLinkedQueue> ALLOCATION_STASH = new ConcurrentLinkedQueue>(); - private static final AtomicInteger ALLOCATION_STASH_SIZE = new AtomicInteger(0); - private static final int ALLOCATION_STASH_MAX_SIZE = 16; + static final ConcurrentHashMap TOKENS = new ConcurrentHashMap(); private UiThreadExecutor() { // should not be instantiated @@ -60,56 +51,21 @@ public static void runTask(String id, Runnable task, long delay) { HANDLER.postDelayed(task, delay); return; } - JUST_POSTED_TASKS.add(task); - HANDLER.postDelayed(task, delay); - - Set runnables = TASKS.get(id); - if (runnables == null) { - runnables = allocateRunnables(); - runnables.add(task); - - while (true) { - Set oldRunnables = TASKS.putIfAbsent(id, runnables); - if (oldRunnables != null) { - if (oldRunnables.isEmpty()) { - //it is about to be removed - lets try to replace it - if (!TASKS.replace(id, oldRunnables, runnables)) { - //it is already has bean replaced by different thread - lets try again - continue; - } - } else { - recycle(runnables); - oldRunnables.add(task); - } - } - break; - } - } else { - runnables.add(task); - } - - //if the task has already been completed - make sure to clean up - if (!JUST_POSTED_TASKS.remove(task)) { - done(id, task); - } - } - - private static Set allocateRunnables() { - Set runnables = ALLOCATION_STASH.poll(); - if (runnables != null) { - ALLOCATION_STASH_SIZE.getAndDecrement(); - return runnables; - } - return Collections.newSetFromMap(new ConcurrentHashMap()); + long time = SystemClock.uptimeMillis() + delay; + HANDLER.postAtTime(task, getToken(id), time); } - private static void recycle(Set runnables) { - if (ALLOCATION_STASH_SIZE.getAndIncrement() < ALLOCATION_STASH_MAX_SIZE) { - runnables.clear(); - ALLOCATION_STASH.offer(runnables); - } else { - ALLOCATION_STASH_SIZE.getAndDecrement(); + private static Object getToken(String id) { + Object token = TOKENS.get(id); + if (token == null) { + token = new Object(); + Object oldObject = TOKENS.putIfAbsent(id, token); + //if a concurrent thread was faster + if (oldObject != null) { + token = oldObject; + } } + return token; } /** @@ -119,31 +75,12 @@ private static void recycle(Set runnables) { * the cancellation identifier */ public static void cancelAll(String id) { - Set runnables = TASKS.remove(id); - if (runnables != null) { - for (Runnable runnable : runnables) { - HANDLER.removeCallbacks(runnable); - } - recycle(runnables); - } - } - - /** - * Should be called after the task has been executed. It is ok to call it more then once for the case of cleaning up. - * @param id the task id - * @param runnable the task itself - */ - public static void done(String id, Runnable runnable) { - JUST_POSTED_TASKS.remove(runnable); - Set runnables = TASKS.get(id); - if (runnables != null) { - if (runnables.remove(runnable)) { - if (runnables.isEmpty()) { //if it is empty - TASKS.remove(id, runnables); - recycle(runnables); - } - } + Object token = TOKENS.get(id); + if (token == null) { + //nothing to cancel + return; } + HANDLER.removeCallbacksAndMessages(token); } } 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 aab563098f..ea3a994817 100644 --- a/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/handler/UiThreadHandler.java +++ b/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/handler/UiThreadHandler.java @@ -16,7 +16,6 @@ package org.androidannotations.handler; import static com.sun.codemodel.JExpr._new; -import static com.sun.codemodel.JExpr._this; import static com.sun.codemodel.JExpr.lit; import javax.annotation.processing.ProcessingEnvironment; @@ -38,7 +37,6 @@ import com.sun.codemodel.JExpression; import com.sun.codemodel.JMethod; import com.sun.codemodel.JOp; -import com.sun.codemodel.JVar; public class UiThreadHandler extends AbstractRunnableHandler { @@ -46,7 +44,6 @@ public class UiThreadHandler extends AbstractRunnableHandler { 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 static final String METHOD_DONE = "done"; private final APTCodeModelHelper codeModelHelper = new APTCodeModelHelper(); @@ -72,22 +69,13 @@ public void process(Element element, EComponentHolder holder) throws Exception { long delay = annotation.delay(); UiThread.Propagation propagation = annotation.propagation(); - JExpression runnable; - String id = annotation.id(); - if ("".equals(id)) { - runnable = _new(anonymousRunnableClass); - } else { - runnable = delegatingMethod.body().decl(refClass(Runnable.class), "runnable", _new(anonymousRunnableClass)); - previousBody.add(refClass(UiThreadExecutor.class).staticInvoke(METHOD_DONE).arg(id).arg(_this())); - } - 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(id) - .arg(runnable) + .arg(annotation.id()) + .arg(_new(anonymousRunnableClass)) .arg(lit(delay))); } diff --git a/AndroidAnnotations/functional-test-1-5/src/test/java/org/androidannotations/api/UiThreadExecutorTest.java b/AndroidAnnotations/functional-test-1-5/src/test/java/org/androidannotations/api/UiThreadExecutorTest.java index d574350452..bfc639357a 100644 --- a/AndroidAnnotations/functional-test-1-5/src/test/java/org/androidannotations/api/UiThreadExecutorTest.java +++ b/AndroidAnnotations/functional-test-1-5/src/test/java/org/androidannotations/api/UiThreadExecutorTest.java @@ -1,30 +1,51 @@ package org.androidannotations.api; -import org.androidannotations.api.UiThreadExecutor; +import android.os.Handler; +import android.os.Message; +import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; +import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; +import org.robolectric.shadows.ShadowHandler; 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 { + //remove this after upgrading robolectric to 3+ + @BeforeClass + public static void hackOldRobolectric() { + final Handler handler = UiThreadExecutor.HANDLER; + 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; + } + }); + } + @Test public void oneTaskTest() throws Exception { - final CountDownLatch latch = new CountDownLatch(1); + final AtomicBoolean done = new AtomicBoolean(false); UiThreadExecutor.runTask("test", new Runnable() { @Override public void run() { - UiThreadExecutor.done("test", this); - latch.countDown(); + done.set(true); } }, 0); - await(latch); - assertTrue("The task is leaked", UiThreadExecutor.TASKS.isEmpty()); + Robolectric.runUiThreadTasksIncludingDelayedTasks(); + assertTrue("Task is still under execution", done.get()); } @Test @@ -38,16 +59,14 @@ public void run() { @Override public void run() { await(taskStartedLatch); - assertFalse("There is no tasks in map", UiThreadExecutor.TASKS.isEmpty()); - UiThreadExecutor.done("test", this); taskFinishedLatch.countDown(); } }, 0); taskStartedLatch.countDown(); - await(taskFinishedLatch); - assertTrue("The task is leaked", UiThreadExecutor.TASKS.isEmpty()); + Robolectric.runUiThreadTasksIncludingDelayedTasks(); } }.start(); + await(taskFinishedLatch); } private void await(CountDownLatch latch) { From 9aa958081cb61767154cdc5f77fb1f5be0455f61 Mon Sep 17 00:00:00 2001 From: Drozdov Artyom Date: Wed, 29 Apr 2015 00:58:05 +0300 Subject: [PATCH 08/10] add: cleaning up the map at cost of synchronization --- .../api/UiThreadExecutor.java | 62 ++++++++++++++----- .../api/UiThreadExecutorTest.java | 18 +++++- 2 files changed, 64 insertions(+), 16 deletions(-) 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 index bd3b18f21f..4d23bbe351 100644 --- a/AndroidAnnotations/androidannotations-api/src/main/java/org/androidannotations/api/UiThreadExecutor.java +++ b/AndroidAnnotations/androidannotations-api/src/main/java/org/androidannotations/api/UiThreadExecutor.java @@ -17,9 +17,11 @@ import android.os.Handler; import android.os.Looper; +import android.os.Message; import android.os.SystemClock; -import java.util.concurrent.ConcurrentHashMap; +import java.util.HashMap; +import java.util.Map; /** * This class provide operations for @@ -27,9 +29,20 @@ */ public class UiThreadExecutor { - static final Handler HANDLER = new Handler(Looper.getMainLooper()); + 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); + } + } + }; - static final ConcurrentHashMap TOKENS = new ConcurrentHashMap(); + static final Map TOKENS = new HashMap(); private UiThreadExecutor() { // should not be instantiated @@ -52,20 +65,32 @@ public static void runTask(String id, Runnable task, long delay) { return; } long time = SystemClock.uptimeMillis() + delay; - HANDLER.postAtTime(task, getToken(id), time); + HANDLER.postAtTime(task, nextToken(id), time); } - private static Object getToken(String id) { - Object token = TOKENS.get(id); - if (token == null) { - token = new Object(); - Object oldObject = TOKENS.putIfAbsent(id, token); - //if a concurrent thread was faster - if (oldObject != null) { - token = oldObject; + 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); + } } } - return token; } /** @@ -75,7 +100,7 @@ private static Object getToken(String id) { * the cancellation identifier */ public static void cancelAll(String id) { - Object token = TOKENS.get(id); + Token token = TOKENS.remove(id); if (token == null) { //nothing to cancel return; @@ -83,4 +108,13 @@ public static void cancelAll(String id) { HANDLER.removeCallbacksAndMessages(token); } + private static class Token { + int runnablesCount = 0; + final String id; + + private Token(String id) { + this.id = id; + } + } + } diff --git a/AndroidAnnotations/functional-test-1-5/src/test/java/org/androidannotations/api/UiThreadExecutorTest.java b/AndroidAnnotations/functional-test-1-5/src/test/java/org/androidannotations/api/UiThreadExecutorTest.java index bfc639357a..4c1001f9df 100644 --- a/AndroidAnnotations/functional-test-1-5/src/test/java/org/androidannotations/api/UiThreadExecutorTest.java +++ b/AndroidAnnotations/functional-test-1-5/src/test/java/org/androidannotations/api/UiThreadExecutorTest.java @@ -43,11 +43,25 @@ public void oneTaskTest() throws Exception { public void run() { done.set(true); } - }, 0); + }, 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); @@ -61,7 +75,7 @@ public void run() { await(taskStartedLatch); taskFinishedLatch.countDown(); } - }, 0); + }, 10); taskStartedLatch.countDown(); Robolectric.runUiThreadTasksIncludingDelayedTasks(); } From bbe8c027358be95f6a6d331ee911ddafcdf7ab3b Mon Sep 17 00:00:00 2001 From: Drozdov Artyom Date: Wed, 29 Apr 2015 10:21:09 +0300 Subject: [PATCH 09/10] fix: synchronization for removing tokens --- .../java/org/androidannotations/api/UiThreadExecutor.java | 5 ++++- .../org/androidannotations/api/UiThreadExecutorTest.java | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) 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 index 4d23bbe351..b2d3b4f935 100644 --- a/AndroidAnnotations/androidannotations-api/src/main/java/org/androidannotations/api/UiThreadExecutor.java +++ b/AndroidAnnotations/androidannotations-api/src/main/java/org/androidannotations/api/UiThreadExecutor.java @@ -100,7 +100,10 @@ private static void decrementToken(Token token) { * the cancellation identifier */ public static void cancelAll(String id) { - Token token = TOKENS.remove(id); + Token token; + synchronized (TOKENS) { + token = TOKENS.remove(id); + } if (token == null) { //nothing to cancel return; diff --git a/AndroidAnnotations/functional-test-1-5/src/test/java/org/androidannotations/api/UiThreadExecutorTest.java b/AndroidAnnotations/functional-test-1-5/src/test/java/org/androidannotations/api/UiThreadExecutorTest.java index 4c1001f9df..ab9b93ea13 100644 --- a/AndroidAnnotations/functional-test-1-5/src/test/java/org/androidannotations/api/UiThreadExecutorTest.java +++ b/AndroidAnnotations/functional-test-1-5/src/test/java/org/androidannotations/api/UiThreadExecutorTest.java @@ -18,7 +18,7 @@ @RunWith(RobolectricTestRunner.class) public class UiThreadExecutorTest { - //remove this after upgrading robolectric to 3+ + //TODO remove this after upgrading robolectric to 3+ @BeforeClass public static void hackOldRobolectric() { final Handler handler = UiThreadExecutor.HANDLER; From 244bb93d0b1ca6b173d15ac31bc4d54ce1de255e Mon Sep 17 00:00:00 2001 From: Drozdov Artyom Date: Mon, 4 May 2015 23:51:45 +0300 Subject: [PATCH 10/10] refactoring: tests moved to test15 package and UI handlers hacks moved into TestSampleRoboApplication_ --- .../api/UiThreadExecutor.java | 4 +-- .../{api => test15}/UiThreadExecutorTest.java | 24 ++----------- .../roboguice/TestSampleRoboApplication_.java | 34 +++++++++++++++++++ 3 files changed, 38 insertions(+), 24 deletions(-) rename AndroidAnnotations/functional-test-1-5/src/test/java/org/androidannotations/{api => test15}/UiThreadExecutorTest.java (69%) 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 index b2d3b4f935..1d71139904 100644 --- a/AndroidAnnotations/androidannotations-api/src/main/java/org/androidannotations/api/UiThreadExecutor.java +++ b/AndroidAnnotations/androidannotations-api/src/main/java/org/androidannotations/api/UiThreadExecutor.java @@ -29,7 +29,7 @@ */ public class UiThreadExecutor { - static final Handler HANDLER = new Handler(Looper.getMainLooper()) { + private static final Handler HANDLER = new Handler(Looper.getMainLooper()) { @Override public void handleMessage(Message msg) { Runnable callback = msg.getCallback(); @@ -42,7 +42,7 @@ public void handleMessage(Message msg) { } }; - static final Map TOKENS = new HashMap(); + private static final Map TOKENS = new HashMap(); private UiThreadExecutor() { // should not be instantiated diff --git a/AndroidAnnotations/functional-test-1-5/src/test/java/org/androidannotations/api/UiThreadExecutorTest.java b/AndroidAnnotations/functional-test-1-5/src/test/java/org/androidannotations/test15/UiThreadExecutorTest.java similarity index 69% rename from AndroidAnnotations/functional-test-1-5/src/test/java/org/androidannotations/api/UiThreadExecutorTest.java rename to AndroidAnnotations/functional-test-1-5/src/test/java/org/androidannotations/test15/UiThreadExecutorTest.java index ab9b93ea13..361b16fda2 100644 --- a/AndroidAnnotations/functional-test-1-5/src/test/java/org/androidannotations/api/UiThreadExecutorTest.java +++ b/AndroidAnnotations/functional-test-1-5/src/test/java/org/androidannotations/test15/UiThreadExecutorTest.java @@ -1,13 +1,10 @@ -package org.androidannotations.api; +package org.androidannotations.test15; -import android.os.Handler; -import android.os.Message; -import org.junit.BeforeClass; +import org.androidannotations.api.UiThreadExecutor; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; -import org.robolectric.shadows.ShadowHandler; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -18,23 +15,6 @@ @RunWith(RobolectricTestRunner.class) public class UiThreadExecutorTest { - //TODO remove this after upgrading robolectric to 3+ - @BeforeClass - public static void hackOldRobolectric() { - final Handler handler = UiThreadExecutor.HANDLER; - 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; - } - }); - } - @Test public void oneTaskTest() throws Exception { final AtomicBoolean done = new AtomicBoolean(false); 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