All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.robolectric.shadows.ShadowPausedLooper Maven / Gradle / Ivy

package org.robolectric.shadows;

import static org.robolectric.shadow.api.Shadow.directlyOn;
import static org.robolectric.shadow.api.Shadow.invokeConstructor;
import static org.robolectric.util.ReflectionHelpers.ClassParameter.from;

import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
import android.util.Log;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.LooperMode;
import org.robolectric.annotation.RealObject;
import org.robolectric.annotation.Resetter;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.util.Scheduler;

/**
 * The shadow Looper for {@link LooperMode.Mode.PAUSED}.
 *
 * This shadow differs from the legacy {@link ShadowLegacyLooper} in the following ways:\
 *   - Has no connection to {@link org.robolectric.util.Scheduler}. Its APIs are standalone
 *   - The main looper is always paused. Posted messages are not executed unless {@link #idle()} is
 *     called.
 *   - Just like in real Android, each looper has its own thread, and posted tasks get executed in
 *   that thread. -
 *   - There is only a single {@link SystemClock} value that all loopers read from. Unlike legacy
 *     behavior where each {@link org.robolectric.util.Scheduler} kept their own clock value.
 *
 * This class should not be used directly; use {@link ShadowLooper} instead.
 */
@Implements(
    value = Looper.class,
    // turn off shadowOf generation.
    isInAndroidSdk = false)
@SuppressWarnings("NewApi")
public final class ShadowPausedLooper extends ShadowLooper {

  // Keep reference to all created Loopers so they can be torn down after test
  private static Set loopingLoopers =
      Collections.synchronizedSet(Collections.newSetFromMap(new WeakHashMap()));

  @RealObject private Looper realLooper;
  private boolean isPaused = false;
  // the Executor that executes looper messages. Must be written to on looper thread
  private Executor looperExecutor;

  @Implementation
  protected void __constructor__(boolean quitAllowed) {
    invokeConstructor(Looper.class, realLooper, from(boolean.class, quitAllowed));

    loopingLoopers.add(realLooper);
    looperExecutor = new HandlerExecutor(new Handler(realLooper));
  }

  @Override
  public void quitUnchecked() {
    throw new UnsupportedOperationException("this action is not"
                                                + " supported"
                                                + " in " + looperMode() + " mode.");
  }

  @Override
  public boolean hasQuit() {
    throw new UnsupportedOperationException("this action is not"
                                                + " supported"
                                                + " in " + looperMode() + " mode.");
  }

  @Override
  public void idle() {
    executeOnLooper(new IdlingRunnable());
  }

  @Override
  public void idleFor(long time, TimeUnit timeUnit) {
    long endingTimeMs = SystemClock.uptimeMillis() + timeUnit.toMillis(time);
    long nextScheduledTimeMs = getNextScheduledTaskTime().toMillis();
    while (nextScheduledTimeMs != 0 && nextScheduledTimeMs <= endingTimeMs) {
      SystemClock.setCurrentTimeMillis(nextScheduledTimeMs);
      idle();
      nextScheduledTimeMs = getNextScheduledTaskTime().toMillis();
    }
    SystemClock.setCurrentTimeMillis(endingTimeMs);
  }

  @Override
  public boolean isIdle() {
    if (Thread.currentThread() == realLooper.getThread() || isPaused) {
      return shadowQueue().isIdle();
    } else {
      return shadowQueue().isIdle() && shadowQueue().isPolling();
    }
  }

  @Override
  public void unPause() {
    if (realLooper == Looper.getMainLooper()) {
      throw new UnsupportedOperationException("main looper cannot be unpaused");
    }
    executeOnLooper(new UnPauseRunnable());
  }

  @Override
  public void pause() {
    if (!isPaused()) {
      executeOnLooper(new PausedLooperExecutor());
    }
  }

  @Override
  public boolean isPaused() {
    return isPaused;
  }

  @Override
  public boolean setPaused(boolean shouldPause) {
    if (shouldPause) {
      pause();
    } else {
      unPause();
    }
    return true;
  }

  @Override
  public void resetScheduler() {
    throw new UnsupportedOperationException("this action is not"
                                                + " supported"
                                                + " in " + looperMode() + " mode.");
  }

  @Override
  public void reset() {
    throw new UnsupportedOperationException("this action is not"
                                                + " supported"
                                                + " in " + looperMode() + " mode.");
  }

  @Override
  public void idleIfPaused() {
    idle();
  }

  @Override
  public void idleConstantly(boolean shouldIdleConstantly) {
    throw new UnsupportedOperationException("this action is not"
                                                + " supported"
                                                + " in " + looperMode() + " mode.");
  }

  @Override
  public void runToEndOfTasks() {
    idleFor(getLastScheduledTaskTime().toMillis() - SystemClock.uptimeMillis(),
        TimeUnit.MILLISECONDS);
  }

  @Override
  public void runToNextTask() {
    idleFor(getNextScheduledTaskTime().toMillis() - SystemClock.uptimeMillis(),
        TimeUnit.MILLISECONDS);
  }

  @Override
  public void runOneTask() {
    executeOnLooper(new RunOneRunnable());
  }

  @Override
  public boolean post(Runnable runnable, long delayMillis) {
    return new Handler(realLooper).postDelayed(runnable, delayMillis);
  }

