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

io.selendroid.UiThreadController Maven / Gradle / Ivy

package io.selendroid;

import io.selendroid.ServerInstrumentation.InputEventSender;
import io.selendroid.exceptions.SelendroidException;

import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.MessageQueue;
import android.os.SystemClock;

import java.lang.reflect.Method;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;

import static io.selendroid.util.Preconditions.checkState;
import static java.util.concurrent.TimeUnit.MINUTES;

/**
 * Provides facility to inject base-level UI operations (such as KeyEvent's and MotionEvent's)
 * with more advanced synchronization than android Instrumentation class does.
 *
 * The key point is to loop UI thread 'manually', while waiting for the injection to be complete.
 * This allows it to fetch and propagate unhandled exceptions, occurring in application under test
 * and causing a crash, to the local end.
 */
public class UiThreadController implements Handler.Callback {

  private static final int INPUT_EVENT_INJECTION_COMPLETED = 0x1;

  private static final Method MESSAGE_QUEUE_NEXT;
  private final ExecutorService keyEventExecutor = Executors.newSingleThreadExecutor();
  private final Looper mainLooper = Looper.getMainLooper();
  private Handler controllerHandler = new Handler(this);
  private boolean shouldWaitForInputEventCompletion;
  private boolean loopingUiThread;
  private int messageGeneration;

  static {
    try {
      MESSAGE_QUEUE_NEXT = MessageQueue.class.getDeclaredMethod("next");
      MESSAGE_QUEUE_NEXT.setAccessible(true);
    } catch (Exception e) {
      throw new SelendroidException("Unable to fetch MessageQueue.next() method.");
    }
  }

  public void injectInputEventWaitingForCompletion(final InputEventSender eventSender,
                                                   final Object event) {
    // We really do need instance equality
    checkState(Looper.myLooper() == mainLooper, "Should be called on UI thread!");

    final FutureTask injectTask = new InjectingTask(
        new Callable() {
          @Override
          public Void call() throws Exception {
            eventSender.sendEvent(event);
            return null;
          }
        },
        messageGeneration);

    keyEventExecutor.submit(injectTask);
    loopUiThreadUntilInjectionComplete();

    try {
      injectTask.get();
    } catch (Exception e) {
      throw new SelendroidException("Event injection failed.", e);
    }
  }

  @Override
  public boolean handleMessage(Message message) {
    if (message.what == INPUT_EVENT_INJECTION_COMPLETED && message.arg1 == messageGeneration) {
      shouldWaitForInputEventCompletion = false;
      return true;
    }
    return false;
  }

  private void loopUiThreadUntilInjectionComplete() {
    checkState(!loopingUiThread, "Already looping UI thread.");
    loopingUiThread = true;
    shouldWaitForInputEventCompletion = true;
    try {
      long waitUntil = SystemClock.uptimeMillis() + MINUTES.toMillis(1);
      while (SystemClock.uptimeMillis() < waitUntil) {
        if (!shouldWaitForInputEventCompletion) {
          return;
        }
        Message message = invokeMessageQueueNextMethod();
        message.getTarget().dispatchMessage(message);
        message.recycle();
      }
    } finally {
      loopingUiThread = false;
      shouldWaitForInputEventCompletion = false;
      messageGeneration++;
    }
  }

  private Message invokeMessageQueueNextMethod() {
    try {
      return (Message) MESSAGE_QUEUE_NEXT.invoke(Looper.myQueue());
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Encapsulates posting a signal message to indicate that
   * injection has executed.
   */
  private class InjectingTask extends FutureTask {

    private final int myGeneration;

    public InjectingTask(Callable callable, int myGeneration) {
      super(callable);
      this.myGeneration = myGeneration;
    }

    @Override
    protected void done() {
      controllerHandler.sendMessage(Message.obtain(controllerHandler,
          INPUT_EVENT_INJECTION_COMPLETED, myGeneration, 0, null));
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy