blob: dc7346cc872f8c64e9800cfb2fb32f49a79a4edb [file] [log] [blame]
package org.robolectric.shadows;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2;
import static org.robolectric.RuntimeEnvironment.isMainThread;
import static org.robolectric.shadow.api.Shadow.invokeConstructor;
import static org.robolectric.util.ReflectionHelpers.ClassParameter.from;
import android.os.Looper;
import android.os.MessageQueue;
import java.util.Collections;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.TimeUnit;
import org.robolectric.RoboSettings;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.annotation.Resetter;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.util.Scheduler;
/**
* Robolectric enqueues posted {@link Runnable}s to be run
* (on this thread) later. {@code Runnable}s that are scheduled to run immediately can be
* triggered by calling {@link #idle()}.
*
* @see ShadowMessageQueue
*/
@Implements(Looper.class)
@SuppressWarnings("SynchronizeOnNonFinalField")
public class ShadowLooper {
// Replaced SoftThreadLocal with a WeakHashMap, because ThreadLocal make it impossible to access their contents from other
// threads, but we need to be able to access the loopers for all threads so that we can shut them down when resetThreadLoopers()
// is called. This also allows us to implement the useful getLooperForThread() method.
// Note that the main looper is handled differently and is not put in this hash, because we need to be able to
// "switch" the thread that the main looper is associated with.
private static Map<Thread, Looper> loopingLoopers = Collections.synchronizedMap(new WeakHashMap<Thread, Looper>());
private static Looper mainLooper;
private @RealObject Looper realObject;
boolean quit;
@Resetter
public static synchronized void resetThreadLoopers() {
// Blech. We need to keep the main looper because somebody might refer to it in a static
// field. The other loopers need to be wrapped in WeakReferences so that they are not prevented from
// being garbage collected.
if (!isMainThread()) {
throw new IllegalStateException("you should only be calling this from the main thread!");
}
synchronized (loopingLoopers) {
for (Looper looper : loopingLoopers.values()) {
synchronized (looper) {
if (!shadowOf(looper).quit) {
looper.quit();
} else {
// Reset the schedulers of all loopers. This prevents un-run tasks queued up in static
// background handlers from leaking to subsequent tests.
shadowOf(looper).getScheduler().reset();
shadowOf(looper.getQueue()).reset();
}
}
}
}
// Because resetStaticState() is called by ParallelUniverse on startup before prepareMainLooper() is
// called, this might be null on that occasion.
if (mainLooper != null) {
shadowOf(mainLooper).reset();
}
}
@Implementation
protected void __constructor__(boolean quitAllowed) {
invokeConstructor(Looper.class, realObject, from(boolean.class, quitAllowed));
if (isMainThread()) {
mainLooper = realObject;
} else {
loopingLoopers.put(Thread.currentThread(), realObject);
}
resetScheduler();
}
@Implementation
protected static Looper getMainLooper() {
return mainLooper;
}
@Implementation
protected static Looper myLooper() {
return getLooperForThread(Thread.currentThread());
}
@Implementation
protected static void loop() {
shadowOf(Looper.myLooper()).doLoop();
}
private void doLoop() {
if (realObject != Looper.getMainLooper()) {
synchronized (realObject) {
while (!quit) {
try {
realObject.wait();
} catch (InterruptedException ignore) {
}
}
}
}
}
@Implementation
protected void quit() {
if (realObject == Looper.getMainLooper()) throw new RuntimeException("Main thread not allowed to quit");
quitUnchecked();
}
@Implementation(minSdk = JELLY_BEAN_MR2)
protected void quitSafely() {
quit();
}
public void quitUnchecked() {
synchronized (realObject) {
quit = true;
realObject.notifyAll();
getScheduler().reset();
shadowOf(realObject.getQueue()).reset();
}
}
public boolean hasQuit() {
synchronized (realObject) {
return quit;
}
}
/** @deprecated Use `shadowOf({@link Looper#getMainLooper()})` instead. */
@Deprecated
public static ShadowLooper getShadowMainLooper() {
return shadowOf(Looper.getMainLooper());
}
public static Looper getLooperForThread(Thread thread) {
return isMainThread(thread) ? mainLooper : loopingLoopers.get(thread);
}
/**
* Pauses execution of tasks posted to the ShadowLooper. This means that during tests, tasks sent
* to the looper will not execute immediately, but will be queued in a way that is similar to how
* a real looper works. These queued tasks must be executed explicitly by calling {@link
* #runToEndOftasks} or a similar method, otherwise they will not run at all before your test
* ends.
*
* @param looper the looper to pause
*/
public static void pauseLooper(Looper looper) {
shadowOf(looper).pause();
}
/**
* Puts the shadow looper in an "unpaused" state (this is the default state). This means that
* during tests, tasks sent to the looper will execute inline, immediately, on the calling (main)
* thread instead of being queued, in a way similar to how Guava's "DirectExecutorService" works.
* This is likely not to be what you want: it will cause code to be potentially executed in a
* different order than how it would execute on the device, and if you are using certain Android
* APIs (such as view animations) that are non-reentrant, they may not work at all or do
* unpredictable things. For more information, see <a
* href="https://github.com/robolectric/robolectric/issues/3369">this discussion</a>.
*
* @param looper the looper to pause
*/
public static void unPauseLooper(Looper looper) {
shadowOf(looper).unPause();
}
/**
* Puts the main ShadowLooper in an "paused" state.
*
* @see #pauseLooper
*/
public static void pauseMainLooper() {
getShadowMainLooper().pause();
}
/**
* Puts the main ShadowLooper in an "unpaused" state.
*
* @see #unPauseLooper
*/
public static void unPauseMainLooper() {
getShadowMainLooper().unPause();
}
public static void idleMainLooper() {
getShadowMainLooper().idle();
}
/** @deprecated Use {@link #idleMainLooper(long, TimeUnit)}. */
@Deprecated
public static void idleMainLooper(long interval) {
idleMainLooper(interval, TimeUnit.MILLISECONDS);
}
public static void idleMainLooper(long amount, TimeUnit unit) {
getShadowMainLooper().idle(amount, unit);
}
public static void idleMainLooperConstantly(boolean shouldIdleConstantly) {
getShadowMainLooper().idleConstantly(shouldIdleConstantly);
}
public static void runMainLooperOneTask() {
getShadowMainLooper().runOneTask();
}
public static void runMainLooperToNextTask() {
getShadowMainLooper().runToNextTask();
}
/**
* Runs any immediately runnable tasks previously queued on the UI thread,
* e.g. by {@link android.app.Activity#runOnUiThread(Runnable)} or {@link android.os.AsyncTask#onPostExecute(Object)}.
*
* **Note:** calling this method does not pause or un-pause the scheduler.
*
* @see #runUiThreadTasksIncludingDelayedTasks
*/
public static void runUiThreadTasks() {
getShadowMainLooper().idle();
}
/**
* Runs all runnable tasks (pending and future) that have been queued on the UI thread. Such tasks may be queued by
* e.g. {@link android.app.Activity#runOnUiThread(Runnable)} or {@link android.os.AsyncTask#onPostExecute(Object)}.
*
* **Note:** calling this method does not pause or un-pause the scheduler, however the clock is advanced as
* future tasks are run.
*
* @see #runUiThreadTasks
*/
public static void runUiThreadTasksIncludingDelayedTasks() {
getShadowMainLooper().runToEndOfTasks();
}
/**
* Causes {@link Runnable}s that have been scheduled to run immediately to actually run. Does not advance the
* scheduler's clock;
*/
public void idle() {
idle(0, TimeUnit.MILLISECONDS);
}
/**
* Causes {@link Runnable}s that have been scheduled to run within the next {@code intervalMillis} milliseconds to
* run while advancing the scheduler's clock.
*
* @deprecated Use {@link #idle(long, TimeUnit)}.
*/
@Deprecated
public void idle(long intervalMillis) {
idle(intervalMillis, TimeUnit.MILLISECONDS);
}
/**
* Causes {@link Runnable}s that have been scheduled to run within the next specified amount of time to run while
* advancing the scheduler's clock.
*/
public void idle(long amount, TimeUnit unit) {
getScheduler().advanceBy(amount, unit);
}
public void idleConstantly(boolean shouldIdleConstantly) {
getScheduler().idleConstantly(shouldIdleConstantly);
}
/**
* Causes all of the {@link Runnable}s that have been scheduled to run while advancing the scheduler's clock to the
* start time of the last scheduled {@link Runnable}.
*/
public void runToEndOfTasks() {
getScheduler().advanceToLastPostedRunnable();
}
/**
* Causes the next {@link Runnable}(s) that have been scheduled to run while advancing the scheduler's clock to its
* start time. If more than one {@link Runnable} is scheduled to run at this time then they will all be run.
*/
public void runToNextTask() {
getScheduler().advanceToNextPostedRunnable();
}
/**
* Causes only one of the next {@link Runnable}s that have been scheduled to run while advancing the scheduler's
* clock to its start time. Only one {@link Runnable} will run even if more than one has ben scheduled to run at the
* same time.
*/
public void runOneTask() {
getScheduler().runOneTask();
}
/**
* Enqueue a task to be run later.
*
* @param runnable the task to be run
* @param delayMillis how many milliseconds into the (virtual) future to run it
* @return true if the runnable is enqueued
* @see android.os.Handler#postDelayed(Runnable,long)
* @deprecated Use a {@link android.os.Handler} instance to post to a looper.
*/
@Deprecated
public boolean post(Runnable runnable, long delayMillis) {
if (!quit) {
getScheduler().postDelayed(runnable, delayMillis, TimeUnit.MILLISECONDS);
return true;
} else {
return false;
}
}
/**
* Enqueue a task to be run ahead of all other delayed tasks.
*
* @param runnable the task to be run
* @return true if the runnable is enqueued
* @see android.os.Handler#postAtFrontOfQueue(Runnable)
* @deprecated Use a {@link android.os.Handler} instance to post to a looper.
*/
@Deprecated
public boolean postAtFrontOfQueue(Runnable runnable) {
if (!quit) {
getScheduler().postAtFrontOfQueue(runnable);
return true;
} else {
return false;
}
}
public void pause() {
getScheduler().pause();
}
public void unPause() {
getScheduler().unPause();
}
public boolean isPaused() {
return getScheduler().isPaused();
}
public boolean setPaused(boolean shouldPause) {
boolean wasPaused = isPaused();
if (shouldPause) {
pause();
} else {
unPause();
}
return wasPaused;
}
public void resetScheduler() {
ShadowMessageQueue shadowMessageQueue = shadowOf(realObject.getQueue());
if (realObject == Looper.getMainLooper() || RoboSettings.isUseGlobalScheduler()) {
shadowMessageQueue.setScheduler(RuntimeEnvironment.getMasterScheduler());
} else {
shadowMessageQueue.setScheduler(new Scheduler());
}
}
/**
* Causes all enqueued tasks to be discarded, and pause state to be reset
*/
public void reset() {
shadowOf(realObject.getQueue()).reset();
resetScheduler();
quit = false;
}
/**
* Returns the {@link org.robolectric.util.Scheduler} that is being used to manage the enqueued tasks.
* This scheduler is managed by the Looper's associated queue.
*
* @return the {@link org.robolectric.util.Scheduler} that is being used to manage the enqueued tasks.
*/
public Scheduler getScheduler() {
return shadowOf(realObject.getQueue()).getScheduler();
}
public void runPaused(Runnable r) {
boolean wasPaused = setPaused(true);
try {
r.run();
} finally {
if (!wasPaused) unPause();
}
}
private static ShadowLooper shadowOf(Looper looper) {
return Shadow.extract(looper);
}
private static ShadowMessageQueue shadowOf(MessageQueue mq) {
return Shadow.extract(mq);
}
}