1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 * except in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the 10 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 11 * KIND, either express or implied. See the License for the specific language governing 12 * permissions and limitations under the License. 13 */ 14 15 package android.testing; 16 17 import android.os.Handler; 18 import android.os.HandlerThread; 19 import android.os.Looper; 20 import android.os.Message; 21 import android.os.MessageQueue; 22 import android.os.TestLooperManager; 23 import android.util.ArrayMap; 24 25 import androidx.test.InstrumentationRegistry; 26 27 import org.junit.runners.model.FrameworkMethod; 28 29 import java.lang.annotation.ElementType; 30 import java.lang.annotation.Retention; 31 import java.lang.annotation.RetentionPolicy; 32 import java.lang.annotation.Target; 33 import java.util.Map; 34 35 /** 36 * This is a wrapper around {@link TestLooperManager} to make it easier to manage 37 * and provide an easy annotation for use with tests. 38 * 39 * @see TestableLooperTest TestableLooperTest for examples. 40 */ 41 public class TestableLooper { 42 43 /** 44 * Whether to hold onto the main thread through all tests in an attempt to 45 * catch crashes. 46 */ 47 public static final boolean HOLD_MAIN_THREAD = false; 48 49 private Looper mLooper; 50 private MessageQueue mQueue; 51 private MessageHandler mMessageHandler; 52 53 private Handler mHandler; 54 private Runnable mEmptyMessage; 55 private TestLooperManager mQueueWrapper; 56 TestableLooper(Looper l)57 public TestableLooper(Looper l) throws Exception { 58 this(acquireLooperManager(l), l); 59 } 60 TestableLooper(TestLooperManager wrapper, Looper l)61 private TestableLooper(TestLooperManager wrapper, Looper l) { 62 mQueueWrapper = wrapper; 63 setupQueue(l); 64 } 65 TestableLooper(Looper looper, boolean b)66 private TestableLooper(Looper looper, boolean b) { 67 setupQueue(looper); 68 } 69 getLooper()70 public Looper getLooper() { 71 return mLooper; 72 } 73 setupQueue(Looper l)74 private void setupQueue(Looper l) { 75 mLooper = l; 76 mQueue = mLooper.getQueue(); 77 mHandler = new Handler(mLooper); 78 } 79 80 /** 81 * Must be called to release the looper when the test is complete, otherwise 82 * the looper will not be available for any subsequent tests. This is 83 * automatically handled for tests using {@link RunWithLooper}. 84 */ destroy()85 public void destroy() { 86 mQueueWrapper.release(); 87 if (HOLD_MAIN_THREAD && mLooper == Looper.getMainLooper()) { 88 TestableInstrumentation.releaseMain(); 89 } 90 } 91 92 /** 93 * Sets a callback for all messages processed on this TestableLooper. 94 * 95 * @see {@link MessageHandler} 96 */ setMessageHandler(MessageHandler handler)97 public void setMessageHandler(MessageHandler handler) { 98 mMessageHandler = handler; 99 } 100 101 /** 102 * Parse num messages from the message queue. 103 * 104 * @param num Number of messages to parse 105 */ processMessages(int num)106 public int processMessages(int num) { 107 for (int i = 0; i < num; i++) { 108 if (!parseMessageInt()) { 109 return i + 1; 110 } 111 } 112 return num; 113 } 114 115 /** 116 * Process messages in the queue until no more are found. 117 */ processAllMessages()118 public void processAllMessages() { 119 while (processQueuedMessages() != 0) ; 120 } 121 processQueuedMessages()122 private int processQueuedMessages() { 123 int count = 0; 124 mEmptyMessage = () -> { }; 125 mHandler.post(mEmptyMessage); 126 waitForMessage(mQueueWrapper, mHandler, mEmptyMessage); 127 while (parseMessageInt()) count++; 128 return count; 129 } 130 parseMessageInt()131 private boolean parseMessageInt() { 132 try { 133 Message result = mQueueWrapper.next(); 134 if (result != null) { 135 // This is a break message. 136 if (result.getCallback() == mEmptyMessage) { 137 mQueueWrapper.recycle(result); 138 return false; 139 } 140 141 if (mMessageHandler != null) { 142 if (mMessageHandler.onMessageHandled(result)) { 143 mQueueWrapper.execute(result); 144 mQueueWrapper.recycle(result); 145 } else { 146 mQueueWrapper.recycle(result); 147 // Message handler indicated it doesn't want us to continue. 148 return false; 149 } 150 } else { 151 mQueueWrapper.execute(result); 152 mQueueWrapper.recycle(result); 153 } 154 } else { 155 // No messages, don't continue parsing 156 return false; 157 } 158 } catch (Exception e) { 159 throw new RuntimeException(e); 160 } 161 return true; 162 } 163 164 /** 165 * Runs an executable with myLooper set and processes all messages added. 166 */ runWithLooper(RunnableWithException runnable)167 public void runWithLooper(RunnableWithException runnable) throws Exception { 168 new Handler(getLooper()).post(() -> { 169 try { 170 runnable.run(); 171 } catch (Exception e) { 172 throw new RuntimeException(e); 173 } 174 }); 175 processAllMessages(); 176 } 177 178 public interface RunnableWithException { run()179 void run() throws Exception; 180 } 181 182 /** 183 * Annotation that tells the {@link AndroidTestingRunner} to create a TestableLooper and 184 * run this test/class on that thread. The {@link TestableLooper} can be acquired using 185 * {@link #get(Object)}. 186 */ 187 @Retention(RetentionPolicy.RUNTIME) 188 @Target({ElementType.METHOD, ElementType.TYPE}) 189 public @interface RunWithLooper { setAsMainLooper()190 boolean setAsMainLooper() default false; 191 } 192 waitForMessage(TestLooperManager queueWrapper, Handler handler, Runnable execute)193 private static void waitForMessage(TestLooperManager queueWrapper, Handler handler, 194 Runnable execute) { 195 for (int i = 0; i < 10; i++) { 196 if (!queueWrapper.hasMessages(handler, null, execute)) { 197 try { 198 Thread.sleep(1); 199 } catch (InterruptedException e) { 200 } 201 } 202 } 203 if (!queueWrapper.hasMessages(handler, null, execute)) { 204 throw new RuntimeException("Message didn't queue..."); 205 } 206 } 207 acquireLooperManager(Looper l)208 private static TestLooperManager acquireLooperManager(Looper l) { 209 if (HOLD_MAIN_THREAD && l == Looper.getMainLooper()) { 210 TestableInstrumentation.acquireMain(); 211 } 212 return InstrumentationRegistry.getInstrumentation().acquireLooperManager(l); 213 } 214 215 private static final Map<Object, TestableLooper> sLoopers = new ArrayMap<>(); 216 217 /** 218 * For use with {@link RunWithLooper}, used to get the TestableLooper that was 219 * automatically created for this test. 220 */ get(Object test)221 public static TestableLooper get(Object test) { 222 return sLoopers.get(test); 223 } 224 remove(Object test)225 public static void remove(Object test) { 226 sLoopers.remove(test); 227 } 228 229 static class LooperFrameworkMethod extends FrameworkMethod { 230 private HandlerThread mHandlerThread; 231 232 private final TestableLooper mTestableLooper; 233 private final Looper mLooper; 234 private final Handler mHandler; 235 LooperFrameworkMethod(FrameworkMethod base, boolean setAsMain, Object test)236 public LooperFrameworkMethod(FrameworkMethod base, boolean setAsMain, Object test) { 237 super(base.getMethod()); 238 try { 239 mLooper = setAsMain ? Looper.getMainLooper() : createLooper(); 240 mTestableLooper = new TestableLooper(mLooper, false); 241 } catch (Exception e) { 242 throw new RuntimeException(e); 243 } 244 sLoopers.put(test, mTestableLooper); 245 mHandler = new Handler(mLooper); 246 } 247 LooperFrameworkMethod(TestableLooper other, FrameworkMethod base)248 public LooperFrameworkMethod(TestableLooper other, FrameworkMethod base) { 249 super(base.getMethod()); 250 mLooper = other.mLooper; 251 mTestableLooper = other; 252 mHandler = Handler.createAsync(mLooper); 253 } 254 get(FrameworkMethod base, boolean setAsMain, Object test)255 public static FrameworkMethod get(FrameworkMethod base, boolean setAsMain, Object test) { 256 if (sLoopers.containsKey(test)) { 257 return new LooperFrameworkMethod(sLoopers.get(test), base); 258 } 259 return new LooperFrameworkMethod(base, setAsMain, test); 260 } 261 262 @Override invokeExplosively(Object target, Object... params)263 public Object invokeExplosively(Object target, Object... params) throws Throwable { 264 if (Looper.myLooper() == mLooper) { 265 // Already on the right thread from another statement, just execute then. 266 return super.invokeExplosively(target, params); 267 } 268 boolean set = mTestableLooper.mQueueWrapper == null; 269 if (set) { 270 mTestableLooper.mQueueWrapper = acquireLooperManager(mLooper); 271 } 272 try { 273 Object[] ret = new Object[1]; 274 // Run the execution on the looper thread. 275 Runnable execute = () -> { 276 try { 277 ret[0] = super.invokeExplosively(target, params); 278 } catch (Throwable throwable) { 279 throw new LooperException(throwable); 280 } 281 }; 282 Message m = Message.obtain(mHandler, execute); 283 284 // Dispatch our message. 285 try { 286 mTestableLooper.mQueueWrapper.execute(m); 287 } catch (LooperException e) { 288 throw e.getSource(); 289 } catch (RuntimeException re) { 290 // If the TestLooperManager has to post, it will wrap what it throws in a 291 // RuntimeException, make sure we grab the actual source. 292 if (re.getCause() instanceof LooperException) { 293 throw ((LooperException) re.getCause()).getSource(); 294 } else { 295 throw re.getCause(); 296 } 297 } finally { 298 m.recycle(); 299 } 300 return ret[0]; 301 } finally { 302 if (set) { 303 mTestableLooper.mQueueWrapper.release(); 304 mTestableLooper.mQueueWrapper = null; 305 if (HOLD_MAIN_THREAD && mLooper == Looper.getMainLooper()) { 306 TestableInstrumentation.releaseMain(); 307 } 308 } 309 } 310 } 311 createLooper()312 private Looper createLooper() { 313 // TODO: Find way to share these. 314 mHandlerThread = new HandlerThread(TestableLooper.class.getSimpleName()); 315 mHandlerThread.start(); 316 return mHandlerThread.getLooper(); 317 } 318 319 @Override finalize()320 protected void finalize() throws Throwable { 321 super.finalize(); 322 if (mHandlerThread != null) { 323 mHandlerThread.quit(); 324 } 325 } 326 327 private static class LooperException extends RuntimeException { 328 private final Throwable mSource; 329 LooperException(Throwable t)330 public LooperException(Throwable t) { 331 mSource = t; 332 } 333 getSource()334 public Throwable getSource() { 335 return mSource; 336 } 337 } 338 } 339 340 /** 341 * Callback to control the execution of messages on the looper, when set with 342 * {@link #setMessageHandler(MessageHandler)} then {@link #onMessageHandled(Message)} 343 * will get called back for every message processed on the {@link TestableLooper}. 344 */ 345 public interface MessageHandler { 346 /** 347 * Return true to have the message executed and delivered to target. 348 * Return false to not execute the message and stop executing messages. 349 */ onMessageHandled(Message m)350 boolean onMessageHandled(Message m); 351 } 352 } 353