1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.os.test;
18 
19 import static org.junit.Assert.assertTrue;
20 
21 import android.os.Looper;
22 import android.os.Message;
23 import android.os.MessageQueue;
24 import android.os.SystemClock;
25 import android.util.Log;
26 
27 import java.lang.reflect.Constructor;
28 import java.lang.reflect.Field;
29 import java.lang.reflect.InvocationTargetException;
30 import java.lang.reflect.Method;
31 
32 /**
33  * Creates a looper whose message queue can be manipulated
34  * This allows testing code that uses a looper to dispatch messages in a deterministic manner
35  * Creating a TestLooper will also install it as the looper for the current thread
36  */
37 public class TestLooper {
38     protected final Looper mLooper;
39 
40     private static final Constructor<Looper> LOOPER_CONSTRUCTOR;
41     private static final Field THREAD_LOCAL_LOOPER_FIELD;
42     private static final Field MESSAGE_QUEUE_MESSAGES_FIELD;
43     private static final Field MESSAGE_NEXT_FIELD;
44     private static final Field MESSAGE_WHEN_FIELD;
45     private static final Method MESSAGE_MARK_IN_USE_METHOD;
46     private static final String TAG = "TestLooper";
47 
48     private AutoDispatchThread mAutoDispatchThread;
49 
50     static {
51         try {
52             LOOPER_CONSTRUCTOR = Looper.class.getDeclaredConstructor(Boolean.TYPE);
53             LOOPER_CONSTRUCTOR.setAccessible(true);
54             THREAD_LOCAL_LOOPER_FIELD = Looper.class.getDeclaredField("sThreadLocal");
55             THREAD_LOCAL_LOOPER_FIELD.setAccessible(true);
56             MESSAGE_QUEUE_MESSAGES_FIELD = MessageQueue.class.getDeclaredField("mMessages");
57             MESSAGE_QUEUE_MESSAGES_FIELD.setAccessible(true);
58             MESSAGE_NEXT_FIELD = Message.class.getDeclaredField("next");
59             MESSAGE_NEXT_FIELD.setAccessible(true);
60             MESSAGE_WHEN_FIELD = Message.class.getDeclaredField("when");
61             MESSAGE_WHEN_FIELD.setAccessible(true);
62             MESSAGE_MARK_IN_USE_METHOD = Message.class.getDeclaredMethod("markInUse");
63             MESSAGE_MARK_IN_USE_METHOD.setAccessible(true);
64         } catch (NoSuchFieldException | NoSuchMethodException e) {
65             throw new RuntimeException("Failed to initialize TestLooper", e);
66         }
67     }
68 
69 
TestLooper()70     public TestLooper() {
71         try {
72             mLooper = LOOPER_CONSTRUCTOR.newInstance(false);
73 
74             ThreadLocal<Looper> threadLocalLooper = (ThreadLocal<Looper>) THREAD_LOCAL_LOOPER_FIELD
75                     .get(null);
76             threadLocalLooper.set(mLooper);
77         } catch (IllegalAccessException | InstantiationException | InvocationTargetException e) {
78             throw new RuntimeException("Reflection error constructing or accessing looper", e);
79         }
80     }
81 
getLooper()82     public Looper getLooper() {
83         return mLooper;
84     }
85 
getMessageLinkedList()86     private Message getMessageLinkedList() {
87         try {
88             MessageQueue queue = mLooper.getQueue();
89             return (Message) MESSAGE_QUEUE_MESSAGES_FIELD.get(queue);
90         } catch (IllegalAccessException e) {
91             throw new RuntimeException("Access failed in TestLooper: get - MessageQueue.mMessages",
92                     e);
93         }
94     }
95 
moveTimeForward(long milliSeconds)96     public void moveTimeForward(long milliSeconds) {
97         try {
98             Message msg = getMessageLinkedList();
99             while (msg != null) {
100                 long updatedWhen = msg.getWhen() - milliSeconds;
101                 if (updatedWhen < 0) {
102                     updatedWhen = 0;
103                 }
104                 MESSAGE_WHEN_FIELD.set(msg, updatedWhen);
105                 msg = (Message) MESSAGE_NEXT_FIELD.get(msg);
106             }
107         } catch (IllegalAccessException e) {
108             throw new RuntimeException("Access failed in TestLooper: set - Message.when", e);
109         }
110     }
111 
messageQueueNext()112     private Message messageQueueNext() {
113         try {
114             long now = SystemClock.uptimeMillis();
115 
116             Message prevMsg = null;
117             Message msg = getMessageLinkedList();
118             if (msg != null && msg.getTarget() == null) {
119                 // Stalled by a barrier. Find the next asynchronous message in
120                 // the queue.
121                 do {
122                     prevMsg = msg;
123                     msg = (Message) MESSAGE_NEXT_FIELD.get(msg);
124                 } while (msg != null && !msg.isAsynchronous());
125             }
126             if (msg != null) {
127                 if (now >= msg.getWhen()) {
128                     // Got a message.
129                     if (prevMsg != null) {
130                         MESSAGE_NEXT_FIELD.set(prevMsg, MESSAGE_NEXT_FIELD.get(msg));
131                     } else {
132                         MESSAGE_QUEUE_MESSAGES_FIELD.set(mLooper.getQueue(),
133                                 MESSAGE_NEXT_FIELD.get(msg));
134                     }
135                     MESSAGE_NEXT_FIELD.set(msg, null);
136                     MESSAGE_MARK_IN_USE_METHOD.invoke(msg);
137                     return msg;
138                 }
139             }
140         } catch (IllegalAccessException | InvocationTargetException e) {
141             throw new RuntimeException("Access failed in TestLooper", e);
142         }
143 
144         return null;
145     }
146 
147     /**
148      * @return true if there are pending messages in the message queue
149      */
isIdle()150     public synchronized boolean isIdle() {
151         Message messageList = getMessageLinkedList();
152 
153         return messageList != null && SystemClock.uptimeMillis() >= messageList.getWhen();
154     }
155 
156     /**
157      * @return the next message in the Looper's message queue or null if there is none
158      */
nextMessage()159     public synchronized Message nextMessage() {
160         if (isIdle()) {
161             return messageQueueNext();
162         } else {
163             return null;
164         }
165     }
166 
167     /**
168      * Dispatch the next message in the queue
169      * Asserts that there is a message in the queue
170      */
dispatchNext()171     public synchronized void dispatchNext() {
172         assertTrue(isIdle());
173         Message msg = messageQueueNext();
174         if (msg == null) {
175             return;
176         }
177         msg.getTarget().dispatchMessage(msg);
178     }
179 
180     /**
181      * Dispatch all messages currently in the queue
182      * Will not fail if there are no messages pending
183      * @return the number of messages dispatched
184      */
dispatchAll()185     public synchronized int dispatchAll() {
186         int count = 0;
187         while (isIdle()) {
188             dispatchNext();
189             ++count;
190         }
191         return count;
192     }
193 
194     /**
195      * Thread used to dispatch messages when the main thread is blocked waiting for a response.
196      */
197     private class AutoDispatchThread extends Thread {
198         private static final int MAX_LOOPS = 100;
199         private static final int LOOP_SLEEP_TIME_MS = 10;
200 
201         private RuntimeException mAutoDispatchException = null;
202 
203         /**
204          * Run method for the auto dispatch thread.
205          * The thread loops a maximum of MAX_LOOPS times with a 10ms sleep between loops.
206          * The thread continues looping and attempting to dispatch all messages until at
207          * least one message has been dispatched.
208          */
209         @Override
run()210         public void run() {
211             int dispatchCount = 0;
212             for (int i = 0; i < MAX_LOOPS; i++) {
213                 try {
214                     dispatchCount = dispatchAll();
215                 } catch (RuntimeException e) {
216                     mAutoDispatchException = e;
217                 }
218                 Log.d(TAG, "dispatched " + dispatchCount + " messages");
219                 if (dispatchCount > 0) {
220                     return;
221                 }
222                 try {
223                     Thread.sleep(LOOP_SLEEP_TIME_MS);
224                 } catch (InterruptedException e) {
225                     mAutoDispatchException = new IllegalStateException(
226                             "stopAutoDispatch called before any messages were dispatched.");
227                     return;
228                 }
229             }
230             Log.e(TAG, "AutoDispatchThread did not dispatch any messages.");
231             mAutoDispatchException = new IllegalStateException(
232                     "TestLooper did not dispatch any messages before exiting.");
233         }
234 
235         /**
236          * Method allowing the TestLooper to pass any exceptions thrown by the thread to be passed
237          * to the main thread.
238          *
239          * @return RuntimeException Exception created by stopping without dispatching a message
240          */
getException()241         public RuntimeException getException() {
242             return mAutoDispatchException;
243         }
244     }
245 
246     /**
247      * Create and start a new AutoDispatchThread if one is not already running.
248      */
startAutoDispatch()249     public void startAutoDispatch() {
250         if (mAutoDispatchThread != null) {
251             throw new IllegalStateException(
252                     "startAutoDispatch called with the AutoDispatchThread already running.");
253         }
254         mAutoDispatchThread = new AutoDispatchThread();
255         mAutoDispatchThread.start();
256     }
257 
258     /**
259      * If an AutoDispatchThread is currently running, stop and clean up.
260      */
stopAutoDispatch()261     public void stopAutoDispatch() {
262         if (mAutoDispatchThread != null) {
263             if (mAutoDispatchThread.isAlive()) {
264                 mAutoDispatchThread.interrupt();
265             }
266             try {
267                 mAutoDispatchThread.join();
268             } catch (InterruptedException e) {
269                 // Catch exception from join.
270             }
271 
272             RuntimeException e = mAutoDispatchThread.getException();
273             mAutoDispatchThread = null;
274             if (e != null) {
275                 throw e;
276             }
277         } else {
278             // stopAutoDispatch was called when startAutoDispatch has not created a new thread.
279             throw new IllegalStateException(
280                     "stopAutoDispatch called without startAutoDispatch.");
281         }
282     }
283 }
284