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