Skip to content

Commit 930f599

Browse files
Drozdov ArtyomWonderCsabo
authored andcommitted
simple and effective implementation of cancelling ui tasks
1 parent 71414c9 commit 930f599

4 files changed

Lines changed: 56 additions & 119 deletions

File tree

‎AndroidAnnotations/androidannotations-api/src/main/java/org/androidannotations/api/UiThreadExecutor.java‎

Lines changed: 19 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,11 @@
1515
*/
1616
package org.androidannotations.api;
1717

18-
import java.util.Collections;
19-
import java.util.Set;
2018
import java.util.concurrent.ConcurrentHashMap;
21-
import java.util.concurrent.ConcurrentLinkedQueue;
22-
import java.util.concurrent.atomic.AtomicInteger;
2319

2420
import android.os.Handler;
2521
import android.os.Looper;
22+
import android.os.SystemClock;
2623

2724
/**
2825
* This class provide operations for
@@ -32,13 +29,7 @@ public class UiThreadExecutor {
3229

3330
private static final Handler HANDLER = new Handler(Looper.getMainLooper());
3431

35-
private static final ConcurrentHashMap<String, Set<Runnable>> TASKS = new ConcurrentHashMap<String, Set<Runnable>>();
36-
37-
private static final Set<Runnable> JUST_POSTED_TASKS = Collections.newSetFromMap(new ConcurrentHashMap<Runnable, Boolean>());
38-
39-
private static final ConcurrentLinkedQueue<Set<Runnable>> ALLOCATION_STASH = new ConcurrentLinkedQueue<Set<Runnable>>();
40-
private static final AtomicInteger ALLOCATION_STASH_SIZE = new AtomicInteger(0);
41-
private static final int ALLOCATION_STASH_MAX_SIZE = 16;
32+
private static final ConcurrentHashMap<String, Object> TOKENS = new ConcurrentHashMap<String, Object>();
4233

4334
private UiThreadExecutor() {
4435
// should not be instantiated
@@ -60,57 +51,21 @@ public static void runTask(String id, Runnable task, long delay) {
6051
HANDLER.postDelayed(task, delay);
6152
return;
6253
}
63-
JUST_POSTED_TASKS.add(task);
64-
HANDLER.postDelayed(task, delay);
65-
66-
Set<Runnable> runnables = TASKS.get(id);
67-
if (runnables == null) {
68-
runnables = allocateRunnables();
69-
runnables.add(task);
70-
71-
while (true) {
72-
Set<Runnable> oldRunnables = TASKS.putIfAbsent(id, runnables);
73-
if (oldRunnables != null) {
74-
if (oldRunnables.isEmpty()) {
75-
// it is about to be removed - lets try to replace it
76-
if (!TASKS.replace(id, oldRunnables, runnables)) {
77-
// it is already has bean replaced by different
78-
// thread - lets try again
79-
continue;
80-
}
81-
} else {
82-
recycle(runnables);
83-
oldRunnables.add(task);
84-
}
85-
}
86-
break;
87-
}
88-
} else {
89-
runnables.add(task);
90-
}
91-
92-
// if the task has already been completed - make sure to clean up
93-
if (!JUST_POSTED_TASKS.remove(task)) {
94-
done(id, task);
95-
}
96-
}
97-
98-
private static Set<Runnable> allocateRunnables() {
99-
Set<Runnable> runnables = ALLOCATION_STASH.poll();
100-
if (runnables != null) {
101-
ALLOCATION_STASH_SIZE.getAndDecrement();
102-
return runnables;
103-
}
104-
return Collections.newSetFromMap(new ConcurrentHashMap<Runnable, Boolean>());
54+
long time = SystemClock.uptimeMillis() + delay;
55+
HANDLER.postAtTime(task, getToken(id), time);
10556
}
10657

107-
private static void recycle(Set<Runnable> runnables) {
108-
if (ALLOCATION_STASH_SIZE.getAndIncrement() < ALLOCATION_STASH_MAX_SIZE) {
109-
runnables.clear();
110-
ALLOCATION_STASH.offer(runnables);
111-
} else {
112-
ALLOCATION_STASH_SIZE.getAndDecrement();
58+
private static Object getToken(String id) {
59+
Object token = TOKENS.get(id);
60+
if (token == null) {
61+
token = new Object();
62+
Object oldObject = TOKENS.putIfAbsent(id, token);
63+
// if a concurrent thread was faster
64+
if (oldObject != null) {
65+
token = oldObject;
66+
}
11367
}
68+
return token;
11469
}
11570

11671
/**
@@ -120,35 +75,12 @@ private static void recycle(Set<Runnable> runnables) {
12075
* the cancellation identifier
12176
*/
12277
public static void cancelAll(String id) {
123-
Set<Runnable> runnables = TASKS.remove(id);
124-
if (runnables != null) {
125-
for (Runnable runnable : runnables) {
126-
HANDLER.removeCallbacks(runnable);
127-
}
128-
recycle(runnables);
129-
}
130-
}
131-
132-
/**
133-
* Should be called after the task has been executed. It is ok to call it
134-
* more then once for the case of cleaning up.
135-
*
136-
* @param id
137-
* the task id
138-
* @param runnable
139-
* the task itself
140-
*/
141-
public static void done(String id, Runnable runnable) {
142-
JUST_POSTED_TASKS.remove(runnable);
143-
Set<Runnable> runnables = TASKS.get(id);
144-
if (runnables != null) {
145-
if (runnables.remove(runnable)) {
146-
if (runnables.isEmpty()) { // if it is empty
147-
TASKS.remove(id, runnables);
148-
recycle(runnables);
149-
}
150-
}
78+
Object token = TOKENS.get(id);
79+
if (token == null) {
80+
// nothing to cancel
81+
return;
15182
}
83+
HANDLER.removeCallbacksAndMessages(token);
15284
}
15385

15486
}

