docs: 更新包文档,匹配 ADB 隧道架构和当前 API

This commit is contained in:
zyj
2026-03-10 15:52:38 +08:00
parent 8a0b1e6e98
commit 86599c62c9
8 changed files with 188 additions and 91 deletions

View File

@@ -233,6 +233,20 @@ func PushFile(addr, serial, localPath, remotePath string, mode int, debug bool)
return s.SyncPushFile(localPath, remotePath, mode, debug)
}
// PushData 通过 ADB 推送内存数据到设备
// 与 PushFile 功能相同,但数据来源是 []byte 而非本地文件
// 适用于通过 go:embed 嵌入的资源文件
func PushData(addr, serial string, data []byte, remotePath string, mode int, debug bool) (int64, error) {
conn, err := ConnectToDevice(addr, serial, 15*time.Second)
if err != nil {
return 0, err
}
defer conn.Close()
s := InitSync(conn)
return s.SyncPushData(data, remotePath, mode, debug)
}
// ---------- ADB 隧道(与 Python uiautomator2 完全一致) ----------
// CreateTunnel 建立到设备指定端口的 ADB 隧道

View File

@@ -1,21 +1,27 @@
// Package adb 实现了 Android Debug Bridge (ADB) 协议的核心功能。
//
// 本包提供了纯 Go 实现的 ADB 客户端,支持以下功能:
// 本包提供了纯 Go 实现的 ADB 客户端,零外部依赖,支持以下功能:
// - 设备发现与连接管理
// - Shell 命令执行
// - 文件同步传输(推送文件到设备
// - ADB 隧道transport + tcp:<port>,与 Python adbutils 一致
// - 文件同步传输Sync Push 协议)
// - APK 安装
// - UIAutomator2 服务启动
//
// ADB 隧道是本包的核心功能,用于替代 adb forward 端口转发。
// 每次调用 CreateTunnel 会建立一条直通设备端口的原始 TCP 管道,
// 无需占用本地端口,无需额外清理。
//
// 使用示例:
//
// // 连接 ADB 服务器
// conn, err := adb.DialADB("127.0.0.1:5037", 15*time.Second)
// // 列出所有设备
// payload, _ := adb.ListDevicesRaw("127.0.0.1:5037", 15*time.Second)
// devices := adb.ParseDevicesPayload(payload)
//
// // 建立 ADB 隧道到设备端口 9008
// conn, err := adb.CreateTunnel("127.0.0.1:5037", "emulator-5554", 9008)
// defer conn.Close()
// // conn 现在是一条直通设备 9008 端口的 TCP 管道
//
// // 路由到指定设备
// adb.TransportTo(conn, "emulator-5556")
//
// // 执行 Shell 命令
// output, err := adb.ExecShell(conn, "getprop ro.product.model")
// // 推送文件到设备
// adb.PushFile("127.0.0.1:5037", "emulator-5554", "local.txt", "/sdcard/remote.txt", 0644, false)
package adb

View File

