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 if (!setAsMain) { 242 mTestableLooper.getLooper().getThread().setName(test.getClass().getName()); 243 } 244 } catch (Exception e) { 245 throw new RuntimeException(e); 246 } 247 sLoopers.put(test, mTestableLooper); 248 mHandler = new Handler(mLooper); 249 } 250 LooperFrameworkMethod(TestableLooper other, FrameworkMethod base)251 public LooperFrameworkMethod(TestableLooper other, FrameworkMethod base) { 252 super(base.getMethod()); 253 mLooper = other.mLooper; 254 mTestableLooper = other; 255 mHandler = Handler.createAsync(mLooper); 256 } 257 get(FrameworkMethod base, boolean setAsMain, Object test)258 public static FrameworkMethod get(FrameworkMethod base, boolean setAsMain, Object test) { 259 if (sLoopers.containsKey(test)) { 260 return new LooperFrameworkMethod(sLoopers.get(test), base); 261 } 262 return new LooperFrameworkMethod(base, setAsMain, test); 263 } 264 265 @Override invokeExplosively(Object target, Object... params)266 public Object invokeExplosively(Object target, Object... params) throws Throwable { 267 if (Looper.myLooper() == mLooper) { 268 // Already on the right thread from another statement, just execute then. 269 return super.invokeExplosively(target, params); 270 } 271 boolean set = mTestableLooper.mQueueWrapper == null; 272 if (set) { 273 mTestableLooper.mQueueWrapper = acquireLooperManager(mLooper); 274 } 275 try { 276 Object[] ret = new Object[1]; 277 // Run the execution on the looper thread. 278 Runnable execute = () -> { 279 try { 280 ret[0] = super.invokeExplosively(target, params); 281 } catch (Throwable throwable) { 282 throw new LooperException(throwable); 283 } 284 }; 285 Message m = Message.obtain(mHandler, execute); 286 287 // Dispatch our message. 288 try { 289 mTestableLooper.mQueueWrapper.execute(m); 290 } catch (LooperException e) { 291 throw e.getSource(); 292 } catch (RuntimeException re) { 293 // If the TestLooperManager has to post, it will wrap what it throws in a 294 // RuntimeException, make sure we grab the actual source. 295 if (re.getCause() instanceof LooperException) { 296 throw ((LooperException) re.getCause()).getSource(); 297 } else { 298 throw re.getCause(); 299 } 300 } finally { 301 m.recycle(); 302 } 303 return ret[0]; 304 } finally { 305 if (set) { 306 mTestableLooper.mQueueWrapper.release(); 307 mTestableLooper.mQueueWrapper = null; 308 if (HOLD_MAIN_THREAD && mLooper == Looper.getMainLooper()) { 309 TestableInstrumentation.releaseMain(); 310 } 311 } 312 } 313 } 314 createLooper()315 private Looper createLooper() { 316 // TODO: Find way to share these. 317 mHandlerThread = new HandlerThread(TestableLooper.class.getSimpleName()); 318 mHandlerThread.start(); 319 return mHandlerThread.getLooper(); 320 } 321 322 @Override finalize()323 protected void finalize() throws Throwable { 324 super.finalize(); 325 if (mHandlerThread != null) { 326 mHandlerThread.quit(); 327 } 328 } 329 330 private static class LooperException extends RuntimeException { 331 private final Throwable mSource; 332 LooperException(Throwable t)333 public LooperException(Throwable t) { 334 mSource = t; 335 } 336 getSource()337 public Throwable getSource() { 338 return mSource; 339 } 340 } 341 } 342 343 /** 344 * Callback to control the execution of messages on the looper, when set with 345 * {@link #setMessageHandler(MessageHandler)} then {@link #onMessageHandled(Message)} 346 * will get called back for every message processed on the {@link TestableLooper}. 347 */ 348 public interface MessageHandler { 349 /** 350 * Return true to have the message executed and delivered to target. 351 * Return false to not execute the message and stop executing messages. 352 */ onMessageHandled(Message m)353 boolean onMessageHandled(Message m); 354 } 355 } 356