‎AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/handler/UiThreadHandler.java‎

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
package org.androidannotations.handler;
1717

1818
import static com.sun.codemodel.JExpr._new;
19-
import static com.sun.codemodel.JExpr._this;
2019
import static com.sun.codemodel.JExpr.lit;
2120

2221
import javax.annotation.processing.ProcessingEnvironment;
@@ -45,7 +44,6 @@ public class UiThreadHandler extends AbstractRunnableHandler {
4544
private static final String METHOD_MAIN_LOOPER = "getMainLooper";
4645
private static final String METHOD_GET_THREAD = "getThread";
4746
private static final String METHOD_RUN_TASK = "runTask";
48-
private static final String METHOD_DONE = "done";
4947

5048
private final APTCodeModelHelper codeModelHelper = new APTCodeModelHelper();
5149

@@ -71,22 +69,13 @@ public void process(Element element, EComponentHolder holder) throws Exception {
7169
long delay = annotation.delay();
7270
UiThread.Propagation propagation = annotation.propagation();
7371

74-
JExpression runnable;
75-
String id = annotation.id();
76-
if ("".equals(id)) {
77-
runnable = _new(anonymousRunnableClass);
78-
} else {
79-
runnable = delegatingMethod.body().decl(refClass(Runnable.class), "runnable", _new(anonymousRunnableClass));
80-
previousBody.add(refClass(UiThreadExecutor.class).staticInvoke(METHOD_DONE).arg(id).arg(_this()));
81-
}
82-
8372
if (delay == 0 && propagation == UiThread.Propagation.REUSE) {
8473
// Put in the check for the UI thread.
8574
addUIThreadCheck(delegatingMethod, previousBody, holder);
8675
}
8776
delegatingMethod.body().add(refClass(UiThreadExecutor.class).staticInvoke(METHOD_RUN_TASK) //
88-
.arg(id) //
89-
.arg(runnable) //
77+
.arg(annotation.id()) //
78+
.arg(_new(anonymousRunnableClass)) //
9079
.arg(lit(delay)));
9180
}
9281

‎AndroidAnnotations/functional-test-1-5/src/test/java/org/androidannotations/test15/UiThreadExecutorTest.java‎

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,36 +15,32 @@
1515
*/
1616
package org.androidannotations.test15;
1717

18-
import static org.fest.reflect.core.Reflection.staticField;
19-
import static org.junit.Assert.assertFalse;
2018
import static org.junit.Assert.assertTrue;
2119

22-
import java.util.Set;
23-
import java.util.concurrent.ConcurrentHashMap;
2420
import java.util.concurrent.CountDownLatch;
2521
import java.util.concurrent.TimeUnit;
22+
import java.util.concurrent.atomic.AtomicBoolean;
2623

2724
import org.androidannotations.api.UiThreadExecutor;
28-
import org.fest.reflect.reference.TypeRef;
2925
import org.junit.Test;
3026
import org.junit.runner.RunWith;
27+
import org.robolectric.Robolectric;
3128
import org.robolectric.RobolectricTestRunner;
3229

3330
@RunWith(RobolectricTestRunner.class)
3431
public class UiThreadExecutorTest {
3532

3633
@Test
3734
public void oneTaskTest() throws Exception {
38-
final CountDownLatch latch = new CountDownLatch(1);
35+
final AtomicBoolean done = new AtomicBoolean(false);
3936
UiThreadExecutor.runTask("test", new Runnable() {
4037
@Override
4138
public void run() {
42-
UiThreadExecutor.done("test", this);
43-
latch.countDown();
39+
done.set(true);
4440
}
4541
}, 0);
46-
await(latch);
47-
assertTrue("The task is leaked", getTasks().isEmpty());
42+
Robolectric.runUiThreadTasksIncludingDelayedTasks();
43+
assertTrue("Task is still under execution", done.get());
4844
}
4945

5046
@Test
@@ -58,16 +54,14 @@ public void run() {
5854
@Override
5955
public void run() {
6056
await(taskStartedLatch);
61-
assertFalse("There is no tasks in map", getTasks().isEmpty());
62-
UiThreadExecutor.done("test", this);
6357
taskFinishedLatch.countDown();
6458
}
6559
}, 0);
6660
taskStartedLatch.countDown();
67-
await(taskFinishedLatch);
68-
assertTrue("The task is leaked", getTasks().isEmpty());
61+
Robolectric.runUiThreadTasksIncludingDelayedTasks();
6962
}
7063
}.start();
64+
await(taskFinishedLatch);
7165
}
7266