@@ -1,6 +1,7 @@
package adb
import (
"bytes"
"encoding/binary"
"fmt"
"io"
@@ -119,6 +120,84 @@ func (s *Sync) SyncPushFile(localPath, remotePath string, mode int, debug bool)
return total, nil
}
// SyncPushData 将内存中的数据推送到设备
// 与 SyncPushFile 功能相同,但数据来源是 []byte 而非本地文件
// 适用于通过 go:embed 嵌入的资源文件
func (s *Sync) SyncPushData(data []byte, remotePath string, mode int, debug bool) (int64, error) {
// 初始化同步模式
if err := s.StartSync(); err != nil {
return 0, err
}
// 构造 SEND 请求
modeStr := strconv.Itoa(syscall.S_IFREG | mode)
sendPayload := []byte(remotePath + "," + modeStr)
hdr := make([]byte, 8)
copy(hdr[:4], []byte("SEND"))
binary.LittleEndian.PutUint32(hdr[4:], uint32(len(sendPayload)))
if _, err := s.Conn.Write(hdr); err != nil {
return 0, err
}
if _, err := s.Conn.Write(sendPayload); err != nil {
return 0, err
}
if debug {
fmt.Printf("[调试] 发送 SEND 请求: 长度=%d 路径=%s 权限=%s 数据大小=%d\n", len(sendPayload), remotePath, modeStr, len(data))
}
// 分块写入数据
reader := bytes.NewReader(data)
var total int64
buf := make([]byte, maxChunk)
for {
n, rerr := reader.Read(buf)
if n > 0 {
dataHdr := make([]byte, 8)
copy(dataHdr[:4], []byte("DATA"))
binary.LittleEndian.PutUint32(dataHdr[4:], uint32(n))
if _, err := s.Conn.Write(dataHdr); err != nil {
return total, err
}
if _, err := s.Conn.Write(buf[:n]); err != nil {
return total, err
}
total += int64(n)
}
if rerr != nil {
if rerr == io.EOF {
break
}
return total, rerr
}
}
// 发送 DONE 命令(使用当前时间戳)
done := make([]byte, 8)
copy(done[:4], []byte("DONE"))
mtime := uint32(time.Now().Unix())
binary.LittleEndian.PutUint32(done[4:], mtime)
if _, err := s.Conn.Write(done); err != nil {
return total, err
}
if debug {
fmt.Println("[调试] 发送 DONE等待响应")
}
// 读取最终响应
resp, msg, err := ReadSyncStatus(s.Conn)
if err != nil {
return total, err
}
if resp != "OKAY" {
if len(msg) > 0 {
return total, fmt.Errorf("同步失败: %s", string(msg))
}
return total, fmt.Errorf("同步失败: %s", resp)
}
return total, nil
}
// StartSync 启动 ADB 同步模式
// 发送 "sync:" 命令并等待 "OKAY" 响应
func (s *Sync) StartSync() error {

24
assets/embed.go Normal file
View File

@@ -0,0 +1,24 @@
// Package assets 嵌入 UIAutomator2 运行所需的资源文件。
//
// 通过 go:embed 将 u2.jar 和 APK 编译到 Go 二进制中,
// 使用者无需手动管理资源文件路径。
package assets
import "embed"
// 嵌入 UIAutomator2 运行所需的资源文件
var (
//go:embed u2.jar
JarData []byte
//go:embed app-uiautomator.apk
ApkData []byte
//go:embed app-uiautomator-test.apk
ApkTestData []byte
)
// FS 提供对嵌入文件的文件系统访问
//
//go:embed *.jar *.apk
var FS embed.FS

View File

@@ -27,11 +27,10 @@ type Device struct {
serial string // 设备序列号
// UIAutomator2 服务配置
serverPort int // 设备端服务端口(默认 9008
debug bool // 调试模式
jarPath string // 本地 u2.jar 路径(空字符串使用默认路径)
serverPort int // 设备端服务端口(默认 9008
debug bool // 调试模式
// 设备连接接口(通过 adb forward 连接
// 设备连接接口(通过 ADB 隧道直连
dev AdbDevice
// JSON-RPC 调用器
@@ -49,16 +48,15 @@ type Device struct {
}
// NewDevice 创建一个新的 Device 客户端并启动 UIAutomator2 服务
// 使用 adb forward 端口转发,与 Python uiautomator2 相同的方案
// 通过 ADB 隧道直连设备,与 Python uiautomator2 完全一致
//
// serial: 设备序列号(如 "emulator-5554"
// addr: 可选ADB 服务器地址,不传则使用默认值 "127.0.0.1:5037"
//
// 创建后会自动执行:
// 1. 设置 adb forward 端口转发
// 2. 推送 u2.jar 到设备(如果尚未存在)
// 3. 启动 UIAutomator2 服务
// 4. 等待服务就绪
// 1. 推送内嵌的 u2.jar 到设备(如果尚未存在)
// 2. 启动 UIAutomator2 服务
// 3. 等待服务就绪
func NewDevice(serial string, addr ...string) (*Device, error) {
a := DefaultADBAddr
if len(addr) > 0 && addr[0] != "" {
@@ -80,8 +78,8 @@ func NewDevice(serial string, addr ...string) (*Device, error) {
return d.jsonrpcCall(method, params, timeout)
})
// 推送 u2.jar 到设备(仅在文件不存在时推送)
if err := services.InstallServiceJar(a, serial, d.jarPath, false); err != nil {
// 推送内嵌的 u2.jar 到设备(仅在文件不存在时推送)
if err := services.InstallServiceJar(a, serial, false); err != nil {
return nil, fmt.Errorf("安装 u2.jar 失败: %w", err)
}
@@ -200,12 +198,6 @@ func (d *Device) checkAlive() bool {
return string(resp.Content) == "pong"
}
// SetJarPath 设置本地 u2.jar 路径
// 传空字符串则使用默认路径 (assets/u2.jar)
func (d *Device) SetJarPath(path string) {
d.jarPath = path
}
// launchAndWait 启动 UIAutomator2 进程并等待就绪
func (d *Device) launchAndWait() error {
// 通过 ADB shell 启动 UIAutomator2

View File

@@ -1,45 +1,50 @@
// Package libs 提供了完整的 Android UIAutomator2 自动化框架。
//
// 本包是 Python uiautomator2 的 Go 语言实现,通过 ADB 协议与运行在 Android 设备上的
// UIAutomator2 HTTP 服务通信提供设备控制、UI 操作、文本输入等功能。
// 本包是 Python uiautomator2 的 Go 语言实现,通过 ADB 隧道协议与运行在 Android 设备上的
// UIAutomator2 HTTP 服务直连提供设备控制、UI 操作、文本输入等功能。
// 零外部依赖,仅使用 Go 标准库。
//
// 核心组件:
// - Device设备客户端管理 UIAutomator2 服务生命周期,提供所有设备操作
// - UiObjectUI 控件对象,支持点击、输入、滑动、等待等操作
// - SelectorUI 元素选择器,支持文本、类名、资源 ID 等多种查询条件
// - JsonRpcWrapperJSON-RPC 2.0 调用封装
// - InputMethod通过 AdbKeyboard 输入法实现快速文本输入
// - UiObjectUI 控件对象,支持点击、输入、滑动、等待、滚动等操作
// - SelectorUI 元素选择器,支持文本、类名、资源 ID、描述等多种查询条件
// - JsonRpcWrapperJSON-RPC 2.0 调用封装,自动错误映射和重试
// - InputMethod通过 AdbKeyboard 输入法实现快速文本输入(支持中文)
// - SwipeExt扩展滑动操作按方向、比例滑动
// - WatchContext/Watcher:弹窗/对话框自动监控和处理
// - Session应用会话管理自动检测应用状态
// - Settings设备配置管理等待超时、操作延迟等
// - WatchContext弹窗/对话框自动监控和处理
// - Session应用会话管理自动检测应用存活状态
// - Settings线程安全的设备配置管理(等待超时、操作延迟等)
//
// 通信层:
// - AdbHTTPConnection通过 ADB 隧道发送 HTTP 请求
// - AdbTunnelDevice通过 ADB 隧道直连设备,与 Python uiautomator2 方案完全一致
// - http.Transport自定义 DialContext 将 HTTP 连接替换为 ADB 隧道
// - HttpRequest高层 HTTP 请求封装
// - JsonRpcCallJSON-RPC 2.0 请求/响应处理
//
// 使用示例:
//
// // 创建设备连接
// device, err := libs.NewDevice("emulator-5554")
// // 创建设备连接(自动推送 u2.jar、启动服务
// d, err := libs.NewDevice("emulator-5554")
// // 自定义 ADB 地址: libs.NewDevice("emulator-5554", "192.168.1.100:5037")
// if err != nil {
// log.Fatal(err)
// }
// defer device.StopUiautomator()
// defer d.Close()
//
// // 查找并点击按钮
// btn, _ := device.FindElement(map[string]interface{}{"text": "登录"})
// btn.Click()
// // 便捷选择器,链式调用
// d.ByText("登录").Click()
// d.ByResourceId("com.example:id/input").SetText("admin")
//
// // 输入文本
// input, _ := device.FindElement(map[string]interface{}{"resourceId": "com.example:id/username"})
// input.SetText("admin")
// // 多条件组合查找
// d.By(libs.P{"className": "android.widget.Button", "text": "确定"}).Click()
//
// // 使用 AdbKeyboard 输入中文
// im := libs.NewInputMethod(d)
// im.SendKeys("你好世界")
//
// // 使用 Watcher 自动处理弹窗
// watcher := libs.NewWatcher(device)
// watcher.WhenText("同意").Click()
// watcher.Start(2.0)
// defer watcher.Stop()
// w := libs.NewWatchContext(d, true)
// w.WhenText("允许").Click()
// w.Start()
// defer w.Stop()
package libs

View File

@@ -1,5 +1,8 @@
// Package services 提供了 UIAutomator2 服务的安装和部署功能。
//
// 本包负责将 UIAutomator2 相关的资源文件u2.jar、APK
// 推送到 Android 设备并完成安装。
// 本包负责将 UIAutomator2 相关的资源文件推送到 Android 设备并完成安装:
// - InstallServiceJar推送 u2.jar 到设备(自动检查设备端是否已存在,避免重复推送)
// - InstallServiceApk推送并安装 UIAutomator2 APK
//
// 通常无需直接调用本包libs.NewDevice() 会自动完成服务安装和启动。
package services

View File

@@ -2,22 +2,17 @@ package services
import (
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/zhuy1228/go-mobile-uiautomator/adb"
"github.com/zhuy1228/go-mobile-uiautomator/assets"
)
// 默认路径常量
// 设备端路径常量
const (
// DefaultJarLocal 默认本地 JAR 路径
DefaultJarLocal = "assets/u2.jar"
// DefaultJarRemote 默认设备端 JAR 路径
DefaultJarRemote = "/data/local/tmp/u2.jar"
// DefaultApkLocal 默认本地 APK 路径
DefaultApkLocal = "assets/app-uiautomator.apk"
// DefaultApkRemote 默认设备端 APK 临时路径
DefaultApkRemote = "/data/local/tmp/app-uiautomator.apk"
)
@@ -39,63 +34,42 @@ func fileExistsOnDevice(addr, serial, remotePath string) bool {
return strings.TrimSpace(string(out)) == remotePath
}
// InstallServiceJar 将 u2.jar 推送到设备的 /data/local/tmp/ 目录
// InstallServiceJar 将内嵌的 u2.jar 推送到设备的 /data/local/tmp/ 目录
// JAR 文件通过 go:embed 编译到二进制中,使用者无需关心文件路径
//
// addr 为 ADB 服务器地址serial 为设备序列号
// localPath 为本地 JAR 路径,传空字符串则使用默认路径
// force 为 true 时跳过存在性检查,强制推送
func InstallServiceJar(addr, serial, localPath string, force bool) error {
if localPath == "" {
localPath = DefaultJarLocal
}
// 检查本地文件是否存在
if _, err := os.Stat(localPath); os.IsNotExist(err) {
return fmt.Errorf("本地 JAR 文件不存在: %s", localPath)
}
func InstallServiceJar(addr, serial string, force bool) error {
// 非强制模式下,检查设备端文件是否已存在
if !force && fileExistsOnDevice(addr, serial, DefaultJarRemote) {
return nil // 文件已存在,跳过推送
}
_, err := adb.PushFile(addr, serial, localPath, DefaultJarRemote, 0644, false)
_, err := adb.PushData(addr, serial, assets.JarData, DefaultJarRemote, 0644, false)
if err != nil {
return fmt.Errorf("推送 u2.jar 失败: %w", err)
}
return nil
}
// InstallServiceApk 将 UIAutomator2 APK 推送到设备并安装
// InstallServiceApk 将内嵌的 UIAutomator2 APK 推送到设备并安装
// APK 文件通过 go:embed 编译到二进制中,使用者无需关心文件路径
//
// addr 为 ADB 服务器地址serial 为设备序列号
// localPath 为本地 APK 路径,传空字符串则使用默认路径
// force 为 true 时跳过存在性检查,强制推送
func InstallServiceApk(addr, serial, localPath string, force bool) error {
if localPath == "" {
localPath = DefaultApkLocal
}
abs, err := filepath.Abs(localPath)
if err != nil {
return fmt.Errorf("解析 APK 路径失败: %w", err)
}
// 检查本地文件是否存在
if _, err := os.Stat(abs); os.IsNotExist(err) {
return fmt.Errorf("本地 APK 文件不存在: %s", abs)
}
func InstallServiceApk(addr, serial string, force bool) error {
// 非强制模式下,检查设备端文件是否已存在
if !force && fileExistsOnDevice(addr, serial, DefaultApkRemote) {
// 文件已存在,跳过推送,但仍需确保已安装
} else {
_, err = adb.PushFile(addr, serial, abs, DefaultApkRemote, 0644, false)
_, err := adb.PushData(addr, serial, assets.ApkData, DefaultApkRemote, 0644, false)
if err != nil {
return fmt.Errorf("推送 APK 失败: %w", err)
}
}
// 在设备上安装 APK覆盖安装
_, err = adb.InstallApkOnDevice(addr, serial, DefaultApkRemote, "-r", false)
_, err := adb.InstallApkOnDevice(addr, serial, DefaultApkRemote, "-r", false)
if err != nil {
return fmt.Errorf("安装 APK 失败: %w", err)
}