bootstrap之MultiPointerGesture
最后更新于:2022-04-01 06:54:05
# MultiPointerGesture
~~~
package io.appium.android.bootstrap.handler;
import android.view.MotionEvent.PointerCoords;
import com.android.uiautomator.common.ReflectionUtils;
import io.appium.android.bootstrap.*;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.lang.reflect.Method;
import static io.appium.android.bootstrap.utils.API.API_18;
public class MultiPointerGesture extends CommandHandler {
private double computeLongestTime(final JSONArray actions)
throws JSONException {
double max = 0.0;
for (int i = 0; i < actions.length(); i++) {
final JSONArray gestures = actions.getJSONArray(i);
final double endTime = gestures.getJSONObject(gestures.length() - 1)
.getDouble("time");
if (endTime > max) {
max = endTime;
}
}
return max;
}
private PointerCoords createPointerCoords(final JSONObject obj)
throws JSONException {
final JSONObject o = obj.getJSONObject("touch");
final int x = o.getInt("x");
final int y = o.getInt("y");
final PointerCoords p = new PointerCoords();
p.size = 1;
p.pressure = 1;
p.x = x;
p.y = y;
return p;
}
@Override
public AndroidCommandResult execute(final AndroidCommand command)
throws JSONException {
try {
final PointerCoords[][] pcs = parsePointerCoords(command);
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 {
if (API_18) {
final ReflectionUtils utils = new ReflectionUtils();
final Method pmpg = utils.getControllerMethod("performMultiPointerGesture",
PointerCoords[][].class);
final Boolean rt = (Boolean) pmpg.invoke(utils.getController(),
(Object) pcs);
if (rt) {
return getSuccessResult("OK");
} else {
return getErrorResult("Unable to perform multi pointer gesture");
}
} else {
Logger.error("Device does not support API < 18!");
return new AndroidCommandResult(WDStatus.UNKNOWN_ERROR,
"Cannot perform multi pointer gesture on device below API level 18");
}
}
} catch (final Exception e) {
Logger.debug("Exception: " + e);
e.printStackTrace();
return new AndroidCommandResult(WDStatus.UNKNOWN_ERROR, e.getMessage());
}
}
private PointerCoords[] gesturesToPointerCoords(final double maxTime,
final JSONArray gestures) throws JSONException {
// gestures, e.g.:
// [
// {"touch":{"y":529.5,"x":120},"time":0.2},
// {"touch":{"y":529.5,"x":130},"time":0.4},
// {"touch":{"y":454.5,"x":140},"time":0.6},
// {"touch":{"y":304.5,"x":150},"time":0.8}
// ]
// From the docs:
// "Steps are injected about 5 milliseconds apart, so 100 steps may take
// around 0.5 seconds to complete."
final int steps = (int) (maxTime * 200) + 2;
final PointerCoords[] pc = new PointerCoords[steps];
int i = 1;
JSONObject current = gestures.getJSONObject(0);
double currentTime = current.getDouble("time");
double runningTime = 0.0;
final int gesturesLength = gestures.length();
for (int j = 0; j < steps; j++) {
if (runningTime > currentTime && i < gesturesLength) {
current = gestures.getJSONObject(i++);
currentTime = current.getDouble("time");
}
pc[j] = createPointerCoords(current);
runningTime += 0.005;
}
return pc;
}
private PointerCoords[][] parsePointerCoords(final AndroidCommand command)
throws JSONException {
final JSONArray actions = (org.json.JSONArray) command.params().get(
"actions");
final double time = computeLongestTime(actions);
final PointerCoords[][] pcs = new PointerCoords[actions.length()][];
for (int i = 0; i < actions.length(); i++) {
final JSONArray gestures = actions.getJSONArray(i);
pcs[i] = gesturesToPointerCoords(time, gestures);
}
return pcs;
}
}
~~~
多点触控根据你传递过来的参数决定,如果参数是一个控件元素,那么就要调用performMultiPointerGesture方法,如果参数是一系列的点,那么就要调用反射。那么具体来看看2个方法细节。
## 控件
performMultiPointerGesture
~~~
public boolean performMultiPointerGesture(PointerCoords[] ...touches) {
try {
if (API_18) {
// The compile-time SDK expects the wrong arguments, but the runtime
// version in the emulator is correct. So we cannot do:
// `return el.performMultiPointerGesture(touches);`
// Instead we need to use Reflection to do it all at runtime.
Method method = this.el.getClass().getMethod("performMultiPointerGesture", PointerCoords[][].class);
Boolean rt = (Boolean)method.invoke(this.el, (Object)touches);
return rt;
} else {
Logger.error("Device does not support API < 18!");
return false;
}
} catch (final Exception e) {
Logger.error("Exception: " + e + " (" + e.getMessage() + ")");
return false;
}
}
~~~
UiObject中有直接可以调用的performMultiPointerGesture方法,为什么还要用反射呢。上面的方法里的注释是这样解释的:编译的时候sdk会认为参数是错误的,但是运行时却认为是正确的,所以只有在运行时调用才能保证正确性。反射调用的就是运行时的环境,所以它使用了反射调用了performMultiPointerGesture。
## 点组
在api18以上的版本中才有传点组的方法可调用,所以先判断sdk的版本。如果api在18以上,那么就要调用InteractionController..performMultiPointerGesture的方法来执行
bootstrap之UpdateStrings
最后更新于:2022-04-01 06:54:03
# UpdateStrings
~~~
package io.appium.android.bootstrap.handler;
import io.appium.android.bootstrap.AndroidCommand;
import io.appium.android.bootstrap.AndroidCommandResult;
import io.appium.android.bootstrap.CommandHandler;
import io.appium.android.bootstrap.Logger;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import org.json.JSONObject;
/**
* This handler is used to update the apk strings.
*
*/
public class UpdateStrings extends CommandHandler {
/**
* strings.json文件保存的是apk的strings.xml里的内容,在Bootstrap启动前由appium服务器解析并push到设备端的
*
* @return
*/
public static boolean loadStringsJson() {
Logger.debug("Loading json...");
try {
final String filePath = "/data/local/tmp/strings.json";
final File jsonFile = new File(filePath);
// json will not exist for apks that are only on device
// 你的case必须写明apk的路径,如果启动设备上已有的应用而case中没有app路径,此时json文件是不存在的
// because the node server can't extract the json from the apk.
if (!jsonFile.exists()) {
return false;
}
final DataInputStream dataInput = new DataInputStream(
new FileInputStream(jsonFile));
final byte[] jsonBytes = new byte[(int) jsonFile.length()];
dataInput.readFully(jsonBytes);
// this closes FileInputStream
dataInput.close();
final String jsonString = new String(jsonBytes, "UTF-8");
// 将读取出来的信息赋给Find类中的属性,以做后用
Find.apkStrings = new JSONObject(jsonString);
Logger.debug("json loading complete.");
} catch (final Exception e) {
Logger.error("Error loading json: " + e.getMessage());
return false;
}
return true;
}
/*
* @param command The {@link AndroidCommand} used for this handler.
*
* @return {@link AndroidCommandResult}
*
* @see io.appium.android.bootstrap.CommandHandler#execute(io.appium.android.
* bootstrap.AndroidCommand)
*/
@Override
public AndroidCommandResult execute(final AndroidCommand command) {
if (!loadStringsJson()) {
return getErrorResult("Unable to load json file and update strings.");
}
return getSuccessResult(true);
}
}
~~~
在appium初始化的时候,如果你代码中添加了app应用,而不是启动手机设备中已经有的应用,这时候appium会将该app解析,并提取出设备当前语言环境的strings.xml文件里的信息保存在strings.json里,并将其push到手机的/data/local/tmp目录下,当你想要获取应用中用到的字符串时,手机会去该目录下读取strings.json文件并返回给客户端。
所以上面的代码也就是我上面说的过程。
bootstrap之DumpWindowHierarchy
最后更新于:2022-04-01 06:54:01
# DumpWindowHierarchy
~~~
package io.appium.android.bootstrap.handler;
import android.os.Environment;
import com.android.uiautomator.core.UiDevice;
import io.appium.android.bootstrap.AndroidCommand;
import io.appium.android.bootstrap.AndroidCommandResult;
import io.appium.android.bootstrap.CommandHandler;
import io.appium.android.bootstrap.utils.NotImportantViews;
import java.io.File;
/**
* This handler is used to dumpWindowHierarchy.
* https://android.googlesource.com/
* platform/frameworks/testing/+/master/uiautomator
* /library/core-src/com/android/uiautomator/core/UiDevice.java
*/
@SuppressWarnings("ResultOfMethodCallIgnored")
public class DumpWindowHierarchy extends CommandHandler {
// Note that
// "new File(new File(Environment.getDataDirectory(), "local/tmp"), fileName)"
// is directly from the UiDevice.java source code.
private static final File dumpFolder = new File(Environment.getDataDirectory(), "local/tmp");
private static final String dumpFileName = "dump.xml";
private static final File dumpFile = new File(dumpFolder, dumpFileName);
private static void deleteDumpFile() {
if (dumpFile.exists()) {
dumpFile.delete();
}
}
public static boolean dump() {
dumpFolder.mkdirs();
deleteDumpFile();
try {
// dumpWindowHierarchy often has a NullPointerException
UiDevice.getInstance().dumpWindowHierarchy(dumpFileName);
} catch (Exception e) {
e.printStackTrace();
// If there's an error then the dumpfile may exist and be empty.
deleteDumpFile();
}
return dumpFile.exists();
}
/*
* @param command The {@link AndroidCommand} used for this handler.
*
* @return {@link AndroidCommandResult}
*
* @throws JSONException
*
* @see io.appium.android.bootstrap.CommandHandler#execute(io.appium.android.
* bootstrap.AndroidCommand)
*/
@Override
public AndroidCommandResult execute(final AndroidCommand command) {
NotImportantViews.discard(true);
return getSuccessResult(dump());
}
}
~~~
这个方法可能在某些机器上执行到不成功而让很多人对这个方法不甚了解,而我做了很长事件的功能遍历工具的开发,专门研究过这个方法,看看我之前写的文章就理解它是做什么的啦:
[dumpWindowHierarchy](http://blog.csdn.net/itfootball/article/details/dumpWindowHierarchy)
它是获取当前手机界面所有控件信息,然后把树形结构保存在/data/local/tmp的目录下的dump.xml文件中,所以我们看见上面类的定义有很多关于路径、文件的字符串,就是这个原因,appium的这个DumpWindowHierarchy首先根据api的不同设置是否禁止布局压缩。如果api为18以上的,那么就启动布局压缩,这对我们获取有用的控件信息是很有作用的。然后执行dump()方法,dump方法会先创建目录,当然该目录一般都会存在,无需创建。然后删除dump.xml文件,因为要创建新的,必须删除旧的,以免获取不到控件信息的时候,dump.xml里仍然有就信息返回。然后调用
~~~
UiDevice.getInstance().dumpWindowHierarchy(dumpFileName);
~~~
将控件树信息保存在里文件中。
(android官网终于可以上了!)
bootstrap之PressKeyCode&&LongPressKeyCode
最后更新于:2022-04-01 06:53:59
# PressKeyCode
~~~
package io.appium.android.bootstrap.handler;
import com.android.uiautomator.core.UiDevice;
import io.appium.android.bootstrap.AndroidCommand;
import io.appium.android.bootstrap.AndroidCommandResult;
import io.appium.android.bootstrap.CommandHandler;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Hashtable;
/**
* This handler is used to PressKeyCode.
*
*/
public class PressKeyCode extends CommandHandler {
public Integer keyCode;
public Integer metaState;
/*
* @param command The {@link AndroidCommand} used for this handler.
*
* @return {@link AndroidCommandResult}
*
* @throws JSONException
*
* @see io.appium.android.bootstrap.CommandHandler#execute(io.appium.android.
* bootstrap.AndroidCommand)
*/
@Override
public AndroidCommandResult execute(final AndroidCommand command)
throws JSONException {
try {
final Hashtable<String, Object> params = command.params();
Object kc = params.get("keycode");
if (kc instanceof Integer) {
keyCode = (Integer) kc;
} else if (kc instanceof String) {
keyCode = Integer.parseInt((String) kc);
} else {
throw new IllegalArgumentException("Keycode of type " + kc.getClass() + "not supported.");
}
if (params.get("metastate") != JSONObject.NULL) {
metaState = (Integer) params.get("metastate");
UiDevice.getInstance().pressKeyCode(keyCode, metaState);
} else {
UiDevice.getInstance().pressKeyCode(keyCode);
}
return getSuccessResult(true);
} catch (final Exception e) {
return getErrorResult(e.getMessage());
}
}
}
~~~
有的时候手机键盘的一些键需要按,但是又没有像pressBack这种方法供我们直接调用,这个时候我们就需要发送键盘的keycode来模拟这些键被点击。所以PressKeyCode就是封装这类事件的,通过上面的代码可以看出,发送keycode分2类事件,每一类调用的方法不一样
* 单点键:pressKeyCode(keyCode,metaState)
* 组合键: pressKeyCode(keyCode,metaState)
# LongPressKeyCode
~~~
package io.appium.android.bootstrap.handler;
import android.os.SystemClock;
import android.view.InputDevice;
import android.view.InputEvent;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import com.android.uiautomator.common.ReflectionUtils;
import io.appium.android.bootstrap.AndroidCommand;
import io.appium.android.bootstrap.AndroidCommandResult;
import io.appium.android.bootstrap.CommandHandler;
import org.json.JSONException;
import org.json.JSONObject;
import java.lang.reflect.Method;
import java.util.Hashtable;
/**
* This handler is used to LongPressKeyCode.
*
*/
public class LongPressKeyCode extends CommandHandler {
public Integer keyCode;
public Integer metaState;
/*
* @param command The {@link AndroidCommand} used for this handler.
*
* @return {@link AndroidCommandResult}
*
* @throws JSONException
*
* @see io.appium.android.bootstrap.CommandHandler#execute(io.appium.android.
* bootstrap.AndroidCommand)
*/
@Override
public AndroidCommandResult execute(final AndroidCommand command)
throws JSONException {
try {
final ReflectionUtils utils = new ReflectionUtils();
final Method injectEventSync = utils.getControllerMethod("injectEventSync",
InputEvent.class);
final Hashtable<String, Object> params = command.params();
keyCode = (Integer) params.get("keycode");
metaState = params.get("metastate") != JSONObject.NULL ? (Integer) params
.get("metastate") : 0;
final long eventTime = SystemClock.uptimeMillis();
// Send an initial down event
final KeyEvent downEvent = new KeyEvent(eventTime, eventTime,
KeyEvent.ACTION_DOWN, keyCode, 0, metaState,
KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, InputDevice.SOURCE_KEYBOARD);
if ((Boolean) injectEventSync.invoke(utils.getController(), downEvent)) {
// Send a repeat event. This will cause the FLAG_LONG_PRESS to be set.
final KeyEvent repeatEvent = KeyEvent.changeTimeRepeat(downEvent,
eventTime, 1);
injectEventSync.invoke(utils.getController(), repeatEvent);
// Finally, send the up event
final KeyEvent upEvent = new KeyEvent(eventTime, eventTime,
KeyEvent.ACTION_UP, keyCode, 0, metaState,
KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, InputDevice.SOURCE_KEYBOARD);
injectEventSync.invoke(utils.getController(), upEvent);
}
return getSuccessResult(true);
} catch (final Exception e) {
return getErrorResult(e.getMessage());
}
}
}
~~~
长点击某个键盘键,和上面的单击是有区别的,因为没有直接的API可以调用,所以又要用到反射来做这件事,这次的反射呢调用的是InteractionController中的injectEventSync方法,首先会执行ACTION_DOWN,然后在一段时间里会重复执行这个down的动作,等事件到了以后,执行ACTION_UP这个动作松开键盘键。从而达到了我们到长按到要求。
bootstrap之Wake&&PressBack&&TakeScreenshot&&OpenNotification
最后更新于:2022-04-01 06:53:56
# Wake
~~~
package io.appium.android.bootstrap.handler;
import android.os.RemoteException;
import com.android.uiautomator.core.UiDevice;
import io.appium.android.bootstrap.AndroidCommand;
import io.appium.android.bootstrap.AndroidCommandResult;
import io.appium.android.bootstrap.CommandHandler;
/**
* This handler is used to power on the device if it's not currently awake.
*
*/
public class Wake extends CommandHandler {
/*
* @param command The {@link AndroidCommand} used for this handler.
*
* @return {@link AndroidCommandResult}
*
* @throws JSONException
*
* @see io.appium.android.bootstrap.CommandHandler#execute(io.appium.android.
* bootstrap.AndroidCommand)
*/
@Override
public AndroidCommandResult execute(final AndroidCommand command) {
// only makes sense on a device
try {
UiDevice.getInstance().wakeUp();
return getSuccessResult(true);
} catch (final RemoteException e) {
return getErrorResult("Error waking up device");
}
}
}
~~~
wake事件为唤醒手机,有的时候手机处于睡眠状态就需要点亮屏幕,如果此时屏幕就是亮的,那么不做任何操作。
# PressBack
~~~
package io.appium.android.bootstrap.handler;
import com.android.uiautomator.core.UiDevice;
import io.appium.android.bootstrap.AndroidCommand;
import io.appium.android.bootstrap.AndroidCommandResult;
import io.appium.android.bootstrap.CommandHandler;
/**
* This handler is used to press back.
*
*/
public class PressBack extends CommandHandler {
/*
* @param command The {@link AndroidCommand} used for this handler.
*
* @return {@link AndroidCommandResult}
*
* @throws JSONException
*
* @see io.appium.android.bootstrap.CommandHandler#execute(io.appium.android.
* bootstrap.AndroidCommand)
*/
@Override
public AndroidCommandResult execute(final AndroidCommand command) {
UiDevice.getInstance().pressBack();
// Press back returns false even when back was successfully pressed.
// Always return true.
return getSuccessResult(true);
}
}
~~~
在手机上的返回键。
# TakeScreenshot
~~~
package io.appium.android.bootstrap.handler;
import com.android.uiautomator.core.UiDevice;
import io.appium.android.bootstrap.AndroidCommand;
import io.appium.android.bootstrap.AndroidCommandResult;
import io.appium.android.bootstrap.CommandHandler;
import java.io.File;
/**
* This handler is used to TakeScreenshot.
*
*/
public class TakeScreenshot extends CommandHandler {
/*
* @param command The {@link AndroidCommand} used for this handler.
*
* @return {@link AndroidCommandResult}
*
* @throws JSONException
*
* @see io.appium.android.bootstrap.CommandHandler#execute(io.appium.android.
* bootstrap.AndroidCommand)
*/
@Override
public AndroidCommandResult execute(final AndroidCommand command) {
final File screenshot = new File("/data/local/tmp/screenshot.png");
try {
screenshot.getParentFile().mkdirs();
} catch (final Exception e) {
}
if (screenshot.exists()) {
screenshot.delete();
}
UiDevice.getInstance().takeScreenshot(screenshot);
return getSuccessResult(screenshot.exists());
}
}
~~~
截屏并将图片保存在/data/local/tmp路径下的screenshot.png文件中。
# OpenNotification
~~~
package io.appium.android.bootstrap.handler;
import com.android.uiautomator.core.UiDevice;
import io.appium.android.bootstrap.AndroidCommand;
import io.appium.android.bootstrap.AndroidCommandResult;
import io.appium.android.bootstrap.CommandHandler;
import static io.appium.android.bootstrap.utils.API.API_18;
/**
* This handler is used to open the notification shade on the device.
*
*/
public class OpenNotification extends CommandHandler {
/*
* @param command The {@link AndroidCommand} used for this handler.
*
* @return {@link AndroidCommandResult}
*
* @see io.appium.android.bootstrap.CommandHandler#execute(io.appium.android.
* bootstrap.AndroidCommand)
*/
@Override
public AndroidCommandResult execute(final AndroidCommand command) {
// method was only introduced in API Level 18
if (!API_18) {
return getErrorResult("Unable to open notifications on device below API level 18");
}
// does not make sense on an element
if (command.isElementCommand()) {
return getErrorResult("Unable to open notifications on an element.");
}
final UiDevice device = UiDevice.getInstance();
if (device.openNotification()) {
return getSuccessResult(true);
} else {
return getErrorResult("Device failed to open notifications.");
}
}
}
~~~
打开通知栏操作,api18以后在UiDevice中添加了openNotification()方法,打开通知栏。所以该事件就是去调用该方法
bootstrap之ScrollTo
最后更新于:2022-04-01 06:53:54
# ScrollTo
~~~
package io.appium.android.bootstrap.handler;
import com.android.uiautomator.core.UiObject;
import com.android.uiautomator.core.UiObjectNotFoundException;
import com.android.uiautomator.core.UiScrollable;
import com.android.uiautomator.core.UiSelector;
import io.appium.android.bootstrap.*;
import org.json.JSONException;
import java.util.Hashtable;
/**
* This handler is used to scroll to elements in the Android UI.
*
* Based on the element Id of the scrollable, scroll to the object with the
* text.
*
*/
public class ScrollTo extends CommandHandler {
/*
* @param command The {@link AndroidCommand}
*
* @return {@link AndroidCommandResult}
*
* @throws JSONException
*
* @see io.appium.android.bootstrap.CommandHandler#execute(io.appium.android.
* bootstrap.AndroidCommand)
*/
@Override
public AndroidCommandResult execute(final AndroidCommand command)
throws JSONException {
if (!command.isElementCommand()) {
return getErrorResult("A scrollable view is required for this command.");
}
try {
Boolean result;
final Hashtable<String, Object> params = command.params();
final String text = params.get("text").toString();
final String direction = params.get("direction").toString();
final AndroidElement el = command.getElement();
if (!el.getUiObject().isScrollable()) {
return getErrorResult("The provided view is not scrollable.");
}
final UiScrollable view = new UiScrollable(el.getUiObject().getSelector());
if (direction.toLowerCase().contentEquals("horizontal")
|| view.getClassName().contentEquals(
"android.widget.HorizontalScrollView")) {
view.setAsHorizontalList();
}
view.scrollToBeginning(100);
view.setMaxSearchSwipes(100);
result = view.scrollTextIntoView(text);
view.waitForExists(5000);
// make sure we can get to the item
UiObject listViewItem = view.getChildByInstance(
new UiSelector().text(text), 0);
// We need to make sure that the item exists (visible)
if (!(result && listViewItem.exists())) {
return getErrorResult("Could not scroll element into view: " + text);
}
return getSuccessResult(result);
} catch (final UiObjectNotFoundException e) {
return new AndroidCommandResult(WDStatus.NO_SUCH_ELEMENT, e.getMessage());
} catch (final NullPointerException e) { // el is null
return new AndroidCommandResult(WDStatus.NO_SUCH_ELEMENT, e.getMessage());
} catch (final Exception e) {
return new AndroidCommandResult(WDStatus.UNKNOWN_ERROR, e.getMessage());
}
}
}
~~~
在uiautomator中有时候需要在一个滚动的list中找到某一个item,而这个item的位置又不定,这个时候我们可以通过UiScrollable的scrollTo来找到特定text的控件。而bootstrap的这个ScrollTo就是封装这样一种需求的。
首先判断控件是否是可以滚动的,然后创建UiScrollable对象,因为默认的滚动方式是垂直方向的,如果需要的是水平方向的的话,还要设置方向为水平。
~~~
view.setAsHorizontalList();
~~~
然后将滚动控件滚到最开始的地方,然后设置最大的搜索范围为100次,因为不可能永远搜索下去。然后开始调用
~~~
view.scrollTextIntoView(text);
~~~
开始滚动,最后确认是否滚动到制定目标:
~~~
view.waitForExists(5000);
~~~
因为有可能有刷新到时间,所以调用方法到时候传入了时间5秒钟。
~~~
UiObject listViewItem = view.getChildByInstance(
new UiSelector().text(text), 0);
~~~
最后检查结果,获取制定text的对象,判断其是否存在然后返回相应结果给客户端。
bootstrap之GetName&&GetAttribute&&GetDeviceSize&&GetSize&&GetLocation&&GetDataDir
最后更新于:2022-04-01 06:53:52
# GetName
~~~
package io.appium.android.bootstrap.handler;
import com.android.uiautomator.core.UiObjectNotFoundException;
import io.appium.android.bootstrap.*;
import org.json.JSONException;
/**
* This handler is used to get the text of elements that support it.
*
*/
public class GetName extends CommandHandler {
/*
* @param command The {@link AndroidCommand} used for this handler.
*
* @return {@link AndroidCommandResult}
*
* @throws JSONException
*
* @see io.appium.android.bootstrap.CommandHandler#execute(io.appium.android.
* bootstrap.AndroidCommand)
*/
@Override
public AndroidCommandResult execute(final AndroidCommand command)
throws JSONException {
if (!command.isElementCommand()) {
return getErrorResult("Unable to get name without an element.");
}
try {
final AndroidElement el = command.getElement();
return getSuccessResult(el.getContentDesc());
} catch (final UiObjectNotFoundException e) {
return new AndroidCommandResult(WDStatus.NO_SUCH_ELEMENT, e.getMessage());
} catch (final Exception e) { // handle NullPointerException
return getErrorResult("Unknown error");
}
}
}
~~~
最终会调用UiObject.getContentDescription()方法获得控件当描述信息
# GetAttribute
~~~
package io.appium.android.bootstrap.handler;
import com.android.uiautomator.core.UiObjectNotFoundException;
import io.appium.android.bootstrap.*;
import io.appium.android.bootstrap.exceptions.NoAttributeFoundException;
import org.json.JSONException;
import java.util.Hashtable;
/**
* This handler is used to get an attribute of an element.
*
*/
public class GetAttribute extends CommandHandler {
/*
* @param command The {@link AndroidCommand} used for this handler.
*
* @return {@link AndroidCommandResult}
*
* @throws JSONException
*
* @see io.appium.android.bootstrap.CommandHandler#execute(io.appium.android.
* bootstrap.AndroidCommand)
*/
@Override
public AndroidCommandResult execute(final AndroidCommand command)
throws JSONException {
if (command.isElementCommand()) {
// only makes sense on an element
final Hashtable<String, Object> params = command.params();
try {
final AndroidElement el = command.getElement();
final String attr = params.get("attribute").toString();
if (attr.equals("name") || attr.equals("text")
|| attr.equals("className")) {
return getSuccessResult(el.getStringAttribute(attr));
} else {
return getSuccessResult(String.valueOf(el.getBoolAttribute(attr)));
}
} catch (final NoAttributeFoundException e) {
return new AndroidCommandResult(WDStatus.NO_SUCH_ELEMENT,
e.getMessage());
} catch (final UiObjectNotFoundException e) {
return new AndroidCommandResult(WDStatus.NO_SUCH_ELEMENT,
e.getMessage());
} catch (final Exception e) { // el is null
return new AndroidCommandResult(WDStatus.NO_SUCH_ELEMENT,
e.getMessage());
}
} else {
return getErrorResult("Unable to get attribute without an element.");
}
}
}
~~~
该事件是为获取控件相关信息设置的,来看看可以获取哪些信息,其实就是uiautomatorviewer里的信息。
~~~
public String getStringAttribute(final String attr)
throws UiObjectNotFoundException, NoAttributeFoundException {
String res;
if (attr.equals("name")) {
res = getContentDesc();
if (res.equals("")) {
res = getText();
}
} else if (attr.equals("text")) {
res = getText();
} else if (attr.equals("className")) {
res = getClassName();
} else {
throw new NoAttributeFoundException(attr);
}
return res;
}
~~~
文本值:content-desc、text、className.
~~~
public boolean getBoolAttribute(final String attr)
throws UiObjectNotFoundException, NoAttributeFoundException {
boolean res;
if (attr.equals("enabled")) {
res = el.isEnabled();
} else if (attr.equals("checkable")) {
res = el.isCheckable();
} else if (attr.equals("checked")) {
res = el.isChecked();
} else if (attr.equals("clickable")) {
res = el.isClickable();
} else if (attr.equals("focusable")) {
res = el.isFocusable();
} else if (attr.equals("focused")) {
res = el.isFocused();
} else if (attr.equals("longClickable")) {
res = el.isLongClickable();
} else if (attr.equals("scrollable")) {
res = el.isScrollable();
} else if (attr.equals("selected")) {
res = el.isSelected();
} else if (attr.equals("displayed")) {
res = el.exists();
} else {
throw new NoAttributeFoundException(attr);
}
return res;
}
~~~
boolean类型的值:如上等等。
# GetDeviceSize
~~~
package io.appium.android.bootstrap.handler;
import com.android.uiautomator.core.UiDevice;
import io.appium.android.bootstrap.AndroidCommand;
import io.appium.android.bootstrap.AndroidCommandResult;
import io.appium.android.bootstrap.CommandHandler;
import org.json.JSONException;
import org.json.JSONObject;
/**
* This handler is used to get the size of the screen.
*
*/
public class GetDeviceSize extends CommandHandler {
/*
* @param command The {@link AndroidCommand} used for this handler.
*
* @return {@link AndroidCommandResult}
*
* @throws JSONException
*
* @see io.appium.android.bootstrap.CommandHandler#execute(io.appium.android.
* bootstrap.AndroidCommand)
*/
@Override
public AndroidCommandResult execute(final AndroidCommand command) {
if (!command.isElementCommand()) {
// only makes sense on a device
final UiDevice d = UiDevice.getInstance();
final JSONObject res = new JSONObject();
try {
res.put("height", d.getDisplayHeight());
res.put("width", d.getDisplayWidth());
} catch (final JSONException e) {
getErrorResult("Error serializing height/width data into JSON");
}
return getSuccessResult(res);
} else {
return getErrorResult("Unable to get device size on an element.");
}
}
}
~~~
获取屏幕的长和宽,调用的是UiDevice的方法:getDisplayHeight()和getDisplayWidth()
# GetSize
~~~
package io.appium.android.bootstrap.handler;
import android.graphics.Rect;
import com.android.uiautomator.core.UiObjectNotFoundException;
import io.appium.android.bootstrap.*;
import org.json.JSONException;
import org.json.JSONObject;
/**
* This handler is used to get the size of elements that support it.
*
*/
public class GetSize extends CommandHandler {
/*
* @param command The {@link AndroidCommand} used for this handler.
*
* @return {@link AndroidCommandResult}
*
* @throws JSONException
*
* @see io.appium.android.bootstrap.CommandHandler#execute(io.appium.android.
* bootstrap.AndroidCommand)
*/
@Override
public AndroidCommandResult execute(final AndroidCommand command)
throws JSONException {
if (command.isElementCommand()) {
// Only makes sense on an element
final JSONObject res = new JSONObject();
try {
final AndroidElement el = command.getElement();
final Rect rect = el.getBounds();
res.put("width", rect.width());
res.put("height", rect.height());
} catch (final UiObjectNotFoundException e) {
return new AndroidCommandResult(WDStatus.NO_SUCH_ELEMENT,
e.getMessage());
} catch (final Exception e) { // handle NullPointerException
return getErrorResult("Unknown error");
}
return getSuccessResult(res);
} else {
return getErrorResult("Unable to get text without an element.");
}
}
}
~~~
获取控件的宽和高,调用的是UiObject的getBounds()。得到一个矩形,然后获得其宽和高。
# GetLocation
~~~
package io.appium.android.bootstrap.handler;
import android.graphics.Rect;
import io.appium.android.bootstrap.*;
import org.json.JSONException;
import org.json.JSONObject;
/**
* This handler is used to get the text of elements that support it.
*
*/
public class GetLocation extends CommandHandler {
/*
* @param command The {@link AndroidCommand} used for this handler.
*
* @return {@link AndroidCommandResult}
*
* @throws JSONException
*
* @see io.appium.android.bootstrap.CommandHandler#execute(io.appium.android.
* bootstrap.AndroidCommand)
*/
@Override
public AndroidCommandResult execute(final AndroidCommand command)
throws JSONException {
if (!command.isElementCommand()) {
return getErrorResult("Unable to get location without an element.");
}
try {
final JSONObject res = new JSONObject();
final AndroidElement el = command.getElement();
final Rect bounds = el.getBounds();
res.put("x", bounds.left);
res.put("y", bounds.top);
return getSuccessResult(res);
} catch (final Exception e) {
return new AndroidCommandResult(WDStatus.NO_SUCH_ELEMENT, e.getMessage());
}
}
}
~~~
获取控件的起始点坐标。调用的也是getBounds,然后得到其起始点的x,y 坐标
# GetDataDir
~~~
package io.appium.android.bootstrap.handler;
import android.os.Environment;
import io.appium.android.bootstrap.AndroidCommand;
import io.appium.android.bootstrap.AndroidCommandResult;
import io.appium.android.bootstrap.CommandHandler;
/**
* This handler is used to get the data dir.
*
*/
public class GetDataDir extends CommandHandler {
/*
* @param command The {@link AndroidCommand} used for this handler.
*
* @return {@link AndroidCommandResult}
*
* @throws JSONException
*
* @see io.appium.android.bootstrap.CommandHandler#execute(io.appium.android.
* bootstrap.AndroidCommand)
*/
@Override
public AndroidCommandResult execute(final AndroidCommand command) {
return getSuccessResult(Environment.getDataDirectory());
}
}
~~~
获取data的根目录。调用的是android的api:Environment.getDataDirectory()
bootstrap之文本框的操作
最后更新于:2022-04-01 06:53:49
# setText
~~~
package io.appium.android.bootstrap.handler;
import com.android.uiautomator.core.UiDevice;
import com.android.uiautomator.core.UiObjectNotFoundException;
import io.appium.android.bootstrap.*;
import org.json.JSONException;
import java.util.Hashtable;
/**
* This handler is used to set text in elements that support it.
*
*/
public class SetText extends CommandHandler {
/*
* @param command The {@link AndroidCommand} used for this handler.
*
* @return {@link AndroidCommandResult}
*
* @throws JSONException
*
* @see io.appium.android.bootstrap.CommandHandler#execute(io.appium.android.
* bootstrap.AndroidCommand)
*/
@Override
public AndroidCommandResult execute(final AndroidCommand command)
throws JSONException {
if (command.isElementCommand()) {
// Only makes sense on an element
try {
final Hashtable<String, Object> params = command.params();
final AndroidElement el = command.getElement();
String text = params.get("text").toString();
Boolean pressEnter = false;
if (text.endsWith("\\n")) {
pressEnter = true;
text = text.replace("\\n", "");
Logger.debug("Will press enter after setting text");
}
final Boolean result = el.setText(text);
if (pressEnter) {
final UiDevice d = UiDevice.getInstance();
d.pressEnter();
}
return getSuccessResult(result);
} catch (final UiObjectNotFoundException e) {
return new AndroidCommandResult(WDStatus.NO_SUCH_ELEMENT,
e.getMessage());
} catch (final Exception e) { // handle NullPointerException
return getErrorResult("Unknown error");
}
} else {
return getErrorResult("Unable to set text without an element.");
}
}
}
~~~
写了这么多篇啦,上面的前几行代码都熟悉都不能再熟悉了,就不像第一篇文章那样一一介绍了,setText方法就是在可编辑都文本框中输入数据,所以也没什么特殊地方,无非就是调用UiObject的setText方法,不过仔细看看会发现,它做了一些处理,比如字符串的换行符删掉,输入完毕以后按一下enter键完成输入。那么我之前写case的时候这些也需要自己做,因为如果不按enter键的话,有些输入法是会出一些问题的,比如汉语它是先出你输入的字符代表的汉字然后你按enter它才会正确的显示在文本框里的,不象英文输入法直接在输入框里。所以appium考虑到里这些已经帮我们处理啦。
# getText
没什么可说到,就是简单到调用了UiObject的getText方法获取编辑框里的数据。
bootstrap之鼠标操作
最后更新于:2022-04-01 06:53:47
# TouchLongClick
~~~
package io.appium.android.bootstrap.handler;
import android.os.SystemClock;
import com.android.uiautomator.common.ReflectionUtils;
import com.android.uiautomator.core.UiObjectNotFoundException;
import io.appium.android.bootstrap.Logger;
import java.lang.reflect.Method;
/**
* This handler is used to long click elements in the Android UI.
*
*/
public class TouchLongClick extends TouchEvent {
/*
* UiAutomator has a broken longClick, so we'll try to implement it using the
* touchDown / touchUp events.
*/
private boolean correctLongClick(final int x, final int y, final int duration) {
try {
/*
* bridge.getClass() returns ShellUiAutomatorBridge on API 18/19 so use
* the super class.
*/
final ReflectionUtils utils = new ReflectionUtils();
final Method touchDown = utils.getControllerMethod("touchDown", int.class,
int.class);
final Method touchUp = utils.getControllerMethod("touchUp", int.class, int.class);
if ((Boolean) touchDown.invoke(utils.getController(), x, y)) {
SystemClock.sleep(duration);
if ((Boolean) touchUp.invoke(utils.getController(), x, y)) {
return true;
}
}
return false;
} catch (final Exception e) {
Logger.debug("Problem invoking correct long click: " + e);
return false;
}
}
@Override
protected boolean executeTouchEvent() throws UiObjectNotFoundException {
final Object paramDuration = params.get("duration");
int duration = 2000; // two seconds
if (paramDuration != null) {
duration = Integer.parseInt(paramDuration.toString());
}
printEventDebugLine("TouchLongClick", duration);
if (correctLongClick(clickX, clickY, duration)) {
return true;
}
// if correctLongClick failed and we have an element
// then uiautomator's longClick is used as a fallback.
if (isElement) {
Logger.debug("Falling back to broken longClick");
return el.longClick();
}
return false;
}
}
~~~
TouchLongClick类继承于TouchEvent,而TouchEvent继承于CommandHandler.调用TouchEvent的execute的方法中,调用了executeTouchEvent方法,所以我们来看上面的executeTouchEvent就好了,执行长点击事件,在uiautomator里有UiObject.longClick()方法,但是写过case的人知道,有时候这个方法达不到我们的需求,但是我们是自己了反射调用TouchDown和TouchUp两个个方法,而在appium里帮你解决了,它自己就帮你做到了这一点,如果你传入到是控件对象,那无可厚非,还是调用UiObject.longClick方法,如果你想根据坐标,时间在点击的话,那么就调用currectLongClick这个appium给你封装好的方法。
~~~
final ReflectionUtils utils = new ReflectionUtils();
final Method touchDown = utils.getControllerMethod("touchDown", int.class,
int.class);
final Method touchUp = utils.getControllerMethod("touchUp", int.class, int.class);
~~~
通过反射得到uiautomator里的没有公开的类,从而我们想要的方法touchDown和touchUp.
~~~
public ReflectionUtils() throws IllegalArgumentException,
IllegalAccessException, SecurityException, NoSuchFieldException {
final UiDevice device = UiDevice.getInstance();
final Object bridge = enableField(device.getClass(), "mUiAutomationBridge")
.get(device);
if (API_18) {
controller = enableField(bridge.getClass().getSuperclass(),
"mInteractionController").get(bridge);
} else {
controller = enableField(bridge.getClass(), "mInteractionController")
.get(bridge);
}
}
~~~
因为uiautomator api的改动,在api18以上的版本中,mInteractionController是存在于UiAutomationBridge的父类中的变量,而在18以下的版本中它是存在于本类中的。所以反射时会有一点点小小点差异,但总的来说都是要获得InteractionController这个类,因为这个类里面存在有我们要但touch类但方法。最后我们就能轻松调用鼠标的TouchUp和TouchDown他们啦。然后再加上时间,长按就实现啦。
# TouchUp
~~~
package io.appium.android.bootstrap.handler;
import com.android.uiautomator.common.ReflectionUtils;
import com.android.uiautomator.core.UiObjectNotFoundException;
import io.appium.android.bootstrap.Logger;
import java.lang.reflect.Method;
/**
* This handler is used to perform a touchDown event on an element in the
* Android UI.
*
*/
public class TouchDown extends TouchEvent {
@Override
protected boolean executeTouchEvent() throws UiObjectNotFoundException {
printEventDebugLine("TouchDown");
try {
final ReflectionUtils utils = new ReflectionUtils();
final Method touchDown = utils.getControllerMethod("touchDown", int.class,
int.class);
return (Boolean) touchDown.invoke(utils.getController(), clickX, clickY);
} catch (final Exception e) {
Logger.debug("Problem invoking touchDown: " + e);
return false;
}
}
}
~~~
有了上面的分析,对TouchUp和TouchDown还有TouchMove的分析就不用再多说了,都是反射的原理
# TouchDown
类同
# TouchMove
类同
bootstrap之Pinch
最后更新于:2022-04-01 06:53:45
# Pinch
~~~
package io.appium.android.bootstrap.handler;
import com.android.uiautomator.core.UiObjectNotFoundException;
import io.appium.android.bootstrap.*;
import org.json.JSONException;
import java.util.Hashtable;
/**
* This handler is used to pinch in/out elements in the Android UI.
*
* Based on the element Id, pinch in/out that element.
*
*/
public class Pinch extends CommandHandler {
/*
* @param command The {@link AndroidCommand} used for this handler.
*
* @return {@link AndroidCommandResult}
*
* @throws JSONException
*
* @see io.appium.android.bootstrap.CommandHandler#execute(io.appium.android.
* bootstrap.AndroidCommand)
*/
@Override
public AndroidCommandResult execute(final AndroidCommand command)
throws JSONException {
final Hashtable<String, Object> params = command.params();
AndroidElement el;
final String direction = params.get("direction").toString();
final Integer percent = (Integer) params.get("percent");
final Integer steps = (Integer) params.get("steps");
try {
el = command.getElement();
if (el == null) {
return getErrorResult("Could not find an element with elementId: "
+ params.get("elementId"));
}
} catch (final Exception e) { // JSONException, NullPointerException, etc.
return getErrorResult("Unknown error:" + e.getMessage());
}
Logger.debug("Pinching " + direction + " " + percent.toString() + "%"
+ " with steps: " + steps.toString());
boolean res;
if (direction.equals("in")) {
try {
res = el.pinchIn(percent, steps);
} catch (final UiObjectNotFoundException e) {
return getErrorResult("Selector could not be matched to any UI element displayed");
}
} else {
try {
res = el.pinchOut(percent, steps);
} catch (final UiObjectNotFoundException e) {
return getErrorResult("Selector could not be matched to any UI element displayed");
}
}
if (res) {
return getSuccessResult(res);
} else {
return getErrorResult("Pinch did not complete successfully");
}
}
}
~~~
了解2个概念:pinchIn和pinchOut。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-08_568f4d18d31ea.jpg)
2个手指同时按住A和B,同时将A滑向A1,B滑向B1,称为pinchIn
2个手指同时按住A1和B1,同时将A1滑向A,B1滑向B,成为pinchOut.
那么Pinch类就是接受这一指令的,传入的参数有方向direction,percent代表滑到对角线百分之几停止,steps代表滑到时间,一个step代表5毫秒。根据direction来判断是调用UiObject.pinchIn和UiObject.pinchOut.
bootstrap之Drag
最后更新于:2022-04-01 06:53:43
# Drag
~~~
package io.appium.android.bootstrap.handler;
import com.android.uiautomator.core.UiDevice;
import com.android.uiautomator.core.UiObjectNotFoundException;
import io.appium.android.bootstrap.*;
import io.appium.android.bootstrap.exceptions.InvalidCoordinatesException;
import io.appium.android.bootstrap.utils.Point;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Hashtable;
/**
* This handler is used to drag in the Android UI.
*
*/
public class Drag extends CommandHandler {
/*
* @param command The {@link AndroidCommand} used for this handler.
*
* @return {@link AndroidCommandResult}
*
* @throws JSONException
*
* @see io.appium.android.bootstrap.CommandHandler#execute(io.appium.android.
* bootstrap.AndroidCommand)
*/
private static class DragArguments {
public AndroidElement el;
public AndroidElement destEl;
public final Point start;
public final Point end;
public final Integer steps;
public DragArguments(final AndroidCommand command) throws JSONException {
final Hashtable<String, Object> params = command.params();
try {
if (params.get("elementId") != JSONObject.NULL) {
el = command.getElement();
}
} catch (final Exception e) {
el = null;
}
try {
if (params.get("destElId") != JSONObject.NULL) {
destEl = command.getDestElement();
}
} catch (final Exception e) {
destEl = null;
}
start = new Point(params.get("startX"), params.get("startY"));
end = new Point(params.get("endX"), params.get("endY"));
steps = (Integer) params.get("steps");
}
}
private AndroidCommandResult drag(final DragArguments dragArgs) {
Point absStartPos = new Point();
Point absEndPos = new Point();
final UiDevice device = UiDevice.getInstance();
try {
absStartPos = getDeviceAbsPos(dragArgs.start);
absEndPos = getDeviceAbsPos(dragArgs.end);
} catch (final InvalidCoordinatesException e) {
return getErrorResult(e.getMessage());
}
Logger.debug("Dragging from " + absStartPos.toString() + " to "
+ absEndPos.toString() + " with steps: " + dragArgs.steps.toString());
final boolean rv = device.drag(absStartPos.x.intValue(),
absStartPos.y.intValue(), absEndPos.x.intValue(),
absEndPos.y.intValue(), dragArgs.steps);
if (!rv) {
return getErrorResult("Drag did not complete successfully");
}
return getSuccessResult(rv);
}
private AndroidCommandResult dragElement(final DragArguments dragArgs) {
Point absEndPos = new Point();
if (dragArgs.destEl == null) {
try {
absEndPos = getDeviceAbsPos(dragArgs.end);
} catch (final InvalidCoordinatesException e) {
return getErrorResult(e.getMessage());
}
Logger.debug("Dragging the element with id " + dragArgs.el.getId()
+ " to " + absEndPos.toString() + " with steps: "
+ dragArgs.steps.toString());
try {
final boolean rv = dragArgs.el.dragTo(absEndPos.x.intValue(),
absEndPos.y.intValue(), dragArgs.steps);
if (!rv) {
return getErrorResult("Drag did not complete successfully");
} else {
return getSuccessResult(rv);
}
} catch (final UiObjectNotFoundException e) {
return getErrorResult("Drag did not complete successfully"
+ e.getMessage());
}
} else {
Logger.debug("Dragging the element with id " + dragArgs.el.getId()
+ " to destination element with id " + dragArgs.destEl.getId()
+ " with steps: " + dragArgs.steps);
try {
final boolean rv = dragArgs.el.dragTo(dragArgs.destEl.getUiObject(),
dragArgs.steps);
if (!rv) {
return getErrorResult("Drag did not complete successfully");
} else {
return getSuccessResult(rv);
}
} catch (final UiObjectNotFoundException e) {
return getErrorResult("Drag did not complete successfully"
+ e.getMessage());
}
}
}
@Override
public AndroidCommandResult execute(final AndroidCommand command)
throws JSONException {
// DragArguments is created on each execute which prevents leaking state
// across executions.
final DragArguments dragArgs = new DragArguments(command);
if (command.isElementCommand()) {
return dragElement(dragArgs);
} else {
return drag(dragArgs);
}
}
}
~~~
首先来看DragArguments对象。该类为Drag中的私有类,它的构造方法回去解析传入的command,然后获得并存储一些drag方法用到的参数。例如拖拽控件时,被拖拽的控件对象,拖拽到的控件对象。坐标拖拽时,起始点坐标、终点坐标、步骤。所以就把它看成一个实体类就行了。然后分控件和坐标分别调用dragElement()和drag()方法。
## dragElement()
如果拖拽到的控件不存在,那么就要用到结束坐标。如果拖拽到的控件存在,那么就用该控件计算拖拽到坐标,调用UiObject.dragTo()方法来执行命令。
## drag()
这个就好办了,都是坐标值,直接调用UiObject.dragTo来执行。
bootstrap之Flick
最后更新于:2022-04-01 06:53:40
# Flick
~~~
package io.appium.android.bootstrap.handler;
import com.android.uiautomator.core.UiDevice;
import io.appium.android.bootstrap.*;
import io.appium.android.bootstrap.exceptions.InvalidCoordinatesException;
import io.appium.android.bootstrap.utils.Point;
import org.json.JSONException;
import java.util.Hashtable;
/**
* This handler is used to flick elements in the Android UI.
*
* Based on the element Id, flick that element.
*
*/
public class Flick extends CommandHandler {
private Point calculateEndPoint(final Point start, final Integer xSpeed,
final Integer ySpeed) {
final UiDevice d = UiDevice.getInstance();
final Point end = new Point();
final double speedRatio = (double) xSpeed / ySpeed;
double xOff;
double yOff;
final double value = Math.min(d.getDisplayHeight(), d.getDisplayWidth());
if (speedRatio < 1) {
yOff = value / 4;
xOff = value / 4 * speedRatio;
} else {
xOff = value / 4;
yOff = value / 4 / speedRatio;
}
xOff = Integer.signum(xSpeed) * xOff;
yOff = Integer.signum(ySpeed) * yOff;
end.x = start.x + xOff;
end.y = start.y + yOff;
return end;
}
/*
* @param command The {@link AndroidCommand} used for this handler.
*
* @return {@link AndroidCommandResult}
*
* @throws JSONException
*
* @see io.appium.android.bootstrap.CommandHandler#execute(io.appium.android.
* bootstrap.AndroidCommand)
*/
@Override
public AndroidCommandResult execute(final AndroidCommand command)
throws JSONException {
Point start = new Point(0.5, 0.5);
Point end = new Point();
Double steps;
final Hashtable<String, Object> params = command.params();
final UiDevice d = UiDevice.getInstance();
if (command.isElementCommand()) {
AndroidElement el;
try {
el = command.getElement();
start = el.getAbsolutePosition(start);
final Integer xoffset = (Integer) params.get("xoffset");
final Integer yoffset = (Integer) params.get("yoffset");
final Integer speed = (Integer) params.get("speed");
steps = 1250.0 / speed + 1;
end.x = start.x + xoffset;
end.y = start.y + yoffset;
} catch (final Exception e) {
return getErrorResult(e.getMessage());
}
} else {
try {
final Integer xSpeed = (Integer) params.get("xSpeed");
final Integer ySpeed = (Integer) params.get("ySpeed");
final Double speed = Math.min(1250.0,
Math.sqrt(xSpeed * xSpeed + ySpeed * ySpeed));
steps = 1250.0 / speed + 1;
start = getDeviceAbsPos(start);
end = calculateEndPoint(start, xSpeed, ySpeed);
} catch (final InvalidCoordinatesException e) {
return getErrorResult(e.getMessage());
}
}
steps = Math.abs(steps);
Logger.debug("Flicking from " + start.toString() + " to " + end.toString()
+ " with steps: " + steps.intValue());
final boolean res = d.swipe(start.x.intValue(), start.y.intValue(),
end.x.intValue(), end.y.intValue(), steps.intValue());
if (res) {
return getSuccessResult(res);
} else {
return getErrorResult("Flick did not complete successfully");
}
}
}
~~~
代码的步骤和swipe类似,而且最终调用的也是UiDevice.swipe方法,那么我们来看看到底区别在什么地方。首先它也分控件和坐标,分别分析:
# 控件
首先将控件的中心点作为起始坐标,然后根据提供的参数xoffset和yoffset来获取位移数据,speed参数用来计算步骤。
~~~
steps = 1250.0 / speed + 1;
end.x = start.x + xoffset;
end.y = start.y + yoffset;
~~~
起始坐标加上位移就是结束坐标,这个steps的设置还是有点让人摸不着头脑的,我这个1250我且认为是最大位移吧,speed代表每一步走的路程。用1250/speed得到使用多少步到结束点,再加上初始值的那个点就得到steps的值啦。至此起始点坐标、结束点坐标、步骤的值都设置完毕。
# 坐标
严格来说,不能说成坐标,应该算坐标位移,因为才传入的参数其实坐标系的速度xSpeed和ySpeed。x轴移动xSpeed距离,y轴移动ySpeed坐标。然后获取坐标值和steps值。
其中它用1250和位移的平方做了一次比较,取出最小值来计算steps。起始坐标点定位屏幕的中心点坐标。然后再调用end = calculateEndPoint(start, xSpeed, ySpeed);方法获取结束点坐标。
~~~
private Point calculateEndPoint(final Point start, final Integer xSpeed,
final Integer ySpeed) {
final UiDevice d = UiDevice.getInstance();
final Point end = new Point();
final double speedRatio = (double) xSpeed / ySpeed;
double xOff;
double yOff;
final double value = Math.min(d.getDisplayHeight(), d.getDisplayWidth());
if (speedRatio < 1) {
yOff = value / 4;
xOff = value / 4 * speedRatio;
} else {
xOff = value / 4;
yOff = value / 4 / speedRatio;
}
xOff = Integer.signum(xSpeed) * xOff;
yOff = Integer.signum(ySpeed) * yOff;
end.x = start.x + xOff;
end.y = start.y + yOff;
return end;
}
~~~
首先计算位移比speedRatio(x的位移/y的位移),然后获取屏幕宽和高中最小的一个数复制给value.如果speedRatio
最后调用UiDevice.swipe和Swipe中是一样的啦。没什么特别的
# 总结
特别想知道1250代表的是什么。不然老觉得还没理解这个方法的意思。哎
bootstrap之Swipe
最后更新于:2022-04-01 06:53:38
# Swipe
我定义为滑动,但它字面的意思又不是,事件的形式类似于小时候拿着一块石头片,朝水面飞过去,如果你手法可以那么就是swipe走的路线,如果你手法不行,接触水面的时候就没再飞起来那就会被人嘲笑的。
~~~
package io.appium.android.bootstrap.handler;
import com.android.uiautomator.core.UiDevice;
import com.android.uiautomator.core.UiObjectNotFoundException;
import io.appium.android.bootstrap.*;
import io.appium.android.bootstrap.exceptions.InvalidCoordinatesException;
import io.appium.android.bootstrap.utils.Point;
import org.json.JSONException;
import java.util.Hashtable;
/**
* This handler is used to swipe.
*
*/
public class Swipe extends CommandHandler {
/*
* @param command The {@link AndroidCommand} used for this handler.
*
* @return {@link AndroidCommandResult}
*
* @throws JSONException
*
* @see io.appium.android.bootstrap.CommandHandler#execute(io.appium.android.
* bootstrap.AndroidCommand)
*/
@Override
public AndroidCommandResult execute(final AndroidCommand command)
throws JSONException {
final Hashtable<String, Object> params = command.params();
final Point start = new Point(params.get("startX"), params.get("startY"));
final Point end = new Point(params.get("endX"), params.get("endY"));
final Integer steps = (Integer) params.get("steps");
final UiDevice device = UiDevice.getInstance();
Point absStartPos = new Point();
Point absEndPos = new Point();
if (command.isElementCommand()) {
try {
final AndroidElement el = command.getElement();
absStartPos = el.getAbsolutePosition(start);
absEndPos = el.getAbsolutePosition(end, false);
} catch (final UiObjectNotFoundException e) {
return getErrorResult(e.getMessage());
} catch (final InvalidCoordinatesException e) {
return getErrorResult(e.getMessage());
} catch (final Exception e) { // handle NullPointerException
return getErrorResult("Unknown error");
}
} else {
try {
absStartPos = getDeviceAbsPos(start);
absEndPos = getDeviceAbsPos(end);
} catch (final InvalidCoordinatesException e) {
return getErrorResult(e.getMessage());
}
}
Logger.debug("Swiping from " + absStartPos.toString() + " to "
+ absEndPos.toString() + " with steps: " + steps.toString());
final boolean rv = device.swipe(absStartPos.x.intValue(),
absStartPos.y.intValue(), absEndPos.x.intValue(),
absEndPos.y.intValue(), steps);
if (!rv) {
return getErrorResult("The swipe did not complete successfully");
}
return getSuccessResult(rv);
}
}
~~~
不管它如何定义,先分析源码最后再定义。
~~~
final Hashtable<String, Object> params = command.params();
final Point start = new Point(params.get("startX"), params.get("startY"));
final Point end = new Point(params.get("endX"), params.get("endY"));
final Integer steps = (Integer) params.get("steps");
final UiDevice device = UiDevice.getInstance();
Point absStartPos = new Point();
Point absEndPos = new Point();
~~~
首先从命令里取得参数,然后解析出所需要的3个变量:起始点start、终点end、步骤steps。然后获得设备对象,定义2个私有Point对象,以备后用。
然后分条件处理,处理控件还是处理坐标。
## 控件
~~~
final AndroidElement el = command.getElement();
absStartPos = el.getAbsolutePosition(start);
absEndPos = el.getAbsolutePosition(end, false);
~~~
首先获取控件对象,再通过getAbsolutePosition传入不同的参数获得在该控件上点击的起始点和结束点。
~~~
public Point getAbsolutePosition(final Point point,
final boolean boundsChecking) throws UiObjectNotFoundException,
InvalidCoordinatesException {
final Rect rect = el.getBounds();
final Point pos = new Point();
Logger.debug("Element bounds: " + rect.toShortString());
if (point.x == 0) {
pos.x = rect.width() * 0.5 + rect.left;
} else if (point.x <= 1) {
pos.x = rect.width() * point.x + rect.left;
} else {
pos.x = rect.left + point.x;
}
if (boundsChecking) {
if (pos.x > rect.right || pos.x < rect.left) {
throw new InvalidCoordinatesException("X coordinate ("
+ pos.x.toString() + " is outside of element rect: "
+ rect.toShortString());
}
}
if (point.y == 0) {
pos.y = rect.height() * 0.5 + rect.top;
} else if (point.y <= 1) {
pos.y = rect.height() * point.y + rect.top;
} else {
pos.y = rect.left + point.y;
}
if (boundsChecking) {
if (pos.y > rect.bottom || pos.y < rect.top) {
throw new InvalidCoordinatesException("Y coordinate ("
+ pos.y.toString() + " is outside of element rect: "
+ rect.toShortString());
}
}
return pos;
}
~~~
上面的一大段代码,看起来很复杂,其实很简单,业务很容易理解,处理这种点的时候就需要判断很多东西。上面的代码首先分析x坐标然后分析y坐标。x和y坐标的判断和处理时一样的,所以我只讲一下x坐标。
首先判断x坐标是否为0,如果为0,定义初始点的x坐标为控件的中心点的横坐标。如果x的坐标小于1,说明坐标为相对坐标,用百分比来求值,此时就要与宽度做乘积运算得到具体值。如果上面2种情况都不符合,那就是具体坐标值,那就直接元素的x坐标值加上控件的边框左坐标值。最后根据传入的boolean值来判断是否做一个超出边界的验证。如果超出边界就跑出异常。y坐标的获取方式类似。最后得到坐标值并返回,回到execute方法中。
## 坐标
~~~
absStartPos = getDeviceAbsPos(start);
absEndPos = getDeviceAbsPos(end);
~~~
通过调用getDeviceAbsPos()方法得到坐标值来初始化之前声明的私有Point对象.
~~~
protected static Point getDeviceAbsPos(final Point point)
throws InvalidCoordinatesException {
final UiDevice d = UiDevice.getInstance();
final Point retPos = new Point(point); // copy inputed point
final Double width = (double) d.getDisplayWidth();
if (point.x < 1) {
retPos.x = width * point.x;
}
if (retPos.x > width || retPos.x < 0) {
throw new InvalidCoordinatesException("X coordinate ("
+ retPos.x.toString() + " is outside of screen width: "
+ width.toString());
}
final Double height = (double) d.getDisplayHeight();
if (point.y < 1) {
retPos.y = height * point.y;
}
if (retPos.y > height || retPos.y < 0) {
throw new InvalidCoordinatesException("Y coordinate ("
+ retPos.y.toString() + " is outside of screen height: "
+ height.toString());
}
return retPos;
}
~~~
类似于上面的方法,也是要先判断传过来的坐标值是否小于1,如果小于1,当作百分比来球坐标值。如果超出屏幕的范围抛出异常,最后返回坐标值回到execute方法。
===============================================================================================================================
~~~
final boolean rv = device.swipe(absStartPos.x.intValue(),
absStartPos.y.intValue(), absEndPos.x.intValue(),
absEndPos.y.intValue(), steps);
~~~
最后调用UiDevice.swipe方法来执行命令,判断是否执行成功。
# 总结
执行swipe命令有2中命令格式
* 控件
* 坐标
坐标又分为相对坐标百分比和绝对坐标两种方法。
bootstrap之Orientation
最后更新于:2022-04-01 06:53:36
# Orientation
调整屏幕方向的操作。
~~~
package io.appium.android.bootstrap.handler;
import android.os.RemoteException;
import com.android.uiautomator.core.UiDevice;
import io.appium.android.bootstrap.*;
import org.json.JSONException;
import java.util.Hashtable;
/**
* This handler is used to get or set the orientation of the device.
*
*/
public class Orientation extends CommandHandler {
/*
* @param command The {@link AndroidCommand} used for this handler.
*
* @return {@link AndroidCommandResult}
*
* @throws JSONException
*
* @see io.appium.android.bootstrap.CommandHandler#execute(io.appium.android.
* bootstrap.AndroidCommand)
*/
@Override
public AndroidCommandResult execute(final AndroidCommand command)
throws JSONException {
final Hashtable<String, Object> params = command.params();
if (params.containsKey("orientation")) {
// Set the rotation
final String orientation = (String) params.get("orientation");
try {
return handleRotation(orientation);
} catch (final Exception e) {
return getErrorResult("Unable to rotate screen: " + e.getMessage());
}
} else {
// Get the rotation
return getRotation();
}
}
/**
* Returns the current rotation
*
* @return {@link AndroidCommandResult}
*/
private AndroidCommandResult getRotation() {
String res = null;
final UiDevice d = UiDevice.getInstance();
final OrientationEnum currentRotation = OrientationEnum.fromInteger(d
.getDisplayRotation());
Logger.debug("Current rotation: " + currentRotation);
switch (currentRotation) {
case ROTATION_0:
case ROTATION_180:
res = "PORTRAIT";
break;
case ROTATION_90:
case ROTATION_270:
res = "LANDSCAPE";
break;
}
if (res != null) {
return getSuccessResult(res);
} else {
return getErrorResult("Get orientation did not complete successfully");
}
}
/**
* Set the desired rotation
*
* @param orientation
* The rotation desired (LANDSCAPE or PORTRAIT)
* @return {@link AndroidCommandResult}
* @throws RemoteException
* @throws InterruptedException
*/
private AndroidCommandResult handleRotation(final String orientation)
throws RemoteException, InterruptedException {
final UiDevice d = UiDevice.getInstance();
OrientationEnum desired;
OrientationEnum current = OrientationEnum.fromInteger(d
.getDisplayRotation());
Logger.debug("Desired orientation: " + orientation);
Logger.debug("Current rotation: " + current);
if (orientation.equalsIgnoreCase("LANDSCAPE")) {
switch (current) {
case ROTATION_0:
d.setOrientationRight();
desired = OrientationEnum.ROTATION_270;
break;
case ROTATION_180:
d.setOrientationLeft();
desired = OrientationEnum.ROTATION_270;
break;
default:
return getSuccessResult("Already in landscape mode.");
}
} else {
switch (current) {
case ROTATION_90:
case ROTATION_270:
d.setOrientationNatural();
desired = OrientationEnum.ROTATION_0;
break;
default:
return getSuccessResult("Already in portrait mode.");
}
}
current = OrientationEnum.fromInteger(d.getDisplayRotation());
// If the orientation has not changed,
// busy wait until the TIMEOUT has expired
final int TIMEOUT = 2000;
final long then = System.currentTimeMillis();
long now = then;
while (current != desired && now - then < TIMEOUT) {
Thread.sleep(100);
now = System.currentTimeMillis();
current = OrientationEnum.fromInteger(d.getDisplayRotation());
}
if (current != desired) {
return getErrorResult("Set the orientation, but app refused to rotate.");
}
return getSuccessResult("Rotation (" + orientation + ") successful.");
}
}
~~~
这个事件有点小复杂哈,当初研究uiautomator源码时就被它折腾的不行,也只实验了左和上的方向成功。没办法,既然又遇到了,那就只能纯理论讲啦。
execute方法中,首先判断参数中是否含有orientation,如果含有调用handleRotation,否则调用getRotation。
所以execute又分流到上面的2个方法中。
## handleRotation
这种情况是参数里含有orientation,此时,我们来看看该方法中做了哪些事。
~~~
final UiDevice d = UiDevice.getInstance();
OrientationEnum desired;
OrientationEnum current = OrientationEnum.fromInteger(d
.getDisplayRotation());
~~~
首先获取当前设备的方向,然后初始化一个私有变量,以备后用。其中OrientationEnum枚举类里定义了4个方向,fromInteger方法是根据整数值得到相应的枚举值,其中各个值的意思。
~~~
public enum OrientationEnum {
ROTATION_0(0), ROTATION_90(1), ROTATION_180(2), ROTATION_270(3);
public static OrientationEnum fromInteger(final int x) {
switch (x) {
case 0:
return ROTATION_0;
case 1:
return ROTATION_90;
case 2:
return ROTATION_180;
case 3:
return ROTATION_270;
}
return null;
}
~~~
ROTATION_0:你正常查看手机时,竖屏。此时屏幕的方向为0度。此时的power键在顶端。如下:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-08_568f4d185bc2b.jpg)
ROTATION_90:你将上面的屏幕向右顺时针旋转90度,此时设备旋转角度为90度,此时我的power键在右端。如果此时你的设备可以自动旋转屏幕的话,你屏幕里面的内容应该是什么样的?如下所示
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-08_568f4d1877c31.jpg)
ROTATION_180:从90度角再向下顺时针旋转90度,此时我的power键在下端,此时的角度为180.由于我的手机禁止了这种自由旋转,所以此时的屏幕展现在我的面前是这样一番景象:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-08_568f4d1892320.jpg)
ROTATION_270:从180度再顺时针想左旋转90度,此时我power键在左边。此时为270度,展现在我面前的图如下:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-08_568f4d18ada3c.jpg)
如果你理解了上面4个关键字的意思。那么下面理解代码就很简单啦。
handleRotation方法里做完初始化操作以后,就要判断客户端要求是横屏还是竖屏,如果是横屏,处理如下:
~~~
if (orientation.equalsIgnoreCase("LANDSCAPE")) {
switch (current) {
case ROTATION_0:
d.setOrientationRight();
desired = OrientationEnum.ROTATION_270;
break;
case ROTATION_180:
d.setOrientationLeft();
desired = OrientationEnum.ROTATION_270;
break;
default:
return getSuccessResult("Already in landscape mode.");
}
}
~~~
如果是横屏的话,那么只需要处理屏幕处于0度和180度的情况,因为90度和270度都已经是横屏啦,自然不需要再处理。
如果是ROTATION_0,说明设备朝上,此时想要横屏,自然是顺时针向右旋转一下屏幕。此时,正常情况下可以旋转的话,屏幕里的视图应该是从左到右的,所以desired的值才会被设置为ROTATION_270.所以要分清屏幕的角度和视图的角度。下面就是向右顺时针旋转90度后,里面的视图是270度的,此时power键在右端。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-08_568f4d18ada3c.jpg)
如果是ROTATION_180度,说明设备拿反了,power键朝下。此时你向左顺时针旋转90度或者向右逆时针旋转90度,都能达到横屏的效果。源码里是向左旋转的,此时power键朝左,视图和上面是一样的,desired的值为ROTATION——270.
====================================================================================================================================
如果客户端传递过来的命令想要的是竖屏,就要走else里的代码块:
~~~
else {
switch (current) {
case ROTATION_90:
case ROTATION_270:
d.setOrientationNatural();
desired = OrientationEnum.ROTATION_0;
break;
default:
return getSuccessResult("Already in portrait mode.");
}
}
~~~
此时只要处理90度和270的情况。我们要把它变为ROTATION_0的情况,d.setOrientationNatural()是设置设备转到自然方向,该方向就是设备初始设置的方向。说明通常的设备横屏的2个方向可以旋转,竖屏方向就只有一个方向可以旋转。上面的处理完毕后,方法会做一个判断,判断是否旋转成功。
~~~
current = OrientationEnum.fromInteger(d.getDisplayRotation());
// If the orientation has not changed,
// busy wait until the TIMEOUT has expired
final int TIMEOUT = 2000;
final long then = System.currentTimeMillis();
long now = then;
while (current != desired && now - then < TIMEOUT) {
Thread.sleep(100);
now = System.currentTimeMillis();
current = OrientationEnum.fromInteger(d.getDisplayRotation());
}
if (current != desired) {
return getErrorResult("Set the orientation, but app refused to rotate.");
}
return getSuccessResult("Rotation (" + orientation + ") successful.");
~~~
首先获得此时屏幕的方向,然后判断一下与预期的是否相同。如果不相同,等待2秒钟,再获取一次屏幕的方向,如果经过这么一次验证完毕后,当前的屏幕方向仍然和预期的不相同,那么就返回旋转失败的消息给客户端。如果相同的话,就返回旋转成功的消息给客户端。
到此为止handleRotation处理完毕,下面处理参数里不含有orientation的情况。
## getRotation
该方法中就是根据当前的屏幕的方向得到横屏还是竖屏,将结果返回给客户端。很简单。
# 总结
通过上面的分析,说明客户端关于屏幕方向的命令有2种:
* 获取屏幕的方向
* 改变屏幕的方向
大家要特别主要选择方向的定义,设备的方向和里面视图的方向的区别。
bootstrap之WaitForIdle&&Clear
最后更新于:2022-04-01 06:53:33
(上篇文章写完才发现,说好的按顺序但是回头一看完全不是按顺序的)明明WaitForIdle才是第一个。哎,老了,后脑勺不行了。
# WaitForIdle
~~~
package io.appium.android.bootstrap.handler;
import com.android.uiautomator.core.UiDevice;
import io.appium.android.bootstrap.AndroidCommand;
import io.appium.android.bootstrap.AndroidCommandResult;
import io.appium.android.bootstrap.CommandHandler;
import org.json.JSONException;
import java.util.Hashtable;
/**
* This handler is used to clear elements in the Android UI.
*
* Based on the element Id, clear that element.
*
*/
public class WaitForIdle extends CommandHandler {
/*
* @param command The {@link AndroidCommand}
*
* @return {@link AndroidCommandResult}
*
* @throws JSONException
*
* @see io.appium.android.bootstrap.CommandHandler#execute(io.appium.android.
* bootstrap.AndroidCommand)
*/
@Override
public AndroidCommandResult execute(final AndroidCommand command)
throws JSONException {
final Hashtable<String, Object> params = command.params();
long timeout = 10;
if (params.containsKey("timeout")) {
timeout = (Integer) params.get("timeout");
}
UiDevice d = UiDevice.getInstance();
d.waitForIdle(timeout);
return getSuccessResult(true);
}
}
~~~
上面的代码处理过程很简单,首先都是获取命令里面的参数,然后初始化一个timeout私有变量,如果参数里不含时间,那么就用这个默认的时间。然后调用uiautomator的UiDevice的对象里方法waitForIdle(),该方法就在timeout时间内界面上没有其他操作,处于空闲状态。这个就是封装了一下UiDevice的waitForIdle方法而已没啥可讲的。
# Clear
~~~
package io.appium.android.bootstrap.handler;
import com.android.uiautomator.core.UiObjectNotFoundException;
import io.appium.android.bootstrap.*;
import org.json.JSONException;
/**
* This handler is used to clear elements in the Android UI.
*
* Based on the element Id, clear that element.
*
*/
public class Clear extends CommandHandler {
/*
* @param command The {@link AndroidCommand}
*
* @return {@link AndroidCommandResult}
*
* @throws JSONException
*
* @see io.appium.android.bootstrap.CommandHandler#execute(io.appium.android.
* bootstrap.AndroidCommand)
*/
@Override
public AndroidCommandResult execute(final AndroidCommand command)
throws JSONException {
if (command.isElementCommand()) {
try {
final AndroidElement el = command.getElement();
el.clearText();
return getSuccessResult(true);
} catch (final UiObjectNotFoundException e) {
return new AndroidCommandResult(WDStatus.NO_SUCH_ELEMENT,
e.getMessage());
} catch (final Exception e) { // handle NullPointerException
return getErrorResult("Unknown error clearing text");
}
}
return getErrorResult("Unknown error");
}
}
~~~
Clear的方法中就看e1.clearText()方法,其他的我在click中都有涉及。
~~~
private final UiObject el;
public void clearText() throws UiObjectNotFoundException {
el.clearTextField();
}
~~~
实际上调用的是uiautomator中的UiObject.clearTextField(),清楚文本框内的内容。
bootstrap之Click事件
最后更新于:2022-04-01 06:53:31
上一篇文章中讲了bootstrap的工作流程,这篇文章开始来研究一下bootstrap可以接受哪些指令(从源码的角度来看,由于appium的项目现在还处在持续更新中,所以有的指令已经实现,某些或许未来会实现,从源码来看的好处是你知道以后或许未来appium能做到哪些功能)。
在bootstrap项目中的io.appium.android.bootstrap.handler包中的类都是对应的相应的指令的类,里面都有execute方法来执行命令。先上上一篇文章中讲的map。
~~~
private static HashMap<String, CommandHandler> map = new HashMap<String, CommandHandler>();
static {
map.put("waitForIdle", new WaitForIdle());
map.put("clear", new Clear());
map.put("orientation", new Orientation());
map.put("swipe", new Swipe());
map.put("flick", new Flick());
map.put("drag", new Drag());
map.put("pinch", new Pinch());
map.put("click", new Click());
map.put("touchLongClick", new TouchLongClick());
map.put("touchDown", new TouchDown());
map.put("touchUp", new TouchUp());
map.put("touchMove", new TouchMove());
map.put("getText", new GetText());
map.put("setText", new SetText());
map.put("getName", new GetName());
map.put("getAttribute", new GetAttribute());
map.put("getDeviceSize", new GetDeviceSize());
map.put("scrollTo", new ScrollTo());
map.put("find", new Find());
map.put("getLocation", new GetLocation());
map.put("getSize", new GetSize());
map.put("wake", new Wake());
map.put("pressBack", new PressBack());
map.put("dumpWindowHierarchy", new DumpWindowHierarchy());
map.put("pressKeyCode", new PressKeyCode());
map.put("longPressKeyCode", new LongPressKeyCode());
map.put("takeScreenshot", new TakeScreenshot());
map.put("updateStrings", new UpdateStrings());
map.put("getDataDir", new GetDataDir());
map.put("performMultiPointerGesture", new MultiPointerGesture());
map.put("openNotification", new OpenNotification());
}
~~~
我们就按照上面的顺序来讲。首先声明一点,事先了解一下uiautomator的api很有必要,因为这些指令中大多数都是调用uiautomator的方法去操作的,要么直接调用,要么反射调用。我的博客有很多关于这方面的文章,可以先去看看。
# click
~~~
package io.appium.android.bootstrap.handler;
import com.android.uiautomator.core.UiDevice;
import com.android.uiautomator.core.UiObjectNotFoundException;
import io.appium.android.bootstrap.*;
import org.json.JSONException;
import java.util.ArrayList;
import java.util.Hashtable;
/**
* This handler is used to click elements in the Android UI.
*
* Based on the element Id, click that element.
*
*/
public class Click extends CommandHandler {
/*
* @param command The {@link AndroidCommand}
*
* @return {@link AndroidCommandResult}
*
* @throws JSONException
*
* @see io.appium.android.bootstrap.CommandHandler#execute(io.appium.android.
* bootstrap.AndroidCommand)
*/
@Override
public AndroidCommandResult execute(final AndroidCommand command)
throws JSONException {
if (command.isElementCommand()) {
try {
final AndroidElement el = command.getElement();
el.click();
return getSuccessResult(true);
} catch (final UiObjectNotFoundException e) {
return new AndroidCommandResult(WDStatus.NO_SUCH_ELEMENT,
e.getMessage());
} catch (final Exception e) { // handle NullPointerException
return getErrorResult("Unknown error");
}
} else {
final Hashtable<String, Object> params = command.params();
final Double[] coords = { Double.parseDouble(params.get("x").toString()),
Double.parseDouble(params.get("y").toString()) };
final ArrayList<Integer> posVals = absPosFromCoords(coords);
final boolean res = UiDevice.getInstance().click(posVals.get(0),
posVals.get(1));
return getSuccessResult(res);
}
}
}
~~~
该类中的方法就是处理点击事件,首先方法会判断你传入的命令参数是针对控件对象的还是以坐标的形式的。
**控件**
如果是控件的话,首先会获得命令中的控件对象,然后调用click方法,我们进入click方法看看
**AndroidElement.java**
~~~
public boolean click() throws UiObjectNotFoundException {
return el.click();
}
~~~
e1的定义为
~~~
private final UiObject el;
~~~
说明最终调用的是uiautomator中的UiObject类的click方法,这个方法的click方法就是点击该控件的中心点。然后回到Click类中继续往下看,会调用
~~~
return getSuccessResult(true);
~~~
从字面意思来看,走到这一步肯定就是告诉调用者,我只想成功了,跟你说一声。然后我们来看看这个方法里面的具体实现。
~~~
/**
* Returns success along with the payload.
*
* @param value
* @return {@link AndroidCommandResult}
*/
protected AndroidCommandResult getSuccessResult(final Object value) {
return new AndroidCommandResult(WDStatus.SUCCESS, value);
}
~~~
创建AndroidCommandResult新对象,传入WDStatus.SUCCESS,和value(我们这里传入的值为true).首先看一下SUCCESS的值,该值存放在枚举类WDStatus中。
~~~
SUCCESS (0, "The command executed successfully."),
~~~
就是一行文本。好,下面进入AndroidCommandResult类的构造方法里。
~~~
JSONObject json;
public AndroidCommandResult(final WDStatus status, final Object val) {
json = new JSONObject();
try {
json.put("status", status.code());
json.put("value", val);
} catch (final JSONException e) {
Logger.error("Couldn't create android command result!");
}
}
~~~
构造方法里把传入的参数保存在了json对象中,以键值对的形式。好了,条件为控件的情况分析结束,下面开始分析坐标。
**坐标**
如果是坐标的话,程序会获得命令里的坐标参数,保存在Double数组中。
~~~
final Hashtable<String, Object> params = command.params();
final Double[] coords = { Double.parseDouble(params.get("x").toString()),
Double.parseDouble(params.get("y").toString()) };
~~~
接下来会通过absPosFromCoords方法将Double转换为List。所以下面来看absPosFromCoords方法的实现:
~~~
/**
* Given a position, it will return either the position based on percentage
* (by passing in a double between 0 and 1) or absolute position based on the
* coordinates entered.
*
* @param coordVals
* @return ArrayList<Integer>
*/
protected static ArrayList<Integer> absPosFromCoords(final Double[] coordVals) {
final ArrayList<Integer> retPos = new ArrayList<Integer>();
final UiDevice d = UiDevice.getInstance();
final Double screenX = (double) d.getDisplayWidth();
final Double screenY = (double) d.getDisplayHeight();
if (coordVals[0] < 1 && coordVals[1] < 1) {
retPos.add((int) (screenX * coordVals[0]));
retPos.add((int) (screenY * coordVals[1]));
} else {
retPos.add(coordVals[0].intValue());
retPos.add(coordVals[1].intValue());
}
return retPos;
}
~~~
首先会判断传入的坐标是以百分比的形式还是以坐标的形式。如果是百分比说明你传入的不是绝对坐标,而是相对坐标(可以这么理解吧,这样可以适应各个屏幕),这种情况你就需要获取屏幕的尺寸,然后和百分比做计算得到当前屏幕中你所要点击的坐标点。如果你传入的就是坐标,那就直接将Double类型的值转化为Int的值。
经过上面的一番操作以后,会得到确切坐标值保存在数组中返回。
然后程序调用UiDevice的click方法点击啦:
~~~
final boolean res = UiDevice.getInstance().click(posVals.get(0), posVals.get(1));
~~~
最后返回一个成功的AndroidCommandResult对象。
appium框架之bootstrap
最后更新于:2022-04-01 06:53:29
(闲来无事,做做测试..)最近弄了弄appium,感觉挺有意思,就深入研究了下。
看小弟这篇文章之前,先了解一下appium的架构,对你理解有好处,推荐下面这篇文章:[testerhome](http://wenku.baidu.com/link?url=FbswfHp-YmkQKxrTAO61u9OVXp7aBA8TE5YN0hHSV8VkTXGTp1NkK_HbeuwFl1RJ8N3bRxRGlq3TUAq_wf06tv9wEDlUt8Fl8fhoxwD6MHa)
appium是开源项目,可以获得源码:[appium-master](https://github.com/appium/appium)
在eclipse中用maven导入会发现有2个项目:bootstrap和sauce_appium_junit。
sauce_appium_junit是一些测试用例的集合,帮助学习的。bootstrap就是appium架构中放在手机端的一个服务器。就从它开始吧。
## bootstrap结构
如图所示为bootstrap的项目结构
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-08_568f4d1802c85.jpg)
## bootstrap作用
bootstrap在appium中是以jar包的形式存在的,它实际上是一个uiautomator写的case包,通过PC端的命令可以在手机端执行。
## bootstrap源码分析
首先程序的入口为Bootstrap类。所以从该类开始一步一步解释这个项目
**Bootstrap.java**
~~~
package io.appium.android.bootstrap;
import io.appium.android.bootstrap.exceptions.SocketServerException;
import com.android.uiautomator.testrunner.UiAutomatorTestCase;
/**
* The Bootstrap class runs the socket server. uiautomator开发的脚本,可以直接在pc端启动
*/
public class Bootstrap extends UiAutomatorTestCase {
public void testRunServer() {
SocketServer server;
try {
// 启动socket服务器,监听4724端口。
server = new SocketServer(4724);
server.listenForever();
} catch (final SocketServerException e) {
Logger.error(e.getError());
System.exit(1);
}
}
}
~~~
该类继承自UiAutomatorTestCase。所以它才能通过adb shell uiautomator runtest AppiumBootstrap.jar -c io.appium.android.bootstrap.Bootstrap被执行。
该类很简单,就是启动线程,监听4724端口,该端口与appium通信。
然后走server.listenForever()方法。
**SocketServer.java**
~~~
/**
* Listens on the socket for data, and calls {@link #handleClientData()} when
* it's available.
*
* @throws SocketServerException
*/
public void listenForever() throws SocketServerException {
Logger.debug("Appium Socket Server Ready");
//读取strings.json文件的数据
UpdateStrings.loadStringsJson();
// 注册两种监听器:AND和Crash
dismissCrashAlerts();
final TimerTask updateWatchers = new TimerTask() {
@Override
public void run() {
try {
// 检查系统是否有异常
watchers.check();
} catch (final Exception e) {
}
}
};
// 计时器,0.1秒后开始,每隔0.1秒执行一次。
timer.scheduleAtFixedRate(updateWatchers, 100, 100);
try {
client = server.accept();
Logger.debug("Client connected");
in = new BufferedReader(new InputStreamReader(client.getInputStream(),
"UTF-8"));
out = new BufferedWriter(new OutputStreamWriter(client.getOutputStream(),
"UTF-8"));
while (keepListening) {
// 获取客户端数据
handleClientData();
}
in.close();
out.close();
client.close();
Logger.debug("Closed client connection");
} catch (final IOException e) {
throw new SocketServerException("Error when client was trying to connect");
}
}
~~~
该方法中首先调用UpdateStrings.loadStringsJson();该方法如下:
**UpdateStrings**
~~~
/**
* strings.json文件保存的是apk的strings.xml里的内容,在Bootstrap启动前由appium服务器解析并push到设备端的
*
* @return
*/
public static boolean loadStringsJson() {
Logger.debug("Loading json...");
try {
final String filePath = "/data/local/tmp/strings.json";
final File jsonFile = new File(filePath);
// json will not exist for apks that are only on device
// 你的case必须写明apk的路径,如果启动设备上已有的应用而case中没有app路径,此时json文件是不存在的
// because the node server can't extract the json from the apk.
if (!jsonFile.exists()) {
return false;
}
final DataInputStream dataInput = new DataInputStream(
new FileInputStream(jsonFile));
final byte[] jsonBytes = new byte[(int) jsonFile.length()];
dataInput.readFully(jsonBytes);
// this closes FileInputStream
dataInput.close();
final String jsonString = new String(jsonBytes, "UTF-8");
// 将读取出来的信息赋给Find类中的属性,以做后用
Find.apkStrings = new JSONObject(jsonString);
Logger.debug("json loading complete.");
} catch (final Exception e) {
Logger.error("Error loading json: " + e.getMessage());
return false;
}
return true;
}
~~~
然后回到ServerSocket类的listenForever(),此时执行到dismissCrashAlerts();该方法作用是注册一些监听器,观察是否有弹出框或者AND和crash的异常。
~~~
public void dismissCrashAlerts() {
try {
new UiWatchers().registerAnrAndCrashWatchers();
Logger.debug("Registered crash watchers.");
} catch (final Exception e) {
Logger.debug("Unable to register crash watchers.");
}
}
~~~
此时listenForever()方法里执行到注册心跳程序,每隔0.1秒开始执行一遍上面注册的监听器来检查系统是否存在异常。
~~~
final TimerTask updateWatchers = new TimerTask() {
@Override
public void run() {
try {
// 检查系统是否有异常
watchers.check();
} catch (final Exception e) {
}
}
};
// 计时器,0.1秒后开始,每隔0.1秒执行一次。
timer.scheduleAtFixedRate(updateWatchers, 100, 100);
~~~
然后启动数据通道,接受客户端发来的数据和返回结果给客户端。
~~~
client = server.accept();
Logger.debug("Client connected");
in = new BufferedReader(new InputStreamReader(client.getInputStream(),
"UTF-8"));
out = new BufferedWriter(new OutputStreamWriter(client.getOutputStream(),
"UTF-8"));
~~~
接下来就是最重要的方法handleClientData();到此listenForever()方法的主要作用就完成了。现在来看handleClientData()方法做了啥。
~~~
/**
* When data is available on the socket, this method is called to run the
* command or throw an error if it can't.
*
* @throws SocketServerException
*/
private void handleClientData() throws SocketServerException {
try {
input.setLength(0); // clear
String res;
int a;
// (char) -1 is not equal to -1.
// ready is checked to ensure the read call doesn't block.
while ((a = in.read()) != -1 && in.ready()) {
input.append((char) a);
}
final String inputString = input.toString();
Logger.debug("Got data from client: " + inputString);
try {
final AndroidCommand cmd = getCommand(inputString);
Logger.debug("Got command of type " + cmd.commandType().toString());
res = runCommand(cmd);
Logger.debug("Returning result: " + res);
} catch (final CommandTypeException e) {
res = new AndroidCommandResult(WDStatus.UNKNOWN_ERROR, e.getMessage())
.toString();
} catch (final JSONException e) {
res = new AndroidCommandResult(WDStatus.UNKNOWN_ERROR,
"Error running and parsing command").toString();
}
out.write(res);
out.flush();
} catch (final IOException e) {
throw new SocketServerException("Error processing data to/from socket ("
+ e.toString() + ")");
}
}
~~~
该方法中读取客户端发来的数据,利用getCommand()方法获得AndroidCommand对象,然后执行runCommand()方法,获取直接的结果。那么该方法的作用就转移到了runCommand()。所以现在就来看runCommand()方法是啥意思啦。
~~~
/**
* When {@link #handleClientData()} has valid data, this method delegates the
* command.
*
* @param cmd
* AndroidCommand
* @return Result
*/
private String runCommand(final AndroidCommand cmd) {
AndroidCommandResult res;
if (cmd.commandType() == AndroidCommandType.SHUTDOWN) {
keepListening = false;
res = new AndroidCommandResult(WDStatus.SUCCESS, "OK, shutting down");
} else if (cmd.commandType() == AndroidCommandType.ACTION) {
try {
res = executor.execute(cmd);
} catch (final Exception e) {
res = new AndroidCommandResult(WDStatus.UNKNOWN_ERROR, e.getMessage());
}
} else {
// this code should never be executed, here for future-proofing
res = new AndroidCommandResult(WDStatus.UNKNOWN_ERROR,
"Unknown command type, could not execute!");
}
return res.toString();
}
}
~~~
该方法首先做了判断,判断命令数据哪种类型,主要有关机命令和动作命令,我们主要关注动作命令,因为动作有很多种。所以来关注第一个else if中的AndroidCommandExecutor.execute()方法。主线又转移到了该方法中了,切去瞅一眼。
**AndroidCommandExecutor.java**
~~~
/**
* Gets the handler out of the map, and executes the command.
*
* @param command
* The {@link AndroidCommand}
* @return {@link AndroidCommandResult}
*/
public AndroidCommandResult execute(final AndroidCommand command) {
try {
Logger.debug("Got command action: " + command.action());
if (map.containsKey(command.action())) {
return map.get(command.action()).execute(command);
} else {
return new AndroidCommandResult(WDStatus.UNKNOWN_COMMAND,
"Unknown command: " + command.action());
}
} catch (final JSONException e) {
Logger.error("Could not decode action/params of command");
return new AndroidCommandResult(WDStatus.JSON_DECODER_ERROR,
"Could not decode action/params of command, please check format!");
}
}
~~~
该方法中终于要执行命令的实体啦
~~~
if (map.containsKey(command.action())) {
return map.get(command.action()).execute(command);
} else {
return new AndroidCommandResult(WDStatus.UNKNOWN_COMMAND,
"Unknown command: " + command.action());
}
~~~
关键是上面这几行代码,调用了map.get(command.action()).execute(command).看来要想弄懂这个命令的意思,肯定得知道map里存放的对象是哪些,那么在该类中找到map的初始化代码:
~~~
static {
map.put("waitForIdle", new WaitForIdle());
map.put("clear", new Clear());
map.put("orientation", new Orientation());
map.put("swipe", new Swipe());
map.put("flick", new Flick());
map.put("drag", new Drag());
map.put("pinch", new Pinch());
map.put("click", new Click());
map.put("touchLongClick", new TouchLongClick());
map.put("touchDown", new TouchDown());
map.put("touchUp", new TouchUp());
map.put("touchMove", new TouchMove());
map.put("getText", new GetText());
map.put("setText", new SetText());
map.put("getName", new GetName());
map.put("getAttribute", new GetAttribute());
map.put("getDeviceSize", new GetDeviceSize());
map.put("scrollTo", new ScrollTo());
map.put("find", new Find());
map.put("getLocation", new GetLocation());
map.put("getSize", new GetSize());
map.put("wake", new Wake());
map.put("pressBack", new PressBack());
map.put("dumpWindowHierarchy", new DumpWindowHierarchy());
map.put("pressKeyCode", new PressKeyCode());
map.put("longPressKeyCode", new LongPressKeyCode());
map.put("takeScreenshot", new TakeScreenshot());
map.put("updateStrings", new UpdateStrings());
map.put("getDataDir", new GetDataDir());
map.put("performMultiPointerGesture", new MultiPointerGesture());
map.put("openNotification", new OpenNotification());
}
~~~
豁然开朗,该map是形式的map。value值对应的都是一个个的对象,这些对象都继承与CommandHandler,里面都有execute方法,该方法就是根据命令的不同调用不同的对象来执行相关代码获取结果。从map的定义可以看出,appium可以操作手机的命令还不少,我用过的有scrollTo,updateStrings,getDataDir等,上面还有截图、打开通知栏、按下等还没用过,但通过这些命令你也可以了解appium可以做哪些事。
继承CommandHandler的对象有很多,我挑一个来讲讲它具体是干嘛的,其他的我以后会挨个讲,就挑click吧。
加入现在传过来的命令后缀是click的话,那么它会调用Click对象的execute方法。
**Click.java**
~~~
package io.appium.android.bootstrap.handler;
import com.android.uiautomator.core.UiDevice;
import com.android.uiautomator.core.UiObjectNotFoundException;
import io.appium.android.bootstrap.*;
import org.json.JSONException;
import java.util.ArrayList;
import java.util.Hashtable;
/**
* This handler is used to click elements in the Android UI.
*
* Based on the element Id, click that element.
*
*/
public class Click extends CommandHandler {
/*
* @param command The {@link AndroidCommand}
*
* @return {@link AndroidCommandResult}
*
* @throws JSONException
*
* @see io.appium.android.bootstrap.CommandHandler#execute(io.appium.android.
* bootstrap.AndroidCommand)
*/
@Override
public AndroidCommandResult execute(final AndroidCommand command)
throws JSONException {
if (command.isElementCommand()) {
try {
final AndroidElement el = command.getElement();
el.click();
return getSuccessResult(true);
} catch (final UiObjectNotFoundException e) {
return new AndroidCommandResult(WDStatus.NO_SUCH_ELEMENT,
e.getMessage());
} catch (final Exception e) { // handle NullPointerException
return getErrorResult("Unknown error");
}
} else {
final Hashtable<String, Object> params = command.params();
final Double[] coords = { Double.parseDouble(params.get("x").toString()),
Double.parseDouble(params.get("y").toString()) };
final ArrayList<Integer> posVals = absPosFromCoords(coords);
final boolean res = UiDevice.getInstance().click(posVals.get(0),
posVals.get(1));
return getSuccessResult(res);
}
}
}
~~~
该类就一个execute方法这根独苗,execute方法中会先判断传入的参数对象是坐标值还是元素值,如果是元素值那么直接调用AndroidElement中的click方法,一会我们再去看这个方法。如果是坐标的话,它会干什么呢。它会调用UiDevice的click方法,用过UiAutomator的人都知道它是uiautomator包中的类。所以说appium在api16以上的机器上使用的uiautomator机制。貌似有人觉得这好像easy了点。那好吧,我们再分析一个touchDown命令,如果传过来的命令后缀是touchDown,那么它会调用TouchDown对象的execute方法。
~~~
map.put("touchDown", new TouchDown());
~~~
这个类里面的execute方法就有点意思啦。
**TouchDown.java**
~~~
package io.appium.android.bootstrap.handler;
import com.android.uiautomator.common.ReflectionUtils;
import com.android.uiautomator.core.UiObjectNotFoundException;
import io.appium.android.bootstrap.Logger;
import java.lang.reflect.Method;
/**
* This handler is used to perform a touchDown event on an element in the
* Android UI.
*
*/
public class TouchDown extends TouchEvent {
@Override
protected boolean executeTouchEvent() throws UiObjectNotFoundException {
printEventDebugLine("TouchDown");
try {
final ReflectionUtils utils = new ReflectionUtils();
final Method touchDown = utils.getControllerMethod("touchDown", int.class,
int.class);
return (Boolean) touchDown.invoke(utils.getController(), clickX, clickY);
} catch (final Exception e) {
Logger.debug("Problem invoking touchDown: " + e);
return false;
}
}
}
~~~
该方法里用到了反射,调用uiautomator里的隐藏api来执行按下操作。就不具体讲了,后面会挨个说一遍的。
# 总结
说了这么多废话,尝试着用序列图描述一遍吧。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-08_568f4d1834fa6.jpg)
前言
最后更新于:2022-04-01 06:53:27
> 原文出处:[架构设计专栏文章](http://blog.csdn.net/column/details/itfootball-appium.html)
> 作者:[钱辉](http://blog.csdn.net/itfootball)
**本系列文章经作者授权在看云整理发布,未经作者允许,请勿转载!**
#Appium之android平台的源码分析
> appium测试工具的android端源码分析