7367
private void await(CountDownLatch latch) {
@@ -80,9 +74,4 @@ private void await(CountDownLatch latch) {
8074
}
8175
}
8276

83-
private ConcurrentHashMap<String, Set<Runnable>> getTasks() {
84-
return staticField("TASKS").ofType(new TypeRef<ConcurrentHashMap<String, Set<Runnable>>>() {
85-
}).in(UiThreadExecutor.class).get();
86-
}
87-
8877
}

‎AndroidAnnotations/functional-test-1-5/src/test/java/org/androidannotations/test15/roboguice/TestSampleRoboApplication_.java‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
11
package org.androidannotations.test15.roboguice;
22

3+
import static org.fest.reflect.core.Reflection.staticField;
4+
35
import java.lang.reflect.Method;
46

7+
import org.androidannotations.api.UiThreadExecutor;
58
import org.robolectric.Robolectric;
69
import org.robolectric.TestLifecycleApplication;
10+
import org.robolectric.shadows.ShadowHandler;
711

812
import roboguice.RoboGuice;
913
import android.app.Application;
14+
import android.os.Handler;
15+
import android.os.Message;
1016

1117
// CHECKSTYLE:OFF
1218
public class TestSampleRoboApplication_ extends Application implements TestLifecycleApplication {
19+
1320
@Override
1421
public void onCreate() {
1522
super.onCreate();
@@ -28,9 +35,29 @@ public void prepareTest(Object test) {
2835
RoboGuice.overrideApplicationInjector(application, RoboGuice.newDefaultRoboModule(application), new RobolectricSampleTestModule());
2936

3037
RoboGuice.getInjector(application).injectMembers(test);
38+
39+
hackHandler();
3140
}
3241

3342
@Override
3443
public void afterTest(Method method) {
3544
}
45+
46+
// TODO remove this after upgrading robolectric to 3+
47+
private void hackHandler() {
48+
final Handler handler = staticField("HANDLER").ofType(Handler.class).in(UiThreadExecutor.class).get();
49+
ShadowHandler shadowHandler = Robolectric.shadowOf_(handler);
50+
shadowHandler.__constructor__(new Handler.Callback() {
51+
@Override
52+
public boolean handleMessage(Message msg) {
53+
// in the robolectric 2.4 there is a strange code - they call
54+
// Handler's handleMessage (it do nothing)
55+
// instead of dispatch, that actually do the job. This is just a
56+
// dirty work-around. It should be removed
57+
// with newer version of robolectric.
58+
handler.dispatchMessage(msg);
59+
return true;
60+
}
61+
});
62+
}
3663
}

0 commit comments

Comments
 (0)