/*
* 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 getDevice().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);
getDevice().getUiAutomation().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);
getDevice().getUiAutomation().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);
getDevice().getUiAutomation().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 = Configurator.getInstance().getToolType();
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);
}
}
UiDevice getDevice() {
return mDevice;
}
}