blob: c1aaa5c720962a8dcdb6a5eec4b4de8e503c390f [file] [log] [blame]
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.apps.common.testing.ui.espresso.base;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Throwables.propagate;
import com.google.android.apps.common.testing.ui.espresso.IdlingPolicies;
import com.google.android.apps.common.testing.ui.espresso.IdlingPolicy;
import com.google.android.apps.common.testing.ui.espresso.InjectEventSecurityException;
import com.google.android.apps.common.testing.ui.espresso.UiController;
import com.google.android.apps.common.testing.ui.espresso.base.IdlingResourceRegistry.IdleNotificationCallback;
import com.google.android.apps.common.testing.ui.espresso.base.QueueInterrogator.QueueState;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.google.common.collect.Lists;
import android.annotation.SuppressLint;
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 android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.MotionEvent;
import java.util.BitSet;
import java.util.EnumSet;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
import javax.inject.Inject;
import javax.inject.Singleton;
/**
* Implementation of {@link UiController}.
*/
@Singleton
final class UiControllerImpl implements UiController, Handler.Callback {
private static final String TAG = UiControllerImpl.class.getSimpleName();
private static final Callable<Void> NO_OP = new Callable<Void>() {
@Override
public Void call() {
return null;
}
};
/**
* Responsible for signaling a particular condition is met / verifying that signal.
*/
enum IdleCondition {
DELAY_HAS_PAST,
ASYNC_TASKS_HAVE_IDLED,
COMPAT_TASKS_HAVE_IDLED,
KEY_INJECT_HAS_COMPLETED,
MOTION_INJECTION_HAS_COMPLETED,
DYNAMIC_TASKS_HAVE_IDLED;
/**
* Checks whether this condition has been signaled.
*/
public boolean isSignaled(BitSet conditionSet) {
return conditionSet.get(ordinal());
}
/**
* Resets the signal state for this condition.
*/
public void reset(BitSet conditionSet) {
conditionSet.set(ordinal(), false);
}
/**
* Creates a message that when sent will raise the signal of this condition.
*/
public Message createSignal(Handler handler, int myGeneration) {
return Message.obtain(handler, ordinal(), myGeneration, 0, null);
}
/**
* Handles a message that is raising a signal and updates the condition set accordingly.
* Messages from a previous generation will be ignored.
*/
public static boolean handleMessage(Message message, BitSet conditionSet,
int currentGeneration) {
IdleCondition [] allConditions = values();
if (message.what < 0 || message.what >= allConditions.length) {
return false;
} else {
IdleCondition condition = allConditions[message.what];
if (message.arg1 == currentGeneration) {
condition.signal(conditionSet);
} else {
Log.w(TAG, "ignoring signal of: " + condition + " from previous generation: " +
message.arg1 + " current generation: " + currentGeneration);
}
return true;
}
}
public static BitSet createConditionSet() {
return new BitSet(values().length);
}
/**
* Requests that the given bitset be updated to indicate that this condition has been
* signaled.
*/
protected void signal(BitSet conditionSet) {
conditionSet.set(ordinal());
}
}
private final EventInjector eventInjector;
private final BitSet conditionSet;
private final AsyncTaskPoolMonitor asyncTaskMonitor;
private final Optional<AsyncTaskPoolMonitor> compatTaskMonitor;
private final IdlingResourceRegistry idlingResourceRegistry;
private final ExecutorService keyEventExecutor = Executors.newSingleThreadExecutor();
private final QueueInterrogator queueInterrogator;
private final Looper mainLooper;
private Handler controllerHandler;
// only updated on main thread.
private boolean looping = false;
private int generation = 0;
@VisibleForTesting
@Inject
UiControllerImpl(EventInjector eventInjector,
@SdkAsyncTask AsyncTaskPoolMonitor asyncTaskMonitor,
@CompatAsyncTask Optional<AsyncTaskPoolMonitor> compatTaskMonitor,
IdlingResourceRegistry registry,
Looper mainLooper) {
this.eventInjector = checkNotNull(eventInjector);
this.asyncTaskMonitor = checkNotNull(asyncTaskMonitor);
this.compatTaskMonitor = checkNotNull(compatTaskMonitor);
this.conditionSet = IdleCondition.createConditionSet();
this.idlingResourceRegistry = checkNotNull(registry);
this.mainLooper = checkNotNull(mainLooper);
this.queueInterrogator = new QueueInterrogator(mainLooper);
}
@SuppressWarnings("deprecation")
@Override
public boolean injectKeyEvent(final KeyEvent event) throws InjectEventSecurityException {
checkNotNull(event);
checkState(Looper.myLooper() == mainLooper, "Expecting to be on main thread!");
initialize();
loopMainThreadUntilIdle();
FutureTask<Boolean> injectTask = new SignalingTask<Boolean>(
new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
return eventInjector.injectKeyEvent(event);
}
},
IdleCondition.KEY_INJECT_HAS_COMPLETED,
generation);
// Inject the key event.
keyEventExecutor.submit(injectTask);
loopUntil(IdleCondition.KEY_INJECT_HAS_COMPLETED);
try {
checkState(injectTask.isDone(), "Key injection was signaled - but it wasnt done.");
return injectTask.get();
} catch (ExecutionException ee) {
if (ee.getCause() instanceof InjectEventSecurityException) {
throw (InjectEventSecurityException) ee.getCause();
} else {
throw new RuntimeException(ee.getCause());
}
} catch (InterruptedException neverHappens) {
// we only call get() after done() is signaled.
// we should never block.
throw new RuntimeException("impossible.", neverHappens);
}
}
@Override
public boolean injectMotionEvent(final MotionEvent event) throws InjectEventSecurityException {
checkNotNull(event);
checkState(Looper.myLooper() == mainLooper, "Expecting to be on main thread!");
initialize();
FutureTask<Boolean> injectTask = new SignalingTask<Boolean>(
new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
return eventInjector.injectMotionEvent(event);
}
},
IdleCondition.MOTION_INJECTION_HAS_COMPLETED,
generation);
keyEventExecutor.submit(injectTask);
loopUntil(IdleCondition.MOTION_INJECTION_HAS_COMPLETED);
try {
checkState(injectTask.isDone(), "Key injection was signaled - but it wasnt done.");
return injectTask.get();
} catch (ExecutionException ee) {
if (ee.getCause() instanceof InjectEventSecurityException) {
throw (InjectEventSecurityException) ee.getCause();
} else {
throw propagate(ee.getCause() != null ? ee.getCause() : ee);
}
} catch (InterruptedException neverHappens) {
// we only call get() after done() is signaled.
// we should never block.
throw propagate(neverHappens);
} finally {
loopMainThreadUntilIdle();
}
}
@Override
public boolean injectString(String str) throws InjectEventSecurityException {
checkNotNull(str);
checkState(Looper.myLooper() == mainLooper, "Expecting to be on main thread!");
initialize();
// No-op if string is empty.
if (str.length() == 0) {
Log.w(TAG, "Supplied string is empty resulting in no-op (nothing is typed).");
return true;
}
boolean eventInjected = false;
KeyCharacterMap keyCharacterMap = getKeyCharacterMap();
// TODO(user): Investigate why not use (as suggested in javadoc of keyCharacterMap.getEvents):
// http://developer.android.com/reference/android/view/KeyEvent.html#KeyEvent(long,
// java.lang.String, int, int)
KeyEvent[] events = keyCharacterMap.getEvents(str.toCharArray());
checkNotNull(events, "Failed to get events for string " + str);
Log.d(TAG, String.format("Injecting string: \"%s\"", str));
for (KeyEvent event : events) {
checkNotNull(event, String.format("Failed to get event for character (%c) with key code (%s)",
event.getKeyCode(), event.getUnicodeChar()));
eventInjected = false;
for (int attempts = 0; !eventInjected && attempts < 4; attempts++) {
attempts++;
// We have to change the time of an event before injecting it because
// all KeyEvents returned by KeyCharacterMap.getEvents() have the same
// time stamp and the system rejects too old events. Hence, it is
// possible for an event to become stale before it is injected if it
// takes too long to inject the preceding ones.
event = KeyEvent.changeTimeRepeat(event, SystemClock.uptimeMillis(), 0);
eventInjected = injectKeyEvent(event);
}
if (!eventInjected) {
Log.e(TAG, String.format("Failed to inject event for character (%c) with key code (%s)",
event.getUnicodeChar(), event.getKeyCode()));
break;
}
}
return eventInjected;
}
@SuppressLint("InlinedApi")
@VisibleForTesting
@SuppressWarnings("deprecation")
public static KeyCharacterMap getKeyCharacterMap() {
KeyCharacterMap keyCharacterMap = null;
// KeyCharacterMap.VIRTUAL_KEYBOARD is present from API11.
// For earlier APIs we use KeyCharacterMap.BUILT_IN_KEYBOARD
if (Build.VERSION.SDK_INT < 11) {
keyCharacterMap = KeyCharacterMap.load(KeyCharacterMap.BUILT_IN_KEYBOARD);
} else {
keyCharacterMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
}
return keyCharacterMap;
}
@Override
public void loopMainThreadUntilIdle() {
initialize();
checkState(Looper.myLooper() == mainLooper, "Expecting to be on main thread!");
do {
EnumSet<IdleCondition> condChecks = EnumSet.noneOf(IdleCondition.class);
if (!asyncTaskMonitor.isIdleNow()) {
asyncTaskMonitor.notifyWhenIdle(new SignalingTask<Void>(NO_OP,
IdleCondition.ASYNC_TASKS_HAVE_IDLED, generation));
condChecks.add(IdleCondition.ASYNC_TASKS_HAVE_IDLED);
}
if (!compatIdle()) {
compatTaskMonitor.get().notifyWhenIdle(new SignalingTask<Void>(NO_OP,
IdleCondition.COMPAT_TASKS_HAVE_IDLED, generation));
condChecks.add(IdleCondition.COMPAT_TASKS_HAVE_IDLED);
}
if (!idlingResourceRegistry.allResourcesAreIdle()) {
final IdlingPolicy warning = IdlingPolicies.getDynamicIdlingResourceWarningPolicy();
final IdlingPolicy error = IdlingPolicies.getDynamicIdlingResourceErrorPolicy();
final SignalingTask<Void> idleSignal = new SignalingTask<Void>(NO_OP,
IdleCondition.DYNAMIC_TASKS_HAVE_IDLED, generation);
idlingResourceRegistry.notifyWhenAllResourcesAreIdle(new IdleNotificationCallback() {
@Override
public void resourcesStillBusyWarning(List<String> busyResourceNames) {
warning.handleTimeout(busyResourceNames, "IdlingResources are still busy!");
}
@Override
public void resourcesHaveTimedOut(List<String> busyResourceNames) {
error.handleTimeout(busyResourceNames, "IdlingResources have timed out!");
controllerHandler.post(idleSignal);
}
@Override
public void allResourcesIdle() {
controllerHandler.post(idleSignal);
}
});
condChecks.add(IdleCondition.DYNAMIC_TASKS_HAVE_IDLED);
}
try {
loopUntil(condChecks);
} finally {
asyncTaskMonitor.cancelIdleMonitor();
if (compatTaskMonitor.isPresent()) {
compatTaskMonitor.get().cancelIdleMonitor();
}
idlingResourceRegistry.cancelIdleMonitor();
}
} while (!asyncTaskMonitor.isIdleNow() || !compatIdle()
|| !idlingResourceRegistry.allResourcesAreIdle());
}
private boolean compatIdle() {
if (compatTaskMonitor.isPresent()) {
return compatTaskMonitor.get().isIdleNow();
} else {
return true;
}
}
@Override
public void loopMainThreadForAtLeast(long millisDelay) {
initialize();
checkState(Looper.myLooper() == mainLooper, "Expecting to be on main thread!");
checkState(!IdleCondition.DELAY_HAS_PAST.isSignaled(conditionSet), "recursion detected!");
checkArgument(millisDelay > 0);
controllerHandler.postDelayed(new SignalingTask(NO_OP, IdleCondition.DELAY_HAS_PAST,
generation),
millisDelay);
loopUntil(IdleCondition.DELAY_HAS_PAST);
loopMainThreadUntilIdle();
}
@Override
public boolean handleMessage(Message msg) {
if (!IdleCondition.handleMessage(msg, conditionSet, generation)) {
Log.i(TAG, "Unknown message type: " + msg);
return false;
} else {
return true;
}
}
private void loopUntil(IdleCondition condition) {
loopUntil(EnumSet.of(condition));
}
/**
* Loops the main thread until all IdleConditions have been signaled.
*
* Once they've been signaled, the conditions are reset and the generation value
* is incremented.
*
* Signals should only be raised thru SignalingTask instances, and care should be
* taken to ensure that the signaling task is created before loopUntil is called.
*
* Good:
* idlingType.runOnIdle(new SignalingTask(NO_OP, IdleCondition.MY_IDLE_CONDITION, generation));
* loopUntil(IdleCondition.MY_IDLE_CONDITION);
*
* Bad:
* idlingType.runOnIdle(new CustomCallback() {
* @Override
* public void itsDone() {
* // oh no - The creation of this signaling task is delayed until this method is
* // called, so it will not have the right value for generation.
* new SignalingTask(NO_OP, IdleCondition.MY_IDLE_CONDITION, generation).run();
* }
* })
* loopUntil(IdleCondition.MY_IDLE_CONDITION);
*/
private void loopUntil(EnumSet<IdleCondition> conditions) {
checkState(!looping, "Recursive looping detected!");
looping = true;
IdlingPolicy masterIdlePolicy = IdlingPolicies.getMasterIdlingPolicy();
try {
int loopCount = 0;
long start = SystemClock.uptimeMillis();
long end = start + masterIdlePolicy.getIdleTimeoutUnit().toMillis(
masterIdlePolicy.getIdleTimeout());
while (SystemClock.uptimeMillis() < end) {
boolean conditionsMet = true;
boolean shouldLogConditionState = loopCount > 0 && loopCount % 100 == 0;
for (IdleCondition condition : conditions) {
if (!condition.isSignaled(conditionSet)) {
conditionsMet = false;
if (shouldLogConditionState) {
Log.w(TAG, "Waiting for: " + condition.name() + " for " + loopCount + " iterations.");
} else {
break;
}
}
}
if (conditionsMet) {
QueueState queueState = queueInterrogator.determineQueueState();
if (queueState == QueueState.EMPTY || queueState == QueueState.TASK_DUE_LONG) {
return;
} else {
Log.v(
"ESP_TRACE",
"Barrier detected or task avaliable for running shortly.");
}
}
Message message = queueInterrogator.getNextMessage();
String callbackString = "unknown";
String messageString = "unknown";
try {
if (null == message.getCallback()) {
callbackString = "no callback.";
} else {
callbackString = message.getCallback().toString();
}
messageString = message.toString();
} catch (NullPointerException e) {
/*
* Ignore. android.app.ActivityThread$ActivityClientRecord#toString() fails for API level
* 15.
*/
}
Log.v(
"ESP_TRACE",
String.format("%s: MessageQueue.next(): %s, with target: %s, callback: %s", TAG,
messageString, message.getTarget().getClass().getCanonicalName(), callbackString));
message.getTarget().dispatchMessage(message);
message.recycle();
loopCount++;
}
List<String> idleConditions = Lists.newArrayList();
for (IdleCondition condition : conditions) {
if (!condition.isSignaled(conditionSet)) {
idleConditions.add(condition.name());
}
}
masterIdlePolicy.handleTimeout(idleConditions, String.format(
"Looped for %s iterations over %s %s.", loopCount, masterIdlePolicy.getIdleTimeout(),
masterIdlePolicy.getIdleTimeoutUnit().name()));
} finally {
looping = false;
generation++;
for (IdleCondition condition : conditions) {
condition.reset(conditionSet);
}
}
}
private void initialize() {
if (controllerHandler == null) {
controllerHandler = new Handler(this);
}
}
/**
* Encapsulates posting a signal message to update the conditions set after a task has
* executed.
*/
private class SignalingTask<T> extends FutureTask<T> {
private final IdleCondition condition;
private final int myGeneration;
public SignalingTask(Callable<T> callable, IdleCondition condition, int myGeneration) {
super(callable);
this.condition = checkNotNull(condition);
this.myGeneration = myGeneration;
}
@Override
protected void done() {
controllerHandler.sendMessage(condition.createSignal(controllerHandler, myGeneration));
}
}
}