Skip to content

Commit 1a2b4d4

Browse files
brettchabotcopybara-androidxtest
authored andcommitted
Refactor Interrogator MessageQueue access to a TestLooperManagerCompat class.
An upcoming TestLooperManager change will add the ability for Espresso's Interrogator class to use it directly, replacing the use of reflection to access MessageQueue internals. This commit prepares for this change by refactoring MessageQueue access to a TestLooperManagerCompat container class. A future change will enhance TestLooperManagerCompat to use the public TestLooperManager API when the Android runtime environment supports it. PiperOrigin-RevId: 712968907
1 parent 49d644a commit 1a2b4d4

File tree

7 files changed

+197
-93
lines changed

7 files changed

+197
-93
lines changed

espresso/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ The following artifacts were released:
1717
**Bug Fixes**
1818

1919
* Fix deadlock in espresso in Robolectric INSTRUMENTATION_TEST + paused looper.
20+
* Refactor espresso's MessageQueue access into a TestLooperManagerCompat class
2021

2122
**New Features**
2223

espresso/core/java/androidx/test/espresso/base/BUILD

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ android_library(
2525
"IdlingResourceRegistry.java",
2626
"Interrogator.java",
2727
"LooperIdlingResourceInterrogationHandler.java",
28+
"TestLooperManagerCompat.java",
2829
"ViewHierarchyExceptionHandler.java",
2930
],
3031
),
@@ -64,6 +65,7 @@ android_library(
6465
"IdlingResourceRegistry.java",
6566
"Interrogator.java",
6667
"LooperIdlingResourceInterrogationHandler.java",
68+
"TestLooperManagerCompat.java",
6769
],
6870
deps = [
6971
"//espresso/core/java/androidx/test/espresso:interface",

espresso/core/java/androidx/test/espresso/base/Interrogator.java

Lines changed: 33 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -16,29 +16,22 @@
1616

1717
package androidx.test.espresso.base;
1818

19-
import static androidx.test.espresso.util.Throwables.throwIfUnchecked;
2019
import static androidx.test.internal.util.Checks.checkNotNull;
2120
import static androidx.test.internal.util.Checks.checkState;
2221

2322
import android.os.Binder;
2423
import android.os.Looper;
2524
import android.os.Message;
26-
import android.os.MessageQueue;
2725
import android.os.SystemClock;
2826
import android.util.Log;
29-
import java.lang.reflect.Field;
30-
import java.lang.reflect.InvocationTargetException;
31-
import java.lang.reflect.Method;
27+
import androidx.annotation.VisibleForTesting;
3228

3329
/** Isolates the nasty details of touching the message queue. */
3430
final class Interrogator {
3531

3632
private static final String TAG = "Interrogator";
37-
private static final Method messageQueueNextMethod;
38-
private static final Field messageQueueHeadField;
39-
private static final Method recycleUncheckedMethod;
4033

41-
private static final int LOOKAHEAD_MILLIS = 15;
34+
@VisibleForTesting static final int LOOKAHEAD_MILLIS = 15;
4235
private static final ThreadLocal<Boolean> interrogating =
4336
new ThreadLocal<Boolean>() {
4437
@Override
@@ -47,30 +40,7 @@ public Boolean initialValue() {
4740
}
4841
};
4942

50-
static {
51-
try {
52-
// TODO(b/112000181): remove the hidden api access here
53-
messageQueueNextMethod = MessageQueue.class.getDeclaredMethod("next");
54-
messageQueueNextMethod.setAccessible(true);
5543

56-
messageQueueHeadField = MessageQueue.class.getDeclaredField("mMessages");
57-
messageQueueHeadField.setAccessible(true);
58-
} catch (IllegalArgumentException
59-
| NoSuchFieldException
60-
| SecurityException
61-
| NoSuchMethodException e) {
62-
Log.e(TAG, "Could not initialize interrogator!", e);
63-
throw new RuntimeException("Could not initialize interrogator!", e);
64-
}
65-
66-
Method recycleUnchecked = null;
67-
try {
68-
recycleUnchecked = Message.class.getDeclaredMethod("recycleUnchecked");
69-
recycleUnchecked.setAccessible(true);
70-
} catch (NoSuchMethodException expectedOnLowerApiLevels) {
71-
}
72-
recycleUncheckedMethod = recycleUnchecked;
73-
}
7444

7545
/** Informed of the state of the queue and controls whether to continue interrogation or quit. */
7646
interface QueueInterrogationHandler<R> {
@@ -133,17 +103,18 @@ static <R> R loopAndInterrogate(InterrogationHandler<R> handler) {
133103
checkSanity();
134104
interrogating.set(Boolean.TRUE);
135105
boolean stillInterested = true;
136-
MessageQueue q = Looper.myQueue();
106+
TestLooperManagerCompat testLooperManager = TestLooperManagerCompat.acquire(Looper.myLooper());
107+
137108
// We may have an identity when we're called - we want to restore it at the end of the fn.
138109
final long entryIdentity = Binder.clearCallingIdentity();
139110
try {
140111
// this identity should not get changed by dispatching the loop until the observer is happy.
141112
final long threadIdentity = Binder.clearCallingIdentity();
142113
while (stillInterested) {
143114
// run until the observer is no longer interested.
144-
stillInterested = interrogateQueueState(q, handler);
115+
stillInterested = interrogateQueueState(testLooperManager, handler);
145116
if (stillInterested) {
146-
Message m = getNextMessage();
117+
Message m = testLooperManager.next();
147118

148119
// the observer cannot stop us from dispatching this message - but we need to let it know
149120
// that we're about to dispatch.
@@ -153,7 +124,7 @@ static <R> R loopAndInterrogate(InterrogationHandler<R> handler) {
153124
}
154125
stillInterested = handler.beforeTaskDispatch();
155126
handler.setMessage(m);
156-
m.getTarget().dispatchMessage(m);
127+
testLooperManager.execute(m);
157128

158129
// ensure looper invariants
159130
final long newIdentity = Binder.clearCallingIdentity();
@@ -172,48 +143,17 @@ static <R> R loopAndInterrogate(InterrogationHandler<R> handler) {
172143
+ " what="
173144
+ m.what);
174145
}
175-
recycle(m);
146+
testLooperManager.recycle(m);
176147
}
177148
}
178149
} finally {
179150
Binder.restoreCallingIdentity(entryIdentity);
180151
interrogating.set(Boolean.FALSE);
152+
testLooperManager.release();
181153
}
182154
return handler.get();
183155
}
184156

185-
private static void recycle(Message m) {
186-
if (recycleUncheckedMethod != null) {
187-
try {
188-
recycleUncheckedMethod.invoke(m);
189-
} catch (IllegalAccessException | IllegalArgumentException | SecurityException e) {
190-
throwIfUnchecked(e);
191-
throw new RuntimeException(e);
192-
} catch (InvocationTargetException ite) {
193-
if (ite.getCause() != null) {
194-
throwIfUnchecked(ite.getCause());
195-
throw new RuntimeException(ite.getCause());
196-
} else {
197-
throw new RuntimeException(ite);
198-
}
199-
}
200-
} else {
201-
m.recycle();
202-
}
203-
}
204-
205-
private static Message getNextMessage() {
206-
try {
207-
return (Message) messageQueueNextMethod.invoke(Looper.myQueue());
208-
} catch (IllegalAccessException
209-
| IllegalArgumentException
210-
| InvocationTargetException
211-
| SecurityException e) {
212-
throwIfUnchecked(e);
213-
throw new RuntimeException(e);
214-
}
215-
}
216-
217157
/**
218158
* Allows caller to see if the message queue is empty, has a task due soon / long, or has a
219159
* barrier.
@@ -228,36 +168,40 @@ private static Message getNextMessage() {
228168
* queueEmpty(), taskDueSoon(), taskDueLong() or barrierUp(). once and only once.
229169
* @return the result of handler.get()
230170
*/
231-
static <R> R peekAtQueueState(MessageQueue q, QueueInterrogationHandler<R> handler) {
232-
checkNotNull(q);
171+
static <R> R peekAtQueueState(
172+
TestLooperManagerCompat testLooperManager, QueueInterrogationHandler<R> handler) {
173+
checkNotNull(testLooperManager);
233174
checkNotNull(handler);
234175
checkState(
235-
!interrogateQueueState(q, handler),
176+
!interrogateQueueState(testLooperManager, handler),
236177
"It is expected that %s would stop interrogation after a single peak at the queue.",
237178
handler);
238179
return handler.get();
239180
}
240181

182+
static <R> R peekAtQueueState(Looper looper, QueueInterrogationHandler<R> handler) {
183+
TestLooperManagerCompat testLooperManager = TestLooperManagerCompat.acquire(looper);
184+
try {
185+
return peekAtQueueState(testLooperManager, handler);
186+
} finally {
187+
testLooperManager.release();
188+
}
189+
}
190+
241191
private static boolean interrogateQueueState(
242-
MessageQueue q, QueueInterrogationHandler<?> handler) {
243-
synchronized (q) {
244-
final Message head;
245-
try {
246-
head = (Message) messageQueueHeadField.get(q);
247-
} catch (IllegalAccessException e) {
248-
throw new RuntimeException(e);
249-
}
250-
if (null == head) {
251-
// no messages pending - AT ALL!
252-
return handler.queueEmpty();
253-
} else if (null == head.getTarget()) {
254-
// null target is a sync barrier token.
255-
if (Log.isLoggable(TAG, Log.DEBUG)) {
256-
Log.d(TAG, "barrier is up");
192+
TestLooperManagerCompat testLooperManager, QueueInterrogationHandler<?> handler) {
193+
synchronized (testLooperManager.getQueue()) {
194+
Long headWhen = testLooperManager.peekWhen();
195+
if (headWhen == null) {
196+
if (testLooperManager.isBlockedOnSyncBarrier()) {
197+
if (Log.isLoggable(TAG, Log.DEBUG)) {
198+
Log.d(TAG, "barrier is up");
199+
}
200+
return handler.barrierUp();
257201
}
258-
return handler.barrierUp();
202+
return handler.queueEmpty();
259203
}
260-
long headWhen = head.getWhen();
204+
261205
long nowFuz = SystemClock.uptimeMillis() + LOOKAHEAD_MILLIS;
262206
if (Log.isLoggable(TAG, Log.DEBUG)) {
263207
Log.d(

espresso/core/java/androidx/test/espresso/base/LooperIdlingResourceInterrogationHandler.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import android.os.Handler;
2020
import android.os.Looper;
2121
import android.os.Message;
22-
import android.os.MessageQueue;
2322
import androidx.test.espresso.IdlingResource;
2423
import java.util.Locale;
2524
import java.util.concurrent.ConcurrentHashMap;
@@ -69,7 +68,7 @@ public boolean barrierUp() {
6968

7069
// read on main - written on looper
7170
private volatile boolean started = false;
72-
private volatile MessageQueue queue = null;
71+
private volatile Looper looper = null;
7372
private volatile boolean idle = true;
7473

7574
// written on main - read on looper
@@ -97,7 +96,7 @@ static LooperIdlingResourceInterrogationHandler forLooper(Looper l) {
9796
new Runnable() {
9897
@Override
9998
public void run() {
100-
ir.queue = Looper.myQueue();
99+
ir.looper = Looper.myLooper();
101100
ir.started = true;
102101
Interrogator.loopAndInterrogate(ir);
103102
}
@@ -163,7 +162,7 @@ public boolean isIdleNow() {
163162
// make sure nothing has arrived in the queue while the looper thread is waiting to pull a
164163
// new task out of it. There can be some delay between a new message entering the queue and
165164
// the looper thread pulling it out and processing it.
166-
return Boolean.FALSE.equals(Interrogator.peekAtQueueState(queue, queueHasNewTasks));
165+
return Boolean.FALSE.equals(Interrogator.peekAtQueueState(looper, queueHasNewTasks));
167166
}
168167
return false;
169168
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package androidx.test.espresso.base;
2+
3+
import android.os.Looper;
4+
import android.os.Message;
5+
import android.os.MessageQueue;
6+
import androidx.annotation.Nullable;
7+
import java.lang.reflect.Field;
8+
import java.lang.reflect.Method;
9+
10+
/**
11+
* Compat class that supports the {@link android.os.TestLooperManager} Baklava+ functionality on
12+
* older Android SDKs.
13+
*
14+
* <p>Unlike the real TestLooperManager this only supports being used from the Looper's thread.
15+
*/
16+
class TestLooperManagerCompat {
17+
18+
private static final Method messageQueueNextMethod;
19+
private static final Field messageQueueHeadField;
20+
private static final Method recycleUncheckedMethod;
21+
22+
static {
23+
try {
24+
messageQueueNextMethod = MessageQueue.class.getDeclaredMethod("next");
25+
messageQueueNextMethod.setAccessible(true);
26+
messageQueueHeadField = MessageQueue.class.getDeclaredField("mMessages");
27+
messageQueueHeadField.setAccessible(true);
28+
recycleUncheckedMethod = Message.class.getDeclaredMethod("recycleUnchecked");
29+
recycleUncheckedMethod.setAccessible(true);
30+
} catch (ReflectiveOperationException e) {
31+
throw new RuntimeException(e);
32+
}
33+
}
34+
35+
private final MessageQueue queue;
36+
37+
private TestLooperManagerCompat(MessageQueue queue) {
38+
this.queue = queue;
39+
}
40+
41+
static TestLooperManagerCompat acquire(Looper looper) {
42+
return new TestLooperManagerCompat(looper.getQueue());
43+
}
44+
45+
@Nullable
46+
Long peekWhen() {
47+
Message msg = legacyPeek();
48+
if (msg != null && msg.getTarget() == null) {
49+
return null;
50+
}
51+
return msg == null ? null : msg.getWhen();
52+
}
53+
54+
@Nullable
55+
private Message legacyPeek() {
56+
// the legacy MessageQueue implementation synchronizes on itself,
57+
// so this uses the same lock
58+
synchronized (queue) {
59+
try {
60+
return (Message) messageQueueHeadField.get(queue);
61+
} catch (IllegalAccessException e) {
62+
throw new RuntimeException(e);
63+
}
64+
}
65+
}
66+
67+
void execute(Message message) {
68+
message.getTarget().dispatchMessage(message);
69+
}
70+
71+
void release() {
72+
// ignore for now
73+
}
74+
75+
boolean isBlockedOnSyncBarrier() {
76+
Message msg = legacyPeek();
77+
return msg != null && msg.getTarget() == null;
78+
}
79+
80+
Message next() {
81+
try {
82+
return (Message) messageQueueNextMethod.invoke(queue);
83+
} catch (ReflectiveOperationException e) {
84+
throw new RuntimeException(e);
85+
}
86+
}
87+
88+
void recycle(Message m) {
89+
try {
90+
recycleUncheckedMethod.invoke(m);
91+
} catch (ReflectiveOperationException e) {
92+
throw new RuntimeException(e);
93+
}
94+
}
95+
96+
MessageQueue getQueue() {
97+
return queue;
98+
}
99+
}

espresso/core/javatests/androidx/test/espresso/base/BUILD

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,3 +288,16 @@ axt_android_library_test(
288288
"@maven//:org_mockito_mockito_core",
289289
],
290290
)
291+
292+
axt_android_library_test(
293+
name = "EspressoIdleTest",
294+
srcs = ["EspressoIdleTest.java"],
295+
deps = [
296+
"//espresso/core",
297+
"//espresso/core/java/androidx/test/espresso/base:idling_resource_registry",
298+
"//ext/junit",
299+
"//runner/android_junit_runner",
300+
"@maven//:com_google_truth_truth",
301+
"@maven//:junit_junit",
302+
],
303+
)

0 commit comments

Comments
 (0)