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.annotation.NonNull; 18 import android.annotation.Nullable; 19 import android.os.Build; 20 import android.os.Handler; 21 import android.os.HandlerThread; 22 import android.os.Looper; 23 import android.os.Message; 24 import android.os.MessageQueue; 25 import android.os.TestLooperManager; 26 import android.util.ArrayMap; 27 28 import androidx.test.platform.app.InstrumentationRegistry; 29 30 import org.junit.runners.model.FrameworkMethod; 31 32 import java.lang.annotation.ElementType; 33 import java.lang.annotation.Retention; 34 import java.lang.annotation.RetentionPolicy; 35 import java.lang.annotation.Target; 36 import java.lang.reflect.Field; 37 import java.lang.reflect.Method; 38 import java.util.ArrayDeque; 39 import java.util.Map; 40 import java.util.Objects; 41 import java.util.Queue; 42 import java.util.concurrent.atomic.AtomicBoolean; 43 44 /** 45 * This is a wrapper around {@link TestLooperManager} to make it easier to manage 46 * and provide an easy annotation for use with tests. 47 * 48 * @see TestableLooperTest TestableLooperTest for examples. 49 * 50 * @deprecated Use {@link android.os.TestLooperManager} or {@link 51 * org.robolectric.shadows.ShadowLooper} instead. 52 * This class is not actively maintained. 53 * Both of the recommended alternatives allow fine control of execution. 54 * The Robolectric class also allows advancing time. 55 */ 56 public class TestableLooper { 57 58 /** 59 * Whether to hold onto the main thread through all tests in an attempt to 60 * catch crashes. 61 */ 62 public static final boolean HOLD_MAIN_THREAD = false; 63 private static final Field MESSAGE_QUEUE_MESSAGES_FIELD; 64 private static final Field MESSAGE_NEXT_FIELD; 65 private static final Field MESSAGE_WHEN_FIELD; 66 67 private Looper mLooper; 68 private MessageQueue mQueue; 69 private MessageHandler mMessageHandler; 70 71 private Handler mHandler; 72 private TestLooperManager mQueueWrapper; 73 74 /** 75 * Baklava introduces new {@link TestLooperManager} APIs that we can use instead of reflection. 76 */ isAtLeastBaklava()77 private static boolean isAtLeastBaklava() { 78 return Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA; 79 } 80 81 static { 82 if (isAtLeastBaklava()) { 83 MESSAGE_QUEUE_MESSAGES_FIELD = null; 84 MESSAGE_NEXT_FIELD = null; 85 MESSAGE_WHEN_FIELD = null; 86 } else { 87 try { 88 MESSAGE_QUEUE_MESSAGES_FIELD = MessageQueue.class.getDeclaredField("mMessages"); 89 MESSAGE_QUEUE_MESSAGES_FIELD.setAccessible(true); 90 MESSAGE_NEXT_FIELD = Message.class.getDeclaredField("next"); 91 MESSAGE_NEXT_FIELD.setAccessible(true); 92 MESSAGE_WHEN_FIELD = Message.class.getDeclaredField("when"); 93 MESSAGE_WHEN_FIELD.setAccessible(true); 94 } catch (NoSuchFieldException e) { 95 throw new RuntimeException("Failed to initialize TestableLooper", e); 96 } 97 } 98 } 99 TestableLooper(Looper l)100 public TestableLooper(Looper l) throws Exception { 101 this(acquireLooperManager(l), l); 102 } 103 TestableLooper(TestLooperManager wrapper, Looper l)104 private TestableLooper(TestLooperManager wrapper, Looper l) { 105 mQueueWrapper = Objects.requireNonNull(wrapper); 106 setupQueue(l); 107 } 108 TestableLooper(Looper looper, boolean b)109 private TestableLooper(Looper looper, boolean b) { 110 setupQueue(looper); 111 } 112 113 /** 114 * Wrap the given runnable so that it will run blocking on the Looper that will be set up for 115 * the given test. 116 * <p> 117 * This method is required to support any TestRule which needs to run setup and/or teardown code 118 * on the TestableLooper. Whether using {@link AndroidTestingRunner} or 119 * {@link TestWithLooperRule}, the TestRule's Statement evaluates on the test instrumentation 120 * thread, rather than the TestableLooper thread, so access to the TestableLooper is required. 121 * However, {@link #get(Object)} will return {@code null} both before and after the inner 122 * statement is evaluated: 123 * <ul> 124 * <li>Before the test {@link #get} returns {@code null} because while the TestableLooperHolder 125 * is accessible in sLoopers, it has not been initialized with an actual TestableLooper yet. 126 * This method's use of the internal LooperFrameworkMethod ensures that all setup and teardown 127 * of the TestableLooper happen as it would for all other wrapped code blocks. 128 * <li>After the test {@link #get} can return {@code null} because many tests call 129 * {@link #remove} in the teardown method. The fact that this method returns a runnable allows 130 * it to be called before the test (when the TestableLooperHolder is still in sLoopers), and 131 * then executed as teardown after the test. 132 * </ul> 133 * 134 * @param test the test instance (just like passed to {@link #get(Object)}) 135 * @param runnable the operation that should eventually be run on the TestableLooper 136 * @return a runnable that will block the thread on which it is called until the given runnable 137 * is finished. Will be {@code null} if there is no looper for the given test. 138 * @hide 139 */ 140 @Nullable wrapWithRunBlocking( Object test, @NonNull RunnableWithException runnable)141 public static RunnableWithException wrapWithRunBlocking( 142 Object test, @NonNull RunnableWithException runnable) { 143 TestableLooperHolder looperHolder = sLoopers.get(test); 144 if (looperHolder == null) { 145 return null; 146 } 147 try { 148 FrameworkMethod base = new FrameworkMethod(runnable.getClass().getMethod("run")); 149 LooperFrameworkMethod wrapped = new LooperFrameworkMethod(base, looperHolder); 150 return () -> { 151 try { 152 wrapped.invokeExplosively(runnable); 153 } catch (RuntimeException | Error e) { 154 throw e; 155 } catch (Throwable e) { 156 throw new RuntimeException(e); 157 } 158 }; 159 } catch (NoSuchMethodException e) { 160 throw new RuntimeException(e); 161 } 162 } 163 164 public Looper getLooper() { 165 return mLooper; 166 } 167 168 private void setupQueue(Looper l) { 169 mLooper = l; 170 mQueue = mLooper.getQueue(); 171 mHandler = new Handler(mLooper); 172 } 173 174 /** 175 * Must be called to release the looper when the test is complete, otherwise 176 * the looper will not be available for any subsequent tests. This is 177 * automatically handled for tests using {@link RunWithLooper}. 178 */ 179 public void destroy() { 180 mQueueWrapper.release(); 181 if (HOLD_MAIN_THREAD && mLooper == Looper.getMainLooper()) { 182 TestableInstrumentation.releaseMain(); 183 } 184 } 185 186 /** 187 * Sets a callback for all messages processed on this TestableLooper. 188 * 189 * @see {@link MessageHandler} 190 */ 191 public void setMessageHandler(MessageHandler handler) { 192 mMessageHandler = handler; 193 } 194 195 /** 196 * Parse num messages from the message queue. 197 * 198 * @param num Number of messages to parse 199 */ 200 public int processMessages(int num) { 201 return processMessagesInternal(num, null); 202 } 203 204 private int processMessagesInternal(int num, Runnable barrierRunnable) { 205 for (int i = 0; i < num; i++) { 206 if (!processSingleMessage(barrierRunnable)) { 207 return i + 1; 208 } 209 } 210 return num; 211 } 212 213 /** 214 * Process up to a certain number of messages, not blocking if the queue has less messages than 215 * that 216 * @param num the maximum number of messages to process 217 * @return the number of messages processed. This will be at most {@code num}. 218 */ 219 220 public int processMessagesNonBlocking(int num) { 221 final AtomicBoolean reachedBarrier = new AtomicBoolean(false); 222 Runnable barrierRunnable = () -> { 223 reachedBarrier.set(true); 224 }; 225 mHandler.post(barrierRunnable); 226 waitForMessage(mQueueWrapper, mHandler, barrierRunnable); 227 try { 228 return processMessagesInternal(num, barrierRunnable) + (reachedBarrier.get() ? -1 : 0); 229 } finally { 230 mHandler.removeCallbacks(barrierRunnable); 231 } 232 } 233 234 /** 235 * Process messages in the queue until no more are found. 236 */ 237 public void processAllMessages() { 238 while (processQueuedMessages() != 0) ; 239 } 240 241 public long peekWhen() { 242 if (isAtLeastBaklava()) { 243 return peekWhenBaklava(); 244 } else { 245 return peekWhenLegacy(); 246 } 247 } 248 249 private long peekWhenBaklava() { 250 Long when = mQueueWrapper.peekWhen(); 251 if (when != null) { 252 return when; 253 } else { 254 return 0; 255 } 256 } 257 258 private long peekWhenLegacy() { 259 try { 260 Message msg = (Message) MESSAGE_QUEUE_MESSAGES_FIELD.get(mLooper.getQueue()); 261 if (msg != null) { 262 return msg.getWhen(); 263 } else { 264 return 0; 265 } 266 } catch (IllegalAccessException e) { 267 throw new RuntimeException("Access failed in TestableLooper: set - Message.when", e); 268 } 269 } 270 271 public void moveTimeForward(long milliSeconds) { 272 if (isAtLeastBaklava()) { 273 moveTimeForwardBaklava(milliSeconds); 274 } else { 275 moveTimeForwardLegacy(milliSeconds); 276 } 277 } 278 279 private void moveTimeForwardBaklava(long milliSeconds) { 280 // Drain all Messages from the queue. 281 Queue<Message> messages = new ArrayDeque<>(); 282 while (true) { 283 Message message = mQueueWrapper.poll(); 284 if (message == null) { 285 break; 286 } 287 288 // Adjust the Message's delivery time. 289 long newWhen = message.when - milliSeconds; 290 if (newWhen < 0) { 291 newWhen = 0; 292 } 293 message.when = newWhen; 294 messages.add(message); 295 } 296 297 // Repost all Messages back to the queue with a new time. 298 while (true) { 299 Message message = messages.poll(); 300 if (message == null) { 301 break; 302 } 303 304 Runnable callback = message.getCallback(); 305 Handler handler = message.getTarget(); 306 long when = message.getWhen(); 307 308 // The Message cannot be re-enqueued because it is marked in use. 309 // Make a copy of the Message and recycle the original. 310 // This resets {@link Message#isInUse()} but retains all other content. 311 { 312 Message newMessage = Message.obtain(); 313 newMessage.copyFrom(message); 314 newMessage.setCallback(callback); 315 mQueueWrapper.recycle(message); 316 message = newMessage; 317 } 318 319 // Send the Message back to its Handler to be re-enqueued. 320 handler.sendMessageAtTime(message, when); 321 } 322 } 323 324 private void moveTimeForwardLegacy(long milliSeconds) { 325 try { 326 Message msg = (Message) MESSAGE_QUEUE_MESSAGES_FIELD.get(mLooper.getQueue()); 327 while (msg != null) { 328 long updatedWhen = msg.getWhen() - milliSeconds; 329 if (updatedWhen < 0) { 330 updatedWhen = 0; 331 } 332 MESSAGE_WHEN_FIELD.set(msg, updatedWhen); 333 msg = (Message) MESSAGE_NEXT_FIELD.get(msg); 334 } 335 } catch (IllegalAccessException e) { 336 throw new RuntimeException("Access failed in TestableLooper: set - Message.when", e); 337 } 338 } 339 340 private int processQueuedMessages() { 341 int count = 0; 342 Runnable barrierRunnable = () -> { }; 343 mHandler.post(barrierRunnable); 344 waitForMessage(mQueueWrapper, mHandler, barrierRunnable); 345 while (processSingleMessage(barrierRunnable)) count++; 346 return count; 347 } 348 349 private boolean processSingleMessage(Runnable barrierRunnable) { 350 try { 351 Message result = mQueueWrapper.next(); 352 if (result != null) { 353 // This is a break message. 354 if (result.getCallback() == barrierRunnable) { 355 mQueueWrapper.execute(result); 356 mQueueWrapper.recycle(result); 357 return false; 358 } 359 360 if (mMessageHandler != null) { 361 if (mMessageHandler.onMessageHandled(result)) { 362 mQueueWrapper.execute(result); 363 mQueueWrapper.recycle(result); 364 } else { 365 mQueueWrapper.recycle(result); 366 // Message handler indicated it doesn't want us to continue. 367 return false; 368 } 369 } else { 370 mQueueWrapper.execute(result); 371 mQueueWrapper.recycle(result); 372 } 373 } else { 374 // No messages, don't continue parsing 375 return false; 376 } 377 } catch (Exception e) { 378 throw new RuntimeException(e); 379 } 380 return true; 381 } 382 383 /** 384 * Runs an executable with myLooper set and processes all messages added. 385 */ 386 public void runWithLooper(RunnableWithException runnable) throws Exception { 387 new Handler(getLooper()).post(() -> { 388 try { 389 runnable.run(); 390 } catch (Exception e) { 391 throw new RuntimeException(e); 392 } 393 }); 394 processAllMessages(); 395 } 396 397 public interface RunnableWithException { 398 void run() throws Exception; 399 } 400 401 /** 402 * Annotation that tells the {@link AndroidTestingRunner} to create a TestableLooper and 403 * run this test/class on that thread. The {@link TestableLooper} can be acquired using 404 * {@link #get(Object)}. 405 */ 406 @Retention(RetentionPolicy.RUNTIME) 407 @Target({ElementType.METHOD, ElementType.TYPE}) 408 public @interface RunWithLooper { 409 boolean setAsMainLooper() default false; 410 } 411 412 private static void waitForMessage(TestLooperManager queueWrapper, Handler handler, 413 Runnable execute) { 414 for (int i = 0; i < 10; i++) { 415 if (!queueWrapper.hasMessages(handler, null, execute)) { 416 try { 417 Thread.sleep(1); 418 } catch (InterruptedException e) { 419 } 420 } 421 } 422 if (!queueWrapper.hasMessages(handler, null, execute)) { 423 throw new RuntimeException("Message didn't queue..."); 424 } 425 } 426 427 private static TestLooperManager acquireLooperManager(Looper l) { 428 if (HOLD_MAIN_THREAD && l == Looper.getMainLooper()) { 429 TestableInstrumentation.acquireMain(); 430 } 431 return InstrumentationRegistry.getInstrumentation().acquireLooperManager(l); 432 } 433 434 private static final Map<Object, TestableLooperHolder> sLoopers = new ArrayMap<>(); 435 436 /** 437 * For use with {@link RunWithLooper}, used to get the TestableLooper that was 438 * automatically created for this test. 439 */ 440 public static TestableLooper get(Object test) { 441 final TestableLooperHolder looperHolder = sLoopers.get(test); 442 return (looperHolder != null) ? looperHolder.mTestableLooper : null; 443 } 444 445 public static void remove(Object test) { 446 sLoopers.remove(test); 447 } 448 449 /** 450 * Holder object that contains {@link TestableLooper} so that its initialization can be 451 * deferred until a test case is actually run, instead of forcing it to be created at 452 * {@link FrameworkMethod} construction time. 453 * 454 * This deferral is important because some test environments may configure 455 * {@link Looper#getMainLooper()} as part of a {@code Rule} instead of assuming it's globally 456 * initialized and unconditionally available. 457 */ 458 private static class TestableLooperHolder { 459 private final boolean mSetAsMain; 460 private final Object mTest; 461 462 private TestableLooper mTestableLooper; 463 private Looper mLooper; 464 private Handler mHandler; 465 private HandlerThread mHandlerThread; 466 467 public TestableLooperHolder(boolean setAsMain, Object test) { 468 mSetAsMain = setAsMain; 469 mTest = test; 470 } 471 472 public void ensureInit() { 473 if (mLooper != null) return; 474 try { 475 mLooper = mSetAsMain ? Looper.getMainLooper() : createLooper(); 476 mTestableLooper = new TestableLooper(mLooper, false); 477 if (!mSetAsMain) { 478 mTestableLooper.getLooper().getThread().setName(mTest.getClass().getName()); 479 } 480 } catch (Exception e) { 481 throw new RuntimeException(e); 482 } 483 mHandler = new Handler(mLooper); 484 } 485 486 private Looper createLooper() { 487 // TODO: Find way to share these. 488 mHandlerThread = new HandlerThread(TestableLooper.class.getSimpleName()); 489 mHandlerThread.start(); 490 return mHandlerThread.getLooper(); 491 } 492 } 493 494 static class LooperFrameworkMethod extends FrameworkMethod { 495 private TestableLooperHolder mLooperHolder; 496 497 public LooperFrameworkMethod(FrameworkMethod base, TestableLooperHolder looperHolder) { 498 super(base.getMethod()); 499 mLooperHolder = looperHolder; 500 } 501 502 public static FrameworkMethod get(FrameworkMethod base, boolean setAsMain, Object test) { 503 TestableLooperHolder looperHolder = sLoopers.get(test); 504 if (looperHolder == null) { 505 looperHolder = new TestableLooperHolder(setAsMain, test); 506 sLoopers.put(test, looperHolder); 507 } 508 return new LooperFrameworkMethod(base, looperHolder); 509 } 510 511 @Override 512 public Object invokeExplosively(Object target, Object... params) throws Throwable { 513 mLooperHolder.ensureInit(); 514 if (Looper.myLooper() == mLooperHolder.mLooper) { 515 // Already on the right thread from another statement, just execute then. 516 return super.invokeExplosively(target, params); 517 } 518 boolean set = mLooperHolder.mTestableLooper.mQueueWrapper == null; 519 if (set) { 520 mLooperHolder.mTestableLooper.mQueueWrapper = acquireLooperManager( 521 mLooperHolder.mLooper); 522 } 523 try { 524 Object[] ret = new Object[1]; 525 // Run the execution on the looper thread. 526 Runnable execute = () -> { 527 try { 528 ret[0] = super.invokeExplosively(target, params); 529 } catch (Throwable throwable) { 530 throw new LooperException(throwable); 531 } 532 }; 533 Message m = Message.obtain(mLooperHolder.mHandler, execute); 534 535 // Dispatch our message. 536 try { 537 mLooperHolder.mTestableLooper.mQueueWrapper.execute(m); 538 } catch (LooperException e) { 539 throw e.getSource(); 540 } catch (RuntimeException re) { 541 // If the TestLooperManager has to post, it will wrap what it throws in a 542 // RuntimeException, make sure we grab the actual source. 543 if (re.getCause() instanceof LooperException) { 544 throw ((LooperException) re.getCause()).getSource(); 545 } else { 546 throw re.getCause(); 547 } 548 } finally { 549 m.recycle(); 550 } 551 return ret[0]; 552 } finally { 553 if (set) { 554 mLooperHolder.mTestableLooper.mQueueWrapper.release(); 555 mLooperHolder.mTestableLooper.mQueueWrapper = null; 556 if (HOLD_MAIN_THREAD && mLooperHolder.mLooper == Looper.getMainLooper()) { 557 TestableInstrumentation.releaseMain(); 558 } 559 } 560 } 561 } 562 563 @Override 564 protected void finalize() throws Throwable { 565 super.finalize(); 566 if (mLooperHolder.mHandlerThread != null) { 567 mLooperHolder.mHandlerThread.quit(); 568 } 569 } 570 571 private static class LooperException extends RuntimeException { 572 private final Throwable mSource; 573 574 public LooperException(Throwable t) { 575 mSource = t; 576 } 577 578 public Throwable getSource() { 579 return mSource; 580 } 581 } 582 } 583 584 /** 585 * Callback to control the execution of messages on the looper, when set with 586 * {@link #setMessageHandler(MessageHandler)} then {@link #onMessageHandled(Message)} 587 * will get called back for every message processed on the {@link TestableLooper}. 588 */ 589 public interface MessageHandler { 590 /** 591 * Return true to have the message executed and delivered to target. 592 * Return false to not execute the message and stop executing messages. 593 */ 594 boolean onMessageHandled(Message m); 595 } 596 } 597