/* * 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 androidx.test.uiautomator; import android.graphics.Point; import android.os.SystemClock; import android.view.InputDevice; import android.view.MotionEvent; import android.view.MotionEvent.PointerCoords; import android.view.MotionEvent.PointerProperties; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.PriorityQueue; /** * The {@link GestureController} provides methods for performing high-level {@link PointerGesture}s. */ class GestureController { private static final long MOTION_EVENT_INJECTION_DELAY_MILLIS = 5; // Singleton instance private static GestureController sInstance; private UiDevice mDevice; /** Comparator for sorting PointerGestures by start times. */ private static final Comparator<PointerGesture> START_TIME_COMPARATOR = new Comparator<PointerGesture>() { @Override public int compare(PointerGesture o1, PointerGesture o2) { return (int)(o1.delay() - o2.delay()); } }; /** Comparator for sorting PointerGestures by end times. */ private static final Comparator<PointerGesture> END_TIME_COMPARATOR = new Comparator<PointerGesture>() { @Override public int compare(PointerGesture o1, PointerGesture o2) { return (int)((o1.delay() + o2.duration()) - (o2.delay() + o2.duration())); } }; // Private constructor. private GestureController(UiDevice device) { mDevice = device; } /** Returns the {@link GestureController} instance for the given {@link UiDevice}. */ public static GestureController getInstance(UiDevice device) { if (sInstance == null) { sInstance = new GestureController(device); } return sInstance; } /** * Performs the given gesture and waits for the {@code condition} to be met. * * @param condition The {@link EventCondition} to wait for. * @param timeout Maximum amount of time to wait in milliseconds. * @param gestures One or more {@link PointerGesture}s which define the gesture to be performed. * @return The final result returned by the condition. */ public <R> R performGestureAndWait(EventCondition<R> condition, long timeout, PointerGesture ... gestures) { return mDevice.performActionAndWait(new GestureRunnable(gestures), condition, timeout); } /** * Performs the given gesture as represented by the given {@link PointerGesture}s. * * Each {@link PointerGesture} represents the actions of a single pointer from the time when it * is first touched down until the pointer is released. To perform the gesture, this method * tracks the locations of each pointer and injects {@link MotionEvent}s as appropriate. * * @param gestures One or more {@link PointerGesture}s which define the gesture to be performed. */ public void performGesture(PointerGesture ... gestures) { // Initialize pointers int count = 0; Map<PointerGesture, Pointer> pointers = new HashMap<PointerGesture, Pointer>(); for (PointerGesture g : gestures) { pointers.put(g, new Pointer(count++, g.start())); } // Initialize MotionEvent arrays List<PointerProperties> properties = new ArrayList<PointerProperties>(); List<PointerCoords> coordinates = new ArrayList<PointerCoords>(); // Track active and pending gestures PriorityQueue<PointerGesture> active = new PriorityQueue<PointerGesture>(gestures.length, END_TIME_COMPARATOR); PriorityQueue<PointerGesture> pending = new PriorityQueue<PointerGesture>(gestures.length, START_TIME_COMPARATOR); pending.addAll(Arrays.asList(gestures)); // Record the start time long startTime = SystemClock.uptimeMillis(); // Loop MotionEvent event; for (long elapsedTime = 0; !pending.isEmpty() || !active.isEmpty(); elapsedTime = SystemClock.uptimeMillis() - startTime) { // Touchdown any new pointers while (!pending.isEmpty() && elapsedTime > pending.peek().delay()) { PointerGesture gesture = pending.remove(); Pointer pointer = pointers.get(gesture); // Add the pointer to the MotionEvent arrays properties.add(pointer.prop); coordinates.add(pointer.coords); // Touch down int action = MotionEvent.ACTION_DOWN; if (!active.isEmpty()) { // Use ACTION_POINTER_DOWN for secondary pointers. The index is stored at // ACTION_POINTER_INDEX_SHIFT. action = MotionEvent.ACTION_POINTER_DOWN + ((properties.size() - 1) << MotionEvent.ACTION_POINTER_INDEX_SHIFT); } event = getMotionEvent(startTime, startTime + elapsedTime, action, properties, coordinates); mDevice.getAutomatorBridge().injectInputEvent(event, true); // Move the PointerGesture to the active list active.add(gesture); } // Touch up any completed pointers while (!active.isEmpty() && elapsedTime > active.peek().delay() + active.peek().duration()) { PointerGesture gesture = active.remove(); Pointer pointer = pointers.get(gesture); // Update pointer positions pointer.updatePosition(gesture.end()); for (PointerGesture current : active) { pointers.get(current).updatePosition(current.pointAt(elapsedTime)); } int action = MotionEvent.ACTION_UP; int index = properties.indexOf(pointer.prop); if (!active.isEmpty()) { action = MotionEvent.ACTION_POINTER_UP + (index << MotionEvent.ACTION_POINTER_INDEX_SHIFT); } event = getMotionEvent(startTime, startTime + elapsedTime, action, properties, coordinates); mDevice.getAutomatorBridge().injectInputEvent(event, true); properties.remove(index); coordinates.remove(index); } // Move any active pointers for (PointerGesture gesture : active) { Pointer pointer = pointers.get(gesture); pointer.updatePosition(gesture.pointAt(elapsedTime - gesture.delay())); } if (!active.isEmpty()) { event = getMotionEvent(startTime, startTime + elapsedTime, MotionEvent.ACTION_MOVE, properties, coordinates); mDevice.getAutomatorBridge().injectInputEvent(event, true); } } } /** Helper function to obtain a MotionEvent. */ private static MotionEvent getMotionEvent(long downTime, long eventTime, int action, List<PointerProperties> properties, List<PointerCoords> coordinates) { PointerProperties[] props = properties.toArray(new PointerProperties[properties.size()]); PointerCoords[] coords = coordinates.toArray(new PointerCoords[coordinates.size()]); return MotionEvent.obtain(downTime, eventTime, action, props.length, props, coords, 0, 0, 1, 1, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0); } /** Helper class which tracks an individual pointer as part of a MotionEvent. */ private static class Pointer { PointerProperties prop; PointerCoords coords; public Pointer(int id, Point point) { prop = new PointerProperties(); prop.id = id; prop.toolType = MotionEvent.TOOL_TYPE_FINGER; coords = new PointerCoords(); coords.pressure = 1; coords.size = 1; coords.x = point.x; coords.y = point.y; } public void updatePosition(Point point) { coords.x = point.x; coords.y = point.y; } } /** Runnable wrapper around a {@link GestureController#performGesture} call. */ private class GestureRunnable implements Runnable { private PointerGesture[] mGestures; public GestureRunnable(PointerGesture[] gestures) { mGestures = gestures; } @Override public void run() { performGesture(mGestures); } } }