| /* |
| * 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)); |
| } |
| |
| } |
| |
| } |