This commit is contained in:
饭桶狼 ra~ 2021-12-30 14:25:43 +08:00
parent 406372d603
commit 14481759c2
2 changed files with 971 additions and 205 deletions

435
README.md
View File

@ -1,104 +1,118 @@
English | [简体中文](./README_zh-CN.md)
简体中文 | [English](./README_en.md)
# GO-MOBILE-AUTOMATION SDK
# GO手机自动化SDK
A full featured Android mobile automation sdk for golang developers
功能齐全的手机自动化Golang SDK支持编译成二进制可执行文件适合自动化流程部署到云手机脱离ADB连接稳定高效执行
## The purpose
## 目的
If you are an automation developer, you may find the python/javascript echo system provide the developers great capabilities for manipulating devices and apps.
手机自动化领域其实python是绝对的主流其他语言诸如Golang/C#/C++在整个生态中占的份额相对来说比较少。对于安卓手机自动化开发来说,我们起码有下面的工具了:
For Android automation, some well known tools are:
1. Appium多语言/全平台自动化支持)
2. Uiautomator2/ATX (python)
3. 各RPA平台比如云扩科技 (C#)
1. Appium (multi-language primarily javascript/python)
2. uiautomator2 (python)
我们有这么多python库可以用为什么要再做个Golang自动化SDK呢
Our SDK ports the uiautomation2 python library to golang.
1. 容易部署 - Golang一般只需要部署一个二进制可执行文件不像python/javascript需要安装一大堆依赖在墙内如果没有一个稳定可靠的镜像(的确javascript有CNPM也可以自己搭镜像)如果安装依赖挂了就没有然后了。即使下载依赖没有问题也会有一些依赖版本冲突的问题。C#/JAVA有运行时依赖你一般不会知道用户会使用哪个版本的.NET SDK/JDK。
2. 部署在云手机上稳定可靠 - 由于云手机使用虚拟化技术,它不像传统机架集群会遇到各式各样的硬件文件 - 比如电池爆膨了一般2年寿命工作室机更差比如电源供电不足手机断电比如USB线接触不良等等。云手机相对贵一点但是稳定且省人力成本。但是有些云手机使用外网远程adb连接如果自动化脚本host在本地的PC/MACOS上远程操控云手机稳定性非常取决于网络的稳定性。我们当然更希望使用Golang/C++的方式:编译一个二进制可执行文件,推到云手机上定时/按某种触发条件去执行脱离ADB连接
3. 稳定 - 事实上python能直接跑在手机上。pyto-python3这个app提供了host python脚本在手机上的可能。但是手机应用比较容易被Android系统杀死而二进制可执行文件一般不会。
Why do we do this?
所以我们采用Golang做安卓自动化语言是有道理滴
1. Easy to deploy - deploy one executable only (at most several dlls) instead of resolving thousands of dependencies like javascript and python. The bigger scope of this family of projects is to provide a mechanism to orchestrate the automation "scripts" to run cross a bunch of platform/systems. Must have a fast & robustic app distribution and deployment mechanism.
2. For using cloud phones (Android) - Some cloud phone providers have very poor quality adb connection. In case the automation process break because of adb connection failure, we would like the process run inside the phone
3. Robust - apps can easily get killed by Android system, but executable not. That why despite the fact that apps like pyto-python3 provides hosting for python but we don't use it.
再说说其他自动化工具坑的地方:
1. Appium安装复杂而且图像识别只支持模版匹配中间层N多上层是well known的protocol要优化性能或者加一些底层支持非常困难。要部署Appium到一台新的主机虚拟机别提有多烦了
2. UiAutomator2/ATX。这个软件库我是极其推崇它的API支持比我们的Golang SDK要丰富毕竟人家才是原版。但是它不提供直接将流程部署到手机的能力。这不能说是缺陷因为考虑到自动化开发的受众群体python是绝对绝对的主流
3. 云扩RPA。它其实本身是一个很棒的平台但是为了做手机自动化安装N多东西不值当。但是如果有别的需求比如使用低代码做界面层可视化的流程管理工具它是一个不错的选择。这个SDK首先会和云扩RPA做集成。
## Inspired by
唯一要注意的是Android是只读文件系统这意味着打日志的话我们可能需要批量收集在程序里面做批量推送到日志服务器
Inspired by [OpenAtx](https://github.com/openatx) and the [Uiautomation2](https://github.com/openatx/uiautomator2) python library. We entirely use the openatx drivers, leaving the client side sdk written in golang. This benefits users, because they can use the uiautomator2 tool chain, which is super cool.
## 项目启发
项目启发于[OpenAtx](https://github.com/openatx)和[Uiautomation2](https://github.com/openatx/uiautomator2) python库。我事实上在项目中首先大量使用了uiautomator2在玩儿了云手机之后因为有需要自然而然就有了这个Golang库因为Golang比C++简单ARM Cortex A编辑不需要NDK
它事实上是OpenAtx的又一个客户端但是它可以跨平台执行。它实现了大部分Uiautomator2的API。这么做的好处是OpenAtx的工具链包括它的Inspector [weditor](https://github.com/alibaba/web-editor) - 这个真的很好用而且是web应用不需要额外占我太多存储空间
而且有别于AppiumOpenAtx不需要依赖Session同时在一个App上执行流程它可以同时操作一个App点击另一个App的浮层。这是我喜欢并使用OpenAtx的原因。
## Quick start
There are 4 steps:
我分四步讲
1. Setup the Android phone
2. Setup the development environment
3. Start creating a golang project for mobile automation
4. deploy&run
1. 设置安卓手机
2. 设置开发环境
3. 创建Golang自动化项目
4. 部署&执行
### Setup the Android phone
### 设置安卓手机
1. Install the specific version of the app you want to automate.
1. 安装你要自动化的APP注意安装特定的版本 - 因为不同版本同一个元素的属性很可能是不一样的 - 即使同一个版本有些属性其实也会变 - 反映到脚本中就是xpath
```
$ adb install [package.apk]
```
2. Download atx-agent from [here](https://github.com/openatx/atx-agent/releases) choose the armv7 version unless you run a x86 phone simulator
3. Untar atx-agent and install, follow the intallation insttructions [here](https://github.com/openatx/atx-agent)
2. 下载 atx-agent: [点这里](https://github.com/openatx/atx-agent/releases) 选择 armv7 除非你部署的手机是x86手机模拟器
3. 解压 atx-agent 并安装, 这里有安装说明: [点这里](https://github.com/openatx/atx-agent) 或者看下面的命令行指令:
```
$ adb push atx-agent /data/local/tmp
$ adb shell chmod 755 /data/local/tmp/atx-agent
# launch atx-agent in daemon mode
# 后台模式执行atx-agent
$ adb shell /data/local/tmp/atx-agent server -d
# stop already running atx-agent and start daemon
# 或者后台模式重启atx-agent
$ adb shell /data/local/tmp/atx-agent server -d --stop
```
4. Download app-uiautomator-test.apk and app-uiautomator.apk from [here](https://github.com/openatx/android-uiautomator-server/releases) and install using adb install
4. 下载 app-uiautomator-test.apk 和 app-uiautomator.apk [点这里](https://github.com/openatx/android-uiautomator-server/releases) 然后用adb install命令安装
```
$ adb install app-uiautomator-test.apk
$ adb install app-uiautomator.apk
```
5. Grant all priviledges to the app "ATX"
6. Open app "ATX" and click "启动UIAUTOMATOR", click "开启悬浮窗"
5. 给应用 "ATX" 所有权限,并且打开 "ATX" 检查有任何运行时权限请求的话,选择一直许可。
6. 打开应用 "ATX" 点击 "启动UIAUTOMATOR", 点击 "开启悬浮窗"
## Setup the development environment
## 设置开发环境
1. Install Python3(version 3.6+) from [here](https://www.python.org/downloads/)
2. Install [weditor](https://github.com/alibaba/web-editor)
1. 安装 Python3(版本 3.6+) from [here](https://www.python.org/downloads/)
2. 安装 [weditor](https://github.com/alibaba/web-editor)
```
$ pip3 install -U weditor
```
3. You can now open weditor as ui inspector for android applications, by typing
3. 打开 weditor, 它就是你的UI Inspector在命令行中输入,
```
$ weditor
```
## Create a golang automation project
## 创建Golang自动化项目
1. Create a folder called helloworld
2. Open terminal and type in
1. 创建文件夹 helloworld
2. 打开命令行并输入
```
$ go mod init helloworld
```
3. Add dependency
3. 添加依赖
```
$ go get github.com/fantonglang/go-mobile-automation
```
4. Add the program entry - create main.go file
5. [Here](https://github.com/fantonglang/go-mobile-automation-examples/blob/main/douyin-luo-live/main.go) is an example of main.go file
4. 添加程序入口 - 创建 main.go 文件
5. [这里](https://github.com/fantonglang/go-mobile-automation-examples/blob/main/douyin-luo-live/main.go) 是示例 main.go 文件
## Deploy&Run
## 部署&执行
```
# cross build linux/arm target
# 交叉编译(cross build) linux/arm 可执行文件
$ GOOS=linux GOARCH=arm go build
# deploy - helloworld is the executable name, which is the same with the go module name
# 部署 - helloworld 是可执行文件名和Go模块名是相同的
$ adb push helloworld /data/local/tmp
$ adb shell chmod 755 /data/local/tmp/helloworld
# run
# 运行
$ adb shell /data/local/tmp/helloworld
```
If you start a background process, you don't need the phone to connect with your PC/macos. Note that you can transfer your linux shell knowledge to using the adb shell.
如果你用后台模式启动程序程序启动之后就不需要连接电脑。adb shell命令和普通linux系统是一样的。你可以输入
```
# nohup保证当关闭terminal session的时候程序不会被杀死&保证在后台运行程序
$ adb shell nohup /data/local/tmp/helloworld &
```
## Examples
[This](https://github.com/fantonglang/go-mobile-automation-examples/tree/main/douyin-luo-live) is a working example. Read the comments in main.go carefully. This helps to resolve all dependencies and environment requirements before you start. The comments also give the commands for compilation, deployment, and execution.
## 例子
[这里](https://github.com/fantonglang/go-mobile-automation-examples/blob/main/douyin-luo-live) 是一个能跑起来的例子。仔细阅读main函数的注释里面有手机设置环境安装编译调试部署执行的教程。
# APIS
@ -136,9 +150,9 @@ If you start a background process, you don't need the phone to connect with your
## Connect to a device
There are two types of connection:
有两种类型的连接:
1. If the executable is deployed in the phone, use
1. 如果程序是部署在手机上,使用如下代码:
``` go
package main
@ -147,10 +161,10 @@ import (
"github.com/fantonglang/go-mobile-automation/apis"
)
...
// you don't need to specify device id, because there is no PC connection
// 不需要指定设备ID因为程序并不需要从电脑通过ADB连接手机
d := apis.NewNativeDevice()
```
2. If you debug and deploy in PC/macos, use
2. 如果是在电脑端调试或部署,使用如下代码:
``` go
package main
@ -160,7 +174,7 @@ import (
)
//here c574dd45 is the device id, replace it with yours own
//这里 c574dd45 是设备ID, 你可以从adb devices指令中获取到它, 把它替换成你自己的手机设备ID
d, err := apis.NewHostDevice("c574dd45")
if err != nil {
log.Println("failed connecting to device")
@ -168,7 +182,7 @@ if err != nil {
}
```
Combine 2 code snippets. The following code enables the same piece of code working on the both deployments.
结合上述两个代码片段,下面的代码既能在电脑端(假定是x86架构)调试的时候工作又能在Android设备中部署之后工作。它判断运行时如果是ARM使用手机的方式连接反之使用电脑的方式连接。特别注意苹果MAC最近的ARM架构电脑如果这种情况最好判断一下系统(GOOS)
``` go
package main
@ -182,7 +196,7 @@ func getDevice() *apis.Device {
if runtime.GOARCH == "arm" {
return apis.NewNativeDevice()
}
//here c574dd45 is the device id, replace it with yours own
//这里 c574dd45 是设备ID, 你可以从adb devices指令中获取到它, 把它替换成你自己的手机设备ID
_d, err := apis.NewHostDevice("c574dd45")
if err != nil {
log.Println("101: failed connecting to device")
@ -194,24 +208,22 @@ func getDevice() *apis.Device {
d := getDevice()
```
Take extra notice if your macos is the ARM architecture. Then you may judge based on GOOS.
## Device APIS
This part showcases how to perform common device operations:
这部分展示如何执行常见的设备操作
### Shell commands
Example: Force stop douyin(China tiktok) app
示例: 强制停止抖音APP
```go
d.Shell(`am force-stop com.ss.android.ugc.aweme`)
```
Example: Start douyin app
示例: 打开抖音APP
```go
// You can find the app main activity by using the dumpsys command, hence I didn't implement the uiautomator2 equivalent session API for now.
// 使用dumpsys命令你可以找到APP的启动Activity。所以目前并不实现uiautomator2的启动APP API以及session API。
d.Shell(`am start -n "com.ss.android.ugc.aweme/.main.MainActivity"`)
```
### Retrieve the device info
Get detailed app info
获取详细设备信息
```go
info, err := d.DeviceInfo()
if err != nil {
@ -226,7 +238,7 @@ if err != nil {
fmt.Println(string(bytes))
```
Below is a possible output:
下面是可能的输出:
```json
{
@ -241,7 +253,7 @@ Below is a possible output:
}
```
Get window size:
获取窗口大小:
```GO
w, h, err := d.WindowSize()
@ -250,14 +262,14 @@ if err != nil {
return
}
fmt.Printf("w: %d, h: %d\n", w, h)
// device upright output example: w: 1080, h: 2340
// device horizontal output example: w: 1080, h: 2340
// 设备竖屏时的输出示例: w: 1080, h: 2340
// 设备横屏时的输出示例: w: 1080, h: 2340
```
### Clipboard
Get of set clipboard content
获取和设置剪切板内容
set clipboard
设置剪切板内容
```go
err := d.SetClipboard("aaa")
if err != nil {
@ -265,7 +277,7 @@ if err != nil {
return
}
```
get clipboard: This doesn't work in Android > 9.0. Most cloud phone work on lower Android version. I don't mind.
获取剪切板内容: 在Android大于9.0这个API并不工作. 但是大多数云手机使用低版本Android(比如7.0)所以我并不care
```go
a, err := d.GetClipboard()
if err != nil {
@ -277,24 +289,24 @@ fmt.Println(a)
### Key Events
* Turn on/off screen
* 打开/关闭屏幕
```go
err := d.KeyEvent(KEYCODE_POWER) // press power key to turn on/off screen
err := d.KeyEvent(KEYCODE_POWER) // 打开关闭屏幕都是按电源键
```
* Home key
* Home
```go
err := d.KeyEvent(KEYCODE_HOME)
```
d.KeyEvent is basically the Android "input keyevent " command, please refer to [this doc](https://developer.android.com/reference/android/view/KeyEvent), or if you're behind gfw, [this doc](https://blog.csdn.net/feizhixuan46789/article/details/16801429)
d.KeyEvent 是在调用 Android 的 "input keyevent " 命令, 请参照 [这个文档](https://developer.android.com/reference/android/view/KeyEvent), 或者你如果没有翻墙工具, 参照 [这个文档](https://blog.csdn.net/feizhixuan46789/article/details/16801429)
### Press Key
Example: press Home key
示例: 按Home键
```go
err := d.Press("home")
```
supported keys are:
Press API支持下面的按键:
```go
VSK_HOME = "home"
VSK_BACK = "back"
@ -317,153 +329,155 @@ VSK_POWER = "power"
```
### New command timeout
How long (in seconds) will wait for a new command from the client before assuming the client quit and ending the uiautomator service
设置Uiautomator服务的超时时间
```go
err := d.SetNewCommandTimeout(300) // unit is second
err := d.SetNewCommandTimeout(300) // 单位秒
```
### Screenshot
* Screenshot and save - notice Android has readonly file system, this API is only available for host(PC/macos).
* 截屏并保存文件 - 注意 Android 使用只读文件系统, 这个API只有在电脑端有效.
```go
err := d.ScreenshotSave("sc.png")
```
* Screenshot and get bytes (preferred, because opencv can accept bytes directly by "cv::imdecode" function)
* 截屏并获取[]byte字节流 (推荐, 因为opencv使用cv::imdecode函数能直接读取字节流)
```go
bytes, err := d.ScreenshotBytes()
```
* Screenshot and get image.Image object
* 截屏并获取image.Image对象 如果需要不同的图片编码比如jpeg/png使用image.Image可以帮你做到转换
```go
img, format, err := d.Screenshot() // img is the image.Image object, example of format: "jpeg"
img, format, err := d.Screenshot() // img 是 image.Image 对象, format的示例: "jpeg"
```
### UI Hierarchy
* Get hierachy text
* 获取UI结构的XML文本
```go
content, err := d.DumpHierarchy(false, false) // content is the text
content, err := d.DumpHierarchy(false, false) // content 是文本
```
* Transform hierachy text to *xmlquery.Node object. (This is useful if you want to do xpath query based on a snapshot - this is a lot faster)
* 将UI结构的XML文本转换成 *xmlquery.Node 对象. (我们如果要基于snaphot执行xpath查询那 *xmlquery.Node 对象是非常有用的 - 它不涉及Uiautomator调用所以速度会非常快适合广告页面识别关闭的场景)
```go
doc, err := FormatHierachy(content) // doc is the *xmlquery.Node object
doc, err := FormatHierachy(content) // doc 是 *xmlquery.Node 对象
```
### Touch
Simulate "mouse press down", "mouse hold and move", "mouse up release"
* Get touch object
模拟“手指按下触屏”,“手指按住触屏拖动”,以及“手指离开触屏”
* 获取 touch 对象
```go
touch := d.Touch()
```
* Mouse press down at a position
* 手指按下触屏 - 在某个位置
```go
/* press at (relative to the top-left corner)
* x: 50% position of the width
* y: 60% position of the height
/* (相对于屏幕左上角)手指按下触屏,位置为
* x: 50% 宽度坐标
* y: 60% 高度坐标
*/
err := touch.Down(0.5, 0.6)
```
* Mouse hold and move
* 手指按住触屏拖动 - 到某个位置
```go
/* then move to (relative to the top-left corner)
* x: 50% position of the width
* y: 10% position of the height
/* (相对于屏幕左上角)然后拖动到下述位置
* x: 50% 宽度坐标
* y: 10% 高度坐标
*/
err := touch.Move(0.5, 0.1)
```
* Mouse up release
* 手指离开触屏
```go
/* then mouse up release at (relative to the top-left corner)
* x: 50% position of the width
* y: 10% position of the height
/* (相对于屏幕左上角)然后手指离开触屏,位置为
* x: 50% 宽度坐标
* y: 10% 高度坐标
*/
err := touch.Up(0.5, 0.1)
```
### Click
Click on screen given coordinates
在指定坐标位置点击屏幕
* coordinates using percentage
* 使用百分比 - 如果x,y的任意一个值在 [0,1) 的范围内,它就是表示百分比,反之如果在[1, maxScreenWidth/maxScreenHeight]的范围内,它就是绝对坐标值
```go
/* click relative to the top-left corner
* x: 48.1% position of the width
* y: 24.6% position of the height
/* 相对于屏幕左上角点击
* x: 48.1% 宽度坐标
* y: 24.6% 高度坐标
*/
err := d.Click(0.481, 0.246)
```
* coordinates using absolute pixel values
* 使用绝对坐标 - 如果x,y的任意一个值在 [0,1) 的范围内,它就是表示百分比,反之如果在[1, maxScreenWidth/maxScreenHeight]的范围内,它就是绝对坐标值
```go
/* click relative to the top-left corner
* at (x: 481, y: 246)
*/
// 相对于屏幕左上角点击(x: 481, y: 246)
err := d.Click(481, 246)
```
### Double Click
双击
```go
err := d.DoubleClickDefault(0.481, 0.246)
```
### Long Click
Mouse click, but there is a certain time interval(0.5s) between mouse down and up
长按点击相当于按下和松开之间隔了一个给定时间默认0.5s。函数名后面带Default的一般有一个非Default版本提供更多参数选择。
```go
err := d.LongClickDefault(0.481, 0.246)
```
### Swipe
* Swipe from one point (fx, fy) to another (tx, ty)
滑动
* 从起始点 (fx, fy) 滑动到终点 (tx, ty)
```go
var fx, fy, tx, ty float32 = 0.5, 0.5, 0, 0
err := d.SwipeDefault(fx, fy, tx, ty)
```
* Swipe points, you can specify more than 2 points
* 多点滑动, 参数可以指定多个apis.Point4Swipe对象代表滑动途径的坐标点
```go
// swipe from (x=width*0.5, y=height*0.9) to (x=width*0.5,y=height*0.1)
// 滑动途经 起点(x=width*0.5, y=height*0.9) 到 终点(x=width*0.5,y=height*0.1)滑动总时长0.1秒
err := d.SwipePoints(0.1, apis.Point4Swipe{0.5, 0.9}, apis.Point4Swipe{0.5, 0.1})
```
Drag from one point (fx, fy) to another (tx, ty)
* 从起点(fx, fy) 拖动到 终点(tx, ty)
```go
var fx, fy, tx, ty float32 = 0.5, 0.5, 0, 0
err := d.DragDefault(fx, fy, tx, ty)
```
### Set Orientation
Accepts 4 orientation parameters:
* "n" - means natural
* "l" - means left
* "u" - means upsidedown
* "r" - means right
设置屏幕方向,接受下面这四种参数:
* "n" - 代表正常的竖屏
* "l" - 代表朝左的横屏
* "u" - 代表倒过来的竖屏
* "r" - 代表朝右的横屏
```go
err := d.SetOrientation("n")
```
### Open Quick Settings
打开[快速设置菜单](https://www.lifewire.com/quick-settings-menu-android-4121299)
```go
err := d.OpenQuickSettings()
```
### Open Url
打开浏览器输入URL打开网页
```go
err := d.OpenUrl("https://bing.com")
```
### Show float window
This operation is openatx specific - open a float window to keep the automator app in the front and prevent it from getting killed
显示弹窗。这个操作是openatx特有的 - 它打开一个浮层来做应用保活。在这个SDK的产品级部署上面我们在每次流程开始时都会用d.Shell函数打开ATX应用再使用自研的分辨率无关的控件图像识别找到“启动UIAUTOMATOR”按钮并点击再打开ATX应用用同样的方法找到“开启悬浮窗”按钮并点击。以避免在流程执行的过程中由于uiautomator已经被杀死而重新唤起它 - 虽然这个操作会自动发生,但是有时候会很慢。
```go
err := d.ShowFloatWindow(true)
```
## Input Method
Type text, (you will switch to a special input method)
* Clear Text
用来输入文字,(会使用一个特殊的输入法)
* 清除文字
```go
err := d.ClearText()
```
* Send Action
* 发送动作
```go
err := ime.SendAction(SENDACTION_SEARCH)
```
The following actions are supported:
支持下面的动作:
```go
SENDACTION_GO = 2
SENDACTION_SEARCH = 3
@ -473,33 +487,33 @@ SENDACTION_DONE = 6
SENDACTION_PREVIOUS = 7
```
* Send Keys - Type text
* 发送按键 - 输入文字包括中文以及其他Unicode
```go
err := ime.SendKeys("aaa", true)
```
## XPATH
XPATH is the most important way of finding UI Element.
在Android自动化中XPATH是最重要的寻找UI元素的方法。快速又强大
### Finding elements
* Find multiple elements by xpath
* 通过Xpath查找多个元素
```go
els := d.XPath(`//*[@text="your-control-text"]`).All()
for _, el := range els {
...
}
```
* Find one element by xpath
* 通过Xpath查找一个元素首个或者nil
```go
el := d.XPath(`//*[@text="your-control-text"]`).First()
```
* Check element exists by xpath
* 通过Xpath检查元素是否存在, el.First()函数当元素不存在的时候返回nil
```go
if d.XPath(`//*[@text="your-control-text"]`).First() != nil {
...
}
```
* Wait element appear
* 等待元素出现
```go
el := d.XPath(`//*[@text="your-control-text"]`).Wait(time.Minute)
if el == nil {
@ -508,7 +522,7 @@ if el == nil {
}
...
```
* Wait element disappear
* 等待元素消失
```go
ok := d.XPath(`//*[@text="your-control-text"]`).WaitGone(time.Minute)
if !ok {
@ -516,16 +530,16 @@ if !ok {
return
}
```
* If you want run xpath query based on ui hierachy snaphot,
* 如果我们基于UI结构的snapshot来做Xpath查询, 这样基于一次uiautomator的调用我们可以执行多次Xpath查询这在需要广告识别的场景非常有效
```go
content, _ := d.DumpHierarchy(false, false) // content is the text
doc, _ := FormatHierachy(content) // doc is the *xmlquery.Node object
content, _ := d.DumpHierarchy(false, false) // content 是文本
doc, _ := FormatHierachy(content) // doc 是 *xmlquery.Node 对象
...
el := d.XPath2(`//*[@text="your-control-text"]`, doc).First()
```
### Xpath elements API
* Children of Xpath element
* Xpath元素的子元素子级后代
```go
// el := d.XPath(`//*[@resource-id="com.taobao.taobao:id/rv_main_container"]`).First()
children := el.Children()
@ -533,7 +547,7 @@ for _, c := range children {
...
}
```
* Siblings of Xpath element
* Xpath元素的兄弟元素
```go
// el := d.XPath(`//*[@resource-id="com.taobao.taobao:id/rv_main_container"]/android.widget.FrameLayout[1]`).First()
siblings := el.Siblings()
@ -541,7 +555,7 @@ for _, s := range siblings {
...
}
```
* Find descendants based on xpath
* 通过Xpath查找Xpath元素的后代元素它基于查找Xpath元素时获取的UI结构snapshot所以不会涉及Uiautomator调用
```go
// el := d.XPath(`//*[@resource-id="com.taobao.taobao:id/rv_main_container"]`).First()
children := el.Find(`//android.support.v7.widget.RecyclerView`)
@ -549,40 +563,42 @@ for _, c := range children {
fmt.Println(*c.Info())
}
```
* Find bounding rect
This describes the bounding box surrounding this control
* 获取 bounding rect
获取控件的边框,注释中解释了返回值类型
```go
bounds := el.Bounds()
/* bounds has the type of *apis.Bounds:
/* bounds 的类型为 *apis.Bounds:
* type Bounds struct {
* LX int // top-left-x
* LY int // top-left-y
* RX int // right-bottom-x
* RY int // right-bottom-y
* LX int // 左上角x
* LY int // 左上角y
* RX int // 右下角x
* RY int // 右下角y
* }
*/
```
* Find Rect
This also describes the bounding box surrounding this control
* 获取 Rect
也是获取控件的边框,注释中解释了返回值类型
```go
rect := el.Rect()
/* rect has the type of *apis.Rect:
/* rect 的类型为 *apis.Rect:
* type Bounds struct {
* LX int // top-left-x
* LY int // top-left-y
* Width int // width of control
* Height int // height of control
* LX int // 左上角x
* LY int // 左上角y
* Width int // 控件宽度
* Height int // 控件高度
* }
*/
```
* Get text of control - the text shown in user interface
* 获取控件在UI中显示的文本
```go
text := el.Text() // text is string
text := el.Text() // text 是文本
```
* Get control's info - which is everything, including text and bounding box
* 获取控件的所有信息 - 包括文本和边框,注释中解释了返回值类型
```go
info := el.Info()
/* info has the type of *apis.Info
/* info 的类型是 *apis.Info
* type Info struct {
* Text string
* Focusable bool
@ -601,137 +617,148 @@ info := el.Info()
* }
*/
```
* Get center position
* 获取元素中心点位置
```go
x, y, ok := el.Center()
```
* Click
* 点击
```go
ok := el.Click()
```
* Swipe inside the control - if the control is a (Recycler)List
* 在控件中滑动 - 如果控件是(Recycler)List
```go
dir := apis.SWIPE_DIR_LEFT
var scale float32 = 0.8
ok := el.SwipeInsideList(dir, scale)
/* dir can use these 4 values:
* SWIPE_DIR_LEFT = 1 // swipe right to left
* SWIPE_DIR_RIGHT = 2 // swipe left to right
* SWIPE_DIR_UP = 3 // swipe bottom to top
* SWIPE_DIR_DOWN = 4 // swipe top to bottom
/* 滑动方向dir 有四种值(int):
* SWIPE_DIR_LEFT = 1 // 从右向左滑动
* SWIPE_DIR_RIGHT = 2 // 从左向右滑动
* SWIPE_DIR_UP = 3 // 从下到上滑动
* SWIPE_DIR_DOWN = 4 // 从上到下滑动
scale is the percentage of width/height swiped
scale 是滑动距离的比例相对于在此滑动方向下宽度或者高度。比如再这个例子中我们是从右到左横向滑动那么scale = 0.8就意味着滑动80%的宽度距离
*/
```
* Type text
* 输入文字
```go
ok := el.Type("aaa")
```
* Screenshot - take screenshot of this control
* 截图 - 控件截图
```go
img := el.Screenshot() // img is the image.Image type
img := el.Screenshot() // img 是 image.Image 类型对象
```
## UI Object
Find ui elements via attribute matching search. In most platforms including iOS and windows UIA, accessibility api(UI Object) is far more efficient than xpath. For example, windows UIA, to get the full xml structure takes very long time, because fetching element's info involves cross-process COM calls which takes time. But here in Android, this is not the case. Xpath is as fast as accessibility(UI Object) and far more powerful. I would prefer suggest you to use xpath.
通过属性匹配方式查找UI元素. 在大多数平台包括iOS和Windows UIA, 这种典型的辅助功能API(UI Object)远比Xpath更快. 举例来说, windows UIA, 获取桌面根元素下的XML UI结构异常的慢, 因为获取元素的信息会有大量的夸进程COM调用它是很慢的. 但是在安卓获取XML UI结构非常的快基本和UI Object方式一样快. 而且Xpath还更强大支持更有表现力的查询。
与其你使用UI Object方式查找元素我会更建议你使用上一节的Xpath方式。我给两种方式提供了类似的元素操作API比如Info, Click, Wait等。
有些API在UI Object中看起来不那么自然比如Count那是因为获取所有元素在UI Object可能会牵涉非常多次的Uiautomator调用。与其让用户因为一个不那么好的查询条件等太长时间不如把底层暴露给用户让用户自己决定实现。
### construct query
UI object API doesn't effectively fetch any elements util you call (*UIObject).Get() *UiElement, (*UIObject).Wait(timeout time.Duration) int, or (*UIObject).WaitGone() bool
UI Object API事实上在调用这几个API前是不调用Uiautomator的:
1. (*UIObject).Get() *UiElement,
2. (*UIObject).Wait(timeout time.Duration) int,
3. (*UIObject).WaitGone() bool
* Construct query based on attribute values
* 基于属性值构造UI Object查询
```go
uo := d.UiObject(apis.NewUiObjectQuery("resourceId", `com.taobao.taobao:id/rv_main_container`)) // this api accepts multiple NewUiObjectQuery in args, with and relationship
uo := d.UiObject(apis.NewUiObjectQuery("resourceId", `com.taobao.taobao:id/rv_main_container`)) // 这个 api 接受多个 NewUiObjectQuery 参数, AND关系
```
* Construct sibling query - I find this api's behavior is a bit awkward. Notice that.
* 构造兄弟元素UI Object查询 - 这个API的Uiautomator返回有点怪稍微注意一下
```go
c := d.UiObject(apis.NewUiObjectQuery("resourceId", `com.taobao.taobao:id/sv_search_view`)).Sibling(apis.NewUiObjectQuery("className", "android.widget.FrameLayout"))
```
* Construct decendant query
* 构造后代元素UI Object查询 - 为了和uiautomation2 python库尽可能保持API一致我使用Child的方法名称而不是Descendant
```go
c := d.UiObject(apis.NewUiObjectQuery("resourceId", `com.taobao.taobao:id/rv_main_container`)).Child(apis.NewUiObjectQuery("className", "android.widget.LinearLayout"))
```
* Construct indexed query
* 构建指数UI Object查询 - 一个查询条件可能返回多个元素当你通过Count API知道一共有多少个匹配的元素时你可以指定小于Count值的Index从0开始用于指定获取哪个元素
```go
c := d.UiObject(
apis.NewUiObjectQuery("className", `android.support.v7.widget.RecyclerView`)).Index(0)
```
### execute ui object query
* get first ui element
执行UI Object查询
* 获取第一个UI元素
```go
el := d.UiObject(apis.NewUiObjectQuery("resourceId", `com.taobao.taobao:id/rv_main_container`)).Get() // returns an *apis.UiElement
el := d.UiObject(apis.NewUiObjectQuery("resourceId", `com.taobao.taobao:id/rv_main_container`)).Get() // 返回 *apis.UiElement 对象
```
* get element count
* 获取匹配查询的元素数量
```go
count := d.UiObject(apis.NewUiObjectQuery("resourceId", `com.taobao.taobao:id/rv_main_container`)).Count()
```
* get the nth element - here in the example - third(with .Index(2))
* 获取第N个元素 - 代码示例片段中 - 是第三个( .Index(2)因为index值是从0开始的)
```go
el := d.UiObject(apis.NewUiObjectQuery("resourceId", `com.taobao.taobao:id/rv_main_container`)).Index(2).Get()
```
* wait element appear
* 等待元素出现
```go
count := d.UiObject(apis.NewUiObjectQuery("resourceId", `com.taobao.taobao:id/rv_main_container`)).Wait(time.Minute)
// if element doesn't appear in 1 minute, it returns -1
// 返回匹配的元素的数量,如果在参数给定的时间内不出现匹配元素,返回 -1
```
* wait element disappear
* 等待元素消失
```go
ok := d.UiObject(apis.NewUiObjectQuery("resourceId", `com.taobao.taobao:id/rv_main_container`)).WaitGone(time.Minute)
// returns true if disappear, false otherwise
// 如果消失返回true反之false
```
### ui object element apis
* Get info
UI Object元素API
* 获取控件的所有信息 - 包括文本和边框,注释中解释了返回值类型
```go
info := el.Info() // the info's type is same as xpath element's info api
info := el.Info() // info的类型与xpath元素的同名API相同
```
* Get bounding rect
* 获取 bounding rect
获取控件的边框,注释中解释了返回值类型
```go
bounds := el.Bounds() // the bounds's type is the same as xpath element's bounds api
bounds := el.Bounds() // bounds的类型与xpath元素的同名API相同
```
* Get rect
* 获取 Rect
也是获取控件的边框,注释中解释了返回值类型
```go
rect := el.Rect() // the rect's type is the same as xpath element's rects api
rect := el.Rect() // rect的类型与xpath元素的同名API相同
```
* Get center position
* 获取元素中心点位置
```go
x, y, ok := el.Center()
```
* Click
* 点击
```go
ok := el.Click()
```
* Get text of control - the text shown in user interface
* 获取控件在UI中显示的文本
```go
text := el.Text() // text is string
text := el.Text() // text 是文本
```
* Swipe inside the control - if the control is a (Recycler)List
* 在控件中滑动 - 如果控件是(Recycler)List
```go
dir := apis.SWIPE_DIR_LEFT
var scale float32 = 0.8
ok := el.SwipeInsideList(dir, scale)
/* dir can use these 4 values:
* SWIPE_DIR_LEFT = 1 // swipe right to left
* SWIPE_DIR_RIGHT = 2 // swipe left to right
* SWIPE_DIR_UP = 3 // swipe bottom to top
* SWIPE_DIR_DOWN = 4 // swipe top to bottom
/* 滑动方向dir 有四种值(int):
* SWIPE_DIR_LEFT = 1 // 从右向左滑动
* SWIPE_DIR_RIGHT = 2 // 从左向右滑动
* SWIPE_DIR_UP = 3 // 从下到上滑动
* SWIPE_DIR_DOWN = 4 // 从上到下滑动
scale is the percentage of width/height swiped
scale 是滑动距离的比例相对于在此滑动方向下宽度或者高度。比如再这个例子中我们是从右到左横向滑动那么scale = 0.8就意味着滑动80%的宽度距离
*/
```
* Type text
* 输入文字
```go
ok := el.Type("aaa")
```
* Screenshot - take screenshot of this control
* 截图 - 控件截图
```go
img := el.Screenshot() // img is the image.Image type
img := el.Screenshot() // img 是 image.Image 类型对象
```
# If you want to support author, please donate(wechat), thanks
![image](https://github.com/fantonglang/go-mobile-automation-examples/blob/main/doc/wechat.jpg)
**如果想支持作者,左边是微信打赏码;如果想和作者交朋友或者一起做好玩的编程事情,右边是微信加好友的二维码**
# Contact me if you'd like working with me on the computer vision & speech recognition part.
![image](https://github.com/fantonglang/go-mobile-automation-examples/blob/main/doc/wechat2.jpg)
| ![image](https://github.com/fantonglang/go-mobile-automation-examples/blob/main/doc/wechat.jpg) | ![image](https://github.com/fantonglang/go-mobile-automation-examples/blob/main/doc/wechat2.jpg) |
| -- | -- |

739
README_en.md Normal file
View File

@ -0,0 +1,739 @@
[简体中文](./README.md) | English
# GO-MOBILE-AUTOMATION SDK
A full featured Android mobile automation sdk for golang developers
## The purpose
If you are an automation developer, you may find the python/javascript echo system provide the developers great capabilities for manipulating devices and apps.
For Android automation, some well known tools are:
1. Appium (multi-language primarily javascript/python)
2. uiautomator2 (python)
Our SDK ports the uiautomation2 python library to golang.
Why do we do this?
1. Easy to deploy - deploy one executable only (at most several dlls) instead of resolving thousands of dependencies like javascript and python. The bigger scope of this family of projects is to provide a mechanism to orchestrate the automation "scripts" to run cross a bunch of platform/systems. Must have a fast & robustic app distribution and deployment mechanism.
2. For using cloud phones (Android) - Some cloud phone providers have very poor quality adb connection. In case the automation process break because of adb connection failure, we would like the process run inside the phone
3. Robust - apps can easily get killed by Android system, but executable not. That why despite the fact that apps like pyto-python3 provides hosting for python but we don't use it.
## Inspired by
Inspired by [OpenAtx](https://github.com/openatx) and the [Uiautomation2](https://github.com/openatx/uiautomator2) python library. We entirely use the openatx drivers, leaving the client side sdk written in golang. This benefits users, because they can use the uiautomator2 tool chain, which is super cool.
## Quick start
There are 4 steps:
1. Setup the Android phone
2. Setup the development environment
3. Start creating a golang project for mobile automation
4. deploy&run
### Setup the Android phone
1. Install the specific version of the app you want to automate.
```
$ adb install [package.apk]
```
2. Download atx-agent from [here](https://github.com/openatx/atx-agent/releases) choose the armv7 version unless you run a x86 phone simulator
3. Untar atx-agent and install, follow the intallation insttructions [here](https://github.com/openatx/atx-agent)
```
$ adb push atx-agent /data/local/tmp
$ adb shell chmod 755 /data/local/tmp/atx-agent
# launch atx-agent in daemon mode
$ adb shell /data/local/tmp/atx-agent server -d
# stop already running atx-agent and start daemon
$ adb shell /data/local/tmp/atx-agent server -d --stop
```
4. Download app-uiautomator-test.apk and app-uiautomator.apk from [here](https://github.com/openatx/android-uiautomator-server/releases) and install using adb install
```
$ adb install app-uiautomator-test.apk
$ adb install app-uiautomator.apk
```
5. Grant all priviledges to the app "ATX"
6. Open app "ATX" and click "启动UIAUTOMATOR", click "开启悬浮窗"
## Setup the development environment
1. Install Python3(version 3.6+) from [here](https://www.python.org/downloads/)
2. Install [weditor](https://github.com/alibaba/web-editor)
```
$ pip3 install -U weditor
```
3. You can now open weditor as ui inspector for android applications, by typing
```
$ weditor
```
## Create a golang automation project
1. Create a folder called helloworld
2. Open terminal and type in
```
$ go mod init helloworld
```
3. Add dependency
```
$ go get github.com/fantonglang/go-mobile-automation
```
4. Add the program entry - create main.go file
5. [Here](https://github.com/fantonglang/go-mobile-automation-examples/blob/main/douyin-luo-live/main.go) is an example of main.go file
## Deploy&Run
```
# cross build linux/arm target
$ GOOS=linux GOARCH=arm go build
# deploy - helloworld is the executable name, which is the same with the go module name
$ adb push helloworld /data/local/tmp
$ adb shell chmod 755 /data/local/tmp/helloworld
# run
$ adb shell /data/local/tmp/helloworld
```
If you start a background process, you don't need the phone to connect with your PC/macos. Note that you can transfer your linux shell knowledge to using the adb shell.
## Examples
[This](https://github.com/fantonglang/go-mobile-automation-examples/blob/main/douyin-luo-live) is a working example. Read the comments in main.go carefully. This helps to resolve all dependencies and environment requirements before you start. The comments also give the commands for compilation, deployment, and execution.
# APIS
**[Connect to a device](#connect-to-a-device)**
**[Device APIS](#device-apis)**
- **[Shell commands](#shell-commands)**
- **[Retrieve the device info](#retrieve-the-device-info)**
- **[Clipboard](#clipboard)**
- **[Key Events](#key-events)**
- **[Press Key](#press-key)**
- **[New command timeout](#new-command-timeout)**
- **[Screenshot](#screenshot)**
- **[UI Hierarchy](#ui-hierarchy)**
- **[Touch](#touch)**
- **[Click](#click)**
- **[Double Click](#double-click)**
- **[Long Click](#long-click)**
- **[Swipe](#swipe)**
- **[Set Orientation](#set-orientation)**
- **[Open Quick Settings](#open-quick-settings)**
- **[Open Url](#open-url)**
- **[Show float window](#show-float-window)**
**[Input Method](#input-method)**
**[XPATH](#xpath)**
- **[Finding elements](#finding-elements)**
- **[Xpath elements API](#xpath-elements-api)**
**[UI Object](#ui-object)**
- **[construct query](#construct-query)**
- **[execute ui object query](#execute-ui-object-query)**
- **[ui object element apis](#ui-object-element-apis)**
## Connect to a device
There are two types of connection:
1. If the executable is deployed in the phone, use
``` go
package main
import (
"log"
"github.com/fantonglang/go-mobile-automation/apis"
)
...
// you don't need to specify device id, because there is no PC connection
d := apis.NewNativeDevice()
```
2. If you debug and deploy in PC/macos, use
``` go
package main
import (
"log"
"github.com/fantonglang/go-mobile-automation/apis"
)
//here c574dd45 is the device id, replace it with yours own
d, err := apis.NewHostDevice("c574dd45")
if err != nil {
log.Println("failed connecting to device")
return
}
```
Combine 2 code snippets. The following code enables the same piece of code working on the both deployments.
``` go
package main
import (
"log"
"runtime"
"github.com/fantonglang/go-mobile-automation/apis"
)
func getDevice() *apis.Device {
if runtime.GOARCH == "arm" {
return apis.NewNativeDevice()
}
//here c574dd45 is the device id, replace it with yours own
_d, err := apis.NewHostDevice("c574dd45")
if err != nil {
log.Println("101: failed connecting to device")
return nil
}
return _d
}
...
d := getDevice()
```
Take extra notice if your macos is the ARM architecture. Then you may judge based on GOOS.
## Device APIS
This part showcases how to perform common device operations:
### Shell commands
Example: Force stop douyin(China tiktok) app
```go
d.Shell(`am force-stop com.ss.android.ugc.aweme`)
```
Example: Start douyin app
```go
// You can find the app main activity by using the dumpsys command, hence I didn't implement the uiautomator2 equivalent session API for now.
d.Shell(`am start -n "com.ss.android.ugc.aweme/.main.MainActivity"`)
```
### Retrieve the device info
Get detailed device info
```go
info, err := d.DeviceInfo()
if err != nil {
log.Println("get device info failed")
return
}
bytes, err := json.Marshal(info)
if err != nil {
log.Println("error marshalling")
return
}
fmt.Println(string(bytes))
```
Below is a possible output:
```json
{
...
"version":"11",
"serial":"c574dd45",
...
"sdk":30,
"agentVersion":"0.10.0",
"display":{"width":1080,"height":2340}
...
}
```
Get window size:
```GO
w, h, err := d.WindowSize()
if err != nil {
log.Println("get window size failed")
return
}
fmt.Printf("w: %d, h: %d\n", w, h)
// device upright output example: w: 1080, h: 2340
// device horizontal output example: w: 1080, h: 2340
```
### Clipboard
Get or set clipboard content
set clipboard
```go
err := d.SetClipboard("aaa")
if err != nil {
log.Println("error clipboard")
return
}
```
get clipboard: This doesn't work in Android > 9.0. Most cloud phone work on lower Android version. I don't mind.
```go
a, err := d.GetClipboard()
if err != nil {
log.Println("error clipboard")
return
}
fmt.Println(a)
```
### Key Events
* Turn on/off screen
```go
err := d.KeyEvent(KEYCODE_POWER) // press power key to turn on/off screen
```
* Home key
```go
err := d.KeyEvent(KEYCODE_HOME)
```
d.KeyEvent is basically the Android "input keyevent " command, please refer to [this doc](https://developer.android.com/reference/android/view/KeyEvent), or if you're behind gfw, [this doc](https://blog.csdn.net/feizhixuan46789/article/details/16801429)
### Press Key
Example: press Home key
```go
err := d.Press("home")
```
supported keys are:
```go
VSK_HOME = "home"
VSK_BACK = "back"
VSK_LEFT = "left"
VSK_RIGHT = "right"
VSK_UP = "up"
VSK_DOWN = "down"
VSK_CENTER = "center"
VSK_MENU = "menu"
VSK_SEARCH = "search"
VSK_ENTER = "enter"
VSK_DELETE = "delete"
VSK_DEL = "del"
VSK_RECENT = "recent" //recent apps
VSK_VOLUME_UP = "volume_up"
VSK_VOLUME_DOWN = "volume_down"
VSK_VOLUME_MUTE = "volume_mute"
VSK_CAMERA = "camera"
VSK_POWER = "power"
```
### New command timeout
How long (in seconds) will wait for a new command from the client before assuming the client quit and ending the uiautomator service
```go
err := d.SetNewCommandTimeout(300) // unit is second
```
### Screenshot
* Screenshot and save - notice Android has readonly file system, this API is only available for host(PC/macos).
```go
err := d.ScreenshotSave("sc.png")
```
* Screenshot and get bytes (preferred, because opencv can accept bytes directly by "cv::imdecode" function)
```go
bytes, err := d.ScreenshotBytes()
```
* Screenshot and get image.Image object
```go
img, format, err := d.Screenshot() // img is the image.Image object, example of format: "jpeg"
```
### UI Hierarchy
* Get hierachy text
```go
content, err := d.DumpHierarchy(false, false) // content is the text
```
* Transform hierachy text to *xmlquery.Node object. (This is useful if you want to do xpath query based on a snapshot - this is a lot faster)
```go
doc, err := FormatHierachy(content) // doc is the *xmlquery.Node object
```
### Touch
Simulate "mouse press down", "mouse hold and move", "mouse up release"
* Get touch object
```go
touch := d.Touch()
```
* Mouse press down at a position
```go
/* press at (relative to the top-left corner)
* x: 50% position of the width
* y: 60% position of the height
*/
err := touch.Down(0.5, 0.6)
```
* Mouse hold and move
```go
/* then move to (relative to the top-left corner)
* x: 50% position of the width
* y: 10% position of the height
*/
err := touch.Move(0.5, 0.1)
```
* Mouse up release
```go
/* then mouse up release at (relative to the top-left corner)
* x: 50% position of the width
* y: 10% position of the height
*/
err := touch.Up(0.5, 0.1)
```
### Click
Click on screen given coordinates
* coordinates using percentage
```go
/* click relative to the top-left corner
* x: 48.1% position of the width
* y: 24.6% position of the height
*/
err := d.Click(0.481, 0.246)
```
* coordinates using absolute pixel values
```go
/* click relative to the top-left corner
* at (x: 481, y: 246)
*/
err := d.Click(481, 246)
```
### Double Click
```go
err := d.DoubleClickDefault(0.481, 0.246)
```
### Long Click
Mouse click, but there is a certain time interval(0.5s) between mouse down and up
```go
err := d.LongClickDefault(0.481, 0.246)
```
### Swipe
* Swipe from one point (fx, fy) to another (tx, ty)
```go
var fx, fy, tx, ty float32 = 0.5, 0.5, 0, 0
err := d.SwipeDefault(fx, fy, tx, ty)
```
* Swipe points, you can specify more than 2 points
```go
// swipe from (x=width*0.5, y=height*0.9) to (x=width*0.5,y=height*0.1)
err := d.SwipePoints(0.1, apis.Point4Swipe{0.5, 0.9}, apis.Point4Swipe{0.5, 0.1})
```
* Drag from one point (fx, fy) to another (tx, ty)
```go
var fx, fy, tx, ty float32 = 0.5, 0.5, 0, 0
err := d.DragDefault(fx, fy, tx, ty)
```
### Set Orientation
Accepts 4 orientation parameters:
* "n" - means natural
* "l" - means left
* "u" - means upsidedown
* "r" - means right
```go
err := d.SetOrientation("n")
```
### Open Quick Settings
```go
err := d.OpenQuickSettings()
```
### Open Url
```go
err := d.OpenUrl("https://bing.com")
```
### Show float window
This operation is openatx specific - open a float window to keep the automator app in the front and prevent it from getting killed
```go
err := d.ShowFloatWindow(true)
```
## Input Method
Type text, (you will switch to a special input method)
* Clear Text
```go
err := d.ClearText()
```
* Send Action
```go
err := ime.SendAction(SENDACTION_SEARCH)
```
The following actions are supported:
```go
SENDACTION_GO = 2
SENDACTION_SEARCH = 3
SENDACTION_SEND = 4
SENDACTION_NEXT = 5
SENDACTION_DONE = 6
SENDACTION_PREVIOUS = 7
```
* Send Keys - Type text
```go
err := ime.SendKeys("aaa", true)
```
## XPATH
XPATH is the most important way of finding UI Element.
### Finding elements
* Find multiple elements by xpath
```go
els := d.XPath(`//*[@text="your-control-text"]`).All()
for _, el := range els {
...
}
```
* Find one element by xpath
```go
el := d.XPath(`//*[@text="your-control-text"]`).First()
```
* Check element exists by xpath
```go
if d.XPath(`//*[@text="your-control-text"]`).First() != nil {
...
}
```
* Wait element appear
```go
el := d.XPath(`//*[@text="your-control-text"]`).Wait(time.Minute)
if el == nil {
log.Println("element doesn't appear within 1 minute")
return
}
...
```
* Wait element disappear
```go
ok := d.XPath(`//*[@text="your-control-text"]`).WaitGone(time.Minute)
if !ok {
log.Println("element doesn't disappear within 1 minute")
return
}
```
* If you want run xpath query based on ui hierachy snaphot,
```go
content, _ := d.DumpHierarchy(false, false) // content is the text
doc, _ := FormatHierachy(content) // doc is the *xmlquery.Node object
...
el := d.XPath2(`//*[@text="your-control-text"]`, doc).First()
```
### Xpath elements API
* Children of Xpath element
```go
// el := d.XPath(`//*[@resource-id="com.taobao.taobao:id/rv_main_container"]`).First()
children := el.Children()
for _, c := range children {
...
}
```
* Siblings of Xpath element
```go
// el := d.XPath(`//*[@resource-id="com.taobao.taobao:id/rv_main_container"]/android.widget.FrameLayout[1]`).First()
siblings := el.Siblings()
for _, s := range siblings {
...
}
```
* Find descendants based on xpath
```go
// el := d.XPath(`//*[@resource-id="com.taobao.taobao:id/rv_main_container"]`).First()
children := el.Find(`//android.support.v7.widget.RecyclerView`)
for _, c := range children {
fmt.Println(*c.Info())
}
```
* Find bounding rect
This describes the bounding box surrounding this control
```go
bounds := el.Bounds()
/* bounds has the type of *apis.Bounds:
* type Bounds struct {
* LX int // top-left-x
* LY int // top-left-y
* RX int // right-bottom-x
* RY int // right-bottom-y
* }
*/
```
* Find Rect
This also describes the bounding box surrounding this control
```go
rect := el.Rect()
/* rect has the type of *apis.Rect:
* type Bounds struct {
* LX int // top-left-x
* LY int // top-left-y
* Width int // width of control
* Height int // height of control
* }
*/
```
* Get text of control - the text shown in user interface
```go
text := el.Text() // text is string
```
* Get control's info - which is everything, including text and bounding box
```go
info := el.Info()
/* info has the type of *apis.Info
* type Info struct {
* Text string
* Focusable bool
* Enabled bool
* Focused bool
* Scrollable bool
* Selected bool
* ClassName string
* Bounds *Bounds
* ContentDescription string
* LongClickable bool
* PackageName string
* ResourceName string
* ResourceId string
* ChildCount int
* }
*/
```
* Get center position
```go
x, y, ok := el.Center()
```
* Click
```go
ok := el.Click()
```
* Swipe inside the control - if the control is a (Recycler)List
```go
dir := apis.SWIPE_DIR_LEFT
var scale float32 = 0.8
ok := el.SwipeInsideList(dir, scale)
/* dir can use these 4 values:
* SWIPE_DIR_LEFT = 1 // swipe right to left
* SWIPE_DIR_RIGHT = 2 // swipe left to right
* SWIPE_DIR_UP = 3 // swipe bottom to top
* SWIPE_DIR_DOWN = 4 // swipe top to bottom
scale is the percentage of width/height swiped
*/
```
* Type text
```go
ok := el.Type("aaa")
```
* Screenshot - take screenshot of this control
```go
img := el.Screenshot() // img is the image.Image type
```
## UI Object
Find ui elements via attribute matching search. In most platforms including iOS and windows UIA, accessibility api(UI Object) is far more efficient than xpath. For example, windows UIA, to get the full xml structure takes very long time, because fetching element's info involves cross-process COM calls which takes time. But here in Android, this is not the case. Xpath is as fast as accessibility(UI Object) and far more powerful. I would prefer suggest you to use xpath.
### construct query
UI object API doesn't effectively fetch any elements util you call (*UIObject).Get() *UiElement, (*UIObject).Wait(timeout time.Duration) int, or (*UIObject).WaitGone() bool
* Construct query based on attribute values
```go
uo := d.UiObject(apis.NewUiObjectQuery("resourceId", `com.taobao.taobao:id/rv_main_container`)) // this api accepts multiple NewUiObjectQuery in args, with and relationship
```
* Construct sibling query - I find this api's behavior is a bit awkward. Notice that.
```go
c := d.UiObject(apis.NewUiObjectQuery("resourceId", `com.taobao.taobao:id/sv_search_view`)).Sibling(apis.NewUiObjectQuery("className", "android.widget.FrameLayout"))
```
* Construct decendant query
```go
c := d.UiObject(apis.NewUiObjectQuery("resourceId", `com.taobao.taobao:id/rv_main_container`)).Child(apis.NewUiObjectQuery("className", "android.widget.LinearLayout"))
```
* Construct indexed query
```go
c := d.UiObject(
apis.NewUiObjectQuery("className", `android.support.v7.widget.RecyclerView`)).Index(0)
```
### execute ui object query
* get first ui element
```go
el := d.UiObject(apis.NewUiObjectQuery("resourceId", `com.taobao.taobao:id/rv_main_container`)).Get() // returns an *apis.UiElement
```
* get element count
```go
count := d.UiObject(apis.NewUiObjectQuery("resourceId", `com.taobao.taobao:id/rv_main_container`)).Count()
```
* get the nth element - here in the example - third(with .Index(2))
```go
el := d.UiObject(apis.NewUiObjectQuery("resourceId", `com.taobao.taobao:id/rv_main_container`)).Index(2).Get()
```
* wait element appear
```go
count := d.UiObject(apis.NewUiObjectQuery("resourceId", `com.taobao.taobao:id/rv_main_container`)).Wait(time.Minute)
// if element doesn't appear in 1 minute, it returns -1
```
* wait element disappear
```go
ok := d.UiObject(apis.NewUiObjectQuery("resourceId", `com.taobao.taobao:id/rv_main_container`)).WaitGone(time.Minute)
// returns true if disappear, false otherwise
```
### ui object element apis
* Get info
```go
info := el.Info() // the info's type is same as xpath element's info api
```
* Get bounding rect
```go
bounds := el.Bounds() // the bounds's type is the same as xpath element's bounds api
```
* Get rect
```go
rect := el.Rect() // the rect's type is the same as xpath element's rects api
```
* Get center position
```go
x, y, ok := el.Center()
```
* Click
```go
ok := el.Click()
```
* Get text of control - the text shown in user interface
```go
text := el.Text() // text is string
```
* Swipe inside the control - if the control is a (Recycler)List
```go
dir := apis.SWIPE_DIR_LEFT
var scale float32 = 0.8
ok := el.SwipeInsideList(dir, scale)
/* dir can use these 4 values:
* SWIPE_DIR_LEFT = 1 // swipe right to left
* SWIPE_DIR_RIGHT = 2 // swipe left to right
* SWIPE_DIR_UP = 3 // swipe bottom to top
* SWIPE_DIR_DOWN = 4 // swipe top to bottom
scale is the percentage of width/height swiped
*/
```
* Type text
```go
ok := el.Type("aaa")
```
* Screenshot - take screenshot of this control
```go
img := el.Screenshot() // img is the image.Image type
```
# If you want to support author, please donate(wechat), thanks
![image](https://github.com/fantonglang/go-mobile-automation-examples/blob/main/doc/wechat.jpg)
# Contact me if you'd like working with me on the computer vision & speech recognition part.
![image](https://github.com/fantonglang/go-mobile-automation-examples/blob/main/doc/wechat2.jpg)