mirror of
https://github.com/appium/appium.git
synced 2026-04-30 15:30:20 -05:00
Merge pull request #2335 from imurchie/isaac-actions-fix
Allow android multi touch actions without an element
This commit is contained in:
@@ -1004,18 +1004,26 @@ androidController.tap = function (elementId, x, y, count, cb) {
|
||||
this.proxy(opts, loop);
|
||||
}.bind(this);
|
||||
|
||||
if (x !== 0 || y !== 0) {
|
||||
this.proxy(["element:getLocation", {elementId: elementId}], function (err, res) {
|
||||
if (err) return cb(err);
|
||||
var value = res.value;
|
||||
x += parseInt(value.x);
|
||||
y += parseInt(value.y);
|
||||
if (elementId) {
|
||||
// we are either tapping on the default location of the element
|
||||
// or an offset from the top left corner
|
||||
if (x !== 0 || y !== 0) {
|
||||
this.proxy(["element:getLocation", {elementId: elementId}], function (err, res) {
|
||||
if (err) return cb(err);
|
||||
var value = res.value;
|
||||
x += parseInt(value.x);
|
||||
y += parseInt(value.y);
|
||||
|
||||
opts = ["click", {x: x, y: y}];
|
||||
opts = ["click", {x: x, y: y}];
|
||||
loop();
|
||||
}.bind(this));
|
||||
} else {
|
||||
opts = ["element:click", {elementId: elementId}];
|
||||
loop();
|
||||
}.bind(this));
|
||||
}
|
||||
} else {
|
||||
opts = ["element:click", {elementId: elementId}];
|
||||
// we have absolute coordinates
|
||||
opts = ["click", {x: x, y: y}];
|
||||
loop();
|
||||
}
|
||||
};
|
||||
@@ -1156,6 +1164,7 @@ androidController.parseTouch = function (gestures, cb) {
|
||||
x: (gesture.options.x || 0),
|
||||
y: (gesture.options.y || 0)
|
||||
};
|
||||
|
||||
touchStateObject = {
|
||||
timeOffset: 0.005,
|
||||
touch: tapPoint
|
||||
@@ -1221,11 +1230,19 @@ androidController.performMultiAction = function (elementId, actions, cb) {
|
||||
}.bind(this), function (err) {
|
||||
if (err) return cb(err);
|
||||
|
||||
var opts = {
|
||||
elementId: elementId,
|
||||
actions: states
|
||||
};
|
||||
return this.proxy(["element:performMultiPointerGesture", opts], cb);
|
||||
var opts;
|
||||
if (elementId) {
|
||||
opts = {
|
||||
elementId: elementId,
|
||||
actions: states
|
||||
};
|
||||
return this.proxy(["element:performMultiPointerGesture", opts], cb);
|
||||
} else {
|
||||
opts = {
|
||||
actions: states
|
||||
};
|
||||
return this.proxy(["performMultiPointerGesture", opts], cb);
|
||||
}
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
|
||||
+36
-20
@@ -22,42 +22,58 @@ import io.appium.android.bootstrap.exceptions.ElementNotInHashException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
|
||||
public class MultiPointerGesture extends CommandHandler {
|
||||
public class MultiPointerGesture extends TouchableEvent {
|
||||
|
||||
@Override
|
||||
public AndroidCommandResult execute(final AndroidCommand command)
|
||||
throws JSONException {
|
||||
if (!command.isElementCommand()) {
|
||||
return getErrorResult("A gesturable view is required for this command.");
|
||||
}
|
||||
|
||||
try {
|
||||
JSONArray actions = (org.json.JSONArray)command.params().get("actions");
|
||||
PointerCoords[][] pcs = parsePointerCoords(command);
|
||||
|
||||
double time = computeLongestTime(actions);
|
||||
|
||||
PointerCoords[][] pcs = new PointerCoords[actions.length()][];
|
||||
for (int i = 0; i < actions.length(); i++) {
|
||||
JSONArray gestures = actions.getJSONArray(i);
|
||||
|
||||
pcs[i] = gesturesToPointerCoords(time, gestures);
|
||||
}
|
||||
|
||||
final AndroidElement el = command.getElement();
|
||||
if (el.performMultiPointerGesture(pcs)) {
|
||||
return getSuccessResult("OK");
|
||||
if (command.isElementCommand()) {
|
||||
final AndroidElement el = command.getElement();
|
||||
if (el.performMultiPointerGesture(pcs)) {
|
||||
return getSuccessResult("OK");
|
||||
} else {
|
||||
return getErrorResult("Unable to perform multi pointer gesture");
|
||||
}
|
||||
} else {
|
||||
return getErrorResult("Unable to perform multi pointer gesture");
|
||||
Object controller = getController();
|
||||
final Method pmpg = getMethod("performMultiPointerGesture", controller);
|
||||
Boolean rt = (Boolean)pmpg.invoke(controller, (Object)pcs);
|
||||
if (rt.booleanValue()) {
|
||||
return getSuccessResult("OK");
|
||||
} else {
|
||||
return getErrorResult("Unable to perform multi pointer gesture");
|
||||
}
|
||||
}
|
||||
} catch (final ElementNotInHashException e) {
|
||||
return new AndroidCommandResult(WDStatus.NO_SUCH_ELEMENT, e.getMessage());
|
||||
} catch (final Exception e) {
|
||||
Logger.debug("Exception: " + e);
|
||||
e.printStackTrace();
|
||||
return new AndroidCommandResult(WDStatus.UNKNOWN_ERROR, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private PointerCoords[] gesturesToPointerCoords(double maxTime, JSONArray gestures) throws Exception {
|
||||
private PointerCoords[][] parsePointerCoords(AndroidCommand command)
|
||||
throws JSONException {
|
||||
JSONArray actions = (org.json.JSONArray)command.params().get("actions");
|
||||
|
||||
double time = computeLongestTime(actions);
|
||||
|
||||
PointerCoords[][] pcs = new PointerCoords[actions.length()][];
|
||||
for (int i = 0; i < actions.length(); i++) {
|
||||
JSONArray gestures = actions.getJSONArray(i);
|
||||
|
||||
pcs[i] = gesturesToPointerCoords(time, gestures);
|
||||
}
|
||||
|
||||
return pcs;
|
||||
}
|
||||
|
||||
private PointerCoords[] gesturesToPointerCoords(double maxTime, JSONArray gestures)
|
||||
throws JSONException {
|
||||
// gestures, e.g.:
|
||||
// [
|
||||
// {"touch":{"y":529.5,"x":120},"time":0.2},
|
||||
|
||||
+3
-38
@@ -23,18 +23,9 @@ import com.android.uiautomator.core.UiObjectNotFoundException;
|
||||
/**
|
||||
* This handler is and abstract class that contains all the common code for
|
||||
* touch event handlers.
|
||||
*
|
||||
*
|
||||
*/
|
||||
public abstract class TouchEvent extends CommandHandler {
|
||||
private static Field enableField(final Class<?> clazz, final String field)
|
||||
throws SecurityException, NoSuchFieldException {
|
||||
Logger.debug("Updating class \"" + clazz + "\" to enable field \"" + field
|
||||
+ "\"");
|
||||
final Field fieldObject = clazz.getDeclaredField(field);
|
||||
fieldObject.setAccessible(true);
|
||||
return fieldObject;
|
||||
}
|
||||
|
||||
public abstract class TouchEvent extends TouchableEvent {
|
||||
protected AndroidElement el;
|
||||
|
||||
protected int clickX;
|
||||
@@ -46,7 +37,7 @@ public abstract class TouchEvent extends CommandHandler {
|
||||
protected boolean isElement;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param command
|
||||
* The {@link AndroidCommand}
|
||||
* @return {@link AndroidCommandResult}
|
||||
@@ -113,32 +104,6 @@ public abstract class TouchEvent extends CommandHandler {
|
||||
protected abstract boolean executeTouchEvent()
|
||||
throws UiObjectNotFoundException;
|
||||
|
||||
/*
|
||||
* getAutomatorBridge is private so we access the bridge via reflection to use
|
||||
* the touchDown / touchUp / touchMove methods.
|
||||
*/
|
||||
protected Object getController() throws IllegalArgumentException,
|
||||
IllegalAccessException, SecurityException, NoSuchFieldException {
|
||||
final UiDevice device = UiDevice.getInstance();
|
||||
final Object bridge = enableField(device.getClass(), "mUiAutomationBridge")
|
||||
.get(device);
|
||||
final Object controller = enableField(bridge.getClass().getSuperclass(),
|
||||
"mInteractionController").get(bridge);
|
||||
return controller;
|
||||
|
||||
}
|
||||
|
||||
protected Method getMethod(final String name, final Object controller)
|
||||
throws NoSuchMethodException, SecurityException {
|
||||
final Class<?> controllerClass = controller.getClass();
|
||||
|
||||
Logger.debug("Finding methods on class: " + controllerClass);
|
||||
final Method method = controllerClass.getDeclaredMethod(name, int.class,
|
||||
int.class);
|
||||
method.setAccessible(true);
|
||||
return method;
|
||||
}
|
||||
|
||||
/**
|
||||
* Variables persist across executions. initialize must be called at the start
|
||||
* of execute.
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package io.appium.android.bootstrap.handler;
|
||||
|
||||
import io.appium.android.bootstrap.CommandHandler;
|
||||
import io.appium.android.bootstrap.Logger;
|
||||
|
||||
import android.view.MotionEvent.PointerCoords;
|
||||
|
||||
import com.android.uiautomator.core.UiDevice;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* This handler is and abstract class that contains all the common code for
|
||||
* touch event handlers.
|
||||
*
|
||||
*/
|
||||
public abstract class TouchableEvent extends CommandHandler {
|
||||
private static Field enableField(final Class<?> clazz, final String field)
|
||||
throws SecurityException, NoSuchFieldException {
|
||||
Logger.debug("Updating class \"" + clazz + "\" to enable field \"" + field
|
||||
+ "\"");
|
||||
final Field fieldObject = clazz.getDeclaredField(field);
|
||||
fieldObject.setAccessible(true);
|
||||
return fieldObject;
|
||||
}
|
||||
|
||||
/*
|
||||
* getAutomatorBridge is private so we access the bridge via reflection to use
|
||||
* the touchDown / touchUp / touchMove methods.
|
||||
*/
|
||||
protected Object getController() throws IllegalArgumentException,
|
||||
IllegalAccessException, SecurityException, NoSuchFieldException {
|
||||
final UiDevice device = UiDevice.getInstance();
|
||||
final Object bridge = enableField(device.getClass(), "mUiAutomationBridge")
|
||||
.get(device);
|
||||
final Object controller = enableField(bridge.getClass().getSuperclass(),
|
||||
"mInteractionController").get(bridge);
|
||||
return controller;
|
||||
|
||||
}
|
||||
|
||||
protected Method getMethod(final String name, final Object controller)
|
||||
throws NoSuchMethodException, SecurityException {
|
||||
final Class<?> controllerClass = controller.getClass();
|
||||
|
||||
Logger.debug("Finding methods on class: " + controllerClass);
|
||||
final Method method;
|
||||
if (name.equals("performMultiPointerGesture")) {
|
||||
// multi pointer gestures take a 2d array of coordinates
|
||||
method = controllerClass.getDeclaredMethod(name, PointerCoords[][].class);
|
||||
} else {
|
||||
// all the other touch events send two ints
|
||||
method = controllerClass.getDeclaredMethod(name, int.class, int.class);
|
||||
}
|
||||
method.setAccessible(true);
|
||||
return method;
|
||||
}
|
||||
}
|
||||
@@ -266,6 +266,21 @@ exports.performTouch = function (req, res) {
|
||||
gestures = gestures.actions;
|
||||
}
|
||||
|
||||
// press-wait-moveTo-release is `swipe`, so use native method
|
||||
if (gestures.length === 4) {
|
||||
if ((gestures[0].action === 'press') && (gestures[1].action === 'wait') && (gestures[2].action === 'moveTo') && (gestures[3].action === 'release')) {
|
||||
var body = {
|
||||
startX: gestures[0].options.x,
|
||||
startY: gestures[0].options.y,
|
||||
endX: gestures[2].options.x,
|
||||
endY: gestures[2].options.y,
|
||||
duration: gestures[1].options.ms
|
||||
};
|
||||
req.body = body;
|
||||
return exports.mobileSwipe(req, res);
|
||||
}
|
||||
}
|
||||
|
||||
req.device.performTouch(gestures, getResponseHandler(req, res));
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user