Skip to content

Commit 662ec1e

Browse files
brettchabotcopybara-androidxtest
authored andcommitted
Make TestLooperManagerCompat call TestLooperManager APIs when available.
PiperOrigin-RevId: 706866885
1 parent eb37e0e commit 662ec1e

File tree

7 files changed

+264
-98
lines changed

7 files changed

+264
-98
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: 37 additions & 94 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,47 +168,50 @@ 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.
192+
TestLooperManagerCompat testLooperManager, QueueInterrogationHandler<?> handler) {
193+
194+
Long headWhen = testLooperManager.peekWhen();
195+
if (null == headWhen) {
196+
if (testLooperManager.isBlockedOnSyncBarrier()) {
255197
if (Log.isLoggable(TAG, Log.DEBUG)) {
256198
Log.d(TAG, "barrier is up");
257199
}
258200
return handler.barrierUp();
259201
}
260-
long headWhen = head.getWhen();
261-
long nowFuz = SystemClock.uptimeMillis() + LOOKAHEAD_MILLIS;
262-
if (Log.isLoggable(TAG, Log.DEBUG)) {
263-
Log.d(
264-
TAG,
265-
"headWhen: " + headWhen + " nowFuz: " + nowFuz + " due long: " + (nowFuz < headWhen));
266-
}
267-
if (nowFuz > headWhen) {
202+
return handler.queueEmpty();
203+
}
204+
205+
long nowFuz = SystemClock.uptimeMillis() + LOOKAHEAD_MILLIS;
206+
if (Log.isLoggable(TAG, Log.DEBUG)) {
207+
Log.d(
208+
TAG,
209+
"headWhen: " + headWhen + " nowFuz: " + nowFuz + " due long: " + (nowFuz < headWhen));
210+
}
211+
if (nowFuz > headWhen) {
268212
return handler.taskDueSoon();
269213
}
270214
return handler.taskDueLong();
271-
}
272215
}
273216

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

0 commit comments

Comments
 (0)