go-mobile-automation/README.md

767 lines
26 KiB
Markdown
Raw Permalink Normal View History

2021-12-30 14:25:43 +08:00
简体中文 | [English](./README_en.md)
2021-12-29 20:49:35 +08:00
2024-06-27 15:44:36 +08:00
8964防csdn
2021-12-30 14:25:43 +08:00
# GO手机自动化SDK
2021-12-29 20:49:35 +08:00
2021-12-30 14:25:43 +08:00
功能齐全的手机自动化Golang SDK支持编译成二进制可执行文件适合自动化流程部署到云手机脱离ADB连接稳定高效执行
2021-12-29 20:49:35 +08:00
2021-12-30 14:25:43 +08:00
## 目的
2021-12-29 20:49:35 +08:00
2021-12-30 14:25:43 +08:00
手机自动化领域其实python是绝对的主流其他语言诸如Golang/C#/C++在整个生态中占的份额相对来说比较少。对于安卓手机自动化开发来说,我们起码有下面的工具了:
2021-12-29 20:49:35 +08:00
2021-12-30 14:25:43 +08:00
1. Appium多语言/全平台自动化支持)
2. Uiautomator2/ATX (python)
2022-03-27 13:30:10 +08:00
3. 各RPA平台
2021-12-29 20:49:35 +08:00
2021-12-30 14:25:43 +08:00
我们有这么多python库可以用为什么要再做个Golang自动化SDK呢
2021-12-29 20:49:35 +08:00
2021-12-30 14:25:43 +08:00
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系统杀死而二进制可执行文件一般不会。
2021-12-29 20:49:35 +08:00
2021-12-30 14:25:43 +08:00
所以我们采用Golang做安卓自动化语言是有道理滴
2021-12-29 20:49:35 +08:00
2021-12-30 14:25:43 +08:00
再说说其他自动化工具坑的地方:
1. Appium安装复杂而且图像识别只支持模版匹配中间层N多上层是well known的protocol要优化性能或者加一些底层支持非常困难。要部署Appium到一台新的主机虚拟机别提有多烦了
2. UiAutomator2/ATX。这个软件库我是极其推崇它的API支持比我们的Golang SDK要丰富毕竟人家才是原版。但是它不提供直接将流程部署到手机的能力。这不能说是缺陷因为考虑到自动化开发的受众群体python是绝对绝对的主流
2022-03-27 13:30:10 +08:00
3. 云扩RPA。不用说了烂的一匹。
2021-12-29 20:49:35 +08:00
2021-12-30 14:25:43 +08:00
唯一要注意的是Android是只读文件系统这意味着打日志的话我们可能需要批量收集在程序里面做批量推送到日志服务器
2021-12-29 20:49:35 +08:00
2021-12-30 14:25:43 +08:00
## 项目启发
项目启发于[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的原因。
2021-12-29 20:49:35 +08:00
## Quick start
2021-12-30 14:25:43 +08:00
我分四步讲
2021-12-29 20:49:35 +08:00
2021-12-30 14:25:43 +08:00
1. 设置安卓手机
2. 设置开发环境
3. 创建Golang自动化项目
4. 部署&执行
2021-12-29 20:49:35 +08:00
2021-12-30 14:25:43 +08:00
### 设置安卓手机
2021-12-29 20:49:35 +08:00
2021-12-30 14:25:43 +08:00
1. 安装你要自动化的APP注意安装特定的版本 - 因为不同版本同一个元素的属性很可能是不一样的 - 即使同一个版本有些属性其实也会变 - 反映到脚本中就是xpath
2021-12-29 20:49:35 +08:00
```
$ adb install [package.apk]
```
2021-12-30 14:25:43 +08:00
2. 下载 atx-agent: [点这里](https://github.com/openatx/atx-agent/releases) 选择 armv7 除非你部署的手机是x86手机模拟器
3. 解压 atx-agent 并安装, 这里有安装说明: [点这里](https://github.com/openatx/atx-agent) 或者看下面的命令行指令:
2021-12-29 20:49:35 +08:00
```
$ adb push atx-agent /data/local/tmp
$ adb shell chmod 755 /data/local/tmp/atx-agent
2021-12-30 14:25:43 +08:00
# 后台模式执行atx-agent
2021-12-29 20:49:35 +08:00
$ adb shell /data/local/tmp/atx-agent server -d
2021-12-30 14:25:43 +08:00
# 或者后台模式重启atx-agent
2021-12-29 20:49:35 +08:00
$ adb shell /data/local/tmp/atx-agent server -d --stop
```
2021-12-30 14:25:43 +08:00
4. 下载 app-uiautomator-test.apk 和 app-uiautomator.apk [点这里](https://github.com/openatx/android-uiautomator-server/releases) 然后用adb install命令安装
2021-12-29 20:49:35 +08:00
```
$ adb install app-uiautomator-test.apk
$ adb install app-uiautomator.apk
```
2021-12-30 14:25:43 +08:00
5. 给应用 "ATX" 所有权限,并且打开 "ATX" 检查有任何运行时权限请求的话,选择一直许可。
6. 打开应用 "ATX" 点击 "启动UIAUTOMATOR", 点击 "开启悬浮窗"
2021-12-29 20:49:35 +08:00
2021-12-30 14:25:43 +08:00
## 设置开发环境
2021-12-29 20:49:35 +08:00
2021-12-30 14:25:43 +08:00
1. 安装 Python3(版本 3.6+) from [here](https://www.python.org/downloads/)
2. 安装 [weditor](https://github.com/alibaba/web-editor)
2021-12-29 20:49:35 +08:00
```
$ pip3 install -U weditor
```
2021-12-30 14:25:43 +08:00
3. 打开 weditor, 它就是你的UI Inspector在命令行中输入,
2021-12-29 20:49:35 +08:00
```
$ weditor
```
2021-12-30 14:25:43 +08:00
## 创建Golang自动化项目
2021-12-29 20:49:35 +08:00
2021-12-30 14:25:43 +08:00
1. 创建文件夹 helloworld
2. 打开命令行并输入
2021-12-29 20:49:35 +08:00
```
$ go mod init helloworld
```
2021-12-30 14:25:43 +08:00
3. 添加依赖
2021-12-29 20:49:35 +08:00
```
$ go get github.com/fantonglang/go-mobile-automation
```
2021-12-30 14:25:43 +08:00
4. 添加程序入口 - 创建 main.go 文件
5. [这里](https://github.com/fantonglang/go-mobile-automation-examples/blob/main/douyin-luo-live/main.go) 是示例 main.go 文件
2021-12-29 20:49:35 +08:00
2021-12-30 14:25:43 +08:00
## 部署&执行
2021-12-29 20:49:35 +08:00
```
2021-12-30 14:25:43 +08:00
# 交叉编译(cross build) linux/arm 可执行文件
2021-12-29 20:49:35 +08:00
$ GOOS=linux GOARCH=arm go build
2021-12-30 14:25:43 +08:00
# 部署 - helloworld 是可执行文件名和Go模块名是相同的
2021-12-29 20:49:35 +08:00
$ adb push helloworld /data/local/tmp
$ adb shell chmod 755 /data/local/tmp/helloworld
2021-12-30 14:25:43 +08:00
# 运行
2021-12-29 20:49:35 +08:00
$ adb shell /data/local/tmp/helloworld
```
2021-12-30 14:25:43 +08:00
如果你用后台模式启动程序程序启动之后就不需要连接电脑。adb shell命令和普通linux系统是一样的。你可以输入
```
# nohup保证当关闭terminal session的时候程序不会被杀死&保证在后台运行程序
$ adb shell nohup /data/local/tmp/helloworld &
```
2021-12-29 20:49:35 +08:00
2021-12-30 14:25:43 +08:00
## 例子
[这里](https://github.com/fantonglang/go-mobile-automation-examples/blob/main/douyin-luo-live) 是一个能跑起来的例子。仔细阅读main函数的注释里面有手机设置环境安装编译调试部署执行的教程。
2021-12-29 21:01:55 +08:00
2021-12-29 20:49:35 +08:00
# 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)**
2021-12-30 14:25:43 +08:00
2021-12-29 20:49:35 +08:00
**[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
2021-12-30 14:25:43 +08:00
有两种类型的连接:
2021-12-29 20:49:35 +08:00
2021-12-30 14:25:43 +08:00
1. 如果程序是部署在手机上,使用如下代码:
2021-12-29 20:49:35 +08:00
``` go
package main
import (
"log"
"github.com/fantonglang/go-mobile-automation/apis"
)
...
2021-12-30 14:25:43 +08:00
// 不需要指定设备ID因为程序并不需要从电脑通过ADB连接手机
2021-12-29 20:49:35 +08:00
d := apis.NewNativeDevice()
```
2021-12-30 14:25:43 +08:00
2. 如果是在电脑端调试或部署,使用如下代码:
2021-12-29 20:49:35 +08:00
``` go
package main
import (
"log"
"github.com/fantonglang/go-mobile-automation/apis"
)
2021-12-30 14:25:43 +08:00
//这里 c574dd45 是设备ID, 你可以从adb devices指令中获取到它, 把它替换成你自己的手机设备ID
2021-12-29 20:49:35 +08:00
d, err := apis.NewHostDevice("c574dd45")
if err != nil {
log.Println("failed connecting to device")
return
}
```
2021-12-30 14:25:43 +08:00
结合上述两个代码片段,下面的代码既能在电脑端(假定是x86架构)调试的时候工作又能在Android设备中部署之后工作。它判断运行时如果是ARM使用手机的方式连接反之使用电脑的方式连接。特别注意苹果MAC最近的ARM架构电脑如果这种情况最好判断一下系统(GOOS)
2021-12-29 20:49:35 +08:00
``` go
package main
import (
"log"
"runtime"
"github.com/fantonglang/go-mobile-automation/apis"
)
func getDevice() *apis.Device {
if runtime.GOARCH == "arm" {
return apis.NewNativeDevice()
}
2021-12-30 14:25:43 +08:00
//这里 c574dd45 是设备ID, 你可以从adb devices指令中获取到它, 把它替换成你自己的手机设备ID
2021-12-29 20:49:35 +08:00
_d, err := apis.NewHostDevice("c574dd45")
if err != nil {
log.Println("101: failed connecting to device")
return nil
}
return _d
}
...
d := getDevice()
```
## Device APIS
2021-12-30 14:25:43 +08:00
这部分展示如何执行常见的设备操作
2021-12-29 20:49:35 +08:00
### Shell commands
2021-12-30 14:25:43 +08:00
示例: 强制停止抖音APP
2021-12-29 20:49:35 +08:00
```go
d.Shell(`am force-stop com.ss.android.ugc.aweme`)
```
2021-12-30 14:25:43 +08:00
示例: 打开抖音APP
2021-12-29 20:49:35 +08:00
```go
2021-12-30 14:25:43 +08:00
// 使用dumpsys命令你可以找到APP的启动Activity。所以目前并不实现uiautomator2的启动APP API以及session API。
2021-12-29 20:49:35 +08:00
d.Shell(`am start -n "com.ss.android.ugc.aweme/.main.MainActivity"`)
```
### Retrieve the device info
2021-12-30 14:25:43 +08:00
获取详细设备信息
2021-12-29 20:49:35 +08:00
```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))
```
2021-12-30 14:25:43 +08:00
下面是可能的输出:
2021-12-29 20:49:35 +08:00
```json
{
...
"version":"11",
"serial":"c574dd45",
...
"sdk":30,
"agentVersion":"0.10.0",
"display":{"width":1080,"height":2340}
...
}
```
2021-12-30 14:25:43 +08:00
获取窗口大小:
2021-12-29 20:49:35 +08:00
```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)
2021-12-30 14:25:43 +08:00
// 设备竖屏时的输出示例: w: 1080, h: 2340
// 设备横屏时的输出示例: w: 1080, h: 2340
2021-12-29 20:49:35 +08:00
```
### Clipboard
2021-12-30 14:25:43 +08:00
获取和设置剪切板内容
2021-12-29 20:49:35 +08:00
2021-12-30 14:25:43 +08:00
设置剪切板内容
2021-12-29 20:49:35 +08:00
```go
err := d.SetClipboard("aaa")
if err != nil {
log.Println("error clipboard")
return
}
```
2021-12-30 14:25:43 +08:00
获取剪切板内容: 在Android大于9.0这个API并不工作. 但是大多数云手机使用低版本Android(比如7.0)所以我并不care
2021-12-29 20:49:35 +08:00
```go
a, err := d.GetClipboard()
if err != nil {
log.Println("error clipboard")
return
}
fmt.Println(a)
```
### Key Events
2021-12-30 14:25:43 +08:00
* 打开/关闭屏幕
2021-12-29 20:49:35 +08:00
```go
2021-12-30 14:25:43 +08:00
err := d.KeyEvent(KEYCODE_POWER) // 打开关闭屏幕都是按电源键
2021-12-29 20:49:35 +08:00
```
2021-12-30 14:25:43 +08:00
* Home键
2021-12-29 20:49:35 +08:00
```go
err := d.KeyEvent(KEYCODE_HOME)
```
2021-12-30 14:25:43 +08:00
d.KeyEvent 是在调用 Android 的 "input keyevent " 命令, 请参照 [这个文档](https://developer.android.com/reference/android/view/KeyEvent), 或者你如果没有翻墙工具, 参照 [这个文档](https://blog.csdn.net/feizhixuan46789/article/details/16801429)
2021-12-29 20:49:35 +08:00
### Press Key
2021-12-30 14:25:43 +08:00
示例: 按Home键
2021-12-29 20:49:35 +08:00
```go
err := d.Press("home")
```
2021-12-30 14:25:43 +08:00
Press API支持下面的按键:
2021-12-29 20:49:35 +08:00
```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
2021-12-30 14:25:43 +08:00
设置Uiautomator服务的超时时间
2021-12-29 20:49:35 +08:00
```go
2021-12-30 14:25:43 +08:00
err := d.SetNewCommandTimeout(300) // 单位秒
2021-12-29 20:49:35 +08:00
```
### Screenshot
2021-12-30 14:25:43 +08:00
* 截屏并保存文件 - 注意 Android 使用只读文件系统, 这个API只有在电脑端有效.
2021-12-29 20:49:35 +08:00
```go
err := d.ScreenshotSave("sc.png")
```
2021-12-30 14:25:43 +08:00
* 截屏并获取[]byte字节流 (推荐, 因为opencv使用cv::imdecode函数能直接读取字节流)
2021-12-29 20:49:35 +08:00
```go
bytes, err := d.ScreenshotBytes()
```
2021-12-30 14:25:43 +08:00
* 截屏并获取image.Image对象 如果需要不同的图片编码比如jpeg/png使用image.Image可以帮你做到转换
2021-12-29 20:49:35 +08:00
```go
2021-12-30 14:25:43 +08:00
img, format, err := d.Screenshot() // img 是 image.Image 对象, format的示例: "jpeg"
2021-12-29 20:49:35 +08:00
```
### UI Hierarchy
2021-12-30 14:25:43 +08:00
* 获取UI结构的XML文本
2021-12-29 20:49:35 +08:00
```go
2021-12-30 14:25:43 +08:00
content, err := d.DumpHierarchy(false, false) // content 是文本
2021-12-29 20:49:35 +08:00
```
2021-12-30 14:25:43 +08:00
* 将UI结构的XML文本转换成 *xmlquery.Node 对象. (我们如果要基于snaphot执行xpath查询那 *xmlquery.Node 对象是非常有用的 - 它不涉及Uiautomator调用所以速度会非常快适合广告页面识别关闭的场景)
2021-12-29 20:49:35 +08:00
```go
2021-12-30 14:25:43 +08:00
doc, err := FormatHierachy(content) // doc 是 *xmlquery.Node 对象
2021-12-29 20:49:35 +08:00
```
### Touch
2021-12-30 14:25:43 +08:00
模拟“手指按下触屏”,“手指按住触屏拖动”,以及“手指离开触屏”
* 获取 touch 对象
2021-12-29 20:49:35 +08:00
```go
touch := d.Touch()
```
2021-12-30 14:25:43 +08:00
* 手指按下触屏 - 在某个位置
2021-12-29 20:49:35 +08:00
```go
2021-12-30 14:25:43 +08:00
/* (相对于屏幕左上角)手指按下触屏,位置为
* x: 50% 宽度坐标
* y: 60% 高度坐标
2021-12-29 20:49:35 +08:00
*/
err := touch.Down(0.5, 0.6)
```
2021-12-30 14:25:43 +08:00
* 手指按住触屏拖动 - 到某个位置
2021-12-29 20:49:35 +08:00
```go
2021-12-30 14:25:43 +08:00
/* (相对于屏幕左上角)然后拖动到下述位置
* x: 50% 宽度坐标
* y: 10% 高度坐标
2021-12-29 20:49:35 +08:00
*/
err := touch.Move(0.5, 0.1)
```
2021-12-30 14:25:43 +08:00
* 手指离开触屏
2021-12-29 20:49:35 +08:00
```go
2021-12-30 14:25:43 +08:00
/* (相对于屏幕左上角)然后手指离开触屏,位置为
* x: 50% 宽度坐标
* y: 10% 高度坐标
2021-12-29 20:49:35 +08:00
*/
err := touch.Up(0.5, 0.1)
```
### Click
2021-12-30 14:25:43 +08:00
在指定坐标位置点击屏幕
2021-12-29 20:49:35 +08:00
2021-12-30 14:25:43 +08:00
* 使用百分比 - 如果x,y的任意一个值在 [0,1) 的范围内,它就是表示百分比,反之如果在[1, maxScreenWidth/maxScreenHeight]的范围内,它就是绝对坐标值
2021-12-29 20:49:35 +08:00
```go
2021-12-30 14:25:43 +08:00
/* 相对于屏幕左上角点击
* x: 48.1% 宽度坐标
* y: 24.6% 高度坐标
2021-12-29 20:49:35 +08:00
*/
err := d.Click(0.481, 0.246)
```
2021-12-30 14:25:43 +08:00
* 使用绝对坐标 - 如果x,y的任意一个值在 [0,1) 的范围内,它就是表示百分比,反之如果在[1, maxScreenWidth/maxScreenHeight]的范围内,它就是绝对坐标值
2021-12-29 20:49:35 +08:00
```go
2021-12-30 14:25:43 +08:00
// 相对于屏幕左上角点击(x: 481, y: 246)
2021-12-29 20:49:35 +08:00
err := d.Click(481, 246)
```
### Double Click
2021-12-30 14:25:43 +08:00
双击
2021-12-29 20:49:35 +08:00
```go
err := d.DoubleClickDefault(0.481, 0.246)
```
### Long Click
2021-12-30 14:25:43 +08:00
长按点击相当于按下和松开之间隔了一个给定时间默认0.5s。函数名后面带Default的一般有一个非Default版本提供更多参数选择。
2021-12-29 20:49:35 +08:00
```go
err := d.LongClickDefault(0.481, 0.246)
```
### Swipe
2021-12-30 14:25:43 +08:00
滑动
* 从起始点 (fx, fy) 滑动到终点 (tx, ty)
2021-12-29 20:49:35 +08:00
```go
var fx, fy, tx, ty float32 = 0.5, 0.5, 0, 0
err := d.SwipeDefault(fx, fy, tx, ty)
```
2021-12-30 14:25:43 +08:00
* 多点滑动, 参数可以指定多个apis.Point4Swipe对象代表滑动途径的坐标点
2021-12-29 20:49:35 +08:00
```go
2021-12-30 14:25:43 +08:00
// 滑动途经 起点(x=width*0.5, y=height*0.9) 到 终点(x=width*0.5,y=height*0.1)滑动总时长0.1秒
2021-12-29 20:49:35 +08:00
err := d.SwipePoints(0.1, apis.Point4Swipe{0.5, 0.9}, apis.Point4Swipe{0.5, 0.1})
```
2021-12-30 14:25:43 +08:00
* 从起点(fx, fy) 拖动到 终点(tx, ty)
2021-12-29 20:49:35 +08:00
```go
var fx, fy, tx, ty float32 = 0.5, 0.5, 0, 0
err := d.DragDefault(fx, fy, tx, ty)
```
### Set Orientation
2021-12-30 14:25:43 +08:00
设置屏幕方向,接受下面这四种参数:
* "n" - 代表正常的竖屏
* "l" - 代表朝左的横屏
* "u" - 代表倒过来的竖屏
* "r" - 代表朝右的横屏
2021-12-29 20:49:35 +08:00
```go
err := d.SetOrientation("n")
```
### Open Quick Settings
2021-12-30 14:25:43 +08:00
打开[快速设置菜单](https://www.lifewire.com/quick-settings-menu-android-4121299)
2021-12-29 20:49:35 +08:00
```go
err := d.OpenQuickSettings()
```
### Open Url
2021-12-30 14:25:43 +08:00
打开浏览器输入URL打开网页
2021-12-29 20:49:35 +08:00
```go
err := d.OpenUrl("https://bing.com")
```
### Show float window
2021-12-30 14:25:43 +08:00
显示弹窗。这个操作是openatx特有的 - 它打开一个浮层来做应用保活。在这个SDK的产品级部署上面我们在每次流程开始时都会用d.Shell函数打开ATX应用再使用自研的分辨率无关的控件图像识别找到“启动UIAUTOMATOR”按钮并点击再打开ATX应用用同样的方法找到“开启悬浮窗”按钮并点击。以避免在流程执行的过程中由于uiautomator已经被杀死而重新唤起它 - 虽然这个操作会自动发生,但是有时候会很慢。
2021-12-29 20:49:35 +08:00
```go
err := d.ShowFloatWindow(true)
```
## Input Method
2021-12-30 14:25:43 +08:00
用来输入文字,(会使用一个特殊的输入法)
* 清除文字
2021-12-29 20:49:35 +08:00
```go
err := d.ClearText()
```
2021-12-30 14:25:43 +08:00
* 发送动作
2021-12-29 20:49:35 +08:00
```go
err := ime.SendAction(SENDACTION_SEARCH)
```
2021-12-30 14:25:43 +08:00
支持下面的动作:
2021-12-29 20:49:35 +08:00
```go
SENDACTION_GO = 2
SENDACTION_SEARCH = 3
SENDACTION_SEND = 4
SENDACTION_NEXT = 5
SENDACTION_DONE = 6
SENDACTION_PREVIOUS = 7
```
2021-12-30 14:25:43 +08:00
* 发送按键 - 输入文字包括中文以及其他Unicode
2021-12-29 20:49:35 +08:00
```go
err := ime.SendKeys("aaa", true)
```
## XPATH
2021-12-30 14:25:43 +08:00
在Android自动化中XPATH是最重要的寻找UI元素的方法。快速又强大
2021-12-29 20:49:35 +08:00
### Finding elements
2021-12-30 14:25:43 +08:00
* 通过Xpath查找多个元素
2021-12-29 20:49:35 +08:00
```go
els := d.XPath(`//*[@text="your-control-text"]`).All()
for _, el := range els {
...
}
```
2021-12-30 14:25:43 +08:00
* 通过Xpath查找一个元素首个或者nil
2021-12-29 20:49:35 +08:00
```go
el := d.XPath(`//*[@text="your-control-text"]`).First()
```
2021-12-30 14:25:43 +08:00
* 通过Xpath检查元素是否存在, el.First()函数当元素不存在的时候返回nil
2021-12-29 20:49:35 +08:00
```go
if d.XPath(`//*[@text="your-control-text"]`).First() != nil {
...
}
```
2021-12-30 14:25:43 +08:00
* 等待元素出现
2021-12-29 20:49:35 +08:00
```go
el := d.XPath(`//*[@text="your-control-text"]`).Wait(time.Minute)
if el == nil {
log.Println("element doesn't appear within 1 minute")
return
}
...
```
2021-12-30 14:25:43 +08:00
* 等待元素消失
2021-12-29 20:49:35 +08:00
```go
ok := d.XPath(`//*[@text="your-control-text"]`).WaitGone(time.Minute)
if !ok {
log.Println("element doesn't disappear within 1 minute")
return
}
```
2021-12-30 14:25:43 +08:00
* 如果我们基于UI结构的snapshot来做Xpath查询, 这样基于一次uiautomator的调用我们可以执行多次Xpath查询这在需要广告识别的场景非常有效
2021-12-29 20:49:35 +08:00
```go
2021-12-30 14:25:43 +08:00
content, _ := d.DumpHierarchy(false, false) // content 是文本
doc, _ := FormatHierachy(content) // doc 是 *xmlquery.Node 对象
2021-12-29 20:49:35 +08:00
...
el := d.XPath2(`//*[@text="your-control-text"]`, doc).First()
```
### Xpath elements API
2021-12-30 14:25:43 +08:00
* Xpath元素的子元素子级后代
2021-12-29 20:49:35 +08:00
```go
// el := d.XPath(`//*[@resource-id="com.taobao.taobao:id/rv_main_container"]`).First()
children := el.Children()
for _, c := range children {
...
}
```
2021-12-30 14:25:43 +08:00
* Xpath元素的兄弟元素
2021-12-29 20:49:35 +08:00
```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 {
...
}
```
2021-12-30 14:25:43 +08:00
* 通过Xpath查找Xpath元素的后代元素它基于查找Xpath元素时获取的UI结构snapshot所以不会涉及Uiautomator调用
2021-12-29 20:49:35 +08:00
```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())
}
```
2021-12-30 14:25:43 +08:00
* 获取 bounding rect
获取控件的边框,注释中解释了返回值类型
2021-12-29 20:49:35 +08:00
```go
bounds := el.Bounds()
2021-12-30 14:25:43 +08:00
/* bounds 的类型为 *apis.Bounds:
2021-12-29 20:49:35 +08:00
* type Bounds struct {
2021-12-30 14:25:43 +08:00
* LX int // 左上角x
* LY int // 左上角y
* RX int // 右下角x
* RY int // 右下角y
2021-12-29 20:49:35 +08:00
* }
*/
```
2021-12-30 14:25:43 +08:00
* 获取 Rect
也是获取控件的边框,注释中解释了返回值类型
2021-12-29 20:49:35 +08:00
```go
rect := el.Rect()
2021-12-30 14:25:43 +08:00
/* rect 的类型为 *apis.Rect:
2021-12-29 20:49:35 +08:00
* type Bounds struct {
2021-12-30 14:25:43 +08:00
* LX int // 左上角x
* LY int // 左上角y
* Width int // 控件宽度
* Height int // 控件高度
2021-12-29 20:49:35 +08:00
* }
*/
```
2021-12-30 14:25:43 +08:00
* 获取控件在UI中显示的文本
2021-12-29 20:49:35 +08:00
```go
2021-12-30 14:25:43 +08:00
text := el.Text() // text 是文本
2021-12-29 20:49:35 +08:00
```
2021-12-30 14:25:43 +08:00
* 获取控件的所有信息 - 包括文本和边框,注释中解释了返回值类型
2021-12-29 20:49:35 +08:00
```go
info := el.Info()
2021-12-30 14:25:43 +08:00
/* info 的类型是 *apis.Info
2021-12-29 20:49:35 +08:00
* 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
* }
*/
```
2021-12-30 14:25:43 +08:00
* 获取元素中心点位置
2021-12-29 20:49:35 +08:00
```go
x, y, ok := el.Center()
```
2021-12-30 14:25:43 +08:00
* 点击
2021-12-29 20:49:35 +08:00
```go
ok := el.Click()
```
2021-12-30 14:25:43 +08:00
* 在控件中滑动 - 如果控件是(Recycler)List
2021-12-29 20:49:35 +08:00
```go
dir := apis.SWIPE_DIR_LEFT
var scale float32 = 0.8
ok := el.SwipeInsideList(dir, scale)
2021-12-30 14:25:43 +08:00
/* 滑动方向dir 有四种值(int):
* SWIPE_DIR_LEFT = 1 // 从右向左滑动
* SWIPE_DIR_RIGHT = 2 // 从左向右滑动
* SWIPE_DIR_UP = 3 // 从下到上滑动
* SWIPE_DIR_DOWN = 4 // 从上到下滑动
2021-12-29 20:49:35 +08:00
2021-12-30 14:25:43 +08:00
scale 是滑动距离的比例相对于在此滑动方向下宽度或者高度。比如再这个例子中我们是从右到左横向滑动那么scale = 0.8就意味着滑动80%的宽度距离
2021-12-29 20:49:35 +08:00
*/
```
2021-12-30 14:25:43 +08:00
* 输入文字
2021-12-29 20:49:35 +08:00
```go
ok := el.Type("aaa")
```
2021-12-30 14:25:43 +08:00
* 截图 - 控件截图
2021-12-29 20:49:35 +08:00
```go
2021-12-30 14:25:43 +08:00
img := el.Screenshot() // img 是 image.Image 类型对象
2021-12-29 20:49:35 +08:00
```
## UI Object
2021-12-30 14:25:43 +08:00
通过属性匹配方式查找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调用。与其让用户因为一个不那么好的查询条件等太长时间不如把底层暴露给用户让用户自己决定实现。
2021-12-29 20:49:35 +08:00
### construct query
2021-12-30 14:25:43 +08:00
UI Object API事实上在调用这几个API前是不调用Uiautomator的:
1. (*UIObject).Get() *UiElement,
2. (*UIObject).Wait(timeout time.Duration) int,
3. (*UIObject).WaitGone() bool
2021-12-29 20:49:35 +08:00
2021-12-30 14:25:43 +08:00
* 基于属性值构造UI Object查询
2021-12-29 20:49:35 +08:00
```go
2021-12-30 14:25:43 +08:00
uo := d.UiObject(apis.NewUiObjectQuery("resourceId", `com.taobao.taobao:id/rv_main_container`)) // 这个 api 接受多个 NewUiObjectQuery 参数, AND关系
2021-12-29 20:49:35 +08:00
```
2021-12-30 14:25:43 +08:00
* 构造兄弟元素UI Object查询 - 这个API的Uiautomator返回有点怪稍微注意一下
2021-12-29 20:49:35 +08:00
```go
c := d.UiObject(apis.NewUiObjectQuery("resourceId", `com.taobao.taobao:id/sv_search_view`)).Sibling(apis.NewUiObjectQuery("className", "android.widget.FrameLayout"))
```
2021-12-30 14:25:43 +08:00
* 构造后代元素UI Object查询 - 为了和uiautomation2 python库尽可能保持API一致我使用Child的方法名称而不是Descendant
2021-12-29 20:49:35 +08:00
```go
c := d.UiObject(apis.NewUiObjectQuery("resourceId", `com.taobao.taobao:id/rv_main_container`)).Child(apis.NewUiObjectQuery("className", "android.widget.LinearLayout"))
```
2021-12-30 14:25:43 +08:00
* 构建指数UI Object查询 - 一个查询条件可能返回多个元素当你通过Count API知道一共有多少个匹配的元素时你可以指定小于Count值的Index从0开始用于指定获取哪个元素
2021-12-29 20:49:35 +08:00
```go
c := d.UiObject(
apis.NewUiObjectQuery("className", `android.support.v7.widget.RecyclerView`)).Index(0)
```
### execute ui object query
2021-12-30 14:25:43 +08:00
执行UI Object查询
* 获取第一个UI元素
2021-12-29 20:49:35 +08:00
```go
2021-12-30 14:25:43 +08:00
el := d.UiObject(apis.NewUiObjectQuery("resourceId", `com.taobao.taobao:id/rv_main_container`)).Get() // 返回 *apis.UiElement 对象
2021-12-29 20:49:35 +08:00
```
2021-12-30 14:25:43 +08:00
* 获取匹配查询的元素数量
2021-12-29 20:49:35 +08:00
```go
count := d.UiObject(apis.NewUiObjectQuery("resourceId", `com.taobao.taobao:id/rv_main_container`)).Count()
```
2021-12-30 14:25:43 +08:00
* 获取第N个元素 - 代码示例片段中 - 是第三个( .Index(2)因为index值是从0开始的)
2021-12-29 20:49:35 +08:00
```go
el := d.UiObject(apis.NewUiObjectQuery("resourceId", `com.taobao.taobao:id/rv_main_container`)).Index(2).Get()
```
2021-12-30 14:25:43 +08:00
* 等待元素出现
2021-12-29 20:49:35 +08:00
```go
count := d.UiObject(apis.NewUiObjectQuery("resourceId", `com.taobao.taobao:id/rv_main_container`)).Wait(time.Minute)
2021-12-30 14:25:43 +08:00
// 返回匹配的元素的数量,如果在参数给定的时间内不出现匹配元素,返回 -1
2021-12-29 20:49:35 +08:00
```
2021-12-30 14:25:43 +08:00
* 等待元素消失
2021-12-29 20:49:35 +08:00
```go
ok := d.UiObject(apis.NewUiObjectQuery("resourceId", `com.taobao.taobao:id/rv_main_container`)).WaitGone(time.Minute)
2021-12-30 14:25:43 +08:00
// 如果消失返回true反之false
2021-12-29 20:49:35 +08:00
```
### ui object element apis
2021-12-30 14:25:43 +08:00
UI Object元素API
* 获取控件的所有信息 - 包括文本和边框,注释中解释了返回值类型
2021-12-29 20:49:35 +08:00
```go
2021-12-30 14:25:43 +08:00
info := el.Info() // info的类型与xpath元素的同名API相同
2021-12-29 20:49:35 +08:00
```
2021-12-30 14:25:43 +08:00
* 获取 bounding rect
获取控件的边框,注释中解释了返回值类型
2021-12-29 20:49:35 +08:00
```go
2021-12-30 14:25:43 +08:00
bounds := el.Bounds() // bounds的类型与xpath元素的同名API相同
2021-12-29 20:49:35 +08:00
```
2021-12-30 14:25:43 +08:00
* 获取 Rect
也是获取控件的边框,注释中解释了返回值类型
2021-12-29 20:49:35 +08:00
```go
2021-12-30 14:25:43 +08:00
rect := el.Rect() // rect的类型与xpath元素的同名API相同
2021-12-29 20:49:35 +08:00
```
2021-12-30 14:25:43 +08:00
* 获取元素中心点位置
2021-12-29 20:49:35 +08:00
```go
x, y, ok := el.Center()
```
2021-12-30 14:25:43 +08:00
* 点击
2021-12-29 20:49:35 +08:00
```go
ok := el.Click()
```
2021-12-30 14:25:43 +08:00
* 获取控件在UI中显示的文本
2021-12-29 20:49:35 +08:00
```go
2021-12-30 14:25:43 +08:00
text := el.Text() // text 是文本
2021-12-29 20:49:35 +08:00
```
2021-12-30 14:25:43 +08:00
* 在控件中滑动 - 如果控件是(Recycler)List
2021-12-29 20:49:35 +08:00
```go
dir := apis.SWIPE_DIR_LEFT
var scale float32 = 0.8
ok := el.SwipeInsideList(dir, scale)
2021-12-30 14:25:43 +08:00
/* 滑动方向dir 有四种值(int):
* SWIPE_DIR_LEFT = 1 // 从右向左滑动
* SWIPE_DIR_RIGHT = 2 // 从左向右滑动
* SWIPE_DIR_UP = 3 // 从下到上滑动
* SWIPE_DIR_DOWN = 4 // 从上到下滑动
2021-12-29 20:49:35 +08:00
2021-12-30 14:25:43 +08:00
scale 是滑动距离的比例相对于在此滑动方向下宽度或者高度。比如再这个例子中我们是从右到左横向滑动那么scale = 0.8就意味着滑动80%的宽度距离
2021-12-29 20:49:35 +08:00
*/
```
2021-12-30 14:25:43 +08:00
* 输入文字
2021-12-29 20:49:35 +08:00
```go
ok := el.Type("aaa")
```
2021-12-30 14:25:43 +08:00
* 截图 - 控件截图
2021-12-29 20:49:35 +08:00
```go
2021-12-30 14:25:43 +08:00
img := el.Screenshot() // img 是 image.Image 类型对象
2021-12-29 20:49:35 +08:00
```
2021-12-30 14:25:43 +08:00
**如果想支持作者,左边是微信打赏码;如果想和作者交朋友或者一起做好玩的编程事情,右边是微信加好友的二维码**
2021-12-29 20:49:35 +08:00
2021-12-30 14:25:43 +08:00
| ![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) |
2022-03-27 13:30:10 +08:00
| -- | -- |