  @Override
  public boolean postAtFrontOfQueue(Runnable runnable) {
    return new Handler(realLooper).postAtFrontOfQueue(runnable);
  }

  // this API doesn't make sense in LooperMode.PAUSED, but just retain it for backwards
  // compatibility for now
  @Override
  public void runPaused(Runnable runnable) {
    if (isPaused && Thread.currentThread() == realLooper.getThread()) {
      // just run
      runnable.run();
    } else {
      throw new UnsupportedOperationException();
    }
  }

  @Override
  public Duration getNextScheduledTaskTime() {
    return shadowQueue().getNextScheduledTaskTime();
  }

  @Override
  public Duration getLastScheduledTaskTime() {
    return shadowQueue().getLastScheduledTaskTime();
  }

  @Resetter
  public static synchronized void resetLoopers() {
    if (looperMode() != LooperMode.Mode.PAUSED) {
      // ignore if not realistic looper
      return;
    }

    Collection loopersCopy = new ArrayList(loopingLoopers);
    for (Looper looper : loopersCopy) {
      ShadowPausedMessageQueue shadowQueue = Shadow.extract(looper.getQueue());
      if (shadowQueue.isQuitAllowed()) {
        looper.quit();
        loopingLoopers.remove(looper);
      } else {
        shadowQueue.reset();
      }
    }
  }

  @Implementation
  protected static void prepareMainLooper() {
    directlyOn(Looper.class, "prepareMainLooper");
    ShadowPausedLooper pausedLooper = Shadow.extract(Looper.getMainLooper());
    pausedLooper.isPaused = true;
  }

  @Implementation
  protected void quit() {
    if (isPaused()) {
      executeOnLooper(new UnPauseRunnable());
    }
    directlyOn(realLooper, Looper.class, "quit");
  }

  @Implementation(minSdk = Build.VERSION_CODES.JELLY_BEAN_MR2)
  protected void quitSafely() {
    if (isPaused()) {
      executeOnLooper(new UnPauseRunnable());
    }
    directlyOn(realLooper, Looper.class, "quitSafely");
  }

  @Override
  public Scheduler getScheduler() {
    throw new UnsupportedOperationException(
        String.format("this action is not supported in %s mode.", looperMode()));
  }

  private static ShadowPausedMessage shadowMsg(Message msg) {
    return Shadow.extract(msg);
  }

  private ShadowPausedMessageQueue shadowQueue() {
    return Shadow.extract(realLooper.getQueue());
  }

  /** Executes the given runnable on the loopers thread, and waits for it to complete. */
  private void executeOnLooper(ControlRunnable runnable) {
    if (Thread.currentThread() == realLooper.getThread()) {
      runnable.run();
    } else {
      if (realLooper.equals(Looper.getMainLooper())) {
        throw new UnsupportedOperationException(
            "main looper can only be controlled from main thread");
      }
      looperExecutor.execute(runnable);
      runnable.waitTillComplete();
    }
  }

  private void setLooperExecutor(Executor executor) {
    looperExecutor = executor;
  }

  /** A runnable that changes looper state, and that must be run from looper's thread */
  private abstract static class ControlRunnable implements Runnable {

    protected final CountDownLatch runLatch = new CountDownLatch(1);

    public void waitTillComplete() {
      try {
        runLatch.await();
      } catch (InterruptedException e) {
        Log.w("ShadowPausedLooper", "wait till idle interrupted");
      }
    }
  }

  private class IdlingRunnable extends ControlRunnable {

    @Override
    public void run() {
      while (!shadowQueue().isIdle()) {
        Message msg = shadowQueue().getNext();
        msg.getTarget().dispatchMessage(msg);
        shadowMsg(msg).recycleUnchecked();
      }
      runLatch.countDown();
    }
  }

  private class RunOneRunnable extends ControlRunnable {

    @Override
    public void run() {
      Message msg = shadowQueue().poll();
      if (msg != null) {
        SystemClock.setCurrentTimeMillis(shadowMsg(msg).getWhen());
        msg.getTarget().dispatchMessage(msg);
      }
      runLatch.countDown();
    }
  }

  /**
   * A runnable that will block normal looper execution of messages aka will 'pause' the looper.
   *
   * 

Message execution can be triggered by posting messages to this runnable. */ private class PausedLooperExecutor extends ControlRunnable implements Executor { private final LinkedBlockingQueue executionQueue = new LinkedBlockingQueue<>(); @Override public void execute(Runnable runnable) { executionQueue.add(runnable); } @Override public void run() { setLooperExecutor(this); isPaused = true; runLatch.countDown(); while (true) { try { Runnable runnable = executionQueue.take(); runnable.run(); if (runnable instanceof UnPauseRunnable) { setLooperExecutor(new HandlerExecutor(new Handler(realLooper))); return; } } catch (InterruptedException e) { // ignore } } } } private class UnPauseRunnable extends ControlRunnable { @Override public void run() { isPaused = false; runLatch.countDown(); } } private static class HandlerExecutor implements Executor { private final Handler handler; private HandlerExecutor(Handler handler) { this.handler = handler; } @Override public void execute(Runnable runnable) { handler.post(runnable); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy