完成基础功能
This commit is contained in:
779
README.md
779
README.md
@@ -1,123 +1,472 @@
|
||||
# go-mobile-uiautomator
|
||||
# Go Mobile UIAutomator
|
||||
|
||||
一个用 Go 语言实现的 Android 移动设备 UI 自动化工具,通过 ADB 协议与 Android 设备通信,实现应用安装、文件传输和 UI 自动化测试功能。
|
||||
<p align="center">
|
||||
<a href="https://github.com/zhuy1228/go-mobile-uiautomator">
|
||||
<img src="https://img.shields.io/badge/Go-1.21+-00ADD8?style=flat-square&logo=go" alt="Go Version">
|
||||
</a>
|
||||
<a href="LICENSE">
|
||||
<img src="https://img.shields.io/badge/License-MIT-green?style=flat-square" alt="License">
|
||||
</a>
|
||||
<a href="#">
|
||||
<img src="https://img.shields.io/badge/dependencies-0-brightgreen?style=flat-square" alt="Zero Dependencies">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## 特性
|
||||
Go 语言实现的 Android UIAutomator2 客户端库。纯 Go 实现,零外部依赖,通过 ADB 隧道协议与 Android 设备直连,提供完整的 UI 自动化测试能力。
|
||||
|
||||
- ✅ **纯 Go 实现** - 不依赖外部 ADB 命令行工具
|
||||
- ✅ **跨平台** - 支持 Windows/Linux/macOS
|
||||
- ✅ **轻量级** - 无需安装 Android SDK
|
||||
- ✅ **低延迟** - 直接 TCP 连接,无中间层
|
||||
- ✅ **功能完整** - 支持设备发现、文件传输、APK 安装、Shell 执行
|
||||
- ✅ **UIAutomator2** - 内置 UIAutomator2 服务支持
|
||||
参考 Python [openatx/uiautomator2](https://github.com/openatx/uiautomator2) 项目实现,连接方案与 Python 版本完全一致。
|
||||
|
||||
## 环境要求
|
||||
## 功能特性
|
||||
|
||||
- Go 1.25.1+
|
||||
- 已启动的 ADB 服务器(Android SDK 自带)
|
||||
- Android 设备已开启 USB 调试
|
||||
- ✅ **ADB 隧道** - 纯 Go 实现 ADB TCP 协议,通过 ADB 隧道直连设备,无需端口转发
|
||||
- ✅ **设备管理** - 设备发现、连接、信息查询
|
||||
- ✅ **UIAutomator2 服务** - 自动启动和管理 UIAutomator2 服务端
|
||||
- ✅ **UI 元素操作** - 点击、滑动、输入、拖拽、手势等完整 UI 操作
|
||||
- ✅ **元素定位** - 支持 text、resourceId、className、description 等多种定位方式
|
||||
- ✅ **便捷选择器** - `d.ByText("登录").Click()` Python 风格链式调用
|
||||
- ✅ **输入法集成** - AdbKeyboard 输入法支持,支持中文输入
|
||||
- ✅ **Watcher 监控** - 自动监控并处理弹窗(权限提示、广告等)
|
||||
- ✅ **文件传输** - 通过 ADB Sync 协议推送文件
|
||||
- ✅ **应用管理** - 启动、停止、卸载、清除应用数据
|
||||
- ✅ **截图功能** - 设备屏幕截图
|
||||
- ✅ **剪贴板** - 读写设备剪贴板
|
||||
- ✅ **Session 会话** - 应用会话管理,自动检测应用存活状态
|
||||
- ✅ **配置管理** - 线程安全的设备参数配置
|
||||
- ✅ **错误体系** - 完整的错误类型定义和自动重试机制
|
||||
- ✅ **JSON-RPC** - 完整的 JSON-RPC 2.0 通信层
|
||||
|
||||
## 安装
|
||||
|
||||
### 作为项目引用
|
||||
|
||||
```bash
|
||||
go get github.com/zhuy1228/go-mobile-uiautomator@latest
|
||||
```
|
||||
|
||||
### 从源码构建
|
||||
|
||||
```bash
|
||||
git clone https://github.com/zhuy1228/go-mobile-uiautomator.git
|
||||
cd go-mobile-uiautomator
|
||||
go mod download
|
||||
go get github.com/zhuy1228/go-mobile-uiautomator
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 基础设置
|
||||
|
||||
确保 ADB 服务器正在运行:
|
||||
|
||||
```bash
|
||||
adb devices
|
||||
```
|
||||
|
||||
### 2. 运行示例
|
||||
|
||||
修改 `cmd/main.go` 中的设备配置:
|
||||
### 连接设备
|
||||
|
||||
```go
|
||||
import "github.com/zhuy1228/go-mobile-uiautomator/adb"
|
||||
package main
|
||||
|
||||
const serial = "your-device-serial" // 你的设备序列号
|
||||
const addr = "127.0.0.1:5037" // ADB 服务器地址
|
||||
```
|
||||
|
||||
运行程序:
|
||||
|
||||
```bash
|
||||
go run cmd/main.go
|
||||
```
|
||||
|
||||
### 3. 启动 UIAutomator2 服务
|
||||
|
||||
程序会自动:
|
||||
1. 列出已连接的设备
|
||||
2. 推送 `u2.jar` 到设备
|
||||
3. 启动 UIAutomator2 服务
|
||||
4. 输出服务日志
|
||||
|
||||
## 核心功能
|
||||
|
||||
### 设备管理
|
||||
|
||||
```go
|
||||
// 列出所有设备
|
||||
payload, _ := adb.ListDevicesRaw(addr, 15*time.Second)
|
||||
devices := adb.ParseDevicesPayload(payload)
|
||||
|
||||
// 通过 Product 名称查找设备
|
||||
serial, err := adb.FindSerialByProduct(addr, "23113RKC6C")
|
||||
|
||||
// 建立设备连接
|
||||
conn, err := adb.DialADB(addr, 15*time.Second)
|
||||
defer conn.Close()
|
||||
adb.TransportTo(conn, serial)
|
||||
```
|
||||
|
||||
### Shell 命令执行
|
||||
|
||||
```go
|
||||
// 执行 Shell 命令
|
||||
conn, _ := adb.DialADB(addr, 15*time.Second)
|
||||
adb.TransportTo(conn, serial)
|
||||
output, err := adb.ExecShell(conn, "getprop")
|
||||
fmt.Println(string(output))
|
||||
```
|
||||
|
||||
### 文件传输
|
||||
|
||||
```go
|
||||
// 推送文件到设备
|
||||
sync := adb.InitSync(conn)
|
||||
n, err := sync.SyncPushFile(
|
||||
"local/path/file.png", // 本地文件路径
|
||||
"/sdcard/remote.png", // 设备目标路径
|
||||
0644, // 文件权限
|
||||
true, // 调试模式
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/zhuy1228/go-mobile-uiautomator/libs"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 创建设备连接(自动推送 u2.jar、启动 UIAutomator2 服务)
|
||||
d, err := libs.NewDevice("设备序列号")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer d.Close()
|
||||
|
||||
// 自定义 ADB 地址
|
||||
// d, err := libs.NewDevice("设备序列号", "192.168.1.100:5037")
|
||||
|
||||
// 获取设备信息
|
||||
info, err := d.Info()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Printf("屏幕尺寸: %v x %v\n", info["displayWidth"], info["displayHeight"])
|
||||
}
|
||||
```
|
||||
|
||||
### APK 安装
|
||||
### 设备发现
|
||||
|
||||
```go
|
||||
// 安装 APK
|
||||
services.InstallServiceApk(addr, serial)
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
"github.com/zhuy1228/go-mobile-uiautomator/adb"
|
||||
"github.com/zhuy1228/go-mobile-uiautomator/libs"
|
||||
)
|
||||
|
||||
// 或手动安装
|
||||
remotePath := "/data/local/tmp/app.apk"
|
||||
output, err := adb.InstallApkOnDevice(addr, serial, remotePath, "-r", true)
|
||||
// 列出所有连接的设备
|
||||
payload, err := adb.ListDevicesRaw(libs.DefaultADBAddr, 15*time.Second)
|
||||
devices := adb.ParseDevicesPayload(payload)
|
||||
for _, dev := range devices {
|
||||
fmt.Printf("设备: %s 状态: %s\n", dev.Serial, dev.State)
|
||||
}
|
||||
```
|
||||
|
||||
### 应用管理
|
||||
|
||||
```go
|
||||
// 启动应用(stop=true 先停止再启动)
|
||||
d.AppStart("com.example.app", "", true)
|
||||
|
||||
// 启动应用并指定 Activity
|
||||
d.AppStart("com.example.app", ".MainActivity", false)
|
||||
|
||||
// 获取当前运行的应用信息
|
||||
info, _ := d.AppCurrent()
|
||||
fmt.Printf("当前应用: %s PID: %s\n", info["package"], info["pid"])
|
||||
|
||||
// 等待应用启动(超时 20 秒,front=true 等待应用到前台)
|
||||
pid, err := d.AppWait("com.example.app", 20.0, true)
|
||||
|
||||
// 停止应用
|
||||
d.AppStop("com.example.app")
|
||||
|
||||
// 清除应用数据
|
||||
d.AppClear("com.example.app")
|
||||
|
||||
// 卸载应用
|
||||
d.AppUninstall("com.example.app")
|
||||
```
|
||||
|
||||
### UI 元素查找
|
||||
|
||||
```go
|
||||
// ===== 便捷方法(推荐) =====
|
||||
|
||||
// 通过 text 查找并操作
|
||||
d.ByText("登录").Click()
|
||||
|
||||
// 通过 textContains 模糊查找
|
||||
d.ByTextContains("登").Click()
|
||||
|
||||
// 通过 resourceId 查找
|
||||
d.ByResourceId("com.example:id/btn_login").Click()
|
||||
|
||||
// 通过 description 查找
|
||||
d.ByDescription("搜索").Click()
|
||||
|
||||
// 通过 className 查找
|
||||
d.ByClassName("android.widget.EditText").SetText("Hello")
|
||||
|
||||
// 多条件组合查找
|
||||
el := d.By(libs.P{
|
||||
"className": "android.widget.TextView",
|
||||
"text": "确定",
|
||||
})
|
||||
el.Click()
|
||||
|
||||
// ===== FindElement 方法 =====
|
||||
|
||||
el, err := d.FindElement(map[string]interface{}{
|
||||
"resourceId": "com.example:id/btn_login",
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
el.Click()
|
||||
|
||||
// 检查元素是否存在
|
||||
exists, _ := d.Exists(map[string]interface{}{"text": "登录"})
|
||||
```
|
||||
|
||||
### 等待操作
|
||||
|
||||
```go
|
||||
el := d.ByText("登录")
|
||||
|
||||
// 等待元素出现(超时 10 秒)
|
||||
found, _ := el.Wait(true, 10.0)
|
||||
|
||||
// 等待元素消失
|
||||
gone, _ := el.WaitGone(10.0)
|
||||
|
||||
// 等待元素出现,不存在则返回错误
|
||||
err := el.MustWait(10.0)
|
||||
|
||||
// 设置全局隐式等待超时
|
||||
d.ImplicitlyWait(20.0)
|
||||
```
|
||||
|
||||
### 点击操作
|
||||
|
||||
```go
|
||||
// 坐标点击
|
||||
d.Click(500, 800)
|
||||
|
||||
// 双击
|
||||
d.DoubleClick(500, 800, 0)
|
||||
|
||||
// 长按坐标(2 秒)
|
||||
d.LongClick(500, 800, 2.0)
|
||||
|
||||
// 元素点击
|
||||
d.ByText("确定").Click()
|
||||
|
||||
// 带偏移的点击(相对元素宽高的比例,0.5 = 中心)
|
||||
d.ByText("确定").ClickWithOffset(0.5, 0.5)
|
||||
|
||||
// 点击元素(如果存在),返回是否成功
|
||||
clicked := d.ByText("确定").ClickExists(5.0)
|
||||
|
||||
// 持续点击直到元素消失(最多重试 5 次,间隔 1 秒)
|
||||
d.ByText("确定").ClickGone(5, 1.0)
|
||||
|
||||
// 元素长按(1.5 秒)
|
||||
d.ByText("确定").LongClick(1.5)
|
||||
```
|
||||
|
||||
### 文本输入
|
||||
|
||||
```go
|
||||
el := d.ByResourceId("com.example:id/input")
|
||||
|
||||
// 获取元素文本
|
||||
text, _ := el.GetText()
|
||||
|
||||
// 设置文本(先清空再输入)
|
||||
el.SetText("Hello World")
|
||||
|
||||
// 清除文本
|
||||
el.ClearText()
|
||||
|
||||
// 追加文本
|
||||
el.SendKeys("additional text")
|
||||
|
||||
// 使用 AdbKeyboard 输入法(支持中文)
|
||||
im := libs.NewInputMethod(d)
|
||||
im.SendKeys("你好世界")
|
||||
im.ClearText() // 清除文本
|
||||
im.SendAction("done") // 模拟输入法动作
|
||||
```
|
||||
|
||||
### 滑动操作
|
||||
|
||||
```go
|
||||
// 坐标滑动(steps 控制速度,每步约 5ms)
|
||||
d.Swipe(500, 1500, 500, 500, 55)
|
||||
|
||||
// 按持续时间滑动(0.5 秒)
|
||||
d.SwipeWithDuration(500, 1500, 500, 500, 0.5)
|
||||
|
||||
// 多点连续滑动
|
||||
d.SwipePoints([][2]int{{500, 1500}, {500, 1000}, {500, 500}}, 0.2)
|
||||
|
||||
// 使用 SwipeExt 扩展滑动
|
||||
se := libs.NewSwipeExt(d)
|
||||
se.Up(0.8, 55) // 向上滑动 80% 屏幕
|
||||
se.Down(0.5, 55) // 向下滑动 50% 屏幕
|
||||
se.Left(0.6, 55) // 向左滑动 60% 屏幕
|
||||
se.Right(0.4, 55) // 向右滑动 40% 屏幕
|
||||
|
||||
// 元素内滑动
|
||||
d.ByClassName("android.widget.ListView").UiSwipe("up", 55)
|
||||
d.ByClassName("android.widget.ListView").UiSwipe("down", 55)
|
||||
|
||||
// 拖拽
|
||||
d.Drag(100, 200, 500, 600, 0.5)
|
||||
d.ByText("拖拽我").DragTo(500, 600, 0.5)
|
||||
```
|
||||
|
||||
### 滚动查找
|
||||
|
||||
```go
|
||||
list := d.ByClassName("android.widget.ListView")
|
||||
|
||||
// 向前/向后滚动
|
||||
list.ScrollForward(true, 55) // 垂直向前
|
||||
list.ScrollBackward(true, 55) // 垂直向后
|
||||
|
||||
// 滚动到顶部/底部
|
||||
list.ScrollToBeginning(true, 10, 55) // 最多 10 次
|
||||
list.ScrollToEnd(true, 10, 55)
|
||||
|
||||
// 滚动直到目标元素可见
|
||||
list.ScrollTo(map[string]interface{}{"text": "目标文本"}, true)
|
||||
|
||||
// 快速滑动(Fling)
|
||||
list.FlingForward(true)
|
||||
list.FlingBackward(true)
|
||||
list.FlingToBeginning(true, 10)
|
||||
list.FlingToEnd(true, 10)
|
||||
```
|
||||
|
||||
### 手势操作
|
||||
|
||||
```go
|
||||
el := d.ByResourceId("com.example:id/image")
|
||||
|
||||
// 捏合(缩小)
|
||||
el.PinchIn(50, 10)
|
||||
|
||||
// 捏合(放大)
|
||||
el.PinchOut(50, 10)
|
||||
```
|
||||
|
||||
### 子元素和兄弟元素
|
||||
|
||||
```go
|
||||
// 查找子元素
|
||||
parent := d.ByClassName("android.widget.LinearLayout")
|
||||
child, _ := parent.Child(map[string]interface{}{"className": "android.widget.Button"})
|
||||
child.Click()
|
||||
|
||||
// 查找兄弟元素
|
||||
sibling, _ := d.ByText("标题").Sibling(map[string]interface{}{"className": "android.widget.Button"})
|
||||
sibling.Click()
|
||||
|
||||
// 通过文本在子元素中搜索
|
||||
item, _ := parent.ChildByText("目标文本", map[string]interface{}{"className": "android.widget.TextView"})
|
||||
|
||||
// 获取匹配元素数量
|
||||
count, _ := d.ByClassName("android.widget.Button").Count()
|
||||
|
||||
// 获取指定索引的元素
|
||||
d.ByClassName("android.widget.Button").Instance(0).Click() // 第一个
|
||||
d.ByClassName("android.widget.Button").Instance(-1).Click() // 最后一个
|
||||
```
|
||||
|
||||
### Watcher 弹窗监控
|
||||
|
||||
```go
|
||||
// 创建 Watcher 上下文(builtin=true 自动添加常见弹窗规则)
|
||||
w := libs.NewWatchContext(d, true)
|
||||
|
||||
// 添加监控规则:点击"同意"按钮
|
||||
w.WhenText("同意").Click()
|
||||
|
||||
// 添加监控规则:点击"允许"按钮
|
||||
w.WhenText("ALLOW").Click()
|
||||
|
||||
// 添加监控规则:按下返回键关闭弹窗
|
||||
w.WhenText("取消").Press("back")
|
||||
|
||||
// 添加监控规则:自定义回调
|
||||
w.WhenDescription("广告").Call(func(d *libs.Device) error {
|
||||
fmt.Println("检测到广告弹窗")
|
||||
return d.Press("back")
|
||||
})
|
||||
|
||||
// 启动监控(后台运行)
|
||||
w.Start()
|
||||
|
||||
// 等待页面稳定(5 秒内无弹窗,超时 30 秒)
|
||||
w.WaitStable(5.0, 30.0)
|
||||
|
||||
// 停止监控
|
||||
w.Stop()
|
||||
```
|
||||
|
||||
### Session 会话管理
|
||||
|
||||
```go
|
||||
// 创建应用会话(attach=false 启动新实例)
|
||||
session, err := libs.NewSession(d, "com.example.app", false)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// 检查应用是否仍在运行
|
||||
if session.Running() {
|
||||
fmt.Println("应用运行中, PID:", session.PID())
|
||||
}
|
||||
|
||||
// 会话中的 JSON-RPC 调用会自动检查应用状态
|
||||
// 如果应用退出,会返回 SessionBrokenError
|
||||
|
||||
// 重启应用
|
||||
session.Restart()
|
||||
|
||||
// 关闭会话
|
||||
session.Close()
|
||||
```
|
||||
|
||||
### 截图
|
||||
|
||||
```go
|
||||
// 获取截图(JPEG 格式字节数据)
|
||||
imgData, err := d.Screenshot()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// 保存到文件
|
||||
os.WriteFile("screenshot.png", imgData, 0644)
|
||||
```
|
||||
|
||||
### 设备控制
|
||||
|
||||
```go
|
||||
// 亮屏/息屏
|
||||
d.ScreenOn()
|
||||
d.ScreenOff()
|
||||
|
||||
// 解锁屏幕
|
||||
d.Unlock()
|
||||
|
||||
// 按键操作
|
||||
d.Press("home")
|
||||
d.Press("back")
|
||||
d.Press("recent")
|
||||
d.Press("volume_up")
|
||||
d.Press("volume_down")
|
||||
d.Press("power")
|
||||
d.Press("enter")
|
||||
|
||||
// 发送按键码
|
||||
d.PressKeyCode(4, 0) // KEYCODE_BACK
|
||||
|
||||
// 长按按键
|
||||
d.LongPress("power")
|
||||
|
||||
// 发送按键事件
|
||||
d.Keyevent("KEYCODE_MENU")
|
||||
|
||||
// 屏幕方向
|
||||
orientation, _ := d.Orientation() // 获取当前方向
|
||||
d.SetOrientation("natural") // 竖屏
|
||||
d.SetOrientation("left") // 左横屏
|
||||
d.FreezeRotation(true) // 冻结旋转
|
||||
|
||||
// 打开通知栏/快速设置
|
||||
d.OpenNotification()
|
||||
d.OpenQuickSettings()
|
||||
|
||||
// 打开 URL
|
||||
d.OpenURL("https://www.example.com")
|
||||
|
||||
// 剪贴板
|
||||
d.SetClipboard("复制的文本")
|
||||
text, _ := d.GetClipboard()
|
||||
|
||||
// Toast
|
||||
d.MakeToast("提示消息", 2000) // 2000 毫秒
|
||||
toast, _ := d.GetLastToast()
|
||||
d.ClearToast()
|
||||
|
||||
// 获取页面层级结构
|
||||
hierarchy, _ := d.DumpHierarchy(false, 50)
|
||||
|
||||
// 执行 Shell 命令
|
||||
result, _ := d.Shell("ls", "/sdcard/")
|
||||
fmt.Println(result.Output)
|
||||
|
||||
// 获取窗口大小
|
||||
w, h, _ := d.WindowSize()
|
||||
fmt.Printf("窗口大小: %d x %d\n", w, h)
|
||||
|
||||
// 获取设备 WLAN IP
|
||||
ip, _ := d.WlanIP()
|
||||
|
||||
// 推送文件到设备
|
||||
d.Push("local/file.txt", "/sdcard/file.txt")
|
||||
```
|
||||
|
||||
### 配置管理
|
||||
|
||||
```go
|
||||
// 设置等待超时(秒)
|
||||
d.Settings().Set("wait_timeout", 30.0)
|
||||
|
||||
// 获取配置
|
||||
timeout := d.Settings().GetFloat64("wait_timeout")
|
||||
|
||||
// 设置操作延迟
|
||||
d.Settings().Set("operation_delay", []float64{0.5, 1.0}) // [操作前延迟, 操作后延迟]
|
||||
|
||||
// 设置需要延迟的操作方法
|
||||
d.Settings().Set("operation_delay_methods", []string{"click", "swipe"})
|
||||
|
||||
// 设置隐式等待超时
|
||||
d.ImplicitlyWait(20.0)
|
||||
```
|
||||
|
||||
### UI 选择器
|
||||
@@ -126,47 +475,108 @@ output, err := adb.InstallApkOnDevice(addr, serial, remotePath, "-r", true)
|
||||
// 创建选择器
|
||||
selector := libs.MustNew(map[string]interface{}{
|
||||
"className": "android.widget.TextView",
|
||||
"text": "登录",
|
||||
"text": "登录",
|
||||
})
|
||||
|
||||
// 添加子元素选择
|
||||
selector.Child(map[string]interface{}{
|
||||
"resourceId": "com.example:id/button",
|
||||
"instance": 0,
|
||||
"instance": 0,
|
||||
})
|
||||
|
||||
// 序列化为 JSON
|
||||
jsonData, _ := selector.ToJSON()
|
||||
```
|
||||
|
||||
### APK 安装
|
||||
|
||||
```go
|
||||
import "github.com/zhuy1228/go-mobile-uiautomator/services"
|
||||
|
||||
// 安装 UIAutomator2 JAR(自动检查设备端是否已存在)
|
||||
err := services.InstallServiceJar(addr, serial, "", false)
|
||||
|
||||
// 安装 UIAutomator2 APK
|
||||
err = services.InstallServiceApk(addr, serial, "", false)
|
||||
|
||||
// 强制重新安装
|
||||
err = services.InstallServiceApk(addr, serial, "/path/to/custom.apk", true)
|
||||
```
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
go-mobile-uiautomator/
|
||||
├── adb/ # ADB 协议实现
|
||||
│ ├── connect.go # 连接管理、命令发送/接收
|
||||
│ ├── device.go # 设备发现、APK 安装
|
||||
│ └── sync.go # 文件同步协议
|
||||
├── assets/ # 资源文件
|
||||
│ ├── u2.jar # UIAutomator2 服务端
|
||||
│ ├── app-uiautomator.apk
|
||||
│ └── sync.sh
|
||||
├── cmd/ # 主程序
|
||||
├── adb/ # ADB 协议实现
|
||||
│ ├── connect.go # 连接管理、ADB 隧道、命令发送/接收
|
||||
│ ├── device.go # 设备发现、APK 安装
|
||||
│ ├── sync.go # 文件同步协议(Sync Push)
|
||||
│ └── doc.go # 包文档
|
||||
├── libs/ # UIAutomator2 客户端核心库
|
||||
│ ├── device.go # 核心设备客户端(连接、操作、应用管理)
|
||||
│ ├── uiobject.go # UI 元素操作(点击、输入、滑动、滚动)
|
||||
│ ├── jsonrpc.go # JSON-RPC 2.0 通信层
|
||||
│ ├── selector.go # UI 选择器(定位条件构建)
|
||||
│ ├── request.go # ADB 隧道 + 自定义 HTTP Transport
|
||||
│ ├── input.go # AdbKeyboard 输入法集成
|
||||
│ ├── watcher.go # Watcher 弹窗自动监控
|
||||
│ ├── session.go # 应用会话管理
|
||||
│ ├── swipe_ext.go # 扩展滑动操作
|
||||
│ ├── settings.go # 线程安全配置管理
|
||||
│ ├── errors.go # 完整错误类型定义
|
||||
│ ├── proto.go # 协议常量和枚举
|
||||
│ └── doc.go # 包文档
|
||||
├── services/ # 服务模块
|
||||
│ ├── install_service.go # UIAutomator2 安装服务
|
||||
│ └── doc.go # 包文档
|
||||
├── assets/ # 资源文件
|
||||
│ ├── u2.jar # UIAutomator2 服务端
|
||||
│ ├── app-uiautomator.apk # UIAutomator2 APK
|
||||
│ └── sync.sh # 资源同步脚本
|
||||
├── cmd/ # 主程序入口
|
||||
│ └── main.go
|
||||
├── config/ # 配置管理
|
||||
│ └── index.go
|
||||
├── libs/ # 工具库
|
||||
│ ├── request.go # HTTP over ADB
|
||||
│ └── selector.go # UI 选择器
|
||||
├── services/ # 服务模块
|
||||
│ └── install_service.go # UIAutomator2 安装
|
||||
├── test/
|
||||
│ └── test.py
|
||||
├── config.yaml # 配置文件
|
||||
├── go.mod
|
||||
├── go.mod # Go 模块定义(零外部依赖)
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 架构说明
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ 用户代码 │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ libs.Device / libs.UiObject / libs.Session │
|
||||
│ (设备操作 / UI 元素 / 会话管理) │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ libs.JsonRpcWrapper │
|
||||
│ (JSON-RPC 2.0 通信层) │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ libs.AdbTunnelDevice + http.Transport │
|
||||
│ (ADB 隧道 + 标准 HTTP 客户端) │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ adb.CreateTunnel (纯 Go ADB 协议) │
|
||||
│ (TCP → ADB Server → transport → tcp:9008) │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ Android 设备 │
|
||||
│ UIAutomator2 HTTP 服务 (端口 9008) │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 连接原理
|
||||
|
||||
与 Python uiautomator2 采用完全相同的 ADB 隧道方案:
|
||||
|
||||
1. **每次 HTTP 请求**通过 `adb.CreateTunnel()` 建立一条 ADB 隧道到设备端口 9008
|
||||
2. `http.Transport` 的 `DialContext` 被替换为 ADB 隧道连接
|
||||
3. Go 标准 `http.Client` 在隧道上发送完整的 HTTP/1.1 请求
|
||||
4. 请求完成后隧道自动关闭,**无需 `adb forward`,不占用本地端口**
|
||||
|
||||
```
|
||||
Go http.Client → http.Transport.DialContext → adb.CreateTunnel()
|
||||
↓ ↓
|
||||
HTTP/1.1 请求 ←→ ADB Server ←→ 设备:9008 (UIAutomator2)
|
||||
```
|
||||
|
||||
## ADB 协议实现
|
||||
|
||||
本项目实现了以下 ADB 协议功能:
|
||||
@@ -176,95 +586,118 @@ go-mobile-uiautomator/
|
||||
| 设备列表 | `host:devices-l` | 列出所有连接的设备 |
|
||||
| 设备路由 | `host:transport:<serial>` | 切换到指定设备 |
|
||||
| Shell 执行 | `shell:<command>` | 执行 Shell 命令 |
|
||||
| ADB 隧道 | `tcp:<port>` | 建立到设备端口的直连隧道 |
|
||||
| 文件同步 | `sync:` | 启动文件传输协议 |
|
||||
| 文件推送 | `SEND/DATA/DONE` | Sync 协议传输文件 |
|
||||
|
||||
## 配置说明
|
||||
## 错误类型
|
||||
|
||||
`config.yaml` 配置文件:
|
||||
本库定义了完整的错误类型层次,便于精确错误处理:
|
||||
|
||||
```yaml
|
||||
appName: "Go Desk"
|
||||
port: 6997 # 本地监听端口
|
||||
wsUrl: "106.12.33.188:6996" # WebSocket 地址
|
||||
stunUrl: "stun:106.12.33.188:3478" # STUN 服务器
|
||||
apiUrl: "http://106.12.33.188:6996" # API 地址
|
||||
siteFileDir: "www" # 网页文件目录
|
||||
```
|
||||
|
||||
加载配置:
|
||||
|
||||
```go
|
||||
cfg, err := config.LoadConfig()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println(cfg.Port, cfg.WsUrl)
|
||||
```
|
||||
| 错误类型 | 说明 |
|
||||
|---------|------|
|
||||
| `DeviceError` | 设备通用错误 |
|
||||
| `ConnectError` | 连接错误 |
|
||||
| `HTTPError` | HTTP 请求错误 |
|
||||
| `HTTPTimeoutError` | HTTP 超时错误 |
|
||||
| `AdbShellError` | ADB Shell 执行错误 |
|
||||
| `RPCError` | JSON-RPC 调用错误 |
|
||||
| `RPCUnknownError` | 未知 RPC 错误 |
|
||||
| `RPCInvalidError` | 无效 RPC 响应 |
|
||||
| `RPCStackOverflowError` | Java 端栈溢出 |
|
||||
| `UiObjectNotFoundError` | UI 元素未找到 |
|
||||
| `UiAutomationNotConnectedError` | UIAutomation 服务断开 |
|
||||
| `HierarchyEmptyError` | UI 层级为空 |
|
||||
| `LaunchUiAutomationError` | UIAutomator2 启动失败 |
|
||||
| `SessionBrokenError` | 应用会话中断 |
|
||||
| `AppNotFoundError` | 应用未安装 |
|
||||
| `InputIMEError` | 输入法错误 |
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 如何获取设备序列号?
|
||||
|
||||
A: 运行 `adb devices` 或使用代码:
|
||||
运行 `adb devices` 或使用代码:
|
||||
|
||||
```go
|
||||
payload, _ := adb.ListDevicesRaw("127.0.0.1:5037", 15*time.Second)
|
||||
payload, _ := adb.ListDevicesRaw(libs.DefaultADBAddr, 15*time.Second)
|
||||
devices := adb.ParseDevicesPayload(payload)
|
||||
for _, dev := range devices {
|
||||
fmt.Println("Serial:", dev.Serial)
|
||||
}
|
||||
```
|
||||
|
||||
### Q: 文件推送失败怎么办?
|
||||
|
||||
A: 检查以下几点:
|
||||
1. 设备已正确连接且授权
|
||||
2. 目标路径有写入权限(如 `/sdcard/` 需要存储权限)
|
||||
3. 文件路径使用绝对路径
|
||||
4. 启用调试模式查看详细日志
|
||||
|
||||
### Q: UIAutomator2 服务启动失败?
|
||||
|
||||
A: 确保:
|
||||
确保:
|
||||
1. `assets/u2.jar` 文件存在
|
||||
2. 设备已获取 root 权限或使用 `/data/local/tmp/` 路径
|
||||
3. CLASSPATH 环境变量正确设置
|
||||
2. 设备已正确连接且已授权 USB 调试
|
||||
3. 使用 `NewDevice()` 会自动推送 JAR 并启动服务,无需手动管理
|
||||
|
||||
### Q: 如何处理弹窗干扰自动化?
|
||||
|
||||
使用 Watcher 机制自动处理弹窗:
|
||||
|
||||
```go
|
||||
w := libs.NewWatchContext(d, true) // builtin=true 自带常见弹窗规则
|
||||
w.WhenText("允许").Click()
|
||||
w.Start()
|
||||
defer w.Stop()
|
||||
```
|
||||
|
||||
### Q: 如何输入中文?
|
||||
|
||||
使用 AdbKeyboard 输入法:
|
||||
|
||||
```go
|
||||
im := libs.NewInputMethod(d)
|
||||
im.SendKeys("你好世界")
|
||||
```
|
||||
|
||||
### Q: 连接已有服务(不启动新服务)?
|
||||
|
||||
```go
|
||||
// 适用于 UIAutomator2 已经在设备上运行的场景
|
||||
d := libs.NewDeviceWithoutStart("emulator-5554")
|
||||
d.ByText("Hello").Click()
|
||||
```
|
||||
|
||||
## 开发计划
|
||||
|
||||
- [ ] 添加完整的错误处理和重连机制
|
||||
- [ ] 实现 WebSocket 远程控制
|
||||
- [ ] 支持批量设备管理
|
||||
- [ ] 添加 UI 自动化测试框架
|
||||
- [ ] 完善 HTTP over ADB 功能集成
|
||||
- [ ] 添加设备截图和录屏功能
|
||||
- [ ] 实现日志记录系统
|
||||
- [x] 纯 Go ADB 协议实现
|
||||
- [x] ADB 隧道直连(与 Python 一致)
|
||||
- [x] 设备发现和连接管理
|
||||
- [x] JSON-RPC 2.0 通信层
|
||||
- [x] 完整 UI 元素操作(点击、滑动、输入、拖拽)
|
||||
- [x] 便捷选择器(ByText、ByResourceId 等)
|
||||
- [x] Watcher 弹窗自动监控
|
||||
- [x] Session 会话管理
|
||||
- [x] AdbKeyboard 输入法集成
|
||||
- [x] 截图功能
|
||||
- [x] 应用生命周期管理
|
||||
- [x] 完整错误类型体系
|
||||
- [x] 线程安全配置管理
|
||||
- [ ] XPath 选择器支持
|
||||
- [ ] 设备录屏功能
|
||||
- [ ] 批量设备管理
|
||||
- [ ] 完整单元测试
|
||||
|
||||
## 依赖
|
||||
|
||||
**零外部依赖** — 仅使用 Go 标准库。
|
||||
|
||||
```go
|
||||
module github.com/zhuy1228/go-mobile-uiautomator
|
||||
|
||||
require (
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
go 1.25.1
|
||||
```
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
|
||||
## 贡献
|
||||
|
||||
欢迎提交 Issue 和 Pull Request!
|
||||
|
||||
## 作者
|
||||
|
||||
zhuy1228
|
||||
|
||||
## 致谢
|
||||
|
||||
本项目参考了以下开源项目:
|
||||
- [UIAutomator2](https://github.com/openatx/uiautomator2)
|
||||
- [Android ADB Protocol](https://android.googlesource.com/platform/packages/modules/adb/)
|
||||
- [openatx/uiautomator2](https://github.com/openatx/uiautomator2) — Python Android 自动化框架
|
||||
- [Android ADB Protocol](https://android.googlesource.com/platform/packages/modules/adb/) — ADB 协议规范
|
||||
|
||||
Reference in New Issue
Block a user