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