Skip to content

Commit 9c5afd7

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: 706812503
1 parent eb37e0e commit 9c5afd7

File tree

7 files changed

+163
-90
lines changed

7 files changed

+163
-90
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: 28 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -16,27 +16,19 @@
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;
3227

3328
/** Isolates the nasty details of touching the message queue. */
3429
final class Interrogator {
3530

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

4133
private static final int LOOKAHEAD_MILLIS = 15;
4234
private static final ThreadLocal<Boolean> interrogating =
@@ -47,30 +39,7 @@ public Boolean initialValue() {
4739
}
4840
};
4941

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

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-
}
7443

7544
/** Informed of the state of the queue and controls whether to continue interrogation or quit. */
7645
interface QueueInterrogationHandler<R> {
@@ -133,17 +102,18 @@ static <R> R loopAndInterrogate(InterrogationHandler<R> handler) {
133102
checkSanity();
134103
interrogating.set(Boolean.TRUE);
135104
boolean stillInterested = true;
136-
MessageQueue q = Looper.myQueue();
105+
TestLooperManagerCompat testLooperManager = TestLooperManagerCompat.acquire(Looper.myLooper());
106+
137107
// We may have an identity when we're called - we want to restore it at the end of the fn.
138108
final long entryIdentity = Binder.clearCallingIdentity();
139109
try {
140110
// this identity should not get changed by dispatching the loop until the observer is happy.
141111
final long threadIdentity = Binder.clearCallingIdentity();
142112
while (stillInterested) {
143113
// run until the observer is no longer interested.
144-
stillInterested = interrogateQueueState(q, handler);
114+
stillInterested = interrogateQueueState(testLooperManager, handler);
145115
if (stillInterested) {
146-
Message m = getNextMessage();
116+
Message m = testLooperManager.next();
147117

148118
// the observer cannot stop us from dispatching this message - but we need to let it know
149119
// that we're about to dispatch.
@@ -153,7 +123,7 @@ static <R> R loopAndInterrogate(InterrogationHandler<R> handler) {
153123
}
154124
stillInterested = handler.beforeTaskDispatch();
155125
handler.setMessage(m);
156-
m.getTarget().dispatchMessage(m);
126+
testLooperManager.execute(m);
157127

158128
// ensure looper invariants
159129
final long newIdentity = Binder.clearCallingIdentity();
@@ -172,48 +142,17 @@ static <R> R loopAndInterrogate(InterrogationHandler<R> handler) {
172142
+ " what="
173143
+ m.what);
174144
}
175-
recycle(m);
145+
testLooperManager.recycle(m);
176146
}
177147
}
178148
} finally {
179149
Binder.restoreCallingIdentity(entryIdentity);
180150
interrogating.set(Boolean.FALSE);
151+
testLooperManager.release();
181152
}
182153
return handler.get();
183154
}
184155

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-
217156
/**
218157
* Allows caller to see if the message queue is empty, has a task due soon / long, or has a
219158
* barrier.
@@ -228,36 +167,40 @@ private static Message getNextMessage() {
228167
* queueEmpty(), taskDueSoon(), taskDueLong() or barrierUp(). once and only once.
229168
* @return the result of handler.get()
230169
*/
231-
static <R> R peekAtQueueState(MessageQueue q, QueueInterrogationHandler<R> handler) {
232-
checkNotNull(q);
170+
static <R> R peekAtQueueState(
171+
TestLooperManagerCompat testLooperManager, QueueInterrogationHandler<R> handler) {
172+
checkNotNull(testLooperManager);
233173
checkNotNull(handler);
234174
checkState(
235-
!interrogateQueueState(q, handler),
175+
!interrogateQueueState(testLooperManager, handler),
236176
"It is expected that %s would stop interrogation after a single peak at the queue.",
237177
handler);
238178
return handler.get();
239179
}
240180

181+
static <R> R peekAtQueueState(Looper looper, QueueInterrogationHandler<R> handler) {
182+
TestLooperManagerCompat testLooperManager = TestLooperManagerCompat.acquire(looper);
183+
try {
184+
return peekAtQueueState(testLooperManager, handler);
185+
} finally {
186+
testLooperManager.release();
187+
}
188+
}
189+
241190
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.
191+
TestLooperManagerCompat testLooperManager, QueueInterrogationHandler<?> handler) {
192+
193+
Long headWhen = testLooperManager.peekWhen();
194+
if (null == headWhen) {
195+
if (testLooperManager.isBlockedOnSyncBarrier()) {
255196
if (Log.isLoggable(TAG, Log.DEBUG)) {
256197
Log.d(TAG, "barrier is up");
257198
}
258199
return handler.barrierUp();
259200
}
260-
long headWhen = head.getWhen();
201+
return handler.queueEmpty();
202+
}
203+
261204
long nowFuz = SystemClock.uptimeMillis() + LOOKAHEAD_MILLIS;
262205
if (Log.isLoggable(TAG, Log.DEBUG)) {
263206
Log.d(
@@ -268,7 +211,6 @@ private static boolean interrogateQueueState(
268211
return handler.taskDueSoon();
269212
}
270213
return handler.taskDueLong();
271-
}
272214
}
273215

274216
private static void checkSanity() {

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: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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 androidx.test.internal.platform.reflect.ReflectiveField;
8+
import androidx.test.internal.platform.reflect.ReflectiveMethod;
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 ReflectiveMethod<Message> messageQueueNextMethod =
19+
new ReflectiveMethod<>(MessageQueue.class, "next");
20+
21+
private static final ReflectiveField<Message> messageQueueHeadField =
22+
new ReflectiveField<>(MessageQueue.class, "mMessages");
23+
24+
private static final ReflectiveMethod<Void> recycleUncheckedMethod =
25+
new ReflectiveMethod<>(Message.class, "recycleUnchecked");
26+
private final MessageQueue queue;
27+
28+
private TestLooperManagerCompat(MessageQueue queue) {
29+
this.queue = queue;
30+
}
31+
32+
static TestLooperManagerCompat acquire(Looper looper) {
33+
return new TestLooperManagerCompat(looper.getQueue());
34+
}
35+
36+
@Nullable
37+
Long peekWhen() {
38+
Message msg = legacyPeek();
39+
if (msg != null && msg.getTarget() == null) {
40+
return null;
41+
}
42+
return msg == null ? null : msg.getWhen();
43+
}
44+
45+
private @Nullable Message legacyPeek() {
46+
synchronized (queue) {
47+
return messageQueueHeadField.get(queue);
48+
}
49+
}
50+
51+
void execute(Message message) {
52+
message.getTarget().dispatchMessage(message);
53+
}
54+
55+
void release() {
56+
// ignore for now
57+
}
58+
59+
boolean isBlockedOnSyncBarrier() {
60+
Message msg = legacyPeek();
61+
return msg != null && msg.getTarget() == null;
62+
}
63+
64+
Message next() {
65+
return messageQueueNextMethod.invoke(queue);
66+
}
67+
68+
void recycle(Message m) {
69+
recycleUncheckedMethod.invoke(m);
70+
}
71+
}

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,3 +171,15 @@ axt_android_library_test(
171171
"@maven//:org_mockito_mockito_core",
172172
],
173173
)
174+
175+
axt_android_library_test(
176+
name = "EspressoIdleTest",
177+
srcs = ["EspressoIdleTest.java"],
178+
deps = [
179+
"//espresso/core",
180+
"//ext/junit",
181+
"//runner/android_junit_runner",
182+
"@maven//:com_google_truth_truth",
183+
"@maven//:junit_junit",
184+
],
185+
)

0 commit comments

Comments
 (0)